手写React Fiber渲染逻辑 一

1,196 阅读5分钟

接上一篇 React Fiber原理

上一篇介绍了React Fiber的基本原理,本文会带你用React fiber实现react的首次渲染逻辑

新建项目

首先用create-react-app来创建一个react应用

  • 删除src中的无用文件,保留index.js文件
  • 检查package文件,react版本为17及以上的话,需要将react版本降为16
  • react17采用了runtime模式,在文件开头不用引入React,会默认用项目中的react进行渲染,所以需要把版本降为16,用我们自己实现的react进行渲染
  • 先用原生的react和react-dom来进行渲染
import React from 'react';
import ReactDOM from 'react-dom';

let style = { border: '3px solid red', margin: '5px' };
let element = (
  <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(element, document.getElementById('root'));

image.png

虚拟Dom

虚拟Dom介绍

  • 项目打包时,jsx语法是通过babel来进行转换的,会将jsx语法替换成React.createElement()的方式,通过这个方法来创建虚拟dom
  • 通过 babel官网,我们可以看到转换后的代码
  • React.createElement()参数,第一个是节点的类型,第二个是节点的属性,后面的都是节点的children,会按照dom树结构,嵌套进行创建

image.png

实现createElement方法

  • 新建react文件夹,里面放我们自己写的react image.png

  • constants.js 放一些需要定义的常量

// type属性 表示这是一个文本元素 文本节点的fiber {tag:TAG_TEXT,type:ELEMENT_TEXT}
export const ELEMENT_TEXT = Symbol.for('ELEMENT_TEXT');

// tag属性
//React应用需要一个根Fiber
export const TAG_ROOT = Symbol.for('TAG_ROOT');
//原生的节点 span div p  函数组件 类组件
export const TAG_HOST = Symbol.for('TAG_HOST');
//这是文本节点
export const TAG_TEXT = Symbol.for('TAG_TEXT');
//这是类组件
export const TAG_CLASS = Symbol.for('TAG_CLASS');
//函数组件
export const TAG_FUNCTION_COMPONENT = Symbol.for('TAG_FUNCTION_COMPONENT');

// effectTag属性
//插入节点
export const PLACEMENT = Symbol.for('PLACEMENT');
//更新节点
export const UPDATE = Symbol.for('UPDATE');
//删除节点
export const DELETION = Symbol.for('DELETION');

  • react.js对应原生的react库,导出createElement方法,主要负责创建虚拟dom
// react.js
import { ELEMENT_TEXT } from './constants';

/**
 * 创建虚拟Dom的方法
 * @param {*} type 元素类型 div span p
 * @param {*} config 配置对象  元素属性
 * @param  {...any} children 所有的儿子,这里会做成一个数组
 */
function createElement(type, config, ...children) {
  delete config.__self;
  delete config.__source;

  // 做了兼容处理,如果是文本类型,返回元素对象
  const newChildren = children.map(child => {
    // child是一个React.createElement返回的React元素
    if (typeof child === 'object') return child;
    // child是字符串
    return {
      type: ELEMENT_TEXT,
      props: { text: child, children: [] },
    };
  });
  return {
    type,
    props: {
      ...(config || {}), // 有可能为null
      children: newChildren,
    },
  };
}

const React = {
  createElement,
};

export default React;

  • react.js对应原生的react-dom库,导出render方法 主要负责页面渲染
import { TAG_ROOT } from './constants';
import { scheduleRoot } from './scheduler';

/**
 * 把一个元素渲染到容器内部
 * @param {*} element 渲染的元素 虚拟Dom
 * @param {*} container 挂载的节点
 */
function render(element, container) {
  let rootFiber = {
    tag: TAG_ROOT, // 根fiber
    stateNode: container, // 真实dom节点
    //props.children是一个数组,里面放的是React元素 虚拟DOM 后面会根据每个React元素创建 对应的Fiber
    props: { children: [element] },
  };
  // 开始调度
  scheduleRoot(rootFiber);
}

const ReactDOM = {
  render,
};

export default ReactDOM;

实现调度核心逻辑

image.png

  • scheduler.js核心调度逻辑,导出scheduleRoot方法,供render方法调用,传入根fiber执行调度逻辑
import { TAG_HOST, TAG_ROOT, TAG_TEXT, PLACEMENT } from './constants';
import { reconcileChildren } from './reconcileChildren';
import { setProps } from './utils';

let workInProgressRoot = null; //正在渲染中的根Fiber
let nextUnitOfWork = null; //下一个工作单元

// 暴露给外部
export function scheduleRoot(rootFiber) {
  workInProgressRoot = rootFiber;
  nextUnitOfWork = workInProgressRoot;
}

// 1.工作循环,每帧结束都会执行
function workLoop(deadline) {
  let shouldYield = false; // 是否暂停
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    shouldYield = deadline.timeRemaining() < 1;
  }

  if (!nextUnitOfWork && workInProgressRoot) {
    console.log('调和阶段结束');
    commitRoot();
  }

  //不管有没有任务,都请求再次调度 每一帧都要执行一次workLoop,检查有没有要执行的任务
  requestIdleCallback(workLoop, { timeout: 500 });
}

