揭秘 Vue-3.0 最具潛力的 API

尤雨溪發(fā)布了 Vue Function-based API RFC职员,說是 3.0 最重要的 RFC拒贱。

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

有人表示喜歡和贊賞脚乡,有人卻表示“這不就是抄 React 嗎蜒滩?我干嘛不直接學React去了”滨达。

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

function 強化跟 value 強化辆飘,是一個能力相當?shù)膶ε寄P汀R粋€是 a -> data 谓传,另一個則是 data -> a蜈项。后者也是現(xiàn)在函數(shù)式研究的一個方向,叫 codata(點擊 codata in action 了解更多)续挟。

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

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

今天我們要揭示的直颅,不是上面那個最重要的博个,而是最具潛力的、最能表征 Vue 路線的 API功偿。

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

它就是 Advanced Reactivity API共耍,Provide standalone APIs for creating and observing reactive state投蝉。(注:請先大概看一下 API 介紹,它是理解后續(xù)內容的基礎)征堪。

某種意義上瘩缆,vue 暴露的內部 api(reactivity api)比 react 暴露的內部 api(hooks),具有更強的表達能力和普適性佃蚜。它比 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 來用呢肮之?

哼哼,這個難不倒我們卜录。

我們來親自手擼一個簡單的 vue 3.0 reactivity api戈擒,不就行了嗎?

具體如何實現(xiàn)艰毒,不是這篇文章的重點筐高,按下不表。如果你等不急看代碼和效果现喳,可以點擊這里訪問DEMO(我基于 reactivity-api 實現(xiàn)了 counter 和 todo-list 效果)凯傲。你會發(fā)現(xiàn) reactivity.js 已經(jīng)被編譯和壓縮過了,可讀性很低嗦篱。這是因為冰单,最近前端社區(qū)有一些不良風氣,一些小朋友灸促,從各處抄了一點代碼诫欠,就覺得實現(xiàn)了 vue/react 的核心涵卵。過分自信的在四處發(fā)表錯漏百出、富有偏見的觀點荒叼。因此我們特意做了一下處理轿偎,增加點抄襲成本,反正這不妨礙我們此次的演示目的被廓。

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

image

首先實現(xiàn)一個 watchable 函數(shù)坏晦,可以將任意對象或數(shù)組,變成可 watch 的嫁乘,它有第二個參數(shù)昆婿,options,其中 options.map 決定 set 階段時如何儲存到 target蜓斧。

state 采用遞歸的方式仓蛆,將整顆樹都 watch 起來。value 則只 watch value 字段挎春。

盡管 vue 被認為不夠 fp看疙,不過我們其實可以插上一些 fp 的翅膀,比如將 value 視為 monad直奋,實現(xiàn) fmap, ap, bind 等函數(shù)能庆。

image

fmap 是基于一個 watchable a,和 a -> b 的 f 函數(shù)帮碰,構造一個 watchable b 對象相味。這里簡單 watch a拾积,然后在賦值給 b 的階段殉挽,調用 f(a) 構造新的值即可。

ap 則是 watchable f 和 watchable a拓巧,構造一個 watchable b斯碌,b 是 f(a) 的產(chǎn)物。

bind 是 watchable a 加上類型未 a -> watchable b 的 f肛度,實現(xiàn)基于上一個 value 的值構造下一個 watchable 的功能傻唾。

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

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

vue 跟 rxjs 這種特殊的值凛辣,可以直接衍生出 view。首先實現(xiàn)一個 combinaLatest([value]) 职烧,得到一個在 value 范疇內構造數(shù)組的方式扁誓,然后通過 [[key, value]] 防泵,從處理數(shù)組的方式中,配合 fromEntries 衍生出 value層面構造 object 的方式蝗敢。而 virtual-ui-model 就是用特定的 object 表征 ui 對象捷泞。因此,我們基于 object 可以實現(xiàn) view寿谴,它代表了一個在時間序列中動態(tài)輸出的視圖流锁右,并且因為 combinaLatest 自動復用未變化的值,使得 view -> view 輸出的結構讶泰,總是結構共享的骡湖,利于 diff 算法。

image

實現(xiàn) combineArray:如果一個數(shù)組里存在一個 reactive value峻厚,那么它也返回一個 reactive array响蕴,它每次輸出一個純數(shù)組。如果數(shù)組里不包含 reactive value惠桃,它什么也不包裝浦夷,直接返回該數(shù)組。相當于 Promise.all(list)辜王,只不過它有可能不返回 promise/reactive-value劈狐。

image

