ahooks源码系列(五):effect 相关的 hook

useUpdateEffect、useUpdateLayoutEffect

这两个 hook 在之前的文章里面说过,不过还是拿出来再走一遍吧

//入口函数为 createUpdateEffect
//useUpdateEffect 传入 useEffect
export default createUpdateEffect(useEffect);
//useUpdateLayoutEffect 传入 useLayoutEffect
export default createUpdateEffect(useLayoutEffect);
//代码实现
export const createUpdateEffect: (
hook: effectHookType,
) => effectHookType = hook => (effect, deps) => {
const isMounted = useRef(false);
// for react-refresh
hook(() => {
return () => {
isMounted.current = false;
};
}, []);
hook(() => {
// 首次执行完时候,设置为 true,从而下次依赖更新的时候可以执行逻辑
if (!isMounted.current) {
isMounted.current = true;
} else {
return effect();
}
}, deps);
};
//入口函数为 createUpdateEffect




//useUpdateEffect 传入 useEffect
export default createUpdateEffect(useEffect);


//useUpdateLayoutEffect 传入 useLayoutEffect
export default createUpdateEffect(useLayoutEffect);





//代码实现
export const createUpdateEffect: (
  hook: effectHookType,
) => effectHookType = hook => (effect, deps) => {
  const isMounted = useRef(false);



  // for react-refresh
  hook(() => {
    return () => {
      isMounted.current = false;
    };
  }, []);



  hook(() => {
    // 首次执行完时候,设置为 true,从而下次依赖更新的时候可以执行逻辑
    if (!isMounted.current) {
      isMounted.current = true;
    } else {
      return effect();
    }
  }, deps);
};
//入口函数为 createUpdateEffect //useUpdateEffect 传入 useEffect export default createUpdateEffect(useEffect); //useUpdateLayoutEffect 传入 useLayoutEffect export default createUpdateEffect(useLayoutEffect); //代码实现 export const createUpdateEffect: ( hook: effectHookType, ) => effectHookType = hook => (effect, deps) => { const isMounted = useRef(false); // for react-refresh hook(() => { return () => { isMounted.current = false; }; }, []); hook(() => { // 首次执行完时候,设置为 true,从而下次依赖更新的时候可以执行逻辑 if (!isMounted.current) { isMounted.current = true; } else { return effect(); } }, deps); };

这两个 hook 都通过 createUpdateEffect 入口函数,只不过一个传 useEffect,一个传 useLayoutEffect 作为参数。他们和 useEffectuseLayoutEffect 的用法一样,只不过这两个 hook 会忽略首次执行

思路

  • 内部通过 useRef 初始化一个标识 isMounted,用来判断组件是否挂载
  • 然后执行 hook(也就是 useEffectuseLayoutEffect),当组件首次挂载时,会执行更改标识 isMounted.current = true,也就是忽略了首次执行
  • 当依赖发生改变时,在执行回调 effect

useAsyncEffect

useAsyncEffect 支持传入一个异步的 callback

我们知道 useEffect 是不允许传入异步的 callback 的,比如:

useEffect(async () => {
await fetch(url)
}, [])
useEffect(async () => {
  await fetch(url)
}, [])
useEffect(async () => { await fetch(url) }, [])

这样写是会报错的,因为 useEffect 会在重新渲染时,清除上一次的副作用,或者在组件销毁时,清除副作用,它应该返回一个 cleanup 函数,而不是返回 Promise,这会导致 React 在调用销毁函数的时候报错,因为当返回值是异步时,无法预知代码的执行情况,所以可能导致一些难以发现的 bug

从源码的角度来看这个问题的话:

