关于html5:学习笔记构建你自己的React

Build your own React

Build your own React 的学习笔记

预览

Build your own React CN

前言

重写React, 遵循React代码中的架构, 然而没有进行优化。基于React16.8, 应用hook并删除了所有与类相干的代码。

零: review

首先回顾一些React的概念,上面是一个简略的React应用程序。一共三行代码,第一行定义了一个React元素, 第二行获取了DOM节点, 最初一行将React元素渲染到容器中。

const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)

第一行中,咱们应用了JSX, JSX不是无效的JavaScript,咱们应用原生js替换它。通常通过Babel等构建工具,JSX转换为JS。应用createElement替换JSX标记,并将标签名,props,子级作为参数。

const element = React.createElement(
  "h1",
  { title: "foo" },
  "Hello"
);

React.createElement, 会依据参数创立一个对象。除了一些验证外,这就是React.createElement所做的全副。咱们能够间接React.createElement函数替换成它的输入。

const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello",
  },
}

一个一般的JavaScript对象, 次要有两个属性typepropstype属性是一个字符串,示意咱们创立的DOM节点的类型。它也能够是一个函数,然而咱们留在前面说。props是一个对象, props中有一个非凡的属性children。在以后的状况children是字符串,然而通常状况下它是蕴含更多元素的数组。接下来咱们须要替换ReactDOM.render

首先应用type属性,创立一个节点。咱们将element的所有props调配给该节点,目前只有title属性。而后咱们为子节点创立节点。咱们的children是一个字符串,因而咱们创立一个文本节点。

为什么应用createTextNode而不是innerText呢?因为在之后都会以雷同的形式解决所有元素。

最初将textNode增加到h1中,h1增加到container中。

const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello",
  },
}
​const container = document.getElementById("root")

const node = document.createElement(element.type)
node["title"] = element.props.title
const text = document.createTextNode("")
text["nodeValue"] = element.props.children
node.appendChild(text)
container.appendChild(node)

目前咱们领有了和之前一样的程序,然而没有应用React。

一: createElement

咱们从一个新的程序开始,这次咱们应用本人的React替换原来的React代码。

const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)
const container = document.getElementById("root")
ReactDOM.render(element, container)

咱们从编写本人的createElement开始。

const element = createElement(
  "div",
  { id: "foo" },
  createElement("a", null, "bar"),
  createElement("b")
)
const container = document.getElementById("root")
render(element, container)

createElement须要做的就是创立一个typeprops的对象。createElement函数中, children参数应用rest运算符, children始终就会为数组。

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

例如, createElement("div", null, a, b)会返回:

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

目前children数组中会蕴含原始值,比方字符串和数字。咱们须要对它们进行包装。咱们创立一个非凡的类型TEXT_ELEMENT

在React源码中,不会包装原始值, 或者在没有子级的状况下创立空的数组。咱们这样做的目标是为了简化咱们的代码.

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

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

咱们如何让Babel在编译的过程中,应用咱们本人创立的createElement呢?咱们在配置babel@babel/preset-react插件时自定义pragma参数

二: render

接下来咱们须要编写本人的ReactDOM.render

目前咱们只关怀向DOM中增加内容,稍后解决更新和删除

咱们首先应用元素的类型创立DOM节点,而后将新节点增加到容器中

function render(element, container) {
  const dom = document.createElement(element.type)
  container.appendChild(dom)
}

咱们须要递归的为每一个children元素做雷同的事件

function render(element, container) {
  const dom = document.createElement(element.type)
  element.props.children.forEach(child =>
    render(child, dom)
  )
  container.appendChild(dom)
}

之前增加了文本元素的节点,所以在创立节点时须要判断元素的类型

function render(element, container) {
  const dom =
    element.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type)

  element.props.children.forEach(child =>
    render(child, dom)
  )
  container.appendChild(dom)
}

最初咱们须要将元素的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)
}

目前为止,咱们曾经有了一个将JSX出现到DOM的库。

