手写React Fiber渲染逻辑 二

1,500 阅读4分钟

接上一篇 手写React Fiber渲染逻辑 一

本章内容介绍:用React fiber实现更新渲染逻辑,实现类组件、函数组件和Hooks
对React Fiber不太了解的,可以翻看上两篇文章

实现更新

调试用例

  • 在index.html中新增两个按钮
    <div id="root"></div>
    <button id="reRender2">reRender2</button>
    <button id="reRender3">reRender3</button>
  • 在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'));
});
  • 点击两个按钮实现页面的第二和第三次更新渲染,效果如下:

Untitled.gif

alternate指针

老的fiber指当前页面已经渲染的节点,新fiber指将要渲染的节点

  • 初次渲染完毕后,会给每个节点创建一个fiber对象,页面发生更新时,会创建一个新的fiber树
  • 如图所示,currentRoot是我们页面当前显示的节点,当页面更新时,会创建一个新的根fiber workInProgressRoot
  • 每个新fiber节点都会有一个alternate指针,指向对应的老的fiber节点
  • alternate指针的作用就是进行新老节点的对比,进行dom-diff

代码实现

  • 在scheduler.js中添加两个全局变量
  • 提交完成后,让currentRoot指向workInProgressRoot,将workInProgressRoot置为null
let currentRoot = null; //当前的根Fiber
let deletions = []; //要删除的fiber节点

// -----------commit阶段-----------

function commitRoot() {
  .......
  currentRoot = workInProgressRoot;
  workInProgressRoot = null;
}

  • 更新时,render函数再次调用scheduleRoot方法,如果currentRoot有值就是更新,让rootFiberalternate指针,指向currentRoot
export function scheduleRoot(rootFiber) {
  if (currentRoot) {
    // 更新
    rootFiber.alternate = currentRoot;
    workInProgressRoot = rootFiber;
  } else {
    // 第一次渲染
    workInProgressRoot = rootFiber;
  }

  nextUnitOfWork = workInProgressRoot;
}

修改reconcileChildren方法

此处只做简单的diff,遍历一遍,如果新老节点类型相同就复用,不同就重新创建,没有实现react的diff算法

  • 遍历新的虚拟dom,并找到对应的老的oldFiber,进行比较,然后生成新的fiber树,每个新fiber都有alternate指针,指向oldFiber

  • 老的fiber节点与新的虚拟dom进行对比,如果类型相同,就复用老的fiber节点的stateNode,并将newFiberalternate指向oldFiber,将effectTag标记为UPDATE更新

  • 如果类型不同,创建newFiber时将effectTag标记为PLACEMENT插入,不复用老节点的stateNode,并将老节点oldFiber加入deletions中,提交时进行删除

/**
 * 创建fiber 构建fiber树
 * @param {*} currentFiber 当前fiber
 * @param {*} newChildren 当前节点的子节点,虚拟dom数组
 * @param {*} deletions 指向全局变量 deletions, 放置要删除的节点
 */
export function reconcileChildren(currentFiber, newChildren, deletions) {
  let newChildIndex = 0; //新虚拟DOM数组中的索引
  // 老的父fiber的第一个子fiber
  let oldFiber = currentFiber.alternate && currentFiber.alternate.child;
  let prevSibling;
  while (newChildIndex < newChildren.length || oldFiber) {
    const newChild = newChildren[newChildIndex];

    // 两个节点是不是相同类型 span div...
    const sameType = oldFiber && newChild && newChild.type === oldFiber.type;
    let newFiber;
    let tag;
    if (newChild && newChild.type === ELEMENT_TEXT) {
      tag = TAG_TEXT; //文本
    } else if (newChild && typeof newChild.type === 'string') {
      tag = TAG_HOST; //原生DOM组件
    }

    // 类型相同就更新,不同就重新创建插入
    if (sameType) {
      // 更新
      newFiber = {
        tag: oldFiber.tag, //原生DOM组件
        type: oldFiber.type, //具体的元素类型
        props: newChild.props, //新的属性对象
        stateNode: oldFiber.stateNode, //复用老fiber的dom
        return: currentFiber, //父Fiber
        alternate: oldFiber, //上一个Fiber 指向旧树中的节点
        effectTag: UPDATE, //更新节点
        nextEffect: null,
      };
    } 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);
      }
    }

    // 构建fiber链表
    if (newFiber) {
      if (newChildIndex === 0) {
        currentFiber.child = newFiber; //第一个子节点挂到父节点的child属性上
      } else {
        prevSibling.sibling = newFiber;
      }
      prevSibling = newFiber; //然后newFiber变成了上一个哥哥了
    }

    if (oldFiber) {
      oldFiber = oldFiber.sibling;
    }

    newChildIndex++;
  }
}

