手写mini React,理解React渲染原理

166 阅读17分钟

一. 基本概念

1.1 jsx语法

jsxreact提供的语法糖,经过babel转换处理之后会变成React.createElement方法调用,例如<App />会转成React.createElement(function App() {}, null),所以转换后的代码本质是调用了React.createElement方法执行相关的代码逻辑。

1.2 Fiber架构

React中每个组件或元素标签都对应一个FiberNode节点,节点间会建立父子,兄弟关联关系,构建成FiberNode Tree,即虚拟DOM树。

当触发更新逻辑时,会构建一棵新的虚拟DOM树,与旧的虚拟DOM树进行比对,找出新增/删除/变更FiberNode节点,在更新DOM树阶段,会递归遍历虚拟DOM树,找到需要处理的FiberNode节点,例如需要删除该FiberNode节点,那么会将该FiberNode节点对应的DOM节点从DOM树中移除,完成DOM树的更新。

通过ReactFiber架构,我们可以精确找到真实DOM树需要更新的节点,进行相应逻辑处理完成更新操作

二. 核心对象

2.1 ReactElement

当调用React.createElement方法时会创建ReactElement对象,这个对象会记录组件方法或元素标签类型以及传入的props

2.2 FiberRootNode

这个对象会记录根DOM节点和最新构建的FiberNode Tree根节点

2.3 FiberNode

每个ReactElement对象都会创建对应的FiberNode节点,节点之间建立父子,兄弟关联关系,由此构建成虚拟DOM树,同时节点会有stateNode属性记录对应的真实DOM节点

三. 实现mini React

3.1 测试代码

注意测试项目要支持解析jsx语法且添加了@babel/preset-react这个预设

目标是跑通这段测试代码,逻辑很简单,渲染App组件的内容,当点击hello react这个h1标签时要切换展示HelloWorld组件

import React, { createRoot, useState } from './mini-react'

function HelloWorld() {
  return <h1>are you ok?</h1>
}

function App() {
    const [visible, setVisible] = useState(true)
    
    return (
        <div>
            <h1 onClick={() => setVisible(!visible)}>hello react</h1>
            {visible && <HelloWorld />}
        </div>
    )
}

const root = createRoot(document.getElementById('app'))
root.render(<App />)

3.2 目录结构

mini-react
|- hooks
    |-- useState.js
|- ReactElement.js
|- FiberNode.js
|- FiberRootNode.js
|- ReactDOMRoot.js
|- ReactFiberReconciler.js
|- index.js

3.3 将组件方法或元素标签转成ReactElement对象

3.3.1 定义ReactElement对象原型

/**
 * @param {*} type 组件方法或元素标签类型,例如function App() {}或h1
 * @param {*} key 组件方法或元素标签的标识符,例如<App key='app' />的key属性值
 * @param {*} props 入参属性,例如<h1 onClick={() => {}} />的onClick属性
 */
function ReactElement(type, key, props) {
  this.type = type
  this.key = key
  this.props = props
}

3.3.2 提供createElement方法创建ReactElement对象

/**
 * @param {*} type 组件方法或元素标签类型,例如function App() {}或h1
 * @param {*} config ReactElement的key和props
 * @param {*} childrens child ReactElement对象数组
 */
function createElement(type, config, ...childrens) {
  const { key = null, ...props } = config || {}
  if (childrens.length) {
    props.children = childrens.length > 1 ? childrens : childrens[0]
  }
  return new ReactElement(type, key, props)
}

export { createElement }

3.4 创建FiberRootNode对象,记录FiberNode Tree根节点和根DOM节点

3.4.1 定义FiberNode对象原型

这里解析下flagssubstreeFlags两个属性,flags是当前节点状态,例如插入,删除,更新等状态,而substreeFlagschild FiberNode节点状态,在更新DOM阶段,会遍历FiberNode Tree节点,通过substreeFlags属性值可以判断child FiberNode节点是否需要更新,如果值为NoFlags,说明无需处理,那么就可以不用继续递归遍历其child FiberNode节点

// 根FiberNode节点对应类型
export const HostRoot = 3
// 组件方法FiberNode节点对应类型
export const FunctionComponent = 0
// 元素标签FiberNode节点对应类型
export const HostComponent = 5
// 纯文本FiberNode节点对应类型
export const HostText = 6

// 表示该FiberNode节点无需处理
export const NoFlags = 0
// 表示该FiberNode节点是新增节点,需要插入DOM树中
export const Placement = 2
// 表示该FiberNode节点需要更新处理
export const Update = 4
// 表示该FiberNode节点有子节点要删除
export const ChildDeletion = 16

