创建项目,修改内容

yarn create react-app react-dom-diff

使用react官方脚手架create-react-app搭建处理的项目如下:

react-dom-diff
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
│   ├── favicon.ico
│   ├── index.html
│   └── manifest.json
└── src
    ├── App.css
    ├── App.js
    ├── App.test.js
    ├── index.css
    ├── index.js
    ├── logo.svg
    └── serviceWorker.js

将不需要的文件删除,src 保留index.js,并修改如下:

import React from 'react';
import ReactDOM from 'react-dom';

let element = <div key="title" id="title">title</div>
ReactDOM.render(element, document.getElementById('root'));

很显然,页面上会渲染出来一个div,内容是title,react是怎么实现这整个流程渲染的呢?

createElement方法的实现

我们先来打印下element是什么内容,看下长什么样子?

为什么会长这样?首先是使用了jsx语法,

let element = <div key="title" id="title">title</div>

这个有趣的标签语法既不是字符串也不是 HTML。

它被称为 JSX,是一个 JavaScript 的语法扩展。我们建议在 React 中配合使用 JSX,JSX 可以很好地描述 UI 应该呈现出它应有交互的本质形式。JSX 可能会使人联想到模版语言,但它具有 JavaScript 的全部功能。

react中jsx介绍了为什么要使用jsx以及jsx的优点,还有如何使用,这里就不在介绍了,来解释一下为啥长这个样子,Babel 会把 JSX 转译成一个名为 React.createElement() 函数调用。

以下两种示例代码完全等效:

const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);
const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);

使用babel在线工具能看到babel解析的结果:

那就说明react中有createElement这个方法,来实现下:

react.js

mport { REACT_ELEMENT_TYPE } from './ReactSymbols'
const RESERVED_PROPS = {
    key: true,
    ref: true,
    __self: true,
    __source: true
}
/**
 * 创建虚拟DOM
 * @param {*} type 元素的类型
 * @param {*} config 配置对象
 * @param {*} children 第一个儿子,如果有多个儿子的话会依次放在后面
 */
function createElement(type, config, children) {
    let propName;
    const props = {};
    let key = null;
    let ref = null;
    if (config) {
        if (config.key) {
            key = config.key;
        }
        if (config.ref) {
            ref = config.ref;
        }
        for (propName in config) {
            if (!RESERVED_PROPS.hasOwnProperty(propName)) {
                props[propName] = config[propName];
            }
        }
    }
    const childrenLength = arguments.length - 2;
    if (childrenLength === 1) {
        props.children = children;
    } else if (childrenLength > 1) {
        const childArray = new Array(childrenLength);
        for (let i = 0; i < childrenLength; i++) {
            childArray[i] = arguments[i + 2];
        }
        props.children = childArray;
    }
    return {
        $$typeof: REACT_ELEMENT_TYPE,//类型是一个React元素
        type,
        ref,
        key,
        props
    }
}
const React = {
    createElement
}
export default React;

ReactSymbols.js

export const REACT_ELEMENT_TYPE = Symbol.for('react.element');

配置下启动脚本,package.json中修改如下:

"scripts": {
  "start": "set DISABLE_NEW_JSX_TRANSFORM=true&&react-scripts start",
  "build": "set DISABLE_NEW_JSX_TRANSFORM=true&&react-scripts build",
  "test": "set DISABLE_NEW_JSX_TRANSFORM=true&&react-scripts test",
  "eject": "set DISABLE_NEW_JSX_TRANSFORM=true&&react-scripts eject"
},

修改这个原因是新版本react换了jsx的编译器,目的是为了使用我们自己写的createElement

修改React的引入路径, ReactDOM暂时还使用官方的。

import React from './react';

重新启动编译下,可以看到打印log如下:

这个结果跟使用官方的基本一致,某些属性我们暂时没有做添加。

ReactDOM.render的实现

ReactDOM.render(element, container[, callback])

在提供的 container 里渲染一个 React 元素,并返回对该组件的引用(或者针对无状态组件返回 null)。如果 React 元素之前已经在 container 里渲染过,这将会对其执行更新操作,并仅会在必要时改变 DOM 以映射最新的 React 元素。如果提供了可选的回调函数,该回调将在组件被渲染或更新之后被执行。

react-dom.js

import { createFiberRoot } from './ReactFiberRoot';
import { updateContainer } from './ReactFiberReconciler';
function render(element, container) {
    let fiberRoot = createFiberRoot(container);
    updateContainer(element, fiberRoot);
}
const ReactDOM = {
    render
}
export default ReactDOM;
let fiberRoot = createFiberRoot(container);
updateContainer(element, fiberRoot);

