揭秘 Vue 3.0 最具潛力的 API(轉(zhuǎn)存)

作者簡介

古映杰,攜程研發(fā)高級(jí)經(jīng)理,負(fù)責(zé)前端框架和基礎(chǔ)設(shè)施的設(shè)計(jì)、研發(fā)與維護(hù)。開源項(xiàng)目react-lite和react-imvc作者。

尤雨溪6月份發(fā)布了 Vue Function-based API RFC,說是 3.0 最重要的 RFC。

文章發(fā)布后,引起了許多人的討論和爭執(zhí)。

有人表示喜歡和贊賞,有人卻表示“這不就是抄 React 嗎?我干嘛不直接學(xué)React去了”。

從個(gè)人角度,相比 vue 之前的 class-component 提案,我更欣賞現(xiàn)在的 function-based 模型。表面上看它好像是 react-hooks 的翻版。其實(shí)它跟 react-hooks 走的函數(shù)增強(qiáng)路線不同,vue-hooks 是一個(gè) value 增強(qiáng)的路線。

function 強(qiáng)化跟 value 強(qiáng)化,是一個(gè)能力相當(dāng)?shù)膶?duì)偶模型。一個(gè)是 a -> data ,另一個(gè)則是 data -> a。后者也是現(xiàn)在函數(shù)式研究的一個(gè)方向,叫 codata。

react 路線:如何從普通的 value 中,通過函數(shù)管道,輸出一個(gè) view。

vue 路線:如何從一個(gè)特殊的(響應(yīng)式的)值中,衍生出普通的值以及 view。

今天我們要揭示的,不是上面那個(gè)最重要的,而是最具潛力的、最能表征 Vue 路線的 API。

眾所周知,Vue 當(dāng)年的核心競爭力之一就是使用 ES5 的 Object.defineProperty 的 getter/setter 改良了當(dāng)時(shí)的 MVVM 使用臟檢查或者 get()/set() 函數(shù)。如今基于 ES2015 Proxy 升級(jí)成了新的 reactivity api。

它就是 Advanced Reactivity API,Provide standalone APIs for creating and observing reactive state。(注:請(qǐng)先大概看一下 API 介紹,它是理解后續(xù)內(nèi)容的基礎(chǔ))。

某種意義上,vue 暴露的內(nèi)部 api(reactivity api)比 react 暴露的內(nèi)部 api(hooks),具有更強(qiáng)的表達(dá)能力和普適性。它比 react 更完整,因?yàn)?value既可以衍生出 state 也可以衍生出 view$,它自帶了狀態(tài)管理和視圖,且兩者是無縫對(duì)接的。

react hooks 只能借鑒思路。在別的地方使用時(shí),要去重新實(shí)現(xiàn),是一種模式。而 reactvity api 可以直接作為 library 來用。比如,擁有了這個(gè) API,我們可以實(shí)現(xiàn)出類似 cyclejs, rxjs, immer, react-hooks 的特性。

那么問題來了,vue 3.0 還沒有發(fā)布,我們沒有代碼,怎么演示和證明 reactivity api 可以作為 library 來用呢?

哼哼,這個(gè)難不倒我們。

我們來親自手?jǐn)]一個(gè)簡單的 vue 3.0 reactivity api,不就行了嗎?

具體如何實(shí)現(xiàn),不是這篇文章的重點(diǎn),按下不表。如果你等不急看代碼和效果,可以點(diǎn)擊這里訪問DEMO(我基于 reactivity-api 實(shí)現(xiàn)了 counter 和 todo-list 效果)。

你會(huì)發(fā)現(xiàn) reactivity.js 已經(jīng)被編譯和壓縮過了,可讀性很低。這是因?yàn)?,最近前端社區(qū)有一些不良風(fēng)氣,一些小朋友,從各處抄了一點(diǎn)代碼,就覺得實(shí)現(xiàn)了 vue/react 的核心。過分自信的在四處發(fā)表錯(cuò)漏百出、富有偏見的觀點(diǎn)。因此我們特意做了一下處理,增加點(diǎn)抄襲成本,反正這不妨礙我們此次的演示目的。

如何實(shí)現(xiàn) cyclejs-like 的 reactive-view

image

首先實(shí)現(xiàn)一個(gè) watchable 函數(shù),可以將任意對(duì)象或數(shù)組,變成可 watch 的,它有第二個(gè)參數(shù),options,其中 options.map 決定 set 階段時(shí)如何儲(chǔ)存到 target。

