关于前端:手写React的Fiber架构深入理解其原理

40次阅读

共计 16019 个字符,预计需要花费 41 分钟才能阅读完成。

相熟 React 的敌人都晓得,React 反对 jsx 语法,咱们能够间接将 HTML 代码写到 JS 两头,而后渲染到页面上,咱们写的 HTML 如果有更新的话,React 还有虚构 DOM 的比照,只更新变动的局部,而不从新渲染整个页面,大大提高渲染效率。到了 16.x,React 更是应用了一个被称为 Fiber 的架构,晋升了用户体验,同时还引入了 hooks 等个性。那暗藏在 React 背地的原理是怎么的呢,Fiberhooks 又是怎么实现的呢?本文会从 jsx 动手,手写一个简易版的 React,从而深刻了解 React 的原理。

本文次要实现了这些性能:

简易版 Fiber 架构

简易版 DIFF 算法

简易版函数组件

简易版 Hook: useState

娱乐版 Class 组件

本文代码地址:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/fiber-and-hooks

本文程序跑起来成果如下:

JSX 和 creatElement

以前咱们写 React 要反对 JSX 还须要一个库叫JSXTransformer.js,起初 JSX 的转换工作都集成到了 babel 外面了,babel 还提供了在线预览的性能,能够看到转换后的成果,比方上面这段简略的代码:

const App =
(
  <div>
    <h1 id="title">Title</h1>
    <a href="xxx">Jump</a>
    <section>
      <p>
        Article
      </p>
    </section>
  </div>
);

通过 babel 转换后就变成了这样:

下面的截图能够看出咱们写的 HTML 被转换成了React.createElement,咱们将下面代码略微格式化来看下:

var App = React.createElement(
  'div',
  null,
  React.createElement(
    'h1',
    {id: 'title',},
    'Title',
  ),
  React.createElement(
    'a',
    {href: 'xxx',},
    'Jump',
  ),
  React.createElement(
    'section',
    null,
    React.createElement('p', null, 'Article'),
  ),
);

从转换后的代码咱们能够看出 React.createElement 反对多个参数:

  1. type,也就是节点类型
  2. config, 这是节点上的属性,比方 idhref
  3. children, 从第三个参数开始就全副是 children 也就是子元素了,子元素能够有多个,类型能够是简略的文本,也能够还是 React.createElement,如果是React.createElement,其实就是子节点了,子节点上面还能够有子节点。这样就用React.createElement 的嵌套关系实现了 HTML 节点的树形构造。

让咱们来残缺看下这个简略的 React 页面代码:

渲染在页面上是这样:

这外面用到了 React 的中央其实就两个,一个是 JSX,也就是 React.createElement,另一个就是ReactDOM.render,所以咱们手写的第一个指标就有了,就是createElementrender这两个办法。

手写 createElement

对于 <h1 id="title">Title</h1> 这样一个简略的节点,原生 DOM 也会附加一大堆属性和办法在下面,所以咱们在 createElement 的时候最好能将它转换为一种比较简单的数据结构,只蕴含咱们须要的元素,比方这样:

{
  type: 'h1',
  props: {
    id: 'title',
    children: 'Title'
  }
}

有了这个数据结构后,咱们对于 DOM 的操作其实能够转化为对这个数据结构的操作,新老 DOM 的比照其实也能够转化为这个数据结构的比照,这样咱们就不须要每次操作都去渲染页面,而是等到须要渲染的时候才将这个数据结构渲染到页面上。这其实就是虚构 DOM!而咱们 createElement 就是负责来构建这个虚构 DOM 的办法,上面咱们来实现下:

function createElement(type, props, ...children) {
  // 外围逻辑不简单,将参数都塞到一个对象上返回就行
  // children 也要放到 props 外面去,这样咱们在组件外面就能通过 this.props.children 拿到子元素
  return {
    type,
    props: {
      ...props,
      children
    }
  }
}

