学习Build your own React.

187 阅读6分钟

环境

  1. 使用Vite创建一个React项目
  • 修改vite.config.js
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [],
  esbuild: {
    jsxFactory: "Art.createElement",
    jsxFragment: "Art.Fragment",
  },
});

  1. 删掉src总所有文件,新建main.jsx, Art.js
import Art from "./Art";
const element = <h1>Hello Art!</h1>;
function createElement() {}

const Art = {
  createElement,
};
export default Art;

  1. npm run dev, 打开浏览器,可以看到已经成功编译成指定的函数了
import Art from "/src/Art.js";
const element = /* @__PURE__ */
Art.createElement("h1", null, "Hello Art!");

V1 Basic

createElement

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) =>
        typeof child !== "object" ? createTextElement(child) : child
      ),
    },
  };
}
// 统一结构,方便处理
function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  };
}

使用上述的代码,编译后能得到下面的vdom

{
  "type": "h1",
    "props": {
    "children": [
      {
        "type": "TEXT_ELEMENT",
        "props": {
          "nodeValue": "Hello Art!",
          "children": []
        }
      }
    ]
  }
}

render函数

在Art中新建render函数,作用是将vdom tree => DOM

function render(element, container) {
  // 将vdom tree => DOM
  // base case
  const { type, props } = element;

  const dom =
    type === "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(type);

  // 复制属性
  const isProperty = (key) => key !== "children";
  Object.keys(props)
    .filter(isProperty)
    .forEach((name) => {
      dom[name] = props[name];
    });

  // make progrss, 递归渲染chidren
  element.props.children.map((child) => render(child, dom));
  // 添加到DOM中
  container.appendChild(dom);
}

V2 Concurrent

V1版本的的问题是,当VDOM树很大时, 会阻塞主线程

  • requestIdleCallback可以在浏览器空闲的时候指向回调函数,类似raf(react用这个)
  • 我们需要把渲染任务分割成一个个更小的子任务,完成一个子任务后可以被中断, 然后继续下一个任务
let nextUnitOfWork = null

function workLoop(deadline){
  let shouldYield = false
  while (nextUnitOfWork&&!shouldYield){
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    // 否则会一直循环
    shouldYield = deadline.timeRemaining() < 1
  }
}
requestIdleCallback(workLoop)
performUnitOfWork(nextUnitOfWork)
  • 疑问暂未解决

为什么要使用workLoop这种形式呢,而不是直接在原来的render函数中使用requestIdleCallback?

function render(element, container) {
  // 将vdom tree => DOM
  // base case
  const { type, props } = element;

  const dom =
    type === "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(type);

  // 复制属性
  const isProperty = (key) => key !== "children";
  Object.keys(props)
    .filter(isProperty)
    .forEach((name) => {
      dom[name] = props[name];
    });

  // make progrss, 递归渲染
  element.props.children.forEach((child) =>
    requestIdleCallback(() => render(child, dom))
  );
  // 添加到DOM中
  container.appendChild(dom);
}

Fiber tree

fiber1.png
Fiber tree其中一个作用是能够更好的找到下一个节点

render
  • 创建fiber tree根节点
performUnitOfWork
  1. 创建该fiber对应的dom
  2. 创建该fiber对应elment的children的fiber
  3. 设置下一个nextUnitOfWork

function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDOM(fiber);
  }
  // 添加到DOM
  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom);
  }
  const elements = fiber.props.children;

  let index = 0;
  let previousSibling = null;
  while (index < elements.length) {
    const ele = elements[index];
    const { type, props } = ele;
    const newFiber = {
      type,
      props,
      dom: null,
      parent: fiber,
      child: null,
      sibling: null,
    };

    if (index === 0) {
      fiber.child = newFiber;
    } else {
      previousSibling.sibling = newFiber;
    }
    previousSibling = newFiber;
    index++;
  }

  /* 寻找下一个unit的过程和rendre递归的过程是一样的*/
  //   child
  if (fiber.child) {
    return fiber.child;
  }

  // sibling
  if (fiber.sibling) {
    return fiber.sibling;
  }
  //  parent
  let next = fiber.parent;
  while (next) {
    if (next.sibling) {
      return next.sibling;
    }
    next = next.parent;
  }
}

