实现 mini-react

97 阅读12分钟

基于vite搭建项目

pnpm create vite

因为 vite 有解析 jsx 文件的功能,为了能实现 jsx 的解析,所以使用 vite 创建项目

最简实现-v1

  1. 创建 dom 节点,设置 id
  2. 把 dom 添加到 #root 容器
  3. 创建 textNode,设置 nodeValue
  4. 把 textNode 添加到 dom 容器
// main.js
const dom = document.createElement('div')
dom.id = 'app'
document.querySelector('#root').append(dom)
const textNode = document.createTextNode('')
textNode.nodeValue = 'app'
dom.append(textNode)
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <!-- 修改默认的 id 为 `root` -->
    <div id="root"></div>
    <script type="module" src="/main.js"></script>
  </body>
</html>

最简实现-v2

改进:实现动态创建

  1. 动态创建 dom - render 实现 dom 的渲染
  2. 动态创建 vdom - createElemet、createTextNode
// main.js
function render(el, container) {
  // 1. 创建 dom
  const dom = el.type === 'TEXT_ELEMENT'
      ? document.createTextNode('')
      : document.createElement(el.type)

  // 2. 处理 props
  Object.keys(el.props).forEach((key) => {
    if (key !== "children") {
      dom[key] = el.props[key]
    }
  })

  // 递归 children
  const children = el.props.children
  children.forEach(child => {
    render(child, dom)
  })

  // 添加 dom 到容器
  container.append(dom)
}

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

// 扩展运算符`...children`处理多个 child
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      // 考虑 child 是字符串的情况
      children: children.map((child) =>
        typeof child === "string" ? createTextNode(child) : child
      ),
    },
  }
}

const App = createElement(
  "div",
  { id: "app" },
  "Hello ",
  createTextNode("App")
)

render(App, document.querySelector('#root'))
  1. 实现 ReactDom API
  • core/React.js
// core/React.js
function render(el, container) {
  // 1. 创建 dom
  const dom = el.type === 'TEXT_ELEMENT'
      ? document.createTextNode('')
      : document.createElement(el.type)

  // 2. 处理 props
  Object.keys(el.props).forEach((key) => {
    if (key !== 'children') {
      dom[key] = el.props[key]
    }
  })

  // 3. 递归 children
  const children = el.props.children
  children.forEach(child => {
    render(child, dom)
  })

  // 4. 添加 dom 到容器
  container.append(dom)
}

// 动态创建 text 类型 vdom
function createTextNode(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  }
}

// 动态创建标签类型 vdom
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) =>
        typeof child === "string" ? createTextNode(child) : child
      ),
    },
  }
}

const React = {
  render,
  createElement,
}

export default React
  • core/ReactDom.js

实现 ReactDom API,实现 dom 添加到 html 页面

import React from './React.js'

const ReactDom = {
  createRoot: function(container) {
    return {
      render(App) {
        React.render(App, container)
      }
    }
  }
}

export default ReactDom
  • App.js
// App.js
import React from './core/React.js'

const App = React.createElement(
  'div',
  { id: 'app' },
  'Hello ',
  'App'
)

export default App
  • main.js
// main.js
import ReactDom from './core/ReactDom.js'
import App from './App.js'

ReactDom.createRoot(document.querySelector('#root')).render(App)

实现 jsx

// App.jsx
import React from './core/React.js'

const App = <div>Hello App jsx</div>

export default App
// main.js
import ReactDom from './core/ReactDom.js'
import App from './App.jsx'

ReactDom.createRoot(document.querySelector('#root')).render(App)

fiber 架构

使用 requestIdleCallback 实现,利用浏览器空闲时间处理 dom

// main.js
let nextWorkOfUnit = null

function render(el, container) {
  nextWorkOfUnit = {
    dom: container,
    props: {
      children: [el],
    }
  }
}

function workLoop(deadline) {
  let shouldYeild = false

  while(!shouldYeild && nextWorkOfUnit) {
    nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit) // TODO

    shouldYeild = deadline.timeRemaining() < 1
  }

  requestIdleCallback(workLoop)
}

