likes
comments
collection
share

手撕hook

作者站长头像
站长
· 阅读数 33

Hook 是系统运行到一定时候调用该时机所注册的回调函数

hook是函数组件里才可以使用的,你要知道,每一次渲染组件,都会调用一遍函数组件

也就是说,每一次更新state,函数组件都会从函数体第一行开始执行到返回节点

话不多说,先上代码

首先,定义render函数,假设每次更新都会调用这个函数。

function render(){
    console.log('--- render start ---');
    
    // 在这里调用 hook 函数
    
    console.log('--- render end ---');
}

然后,你得知道,每定义一个hook,hook链表就会变长一点,不管是,useState, useEffect, 还是 useMemo。

所以,我们先来初始化hook链表,我们先不管这个hook链表应该放在哪里,你只需要知道,它是从函数外部拿进来的。

let hookIndex = 0;  // 当前钩子🪝的索引
let hooks = [];     // hooks链表

接下来通过这个外部的hook链表来实现useState

首先得知道useState的使用方式,和作用

  1. 使用方式:const [state, setState] = useState(initstate);
  1. 作用:初始化一个状态,并返回当前状态和改变状态的函数,改变状态(调用函数),就会触发渲染
const useState = (function(){
   const create = function(init, index){
        // 初始化状态和更新函数
        const _state = init;
        const _setter = function(newState){
           if(typeof newState === 'function'){
               hooks[index] = newState(hooks[index]);
           } 
           else{
               hooks[index] = newState;
           }
           // 调用状态更改函数就渲染
           render();
        }       
        return hooks[index] = [_state, _setter];
   } 
   return function(init){
       const curHook = hooks[hookIndex] ?? create(init, hookIndex);
       hookIndex++;
       return curHook;
   }
})()

这样一个state就实现了,基本思路就是,查看当前hooks中是否有值,没有就去创建它并放入链表。

接着是useEffect,它是有依赖数组的

只有依赖数组中的某一个值改变,它才会去调用回调

所以怎么实现它呢?-=-=-。它是比较依赖项是否改变,所以肯定有一个上一次的依赖数组。

所以我们用hooks将上一次的依赖数组存起来,等到下一次渲染的时候比较。

const useEffect = function (callback, deps){
    if(!deps){
        // 如果没有依赖项,那么每次刷新都会调用
        return callback();
    }
    const lastDeps = hooks[hookIndex];
    if(!lastDeps){
        // 是否是初次渲染,因为初次渲染hooks里是空的
        callback();
    }
    else if(deps.some((item, index)=> item !== lastDeps[index])){
        // 判断依赖项是否发生变化
        callback();
    }
    // 将这一次的依赖项存入hooks,以备下一次比较
    hooks[hookIndex] = deps;
    hookIndex ++;
}

如果是空数组,就会只在第一渲染的时候调用,因为,数组是空的,不会有依赖项改变。

所以这样就可以模拟 componentDidMount

接着是 useMemo,它也是有依赖数组的,不过它也会将回调函数的结果存起来,只有等到依赖项改变的时候,去更新回调函数的结果。

和useEffect实现思路一样

const useMemo = function(factory, deps){
    if(!deps){
        return factory();
    }
    const lastDeps = hooks[hookIndex] && hooks[hookIndex][0];
    let res;
    if(!lastDeps){
        res = factory();
    }
    else if(deps.some((item, index)=> item !== lastDeps[index])){
        res = factory();
    }
    else {
        res = hooks[hookIndex][1];
    }
    hooks[hookIndex] = [deps, res]
    hookIndex ++;
    return res;
}

然后顺便写个useCallback

比如说传入的回调是通过函数返回的,那么只会保存第一次返回的结果,等到依赖项改变才会更新回调

const useCallBack = function (callback, deps) {
    if (!deps) {
        return callback;
    }
    let res;
    const curDeps = hooks[hookIndex] && hooks[hookIndex][0];
    if (!curDeps) {
        // 首次渲染的时候会更新回调
        res = callback;
    }
    else if (deps.some((cur, index) => cur !== curDeps[index])) {
        // 更新依赖项的时候才会更新回调
        res = callback;
    }
    else {
        // 依赖项无变化返回存储的值
        res = hooks[hookIndex][1];
    }
    hooks[hookIndex] = [deps, res];
    hookIndex++;
    return res;
}

如何调用

const useWidth = function(){
    const [width, setWidth] = useState(window.innerWidth);
    useEffect(()=>{
        window.onresize = function(){
            setWidth(window.innerWidth);
        }
        // 跪求大佬讲解如何实现清除副作用
    },[])
    return width;
}

function render(){
    hookIndex = 0;
    const width = useWidth();
    useEffect(()=>{
        console.log('窗口宽度发生变化了');
    },[width]);
    console.log('width :' + width);
    return setState;
}

render();
// 缩放窗口看会发生什么

本人学艺不精,如有对hook的误解,恳请各位指正。