Render and Commit

刚刚我们所写的performUnitWork创建好dom, 会添加到DOM, 因为我们的workLoop是可以被浏览器中断的,因此可能看到不完整的UI。我们可以使用一个wipRoot记录fiber tree根节点,等所有的工作完全之后,一次性commit到DOM中

完整代码
//
let nextUnitOfWork = null;
let wipRoot = null;
function render(element, container) {
  // 创建fiber tree根节点
  wipRoot = {
    type: null,
    props: {
      children: [element],
    },
    dom: container,
    parent: null,
    child: null,
    sibling: null,
  };
  nextUnitOfWork = wipRoot;
}

function workLoop(deadline) {
  // 一个workLoop可以完成多个
  let shoudYield = false;

  while (nextUnitOfWork && !shoudYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    // Idle < 1时,callback返回, 一个workLoop可以完成多个unitOf work。
    // 如果没有这行,会一直循环直到所有任务完成,这样就相当于渲染整颗vdom了
    shoudYield = deadline.timeRemaining() < 1;
  }
  // 此时rendering工作已经完成,进入commit阶段
  if (!nextUnitOfWork && wipRoot) {
    commitRoot();
  }

  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDOM(fiber);
  }

  const elements = fiber.props.children;

  let index = 0;
  let previousSibling = null;
  while (index < elements.length) {
    const ele = elements[index];
    const { type, props } = ele;
    const newFiber = {
      type,
      props,
      dom: null,
      parent: fiber,
      child: null,
      sibling: null,
    };

    if (index === 0) {
      fiber.child = newFiber;
    } else {
      previousSibling.sibling = newFiber;
    }
    previousSibling = newFiber;
    index++;
  }

  /* 寻找下一个unit的工作和rendre递归的过程是一样的*/
  //   child
  if (fiber.child) {
    return fiber.child;
  }

  // sibling
  if (fiber.sibling) {
    return fiber.sibling;
  }

  //  parent
  let next = fiber.parent;
  while (next) {
    if (next.sibling) {
      return next.sibling;
    }
    next = next.parent;
  }
}

function commitRoot() {
  //   注意 这里是child
  commitWork(wipRoot.child);
  wipRoot = null;
}
function commitWork(fiber) {
  // base case
  if (!fiber) {
    return;
  }
  const domParent = fiber.parent.dom;
  domParent.appendChild(fiber.dom);
  // make progress
  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

function createDOM(fiber) {
  const { type, props } = fiber;

  const dom =
    type === "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(type);

  // 复制属性
  const isProperty = (key) => key !== "children";
  Object.keys(props)
    .filter(isProperty)
    .forEach((name) => {
      dom[name] = props[name];
    });
  return dom;
}

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) =>
        typeof child !== "object" ? createTextElement(child) : child
      ),
    },
  };
}

function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  };
}

const Art = {
  createElement,
  render,
};
export default Art;

V3 Reconciliation

主要完成更新操作,diff
新增currentRoot记录旧的fiber tree根节点,

// 下一个要处理的根节点
let nextUnitOfWork = null;
// working in progrss fiber tree/正在处理的fiber tree根节点
let wipRoot = null;
// 当前已经commit到DOM里面的的fiber tree根节点
let currentRoot = null;

将wipRoot commit之后,currentRoot = wipRoot

function commitRoot(){
  commitWork(wipRoot.child)
  currentRoot = wipRoot
	wipRoot = null
}

重构performUnitOfWork, 将创建fiber的逻辑抽离出来。每个节点新增alternate属性,用于记录相应的旧fiber

function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDOM(fiber);
  }
  // diff/reconcile
  const elements = fiber.props.children;
  reconcileChildren(fiber, elements);

  /* 寻找下一个unit的工作和rendre递归的过程是一样的*/
  //   child
  if (fiber.child) {
    return fiber.child;
  }

  // sibling
  if (fiber.sibling) {
    return fiber.sibling;
  }

  //  parent
  let next = fiber.parent;
  while (next) {
    if (next.sibling) {
      return next.sibling;
    }
    next = next.parent;
  }
}