其中,这2行代码是比较核心的,createFiberRoot函数就是根据容器对象( div#root)来生成一个根fiber。

关于fiber架构以及调度我会单独再用文章去做阐述,我们这里只简单了解下fiber。

fiber是什么?

  • 为了让渲染的过程可以不断,我们可以把整个渲染任务分成若干个task(工作单元),每个工作单元就是一个fiber
  • 每个虚拟DOM节点内部表示为一个fiber对象
  • render阶段会根据虚拟DOM以深度优先的方式构建fiber

fiber数据结构

function FiberNode(){
  this.tag = tag;                  // fiber 标签 证明是什么类型fiber。
  this.key = key;                  // key调和子节点时候用到。 
  this.type = null;                // dom元素是对应的元素类型,比如div,组件指向组件对应的类或者函数。  
  this.stateNode = null;           // 指向对应的真实dom元素,类组件指向组件实例,可以被ref获取。
 
  this.return = null;              // 指向父级fiber
  this.child = null;               // 指向子级fiber
  this.sibling = null;             // 指向兄弟fiber 
  this.index = 0;                  // 索引

  this.ref = null;                 // ref指向,ref函数,或者ref对象。

  this.pendingProps = pendingProps;// 在一次更新中,代表element创建
  this.memoizedProps = null;       // 记录上一次更新完毕后的props
  this.updateQueue = null;         // 类组件存放setState更新队列,函数组件存放
  this.memoizedState = null;       // 类组件保存state信息,函数组件保存hooks信息,dom元素为null
  this.dependencies = null;        // context或是时间的依赖项

  this.mode = mode;                //描述fiber树的模式,比如 ConcurrentMode 模式

  this.effectTag = NoEffect;       // effect标签,用于收集effectList
  this.nextEffect = null;          // 指向下一个effect

  this.firstEffect = null;         // 第一个effect
  this.lastEffect = null;          // 最后一个effect

  this.expirationTime = NoWork;    // 通过不同过期时间,判断任务是否过期, 在v17版本用lane表示。

  this.alternate = null;           //双缓存树,指向缓存的fiber。更新阶段,两颗树互相交替。
}

type 就是react的元素类型

export const FunctionComponent = 0;       // 对应函数组件
export const ClassComponent = 1;          // 对应的类组件
export const IndeterminateComponent = 2;  // 初始化的时候不知道是函数组件还是类组件 
export const HostRoot = 3;                // Root Fiber 可以理解为跟元素 , 通过reactDom.render()产生的根元素
export const HostPortal = 4;              // 对应  ReactDOM.createPortal 产生的 Portal 
export const HostComponent = 5;           // dom 元素 比如 <div>
export const HostText = 6;                // 文本节点
export const Fragment = 7;                // 对应 <React.Fragment> 
export const Mode = 8;                    // 对应 <React.StrictMode>   
export const ContextConsumer = 9;         // 对应 <Context.Consumer>
export const ContextProvider = 10;        // 对应 <Context.Provider>
export const ForwardRef = 11;             // 对应 React.ForwardRef
export const Profiler = 12;               // 对应 <Profiler/ >
export const SuspenseComponent = 13;      // 对应 <Suspense>
export const MemoComponent = 14;          // 对应 React.memo 返回的组件

再回来看下createFiberRoot的实现

export function createFiberRoot(containerInfo) {
  const fiberRoot = { containerInfo };//fiberRoot指的就是容器对象containerInfo div#root
  //创建fiber树的根节点 
  const hostRootFiber = createHostRootFiber();
  //当前的fiberRoot的current指向这个根fiber
  //current当前的意思,它指的当前跟我们页面中真实DOM相同的fiber树
  fiberRoot.current = hostRootFiber;
  //让此根fiber的真实DOM节点指向fiberRoot  div#root   stateNode就是指的真实DOM的意思
  hostRootFiber.stateNode = fiberRoot;
  return fiberRoot;
}

createHostRootFiber的实现如下

import { HostComponent, HostRoot } from './ReactWorkTags';
export function createHostRootFiber() {
    return createFiber(HostRoot);
}

HostRoot其实就是对应的3,也就是对应的根元素

/**
 * 创建fiber节点
 * @param {*} tag fiber的标签 HostRoot指的是根节点 div span HostComponent
 * @param {*} pendingProps 等待生效的属性对象
 * @param {*} key 
 */
function createFiber(tag, pendingProps, key) {
    return new FiberNode(tag, pendingProps, key);
}

FiberNode是一个构造函数,createFiber就是根据不同类型,生成不同的fiber节点

function FiberNode(tag, pendingProps, key) {
    this.tag = tag;
    this.pendingProps = pendingProps;
    this.key = key;
}

FiberNode其实有很多属性,暂且只添加这3个,updateContainer(element, fiberRoot)这个暂时没有实现,先注释掉,打印下fiberRoot看下长什么样:

function render(element, container) {
  let fiberRoot = createFiberRoot(container);
+  console.log(fiberRoot);
+  // updateContainer(element, fiberRoot);
}

用下图可以表示出fiberRoot与hostRootFiber的关系:

到此,我们只是描述出了一个节点对应的fiber,渲染或者更新的时候fiber内部是如何知道怎么更新的呢?

export function createFiberRoot(containerInfo) {
  const fiberRoot = { containerInfo };//fiberRoot指的就是容器对象containerInfo div#root
  //创建fiber树的根节点
  const hostRootFiber = createHostRootFiber();
  //当前的fiberRoot的curent指向这个根fiber
  //current当前的意思,它指的当前跟我们页面中真实DOM相同的fiber树
  fiberRoot.current = hostRootFiber;
  //让此根fiber的真实DOM节点指向fiberRoot  div#root   stateNode就是指的真实DOM的意思
  hostRootFiber.stateNode = fiberRoot;
+  initializeUpdateQueue(hostRootFiber);
  return fiberRoot;
}

实际上在==createFiberRoot()==的时候会有一个initializeUpdateQueue(hostRootFiber)的操作,初始化更新队列,所有的fiber都会等待理更新内容放在更新队列中,来看下initializeUpdateQueue的实现:

initializeUpdateQueue()

export function initializeUpdateQueue(fiber) {
  const updateQueue = {
    shared: {
      pending: null
    }
  }
  fiber.updateQueue = updateQueue;
}

export function createUpdate() {
  return {};
}

这个其实就很简单,给fiber添加了一个updateQueue的属性,形成一个环状链表,如下图:

initializeUpdateQueue不仅有createUpdate方法还有enqueueUpdate方法,enqueueUpdate的操作就是向当前的fiber的更新队列中添加一个更新,来实现下:

/**
 * 向当前的fiber的更新队列中添加一个更新
 * @param {*} fiber
 * @param {*} update
 */
export function enqueueUpdate(fiber, update) {
  let updateQueue = fiber.updateQueue;
  const sharedQueue = updateQueue.shared;
  const pending = sharedQueue.pending;
  if (!pending) {
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  sharedQueue.pending = update;
}

怎么理解这个呢,首先在initializeUpdateQueue的时候,fiber添加了一个updateQueue,而updateQueue中有shared,shared中有个pending,这些都很好理解。

if (!pending) {
	update.next = update;
}

pending为null的时候,把这个更新的next指向自己,什么意思呢,假如现在有个update1,它的指向如下图:

sharedQueue.pending = update;

在最后面有这样的一个执行,执行完之后就可以用下图来表示:

else {
	update.next = pending.next;
	pending.next = update;
}

如果还有下次更新那么就会走到else分支,如果还有个update2,通过update.next = pending.next操作之后就相当于update2.next = update1.next,然后把pending的next指向这次更新,如下2图所示:

上图为update.next = pending.next;

上图为pending.next = update;

如果有更多,那就像下图一样,依次往下:

pending永远都指向最新的更新,pending.next永远指向第一个更新

initializeUpdateQueue()测试

initializeUpdateQueue.test.js


function initializeUpdateQueue(fiber) {
  const updateQueue = {
    shared: {
      pending: null
    }
  }
  fiber.updateQueue = updateQueue;
}

function createUpdate() {
  return {};
}


function enqueueUpdate(fiber, update) {
  let updateQueue = fiber.updateQueue;
  const sharedQueue = updateQueue.shared;
  const pending = sharedQueue.pending;
  if (!pending) {
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  sharedQueue.pending = update;
}

let fiber = { baseState: { number: 0 } };
initializeUpdateQueue(fiber);
const update1 = createUpdate();
update1.payload = { number: 1 };//update1 = {payload:{number:1}}
//把update1添加到更新队列链表里
enqueueUpdate(fiber, update1);
const update2 = createUpdate();
update2.payload = { number: 2 };//update2 = {payload:{number:2}}
//把update2添加到更新队列链表里
enqueueUpdate(fiber, update2);
const update3 = createUpdate();
update3.payload = { number: 3 };//update3 = {payload:{number:3}}
//把update1添加到更新队列链表里
enqueueUpdate(fiber, update3);

console.log(fiber.updateQueue.shared.pending);

可以看到结果是pending指向{number:3},它的next指向{number:1},是一个循环链表的结构。

再看看fiberRoot的打印结果,fiberRoot.current中就增加了一个updateQuene,现在还没有更新,所以它的pending就是null。那接下来,我们需要处理什么呢,没错就是在render注释掉的updateContainer()方法。

updateContainer()

updateContainer的作用就是把虚拟DOM element变成真实DOM插入或者说渲染到container容器中

import { createUpdate, enqueueUpdate } from './ReactUpdateQueue';
import { scheduleUpdateOnFiber } from './ReactFiberWorkLoop';

/**
 * 把虚拟DOM element变成真实DOM插入或者说渲染到container容器中
 * @param {*} element
 * @param {*} container
 */
export function updateContainer(element, container) {
  //获取hostRootFiber fiber根的根节点
  //正常来说一个fiber节点会对应一个真实DOM节点,hostRootFiber对应的DOM节点就是containerInfo div#root
  const current = container.current;
  const update = createUpdate();
  update.payload = { element };
  enqueueUpdate(current, update);
  scheduleUpdateOnFiber(current);
}

createUpdateenqueueUpdate在之前已经实现过验证过了,scheduleUpdateOnFiber顾名思义就是调度fiber的更新队列,接下来我们实现scheduleUpdateOnFiber这个方法。

scheduleUpdateOnFiber()的实现

export function scheduleUpdateOnFiber(fiber) {
  const fiberRoot = markUpdateLaneFromFiberToRoot(fiber);
  performSyncWorkOnRoot(fiberRoot);
}

这个方法,不管如何更新,不管谁来更新,都会调度到这个方法里,markUpdateLaneFromFiberToRoot就是根据当前fiber节点找到根fiber,并且返回了根fiber对应的真实DOM节点,来实现下:

function markUpdateLaneFromFiberToRoot(sourceFiber) {
  let node = sourceFiber;
  let parent = node.return;
  while (parent) {
    node = parent;
    parent = parent.return;
  }
  //node其实肯定fiber树的根节点,其实就是 hostRootFiber .stateNode div#root
  return node.stateNode;
}

在拿到根节点之后,执行performSyncWorkOnRoot(fiberRoot),它的主要用途就是根据老的fiber树和更新对象创建新的fiber树,然后根据新的fiber树更新真实DOM。具体如下:

// 当前正在更新的根
let workInProgressRoot = null;
// 当前正在更新fiber节点
let workInProgress = null;
/**
 * 根据老的fiber树和更新对象创建新的fiber树,然后根据新的fiber树更新真实DOM
 * @param {*} fiberRoot
 */
function performSyncWorkOnRoot(fiberRoot) {
  workInProgressRoot = fiberRoot;
  workInProgress = createWorkInProgress(workInProgressRoot.current);
}

创建了2个变量,workInProgressRoot和workInProgress,分别是当前正在更新的根和当前正在更新fiber节点。

双缓冲

双缓存机制是一种在内存中构建并直接替换的技术。React在协调的过程中就使用了这种技术。

在React中同时存在着两棵fiber tree。一棵是在屏幕上显示的dom对应的fiber tree,称为current fiber tree,而还有一棵是当触发新的更新任务时,React在内存中构建的fiber tree,称为workInProgress fiber tree。

current fiber tree和workInProgress fiber tree中的fiber节点通过alternate属性进行连接。

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;

React应用的根节点中也存在current属性,利用current属性在不同fiber tree的根节点之间进行切换的操作,就能够完成current fiber tree与workInProgress fiber tree之间的切换。

在协调阶段,React利用diff算法,将产生update的React elementcurrent fiber tree中的节点进行比较,并最终在内存中生成workInProgress fiber tree。此时Renderer会依据workInProgress fiber tree将update渲染到页面上。同时根节点的current属性会指向workInProgress fiber tree,此时workInProgress fiber tree就变为current fiber tree。

createWorkInProgress

createWorkInProgress就是根据老fiber创建新的fiber,具体实现如下:

/**
 * 根据老fiber创建新的fiber
 * @param {*} current 老fiber
 * @param {*} pendingProps 等待生效的属性对象
 */
export function createWorkInProgress(current, pendingProps) {
  let workInProgress = current.alternate;
  if (!workInProgress) {
    workInProgress = createFiber(current.tag, pendingProps, current.key);
    workInProgress.type = current.type;
    workInProgress.stateNode = current.stateNode;
    workInProgress.alternate = current;
    current.alternate = workInProgress;
  } else {
    workInProgress.pendingProps = pendingProps;
  }
  workInProgress.flags = NoFlags;
  workInProgress.child = null;
  workInProgress.sibling = null;
  workInProgress.updateQueue = current.updateQueue;
  //在dom diff的过程会给fiber添加副作用
  workInProgress.firstEffect = workInProgress.lastEffect = workInProgress.nextEffect = null;
  return workInProgress;
}

createWorkInProgress在执行的时候,先去拿current.alternate,此时肯定是没有的,那我们就根据这个创建一个新的fiber,并且通过

workInProgress.alternate = current;
current.alternate = workInProgress;

workInProgress和current的alternate互相指向,这个也就是在react中的双缓冲使用了。

current和workInProgress就会在后面跟新的时候来回切换,workInProgress就是当前正在构建的fiber树,current代表的是和当前页面中真实DOM完全一样的fiber树,在后面dom-diff的时候会比较他们的区别。

可以从打印的日志中可以看到,current中多了alternate。到此render阶段根据vdom生成fiber树就此完成,接下来就是fiber树的工作了。

workLoopSync工作循环

function performSyncWorkOnRoot(fiberRoot) {
  workInProgressRoot = fiberRoot;
  workInProgress = createWorkInProgress(workInProgressRoot.current);
+	workLoopSync();//执行工作循环,构建副作用链
}
/**
 * 开始自上而下构建新的fiber树
 */
function workLoopSync() {
  while (workInProgress) {
    performUnitOfWork(workInProgress);
  }
}

workLoopSync的时候,react是有一个调度任务的,根据浏览器以及程序的优先级来判断是否药中断fibre的render。performUnitOfWork为处理每一个fiber,可以看到每个节点都会走beginWorkcompleteUnitOfWork,对应的就是fiber的开始和结束。unitOfWork.alternate前面也说过了对应的就是和当前页面中真实DOM完全一样的fiber树。

/**
 * 执行单个工作单元
 * unitOfWork 要处理的fiber
 */
function performUnitOfWork(unitOfWork) {
  //获取当前正在构建的fiber的替身
  const current = unitOfWork.alternate;
  //开始构建当前fiber的子fiber链表
  //它会返回下一个要处理的fiber,一般都是unitOfWork的大儿子
  //div#title这个fiber 它的返回值是一个null
  let next = beginWork(current, unitOfWork);
  //在beginWork后,需要把新属性同步到老属性上
  //div id =1 memoizedProps={id:2}   pendingProps ={id:2}
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  //当前的fiber还有子节点
  if (next) {
    workInProgress = next;
  } else {
    //如果当前fiber没有子fiber,那么当前的fiber就算完成
    completeUnitOfWork(unitOfWork);
  }
}

beginWork就是构建当前fiber的子fiber链表,并且返回下一个要处理的fiber。

/**
 * 创建当前fiber的子fiber
 * @param current
 * @param workInProgress
 * @returns {*} 下一个fiber
 */
export function beginWork(current, workInProgress) {
  switch (workInProgress.tag) {
    case HostRoot:
      return updateHostRoot(current, workInProgress);
    case HostComponent:
      return updateHostComponent(current, workInProgress);
    default:
      break;
  }
}

Fiber 执行阶段

  • 每次渲染有两个阶段:Reconciliation(协调\render 阶段)和 Commit(提交阶段)
  • 协调阶段: 可以认为是 Diff 阶段, 这个阶段可以被中断, 这个阶段会找出所有节点变更,例如节点新增、删除、属性变更等等, 这些变更 React 称之为副作用(Effect)
  • 提交阶段: 将上一个阶段计算出来的需要处理的副作用(Effects)一次性执行了。这个阶段必须同步执行,不能被打断

render 阶段遍历规则

  • render 阶段会构建 fiber 树
let A1 = { type: "div", key: "A1" };
let B1 = { type: "div", key: "B1", return: A1 };
let B2 = { type: "div", key: "B2", return: A1 };
let C1 = { type: "div", key: "C1", return: B1 };
let C2 = { type: "div", key: "C2", return: B1 };
A1.child = B1;
B1.sibling = B2;
B1.child = C1;
C1.sibling = C2;
module.exports = A1;
  • 从顶点开始遍历

  • 如果有第一个儿子,先遍历第一个儿子

  • 如果没有第一个儿子,标志着此节点遍历完成

  • 如果有弟弟遍历弟弟

  • 如果有没有下一个弟弟,返回父节点标识完成父节点遍历,如果有叔叔遍历叔叔

  • 没有父节点遍历结束

  • 遍历规则

  • 1.下一个节点:先ル子,后弟弟,再叔叔

  • 2.自己所有子节点完成后自己完成

  • 先儿子,后弟弟,再叔叔,辈份越小越优先

  • 什么时候一个节点遍历完成? 没有子节点,或者所有子节点都遍历完成了

  • 没爹了就表示全部遍历完成了

上图就是对应节点的fiber树,下图是节点遍历的顺序

beginWork的时候根据fiber的tag(类型)进行不同节点的更新。

/**
 * 更新或者说挂载根节点
 * 依据什么构建fiber树? 虚拟DOM
 * @param {*} current 老fiber
 * @param {*} workInProgress 构建中的新fiber
 */
function updateHostRoot(current, workInProgress) {
  const updateQueue = workInProgress.updateQueue;
  //获取要渲染的虚拟DOM <div key="title" id="title">title</div>
  const nextChildren = updateQueue.shared.pending.payload.element;//element
  //处理子节点,根据老fiber和新的虚拟DOM进行对比,创建新的fiber树
  reconcileChildren(current, workInProgress, nextChildren);
  //返回第一个子fiber
  return workInProgress.child;
}
function updateHostComponent(current, workInProgress) {
  //获取 此原生组件的类型 span p
  const type = workInProgress.type;
  //新属性
  const nextProps = workInProgress.pendingProps;//props.children
  let nextChildren = nextProps.children;
  //在react对于如果一个原生组件,它只有一个儿子,并且这个儿子是一个字符串的话,有一个优化
  //不会对此儿子创建一个fiber节点,而是把它当成一个属性来处理
  let isDirectTextChild = shouldSetTextContent(type, nextProps);
  if (isDirectTextChild) {
    nextChildren = null;
  }
  //处理子节点,根据老fiber和新的虚拟DOM进行对比,创建新的fiber树
  reconcileChildren(current, workInProgress, nextChildren);
  //返回第一个子fiber
  return workInProgress.child;
}

不管是什么样类型的fiber(根fiber,类组件,函数组件,原生DOM…)都会走到reconcileChildren这个方法中,这个地方才是dom-diff真正执行的阶段。

export function reconcileChildren(current, workInProgress, nextChildren) {
  //如果current有值,说明这是一类似于更新的作品
  if (current) {
    //进行比较 新老内容,得到差异进行更新
    workInProgress.child = reconcileChildFibers(
      workInProgress,//新fiber
      current.child,//老fiber的第一个子fiber节点
      nextChildren //新的虚拟DOM
    );
  } else {
    ///初次渲染,不需要比较 ,全是新的
    workInProgress.child = mountChildFibers(
      workInProgress,//新fiber
      current && current.child,//老fiber的第一个子fiber节点
      nextChildren //新的虚拟DOM
    );
  }
}

reconcileChildFibersmountChildFibers一个是更新,一个是初次渲染,所需要的参数都是一样,新建一个childReconciler函数:

childReconciler

function childReconciler(shouldTrackSideEffects) {
  function reconcileChildFibers(returnFiber, currentFirstChild, newChild) {
    
  }
  return reconcileChildFibers;
}
export const reconcileChildFibers = childReconciler(true);
export const mountChildFibers = childReconciler(false);

通过shouldTrackSideEffects参数来控制是更新还是渲染操作。

/**
 * @param {*} returnFiber 新的父fiber
 * @param {*} currentFirstChild current就是老的意思,老的第一个子fiber
 * @param {*} newChild 新的虚拟DOM
 */
function reconcileChildFibers(returnFiber, currentFirstChild, newChild) {
  //判断newChild是不是一个对象,如果是的话说明新的虚拟DOM只有一个React元素节点
  const isObject = typeof newChild === 'object' && ( newChild );
  //说明新的虚拟DOM是单节点
  if (isObject) {
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE:
        return placeSingleChild(reconcileSingleElement(
          returnFiber, currentFirstChild, newChild
        ));
    }
  }
}

reconcileSingleElement协调单元素节点

function reconcileSingleElement(returnFiber, currentFirstChild, element) {
  const created = createFiberFromElement(element);//div#title
  created.return = returnFiber;
  return created;
}
/**
 * 根据虚拟DOM元素创建fiber节点
 * @param {*} element 虚拟节点
 */
export function createFiberFromElement(element) {
  const { key, type, props } = element;
  let tag;
  if (typeof type === 'string') {// span div p
    tag = HostComponent;//标签等于原生组件
  }
  const fiber = createFiber(tag, props, key);
  fiber.type = type;
  return fiber;
}
function placeSingleChild(newFiber) {
  // 如果当前需要跟踪副作用,并且当前这个新的fiber它的替身不存在
  if (shouldTrackSideEffects && !newFiber.alternate) {
    //给这个新fiber添加一个副作用,表示在未来提前阶段的DOM操作中会向真实DOM树中添加此节点
    newFiber.flags = Placement; // Placement创建或者挂载
  }
  return newFiber;
}

completeUnitOfWork

performUnitOfWork中已经处理了beginWork,接下来就是处理completeUnitOfWork

function completeUnitOfWork(unitOfWork) {
  let completedWork = unitOfWork;
  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;
    // 完成此fiber对应的真实DOM节点创建和属性赋值的功能
    completeWork(current, completedWork);
    
  } while (workInProgress);
}
export function completeWork(current, workInProgress) {
  const newProps = workInProgress.pendingProps;
  switch (workInProgress.tag) {
    case HostComponent:
      //创建真实的DOM节点
      const type = workInProgress.type;//div p span
      //创建此fiber的真实DOM
      const instance = createInstance(type, newProps);
      //让此Fiber的真实DOM属性指向instance
      workInProgress.stateNode = instance;
      //给真实DOM添加属性 包括如果独生子是字符串或数字的情况
      finalizeInitialChildren(instance, type, newProps);
      break;
    default:
      break;
  }
}

createInstancefinalizeInitialChildren都是react封装的根据虚拟Dom的一些DOM操作,但是为了抹平平台的差异性,都会有对应平台的操作dom的方法,比如在浏览器环境createInstance就对应了document.createElement(type)方法。

export function createInstance(type) {
  return createElement(type);
}

export function finalizeInitialChildren(domElement, type, props) {
  setInitialProperties(domElement, type, props);
}
export function createElement(type) {
  return document.createElement(type);
}

export function setInitialProperties(domElement, type, props) {
  for (const propKey in props) {
    const nextProp = props[propKey];
    if (propKey === 'children') {
      if (typeof nextProp === 'string' || typeof nextProp === 'number') {
        domElement.textContent = nextProp;
      }
    } else if (propKey === 'style') {
      for (let stylePropKey in nextProp) {
        domElement.style[stylePropKey] = nextProp[stylePropKey]
      }
    } else {
      domElement[propKey] = nextProp;
    }
  }
}

completeUnitOfWork 在完成此fiber对应的真实DOM节点创建和属性赋值的功能之后,我们还需要收集当前fiber的副作用到父fiber上。

function completeUnitOfWork(unitOfWork) {
  let completedWork = unitOfWork;
  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;
    // 完成此fiber对应的真实DOM节点创建和属性赋值的功能
    completeWork(current, completedWork);
+    collectEffectList(returnFiber, completedWork);
  } while (workInProgress);
}