三: 并发模式

在这之前,咱们须要重构代码。

递归渲染存在问题,一旦开始渲染就无奈进行,直到咱们渲染实现整个树。如果树很大,会阻塞主线程过长的工夫。

????️: React Fiber架构应用了链表树实现了可中断渲染,如果大家有趣味能够参考这篇文章

因而咱们须要把工作分解成几个小单元,在咱们实现每个单元后,有重要的事件要做,咱们中断渲染。

咱们应用requestIdleCallback实现循环, 浏览器会在闲暇时,执行requestIdleCallback的回调。React的外部并不应用requestIdleCallback, React外部应用scheduler package, 通过requestIdleCallback咱们还能够取得咱们还有多少可用工夫用于渲染。

????️: 对于requestIdleCallback的更多细节能够查看这篇文章,详解 requestIdleCallback

let nextUnitOfWork = null
​
function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    shouldYield = deadline.timeRemaining() < 1
  }
  requestIdleCallback(workLoop)
}
​
requestIdleCallback(workLoop)
​
function performUnitOfWork(nextUnitOfWork) {
  // TODO
}

????️: nextUnitOfWork变量放弃了Fiber中须要工作节点援用或者为null, 如果是null示意没有工作。

要开始咱们的workLoop, 咱们须要第一个工作单元(Fiber节点),而后编写performUnitOfWork函数,performUnitOfWork函数执行工作,并返回下一个须要工作的节点。

四: Fibers

咱们须要一个数据结构Fiber树(链表树)。每一个元素都有对应的Fiber节点, 每一个Fiber是一个工作单元。

假如咱们须要渲染这样的一颗树:

render(
  <div>
    <h1>
      <p />
      <a />
    </h1>
    <h2 />
  </div>,
  container
)

render中,创立Fiber,并将根节点的Fiber调配给nextUnitOfWork变量。余下的工作在performUnitOfWork函数进行,须要做三件事:

  1. 将元素增加到DOM
  2. 为子节创立Fiber
  3. 返回下一个工作单元

Fiber树是一个链表树,每一个Fiber节点有child, parent, sibling属性

  • child, 第一个子级的援用
  • sibling, 第一个同级的援用
  • parent, 父级的援用

????️: 在React的Fiber节点中,应用return字段保留了对父Fiber节点的援用

遍历Fiber树(链表树)时应用了深度优先遍历,说一下遍历的过程:

  1. 从根节点root获取第一个子节点
  2. 如果root有子节点,将以后指针设置为第一个子节点,并进入下一次迭代。(深度优先遍历)
  3. 如果root的第一个子节点,没有子节点,则尝试获取它的第一个兄弟节点。
  4. 如果有兄弟节点,将以后指针设置为第一个子节点,而后兄弟节点进入深度优先遍历。
  5. 如果没有兄弟节点,则返回根节点root。尝试获取父节点的兄弟节点。
  6. 如果父节点没有兄弟节点,则返回根节点root。最初完结遍历。

好,接下来咱们开始增加代码, 将创立的DOM的代码独自抽离出, 稍后应用它

function createDom(fiber) {
  const dom = fiber.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]
    })
  return dom
}

render函数中,将nextUnitOfWork变量设置为Fiber节点树的根

function render(element, container) {
  nextUnitOfWork = {
    dom: container,
    props: {
      children: [element],
    },
  }
}

当浏览器准备就绪,调用workLoop,开始解决根节点

let nextUnitOfWork = null
​
function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    shouldYield = deadline.timeRemaining() < 1
  }
  requestIdleCallback(workLoop)
}
​
requestIdleCallback(workLoop)
​
function performUnitOfWork(fiber) {
  // 增加DOM节点
  // 创立Fiber
  // 获取下一个解决工作的Fiber节点
}

首先创立DOM, 并增加到Fiber节点的dom字段中,咱们在dom字段中保留对dom的援用

function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
​
  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }
}