effectTag用于commit phase判断应该进行什么操作

function reconcileChildren(wipFiber, elements) {
  // 用于判断是不是第一个孩子
  let index = 0;
  // 记录前面一个sibling
  let previousSibling = null;
  // 定位到第一个旧的child(因为是reconcileChildren)
  let oldFiber = wipFiber.alternate?.child;
  /**
   * 需要比较elements和oldFibers, elements是新的, oldFibers是旧的。 一个遍历数组,一个遍历链表
   * 相当于两个数组的比较
   */
  while (index < elements.length || oldFiber) {
    const element = elements[index];
    let newFiber = null;

    const sameType = element?.type === oldFiber?.type;

    if (sameType) {
      // 可以复用dom
      newFiber = {
        type: element.type,
        props: element.props,
        dom: oldFiber.dom,
        alternate: oldFiber,
        parent: wipFiber,
        child: null,
        sibling: null,
        effectTag: "UPDATE",
      };
    }
    if (!sameType && element) {
      // 有新的element
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null,
        alternate: oldFiber,
        parent: wipFiber,
        child: null,
        sibling: null,
        effectTag: "PLACEMENT",
      };
    }
    if (!sameType && oldFiber) {
      // 删除旧的
      // 因为我们正在新建的这颗wipRoot不会保存被删除的节点,
      // 因此需要放在其它地方
      oldFiber.effectTag = "DELETION";
      deletions.push(oldFiber);
    }
    // 切换到下一个sibling
    oldFiber = oldFiber?.sibling;
    if (index === 0) {
      wipFiber.child = newFiber;
    } else if (element) {
      previousSibling.sibling = newFiber;
    }
    previousSibling = newFiber;
    index++;
  }
}

完整代码

// 下一个要处理的根节点
let nextUnitOfWork = null;
// working in progrss fiber tree/正在处理的fiber tree根节点
let wipRoot = null;
// 当前已经commit到DOM里面的的fiber tree根节点
let currentRoot = null;
// 记录需要删除的
let deletions = null;
function render(element, container) {
  // 创建fiber tree根节点
  wipRoot = {
    type: null,
    props: {
      children: [element],
    },
    dom: container,
    alternate: currentRoot,
    parent: null,
    child: null,
    sibling: null,
  };
  // 初始化
  deletions = [];
  nextUnitOfWork = wipRoot;
}

requestIdleCallback(workLoop);
function workLoop(deadline) {
  // 一个workLoop可以完成多个
  let shoudYield = false;

  while (nextUnitOfWork && !shoudYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    // Idle < 1时,callback返回, 一个workLoop可以完成多个unitOf work。
    // 如果没有这行,会一直循环直到所有任务完成,这样就相当于渲染整颗vdom了
    shoudYield = deadline.timeRemaining() < 1;
  }
  if (!nextUnitOfWork && wipRoot) {
    commitRoot();
  }
  requestIdleCallback(workLoop);
}

function reconcileChildren(wipFiber, elements) {
  // 用于判断是不是第一个孩子
  let index = 0;
  // 记录前面一个sibling
  let previousSibling = null;
  // 定位到第一个旧的child(因为是reconcileChildren)
  let oldFiber = wipFiber.alternate?.child;
  /**
   * 需要比较elements和oldFibers, elements是新的, oldFibers是旧的。 
   * 一个遍历数组,一个遍历链表
   * 相当于两个数组的比较
   */
  while (index < elements.length || oldFiber) {
    const element = elements[index];
    let newFiber = null;

    const sameType = element?.type === oldFiber?.type;

    if (sameType) {
      // 可以复用dom
      newFiber = {
        type: element.type,
        props: element.props,
        dom: oldFiber.dom,
        alternate: oldFiber,
        parent: wipFiber,
        child: null,
        sibling: null,
        effectTag: "UPDATE",
      };
    }
    if (!sameType && element) {
      // 有新的element
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null,
        alternate: oldFiber,
        parent: wipFiber,
        child: null,
        sibling: null,
        effectTag: "PLACEMENT",
      };
    }
    if (!sameType && oldFiber) {
      // 删除旧的
      // 因为我们正在新建的这颗wipRoot不会保存被删除的节点,因此需要放在其它地方
      oldFiber.effectTag = "DELETION";
      deletions.push(oldFiber);
    }
    // 切换到下一个sibling
    oldFiber = oldFiber?.sibling;
    if (index === 0) {
      wipFiber.child = newFiber;
    } else if (element) {
      previousSibling.sibling = newFiber;
    }
    previousSibling = newFiber;
    index++;
  }
}

