代码已上传:gitee.com/eamonyang/m...
一,实现原生标签,class 组件和函数组件的初次渲染
- 创建根节点,调用 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 />)
- 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
- 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
- workloop循环处理 fiber,每次都会判断剩余时间,决定是否暂停
- workloop 每次执行,都会开启单元工作performUnitOfWork,所有的 fiber 执行完成后,进行 commit
- 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
- 在初次渲染阶段,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
- 在初次渲染阶段,调度子节点,主要就是根据传入的 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
}
}
- 根据不同类型,操作也有所不同,原生标签就是更新属性,文本节点就是创建节点并插入内容,函数组件从 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)
}
- 其他方法,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 调度
- 使用最小堆的数据结构保存任务列表
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
}
- 使用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)
}
}
}
- 修改 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 流程如下
- 第一轮遍历新节点, 比较旧节点能否复用, 如果不能复用,跳出循环
- 检查是否完成了新节点的遍历, 如果已经遍历完成了, 就删除剩余的旧节点
- 初次渲染, 如果没有对应的旧节点, 就是初次渲染, 将 newChildren 数组中的每一个元素都生成一个 fiber 对象, 然后串联
- 如果第一轮遍历跳出以后, 可能是新节点全都经过了遍历, 也可能是由于不能复用而提前跳出循环
-
- 将剩余的旧节点放入到map, 便于后续调用
- 遍历剩余的新节点, 通过新节点的key去map中查找有没有能复用的节点, 如果找到了, 就复用, 然后从map中删除
- 剩余的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))
}
}
// ...