React 初始化流程
-
JSX 会经过 babel 编译成 React.createElement 递归调用的表达式,React.createElement 会在 render 函数被调用的时候执行,换句话说,当 render 函数被调用的时候,会返回一个 element(组成虚拟 DOM 树的节点)。
-
element 类型为对象时分为原生 DOM(调用 ReactDOMComponent)和自定义类(ReactCompositeComponentWrapper),不是对象分为文本节点(ReactDOMTextComponent)和空节点(ReactDOMEmptyComponent)。
上述四种类常见的方法 mountComponent 用于创建组件,而 updateComponent 用于用户更新组件。而我们自定义组件的生命周期函数以及 render 函数都是在这些私有类的方法里被调用的。
其中 ReactDOMComponent 的 mountComponent 方法会自己操作浏览器 DOM 元素。而 ReactCompositeComponentWrapper 的则是实例化自定义组件,最后是通过递归调用到 ReactDOMComponent 的 mountComponent 方法来得到真实 DOM。
- ReactCompositeComponentWrapper mountComponent 的过程:
- 得到实例化 App 对象 instance
- renderedElement = instance.render();
- 初始化 renderedElement 得到 child
- child.mountComponent(container)
- 在第一步得到 instance 对象之后,就会去看 instance.componentWillMount 是否有被定义,有的话调用,而在整个渲染过程结束之后调用 componentDidMount。
setState 流程
newState 存入 pendingState 队列
- 根据一个变量 isBatchingUpdates 判断是直接更新 state 还是放到队列中。也就是决定是将组件放到 dirtyComponents 中,还是遍历 dirtyComponents,调用 updateComponent,去更新 state 或者 props。
- isBatchingUpdates 默认是 false,React 在调用事件处理函数之前就会调用 batchedUpdates 改变值,以此让 state 不会立即更新。
- batchedUpdates 是通过事务的方式去保证一次更新的完整
更新过程中 ReactCompositeComponentWrapper 的 updateComponent 流程:
- 计算出 nextState
- render() 得到 nextRenderElement
与 preRenderElement 进行 diff 比较,更新节点。
相关生命周期:
- shouldComponentUpdate 在第一步调用得到 nextState 之后调用
当 shouldComponentUpdate 返回 true 的时候,会先调用componentWillUpdate,在整个更新过程结束之后调用 componentDidUpdate。
ReactDOMComponent 的 updateComponent 流程就是直接更新浏览器 DOM 元素。
React 优化
优化的方向有两个,一个减少 render 次数,也就是减少 diff 计算。还有一个是减少计算的量,主要是减少重复计算,对于函数式组件来说,每次 render 都会重新从头开始执行函数调用。在类组件中主要使用 shouldComponentUpdate 生命周期和 PureComponent 组件去减少 render 次数,函数式组件主要使用:
- React.memo:等同于 PureComponent,用它包裹子组件,当父组件需要重新 render 的时候,如果传给自己的 props 不变,就不会触发重新 render。memo 可以添加第二个参数,是个函数,参数为前后 props,返回 true 不需要重新 render。
- useCallback:应用场景是父组件向子组件传递方法,当父组件重新渲染时,代码都会重新执行。所以就算子组件包裹了 React.memo,也会重新渲染。可以通过 useCallback 进行记忆传递的方法,并将记忆的方法传递给子组件。
- useMemo:如果在组件有个变量的值需要大量的计算才可以得出,因为函数组件重新渲染就会重新执行代码,所以该变量的值也会重新计算,就可以 useMemo 做计算结果缓存。
Fiber
其作用是会在浏览器空闲时期依次调用函数, 这就可以在主事件循环中执行后台或低优先级的任务,而且不会对像动画和用户交互这样延迟触发而且关键的事件产生影响。函数一般会按先进先调用的顺序执行,除非函数在浏览器调用它之前就到了它的超时时间。
fiber 借助单链表数据结构将 diff 算法的递归遍历变为循环遍历。
当执行 setState() 或首次 render() 时,进入工作循环,循环体中处理的单元为 Fiber Node, 即是拆分任务的最小单位,从根节点开始,自顶向下逐节点构造 workInProgress tree(构建中的新 Fiber Tree)。
beginWork() 主要做的事情是从顶向下生成所有的 Fiber Node,并标记 Diff。
completeUnitOfWork() 当没有子节点,开始遍历兄弟节点作为下一个处理单元,处理完兄弟节点开始向上回溯,直到再次回去根节点为止,将收集向上回溯过程中的所有 diff,拿到 diff 后开始进入 commit 阶段。
构建 workInProgress tree 的过程就是 diff 的过程,通过 requestIdleCallback 来调度执行一组任务,每完成一个任务后回来看看有没有插队的(更紧急的),把时间控制权交还给主线程,直到下一次 requestIdleCallback 回调再继续构建 workInProgress tree。
requestIdleCallback 的兼容方案
需要原因:兼容性不好,目前只能一秒调用回调 20 次。
因为 diff 的过程需要多次间隔调用,由此可以借助 requestAnimationFrame,它回调方法会在每次重绘前执行,另外它还存在一个瑕疵:页面处于后台时该回调函数不会执行,所以需要 setTimeout 进行补救。两个定时器内部互相取消对方。
在一帧当中,浏览器可能会响应用户的交互事件、执行 JS、进行渲染的一系列计算绘制,diff 就是在执行 JS 这个过程中。如果在一帧范围内没有执行完毕就会出现掉帧,影响用户体验,所以就需要在当下存在空闲时间我们才去执行任务。否则就等到下一帧的空闲时间继续执行。
是否有时间继续 diff 是通过计算剩余时间来判断,简单来说就是假设当前时间为 5000,浏览器支持 60 帧,那么 1 帧近似 16 毫秒,那么就会计算出下一帧时间为 5016。得出下一帧时间以后,我们只需对比当前时间是否小于下一帧时间即可,这样就能清楚地知道是否还有空闲时间去执行任务。
在事件循环中,渲染以后只有宏任务是最先会被执行的,所以选择优先级高的 MessageChannel。
调度的时候先判断任务是否过期,没有过期先计算下一帧时间(通过 requestAnimationFrame ),再调用 port.postMessage(undefined)(过期直接调用) ,这样渲染之后 channel.port1.onmessage 就会执行(任务没过期就要对比当前时间和下一帧时间,还有时间就执行任务,没有就看下一帧是否能执行任务,过期则直接执行)。
事件机制
React 事件并没有绑定在真实的 Dom 节点上,而是通过事件代理,在最外层的 document 上对事件进行统一分发,原生事件在目标阶段执行,React 在冒泡阶段执行。
组件挂载更新时,给 document 注册原生事件回调为 dispatchEvent (统一的事件分发机制)。
事件初始化,添加到 listenerBank,结构是: listenerBank[registrationName][key]
触发事件时:
触发 document 注册原生事件的回调 dispatchEvent
获取到触发这个事件最深一级的元素
遍历这个元素的所有父元素,依次对每一级元素进行处理。
构造合成事件。
将每一级的合成事件存储在 eventQueue 事件队列中。
遍历 eventQueue。
通过 isPropagationStopped 判断当前事件是否执行了阻止冒泡方法。
如果阻止了冒泡,停止遍历,否则通过 executeDispatch 执行合成事件。
释放处理完成的事件。
网友评论