// 2.执行每个fiber的工作
function performUnitOfWork(currentFiber) {
  beginWork(currentFiber);
  // 先遍历大儿子
  if (currentFiber.child) {
    return currentFiber.child;
  }
  while (currentFiber) {
    // 没有子节点的先完成
    completeUnitOfWork(currentFiber);
    // 看看有没有弟弟,有的话遍历弟弟
    if (currentFiber.sibling) {
      return currentFiber.sibling;
    }
    // 子节点都完成了,让父亲完成,父亲是root节点,return是null, 跳出while循环
    currentFiber = currentFiber.return;
  }
}

// 3.开始工作
// 两个功能:1.创建真实DOM 2.创建子fiber
function beginWork(currentFiber) {
  switch (currentFiber.tag) {
    case TAG_ROOT:
      updateHostRoot(currentFiber);
      break;
    case TAG_TEXT:
      updateHostText(currentFiber);
      break;
    case TAG_HOST:
      updateHost(currentFiber);
      break;
    default:
      break;
  }
}

// 根fiber, stateNode是外部传入的
function updateHostRoot(currentFiber) {
  const newChildren = currentFiber.props.children;
  reconcileChildren(currentFiber, newChildren);
}

// 文本类型
function updateHostText(currentFiber) {
  // 没有创建真实dom,进行创建
  // 文本类型,不存在子节点,不需要执行reconcileChildren
  if (!currentFiber.stateNode) {
    currentFiber.stateNode = createDOM(currentFiber);
  }
}

// 原生类型
function updateHost(currentFiber) {
  // 没有创建真实dom,进行创建
  if (!currentFiber.stateNode) {
    currentFiber.stateNode = createDOM(currentFiber);
  }
  const newChildren = currentFiber.props.children;
  reconcileChildren(currentFiber, newChildren);
}

// 创建真实dom
function createDOM(currentFiber) {
  if (currentFiber.tag === TAG_TEXT) {
    return document.createTextNode(currentFiber.props.text);
  } else if (currentFiber.tag === TAG_HOST) {
    // span div 这些原生标签
    const stateNode = document.createElement(currentFiber.type);
    updateDOM(stateNode, {}, currentFiber.props);
    return stateNode;
  }
}

// 更新真实dom的属性
function updateDOM(stateNode, oldProps, newProps) {
  // 存在,而且是个真实dom
  if (stateNode && stateNode.setAttribute) {
    setProps(stateNode, oldProps, newProps);
  }
}

// 4.完成工作,收集有副作用的fiber,构建effect list
function completeUnitOfWork(currentFiber) {
  let returnFiber = currentFiber.return;
  if (!returnFiber) return;

  if (!returnFiber.firstEffect) {
    returnFiber.firstEffect = currentFiber.firstEffect;
  }

  if (currentFiber.lastEffect) {
    if (returnFiber.lastEffect) {
      // 让上一单元的lastEffect.nextEffect 指向下一单元的firstEffect
      returnFiber.lastEffect.nextEffect = currentFiber.firstEffect;
    }
    returnFiber.lastEffect = currentFiber.lastEffect;
  }

  // 有effectTag,说明有副作用,需要收集
  if (currentFiber.effectTag) {
    if (returnFiber.lastEffect) {
      returnFiber.lastEffect.nextEffect = currentFiber;
    } else {
      returnFiber.firstEffect = currentFiber;
    }
    returnFiber.lastEffect = currentFiber;
  }
}

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

function commitRoot() {
  let currentFiber = workInProgressRoot.firstEffect;
  console.log(workInProgressRoot.firstEffect);
  while (currentFiber) {
    commitWork(currentFiber);
    currentFiber = currentFiber.nextEffect;
  }
  // 提交完成
  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);
  }
  currentFiber.effectTag = null;
}