有了 combineArray,可以實現(xiàn) combineObject呐馆,正如前面說的肥缔,就是 entries和 fromEntries 的轉換。

image

再封裝一下汹来,得到一個 combine 函數(shù)续膳,可以將任意結構,構造成 reactive-value收班,只要子結構了包含 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啼辣,切換成我們自己構造的 createElement。

image

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

image

最后,實現(xiàn)一個 map 函數(shù)舆声,用來 map 一個 reactive-value花沉,既支持數(shù)組,也支持非數(shù)組媳握。

準備工作做好了碱屁,把它們 import 進來。

image

回顧一下我們的 combineArray 是如何更新的蛾找,它不是直接賦值娩脾,而是先淺拷貝,再復制打毛。

image

?這意味著柿赊,它總是返回 immutable-list,因為它跟 immer 一樣 copy-on-write幻枉。

我們免費得到了一個行走的 immer碰声,不需要 produce 包裹。combine 一下熬甫,然后隨便改胰挑,watch 函數(shù)都會拿到結構共享的 immutable data。

如果沒有實現(xiàn)這一點椿肩,combine react-element 時瞻颂,子樹直接被修改,react 進行diff 時檢測不出來子樹有變化覆旱,就不會去更新視圖了蘸朋。

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

image

?看這個代碼扣唱,是不是覺得非常有趣?既像 vue 那樣可以用 js 賦值操作团南,又像 react-hooks 那樣的形式噪沙,還像 cycle.js 一樣在組件內部可以操作 reactive value。

它怎么做到自動更新視圖的呢吐根?

因為 let count = value(0)正歼,它是一個 reactive-value。它被 handleIncre 和 handleDecre 修改拷橘,它同時用在了 jsx 里局义。我們的 createElement 會檢測到這個 jsx 結構里包含一個 reactive-value喜爷,因此它會被整個 combine 起來,成為一個大的響應式的值 view.value萄唇。

image

前面我們將 jsx 編譯從 React.createElement 切換到我們的 createElement 函數(shù)檩帐,因此 <Counter /> 組件不是返回 react-element,而是返回我們的 reactive-value另萤,它是一個響應式的值湃密,可以被 watch。我們 watch 這個 <Counter />四敞,然后拿到它真正的 react-element泛源,再用 ReactDOM 渲染到 root 節(jié)點。

看起來像下面那樣忿危。

image

只支持一個 counter达箍,看起來可能是一個特例,我們可以再實現(xiàn)一個 todo list铺厨。

image

?TodoApp 組件里構造一個 reactive-state幻梯,然后傳遞給 TodoInput 和 Todos。

image

?TodoInput 里構造一個 reactive text努释,作為局部狀態(tài)碘梢,綁定到 input 元素。

點擊 add 按鈕時伐蒂,構造一個 todo煞躬,直接 push 到 todos 里即可。

其它用到 todos 的地方逸邦,會自動檢測到 todos 變化而進行局部渲染恩沛。比如我們的 Todos。

image

?它通過 map 函數(shù)缕减,將 reactive todos 映射成 Todo 組件雷客,每當 todos 變化時,這個 map 函數(shù)就會自動再次執(zhí)行桥狡,然后 top-level 的 app 就會拿到一個 immutable vdom搅裙,除了 todos 以外,其它結構復用原來的引用裹芝。

image

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

整體看上去像下面那樣兄朋。

image

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

react 在這里只是起來了一個 renderer 的作用峡扩,理論上蹭越,用任意 vdom library 都行。

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

image

上面的 test 是一個 reactive state有额,里面深層節(jié)點里包含了 reactive-value般又。

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

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

我們構造了 3 個方法脆栋,分別深度更新不同的字段倦卖,然后隨機使用這些更新方法。它們不會引起其它字段的引用變化椿争,共享沒有變化的結構怕膛。

image

?

比如,randomMethod a 只引起了 a 字段的更新秦踪,因此 c 和 g 字段跟 prev 對比是相等的褐捻。

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

