实现 miniReact

代码已上传:gitee.com/eamonyang/m...

一,实现原生标签,class 组件和函数组件的初次渲染

  1. 创建根节点,调用 render 方法
javascript 复制代码
// main.jsx
import ReactDOM from "./lib/react-dom/ReactDOM";
import App from './App.jsx'

const root = ReactDOM.createRoot(document.getElementById('root'))

root.render(<App />)
  1. render 中传入内容,调用更新容器的方法 updateContaienr
javascript 复制代码
// react-dom/ReactDOM.js
import createFiber from "../reconciler/ReactFiber";
import scheduleUpdateOnFiber from '../reconciler/ReactFiberWorkLoop'

/**
 * 更新容器
 * @param {*} element 要挂载的 vnode 树
 * @param {*} container 容器DOM 节点
 */
function updateContainer(element, container) {
  const fiber = createFiber(element, {
    type: container.nodeName.toLowerCase(),
    stateNode: container
  })
  scheduleUpdateOnFiber(fiber)
}

class ReactDOMRoot {
  constructor(container) {
    this._internalRoot = container
  }
  render(children) {
    updateContainer(children, this._internalRoot)
  }
}

const ReactDOM = {
  
  /**
   *
   * @param {*} container 要挂载的根 DOM 节点
   * @return {*} 返回一个对象,上面有 render 方法
   */
  createRoot(container) {
    return new ReactDOMRoot(container)
  }
}

export default ReactDOM
  1. updateContaienr中创建 fiber,并调用 scheduleUpdateOnFiber 开始调度 workloop
typescript 复制代码
// reconciler/ReactFiber.js
import { Placement, isStr, isFn, isUndefined } from "../shared/utils";

import {FunctionComponent, ClassComponent, HostComponent, HostText, Fragment} from "./ReactWorkTags";

/**
 *
 * @param {*} vnode 当前节点的 vnode 节点
 * @param {*} returnFiber 父 fiber节点
 * @return {*}  创建的 fiber 对象
 */
function createFiber(vnode, returnFiber) {
  const fiber = {
    type: vnode.type, // 类型
    key: vnode.key, // key
    props: vnode.props, // props
    stateNode: null, // 对应的 DOM节点
    child: null, // 子 fiber
    sibling: null, // 兄弟 fiber
    return: returnFiber, // 父 fiber
    flags: Placement, // 操作标记
    index: null, // 当前节点在当前层级的位置
    alternate: null, // 旧的 fiber对象
    memorizedState: null // 不同类型的 hooks,存储的内容也不同
  }
  const type = vnode.type
  if (isStr(type)) {
    // 原生标签
    fiber.tag = HostComponent
  } else if (isFn(type)) {
    // 函数类型,根据isReactComponent区分是函数组件还是类组件
    if (type.prototype.isReactComponent) {
      fiber.tag = ClassComponent
    } else {
      fiber.tag = FunctionComponent
    }
  } else if (isUndefined(type)) {
    // 文本节点,需要手动添加 props
    fiber.tag = HostText
    fiber.props = {
      children: vnode
    }
  } else {
    fiber.tag = Fragment
  }
  // console.log('createFiber', fiber.type);
  return fiber
}

export default createFiber
  1. workloop循环处理 fiber,每次都会判断剩余时间,决定是否暂停
  2. workloop 每次执行,都会开启单元工作performUnitOfWork,所有的 fiber 执行完成后,进行 commit
  3. performUnitOfWork 按照深度优先,依次对节点执行 beginWork 和 completeWork
javascript 复制代码
// reconciler/ReactFiberWorkLoop.js
// react 的整体执行流程
import beginWork from './ReactFiberBeginWork'
import completeWork from './ReactFiberCompleteWork'
import commitWork from './ReactFiberCommitWork'
let wip = null // work in progress 表示正在进行工作的 fiber
let wipRoot = null // 根节点的fiber 对象

// 先使用 requestIdleCallback 进行模拟调度
function scheduleUpdateOnFiber(fiber) {
  wip = fiber
  wipRoot = fiber
  requestIdleCallback(workloop)
}

// 浏览器每一帧有空闲的时候,执行 workLoop
function workloop(deadline) {
  // 有剩余 fiber 需要处理,且还有剩余时间
  while (wip && deadline.timeRemaining() > 0) {
    performUnitOfWork();
  }
  // 整个 fiber 树全都处理完成了,提交到 DOM 节点
  if (!wip) {
    commitRoot()
  }
}
/*
  处理一个fiber 节点
  1,处理当前的 fiber 对象
  2,深度优先遍历子节点
*/
function performUnitOfWork() {
  beginWork(wip)
  // 如果有子节点,就处理子节点
  if (wip.child) {
    wip = wip.child
    return
  }
  completeWork(wip)
  // 如果没有子节点,就找到当前兄弟节点
  let next = wip
  while (next) {
    if (next.sibling) {
      wip = next.sibling
      return
    }
    // 没有兄弟节点了,就去父亲那一层继续寻找兄弟节点
    next = next.return
    completeWork(next)
  }
  wip = null
}