提交阶段

  • 提交时,先清空deletions,将要删除的节点删除
  • 然后根据节点的effectTag来进行插入和更新操作
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;
  const domReturn = returnFiber.stateNode;

  if (currentFiber.effectTag === PLACEMENT && currentFiber.stateNode) {
    //如果是新增DOM节点
    domReturn.appendChild(currentFiber.stateNode);
  } else if (currentFiber.effectTag === DELETION) {
    // 删除
    domReturn.removeChild(currentFiber.stateNode);
  } else if (currentFiber.effectTag === UPDATE && currentFiber.stateNode) {
    // 更新
    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);
      }
    } else {
    }
  }

  currentFiber.effectTag = null;
}

commit记录

双缓冲机制

  • react中为了避免重复创建销毁fiber对象,造成不必要的内存开销,采用了双缓冲机制
  • 页面更新一次之后,每个节点会有两个fiber对象,newFibernewFiber.alternate指向的oldFiber
  • 当第三次渲染时,不会重新创建newFiber,而是会复用老的fiber对象,如图所示

双缓冲机制代码实现

  • scheduleRoot方法修改
  • 第一次更新之后的更新逻辑,不直接使用rootFiber,而是将currentRoot.alternateworkInProgressRoot使用,更新它的props
export function scheduleRoot(rootFiber) {
  if (currentRoot && currentRoot.alternate) {
    // 第一次更新之后的更新
    // 双缓冲机制,复用之前的fiber对象
    workInProgressRoot = currentRoot.alternate;
    workInProgressRoot.alternate = currentRoot;
    if (rootFiber) {
      // 更新复用fiber节点的props
      workInProgressRoot.props = rootFiber.props;
    }
  } else if (currentRoot) {
    // 第一次更新
    rootFiber.alternate = currentRoot;
    workInProgressRoot = rootFiber;
  } else {
    // 第一次渲染
    workInProgressRoot = rootFiber;
  }

  // 清除effect list
  workInProgressRoot.firstEffect = workInProgressRoot.lastEffect = null;
  nextUnitOfWork = workInProgressRoot;
}
  • reconcileChildren方法修改
  • 节点类型相同时,如果老节点有alternate,就把老节点的alternate,拿过来更新一下属性,作为newFiber使用
    if (sameType) {
      // 更新
      if (oldFiber.alternate) {
        // 双缓冲机制,复用老的fiber
        newFiber = oldFiber.alternate;
        newFiber.props = newChild.props;
        newFiber.alternate = oldFiber;
        newFiber.effectTag = UPDATE;
      } else {
        newFiber = {
          tag: oldFiber.tag, //原生DOM组件
          type: oldFiber.type, //具体的元素类型
          props: newChild.props, //新的属性对象
          stateNode: oldFiber.stateNode, //复用老fiber的dom
          return: currentFiber, //父Fiber
          alternate: oldFiber, //上一个Fiber 指向旧树中的节点
          effectTag: UPDATE, //更新节点
          nextEffect: null,
        };
      }
    }
  • 这样我们双缓冲机制的更新逻辑就实现了

commit记录

类组件渲染

测试类组件

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

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'));

UpdateQueue

  • 更新队列,是个单链表结构
  • 每次setState都会将要更新的数据放在队列中,到一定时机进行批量更新

image.png

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;
    }
}

类组件代码实现

  • 在react.js文件中声明一个Componet类
  • internalFiber指向Componet类对应的fiber节点,fiber节点有updateQueue
  • 调用setState方法时,会先将要更新的payload放入更新队列,然后执行scheduleRoot方法
class Component {
  constructor(props) {
    this.props = props;
  }

  setState(payload) {
    this.internalFiber.updateQueue.enqueueUpdate(new Update(payload));
    scheduleRoot();
  }
}
// 函数组件标识
Component.prototype.isReactComponent = true;

const React = {
  createElement,
  Component,
};

scheduler.js逻辑修改

  • 当类组件setState时 传入的rootFiber为空,需要处理rootFiber为空的情况
