实现一个mini-react

慕容俭
2023-12-01

首先 说下原理

react自v16以后发生了很多变化,v16以后底层的“虚拟DOM”不再是简单JSON数据了,React采用了最新的Fiber(双向链表)的数据结构,作为“协调”(Diff)运算的基础数据。React背后还提供了强大的 react-reconciler 和 scheduler 库实现Fiber链表的生成、协调与调度。相比vue组件,react在较大组件方面的性能更高。如果要手写一个简易版本的React,其核心要实现以下功能,createElement(用于创建元素)、createDOM/updateDOM(用于创建和更新DOM)、render/workLoop(用于生成Fiber和协调运算)、commitWork(用于提交)等,如果还有支持Hooks,还得封闭Hooks相关的方法。

思路

  • 下载react官网仓库中的代码,搞清楚目录结构、各个包的作用。

  • 下载地址:https://github.com/facebook/react,进一步查看packages目录。

  • react-dom 这是DOM渲染的若干功能。

  • react 这是React核心语法及其API封装的包

  • react-reconciler 用于生成“Fiber树”和“协调运算”的。

  • scheduler,它是模拟requestIdleCallback(fn)的兼容性实现,用于执行复杂的任务,当浏览器主线程有“空闲”时执行这些复杂的任务,不霸占浏览器主线程。

  • 用工程化环境或者HTML页面,引入react.js和react.dom.js,在源码中进行调试学习。

  • 如果采用HTML页面的方式来分析React源码,还要引入babel.js,对JSX语法进行编译,在script标签还要添加 type=‘text/babel’。

  • 慢慢通过删减、调试的方式,把react.js中无用的逻辑都删除,得到一个mini-react。

我们需要什么

实现一个简易react

需要用到babel.min.js
react.development.js
react-dom.development.js

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>React</title>
</head>
<body>

  <div id="app"></div><hr>

  <div id='root'></div>

  <script src='./dist/babel.min.js'></script>
  <script src='./dist/react.development.js'></script>
  <script src='./dist/react-dom.development.js'></script>

  <script type='text/javascript'>
    // 创建了一个React元素
    const app = React.createElement('h1', {title:'testReact'}, 'Hello React')
    ReactDOM.render(app, document.getElementById('app'))
  </script>

  <script type='text/babel'>
    const App = () => {
      const [num, setNum] = React.useState(0)
      return (
        <div>
          <h1>{ num }</h1>
          <button onClick={()=>setNum(num-1)}>自减</button>
          <button onClick={()=>setNum(num+1)}>自增</button>
        </div>
      )
    }
    ReactDOM.render(<App />, document.getElementById('root'))
  </script>

</body>
</html>

首先实现基本的render和react-dom


// return Fiber基础单元 = { type, props: { children } }
// type 表示当前节点的HTML元素名,也有可能是'TEXT-ELEMENT'
// props 表示当前节点的jsx属性,还包括一个特殊属性children
function createElement(type, props, ...children) {
  // 返回一个Fiber单元
  return {
    type,
    props: {
      ...props,
      children: children.map(ele=>(typeof ele==='object') ? ele : createTextElement(ele))
    }
  }
}

// 文本Fiber
function createTextElement(text) {
  return {
    type: 'TEXT_ELEMENT',
    props: {
      nodeValue: text,
      children: []
    }
  }
}


function isProperty (key) {
  return key !== 'children'
}

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

  // 遍历element.props上的非children属性,并将其添加到DOM上
  Object.keys(element.props)
    .filter(isProperty)
    .forEach(attr=>dom[attr]=element.props[attr])

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

  container.appendChild(dom)
}

const React = {
  createElement,
  render
}

引入Fiber思想


// return Fiber基础单元 = { type, props: { children } }
// type 表示当前节点的HTML元素名,也有可能是'TEXT-ELEMENT'
// props 表示当前节点的jsx属性,还包括一个特殊属性children
function createElement(type, props, ...children) {
  // 返回一个Fiber单元
  return {
    type,
    props: {
      ...props,
      children: children.map(ele=>(typeof ele==='object') ? ele : createTextElement(ele))
    }
  }
}

// 文本Fiber
function createTextElement(text) {
  return {
    type: 'TEXT_ELEMENT',
    props: {
      nodeValue: text,
      children: []
    }
  }
}