// 执行该方法的时候,说明协调工作已经完成,进入到渲染阶段
function commitRoot() {
  commitWork(wipRoot)
  wipRoot = null
}

export default scheduleUpdateOnFiber
  1. 在初次渲染阶段,beginWork 就是根据传入的 fiber 的 tag,判断是不同类型的节点,执行不同的操作,主要的操作内容就是:创建 dom 节点,更新节点属性,调度子节点
javascript 复制代码
// reconciler/ReactFiberBeginWork.js
import {FunctionComponent, ClassComponent, HostText, HostComponent,Fragment} from "./ReactWorkTags";
import {updateHostComponent, updateHostTextComponent, updateFunctionComponent, updateClassComponent} from "./ReactFiberReconciler";


/**
 * 根据 fiber 的 tag 调用不同的方法处理
 * @param {*} wip
 */
function beginWork(wip) {
  const tag = wip.tag
  switch (tag) {
    case HostComponent: {
      updateHostComponent(wip)
      break
    }
    case FunctionComponent: {
      updateFunctionComponent(wip)
      break
    }
    case ClassComponent: {
      updateClassComponent(wip)
      break
    }
    case HostText: {
      updateHostTextComponent(wip)
      break
    }
    case Fragment: {
      break
    }
  }
}

export default beginWork
  1. 在初次渲染阶段,调度子节点,主要就是根据传入的 vnode 生成新的 fiber,然后和之前的 fiber 插入链表进行连接
javascript 复制代码
// reconciler/ReactChildFiber.js
import {isStr, isArray} from '../shared/utils'
import createFiber from './ReactFiber'


/**
 * 协调子节点,和 diff
 * @export
 * @param {*} returnFiber 父 fiber
 * @param {*} children 子节点的vnode 数组
 */
export function reconcileChildren(returnFiber, children) {
  // 文本节点,已经在 updateNode 中处理过了,直接跳过
  if (isStr(children)) {
    return
  }
  // 转为数组
  const newChildren = isArray(children) ? children : [children]
  // 上一个 fiber 对象
  let previousNewFiber = null
  // 上一个 fiber 对象对应的旧 fiber 对象
  let oldFiber = returnFiber.alternate?.child
  let i = 0; // children 的索引
  let lastPlacedIndex = 0 // 上一次 DOM 节点插入的最远位置
  let shouldTrackSideEffects = !!returnFiber.alternate // 是否要追踪副作用
  for (; oldFiber && i < newChildren.length; i++) {
    // 第一次是不会进入该循环的,因为没有 oldFiber
  }
  if (i === newChildren.length) {
    // 如果还有旧的 fiber 节点,需要将其删除
  }
  // 初次渲染
  if (!oldFiber) {
    // 将 newChildren 中每一个元素生成一个 fiber 对象,并串联起来
    for (; i < newChildren.length; i++) {
      const newChildVnode = newChildren[i]
      if (newChildVnode === null) {
        continue
      }
      // 根据 vnode 生成新的 fiber
      const newFiber = createFiber(newChildVnode, returnFiber)
      // 更新lastPlacedIndex
      lastPlacedIndex = placeChild(
        newFiber,
        lastPlacedIndex,
        i,
        shouldTrackSideEffects
      )
      // 将新生成的 fiber 插入到 fiber 链表中
      if (previousNewFiber === null) {
        // 是第一个子节点
        returnFiber.child = newFiber
      } else {
        // 不是第一个子节点
        previousNewFiber.sibling = newFiber
      }
      previousNewFiber = newFiber
    }
  }
}


/**
 * 更新lastPlacedIndex
 * @export
 * @param {*} newFiber 上面新创建的 fiber
 * @param {*} lastPlacedIndex 上一次的lastPlacedIndex,初始为 0
 * @param {*} newIndex 当前的下表,初始也是 0
 * @param {*} shouldTrackSideEffect // 用于判断 returnFiber 是初次渲染还是更新
 */
export function placeChild(newFiber, lastPlacedIndex, newIndex, shouldTrackSideEffect) {
  newFiber.index = newIndex
  if (!shouldTrackSideEffect) {
    // 初次渲染,就不需要记录节点位置了
    return lastPlacedIndex
  }
}
  1. 根据不同类型,操作也有所不同,原生标签就是更新属性,文本节点就是创建节点并插入内容,函数组件从 fiber 上获取到 type 和 props,type 调用传入 props 就获取到了 children,接着进行子节点的调度。class 组件 fiber 上的 type 就是 class,实例化之后调用实例上面的 render方法,就得到了 children,然后进行子节点的调度。
javascript 复制代码
// reconciler/ReactFiberReconciler.js
import {updateNode} from '../shared/utils'
import {reconcileChildren} from './ReactChildFiber'

/**
 *
 * @param {*} wip 需要处理的 fiber 节点,这里是 HostComponent
 */