// 暴露给外部, 当类组件setState时 传入的rootFiber为空
export function scheduleRoot(rootFiber) {
  if (currentRoot && currentRoot.alternate) {
    // 第一次之后的更新
    // 双缓冲机制,复用之前的fiber对象
    workInProgressRoot = currentRoot.alternate;
    workInProgressRoot.alternate = currentRoot;
    if (rootFiber) {
      // 更新复用fiber节点的props
      workInProgressRoot.props = rootFiber.props;
    }
  } else if (currentRoot) {
    // 第一次更新
+    if (rootFiber) {
+      rootFiber.alternate = currentRoot;
+      workInProgressRoot = rootFiber;
+    } else {
+      workInProgressRoot = {
+        ...currentRoot,
+        alternate: currentRoot,
+      };
+    }
  } else {
    workInProgressRoot = rootFiber;
  }

  // 清除effect list
  workInProgressRoot.firstEffect = workInProgressRoot.lastEffect = null;
  nextUnitOfWork = workInProgressRoot;
}

新增 updateClassComponent处理类组件的更新

  • 类组件的stateNode不是真实dom元素,是类组件的实例
  • 每个类组件实例都有一个internalFiber指向类组件对应的fiber节点
  • 给类组件实例的fiber节点创建一个updateQueue,用来处理类组件的更新逻辑

// 类组件
function updateClassComponent(currentFiber) {
  if (!currentFiber.stateNode) {
    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, deletions);
}

reconcileChildren处理类组件的fiber创建

  • 判断是否是类组件,给类组件添加对应tag
  • 创建fiber时给每个fiber添加updateQueue属性,处理组件更新
export function reconcileChildren(currentFiber, newChildren, deletions) {
  let newChildIndex = 0; //新虚拟DOM数组中的索引
  // 老的父fiber的第一个子fiber
  let oldFiber = currentFiber.alternate && currentFiber.alternate.child;
  if (oldFiber) oldFiber.firstEffect = oldFiber.lastEffect = oldFiber.nextEffect = null;

  let prevSibling;
  while (newChildIndex < newChildren.length || oldFiber) {
    const newChild = newChildren[newChildIndex];

    // 两个节点是不是相同类型 span div...
    const sameType = oldFiber && newChild && newChild.type === oldFiber.type;
    let newFiber;
    let tag;

+    // class 会被编译成函数,所以用function判断, 用 isReactComponent来判断是否是类组件
+    if (
+      newChild &&
+      typeof newChild.type === 'function' &&
+      newChild.type.prototype.isReactComponent
+    ) {
+      tag = TAG_CLASS;
+    }  else if (newChild && typeof newChild.type === 'function') {
+      // 函数组件
+      tag = TAG_FUNCTION_COMPONENT;
    } else 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) {
        // 双缓冲机制,复用老的fiber
        newFiber = oldFiber.alternate;
        newFiber.props = newChild.props;
        newFiber.alternate = oldFiber;
        newFiber.effectTag = UPDATE;
+        newFiber.updateQueue = oldFiber.updateQueue || new UpdateQueue();
      } else {
        newFiber = {
          tag: oldFiber.tag, //原生DOM组件
          type: oldFiber.type, //具体的元素类型
          props: newChild.props, //新的属性对象
          stateNode: oldFiber.stateNode, //复用老fiber的dom
          return: currentFiber, //父Fiber
          alternate: oldFiber, //上一个Fiber 指向旧树中的节点
          effectTag: UPDATE, //更新节点
+          updateQueue: oldFiber.updateQueue || new UpdateQueue(),
        };
      }
    } else {
      // 新建
      if (newChild) {
        // 创建fiber
        newFiber = {
          tag, //原生DOM组件
          type: newChild.type, //具体的元素类型
          props: newChild.props, //新的属性对象
          stateNode: null, //stateNode肯定是空的
          return: currentFiber, //父Fiber
          effectTag: PLACEMENT, //插入节点
+          updateQueue: new UpdateQueue(),
        };
      }
      if (oldFiber) {
        oldFiber.effectTag = DELETION;
        deletions.push(oldFiber);
      }
    }

    // 构建fiber链表
    if (newFiber) {
      if (newChildIndex === 0) {
        currentFiber.child = newFiber; //第一个子节点挂到父节点的child属性上
      } else {
        prevSibling.sibling = newFiber;
      }
      prevSibling = newFiber; //然后newFiber变成了上一个哥哥了
    }

    if (oldFiber) {
      oldFiber = oldFiber.sibling;
    }

    newChildIndex++;
  }
}

类组件的提交逻辑

  • 由于类组件没有真实dom(stateNode对应的是类的实例,不是真实dom),所以删除和更新时需要进行特殊处理
  • 需要递归往下查找到有真实dom的子节点,然后进行删除或插入