// 利用浏览器空闲时间处理 dom
requestIdleCallback(workLoop)
// main.js
function performWorkOfUnit(fiber) {
  // 创建 dom
  if(!fiber.dom) {
    const dom = (fiber.dom = fiber.type === 'TEXT_ELEMENT'
      ? document.createTextNode('')
      : document.createElement(fiber.type))

    // 🌟 添加 dom 到 容器
    fiber.parent.dom.append(fiber.dom)

    // 处理 props
    Object.keys(fiber.props).forEach((key) => {
      if (key !== 'children') {
        dom[key] = fiber.props[key]
      }
    })
  }

  // 处理 children
  const children = fiber.props.children
  let prevChild = null
  children.forEach((child, index) => {

    let newFiber = {
      type: child.type,
      props: child.props,
      dom: null,
      parent: fiber,
      child: null,
      sibling: null,
    }

    // 初始化 child、sibling
    if(index === 0) {
      fiber.child = newFiber // 首先指向第一个孩子节点
    } else {
      prevChild.sibling = newFiber // 如果有多个孩子节点,就接着连接到 child 的后面,用 sibling 表示兄弟节点
    }
    prevChild = newFiber
  })

  // 链表指针指向下一个节点
  if(fiber.child) {
    return fiber.child
  }
  
  // 如果没有 child,则指向兄弟节点
  if(fiber.sibling) {
    return fiber.sibling
  }
  
  // 向上找父节点的兄弟节点
  return fiber.parent?.sibling
}

实现统一提交

在完成整个链表的转换后,统一提交,从root开始

let nextWorkOfUnit = null
let wipRoot = null // 整个应用树的根节点

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

// 不在这里添加dom
function performWorkOfUnit(fiber) {
  // 创建 dom
  if(!fiber.dom) {
    const dom = (fiber.dom = fiber.type === 'TEXT_ELEMENT'
      ? document.createTextNode('')
      : document.createElement(fiber.type))

    // // 添加 dom 到 容器
    // fiber.parent.dom.append(fiber.dom)

    // 处理 props
    Object.keys(fiber.props).forEach((key) => {
      if (key !== 'children') {
        dom[key] = fiber.props[key]
      }
    })
  }
  
  // 省略...
}

// 改为在完成整个链表结构转换后,统一添加
function commitRoot() {
  commitWork(wipRoot.child)
  wipRoot =  null
}

function commitWork(fiber) {
  if(!fiber) return

  fiber.parent.dom.append(fiber.dom)

  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

function workLoop(deadline) {
  let shouldYeild = false

  while(!shouldYeild && nextWorkOfUnit) {
    nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit)

    shouldYeild = deadline.timeRemaining() < 1
  }

  // 完成所有节点的链表结构转换后,统一添加
  if(!nextWorkOfUnit && wipRoot) {
    commitRoot()
  }

  requestIdleCallback(workLoop)
}

实现 function component

funtion component 的 type 为一个函数,所以可以调用 type(),返回函数组件的 children,但是需要放到一个数组里面,保持数据结构一致

主要关注以下几点:

1. 区分函数组件、普通标签

创建 DOM 的条件:函数组件不需要创建 dom

function performWorkOfUnit(fiber) {
  const isFunctionComponent = typeof fiber.type === 'function'

  if(isFunctionComponent) {
    updateFunctionComponent(fiber)
  } else {
    updateHostComponent(fiber)
  }

  // 链表指针指向下一个节点
  if(fiber.child) {
    return fiber.child
  }

  if(fiber.sibling) {
    return fiber.sibling
  }

  return fiber.parent?.sibling
}
function updateFunctionComponent(fiber) {
  // 函数组件不需要创建 dom
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}

function updateHostComponent(fiber) {
  // 创建 dom
  if(!fiber.dom) {
    const dom = (fiber.dom = createDom(fiber))

    // 处理 props
    Object.keys(fiber.props).forEach((key) => {
      if (key !== 'children') {
        dom[key] = fiber.props[key]
      }
    })
  }

  const children = fiber.props.children
  reconcileChildren(fiber, children)
}