export function updateHostComponent(wip) {
  if (!wip.stateNode) {
    wip.stateNode = document.createElement(wip.type)
    // 更新节点属性
    updateNode(wip.stateNode, {}, wip.props)
  }
  // 处理子节点
  reconcileChildren(wip, wip.props.children)
}

/**
 * 更新文本节点
 * @export
 * @param {*} wip
 */
export function updateHostTextComponent(wip){
  console.log(wip, 'HostTextComponent');
  wip.stateNode = document.createTextNode(wip.props.children)
}


/**
 * 更新函数组件
 * @export
 * @param {*} wip
 * 从wip 上获取的 type 就是这个函数,调用后获取到 children,传入reconcileChildren执行
 */
export function updateFunctionComponent(wip) {
  console.log('updateFunctionComponent');
  const {type, props} = wip
  const children = type(props)
  reconcileChildren(wip, children)
}

/**
 * 更新类组件
 * @export
 * @param {*} wip
 * 从 wip 上获取的 type 就是这个类,实例化以后调用其上的 render 方法获取到 children
 */
export function updateClassComponent(wip) {
  console.log('updateClassComponent');
  const {type, props} = wip
  const instance = new type(props)
  const children = instance.render()
  reconcileChildren(wip, children)

}
  1. 其他方法,shared 和 react
javascript 复制代码
// shared/utils.js
// 存放工具方法的文件

/**
 * 对 fiber 对象要做的操作进行的标记
 */

// 没有任何操作
export const NoFlags = 0b00000000000000000000;
// 节点新增、插入、移动
export const Placement = 0b0000000000000000000010; // 2
// 节点更新属性
export const Update = 0b0000000000000000000100; // 4
// 删除节点
export const Deletion = 0b0000000000000000001000; // 8

/**
 * 判断参数 s 是否为字符串
 * @param {*} s
 * @returns
 */
export function isStrOrNum(s) {
  return typeof s === "string" || typeof s === "number";
}

/**
 * 判断参数 fn 是否为函数
 * @param {*} fn
 * @returns
 */
export function isFn(fn) {
  return typeof fn === "function";
}

/**
 * 判断参数 s 是否为 undefined
 * @param {*} s
 * @returns
 */
export function isUndefined(s) {
  return s === undefined;
}

/**
 * 判断参数 arr 是否为数组
 * @param {*} arr
 * @returns
 */
export function isArray(arr) {
  return Array.isArray(arr);
}


/**
 * 更新DOM 节点上的属性
 * @export
 * @param {*} node 真实DOM 节点
 * @param {*} prevValue 旧值
 * @param {*} nextVal 新值
 * 对旧值和新值分别进行处理
 */
export function updateNode(node, prevValue, nextVal) {
  // 处理旧值
  Object.keys(prevValue).forEach(k => {
    if (k === 'children') {
      // 如果 children 是字符串,就说明是文本节点,置空
      if (isStrOrNum(prevValue[k])) {
        node.textContent = ''
      }
    } else if (k.startsWith('on')) {
      // 绑定事件,将其移除,注意 change 事件背后绑定的是input
      let eventName = k.slice(2).toLocaleLowerCase()
      if (eventName === 'change') {
        eventName = 'input'
      }
      node.removeEventListener(eventName, prevValue[k])
    } else {
      // 普通属性,如果新值中不存在这个属性,就将其移除
      if (!(k in nextVal)) {
        node[k] = ''
      }
    }
  })
  // 处理新值,流程和上面一样
  Object.keys(nextVal).forEach(k => {
    if (k === 'children') {
      if (isStrOrNum(nextVal[k])) {
        node.textContent = nextVal[k]
      }
    } else if (k.startsWith('on')) {
      let eventName = k.slice(2).toLocaleLowerCase()
      if (eventName === 'change') {
        eventName = 'input'
      }
      node.addEventListener(eventName, nextVal[k])
    } else {
      node[k] = nextVal[k]
    }
  })
}
javascript 复制代码
// react/React.js
function Component(props) {
  this.props = props
}

Component.prototype.isReactComponent = true

const React = {
  Component
}

export default React
ini 复制代码
// reconciler/ReactWorkTag
/**
 * 不同类型的 fiber 对象,有不同的 tag 值
 */
export const FunctionComponent = 0; // 函数组件
export const ClassComponent = 1; // 类组件
export const IndeterminateComponent = 2; // 初始化的时候不知道是函数组件还是类组件
export const HostRoot = 3; // Root Fiber 可以理解为根元素 , 通过reactDom.render()产生的根元素
export const HostPortal = 4; // 对应  ReactDOM.createPortal 产生的 Portal
export const HostComponent = 5; // dom 元素 比如 <div>
export const HostText = 6; // 文本节点
export const Fragment = 7; // 对应 <React.Fragment>
export const Mode = 8; // 对应 <React.StrictMode>
export const ContextConsumer = 9; // 对应 <Context.Consumer>
export const ContextProvider = 10; // 对应 <Context.Provider>
export const ForwardRef = 11; // 对应 React.ForwardRef
export const Profiler = 12; // 对应 <Profiler/ >
export const SuspenseComponent = 13; // 对应 <Suspense>
export const MemoComponent = 14; // 对应 React.memo 返回的组件
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const ScopeComponent = 21;
export const OffscreenComponent = 22;
export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;

