依据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 (
    <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();
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容