function createDom(fiber) {
  return fiber.type === 'TEXT_ELEMENT'
      ? document.createTextNode('')
      : document.createElement(fiber.type)
}

2. 处理 children

都有相同的 children 处理逻辑,抽取为 reconcileChildren

function reconcileChildren(fiber, children) {
  let prevChild = null

  children.forEach((child, index) => {
    let newFiber = {
      type: child.type,
      props: child.props,
      dom: null,
      parent: fiber,
      child: null,
      sibling: null,
    }

    if(index === 0) {
      fiber.child = newFiber
    } else {
      prevChild.sibling = newFiber
    }
    prevChild = newFiber
  })
}

3. 处理函数组件没有 dom 属性无法 append 的问题

while 循环向上寻找有 fiber.dom 的节点进行 append

function commitWork(fiber) {
  if(!fiber) return

  // 区分函数组件节点和普通节点,因为函数组件不存在 fiber.dom
  // 所以其孩子节点需要向上寻找有 fiber.dom 的祖先节点进行 append
  let fiberParent = fiber.parent
  while(!fiberParent.dom) {
    fiberParent = fiberParent.parent
  }

  if(fiber.dom) {
    fiberParent.dom.append(fiber.dom)
  }

  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

4. 处理多个函数组件相邻,没找到 sibling 的问题

向上找有 sibling 的节点,指向它

function performWorkOfUnit(fiber) {
 const isFunctionComponent = typeof fiber.type === 'function'

 if(isFunctionComponent) {
   updateFunctionComponent(fiber)
 } else {
   updateHostComponent(fiber)
 }

 // 链表指针指向下一个节点
 if(fiber.child) {
   return fiber.child
 }

 // if(fiber.sibling) {
 //   return fiber.sibling
 // }

 // return fiber.parent?.sibling

 // 多个函数组件相邻,因为函数组件的fiber没有sibling属性,需要向上寻找有 sibling 属性的节点指向下一个节点
 let newFiber = fiber
 while(newFiber) {
   if(newFiber.sibling) {
     return newFiber.sibling
   }
   newFiber = newFiber.parent
 }
}

4. 处理函数组件传参, textNode 为 number 的情况

修改 createElement 判断 isTextNode

function createElement(type, props, ...children) {
 return {
   type,
   props: {
     ...props,
     children: children.map((child) => {
       const isTextNode = 
         typeof child === 'string' || typeof child === 'number'
       
       return isTextNode ? createTextNode(child) : child
     }),
   },
 }
}

测试

import React from './core/React.js'

function Foo() {
  return (
    <div>
      Foo
      <p>Foo content</p>
    </div>
  )
}

function Bar({ num }) {
  return (
    <div>
      Bar
      <p>Bar content</p>
      <p>num is: {num}</p>
    </div>
  )
}

function AppContainer() {
  return (
    <div>
      <h1>App</h1>
      <Foo />
      <Bar num={10} />
    </div>
  )
}

const App = AppContainer()

export default App

image.png

更新

更新 vdom

  1. 如何得到新的 dom 树 通过添加一个 currentRoot 控制新的链表结构,使用 alternate 指向旧节点
// React.js
let nextWorkOfUnit = null // 指向下一个fiber任务
let wipRoot = null // 实现统一提交
let currentRoot = null // 用于更新 dom
  1. 如何找到老的节点
    1. 初始化旧指针 oldFiber = fiber.alternate?.child
    2. 判断是否改变 isSameType
    3. isSameType,newFiber 属性
    4. 移动指针
// React.js
function reconcileChildren(fiber, children) {
  let prevChild = null
  let oldFiber = fiber.alternate?.child // 旧fiber,指向当前fiber的后备指针的child

  children.forEach((child, index) => {
    // 判断新旧fiber是否同类型
    const isSameType = oldFiber && oldFiber.type === child.type
    let newFiber = {
      type: child.type,
      props: child.props,
      parent: fiber,
      child: null,
      sibling: null,
      
    }

    if(isSameType) {
      newFiber = {
        ...newFiber,
        dom: oldFiber.dom,
        alternate: oldFiber,
      }
    } else {
      newFiber = {
        ...newFiber,
        dom: null,
      }
    }

    // 移动指针
    if(oldFiber) {
      oldFiber = oldFiber.sibling
    }

    // 初始化 child、sibling
    if(index === 0) {
      fiber.child = newFiber
    } else {
      prevChild.sibling = newFiber
    }
    prevChild = newFiber
  })
}
  1. 如何 diff props、绑定事件
    1. newFiber 添加属性 effectTag,标识是更新(update)/占位(placement)
// React.js
function reconcileChildren(fiber, children) {
  // 省略...
  children.forEach((child, index) => {
  
    // 省略...
    
    if(isSameType) {
      newFiber = {
        ...newFiber,
        dom: oldFiber.dom,
        alternate: oldFiber,
        effectTag: 'update'
      }
    } else {
      newFiber = {
        ...newFiber,
        dom: null,
        effectTag: 'placement',
      }
    }

    // 省略...
  })
}
  1. 在 commitWork 时,判断 effectTag 进行更新/创建
function commitWork(fiber) {
  if(!fiber) return

  // 区分函数组件节点和普通节点,因为函数组件不存在 fiber.dom
  // 所以其孩子节点需要向上寻找有 fiber.dom 的祖先节点进行 append
  let fiberParent = fiber.parent
  while(!fiberParent.dom) {
    fiberParent = fiberParent.parent
  }

  // fiber.effectTag 与 fiber.dom,优先判断 fiber.effectTag,
  // 只要是 update,就走 update 逻辑
  if(fiber.effectTag === 'update') {
    updateProps(fiber.dom, fiber.props, fiber.alternate?.props)
    
  } else if(fiber.effectTag === 'placement') {
    if(fiber.dom) {
      fiberParent.dom.append(fiber.dom)
    }
  }

  commitWork(fiber.child)
  commitWork(fiber.sibling)
}
  1. 更新 props、绑定事件
function updateProps(dom, nextProps, prevProps) {
  Object.keys(prevProps).forEach(key => {
    if(key !== 'children') {
      // 旧有新无,删除
      if(!(key in nextProps)) {
        dom.removeAttribute(key)
      }
    }
  })

  
  Object.keys(nextProps).forEach((key) => {
    if (key !== 'children') {
      // 新有旧有,修改;新有旧无,新增
      if(nextProps[key] !== prevProps[key]) {
        // 绑定事件
        if(key.startsWith('on')) {
          const eventName = key.slice(2).toLowerCase()
          dom.removeEventListener(eventName, prevProps[key])
          dom.addEventListener(eventName, nextProps[key])
        
          // 绑定 props 属性
        } else {
          dom[key] = nextProps[key]
        }
      }
    }
  })
}

更新 children

  1. type 不一致(!iSameType),删除旧的,创建新的
    1. 收集 oldFiber 到 deletions,在 commitWork 之前删除
// React.js
let nextWorkOfUnit = null
let wipRoot = null
let currentRoot = null // 用于更新 dom
let deletions = [] // 收集旧的 fiber,删除

收集,!iSameType 收集 oldFiber

// 收集 oldFiber
function reconcileChildren(fiber, children) {
  let prevChild = null
  let oldFiber = fiber.alternate?.child // 旧fiber,指向当前fiber的后备指针的child

  children.forEach((child, index) => {
    // 判断新旧fiber是否同类型
    const isSameType = oldFiber && oldFiber.type === child.type
    let newFiber = {
      type: child.type,
      props: child.props,
      parent: fiber,
      child: null,
      sibling: null,
      
    }

    if(isSameType) {
      newFiber = {
        ...newFiber,
        dom: oldFiber.dom,
        alternate: oldFiber,
        effectTag: 'update'
      }
    } else {
      newFiber = {
        ...newFiber,
        dom: null,
        effectTag: 'placement',
      }

      // !isSameType,收集oldFiber
      if(oldFiber) {
        deletions.push(oldFiber)
      }
    }

    // 移动指针
    if(oldFiber) {
      oldFiber = oldFiber.sibling
    }

    // 初始化 child、sibling
    if(index === 0) {
      fiber.child = newFiber
    } else {
      prevChild.sibling = newFiber
    }
    prevChild = newFiber
  })
}

删除,在 commitWork 之前先删除oldFiber.dom

// 删除
function commitRoot() {
  deletions.forEach(commitDeletions) // 在 commitWork 之前先删除oldFiber.dom
  commitWork(wipRoot.child)
  currentRoot = wipRoot
  wipRoot =  null
  deletions = [] // 完成一次提交,需要清空这一轮的旧节点集合
}

function commitDeletions(fiber) {
  // 函数组件
  if(!fiber.dom) {
    commitDeletions(fiber.child)
  } else {
    let fiberParent = fiber.parent
    while(!fiberParent.dom) {
      fiberParent = fiberParent.parent
    }
    // 删除 oldFiber.dom
    fiberParent.dom.removeChild(fiber.dom)
  }
}
  1. 新的比老的少,多出来的删除

    循环children结束,还有 oldFiber 的话,需要收集到 deletions(旧有新无)

// 收集 oldFiber
function reconcileChildren(fiber, children) {
  // 省略...

  children.forEach((child, index) => {
    // 省略...
  })

  // 循环结束,还有 oldFiber 的话,需要收集到 deletions(旧有新无)
  while(oldFiber) {
    deletions.push(oldFiber)

    // 移动指针指向兄弟节点
    oldFiber = oldFiber.sibling
  }
}
  1. 解决 edge case
let bar = <div>bar</div>
let foo = <div>foo</div>
let show = false
function Foo() {
  return (
    <div id="foo">
      {show ? foo : bar}
    </div>
  )
}

对于这种情况,jsx 会把 show 解析为 div#foo 的一个 child,所以 child 有可能是一个 boolean 值,需要对 child 做一下判断

function reconcileChildren(fiber, children) {
  let prevChild = null
  let oldFiber = fiber.alternate?.child // 旧fiber,指向当前fiber的后备指针的child

  children.forEach((child, index) => {
    // 判断新旧fiber是否同类型
    const isSameType = oldFiber && oldFiber.type === child.type
    // 这里就先不初始化,让其为 undefined
    let newFiber

    if(isSameType) {
      newFiber = {
        type: child.type,
        props: child.props,
        parent: fiber,
        child: null,
        sibling: null,
        dom: oldFiber.dom,
        alternate: oldFiber,
        effectTag: 'update'
      }
    } else {
      // child 为 true,才赋值给 newFiber
      if(child) {
        newFiber = {
          type: child.type,
          props: child.props,
          parent: fiber,
          child: null,
          sibling: null,
          dom: null,
          effectTag: 'placement',
        }
      }

      // !isSameType,收集oldFiber
      if(oldFiber) {
        deletions.push(oldFiber)
      }
    }

    // 移动指针
    if(oldFiber) {
      oldFiber = oldFiber.sibling
    }

    // 初始化 child、sibling
    if(index === 0) {
      fiber.child = newFiber
    } else {
      prevChild.sibling = newFiber
    }
    
    // 如果存在 newFiber 的话才把 prevChild 更新为 newFiber
    if(newFiber) {
      prevChild = newFiber
    }
  })

  // 省略...
}

测试

// App.jsx
import React from './core/React.js'

let count = 1
let props = { id: 'foo' }
let bar = <div>bar</div>
let show = false
function Foo() {
  let foo = <div>foo</div>

  function handleClick() {
    count++
    props = { className: 'foo' }
    show = !show
    console.log('foo click count: ', count)
    React.update()
  }
  return (
    <div {...props}>
      {show ? foo : bar}
      <button onClick={handleClick}>foo click</button>
    </div>
  )
}

function AppContainer() {
  return (
    <div id="app">
      <h1>App</h1>
      <Foo />
    </div>
  )
}

const App = AppContainer()

export default App

初始状态

image.png

image.png

更新后

image.png

image.png

  1. 优化,无需更新的函数组件不用处理
// App.jsx
import React from './core/React.js'

function Foo() {
  console.log('foo function component')

  function handleClick() {
    console.log('foo click')
    React.update()
  }

  return (
    <div id="foo">
      <h1>Foo</h1>
      <button onClick={handleClick}>foo click</button>
    </div>
  )
}

function Bar() {
  console.log('bar function component')

  function handleClick() {
    console.log('bar click')
    React.update()
  }

  return (
    <div id="bar">
      <h1>Bar</h1>
      <button onClick={handleClick}>bar click</button>
    </div>
  )
}

function App() {
  console.log('app function component')

  function handleClick() {
    console.log('app click')
    React.update()
  }

  return (
    <div id="app">
      <h1>App</h1>
      <button onClick={handleClick}>app click</button>
      <Foo />
      <Bar />
    </div>
  )
}

export default App

像这样子,每点击按钮更新 Foo、Bar,都会从根节点一直往下直到最后一个函数组件 Bar 调用一遍 image.png

因为在 update 函数内,nextWorkOfUnit 每次都是更新为 wipRoot,指向的是 currentRoot 链表头节点

function update() {
  wipRoot = {
    dom: currentRoot.dom,
    props: currentRoot.props,
    alternate: currentRoot // 后备指针,指向上一次的 root(currentRoot,在 commitWork 之后被赋值了)
  }
  nextWorkOfUnit = wipRoot
}

但更新函数组件,起始点和终点还是可以控制在当前函数组件树上的。

  • 起始点:进行调用函数组件的时候

  • 终点:当遍历完整棵树,开始处理兄弟节点之前

  1. 要控制 nextWorkOfUnit 为当前函数组件的根节点,需要引入变量 wipFiber,在函数组件被调用的时候更新值
// React.js
let nextWorkOfUnit = null
let wipRoot = null
let currentRoot = null // 用于更新 dom
let deletions = []
let wipFiber = [] // 用于指向当前更新的函数组件根节点

function updateFunctionComponent(fiber) {
  // 更新 wipFiber 为当前函数组件的根节点
  wipFiber = fiber

  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}
  1. 在 update 里获取 wipFiber
function update() {

  console.log(wipFiber)

  wipRoot = {
    dom: currentRoot.dom,
    props: currentRoot.props,
    alternate: currentRoot // 后备指针,指向上一次的 root(currentRoot,在 commitWork 之后被赋值了)
  }
  nextWorkOfUnit = wipRoot
}

但发现wipFiber 被最后的函数组件fiber覆盖了

image.png

2.1 update函数返回闭包,在函数组件被调用时候立即调用 React.update,获取返回值,这样可以避免获取的 wipFiber 被最后的函数组件覆盖的问题

// React.js
function update() {
  const currentFiber = wipFiber

  return () => {
    wipRoot = {
      ...currentFiber,
      alternate: currentFiber,
    }

    nextWorkOfUnit = wipRoot
  }
}
// App.jsx
import React from './core/React.js'

function Foo() {
  console.log('foo function component')

  const update = React.update()

  function handleClick() {
    console.log('foo click')
    update()
  }

  return (
    <div id="foo">
      <h1>Foo</h1>
      <button onClick={handleClick}>foo click</button>
    </div>
  )
}

function Bar() {
  console.log('bar function component')
  const update = React.update()

  function handleClick() {
    console.log('bar click')
    update()
  }

  return (
    <div id="bar">
      <h1>Bar</h1>
      <button onClick={handleClick}>bar click</button>
    </div>
  )
}

function App() {
  console.log('app function component')
  const update = React.update()


  function handleClick() {
    console.log('app click')
    update()
  }

  return (
    <div id="app">
      <h1>App</h1>
      <button onClick={handleClick}>app click</button>

      <Foo />

      <Bar />
    </div>
  )
}

export default App

再看下输出的wipFiber是正常对应到的函数组件

image.png

  1. 终点:当遍历完整棵树,开始处理兄弟节点之前

(1)在执行完 performWorkOfUnit,得到新的 nextWorkOfUnit,nextWorkOfUnit 可能是 Foo 的 child,也可能是 Foo 的 sibling,也可能是到达了最后的节点了返回undefined

(2)此时,通过判断 wipRoot.sibling?.type 与 nextWorkOfUnit?.type 是否相等,可以得知相等则nextWorkOfUnit指向了 Foo 的 sibling,处于 Foo vdom 树的末尾,则可以结束 此 Foo vdom 树的更新,把 nextWorkOfUnit 置为 undefined,表示不再进行下一轮循环,也就不会对 Foo 的 sibling 做处理

function workLoop(deadline) {
  let shouldYeild = false

  while(!shouldYeild && nextWorkOfUnit) {
    nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit)

    // 如果nextWorkOfUnit是当前函数组件树的兄弟节点,则结束
    if(wipRoot.sibling?.type === nextWorkOfUnit?.type) {
      console.log(',,,', wipRoot, nextWorkOfUnit)
      nextWorkOfUnit = undefined // 置为undefined退出 while 循环
    }

    shouldYeild = deadline.timeRemaining() < 1
  }

  // 完成所有节点的链表结构转换后,统一添加
  if(!nextWorkOfUnit && wipRoot) {
    commitRoot()
  }

  requestIdleCallback(workLoop)
}