上述代码是 React 的 createElement 简化版,对源码感兴趣的敌人能够看这里:https://github.com/facebook/react/blob/60016c448bb7d19fc989acd05dda5aca2e124381/packages/react/src/ReactElement.js#L348

手写 render

上述代码咱们用 createElement 将 JSX 代码转换成了虚构 DOM,那真正将它渲染到页面的函数是 render,所以咱们还须要实现下这个办法,通过咱们个别的用法ReactDOM.render(<App />,document.getElementById('root')); 能够晓得他接管两个参数:

  1. 根组件,其实是一个 JSX 组件,也就是一个 createElement 返回的虚构 DOM
  2. 父节点,也就是咱们要将这个虚构 DOM 渲染的地位

有了这两个参数,咱们来实现下 render 办法:

function render(vDom, container) {
  let dom;
  // 查看以后节点是文本还是对象
  if(typeof vDom !== 'object') {dom = document.createTextNode(vDom)
  } else {dom = document.createElement(vDom.type);
  }

  // 将 vDom 上除了 children 外的属性都挂载到真正的 DOM 下来
  if(vDom.props) {Object.keys(vDom.props)
      .filter(key => key != 'children')
      .forEach(item => {dom[item] = vDom.props[item];
      })
  }
  
  // 如果还有子元素,递归调用
  if(vDom.props && vDom.props.children && vDom.props.children.length) {vDom.props.children.forEach(child => render(child, dom));
  }

  container.appendChild(dom);
}

上述代码是简化版的 render 办法,对源码感兴趣的敌人能够看这里:https://github.com/facebook/react/blob/3e94bce765d355d74f6a60feb4addb6d196e3482/packages/react-dom/src/client/ReactDOMLegacy.js#L287

当初咱们能够用本人写的 createElementrender来替换原生的办法了:

能够失去一样的渲染后果:

为什么须要 Fiber

下面咱们简略的实现了虚构 DOM 渲染到页面上的代码,这部分工作被 React 官网称为 renderer,renderer 是第三方能够本人实现的一个模块,还有个外围模块叫做 reconsiler,reconsiler 的一大性能就是大家熟知的 diff,他会计算出应该更新哪些页面节点,而后将须要更新的节点虚构 DOM 传递给 renderer,renderer 负责将这些节点渲染到页面上。然而这个流程有个问题,尽管 React 的 diff 算法是通过优化的,然而他却是同步的,renderer 负责操作 DOM 的 appendChild 等 API 也是同步的,也就是说如果有大量节点须要更新,JS 线程的运行工夫可能会比拟长,在这段时间浏览器是不会响应其余事件的,因为 JS 线程和 GUI 线程是互斥的,JS 运行时页面就不会响应,这个工夫太长了,用户就可能看到卡顿,特地是动画的卡顿会很显著。在 React 的官网演讲中有个例子,能够很显著的看到这种同步计算造成的卡顿:

而 Fiber 就是用来解决这个问题的,Fiber 能够将长时间的同步工作拆分成多个小工作,从而让浏览器可能抽身去响应其余事件,等他空了再回来持续计算,这样整个计算流程就显得平滑很多。上面是应用 Fiber 后的成果:

怎么来拆分

下面咱们本人实现的 render 办法间接递归遍历了整个 vDom 树,如果咱们在中途某一步停下来,下次再调用时其实并不知道上次在哪里停下来的,不晓得从哪里开始,所以 vDom 的树形构造并不满足中途暂停,下次持续的需要,须要革新数据结构。另一个须要解决的问题是,拆分下来的小工作什么时候执行?咱们的目标是让用户有更晦涩的体验,所以咱们最好不要阻塞高优先级的工作,比方用户输出,动画之类,等他们执行完了咱们再计算。那我怎么晓得当初有没有高优先级工作,浏览器是不是闲暇呢?总结下来,Fiber 要想达到目标,须要解决两个问题:

  1. 新的任务调度,有高优先级工作的时候将浏览器让进去,等浏览器空了再继续执行
  2. 新的数据结构,能够随时中断,下次进来能够接着执行