collectEffectList

  • 为了避免遍历fiber树寻找有副作用的节点,所有才有了effectList
  • 在fiber树构建过程中,每当一个fiber节点的flags的字段不为noFlags时代表需要执行副作用,fiber节点添加到effectList中去
  • effectList是一个单向链表,lastEffect代表链表中的第一个节点,lastEffect代表链表中最后一个节点
  • fiber树的构建是深度优先遍历的,也就是先向下构建子级fiber节点,子级节点构建完成后,再向上构建父级fiber节点,所以effectList中总是子级fiber节点在最前面
  • fiber节点构建完成的操作执行在completeUnitOfWork方法,在这个方法里,不仅会对节点完成构建,也会将有flag的fiber节点添加到effectList中去
let rootFiber = { key: 'rootFiber' };
let fiberA = { key: 'A', flags: Placement };
let fiberB = { key: 'B', flags: Placement };
let fiberC = { key: 'C', flags: Placement };

假设有这样的一个fiber树,对应的图如下:

对于这样的一个结构,按照fiber节点的遍历顺序,应该就是rootFiber->A-B-C

function collectEffectList(returnFiber, completedWork) {
  const flags = completedWork.flags;
  //如果此完成的fiber有副使用,那么就需要添加到effectList里
  if (flags) {
    returnFiber.firstEffect = completedWork;
    returnFiber.lastEffect = completedWork;
  }
}