二,实现scheduler 调度

  1. 使用最小堆的数据结构保存任务列表
scss 复制代码
// scheduler/SchedulerMinHeap
// 最小堆的实现
/*
  堆是一种完全二叉树,它的的每个节点都小于等于它的子节点。
  在js中可以用数组来实现最小堆。
  对于数组索引为index的节点,它的父节点索引是Math.floor(index - 1) / 2,
  左右子节点的索引分别是2 * index + 1和2 * index + 2。
  最小堆的堆顶元素是最小的值,所以获取最小值的时间复杂度是O(1)。
  对应到我们的任务队列中,堆顶就是过期时间最小(需要最早执行)的任务。
*/

// 返回第一个任务,也就是最紧急的任务
export function peek(heap) {
  return heap.length === 0 ? null : heap[0]
}

// 推入一个任务到最后, 然后调整到合适的位置
export function push(heap, task) {
  const index = heap.length
  heap.push(task)
  siftUp(heap, task, index)
}

// 弹出一个任务,然后将最后一个任务移动到最前面,然后向下调整到合适的位置
export function pop(heap) {
  if (heap.length === 0) {
    return null
  }
  const first = heap[0]
  const last = heap.pop()
  if (first !== last) {
    heap[0] = last
    siftDown(heap, last, 0)
  }
  return first
}

// 向上调整
function siftUp(heap, node, i) {
  let index = i
  while (index > 0) {
    const parentIndex = index - 1 >> 1
    const parent = heap[parentIndex]
    if (compare(parent, node) > 0) {
      heap[parentIndex] = node
      heap[index] = parent
      index = parentIndex
    } else {
      return
    }
  }
}

// 向下调整
function siftDown(heap, node, i) {
  let index = i
  const length = heap.length
  const halfLen = length >> 1
  while(index < halfLen) {
    const leftIndex = index * 2 + 1
    const rightIndex = index * 2 + 2
    const left = heap[leftIndex]
    const right = rightIndex < length ? heap[rightIndex] : undefined
    if (compare(left, node) < 0) {
      if (rightIndex < length && compare(right, left) < 0) {
        heap[index] = right
        heap[rightIndex] = node
        index = rightIndex
      } else {
        heap[index] = left
        heap[leftIndex] = node
        index = leftIndex
      }
    } else if (right !== undefined && compare(right, node) < 0) {
      heap[index] = right
      heap[rightIndex] = node
      index = rightIndex
    } else {
      return
    }
  }
}

function compare(a, b) {
  const diff = a.sortIndex - b.sortIndex
  return diff !== 0 ? diff : a.id - b.id
}
  1. 使用MessageChannel实现调度器
javascript 复制代码
// 调度器的具体实现,简化为单任务队列,实际还有异步队列,和 lane 模型的优先级

import {peek, pop, push} from './SchedulerMinHeap'
import {getCurrentTime} from '../shared/utils'

// 任务队列
const taskQueue = []
// 任务计数
let taskIdCounter = 1
// 是否还有剩余时间
const hasTimeRemaining = true

// 通过MessageChannel实现
const {port1, port2} = new MessageChannel()

// 组装一个任务对象,放入任务队列
export default function scheduleCallback(callback) {
  // 获取当前时间
  const currentTime = getCurrentTime()
  // 简化为所有的优先级都相同,实际会根据不同的任务类型,设置不同的过期时间
  const timeout = -1
  // 过期时间
  const expirationTime = currentTime + timeout
  // 任务对象
  const newTask = {
    id: taskIdCounter++,
    callback,
    expirationTime,
    sortIndex: expirationTime
  }
  // 推入任务队列,然后开始调度,产生一个宏任务
  push(taskQueue, newTask)
  port1.postMessage(null)
}

// 每次port1.postMessage(null)时,就会触发port2.onmessage
port2.onmessage = function() {
  const currentTime = getCurrentTime()
  let currentTask = peek(taskQueue)
  while(currentTask) {
    // 如果还没到过期时间,且没有剩余时间了,就跳出循环,结束本轮任务
    if (currentTask.expirationTime > currentTime && !hasTimeRemaining) {
      break
    }
    // 说明任务是需要被执行的
    const callback = currentTask.callback
    currentTask.callback = null
    // 执行任务,传入剩余时间
    const taskResult = callback(currentTime - currentTask.expirationTime)
    if (taskResult === undefined) {
      // 说明任务执行完了,将其从任务队列删除,取出下一个任务执行
      pop(taskQueue)
      currentTask = peek(taskQueue)
    }
  }
}
  1. 修改 workLoop
scss 复制代码
function workloop(time) {
  while(wip) {
    if (time < 0) {
      return false
    }
    performUnitOfWork()
  }
  if (!wip && wipRoot) {
    commitRoot()
  }
}