image.png

useState

最简实现

useState 需要重新设置 nextWorkOfUnit

function useState(initial) {
  const currentFiber = wipFiber
  const oldHook = currentFiber.alternate?.stateHook

  const stateHook = {
    state: oldHook ? oldHook.state : initial,
  }

  // 把 stateHook绑定到 currentFiber,方便更新后通过oldHook获取
  currentFiber.stateHook = stateHook

  function setState(action) {
    stateHook.state = action(stateHook.state)

    // 需要更新 wipRoot,nextWorkOfUnit,
    // 以重新调用函数组件->调用useState->更新 state
    wipRoot = {
      ...currentFiber,
      alternate: currentFiber,
    }

    nextWorkOfUnit = wipRoot
  }

  // 以数组形式返回,这样外面可以自定义命名
  return [stateHook.state, setState]
}
// App.jsx
import React from './core/React.js'

function App() {
  console.log('app function component')
  const update = React.update()
  const [count, setCount] = React.useState(10)


  function handleClick() {
    setCount((n) => n + 1)
  }

  return (
    <div id="app">
      <h1>App</h1>
      <div>count is: {count}</div>
      <button onClick={handleClick}>app click</button>
    </div>
  )
}

export default App

image.png

考虑多个 useState

  • 使用数组索引来获取对应的stateHook

  • 每次在函数组件调用的时候,置空