function FiberNode(tag, pendingProps) {
  this.tag = tag // FiberNode节点类型
  this.key = null // 对应ReactElement的key属性
  this.elementType = null // 对应ReactElement的type属性
  this.return = null // 父FiberNode节点
  this.child = null // 子FiberNode节点
  this.sibling = null // 兄弟FiberNode节点
  this.alternate = null // FiberNode副本节点
  this.deletions = null // 删除的FiberNode节点
  this.flags = NoFlags // FiberNode节点状态
  this.subtreeFlags = NoFlags // 子树FiberNode节点状态
  this.stateNode = null // FiberNode节点对应的DOM节点
  this.pendingProps = pendingProps // 对应ReactElement的props属性
}

3.4.2 定义FiberRootNode对象原型

/**
 * @param {*} element DOM节点
 */
function FiberRootNode(element) {
  // 记录根DOM节点
  this.containerInfo = element
  // 创建根FiberNode节点
  const fiber = new FiberNode(HostRoot, null)
  fiber.stateNode = this
  // 记录根FiberNode节点
  this.current = fiber
}

3.4.3 提供createRoot方法创建FiberRootNode对象

/**
 * @param {*} root FiberRootNode对象
 */
function ReactDOMRoot(root) {
  this._internalRoot = root
}

/**
 * @param {*} children ReactElement对象
 */
ReactDOMRoot.prototype.render = function (children) {
  // 获取FiberRootNode对象
  const root = this._internalRoot
  performWorkOnRoot(root, children)
}

/**
 * @param {*} element DOM节点
 */
function createRoot(element) {
  // 添加全局click监听事件
  element.addEventListener('click', (event) => {
    if (event.target.onClick) {
      event.target.onClick()
    }
  })
  const root = new FiberRootNode(element)
  return new ReactDOMRoot(root)
}

export { createRoot }

3.5 首次构建FiberNode Tree

image.png

首次构建FiberNode Tree结构如图所示,先对FiberNode节点之间关联关系,FiberNode节点和DOM节点关联关系有一个初步的了解,下面会逐步讲解从零到一构建FiberNode Tree流程

3.5.1 提供createWorkInProgress方法创建FiberNode副本节点

如果已存在副本节点,则复制旧FiberNode节点属性值

/**
 * @param {*} current FiberNode节点
 * @param {*} pendingProps 对应ReactElement对象的props
 */
function createWorkInProgress(current, pendingProps) {
  let workInProgress = current.alternate
  if (workInProgress !== null) {
    workInProgress.flags = NoFlags
    workInProgress.subtreeFlags = NoFlags
    workInProgress.deletions = null
    workInProgress.pendingProps = pendingProps
  } else {
    workInProgress = new FiberNode(current.tag, pendingProps)
    workInProgress.alternate = current
    current.alternate = workInProgress
  }
  workInProgress.key = current.key
  workInProgress.elementType = current.elementType
  workInProgress.child = current.child
  workInProgress.sibling = current.sibling
  workInProgress.stateNode = current.stateNode
  workInProgress.memoizedState = current.memoizedState
  return workInProgress
}

3.5.2 递归遍历FiberNode节点,创建ReactElement对象对应的FiberNode节点

  1. 调用createWorkInProgress方法创建根FiberNode副本节点,赋值给workInProgress
  2. 调用performUnitOfWork方法递归遍历FiberNode节点,创建ReactElement对应的FiberNode节点,建立关联关系,构建FiberNode Tree
  3. 当递归遍历到当前分支叶子节点时会调用completeUnitOfWork方法进行DOM树构建和收集子树FiberNode节点副作用
function performUnitOfWork(unitOfWork) {
  let nextFiber = beginWork(unitOfWork)
  // next为null,说明已经遍历到当前分支叶子FiberNode节点,则调用completeWork方法构建DOM树和收集子树FiberNode节点副作用
  if (nextFiber === null) {
    completeUnitOfWork(unitOfWork)
  } else {
    workInProgress = nextFiber
  }
}

/**
 * @param {*} root FiberRootNode对象
 * @param {*} children ReactElement对象
 */
function renderRootSync(root, children) {
  const current = root.current
  // 创建根FiberNode副本节点,赋值给workInProgress
  workInProgress = createWorkInProgress(current, null)
  workInProgress.pendingProps = { children }
  // 递归遍历FiberNode节点,创建ReactElement对应的FiberNode节点,建立关联关系,构建FiberNode Tree
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress)
  }
}

需要注意这里采用深度优先遍历算法,即递归遍历到第一个分支叶子节点后返回遍历下一个分支,例如下面这个示例,AB组件平级,A嵌套C组件,那会先递归遍历A -> C这个分支,然后返回遍历B,可以先判断下控制台输出结果会是什么

