实现 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))
  }
}

// ...
相关推荐
tech_zjf几秒前
装饰器:给你的代码穿上品如的衣服
前端·typescript·代码规范
xiejianxin5201 分钟前
如何封装axios和取消重复请求
前端·javascript
parade岁月2 分钟前
从学习ts的三斜线指令到项目中声明类型的最佳实践
前端·javascript
狼性书生4 分钟前
electron + vue3 + vite 渲染进程与渲染进程之间的消息端口通信
前端·javascript·electron
阿里巴巴P8资深技术专家4 分钟前
使用vue3.0+electron搭建桌面应用并打包exe
前端·javascript·vue.js
coder_leon8 分钟前
Vite打包优化实践:从分包到性能提升
前端
shmily_yyA8 分钟前
【2025】Electron 基础一 (目录及主进程解析)
前端·javascript·electron
吞吞071111 分钟前
浅谈前端性能指标、性能监控、以及搭建性能优化体系
前端
arcsin112 分钟前
雨水-electron项目实战登录
前端·electron·node.js
卑微小文21 分钟前
企业级IP代理安全防护:数据泄露风险的5个关键防御点
前端·后端·算法