【译】build your own react - 知识搬运工

·  阅读 4144
【译】build your own react - 知识搬运工

写在前头

最近在学习 react 的源码,但是内,没有什么头绪,也不知道从何看起,一行一行的阅读源码也不是个办法。偶然间 从掘金上看到一篇 React源码揭秘1 架构设计与首屏渲染,发现 pomb 大神的一篇 build-your-own-react 构建一个你自己的 react,迅速激起了了我的兴趣,他从简单的 createElement 函数讲起,然后一步一步带领大家,很详细通俗的讲解了 Fiber 、并发模式等晦涩的概念,最终实现一个 react,此文章是我读完之后的一个大致总结,作为一个知识搬运工,再将我消化的知识以更简洁易懂的方式分享给大家

Step 1 预备知识 JSX 是如何被解析的 (createElement)

如下是一段 简单的 jsx 语法

const element = <h1 title="foo">Hello</h1>

复制代码

它会 被 babel 解析为 如下代码


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

复制代码

接下来我们来实现一个简单的 createElement ,用它来生成 虚拟DOM


function createElement(type, props, ...children) {
  return {
    // 标记 元素类型
    type,
    // 元素的属性
    props: {
      ...props,
      // 子元素
      children: children.map(child =>
        // 为了区分 基本类型 和引用类型,我们单独 用 createTextElement 来创造 文本节点
        typeof child === "object"
          ? child
          : createTextElement(child)
      ),
    },
  }
}
​
function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  }
}


复制代码

Step 2 我们需要 render 到真实的 dom 节点上

function render(element, container) {
  const dom =
    element.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type)
​
  // 排除 特殊属性 "children"
  const isProperty = key => key !== "children"

  // 将元素属性 一一 写入 dom 节点上
  Object.keys(element.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = element.props[name]
    })

​  // 遍历递归 将 子元素 一个一个 都 附到 真实的 dom 节点上
  element.props.children.forEach(child =>
    render(child, dom)
  )
​
  // 最后挂载到 指定的 dom 节点容器上  
  container.appendChild(dom)
}

复制代码

Step 3 并发模式 (Concurrent Mode)

到目前为止,我们好像差不多已经实现另一个 简版的 React 了,可以把 JSX 渲染到 dom 上了,但是有一个问题

就是 我们的 render 函数 是使用 递归 来实现 patch 到 dom 上的,如果我们的 节点层级很大,节点很多的话,可能就会长时间占用浏览器进程,造成阻塞,影响浏览器更高优先级的事务处理(比如说,用户的输入和 ui 交互)

因为 我们需要 把这个大的任务 切割 分为 多个 小的 工作单元,这样的话,如果浏览器有更高优先级的事务处理,我们就可以中断 react 元素的渲染,这我们引入一个概念,称它为 “并发模式”

image


let nextUnitOfWork = nullfunction workLoop(deadline) {
 // 是否要暂停
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    // 执行 一个工作单元 并返回  下一个工作单元
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    // 判断空闲时间是否足够
    shouldYield = deadline.timeRemaining() < 1
  }
  requestIdleCallback(workLoop)
}
​
requestIdleCallback(workLoop)
​
function performUnitOfWork(nextUnitOfWork) {
  // TODO
}
复制代码

image

Step 4 什么是 Fiber ,为何我们需要它

什么是 Fiber (这里引用 React技术揭秘 的解释)

image

Fiber 的结构

image

上图是一棵 Fiber 树。为了组织工作单元,我们需要一个 数据结构,每个元素都有 一个 filber 结构,每个 fiber 都是 一个 工作单元

每个工作单元是如何工作的内,下面的函数 performUnitOfWork里 ,主要做了三件事:

  1. 把元素添加到 dom 中
  2. 为元素的子元素都创建一个 fiber 结构
  3. 找到下一个工作单元


function performUnitOfWork(fiber) {
  // 创建一个 dom 元素,挂载到 fiber 的 dom 属性
  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
  // 保存 上一个 sibling fiber 结构
  let prevSibling = nullwhile (index < elements.length) {
    const element = elements[index]
​
    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null,
    }
​    // 第一个子元素 作为 child,其余的 子元素 作为 sibling
    if (index === 0) {
      fiber.child = newFiber
    } else {
      prevSibling.sibling = newFiber
    }
​    
    prevSibling = newFiber
    index++
  }
​  // step1 如果 有 child fiber ,则返回 child
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber

  while (nextFiber) {
    // step2 如果 有 sibling fiber ,则返回 sibling
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    // step3 ,否则 返回 他的 parent fiber
    nextFiber = nextFiber.parent
  }
}
​


复制代码

Step 5 Render 和 Commit 阶段都做了什么

我们在 上面的 performUnitOfWork 里 ,每次都把 元素 直接 添加到 dom 上,这里 会有一个问题,就是 浏览器 随时都有可能中断我们的操作,这样呈现给用户的就是 一个 不完整的 UI,所以 我们需要 做出些改动,就是 所有工作单元执行完后,我们再一并进行 所有 dom 的 添加


function commitRoot() {
  // TODO add nodes to dom
}


function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    shouldYield = deadline.timeRemaining() < 1
  }
​  // 所有工作单元都执行完后,我们一并 进行 提交 操作,commitRoot 里进行所有元素 往 dom 树 上添加的动作
  if (!nextUnitOfWork && wipRoot) {
    commitRoot()
  }
​
  requestIdleCallback(workLoop)
}
复制代码

Step 6 Reconciliation 协调阶段