function C() {
    console.log('C')
    return <h1>hello C</h1>
}

function A() {
    console.log('A')
    return (
        <div>
            <h1>hello A</h1>
            <C />
        </div>
    )
}

function B() {
    console.log('B')
    return <h1>hello B</h1>
}

function App() {
    return (
        <div>
            <A />
            <B />
        </div>
    )
}

// 控制台输出结果是A -> C -> B

3.5.3 创建ReactElement对象对应的FiberNode节点,建立关联关系

FiberNode节点tag属性代表节点类型,不同节点类型有对应处理逻辑

/**
 * @param {*} workInProgress FiberNode节点
 */
function beginWork(workInProgress) {
  // 获取旧FiberNode节点
  const current = workInProgress.alternate
  switch (workInProgress.tag) {
    case HostRoot: {
      const { children: nextChildren } = workInProgress.pendingProps
      // 创建child ReactElement对象对应的FiberNode节点
      return reconcileChildren(current, workInProgress, nextChildren)
    }
    case FunctionComponent: {
      // 获取组件方法
      const Component = workInProgress.elementType
      // 调用组件方法获取child ReactElement对象
      const nextChildren = Component(workInProgress.pendingProps)
      // 创建child ReactElement对应的FiberNode节点
      return reconcileChildren(current, workInProgress, nextChildren)
    }
    case HostComponent: {
      let { children: nextChildren } = workInProgress.pendingProps
      // 判断nextChildren是否是纯文本,是则不需要创建FiberNode节点,将nextChildren赋值为null
      if (typeof nextChildren === 'string') {
        nextChildren = null
      }
      // 创建child ReactElement对象对应的FiberNode节点
      return reconcileChildren(current, workInProgress, nextChildren)
    }
    case HostText: {
      return null
    }
  }
}

3.5.4 解析reconcileChildren方法

currentnull,调用mountChildFibers方法,即不需要处理child FiberNode节点副作用,变更flags属性值(flags属性作用参考3.4.1小节),因为currentnull说明旧FiberNode Tree中没有当前节点对应的旧FiberNode节点,属于新增节点,我们只需要将当前节点的flags属性值赋值为Placement即可,不需要处理child FiberNode节点flags属性值。

current不为null,调用reconcileChildFibers方法,即需要处理child FiberNode节点副作用。

mountChildFibersreconcileChildFibers都是调用了createChildReconciler方法,获取返回的闭包函数。

createChildReconciler方法接收一个boolean类型入参,用于判断是否处理child FiberNode节点副作用,采用闭包方式,返回创建child FiberNode节点方法。同时createChildReconciler方法内部封装了许多处理child FiberNode节点的逻辑方法,下面会依次说明

function createChildReconciler(shouldTrackSideEffects) {
  function reconcileChildFibers(workInProgress, currentFirstChild, nextChildren) {}

  return reconcileChildFibers
}

const mountChildFibers = createChildReconciler(false)
const reconcileChildFibers = createChildReconciler(true)
/**
 * @param {*} current 旧FiberNode节点
 * @param {*} workInProgress FiberNode节点 
 * @param {*} nextChildren child ReactElement对象
 */
function reconcileChildren(current, workInProgress, nextChildren) {
  if (current === null) {
    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren)
  } else {
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
    )
  }
  return workInProgress.child
}

3.5.5 解析首次渲染reconcileChildFibers方法逻辑

在首次渲染,需要从零到一构建FiberNode Tree,只有根FiberNode节点有对应的旧FiberNode节点,意味着创建App Compoent对应的FiberNode节点时shouldTrackSideEffects会为true,同时因为是首次构建FibreNode Tree,所以App Component对应的FiberNode节点不存在对应的旧FiberNode节点,即该节点是新增节点,将flags属性值赋值为Placement

在后续递归创建child FiberNode节点时,shouldTrackSideEffects会为false,不需要变更flags属性值

