1. 实现useEffect
(1)首先,需要了解useEffect的执行时机,他执行在React对Dom渲染之后,并且浏览器渲染之前。useEffect接收两个参数,第一个为callback函数,第二个为数组。数组里可以定义绑定依赖的值。
(2)定义一个effectHook对象,将useEffect接收的属性都绑定在对象上,然后将其对象绑定到当前的fiber上。由于它的调用时机是在对dom渲染之后、浏览器渲染之前,所以应该在commitWork后也就是渲染视图之前进行统一的链表更新之后执行。
(3)定义commitEffectHooks方法,在内部定义run方法,将wipRoot也就是Dom树根节点进行传入,采用递归的形式对整个dom进行处理。首先判断初始化还是更新,当fiber的alternate不存在时就是初始化的情况,我们直接执行初始化操作fiber.effectHook.callback()即可。更新:我们则需要通过alternate获取旧节点上的oldEffectHook,通过对其内部的deps进行遍历,通过索引值判断新旧节点的deps里的依赖值是否发生更新,如果为true则执行newHook的callback函数。
(4)当出现多个调用useEffect时,我们就需要创建一个队列effectHooks,用于将effectHook进行存储,这个逻辑跟实现useState的逻辑非常相似。在处理functionComponent时对effectHooks进行初始化。那么commitEffectHooks方法的逻辑也要随之更改一些,获取队列effectHooks,对队列进行遍历,通过索引确定每一项,将其与旧节点的deps里的依赖进行对比,进行更新。
(5)代码如下:
function commitEffectHooks() {
function run(fiber) {
if(!fiber) return
if(!fiber.alternate) {
// init
fiber.effectHooks?.forEach(hook => {
hook.callback()
})
} else {
// update
fiber.effectHooks?.forEach((newHook,index) => {
const oldEffectHook = fiber.alternate?.effectHooks[index]
const needUpdate = oldEffectHook?.deps.some((oldDep,i) => {
return oldDep !== newHook.deps[i]
})
needUpdate && newHook.callback()
})
}
run(fiber.child)
run(fiber.sibling)
}
run(wipRoot)
}
let effectHooks
function useEffect(callback,deps) {
const effectHook = {
callback,
deps
}
effectHooks.push(effectHook)
wipFiber.effectHooks = effectHooks
}
2. 实现cleanup
(1)为了清空副作用。调用时机:在调用useEffect之前进行调用的回调函数,当 deps 为空的时候不会调用返回的cleanup。并且初始化不会cleanup逻辑。
(2)首先,将cleanup添加到对象中,其次就是在调用callback函数的时候,去给cleanup进行赋值。这样就把callback返回的函数进行存储下来。
(3)由于它是在调用useEffect之前进行调用,所以我们需要在执行run方法之前进行对cleanup的处理。它的行为依然是从跟节点进行遍历查找。如果fiber存在,我们需要取出之前的effectHook的cleanup进行调用。并在最后通过递归处理链表上其fiber的child以及兄弟节点。
(4)代码如下:
function commitEffectHooks() {
function run(fiber) {
if(!fiber) return
if(!fiber.alternate) {
// init
fiber.effectHooks?.forEach(hook => {
hook.cleanup = hook.callback()
})
} else {
// update
fiber.effectHooks?.forEach((newHook,index) => {
const oldEffectHook = fiber.alternate?.effectHooks[index]
const needUpdate = oldEffectHook?.deps.some((oldDep,i) => {
return oldDep !== newHook.deps[i]
})
needUpdate && (newHook.cleanup = newHook.callback())
})
}
run(fiber.child)
run(fiber.sibling)
}
function runCleanup(fiber) {
if(!fiber) return
fiber.alternate?.effectHooks?.forEach(hook => {
if(hook.deps.length > 0) {
hook.cleanup && hook.cleanup()
}
})
runCleanup(fiber.child)
runCleanup(fiber.sibling)
}
runCleanup(wipRoot)
run(wipRoot)
}
function Counter() {
const [count,setCount] = React.useState(20)
const [bar,setBar] = React.useState('bar')
function handClick() {
setCount((c) => {
return c+1
})
setBar("barbar")
}
// useEffect:调用时机在React完成对Dom渲染之后,并且浏览器完成绘制之前
// cleanup: 在调用useEffect之前进行调用,当 deps 为空的时候不会调用返回的cleanup
React.useEffect(() => {
console.log('init');
return () => {
console.log('cleanup 0');
}
},[])
React.useEffect(() => {
console.log('update',count);
return () => {
console.log('cleanup 1');
}
},[count])
React.useEffect(() => {
console.log('update',count);
return () => {
console.log('cleanup 2');
}
},[count])
return (
<div>
count:{count}
<div>
{bar}
</div>
<button onClick={ handClick }>点击</button>
</div>
)
}
3. 迷你react的实现版就到此结束了,大家可以尝试自己写一个属于自己的mini-react,整个过程强调的还是多学习一些代码编写的思想,我觉得很关键。