state 采用遞歸的方式,將整顆樹都 watch 起來。value 則只 watch value 字段。
盡管 vue 被認(rèn)為不夠 fp,不過我們其實(shí)可以插上一些 fp 的翅膀,比如將 value 視為 monad,實(shí)現(xiàn) fmap, ap, bind 等函數(shù)。

image

fmap 是基于一個(gè) watchable a,和 a -> b 的 f 函數(shù),構(gòu)造一個(gè) watchable b 對(duì)象。這里簡單 watch a,然后在賦值給 b 的階段,調(diào)用 f(a) 構(gòu)造新的值即可。

ap 則是 watchable f 和 watchable a,構(gòu)造一個(gè) watchable b,b 是 f(a) 的產(chǎn)物。

bind 是 watchable a 加上類型為 a -> watchable b 的 f,實(shí)現(xiàn)基于上一個(gè) value 的值構(gòu)造下一個(gè) watchable 的功能。

至此,我們有了 state, value, 兩個(gè)構(gòu)造函數(shù),有了 watch 監(jiān)聽函數(shù), 有了 fmap, ap, bind 基于 value 構(gòu)造下一個(gè) value 的基本操作。

實(shí)現(xiàn) reactive view 用不到 computed,因此我們沒有去實(shí)現(xiàn)它。

vue 跟 rxjs 這種特殊的值,可以直接衍生出 view。首先實(shí)現(xiàn)一個(gè) combinaLatest([value]) ,得到一個(gè)在 value 范疇內(nèi)構(gòu)造數(shù)組的方式,然后通過 [[key, value]] ,從處理數(shù)組的方式中,配合 fromEntries 衍生出 value層面構(gòu)造 object 的方式。而 virtual-ui-model 就是用特定的 object 表征 ui 對(duì)象。 因此,我們基于 object 可以實(shí)現(xiàn) view,它代表了一個(gè)在時(shí)間序列中動(dòng)態(tài)輸出的視圖流,并且因?yàn)?combinaLatest 自動(dòng)復(fù)用未變化的值,使得 view -> view 輸出的結(jié)構(gòu),總是結(jié)構(gòu)共享的,利于 diff 算法。

image

實(shí)現(xiàn) combineArray:如果一個(gè)數(shù)組里存在一個(gè) reactive value,那么它也返回一個(gè) reactive array,每次輸出一個(gè)純數(shù)組。如果數(shù)組里不包含 reactive value,它什么也不包裝,直接返回該數(shù)組。相當(dāng)于 Promise.all(list),只不過它有可能不返回 promise/reactive-value。

image

有了 combineArray,可以實(shí)現(xiàn) combineObject,正如前面說的,就是 entries和 fromEntries 的轉(zhuǎn)換。

image

再封裝一下,得到一個(gè) combine 函數(shù),可以將任意結(jié)構(gòu),構(gòu)造成 reactive-value,只要子結(jié)構(gòu)了包含 reactive-value,它就 wrap 成一個(gè)整體。

現(xiàn)在我們除了 vue-like 的 reactivity api,還有 combine 函數(shù)了,可以去 combine react-element 了。為什么是 combine react-element ?因?yàn)槲覀兙褪且C明 vue 3.0 的 reactivity api 可以作為 library,脫離 vue 來用。因此就用在其競爭對(duì)手 react 身上(其實(shí)是因?yàn)槲冶容^熟悉 react)。
我們會(huì)將 jsx 的編譯函數(shù)從 React.createElement,切換成我們自己構(gòu)造的 createElement。

image

createElement 將可能包含 reactivity-value 的 type, props, children,給 combine 起來。檢測到 component 用 monad 的 bind,此時(shí)我們將組件描述為 bind 的 f 參數(shù)。檢測到 element 我們用 functor 的 fmap,將 props 映射成 react-element。

image

最后,實(shí)現(xiàn)一個(gè) map 函數(shù),用來 map 一個(gè) reactive-value,既支持?jǐn)?shù)組,也支持非數(shù)組。

準(zhǔn)備工作做好了,把它們 import 進(jìn)來。

image

回顧一下我們的 combineArray 是如何更新的,它不是直接賦值,而是先淺拷貝,再賦值。

image

這意味著,它總是返回 immutable-list,因?yàn)樗?immer 一樣 copy-on-write。

我們免費(fèi)得到了一個(gè)行走的 immer,不需要 produce 包裹。combine 一下,然后隨便改,watch 函數(shù)都會(huì)拿到結(jié)構(gòu)共享的 immutable data。

如果沒有實(shí)現(xiàn)這一點(diǎn),combine react-element 時(shí),子樹直接被修改,react 進(jìn)行diff 時(shí)檢測不出來子樹有變化,就不會(huì)去更新視圖了。