????️: 在React的Fiber节点中,stateNode字段,保留对class组件实例的援用, DOM节点或其余与Fiber节点相关联的React元素类实例的援用。

接下来为每一个子元素创立Fiber节点。同时因为Fiber树是一个链表树,所以咱们须要为Fiber节点增加child, parent, sibling字段

function performUnitOfWork(nextUnitOfWork) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
​
  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }

  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, // 父Fiber节点的援用
      dom: null,
    }

    if (index === 0) {
      // 父Fiber节点增加child字段
      fiber.child = newFiber
    } else {
      // 同级的Fiber节点增加sibling字段
      prevSibling.sibling = newFiber
    }
​
    prevSibling = newFiber
    index++
  }
}

在实现的以后节点的工作后,咱们须要返回下一个节点。因为是深度优先遍历,首先尝试遍历child,而后是sibling, 最初回溯到parent, 尝试遍历parentsibling

function performUnitOfWork(nextUnitOfWork) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
​
  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }

  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字段
      fiber.child = newFiber
    } else {
      // 同级的Fiber节点增加sibling字段
      prevSibling.sibling = newFiber
    }
​
    prevSibling = newFiber
    index++
  }

  // 首先尝试子节点
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    // 尝试同级节点
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}

五: render 和 commit

目前存在的问题,在遍历Fiber树的时候,咱们目前会在这里向DOM中增加新节点,因为咱们应用requestIdleCallback, 浏览器可能会中断咱们的渲染,用户会看到不残缺的UI。这违反了一致性的准则。

????️: React的外围准则之一是”一致性”, 它总是一次性更新DOM, 不会显示局部后果。

????️: 在React的源码中, React分为两个阶段执行工作, render阶段和commit阶段。render阶段的工作是能够异步执行的,React依据可用工夫解决一个或者多个Fiber节点。当产生一些更重要的事件时,React会进行并保留已实现的工作。等重要的事件解决实现后,React从中断处持续实现工作。然而有时可能会放弃曾经实现的工作,从顶层从新开始。此阶段执行的工作是对用户是不可见的,因而能够实现暂停。然而在commit阶段始终是同步的它会产生用户可见的变动, 例如DOM的批改. 这就是React须要一次性实现它们的起因。

咱们须要删除performUnitOfWork函数中更改DOM的代码。

function performUnitOfWork(nextUnitOfWork) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }

  const elements = fiber.props.children

  // ...

咱们须要保留Fiber树根的援用, 咱们称其为正在工作的root或wipRoot

????️: 在React中Fiber树的根被称为HostRoot。咱们能够在通过容器的DOM节点获取, 容器DOM._reactRootContainer._internalRoot.current

????️: wipRoot相似React源码中workInProgress tree的根节点,在React应用程序中,咱们能够通过容器DOM._reactRootContainer._internalRoot.current.alternate, 获取workInProgress tree的根节点。

let wipRoot = null

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
  }
  nextUnitOfWork = wipRoot
}

实现了所有的工作。咱们须要把整个Fiber树更新到DOM上。咱们须要在commitRoot函数中实现这个性能。

function commitWork(fiber) {
  if (!fiber) {
    return
  }
  const domParent = fiber.parent.dom
  domParent.appendChild(fiber.dom)
  // 递归子节点
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

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

function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    shouldYield = deadline.timeRemaining() < 1
  }
  // 如果nextUnitOfWork为假, 阐明所有的工作都曾经做完了, 咱们须要进入commit阶段
  if (!nextUnitOfWork && wipRoot) {
    // 增加dom
    commitRoot()
  }
}

????️: 在React的源码中commit阶段从completeRoot函数开始,在开始任何工作前,它将FiberRootfinishedWork属性设置为null。

六: 协调

目前为止,咱们仅仅向DOM中增加了内容,然而更新和删除呢?咱们须要将render函数接管到元素和提交到DOM上的最初的Fiber树进行比照。

