基础源码编写
一:render的基础实现
将index.js 文件替换成自己实现的react
import reactDom from './cwhReact/react-dom' import Component from './cwhReact/component' function FuncComponent (props){ return <div className='func'> <p>这是函数组件:{props.name}</p> </div> } class ClassComponent extends Component{ render(){ return <div className='class'> <p>这是类组件:{this.props.name}</p> </div> } } const vnode = ( <div className="App"> <div className="father"> <div className='son'> son </div> </div> <h1>h1 - 文本</h1> <a href='https://www.baidu.com'>a - 链接</a> <FuncComponent name='function'/> <ClassComponent name='class'/> </div> ); reactDom.render(vnode,document.getElementById('root'));
在react17中,默认会将文中的vnode转换为jsx对象,其数据结构为
vnode:{ // Symbol标识当前节点,为了防止 XSS 攻击,使用 Symbol 类型是因为 JSON 中无法传递 Symbol。 // React 会检查 element.$$typeof 然后拒绝处理非法的元素。 $$typeof:Symbol(react.element), key:null, // 当前节点的key props:{ // 当前节点拥有的属性及其子节点 children:[ vnode1, // 子节点1 vnode2, // 子节点2 ], classname:'App', // 当前节点的样式名称 // ... 其他属性 }, ref:null, // 当前节点的ref指向 type:'div', // 原生标签会返回字符串形式的节点类型,文本类型无type owner:null, }
Component基类
// Component基类 function Component (props){ this.props = props; // 使其内部能够通过this.props访问 } // 类组件的标识,为何不用布尔类型?历史遗留问题,在pureComponent中是使用布尔值标识的 Component.prototype.isReactComponent = {} ; export default Component
react-dom 实现
// 初始渲染 - 直接生成全部 // 更新渲染 - 需要diff比较 function render(vnode, container) { console.log('vnode ===> ', vnode); // 将vnode => node const node = vnode2Node(vnode); // 将node 挂载到container上 container.appendChild(node); } function vnode2Node(vnode) { let node; const { type } = vnode; if (typeof type === 'string') { node = updateHostComponent(vnode) } else if (typeof type === 'function') { // 对于vnode来说,类组件和函数组件都是以function的方式传入的,可以在Component基类的原型链上添加标识 node = type.prototype.isReactComponent ? updateClassCompinent(vnode) : updateFunctionCompinent(vnode); } else { node = updateTextComponent(vnode) } return node; } // 原生标签 function updateHostComponent(vnode) { const { type, props } = vnode; let node = document.createElement(type); nodeAddAttribute(node, props) reconcileChildren(node, props.children) return node; } // 函数组件 function updateFunctionCompinent(vnode) { const { type, props } = vnode let vvnode = type(props); // 执行函数组件,使用其props参数,会得到其返回的vnode,此处为了不与入参重名,命名为vvnode // 使用vnode->node函数转换虚拟dom return vnode2Node(vvnode); } // 类组件 function updateClassCompinent(vnode) { const { type, props } = vnode let instance = new type(props); // 类组件需要new出实例 let vvnode = instance.render(); // 类组件在render函数中返回vnode // 使用vnode->node函数转换虚拟dom return vnode2Node(vvnode); } // 文本标签 function updateTextComponent(vnode) { let node = document.createTextNode(vnode); return node; } // 将子节点vnode挂载到父节点node上 function reconcileChildren(parentNode, children) { let childrenArr = Array.isArray(children) ? children : [children]; // 原本children可能是对象或者数组,此处统一转成数组 // 遍历数组,将子节点逐个添加到父节点上。 childrenArr.forEach(vnode => { render(vnode, parentNode); }) } // 添加属性 function nodeAddAttribute(node, attrVal) { // 排除里面的children属性,将其余属性添加到当前节点 Object.keys(attrVal).filter(k => k !== 'children').forEach(key => { node[key] = attrVal[key] }) } // eslint-disable-next-line import/no-anonymous-default-export export default { render };
fiber架构下使用diff算法的render
// 初始渲染 - 直接生成全部 // 更新渲染 - 需要diff比较 let wipRoot = null; // work in progress 的根节点 // 浏览器空闲时候调用的函数 window.requestIdleCallback(workLoop); // 作为链表指针指向下一个单元任务 let nextUnitOfWork = null; // fiber对象的数据结构 // fiber:{ // type 类型 // key // props // 属性值 // stateNode // 当前的dom节点 // child // 第一个子节点 // sibling // 下一个子节点 // return // 父节点 // } // 执行单元任务 function performUnitOfWork(workInProgress) { // workInProgress 就是表示当前传入的待执行的fiber // step1 执行任务 const { type } = workInProgress; if (typeof type === 'string') { // 原生标签 updateHostComponent(workInProgress) } else if (typeof type === 'function') { // 函数组件 updateFunctionCompinent(workInProgress) } else if (typeof type === 'undefined') { // 文本组件 updateTextComponent(workInProgress) } // step2 返回下一个任务 if (workInProgress.child) { return workInProgress.child; // 有子节点,返回子节点fiber } let nextFiber = workInProgress; // 先把当前的fiber存储起来 while (nextFiber) { if (nextFiber.sibling) { // 如果当前节点有兄弟节点fiber,则返回 return nextFiber.sibling; } // 无则继续往上传递,直到根节点的fiber没有上一级,则跳出while循环 nextFiber = nextFiber.return; } } // 链表循环 function workLoop(IdleDeadline) { // requestIdleCallback调用时会返回浏览器的空闲时间 // 在此处执行我们的单元任务(剩余空闲时间>1时执行) while (nextUnitOfWork && IdleDeadline.timeRemaining() > 1) { // 执行任务,并且返回下一个任务 nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } // 执行完任务后提交 if (!nextUnitOfWork && wipRoot) { commitRoot(); } } // 提交节点 function commitRoot() { commitWork(wipRoot.child); wipRoot = null; } // 执行提交任务 function commitWork(workInProgress) { // 提交自己 if (!workInProgress) { return; } let parentNodeFiber = workInProgress.return; // 更新它的父节点 // 父fiber没有dom节点时,向上查找 while(!parentNodeFiber.stateNode){ parentNodeFiber = parentNodeFiber.return; } let parentNode = parentNodeFiber.stateNode; // 通过父节点的stateNode定位到父级dom节点 if (workInProgress.stateNode) { // 将当前fiber的stateNode,真实dom节点添加到父级dom节点上 parentNode.appendChild(workInProgress.stateNode) } // 提交子节点child commitWork(workInProgress.child) // 提交其他子节点sibling commitWork(workInProgress.sibling) } function render(vnode, container) { // 初始化根节点 wipRoot = { type: 'div', props: { children: { ...vnode } }, stateNode: container } nextUnitOfWork = wipRoot; // 给链表的下一次单元任务传入workInProgress } // 废除 // function render(vnode, container) { // console.log('vnode ===> ', vnode); // // 将vnode => node // const node = creteNode(vnode); // // 将node 挂载到container上 // container.appendChild(node); // } // 废除 // function vnode2Node(vnode) { // let node; // const { // type // } = vnode; // if (typeof type === 'string') { // node = updateHostComponent(vnode) // } else if (typeof type === 'function') { // // 对于vnode来说,类组件和函数组件都是以function的方式传入的,可以在Component基类的原型链上添加标识 // node = type.prototype.isReactComponent ? updateClassComponent(vnode) : updateFunctionCompinent(vnode); // } else { // node = updateTextComponent(vnode) // } // return node; // } // 将原生标签的workInProgress 转变为 node节点 function creteNode(workInProgress) { const { type, props } = workInProgress; let node; node = document.createElement(type); // 创建dom节点 nodeAddAttribute(node, props) // 更新dom的属性 return node; } // 原生标签 function updateHostComponent(workInProgress) { const { props } = workInProgress; // fiber的stateNode指向当前dom节点 if (!workInProgress.stateNode) { workInProgress.stateNode = creteNode(workInProgress); } // 协调 reconcileChildren(workInProgress, props.children) console.log('workInProgress ===>', workInProgress); } // 函数组件 function updateFunctionCompinent(workInProgress) { const { type, props } = workInProgress let vvnode = type(props); // 执行函数组件,使用其props参数,会得到其返回的vnode,此处为了不与入参重名,命名为vvnode // 将其 协调成fiber结构 reconcileChildren(workInProgress, vvnode) } // 类组件 function updateClassComponent(vnode) { const { type, props } = vnode let instance = new type(props); // 类组件需要new出实例 let vvnode = instance.render(); // 类组件在render函数中返回vnode // 使用vnode->node函数转换虚拟dom // return vnode2Node(vvnode); } // 文本标签 function updateTextComponent(workInProgress) { if(!workInProgress.stateNode){ workInProgress.stateNode = document.createTextNode(workInProgress.props); } } // 协调子节点,将子节点workInProgress也转变成fiber结构 function reconcileChildren(workInProgress, children) { if (typeof children === 'string' || typeof children === 'number') { // 基础文本/数值,不再转成fiber结构 return; } let childrenArr = Array.isArray(children) ? children : [children]; // 原本children可能是对象或者数组,此处统一转成数组 let previousNewFiber = null; // 用于暂存在for循环中生成的fiber,用于在循环下一次的fibe是,给其上一个fiberr的sibling节点赋值 for (let i = 0; i < childrenArr.length; i++) { let child = childrenArr[i]; let newFiber = { type: child.type, props: { ...child.props }, stateNode: null, child: null, sibling: null, return: workInProgress } if(typeof child === 'string'){ newFiber.props = child; } // 把第一个生成的fiber当做workInProgress的child if (i === 0) { workInProgress.child = newFiber } else { // 除了第一个生成的fiber,其余都可作为上一个newFiber的sibling节点(链表结构,每个fiber都能指向下一个fiber) previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; // 更新上一个fiber节点 } } // 添加属性 function nodeAddAttribute(node, attrVal) { // children属性中存放着文本,也需要添加到当前节点(新加文本节点插入) Object.keys(attrVal).forEach(key => { if (key === 'children') { if (typeof attrVal[key] === 'string') { node.textContent = attrVal[key] } } else { node[key] = attrVal[key] } }) } // eslint-disable-next-line import/no-anonymous-default-export export default { render };
Fiber架构
一:前言
在 React Fiber 架构面世一年多后,最近 React 又发布了最新版 16.8.0,又一激动人心的特性:React Hooks 正式上线,让我升级 React 的意愿越来越强烈了。在升级之前,不妨回到原点,了解下人才济济的 React 团队为什么要大费周章,重写 React 架构,而 Fiber 又是个什么概念。
二、React 15 的问题
在页面元素很多,且需要频繁刷新的场景下,React 15 会出现掉帧的现象。请看以下例子:
https://claudiopro.github.io/...其根本原因,是大量的同步计算任务阻塞了浏览器的 UI 渲染。默认情况下,JS 运算、页面布局和页面绘制都是运行在浏览器的主线程当中,他们之间是互斥的关系。如果 JS 运算持续占用主线程,页面就没法得到及时的更新。当我们调用
setState
更新页面的时候,React 会遍历应用的所有节点,计算出差异,然后再更新 UI。整个过程是一气呵成,不能被打断的。如果页面元素很多,整个过程占用的时机就可能超过 16 毫秒,就容易出现掉帧的现象。针对这一问题,React 团队从框架层面对 web 页面的运行机制做了优化,得到很好的效果。
三、解题思路
解决主线程长时间被 JS 运算占用这一问题的基本思路,是将运算切割为多个步骤,分批完成。也就是说在完成一部分任务之后,将控制权交回给浏览器,让浏览器有时间进行页面的渲染。等浏览器忙完之后,再继续之前未完成的任务。
旧版 React 通过递归的方式进行渲染,使用的是 JS 引擎自身的函数调用栈,它会一直执行到栈空为止。而
window.requestIdleCallback()会在浏览器空闲时期依次调用函数,这就可以让开发者在主事件循环中执行后台或低优先级的任务,而且不会对像动画和用户交互这些延迟触发但关键的事件产生影响。函数一般会按先进先调用的顺序执行,除非函数在浏览器调用它之前就到了它的超时时间。Fiber
实现了自己的组件调用栈,它以链表的形式遍历组件树,可以灵活的暂停、继续和丢弃执行的任务。实现方式是使用了浏览器的requestIdleCallback
这一 API。官方的解释是这样的:有了解题思路后,我们再来看看 React 具体是怎么做的。
四、React 的答卷
React 框架内部的运作可以分为 3 层:
- Virtual DOM 层,描述页面长什么样。
- Reconciler 层,负责调用组件生命周期方法,进行 Diff 运算等。
- Renderer 层,根据不同的平台,渲染出相应的页面,比较常见的是 ReactDOM 和 ReactNative。
这次改动最大的当属 Reconciler 层了,React 团队也给它起了个新的名字,叫
Fiber Reconciler
。这就引入另一个关键词:Fiber。Fiber 其实指的是一种数据结构,它可以用一个纯 JS 对象来表示:
const fiber = { stateNode, // 节点实例 child, // 子节点 sibling, // 兄弟节点 return, // 父节点 }
为了加以区分,以前的 Reconciler 被命名为
Stack Reconciler
。Stack Reconciler 运作的过程是不能被打断的,必须一条道走到黑:
而 Fiber Reconciler 每执行一段时间,都会将控制权交回给浏览器,可以分段执行:
为了达到这种效果,就需要有一个调度器 (Scheduler) 来进行任务分配。任务的优先级有六种:
- synchronous,与之前的Stack Reconciler操作一样,同步执行
- task,在next tick之前执行
- animation,下一帧之前执行
- high,在不久的将来立即执行
- low,稍微延迟执行也没关系
- offscreen,下一次render时或scroll时才执行
优先级高的任务(如键盘输入)可以打断优先级低的任务(如Diff)的执行,从而更快的生效。
Fiber Reconciler 在执行过程中,会分为 2 个阶段。
- 阶段一,生成 Fiber 树,得出需要更新的节点信息。这一步是一个渐进的过程,可以被打断。
- 阶段二,将需要更新的节点一次过批量更新,这个过程不能被打断。
阶段一可被打断的特性,让优先级更高的任务先执行,从框架层面大大降低了页面掉帧的概率。
五、Fiber 树
Fiber Reconciler 在阶段一进行 Diff 计算的时候,会生成一棵 Fiber 树。这棵树是在 Virtual DOM 树的基础上增加额外的信息来生成的,它本质来说是一个链表。
Fiber 树在首次渲染的时候会一次过生成。在后续需要 Diff 的时候,会根据已有树和最新 Virtual DOM 的信息,生成一棵新的树。这颗新树每生成一个新的节点,都会将控制权交回给主线程,去检查有没有优先级更高的任务需要执行。如果没有,则继续构建树的过程:
如果过程中有优先级更高的任务需要进行,则 Fiber Reconciler 会丢弃正在生成的树,在空闲的时候再重新执行一遍。
在构造 Fiber 树的过程中,Fiber Reconciler 会将需要更新的节点信息保存在
Effect List
当中,在阶段二执行的时候,会批量更新相应的节点。六、总结
本文从 React 15 存在的问题出发,介绍 React Fiber 解决问题的思路,并介绍了 Fiber Reconciler 的工作流程。从
Stack Reconciler
到Fiber Reconciler
,源码层面其实就是干了一件递归改循环的事情。
GitHub地址:
https://github.com/CWH0908/MyReact
参考文章:
教学视频: