信仰崩了?Preact 開始採用 Vue3 的響應式設計

語言: CN / TW / HK

theme: channing-cyan

前言

不知大家有沒有聽過Preact這個框架,就算沒聽過Preact那也應該聽過React吧?

一字之差,preactreact多了個p!(聽起來咋不像啥好話呢)

這個P代表的是 Performance高效能版React的意思。Preact一開始是CodePen上的一個小專案,有點類似於咱們國內常見的《三百行程式碼帶你實現個React》這類文章,用最少的程式碼實現了React的最基本功能,然後放到CodePen上供大家~敬仰~學習。

當然這是很多年前的事了,那時候這種東西很容易火,想想N年前你看過的第一篇《三百行實現個Vue》《三百行實現個React》之類的文章是不是競爭對手很少、很容易引發大量的關注度。不過現在不行了,太卷!這類文章隔三差五的就能看到一篇,同質化嚴重,導致大家都有點審美疲勞了。

但在那個年代Preact就是這麼火起來的,三百行實現了個React引發大量關注度之後,作者覺得自己做的這玩意好像還挺不錯的哈!於是開始繼續完善,完善後拿去一測試:效能簡直完爆React呀!我這玩意不僅體積比你小、效能還比你高。就這樣作者開始有些膨脹了、開始飄了!

那我就給這個框架起個名叫Preact吧!

Performance版的React

Preact 簡介

開啟Preact官網,映入眼簾的便是它的最大賣點:

只有3KB大小、並且與React擁有相同的API。真的只有3KB麼?虛擬DOMDiff演算法、類元件、Hooks… 這些就算實現的再怎麼巧妙也需要很多程式碼才行吧?我們直接用Vite來建立一個Preact專案來試下:

js npm create vite

如果螢幕前的你用的是VSCode這個編輯器的話,可以安裝一下Import Cost這個外掛:

安裝好之後我們來看一下主檔案(main.jsx):

臥槽?gizpped真的只有3.幾K!不過這演算法有點雞賊啊,來了個向下取整:

這讓我想起了最近非常火的TurbopackVite快十倍的宣傳口號,遭尤大怒懟:1k 元件的案例下有數字的四捨五入問題,Turbopack15ms 被向下取整為 0.01s,而到了 Vite 這裡 87ms 被向上取整為 0.09s。這把本來接近 6 倍的差距擴大到了 10 倍。

不過即使這樣,3.8K依然是一個很驚人的成就。是不是隻是render這個函式就佔了3.8K啊?我們再引點東西試試:

難以置信!引了這麼多hooks居然只多加了0.1K!我還是不太相信用0.1K的程式碼就能實現出React Hooks來,肯定是用了什麼特殊的演算法專門針對了這一場景做了優化,我們按照官網的寫法來重新引一下:

這回體積明顯增大了不少:

不過咋感覺自己跟個槓精似的呢😂 人家說了3KB我卻非要以各種方式證明肯定不止3KB,這樣不好。Preact真的已經很輕量了,一般人想要實現這麼多功能還真做不到只用這麼少的程式碼,PreactP肯定還是名不虛傳的👍

不過剛剛試了下VueVue好像就沒有針對這種場景做專門的優化,不僅沒優化反而還劣化了:

實際上只引入某幾個函式的話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釋出後大家就瘋狂@DanDan看完後直接來了句:這與React的發展理念不是很吻合。(潛臺詞:我們才不會在React裡實現Vue呢)

其實我覺得也是,React的發展理念本來就跟Vue走的是完全不同的兩種路線,誇張點說就是道不同不相為謀。

那肯定有人說:不對呀,Vue3Composition API不是抄襲的React麼?

這麼說吧:大佬們借鑑的是思路,菜鳥們借鑑的才是程式碼。瞭解過VueReact他倆底層實現的朋友們應該都清楚他倆的差距有多大,尤雨溪在某次採訪時說過Vue3一開始本打算實現成類元件,既然是類那就離不開裝飾器的話題,尤大說他們甚至都已經實現出來了一版類元件寫法的Vue3。只不過他覺得這樣相對於Vue2而言除了對TS的支援度之外幾乎沒有其他什麼特別明顯的優勢,而且裝飾器提案發展了N年卻遲遲未能落地,尤大覺得這樣遙遙無期,而且就算真的在將來的某一天落地了,是不是也已經與現在TS實現的那版裝飾器天差地別了?

