信仰崩了?Preact 開始採用 Vue3 的響應式設計
theme: channing-cyan
前言
不知大家有沒有聽過Preact
這個框架,就算沒聽過Preact
那也應該聽過React
吧?
一字之差,preact
比react
多了個p
!(聽起來咋不像啥好話呢)
這個P
代表的是 Performance
,高效能版React
的意思。Preact
一開始是CodePen
上的一個小專案,有點類似於咱們國內常見的《三百行程式碼帶你實現個React》
這類文章,用最少的程式碼實現了React
的最基本功能,然後放到CodePen
上供大家~敬仰~學習。
當然這是很多年前的事了,那時候這種東西很容易火,想想N
年前你看過的第一篇《三百行實現個Vue》
、《三百行實現個React》
之類的文章是不是競爭對手很少、很容易引發大量的關注度。不過現在不行了,太卷!這類文章隔三差五的就能看到一篇,同質化嚴重,導致大家都有點審美疲勞了。
但在那個年代Preact
就是這麼火起來的,三百行實現了個React
引發大量關注度之後,作者覺得自己做的這玩意好像還挺不錯的哈!於是開始繼續完善,完善後拿去一測試:效能簡直完爆React
呀!我這玩意不僅體積比你小、效能還比你高。就這樣作者開始有些膨脹了、開始飄了!
那我就給這個框架起個名叫
Preact
吧!
Performance
版的React
!
Preact 簡介
開啟Preact
官網,映入眼簾的便是它的最大賣點:
只有3KB
大小、並且與React
擁有相同的API
。真的只有3KB
麼?虛擬DOM
、Diff
演算法、類元件、Hooks
… 這些就算實現的再怎麼巧妙也需要很多程式碼才行吧?我們直接用Vite
來建立一個Preact
專案來試下:
js
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 Signals
說到"學"
,Preact
原本一直都是React
的忠實粉絲,可最近它卻開發了一個叫做@preact/signals
的東西,這是幹嘛的?Preact
的創始人Jason Miller以及Preact DevTools
的創始人Marvin Hagemeister共同寫了篇部落格:《Introducing Signals》
點開文章,首先映入眼簾的便是這樣一個案例:
```js import { signal, computed } from "@preact/signals";
const count = signal(0); const double = computed(() => count.value * 2);
function Counter() { return ( ); } ```
等等!這個.value
、這個computed
、以及這個在jsx
的大括號{}
中不用寫.value
的語法…
怎麼這麼似曾相識呢?好像在哪裡見過類似的寫法:
```html
```
與
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
檔案裡給出的第一個例子:
```js 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
,這裡就不過多贅述了,來看下一個案例:
```js 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
的其中一個缺點:無法精確控制到底監聽了哪個響應式變數。
比方說我們寫了這樣一段邏輯:
```js 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
裡還是很好實現的:
```js import { ref, watch } from 'vue';
const a = ref(0); const b = ref(0);
watch(a, value => { console.log(value);
b.value++; }); ```
但在watchEffect
那段程式碼裡就相當於在watch
中寫了這樣一段程式碼:
```js 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
),如果你寫出類似上面那樣的程式碼:
```js 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
函式導致死迴圈。但如果你沒了解過響應式原理的話可能就不太清楚我說的到底是什麼意思,建議閱讀一下這篇:
這是尤大在國外的VueMastery
教程網站中直播的手寫簡易版Vue
,文章裡有詳細的圖例來幫助大家快速理解,短短几十行程式碼就能實現一個簡易的響應式系統,吃透了原理你就會明白為什麼這樣寫會導致死迴圈了。
那為啥Vue
那邊的程式碼沒死迴圈呢?這是因為Vue
做了這樣一層判斷:如果你在effect
/ watchEffect
裡觸發了setter
,那便不會觸發對應的effect
/ watchEffect
函式,這樣就可以避免死迴圈了。
那Preact
沒做這樣的處理怎麼辦呢?那我們就避免在effect
裡既對signal
進行取值操作同時又對它進行賦值操作唄!
不過這樣做肯定是不行的哈,你這太不專業了,所有成熟的響應式庫沒有哪個會放著這個問題不去解決的。比方說Solid.js
,他就有一個叫untrack
的函式,假如我們在effect
裡想要獲取到一個響應式的值但卻並不想它被收集到依賴裡面去就可以寫成這樣:
```js 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
則不會。如果你能理解上面這段程式碼的話,那相信你肯定能理解下面這段程式碼:
```js 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
,直接看例子就不解釋了:
```js 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
,也不過多解釋了,直接上案例:
```js 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"; ```
```js 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
函式的,所以我們如果連著改兩次就會連續執行兩次,我寫了一個簡化版的案例給大家看一下:
```js import { signal, effect } from "@preact/signals-core";
const a = signal(0);
effect(() => console.log(a.value));
a.value++; a.value++; ```
控制檯列印結果:
就挺讓人無語的…… 這也能水個API
出來?人家Vue
預設就是分批處理的,我們在Vue
裡寫一段同樣的程式碼來看看Vue
是怎麼執行的:
```js 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
再來試一把:
```js 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
這樣:
```js 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
的人都去從原始碼裡找答案麼?算了不吐槽了,咱們繼續來看例子:
```js 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
。
咱們繼續來看下一個案例:
```js 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
函式還可以巢狀著寫:
```js 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
函式執行完成時才會更新對應的值。
React 及 Preact
core
的核心部分講完了,那就繼續看看@preact/signals
以及@preact/signals-react
吧!它倆用法都一樣:
```js 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
Value: {count.value}
; } ``````js import { useSignal, useComputed } from "@preact/signals-react";
function Counter() { const count = useSignal(0); const double = useComputed(() => count.value * 2);
return ( ); } ```
就有點類似於在React
裡寫Vue
的那種感覺。
後續
晚上回家一頓翻,終於找著了《Vue.js設計與實現》這本書,宣告一下本文真不是這本書的軟廣告,多賣出去一本我也不會得到什麼分成。真就是我寫那個例子的時候找不到文件又不知道咋用,README
讓去看TS
宣告結果看了個any
…
我是真沒耐心去特別仔細的研究@vue/reactivity
的原始碼,我覺得理解了大概的原理就行不必那麼死摳細節,畢竟咱們一不靠賣原始碼課賺錢、二也不負責維護Vue
、三也不像一些大佬似的沒事就以鑽研為樂、四也不至於研究完原始碼就能升職加薪什麼的…
不過好在我之前看過那本書裡面寫的挺詳細的好像有scheduler
、lazy
之類的欄位是用來幹嘛的並且還給出了實現以及用例。我又看了一遍響應式那章,之前靠猜以為lazy
是用來模仿watch
的,結果寫了{ lazy: true }
之後直接不運行了,這是因為寫了{ lazy: true }
就從自動擋變手動擋了!返回一個函式讓你自己去決定啥時候執行:
```js 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
沒有任何引數:
```js 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
的引數:
```js // 3.2 以前 effect( () => {}, { scheduler (fn) { console.log(fn) } } )
// 3.2 之後(含 3.2) const fn = effect( () => {}, { scheduler () { console.log(fn) } } ) ```
那我們就可以根據這一變化來重寫書中給出的排程執行的案例了:
```js 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
來做個實驗:
```js 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
裡寫一句因與返回值重複故刪之類的話唄!啥也不寫就非得讓人去看原始碼
往期精彩文章
- 《無虛擬 DOM 版 Vue 即將到來》
- 《Vue3又出新語法 到底何時才能折騰完?》
- 《[譯]Vue官方成員:Vite生態發展的怎麼樣了》
- 《UI:你們有沒有什麼花哨點的元件庫給我參考一下?》
- 《移動端佈局面試題 全面考察你的CSS功底(居中篇)》
- 《在Vue專案中使用React超火的CSS-in-JS庫: styled-components》
- 《Vue第二波ref語法提案來襲 這次會進入到標準嗎?》
- 《產品經理:鴻蒙那個開場動畫挺帥的 給咱們頁面也整一個唄》
- 《產品經理:能不能讓這串數字滾動起來?》
- 《什麼?僅靠H5標籤就能實現收拉效果?》
- 《金③銀④ 分享一道曾讓我栽在二面的面試題》
- 《雙11小黑盒很炫酷?咱們用CSS變數來改進一下!》
- 《千萬別小瞧九宮格 一道題就能讓候選人原形畢露!》
- 《將原型物件設定成Proxy後的一系列迷惑行為》
- 《終於輪到Vue來帶給React靈感了?》
- 《Vue3在IOS下的一個小坑》
- 《[譯]尤雨溪:Vue3的設計過程》
- 信仰崩了?Preact 開始採用 Vue3 的響應式設計
- 誰說前端不能搞紅黑樹,用這55張圖拿JS一起手撕紅黑樹
- Vue 無虛擬 DOM 模式即將到來
- 尤雨溪歐洲獻舞:即將釋出 Vue 2 的最後一個版本
- Vue3又出新語法 到底何時才能折騰完?
- [譯]Vue官方成員:Vite生態發展的怎麼樣了
- 波浪動畫很常見,但這個波浪元件絕對不常見
- vite1.0還沒學呢 這就出2.0了?
- 僅靠H5標籤就能實現收拉效果?我說的是真的!
- 整治GitHub不文明現象!微軟推出評論區!
- Vue 3.0.3 : 新增CSS變數傳遞以及最新的Ref提案
- [譯]尤雨溪: Ref語法糖提案
- 雙11小黑盒很炫酷?咱們用CSS變數來改進一下!
- IE 永不死?微軟開始將不相容 IE 的網站自動重定向至 Edge
- 千萬別小瞧九宮格 一道題就能讓候選人原形畢露!
- 移動端佈局面試題 全面考察你的CSS功底(居中篇)
- 一道騰訊面試題竟引發我與好友兩人爭執不下
- 好訊息,Vue3官方文件出中文版的啦!
- 新版vue-router的hooks用法
- [譯]尤雨溪:Vue3的設計過程