第一步:

collectEffectList(fiberA, fiberB);

B把自己的fiber给A,那么用图表示就如下:

第二步:

collectEffectList(fiberA, fiberC);

C把自己的fiber给A,C应该是在B的后面,也就是B应该有个nextEffect指向C ,所以collectEffectList就修改如下:

function collectEffectList(returnFiber, completedWork) {
  const flags = completedWork.flags;
  //如果此完成的fiber有副使用,那么就需要添加到effectList里
  if (flags) {
    //如果父fiber有lastEffect的话,说明父fiber已经有effect链表
    if (returnFiber.lastEffect) {
      returnFiber.lastEffect.nextEffect = completedWork;
    } else {
      returnFiber.firstEffect = completedWork;
    }
    returnFiber.lastEffect = completedWork;
  }
}

可以用下图来表示第二步的操作结果:

第三步:

collectEffectList(rootFiber, fiberA);

这个时候就需要把A收集好的给到rootFiber,靠目前这个逻辑是实现不了,只能把A给到rootFiber,A收集的BC都丢失了,再来继续改造下collectEffectList方法:

function collectEffectList(returnFiber, completedWork) {
+  //如果父亲 没有effectList,那就让父亲 的firstEffect链表头指向自己的头
+  if (!returnFiber.firstEffect) {
+    returnFiber.firstEffect = completedWork.firstEffect;
+  }
  const flags = completedWork.flags;
  //如果此完成的fiber有副使用,那么就需要添加到effectList里
  if (flags) {
    //如果父fiber有lastEffect的话,说明父fiber已经有effect链表
    if (returnFiber.lastEffect) {
      returnFiber.lastEffect.nextEffect = completedWork;
    } else {
      returnFiber.firstEffect = completedWork;
    }
    returnFiber.lastEffect = completedWork;
  }
}