vue 3.0 的 reactivity api椅邓,更多的是承擔 connect, computed, combine 等結構關聯(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 的結合,就得到了一個 producer + combinator聊疲。比如茬底,我們要構造一個 count,它不只是在 count.value += 1 的時候被動產(chǎn)生新的 value获洲,而是可以通過某個機制,不斷自動產(chǎn)生殿如。

image

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

不過爱致,額外引入 react-hooks,跟 vue-reactivity 并行寒随,會顯得很奇怪糠悯,應該用后者實現(xiàn)前者的機制。就是 re-run 時妻往,重用 state/value互艾,并且 state/value 的變化,會引起函數(shù)的 re-run讯泣。

useEffect 應該是 watch 自身纫普,是一個語法糖。watch(self, effect)好渠。

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

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

image

?實現(xiàn) 3 個增強函數(shù)的函數(shù)霍掺,resumable 增強函數(shù) re-run 自身的能力匾荆,referencable 增強函數(shù)持久化內部狀態(tài)的能力。reactive 增強函數(shù)使用 reactivity api 的能力抗楔。

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

image

reactivie 就是將 prodcuer 的返回值连躏,掛載到 value.value 上剩岳,自身永遠返回 value,因此外部總是拿到一個 reactive value入热。

image

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

image

?useEffect 在使用 storage 方法時勺良,通過 reactive enhancer 拿到了 value$绰播,watch 它,并返回 unwatch尚困。

image

?useReactive 在內部構造 reactive value/state蠢箩,watch 它,然后使用 resumable enhancer 提供的 resume 方法,觸發(fā)重新執(zhí)行谬泌。

image

?然后用 useReactive滔韵,分別實現(xiàn) useState,和 useValue掌实。再用它們實現(xiàn)一個 interval 函數(shù)陪蜻,可以輸出一個自行變化的 count。

image

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

image

?效果宴卖,有一個 tick 自動隨時間而變化,不需要額外的地方去 count.value += 1邻悬。

image

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

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

image

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

image

?因為 map 函數(shù)已經(jīng)定義過了分苇,因此這個 map operator 只好改名為 mapx。

filter operator 就是通過 predicate 函數(shù)屁桑,有選擇的將 source的值医寿,轉移到 sink

image

?take 和 scan 則分別是內部計數(shù)和累加acc蘑斧,代碼都很簡單靖秩。

image

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

image

?輸出 10 個奇數(shù)的數(shù)組竖瘾。如下圖所示沟突。

image

總結

需要說明的是,目前的模擬是一個粗糙的做法捕传,有很多沒有處理惠拭,比如 unwatch 的時機,它幾乎一定會內存泄露庸论。需要更精細的去實現(xiàn)和控制职辅,才能得到一個可用的形態(tài),當下只是演示一下思路 聂示。

這些 demo 只是演示一些能力域携。沒有考慮實際項目里怎么用,不管大小鱼喉,都不要用這個方案秀鞭。

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

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

vue 3.0 reactivity api 的能力還不局限于上面演示的宠默,感興趣的同學,可以自行探索一下灵巧。

可以點擊這里訪問DEMO

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末搀矫,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子刻肄,更是在濱河造成了極大的恐慌瓤球,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件敏弃,死亡現(xiàn)場離奇詭異卦羡,居然都是意外死亡,警方通過查閱死者的電腦和手機麦到,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進店門绿饵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瓶颠,你說我怎么就攤上這事拟赊。” “怎么了粹淋?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵吸祟,是天一觀的道長。 經(jīng)常有香客問我桃移,道長屋匕,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任借杰,我火速辦了婚禮过吻,結果婚禮上,老公的妹妹穿的比我還像新娘第步。我一直安慰自己疮装,他們只是感情好,可當我...
    茶點故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布粘都。 她就那樣靜靜地躺著廓推,像睡著了一般。 火紅的嫁衣襯著肌膚如雪翩隧。 梳的紋絲不亂的頭發(fā)上樊展,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天,我揣著相機與錄音,去河邊找鬼专缠。 笑死雷酪,一個胖子當著我的面吹牛,可吹牛的內容都是我干的涝婉。 我是一名探鬼主播哥力,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼墩弯!你這毒婦竟也來了吩跋?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤渔工,失蹤者是張志新(化名)和其女友劉穎锌钮,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體引矩,經(jīng)...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡梁丘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了旺韭。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片氛谜。...
    茶點故事閱讀 40,615評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖茂翔,靈堂內的尸體忽然破棺而出混蔼,到底是詐尸還是另有隱情,我是刑警寧澤珊燎,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布惭嚣,位于F島的核電站,受9級特大地震影響悔政,放射性物質發(fā)生泄漏晚吞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一谋国、第九天 我趴在偏房一處隱蔽的房頂上張望槽地。 院中可真熱鬧,春花似錦芦瘾、人聲如沸捌蚊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽缅糟。三九已至,卻和暖如春祷愉,著一層夾襖步出監(jiān)牢的瞬間窗宦,已是汗流浹背赦颇。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留赴涵,地道東北人媒怯。 一個月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像髓窜,于是被迫代替她去往敵國和親扇苞。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,630評論 2 359

推薦閱讀更多精彩內容