function commitWork(currentFiber) {
  if (!currentFiber) return;
  let returnFiber = currentFiber.return;

  // 类组件fiber 中的stateNode是类的实例,不是真实dom,需要往上查找到真实dom的节点
  while (returnFiber.tag === TAG_CLASS) {
    returnFiber = returnFiber.return;
  }

  const domReturn = returnFiber.stateNode;

  if (currentFiber.effectTag === PLACEMENT && currentFiber.stateNode) {
    // 类组件没有真实dom,需要往下查找
    let nextFiber = currentFiber;
    while (nextFiber.tag === TAG_CLASS) {
      nextFiber = nextFiber.child;
    }

    //如果是新增DOM节点
    domReturn.appendChild(nextFiber.stateNode);
  } else if (currentFiber.effectTag === DELETION) {
    // 删除
    commitDeletion(currentFiber, domReturn);
  } else if (currentFiber.effectTag === UPDATE && currentFiber.stateNode) {
    // 更新
    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);
      }
    } else {
    }
  }

  currentFiber.effectTag = null;
}

// 删除类组件dom
function commitDeletion(currentFiber, domReturn) {
  if (currentFiber.tag === TAG_CLASS) {
    // 往下找
    commitDeletion(currentFiber.child, domReturn);
  } else {
    domReturn.removeChild(currentFiber.stateNode);
  }
}
  • 最终效果如下

Untitled.gif

commit记录

实现函数组件与Hooks

  • 声明全局变量 workInProgressFiberhookIndex
let workInProgressFiber = null; // 正在工作中的fiber
let hookIndex = 0; // hook索引
  • 在scheduler.js中添加函数组件的update方法updateFunctionComponent
  • currentFiber.type 对应函数组件的函数名,调用 currentFiber.type(currentFiber.props)相当于执行函数组件,返回一个虚拟节点
  • 执行函数组件时,会调用函数中引用的react hooks方法
// 函数组件
function updateFunctionComponent(currentFiber) {
  workInProgressFiber = currentFiber;
  hookIndex = 0;
  workInProgressFiber.hooks = [];

  const newChildren = [currentFiber.type(currentFiber.props)];
  reconcileChildren(currentFiber, newChildren, deletions);
}
  • 在scheduler.js中实现useReduceruseState两个hooks
  • useState是通过useReducer来实现的
  • 函数组件的fiber会有hooks属性来存放用到的hooks,hooks包含两个属性state状态和updateQueue更新队列
  • 第一次执行useReducer时会将初始状态存到fiber.hooks中,函数组件更新时,会使用存储在fiber.hooks中的状态
  • dispatch方法可以修改 hooks中的state,并触发scheduleRoot进行重新渲染
// -------------------------------hooks--------------------------------------
export function useReducer(reducer, initialValue) {
  let oldHook =
    workInProgressFiber.alternate &&
    workInProgressFiber.alternate.hooks &&
    workInProgressFiber.alternate.hooks[hookIndex];
  let newHook = oldHook;
  if (oldHook) {
    newHook.state = oldHook.updateQueue.forceUpdate(oldHook.state);
  } else {
    newHook = {
      state: initialValue,
      updateQueue: new UpdateQueue(),
    };
  }
  const dispatch = action => {
    console.log('action', action);
    if (typeof action === 'function') {
      action = action(newHook.state);
    }
    const newState = reducer ? reducer(newHook.state, action) : action;
    newHook.updateQueue.enqueueUpdate(new Update(newState));
    scheduleRoot();
  };

  // 将hook的数据放在 fiber的hooks中,并让hookIndex指针后移
  workInProgressFiber.hooks[hookIndex++] = newHook;
  return [newHook.state, dispatch];
}

export function useState(initState) {
  return useReducer(null, initState);
}

  • 在react中引用useReducer useState并导出
import { scheduleRoot, useReducer, useState } from './scheduler';

........

const React = {
  createElement,
  Component,
  useReducer,
  useState,
};

export default React;

  • 在index.js中引用hooks,用hooks实现一个计数器案例
import React from './react/react';
import ReactDOM from './react/react-dom';

// Hooks
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>
      <h3 onClick={() => setNumberState(state => ({ number: state.number + 1 }))}>
        useState Count: {numberState.number}
      </h3>
      <hr />
      <h3 onClick={() => dispatch({ type: 'ADD' })}>useReducer Count: {countState.count}</h3>
    </div>
  );
}

ReactDOM.render(<FunctionCounter />, document.getElementById('root'));

commit记录

代码奉上

有漏洞和不足之处,还请大家指正