returnFiber没有firstEffect,说明没有effectList,那就让父亲 的firstEffect链表头指向自己的头,A的firstEffect不就是B么,可以用下图表示:

collectEffectList还要继续改造下:

function collectEffectList(returnFiber, completedWork) {
  //如果父亲 没有effectList,那就让父亲 的firstEffect链表头指向自己的头
  if (!returnFiber.firstEffect) {
    returnFiber.firstEffect = completedWork.firstEffect;
  }
  //如果自己有链表尾,说明自己身上是有链的
  if (completedWork.lastEffect) {
    //并且父亲也有链表尾,说明父亲身上也是有链的
    if (returnFiber.lastEffect) {
      //把自己身上的effectList挂接到父亲的链表尾部
      returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
    }
    returnFiber.lastEffect = completedWork.lastEffect;
  }
  const flags = completedWork.flags;
  //如果此完成的fiber有副使用,那么就需要添加到effectList里
  if (flags) {
    //如果父fiber有lastEffect的话,说明父fiber已经有effect链表
    if (returnFiber.lastEffect) {
      returnFiber.lastEffect.nextEffect = completedWork;
    } else {
      returnFiber.firstEffect = completedWork;
    }

    returnFiber.lastEffect = completedWork;
  }
}

第三步完成之后整个链表如下:

