搭建一个你自己的React

  • 作者:ellilachen
  • 时间:2021-02-14
  • 431人已阅读
导语:最近在看 React 源码,我是从 babel 转义后生成的 createElement 函数和 render 函数开始读起,然后发现这实在是太难看懂了,没有对 React 一个整体的把握,而是一个函数一个函数跳着看,这样可以大概看懂,但是却难以感知 React 代码如此设计的原因。因此,我觉得好的阅读 React 源码的方式是先整体上把握它的架构和设计理念,而要学习这些,可以先从头手撸一个属于自己的 React 开始。

我在网上找到了一篇很好的文章:build your own react, 如果没有科学上网或者英文太长不看的话可以看以下的文章,是在阅读这篇博客时自己对它的简单翻译以及加上了个人的理解,通过以下的阅读,你可以大概了解 React 的三个阶段:调度、协调和渲染,还可以大概了解它的一些函数的作用,如performUnitWork等。麻雀虽小,五脏俱全。这篇文章还涉及到了并发模式、Hooks设计等,对于没有阅读过 React 源码的人很适合~

接下来会通过这几个步骤,依次搭建 React

  • Step I: The createElement Function
  • Step II: The render Function
  • Step III: Concurrent Mode
  • Step IV: Fibers
  • Step V: Render and Commit Phases
  • Step VI: Reconciliation
  • Step VII: Function Components
  • Step VIII: Hooks

Build your React

  1. JSX

    JSX 是 React 的一种标签语法,形如:

const App = () => <div>你好,世界</div>

​ 这不是一种有效的JS语法,可以使用类似于 Babel 的编译工具,通过调用 createElement 函数,把标签名字type, 传入参数props还有子节点children,作为 createElement 的入参,转化为有效的 JS 语法。

例如,函数式组件App过 babel 转义后得到以下