// 特别注意:如何区分element和fiber呢?
// element上没有link指针,它是孤立的,它纯粹的 {type,props}
// fiber是element添加了link指针后的结果,它会形成与其它fiber的链接关系
function createDom (fiber) {
  const dom =
    fiber.type === 'TEXT_ELEMENT'
      ? document.createTextNode(fiber.props.nodeValue)
      : document.createElement(fiber.type)

  // 遍历element.props上的非children属性,并将其添加到DOM上
  Object.keys(fiber.props)
    .filter(isProperty)
    .forEach(attr=>dom[attr]=fiber.props[attr])

  // 没有遍历fiber.props.children,也没有执行DOM插入操作
  return dom
}

// 相当于你正在封装ReactDOM库(***)
// 入参:element是Fiber单元,container挂载的节点
// 判断一个key是不是非children属性,
function isProperty (key) {
  return key !== 'children'
}

// render(<App/>, document.getElementById('app'))
function render(element, container) {
  // 把root这个工作单元,设置成第一个工作单元(第一个Fiber单元)
  nextUnitOfWork = {
    dom: container,   // 当前element将要挂载的父节点,这相当于是vue.$el
    props: {
      children: [element]  // 这个element相当是APP这个元素
    }
  }
  // 当第一个工作单元被设置完成时。其实此时,浏览器早已准备好了,浏览器要执行requestIdleCallback(workLoop)
}

// --------------------------------------------------------

let nextUnitOfWork = null   // 即将执行的下一个“工作单元”
function workLoop(dealine) {
  let shouldYield = false  // 当它为假时,表示浏览器中断了当前的工作
  while (nextUnitOfWork && !shouldYield) {
    // 如果有下一个事儿(工作单元),并且浏览没有中断当前工作
    // 如果浏览器没有中断当前工作,说明浏览器还是比较“空闲”的

    // 意思:执行当前的工作单元,执行完成后再返回一个新的工作单元
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    // 当nextUnitOfWork为空时,说明没有任务了,停下来
    // 当浏览器主线程比较忙时,也要停下来,这个“停下来”是浏览器强制的
    shouldYield = dealine.timeRemaining() < 1  // 单位是:毫秒
  }
  // 使用reqiuestIdleCallback()实现render的异步工作
  // 因为这个api有兼容性问题,所以react官方自己实现了scheduler调试
  requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)

// 要想让requestIdleCallback()开始工作,我们需要给nextUnitOfWork第一个工作单元。由谁来执行工作呢?由performUnitOfWork执行工作,这个方法不仅仅是执行工作,还要能够返回下一个要执行的工作。
function performUnitOfWork(fiber) {
  // 第一件事:把当前工作单元(Fiber单元)添加到真实的DOM中去
  if (!fiber.dom) fiber.dom = createDom(fiber)
  if (fiber.parent) fiber.parent.dom.appendChild(fiber.dom)

  // 第二件事:遍历当前Fiber单元的孩子们,并给每一个孩子创建一个Fiber
  // element是什么时候变成有指针的Fiber的?答案,就在此刻。
  const elements = fiber.props.children
  let index = 0  // 用于while循环
  let prevSibling = null  // 上一个兄弟节点
  while (index < elements.length) {
    let element = elements[index]
    // 给循环中的这个element添加指针、dom,将其变成真正意义的Fiber单元
    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,  // ? 你添加了parent指针,请求sibling、child指针在哪里?
      dom: null
    }
    if (index === 0) {
      // 当循环第一个子节点时,那么这个子节点其实就是当前fiber的child
      fiber.child = newFiber
    } else {
      // 当循环第二个、第三个。。。子节点时,把上一个节点的sibling指向当前被循环的这个节点
      prevSibling.sibling = newFiber
    }
    // 循环第一个元素时,prevSibling等于null,继续循环时,prevSibling就等于上一次循环那个节点。
    prevSibling = newFiber
    // 不断地循环
    index++
  }
  // 第三件事:根据Fiber工作流程原则,找到下一个工作单元并返回
  // 先孩子节点,如果有,直接返回;如果没有,进入下步
  if (fiber.child) return fiber.child
  // 找下一个兄弟(这个兄弟有可能是当前fiber的下一个兄弟,也可能是祖宗的下一个兄弟)
  let next = fiber
  while (next) {
    if (next.sibling) return next.sibling
    next = next.parent
  }
}

