资源
1. React中文网:https://react.docschina.org
2. React源码:https://github.com/facebook/react
学习目标
1. 掌握虚拟dom、diff策略
2. 掌握fiber原理及实现
reconciliation协调
地址:https://zh-hans.reactjs.org/docs/reconciliation.html
设计动力
在某一时间节点调用 React 的 render() 方法,会创建一棵由 React 元素组成的树。在下一次 state 或props 更新时,相同的 render() 方法会返回一棵不同的树。React 需要基于这两棵树之间的差别来判 断如何有效率的更新 UI 以保证当前 UI 与最新的树保持同步。
这个算法问题有一些通用的解决方案,即生成将一棵树转换成另一棵树的最小操作数。然而,即使在最前沿的算法中(http://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf),该算法的复杂程度为 O(n 3 ),其中 n 是树中元素的数量。如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围。这个开销实在是太过高昂。于是 React 在以下两个假设的基础之上提出了一套 O(n) 的启发式算法:
1. 两个不同类型的元素会产生出不同的树; 2. 开发者可以通过 prop 来暗示哪些子元素在不同的渲染下能保持稳定;
在实践中,我们发现以上假设在几乎所有实用的场景下都成立。
diffing算法
算法复杂度O(n)
diff 策略
1. 同级比较,Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。2. 拥有不同类型的两个组件将会生成不同的树形结构。
例如:div->p, CompA->CompB
3. 开发者可以通过 key prop 来暗示哪些子元素在不同的渲染下能保持稳定
diff过程
比对两个虚拟dom时会有三种操作:删除、替换和更新
vnode是现在的虚拟dom,newVnode是新虚拟dom。删除:newVnode不存在时 替换:vnode和newVnode类型不同或key不同时 更新:有相同类型和key但vnode和newVnode不同时
在实践中也证明这三个前提策略是合理且准确的,它保证了整体界面构建的性能。
fiber
为什么需要fiber
React Conf 2017 Fiber介绍视频:https://www.youtube.com/watch?v=ZCuYPiUIONs
React的killer feature: virtual dom
1为什么需要fiber
对于大型项目,组件树会很大,这个时候递归遍历的成本就会很高,会造成主线程被持续占用,结
果就是主线程上的布局、动画等周期性任务就无法立即得到处理,造成视觉上的卡顿,影响用户体
验。
2任务分解的意义 解决上面的问题
3增量渲染(把渲染任务拆分成块,匀到多帧)
4更新时能够暂停,终止,复用渲染任务
5给不同类型的更新赋予优先级
6并发方面新的基础能力
7更流畅
为了加以区分,以前的 Reconciler 被命名为Stack Reconciler。Stack Reconciler 运作的过程是不能被打断的,必须一条道走到黑:
而 Fiber Reconciler 每执行一段时间,都会将控制权交回给浏览器,可以分段执行:
为了达到这种效果,就需要有一个调度器 (Scheduler) 来进行任务分配。任务的优先级有六种:
synchronous,与之前的Stack Reconciler操作一样,同步执行
task,在next tick之前执行
animation,下一帧之前执行
high,在不久的将来立即执行
low,稍微延迟执行也没关系
offscreen,下一次render时或scroll时才执行
优先级高的任务(如键盘输入)可以打断优先级低的任务(如Diff)的执行,从而更快的生效。
什么是fiber
A Fiber is work on a Component that needs to be done or was done. There can be more than one per component. fiber是指组件上将要完成或者已经完成的任务,每个组件可以一个或者多个。Fiber Reconciler 在执行过程中,会分为 2 个阶段。
阶段一可被打断的特性,让优先级更高的任务先执行,从框架层面大大降低了页面掉帧的概率。
阶段二,将需要更新的节点一次过批量更新,这个过程不能被打断。
阶段一可被打断的特性,让优先级更高的任务先执行,从框架层面大大降低了页面掉帧的概率
Fiber 树
Fiber Reconciler 在阶段一进行 Diff 计算的时候,会生成一棵 Fiber 树。这棵树是在 Virtual DOM 树的基础上增加额外的信息来生成的,它本质来说是一个链表。
Fiber 树在首次渲染的时候会一次过生成。在后续需要 Diff 的时候,会根据已有树和最新 Virtual DOM 的信息,生成一棵新的树。这颗新树每生成一个新的节点,都会将控制权交回给主线程,去检查有没有优先级更高的任务需要执行。如果没有,则继续构建树的过程:
如果过程中有优先级更高的任务需要进行,则 Fiber Reconciler 会丢弃正在生成的树,在空闲的时候再重新执行一遍。在构造 Fiber 树的过程中,Fiber Reconciler 会将需要更新的节点信息保存在Effect List当中,在阶段二执行的时候,会批量更新相应的节点。
实现fiber
window.requestIdleCallback(callback,[,options])方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间 timeout ,则有可能为了在超时前执行函数而打乱执行顺序。你可以在空闲回调函数中调用 requestIdleCallback() ,以便在下一次通过事件循环之前调度另一个回调。
callback
一个在事件循环空闲时即将被调用的函数的引用。函数会接收到一个名为 IdleDeadline(https://developer.mozilla.org/zh-CN/docs/Web/API/IdleDeadline) 的参数,这 个参数可以获取当前空闲时间以及回调是否在超时时间前已经执行的状态。
options 可选 包括可选的配置参数。具有如下属性:
timeout :如果指定了timeout并具有一个正值,并且尚未通过超时毫秒数调用回调,那么回调会 在下一次空闲时期被强制执行,尽管这样很可能会对性能造成负面影响。
react中requestIdleCallback的hack在react/packages/scheduler/src/forks/SchedulerHostConfig.default.js。
实现fiber
Fiber 是 React 16 中新的协调引擎。它的主要目的是使 Virtual DOM 可以进行增量式渲染。
一个更新过程可能被打断,所以React Fiber一个更新过程被分为两个阶段(Phase):第一个阶段Reconciliation Phase和第二阶段Commit Phase。
import React from "./kreact/";import ReactDOM, {useState} from "./kreact/react-dom";import Component from "./kreact/Component";import "./index.css";class ClassComponent extends Component { static defaultProps = { color: "pink" }; render() { return ( <div className="border"> <div className={this.props.color}>color div> {this.props.name}div>); }}function FunctionComponent({name}) { const [count, setCount] = useState(0); return ( <div className="border">{name}<button onClick={() => setCount(count + 1)}> {count}: count addbutton>
"border"> {count % 2 ? (
<button onClick={() => console.log("omg")}>clickbutton> ):(
<div>omgdiv>
)}
</div>
div>
); }
const jsx = (
);
ReactDOM.render(jsx, document.getElementById("root"));
./react-dom.js
import {TEXT, PLACEMENT, UPDATE, DELETION} from "./const";// 下一个单元任务let nextUnitOfWork = null;// work in progress fiber root let wipRoot = null;// 现在的根节点let currentRoot = null;let deletions = null;// fiber 结构 /*** child 第一个子元素* sibling 下一个兄弟节点 * return 父节点* node 存储当前node节点 */function render(vnode, container) { wipRoot = { node: container, props: { children: [vnode] }, base: currentRoot }; nextUnitOfWork = wipRoot; deletions = [];}// vnode->node// 生成node节点function createNode(vnode) { const {type, props} = vnode; let node = null; if (type === TEXT) { node = document.createTextNode(""); } else if (typeof type === "string") { node = document.createElement(type); } updateNode(node, {}, props); return node;}function reconcileChildren(workInProgressFiber, children) { // 构建fiber结构// 更新 删除 新增let prevSibling = null; let oldFiber = workInProgressFiber.base && workInProgressFiber.base.child; for (let i = 0; i < children.length; i++) { let child = children[i]; let newFiber = null; const sameType = child && oldFiber && child.type === oldFiber.type; if (sameType) {// 类型相同 复用 newFiber = { type: oldFiber.type, props: child.props, node: oldFiber.node, base: oldFiber, return: workInProgressFiber, effectTag: UPDATE}; }if (!sameType && child) {// 类型不同 child存在 新增插入 newFiber = { type: child.type, props: child.props, node: null, base: null, return: workInProgressFiber, effectTag: PLACEMENT}; }if (!sameType && oldFiber) { // 删除 oldFiber.effectTag = DELETION; deletions.push(oldFiber); } if (oldFiber) { oldFiber = oldFiber.sibling; }// 形成链表结构 if (i === 0) { workInProgressFiber.child = newFiber; } else {// i>0 prevSibling.sibling = newFiber; } prevSibling = newFiber; }}function updateNode(node, preVal, nextVal) { Object.keys(preVal) .filter(k => k !== "children") .forEach(k => {if (k.slice(0, 2) === "on") {// 简单处理 on开头当做事件let eventName = k.slice(2).toLowerCase(); node.removeEventListener(eventName, preVal[k]); } else { if (!(k in nextVal)) { node[k] = ""; }} }); Object.keys(nextVal) .filter(k => k !== "children") .forEach(k => {if (k.slice(0, 2) === "on") {// 简单处理 on开头当做事件let eventName = k.slice(2).toLowerCase(); node.addEventListener(eventName, nextVal[k]); } else { node[k] = nextVal[k];} });}function updateFunctionComponent(fiber) { wipFiber = fiber; wipFiber.hooks = []; hookIndex = 0; const {type, props} = fiber; const children = [type(props)]; reconcileChildren(fiber, children);}function updateClassComponent(fiber) { // 略。。。}function performUnitOfWork(fiber) { // 1. 执行当前任务 // 执行当前任务 const {type} = fiber; if (typeof type === "function") { type.isReactComponent ? updateClassComponent(fiber) : updateFunctionComponent(fiber);} else {// 原生标签 updateHostComponent(fiber);}// 2、 返回下一个任务 // 原则就是:先找子元素 if (fiber.child) { return fiber.child; }// 如果没有子元素 寻找兄弟元素 let nextFiber = fiber; while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling;} nextFiber = nextFiber.return; }}function updateHostComponent(fiber) { if (!fiber.node) { fiber.node = createNode(fiber); } // todo reconcileChildren const {children} = fiber.props; reconcileChildren(fiber, children);}function workLoop(deadline) {// 有下一个任务,并且当前帧还没有结束while (nextUnitOfWork && deadline.timeRemaining() > 1) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } if (!nextUnitOfWork && wipRoot) { commitRoot();} requestIdleCallback(workLoop);}requestIdleCallback(workLoop);// ! commit阶段function commitRoot() { deletions.forEach(commitWorker); commitWorker(wipRoot.child); currentRoot = wipRoot; wipRoot = null; }function commitWorker(fiber) { if (!fiber) {return; }// 向上查找let parentNodeFiber = fiber.return; while (!parentNodeFiber.node) { parentNodeFiber = parentNodeFiber.return; } const parentNode = parentNodeFiber.node; if (fiber.effectTag === PLACEMENT && fiber.node !== null) { parentNode.appendChild(fiber.node); } else if (fiber.effectTag === UPDATE && fiber.node !== null) { updateNode(fiber.node, fiber.base.props, fiber.props); } else if (fiber.effectTag === DELETION && fiber.node !== null) { commitDeletions(fiber, parentNode); } commitWorker(fiber.child); commitWorker(fiber.sibling);}function commitDeletions(fiber, parentNode) { if (fiber.node) { parentNode.removeChild(fiber.node); } else { commitDeletions(fiber.child, parentNode); }}// !hook 实现// 当前正在工作的fiberlet wipFiber = null;let hookIndex = null;export function useState(init) { const oldHook = wipFiber.base && wipFiber.base.hooks[hookIndex]; const hook = {state: oldHook ? oldHook.state : init, queue: []}; const actions = oldHook ? oldHook.queue : []; actions.forEach(action => (hook.state = action)); const setState = action => { hook.queue.push(action); wipRoot = { node: currentRoot.node, props: currentRoot.props, base: currentRoot }; nextUnitOfWork = wipRoot; deletions = []; }; wipFiber.hooks.push(hook); hookIndex++; return [hook.state, setState];}export default { render };
const.js
export const TEXT = "TEXT";export const PLACEMENT = "PLACEMENT";export const UPDATE = "UPDATE";export const DELETION = "DELETION";