由浅入深React的Fiber架构

fiber-cover

目的是初识fiber并实现react基础功能,请带着下面几个问题去阅读此文。html

  • React15存在哪些痛点?Fiber是什么?React16为何须要引入Fiber?
  • 如何实现React16下的虚拟DOM?
  • 如何实现Fiber的数据结构和遍历算法?
  • 如何实现Fiber架构下可中断和可恢复的的任务调度?
    • 如何指定数量更新?如何批量更新?
  • 如何实现Fiber架构下的组件渲染和反作用收集提交?
  • 如何实现Fiber中的调和和双缓冲优化策略?
  • 如何实现useReducer和useState等Hooks?
  • 如何实现expirationTime 任务的优先级 任务调度 超时时间的处理?
  • 如何实现reconcile domdiff的优化key处理?
  • 如何实现合成事件 SyntheticEvent?
  • 如何实现ref useEffect?

此文章首发于@careteen/react,转载请注明来源便可。仓库存放全部实现代码和示例,感兴趣的能够fork调试。node

目录

React15的调度策略

JavaScript就像一条单行道。react

JavaScript是单线程运行的。在浏览器环境中,他须要负责页面的JavaScript解析和执行、绘制、事件处理、静态资源加载和处理。并且只能一个任务一个任务的执行,若是其中某个任务耗时很长,那后面的任务则执行不了,在浏览器端则会呈现卡死的状态。git

browser-render

React15的渲染和diff会递归比对VirtualDOM树找出有增删改的节点,而后同步更新他们,整个过程是一鼓作气的。那么若是页面节点数量很是庞大,React会一直霸占着浏览器资源,一则会致使用户触发的事件得不到响应,二则会致使掉帧,用户会感知到这些卡顿。github

因此针对上述痛点,咱们指望将找出有增删改的节点,而后同步更新他们这个过程分解成两个独立的部分,或者经过某种方式能让整个过程可中断可恢复的执行,相似于多任务操做系统的单处理器调度。算法

为了实现进程的并发,操做系统会按照必定的调度策略,将CPU的执行权分配给多个进程,多个进程都有被执行的机会,让他们交替执行,造成一种同时在运行的假象。由于CPU速度太快,人类根本感受不到。实际上在单核的物理环境下同时只有一个程序在运行。chrome

浏览器任务调度策略和渲染流程

pubg-stuck

玩游戏时须要流畅的刷新率,也就是至少60赫兹。否则游戏体验极差。npm

那么一个帧包含什么呢?api

a-frame

一帧平均是16.66ms,主要分为如下几个部分数组

  • 脚本执行
  • 样式计算
  • 布局
  • 重绘
  • 合成

在样式计算以前会执行脚本计算中使用到requestAnimationFrame的callback

若是你还不了解requestAnimationFrame,前往mdn查看实现的进度条示例。

在合成后还存在一个空闲阶段,即合成及以前的全部步骤耗时若不足16.66ms,剩下的时间浏览器为咱们提供了requestIdleCallback进行调用,对其充分利用。

requestIdleCallback目前只支持chrome,须要polyfill

requestIdleCallback-api

大体流程以下:

requestIdleCallback-flow

requestIdleCallback示例

requestIdleCallback使开发者可以在主事件循环上执行后台和低优先级工做,而不会影响延迟关键事件,如动画和输入响应。

链表的优点

因为数组的大小是固定的,从数组的起点或者中间插入或移除项的成本很高。链表相对于传统的数组的优点在于添加或移除元素的时候不须要移动其余元素,须要添加和移除不少元素时,最好的选择是链表,而非数组。 链表在React的Fiber架构和Hooks实现发挥很大的做用。

更多关于链表的实现和使用

模拟setState

setState

如上可使用链表实现相似于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()参数可对象可函数。

Fiber架构

Fiber出现前怎么作

React15及以前,React会递归比对VirtualDOM树,找出须要变更的节点,而后同步更新它们。这个过程React称为Reconciliation(协调)

Reconciliation期间,React会一直占用着浏览器资源,一则会致使用户触发的事件得不到响应, 二则会致使掉帧,用户可能会感受到卡顿。下面将模拟其遍历过程。

React15的DOMDIFF

dom-tree

将上图节点结构映射成虚拟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

Fiber是什么

  • Fiber是一个执行单元
  • Fiber也是一种数据结构

Fiber是一个执行单元

上面浏览器任务调度过程提到在页面合成后还存在一个空闲阶段requestIdleCallback

下图为React结合空闲阶段的调度过程

fiber-flow

这是一种合做式调度,须要程序和浏览器互相信任。浏览器做为领导者,会分配执行时间片(即requestIdleCallback)给程序去选择调用,程序须要按照约定在这个时间内执行完毕,并将控制权交还浏览器。

Fiber是一个执行单元,每次执行完一个执行单元,React就会检查如今还剩多少时间,若是没有时间就将控制权交还浏览器;而后继续进行下一帧的渲染。

Fiber也是一种数据结构

fiber-structure

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
复制代码

Fiber小结

  • 咱们能够经过某些调度策略合理分配CPU资源,从而提升用户的响应速度
  • 经过Fiber架构,让本身的Reconciliation过程变得可被中断,适时地让出CPU执行权,可让浏览器及时地响应用户的交互

Fiber执行阶段

每次渲染有两个阶段:Reconciliation(协调/render)阶段和Commit(提交)阶段

  • 协调/render阶段:能够认为是Diff阶段,这个阶段能够被中断,这个阶段会找出全部节点变动,例如节点增删改等等,这些变动在React中称为Effect(反作用)。
  • 提交阶段:将上一个阶段计算出来的须要处理的反作用一次性执行。这个阶段不能中断,必须同步一次性执行完。