完整的代码逻辑如下:

function collectEffectList(returnFiber, completedWork) {
  //如果父亲 没有effectList,那就让父亲 的firstEffect链表头指向自己的头
  if (!returnFiber.firstEffect) {
    returnFiber.firstEffect = completedWork.firstEffect;
  }
  //如果自己有链表尾
  if (completedWork.lastEffect) {
    //并且父亲也有链表尾
    if (returnFiber.lastEffect) {
      //把自己身上的effectList挂接到父亲的链表尾部
      returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
    }
    returnFiber.lastEffect = completedWork.lastEffect;
  }
  const flags = completedWork.flags;
  //如果此完成的fiber有副使用,那么就需要添加到effectList里
  if (flags) {
    //如果父fiber有lastEffect的话,说明父fiber已经有effect链表
    if (returnFiber.lastEffect) {
      returnFiber.lastEffect.nextEffect = completedWork;
    } else {
      returnFiber.firstEffect = completedWork;
    }
    returnFiber.lastEffect = completedWork;
  }
}

测试下collectEffectList的结果

let rootFiber = { key: 'rootFiber' };
let fiberA = { key: 'A', flags: Placement };
let fiberB = { key: 'B', flags: Placement };
let fiberC = { key: 'C', flags: Placement };

collectEffectList(fiberA, fiberB);
collectEffectList(fiberA, fiberC);
collectEffectList(rootFiber, fiberA);
let effectList = '';
let nextEffect = rootFiber.firstEffect;
while (nextEffect) {
  effectList += `${nextEffect.key}=>`;
  nextEffect = nextEffect.nextEffect;
}
effectList += 'null';
console.log(effectList);