requestIdleCallback(workLoop, { timeout: 500 });

performUnitOfWork方法

  • 主要控制节点的遍历顺序和完成顺序
  • 遍历顺序:自己 -> 儿子 -> 弟弟
  • 完成顺序:儿子 -> 自己 -> 弟弟 -> 父亲
function performUnitOfWork(currentFiber) {
  // 1.先遍历自己
  beginWork(currentFiber);
  // 2.遍历大儿子
  if (currentFiber.child) {
    return currentFiber.child;
  }
  while (currentFiber) {
    // 3.1 没有子节点的先完成
    // 3.2 儿子和弟弟完成,让自己完成
    completeUnitOfWork(currentFiber);
    // 4. 看看有没有弟弟,有的话遍历弟弟
    if (currentFiber.sibling) {
      return currentFiber.sibling;
    }
    // 5. 子节点都完成了,让父亲完成,父亲是root节点,return是null, 跳出while循环
    currentFiber = currentFiber.return;
  }
}

beginWork方法

  • 1.根据不同类型,创建真实Dom,检查stateNode有没有值,没有的话就进行创建
  • 2.执行reconcileChildren方法,根据子节点虚拟dom,创建fiber,构建fiber树
function beginWork(currentFiber) {
  switch (currentFiber.tag) {
    case TAG_ROOT:
      // 根节点
      updateHostRoot(currentFiber);
      break;
    case TAG_TEXT:
      // 文本节点
      updateHostText(currentFiber);
      break;
    case TAG_HOST:
      // 原生标签
      updateHost(currentFiber);
      break;
    default:
      break;
  }
}

// 根fiber, stateNode是外部传入的
function updateHostRoot(currentFiber) {
  const newChildren = currentFiber.props.children;
  reconcileChildren(currentFiber, newChildren);
}

// 文本类型
function updateHostText(currentFiber) {
  // 没有创建真实dom,进行创建
  // 文本类型,不存在子节点,不需要执行reconcileChildren
  if (!currentFiber.stateNode) {
    currentFiber.stateNode = createDOM(currentFiber);
  }
}

// 原生类型
function updateHost(currentFiber) {
  // 没有创建真实dom,进行创建
  if (!currentFiber.stateNode) {
    currentFiber.stateNode = createDOM(currentFiber);
  }
  const newChildren = currentFiber.props.children;
  reconcileChildren(currentFiber, newChildren);
}

// 创建真实dom
function createDOM(currentFiber) {
  if (currentFiber.tag === TAG_TEXT) {
    return document.createTextNode(currentFiber.props.text);
  } else if (currentFiber.tag === TAG_HOST) {
    // span div 这些原生标签
    const stateNode = document.createElement(currentFiber.type);
    updateDOM(stateNode, {}, currentFiber.props);
    return stateNode;
  }
}

// 更新真实dom的属性
function updateDOM(stateNode, oldProps, newProps) {
  // 存在,而且是个真实dom
  if (stateNode && stateNode.setAttribute) {
    setProps(stateNode, oldProps, newProps);
  }
}
  • 更新/设置dom属性
  • 点击事件采用了原生方式进行设置,没有用到react合成事件
// utils.js,
// 给元素设置属性
export function setProps(dom, oldProps, newProps) {
  for (let key in oldProps) {
    if (key !== 'children') {
      if (newProps.hasOwnProperty(key)) {
        setProp(dom, key, newProps[key]); // 新老都有,则更新
      } else {
        dom.removeAttribute(key); //老props里有此属性,新 props没有,则删除
      }
    }
  }
  for (let key in newProps) {
    if (key !== 'children') {
      if (!oldProps.hasOwnProperty(key)) {
        //老的没有,新的有,就添加此属性
        setProp(dom, key, newProps[key]);
      }
    }
  }
}
// 设置单个
function setProp(dom, key, value) {
  if (/^on/.test(key)) {
    //onClick
    dom[key.toLowerCase()] = value; //没有用合成事件
  } else if (key === 'style') {
    if (value) {
      for (let styleName in value) {
        dom.style[styleName] = value[styleName];
      }
    }
  } else {
    dom.setAttribute(key, value);
  }
}

reconcileChildren方法

  • 根据子节点虚拟dom,创建子节点fiber,构建fiber树
  • 将大儿子挂在当前fiber的child
  • 将弟弟们挂在大儿子的sibling
  • 首次创建要将副作用标识 effectTag设置为 PLACEMENT