現(xiàn)在可以實(shí)現(xiàn)一個(gè) Counter 組件試試。

image

看這個(gè)代碼,是不是覺得非常有趣?既像 vue 那樣可以用 js 賦值操作,又像 react-hooks 那樣的形式,還像 cycle.js 一樣在組件內(nèi)部可以操作 reactive value。

它怎么做到自動(dòng)更新視圖的呢?

因?yàn)?let count = value(0),它是一個(gè) reactive-value。它被 handleIncre 和 handleDecre 修改,它同時(shí)用在了 jsx 里。我們的 createElement 會(huì)檢測到這個(gè) jsx 結(jié)構(gòu)里包含一個(gè) reactive-value,因此它會(huì)被整個(gè) combine 起來,成為一個(gè)大的響應(yīng)式的值 view.value。

image

前面我們將 jsx 編譯從 React.createElement 切換到我們的 createElement 函數(shù),因此 <Counter /> 組件不是返回 react-element,而是返回我們的 reactive-value,它是一個(gè)響應(yīng)式的值,可以被 watch。我們 watch 這個(gè) <Counter />,然后拿到它真正的 react-element,再用 ReactDOM 渲染到 root 節(jié)點(diǎn)。
看起來像下面那樣。

image

只支持一個(gè) counter,看起來可能是一個(gè)特例,我們可以再實(shí)現(xiàn)一個(gè) todo list。

image

TodoApp 組件里構(gòu)造一個(gè) reactive-state,然后傳遞給 TodoInput 和 Todos。

image

TodoInput 里構(gòu)造一個(gè) reactive text,作為局部狀態(tài),綁定到 input 元素。

點(diǎn)擊 add 按鈕時(shí),構(gòu)造一個(gè) todo,直接 push 到 todos 里即可。
其它用到 todos 的地方,會(huì)自動(dòng)檢測到 todos 變化而進(jìn)行局部渲染。比如我們的 Todos。

image

它通過 map 函數(shù),將 reactive todos 映射成 Todo 組件,每當(dāng) todos 變化時(shí),這個(gè) map 函數(shù)就會(huì)自動(dòng)再次執(zhí)行,然后 top-level 的 app 就會(huì)拿到一個(gè) immutable vdom,除了 todos 以外,其它結(jié)構(gòu)復(fù)用原來的引用。

image

Todo 里面很簡單,就是展示一下,支持 toggle 和 remove 什么的。

整體看上去像下面那樣。

image

可以看到,我們從未調(diào)用 setState/setValue 等觸發(fā)函數(shù),只用到了原生 js 的方法和賦值操作。以一種符合直覺的方式,構(gòu)建了我們這個(gè) reactive todo-list。

react 在這里只是起來了一個(gè) renderer 的作用,理論上,用任意 vdom library 都行。

如何用 combine 函數(shù)實(shí)現(xiàn)行走的 immer

image

上面的 test 是一個(gè) reactive state,里面深層節(jié)點(diǎn)里包含了 reactive-value。

mobx 作者的 immer,是現(xiàn)用現(xiàn)拋,nextState = produce(state, update)。

我們 reactive-state 的版本則是,draftState 不必限制在 update 函數(shù)里,可以在外面隨意傳遞和使用,watch 函數(shù)拿到的總是 immutable 的。

我們構(gòu)造了 3 個(gè)方法,分別深度更新不同的字段,然后隨機(jī)使用這些更新方法。它們不會(huì)引起其它字段的引用變化,共享沒有變化的結(jié)構(gòu)。

image

比如,randomMethod a 只引起了 a 字段的更新,因此 c 和 g 字段跟 prev 對(duì)比是相等的。

如何用 reactivity api 實(shí)現(xiàn) react-hooks 的機(jī)制?

vue 3.0 的 reactivity api,更多的是承擔(dān) connect, computed, combine 等結(jié)構(gòu)關(guān)聯(lián)的動(dòng)作,它沒有作為 source 去 produce data。data 是外部傳入 state/value,以及 reactive-state 在別的地方被 mutate 出新數(shù)據(jù)。

而 react-hooks 其實(shí)是一個(gè) producer,它不斷的 re-execute 自身,產(chǎn)生很多次 return data 的過程。
react-hooks 跟 reactivity api 的結(jié)合,就得到了一個(gè) producer + combinator。比如,我們要構(gòu)造一個(gè) count,它不只是在 count.value += 1 的時(shí)候被動(dòng)產(chǎn)生新的 value,而是可以通過某個(gè)機(jī)制,不斷自動(dòng)產(chǎn)生。