因而在commit咱们须要保留最初的Fiber树的援用,咱们称之为currentRoot。咱们还将alternate字段增加到每一个Fiber节点上,alternate字段上保留了currentRoot的援用。

????️: 在React源码中,在第一次渲染实现后,React会生成一个Fiber树。该树映射了应用程序的状态,这颗树被称为current tree。当应用程序开始更新时,React会构建一个workInProgress tree, workInProgress tree映射了将来的状态。

????️: 所有的工作都是在workInProgress tree上的Fiber上进行的。当React开始遍历Fiber时,它会为每一个现有的Fiber节点创立一个备份, 在alternate字段中,备份形成了workInProgress tree

let nextUnitOfWork = null
let wipRoot = null
let currentRoot = null

function commitWork(fiber) {
  if (!fiber) {
    return
  }
  const domParent = fiber.parent.dom
  domParent.appendChild(fiber.dom)
  // 递归子节点
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

function commitRoot() {
  commitWork(wipRoot.child)
  // 保留最近一次输入到页面上的Fiber树
  currentRoot = wipRoot
  wipRoot = null
}

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
    alternate: currentRoot,
  }
  nextUnitOfWork = wipRoot
}

接下来咱们须要从performUnitOfWork函数中将创立Fiber的代码提取进去,一个新的reconcileChildren函数。在这里咱们将对currentRoot(当前页面对应的Fiber树)与新元素进行协调。

function reconcileChildren(wipFiber, elements) {
  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字段,child指向了第一个子节点
      wipFiber.child = newFiber
    } else {
      // 同级的Fiber节点增加sibling字段
      prevSibling.sibling = newFiber
    }
​
    prevSibling = newFiber
    index++
  }
}

function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
​
  const elements = fiber.props.children
  reconcileChildren(wipFiber, elements)

  // 首先尝试子节点
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    // 尝试同级节点
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}

咱们同时遍历旧的Fiber树,既wipFiber.alternate,和须要协调的新的元素。如果咱们疏忽遍历链表和数组的模版代码。那么在while循环中,最重要的就是oldFiberelementelement是咱们须要渲染的DOM, oldFiber是上次渲染的Fiber。咱们须要比拟它们,以确定DOM是否须要任何的更改。

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

    // TODO compare oldFiber to element

    // ....

    if (oldFiber) {
      oldFiber = oldFiber.sibling
    }

    if (index === 0) {
      // 父Fiber节点增加child字段,child指向了第一个子节点
      wipFiber.child = newFiber
    } else {
      // 同级的Fiber节点增加sibling字段
      prevSibling.sibling = newFiber
    }
​
    prevSibling = newFiber
    index++
  }
}

为了比拟它们咱们应用以下的规定:

  1. 如果oldFiberelement具备雷同的类型,咱们保留DOM节点,并应用新的props更新
  2. 如果类型不同,并且有新元素。咱们须要创立一个新的DOM节点。
  3. 如果类型不同,存在之前的Fiber,咱们须要移除旧节点
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

    // 判断是否是同类型
    const sameType =
      oldFiber &&
      element &&
      element.type == oldFiber.type

    if (sameType) {
      // 更新节点
    }

    if (!sameType && element) {
      // 新增节点
    }

    if (!sameType && oldFiber) {
      // 删除节点
    }

    if (oldFiber) {
      oldFiber = oldFiber.sibling
    }

    if (index === 0) {
      // 父Fiber节点增加child字段,child指向了第一个子节点
      wipFiber.child = newFiber
    } else {
      // 同级的Fiber节点增加sibling字段
      prevSibling.sibling = newFiber
    }
​
    prevSibling = newFiber
    index++
  }
}

在React中,React应用了key, 能够更好的进行协调,应用key能够检测元素在列表中地位是否扭转,更好的复用节点。

当之前的Fiber和新元素具备雷同的类型时,咱们创立一个新的Fiber节点,保留旧Fiber的DOM节点和元素的props。

并且为Fiber增加了一个新的属性effectTag, 稍后在commit阶段应用