三,实现diff算法

对ReactChildFiber 文件进行修改,之前在子节点调度的步骤中只处理了初次渲染的情况,现在处理更新的情况,进行 diff,react 中的 diff 算法存在三条优化策略

  • 只对同级节点进行 diff, 如果是不同层级的节点,就不会尝试进行复用
  • 如果节点元素的类型不相同,就直接认为是不同的节点,不会尝试进行复用
  • 通过 key 暗示可以保持稳定的元素

整体的 diff 流程如下

  1. 第一轮遍历新节点, 比较旧节点能否复用, 如果不能复用,跳出循环
  2. 检查是否完成了新节点的遍历, 如果已经遍历完成了, 就删除剩余的旧节点
  3. 初次渲染, 如果没有对应的旧节点, 就是初次渲染, 将 newChildren 数组中的每一个元素都生成一个 fiber 对象, 然后串联
  4. 如果第一轮遍历跳出以后, 可能是新节点全都经过了遍历, 也可能是由于不能复用而提前跳出循环
    1. 将剩余的旧节点放入到map, 便于后续调用
    2. 遍历剩余的新节点, 通过新节点的key去map中查找有没有能复用的节点, 如果找到了, 就复用, 然后从map中删除
  1. 剩余的map节点就没用了, 删除

修改后的代码如下

ini 复制代码
// ReactChildFiber
import {isStrOrNum, isArray, Update} from '../shared/utils'
import createFiber from './ReactFiber'
import {sameNode, placeChild, deleteChild, deleteRemainingChildren, mapRemainingChildren} from './ReactChildFiberAssistant'


/**
 * 协调子节点,和 diff
 * @export
 * @param {*} returnFiber 父 fiber
 * @param {*} children 子节点的vnode 数组
 */
export function reconcileChildren(returnFiber, children) {
  // 文本节点,已经在 updateNode 中处理过了,直接跳过
  if (isStrOrNum(children)) {
    return
  }
  // 转为数组
  const newChildren = isArray(children) ? children : [children]
  // 上一个 fiber 对象
  let previousNewFiber = null
  // 上一个 fiber 对象对应的旧 fiber 对象
  let oldFiber = returnFiber.alternate?.child
  let i = 0; // children 的索引
  let lastPlacedIndex = 0 // 上一次 DOM 节点插入的最远位置
  let shouldTrackSideEffects = !!returnFiber.alternate // 是否要追踪副作用

  // 该变量有两个作用:1. 存储下一个旧的 fiber 对象 2. 临时存储当前的旧的 fiber 对象
  let nextOldFiber = null;

  // 1, 第一轮遍历新节点, 比较旧节点能否复用, 如果不能复用,跳出循环
  for (; oldFiber && i < newChildren.length; i++) {
    // 第一次是不会进入该循环的,因为没有 oldFiber
    const newChild = newChildren[i]
    if (!newChild) {
      continue
    }
    // 旧节点的索引大于当前新节点, 有可能是因为顺序变化了, 需要保留住这个节点, 这样在第二轮遍历的时候,还有可能被复用
    if (oldFiber.index > i) {
      nextOldFiber = oldFiber
      oldFiber = null
    } else {
      nextOldFiber = oldFiber.sibling
    }
    // 判断能否复用
    const same = sameNode(newChild, oldFiber)
    if (!same) {
      if (oldFiber === null) {
        // 将oldFiber原本的值还原,后面还要用
        oldFiber = nextOldFiber
      }
      break
    }
    // 说明可以复用
    const newFiber = createFiber(newChild, returnFiber)
    Object.assign(newFiber, {
      stateNode: oldFiber.stateNode,
      alternate: oldFiber,
      props: newChild.props,
      flags: Update
    })
    lastPlacedIndex = placeChild(
      newFiber,
      lastPlacedIndex,
      i,
      shouldTrackSideEffects
    )
    // 如果是第一个节点, 它就是父节点的child, 否则就是上一个节点的兄弟
    if (previousNewFiber === null) {
      returnFiber.child = newFiber
    } else {
      previousNewFiber.sibling = newFiber
    }
    // 更新上一个节点为当前节点
    previousNewFiber = newFiber
    // oldFiber为下一个节点的信息
    oldFiber = nextOldFiber
  }
  // 2, 检查是否完成了新节点的遍历, 如果已经遍历完成了, 就删除剩余的旧节点
  if (i === newChildren.length) {
    // 4.1 如果还有旧的 fiber 节点,需要将其删除, return结束遍历
    deleteRemainingChildren(returnFiber, oldFiber)
    return
  }
  // 3, 初次渲染, 如果没有对应的旧节点, 就是初次渲染, 将 newChildren 数组中的每一个元素都生成一个 fiber 对象, 然后串联

  if (!oldFiber) {
    // 将 newChildren 中每一个元素生成一个 fiber 对象,并串联起来
    for (; i < newChildren.length; i++) {
      const newChildVnode = newChildren[i]
      if (!newChildVnode) {
        continue
      }
      // 根据 vnode 生成新的 fiber
      const newFiber = createFiber(newChildVnode, returnFiber)
      // 更新lastPlacedIndex
      lastPlacedIndex = placeChild(
        newFiber,
        lastPlacedIndex,
        i,
        shouldTrackSideEffects
      )
      // 将新生成的 fiber 插入到 fiber 链表中
      if (previousNewFiber === null) {
        // 是第一个子节点
        returnFiber.child = newFiber
      } else {
        // 不是第一个子节点
        previousNewFiber.sibling = newFiber
      }
      previousNewFiber = newFiber
    }
  }

  // 4.2 遍历剩余的新节点, 通过新节点的key去map中查找有没有能复用的节点, 如果找到了, 就复用, 然后从map中删除
  // 处理新旧节点都还有剩余的情况, 创建一个map, 存储旧的剩余节点
  const existingChildren = mapRemainingChildren(oldFiber)
  for (; i < newChildren.length; i++) {
    const newChild = newChildren[i]
    if (newChild === null) {
      continue
    }
    const newFiber = createFiber(newChild, returnFiber)
    const matchedFiber = existingChildren.get(newFiber.key || newFiber.index)
    if (matchedFiber) {
      Object.assign(newFiber, {
        stateNode: matchedFiber.stateNode,
        alternate: matchedFiber.alternate,
        props: newChild.props,
        flags: Update
      })
      existingChildren.delete(newFiber.key || newFiber.index)
    }
    // 更新 lastPlacedIndex 的值
    lastPlacedIndex = placeChild(
      newFiber,
      lastPlacedIndex,
      i,
      shouldTrackSideEffects
    );
    // 形成链表
    if (previousNewFiber === null) {
      returnFiber.child = newFiber
    } else {
      previousNewFiber.sibling = newFiber
    }
    previousNewFiber = newFiber
  }
  // 5, 剩余的map节点就没用了, 删除
  if (shouldTrackSideEffects) {
    existingChildren.forEach((child) => {
      deleteChild(returnFiber, child);
    });
  }
}