requestIdleCallback

requestIdleCallback是一个试验中的新 API,这个 API 调用形式如下:

// 开启调用
var handle = window.requestIdleCallback(callback[, options])

// 完结调用
Window.cancelIdleCallback(handle) 

requestIdleCallback接管一个回调,这个回调会在浏览器闲暇时调用,每次调用会传入一个 IdleDeadline,能够拿到以后还空余多久,options 能够传入参数最多等多久,等到了工夫浏览器还不空就强制执行了。应用这个 API 能够解决任务调度的问题,让浏览器在闲暇时才计算 diff 并渲染。更多对于 requestIdleCallback 的应用能够查看 MDN 的文档。然而这个 API 还在试验中,兼容性不好,所以 React 官网本人实现了一套。本文会持续应用 requestIdleCallback 来进行任务调度,咱们进行任务调度的思维是将工作拆分成多个小工作,requestIdleCallback外面一直的把小工作拿进去执行,当所有工作都执行完或者超时了就完结本次执行,同时要注册下次执行,代码架子就是这样:

function workLoop(deadline) {while(nextUnitOfWork && deadline.timeRemaining() > 1) {
    // 这个 while 循环会在工作执行完或者工夫到了的时候完结
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }

  // 如果工作还没完,然而工夫到了,咱们须要持续注册 requestIdleCallback
  requestIdleCallback(workLoop);
}

// performUnitOfWork 用来执行工作,参数是咱们的以后 fiber 工作,返回值是下一个工作
function performUnitOfWork(fiber) { }
requestIdleCallback(workLoop);

上述 workLoop 对应 React 源码看这里。

Fiber 可中断数据结构

下面咱们的 performUnitOfWork 并没有实现,然而从下面的构造能够看进去,他接管的参数是一个小工作,同时通过这个小工作还能够找到他的下一个小工作,Fiber 构建的就是这样一个数据结构。Fiber 之前的数据结构是一棵树,父节点的 children 指向了子节点,然而只有这一个指针是不能实现中断持续的。比方我当初有一个父节点 A,A 有三个子节点 B,C,D,当我遍历到 C 的时候中断了,从新开始的时候,其实我是不晓得 C 上面该执行哪个的,因为只晓得 C,并没有指针指向他的父节点,也没有指针指向他的兄弟。Fiber 就是革新了这样一个构造,加上了指向父节点和兄弟节点的指针:

下面的图片还是来自于官网的演讲,能够看到和之前父节点指向所有子节点不同,这里有三个指针:

  1. child: 父节点指向 第一个子元素 的指针。
  2. sibling:从第一个子元素往后,指向下一个兄弟元素。
  3. return:所有子元素都有的指向父元素的指针。

有了这几个指针后,咱们能够在任意一个元素中断遍历并复原,比方在上图 List 处中断了,复原的时候能够通过 child 找到他的子元素,也能够通过 return 找到他的父元素,如果他还有兄弟节点也能够用 sibling 找到。Fiber 这个构造形状看着还是棵树,然而没有了指向所有子元素的指针,父节点只指向第一个子节点,而后子节点有指向其余子节点的指针,这其实是个链表。

实现 Fiber

当初咱们能够本人来实现一下 Fiber 了,咱们须要将之前的 vDom 构造转换为 Fiber 的数据结构,同时须要可能通过其中任意一个节点返回下一个节点,其实就是遍历这个链表。遍历的时候从根节点登程,先找子元素,如果子元素存在,间接返回,如果没有子元素了就找兄弟元素,找完所有的兄弟元素后再返回父元素,而后再找这个父元素的兄弟元素。整个遍历过程其实是个深度优先遍历,从上到下,而后最初一行开始从左到右遍历。比方下图从 div1 开始遍历的话,遍历的程序就应该是 div1 -> div2 -> h1 -> a -> div2 -> p -> div1。能够看到这个序列中,当咱们return 父节点时,这些父节点会被第二次遍历,所以咱们写代码时,return的父节点不会作为下一个工作返回,只有 siblingchild才会作为下一个工作返回。