function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDOM(fiber);
  }
  // diff/reconcile
  const elements = fiber.props.children;
  reconcileChildren(fiber, elements);

  /* 寻找下一个unit的工作和rendre递归的过程是一样的*/
  //   child
  if (fiber.child) {
    return fiber.child;
  }

  // sibling
  if (fiber.sibling) {
    return fiber.sibling;
  }

  //  parent
  let next = fiber.parent;
  while (next) {
    if (next.sibling) {
      return next.sibling;
    }
    next = next.parent;
  }
}

function commitRoot() {
  // 删掉旧的
  deletions.forEach(commitWork);
  // commit wipRoot
  commitWork(wipRoot.child);
  currentRoot = wipRoot;
  wipRoot = null;
}
function commitWork(fiber) {
  // base case
  if (!fiber) {
    return;
  }
  const domParent = fiber.parent.dom;
  if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
    // TODO, 为什么这里要判断dom
    domParent.appendChild(fiber.dom);
  } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
    // alternate属性用在这里
    updateDOM(fiber.dom, fiber.alternate.props, fiber.props);
  } else if (fiber.effectTag === "DELETION") {
    domParent.removeChild(fiber.dom);
  }
  // make progress
  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

function updateDOM(dom, oldProps, newProps) {
  const isEvent = (key) => key.startsWith("on");
  const isProperty = (key) => key !== "children" && !isEvent(key);
  const isNew = (key) => oldProps[key] !== newProps[key];
  const isGoneProperty = (key) => !(key in newProps);
  const isGoneEvent = (key) => isGoneProperty(key) || isNew(key);
  // 删除旧的属性
  Object.keys(oldProps)
    .filter(isProperty)
    .filter(isGoneProperty)
    .forEach((key) => {
      // delete
      dom[key] = "";
    });

  // 移除旧的事件监听, 事件handle变化了就要移除,
  // 和普通的属性稍微不一样
  Object.keys(oldProps)
    .filter(isEvent)
    .filter(isGoneEvent)
    .forEach((key) => {
      const eventType = key.toLowerCase().substring(2);
      dom.removeEventListener(eventType, oldProps[key]);
    });

  // 更新/新增的属性, 不变的保持不变
  Object.keys(newProps)
    .filter(isProperty)
    .filter(isNew)
    .forEach((key) => {
      dom[key] = newProps[key];
    });
  // 注册事件监听
  Object.keys(newProps)
    .filter(isEvent)
    .filter(isNew)
    .forEach((key) => {
      const eventType = key.toLowerCase().substring(2);
      dom.addEventListener(eventType, newProps[key]);
    });
}

function createDOM(fiber) {
  const { type, props } = fiber;

  const dom =
    type === "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(type);

  // 使用updateDOM
  updateDOM(dom, {}, props);
  return dom;
}

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) =>
        typeof child !== "object" ? createTextElement(child) : child
      ),
    },
  };
}

function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  };
}

const Art = {
  createElement,
  render,
};
export default Art;

V4 函数组件

函数组件有个特殊的地方是children. React几乎所有东西都是Props.
考虑一下2与7-8的区别,

  • 2 只有运行App之后才能得到
  • 7-8仅仅是element(组件App的实例)的props中的children, 该elment的实际要渲染的children是运行App函数后得到的 children = App(element.props)
function App(children){
  return <h1>{children}</h1>
}

const element = 
  <App>
    <h2></h2>
    <h3></h3>
  </App>

函数组件有对应的element(vdom), 但该element没有相应的dom, 因此遇到这种element时候,需要特殊处理。