ReactChildFiberAssistant.js文件代码如下

javascript 复制代码
import {Placement} from "../shared/utils";

/**
 * 判断节点能否复用
 * @export
 * @param {*} a
 * @param {*} b
 * @return {*} 
 * 同一层级, type 和 key 都相等
 */
export function sameNode(a, b) {
  return a && b && a.type === b.type && a.key === b.key
}

/**
 * 更新lastPlacedIndex
 * @export
 * @param {*} newFiber 上面新创建的 fiber
 * @param {*} lastPlacedIndex 上一次的lastPlacedIndex,也就是上一次插入的最远位置, 初始为 0
 * @param {*} newIndex 当前的下表,初始也是 0
 * @param {*} shouldTrackSideEffect // 用于判断 returnFiber 是初次渲染还是更新
 * lastPlacedIndex这个值的变化,反映了是修改还是移动
 */
export function placeChild(newFiber, lastPlacedIndex, newIndex, shouldTrackSideEffect) {
  newFiber.index = newIndex
  if (!shouldTrackSideEffect) {
    // 初次渲染,就不需要记录节点位置了
    return lastPlacedIndex
  }
  // 旧的Fiber节点
  const current = newFiber.alternate
  if (current) {
    const oldIndex = current.index
    if (oldIndex < lastPlacedIndex) {
      newFiber.flags |= Placement
      return lastPlacedIndex
    } else {
      return oldIndex
    }
  } else {
    newFiber.flags |= Placement
    return lastPlacedIndex
  }
}
/**
 *
 * @export
 * @param {*} returnFiber 父Fiber
 * @param {*} childToDelete 需要删除的子Fiber
 * 这里的删除只是进行标记, 真正的删除在commit阶段
 */
export function deleteChild(returnFiber, childToDelete) {
  const deletions = returnFiber.deletions
  if (deletions) {
    returnFiber.deletions.push(childToDelete)
  } else {
    returnFiber.deletions = [childToDelete]
  }
}
/**
 * 删除多个节点, 一个个删除
 * @export
 * @param {*} returnFiber
 * @param {*} currentFirstChild
 */
export function deleteRemainingChildren(returnFiber, currentFirstChild) {
  let childToDelete = currentFirstChild
  while(childToDelete) {
    deleteChild(returnFiber, childToDelete)
    childToDelete = childToDelete.sibling
  }
}
/**
 * 将旧子节点存入一个map中
 * @export
 * @param {*} currentFirstChild
 */
export function mapRemainingChildren(currentFirstChild) {
  const existingChildren = new Map()
  let existingChild = currentFirstChild
  while(existingChild) {
    existingChildren.set(existingChild.key || existingChild.index, existingChild)
    existingChild = existingChild.sibling
  }
  return existingChildren
}

四,实现 useReducer 和 useHooks

hook 是一个对象,大致结构如下:

yaml 复制代码
const hooks = {
  memoizedState: null,
  baseState: null,
  baseQueue: null,
  queue: null,
  next: null,
}