// performUnitOfWork 用来执行工作,参数是咱们的以后 fiber 工作,返回值是下一个工作
function performUnitOfWork(fiber) {
  // 根节点的 dom 就是 container,如果没有这个属性,阐明以后 fiber 不是根节点
  if(!fiber.dom) {fiber.dom = createDom(fiber);   // 创立一个 DOM 挂载下来
  } 

  // 如果有父节点,将以后节点挂载到父节点上
  if(fiber.return) {fiber.return.dom.appendChild(fiber.dom);
  }

  // 将咱们后面的 vDom 构造转换为 fiber 构造
  const elements = fiber.children;
  let prevSibling = null;
  if(elements && elements.length) {for(let i = 0; i < elements.length; i++) {const element = elements[i];
      const newFiber = {
        type: element.type,
        props: element.props,
        return: fiber,
        dom: null
      }

      // 父级的 child 指向第一个子元素
      if(i === 0) {fiber.child = newFiber;} else {
        // 每个子元素领有指向下一个子元素的指针
        prevSibling.sibling = newFiber;
      }

      prevSibling = newFiber;
    }
  }

  // 这个函数的返回值是下一个工作,这其实是一个深度优先遍历
  // 先找子元素,没有子元素了就找兄弟元素
  // 兄弟元素也没有了就返回父元素
  // 而后再找这个父元素的兄弟元素
  // 最初到根节点完结
  // 这个遍历的程序其实就是从上到下,从左到右
  if(fiber.child) {return fiber.child;}

  let nextFiber = fiber;
  while(nextFiber) {if(nextFiber.sibling) {return nextFiber.sibling;}

    nextFiber = nextFiber.return;
  }
}

React 源码中的 performUnitOfWork 看这里,当然比咱们这个简单很多。

对立 commit DOM 操作

下面咱们的 performUnitOfWork 一边构建 Fiber 构造一边操作 DOMappendChild,这样如果某次更新好几个节点,操作了第一个节点之后就中断了,那咱们可能只看到第一个节点渲染到了页面,后续几个节点等浏览器空了才陆续渲染。为了防止这种状况,咱们应该将 DOM 操作都收集起来,最初对立执行,这就是 commit。为了可能记录地位,咱们还须要一个全局变量workInProgressRoot 来记录根节点,而后在 workLoop 检测如果工作执行完了,就commit:

function workLoop(deadline) {while(nextUnitOfWork && deadline.timeRemaining() > 1) {
    // 这个 while 循环会在工作执行完或者工夫到了的时候完结
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }

  // 工作做完后对立渲染
  if(!nextUnitOfWork && workInProgressRoot) {commitRoot();
  }

  // 如果工作还没完,然而工夫到了,咱们须要持续注册 requestIdleCallback
  requestIdleCallback(workLoop);
}

因为咱们是在 Fiber 树齐全构建后再执行的 commit,而且有一个变量workInProgressRoot 指向了 Fiber 的根节点,所以咱们能够间接把 workInProgressRoot 拿过去递归渲染就行了:

// 对立操作 DOM
function commitRoot() {commitRootImpl(workInProgressRoot.child);    // 开启递归
  workInProgressRoot = null;     // 操作完后将 workInProgressRoot 重置
}

function commitRootImpl(fiber) {if(!fiber) {return;}

  const parentDom = fiber.return.dom;
  parentDom.appendChild(fiber.dom);

  // 递归操作子元素和兄弟元素
  commitRootImpl(fiber.child);
  commitRootImpl(fiber.sibling);
}