????️: 在React源码中effectTag, effectTag编码的是与Fiber节点相干的effects(副作用)。React中effectTag应用了数字的模式存储,应用了按位或结构了一个属性集。更多内容请查看

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

    // 判断是否是同类型
    const sameType =
      oldFiber &&
      element &&
      element.type == oldFiber.type

    if (sameType) {
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: "UPDATE",
      }
    }

    if (!sameType && element) {
      // 新增节点
    }

    if (!sameType && oldFiber) {
      // 删除节点
    }

    // ...
  }
}

对于新增的节点,咱们在effectTag属性上,应用PLACEMENT标记进行标记。

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

    // 判断是否是同类型
    const sameType =
      oldFiber &&
      element &&
      element.type == oldFiber.type

    // ...

    if (!sameType && element) {
      // 新增节点
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null,
        parent: wipFiber,
        alternate: null,
        effectTag: "PLACEMENT",
      }
    }

    if (!sameType && oldFiber) {
      // 删除节点
    }

    // ...
  }
}

对于须要删除节点,咱们不创立新的Fiber,而是将effectTag设置为DELETION, 并增加到旧的Fiber节点上。

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

    // 判断是否是同类型
    const sameType =
      oldFiber &&
      element &&
      element.type == oldFiber.type

    // ...

    if (!sameType && oldFiber) {
      // 删除节点
      oldFiber.effectTag = "DELETION"
      deletions.push(oldFiber)
    }

    // ...
  }
}

当咱们在commit时, 咱们从新构建的Fiber节点树开始遍历,因为没有须要保留删除的旧节点。所以咱们须要额定应用一个数组deletions保留须要删除的旧节点

????️: 在React的源码,workInProgress tree的Fiber节点领有current tree对应节点的援用。反之亦然。

????️: 在React的源码中,会把所有须要在commit阶段,执行副作用的Fiber节点,构建为线性列表,以不便疾速迭代。迭代线性列表要比迭代树快的多,因为不须要迭代没有side-effects的节点。

let deletions = null

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
    alternate: currentRoot,
  }
  deletions = []
  nextUnitOfWork = wipRoot
}

当咱们进入commit阶段时,应用该数组中的Fiber

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

当初让我批改commitWork函数以解决新的effectTag字段

如果effectTagPLACEMENT, 与之前一样,将DOM增加增加到父节点上

function commitWork(fiber) {
  if (!fiber) {
    return
  }
  const domParent = fiber.parent.dom
  // 对于新增节点的解决
  if (
    fiber.effectTag === "PLACEMENT" &&
    fiber.dom != null
  ) {
    domParent.appendChild(fiber.dom)
  }
  // 递归解决子节点
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

如果effectTagDELETION, 咱们从父节点上删除节点

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 === "DELETION") {
    // 对于删除节点的解决
    domParent.removeChild(fiber.dom)
  }
  // 递归解决子节点
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

如果effectTagUPDATE, 咱们应用新的props更新当初的DOM

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 === "DELETION") {
    // 对于删除节点的解决
    domParent.removeChild(fiber.dom)
  } else if (
    fiber.effectTag === "UPDATE" &&
    fiber.dom != null
  ) {
    // 对于须要更新节点的解决
    updateDom(
      fiber.dom,
      fiber.alternate.props,
      fiber.props
    )
  }
  // 递归解决子节点
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

接下来须要实现updateDom函数

function updateDom(dom, prevProps, nextProps) {
  // TODO
}

咱们应用旧的Fiber的props和新的Fiber的props进行比拟,移除删除的的props,增加或更新已更改的props

// 用于排除children属性
const isProperty = key => key !== "children"
// 用于判断是否更新了属性
const isNew = (prev, next) => key => prev[key] !== next[key]
// 用于判断在新的props上是否有属性
const isGone = (prev, next) => key => !(key in next)

