React-hook源码阅读(下) - 一文搞定useEffect及其他effect hook
这一篇, 我们走入 Effect
相关的原理, 涉及的 hook
有 useEffect
, useLayoutEffect
, useInsertionEffect
, useImperativeHandle
总体文章的思路为先通过useEffect
较详细了解Effect
相关处理, 然后比较其他Hook
。 如果对源码不是很感兴趣可以直接看总结
一、useEffect
mountEffect
还是老规矩, 先找初次挂载的入口mountEffect
。 这里通过判断了Mode
判断他走入哪个分支, 本质上都是调用了mountEffectImpl
。 只不过fiber flags
传入的多了MountPassiveDev
。 不过我们的关注重点一般都在hook flags
的Passive
function mountEffect(create, deps) {
if ( (currentlyRenderingFiber$1.mode & StrictEffectsMode) !== NoMode) {
return mountEffectImpl(MountPassiveDev | Passive | PassiveStatic, Passive$1, create, deps);
} else {
return mountEffectImpl(Passive | PassiveStatic, Passive$1, create, deps);
}
}
接着看他调用的 mountEffectImpl
。 这里一共做了三步, 具体可看注释
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps; // 这里对传入的deps做了处理
// 这里给当前的Fiber打上了flags标签, 表示有副作用, 在commit阶段会使用到
currentlyRenderingFiber.flags |= fiberFlags;
// 挂载上hook的链表
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined, // 可以看到这里暂时是不收集destroy的
nextDeps,
);
}
接着继续看pushEffect
。 这里主要就是根据useEffect hook
对象形成完整的Effect
链表。 放在hook
链表上以及updateQueue
function pushEffect(tag, create, destroy, deps) {
// 生成了对应的Effect对象, 最后是要挂上hook链表的
var effect = {
tag: tag,
create: create,
destroy: destroy,
deps: deps,
// Circular
next: null
};
// 拿到当前Fiber的updateQueue
var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;
// 这里就是生成链表的过程了, 主要是处理了updateQueue和effect链表, 具体效果可以看下面的图示
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
var lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
var firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
来个例子, 我们来看一下走完hook
之后该函数Fiber
变成啥样啦
export default function MyApp() {
const [count, setCount] = useState(1);
const [count1, setCount1] = useState(2);
useEffect(() => {
console.log('1');
return () => console.log('3')
}, [])
useEffect(() => {
console.log('2');
}, [count])
return (
<div onClick={() => setCount(count + 1)}>{count}</div>
)
}
updateEffect
了解完mountEffect
做了什么后我们继续看updateEffect
。 可以看到也是传入Passive
function updateEffect(create, deps) {
return updateEffectImpl(Passive, Passive$1, create, deps);
}
- 直接看
updateEffectImpl
。 这里的流程也很清晰, 就不赘述了
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps; // 拿到现在的依赖
let destroy = undefined;
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// 这里比较了前后依赖, 如果一致的话处理hook之后就reuturn出去了
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
// 如果依赖不一致的话则给Fiber打上Passive的标志
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags, // effect的tag由hookFlags决定
create,
destroy,
nextDeps,
);
}
从mountEffect
和udpateEffect
都有一个关键的操作就是给当前的Fiber flags
打上Passive
的标志。 该标志在commit
阶段的时候对Effect
进行处理。
从这里你就能知道为什么
useEffect deps
传入空数组的时候只会在初次渲染调用一次: 因为两个空数组中没得比较,故每次都认为l两个数组中的每一项都是一致的, 是没有变化的, 会直接return
出去, 而不会打上flags
标签useEffect
为什么无论如何都最少会调用一次: 因为mountEffect
的时候没有其他条件可以return
出去, 一定会打上flags
标签
如何处理useEffect(重点)
异步原理
先来一张宏观图(录制的是初次渲染阶段), 可以看到图的左边是进入了commit
段,最后通过DOM API appendChild
将处理好的DOM
树挂载到页面上,然后交出线程, 浏览器进行渲染。 接着再执行了图的右边部分, 这一部分实际上就是在处理触发的useEffect
的回调。
总的流程就是 commit
阶段(宏任务) - 浏览器渲染 - useEffect
相关回调执行(宏任务)
- 从链接点进去都有蛮详细的解释了, 这里就不多说了
- 以下具体的源码流程建议先看一篇commit的基础源码之后再看这个, 会舒服流畅很多, 因为都是同一个套路。 如果看不下去的话可以跳过源码直接看总结
那么我们继续看flushPassiveEffects
这个函数究竟做了什么?
这里初始化了挺多东西, 我省略了,我们直接关注主流程。 可以看到主要就是调用了flushPassiveEffectsImpl
export function flushPassiveEffects(): boolean {
if (rootWithPendingPassiveEffects !== null) {
.....
try {
......
return flushPassiveEffectsImpl();
} finally {
......
}
}
return false;
}
- 那么
flushPassiveEffectsImpl
又做了什么。 最重要的就是commitPassiveUnmountEffects
和commitPassiveMountEffects
。 接下去会重点介绍这两个函数
function flushPassiveEffectsImpl() {
.....
// 这里就是主流程了
commitPassiveUnmountEffects(root.current);
commitPassiveMountEffects(root, root.current, lanes, transitions);
....
flushSyncCallbacks();
return true;
}
处理destory
我称之为React常见四件套
该函数就是一个入口函数
export function commitPassiveUnmountEffects(firstChild: Fiber): void {
nextEffect = firstChild;
commitPassiveUnmountEffects_begin();
}
这里的
commitPassiveUnmountEffects_begin
做了更多的事情, 为了整体性(而且涉及后面需要具体讲的逻辑), 故这部分放在后面讲
那么我们就只需要看两点
- 什么节点是目标节点
- 目标节点做了什么事情
对于第一点我们看xxcomplete
, 作为commitPassiveUnmountOnFiber
的入口函数, 这里限制了flags
上有Passive
, 可以回顾之前的mountEffect
和updateEffect
, 我们给节点打上的flags
就有Passvive
if ((fiber.flags & Passive) !== NoFlags) {
commitPassiveUnmountOnFiber(fiber);
}
对于第二点, 我们回归关注commitPassiveUnmountOnFiber
。 这里通过判断了组件函数, 走入commitHookEffectListUnmount
function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
....
commitHookEffectListUnmount(
HookPassive | HookHasEffect,
finishedWork,
finishedWork.return,
);
...
break;
}
}
}
这个函数就是最终真正处理Unmount
的核心函数了。 可以看到这里就是拿到当前Fiber
上的updateQueue
, 然后从头开始, 进行遍历, 对于每一个effect
, 拿出来effect
的destroy
,然后清空了, 再调用safelyCallDestroy
(其实就是执行destroy
函数, 之所以命名为safelyxx
就是因为里面多了try catch
去捕获错误)去处理
function commitHookEffectListUnmount(
flags: HookFlags,
finishedWork: Fiber,
nearestMountedAncestor: Fiber | null,
) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & flags) === flags) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
无奖竞猜: 初次执行调用flushPassiveEffects
走到这里会执行destory
吗
答案是不会的。 你可以回看mountEffect
, 我们在挂载到hook
链表上的时候, 此时存入的destroy
为undefined
小总结
commitPassiveUnmountEffects函数主要流程就是找到Fiber
树上带有Passive
的节点, 取出它的destroy
函数, 然后执行。 并且清空destroy
函数
执行完commitPassiveUnmountEffects
的下一步就是执行commitPassiveMountEffects
, 我们继续看
处理create
commitPassiveMountEffects
作为入口函数, 可以看到这里的流程也是经典四件套
我们还是关注两个点
- 什么节点是目标节点
- 目标节点做了什么事情
对于第一点, 看commitPassiveMountEffects_complete
函数。 可以看到这里依旧是判断了flags
上带有Passive
if ((fiber.flags & Passive) !== NoFlags) {
commitPassiveMountOnFiber(
root,
fiber,
committedLanes,
committedTransitions,
);
}
对于第二个点, 看commitPassiveMountOnFiber
函数。 这里的逻辑还是相当多的, 对Fiber tag
进行了判断, 其中对函数组件, 根组件等都有不同的处理。 这里关注函数组件。 可以看到调用了函数commitHookEffectListMount
function commitPassiveMountOnFiber(
finishedRoot: FiberRoot,
finishedWork: Fiber,
committedLanes: Lanes,
committedTransitions: Array<Transition> | null,
): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
...
commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork);
break;
}
case HostRoot: ....
case LegacyHiddenComponent:
case OffscreenComponent:....
case CacheComponent: ....
break;
}
}
}
继续看commitHookEffectListMount
。 可以看到这里的逻辑有点类似于commitHookEffectListUnmount
。 也是去找到Fiber
节点的updateQueue
, 然后遍历他, 拿到每一个Effect
上的create()
函数, 这也就是我们传入给useEffect
的回调函数, 拿到之后对他进行调用, 调用的结果再赋值给Effect
的destory
function commitHookEffectListMount(flags: HookFlags, finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & flags) === flags) {
// Mount
const create = effect.create;
effect.destroy = create();
}
.....
effect = effect.next;
} while (effect !== firstEffect);
}
}
小总结
这个函数的作用就是拿到Fiber
上的updateQueue
, 然后执行每一个Effect
上的create()
函数, 并且把执行结果赋值给Effect
上的destory
到这里就能解答为什么我们在mountEffect
的时候不直接给effect
的destory
进行赋值
如果一开始就直接赋值的话(而且你要赋值的话也得调用吧, 这调用时间也不合理hhh), 那么我们在初次处理useEffect
的时候就会去调用destory
函数了。 这显然不符合destory
的定义
那我们对destory
的定义是什么: 在每次依赖项变更重新渲染后,React
将首先使用旧值运行cleanup
函数,然后使用新值运行setup
函数
那怎么做到旧值执行destory
, 新值执行create
函数的呢
OK, 你以为走到这里就结束了? 别忘记我们上面跳过的commitPassiveUnmountEffects_begin
。
处理卸载的destory
commitPassiveUnmountEffects_begin
作为入口函数
这里主要是对删除的节点进行额外的处理。 为什么呢,要考虑到当我们一个组件要被卸载的时候, 其useEffect
的cleanup
函数应该都被执行一次。 这一块的逻辑目的就是这个
那我们根据上面的思路来说: 推测这里的思路应该就是找到被删除的Fiber
节点, 然后拿到他的updateQueue
, 如果updateQueue
不为空的话, 就遍历每一个effect
对象, 拿到destory
函数进行执行
接下去看源码和我们推测的一不一样
function commitPassiveUnmountEffects_begin() {
while (nextEffect !== null) {
const fiber = nextEffect;
const child = fiber.child;
// 子节点有删除节点, ChildDeletion就是当子节点被删除时会打上的flags
// 这一块就是挨个拿到删除的节点,然后调用commitPassiveUnmountEffectsInsideOfDeletedTree_begin
if ((nextEffect.flags & ChildDeletion) !== NoFlags) {
const deletions = fiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const fiberToDelete = deletions[i];
nextEffect = fiberToDelete;
commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
fiberToDelete,
fiber,
);
}
// 断开Child链和Sibling链
const previousFiber = fiber.alternate;
if (previousFiber !== null) {
let detachedChild = previousFiber.child;
if (detachedChild !== null) {
previousFiber.child = null;
do {
const detachedSibling = detachedChild.sibling;
detachedChild.sibling = null;
detachedChild = detachedSibling;
} while (detachedChild !== null);
}
}
nextEffect = fiber;
}
}
// 这里删除了无关逻辑....
}
}
进来了commitPassiveUnmountEffectsInsideOfDeletedTree_begin
之后会顺着他的Child
链和Sibling
链(逻辑位于commitPassiveUnmountEffectsInsideOfDeletedTree_complete
)调用commitPassiveUnmountInsideDeletedTreeOnFiber
。 这个函数内部其实本质上就是调用了commitHookEffectListUnmount
。commitHookEffectListUnmount
就是处理destory
的核心函数. 也就是说和我们推测的流程是一致的。
function commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
deletedSubtreeRoot: Fiber,
nearestMountedAncestor: Fiber | null,
) {
while (nextEffect !== null) {
const fiber = nextEffect;
commitPassiveUnmountInsideDeletedTreeOnFiber(fiber, nearestMountedAncestor);
const child = fiber.child;
if (child !== null) {
child.return = fiber;
nextEffect = child;
} else {
commitPassiveUnmountEffectsInsideOfDeletedTree_complete(
deletedSubtreeRoot,
);
}
}
}
总结
我们梳理一下useEffect
的相关流程
首先是初次渲染阶段
render
阶段的时候, 此时调用的是mountEffect
, 会形成hook
对象挂上hook
链表。 并且形成Effect
链表,udpateQueue
存放对应的Effect
链表。还会给Fiber
打上Passive
的flag
commit
阶段的时候,在准备阶段利用MessageChannel
, 让处理useEffect
的流程异步执行, 也就是推到下一个宏任务处理- 异步处理
useEffect
:- 首先先处理被删除的节点执行
destory
函数(这里一般没有的) - 然后找到打上
Passive
的Fiber
节点(所有使用到useEffect
的节点都会有该标签) - 然后会先处理
destory
, 因为此时destory
为undefined
, 故不处理。 - 然后再处理
create
, 此时是遍历updateQueue
上存放的effect
链表, 然后挨个执行create
函数。 其执行结果再赋值给distory
- 首先先处理被删除的节点执行
(这也就是为什么useEffect
在初次渲染的时候会执行一次)
接着函数可能因其他时候产生了变动, 重新执行组件函数, 此时走入了update
流程
render
阶段的时候, 此时调用的是updateEffect
, 对前后的deps
数组进行浅比较的判断, 如果前后依赖一致的话则不处理, 不一致的话则给当前的Fiber
打上Passive
的flag
commit
阶段和初次渲染一致- 异步处理
useEffect
:- 首先先处理被删除的节点执行
destory
函数 - 然后找到打上
Passive
的Fiber
节点(这里只有依赖改变了的节点才会打) - 然后对于目标节点会先处理
destory
, 此时因为初次渲染/上一次commit
处理过, 所以如果代码中有的话这里一般都会有, 也是遍历updateQueue
上的Effect
链表, 拿到destory
函数进行执行 create
处理同初次渲染
- 首先先处理被删除的节点执行
二、useLayoutEffect
mountLayoutEffect
还是先看mountLayoutEffect
。诶你会发现, 这里和mountEffect
的逻辑也基本一致, 都是调用了mountEffectImpl
, 最大的区别就是这里打上的hook flags
是HookLayout
function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
let fiberFlags: Flags = UpdateEffect;
if (enableSuspenseLayoutEffectSemantics) {
fiberFlags |= LayoutStaticEffect;
}
return mountEffectImpl(fiberFlags, HookLayout, create, deps);
}
udpateLayoutEffect
再看udpateLayoutEffect
, 也是直接调用的updateEffectImpl
。 只不过他传入的flags
还是HookLayout
function updateLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
}
既然是调用的一样的函数, 那么就说明他形成的hook
对象, effect
链都是一致的逻辑, 包括updateQueue
。 体现不一样的就是每一个effect
对象中的tag
。 这个tag
跟我们传入的flags
有关
你会发现我们上面讲述的React
如何处理useEffect
的过程都是依赖着Passive
的标签去寻找, 那么我们commit
阶段处理useLayoutEffect
的逻辑自然就不一样了
commit
阶段是在mutation
阶段去处理useLayoutEffect
上的destory
函数。 在layout
阶段处理useLayoutEffect
上的create
函数
处理destory
对于destory
函数, 其实本质上调用的还是commitPassiveUnmountOnFiber
。 只不过和useEffect
调用时机不一样
deps变更导致
对于deps
产生变更需要执行的destory
函数的执行路线是
commitMutationEffects
(进入mutation
阶段)->
commitMutationEffectsOnFiber
(这里通过switch case tag
进行筛选到FunctionComponent
之后又通过筛选了fiber flags
是否存在Update
) ->
commitHookEffectListUnmount
(这个在上面已经有讲解相关逻辑了, 注意这里传入的flags
为Layout | HasEffect
) ->
safelyCallDestroy
(执行destory
函数)
卸载导致
对于删除节点的话, 这里已经有详细讲解了。 调用逻辑就是
commitMutationEffects
(进入mutation
阶段)->
commitMutationEffectsOnFiber
-> ( 这里无论Switch Case tag
到哪个都一定会进入下一步, 上面deps
的变更没有写是因为不重要hhh, 反正又会递归回来) ->
recursivelyTraverseMutationEffects
(这个的第一步目标就是处理删除节点)->
commitDeletionEffects
(删除节点流程的入口函数) ->
safelyCallDestroy
处理create
不同的就是传入的参数不同, useEffect
调用的时候传入的flags
为 HookPassive | HookHasEffect
。 useLayoutEffect
调用的时候传入的flags
为 HookLayout | HookHasEffect
。 commitHookEffectListMount
内部就是通过传入的flags
进行识别调用的
总结
useLayoutEffect
和useEffect
的处理逻辑基本都是一样的。 唯一不同的就是执行的时间。 基于flags
的区别的做到一样的逻辑达到两种效果。
这里再总结一波加深印象
useLayoutEffect
的destory
函数执行时机在commit
阶段的mutation
阶段, create
函数执行时间在commit
阶段的layout
阶段
useEffect
的destory
函数和create
函数都是利用MessageChannel
达到在渲染之后的宏任务进行。 同步顺序执行就是先destory
后create
三、useInsertionEffect
mountInsertionEffect
可以看到也是调用了mountEffectImpl
, 只不过传入的hook flags
为HookInsertion
function mountInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(UpdateEffect, HookInsertion, create, deps);
}
updateInsertionEffect
可以看到也是调用了updateEffectImpl
,只不过传入的hook flags
为HookInsertion
function updateInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(UpdateEffect, HookInsertion, create, deps);
}
处理destory
其实我们在了解useLayoutEffect的时候已经接触了, 跟useLayout的调用时间是一样的
卸载导致
一样在commitDeletionEffectsOnFiber
中,如图所示
deps变更导致
一样在commitMutationEffectsOnFiber
中如图所示
处理create
hhh你看上面那个图, 其实已经处理了, 调用了commtHookEffectListUnmount
处理destory
之后就调用了commitHookEffectListMount
去处理create
总结
可以看到, 其实上面三种hook都是依赖着flags在实现在不同时机去执行逻辑。
- 对于
useInsertionEffect
来说flag
为Insertion
- 对于
useLayoutEffect
来说flag
为Layout
- 对于
useEffect
来说flag
为Passive
我们把图再更新一下
四、useImperativeHandle
也许你会好奇, 诶我怎么把
useImperativeHandle
放在这里讲, 这一篇不是在将effect相关的吗。 别急, 你往下看看
诶你会发现这不就类似于
Effect
的逻辑, 在依赖变更之后, 去执行一些操作。 让我们探究一下是不是这回事
Ok, 接下去按照我们的套路先看mountImperativeHandle
mountImperativeHandle
你会发现, 他依旧使用了mountEffectImpl
。 且他的hook flags
传入的是HookLayout
。 故我们就了解了他的调用时机。 这里传入的create
函数是imperativeHandleEffect
。 然后将我们传入的ref
, create
作为参数传入
function mountImperativeHandle<T>(
ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
create: () => T,
deps: Array<mixed> | void | null,
): void {
// 这里对deps进行了处理, 将ref也放入deps中
const effectDeps =
deps !== null && deps !== undefined ? deps.concat([ref]) : null;
let fiberFlags: Flags = UpdateEffect;
if (enableSuspenseLayoutEffectSemantics) {
fiberFlags |= LayoutStaticEffect;
}
return mountEffectImpl(
fiberFlags,
HookLayout,
imperativeHandleEffect.bind(null, create, ref),
effectDeps,
);
}
imperativeHandleEffect
又是什么呢。 可以看到基本逻辑是通过调用传入的create
函数, 赋值给ref
。 然后处理了destory
函数, 在更新的时候能够重置ref
值。
function imperativeHandleEffect<T>(
create: () => T,
// 传入的ref可以是ref对象也可以是一个函数
ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
) {
if (typeof ref === 'function') {
const refCallback = ref;
const inst = create(); // 拿到暴露给父组件的ref对象
refCallback(inst); // 作为参数传入ref函数
return () => {
refCallback(null); // 这是destory函数,给他重置
};
} else if (ref !== null && ref !== undefined) {
const refObject = ref;
const inst = create(); // 拿到暴露给父组件的ref对象
refObject.current = inst; // 给传入的ref赋值
return () => {
refObject.current = null; // destory函数, 给他重置
};
}
}
updateImperativeHandle
这里的逻辑没什么好讲了, 跟mountImperativeHandle
差不多
function updateImperativeHandle<T>(
ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
create: () => T,
deps: Array<mixed> | void | null,
): void {
const effectDeps =
deps !== null && deps !== undefined ? deps.concat([ref]) : null;
return updateEffectImpl(
UpdateEffect,
HookLayout,
imperativeHandleEffect.bind(null, create, ref),
effectDeps,
);
}
故useImperativeHandle
的一整个调用逻辑和useLayoutEffect
都很类似。 区别就在于,useLayoutEffect
的create
函数和destory
函数是我们传入的参数。 而useImperativeHandle
的create
函数和destory
函数都是固定的
结束语
React hook
的相关逻辑就到这一篇结束, 三篇下来我们一共了解了11个hook
原理, 基本覆盖了常见的hook
。 相信你看完这三篇也能够有一定收获, 对于什么场景使用什么hook
也有了一定的了解。
转载自:https://juejin.cn/post/7304540179183370255