美文网首页
react fiber源码实现

react fiber源码实现

作者: 一土二月鸟 | 来源:发表于2020-09-15 07:32 被阅读0次

核心思路及思想

  1. 利用requestIdleCallback实现每帧空余时间处理单个fiber,提升render阶段的性能。
  2. render阶段以fiber为处理单元,收集effect(生成真实dom)形成单链表,如果有大儿子先完成大儿子,然后是大儿子的弟弟,然后是大儿子的父亲。然后以父亲重新作为起点继续遍历。直到回到根节点,根节点完成后,将持有完成的effect list(单链表)。
  3. 最终将单链表进行commit,将effect逐层挂载到父级的真实dom上。
  4. 补充:
    4.1 fiber之所以比react15版本性能有很大提升,得益于fiber的数据结构包含了大儿子、大儿子弟弟、大儿子的父亲。
    4.2 这样当帧时间不够,暂停运行时,全局的nextWorkFiber保存了上下级关系,可以随时找到下一个需要处理的fiber。
    4.3 根fiber的props.children中挂载了整个dom树,每次render调和生成fiber时,会在fiber的.props.children中挂载子dom树。
    4.4 因此fiber可以暂停,并持续完成手机effect list的工作

以下代码可直接全部拷贝正常执行

  • constants.js
// 表示一个文本元素
export const ELEMENT_TEXT = Symbol("ELEMENT_TEXT");
// 根节点
export const TAG_ROOT = Symbol("TAG_ROOT");
// 原生节点  div span class 函数
export const TAG_HOST = Symbol("TAG_HOST");
// 文本节点
export const TAG_TEXT = Symbol("TAG_TEXT");

// 插入节点
export const PLACEMENT = Symbol("PLACEMENT");
// 更新节点
export const UPDATE = Symbol("UPDATE");
// 删除节点
export const DELETETION = Symbol("DELETETION");
  • index.js
import React from './react';
import ReactDOM from './react-dom';

const style = {
  border: '1px solid red',
  margin: '20px',
}

let el = (
  <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(el, document.getElementById('root'));
  • react.js
import { ELEMENT_TEXT } from './constants';

function createElement(type, config, ...children) {

    delete config.__self; // 便于理解不删除也行
    delete config.__source; // 便于理解不删除也行

    return {
      type,
      props: {
        ...config,
        children: children.map(child => {
          return typeof child === 'object' ? child : {
            type: ELEMENT_TEXT,
            props: { text: child, children: [] }
          }
        })
      }
    }

}

const React = {
  createElement
}

export default React;
  • react-dom.js
import { TAG_ROOT } from './constants';
import { scheduleRootFiber } from './schedule';

/**
 * render 是把一个元素渲染到一个容器内部
 */
function render (element, container) {  // container = root Dom节点
  let rootFiber = {
    tag: TAG_ROOT, // 每个fiber都有一个tag属性标识此元素的类型
    stateNode: container, // 一般情况下,此元素如果是host节点,stateNode将指向真实dom元素
    // props.children是一个数组,里面存放的是要被渲染的react元素(虚拟dom),后面会把每个react元素创建对应的fiber
    props: { // 虚拟dom和fiber节点的属性区别:虚拟dom有type和props,fiber有tag、stateNode、props、type、return、child、sibling...等更丰富的属性
      children: [ element ] // element为要被渲染的虚拟dom
    }
  }
  scheduleRootFiber(rootFiber);  
}

const ReactDOM = {
  render
}

export default ReactDOM;
  • utils.js
export function setProps (dom, oldProps, newProps) {
  for(let key in oldProps) {

  }
  for(let key in newProps) {
    if(key !== 'children') {
      setProp(dom, key, newProps[key])
    }
  }
}

function setProp(dom, key, value) {
  if(/^on/.test(key)) { // onClick onChange
    dom[key.toLowerCase()] = value; 
  } else if( key === 'style' ) {
    for(let key in value) {
      dom.style[key] = value[key];
    }
  } else if( key === 'className' ) {
    dom.class = value;
  } else {
    dom.setAttribute(key, value);
  }
}
  • schedule.js
/**
 * react工作的两个重要阶段:diff阶段、commit阶段
 * diff阶段(render阶段):
 * 1. 对比新旧的虚拟dom,进行增量更新或创建,这个
 * 阶段可能比较花时间,所以我们对任务要进行拆分,拆分的维度为虚拟dom节点,此阶段可以暂停(此帧时间不够了,优先执行下一帧)。
 * 2. diff(render)阶段的目的是收集哪些节点发生了变化,最终生成一个effectlist(单链表)
 * 3. diff阶段有两个任务:1.根据虚拟dom生成fiber树 2.收集effectList
 * 
 * commit阶段:进行dom更新创建阶段,此阶段不能暂停,要一气呵成(这样做页面不会出现卡顿)
 */

/**
 * render阶段代码处理逻辑
 * 1. 将virtual dom tree mount 到rootFiber上
 * 2. 将rootFiber上的virtual dom进行循环遍历 创建fiber
 */

import { setProps } from "./utils";
import { ELEMENT_TEXT, TAG_TEXT, TAG_HOST, PLACEMENT, TAG_ROOT } from "./constants";

let nextUnitOfWork = null; // 下一个执行单元
let workInProgressRoot = null; // RootFiber应用的根,用于单链表的火车头?
export function scheduleRootFiber(rootFiber) { // { tag: TAG_ROOT, stateNode: container, props: { children: [ element ] } }
  workInProgressRoot = rootFiber;
  nextUnitOfWork = rootFiber;
}

// react告诉浏览器,在每帧的空余时间,执行workLoop
requestIdleCallback(workLoop, { timeout: 500 });

// 循环执行工作 nextUnitOfWork
function workLoop(deadline) {

  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextUnitOfWork) { // 有任务,并且时间充裕或已超时,则继续执行
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }

  if (!nextUnitOfWork) {
    let firstEffect = workInProgressRoot.firstEffect;
    commit(firstEffect);
    console.log('render阶段结束');
  }

  requestIdleCallback(workLoop, { timeout: 500 }); // 如果时间片到期后,无论是否还有nextUnitOfWork未处理完,都执行一遍requestIdleCallback
}