reconcile 和谐

reconcile 其实就是虚构 DOM 树的 diff 操作,须要删除不须要的节点,更新批改过的节点,增加新的节点。为了在中断后能回到工作地位,咱们还须要一个变量 currentRoot,而后在fiber 节点外面增加一个属性 alternate,这个属性指向上一次运行的根节点,也就是currentRootcurrentRoot 会在第一次 render 后的 commit 阶段赋值,也就是每次计算完后都会把当次状态记录在 alternate 上,前面更新了就能够把 alternate 拿进去跟新的状态做 diff。而后 performUnitOfWork 外面须要增加和谐子元素的代码,能够新增一个函数reconcileChildren。这个函数外面不能简略的创立新节点了,而是要将老节点跟新节点拿来比照,比照逻辑如下:

  1. 如果新老节点类型一样,复用老节点 DOM,更新 props
  2. 如果类型不一样,而且新的节点存在,创立新节点替换老节点
  3. 如果类型不一样,没有新节点,有老节点,删除老节点

留神删除老节点的操作是间接将 oldFiber 加上一个删除标记就行,同时用一个全局变量 deletions 记录所有须要删除的节点:

      // 比照 oldFiber 和以后 element
      const sameType = oldFiber && element && oldFiber.type === element.type;  // 检测类型是不是一样
      // 先比拟元素类型
      if(sameType) {
        // 如果类型一样,复用节点,更新 props
        newFiber = {
          type: oldFiber.type,
          props: element.props,
          dom: oldFiber.dom,
          return: workInProgressFiber,
          alternate: oldFiber,          // 记录下上次状态
          effectTag: 'UPDATE'           // 增加一个操作标记
        }
      } else if(!sameType && element) {
        // 如果类型不一样,有新的节点,创立新节点替换老节点
        newFiber = {
          type: element.type,
          props: element.props,
          dom: null,                    // 构建 fiber 时没有 dom,下次 perform 这个节点是才创立 dom
          return: workInProgressFiber,
          alternate: null,              // 新增的没有老状态
          effectTag: 'REPLACEMENT'      // 增加一个操作标记
        }
      } else if(!sameType && oldFiber) {
        // 如果类型不一样,没有新节点,有老节点,删除老节点
        oldFiber.effectTag = 'DELETION';   // 增加删除标记
        deletions.push(oldFiber);          // 一个数组收集所有须要删除的节点
      }

而后就是在 commit 阶段解决真正的 DOM 操作,具体的操作是依据咱们的 effectTag 来判断的:

function commitRootImpl(fiber) {if(!fiber) {return;}

  const parentDom = fiber.return.dom;
  if(fiber.effectTag === 'REPLACEMENT' && fiber.dom) {parentDom.appendChild(fiber.dom);
  } else if(fiber.effectTag === 'DELETION') {parentDom.removeChild(fiber.dom);
  } else if(fiber.effectTag === 'UPDATE' && fiber.dom) {
    // 更新 DOM 属性
    updateDom(fiber.dom, fiber.alternate.props, fiber.props);
  }

  // 递归操作子元素和兄弟元素
  commitRootImpl(fiber.child);
  commitRootImpl(fiber.sibling);
}

替换和删除的 DOM 操作都比较简单,更新属性的会略微麻烦点,须要再写一个辅助函数 updateDom 来实现:

// 更新 DOM 的操作
function updateDom(dom, prevProps, nextProps) {
  // 1. 过滤 children 属性
  // 2. 老的存在,新的没了,勾销
  // 3. 新的存在,老的没有,新增
  Object.keys(prevProps)
    .filter(name => name !== 'children')
    .filter(name => !(name in nextProps))
    .forEach(name => {if(name.indexOf('on') === 0) {dom.removeEventListener(name.substr(2).toLowerCase(), prevProps[name], false);
      } else {dom[name] = '';
      }
    });

  Object.keys(nextProps)
    .filter(name => name !== 'children')
    .forEach(name => {if(name.indexOf('on') === 0) {dom.addEventListener(name.substr(2).toLowerCase(), nextProps[name], false);
      } else {dom[name] = nextProps[name];
      }
    });
}