不同类型的 hook, memoizedState 保存了不同的值

  • useState:对于 const [state, updateState] = useState(initialState), memoizedState保存的是 state 的值
  • useReducer:对于 const [state, dispatch] = useReducer(reducer, {}), memoizedState保存的是 state 的值
  • useEffect:对于useEffect(callback, [...deps]), memoizedState保存的是 [callback, [...deps])]等数据

一个组件中的 hook 会以链表的形式串起来,FiberNode 的 memoizedState 中保存了 hook 链表中的第一个Hook。

在更新的时候会复用之前的 Hook,如果在 if 语句中增加或者删除 hook,在复用的时候,会产生复用的 hook 和之前的 hooks 不一致的问题。我们在模拟实现的时候就先不考虑复用的情况了。

javascript 复制代码
// /react/reactHooks.js

import scheduleUpdateOnFiber from '../reconciler/ReactFiberWorkLoop'

// 全局变量
let currentlyRenderingFiber = null // 当前渲染的 fiber
let workInProgressHook = null // 当前正在处理的 hook
let currentHook = null // 当前处理完的 hook


/**
 * 对当前 fiber 以及 hooks 初始化
 * @export
 * @param {*} wip
 */
export function renderWithHooks(wip) {
  // 将当前正在渲染的 fiber 对象赋值给 currentlyRenderingFiber
  currentlyRenderingFiber = wip
  currentlyRenderingFiber.memorizedState = null
  workInProgressHook = null
  currentlyRenderingFiber.updateQueue = []
}

/**
 * 返回一个 hook 对象, 并且让workInProgressHook指向 hook 链表的最后一个 hook
 * @return {*} 
 */
function updateWorkInProgressHook() {
  let hook = null
  // 旧的 fiber 对象
  const current = currentlyRenderingFiber.alternate
  if (current) {
    // 有旧的 fiber 对象,说明不是第一次渲染
    currentlyRenderingFiber.memorizedState = current.memorizedState
    if (workInProgressHook) {
      // 链表已存在
      workInProgressHook = hook = workInProgressHook.next
      currentHook = currentHook.next
    } else {
      // 链表不存在
      workInProgressHook = hook = currentlyRenderingFiber.memorizedState
      currentHook = current.memorizedState
    }
  } else {
    // 首次渲染
    hook = {
      memorizedState: null, // 存储数据
      next: null // 指向 下一个hook
    }
    // 说明链表上已经有 hook 了,将刚刚生成的 hook 挂载在链表上
    if (workInProgressHook) {
      workInProgressHook = workInProgressHook.next = hook
    } else {
      // 链表上还没有 hook
      workInProgressHook = currentlyRenderingFiber.memorizedState = hook
    }
  }
  return hook
}

/**
 * 根据传入的 reducer, 计算最新状态
 *
 * @param {*} fiber
 * @param {*} hook
 * @param {*} reducer
 * @param {*} action
 */
function dispatchReducerAction(fiber, hook, reducer, action) {
  // 计算出最新状态并挂载
  hook.memorizedState = reducer ? reducer(hook.memorizedState) : action
  // 状态更新完毕后,该 fiber 就是旧的 fiber
  fiber.alternate = {...fiber}
  // 不去更新相邻节点
  fiber.sibling = null
  scheduleUpdateOnFiber(fiber)
}

export function useState(initialState) {
  return useReducer(null, initialState)
}

/**
 * 
 * @export
 * @param {*} reducer 改变状态的纯函数
 * @param {*} initialState 初始化状态
 * @return {*} 
 */
export function useReducer(reducer, initialState) {
  // 拿到最新的 hook
  const hook = updateWorkInProgressHook()
  // 首次渲染
  if (!currentlyRenderingFiber.alternate) {
    hook.memorizedState = initialState
  }
  const dispatch = dispatchReducerAction.bind(
    null,
    currentlyRenderingFiber,
    hook,
    reducer
  )
  return [hook.memorizedState, dispatch]
}

在updateFunctionComponent时候先处理 hooks

javascript 复制代码
// /reconciler/ReactFiberReconciler.js

// ...
import {renderWithHooks} from '../react/ReactHooks'
// ...
/**
 * 更新函数组件
 * @export
 * @param {*} wip
 * 从wip 上获取的 type 就是这个函数,调用后获取到 children,传入reconcileChildren执行
 */
export function updateFunctionComponent(wip) {
  // 处理 hooks
  renderWithHooks(wip)
  const {type, props} = wip
  const children = type(props)
  reconcileChildren(wip, children)
}

在 commit 的时候,要去根据操作标记进行更新

scss 复制代码
// /reconciler/ReactFiberCommitWork.js

// ...
function commitNode(wip) {
  // 获取父节点的 DOM 对象
  const parentNodeDOM = getParentDOM(wip.return)
  // 从 fiber 对象上拿到 flags 和 stateNode
  const {flags, stateNode} = wip
  if (flags & Placement && stateNode) {
    parentNodeDOM.appendChild(wip.stateNode)
  }
  if (flags & Update && stateNode) {
    updateNode(stateNode, wip.alternate.props, wip.props)
  }
}