到目前为止,我们只处理 添加 dom 的情况,那么 update 和 remove dom 的情况 该怎么办内,这个时候,我们就需要在 commit 阶段完成后,用 一个 变量来保存旧的 fiber 树(称为 currentRoot) 来 和 当前(WipRoot: Work in progress )要修改的 fiber 树 进行比较,我们还在 每个 wipRoot 上新增一个 属性 alternate 用来 链接 旧的 fiber 树(上一次 commit 后的 )


function commitRoot() {
  commitWork(wipRoot.child)
  // commit 阶段完成后,保存当前 fiber 树
  currentRoot = wipRoot
  wipRoot = null
}
​

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
    // 和上一次的 commit 阶段的 旧 fiber 树建立连接
    alternate: currentRoot,
  }
  nextUnitOfWork = wipRoot
}
​
let currentRoot = null

复制代码

image

这里的比较规则如下:

  1. 如果旧的 fiber 元素 和新元素具有相同的类型,那么再进一步进行比较 他们的 属性
  2. 如果类型不同,并且有一个新元素,则需要创建一个新的DOM节点
  3. 如果类型不同,并且有一个旧 fiber 元素,则移除旧的节点 这里React也使用 key 进行比较。例如,它检测到子元素在元素数组中的位置发生了变化。

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

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",
      }
    }
    // 类型不同,但是 新 fiber 元素存在,则进行 新增(新增新的 fiber)
    if (element && !sameType) {
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null,
        parent: wipFiber,
        alternate: null,
        effectTag: "PLACEMENT",
      }
    }
    // 类型不同,但是 旧 fiber 树存在,则进行 移除 (先收集起来,在 commit 阶段一并移除)
    if (oldFiber && !sameType) {
      oldFiber.effectTag = "DELETION"
      deletions.push(oldFiber)
    }
    // 下个循环 对 兄弟 fiber 进行比较 (和 下面的  i++ 一个道理)
    if (oldFiber) {
      oldFiber = oldFiber.sibling
    }
    // 如果是 第一个 子元素,则把 新的 fiber 挂到 wipFiber 的  child 属性上
    if (index === 0) {
      wipFiber.child = newFiber
    } else if (element) {
      // 其他的 子元素 ,挂到 上一个子元素的 sibling 属性上
      prevSibling.sibling = newFiber
    }

    prevSibling = newFiber
    index++
  }
}


复制代码

reconcile 协调阶段完成后,我们进入commit阶段

function commitRoot() {
  // 移除 刚才收集的 旧节点
  deletions.forEach(commitWork)
  // commit 当前 wipRoot 的 child 元素
  commitWork(wipRoot.child)
  // 改变当前 root 指向
  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
  ) {
   // 更新 dom 的 属性(新增新属性和移除旧属性) 及 事件的添加和移除处理
    updateDom(
      fiber.dom,
      fiber.alternate.props,
      fiber.props
    )
  } else if (fiber.effectTag === "DELETION") {
    domParent.removeChild(fiber.dom)
  }

  commitWork(fiber.child)
  commitWork(fiber.sibling)
}
复制代码

接下来处理 更新 dom的 操作

// 事件属性
const isEvent = key => key.startsWith("on")
// 除 事件属性 和 特殊属性 children 外的属性
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(dom, prevProps, nextProps) {
  // 移除旧事件
  Object.keys(prevProps)
    .filter(isEvent)
    .filter(
      key =>
        !(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]
      )
    })
}

复制代码

Step 7 函数式组件 Function Components

接下来,我们来实现 函数式组件


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

复制代码

函数式组件有两点不同,如下:

  1. 函数式组件没有 dom 节点 ?
  2. 他的 children 属性 不在 props 上,而是 他的返回值

那么我们需要改动如下


function performUnitOfWork(fiber) {
  const isFunctionComponent =
    fiber.type instanceof Function
  // 区分 函数式组件
  if (isFunctionComponent) {
    updateFunctionComponent(fiber)
  } else {
    updateHostComponent(fiber)
  }
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}
​
function updateFunctionComponent(fiber) {
  // 执行函数式组件获取到 children 
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}
​
function updateHostComponent(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
  reconcileChildren(fiber, fiber.props.children)
}

复制代码

然后 commitWork 也要改动下:

function commitWork(fiber) {
  if (!fiber) {
    return
  }
​
  let domParentFiber = fiber.parent
 // 递归找到 含有 dom 节点的 元素
  while (!domParentFiber.dom) {
    domParentFiber = domParentFiber.parent
  }
  const domParent = domParentFiber.domif (
    fiber.effectTag === "PLACEMENT" &&
    fiber.dom != null
  ) {
    domParent.appendChild(fiber.dom)
  } 
  // ignore

  else if (fiber.effectTag === "DELETION") {
    commitDeletion(fiber, domParent)
  }

  // ignore
 
}

function commitDeletion(fiber, domParent) {
  if (fiber.dom) {
    domParent.removeChild(fiber.dom)
  } else {
    // 删除节点,直到有 dom 节点的元素为止
    commitDeletion(fiber.child, domParent)
  }
}

复制代码

Step 8 Hooks 钩子

最后一步,让我们给函数式组件增加 状态(state)

// 保存当前的 fiber
let wipFiber = null
// 保存当前执行 hook 的索引,区分每次执行是哪个 hook 
let hookIndex = nullfunction updateFunctionComponent(fiber) {
  wipFiber = fiber
  hookIndex = 0
  wipFiber.hooks = []
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}
复制代码

接下来实现 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 => {
    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,虽然 和 React 实际上的代码 有所出入,但是可以更方便的帮助大家理解源码,想进一步研究和阅读源码,推荐大家看看 卡颂 大佬总结的 React技术揭秘 (react.iamkasong.com/)

原文:pomb.us/build-your-…
译文:github.com/Yangfan2016…
本文涉及到的源码:codesandbox.io/s/didact-8-…

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改