updateDom的代码写的比较简单,事件只解决了简略的 on 结尾的,兼容性也有问题,prevPropsnextProps 可能会遍历到雷同的属性,有反复赋值,然而总体原理还是没错的。要想把这个解决写全,代码量还是不少的。

函数组件

函数组件是 React 外面很常见的一种组件,咱们后面的 React 架构其实曾经写好了,咱们这里来反对下函数组件。咱们之前的 fiber 节点上的 type 都是 DOM 节点的类型,比方 h1 什么的,然而函数组件的节点 type 其实就是一个函数了,咱们须要对这种节点进行独自解决。

首先须要在更新的时候检测以后节点是不是函数组件,如果是,children的解决逻辑会略微不一样:

// performUnitOfWork 外面
// 检测函数组件
function performUnitOfWork(fiber) {
  const isFunctionComponent = fiber.type instanceof Function;
  if(isFunctionComponent) {updateFunctionComponent(fiber);
  } else {updateHostComponent(fiber);
  }
  
  // ... 上面省略 n 行代码...
}

function updateFunctionComponent(fiber) {
  // 函数组件的 type 就是个函数,间接拿来执行能够取得 DOM 元素
  const children = [fiber.type(fiber.props)];

  reconcileChildren(fiber, children);
}

// updateHostComponent 就是之前的操作,只是独自抽取了一个办法
function updateHostComponent(fiber) {if(!fiber.dom) {fiber.dom = createDom(fiber);   // 创立一个 DOM 挂载下来
  } 

  // 将咱们后面的 vDom 构造转换为 fiber 构造
  const elements = fiber.props.children;

  // 和谐子元素
  reconcileChildren(fiber, elements);
}

而后在咱们提交 DOM 操作的时候因为函数组件没有 DOM 元素,所以须要留神两点:

  1. 获取父级 DOM 元素的时候须要递归网上找真正的 DOM
  2. 删除节点的时候须要递归往下找真正的节点

咱们来批改下commitRootImpl:

function commitRootImpl() {
  // const parentDom = fiber.return.dom;
  // 向上查找真正的 DOM
  let parentFiber = fiber.return;
  while(!parentFiber.dom) {parentFiber = parentFiber.return;}
  const parentDom = parentFiber.dom;
  
  // ... 这里省略 n 行代码...
  
  if{fiber.effectTag === 'DELETION'} {commitDeletion(fiber, parentDom);
  }
}

function commitDeletion(fiber, domParent) {if(fiber.dom) {
    // dom 存在,是一般节点
    domParent.removeChild(fiber.dom);
  } else {
    // dom 不存在,是函数组件, 向下递归查找实在 DOM
    commitDeletion(fiber.child, domParent);
  }
}

当初咱们能够传入函数组件了:

import React from './myReact';
const ReactDOM = React;

function App(props) {
  return (
    <div>
      <h1 id="title">{props.title}</h1>
      <a href="xxx">Jump</a>
      <section>
        <p>
          Article
        </p>
      </section>
    </div>
  );
}

ReactDOM.render(
  <App title="Fiber Demo"/>,
  document.getElementById('root')
);

实现 useState

useState是 React Hooks 外面的一个 API,相当于之前 Class Component 外面的 state,用来治理组件外部状态,当初咱们曾经有一个简化版的React 了,咱们也能够尝试下来实现这个 API。

简略版

咱们还是从用法动手来实现最简略的性能,咱们个别应用 useState 是这样的:

function App(props) {const [count, setCount] = React.useState(1);
  const onClickHandler = () => {setCount(count + 1);
  }
  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={onClickHandler}>Count+1</button>
    </div>
  );
}

