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

之前根据 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 (
    

You clicked {count} times

); }

总结: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的原理,写一个简易的 useEffect)