不知大家有没有听过Preact
这个框架,就算没听过Preact
那也应该听过React
吧?
一字之差,preact
比react
多了个p
!(听起来咋不像啥好话呢)
这个P
代表的是 Performance
,高性能版React
的意思。Preact
一开始是CodePen
上的一个小项目,有点类似于咱们国内常见的《三百行代码带你实现个React》
这类文章,用最少的代码实现了React
的最基本功能,然后放到CodePen
上供大家敬仰学习。
当然这是很多年前的事了,那时候这种东西很容易火,想想N
年前你看过的第一篇《三百行实现个Vue》
、《三百行实现个React》
之类的文章是不是竞争对手很少、很容易引发大量的关注度。不过现在不行了,太卷!这类文章隔三差五的就能看到一篇,同质化严重,导致大家都有点审美疲劳了。
但在那个年代Preact
就是这么火起来的,三百行实现了个React
引发大量关注度之后,作者觉得自己做的这玩意好像还挺不错的哈!于是开始继续完善,完善后拿去一测试:性能简直完爆React
呀!我这玩意不仅体积比你小、性能还比你高。就这样作者开始有些膨胀了、开始飘了!
那我就给这个框架起个名叫
Preact
吧!
Performance
版的React
!
打开Preact
官网,映入眼帘的便是它的最大卖点:
只有3KB
大小、并且与React
拥有相同的API
。真的只有3KB
么?虚拟DOM
、Diff
算法、类组件、Hooks
… 这些就算实现的再怎么巧妙也需要很多代码才行吧?我们直接用Vite
来创建一个Preact
项目来试下:
npm create vite
如果屏幕前的你用的是VSCode
这个编辑器的话,可以安装一下Import Cost这个插件:
安装好之后我们来看一下主文件(main.jsx
):
卧槽?gizpped
真的只有3.几K
!不过这算法有点鸡贼啊,来了个向下取整:
这让我想起了最近非常火的Turbopack
比Vite
快十倍的宣传口号,遭尤大怒怼:1k
组件的案例下有数字的四舍五入问题,Turbopack
的 15ms
被向下取整为 0.01s
,而到了 Vite
这里 87ms
被向上取整为 0.09s
。这把本来接近 6
倍的差距扩大到了 10
倍。
不过即使这样,3.8K
依然是一个很惊人的成就。是不是只是render
这个函数就占了3.8K
啊?我们再引点东西试试:
难以置信!引了这么多hooks
居然只多加了0.1K
!我还是不太相信用0.1K
的代码就能实现出React Hooks
来,肯定是用了什么特殊的算法专门针对了这一场景做了优化,我们按照官网的写法来重新引一下:
这回体积明显增大了不少:
不过咋感觉自己跟个杠精似的呢 人家说了3KB
我却非要以各种方式证明肯定不止3KB
,这样不好。Preact
真的已经很轻量了,一般人想要实现这么多功能还真做不到只用这么少的代码,Preact
的P
肯定还是名不虚传的
不过刚刚试了下Vue
,Vue
好像就没有针对这种场景做专门的优化,不仅没优化反而还劣化了:
实际上只引入某几个函数的话Vue
没有这么大,这是把Vue
全量引入的大小,尤大
还不快跟人学学。
说到"学"
,Preact
原本一直都是React
的忠实粉丝,可最近它却开发了一个叫做@preact/signals
的东西,这是干嘛的?Preact
的创始人Jason Miller以及Preact DevTools
的创始人Marvin Hagemeister共同写了篇博客:《Introducing Signals》
点开文章,首先映入眼帘的便是这样一个案例:
import { signal, computed } from "@preact/signals";
const count = signal(0);
const double = computed(() => count.value * 2);
function Counter() {
return (
<button onClick={() => count.value++}>
{count} x 2 = {double}
</button>
);
}
等等!这个.value
、这个computed
、以及这个在jsx
的大括号{}
中不用写.value
的语法…
怎么这么似曾相识呢?好像在哪里见过类似的写法:
<script setup>
import { ref, computed } from "vue";
const count = ref(0);
const double = computed(() => count.value * 2);
script>
<template>
<button @click="count++">
{{count}} x 2 = {{double}}
button>
template>
与
hooks
不同,signals
可以在组件内部或外部使用。signals
在类组件也可以很好的运行,因此您可以按照自己的节奏引入它们,并根据现状,在几个组件中试用它们,并随着时间的推移逐渐采用它们。——Preact团队
那这样不是越写越Vue
了吗?还叫什么Preact
啊,叫Vreact
多好!
尤雨溪:这还真特娘的是个好主意!我这就把拉你进 Vue 核心群里来!
我们来看看Preact
团队为何要实现个P
版的Composition API
:
听说最近尤大
被骂了,为啥被骂呢?因为好像有次字节邀请了尤大
直播,那尤大
肯定得借此机会好好宣传一下Vue
啊!不过你光说Vue
有多好,观众可能无法感受到。就像如果七龙珠
直接让超级赛亚人出场,并且用那个战斗力探测仪显示一个战力:
虽说凭这个确实能让人感受到超级赛亚人很强,但如果要是能有个对比的话那才是最完美的剧情,所以才有了大反派弗利萨
的出场机会:
同理,尤大如果光在那罗列数据那肯定不如有个对比来的直观,那就把React
拉来对比一番呗!既然是为了宣传Vue
,那必须得拿Vue
的优点跟React
的缺点比啦!这样的对比难免会有失偏颇,让React
的粉丝们怒不可遏,在群里疯狂批判尤大。
在一捧一踩(黑React
)的宣传过程中呢,尤大花费最多时间宣传的就是以下两点:
React Hooks
的一些心智负担React
强其实这两点多少还是有点有失偏颇,因为Vue
在解决了一种心智负担的同时又带来了另一种心智负担,而且性能也要看场景的,尤大只强调了对Vue
有利的场景来宣传…
不过React
在某些层面来讲确实有些剑走偏锋了哈,导致性能不是特别理想。Preact
也是这么认为的,他们还特意搞了张火焰图:
左边用的是Preact Hooks
,右边用的是Composition API
… 哦不,是Preact Signals
。可以看到Signals
的表现完胜Hooks
!
那Preact
的老师React
有在React
里实现Vue
的计划吗?答案是否定的,自从Preact Signals
发布后大家就疯狂@Dan
,Dan
看完后直接来了句:这与React
的发展理念不是很吻合。(潜台词:我们才不会在React
里实现Vue
呢)
其实我觉得也是,React
的发展理念本来就跟Vue
走的是完全不同的两种路线,夸张点说就是道不同不相为谋。
那肯定有人说:不对呀,Vue3
的Composition API
不是抄袭的React
么?
这么说吧:大佬们借鉴的是思路,菜鸟们借鉴的才是代码。了解过Vue
、React
他俩底层实现的朋友们应该都清楚他俩的差距有多大,尤雨溪在某次采访时说过Vue3
一开始本打算实现成类组件,既然是类那就离不开装饰器的话题,尤大说他们甚至都已经实现出来了一版类组件写法的Vue3
。只不过他觉得这样相对于Vue2
而言除了对TS
的支持度之外几乎没有其他什么特别明显的优势,而且装饰器提案发展了N年却迟迟未能落地,尤大觉得这样遥遥无期,而且就算真的在将来的某一天落地了,是不是也已经与现在TS
实现的那版装饰器天差地别了?
Angular
用装饰器用的好好的那是因为人家强制要求使用TS
,但Vue
显然不可能这样做。而且为了防止未来装饰器有变动(其实最近已经Stage3
的装饰器已经和TS
装饰器不一样了),许多曾经使用装饰器语法的库为了规避这个风险也已经改用了别的写法,如:MobX
、React DnD
等…
推荐阅读:《mobx6.0为什么移除装饰器》
正当尤雨溪为此抓耳挠腮、夜不能寐之时,React Hooks
横空出世了!这种函数式组件瞬间就让尤大眼前一亮,他脑袋里的灯泡在那一刹那间被点亮了:
这不就是自己一直苦苦寻找、对TS
友好、方便代码复用、语法简洁、低耦合的解决方案么!
但实际上吧,尤大只是参考了这种函数式的设计,如今的Composition API
原理与React Hooks
相去甚远。真要说抄借鉴的话,尤雨溪已经大大方方承认了是受到了React Hooks
的启发,代码层面借鉴的是Meteor Tracker、nx-js/observer-util、salesforce/observable-membrane这三个库。响应式库其实早已不新鲜了,只是之前尤大没能跳出Vue2
的思维限制,直到看到了React Hooks
才想到可以这样写,然后再一调研发现市面上早就有了函数式的响应式库,Composition API
就是这么来的。
推荐阅读:《[译]尤雨溪:Vue3的设计过程》
不过他在Composition API
之前确实抄模仿了React
的原理设计出来了vue-hooks,以用来探索这种函数式组件的可行性。不过好在后来他发现了Meteor Tracker、nx-js/observer-util、salesforce/observable-membrane这几个库并及时悬崖勒马,没有在这个方向上继续深挖,不然的话Vue3
可能就要变成套壳React
了。
那究竟为什么没有在此方向继续深挖呢?难道说那仨库的解决方案比React Hooks
还要好吗?对此我只想说:
抛开了场景谈好坏都是在耍流氓
这两种方案各有优缺点,巧合的是:双方彼此间的优点恰恰好好就是对方身上的缺点。典型的性格互补么这不是:
有人喜欢内向的、有人喜欢外向的、但也有人想当一个缝合怪:为啥不能内外双向呢?该内向的时候就内向,该外向的时候就外向呗!Preact
就是这样想的,他们单独提供了一个叫@preact/signals
的包,你要是更在意性能呢,那就用@preact/signals
、你要是更在意类似React
的开发体验呢,那就不用呗!
Preact
版的composition api
主要分为三个部分:
@preact/signals-core
@preact/signals
@preact/signals-react
从命名上来看,@preact/signals-core
应该是与框架无关的核心实现、@preact/signals
是给Preact
的特供产品、而@preact/signals-react
则是给React
提供的特供产品。
我们先来看一下核心实现的用法,这是他们README
文件里给出的第一个例子:
import { signal } from "@preact/signals-core";
const counter = signal(0);
// Read value from signal, logs: 0
console.log(counter.value);
// Write to a signal
counter.value = 1;
非常好理解,就是把原来composition api
里的ref
换成了signal
,这里就不过多赘述了,来看下一个案例:
const counter = signal(0);
const effectCount = signal(0);
effect(() => {
console.log(counter.value);
// Whenever this effect is triggered, increase `effectCount`.
// But we don't want this signal to react to `effectCount`
effectCount.value = effectCount.peek() + 1;
});
这个effect
也和composition api
里的effect
如出一辙,不过有同学可能会问了:composition api
里没有effect
呀?你说的是watchEffect
吗?我这里表述的可能不是特别准确,准确来讲的话应该是和@vue/reactivity
里的effect
如出一辙。
那么问题来了:@vue/reactivity
不就是composition api
吗?其实他俩确实非常的…容易混淆,准确来讲@vue/reactivity
是可以运行在完全脱离vue
的环境之下的,而composition api
是根据vue
的环境进行的进一步更好用的封装。composition api
包含了@vue/reactivity
。
那composition api
和@vue/composition-api
又有啥区别呢?区别就是composition api
只是一个概念,而@vue/composition-api
是一个实现了composition api
的项目。当初尤雨溪提出composition api
的时候(那时候还不叫composition api
,好像叫什么functional base api
)遭到了大量质疑的声音,于是有个大佬就用Vue2
现有的API
实现了一版尤雨溪的提案,尤雨溪觉得这玩意非常不错!你们老喷我是因为你们没有体验过函数式的好,你们先用用试试,试完了保证你们直呼真香!于是联系该作者把Vue2
版的composition api
合并到Vue
的仓库中并发布为@vue/composition-api
。但谁也不会用爱发电对不,刚开始当个娱乐项目给你宣传了,时间一长也没啥收益,该作者也就不维护了。此时另一位大佬出现了,他说既然没人维护了那就交给我吧!他就是肝帝AntFu
:
一整年就两三天是灭着的,剩下的时间无论刮风还是下雨,都无法阻挡大佬提交代码的脚步。甚至那两三天我都怀疑是有什么不可抗力导致的,比方说来台风断电啦或者在飞机上没法提交,下了飞机直接就进入另一个时区(第二天)啦之类的原因,他甚至比尤雨溪都勤快:
不过拿他俩比有点不太公平哈,尤大有家有孩子,而且还要带领两个团队(Vue
、Vite
),写代码的时间自然会少很多。而傅佬年轻没结婚没孩子、也无需带领团队啥的,自然就会有很多时间做自己喜欢做的事情。不过我翻了一下尤大迭代最疯狂的2016
年,也依然没我傅哥勤快:
这就是我傅哥为何能如此高产的原因。
有点扯远了哈,没接触过@vue/reactivity
的effect
同学暂且先把它理解为composition api
的watchEffect
,在这里开始出现了一个与composition api
不太一样的api
了哈,.peek()
是什么鬼?为了帮助大家快速理解这玩意,我们需要对比一下composition api
里两个相似功能的api
:watch
和watchEffect
。
这俩api
功能相似但各有优缺点,我们只说watchEffect
不如watch
的其中一个缺点:无法精确控制到底监听了哪个响应式变量。
比方说我们写了这样一段逻辑:
import { ref, watchEffect } from 'vue';
const a = ref(0);
const b = ref(0);
watchEffect(() => {
console.log(a.value);
b.value++;
});
每当我们改动a.value
的值时,b.value
就会++
。这是我们希望的逻辑,但不幸的是,每当我们改动b.value
的值时,b.value
还是会++
。这在watch
里还是很好实现的:
import { ref, watch } from 'vue';
const a = ref(0);
const b = ref(0);
watch(a, value => {
console.log(value);
b.value++;
});
但在watchEffect
那段代码里就相当于在watch
中写了这样一段代码:
import { ref, watch } from 'vue';
const a = ref(0);
const b = ref(0);
watch([a, b], ([valueA, valueB]) => {
console.log(valueA);
b.value = valueB + 1;
});
Vue
的方案是既提供一个自动收集依赖的watchEffect
,同时也提供一个手动收集依赖的watch
。
而Preact
的方案则是只提供一个effect
(类似Vue
的watchEffect
),如果你写出类似上面那样的代码:
import { signal, effect } from '@preact/signals-core';
const a = signal(0);
const b = signal(0);
effect(() => {
console.log(a.value);
b.value++;
});
那就直接报错给你看:
为什么会报错呢?了解过响应式原理的同学应该不难理解,就是触发getter
的时候又会触发setter
,而触发了setter
又会导致重新运行effect
函数导致死循环。但如果你没了解过响应式原理的话可能就不太清楚我说的到底是什么意思,建议阅读一下这篇:
《尤大:怎么还生啃源码呢?我这就亲手给你写个丐版Vue》
这是尤大在国外的VueMastery
教程网站中直播的手写简易版Vue
,文章里有详细的图例来帮助大家快速理解,短短几十行代码就能实现一个简易的响应式系统,吃透了原理你就会明白为什么这样写会导致死循环了。
那为啥Vue
那边的代码没死循环呢?这是因为Vue
做了这样一层判断:如果你在effect
/ watchEffect
里触发了setter
,那便不会触发对应的effect
/ watchEffect
函数,这样就可以避免死循环了。
那Preact
没做这样的处理怎么办呢?那我们就避免在effect
里既对signal
进行取值操作同时又对它进行赋值操作呗!
不过这样做肯定是不行的哈,你这太不专业了,所有成熟的响应式库没有哪个会放着这个问题不去解决的。比方说Solid.js
,他就有一个叫untrack
的函数,假如我们在effect
里想要获取到一个响应式的值但却并不想它被收集到依赖里面去就可以写成这样:
import { createSignal, createEffect, untrack } from "solid-js";
const [a, setA] = createSignal(0);
const [b, setB] = createSignal(0);
createEffect(() => {
console.log(a());
console.log(untrack(b));
});
这样只有a
改变时会触发effect
函数,b
则不会。如果你能理解上面这段代码的话,那相信你肯定能理解下面这段代码:
import { signal, effect } from '@preact/signals-core';
const a = signal(0);
const b = signal(0);
effect(() => {
console.log(a.value);
b.value = b.peek() + 1;
});
我还专门去查了一下peek
是啥意思,是偷窥
的意思。有时候觉得老外起的api
名翻译过来还蛮有意思的,就是说我在effect
里需要获取到某个响应式变量的值,但直接获取会被追踪到,所以我不直接获取,我要“偷窥”一眼它的值,这样就不会被追踪到啦!(这个api
虽然很调皮,但有些略显猥琐)
接下来看下一个api
:computed
。就我不说你们都能猜到这是干啥的,这就是Vue
的那个computed
,直接看例子就不解释了:
import { signal, computed } from "@preact/signals-core";
const name = signal("Jane");
const surname = signal("Doe");
const fullName = computed(() => name.value + " " + surname.value);
// Logs: "Jane Doe"
console.log(fullName.value);
// Updates flow through computed, but only if someone
// subscribes to it. More on that later.
name.value = "John";
// Logs: "John Doe"
console.log(fullName.value);
下一个api
是effect
,其实在.peek()
那个“偷窥”案例中就已经用过effect
了,它就是@vue/reactivity
里的effect
,也不过多解释了,直接上案例:
import { signal, computed, effect } from "@preact/signals-core";
const name = signal("Jane");
const surname = signal("Doe");
const fullName = computed(() => name.value + " " + surname.value);
// Logs: "Jane Doe"
effect(() => console.log(fullName.value));
// Updating one of its dependencies will automatically trigger
// the effect above, and will print "John Doe" to the console.
name.value = "John";
import { signal, computed, effect } from "@preact/signals-core";
const name = signal("Jane");
const surname = signal("Doe");
const fullName = computed(() => name.value + " " + surname.value);
// Logs: "Jane Doe"
const dispose = effect(() => console.log(fullName.value));
// Destroy effect and subscriptions
dispose();
// Update does nothing, because no one is subscribed anymore.
// Even the computed `fullName` signal won't change, because it knows
// that no one listens to it.
surname.value = "Doe 2";
接下来这个api
可能会有些令大家陌生了,叫batch
,分批处理的意思,来看如下案例:
import { signal, computed, effect, batch } from "@preact/signals-core";
const name = signal("Jane");
const surname = signal("Doe");
const fullName = computed(() => name.value + " " + surname.value);
// Logs: "Jane Doe"
effect(() => console.log(fullName.value));
// Combines both signal writes into one update. Once the callback
// returns the `effect` will trigger and we'll log "Foo Bar"
batch(() => {
name.value = "Foo";
surname.value = "Bar";
});
有一定开发经验的同学应该一下子就能看出这段代码想表达什么意思了(如果看不懂的话去反思一下),就是当我们修改值的时候是同步出发对应的effect
函数的,所以我们如果连着改两次就会连续运行两次,我写了一个简化版的案例给大家看一下:
import { signal, effect } from "@preact/signals-core";
const a = signal(0);
effect(() => console.log(a.value));
a.value++;
a.value++;
控制台打印结果:
就挺让人无语的…… 这也能水个API
出来?人家Vue
默认就是分批处理的,我们在Vue
里写一段同样的代码来看看Vue
是怎么运行的:
import { ref, watchEffect } from 'vue';
const a = ref(0);
watchEffect(() => console.log(a.value));
a.value++;
a.value++;
控制台打印结果:
不过之前咱们不是说Vue
的响应式依赖是@vue/reactivity
么?Composition API
是Vue
在@vue/reactivity
的基础上再次封装,让它变得更好用更适合Vue
项目。那会不会是它封装了批处理才导致这样的结果的呢?我们先不用import xxx from 'vue'
这种形式了,这样的话用的是Composition API
,我们这次用@vue/reactivity
再来试一把:
import { ref, effect } from '@vue/reactivity';
const a = ref(0);
effect(() => console.log(a.value));
a.value++;
a.value++;
果不其然,这次的结果终于和Preact
保持一致了:
误会了哈!我还寻思@preact/signals-core
也太不专业了,人家@vue/reactivity
默认就支持的东西……
不过既然@vue/reactivity
默认也是同步的,那怎么分批处理呢?想让它像@preact/signals-core
这样:
import { signal, effect, batch } from "@preact/signals-core";
const a = signal(0);
effect(() => console.log(a.value));
batch(() => {
a.value++;
a.value++;
})
在@vue/reactivity
中要想要达到同样效果的话… 关键是这个@vue/reactivity
连个文档都没有!Vue
官网上的Composition API
是又封装了一层,用法已经不一样了。比方说Composition API
里的watch
在@vue/reactivity
里就没有,而且watchEffect
和effect
表现也不太一致,@vue/reactivity
的README
写的也特别简陋:
就很无奈,我想知道这个库怎么用就只能去看看它的TS
定义,看看都有哪些API
以及都有哪些用法。哪怕不像Vue
那样有个专门的官网,那你在README
里写几个简单的事例也行啊!就像@preact/signals-core
那样,能耽误你几小时?
吐槽归吐槽,想知道咋用还是得去看代码,在了src/effect.ts
后我发现这样一段代码:
果然还是和Composition API
里的watchEffect
参数不一致,我们能看到有个lazy
字段,从名字上来看应该就是它了吧。我还特意去Vue
官网看了一下watchEffect
的第二个参数都有哪些字段,watchEffect
就没有lazy
这个字段,取而代之的是flush
字段:
用法这么大差异,连个文档都不写。尤大,你是想让每个用@vue/reactivity
的人都去从源码里找答案么?算了不吐槽了,咱们继续来看例子:
import { ref, effect } from "@vue/reactivity";
const a = ref(0);
effect(() => console.log(a.value), { lazy: true });
a.value++;
a.value++;
加了{ lazy: true }
以后控制台啥都不打印了!尤大你是要气死我呀!那这个lazy
到底是用来干啥的?可能是用来代替Composition API
里的watch
的吧?wacth
会自动执行一次,effect
则不会这样。那也不对啊,watch
只是刚开始的时候不会自动执行一次,但当依赖变化时还是会运行啊,这怎么连运行都不运行了?不是你别让我猜呀!想知道你这库咋用就两种方式:要么看源码要么就靠猜…… 那如果不是lazy
的话那就是scheduler
字段?想看看你这咋用,结果你给我来个这:
文档不写就算了,你还定义了一堆any
类型… 这特么到底咋用啊?好像以前看过的《Vue.js设计与实现》里有写过,不过那本书搬家放在哪里想不起来了,等我找到后再把例子给补上。
之前还想吐槽@preact/signals-core
不专业,Vue
早就支持的功能它还要专门出一个API
。现在看来还是我太年轻,与框架无关的@vue/reactivity
连个文档都没有,都不知道怎么支持这个批量更新,不专业的反而是@vue/reactivity
。
咱们继续来看下一个案例:
import { signal, computed, effect, batch } from "@preact/signals-core";
const counter = signal(0);
const double = computed(() => counter.value * 2);
const tripple = computed(() => counter.value * 3);
effect(() => console.log(double.value, tripple.value));
batch(() => {
counter.value = 1;
// Logs: 2, despite being inside batch, but `tripple`
// will only update once the callback is complete
console.log(double.value);
});
// Now we reached the end of the batch and call the effect
这是啥意思呢?就是我们在batch
函数里访问了一个计算属性,按理说要等batch
函数运行完了才会去更新,但这个计算属性依赖的值在batch
里刚刚被改过,为了让我们能拿到正确的值,不等batch
执行完就直接更新这个计算属性。但也不是所有依赖counter
的计算属性都会被更新,没在batch
函数里被访问到的tripple
就会等batch
函数运行完毕后再去进行更新。
batch
函数还可以嵌套着写:
import { signal, computed, effect, batch } from "@preact/signals-core";
const counter = signal(0);
effect(() => console.log(counter.value));
batch(() => {
batch(() => {
// Signal is invalidated, but update is not flushed because
// we're still inside another batch
counter.value = 1;
});
// Still not updated...
});
// Now the callback completed and we'll trigger the effect.
当最外层的batch
函数运行完成时才会更新对应的值。
core
的核心部分讲完了,那就继续看看@preact/signals
以及@preact/signals-react
吧!它俩用法都一样:
import { signal } from "@preact/signals-react";
const count = signal(0);
function CounterValue() {
// Whenver the `count` signal is updated, we'll
// re-render this component automatically for you
return <p>Value: {count.value}</p>;
}
import { useSignal, useComputed } from "@preact/signals-react";
function Counter() {
const count = useSignal(0);
const double = useComputed(() => count.value * 2);
return (
<button onClick={() => count.value++}>
Value: {count.value}, value x 2 = {double.value}
</button>
);
}
就有点类似于在React
里写Vue
的那种感觉。
晚上回家一顿翻,终于找着了《Vue.js设计与实现》这本书,声明一下本文真不是这本书的软广告,多卖出去一本我也不会得到什么分成。真就是我写那个例子的时候找不到文档又不知道咋用,README
让去看TS
声明结果看了个any
…
我是真没耐心去特别仔细的研究@vue/reactivity
的源码,我觉得理解了大概的原理就行不必那么死抠细节,毕竟咱们一不靠卖源码课赚钱、二也不负责维护Vue
、三也不像一些大佬似的没事就以钻研为乐、四也不至于研究完源码就能升职加薪什么的…
不过好在我之前看过那本书里面写的挺详细的好像有scheduler
、lazy
之类的字段是用来干嘛的并且还给出了实现以及用例。我又看了一遍响应式那章,之前靠猜以为lazy
是用来模仿watch
的,结果写了{ lazy: true }
之后直接不运行了,这是因为写了{ lazy: true }
就从自动挡变手动挡了!返回一个函数让你自己去决定啥时候运行:
import { ref, effect } from '@vue/reactivity'
const a = ref(0)
const fn = effect(() => console.log(a.value), { lazy: true })
a.value++
fn()
打印结果:
那这样写有什么意义呢?这样写确实没什么意义,本来能自动运行的函数非要让你手动运行。这样做的意义主要是为了实现computed
的,咱们想要的是@preact/signals-core
里的batch
批处理功能,书中的scheduler
选项接收一个参数,但实测当前最新版本的@vue/reactvity
没有任何参数:
import { ref, effect } from '@vue/reactivity'
const a = ref(0)
effect(
() => console.log(a.value),
{
scheduler (fn) {
console.log(fn)
}
}
)
a.value++
打印结果:
盲猜可能是版本变化导致的用法不一致行为,我们把@vue/reactivity
的版本改成3.0
后再来打印一下:
这回有值了,那为什么会把这个参数删掉呢?我们只能从CHANGELOG
里找答案了:
从有限的信息我们可以得知大概是从3.2
及后续版本删掉了的,3.0.x
及3.1.x
与书中用法保持一致。在书中scheduler
的参数十分重要,书中就是基于这个参数来实现的批处理能力。想知道新用法么?我不告诉你!就是不写文档嘿嘿!看源码去吧!
这让我突然想起尤大在某纪录片中吐槽有些人就是不看文档,我也想吐个槽:你特么倒是写呀!
没办法了,先钻研一下源码吧!经过我一段时间的钻研呢,大概得出来了一个结论:在3.2
之后effect
的返回值其实就相当于3.2
之前scheduler
的参数:
// 3.2 以前
effect(
() => {},
{
scheduler (fn) {
console.log(fn)
}
}
)
// 3.2 之后(含 3.2)
const fn = effect(
() => {},
{
scheduler () {
console.log(fn)
}
}
)
那我们就可以根据这一变化来重写书中给出的调度执行的案例了:
import { ref, effect } from '@vue/reactivity'
const jobQueue = new Set()
const p = Promise.resolve()
let isFlushing = false
function flushJob() {
if (isFlushing) return
isFlushing = true
p.then(() => {
jobQueue.forEach(job => job())
}).finally(() => {
isFlushing = false
})
}
const a = ref(0)
const fn = effect(
() => console.log(a.value),
{
scheduler () {
jobQueue.add(fn)
flushJob()
}
}
)
a.value++
a.value++
这次的打印结果就与@preact/signals-core
保持一致了:
为什么会在3.2
以后去掉这个这个参数呢?我觉得是因为这个参数与effect
的返回值一致,相当于重复了,不信的话我们来拿3.0
来做个实验:
import { ref, effect } from '@vue/reactivity'
const a = ref(0)
const fn = effect(
() => console.log(a.value),
{
scheduler (func) {
console.log(fn === func)
}
}
)
a.value++
打印结果:
吐槽:重复了你就在
CHANGELOG
里写一句因与返回值重复故删之类的话呗!啥也不写就非得让人去看源码