实现一个mini-react

Posted 我是真的不会前端

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实现一个mini-react相关的知识,希望对你有一定的参考价值。

首先 说下原理

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) 
  以上是关于实现一个mini-react的主要内容,如果未能解决你的问题,请参考以下文章

实现一个mini-react

React 18发布,仅用400行代码就能实现一个Mini-React

mini-react新版本stack架构

jQuery 利用 parent() parents() 寻找父级 或祖宗元素

你可能这个夏天都看不到祖宗布!合约阶梯又填新比赛!AOP为何突然消失!

“图灵测试不重要”,一个违背机器人界祖宗的决定