Reconciliation阶段

下面将上面讲到的几个知识点串联起来使用。

此阶段测试例子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)
}

复制代码

fiber-travers
遍历执行单元流程以下

  1. 从根节点开始遍历
  2. 若是没有长子,则标识当前节点遍历完成。completeUnitOfWork中收集
  3. 若是没有相邻兄弟,则返回父节点标识父节点遍历完成。completeUnitOfWork中收集
  4. 若是没有父节点,标识全部遍历完成。over
  5. 若是有长子,则遍历;beginWork中收集;收集完后返回其长子,回到第2步循环遍历
  6. 若是有相邻兄弟,则遍历;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
}
复制代码

Commit阶段

相似于Git的分支功能,从旧树里面fork一份,在新分支中进行添加、删除、更新操做,而后再进行提交。

git-branch

此阶段测试例子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标识当前节点的反作用类型,第一次渲染为新增PLACEMENTnextEffect标识下一个有反作用的节点。

而后再完善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)
}
复制代码

以下为上述的渲染效果和打印完成的收集顺序

fiber-commit-result

React使用Fiber

准备环境

使用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以后打印结果以下

react-vdom

借用脚手架的babel编译,咱们直接写JSX语法代码。

实现createElement方法

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')
);
复制代码

指望的渲染结果

react-target-render

此时须要定义一些列常量

// 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

  • 原生DOM节点/原生文本节点:直接建立真是DOM节点赋值给stateNode
  • 下面将会对其进行扩展
    • 类组件:须要new一个组件实例挂载到stateNode
    • 函数式组件:没有实例,stateNode为null

reconcileChildren也是对不一样类型节点作处理。

渲染小结

再次巩固下上一节的两个阶段及调度规则

  • 从根节点开始渲染和调度主要有两个阶段
    • render阶段:此阶段比较花时间,咱们能够对任务进行拆分,拆分的维度虚拟DOM。此阶段借助requestIdleCallback能够实现暂停
    • diff阶段:对比新旧的虚拟DOM,进行增量、更新、建立
  • render阶段成果是effect list,收集节点的增删改
  • render阶段有两个任务
    • 根据虚拟DOM生成fiber树
    • 收集effectlist
  • commit阶段,进行DOM更新建立阶段,此阶段不能暂停,要一鼓作气

调度规则

  • 遍历链规则:先长子、后兄弟、再二叔
  • 完成链规则:全部儿子所有完成,则本身完成
  • effect链:同完成链

实现元素的更新

其中使用到双缓冲优化策略,下面将重点介绍

相似于图形化领域绘制引擎经常使用的双缓冲技术。先将图片绘制到一个缓冲区,再一次性传递给屏幕进行显示,这样能够防止屏幕抖动,优化渲染性能。

操做页面进而从新渲染,指望第一次更新为变动A1/B1/C1/C二、新增B3,第二次更新为变动A1/B1/C1/C二、删除B3

react-target-reRender

对应新增代码以下

<!-- 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')
  );
});
复制代码

双缓冲更新策略

fiber-update-process-1

fiber-update-process-2

  • 将每次渲染完后的fiber树赋值给currentRoot
  • 第一次更新时将rooterFiberalternate指向上一次渲染好的currentRoot
  • 第二次以后的更新将workInProgressRoot指向currentRoot.alternate,而后将当前的workInProgressRoot.alternate指向上一次渲染好的currentRoot
  • ...
  • 进而达到复用fiber对象树
变更代码以下
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++;
  }
}
复制代码

实现类组件

fiber-classCom-process-1
构建一个计数器

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);
+}
复制代码

与类组件不同的是函数式组件没有实例,故直接将函数执行的返回值递归调度。

实现Hooks

使用以下

// 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)
+}
复制代码

总结

看完上面很是干的简易实现,再来回顾一开始的几个问题:

  • React15存在哪些痛点?Fiber是什么?React16为何须要引入Fiber?
    • 渲染和diff阶段一鼓作气,节点树庞大时会致使页面卡死
    • Fiber并不神秘,只是将Virtual-DOM转变为一种链表结构
    • 链表结构配合requestIdleCallback可实现可中断可恢复的调度机制
  • 如何实现React16下的虚拟DOM?
    • 同React15
  • 如何实现Fiber的数据结构和遍历算法?
  • 如何实现Fiber架构下可中断和可恢复的的任务调度?
    • 如何指定数量更新?如何批量更新?
    • 借助requestIdleCallback交由浏览器在一帧渲染后的给出的空闲时间内实现指定数量跟新,批量更新能够直接跳过这个API,按以前的方式
  • 如何实现Fiber架构下的组件渲染和反作用收集提交?
    • 执行的收集顺序相似于二叉树的先序遍历
    • 完成的收集顺序相似于二叉树的后序遍历
  • 如何实现Fiber中的调和和双缓冲优化策略?
    • 在Fiber结构中增长一个alternate字段标识上一次渲染好的Fiber树,下次渲染时可复用
  • 如何实现useReducer和useState等Hooks?
  • 如何实现expirationTime 任务的优先级 任务调度 超时时间的处理?
  • 如何实现reconcile domdiff的优化key处理?
  • 如何实现合成事件 SyntheticEvent?
  • 如何实现ref useEffect?

但仍然还有后面几个问题没有解答,下篇文章继续探索...

参考资料

相关文章
相关标签/搜索