function createChildReconciler(shouldTrackSideEffects) {  
  function placeChild(newFiber) {
    // 当需要处理副作用时,且旧FiberNode Tree中没有新节点对应的旧节点时,说明是新增节点,将其flags属性值赋值为Placement
    if (shouldTrackSideEffects && newFiber.alternate === null) {
      newFiber.flags |= Placement
    }
    return newFiber
  }

  // 创建ReactElement对应的FiberNode节点
  function createFiberFormElement(element) {
    let fiber
    if (typeof element.type === 'function') {
      fiber = new FiberNode(FunctionComponent, element.props)
    } else {
      fiber = new FiberNode(HostComponent, element.props)
    }
    fiber.key = element.key
    fiber.elementType = element.type
    return fiber
  }

  // 创建文本对应的FiberNode节点
  function creaetFiberFromText(text) {
    const fiber = new FiberNode(HostText, text)
    return fiber
  }
  
  function reconcileSingleTextNode(returnFiber, currentFirstChild, text) {
    const fiber = creaetFiberFromText(text)
    fiber.return = returnFiber
    return placeChild(fiber)
  }
  
  // 创建单个ReactElement对象对应的FiberNode节点
  function reconcileSingleElement(returnFiber, currentFirstChild, newChild) {
    const fiber = createFiberFormElement(newChild)
    fiber.return = returnFiber
    return placeChild(fiber)
  }
  
  // 创建多个ReactElement对象对应的FiberNode节点
 function reconcileChildrenArray(returnFiber, oldFiber, newChild) {
    let firstChildFiber = null
    let prevChildFiber = null
    for (let newIdx = 0; newIdx < newChild.length; newIdx++) {
      let nextFiber = null
      if (typeof newChild[newIdx] === 'object' && newChild[newIdx] !== null) {
        nextFiber = createFiberFormElement(newChild[newIdx])
      } else if (typeof newChild[newIdx] === 'string') {
        nextFiber = creaetFiberFromText(newChild[newIdx])
      }
      if (nextFiber) {
        nextFiber.return = returnFiber
        // 记录第一个child FiberNode节点
        if (firstChildFiber === null) {
          firstChildFiber = nextFiber
        }
        // 建立兄弟FiberNode节点关联关系
        if (prevChildFiber === null) {
          prevChildFiber = nextFiber
        } else {
          prevChildFiber.sibling = nextFiber
          prevChildFiber = nextFiber
        }
      }
    }
    return firstChildFiber
  }

  /**
   * @param {*} returnFiber 父FiberNode节点
   * @param {*} currentFirstChild 旧FiberNode节点的子节点
   * @param {*} newChild ReactElement对象
   */
  function reconcileChildFibers(returnFiber, currentFirstChild, newChild) {
    if (typeof newChild === 'object' && newChild !== null) {
      if (Array.isArray(newChild)) {
        return reconcileChildrenArray(returnFiber, currentFirstChild, newChild)
      }
      const firstChildFiber = reconcileSingleElement(
        returnFiber,
        currentFirstChild,
        newChild,
      )
      return firstChildFiber
    }
    if (typeof newChild === 'string') {
      const firstChildFiber = reconcileSingleTextNode(
        returnFiber,
        currentFirstChild,
        newChild,
      )
      return firstChildFiber
    }
    return null
  }
  
  return reconcileChildFibers
}

3.6 构建DOM

3.6.1 递归遍历FiberNode节点,构建FiberNode节点对应的DOM树和收集子树FiberNode节点副作用

当构建FiberNode Tree,递归遍历到分支叶子节点时会调用completeUnitOfWork创建构建该节点对应DOM节点,递归父节点构建DOM树,采用自底向上的递归方式

/**
 * @param {*} unitOfWork FiberNode节点
 */
function completeUnitOfWork(unitOfWork) {
  while (unitOfWork) {
    let nextFiber = unitOfWork
    // 构建FiberNode节点对应的DOM树和收集子树FiberNode节点副作用
    completeWork(nextFiber)
    // 获取兄弟节点
    nextFiber = nextFiber.sibling
    // 如果有兄弟节点,则调用performUnitOfWork方法继续构建FiberNode Tree
    if (nextFiber !== null) {
      workInProgress = nextFiber
      break
    }
    // 递归父FiberNode节点
    unitOfWork = unitOfWork.return
    workInProgress = unitOfWork
  }
}

3.6.2 收集子树FiberNode节点副作用

遍历child FiberNode,收集副作用

function bubbleProperties(workInProgress) {
  let subtreeFlags = NoFlags
  let child = workInProgress.child
  while (child) {
    subtreeFlags |= child.flags
    subtreeFlags |= child.subtreeFlags
    child = child.sibling
  }
  workInProgress.subtreeFlags |= subtreeFlags
}

3.6.3 构建FiberNode节点对应的DOM

// 设置DOM属性值
function setProp(el, key, value) {
  switch (key) {
    case 'children': {
      if (typeof value === 'string') {
        el.textContent = value
      }
      break
    }
    case 'onClick': {
      el.onClick = value
      break
    }
  }
}