Angular用裝飾器用的好好的那是因為人家強制要求使用TS,但Vue顯然不可能這樣做。而且為了防止未來裝飾器有變動(其實最近已經Stage3的裝飾器已經和TS裝飾器不一樣了),許多曾經使用裝飾器語法的庫為了規避這個風險也已經改用了別的寫法,如:MobXReact DnD等…

推薦閱讀:《mobx6.0為什麼移除裝飾器》

正當尤雨溪為此抓耳撓腮、夜不能寐之時,React Hooks橫空出世了!這種函式式元件瞬間就讓尤大眼前一亮,他腦袋裡的燈泡在那一剎那間被點亮了:

這不就是自己一直苦苦尋找、對TS友好、方便程式碼複用、語法簡潔、低耦合的解決方案麼!

但實際上吧,尤大隻是參考了這種函式式的設計,如今的Composition API原理與React Hooks相去甚遠。真要說~抄~借鑑的話,尤雨溪已經大大方方承認了是受到了React Hooks的啟發,程式碼層面借鑑的是Meteor Trackernx-js/observer-utilsalesforce/observable-membrane這三個庫。響應式庫其實早已不新鮮了,只是之前尤大沒能跳出Vue2的思維限制,直到看到了React Hooks才想到可以這樣寫,然後再一調研發現市面上早就有了函式式的響應式庫,Composition API就是這麼來的。

推薦閱讀:《[譯]尤雨溪:Vue3的設計過程》

不過他在Composition API之前確實~抄~模仿了React的原理設計出來了vue-hooks,以用來探索這種函式式元件的可行性。不過好在後來他發現了Meteor Trackernx-js/observer-utilsalesforce/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

一整年就兩三天是滅著的,剩下的時間無論颳風還是下雨,都無法阻擋大佬提交程式碼的腳步。甚至那兩三天我都懷疑是有什麼不可抗力導致的,比方說來臺風斷電啦或者在飛機上沒法提交,下了飛機直接就進入另一個時區(第二天)啦之類的原因,他甚至比尤雨溪都勤快:

不過拿他倆比有點不太公平哈,尤大有家有孩子,而且還要帶領兩個團隊(VueVite),寫程式碼的時間自然會少很多。而佬年輕沒結婚沒孩子、也無需帶領團隊啥的,自然就會有很多時間做自己喜歡做的事情。不過我翻了一下尤大迭代最瘋狂的2016年,也依然沒我傅哥勤快:

這就是我傅哥為何能如此高產的原因。

有點扯遠了哈,沒接觸過@vue/reactivityeffect同學暫且先把它理解為composition apiwatchEffect,在這裡開始出現了一個與composition api不太一樣的api了哈,.peek()是什麼鬼?為了幫助大家快速理解這玩意,我們需要對比一下composition api裡兩個相似功能的apiwatchwatchEffect

這倆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(類似VuewatchEffect),如果你寫出類似上面那樣的程式碼:

```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函式導致死迴圈。但如果你沒了解過響應式原理的話可能就不太清楚我說的到底是什麼意思,建議閱讀一下這篇:

《尤雨溪國外教程:親手帶你寫個簡易版的Vue!》

這是尤大在國外的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雖然很調皮,但有些略顯猥瑣)

接下來看下一個apicomputed。就我不說你們都能猜到這是幹啥的,這就是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); ```

下一個apieffect,其實在.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 APIVue@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裡就沒有,而且watchEffecteffect表現也不太一致,@vue/reactivityREADME寫的也特別簡陋:

機翻一下:

就很無奈,我想知道這個庫怎麼用就只能去看看它的TS定義,看看都有哪些API以及都有哪些用法。哪怕不像Vue那樣有個專門的官網,那你在README裡寫幾個簡單的事例也行啊!就像@preact/signals-core那樣,能耽誤你幾小時?

吐槽歸吐槽,想知道咋用還是得去看程式碼,在了src/effect.ts後我發現這樣一段程式碼:

WX20221107-143238.png

果然還是和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、三也不像一些大佬似的沒事就以鑽研為樂、四也不至於研究完原始碼就能升職加薪什麼的…

不過好在我之前看過那本書裡面寫的挺詳細的好像有schedulerlazy之類的欄位是用來幹嘛的並且還給出了實現以及用例。我又看了一遍響應式那章,之前靠猜以為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.x3.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裡寫一句因與返回值重複故刪之類的話唄!啥也不寫就非得讓人去看原始碼

往期精彩文章