之前根据 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++;
}
}
使用结果如下:
另外,我们来换换依赖项,分别实验一下其他结果:
貌似到这里,都进行的很不错。然而 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();
运行结果如下:
总结
这个简易实现和 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();