ReactDOM.render(
  <App title="Fiber Demo"/>,
  document.getElementById('root')
);

上述代码能够看出,咱们的 useState 接管一个初始值,返回一个数组,外面有这个 state 的以后值和扭转 state 的办法,须要留神的是 App 作为一个函数组件,每次 render 的时候都会运行,也就是说外面的局部变量每次 render 的时候都会重置,那咱们的 state 就不能作为一个局部变量,而是应该作为一个全副变量存储:

let state = null;
function useState(init) {

  state = state === null ? init : state;

  // 批改 state 的办法
  const setState = value => {
    state = value;

    // 只有批改了 state,咱们就须要重新处理节点
    workInProgressRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot
    }

    // 批改 nextUnitOfWork 指向 workInProgressRoot,这样下次就会解决这个节点了
    nextUnitOfWork = workInProgressRoot;
    deletions = [];}

  return [state, setState]
}

这样其实咱们就能够应用了:

反对多个 state

下面的代码只有一个 state 变量,如果咱们有多个 useState 怎么办呢?为了能反对多个 useState,咱们的state 就不能是一个简略的值了,咱们能够思考把他改成一个数组,多个 useState 依照调用程序放进这个数组外面,拜访的时候通过下标来拜访:

let state = [];
let hookIndex = 0;
function useState(init) {
  const currentIndex = hookIndex;
  state[currentIndex] = state[currentIndex] === undefined ? init : state[currentIndex];

  // 批改 state 的办法
  const setState = value => {state[currentIndex] = value;

    // 只有批改了 state,咱们就须要重新处理这个节点
    workInProgressRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot
    }

    // 批改 nextUnitOfWork 指向 workInProgressRoot,这样下次就会解决这个节点了
    nextUnitOfWork = workInProgressRoot;
    deletions = [];}

  hookIndex++;

  return [state[currentIndex], setState]
}

来看看多个 useState 的成果:

反对多个组件

下面的代码尽管咱们反对了多个 useState,然而依然只有一套全局变量,如果有多个函数组件,每个组件都来操作这个全局变量,那相互之间不就是净化了数据了吗?所以咱们数据还不能都存在全局变量下面,而是应该存在每个fiber 节点上,解决这个节点的时候再将状态放到全局变量用来通信:

// 申明两个全局变量,用来解决 useState
// wipFiber 是以后的函数组件 fiber 节点
// hookIndex 是以后函数组件外部 useState 状态计数
let wipFiber = null;
let hookIndex = null;

因为 useState 只在函数组件外面能够用,所以咱们之前的 updateFunctionComponent 外面须要初始化解决 useState 变量:

function updateFunctionComponent(fiber) {
  // 反对 useState,初始化变量
  wipFiber = fiber;
  hookIndex = 0;
  wipFiber.hooks = [];        // hooks 用来存储具体的 state 序列
  
  // ...... 上面代码省略......
}

因为 hooks 队列放到 fiber 节点下来了,所以咱们在 useState 取之前的值时须要从 fiber.alternate 上取,残缺代码如下:

function useState(init) {
  // 取出上次的 Hook
  const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex];

  // hook 数据结构
  const hook = {state: oldHook ? oldHook.state : init      // state 是每个具体的值}

  // 将所有 useState 调用依照程序存到 fiber 节点上
  wipFiber.hooks.push(hook);
  hookIndex++;

  // 批改 state 的办法
  const setState = value => {
    hook.state = value;

    // 只有批改了 state,咱们就须要重新处理这个节点
    workInProgressRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot
    }

    // 批改 nextUnitOfWork 指向 workInProgressRoot,这样下次 requestIdleCallback 就会解决这个节点了
    nextUnitOfWork = workInProgressRoot;
    deletions = [];}

  return [hook.state, setState]
}

下面代码能够看出咱们在将 useState 和存储的 state 进行匹配的时候是用的 useState 的调用程序匹配 state 的下标,如果这个下标匹配不上了,state就错了,所以 React 外面不能呈现这样的代码:

if (something) {const [state, setState] = useState(1);
}

上述代码不能保障每次 something 都满足,可能导致 useState 这次 render 执行了,下次又没执行,这样新老节点的下标就匹配不上了,对于这种代码,React会间接报错:

用 Hooks 模仿 Class 组件

这个性能纯正是娱乐性性能,通过后面实现的 Hooks 来模仿实现 Class 组件,这个并不是 React 官网的实现形式哈~ 咱们能够写一个办法将 Class 组件转化为后面的函数组件:

function transfer(Component) {return function(props) {const component = new Component(props);
    let [state, setState] = useState(component.state);
    component.props = props;
    component.state = state;
    component.setState = setState;

    return component.render();}
}

而后就能够写 Class 了,这个 Class 长得很像咱们在 React 外面写的 Class,有 state,setStaterender

import React from './myReact';

class Count4 {constructor(props) {
    this.props = props;
    this.state = {count: 1}
  }

  onClickHandler = () => {
    this.setState({count: this.state.count + 1})
  }

  render() {
    return (
      <div>
        <h3>Class component Count: {this.state.count}</h3>
        <button onClick={this.onClickHandler}>Count+1</button>
      </div>
    ); 
  }
}

// export 的时候用 transfer 包装下
export default React.transfer(Count4);

而后应用的时候间接:

<div>
  <Count4></Count4>
</div>

当然你也能够在 React 外面建一个空的 class Component,让Count4 继承他,这样就更像了。

好了,到这里咱们代码就写完了,残缺代码能够看我 GitHub。

总结

  1. 咱们写的 JSX 代码被 babel 转化成了React.createElement
  2. React.createElement返回的其实就是虚构 DOM 构造。
  3. ReactDOM.render办法是将虚构 DOM 渲染到页面的。
  4. 虚构 DOM 的和谐和渲染能够简略粗犷的递归,然而这个过程是同步的,如果须要解决的节点过多,可能会阻塞用户输出和动画播放,造成卡顿。
  5. Fiber 是 16.x 引入的新个性,用途是将同步的和谐变成异步的。
  6. Fiber 革新了虚构 DOM 的构造,具备 父 -> 第一个子 子 -> 兄 子 -> 父 这几个指针,有了这几个指针,能够从任意一个 Fiber 节点找到其余节点。
  7. Fiber 将整棵树的同步工作拆分成了每个节点能够独自执行的异步执行构造。
  8. Fiber 能够从任意一个节点开始遍历,遍历是深度优先遍历,程序是 父 -> 子 -> 兄 -> 父,也就是从上往下,从左往右。
  9. Fiber 的和谐阶段能够是异步的小工作,然而提交阶段 (commit) 必须是同步的。因为异步的 commit 可能让用户看到节点一个一个接连呈现,体验不好。
  10. 函数组件其实就是这个节点的 type 是个函数,间接将 type 拿来运行就能够失去虚构 DOM。
  11. useState是在 Fiber 节点上增加了一个数组,数组外面的每个值对应了一个 useStateuseState 调用程序必须和这个数组下标匹配,不然会报错。

参考资料

A Cartoon Intro to Fiber

妙味课堂大圣老师:手写 react 的 fiber 和 hooks 架构

React Fiber

这可能是最艰深的 React Fiber(工夫分片) 打开方式

浅析 React Fiber

React Fiber 架构

文章的最初,感激你破费贵重的工夫浏览本文,如果本文给了你一点点帮忙或者启发,请不要悭吝你的赞和 GitHub 小星星,你的反对是作者继续创作的能源。

作者博文 GitHub 我的项目地址:https://github.com/dennis-jiang/Front-End-Knowledges

正文完
 0