function appendAllChildren(el, workInProgress) {
  let nextChild = workInProgress.child
  while (nextChild) {
    if (nextChild.tag === HostComponent || nextChild.tag === HostText) {
      el.appendChild(nextChild.stateNode)
    } else {
      appendAllChildren(el, nextChild)
    }
    nextChild = nextChild.sibling
  }
}

function completeWork(workInProgress) {
  switch (workInProgress.tag) {
    // HostRoot和FunctionComponent类型的FiberNode节点没有对应的DOM节点,没有构建DOM树逻辑,只需要收集子树FiberNode节点副作用即可
    case HostRoot:
    case FunctionComponent:
      bubbleProperties(workInProgress)
      return null
    case HostComponent: {
      // 创建DOM节点
      const { elementType, pendingProps } = workInProgress
      const instance = document.createElement(elementType)
      // 将DOM节点赋值给FiberNode的stateNode属性
      workInProgress.stateNode = instance
      // 设置DOM节点属性
      for (const propKey in pendingProps) {
        const prpoValue = pendingProps[propKey]
        setProp(instance, propKey, prpoValue)
      }
      // 递归遍历FiberNode节点将对应的DOM节点添加到父DOM节点中,构建DOM树
      appendAllChildren(instance, workInProgress)
      // 收集子树FiberNode节点副作用
      bubbleProperties(workInProgress)
      return null
    }
    case HostText: {
      const { pendingProps } = workInProgress
      const instance = document.createTextNode(pendingProps)
      workInProgress.stateNode = instance
      bubbleProperties(workInProgress)
      return null
    }
  }
}

3.7 首次更新DOM

3.7.1 递归遍历FiberNode节点执行副作用处理逻辑

采用深度优先遍历递归算法,优先处理叶子节点副作用逻辑

在首次渲染更新DOM阶段,只需要将App Component组件的child FiberNode节点对应的DOM插入指定节点即可完成DOM更新

// 获取父DOM节点
function getParentNode(finishWork) {
  let parentFiber = finishWork.return
  while (parentFiber !== null) {
    if (parentFiber.tag === FunctionComponent) {
      parentFiber = parentFiber.return
      continue
    }
    break
  }
  return parentFiber.tag === HostRoot
    ? parentFiber.stateNode.containerInfo
    : parentFiber.stateNode
}

function recursivelyTraverseMutationEffects(finishWork) {
  // 为true说明子树FiberNode节点有副作用需要处理,递归遍历child FiberNode
  if (finishWork.subtreeFlags & (Placement | Update | ChildDeletion)) {
    let child = finishWork.child
    while (child) {
      commitMutationEffectsOnFiber(child)
      child = child.sibling
    }
  }
}

function commitMutationEffectsOnFiber(finishWork) {
  // 采用深度优先遍历算法进行DOM更新
  switch (finishWork.tag) {
    case HostRoot: {
      recursivelyTraverseMutationEffects(finishWork)
      break
    }
    case FunctionComponent: {
      recursivelyTraverseMutationEffects(finishWork)
      // 为true则需要将对应DOM节点插入到父节点DOM中
      if (finishWork.flags & Placement) {
        // 获取父节点DOM
        const parentNode = getParentNode(finishWork)
        // 获取子节点对应的DOM
        appendAllChildren(parentNode, finishWork)
      }
      break
    }
    case HostComponent: {
      break
    }
    case HostText: {
      break
    }
  }
}

function commitRoot(root) {
  const finishWork = root.current.alternate
  // 递归遍历FiberNode节点,执行副作用处理逻辑
  commitMutationEffectsOnFiber(finishWork)
  // 将FiberRootNode对象current属性指向新的FiiberNode Tree根节点
  root.current = finishWork
}

3.8 实现简单版本useState

3.8.1 修改FiberNode对象原型定义

新增memoizedState属性用于记录useState数据

function FiberNode(tag) {
    this.memoizedState = null // 记录useState数据
}

3.8.2 useState方法实现

每次调用useState方法都会创建一个hook,用来保存state数据,以及收集更新state的方法。每个hook通过next指针进行索引,构成一个单链表结构。

当首次调用组件方法时,调用mountState方法处理逻辑,即创建hook,记录初始值,将hook赋值给当前节点的memoizedState属性,返回初始值和dispatch方法

当触发更新重新调用组件方法时,调用updateState方法处理,即创建hook,获取旧的state值,执行更新state的方法时作为入参,获取新的state值赋值给hook,将hook赋值给当前节点的memoizedState属性,返回初始值和dispatch方法

dispatch方法会收集更新state的方法,触发更新重新渲染

// 记录当前FiberNode节点
let currentlyRenderingFiber = null
// 记录旧state hook
let currentHook = null
// 记录新state hook
let workInProgressHook = null