var App = function App() {
  return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", null, "Text in div ndoe");
};
  1. createElement

    createElement 通过它传入的参数创建一个 element 对象,还包括对入参做了一些校验。element 是一个包含 type 和 props 的对象,例如 createElement("div", null, [a, b]) 会返回

    {
      "type": "div",
      "props": { "children": [a, b] }
    }
    

    creatElement的简单实现:

    function createElement(type, props, ...children) {
      return {
        type,
        props: {
          ...props,
          children,
        },
      }
    }
    

    Children 中可能包含为值为primitive的节点(纯文本),因此需要做下区分

    function createElement(type, props, ...children) {
      return {
        type,
        props: {
          ...props,
           children: children.map(child =>
            typeof child === "object"
              ? child
              : createTextElement(child)
          ),
        },
      }
    }
    function createTextElement(text) {
      return {
        type: "TEXT_ELEMENT",
        props: {
          nodeValue: text,
          children: [],
        },
      }
    }
    
  2. Render 函数

    Render 函数是来建立和 Dom 节点的联系的,包括添加节点、更新节点、删除节点。

    新增:递归子节点,根据element的类型,如果为文本节点,则调用document.createTextNode, 否则调用document.createElement,然后再把 props 添加到节点属性上。

    function render(element, container) {
      const dom =
        element.type == "TEXT_ELEMENT"
          ? document.createTextNode("")
          : document.createElement(element.type);
      const isProperty = key => key !== "children";
      Object.keys(element.props)
        .filter(isProperty)
        .forEach(name => {
          dom[name] = element.props[name];
        });
      element.props.children.forEach(child => render(child, dom));
      container.appendChild(dom);
    }
    
  3. 并发 concurrent 模式

    如果是单纯的递归调用的话,需要等待整颗 element 树渲染完,可能会造成主线程阻塞太长时间,所以我们需要把前面的这些工作拆分成多个小单元(unit),在每个工作单元结束后,假如浏览器需要执行一些优先级比较高的工作,如保证动画的流畅运行,则中断 react 的渲染,待空闲后再次调起。目前浏览器提供requestIdleCallback API 来把在浏览器的空闲时段内调用的函数排队。React 自己实现了 schedule package, 概念上是一致的。

    截至2019年11月,并发模式在React中还不稳定。 循环的稳定版本看起来像这样:

    while (nextUnitOfWork) {    
      nextUnitOfWork = performUnitOfWork(   
        nextUnitOfWork  
      ) 
    }
    

    为了让 work Loop 开启,我们需要需要实现 performUnitWork 函数,该函数不仅需要实现 work Loop 还需要返回下一个 work unit

  4. Fiber

    Fiber 就是 React 中的虚拟 Dom,fiber 树是用来组织安排工作单元的,每个 element 对应一个 fiber,每个fiber都将会是一个工作单元。在 render 阶段我们会创建一个root fiber 并将其设置为 nextUnitWork,剩下的工作就交给 performUnitWork, 在这里我们需要给每个fiber都做三件事:

    • 添加元素到dom
    • 为 element 的 chilren 创建 fibers
    • 选择 nextUnitOfWork

    为了方便实现上边的工作,采用树结构,所以每个fiber 节点都会有指向第一个子节点、父节点、兄弟节点的指针。寻找下一个工作fiber的顺序:子节点 -> 兄弟节点 -> 父节点的兄弟节点(若无,一直向上找直到root)

    function createDom(fiber) {
      // 创建真实Dom节点
    }
    function render(element, container) {
      nextUnitOfWork = {
        dom: container,
        props: {
          children: [element],
        },
      }
    }
    let nextUnitOfWork = null
    function workLoop() {
      // 使用 requestIdleCallback 实现调度
      let shouldYield = false
      while (nextUnitOfWork && !shouldYield) {
        nextUnitOfWork = performUnitOfWork(
          nextUnitOfWork
        )
        shouldYield = deadline.timeRemaining() < 1
      }
      requestIdleCallback(workLoop)
    }
    function performUnitOfWork(fiber) {
      // add dom node
      if (!fiber.dom) {
        fiber.dom = createDom(fiber)
      }
      if (fiber.parent) {
        fiber.parent.dom.appendChild(fiber.dom)
      }
      // create new fibers
      const elements = fiber.props.children
      let index = 0
      let prevSibling = null
    
      while (index < elements.length) {
        const element = elements[index]
        const newFiber = {
          type: element.type,
          props: element.props,
          parent: fiber,
          dom: null,
        }
      }
      
       if (index === 0) {
          fiber.child = newFiber
        } else {
          prevSibling.sibling = newFiber
        }
        prevSibling = newFiber
        index++
      // return next unit of work
      if (fiber.child) {
        return fiber.child
      }
      let nextFiber = fiber
      while (nextFiber) {
        if (nextFiber.sibling) {
          return nextFiber.sibling
        }
        nextFiber = nextFiber.parent
      }
    }
    
  5. render 和 commit 阶段

    在上面的处理中,由于浏览器是可以中断 work loop的,所以可能会出现UI不完整的问题,因此需要把对 DOM 结构的改变增加一个commit阶段。所以我们需要跟踪 fiber tree 的 root,只有当我们知道已经完成了所有的工作(没有 nextUnitWork了),才执行commit,提交整棵 fiber 树到 Dom 结构的变更。

    function render(element, container) {
      wipRoot = {
        dom: container,
        props: {
          children: [element],
        },
      }
      nextUnitOfWork = wipRoot
    }
    
    let nextUnitOfWork = null
    let wipRoot = null
    
    function workLoop() {
      // 使用 requestIdleCallback 实现调度
      let shouldYield = false
      while (nextUnitOfWork && !shouldYield) {
        nextUnitOfWork = performUnitOfWork(
          nextUnitOfWork
        )
        shouldYield = deadline.timeRemaining() < 1
      }
       if (!nextUnitOfWork && wipRoot) {
        commitRoot()
      }
      requestIdleCallback(workLoop)
    }
    
    function commitRoot() {
      commitWork(wipRoot.child)
      wipRoot = null
    }
    
    function commitWork(fiber) {
      if (!fiber) {
        return
      }
      const domParent = fiber.parent.dom
      domParent.appendChild(fiber.dom)
      commitWork(fiber.child)
      commitWork(fiber.sibling)
    }
    
  6. 调和阶段

    在上边一直只讲了 dom 节点的添加,下面开始讲更新和删除。

    为了判断出节点的下一个状态,我们需要在 render 阶段中比较将收到的 elements 和上一次我们提交到 dom 的 fiber 树做对比。因此要添加一个currentRoot来保存对上一次提交到 dom 的 fiber 树的引用,同时也在每一个 fiber 上添加 alternate 属性,指向上一个旧的 fiber.

    上面的 performUnitOfWork 一共做了三步操作:1. 添加元素到dom 2.为 element 的 chilren 创建 fibers 3.选择 nextUnitOfWork。现在要对第二步创建新 fibers 这里的代码做解构,拆分到reconcileChildren函数中, 在这里进行新旧Fiber的比较,打上比较的标签:

    (1) 如果类型相同,可以保持 dom 节点,使用新的属性替换;

    (2) 如果类型不同并且有新的 element,则需要创建一个新的 dom 节点;

    (3) 如果类型不同并存在旧fiber 的话,需要删除

    function render(element, container) {
      wipRoot = {
        dom: container,
        props: {
          children: [element],
        },
        alternate: currentRoot,
      }
      deletions = []
      nextUnitOfWork = wipRoot
    }
    
    let nextUnitOfWork = null
    let currentRoot = null
    let wipRoot = null
    let deletions = null
    
    // 从上面的 performUnitOfWork 中拆出来
    function performUnitOfWork(fiber) {
      // 1. 添加元素到dom  
      // 2.为 element 的 chilren 创建 fibers 
      const elements = fiber.props.children
      reconcileChildren(fiber, elements)
      // 3.选择 nextUnitOfWork。
    }
    function reconcileChildren(wipFiber, elements) {
      let index = 0
      let oldFiber = wipFiber.alternate && wipFiber.alternate.child
      let prevSibling = null
    
      while (index < elements.length || oldFiber != null) {
        const element = elements[index]
        let newFiber = null
        // 进行 oldFiber 和 element 的比较
        const sameType = oldFiber && element && element.type == oldFiber.type
        if (sameType) {
          // update the node
          newFiber = {
            type: oldFiber.type,
            props: element.props,
            dom: oldFiber.dom,
            parent: wipFiber,
            alternate: oldFiber,
            effectTag: "UPDATE",
          }
        }
        if (element && !sameType) {
          // add this node
          newFiber = {
            type: element.type,
            props: element.props,
            dom: null,
            parent: wipFiber,
            alternate: null,
            effectTag: "PLACEMENT",
          }
        }
        if (oldFiber && !sameType) {
          // delete the oldFiber's node
          oldFiber.effectTag = "DELETION"
          deletions.push(oldFiber)
        }
      }
      
       if (index === 0) {
          fiber.child = newFiber
        } else {
          prevSibling.sibling = newFiber
        }
        prevSibling = newFiber
        index++
    }
    
    function commitRoot() {
      deletions.forEach(commitWork)
      commitWork(wipRoot.child)
      currentRoot = wipRoot
      wipRoot = null
    }
    
    function commitWork(fiber) {
      if (!fiber) {
        return
      }
      const domParent = fiber.parent.dom
      if (
        fiber.effectTag === "PLACEMENT" &&
        fiber.dom != null
      ) {
        domParent.appendChild(fiber.dom)
      } else if (
        fiber.effectTag === "UPDATE" &&
        fiber.dom != null
      ) {
        updateDom(
          fiber.dom,
          fiber.alternate.props,
          fiber.props
        )
      } else if (fiber.effectTag === "DELETION") {
        domParent.removeChild(fiber.dom)
      }
      domParent.appendChild(fiber.dom)
      commitWork(fiber.child)
      commitWork(fiber.sibling)
    }
    const isEvent = key => key.startsWith("on")
    const isProperty = key =>
      key !== "children" && !isEvent(key)
    const isNew = (prev, next) => key =>
      prev[key] !== next[key]
    const isGone = (prev, next) => key => !(key in next)
    function updateDom() {
      // 删除或者改变事件监听
      // 删除旧的属性
      // 添加新属性
      // 添加新的事件监听
    }
    
  7. 函数式组件

    function组件和class组件区别在于:

    1. Functional组件的fiber没有dom节点
    2. children是通过直接运行函数得到的,而不是通过children属性

    在 performUnitOfWork 中判断是否是函数式组件,是的话执行updateFunctionComponent更新,否执行原来的更新方式

    const isFunctionComponent = fiber.type instanceof Function
    if (isFunctionComponent) {
      updateFunctionComponent(fiber)
    } else {
      updateHostComponent(fiber)
    }
    
    function updateFunctionComponent(fiber) {
      const children = [fiber.type(fiber.props)]
      reconcileChildren(fiber, children)
    }
    
    
    function updateHostComponent(fiber) {
      if (!fiber.dom) {
        fiber.dom = createDom(fiber)
      }
      reconcileChildren(fiber, fiber.props.children)
    }
    

    由于fiber 没有 dom 节点,所以在 commitWork 中也要做两个更改:

    1. domParent 需要一直向上找

      let domParentFiber = fiber.parent
      while (!domParentFiber.dom) {
        domParentFiber = domParentFiber.parent
      }
      const domParent = domParentFiber.dom
      
    2. 删除节点的时候也要找到有child是有dom节点的

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

    为了能在函数式组件中保持状态,我们需要设置一些全局变量: hooks 数组还有当前的 hook index

    let wipFiber = null
    let hookIndex = null
    
    function updateFunctionComponent(fiber) {
      wipFiber = fiber
      hookIndex = 0
      wipFiber.hooks = []
      const children = [fiber.type(fiber.props)]
      reconcileChildren(fiber, children)
    }
    

    useState 中通过 alternate 和 hookIndex 检查是否有旧的 hook,若有,则从旧的 hook 中拷贝 state 到新的 hook 中,如无,则进行hook的初始化

    function useState(initial) {
      const oldHook =
        wipFiber.alternate &&
        wipFiber.alternate.hooks &&
        wipFiber.alternate.hooks[hookIndex]
      const hook = {
        state: oldHook ? oldHook.state : initial,
      }
      wipFiber.hooks.push(hook)
      hookIndex++
      return [hook.state]
    }
    

    为了实现提供一个实现状态更新的函数,useState 函数还要再返回一个 setState 函数,并在 hook 中添加一个 queue 属性,把调用 setState 的动作推入 queue 数组保存起来,这样,下次会从队列中取出action, 依次执行,更新state

    function useState(initial) {
      const oldHook =
        wipFiber.alternate &&
        wipFiber.alternate.hooks &&
        wipFiber.alternate.hooks[hookIndex]
      const hook = {
        state: oldHook ? oldHook.state : initial,
        queue: []
      }
      
      const actions = oldHook ? oldHook.queue : []
      actions.forEach(action => {
        hook.state = action(hook.state)
      })
    
      const setState = action => {
        hook.queue.push(action)
        wipRoot = {
          dom: currentRoot.dom,
          props: currentRoot.props,
          alternate: currentRoot,
        }
        nextUnitOfWork = wipRoot
        deletions = []
      }
      wipFiber.hooks.push(hook)
      hookIndex++
      return [hook.state, setState]
    }
    

React 架构

React16架构可以分为三层:

  • Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler
  • Reconciler(协调器)—— 负责找出变化的组件
  • Renderer(渲染器)—— 负责将变化的组件渲染到页面上

从以上对React的简单实现大概可以感知这三层是如何实现的,并且应该也清楚了一些基本概念,如虚拟Dom节点对应Fiber,diff算法对应reconcile.

从浏览器中截出React的函数调用栈,可以看到是有明显分出层次的:

image.png
下期将会接着从这三层对 React 的源码进行细化分析,学习 React 为保证性能做出的优化。

Top