Hi everyone, this is Kasong.
Do you hate the Hooks
calling sequence of Hooks
(060ed0fd26591a cannot be written in conditional statements)?
Have you ever encountered a useEffect
used in state
, and forgot to add it to the dependency, causing problems with the timing of the
useEffect
Blame yourself for being careless? Blame yourself for not reading the document?
Promise me, don't blame yourself.
The root cause is that React
did not Hooks
as a responsive update.
Is it difficult to implement? This article will use 50 lines of code to implement the unlimited version of Hooks
, and the knowledge involved is also Vue
, Mobx
and other libraries based on responsive updates.
The correct way to eat this article is to read it with a computer after collecting it, and follow me to type the code (see the end of the article for the Demo
Don’t blame yourself if the mobile party is dumbfounded, it’s because you eat it in the wrong way.
Note: The code in this article comes from Ryan Carniato's article Building a Reactive Library from ScratchSolidJS
brother is the author of 060ed0fd265aa4
Rise from the ground
First, implement useState
:
function useState(value) {
const getter = () => value;
const setter = (newValue) => value = newValue;
return [getter, setter];
}
The first item of the return value array is responsible for obtaining values, and the second item is responsible for assigning values. Compared to React
, we have a small change: the first parameter of the return value is a function instead of state
itself.
The usage is as follows:
const [count, setCount] = useState(0);
console.log(count()); // 0
setCount(1);
console.log(count()); // 1
No black magic
Next, implement useEffect
, including several key points:
- Dependent
state
change,useEffect
callback execution - No need to explicitly specify dependencies (that is, the second parameter of
React
useEffect
for example:
const [count, setCount] = useState(0);
useEffect(() => {
window.title = count();
})
useEffect(() => {
console.log('没我啥事儿')
})
count
changes, the first useEffect
will execute the callback (because it relies on count
internally), but the second useEffect
will not be executed.
There is no black magic on the front end, how is it achieved here?
The answer is: subscribe to publish.
Continue to use the above example to explain the timing of the establishment of a subscription-publishing relationship:
const [count, setCount] = useState(0);
useEffect(() => {
window.title = count();
})
When useEffect
defined, its callback will be executed immediately, and it will be executed internally:
window.title = count();
count
will establish the subscription and publication relationship between effect
and state
The next execution setCount
when (setter) will notify subscribed count
change useEffect
, execute its callback function.
The relationship between the data structure is shown in the figure:
Each useState
has a set subs
, which is used to save subscribed to the state change effect
.
effect
is the data structure corresponding to useEffect
const effect = {
execute,
deps: new Set()
}
among them:
execute
: the callback function ofuseEffect
deps
: TheuseEffect
state
that the 060ed0fd265db4 depends on corresponds tosubs
I know you are a little dizzy. Look at the structure diagram above, slowly, let's continue.
Implement useEffect
First, a stack is needed to save the currently executing effect
. In this way, when getter
is state
effect
should be connected with.
for example:
// effect1
useEffect(() => {
window.title = count();
})
// effect2
useEffect(() => {
console.log('没我啥事儿')
})
count
need to know what is in the execution effect1
context of (but not effect2
), in order to effect1
establish contact.
// 当前正在执行effect的栈
const effectStack = [];
Next, implement useEffect
, including the following function points:
- Reset the dependencies before each
useEffect
callback is executed (thestate
of thegetter
inside the callback will rebuild the dependencies) - When the callback is executed, make sure that the current
effect
is at the topeffectStack
effect
from the top of the stack after the callback is executed
code show as below:
function useEffect(callback) {
const execute = () => {
// 重置依赖
cleanup(effect);
// 推入栈顶
effectStack.push(effect);
try {
callback();
} finally {
// 出栈
effectStack.pop();
}
}
const effect = {
execute,
deps: new Set()
}
// 立刻执行一次,建立依赖关系
execute();
}
cleanup
used to remove the effect
with all his dependent state
link between, including:
- Subscribe relationship: the
effect
all subscribedstate
changes removed - Dependency: Remove
state
effect
depends on
function cleanup(effect) {
// 将该effect订阅的所有state变化移除
for (const dep of effect.deps) {
dep.delete(effect);
}
// 将该effect依赖的所有state移除
effect.deps.clear();
}
After removal, execute the useEffect
callback to rebuild the relationship one by one.
Transform useState
Next, transform useState
to complete the logic of establishing a subscription-publishing relationship. The main points are as follows:
- Call
getter
get the current context wheneffect
, build relationships - When calling
setter
effect
callbacks that subscribe to thestate
change to execute
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
also includes the establishment of two relationships:
function subscribe(effect, subs) {
// 订阅关系建立
subs.add(effect);
// 依赖关系建立
effect.deps.add(subs);
}
Let's try it out:
const [name1, setName1] = useState('KaSong');
useEffect(() => console.log('谁在那儿!', name1()))
// 打印: 谁在那儿! KaSong
setName1('KaKaSong');
// 打印: 谁在那儿! KaKaSong
Implement useMemo
Next, based on the existing two hook
achieve useMemo
:
function useMemo(callback) {
const [s, set] = useState();
useEffect(() => set(callback()));
return s;
}
Automatic dependency tracking
This set of 50 lines of Hooks
also has a powerful hidden feature: automatic dependency tracking.
Let's extend the above example:
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()))
Now we have 3 state
: name1
, name2
, showAll
.
whoIsHere
as memo
, depends on the above three state
.
Finally, when whoIsHere
changes, the useEffect
callback will be triggered.
When the above code runs, based on the initial three state
, whoIsHere
calculated, and then the useEffect
callback will be triggered and printed:
// 打印:谁在那儿! KaSong 和 XiaoMing
Next call:
setName1('KaKaSong');
// 打印:谁在那儿! KaKaSong 和 XiaoMing
triggerShowAll(false);
// 打印:谁在那儿! KaKaSong
The following things are interesting when called:
setName2('XiaoHong');
And there is no log
printed.
This is because when triggerShowAll(false)
causes showAll state
to false
, whoIsHere
enters the following logic:
if (!showAll()) {
return name1();
}
name2
not executed, name2
and whoIsHere
no longer have a subscription publishing relationship!
Only after triggerShowAll(true)
, whoIsHere
enters the following logic:
return `${name1()} 和 ${name2()}`;
At this time, whoIsHere
will rely on name1
and name2
.
Automatic dependency tracking, isn’t it cool~
to sum up
At this point, based on subscription publication , we can achieve the automatic dependency tracking unlimited Hooks
.
Is this concept only used in recent years?
As early as the beginning of 2010, KnockoutJS
used this fine-grained way to implement responsive updates.
I don't know at that time, Steve Sanderson ( KnockoutJS
) did you foresee that fine-grained updates will be widely used in various libraries and frameworks today, 10 years later.
Here is: complete online Demo link
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。