// 组件方法调用装饰器,在调用组件方法前后做一些逻辑处理
export function renderWithHooks(current, workInProgress, Component, props) {
  currentlyRenderingFiber = workInProgress
  if (current !== null) {
    currentHook = current.memoizedState
  }
  workInProgressHook = null
  const children = Component(props)
  currentlyRenderingFiber = null
  currentHook = null
  return children
}

// 获取FiberRootNode对象
function getRootForUpdatedFiber(fiber) {
  while (fiber.tag !== HostRoot) {
    fiber = fiber.return
  }
  return fiber.stateNode
}

// 触发更新方法
function dispatchSetState(fiber, hook, action) {
  // 获取FiberRootNode对象
  const root = getRootForUpdatedFiber(fiber)
  const executor = typeof action === 'function' ? action : () => action
  // 收集更新state的方法,在创建新FiberNode节点时执行,将新的state赋值给节点的memoizedState属性
  hook.queue.push((state) => executor(state))
  // 触发更新
  performWorkOnRoot(root, null)
}

// 创建state hook
function mountWorkInProgressHook() {
  const hook = {
    memoizedState: null, // 记录更新state值
    queue: [], // 更新state方法队列
    next: null, // 记录下一个hook
  }
  return hook
}

// 首次调用组件方法处理逻辑
function mountState(initialState) {
  const hook = mountWorkInProgressHook()
  hook.memoizedState = initialState
  // 建立hook链表
  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook
  } else {
    workInProgressHook.next = hook
    workInProgressHook = hook
  }
  const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, hook)
  return [hook.memoizedState, dispatch]
}

// 触发更新再次调用函数组件处理逻辑
function updateState() {
  const hook = mountWorkInProgressHook()
  // 调用更新state方法获取新的state
  hook.memoizedState = currentHook.queue.reduce(
    (state, action) => action(state),
    currentHook.memoizedState,
  )
  // 建立hook链表
  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook
  } else {
    workInProgressHook.next = hook
    workInProgressHook = hook
  }
  currentHook = currentHook.next
  const dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, hook)
  return [hook.memoizedState, dispatch]
}

function useState(initialState) {
  const current = currentlyRenderingFiber.alternate
  if (current === null) {
    return mountState(initialState)
  } else {
    return updateState()
  }
}

export { useState }

3.8.3 组件调用添加装饰器

修改组件方法调用逻辑,作为renderWithHooks方法入参内部调用

function beginWork(workInProgress) {
  const current = workInProgress.alternate
  switch (workInProgress.tag) {
    case FunctionComponent: {
      // 获取组件方法
      const Component = workInProgress.elementType
      const nextChildren = renderWithHooks(
        current,
        workInProgress,
        Component,
        workInProgress.pendingProps,
      )
    }
  }
}

3.9 触发更新构建新FiberNode Tree

image.png

FiberNode Tree结构如图所示,可以看到这时候已经有两课FiberNode TreeFiberNodeRoot对象的current指向最新的FiberNode Tree根节点,当触发更新时,会进行diff逻辑处理,判断旧FiberNode节点能否复用,旧FiberNode是否要删除等

3.9.1 复用旧根FiberNode节点child FiberNode

function beginWork(workInProgress) {
  switch (workInProgress.tag) {
    case HostRoot: {
      if (current.child) {
        // 复用旧child FiberNode节点
        const newChild = createWorkInProgress(
          current.child,
          current.pendingProps,
        )
        newChild.return = workInProgress
        workInProgress.child = newChild
        return workInProgress.child
      }
      const { children: nextChildren } = workInProgress.pendingProps
      // 创建child ReactElement对象对应的FiberNode节点
      return reconcileChildren(current, workInProgress, nextChildren)
    }
  }
}

3.9.2 补充reconcileChildFibers方法diff逻辑

3.9.2.1 新增deleteChild方法

用于收集要删除的旧Fiber节点,以及变更父节点的flags属性值为ChildDeletion

function createChildReconciler(shouldTrackSideEffects) {
  // 遍历旧FiberNode节点及其兄弟节点添加到父节点的deletions属性中,将父节点的flags赋值为ChildDeletion
  function deleteChild(returnFiber, oldFiber, traverse = true) {
    if (oldFiber === null) {
      return null
    }
    returnFiber.flags |= ChildDeletion
    returnFiber.deletions = returnFiber.deletions || []
    returnFiber.deletions.push(oldFiber)
    oldFiber = oldFiber.sibling
    while (oldFiber && traverse) {
      returnFiber.deletions.push(oldFiber)
      oldFiber = oldFiber.sibling
    }
    return null
  }
}
3.9.2.2 补充reconcileSingleTextNode方法diff逻辑
  1. 旧节点类型不是HostText,那么删除该节点及其兄弟节点
  2. 旧节点类型是HostText,复用该旧节点,由于新节点没有兄弟节点,需要删除旧节点的兄弟节点