结果就如下:

B=>C=>A=>null

commitRoot

接下来就是fiber的commit提交阶段

function performSyncWorkOnRoot(fiberRoot) {
  workInProgressRoot = fiberRoot;
  workInProgress = createWorkInProgress(workInProgressRoot.current);
  workLoopSync();//执行工作循环,构建副作用链
+  commitRoot();//提交,修改DOM
}
function commitRoot() {
  //指向新构建的fiber树
  const finishedWork = workInProgressRoot.current.alternate;
  workInProgressRoot.finishedWork = finishedWork;
  commitMutationEffects(workInProgressRoot);
}
function commitMutationEffects(root) {
  const finishedWork = root.finishedWork;
  let nextEffect = finishedWork.firstEffect;
  let effectsList = '';
  while (nextEffect) {
    effectsList += `(${getFlags(nextEffect.flags)}#${nextEffect.type}#${nextEffect.key})`;
    nextEffect = nextEffect.nextEffect;
  }
  effectsList += 'null';
  console.log(effectsList);
  root.current = finishedWork;
}

接下来就是要把真正的节点给插入

function commitMutationEffects(root) {
  const finishedWork = root.finishedWork;
  let nextEffect = finishedWork.firstEffect;
  let effectsList = '';
  while (nextEffect) {
    effectsList += `(${getFlags(nextEffect.flags)}#${nextEffect.type}#${nextEffect.key})`;
+    const flags = nextEffect.flags;
+    if (flags === Placement) {
+      commitPlacement(nextEffect);
+    }
    nextEffect = nextEffect.nextEffect;
  }
  effectsList += 'null';
  console.log(effectsList);
  root.current = finishedWork;
}
function commitPlacement(nextEffect) {
  let stateNode = nextEffect.stateNode;
  let parentStateNode = getParentStateNode(nextEffect);
  parentStateNode.appendChild(stateNode);
}
function getParentStateNode(fiber) {
  let parent = fiber.return;
  do {
    if (parent.tag === HostComponent) {//原生元素
      return parent.stateNode;
    } else if (parent.tag === HostRoot) {//根
      return parent.stateNode.containerInfo;
    } else {
      //函数组件或类组件
      parent = parent.return;
    }
  } while (parent);
}

到此,节点在页面上显示出来,完成了react的初次渲染。下一节来正真的看下react中的dom-diff。

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