简易实现usestate(模拟实现)


// return Fiber基础单元 = { type, props: { children } }
// type 表示当前节点的HTML元素名,也有可能是'TEXT-ELEMENT'
// props 表示当前节点的jsx属性,还包括一个特殊属性children
function createElement(type, props, ...children) {
  // 返回一个Fiber单元
  return {
    type,
    props: {
      ...props,
      children: children.map(ele=>(typeof ele==='object') ? ele : createTextElement(ele))
    }
  }
}

// 文本Fiber
function createTextElement(text) {
  return {
    type: 'TEXT_ELEMENT',
    props: {
      nodeValue: text,
      children: []
    }
  }
}

// 特别注意:如何区分element和fiber呢?
// element上没有link指针,它是孤立的,它纯粹的 {type,props}
// fiber是element添加了link指针后的结果,它会形成与其它fiber的链接关系
function createDom (fiber) {
  const dom =
    fiber.type === 'TEXT_ELEMENT'
      ? document.createTextNode('')
      : document.createElement(fiber.type)

  // 遍历element.props上的非children属性,并将其添加到DOM上
  updateDom(dom, {}, fiber.props)

  // 没有遍历fiber.props.children,也没有执行DOM插入操作
  return dom
}

// 相当于你正在封装ReactDOM库(***)
// 入参:element是Fiber单元,container挂载的节点

// render(<App/>, document.getElementById('app'))
function render(element, container) {
  // 把root这个工作单元,设置成第一个工作单元(第一个Fiber单元)
  wipRoot = {
    dom: container,   // 当前element将要挂载的父节点,这相当于是vue.$el
    props: {
      children: [element]  // 这个element相当是<APP/>这个元素
    },
    alternate: currentRoot
    // wipRoot正在render那个根Fiber
    // currentRoot 是当前节点的旧Fiber
  }
  deletions = []
  nextUnitOfWork = wipRoot
  // 当第一个工作单元被设置完成时。其实此时,浏览器早已准备好了,浏览器要执行requestIdleCallback(workLoop)
}

// --------------------------------------------------------

let nextUnitOfWork = null   // 即将执行的下一个“工作单元”
let wipRoot = null          // 记录当前正在执行工作单元的root节点
let currentRoot = null      // 它指向上一次已经commit过的那颗“Fiber树”
let deletions = []          // 用于收集每次更新阶段中即将要删除的Fiber节点

function workLoop(dealine) {
  let shouldYield = false  // 当它为假时,表示浏览器中断了当前的工作
  while (nextUnitOfWork && !shouldYield) {
    console.log('render start')
    // 如果有下一个事儿(工作单元),并且浏览没有中断当前工作
    // 如果浏览器没有中断当前工作,说明浏览器还是比较“空闲”的
    console.log('nextUnitOfWork', nextUnitOfWork)
    // 意思:执行当前的工作单元,执行完成后再返回一个新的工作单元
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    console.log('nextUnitOfWork', nextUnitOfWork)
    // 问题:这个while循环将一直工作,那什么时候能停下来了?
    // 当nextUnitOfWork为空时,说明没有任务了,停下来
    // 当浏览器主线程比较忙时,也要停下来,这个“停下来”是浏览器强制的
    shouldYield = dealine.timeRemaining() < 1  // 单位是:毫秒
  }
  // 什么时候该commit()提交更新真实DOM呢?
  if (!nextUnitOfWork && wipRoot) {
    console.log('commit start')
    commitRoot()
  }
  // 使用reqiuestIdleCallback()实现render的异步工作
  // 因为这个api有兼容性问题,所以react官方自己实现了scheduler调试
  requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)