let stateHooks // 用于存储 stateHook
let stateHookIndex // 用于记录当前 stateHook 的 index

function useState(initial) {
  const currentFiber = wipFiber
  const oldHook = currentFiber.alternate?.stateHooks[stateHookIndex]

  const stateHook = {
    state: oldHook ? oldHook.state : initial,
  }

  stateHooks.push(stateHook)
  stateHookIndex++

  currentFiber.stateHooks = stateHooks

  function setState(action) {
    stateHook.state = action(stateHook.state)

    // 需要更新 wipRoot,nextWorkOfUnit,
    // 以重新调用函数组件->调用useState->更新 state
    wipRoot = {
      ...currentFiber,
      alternate: currentFiber,
    }

    nextWorkOfUnit = wipRoot
  }

  return [stateHook.state, setState]
}

function updateFunctionComponent(fiber) {
  wipFiber = fiber
  stateHooks = [] // 每次调用函数组件,先清空
  stateHookIndex = 0 // 先置0

  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}
// App.jsx
import React from './core/React.js'

function App() {
  console.log('app function component')
  const update = React.update()
  const [count, setCount] = React.useState(10)
  const [name, setName] = React.useState('TOM')


  function handleClick() {
    setName((n) => n + ' Jerry')
    setCount((n) => n + 1)
    
  }

  return (
    <div id="app">
      <h1>App</h1>
      <div>count is: {count}</div>
      <div>name is: {name}</div>
      <button onClick={handleClick}>app click</button>
    </div>
  )
}