image

這個(gè)結(jié)構(gòu)看起來跟 rxjs 倒很像。有 next, cleanup/unsubscribe,默認(rèn)自帶 startWith 操作符。后續(xù)我們可以實(shí)現(xiàn) merge, combine, concat, filter, take 等其它 operator。這樣直接 vue, react, rxjs 的 pattern 一家親了~

不過,額外引入 react-hooks,跟 vue-reactivity 并行,會(huì)顯得很奇怪,應(yīng)該用后者實(shí)現(xiàn)前者的機(jī)制。就是 re-run 時(shí),重用 state/value,并且 state/value 的變化,會(huì)引起函數(shù)的 re-run。

useEffect 應(yīng)該是 watch 自身,是一個(gè)語法糖。watch(self, effect)。

如此,區(qū)分出了兩種 reactivity 形態(tài),一種是在 producer 外部的 free-order-reactivity,一種是在 producer 內(nèi)部的 fixed-order-reactivity。

實(shí)現(xiàn)起來很簡單。

image

實(shí)現(xiàn) 3 個(gè)增強(qiáng)函數(shù)的函數(shù),resumable 增強(qiáng)函數(shù) re-run 自身的能力,referencable 增強(qiáng)函數(shù)持久化內(nèi)部狀態(tài)的能力。reactive 增強(qiáng)函數(shù)使用 reactivity api 的能力。

首先存在一個(gè) env 內(nèi)部環(huán)境,它會(huì)被 resumeable, referencable, reactivie 等 enhancer 進(jìn)行拓展。

image

reactivie 就是將 prodcuer 的返回值,掛載到 value.value 上,自身永遠(yuǎn)返回 value,因此外部總是拿到一個(gè) reactive value。

image

useRef 的實(shí)現(xiàn),直接使用 referencable 提供的 storage 方法即可。

image

useEffect 在使用 storage 方法時(shí),通過 reactive enhancer 拿到了 value$,watch 它,并返回 unwatch。

image

useReactive 在內(nèi)部構(gòu)造 reactive value/state,watch 它,然后使用 resumable enhancer 提供的 resume 方法,觸發(fā)重新執(zhí)行。

image

然后用 useReactive,分別實(shí)現(xiàn) useState,和 useValue。再用它們實(shí)現(xiàn)一個(gè) interval 函數(shù),可以輸出一個(gè)自行變化的 count。

image

把 interval 用在我們之前的 Counter 組件里。

image

效果,有一個(gè) tick 自動(dòng)隨時(shí)間而變化,不需要額外的地方去 count.value += 1。

image

如何用 reactivity api 實(shí)現(xiàn) rxjs-like 的功能?

先實(shí)現(xiàn)一個(gè) rxjs 那樣的 pipe,用法是 pipe(source, operator1, opeator2, operator3) 這類。

image

map operator 的實(shí)現(xiàn),可以直接用 functor 的 fmap,參數(shù)映射一下 pipe 函數(shù)的要求。

image

因?yàn)?map 函數(shù)已經(jīng)定義過了,因此這個(gè) map operator 只好改名為 mapx。
filter operator 就是通過 predicate 函數(shù),有選擇的將 source的值,轉(zhuǎn)移到 sink

image

take 和 scan 則分別是內(nèi)部計(jì)數(shù)和累加acc,代碼都很簡單。

image

將這些 operators 用在我們的 tick 上。

image

輸出 10 個(gè)奇數(shù)的數(shù)組。如下圖所示。

image

總結(jié)

需要說明的是,目前的模擬是一個(gè)粗糙的做法,有很多沒有處理,比如 unwatch 的時(shí)機(jī),它幾乎一定會(huì)內(nèi)存泄露。需要更精細(xì)的去實(shí)現(xiàn)和控制,才能得到一個(gè)可用的形態(tài),當(dāng)下只是演示一下思路 。

這些 demo 只是演示一些能力。沒有考慮實(shí)際項(xiàng)目里怎么用,不管大小,都不要用這個(gè)方案。

等有人基于這個(gè)思路做出一個(gè)完成度更好的庫或者框架,再考慮吧。

到目前為止,我們差不多填完了用 vue reativity api 實(shí)現(xiàn) immer-like, rxjs-like, react-hooks-like, cyclejs-like(就是最初的那個(gè) reactive view) 的坑,應(yīng)該足以展示 vue reactivity api 是一個(gè)更加 primitive 的機(jī)制了(畢竟基于 Proxy)。

vue 3.0 reactivity api 的能力還不局限于上面演示的,感興趣的同學(xué),可以自行探索一下。
原文

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容