function performUnitOfWork(fiber) {
  // TODO 需要重构
  if (!fiber.dom) {
    fiber.dom = createDOM(fiber);
  }
	// ...
}
function updateHostComponent(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDOM(fiber);
  }
  const elements = fiber.props.children;
  reconcileChildren(fiber, elements);
}

function updateFunctionComponent(fiber) {
  // 函数式组件, 需要运行之后才能得到children
  const children = [fiber.type(fiber.props)];
  reconcileChildren(fiber, children);
}

function performUnitOfWork(fiber) {
  if (typeof fiber.type === "string") {
    updateHostComponent(fiber);
  } else {
    updateFunctionComponent(fiber);
  }

  /* 寻找下一个unit的工作和rendre递归的过程是一样的*/
  //   child
  if (fiber.child) {
    return fiber.child;
  }

  // sibling
  if (fiber.sibling) {
    return fiber.sibling;
  }

  //  parent
  let next = fiber.parent;
  while (next) {
    if (next.sibling) {
      return next.sibling;
    }
    next = next.parent;
  }
}

下一步需要处理的是commit, 因为函数组件没有dom, 需要特殊处处理。因此,当一个fiber要挂载(将dom挂到容器里面),需要找到其最近的具有dom的fiber节点。
同样的,如果需要删除一个组件fiber, 需要递归的移除所有子fiber

function commitRoot() {
  // 删掉旧的
  deletions.forEach(commitWork);
  // commit wipRoot
  commitWork(wipRoot.child);
  currentRoot = wipRoot;
  wipRoot = null;
}
function commitWork(fiber) {
  // base case
  if (!fiber) {
    return;
  }
  const parentFiber = fiber.parent;

  while (parentFiber && !parentFiber.dom) {
    parentFiber = parentFiber.parent;
  }
  const domParent = parentFiber.dom;

  if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
    domParent.appendChild(fiber.dom);
  } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
    // alternate属性用在这里
    updateDOM(fiber.dom, fiber.alternate.props, fiber.props);
  } else if (fiber.effectTag === "DELETION") {
    commitDeletion(fiber, domParent);
  }
  // make progress
  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

function commitDeletion(fiber, domParent) {
  if (fiber.dom) {
    domParent.removeChild(fiber.dom);
  } else {
    commitDeletion(fiber.child, domParent);
  }
}

perfect, main.jsx测试一下经典的Counter组件

import Art from "./Art";
const container = document.querySelector("#root");

let count = 0;
const inc = () => {
  count += 1;
  renderer();
};

function Counter() {
  return (
    <h1>
      {count} <button onClick={() => inc()}>click</button>
    </h1>
  );
}

function renderer() {
  const element = (
    <div>
      <Counter />
      <Counter />
      <Counter />
    </div>
  );

  Art.render(element, container);
}

renderer();

V5 useState

V4版本的Counter没有state, 现在添加一个useState

wipFiber = null
hookIndex = null
function updateFunctionComponent(fiber) {
  // 函数式组件, 需要运行之后才能得到children
  wipFiber = fiber;
  wipFiber.hooks = [];
  hookIndex = 0;

  const children = [fiber.type(fiber.props)];
  reconcileChildren(fiber, children);
}



export function useState(initial) {
  const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
  // 判断是不是初次调用,如果不存在旧的状态

  const currentHook = {
    state: oldHook ? oldHook.state : initial,
    // 将每次调用缓存到queue
    queue: [],
  };
  // 转移到新的fiber
  wipFiber.hooks.push(currentHook);
  hookIndex++;

  // 在调用uesState之前先执行上次的actions, 更新curr
  const actions = oldHook ? oldHook.queue : [];
  actions.forEach((action) => {
    currentHook.state = action(currentHook.state);
  });

  const setState = (action) => {
    currentHook.queue.push(action);
    /* 触发更新,类似render函数 */
    wipRoot = {
      type: null,
      props: currentRoot.props,
      dom: currentRoot.dom,
      alternate: currentRoot,
      parent: null,
      child: null,
      sibling: null,
      effectTag: "",
    };
    nextUnitOfWork = wipRoot;
    deletions = [];
  };

  return [currentHook.state, setState];
}