export default App

调用顺序:

  1. updateFunctionComponent -> useState(10) -> useState('Tom')

    • setCount、setName 保存着它们自己的 stateHook 状态
    • setCount,stateHook.state = stateHooks[0]
    • setName, stateHook.state = stateHooks[1]
  2. handleClick

  3. setName((n) => n + ' Jerry')

    stateHook.state = 'Tom Jerry',即 stateHooks[1].state = 'Tim Jerry'

  4. setCount((n) => n + 1)

    stateHook.state = 11,即 stateHooks[0].state = 11

  5. updateFunctionComponent -> useState(10) -> useState('Tom')

  6. 根据索引找到对应的 oldHook 进行更新 state

统一处理 action

添加 stateHook.queue 属性,收集 action

function useState(initial) {
  const currentFiber = wipFiber
  const oldHook = currentFiber.alternate?.stateHooks[stateHookIndex]

  const stateHook = {
    state: oldHook ? oldHook.state : initial,
    queue: oldHook ? oldHook.queue : [],
  }

  // 执行 action
  stateHook.queue.forEach(action => {
    const eagerState = typeof action === 'function' ? action(stateHook.state) : action

    // 状态没变,不做处理
    if(eagerState != stateHook.state) {
      stateHook.state = action(stateHook.state)
    }
  })

  stateHooks.push(stateHook)
  stateHookIndex++
  stateHook.queue = [] // 遍调用完清空

  currentFiber.stateHooks = stateHooks

  function setState(action) {
    // stateHook.state = action(stateHook.state)
    // 收集action
    // 考虑 action 不是函数的情况
    stateHook.queue.push(typeof action === 'function' ? action : () => action)

    // 需要更新 wipRoot,nextWorkOfUnit,
    // 以重新调用函数组件->调用useState->更新 state
    wipRoot = {
      ...currentFiber,
      alternate: currentFiber,
    }

    nextWorkOfUnit = wipRoot
  }

  return [stateHook.state, setState]
}