function commit(currentEffect) {
  while (currentEffect) {
    doCommit(currentEffect)
    currentEffect = currentEffect.nextEffect;
  }
}

function doCommit(currentEffect) {
  if (!currentEffect) return;
  let returnFiber = currentEffect.return;
  let returnDom = returnFiber.stateNode;
  if(currentEffect.effectTag === PLACEMENT) {
    returnDom.appendChild(currentEffect.stateNode);
  }
  currentEffect.effectTag = null;
}

/**
 * 处理一个执行单元
 * 遍历子节点同时收集effect
 * 首先通过beginWork处理根节点,然后处理>>大儿子(大儿子的弟弟完成)>>大儿子的弟弟(大儿子的弟弟完成、大儿子的父亲完成)>>大儿子的叔叔(大儿子的叔叔完成)>>...>>(根节点完成)
 * 根据当前fiber的类型进行对应的处理
 * 处理之后返回其大儿子或者弟弟或者叔叔
 * @param {Object} currentFiber 
 */
function performUnitOfWork(currentFiber) {
  beginWork(currentFiber);
  if (currentFiber.child) {
    return currentFiber.child;
  }

  while (currentFiber) { // 此处while循环、currentFiber的的作用是让最后一个儿子可以一直溯源到自己的父亲及祖先,同时让父亲可以找到自己的弟弟
    completeUnitOfWork(currentFiber); // 所有的儿子完成,自己则完成,最终rootFiber持有整个effect list
    if (currentFiber.sibling) {
      return currentFiber.sibling; // 有弟弟则返回弟弟
    }
    currentFiber = currentFiber.return; // 没有弟弟则找自己的叔叔,如果没有父亲则本次workLoop结束。如果有父亲,则找叔叔,没有叔叔接着向上找父亲,直到没有父亲没为止。
  }
}

/**
 * 处理不同类型的fiber
 * 0. 根据虚拟dom树创建fiber树
 * 1. 创建真实dom stateNode
 * @param {Object} currentFiber 不同类型的fiber
 */
function beginWork(currentFiber) {
  if (currentFiber.tag === TAG_ROOT) { // 处理根fiber,将根节点的每个第一层子节点创建fiber
    updateHostRoot(currentFiber);
  } else if (currentFiber.tag === TAG_TEXT) { // 处理文本fiber,如果是文本节点,创建真实dom
    updateHostText(currentFiber);
  } else if (currentFiber.tag === TAG_HOST) { // 处理host fiber,创建真实dom
    updateHost(currentFiber);
  }
}

/**
 * 收集子fiber的effect,由第一个完成的fiber形成单链表的头部,并将firstEffect和lastEffect都交给父亲,当父亲完成的时候,将first和last交给爷爷。最终祖先将持有单链表的头部first
 * 当前节点完成,收集有副作用的fiber,然后组成effect list
 * 每个fiber都有自己的firstEffect和lastEeffect, firstEffect指向第一个有副作用的子fiber,lastEffect指向最后一个有副作用的子fiber
 * 中间的用nextEffect做一个单链表,firstEffect为大儿子,firstEffect.nextEffect指向二儿子、firstEffect.nextEffect.nextEffect为三儿子。
 * @param {Object} currentFiber 
 */
