你们好,我是卡颂。html
你是否很讨厌Hooks
调用顺序的限制(Hooks
不能写在条件语句里)?前端
你是否遇到过在useEffect
中使用了某个state
,又忘记将其加入依赖项
,致使useEffect
回调执行时机出问题?react
怪本身粗心?怪本身很差好看文档?数组
答应我,不要怪本身。markdown
根本缘由在于React
没有将Hooks
实现为响应式更新。数据结构
是实现难度很高么?本文会用50行代码实现无限制版Hooks
,其中涉及的知识也是Vue
、Mobx
等基于响应式更新的库的底层原理。框架
本文的正确食用方式是收藏后用电脑看,跟着我一块儿敲代码(完整在线Demo
连接见文章结尾)。函数
手机党要是看了懵逼的话不要自责,是你食用方式不对。oop
注:本文代码来自Ryan Carniato的文章Building a Reactive Library from Scratch,老哥是
SolidJS
做者ui
首先来实现useState
:
function useState(value) {
const getter = () => value;
const setter = (newValue) => value = newValue;
return [getter, setter];
}
复制代码
返回值数组第一项负责取值,第二项负责赋值。相比React
,咱们有个小改动:返回值的第一个参数是个函数而不是state
自己。
使用方式以下:
const [count, setCount] = useState(0);
console.log(count()); // 0
setCount(1);
console.log(count()); // 1
复制代码
接下来实现useEffect
,包括几个要点:
依赖的state
改变,useEffect
回调执行
不须要显式的指定依赖项(即React
中useEffect
的第二个参数)
举个例子:
const [count, setCount] = useState(0);
useEffect(() => {
window.title = count();
})
useEffect(() => {
console.log('没我啥事儿')
})
复制代码
count
变化后第一个useEffect
会执行回调(由于他内部依赖count
),可是第二个useEffect
不会执行。
前端没有黑魔法,这里是如何实现的呢?
答案是:订阅发布。
继续用上面的例子来解释订阅发布关系创建的时机:
const [count, setCount] = useState(0);
useEffect(() => {
window.title = count();
})
复制代码
当useEffect
定义后他的回调会马上执行一次,在其内部会执行:
window.title = count();
复制代码
count
执行时会创建effect
与state
之间订阅发布的关系。
当下次执行setCount
(setter)时会通知订阅了count
变化的useEffect
,执行其回调函数。
数据结构之间的关系如图:
每一个useState
内部有个集合subs
,用来保存订阅该state变化的effect
。
effect
是每一个useEffect
对应的数据结构:
const effect = {
execute,
deps: new Set()
}
复制代码
其中:
execute
:该useEffect
的回调函数
deps
:该useEffect
依赖的state
对应subs
的集合
我知道你有点晕。看看上面的结构图,缓缓,咱再继续。
首先须要一个栈来保存当前正在执行的effect
。这样当调用getter
时state
才知道应该与哪一个effect
创建联系。
举个例子:
// effect1
useEffect(() => {
window.title = count();
})
// effect2
useEffect(() => {
console.log('没我啥事儿')
})
复制代码
count
执行时须要知道本身处在effect1
的上下文中(而不是effect2
),这样才能与effect1
创建联系。
// 当前正在执行effect的栈
const effectStack = [];
复制代码
接下来实现useEffect
,包括以下功能点:
每次useEffect
回调执行前重置依赖(回调内部state
的getter
会重建依赖关系)
回调执行时确保当前effect
处在effectStack
栈顶
回调执行后将当前effect
从栈顶弹出
代码以下:
function useEffect(callback) {
const execute = () => {
// 重置依赖
cleanup(effect);
// 推入栈顶
effectStack.push(effect);
try {
callback();
} finally {
// 出栈
effectStack.pop();
}
}
const effect = {
execute,
deps: new Set()
}
// 马上执行一次,创建依赖关系
execute();
}
复制代码
cleanup
用来移除该effect
与全部他依赖的state
之间的联系,包括:
订阅关系:将该effect
订阅的全部state
变化移除
依赖关系:将该effect
依赖的全部state
移除
function cleanup(effect) {
// 将该effect订阅的全部state变化移除
for (const dep of effect.deps) {
dep.delete(effect);
}
// 将该effect依赖的全部state移除
effect.deps.clear();
}
复制代码
移除后,执行useEffect
回调会再逐一重建关系。
接下来改造useState
,完成创建订阅发布关系的逻辑,要点以下:
调用getter
时获取当前上下文的effect
,创建关系
调用setter
时通知全部订阅该state
变化的effect
回调执行
function useState(value) {
// 订阅列表
const subs = new Set();
const getter = () => {
// 获取当前上下文的effect
const effect = effectStack[effectStack.length - 1];
if (effect) {
// 创建联系
subscribe(effect, subs);
}
return value;
}
const setter = (nextValue) => {
value = nextValue;
// 通知全部订阅该state变化的effect回调执行
for (const sub of [...subs]) {
sub.execute();
}
}
return [getter, setter];
}
复制代码
subscribe
的实现,一样包括2个关系的创建:
function subscribe(effect, subs) {
// 订阅关系创建
subs.add(effect);
// 依赖关系创建
effect.deps.add(subs);
}
复制代码
让咱们来试验下:
const [name1, setName1] = useState('KaSong');
useEffect(() => console.log('谁在那儿!', name1()))
// 打印: 谁在那儿! KaSong
setName1('KaKaSong');
// 打印: 谁在那儿! KaKaSong
复制代码
接下来基于已有的2个hook
实现useMemo
:
function useMemo(callback) {
const [s, set] = useState();
useEffect(() => set(callback()));
return s;
}
复制代码
这套50行的Hooks
还有个强大的隐藏特性:自动依赖跟踪。
咱们拓展下上面的例子:
const [name1, setName1] = useState('KaSong');
const [name2, setName2] = useState('XiaoMing');
const [showAll, triggerShowAll] = useState(true);
const whoIsHere = useMemo(() => {
if (!showAll()) {
return name1();
}
return `${name1()} 和 ${name2()}`;
})
useEffect(() => console.log('谁在那儿!', whoIsHere()))
复制代码
如今咱们有3个state
:name1
、name2
、showAll
。
whoIsHere
做为memo
,依赖以上三个state
。
最后,当whoIsHere
变化时,会触发useEffect
回调。
当以上代码运行后,基于初始的3个state
,会计算出whoIsHere
,进而触发useEffect
回调,打印:
// 打印:谁在那儿! KaSong 和 XiaoMing
复制代码
接下来调用:
setName1('KaKaSong');
// 打印:谁在那儿! KaKaSong 和 XiaoMing
triggerShowAll(false);
// 打印:谁在那儿! KaKaSong
复制代码
下面的事情就有趣了,当调用:
setName2('XiaoHong');
复制代码
并无log
打印。
这是由于当triggerShowAll(false)
致使showAll state
为false
后,whoIsHere
进入以下逻辑:
if (!showAll()) {
return name1();
}
复制代码
因为没有执行name2
,因此name2
与whoIsHere
已经没有订阅发布关系了!
只有当triggerShowAll(true)
后,whoIsHere
进入以下逻辑:
return `${name1()} 和 ${name2()}`;
复制代码
此时whoIsHere
才会从新依赖name1
与name2
。
自动的依赖跟踪,是否是很酷~
至此,基于订阅发布,咱们实现了能够自动依赖跟踪的无限制Hooks
。
这套理念是最近几年才有人使用么?
早在2010年初KnockoutJS
就用这种细粒度的方式实现响应式更新了。
不知道那时候,Steve Sanderson(KnockoutJS
做者)有没有预见到10年后的今天,细粒度更新会在各类库和框架中被普遍使用。
这里是:完整在线Demo连接