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

相熟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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理