目的是初识fiber并实现react基础功能,请带着下面几个问题去阅读此文。html
如何实现Fiber架构下可中断和可恢复的的任务调度?node
此文章首发于@careteen/react,转载请注明来源便可。仓库存放全部实现代码和示例,感兴趣的能够fork调试。
链表的优点react
Fiber架构git
React使用Fiberchrome
JavaScript就像一条单行道。
JavaScript是单线程运行的。在浏览器环境中,他须要负责页面的JavaScript解析和执行、绘制、事件处理、静态资源加载和处理。并且只能一个任务一个任务的执行,若是其中某个任务耗时很长,那后面的任务则执行不了,在浏览器端则会呈现卡死的状态。api
React15的渲染和diff会递归比对VirtualDOM树
,找出有增删改的节点,而后同步更新他们,整个过程是一鼓作气的。那么若是页面节点数量很是庞大,React
会一直霸占着浏览器资源,一则会致使用户触发的事件得不到响应,二则会致使掉帧,用户会感知到这些卡顿。数组
因此针对上述痛点,咱们指望将找出有增删改的节点,而后同步更新他们这个过程分解成两个独立的部分,或者经过某种方式能让整个过程可中断可恢复的执行,相似于多任务操做系统的单处理器调度。
为了实现进程的并发,操做系统会按照必定的调度策略,将CPU的执行权分配给多个进程,多个进程都有被执行的机会,让他们交替执行,造成一种同时在运行的假象。由于CPU速度太快,人类根本感受不到。实际上在单核的物理环境下同时只有一个程序在运行。
玩游戏时须要流畅的刷新率,也就是至少60赫兹。否则游戏体验极差。
那么一个帧包含什么呢?
一帧平均是16.66ms,主要分为如下几个部分
在样式计算以前会执行脚本计算中使用到requestAnimationFrame
的callback
若是你还不了解 requestAnimationFrame,前往mdn查看实现的进度条示例。
在合成后还存在一个空闲阶段
,即合成及以前的全部步骤耗时若不足16.66ms
,剩下的时间浏览器为咱们提供了requestIdleCallback
进行调用,对其充分利用。
requestIdleCallback目前只支持chrome,须要 polyfill
大体流程以下:
requestIdleCallback示例
requestIdleCallback使开发者可以在主事件循环上执行后台和低优先级工做,而不会影响延迟关键事件,如动画和输入响应。
因为数组的大小是固定的,从数组的起点或者中间插入或移除项的成本很高。链表相对于传统的数组的优点在于添加或移除元素的时候不须要移动其余元素,须要添加和移除不少元素时,最好的选择是链表,而非数组。 链表在React的Fiber架构和Hooks实现发挥很大的做用。
更多关于链表的实现和使用
如上可使用链表实现相似于React的setState方法
。
// 表示一个节点 class Update { constructor(payload, nextUpdate) { this.payload = payload this.nextUpdate = nextUpdate } }
一个节点须要payload
挂载数据,nextUpdate
指向下一个节点。
// 模拟链表 class UpdateQueue { constructor() { this.baseState = null this.firstUpdate = null this.lastUpdate = null } enqueue(update) { if (!this.firstUpdate) { this.firstUpdate = this.lastUpdate = update } else { this.lastUpdate.nextUpdate = update this.lastUpdate = update } } }
链表初始化时须要baseState
存放数据,firstUpdate
指向第一个节点,lastUpdate
指向最后一个节点。
以及enqueue
将节点链起来。
const isFunction = (func) => { return typeof func === 'function' } class UpdateQueue { forceUpdate() { let currentState = this.baseState || {} let currentUpdate = this.firstUpdate while(currentUpdate) { const nextState = isFunction(currentUpdate.payload) ? currentUpdate.payload(currentState) : currentUpdate.payload currentState = { ...currentState, ...nextState } currentUpdate = currentUpdate.nextUpdate } this.firstUpdate = this.lastUpdate = null return this.baseState = currentState } }
还须要forceUpdate
将全部节点挂载的数据合并。相似于React.setState()
参数可对象可函数。
在React15
及以前,React
会递归比对VirtualDOM
树,找出须要变更的节点,而后同步更新它们。这个过程React
称为Reconciliation(协调)
。
在Reconciliation
期间,React
会一直占用着浏览器资源,一则会致使用户触发的事件得不到响应, 二则会致使掉帧,用户可能会感受到卡顿。下面将模拟其遍历过程。
将上图节点结构映射成虚拟DOM
const root = { key: 'A1', children: [ { key: 'B1', children: [ { key: 'C1', children: [] }, { key: 'C2', children: [] } ] }, { key: 'B2', children: [] } ] }
采用深度优先算法对其遍历
详解DFS
function walk(vdom, cb) { cb && cb(vdom) vdom.children.forEach(child => walk(child, cb)) } // Test walk(root, (node) => { console.log(node.key) // A1 B1 C1 C2 B2 })
在Dom-Diff
时也是如此递归遍历对比,且存在两个很是影响性能的问题。
详解React的Dom-Diff
上面浏览器任务调度过程
提到在页面合成后还存在一个空闲阶段requestIdleCallback
。
下图为React结合空闲阶段的调度过程
这是一种合做式调度,须要程序和浏览器互相信任。浏览器做为领导者,会分配执行时间片(即requestIdleCallback)给程序去选择调用,程序须要按照约定在这个时间内执行完毕,并将控制权交还浏览器。
Fiber是一个执行单元,每次执行完一个执行单元,React就会检查如今还剩多少时间,若是没有时间就将控制权交还浏览器;而后继续进行下一帧的渲染。
React中使用链表将Virtual DOM
连接起来,每个节点表示一个Fiber
class FiberNode { constructor(type, payload) { this.type = type // 节点类型 this.key = payload.key // key this.payload = payload // 挂载的数据 this.return = null // 父Fiber this.child = null // 长子Fiber this.sibling = null // 相邻兄弟Fiber } } // Test const A1 = new FiberNode('div', { key: 'A1' }) const B1 = new FiberNode('div', { key: 'B1' }) const B2 = new FiberNode('div', { key: 'B2' }) const C1 = new FiberNode('div', { key: 'C1' }) const C2 = new FiberNode('div', { key: 'C2' }) A1.child = B1 B1.return = A1 B1.sibling = B2 B1.child = C1 B2.return = A1 C1.return = B1 C1.sibling = C2 C2.return = B1
每次渲染有两个阶段:Reconciliation
(协调/render)阶段和Commit
(提交)阶段
下面将上面讲到的几个知识点串联起来使用。
此阶段测试例子 fiberRender.html,核心代码存放 fiberRender.js。
上面Fiber也是一种数据结构
小结已经构建了Fiber树,而后来开始遍历,在第一次渲染中,全部操做类型都是新增。
根据Virtual DOM
去构建Fiber Tree
nextUnitOfWork = A1 requestIdleCallback(workLoop, { timeout: 1000 })
空闲时间去遍历收集A1
根节点
function workLoop (deadline) { // 这一帧渲染还有空闲时间 || 没超时 && 还存在一个执行单元 while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextUnitOfWork) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork) // 执行当前执行单元 并返回下一个执行单元 } if (!nextUnitOfWork) { console.log('render end !') } else { requestIdleCallback(workLoop, { timeout: 1000 }) } }
这一帧渲染还有空闲时间或没超时 && 还存在一个执行单元
时去执行当前执行单元 并返回下一个执行单元。不知足上面条件后若还存在一个执行单元,会继续下一帧的渲染。
function performUnitOfWork (fiber) { beginWork(fiber) // 开始 if (fiber.child) { return fiber.child } while (fiber) { completeUnitOfWork(fiber) // 结束 if (fiber.sibling) { return fiber.sibling } fiber = fiber.return } } function beginWork (fiber) { console.log('start: ', fiber.key) } function completeUnitOfWork (fiber) { console.log('end: ', fiber.key) }
遍历执行单元流程以下
completeUnitOfWork
中收集completeUnitOfWork
中收集over
beginWork
中收集;收集完后返回其长子,回到第2步
循环遍历beginWork
中收集;收集完后返回其长子,回到第2步
循环遍历执行的收集顺序以下
相似 二叉树的先序遍历
function beginWork (fiber) { console.log('start: ', fiber.key) // A1 B1 C1 C2 B2 }
完成的收集顺序以下
相似 二叉树的后序遍历
function completeUnitOfWork (fiber) { console.log('end: ', fiber.key) // C1 C2 B1 B2 A1 }
相似于Git
的分支功能,从旧树里面fork一份,在新分支中进行添加、删除、更新操做,而后再进行提交。
此阶段测试例子 fiberCommit.html,核心代码存放 fiberCommit.js。
先构造根fiber,stateNode
表示当前节点真实dom。
let container = document.getElementById('root') workInProgressRoot = { key: 'ROOT', // 节点实例(状态): // 对于宿主组件,这里保存宿主组件的实例, 例如DOM节点 // 对于类组件来讲,这里保存类组件的实例 // 对于函数组件说,这里为空,由于函数组件没有实例 stateNode: container, props: { children: [A1] } } nextUnitOfWork = workInProgressRoot // 从RootFiber开始,到RootFiber结束
如上一个阶段的beginWork
收集过程,对其进行完善。即将全部节点fiber化。
function beginWork(currentFiber) { // ++ if (!currentFiber.stateNode) { currentFiber.stateNode = document.createElement(currentFiber.type) // 建立真实DOM for (let key in currentFiber.props) { // 循环属性赋赋值给真实DOM if (key !== 'children' && key !== 'key') currentFiber.stateNode.setAttribute(key, currentFiber.props[key]) } } let previousFiber currentFiber.props.children.forEach((child, index) => { let childFiber = { tag: 'HOST', type: child.type, key: child.key, props: child.props, return: currentFiber, // 当前节点的反作用类型,例如节点更新、删除、移动 effectTag: 'PLACEMENT', // 和节点关系同样,React 一样使用链表来将全部有反作用的Fiber链接起来 nextEffect: null } if (index === 0) { currentFiber.child = childFiber } else { previousFiber.sibling = childFiber } previousFiber = childFiber }) }
其中effectTag
标识当前节点的反作用类型,第一次渲染为新增PLACEMENT
,nextEffect
标识下一个有反作用的节点。
而后再完善completeUnitOfWork
(完成的收集)。
function completeUnitOfWork(currentFiber) { // ++ const returnFiber = currentFiber.return if (returnFiber) { if (!returnFiber.firstEffect) { returnFiber.firstEffect = currentFiber.firstEffect } if (currentFiber.lastEffect) { if (returnFiber.lastEffect) { returnFiber.lastEffect.nextEffect = currentFiber.firstEffect } returnFiber.lastEffect = currentFiber.lastEffect } if (currentFiber.effectTag) { if (returnFiber.lastEffect) { returnFiber.lastEffect.nextEffect = currentFiber } else { returnFiber.firstEffect = currentFiber } returnFiber.lastEffect = currentFiber } } }
目的是将完成的收集造成一个链表结构,配合commitRoot
阶段。
当将全部的执行、完成
收集完成后(即将全部真实DOM、虚拟DOM、Fiber结合,其反作用(增删改)造成一个链表结构),须要对其渲染到页面中。
function workLoop (deadline) { // ... if (!nextUnitOfWork) { console.log('render end !') commitRoot() } else { requestIdleCallback(workLoop, { timeout: 1000 }) } }
找到第一个反作用完成的fiber节点,递归appendChild
到父元素上。
function commitRoot() { // ++ let fiber = workInProgressRoot.firstEffect while (fiber) { console.log('complete: ', fiber.key) // C1 C2 B1 B2 A1 commitWork(fiber) fiber = fiber.nextEffect } workInProgressRoot = null } function commitWork(currentFiber) { currentFiber.return.stateNode.appendChild(currentFiber.stateNode) }
以下为上述的渲染效果和打印完成的收集顺序
使用react-create-app
建立一个项目fiber
// src/index.js import React from 'react' let element = ( <div id="A1"> <div id="B1"> <div id="C1"></div> <div id="C2"></div> </div> <div id="B2"></div> </div> ) console.log(element);
npm i && npm start
以后打印结果以下
借用脚手架的babel编译,咱们直接写JSX语法
代码。
在babel
编译时将JSX
语法转为一个对象,而后调用react下的React.createElement
方法构建虚拟dom。咱们能够以下模拟:
// core/react.js const ELEMENT_TEXT = Symbol.for('ELEMENT_TEXT'); function createElement(type, config, ...children) { return { type, // 元素类型 props: { ...config, children: children.map( child => typeof child === "object" ? child : { type: ELEMENT_TEXT, props: { text: child, children: [] } }) } } } let React = { createElement } export default React;
若是children
中有child是一个React.createElement
返回的React元素
,且是字符串的话,会被转成文本节点。
准备以下结构
// src/index.js import React from 'react' import ReactDOM from 'react-dom' let style = { border: '3px solid green', margin: '5px' }; let element = ( <div id="A1" style={style}> A1 <div id="B1" style={style}> B1 <div id="C1" style={style}>C1</div> <div id="C2" style={style}>C2</div> </div> <div id="B2" style={style}>B2</div> </div> ) ReactDOM.render( element, document.getElementById('root') );
指望的渲染结果
此时须要定义一些列常量
// core/constants.js export const ELEMENT_TEXT = Symbol.for('ELEMENT_TEXT'); // 文本元素 export const TAG_ROOT = Symbol.for('TAG_ROOT'); // 根Fiber export const TAG_HOST = Symbol.for('TAG_HOST'); // 原生的节点 span div p 函数组件 类组件 export const TAG_TEXT = Symbol.for('TAG_TEXT'); // 文本节点 export const PLACEMENT = Symbol.for('PLACEMENT'); // 插入节点
而后借助上述的Reconciliation阶段
,在react-dom.js
中先将虚拟dom构建成一根fiber树
// core/react-dom.js import { TAG_ROOT } from './constants'; import { scheduleRoot } from './scheduler'; function render(element, container) { let rootFiber = { tag: TAG_ROOT, // 这是根Fiber stateNode: container, // 此Fiber对应的DOM节点 props: { children: [element] }, // 子元素就是要渲染的element } scheduleRoot(rootFiber); } export default { render }
而后交由scheduleRoot
进行调度
// core/scheduler.js // ...
代码量较多,主要为Reconciliation阶段
和Commit阶段
的组合代码。
此过程代码存放地址
其中对beginWork
进行细化
function beginWork(currentFiber) { if (currentFiber.tag === TAG_ROOT) { // 若是是根节点 updateHostRoot(currentFiber); } else if (currentFiber.tag === TAG_TEXT) { // 若是是原生文本节点 updateHostText(currentFiber); } else if (currentFiber.tag === TAG_HOST) { // 若是是原生DOM节点 updateHostComponent(currentFiber); } } function updateHostRoot(currentFiber) { // 若是是根节点 const newChildren = currentFiber.props.children; // 直接渲染子节点 reconcileChildren(currentFiber, newChildren); } function updateHostText(currentFiber) { if (!currentFiber.stateNode) { currentFiber.stateNode = createDOM(currentFiber); // 先建立真实的DOM节点 } } function updateHostComponent(currentFiber) { // 若是是原生DOM节点 if (!currentFiber.stateNode) { currentFiber.stateNode = createDOM(currentFiber); // 先建立真实的DOM节点 } const newChildren = currentFiber.props.children; reconcileChildren(currentFiber, newChildren); }
其中主要是针对不一样类型节点赋值给stateNode
stateNode
下面将会对其进行扩展
stateNode
stateNode
为nullreconcileChildren
也是对不一样类型节点作处理。
再次巩固下上一节的两个阶段及调度规则
从根节点开始渲染和调度主要有两个阶段
requestIdleCallback
能够实现暂停effect list
,收集节点的增删改render阶段有两个任务
effectlist
调度规则
其中使用到双缓冲优化策略,下面将重点介绍
相似于图形化领域绘制引擎经常使用的 双缓冲技术。先将图片绘制到一个缓冲区,再一次性传递给屏幕进行显示,这样能够防止屏幕抖动,优化渲染性能。
操做页面进而从新渲染,指望第一次更新为变动A1/B1/C1/C二、新增B3
,第二次更新为变动A1/B1/C1/C二、删除B3
。
对应新增代码以下
<!-- public/index.html --> <div id="root"></div> <button id="reRender1">reRender1</button> <button id="reRender2">reRender2</button> <button id="reRender3">reRender3</button>
为两个按钮绑定事件,从新渲染页面
// src/index.js let reRender2 = document.getElementById('reRender2'); reRender2.addEventListener('click', () => { let element2 = ( <div id="A1-new" style={style}> A1-new <div id="B1-new" style={style}> B1-new <div id="C1-new" style={style}>C1-new</div> <div id="C2-new" style={style}>C2-new</div> </div> <div id="B2" style={style}>B2</div> <div id="B3" style={style}>B3</div> </div> ) ReactDOM.render( element2, document.getElementById('root') ); }); let reRender3 = document.getElementById('reRender3'); reRender3.addEventListener('click', () => { let element3 = ( <div id="A1-new2" style={style}> A1-new2 <div id="B1-new2" style={style}> B1-new2 <div id="C1-new2" style={style}>C1-new2</div> <div id="C2-new2" style={style}>C2-new2</div> </div> <div id="B2" style={style}>B2</div> </div> ) ReactDOM.render( element3, document.getElementById('root') ); });
currentRoot
rooterFiber
的alternate
指向上一次渲染好的currentRoot
workInProgressRoot
指向currentRoot.alternate
,而后将当前的workInProgressRoot.alternate
指向上一次渲染好的currentRoot
<details>
<summary>变更代码以下</summary>
import { setProps } from './utils'; import { ELEMENT_TEXT, TAG_ROOT, TAG_HOST, TAG_TEXT, PLACEMENT, DELETION, UPDATE } from './constants'; +let currentRoot = null;//当前的根Fiber let workInProgressRoot = null;//正在渲染中的根Fiber let nextUnitOfWork = null//下一个工做单元 +let deletions = [];//要删除的fiber节点 export function scheduleRoot(rootFiber) { // {tag:TAG_ROOT,stateNode:container,props: { children: [element] }} + if (currentRoot && currentRoot.alternate) {//偶数次更新 + workInProgressRoot = currentRoot.alternate; + workInProgressRoot.firstEffect = workInProgressRoot.lastEffect = workInProgressRoot.nextEffect = null; + workInProgressRoot.props = rootFiber.props; + workInProgressRoot.alternate = currentRoot; + } else if (currentRoot) {//奇数次更新 + rootFiber.alternate = currentRoot; + workInProgressRoot = rootFiber; + } else { + workInProgressRoot = rootFiber;//第一次渲染 + } nextUnitOfWork = workInProgressRoot; } function commitRoot() { + deletions.forEach(commitWork); let currentFiber = workInProgressRoot.firstEffect; while (currentFiber) { commitWork(currentFiber); currentFiber = currentFiber.nextEffect; } + deletions.length = 0;//先把要删除的节点清空掉 + currentRoot = workInProgressRoot; workInProgressRoot = null; } function commitWork(currentFiber) { if (!currentFiber) { return; } let returnFiber = currentFiber.return;//先获取父Fiber const domReturn = returnFiber.stateNode;//获取父的DOM节点 if (currentFiber.effectTag === PLACEMENT && currentFiber.stateNode != null) {//若是是新增DOM节点 let nextFiber = currentFiber; domReturn.appendChild(nextFiber.stateNode); + } else if (currentFiber.effectTag === DELETION) {//若是是删除则删除并返回 + domReturn.removeChild(currentFiber.stateNode); + } else if (currentFiber.effectTag === UPDATE && currentFiber.stateNode != null) {//若是是更新 + if (currentFiber.type === ELEMENT_TEXT) { + if (currentFiber.alternate.props.text != currentFiber.props.text) { + currentFiber.stateNode.textContent = currentFiber.props.text; + } + } else { + updateDOM(currentFiber.stateNode, currentFiber.alternate.props, currentFiber.props); + } + } currentFiber.effectTag = null; } function reconcileChildren(currentFiber, newChildren) { let newChildIndex = 0;//新虚拟DOM数组中的索引 + let oldFiber = currentFiber.alternate && currentFiber.alternate.child;//父Fiber中的第一个子Fiber + let prevSibling; + while (newChildIndex < newChildren.length || oldFiber) { + const newChild = newChildren[newChildIndex]; + let newFiber; + const sameType = oldFiber && newChild && newChild.type === oldFiber.type;//新旧都有,而且元素类型同样 + let tag; + if (newChild && newChild.type === ELEMENT_TEXT) { + tag = TAG_TEXT;//文本 + } else if (newChild && typeof newChild.type === 'string') { + tag = TAG_HOST;//原生DOM组件 + } + if (sameType) { + if (oldFiber.alternate) { + newFiber = oldFiber.alternate; + newFiber.props = newChild.props; + newFiber.alternate = oldFiber; + newFiber.effectTag = UPDATE; + newFiber.nextEffect = null; + } else { + newFiber = { + tag:oldFiber.tag,//标记Fiber类型,例如是函数组件或者原生组件 + type: oldFiber.type,//具体的元素类型 + props: newChild.props,//新的属性对象 + stateNode: oldFiber.stateNode,//原生组件的话就存放DOM节点,类组件的话是类组件实例,函数组件的话为空,由于没有实例 + return: currentFiber,//父Fiber + alternate: oldFiber,//上一个Fiber 指向旧树中的节点 + effectTag: UPDATE,//反作用标识 + nextEffect: null //React 一样使用链表来将全部有反作用的Fiber链接起来 + } # + } + } else { + if (newChild) {//类型不同,建立新的Fiber,旧的不复用了 + newFiber = { + tag,//原生DOM组件 + type: newChild.type,//具体的元素类型 + props: newChild.props,//新的属性对象 + stateNode: null,//stateNode确定是空的 + return: currentFiber,//父Fiber + effectTag: PLACEMENT//反作用标识 + } + } + if (oldFiber) { + oldFiber.effectTag = DELETION; + deletions.push(oldFiber); + } + } + if (oldFiber) { //比较完一个元素了,老Fiber向后移动1位 + oldFiber = oldFiber.sibling; + } if (newFiber) { if (newChildIndex === 0) { currentFiber.child = newFiber;//第一个子节点挂到父节点的child属性上 } else { prevSibling.sibling = newFiber; } prevSibling = newFiber;//而后newFiber变成了上一个哥哥了 } prevSibling = newFiber;//而后newFiber变成了上一个哥哥了 newChildIndex++; } }
</details>
构建一个计数器
class ClassCounter extends React.Component { constructor(props) { super(props); this.state = { number: 0 }; } onClick = () => { this.setState(state => ({ number: state.number + 1 })); } render() { return ( <div id="counter"> <span>{this.state.number}</span> <button onClick={this.onClick}>加1</button> </div > ) } } ReactDOM.render( <ClassCounter />, document.getElementById('root') );
import { ELEMENT_TEXT } from './constants'; +import { Update, UpdateQueue } from './updateQueue'; +import { scheduleRoot } from './scheduler'; // ... +class Component { + constructor(props) { + this.props = props; + this.updateQueue = new UpdateQueue(); + } + setState(payload) { + this.internalFiber.updateQueue.enqueueUpdate(new Update(payload)); + scheduleRoot(); + } +} +Component.prototype.isReactComponent = true; let React = { createElement, + Component } export default React;
此过程在模拟setState过程已经说明
export class Update { constructor(payload) { this.payload = payload; } } // 数据结构是一个单链表 export class UpdateQueue { constructor() { this.firstUpdate = null; this.lastUpdate = null; } enqueueUpdate(update) { if (this.lastUpdate === null) { this.firstUpdate = this.lastUpdate = update; } else { this.lastUpdate.nextUpdate = update; this.lastUpdate = update; } } forceUpdate(state) { let currentUpdate = this.firstUpdate; while (currentUpdate) { let nextState = typeof currentUpdate.payload === 'function' ? currentUpdate.payload(state) : currentUpdate.payload; state = { ...state, ...nextState }; currentUpdate = currentUpdate.nextUpdate; } this.firstUpdate = this.lastUpdate = null; return state; } }
须要在src/scheduler.js
文件中作以下修改
function beginWork(currentFiber) { if (currentFiber.tag === TAG_ROOT) {//若是是根节点 updateHostRoot(currentFiber); } else if (currentFiber.tag === TAG_TEXT) {//若是是原生文本节点 updateHostText(currentFiber); } else if (currentFiber.tag === TAG_HOST) {//若是是原生DOM节点 updateHostComponent(currentFiber); + } else if (currentFiber.tag === TAG_CLASS) {//若是是类组件 + updateClassComponent(currentFiber) + } } +function updateClassComponent(currentFiber) { + if (currentFiber.stateNode === null) { + currentFiber.stateNode = new currentFiber.type(currentFiber.props); + currentFiber.stateNode.internalFiber = currentFiber; + currentFiber.updateQueue = new UpdateQueue(); + } + currentFiber.stateNode.state = currentFiber.updateQueue.forceUpdate(currentFiber.stateNode.state); + const newChildren = [currentFiber.stateNode.render()]; + reconcileChildren(currentFiber, newChildren); +}
若是是类组件,则new这个类将实例缓存到currentFiber.stateNode
,再将实例的render()方法执行结果
递归调度reconcileChildren
同类组件同样,在各对应地方新增一份else..if
便可
function FunctionCounter() { return ( <h1> Count:0 </h1> ) } ReactDOM.render( <FunctionCounter />, document.getElementById('root') );
function beginWork(currentFiber) { if (currentFiber.tag === TAG_ROOT) {//若是是根节点 updateHostRoot(currentFiber); } else if (currentFiber.tag === TAG_TEXT) {//若是是原生文本节点 updateHostText(currentFiber); } else if (currentFiber.tag === TAG_HOST) {//若是是原生DOM节点 updateHostComponent(currentFiber); } else if (currentFiber.tag === TAG_CLASS) {//若是是类组件 updateClassComponent(currentFiber) + } else if (currentFiber.tag === TAG_FUNCTION) {//若是是函数组件 + updateFunctionComponent(currentFiber); + } } +function updateFunctionComponent(currentFiber) { + const newChildren = [currentFiber.type(currentFiber.props)]; + reconcileChildren(currentFiber, newChildren); +}
与类组件不同的是函数式组件没有实例,故直接将函数执行的返回值递归调度。
使用以下
// src/index.js import React from 'react' import ReactDOM from 'react-dom' // import React from '../../../packages/fiber/core/react'; // import ReactDOM from '../../../packages/fiber/core/react-dom'; function reducer(state, action) { switch (action.type) { case 'ADD': return { count: state.count + 1 }; default: return state; } } function FunctionCounter() { const [numberState, setNumberState] = React.useState({ number: 0 }); const [countState, dispatch] = React.useReducer(reducer, { count: 0 }); return ( <div> <h1 onClick={() => setNumberState(state => ({ number: state.number + 1 }))}> Count: {numberState.number} </h1 > <hr /> <h1 onClick={() => dispatch({ type: 'ADD' })}> Count: {countState.count} </h1 > </div> ) } ReactDOM.render( <FunctionCounter />, document.getElementById('root') );
须要react提供useState/useReducer
两个Hook
// core/react.js +import { scheduleRoot,useState,useReducer} from './scheduler'; let React = { createElement, Component, + useState, + useReducer }
实现过程以下
// core/scheduler.js +import { UpdateQueue, Update } from './updateQueue'; +let workInProgressFiber = null; //正在工做中的fiber +let hookIndex = 0; //hook索引 function updateFunctionComponent(currentFiber) { + workInProgressFiber = currentFiber; + hookIndex = 0; + workInProgressFiber.hooks = []; const newChildren = [currentFiber.type(currentFiber.props)]; reconcileChildren(currentFiber, newChildren); } +export function useReducer(reducer, initialValue) { + let oldHook = + workInProgressFiber.alternate && + workInProgressFiber.alternate.hooks && + workInProgressFiber.alternate.hooks[hookIndex]; + let newHook = oldHook; + if (oldHook) { + oldHook.state = oldHook.updateQueue.forceUpdate(oldHook.state); + } else { + newHook = { + state: initialValue, + updateQueue: new UpdateQueue() + }; + } + const dispatch = action => { + newHook.updateQueue.enqueueUpdate( + new Update(reducer ? reducer(newHook.state, action) : action) + ); + scheduleRoot(); + } + workInProgressFiber.hooks[hookIndex++] = newHook; + return [newHook.state, dispatch]; +} +export function useState(initState) { + return useReducer(null, initState) +}
看完上面很是干的简易实现,再来回顾一开始的几个问题:
[x] React15存在哪些痛点?Fiber是什么?React16为何须要引入Fiber?
[x] 如何实现React16下的虚拟DOM?
[x] 如何实现Fiber的数据结构和遍历算法?
[x] 如何实现Fiber架构下可中断和可恢复的的任务调度?
[x] 如何实现Fiber架构下的组件渲染和反作用收集提交?
[x] 如何实现Fiber中的调和和双缓冲优化策略?
但仍然还有后面几个问题没有解答,下篇文章继续探索...