function updateDom(dom, prevProps, nextProps) {
  // 删除之前的属性
  Object.keys(prevProps)
    .filter(isProperty)
    .filter(isGone(prevProps, nextProps))
    .forEach(name => {
      dom[name] = ""
    })
  // 增加或者更新属性
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      dom[name] = nextProps[name]
    })
}

咱们须要对事件监听器做非凡的解决,如果props以on结尾, 咱们应用不同的形式去解决它们

// 判断props是否是on结尾
const isEvent = key => key.startsWith("on")
// 用于排除children属性,和on结尾的属性
const isProperty = key => key !== "children" && !isEvent(key)

如果事件处理程序产生了更改,咱们须要首先删除,而后增加新的处理程序

????️: 间接在DOM上增加事件处理程序的形式,有点相似preact中的解决形式

function updateDom(dom, prevProps, nextProps) {
  Object.keys(prevProps)
    .filter(isEvent)
    .filter(
      key =>
        // 如果事件处理程序产生了更新,获取新的props上没有
        // 须要先删除之前的处理程序
        !(key in nextProps) ||
        isNew(prevProps, nextProps)(key)
    )
    .forEach(name => {
      const eventType = name
        .toLowerCase()
        .substring(2)
      dom.removeEventListener(
        eventType,
        prevProps[name]
      )
    })

  // 删除之前的属性
  Object.keys(prevProps)
    .filter(isProperty)
    .filter(isGone(prevProps, nextProps))
    .forEach(name => {
      dom[name] = ""
    })
  // 增加或者更新属性
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      dom[name] = nextProps[name]
    })

  // 增加事件监听
  Object.keys(nextProps)
    .filter(isEvent)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      const eventType = name
        .toLowerCase()
        .substring(2)
      dom.addEventListener(
        eventType,
        nextProps[name]
      )
    })
}

七: Function 组件

咱们须要增加的下一件事是对Function组件的反对。咱们批改下咱们的例子。

function App(props) {
  return <h1>Hi {props.name}</h1>
}
const element = <App name="foo" />
const container = document.getElementById("root")
render(element, container)

咱们将jsx转换为js

function App(props) {
  return createElement(
    "h1",
    null,
    "Hi ",
    props.name
  )
}
const element = createElement(App, {
  name: "foo",
})

Function组件和DOM次要有两个不同

  1. Function组件的Fiber没有DOM节点
  2. children来自Function, 而不是间接从DOM中间接获取

咱们查看Fiber的类型是否为函数,并依据类型由不同的函数进行解决,如果是不同的DOM,传入updateHostComponent

function performUnitOfWork(fiber) {
  // 判断是不是函数组件
  const isFunctionComponent =
    fiber.type instanceof Function

  if (isFunctionComponent) {
    updateFunctionComponent(fiber)
  } else {
    updateHostComponent(fiber)
  }
  // 接下来返回下一个须要解决的Fiber节点,因为是深度优先遍历,优先从子节点开始
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}

updateHostComponent和咱们之前做的一样

function updateHostComponent () {
  if (!fiber.dom) {
    // 创立dom节点
    fiber.dom = createDom(fiber)
  }
​ // 子元素
  const elements = fiber.props.children
  // 子元素与旧的Fiber进行子协调
  reconcileChildren(wipFiber, elements)
}

updateFunctionComponent运行函数组件获取children。在咱们的例子中App会返回h1元素。一旦有了children, 协调就能够依照之前的形式进行了。不须要进行任何批改。

function updateFunctionComponent () {
  // 获取Function组件的children
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}

上面咱们须要批改commitWork函数。因为咱们的Function组件的Fiber节点没有DOM节点。咱们须要批改两件事。

首先如果要找到DOM节点的父节点,咱们须要顺次向上查找,找到带有DOM节点的Fiber


function commitWork(fiber) {
  if (!fiber) {
    return
  }
  // 父级Fiber
  let domParentFiber = fiber.parent
  // 直到找到含有dom的Fiber节点
  while (!domParentFiber.dom) {
    domParentFiber = domParentFiber.parent
  }
  const domParent = domParentFiber.dom

  // ...
}