// 要想让requestIdleCallback()开始工作,我们需要给nextUnitOfWork第一个工作单元。由谁来执行工作呢?由performUnitOfWork执行工作,这个方法不仅仅是执行工作,还要能够返回下一个要执行的工作。
function performUnitOfWork(fiber) {
  // 判断是不是函数式组件
  const isFunctionComponent = fiber.type instanceof Function
  console.log('isFunctionComponent', isFunctionComponent)
  if (isFunctionComponent) {
    console.log('---function')
    updateFunctionComponent(fiber)
  } else {
    console.log('---host')
    updateHostComponent(fiber)
  }

  // 第三件事:根据Fiber工作流程原则,找到下一个工作单元并返回
  // 先孩子节点,如果有,直接返回;如果没有,进入下步
  if (fiber.child) return fiber.child
  // 找下一个兄弟(这个兄弟有可能是当前fiber的下一个兄弟,也可能是祖宗的下一个兄弟)
  let next = fiber
  while (next) {
    if (next.sibling) return next.sibling
    next = next.parent
  }
}

function commitRoot() {
  // 删除那些需要移除的Fiber所对应的DOM节点
  deletions.forEach(commitWork)
  // add nodes to dom
  // wipRoot在这里就相当是#app所对应的节点
  // wipRoot.child 这个代表就是<App/>这个Fiber节点
  commitWork(wipRoot.child)
  // 为了执行协调运算,在这里我们要记录这个被commit过的Fiber树
  currentRoot = wipRoot
  // 当DOM渲染更新完成时,清除掉wipRoot
  wipRoot = null
}

// 为了兼容考虑React组件的更新阶段,在这时我们不能“一刀切”地更新DOM。
// 要根据 effectTag 这个标记,来分门别类地渲染或更新DOM.
function commitWork(fiber) {
  if (!fiber) return

  // 向上寻找非函数式Fiber节点来挂载当前Fiber节点
  let parent = fiber.parent  // 这个parent节点可能是函数式的Fiber节点
  while (!parent.dom) {
    parent = parent.parent
  }
  const $el = parent.dom

  // 因为在下面用到了三种标记,这些标记是由“协调运算”添加的
  // 所以,如果下面不区分装载阶段和更新阶段,我们应该在commitWork之前已经完成了协调运算。

  // 新增了DOM节点
  if (fiber.effectTag === 'PLACEMENT' && fiber.dom !== null) {
    $el.appendChild(fiber.dom)
  } else if (fiber.effectTag === 'DELETION') {
    // 删除Fiber节点时,也需要找到一个拥有dom的节点,而不是函数式的Fiber节点
    commitDeletion(fiber, $el)
  } else if (fiber.effectTag === 'UPDATE' && fiber.dom !== null) {
    // 如果oldFiber.effectTag='UPDATE',只是说明这个节点的属性有可能变了
    // 所以,这里我们进一步遍历fiber.props来找出最小变化差异点。
    // 这个udpateDom方法,用于对比当前fiber节点上props的变化
    updateDom(fiber.dom, fiber.alternate.props, fiber.props)
  }

  // 递归插入DOM节点
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

function commitDeletion(fiber, $el) {
  if (fiber.dom) {
    // 删除非函数式的Fiber节点
    $el.removeChild(fiber.dom)
  } else {
    // 删除函数式组件的Fiber,向下寻找可移除的DOM节点
    commitDeletion(fiber.child, $el)
  }
}

// 协调运算(在装载阶段、更新阶段都执行)
function reconcileChildren(fiber, elements) {
  // 约定:element表示当前节点,oldFiber表示是element所对应的旧Fiber
  let index = 0
  // 此时这个oldFiber就是fiber所对应的旧Fiber树中的第一个子Fiber
  // console.log('reconcileChildren oldFiber', fiber)
  let oldFiber = fiber.alternate && fiber.alternate.child  // elements[0]
  // 如果fiber.alternate不存在,说明此时是装载阶段,所有孩子的标记都就是PLACEMENT

  let prevSibling = null  // 表示上一个兄弟节点
  // bug:如果循环没进去,就无法创建Fiber,也不能添加标记。所以commit就没有反应。
  while (index < elements.length || oldFiber !== null) {
    const element = elements[index]
    // 说明:newFiber是即将为element创建的Fiber
    let newFiber = null
    // 判断当前element和oldFiber的type是否相同
    const sameType = (oldFiber && element && oldFiber.type === element.type)
    // 对比一:准备更新这个节点的属性
    if (sameType) {
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        alternate: oldFiber,
        parent: fiber,
        effectTag: 'UPDATE'  // 这个标记是用于commit阶段的
      }
    }
    // 对比二:element是新增的节点
    if (element && !sameType) {
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null,
        alternate: null,
        parent: fiber,
        effectTag: 'PLACEMENT'
      }
    }
    // 对比三:oldFiber被删除了
    if (oldFiber && !sameType) {
      oldFiber.effectTag = "DELETION"
      // 把即将要删除的oldFiber放在一个全局的数组中去
      deletions.push(oldFiber)
    }

    // 执行循环
    if (oldFiber) {
      oldFiber = oldFiber.sibling
    }
    if (index === 0) {
      fiber.child = newFiber
    } else if (element) {
      prevSibling.sibling = newFiber
    }
    prevSibling = newFiber
    index++
  }
}