import {
  ELEMENT_TEXT,
  TAG_TEXT,
  TAG_HOST,
  PLACEMENT,
} from './constants';

/**
 * 创建fiber 构建fiber树
 * @param {*} currentFiber 当前fiber
 * @param {*} newChildren 当前节点的子节点,虚拟dom数组
 */
export function reconcileChildren(currentFiber, newChildren) {
  let newChildIndex = 0; //新虚拟DOM数组中的索引
  let prevSibling;
  while (newChildIndex < newChildren.length) {
    const newChild = newChildren[newChildIndex];
    let tag;
    if (newChild && newChild.type === ELEMENT_TEXT) {
      tag = TAG_TEXT; //文本
    } else if (newChild && typeof newChild.type === 'string') {
      tag = TAG_HOST; //原生DOM组件
    }
    // 创建fiber
    let newFiber = {
      tag, //原生DOM组件
      type: newChild.type, //具体的元素类型
      props: newChild.props, //新的属性对象
      stateNode: null, //stateNode肯定是空的
      return: currentFiber, //父Fiber
      effectTag: PLACEMENT, //副作用标识
      nextEffect: null,
    };

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

    newChildIndex++;
  }
}

收集Effect 构建Effect List

  • completeUnitOfWork完成工作时,收集有副作用的fiber,构建副作用链表
  • 链表通过fiber的属性 firstEffect nextEffect lastEffect进行构建 image.png

构建Effect List步骤

step1. 没有子节点的大儿子先完成

returnFiber和currentFiber 的firstEffect、lastEffect都为空
returnFiber.firstEffect = 大儿子
returnFiber.lastEffect = 大儿子

step2. 没有子节点的弟弟完成

returnFiber.firstEffect = 大儿子
returnFiber.lastEffect = 大儿子
currentFiber 的firstEffect、lastEffect为空

returnFiber.lastEffect.nextEffect = currentFiber = 弟弟;
returnFiber.lastEffect = currentFiber = 弟弟;

step3. 父节点完成

returnFiber的firstEffect、lastEffect为空
currentFiber.firstEffect = 大儿子
currentFiber.lastEffect = 弟弟
大儿子.nextEffect = 弟弟

returnFiber.firstEffect = currentFiber.firstEffect; (父节点的父节点.firstEffect = 大儿子)

returnFiber.lastEffect = currentFiber.lastEffect; (父节点的父节点.lastEffect = 弟弟)

将父节点挂在 弟弟 后面 returnFiber.lastEffect(弟弟).nextEffect = currentFiber(父节点);

父节点的父节点的lastEffect指向 父节点 returnFiber.lastEffect = currentFiber;

step4.下个单元完成

让上一单元的lastEffect.nextEffect 指向下一单元的firstEffect
returnFiber.lastEffect.nextEffect = currentFiber.firstEffect;

function completeUnitOfWork(currentFiber) {
  let returnFiber = currentFiber.return;
  if (!returnFiber) return;

  if (!returnFiber.firstEffect) {
    returnFiber.firstEffect = currentFiber.firstEffect;
  }

  if (currentFiber.lastEffect) {
    if (returnFiber.lastEffect) {
      // 让上一单元的lastEffect.nextEffect 指向下一单元的firstEffect
      returnFiber.lastEffect.nextEffect = currentFiber.firstEffect;
    }
    returnFiber.lastEffect = currentFiber.lastEffect;
  }

  // 有effectTag,说明有副作用,需要收集
  if (currentFiber.effectTag) {
    if (returnFiber.lastEffect) {
      returnFiber.lastEffect.nextEffect = currentFiber;
    } else {
      returnFiber.firstEffect = currentFiber;
    }
    returnFiber.lastEffect = currentFiber;
  }
}

提交CommitRoot

  • 节点遍历完成,effect list收集完毕后,进入提交阶段
  • 提交阶段会根据收集的effect list,进行遍历,将要修改的内容应用到真实Dom上
  • 这个过程是不可中断的
function commitRoot() {
  let currentFiber = workInProgressRoot.firstEffect;
  console.log(workInProgressRoot.firstEffect);
  while (currentFiber) {
    commitWork(currentFiber);
    currentFiber = currentFiber.nextEffect;
  }
  // 提交完成
  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);
  }
  currentFiber.effectTag = null;
}

实现首次渲染

  • 将入口文件中的,react和react-dom库换成自己写的,启动项目
// 自己写的react
import React from './react/react';
import ReactDOM from './react/react-dom';
  • 实现效果

image.png

代码奉上

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