function completeUnitOfWork(currentFiber) {
  const returnFiber = currentFiber.return;
  if (returnFiber) {
    const effectTag = currentFiber.effectTag;
    if (effectTag) { // 自己有副作用 
      if (!returnFiber.firstEffect) { // 2 当第一个节点的父亲完成时,爷爷如果没有firstEffect,则把爸爸的firstEffect给到爷爷的firstEffect
        returnFiber.firstEffect = currentFiber.firstEffect;
      }
      if (currentFiber.lastEffect) { // 此行判断应该可以省略??
        if (returnFiber.lastEffect) { // 4 把叔叔的第一个effect交给爷爷的lastEffect的nextEffect
          returnFiber.lastEffect.nextEffect = currentFiber.firstEffect;
        }
        returnFiber.lastEffect = currentFiber.lastEffect; // 3 把父亲的lastEffect交给爷爷
      }

      if (returnFiber.lastEffect) { // 1. 处理第一个优先完成的节点
        returnFiber.lastEffect.nextEffect = currentFiber; // 此处的currentFiber为前面儿子的弟弟
      } else {
        returnFiber.firstEffect = currentFiber;
      }
      returnFiber.lastEffect = currentFiber;
    }
  }
}

/**
 * 更新根fiber,将根fiber下的第一层virtual dom转成fiber
 * @param {Obeject} currentFiber 根节点
 */
function updateHostRoot(currentFiber) { // 将fiber下的props的children取出来,取出来的是虚拟dom
  let newChildren = currentFiber.props.children; // [element]
  reconcileChildren(currentFiber, newChildren);
}

/**
 * 更新文本fiber的statenode
 * 创建真实dom
 * @param {Object} currentFiber 文本节点
 */
function updateHostText(currentFiber) {
  if (!currentFiber.stateNode) {
    currentFiber.stateNode = createDOM(currentFiber);
  }
}

/**
 * 更新host fiber的stateNode
 * 1. 创建真实dom
 * 2. 将属性添加到真实dom上
 * @param {Object} currentFiber 
 */
function updateHost(currentFiber) {
  if (!currentFiber.stateNode) {
    currentFiber.stateNode = createDOM(currentFiber);
  }
  const newChildren = currentFiber.props.children;
  reconcileChildren(currentFiber, newChildren);
}

/**
 * 根据当前fiber创建第一层虚拟dom的fiber
 * @param {Object} currentFiber 
 * @param {Array} newChildren 存放着虚拟dom
 */
function reconcileChildren(currentFiber, newChildren) { // 将虚拟dom转成fiber, newChildren为virtual dom,转换为fiber后,将生成的fiber全部挂载到当前fiber上
  let prevSibling; // 上一个新的fiber

  newChildren.forEach((newChild, index) => {
    let tag;
    if (newChild.type === ELEMENT_TEXT) {
      tag = TAG_TEXT;
    } else if (typeof newChild.type === 'string') {
      tag = TAG_HOST;
    }
    let newFiber = {
      tag, // fiber 节点的类型, 根节点、属性节点、文本节点
      type: newChild.type, // fiber对应的虚拟dom的标签类型 div span等
      props: newChild.props, // 将虚拟dom的props传给fiber
      stateNode: null, // 此时的host元素还没有对应的dom元素
      return: currentFiber, // 父fiber
      effectTag: PLACEMENT, // 重要: 副作用标识 render阶段我们会收集副作用 增加 删除 更新
      nextEffect: null, // 相当于单链表中的next
    }
    if (index === 0) { // 将第一个child挂载在父亲上
      currentFiber.child = newFiber;
    } else { // 如果是非首个child,将其挂载到哥哥的sibling上
      prevSibling.sibling = newFiber;
    }
    prevSibling = newFiber; // 将当前的fiber缓存下来,供后面如果有弟弟时,让弟弟可以找到哥哥
  });
}

/**
 * 用于将文本fiber 或host fiber创建为真实dom
 * @param {Object} currentFiber 
 */
function createDOM(currentFiber) {
  switch (currentFiber.tag) {
    case TAG_TEXT:
      return document.createTextNode(currentFiber.props.text);
    case TAG_HOST: {
      const stateNode = document.createElement(currentFiber.type);
      updateDOM(stateNode, {}, currentFiber.props);
      return stateNode;
    }
  }
}

/**
 * 更新dom
 * @param {HTMLDivElement} stateNode 
 * @param {*} oldProps 
 * @param {*} newProps 
 */
function updateDOM(stateNode, oldProps, newProps) {
  setProps(stateNode, oldProps, newProps);
}

相关文章

网友评论

      本文标题:react fiber源码实现

      本文链接:https://www.haomeiwen.com/subject/xmjxyktx.html