五,实现useEffect

useEffect:回调函数会在 commit 阶段完成后异步执行,所以它不会阻塞视图渲染。

我们的基本实现思路就是,在useEffect调用时,将effect放入fiber的updateQueue,在commitWork的时候,创建异步任务。

javascript 复制代码
// src/lib/reconciler/ReactFiberCommitWork.js
import {Placement, Update, updateNode} from '../shared/utils'
import {FunctionComponent} from './ReactWorkTags'
import {invokeHooks} from './ReactChildFiberAssistant'
// 。。。
function commitNode(wip) {
  // 获取父节点的 DOM 对象
  const parentNodeDOM = getParentDOM(wip.return)
  // 从 fiber 对象上拿到 flags 和 stateNode
  const {flags, stateNode} = wip
  if (flags & Placement && stateNode) {
    parentNodeDOM.appendChild(wip.stateNode)
  }
  if (flags & Update && stateNode) {
    updateNode(stateNode, wip.alternate.props, wip.props)
  }
  if (wip.tag === FunctionComponent) {
    invokeHooks(wip)
  }
}
// 。。。

invokeHooks就是将wip的updateQueue上面的任务,先执行清除方法,然后创建任务,放入任务队列

javascript 复制代码
// src/lib/reconciler/ReactChildFiberAssistant.js
import scheduleCallback from "../scheduler/Scheduler";

//...

/**
 * 取出fiber中updateQueue中的effects依次执行
 * @export
 * @param {*} wip
 */
export function invokeHooks(wip) {
  const {updateQueue} = wip
  for (let i = 0; i < updateQueue.length; i++) {
    const effect = updateQueue[i]
    // 先执行清除方法
    if (effect.destroy) {
      effect.destroy()
    }
    // 注意这里并非直接执行,而是创建一个任务,放入到任务队列中
    scheduleCallback(() => {
      effect.destroy = effect.create()
    })
  }
}

接下来看useEffect的实现,其实就是创建了一个effect对象挂载到memorizedState上

java 复制代码
// src/lib/react/ReactHooks.js
import {areHookInputEqual} from '../shared/utils'
// ...

/**
 * @export
 * @param {*} create 要执行的副作用
 * @param {*} deps 依赖项
 */
export function useEffect(create, deps) {
  // 获取最后一个hook
  const hook = updateWorkInProgressHook()
  // 存储上一次的销毁函数
  let destroy = null
  if (currentHook) {
    const prevEffect = currentHook.memorizedState
    destroy = prevEffect.destroy
    if (deps) {
      const prevDeps = prevEffect.deps
      if (areHookInputEqual(deps, prevDeps)) {
        return
      }
    }
  }
  const effect = {create, deps, destroy}
  hook.memorizedState = effect
  // 推入updateQueue,而非直接执行
  currentlyRenderingFiber.updateQueue.push(effect)
}
javascript 复制代码
// src/lib/shared/utils.js
export function getCurrentTime() {
  return performance.now();
}

/**
 * 用于检测依赖项是否都相等
 * @export
 * @param {*} nextDeps
 * @param {*} prevDeps
 * @return {*} 
 */
export function areHookInputEqual(nextDeps, prevDeps) {
  if (prevDeps === null) {
    return false
  }
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (Object.is(nextDeps[i], prevDeps[i])) {
      continue
    }
    return false
  }
  return true
}

六,完成删除DOM

最后就是,在节点都新增或修改以后,wip上还有deletions数组表示为要删除的节点,也是在commit阶段进行删除。

scss 复制代码
// src/lib/reconciler/ReactFiberCommitWork.js

// ...
function commitNode(wip) {
  // 获取父节点的 DOM 对象
  const parentNodeDOM = getParentDOM(wip.return)
  // 从 fiber 对象上拿到 flags 和 stateNode
  const {flags, stateNode} = wip
  if (flags & Placement && stateNode) {
    parentNodeDOM.appendChild(wip.stateNode)
  }
  if (flags & Update && stateNode) {
    updateNode(stateNode, wip.alternate.props, wip.props)
  }
  if (wip.deletions) {
    // 说明有需要删除的节点
    commitDeletion(wip.deletions, stateNode || parentNodeDOM);
  }
  if (wip.tag === FunctionComponent) {
    invokeHooks(wip)
  }
}

// 获取fiber所对应的真实DOM
function getStateNode(fiber) {
  let tmp = fiber;
  while (!tmp.stateNode) {
    tmp = tmp.child
  }
  return tmp.stateNode
}

// deletions就是当前fiber要删除的fieber数组
function commitDeletion(deletions, parentNode) {
  for (let i = 0; i < deletions.length; i++) {
    const child = deletions[i]
    // 有可能函数组件或者类组件没有对应的DOM,就要继续向下寻找
    parentNode.removeChild(getStateNode(child))
  }
}

// ...
相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼9 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax