Skip to main content

前置知识

1、packages 目录

目录下的文件夹非常多,我们来看下:

react

React 的核心,包含所有全局 React API,如:

  • React.createElement
  • React.Component
  • React.Children

react-dom

存放操作真实 dom 的一些方法

scheduler

Scheduler(调度器)的实现。

shared

源码中其他模块公用的方法全局变量,比如在shared/ReactSymbols.js (opens new window)中保存React不同组件类型的定义。

// ...
export let REACT_ELEMENT_TYPE = 0xeac7
export let REACT_PORTAL_TYPE = 0xeaca
export let REACT_FRAGMENT_TYPE = 0xeacb
// ...

react-reconciler

我们需要重点关注react-reconciler,在接下来源码学习中 80%的代码量都来自这个包。

他一边对接Scheduler,一边对接不同平台的Renderer

2、React 是什么?

  • React是一个用于构建用户界面的 JavaScript 库

  • 可以通过组件化的方式构建 构建快速响应的大型Web应用程序

3、JSX 是什么

  • jsx
  • JSX 是一个JavaScript的语法扩展,JSX 可以很好地描述 UI 应该呈现出它应有交互的本质形式
  • repl可以在线转换代码
  • astexplorer可以把代码转换成 AST 树
  • react/jsx-runtimereact/jsx-dev-runtime 中的函数只能由编译器转换使用。如果你需要在代码中手动创建元素,你可以继续使用 React.createElement
// 17 babel 转换使用 { runtime: "classic" } 转换成  React.createElement(标签,属性,children)
// 18 babel 转换使用 { runtime: "automatic" } 转换成下面结果,把children抽离出去了
//新的不需要手动引入 React 的
var { jsxDEV } = require("react/jsx-dev-runtime")
jsxDEV("h1", {
style: {
color: "red",
},
children: ["hello"],
})

1.2.1 旧转换

1.2.1.1 jsx.js
const babel = require("@babel/core")
const sourceCode = `
<h1>
hello<span style={{ color: "red" }}>world</span>
</h1>
`
const result = babel.transform(sourceCode, {
plugins: [["@babel/plugin-transform-react-jsx", { runtime: "classic" }]],
})
console.log(result.code)
1.2.1.2 转译结果
React.createElement(
"h1",
null,
"hello",
React.createElement(
"span",
{
style: {
color: "red",
},
},
"world"
)
)

1.2.2 新转换

1.2.2.1 jsx.js
const babel = require("@babel/core");
const sourceCode = `
<h1>
hello<span style={{ color: "red" }}>world</span>
</h1>
`;
const result = babel.transform(sourceCode, {
+ plugins: [["@babel/plugin-transform-react-jsx", { runtime: "automatic" }]],
});
console.log(result.code);
1.2.2.2 转译结果
var { jsxDEV } = require("react/jsx-dev-runtime")
jsxDEV("h1", {
children: [
"hello",
jsxDEV("span", {
style: {
color: "red",
},
children: "world",
}),
],
})

4、Virtual DOM

  • React.createElement 函数所返回的就是一个虚拟 DOM
  • 虚拟 DOM 就是一个描述真实 DOM 的纯 JS 对象

5、 fiber

5.1 性能瓶颈

  • JS 任务执行时间过长
    • 浏览器刷新频率为 60Hz,大概 16.6 毫秒渲染一次,而 JS 线程和渲染线程是互斥的,所以如果 JS 线程执行任务时间超过 16.6ms 的话,就会导致掉帧,导致卡顿,解决方案就是 React 利用空闲的时间进行更新,不影响渲染进行的渲染
    • 把一个耗时任务切分成一个个小任务,分布在每一帧里的方式就叫时间切片

5.2 屏幕刷新率

  • 目前大多数设备的屏幕刷新率为 60 次/秒
  • 浏览器渲染动画或页面的每一帧的速率也需要跟设备屏幕的刷新率保持一致
  • 页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到 60 时,页面是流畅的,小于这个值时,用户会感觉到卡顿
  • 每个帧的预算时间是 16.66 毫秒 (1 秒/60)
  • 1s 60 帧,所以每一帧分到的时间是 1000/60 ≈ 16 ms,所以我们书写代码时力求不让一帧的工作量超过 16ms

5.3 帧

  • 每个帧的开头包括样式计算、布局和绘制
  • JavaScript 执行 Javascript 引擎和页面渲染引擎在同一个渲染线程,GUI 渲染和 Javascript 执行两者是互斥的
  • 如果某个任务执行时间过长,浏览器会推迟渲染

5.4 requestIdleCallback

  • 我们希望快速响应用户,让用户觉得够快,不能阻塞用户的交互
  • requestIdleCallback 使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应
  • 正常帧任务完成后没超过 16 ms,说明时间有富余,此时就会执行 requestIdleCallback 里注册的任务

5.5 fiber

  • 我们可以通过某些调度策略合理分配 CPU 资源,从而提高用户的响应速度
  • 通过 Fiber 架构,让自己的调和过程变成可被中断。 适时地让出 CPU 执行权,除了可以让浏览器及时地响应用户的交互
1.5.5.1 Fiber 是一个执行单元
  • Fiber 是一个执行单元,每次执行完一个执行单元, React 就会检查现在还剩多少时间,如果没有时间就将控制权让出去

5.5.2 Fiber 是一种数据结构
  • React 目前的做法是使用链表, 每个虚拟节点内部表示为一个Fiber
  • 从顶点开始遍历
  • 如果有第一个儿子,先遍历第一个儿子
  • 如果没有第一个儿子,标志着此节点遍历完成
  • 如果有弟弟遍历弟弟
  • 如果有没有下一个弟弟,返回父节点标识完成父节点遍历,如果有叔叔遍历叔叔
  • 没有父节点遍历结束

5.5.3 递归构建 fiber 树

DOM 事件

DOM 事件流

  • 事件流包含三个阶段

    • 事件捕获阶段 document->body->div->button
    • 处于目标阶段 事件目标是真正触发事件的对象 **let** target = event.target || event.srcElement;
    • 事件冒泡阶段 button->div->body->document

addEventListener

element.addEventListener(event, function, useCapture)

阻止冒泡

  • 如果想要阻止事件的传播
    • 在微软的模型中你必须设置事件的cancelBubble的属性为 true
    • 在 W3C 模型中你必须调用事件的stopPropagation()方法
function stopPropagation(event) {
if (!event) {
window.event.cancelBubble = true
}
if (event.stopPropagation) {
event.stopPropagation()
}
}

阻止默认行为

  • 取消默认事件
function preventDefault(event) {
if (!event) {
window.event.returnValue = false
}
if (event.preventDefault) {
event.preventDefault()
}
}

事件系统

  • 合成事件是围绕浏览器原生事件充当跨浏览器包装器的对象,它们将不同浏览器的行为合并为一个 API,这样做是为了确保事件在不同浏览器中显示一致的属性

简单案例

import * as React from "react"
import * as ReactDOM from "react-dom"
class App extends React.Component {
parentRef = React.createRef()
childRef = React.createRef()
componentDidMount() {
this.parentRef.current.addEventListener(
"click",
() => {
console.log("父元素原生捕获")
},
true
)
this.parentRef.current.addEventListener("click", () => {
console.log("父元素原生冒泡")
})
this.childRef.current.addEventListener(
"click",
() => {
console.log("子元素原生捕获")
},
true
)
this.childRef.current.addEventListener("click", () => {
console.log("子元素原生冒泡")
})
document.addEventListener(
"click",
() => {
console.log("document原生捕获")
},
true
)
document.addEventListener("click", () => {
console.log("document原生冒泡")
})
}
parentBubble = () => {
console.log("父元素React事件冒泡")
}
childBubble = () => {
console.log("子元素React事件冒泡")
}
parentCapture = () => {
console.log("父元素React事件捕获")
}
childCapture = () => {
console.log("子元素React事件捕获")
}
render() {
return (
<div ref={this.parentRef} onClick={this.parentBubble} onClickCapture={this.parentCapture}>
<p ref={this.childRef} onClick={this.childBubble} onClickCapture={this.childCapture}>
事件执行顺序
</p>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById("root"))

/**
document原生捕获
父元素React事件捕获
子元素React事件捕获
父元素原生捕获
子元素原生捕获
子元素原生冒泡
父元素原生冒泡
子元素React事件冒泡
父元素React事件冒泡
document原生冒泡
*/