如何完成一个简易的React

86 阅读3分钟

前言

在官方文档中有这样的描述:用于构建用户界面的 JavaScript 库。在武侠中,侠客都会有兵器,有的拿剑,有的拿刀。在我们前端中也一样,有的手中Vue行天下,而我们是腰配React仗天涯。我们想让自己的兵器有杀伤力,就得及时多多磨剑,了解自己的武器,你对了解你的武器(React)吗?

我的400行React代码地址:

github.com/codingJJJ/m…

React是如何构建用户界面的?

  • 通过虚拟DOM,下面我们通过几行简短的代码来看怎么通过jsx来构建一个真实DOM
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <script src="https://cdn.bootcdn.net/ajax/libs/babel-standalone/7.0.0-beta.3/babel.js"></script>
  <title>Document</title>
</head>

<body>
  <div id="root"></div>
  <script>
    // babel在编译时候会调用React.createElement方法
    // 通过createElement定义虚拟DOM
    const createElement = (type, props, ...children) => {
      return {
        type,
        props,
        children
      }
    }
    window.React = { createElement }
  </script>
  <script type="text/babel">
    // 创建一个虚拟DOM
    const element = <ul>
      <li>1</li>
      <li><b>2</b></li>
      <li>3</li>
    </ul>

    console.log(element); // 虚拟dom

    // 虚拟DOM渲染函数
    function render(element, root) {
      if (typeof element === "string" || typeof element === "number") {
        const text = document.createTextNode(element)
        root.appendChild(text)
      } else if (typeof element === 'object') {
        const { type, children } = element;
        const dom = document.createElement(type)
        children && children.forEach((ele) => {
          render(ele, dom)
        })
        root.appendChild(dom)
      }
    }

    render(element, root)
  </script>
</body>

</html>

上面,我们用了一个简单的递归方式构建出了一个界面,但是它有什么缺点吗? 答案:有,我们知道我们一个页面是单线程的方式运行的,如果当jsx比较大,层次比较深时,会影响用户的交互。我们先看一张图

image.png 浏览器通常是以帧的方式去绘制页面,如果当我们的js运行时间过长,超过了帧时长,我们会有明显的卡顿感,甚至还无法完成用户交互,所以为了优化这种渲染方式,React采用了Fiber架构。

  • 什么是fiber? ReactFiber其实是一种数据结构,我们可以理解一个Fiber节点对应一个React节点。fiber是react中最小的执行单元Unit

image.png

image.png 我们发现在fiber中有三根指针,return指向父节点,child指向子节点,sibling指向兄弟节点 其中还有2个节点alternate和stateNode两个指针,stateNode指向当前fiber对应的真实dom,alternate指向的另一颗fiber树的对应fiber节点(下面会介绍)。 这样做有什么好处呢? 避免递归,把执行变成一个异步可中断的dom更新。 是如何实现的呢?

let nextUnitWork
let currentTime = new Date().getTime()

// 判断当前是否有时间执行 有则返回true 没有就返回false
function shouldYield() {
  if (new Date().getTime() - currentTime > 60) {
    currentTime = new Date().getTime()
    return false
  } else {
    return true
  }
}

// 执行工作单元
function performUnitOfWork() {
  // 做一些操作

  // 操作完后将下一个单元交给nextUnitWork
  nextUnitWork = nextUnitWork.nextFiber
}

function workLoopConcurrent() {
  while (nextUnitWork !== null && !shouldYield()) {
    performUnitOfWork(nextUnitWork);
  }
  // 如果还存在需要执行的单元,返回该函数
  if (nextUnitWork) {
    return workLoopConcurrent
  } else {
    null
  }
}
// 开始工作 只能工作60ms
function work(loop) {
  const fn = loop()
  if (typeof fn === 'function') {
    setTimeout(() => {
      work()
    }, 30)
  }
}

work(workLoopConcurrent)

我们看下当我们使用异步可中断的更新后,React的调用栈

image.png 在React官网中,有这样一句话:我们认为,React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式,我们可以看到React在快速响应上做出的努力。 既然我们可以渲染出DOM,怎么更新DOM呢? 双缓存 在React中存在两颗Fiber树,分别是currentFiber和workInprogress Fiber两棵树,currentFiber表示当前页面的fiber树,workInprogress表示更新时,新生成的fiber树。他们各自的fiber节点通过alternate链接。 在mount时,我们会构建fiber树,并通过fiber树生成节点,再提交时,将该fiber树变成currentFiber。 在update时,我们会同样新生成一颗workInprogress fiber树,它根据diff算法来区分是否可以复用该fiber,精确到我们只操作变了的dom部分。 image.png

将碎片化的知识整理一下React的工作流程

render阶段 创建根fiber 遍历element生成fiber树 并收集EffectTag

commit阶段 根据EffectTag更新dom 执行生命周期 将currnetRoot指向工作树

如何学习React源码

  • 1.首先得熟悉使用React库,了解React的基本概念
  • 2.了解一些基础的数据结构算法,如链表,最小堆,二进制运算
  • 3.避免直接入手源码,可以按照某一个功能点去看源码
  • 4.下载React代码,并手动针对特定的功能模块调试
  • 5.推荐学习文档:卡松React技术揭秘:react.iamkasong.com/