美文网首页
依据React Hooks的原理,写一个简易的 useEffec

依据React Hooks的原理,写一个简易的 useEffec

作者: 弱冠而不立 | 来源:发表于2021-04-05 15:56 被阅读0次

    之前根据 react hooks 的原理实现了一个简易的 useState
    然后当然也应该再实现一个简易的 useEffect
    先回顾一下 useEffect 的用法

    import React, { useState, useEffect } from 'react';
    
    function Example() {
      const [count, setCount] = useState(0);
    
      // Similar to componentDidMount and componentDidUpdate:
      useEffect(() => {
        console.log(`You clicked ${count} times`);
      });
    
      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
            Click me
          </button>
        </div>
      );
    }
    

    总结:useEffect 接收俩参数,第一个参数是回调函数,第二个参数是依赖项。根据依赖项的不同,在不同的阶段执行回调函数

    同样预设一个执行环境

    let onClick;
    let onChange;
    
    function render() {
        // _idx 重新置为 0, 也是契合react每次更新时都从 hooks 头节点开始更新每一个 hook
        _idx = 0;
        const [count, setCount] = useState(0);
        const [name, setName] = useState("77");
        useEffect(()=>{
            console.log("effect —— count", count);
            console.log("effect —— name", name);
        }, [name])
        // 使用 onClick, onChange 简单模拟一下更新操作
        onClick = () => { setCount(count + 1) };
        onChange = (name) => { setName(name) };
    }
    
    render();
    console.log("-------------");
    onClick();
    onClick();
    console.log("-------------");
    onChange("kiana")
    onChange("kiana_k423")
    

    上面这段代码,模拟一次渲染,两次点击,两次修改。同时 useEffect 的依赖为 name
    注:useState 用的是上篇文章自己模拟的,点击查看

    根据依赖项的不同我们来分情况执行 callback

    let _memoizedState = []; // 多个 hook 存放在这个数组
    let _idx = 0; // 当前 memoizedState 下标
    /**
     * 模拟实现 useEffect
     *  // deps 的不同对应着不同的情况,
        // 1. deps 不存在时:每次 state 的更新,都需要执行 callback
        // 2. deps 存在,但数组为空时,只需要在挂载也就是初次渲染时执行 callback
        // 3. deps 存在且有依赖项,则对应的依赖性更新时才执行 callback
     * @param {Function} callback 回调函数
     * @param {Array} deps 依赖项
     */
    function useEffect(callback, deps) {
        // 没有依赖项,则每次都执行 callback
        if(!deps) {
            callback();
        } else {
            // 先根据当前下标获取到存储在全局 hooks 列表中当前位置原本的依赖项
            const memoizedDeps = _memoizedState[_idx];
            if(deps.length === 0) {
                // 通过当前 _memoizedState 下标位置是否有 deps 来判断是不是初次渲染
                !memoizedDeps && callback();
                // 同时也要更新全局 hooks 列表当前下标的依赖项的数据
                _memoizedState[_idx] = deps;
            } else {
                // 如果是初次渲染就直接调用 callback
                // 否则就再判断依赖项有没有更新
                memoizedDeps && deps.every((dep, idx) => dep === memoizedDeps[idx]) || callback();
                // 更新当前下标的依赖项的数据
                _memoizedState[_idx] = deps;
            }
            _idx++;
        }
    }
    

    使用结果如下:

    可以看见只在初次渲染和 name 更新的时候打印了结果

    另外,我们来换换依赖项,分别实验一下其他结果:

    依赖项为空数组,只在render阶段执行 callback 不传入依赖项,则每次更新时都执行 callback

    貌似到这里,都进行的很不错。然而 useEffect 的回调函数还有一个很重要的特性,那就是可以返回一个函数,该函数在 willUnMount 阶段执行。

    改进版本,组件销毁时执行 useEffect 回调的返回的函数

    思路也很简单,就是在初次渲染时,每个 useEffect的 callback 都会被执行,然后如果 callback 执行结果有返回值且返回值是函数,就把它推入到一个全局的 effectDestroy 数组,然后在组件 WillUnMount 时依次执行其中的 destroy 函数,具体实现如下:

    const _memoizedState = []; // 多个 hook 存放在这个数组
    let _idx = 0; // 当前 memoizedState 下标
    const _effectDestroy = []; // 存储多个 useEffect 回调函数返回的函数
    /**
     * 模拟实现 useEffect
     *  // deps 的不同对应着不同的情况,
        // 1. deps 不存在时:每次 state 的更新,都需要执行 callback
        // 2. deps 存在,但数组为空时,只需要在挂载也就是初次渲染时执行 callback
        // 3. deps 存在且有依赖项,则对应的依赖性更新时才执行 callback
     * @param {Function} callback 回调函数
     * @param {Array} deps 依赖项
     */
    function useEffect(callback, deps) {
        // 先根据当前下标获取到存储在全局 hooks 列表中当前位置原本的依赖项
        const memoizedDeps = _memoizedState[_idx];
        // 如果当前没有,则证明是初次渲染,无论什么情况都执行一次 callback
        if(!memoizedDeps) {
            const destroy = callback();
            // 同时更新依赖项
            _memoizedState[_idx] = deps;
            // 如果 callback 返回值是一个函数,则先把函数存储在全局的 destory 数组中,随后在willUnMount阶段依次执行
            if(typeof destroy === "function") {
                _effectDestroy.push(destroy);
            }
        // 否则就是 重新渲染 的阶段
        } else {
            // 没有依赖项直接执行 callback
            if(!deps) {
                callback();
            } else {
                // 依赖项不为空数组的时候且依赖项有更新了才去执行 callback 
                deps.length !== 0 && !deps.every((dep, idx) => dep === memoizedDeps[idx]) && callback();
                // 别忘了更新依赖项
                _memoizedState[_idx] = deps;
            }
        }
        _idx++;
    }
    

    模拟react 运行的环境如下:

    let onClick;
    let onChange;
    const willUnMount = () => {
        for(let destroy of _effectDestroy) {
            destroy();
        }
    }
    
    function render() {
        // _idx 表示当前执行到的 hooks 的位置
        // _idx 重新置为 0, 也是契合react每次更新时都从 hooks 头节点开始更新每一个 hook
        _idx = 0;
        const [count, setCount] = useState(0);
        const [name, setName] = useState("77");
        useEffect(()=>{
            console.log("effect —— count", count);
            return () => {
                console.log("count Effect Destroy");
            }
        },[count])
        useEffect(()=>{
            console.log("effect —— name", name);
        },[name])
        // 使用 onClick, onChange 简单模拟一下更新操作
        onClick = () => { setCount(count + 1) };
        onChange = (name) => { setName(name) };
    }
    
    
    console.log("-----render--------------");
    render();
    console.log("-----countChanged--------");
    onClick();
    onClick();
    console.log("-----nameChanged---------");
    onChange("kiana")
    onChange("kiana_k423")
    console.log("-----willUnMount---------");
    willUnMount();
    

    运行结果如下:

    count, name更新时 useEffect 分别执行自己的 callback。最后在 willUnMount 阶段执行 callback 有返回函数的 effect-destroy

    总结

    这个简易实现和 react 源码还是有很大出入的,主要还是因为 react 要考虑的情况有很多,如异步更新,优先级调度和自定义hook等其他场景。react 源码采用的是链表结构,然后链表中每个节点的数据结构定义如下:

     const effect: Effect = {
        tag, // 用来标识依赖项有没有变动
        create, // 用户使用useEffect传入的函数体
        destroy, // 上述函数体执行后生成的用来清除副作用的函数
        deps, // 依赖项列表
        next: (null: any), // 指向下一个 effect
    };
    

    文章为将复杂问题简单化就采用数组结构,然后只关注了核心功能。不过文章的简易实现,也是契合 react 实现的思路的,首先判断当前是初次挂载还是更新阶段,然后如果 callback 中有清除副作用的函数就保存好。通过依赖项的不同来进行不同的处理,最后在销毁前,依次执行之前保存好的清除副作用的函数。另外还可以先看一下之前的有关简易的useState实现

    全部代码如下:

    const _memoizedState = []; // 多个 hook 存放在这个数组
    let _idx = 0; // 当前 memoizedState 下标
    const _effectDestroy = []; // 存储多个 useEffect 回调函数返回的函数
    
    /**
     * 模拟实现 useState
     * @param {any} defaultState 默认值
     * @returns state 和 setState 方法
     */
    function useState(defaultState) {
        // 查看当前位置有没有值
        _memoizedState[_idx] = _memoizedState[_idx] || defaultState;
        // 再一次利用闭包,让 setState 更新的都是对应位置的 state
        const curIdx = _idx;
        function setState(newState) {
            // 更新对应位置的 state
            _memoizedState[curIdx] = newState;
            // 更新完之后触发渲染函数
            render();
        }
    
        // 返回当前 state 在 _memoizedState 的位置
        return [_memoizedState[_idx++], setState];
    }
    
    /**
     * 模拟实现 useEffect
     *  // deps 的不同对应着不同的情况,
        // 1. deps 不存在时:每次 state 的更新,都需要执行 callback
        // 2. deps 存在,但数组为空时,只需要在挂载也就是初次渲染时执行 callback
        // 3. deps 存在且有依赖项,则对应的依赖性更新时才执行 callback
     * @param {Function} callback 回调函数
     * @param {Array} deps 依赖项
     */
    function useEffect(callback, deps) {
        // 先根据当前下标获取到存储在全局 hooks 列表中当前位置原本的依赖项
        const memoizedDeps = _memoizedState[_idx];
        // 如果当前没有,则证明是初次渲染,无论什么情况都执行一次 callback
        if(!memoizedDeps) {
            const destroy = callback();
            // 同时更新依赖项
            _memoizedState[_idx] = deps;
            // 如果 callback 返回值是一个函数,则先把函数存储在全局的 destory 数组中,随后在willUnMount阶段依次执行
            if(typeof destroy === "function") {
                _effectDestroy.push(destroy);
            }
        // 否则就是 重新渲染 的阶段
        } else {
            // 没有依赖项直接执行 callback
            if(!deps) {
                callback();
            } else {
                // 依赖项不为空数组的时候且依赖项有更新了才去执行 callback 
                deps.length !== 0 && !deps.every((dep, idx) => dep === memoizedDeps[idx]) && callback();
                // 别忘了更新依赖项
                _memoizedState[_idx] = deps;
            }
        }
        _idx++;
    }
    
    
    let onClick;
    let onChange;
    const willUnMount = () => {
        for(let destroy of _effectDestroy) {
            destroy();
        }
    }
    
    function render() {
        // _idx 表示当前执行到的 hooks 的位置
        // _idx 重新置为 0, 也是契合react每次更新时都从 hooks 头节点开始更新每一个 hook
        _idx = 0;
        const [count, setCount] = useState(0);
        const [name, setName] = useState("77");
        useEffect(()=>{
            console.log("effect —— count", count);
            return () => {
                console.log("count Effect Destroy");
            }
        },[count])
        useEffect(()=>{
            console.log("effect —— name", name);
        },[name])
        // 使用 onClick, onChange 简单模拟一下更新操作
        onClick = () => { setCount(count + 1) };
        onChange = (name) => { setName(name) };
    }
    
    console.log("-----render--------------");
    render();
    console.log("-----countChanged--------");
    onClick();
    onClick();
    console.log("-----nameChanged---------");
    onChange("kiana")
    onChange("kiana_k423")
    console.log("-----willUnMount---------");
    willUnMount();
    

    相关文章

      网友评论

          本文标题:依据React Hooks的原理,写一个简易的 useEffec

          本文链接:https://www.haomeiwen.com/subject/mpcrkltx.html