function createChildReconciler(shouldTrackSideEffects) {
  function useFiber(fiber, pendingProps) {
    const clone = createWorkInProgress(fiber, pendingProps)
    clone.sibling = null
    return clone
  }
  
  function reconcileSingleTextNode(returnFiber, oldFiber, text) {
    if (oldFiber !== null && oldFiber.tag === HostText) {
      deleteChild(returnFiber, oldFiber.sibling)
      const existing = useFiber(oldFiber, text)
      existing.return = returnFiber
      return existing
    }
    deleteChild(returnFiber, oldFiber)
    const fiber = new FiberNode(HostText, text)
    fiber.return = returnFiber
    return placeChild(fiber)
  }
}
3.9.2.3 补充reconcileSingleElement方法diff逻辑
  1. 如果旧节点和newChildkeyelementType都相同,复用旧节点,由于新节点没有兄弟节点,所以需要删除旧节点的兄弟节点
  2. keyelementType不相同则删除旧节点及其兄弟节点,创建新节点
function createChildReconciler(shouldTrackSideEffects) {
  function reconcileSingleElement(returnFiber, oldFiber, newChild) {
    if (
      oldFiber !== null &&
      oldFiber.key === newChild.key &&
      oldFiber.elementType === newChild.type
    ) {
      deleteChild(returnFiber, oldFiber.sibling)
      const fiber = useFiber(oldFiber, newChild.props)
      fiber.return = returnFiber
      return fiber
    }
    deleteChild(returnFiber, oldFiber)
    const fiber = createFiberFormElement(newChild)
    fiber.return = returnFiber
    return placeChild(fiber)
  }
}
3.9.2.4 补充reconcileChildrenArray方法diff逻辑

采用线性比对的思想,即判断当前新旧节点是否相同,相同则复用,不同则创建新节点,将旧节点标记删除,然后继续比对下一个节点