在删除节点时,咱们须要向下直到找到含有DOM节点的Fiber

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

function commitWork(fiber) {
  // ...

  if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
    // 解决新增
    domParent.appendChild(fiber.dom)
  } else if (fiber.effectTag === "DELETION") {
    // 解决删除
    commitDeletion(fiber, domParent)
  } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
    // 解决更新
    updateDom(
      fiber.dom,
      fiber.alternate.props,
      fiber.props
    )
  }
  // ...
}

八: hooks

最初一步。目前咱们有了Function组件,当初让咱们增加状态。上面是一个计数器的例子

function Counter() {
  const [state, setState] = Didact.useState(1)
  return (
    <h1 onClick={() => setState(c => c + 1)}>
      Count: {state}
    </h1>
  )
}
const element = <Counter />
onst container = document.getElementById("root")
render(element, container)

咱们应用useState获取和更新计数器的值。在调用函数组件前,咱们须要初始化一些全局变量,以便在useState函数中应用它们。

首先获取正在工作的Fiber,咱们在Fiber节点中增加hooks数组,应用数组的目标是为了反对多个useState。并且援用以后hooks的索引。

// 以后正在工作的Fiber
let wipFiber = null
// 以后Fiber的hooks的索引
let hookIndex = null

function updateFunctionComponent () {
  // 正在工作的Fiber
  wipFiber = fiber
  // 以后hooks的索引默认为0
  hookIndex = 0
  // hooks的汇合
  wipFiber.hooks = []
  // 获取Function组件的children
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}

当组件调用useState时,首先咱们查看是否之前是否有hook,如果存在旧的hook把之前的状态复制到新hook。否则,应用初始值初始化hook。

而后将hook增加到Fiber,并将hook的索引加1

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用于接管action, 用于更新状态。setState会将action推入到hook的队列上。

而后咱们执行与render函数中相似的操作,咱们设置nextUnitOfWork开始进行新的渲染阶段。

function useState(initial) {
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex]
  // 判断之前是否有状态
  const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: [], // 更新队列
  }
  const setState = (action) => {
    // action增加到队列中
    hook.queue.push(action)
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    }
    // 当nextUnitOfWork不为空时,就会进入渲染阶段
    nextUnitOfWork = wipRoot
    deletions = []
  }
  wipFiber.hooks.push(hook)
  hookIndex++
  return [hook.state, setState]
}

????️: 这里简化了setState, setState只接管函数作为参数。

然而目前咱们还没有更新state。在下次渲染组件时,咱们从旧的队列中获取所有action。而后将它们逐个利用到新的hook state上。当咱们返回状态时,state会被更新。

????️: 调用setState,不会立即更新state。而是在进入render阶段后更新state,而后useState会返回新的状态。

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) => {
    // action增加到队列中
    hook.queue.push(action)
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    }
    // 当nextUnitOfWork不为空时,就会进入渲染阶段
    nextUnitOfWork = wipRoot
    deletions = []
  }
  wipFiber.hooks.push(hook)
  hookIndex++
  return [hook.state, setState]
}

咱们曾经建设了好了本人的React。

结语

除了帮忙你了解react是工作原理外,本文的另一个目标是让你在后续可能更轻松深刻React。所以咱们屡次应用了和react源码中一样的函数名以及变量名。

咱们省略了很多了React的优化

  • render阶段遍历整棵树,然而React中会跳过没有任何更改的子树。
  • commit阶段,React会进行线性遍历
  • 目前咱们会每次都创立一个新的Fiber,而React中会复用之前的Fiber节点

还有很多…

咱们还能够持续增加性能,比方:

  1. 增加key
  2. 增加useEffect
  3. 应用对象作为款式的props
  4. children扁平化

参考

  • Build your own React(基于hooks实现)
  • Didact: a DIY guide to build your own React(基于class实现)
  • didact

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理