useEffect

    1. useEffect
    • 调用时机:在React完成对DOM的渲染后,并且浏览器完成绘制之前
    • wipFiber.effectHooks
    1. clearup
    • 调用时机:在调用useEffect 之前调用,当deps 为空,不会调用返回的
let effectHooks // 用于存储 useEffect 的 effectHook

function useEffect(callback, deps) {

  const effectHook = {
    callback,
    deps,
    clearup: null,
  }

  effectHooks.push(effectHook)

  // 不需要重新设置 nextWorkOfUnit,
  // 直接把 effectHooks 绑定到当前的函数组件 wipFiber
  wipFiber.effectHooks = effectHooks 
}

function updateFunctionComponent(fiber) {
  effectHooks = [] // 每次调用函数组件,先清空

  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}
function commitRoot() {
  deletions.forEach(commitDeletions) // 在 commitWork 之前先删除 oldFiber
  commitWork(wipRoot.child)
  commitEffectHooks() // 在React完成对DOM的渲染后,并且浏览器完成绘制之前
  currentRoot = wipRoot
  wipRoot =  null
  deletions = [] // 完成一次提交,需要清空这一轮的旧节点集合
}

function commitEffectHooks() {
  function run(fiber) {
    if(!fiber) return

    // 初始化
    if(!fiber.alternate) {
      fiber.effectHooks?.forEach(hook => {
        hook.clearup = hook.callback()
      })
    
    // update
    } else {
      fiber.effectHooks?.forEach((newHook, index) => {
        if(newHook.deps.length > 0) {
          const oldHook = fiber.alternate?.effectHooks?.[index]

          const needEffect = oldHook.deps.some((oldDep, i) => {
            return oldDep !== newHook.deps[i]
          })

          if(needEffect) {
            newHook.clearup = newHook.callback()
          }
        }
      })
    }

    run(fiber.child)
    run(fiber.sibling)
  }

  function runClearup(fiber) {
    if(!fiber) return

    // 执行 alternate 的 clearup
    fiber.alternate?.effectHooks?.forEach(hook => {
      hook.clearup?.()
    })

    runClearup(fiber.child)
    runClearup(fiber.sibling)
  }

  // 在触发 effect callback 前,先把更新前的 clearup 执行了
  runClearup(wipRoot)
  run(wipRoot)
}