function commitHookEffectListUnmount(tag, finishedWork) {
var updateQueue = finishedWork.updateQueue;
var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
var firstEffect = lastEffect.next;
var effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
// Unmount
var destroy = effect.destroy;
effect.destroy = undefined;
// 这里!!!!
if (destroy !== undefined) {
destroy();
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
function commitHookEffectListUnmount(tag, finishedWork) {
  var updateQueue = finishedWork.updateQueue;
  var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;






  if (lastEffect !== null) {
    var firstEffect = lastEffect.next;
    var effect = firstEffect;





    do {
      if ((effect.tag & tag) === tag) {
        // Unmount
        var destroy = effect.destroy;
        effect.destroy = undefined;


        // 这里!!!!
        if (destroy !== undefined) {
          destroy();
        }
      }



      effect = effect.next;
    } while (effect !== firstEffect);
  }
}
function commitHookEffectListUnmount(tag, finishedWork) { var updateQueue = finishedWork.updateQueue; var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { var firstEffect = lastEffect.next; var effect = firstEffect; do { if ((effect.tag & tag) === tag) { // Unmount var destroy = effect.destroy; effect.destroy = undefined; // 这里!!!! if (destroy !== undefined) { destroy(); } } effect = effect.next; } while (effect !== firstEffect); } }

从源码中可以看出,卸载的时候会通过函数组件对应的 fiber 获取 effect 链表,然后遍历链表,获取环链上的每一个节点,如果 destroy 不是 undefined 就执行,所以如果 useEffect 第一个参数传入 async, 那么这里的 destroy 就是一个 promise 对象,对象是不能执行的,所以报错。

那我们如果非要在 useEffect 里面使用异步函数怎么办呢?两种方式

  • 方式一:定义一个函数用来接受异步函数返回的 Promise
    useEffect(() => {
    const asyncFun = async () => {
    //do something
    };
    asyncFun();
    }, []);
    useEffect(() => {
      const asyncFun = async () => {
        //do something
      };
      asyncFun();
    }, []);
    useEffect(() => { const asyncFun = async () => { //do something }; asyncFun(); }, []);
  • 方式二:立即执行函数
    useEffect(() => {
    (async () => {
    // do something
    })();
    }, []);
      useEffect(() => {
        (async () => {
          // do something
        })();
      }, []);
    useEffect(() => { (async () => { // do something })(); }, []);

但是在看 useAsyncEffect 的源码前,我们先来认识一下 Gnerator

Gnerator

Generator 是一个返回值 iterator 对象的函数。而 iterator 叫做迭代器,它要满足迭代器协议:一个拥有 next 方法的对象,且 next 方法要返回形如 {done: boolean, value: any} 的对象,其中 done 用来表示当前的数据结构是否遍历完毕,value 是每一次遍历的值

iterator 有什么用?我们知道,像 Array 这样的数据结构我们是可以去遍历、拓展的,因为它默认实现了 Symbol.iterator 属性,该属性如果被定义了,就可以被遍历、拓展,比如:

let arr = ['a','b','c'];
let iter = arr[Symbol.iterator]();
iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }
let arr = ['a','b','c'];




let iter = arr[Symbol.iterator]();






iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }
let arr = ['a','b','c']; let iter = arr[Symbol.iterator](); iter.next() // { value: 'a', done: false } iter.next() // { value: 'b', done: false } iter.next() // { value: 'c', done: false } iter.next() // { value: undefined, done: true }

我们手动通过为数组 arr 定义 Symbol.iterator 属性,是个函数,调用它时会返回迭代器,也就是 iter,然后通过 next 方法,就可以遍历 arr 了

对于对象来说也是如此:

let obj = {a: "hello", b: "world"};
// 自定义迭代器
function createIterator(items) {
let keys = Object.keys(items);
let i = 0;
return {
next: function () {
let done = (i >= keys.length);
let value = !done ? items[keys[i++]] : undefined;
return {
value: value,
done: done,
};
}
};
}
let iterator = createIterator(obj); // 返回迭代器
console.log(iterator.next()); // "{ value: 'hello', done: false }"
console.log(iterator.next()); // "{ value: 'world', done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
let obj = {a: "hello", b: "world"};




// 自定义迭代器
function createIterator(items) {
    let keys = Object.keys(items);
    let i = 0;
    return {
        next: function () {
            let done = (i >= keys.length);
            let value = !done ? items[keys[i++]] : undefined;
            return {
                value: value,
                done: done,
            };
        }
    };
}

let iterator = createIterator(obj); // 返回迭代器
console.log(iterator.next()); // "{ value: 'hello', done: false }"
console.log(iterator.next());  // "{ value: 'world', done: false }"
console.log(iterator.next());  // "{ value: undefined, done: true }"
let obj = {a: "hello", b: "world"}; // 自定义迭代器 function createIterator(items) { let keys = Object.keys(items); let i = 0; return { next: function () { let done = (i >= keys.length); let value = !done ? items[keys[i++]] : undefined; return { value: value, done: done, }; } }; } let iterator = createIterator(obj); // 返回迭代器 console.log(iterator.next()); // "{ value: 'hello', done: false }" console.log(iterator.next()); // "{ value: 'world', done: false }" console.log(iterator.next()); // "{ value: undefined, done: true }"

我们再回到 Generator 来。刚刚说了,Generator 是用于返回 iterator 迭代器的函数,它的定义如下:

//Generator 定义时要在 function 后加一个星号
function* generator() {
yield 111
yield 222
};
let iterator = generator(); //返回迭代器
iterator.next() // {value: 111, done: false}
iterator.next() // {value: 222, done: false}
iterator.next() // {value: undefined, done: true}
//Generator 定义时要在 function 后加一个星号
function* generator() {
    yield 111
    yield 222
};
let iterator = generator(); //返回迭代器
iterator.next()  // {value: 111, done: false}
iterator.next()  // {value: 222, done: false}
iterator.next()  // {value: undefined, done: true}
//Generator 定义时要在 function 后加一个星号 function* generator() { yield 111 yield 222 }; let iterator = generator(); //返回迭代器 iterator.next() // {value: 111, done: false} iterator.next() // {value: 222, done: false} iterator.next() // {value: undefined, done: true}

Generator 内部通过关键字 yield,可以像 打断点 一样,暂停函数的执行,并且可以从上一次暂停的位置继续往后执行,因此 Generator 可以返回一系列的值,然后像”同步“代码一样执行

这里还有个 Generator 的孪生兄弟,叫做 Symbol.asyncIterator,它两差不多,只不过后者用来指定一个对象的异步迭代器,可用于 for await ... of 循环。比如:

const myAsyncIterable = new Object();
myAsyncIterable[Symbol.asyncIterator] = async function*() {
yield "hello";
yield "async";
yield "iteration!";
};
(async () => {
for await (const x of myAsyncIterable) {
console.log(x);
// expected output:
// "hello"
// "async"
// "iteration!"
}
})();
const myAsyncIterable = new Object();
myAsyncIterable[Symbol.asyncIterator] = async function*() {
    yield "hello";
    yield "async";
    yield "iteration!";
};

(async () => {
    for await (const x of myAsyncIterable) {
        console.log(x);
        // expected output:
        //    "hello"
        //    "async"
        //    "iteration!"
    }

})();
const myAsyncIterable = new Object(); myAsyncIterable[Symbol.asyncIterator] = async function*() { yield "hello"; yield "async"; yield "iteration!"; }; (async () => { for await (const x of myAsyncIterable) { console.log(x); // expected output: // "hello" // "async" // "iteration!" } })();

看到这里,对 Generator 有了个基本的认识,接下来就来看 useAsyncEffect 源码

useAsyncEffect 源码

function isAsyncGenerator(
val: AsyncGenerator<void, void, void> | Promise<void>,
): val is AsyncGenerator<void, void, void> {
return isFunction(val[Symbol.asyncIterator]);
}
function useAsyncEffect(
effect: () => AsyncGenerator<void, void, void> | Promise<void>,
deps?: DependencyList,
) {
useEffect(() => {
const e = effect();
let cancelled = false;
async function execute() {
if (isAsyncGenerator(e)) {
while (true) {
const result = await e.next();
if (result.done || cancelled) {
break;
}
}
} else {
await e;
}
}
execute();
return () => {
cancelled = true;
};
}, deps);
}
export default useAsyncEffect;
function isAsyncGenerator(
  val: AsyncGenerator<void, void, void> | Promise<void>,
): val is AsyncGenerator<void, void, void> {
  return isFunction(val[Symbol.asyncIterator]);
}

function useAsyncEffect(
  effect: () => AsyncGenerator<void, void, void> | Promise<void>,
  deps?: DependencyList,
) {
  useEffect(() => {
    const e = effect();
    let cancelled = false;
    async function execute() {
      if (isAsyncGenerator(e)) {
        while (true) {
          const result = await e.next();
          if (result.done || cancelled) {
            break;
          }
        }
      } else {
        await e;
      }
    }
    execute();
    return () => {
      cancelled = true;
    };
  }, deps);
}

export default useAsyncEffect;
function isAsyncGenerator( val: AsyncGenerator<void, void, void> | Promise<void>, ): val is AsyncGenerator<void, void, void> { return isFunction(val[Symbol.asyncIterator]); } function useAsyncEffect( effect: () => AsyncGenerator<void, void, void> | Promise<void>, deps?: DependencyList, ) { useEffect(() => { const e = effect(); let cancelled = false; async function execute() { if (isAsyncGenerator(e)) { while (true) { const result = await e.next(); if (result.done || cancelled) { break; } } } else { await e; } } execute(); return () => { cancelled = true; }; }, deps); } export default useAsyncEffect;

它的思路为

  • 先执行传入的 effect() 异步回调函数,将其的返回值赋值给 e
  • 然后通过内部 isAsyncGenerator 方法 判断 e 是否是异步 Generator,如果是,就通过可迭代对象的 next 方法执行,直到 done: false 完毕为止;如果不是,那就是个 Promise,直接 await e
  • 最后,内部定义了一个 cancelled 变量,用来 中断执行

因为可能会遇到一些场景:用户在执行 a 操作,但是 a 操作还没完成时,就开始下一轮的 b 操作,那此时 a 操作是没有意义的,可以直接停止往后执行。直接跳过 a 到 b 操作去。这个 cancelled 就是干这个事情的

useUpdate

这个 hook 之前的文章也讲过,这里就一笔带过

useUpdate 会返回一个函数,调用该函数会强制组件重新渲染

import { useCallback, useState } from 'react';
const useUpdate = () => {
const [, setState] = useState({});
return useCallback(() => setState({}), []);
};
export default useUpdate;
import { useCallback, useState } from 'react';




const useUpdate = () => {
  const [, setState] = useState({});


  return useCallback(() => setState({}), []);
};





export default useUpdate;
import { useCallback, useState } from 'react'; const useUpdate = () => { const [, setState] = useState({}); return useCallback(() => setState({}), []); }; export default useUpdate;

很简单,就是当我们调用返回的函数时,useUpdate 内部会通过 setState 更改状态,然后就促使组件重新渲染

useDebounceEffect

该 hook 为 useEffect 增加防抖的能力。

function useDebounceEffect(
effect: EffectCallback,
deps?: DependencyList,
options?: DebounceOptions,
) {
// 通过设置 flag 标识依赖,只有改变的时候,才会触发 useUpdateEffect 中的回调
const [flag, setFlag] = useState({});
// 通过 lodash 的 防抖对 setFlag 包装一层防抖效果
const { run } = useDebounceFn(() => {
setFlag({});
}, options);
// 首次执行
useEffect(() => {
// 每当过了 options.wait 时间后,设置 flag 改变标识
return run();
}, deps);
// 当标识改变时,执行 effect 回调函数,但忽略首次执行
useUpdateEffect(effect, [flag]);
}
export default useDebounceEffect;
function useDebounceEffect(
  effect: EffectCallback,

  deps?: DependencyList,

  options?: DebounceOptions,
) {

  // 通过设置 flag 标识依赖,只有改变的时候,才会触发 useUpdateEffect 中的回调

  const [flag, setFlag] = useState({});






  // 通过 lodash 的 防抖对 setFlag 包装一层防抖效果
  const { run } = useDebounceFn(() => {
    setFlag({});

  }, options);



  // 首次执行
  useEffect(() => {
    // 每当过了 options.wait 时间后,设置 flag 改变标识
    return run();
  }, deps);

  // 当标识改变时,执行 effect 回调函数,但忽略首次执行
  useUpdateEffect(effect, [flag]);
}


export default useDebounceEffect;
function useDebounceEffect( effect: EffectCallback, deps?: DependencyList, options?: DebounceOptions, ) { // 通过设置 flag 标识依赖,只有改变的时候,才会触发 useUpdateEffect 中的回调 const [flag, setFlag] = useState({}); // 通过 lodash 的 防抖对 setFlag 包装一层防抖效果 const { run } = useDebounceFn(() => { setFlag({}); }, options); // 首次执行 useEffect(() => { // 每当过了 options.wait 时间后,设置 flag 改变标识 return run(); }, deps); // 当标识改变时,执行 effect 回调函数,但忽略首次执行 useUpdateEffect(effect, [flag]); } export default useDebounceEffect;

思路如下:

  • 设置 flag 标识符变量,然后对 setFlag 函数做一个防抖(用的是 lodash 的防抖包装了一层)
  • 首次执行时不影响
  • 后续执行时,当 flag 通过 options.wait 时间改变后,才执行 useUpdateEffect 执行 effect 回调。注意:useUpdateEffect忽略首次执行,所以代码前面用的是 useEffect

useThrottleEffect

为 useEffect 增加节流的能力。

它也是通过 lodash.throttle 方法来实现的

function useThrottleEffect(
effect: EffectCallback,
deps?: DependencyList,
options?: ThrottleOptions,
) {
// 通过设置 flag 标识依赖,只有改变的时候,才会触发 useUpdateEffect 中的回调
const [flag, setFlag] = useState({});
// 用来处理函数节流的 Hook。
const { run } = useThrottleFn(() => {
setFlag({});
}, options);
useEffect(() => {
return run();
}, deps);
// 只有在 flag 变化的时候,才执行 effect 函数
useUpdateEffect(effect, [flag]);
}
function useThrottleEffect(
  effect: EffectCallback,

  deps?: DependencyList,

  options?: ThrottleOptions,
) {

  // 通过设置 flag 标识依赖,只有改变的时候,才会触发 useUpdateEffect 中的回调

  const [flag, setFlag] = useState({});






  // 用来处理函数节流的 Hook。
  const { run } = useThrottleFn(() => {
    setFlag({});

  }, options);



  useEffect(() => {
    return run();
  }, deps);


  // 只有在 flag 变化的时候,才执行 effect 函数
  useUpdateEffect(effect, [flag]);
}
function useThrottleEffect( effect: EffectCallback, deps?: DependencyList, options?: ThrottleOptions, ) { // 通过设置 flag 标识依赖,只有改变的时候,才会触发 useUpdateEffect 中的回调 const [flag, setFlag] = useState({}); // 用来处理函数节流的 Hook。 const { run } = useThrottleFn(() => { setFlag({}); }, options); useEffect(() => { return run(); }, deps); // 只有在 flag 变化的时候,才执行 effect 函数 useUpdateEffect(effect, [flag]); }

思路和 useDebounceEffect 一样

useDebounceFn

useDebounceFn 是用来处理防抖函数的 hook

它主要也是使用的 lodash 的 debounce 方法

function useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions) {
const fnRef = useLatest(fn);
const wait = options?.wait ?? 1000;
const debounced = useMemo(
() =>
debounce(
(...args: Parameters<T>): ReturnType<T> => {
return fnRef.current(...args);
},
wait,
options,
),
[],
);
useUnmount(() => {
debounced.cancel();
});
return {
run: debounced,
cancel: debounced.cancel,
flush: debounced.flush,
};
}
export default useDebounceFn;
function useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions) {
  const fnRef = useLatest(fn);

  const wait = options?.wait ?? 1000;







  const debounced = useMemo(
    () =>

      debounce(
        (...args: Parameters<T>): ReturnType<T> => {
          return fnRef.current(...args);
        },
        wait,
        options,
      ),
    [],
  );

  useUnmount(() => {
    debounced.cancel();
  });



  return {
    run: debounced,
    cancel: debounced.cancel,
    flush: debounced.flush,
  };
}

export default useDebounceFn;
function useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions) { const fnRef = useLatest(fn); const wait = options?.wait ?? 1000; const debounced = useMemo( () => debounce( (...args: Parameters<T>): ReturnType<T> => { return fnRef.current(...args); }, wait, options, ), [], ); useUnmount(() => { debounced.cancel(); }); return { run: debounced, cancel: debounced.cancel, flush: debounced.flush, }; } export default useDebounceFn;

思路:

  • useLatest 保证每次都拿到最新的 fn
  • 用 lodash 的 debounce 包一层,内部调用函数时是执行 fnRef.current(...args)
  • 然后返回被包装后的函数 debounced,外部调用时会调用它
  • 然后在组件卸载时会取消 debounce

useThrottleFn

useThrottleFn 是用来处理函数节流的 Hook。

主要用到的是 lodash 的 throttle 方法

function useThrottleFn<T extends noop>(fn: T, options?: ThrottleOptions) {
const fnRef = useLatest(fn);
const wait = options?.wait ?? 1000;
const throttled = useMemo(
() =>
// 最终都是调用了 lodash 的节流函数
throttle(
(...args: Parameters<T>): ReturnType<T> => {
return fnRef.current(...args);
},
wait,
options,
),
[],
);
useUnmount(() => {
throttled.cancel();
});
return {
run: throttled,
// 取消
cancel: throttled.cancel,
// 立即调用
flush: throttled.flush,
};
}
function useThrottleFn<T extends noop>(fn: T, options?: ThrottleOptions) {
  const fnRef = useLatest(fn);

  const wait = options?.wait ?? 1000;







  const throttled = useMemo(
    () =>

      // 最终都是调用了 lodash 的节流函数
      throttle(
        (...args: Parameters<T>): ReturnType<T> => {
          return fnRef.current(...args);
        },
        wait,
        options,
      ),
    [],
  );


  useUnmount(() => {
    throttled.cancel();
  });

  return {
    run: throttled,
    // 取消
    cancel: throttled.cancel,
    // 立即调用
    flush: throttled.flush,
  };
}
function useThrottleFn<T extends noop>(fn: T, options?: ThrottleOptions) { const fnRef = useLatest(fn); const wait = options?.wait ?? 1000; const throttled = useMemo( () => // 最终都是调用了 lodash 的节流函数 throttle( (...args: Parameters<T>): ReturnType<T> => { return fnRef.current(...args); }, wait, options, ), [], ); useUnmount(() => { throttled.cancel(); }); return { run: throttled, // 取消 cancel: throttled.cancel, // 立即调用 flush: throttled.flush, }; }

它的思路和 useDebounceFn 一样

useDeepCompareEffect、useDeepCompareLayoutEffect

useDeepCompareEffectuseDeepCompareLayoutEffect 用法与 useEffect、useLayoutEffect 一致,只不过在依赖发生改变时,使用的是 lodash.isEqual 方法进行深比较

// 入口函数都是 createDeepCompareEffect,只不过一个接受 useEffect 作为参数
// 一个接受 useLayoutEffect 作为参数
export default createDeepCompareEffect(useEffect);
// 源代码
export const createDeepCompareEffect: CreateUpdateEffect = (hook) => (effect, deps) => {
const ref = useRef<DependencyList>();
const signalRef = useRef<number>(0);
if (deps === undefined || !depsEqual(deps, ref.current)) {
ref.current = deps;
signalRef.current += 1;
}
// hook 就是 useEffect / useLayoutEffect
// effect 就是传入的 callback 回调
hook(effect, [signalRef.current]);
};
// 入口函数都是 createDeepCompareEffect,只不过一个接受 useEffect 作为参数
// 一个接受 useLayoutEffect 作为参数
export default createDeepCompareEffect(useEffect);






// 源代码
export const createDeepCompareEffect: CreateUpdateEffect = (hook) => (effect, deps) => {
  const ref = useRef<DependencyList>();
  const signalRef = useRef<number>(0);

  if (deps === undefined || !depsEqual(deps, ref.current)) {
    ref.current = deps;
    signalRef.current += 1;
  }
 
  // hook 就是 useEffect / useLayoutEffect
  // effect 就是传入的 callback 回调
  hook(effect, [signalRef.current]);
};
// 入口函数都是 createDeepCompareEffect,只不过一个接受 useEffect 作为参数 // 一个接受 useLayoutEffect 作为参数 export default createDeepCompareEffect(useEffect); // 源代码 export const createDeepCompareEffect: CreateUpdateEffect = (hook) => (effect, deps) => { const ref = useRef<DependencyList>(); const signalRef = useRef<number>(0); if (deps === undefined || !depsEqual(deps, ref.current)) { ref.current = deps; signalRef.current += 1; } // hook 就是 useEffect / useLayoutEffect // effect 就是传入的 callback 回调 hook(effect, [signalRef.current]); };

思路:

  • 通过 ref 记录上一次的依赖项
  • 然后判断如果本次依赖是 undefined 或者 两次依赖不同发生了改变,就记录本次依赖ref.current = deps
  • 然后更新 signalRef.current,从而触发 hook 中的回调

这两个 hook 的思路是一样的,上面拿的是 useDeepCompareEffect 来做分析

useInterval 和 useTimeout

他俩源码一样,只不过一个用的 setInterval,一个用的 setTimeout

这里拿 useInterval 分析:

const useInterval = (fn: () => void, delay?: number, options: { immediate?: boolean } = {}) => {
const timerCallback = useMemoizedFn(fn);
const timerRef = useRef<NodeJS.Timer | null>(null);
//清除函数
const clear = useCallback(() => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
}, []);
useEffect(() => {
if (!isNumber(delay) || delay < 0) {
return;
}
if (options.immediate) {
//立即执行
timerCallback();
}
// 计时器
timerRef.current = setInterval(timerCallback, delay);
// 下一次渲染时,清除上一次定时器
return clear;
}, [delay, options.immediate]);
return clear;
};
export default useInterval;
const useInterval = (fn: () => void, delay?: number, options: { immediate?: boolean } = {}) => {
  const timerCallback = useMemoizedFn(fn);
  const timerRef = useRef<NodeJS.Timer | null>(null);






  //清除函数
  const clear = useCallback(() => {
    if (timerRef.current) {
      clearInterval(timerRef.current);
    }
  }, []);

  useEffect(() => {
    if (!isNumber(delay) || delay < 0) {
      return;
    }

    if (options.immediate) {
      //立即执行
      timerCallback();
    }
    // 计时器
    timerRef.current = setInterval(timerCallback, delay);
    // 下一次渲染时,清除上一次定时器
    return clear;
  }, [delay, options.immediate]);

  return clear;
};

export default useInterval;
const useInterval = (fn: () => void, delay?: number, options: { immediate?: boolean } = {}) => { const timerCallback = useMemoizedFn(fn); const timerRef = useRef<NodeJS.Timer | null>(null); //清除函数 const clear = useCallback(() => { if (timerRef.current) { clearInterval(timerRef.current); } }, []); useEffect(() => { if (!isNumber(delay) || delay < 0) { return; } if (options.immediate) { //立即执行 timerCallback(); } // 计时器 timerRef.current = setInterval(timerCallback, delay); // 下一次渲染时,清除上一次定时器 return clear; }, [delay, options.immediate]); return clear; }; export default useInterval;

思路:

  • 首先检验 delay,不通过直接 return
  • 然后判断是否立即执行,如果立即执行则就调用 timerCallback
  • 不管是不是立即执行,都开起定时器
  • 然后利用 useEffect 的 cleanup 函数机制,在下一次渲染时清除上一次的定时器

跟 setInterval 的区别如下:

  • 可以支持第三个参数,通过 immediate 能够立即执行我们的定时器。
  • 在变更 delay 的时候,会自动清除旧的定时器,并同时启动新的定时器。
  • 通过 useEffect 的返回清除机制,开发者不需要关注清除定时器的逻辑,避免内存泄露问题。这点是很多开发者会忽略的点。

useTimeout 思路跟它一样

useLockFn

useLockFn 用于给一个异步函数增加竞态锁,防止并发执行

function useLockFn<P extends any[] = any[], V extends any = any>(fn: (...args: P) => Promise<V>) {
const lockRef = useRef(false);
return useCallback(
async (...args: P) => {
if (lockRef.current) return;
lockRef.current = true;
try {
const ret = await fn(...args);
lockRef.current = false;
return ret;
} catch (e) {
lockRef.current = false;
throw e;
}
},
[fn],
);
}
export default useLockFn;
function useLockFn<P extends any[] = any[], V extends any = any>(fn: (...args: P) => Promise<V>) {
  const lockRef = useRef(false);

  return useCallback(
    async (...args: P) => {
      if (lockRef.current) return;
      lockRef.current = true;
      try {
        const ret = await fn(...args);
        lockRef.current = false;
        return ret;
      } catch (e) {
        lockRef.current = false;
        throw e;
      }
    },
    [fn],
  );
}



export default useLockFn;
function useLockFn<P extends any[] = any[], V extends any = any>(fn: (...args: P) => Promise<V>) { const lockRef = useRef(false); return useCallback( async (...args: P) => { if (lockRef.current) return; lockRef.current = true; try { const ret = await fn(...args); lockRef.current = false; return ret; } catch (e) { lockRef.current = false; throw e; } }, [fn], ); } export default useLockFn;

思路:

  • 内部通过 lockRef 来做一个竞态锁的标识,初始化为 false
  • 当执行传入的 fn 时请求时,lockRef.current 设置为 true
  • 当重复请求时,此时竞态锁标识为 true,直接 return,不执行原函数,从而达到加锁的目的

结语

从这几期的源码学习下来,我们发现对于传入的参数,一些会通过 useRefuseLatest存下来,然后当某些逻辑的执行需要条件判断时,基本会把判断条件用 useRef 初始化,所以以后自定义 hook 的时候可以参照这样的方式来

以上内容如有错误,欢迎留言指出,一起进步?,也欢迎大家一起讨论。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYMm9EcZ' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片