尤雨溪發(fā)布了 Vue Function-based API RFC硝岗,說是 3.0 最重要的 RFC渴语。
文章發(fā)布后娇未,引起了許多人的討論和爭執(zhí)。
有人表示喜歡和贊賞,有人卻表示“這不就是抄 React 嗎菱父?我干嘛不直接學(xué)React去了”。
從個人角度剑逃,相比 vue 之前的 class-component 提案浙宜,我更欣賞現(xiàn)在的 function-based 模型。表面上看它好像是 react-hooks 的翻版蛹磺。其實它跟 react-hooks 走的函數(shù)增強(qiáng)路線不同粟瞬,vue-hooks 是一個 value 增強(qiáng)的路線。
function 強(qiáng)化跟 value 強(qiáng)化萤捆,是一個能力相當(dāng)?shù)膶ε寄P腿蛊贰R粋€是 a -> data ,另一個則是 data -> a俗或。后者也是現(xiàn)在函數(shù)式研究的一個方向市怎,叫 codata(點擊 codata in action 了解更多)。
react 路線:如何從普通的 value 中辛慰,通過函數(shù)管道焰轻,輸出一個 view。
vue 路線:如何從一個特殊的(響應(yīng)式的)值中昆雀,衍生出普通的值以及 view辱志。
今天我們要揭示的蝠筑,不是上面那個最重要的,而是最具潛力的揩懒、最能表征 Vue 路線的 API什乙。
總所周知,Vue 當(dāng)年的核心競爭力之一就是從使用 ES5 的 Object.defineProperty 的 getter/setter 改良了當(dāng)時的 MVVM 使用藏檢查或者 get()/set() 函數(shù)已球。如今基于 ES2015 Proxy 升級成了新的 reactivity api臣镣。
它就是 Advanced Reactivity API,Provide standalone APIs for creating and observing reactive state智亮。(注:請先大概看一下 API 介紹忆某,它是理解后續(xù)內(nèi)容的基礎(chǔ))。
某種意義上阔蛉,vue 暴露的內(nèi)部 api(reactivity api)比 react 暴露的內(nèi)部 api(hooks)弃舒,具有更強(qiáng)的表達(dá)能力和普適性。它比 react 更完整状原,因為 value既可以衍生出state
也可以衍生出 view$聋呢,它自帶了狀態(tài)管理和視圖,且兩者是無縫對接的颠区。
react hooks 只能借鑒思路削锰。在別的地方使用時,要去重新實現(xiàn)毕莱,是一種模式器贩。而 reactvity api 可以直接作為 library 來用。比如朋截,擁有了這個 API磨澡,我們可以實現(xiàn)出類似 cyclejs, rxjs, immer, react-hooks 的特性。
那么問題來了质和,vue 3.0 還沒有發(fā)布稳摄,我們沒有代碼,怎么演示和證明 reactivity api 可以作為 library 來用呢饲宿?
哼哼厦酬,這個難不倒我們。
我們來親自手?jǐn)]一個簡單的 vue 3.0 reactivity api瘫想,不就行了嗎仗阅?
具體如何實現(xiàn),不是這篇文章的重點国夜,按下不表减噪。如果你等不急看代碼和效果,可以點擊這里訪問DEMO(我基于 reactivity-api 實現(xiàn)了 counter 和 todo-list 效果)。你會發(fā)現(xiàn) reactivity.js 已經(jīng)被編譯和壓縮過了筹裕,可讀性很低醋闭。這是因為,最近前端社區(qū)有一些不良風(fēng)氣朝卒,一些小朋友证逻,從各處抄了一點代碼,就覺得實現(xiàn)了 vue/react 的核心抗斤。過分自信的在四處發(fā)表錯漏百出囚企、富有偏見的觀點。因此我們特意做了一下處理瑞眼,增加點抄襲成本龙宏,反正這不妨礙我們此次的演示目的。
如何實現(xiàn) cyclejs-like 的 reactive-view
首先實現(xiàn)一個 watchable 函數(shù)伤疙,可以將任意對象或數(shù)組银酗,變成可 watch 的,它有第二個參數(shù)掩浙,options花吟,其中 options.map 決定 set 階段時如何儲存到 target秸歧。
state 采用遞歸的方式厨姚,將整顆樹都 watch 起來。value 則只 watch value 字段键菱。
盡管 vue 被認(rèn)為不夠 fp谬墙,不過我們其實可以插上一些 fp 的翅膀,比如將 value 視為 monad经备,實現(xiàn) fmap, ap, bind 等函數(shù)拭抬。
fmap 是基于一個 watchable a,和 a -> b 的 f 函數(shù)侵蒙,構(gòu)造一個 watchable b 對象造虎。這里簡單 watch a,然后在賦值給b的階段,調(diào)用 f(a) 構(gòu)造新的值即可纷闺。
ap 則是 watchable f 和 watchable a算凿,構(gòu)造一個 watchable b,b 是 f(a) 的產(chǎn)物犁功。
bind 是 watchable a 加上類型未 a -> watchable b 的 f氓轰,實現(xiàn)基于上一個 value 的值構(gòu)造下一個 watchable 的功能。
至此浸卦,我們有了 state, value, 兩個構(gòu)造函數(shù)署鸡,有了 watch 監(jiān)聽函數(shù), 有了 fmap, ap, bind 基于 value 構(gòu)造下一個 value 的基本操作。
實現(xiàn) reactive view 用不到 computed,因此我們沒有去實現(xiàn)它靴庆。
vue 跟 rxjs 這種特殊的值时捌,可以直接衍生出 view。首先實現(xiàn)一個 combinaLatest([value])撒穷,得到一個在value范疇內(nèi)構(gòu)造數(shù)組的方式匣椰,然后通過 [[key, value]] ,從處理數(shù)組的方式中端礼,配合 fromEntries 衍生出 value
層面構(gòu)造object的方式禽笑。而virtual - ui - model 就是用特定的object表征對象。因此蛤奥,我們基于object
可以實現(xiàn) view佳镜,
它代表了一個在時間序列中動態(tài)輸出的視圖流,并且因為combinaLatest自動復(fù)用未變化的值凡桥,使的view->view輸出的結(jié)構(gòu)蟀伸,總是結(jié)構(gòu)共享的,利于 diff 算法缅刽。
實現(xiàn) combineArray:如果一個數(shù)組里存在一個 reactive value啊掏,那么它也返回一個 reactive array,它每次輸出一個純數(shù)組衰猛。如果數(shù)組里不包含 reactive value迟蜜,它什么也不包裝,直接返回該數(shù)組啡省。相當(dāng)于 Promise.all(list)娜睛,只不過它有可能不返回 promise/reactive-value。
有了 combineArray卦睹,可以實現(xiàn) combineObject畦戒,正如前面說的,就是 entries和 fromEntries 的轉(zhuǎn)換结序。
再封裝一下障斋,得到一個 combine 函數(shù),可以將任意結(jié)構(gòu)徐鹤,構(gòu)造成 reactive-value垃环,只要子結(jié)構(gòu)了包含 reactive-value,它就 wrap 成一個整體凳干。
現(xiàn)在我們除了 vue-like 的 reactivity api晴裹,還有 combine 函數(shù)了,可以去 combine react-element 了救赐。為什么是 combine react-element 涧团?因為我們就是要證明 vue 3.0 的 reactivity api 可以作為 library只磷,脫離 vue 來用。因此就用在其競爭對手 react 身上(其實是因為我比較熟悉 react)泌绣。
我們會將 jsx 的編譯函數(shù)從 React.createElement钮追,切換成我們自己構(gòu)造的 createElement。
createElement 將可能包含 reactivity-value 的 type, props, children阿迈,給 combine 起來元媚。檢測到 component 用 monad 的 bind,此時我們將組件描述為 bind 的 f 參數(shù)苗沧。檢測到 element 我們用 functor 的 fmap刊棕,將 props 映射成 react-element。
最后待逞,實現(xiàn)一個 map 函數(shù)甥角,用來 map 一個 reactive-value,既支持?jǐn)?shù)組识樱,也支持非數(shù)組嗤无。
準(zhǔn)備工作做好了,把它們 import 進(jìn)來怜庸。
回顧一下我們的 combineArray 是如何更新的当犯,它不是直接賦值,而是先淺拷貝割疾,再復(fù)制嚎卫。
?這意味著,它總是返回 immutable-list杈曲,因為它跟 immer 一樣 copy-on-write驰凛。
我們免費得到了一個行走的 immer胸懈,不需要 produce 包裹担扑。combine 一下,然后隨便改趣钱,watch 函數(shù)都會拿到結(jié)構(gòu)共享的 immutable data涌献。
如果沒有實現(xiàn)這一點,combine react-element 時首有,子樹直接被修改燕垃,react 進(jìn)行diff 時檢測不出來子樹有變化,就不會去更新視圖了井联。
現(xiàn)在可以實現(xiàn)一個 Counter 組件試試卜壕。
?看這個代碼,是不是覺得非常有趣烙常?既像 vue 那樣可以用 js 賦值操作轴捎,又像 react-hooks 那樣的形式,還像 cycle.js 一樣在組件內(nèi)部可以操作 reactive value。
它怎么做到自動更新視圖的呢侦副?
因為 let count = value(0)侦锯,它是一個 reactive-value。它被 handleIncre 和 handleDecre 修改秦驯,它同時用在了 jsx 里尺碰。我們的 createElement 會檢測到這個 jsx 結(jié)構(gòu)里包含一個 reactive-value,因此它會被整個 combine 起來译隘,成為一個大的響應(yīng)式的值 view.value亲桥。
前面我們將 jsx 編譯從 React.createElement 切換到我們的 createElement 函數(shù),因此 <Counter /> 組件不是返回 react-element固耘,而是返回我們的 reactive-value两曼,它是一個響應(yīng)式的值,可以被 watch玻驻。我們 watch 這個 <Counter />悼凑,然后拿到它真正的 react-element,再用 ReactDOM 渲染到 root 節(jié)點璧瞬。
看起來像下面那樣户辫。
只支持一個 counter,看起來可能是一個特例嗤锉,我們可以再實現(xiàn)一個 todo list渔欢。
?TodoApp 組件里構(gòu)造一個 reactive-state,然后傳遞給 TodoInput 和 Todos瘟忱。
?TodoInput 里構(gòu)造一個 reactive text奥额,作為局部狀態(tài),綁定到 input 元素访诱。
點擊 add 按鈕時垫挨,構(gòu)造一個 todo,直接 push 到 todos 里即可触菜。
其它用到 todos 的地方九榔,會自動檢測到 todos 變化而進(jìn)行局部渲染。比如我們的 Todos涡相。
?它通過 map 函數(shù)哲泊,將 reactive todos 映射成 Todo 組件,每當(dāng) todos 變化時催蝗,這個 map 函數(shù)就會自動再次執(zhí)行切威,然后 top-level 的 app 就會拿到一個 immutable vdom,除了 todos 以外丙号,其它結(jié)構(gòu)復(fù)用原來的引用先朦。
?Todo 里面很簡單且预,就是展示一下,支持 toggle 和 remove 什么的烙无。
整體看上去像下面那樣锋谐。
可以看到,我們從未調(diào)用 setState/setValue 等觸發(fā)函數(shù)截酷,只用到了原生 js 的方法和賦值操作涮拗。以一種符合直覺的方式,構(gòu)建了我們這個 reactive todo-list迂苛。
react 在這里只是起來了一個 renderer 的作用三热,理論上,用任意 vdom library 都行三幻。
如何用 combine 函數(shù)實現(xiàn)行走的 immer
上面的 test 是一個 reactive state就漾,里面深層節(jié)點里包含了 reactive-value。
mobx 作者的 immer 念搬,是現(xiàn)用現(xiàn)拋抑堡,nextState = produce(state, update)。
我們 reactive-state 的版本則是朗徊,draftState 不必限制在 update 函數(shù)里首妖,可以在外面隨意傳遞和使用,watch 函數(shù)拿到的總是 immutable 的爷恳。
我們構(gòu)造了 3 個方法有缆,分別深度更新不同的字段,然后隨機(jī)使用這些更新方法温亲。它們不會引起其它字段的引用變化棚壁,共享沒有變化的結(jié)構(gòu)。
?
比如栈虚,randomMethod a 只引起了 a 字段的更新袖外,因此 c 和 g 字段跟 prev 對比是相等的。
如何用 reactivity api 實現(xiàn) react-hooks 的機(jī)制节芥?
vue 3.0 的 reactivity api在刺,更多的是承擔(dān) connect, computed, combine 等結(jié)構(gòu)關(guān)聯(lián)的動作逆害,它沒有作為 source 去 produce data头镊。data 是外部傳入 state/value,以及 reactive-state 在別的地方被 mutate 出新數(shù)據(jù)魄幕。
而 react-hooks 其實是一個 producer相艇,它不斷的 re-execute 自身,產(chǎn)生很多次 return data 的過程纯陨。
react-hooks 跟 reactivity api 的結(jié)合坛芽,就得到了一個 producer + combinator留储。比如,我們要構(gòu)造一個 count咙轩,它不只是在 count.value += 1 的時候被動產(chǎn)生新的 value获讳,而是可以通過某個機(jī)制,不斷自動產(chǎn)生活喊。
這個結(jié)構(gòu)看起來跟 rxjs 倒很像丐膝。有 next, cleanup/unsubscribe,默認(rèn)自帶 startWith 操作符钾菊。后續(xù)我們可以實現(xiàn) merge, combine, concat, filter, take 等其它 operator帅矗。這樣直接 vue, react, rxjs 的 pattern 一家親了~
不過,額外引入 react-hooks煞烫,跟 vue-reactivity 并行浑此,會顯得很奇怪,應(yīng)該用后者實現(xiàn)前者的機(jī)制滞详。就是 re-run 時凛俱,重用 state/value,并且 state/value 的變化料饥,會引起函數(shù)的 re-run最冰。
useEffect 應(yīng)該是 watch 自身,是一個語法糖稀火。watch(self, effect)暖哨。
如此,區(qū)分出了兩種 reactivity 形態(tài)凰狞,一種是在 producer 外部的 free-order-reactivity篇裁,一種是在 producer 內(nèi)部的 fixed-order-reactivity。
實現(xiàn)起來很簡單赡若。
?實現(xiàn) 3 個增強(qiáng)函數(shù)的函數(shù)达布,resumable 增強(qiáng)函數(shù) re-run 自身的能力,referencable 增強(qiáng)函數(shù)持久化內(nèi)部狀態(tài)的能力逾冬。reactive 增強(qiáng)函數(shù)使用 reactivity api 的能力黍聂。
首先存在一個 env 內(nèi)部環(huán)境,它會被 resumeable, referencable, reactivie 等 enhancer 進(jìn)行拓展身腻。
reactivie 就是將 prodcuer 的返回值产还,掛載到 value[圖片上傳失敗...(image-2b7e3d-1563238657901)]
,因此外部總是拿到一個 reactive value嘀趟。
?useRef 的實現(xiàn)脐区,直接使用 referencable 提供的 storage 方法即可。
?useEffect 在使用 storage 方法時她按,通過 reactive enhancer 拿到了 value$牛隅,watch 它炕柔,并返回 unwatch。
?useReactive 在內(nèi)部構(gòu)造 reactive value/state媒佣,watch 它匕累,然后使用 resumable enhancer 提供的 resume 方法,觸發(fā)重新執(zhí)行默伍。
?然后用 useReactive哩罪,分別實現(xiàn) useState,和 useValue巡验。再用它們實現(xiàn)一個 interval 函數(shù)际插,可以輸出一個自行變化的 count。
?把 interval 用在我們之前的 Counter 組件里显设。
?效果框弛,有一個 tick 自動隨時間而變化,不需要額外的地方去 count.value += 1捕捂。
?如何用 reactivity api 實現(xiàn) rxjs-like 的功能瑟枫?
先實現(xiàn)一個 rxjs 那樣的 pipe,用法是 pipe(source, operator1, opeator2, operator3) 這類指攒。
?map operator 的實現(xiàn)慷妙,可以直接用 functor 的 fmap,參數(shù)映射一下 pipe 函數(shù)的要求允悦。
?因為 map 函數(shù)已經(jīng)定義過了膝擂,因此這個 map operator 只好改名為 mapx。
filter operator 就是通過 predicate 函數(shù)隙弛,有選擇的將 source[圖片上傳失敗...(image-b6bdd4-1563238657901)]
架馋。
?take 和 scan 則分別是內(nèi)部計數(shù)和累加acc,代碼都很簡單全闷。
將這些 operators 用在我們的 tick 上叉寂。
?輸出 10 個奇數(shù)的數(shù)組。如下圖所示总珠。
總結(jié)
需要說明的是屏鳍,目前的模擬是一個粗糙的做法,有很多沒有處理局服,比如 unwatch 的時機(jī)钓瞭,它幾乎一定會內(nèi)存泄露。需要更精細(xì)的去實現(xiàn)和控制腌逢,才能得到一個可用的形態(tài)降淮,當(dāng)下只是演示一下思路 。
這些 demo 只是演示一些能力搏讶。沒有考慮實際項目里怎么用佳鳖,不管大小,都不要用這個方案媒惕。
等有人基于這個思路做出一個完成度更好的庫或者框架系吩,再考慮吧。
到目前為止妒蔚,我們差不多填完了用 vue reativity api 實現(xiàn) immer-like, rxjs-like, react-hooks-like, cyclejs-like(就是最初的那個 reactive view) 的坑穿挨,應(yīng)該足以展示 vue reactivity api 是一個更加 primitive 的機(jī)制了(畢竟基于 Proxy)。
vue 3.0 reactivity api 的能力還不局限于上面演示的肴盏,感興趣的同學(xué)科盛,可以自行探索一下。