function createChildReconciler(shouldTrackSideEffects) {
  function reconcileChildrenArray(returnFiber, oldFiber, newChild) {
    let firstChildFiber = null
    let prevChildFiber = null
    for (let newIdx = 0; newIdx < newChild.length; newIdx++) {
      let nextFiber = null
      if (typeof newChild[newIdx] === 'object' && newChild[newIdx] !== null) {
        const { key, type, props } = newChild[newIdx]
        if (
          oldFiber !== null &&
          oldFiber.key === key &&
          oldFiber.elementType === type
        ) {
          nextFiber = placeChild(useFiber(oldFiber, props))
        } else {
          deleteChild(returnFiber, oldFiber, false)
          nextFiber = placeChild(createFiberFormElement(newChild[newIdx]))
        }
      } else if (typeof newChild[newIdx] === 'string') {
        if (oldFiber !== null && oldFiber.tag === HostText) {
          nextFiber = placeChild(useFiber(oldFiber, newChild[newIdx]))
        } else {
          deleteChild(returnFiber, oldFiber, false)
          nextFiber = placeChild(creaetFiberFromText(newChild[newIdx]))
        }
      } else {
        if (oldFiber) deleteChild(returnFiber, oldFiber, false)
      }
      if (oldFiber) oldFiber = oldFiber.sibling
      if (nextFiber !== null) {
        nextFiber.return = returnFiber
        // 记录第一个child FiberNode节点
        if (firstChildFiber === null) {
          firstChildFiber = nextFiber
        }
        // 建立兄弟FiberNode节点关联关系
        if (prevChildFiber === null) {
          prevChildFiber = nextFiber
        } else {
          prevChildFiber.sibling = nextFiber
          prevChildFiber = nextFiber
        }
      }
    }
    return firstChildFiber
  }
3.9.2.5 修改reconcileChildFibers方法

如果nextChild既不是对象也不是字符串,则将旧节点删除掉

function createChildReconciler(shouldTrackSideEffects) {
  function reconcileChildFibers(workInProgress, currentFirstChild, nextChildren) {
    if (typeof newChild === 'object' && newChild !== null) {
      ...
    }
    if (typeof newChild === 'string') {
      ...
    }
    return deleteChild(returnFiber, currentFirstChild)
  }
}

3.10 构建DOM

3.10.1 补充completeWork方法代码逻辑

对于HostComponentHostText类型的FiberNode节点添加current不为null时对应的处理逻辑,current不为null说明在旧FiberNode Tree中存在新FiberNode节点对应的旧FiberNode节点,且已有DOM节点,所以不需要在走构建DOM树逻辑,只需要在更新DOM阶段,遍历FiberNode节点,执行对应的副作用处理逻辑即可

function completeWork(workInProgress) {
  const current = workInProgress.alternate
  switch (workInProgress.tag) {
    case HostComponent: {
      if (current != null) {
        if (workInProgress.pendingProps) {
          workInProgress.flags |= Update
        }
        bubbleProperties(workInProgress)
        return
      }
      // 创建DOM节点
      const { elementType, pendingProps } = workInProgress
      const instance = document.createElement(elementType)
      // 将DOM节点赋值给FiberNode的stateNode属性
      workInProgress.stateNode = instance
      // 设置DOM节点属性
      for (const propKey in pendingProps) {
        const prpoValue = pendingProps[propKey]
        setProp(instance, propKey, prpoValue)
      }
      // 递归遍历FiberNode节点将对应的DOM节点添加到父DOM节点中,构建DOM树
      appendAllChildren(instance, workInProgress)
      // 收集子树FiberNode节点副作用
      bubbleProperties(workInProgress)
      return null
    }
    case HostText: {
      if (current !== null) {
        if (workInProgress.pendingProps !== current.pendingProps) {
          workInProgress.flags |= Update
        }
        bubbleProperties(workInProgress)
        return null
      }
      const { pendingProps } = workInProgress
      const instance = document.createTextNode(pendingProps)
      workInProgress.stateNode = instance
      bubbleProperties(workInProgress)
      return null
    }
  }
}

3.11 更新DOM

3.11.1 添加删除逻辑

遍历FiberNode节点的deletions属性中的节点,从DOM树中移除相应DOM节点

// 获取子DOM节点
function getChildNode(finishWork) {
  let childFiber = finishWork.child
  while (childFiber !== null) {
    if (childFiber.tag === FunctionComponent) {
      childFiber = childFiber.child
      continue
    }
    break
  }
  return childFiber.stateNode
}

function recursivelyTraverseMutationEffects(finishWork) {
  if (finishWork.deletions !== null) {
    const parentNode =
      finishWork.tag === HostComponent
        ? finishWork.stateNode
        : getParentNode(finishWork)
    // 将旧节点对应的DOM节点从DOM树移除
    finishWork.deletions.forEach((fiber) => {
      const childNode = getChildNode(fiber)
      parentNode.removeChild(childNode)
    })
  }
  // 为true说明子树FiberNode节点有副作用需要处理,递归遍历child FiberNode
  if (finishWork.subtreeFlags & (Placement | Update | ChildDeletion)) {
    let child = finishWork.child
    while (child) {
      commitMutationEffectsOnFiber(child)
      child = child.sibling
    }
  }
}

3.11.2 添加插入DOM节点逻辑

function commitHostPlacement(finishWork) {
  const parentNode = getParentNode(finishWork)
  parentNode.appendChild(finishWork.stateNode)
}

function commitReconciliationEffects(finishWork) {
  // 为true说明需要执行插入DOM节点逻辑
  if (finishWork.flags & Placement) {
    commitHostPlacement(finishWork)
  }
}

function commitMutationEffectsOnFiber(finishWork) {
  switch (finishWork.tag) {
    case HostComponent: {
      recursivelyTraverseMutationEffects(finishWork)
      commitReconciliationEffects(finishWork)
      break
    }
    case HostText: {
      commitReconciliationEffects(finishWork)
      break
    }
  }
}

3.11.3 添加更新逻辑

function commitMutationEffectsOnFiber(finishWork) {
  switch (finishWork.tag) {
    case HostComponent: {
      recursivelyTraverseMutationEffects(finishWork)
      commitReconciliationEffects(finishWork)
      // 更新DOM属性
      if (finishWork.flags & Update) {
        const { pendingProps, stateNode } = finishWork
        for (const propKey in pendingProps) {
          const prpoValue = pendingProps[propKey]
          setProp(stateNode, propKey, prpoValue)
        }
      }
      break
    }
    case HostText: {
      commitReconciliationEffects(finishWork)
      // 修改文本内容
      if (finishWork.flags & Update) {
        finishWork.stateNode.textContent = finishWork.pendingProps
      }
      break
    }
  }
}

四. 总结

React每次更新渲染都会构建虚拟DOM树,与旧虚拟DOM树进行比对,获取节点需要处理的副作用,在更新DOM阶段,遍历虚拟DOM树节点,执行副作用处理逻辑,完成DOM树的更新。代码仓库