// 判断一个属性是不是事件属性
const isEvent = key => key.startsWith('on')
// 判断一个key是不是非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)
// 用于对比、更新当前dom节点的props
function updateDom(dom, prevProps, nextProps) {
  // 把新增的属性,更新到dom上
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      dom[name] = nextProps[name]
    })
  // 把移除的属性,从dom上清除
  Object.keys(prevProps)
    .filter(isProperty)
    .filter(isGone(prevProps, nextProps))
    .forEach(name => {
      dom[name] = ""
    })
  // 把旧的事件处理器,从dom上解绑
  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]
      )
    })
  // 把新的事件处理器,绑定到dom上
  Object.keys(nextProps)
    .filter(isEvent)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      const eventType = name
        .toLowerCase()
        .substring(2)
      dom.addEventListener(
        eventType,
        nextProps[name]
      )
    })
}


function updateHostComponent(fiber) {
  console.log('---fiber', fiber)
  // 第一件事:把当前工作单元(Fiber单元)添加到真实的DOM中去
  if (!fiber.dom) fiber.dom = createDom(fiber)

  // 第二件事:遍历当前Fiber单元的孩子们,并给每一个孩子创建一个Fiber
  // element是什么时候变成有指针的Fiber的?答案,就在此刻。
  const elements = fiber.props.children
  // 协调运算,把孩子们从element变成fiber,添加标记给commit阶段使用
  console.log('elements', elements)
  reconcileChildren(fiber, elements)
}

function updateFunctionComponent(fiber) {
  wipFiber = fiber
  hookIndex = 0
  // wipFiber.hooks = [{},{}]
  wipFiber.hooks = []  // 放的是num这个state所对应的hook数据
  // 如果是函数式组件,要调用它才能得到JSX(即HostComponent)
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}

// 这个数据结构的设计,和真实React底层的那个Hooks数据结构有很大的出入
// 结论:这里只是模拟,以方便大家理解Hooks原理
let wipFiber = null
let hookIndex = null
// const [num, setNum] = useState(100)
// const [name, setName] = useState('周延')
function useState(initial) {
  // 每次有人调用useState时,我们先判断React底层是否已经存在这个hook,如果存在就取底层中缓存的state值返回给组件;如果不存在,我创建hook并将缓存在React的底层。
  const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex]
  // const hook = {state: 100, queue:[]}
  const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: []
  }
  const actions = oldHook ? oldHook.queue : []
  console.log('actions', actions)
  actions.forEach(action=>{
    console.log('action', action)
    console.log('new state', action(hook.state))
    if (typeof action === 'function') {
      hook.state = action(hook.state)
    } else {
      console.log('action type', typeof action)
    }

  })
  // 专门用于更新state变量,并设置第一个工作单元,以触发render阶段。。
  function setState(action) {
    // debugger
    console.log('setState', typeof action, action)
    console.log('hook.state', hook.state)
    hook.queue.push(action)  // 只是把更新state的方法放进队列,并没有调用
    // 还要触发“风吹草动”,那该怎么触发“更新阶段”?
    // 答案:设置第一个工作单元,这样requestIdleCallback()会自动工作
    console.log('wipFiber', wipFiber)
    console.log('wipRoot', wipRoot)
    console.log('currentRoot', currentRoot)
    // 触发更新。。
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    }
    nextUnitOfWork = wipRoot
    deletions = []
  }
  wipFiber.hooks.push(hook)
  hookIndex++
  return [hook.state, setState]
}

const React = {
  createElement,
  render,
  useState
}

 类似资料: