目前,小程序在用戶規模及商業化方面都取得了極大的成功。微信、支付寶、百度、字節跳動等平臺的小程序日活都超過了3億。javascript
咱們在開發小程序時仍然存在諸多痛點:小程序孱弱簡陋的原生開發體驗,註定會出現小程序加強型框架,來提高開發者開發體驗;各家廠商小程序API碎片化的現狀,註定會有多端框架會成爲標配,由跨端框架肩負跨平臺移植挑戰。css
正是由於開發者對於提高小程序開發效率有着強烈需求,小跨端框架發展到現在已經百花齊放、百家爭鳴:除了美團的 mpvue
、網易的 megalo
、滴滴的 chameloen
已經趨於穩定,京東的 Taro
開始探索 taro next
, Hbuilder 的uni-app
產品和生態持續完善,微信新推出了支持H5和微信小程序的 kbone
框架,螞蟻金服的 remax
html
上述的這麼多跨端框架紛繁複雜,咱們能夠從下面兩個維度進行分類:vue
從框架的語法來講,能夠分爲下面兩類:java
Vue 語法node
React 語法 / 類 React 語法react
主流的跨端框架基本遵循 React、Vue 語法,這也比較好理解,能夠複用現有的技術棧,下降學習成本。ios
remax | Taro next | Taro 1/2 | megalo | mpvue | uni-app | chameloen | |
語法 | react | react | 類 react (nerve) | vue | vue | vue | 類 vue |
廠家 | 螞蟻金服 | 京東 | 京東 | 網易考拉 | 美團 | Hbuilder | 滴滴 |
從實現原理上,開源社區的跨端框架大體分爲下面兩類:git
compile time
** 編譯時**github
runtime
** 運行時**
compile time
** 編譯時**的跨端框架,主要的工做量在編譯階段。他們框架約定了一套本身的 DSL
,在編譯打包的過程當中,利用 babel 工具經過 AST 進行轉譯,生成符合小程序規則的代碼。
這種方式容易出現 BUG
,並且開發限制過多。早期的** Taro 1.0 和 2.0** 的版本就是採用的這種方案,下文會有更具體的介紹。
而另一種runtime
** 運行時模式**, 跨端框架真正的在小程序的邏輯層中運行起來 React 或者是 Vue 的運行時,而後經過適配層,實現自定義渲染器。這種方式比靜態編譯有自然的優點,因此 Taro 的最新 Next 版本和 Remax 採用的是這種方案。
經過上文咱們知道小程序跨端框架目前有不少嘛,各個大廠都會有本身的一套,百花齊放。文章篇幅有限,若是要分別拆開講清楚他們各家實現的細節,是一件很困難同時很費時間的事情。
因此,下文會嘗試梳理一下主流小程序一些共用性的通用實現原理, 儘可能會屏蔽忽略掉各家實現一些細枝末節的細節差別,也不會在文章中貼大段的源碼分析,而是經過僞代碼來代替。
下面,咱們會從** Vue 跨端框架**和 React 跨端框架兩個大方向,進入到小程序跨端原理的世界,講解這些跨端框架的核心原理,深刻到源碼底層去分析,揭開他們神祕的面紗。
當你使用 megalo
、mpvue
這些 Vue 跨端框架時,看上去,咱們寫的是vue
的代碼,而後打包編譯以後就能夠運行在小程序內,是否是很神奇?這些框架背後作了哪些事情呢?
實際上,這些** Vue的跨端框架 **核心原理都差很少,都是把 Vue 框架拿過來強行的改了一波,藉助了 vue 的能力。好比說,vue 的編譯打包流程(也就是vue-loader的能力), vue 的響應式雙向綁定、虛擬dom、diff 算法。上面這些東西跨端框架都沒有修改,直接哪來用的。
那麼哪些部分是這些跨端框架本身新加的東西呢?
涉及到 Vue 框架中操做DOM節點的代碼。
這些跨端框架,把本來Vue框架中原生 javascript 操做 DOM 的方法,替換成小程序平臺的 setData()
。爲何要這樣呢?不着急,下文會有比較詳細的講解。
不着急,慢慢來,咱們先從一個最簡單的問題開始。
首先咱們來看,一個 vue
的單文件,究竟作了啥,怎麼就能跑在小程序裏面了?
咱們知道,對於微信小程序來講,須要有 4份文件:.wxml
、.wxss
、.js
、 .json
。
(上面是去微信小程序官網截的圖)
而對於一個 Vue 組件來講,一個 vue 文件有三個部分組成:template
, script
, style
。
那麼,這些跨端框架把Vue 單文件中的 <template>
、 <script>
、<style>
這三個部分對應的代碼,拆一拆,分別處理編譯一下,分到 .wxml
、.wxss
、.js
、 .json
這 4 份文件中,以下圖所示:
咱們分別從<template>
、 <script>
、<style>
這三個部分來討論:
<style>
部分是最簡單的。通常來講,在 h5 環境中的 css 樣式,大部分均可以直接挪到 .wxss
,須要處理的部分比較少,除了少部分不支持的屬性和 小程序的單位轉換<template>
** 轉換到 **.wxml
稍微複雜一點。咱們須要把 h5 的標籤啊、vue特殊的語法替換成小程序的標籤、小程序特殊的語法。替換的工做咱們稱爲 模板替換
,下文會有一個章節用來介紹。<script>
** 到 **.js
, 涉及到 vue
的運行時 如何和 小程序的實例通信的問題,這一部分會用比較多的章節去介紹。接下來,咱們先看模板替換
,也就是template 生成 .wxml
文件的過程。
<template>
到 .wxml
Vue 是採用 template
語法的,各大廠商的小程序也是採用了 template
語法。從 Vue 的 template
轉變成小程序的 template
相對比較簡單,React 的 jsx 轉變爲小程序的 template 就相對比較棘手啦。
Vue 的 template
與小程序的 template
大致是上的語法很相似,可是仍是有不同的地方。例如小程序裏沒有 <div>
標籤,而是小程序廠商提供的 <view>
標籤等等
所以咱們須要把 Vue 模版轉換爲微信小程序的 .wxml
。
例如上圖所示,<div>
標籤須要轉換成 <view>
標籤,一些 vue 中的語法也須要進行轉化成對應小程序平臺的語法。
再好比說,在 Vue 裏面綁定事件經常使用 @methodName
的語法, 轉成小程序模版則須要用 bind
,同時用 tap 事件替換 click 事件。
除了這個,還有一些vue的模板語法,也須要轉成小程序的模板語法
Vue 和小程序插值表達式則是同樣的,採用了雙花括號,能夠不須要作任何轉化
上面展現的這些模板替換,都只是替換爲微信小程序的語法。轉化爲其餘小程序平臺的語法也是相似的思路,以下圖所示:
那麼,模板的轉化具體是如何實現的呢? 咱們的第一想法是經過正則來匹配,可是要寫出匹配出全部狀況的正則是很是困難的。
實際上,mpvue
、megalo
、uni-app
的框架是採用了 ast
來解析轉化模板的。
模板替換過程其實就是兩側對齊語法的過程,把語法不一致的地方改爲同樣的,是一個 case by case 的過程,只要匹配到不一樣狀況下語法便可,比較費功夫可是難度係數不是很高。
接下來咱們看如何把 <script>
中的內容,挪到小程序 .js
中呢?
<script>
到 .js
咱們在 .vue
單文件中的 script 部分中, 一般會寫下面的代碼,咱們會寫一個 Vue 的配置項對象傳入到 Vue 構造函數中,而後 new Vue()
會實例化出來一個 vue 實例。
new Vue({
data(){},
methods: {},
components: {}
})
複製代碼
上面的代碼是徹底能夠跑在小程序的邏輯層裏面的,只要引入vue 便可,畢竟 Vue 大部分就是純粹的 javascript。也就是說,小程序的渲染層裏面是徹底能夠直接運行起來 Vue 的運行時和 React 的運行時的。
可是這樣還不夠,小程序平臺還規定,要在小程序頁面中調用 Page()
方法生成一個 page
實例, Page()
方法是小程序官方提供的 API。
在一個小程序的頁面中,是必須有 Page()
方法的。微信小程序會在進入一個頁面時,掃描該頁面中的 .js
文件,若是沒有調用 Page()
這個方法,頁面會報錯。
以下圖所示,咱們在 <script>
中寫的是 new Vue()
這樣子的代碼,而微信想要的是 Page()
。
那麼,應該怎麼解決呢?
Vue 跨端框架他們拓展了 Vue
的框架,把 Vue
2.0 的源碼直接拷貝過來,改了裏面的初始化方法,在初始化方法中調用了 Page() 方法, 以下面僞代碼所示:
new Vue() {};
Vue.init = () => {
// 在 vue 初始化的時候,調用了 page() 方法
Page()
}
複製代碼
在 vue 實例化的時候,會調用 init
方法,在 init
方法裏面會調用 Page()
函數,生成一個小程序的 page 實例。
這樣,咱們在一個小程序頁面中,就會同時存在一個 vue 的實例,和一個小程序的 page 實例,他們是如何融合起來一塊兒工做的呢?他們之間是如何作到數據管理的? 如何進行通信的呢?
接下來就涉及到 Vue 框架的核心流程了,爲了方便一些不瞭解 Vue 同窗,同時也爲了更好的深刻理解下面講的內容,接下來會稍微講一丟丟 vue 的核心流程。
以下圖左側所示,簡單來講, 一個 .vue
的單文件由三部分構成: template
, script
, style
咱們先看上圖中的橙黃色的路徑,也就是 template
部分的處理過程。
以下圖所示,template
模板部分會在編譯打包的過程當中,被 vue-loader
調用 compile
方法經過詞法分析生成一個 ast
對象,而後調用代碼生成器,通過遍歷 AST 樹遞歸的拼接字符串操做,最終生成一段 render
函數, render
函數最後會存在打包生成的dist 文件中。
能夠看下面這個例子,一段簡單的 template
模板以下所示:
<div class="ctl-view" @click="handleClick">
{{ a }}
</div>
複製代碼
通過編譯以後,經過 ast
進行分析,生成的 render
函數以下:
_c("div",
{ staticClass: "ctl-view", on: { click: _vm.handleClick } },
[_vm._t("default")]
)
複製代碼
render
函數會在第一次 mount時,或者Vue 維護的 data 有更新產生的時候會被執行。
那麼執行下面這段 render
函數會拿到什麼呢?
上面圖中藍色圓圈中的 _c
方法是建立元素類型的vnode
, 而 _v
方法是建立 文本類型的vnode。
Render
函數中會調用這些方法建立不一樣類型的vnode,最終的產物是生成好的虛擬DOM樹 vnode
tree,對應上面圖中 render
函數的下一個階段 vnode。
虛擬DOM樹是對真實DOM樹的抽象,樹中的節點被稱做 vnode
。 vnode
有一個特色, 它保存了這個DOM節點用到了哪些數據 ,這一點很是重要。
Vue
拿到 虛擬dom樹以後,就能夠去和上次老的虛擬dom樹作 patch
diff
對比。
這一步的目的是找出,咱們應該怎麼樣改動現存的老的DOM樹,代價才最小。
patch
階段以後,若是是運行在瀏覽器環境中, vue
實例就會使用真實的原生 javascript 操做DOM的方法(好比說 insertBefore
, appendChild
之類的),去操做DOM節點,更新瀏覽器頁面的視圖。
接下來,咱們再來看一下上面圖中,藍色的線條的路徑。
在new Vue
的時候,Vue 在初始化的時候會對數據 data
作響應式的處理,當有數據發生更新時,最後會調用上文的 render
函數,生成最新的虛擬DOM樹。
接着對比老的虛擬DOM 樹進行 patch
, 找出最小修改代價的vnode
節點進行修改。
上面介紹的流程就是 vue
的總體流程啦。
(若是有不理解的地方,不重要,也不須要擔憂會阻塞下文的閱讀)
咱們要關心的是,下面的類 vue 小程序跨端框架的核心流程。接下來一塊兒來看吧。
在進一步講解以前,咱們先思考一個問題。上圖中,Vue 在 diff 以後就是操做了原生的 DOM 元素,可是各家廠商的小程序不支持原生DOM操做,所以也就沒有修改視圖節點的能力。那麼咱們怎麼樣才能更新小程序的視圖呢?
下面這張圖表明瞭類 vue 小程序跨端框架的核心流程圖。
咋一看這張圖,會發現和上面Vue的圖是很像的。畢竟 megalo
、 mpvue
等小程序框架,本質都是對 vue 的拓展(copy過來改了改)。
仔細和上面的 vue 的核心流程圖一對比,咱們發現,小程序跨端框架的流程圖替換掉 vue
本來的 DOM 操做,替換爲新增的綠色的setData
操做, 同時還多了一個綠色框框中的的 Page()
方法。
Page() 方法上文有介紹過緣由
setData()
是小程序官方提供的 API,用來修改小程序 page 實例上的數據,從而會更新小程序的視圖。
『替換掉 vue
本來的 DOM 操做』這一個點比較容易理解,由於小程序容器並無提供操做小程序節點的 API 方法,這是由於小程序隔離了渲染進程
(渲染層)和邏輯進程
(邏輯層),以下圖所示:
在小程序容器中,邏輯層到渲染層的更新,只能經過 setData()
來實現。
無論是
mpvue
、megalo
,仍是uniapp
,這些類 vue 跨端框架,都是經過這種方法來更新視圖的。並且,在將來可預見的幾年裏,只要小程序廠商不提供修改小程序節點的 API 方法,小程序跨端框架更新 DOM 節點仍然會經過 setData 這種 API
好了,到了這一步,咱們已經知道了,跨端框架替換了 Vue 框架中 **JS 操做DOM 原生節點的 API **爲 **setData() **來更新小程序的頁面。
可是咱們仍是不知道具體背後作了什麼,接下來,看一個具體的例子:
new Vue({
data(){
return {
showToggle: true
}
}
})
// 下面是通過 模板替換 以後的代碼
<view wx:if="{{showToggle}}">
</view>
複製代碼
在上面的例子中,showToggle
這個變量表明的數據是維護在Vue 實例上的。
在頁面初始化的時候,咱們的小程序跨端框架就開始執行了,它會先實例化一個Vue 實例,而後調用小程序官方的 Page() API 生成了小程序的page 實例,並在在 Vue 的 mounted 中會把數據同步到小程序的 page 實例上。
所以在實際頁面打開以後,會同時存在小程序原生的Page
實例和 Vue
實例。vue 實例上有數據(咱們的 data 原本就是定義在 vue 裏面的),小程序Page
實例上也有數據(小程序實例上沒數據無法渲染頁面對吧)。
當 Vue 中的數據發生變化時,會觸發 Vue 響應式的邏輯,走 上圖中Vue 更新的那一套邏輯:從新執行 render 函數 👉🏻 得出一份最新的 Vnode 樹 👉🏻 接下來 Vue去 diff 新舊兩個 Vnode 的樹,找出修改 DOM 節點最高效的操做。注意!接下來不是調用操做 DOM 的 API, 而是調用小程序的 setData()
API 方法, 👉🏻 修改小程序實例上的對應的數據, 從而讓小程序渲染層層去更新視圖。
這一套流程下來咱們發現,通俗來說,數據是歸 Vue 管。 Vue 是一個雙向數據綁定的框架,小程序也是一個雙向數據綁定的東西,這兩個東西放一塊,經過跨端框架的運行時來作中間橋接,把數據同步到小程序中。
事件歸到小程序容器管,小程序觸發各類事件,好比說滾動,事件點擊,小程序容器捕獲到事件後,會去調用在 Vue 註冊的對應的事件處理函數。
上面介紹的模型,是一個通用的Vue 小程序跨端框架的實現。 Vue 的小程序跨端框架基本上思路是一致的。有了這些理解和認識,咱們再來看一下各家小程序框架是如何實現的:
下面是 mpvue 官方網站上的一張原理圖:
從右到左來看,當 Vue 上數據變化時,會經過 mpVue 運行時來通知小程序的實例,從而更新小程序 page 視圖。從左到右,當小程序的渲染層容器觸發了事件後,會經過跨端框架運行時來找到註冊的 vue 的事件回調函數
咱們接着來看,下面是 uni-app 的官網的原理圖,和上面的圖像素級別的類似啊
從右到左來看,當 Vue 上數據變化時,會經過uni-app運行時來通知小程序的實例,從而更新小程序 page 視圖。從左到右,當小程序的渲染層容器觸發了事件後,會經過跨端框架運行時來找到註冊的 vue 的事件回調函數
下面是 megao 官方的一張原理圖,這兩張圖和上面看似長的不同,但表達的的意思是同樣的。
在這個小節中,重點部分有跨端框架模板替換、 vue
的核心流程、跨端框架替換了 Vue 的 javascript 操做真實 DOM 的 API 等。
至此,一個 vue 跨端框架的核心流程就已經走完了。這個流程中,一些跨端框架會進行優化,不一樣的跨端框架會採用不一樣的優化策略,下面咱們以網易的 Megalo 爲例探討。
如今,咱們先假設 Vue 中維護的數據和小程序中維護的數據如出一轍,數據結構徹底相同。那麼當 Vue 中維護的數據發生變化時,直接把**Vue 中維護的數據 **原模原樣的同步到小程序中,以下所示,紫色的部分是直接同步過去的數據。
// vue 中維護的數據
new Vue({
data(){
return {
showToggle: true,
bigObj1: {},
bigObj2: {}
}
}
})
// 小程序中維護的數據
{
showToggle: true,
bigObj1: {},
bigObj2: {}
}
複製代碼
這樣有一個問題是,小程序 setData
是有性能問題的,若是頻繁地進行調用或者一次型更新大量數據,容易形成頁面卡頓。
爲了下降更新頻率的問題,咱們能夠經過加一個截流函數進行限制。
那麼怎樣減小數據更新的量呢?
上面的代碼中, bigObj1
和 bigObj2
是很是很是巨大的對象,可是小程序頁面中徹底沒有用,那麼同步bigObj1
和 bigObj2
對於 setData 來講就是巨大的浪費。
事實上,不少框架都對此作了優化,咱們來看 megalo 是怎麼作的。
假設咱們寫的 Vue 實例上的數據是這樣的:
而後假設咱們寫的 Vue 的 template 是這樣寫:
上面咱們寫的 <template>
模板代碼在編譯的時候會有兩個做用,一方面把模板替換爲小程序的標籤,另外一方面經過 vue-loader
編譯生成 render
函數,以下圖所示:
若是咱們仔細觀察上面編譯出來的 render
函數,會發現 megalo
作了一些手腳,多了一些參數,這些參數是 megalo 本身加上去的標記。
這些標記被 uglify 了,很難懂是什麼意思,下面介紹一下:
h_: 是獨一無二的 id, 是一個累加的數字。只有當前節點依賴了 data 數據時纔有 h_
f_: v-for 循環時的 index
c_: 父親組件的 id
複製代碼
執行上面這段 megalo 生成的 render
函數,一樣會生成一個 vnode
樹用於 patch
。其中的某一些** vnode 節點會稍顯特殊,當一個 vnode節點依賴了vue 實例上的 data 數據時,該 vnode 節點** 的 attrs
屬性上就會有 h_
、 f_
、 c_
的值。
以下圖所示的那樣,只有依賴了 data 數據的那三個節點,纔會有 h_
、 f_
、 c_
的值。
在第一次 mount
的時候,megalo
同步到小程序實例上的數據不是本來 vue 上的數據,那是什麼呢?
megalo 會對上面生成的藍色的 vnode
樹的結構進行摘取 + 扁平化的處理,變成一維數組,而後同步到小程序實例上。
如上圖所示,先把**用到 data 的 Vnode 節點(上圖帶有屬性的那三個節點)**摘取出來再打平成一維數組。
這樣的好處是:當 vue 實例上的數據有不少時,只有那些真正在模板中使用到的數據,纔會在編譯的階段被拼接到 render() 函數中,而後出如今生成的 Vnode 樹上(在編譯階段作的事情),以後纔有資格被 megalo 摘出來扁平化壓縮成一維數組,最後同步到小程序實例上。
那些多餘的沒有用的數據,是永遠不會出如今Vnode 樹上的。
爲啥要扁平化爲一維數組呢?
這是由於 Vnode 層級關係多是很深很複雜的,若是把這種複雜的層級關係也維護在小程序實例中,會比較麻煩。
小程序 setData
方法支持傳入一個對象,對象的 key 是能夠有層次關係的,好比說下面代碼中的 0
表示父組件的 id, 1
表示節點上的id
setData({ '$root.0.h.1.t', 'a' })
複製代碼
若是咱們把節點打平,標識一個惟一的 id, 就能夠只維護一個扁平化的一維數組。
這樣,當 Vue 的 Vnode tree 上某一個 Vnode 節點發生了變更時,咱們須要同步更新小程序上的數據,不須要關心那個 Vnode 節點在樹上的哪一個位置,只須要知道那個節點上的id ——也就是** attrs.h_ 值和父組件的 id ——也就是 attrs.c_** 值 ,而後拼出上面 setData
的 key 路徑——$root.0.h.1.t
,就能夠精準且方便修改小程序實例上的數據了
計算 vnode 節點上的id 是經過 getHid()
, 計算父組件的 id 是經過 getVMId()
方法
getHid()
方法本質上是返回節點上的** attrs.h_ **的值, 上面說過了 **h_ **是獨一無二的 id, 是一個累加的數字。若是模板中有 v-if
循環的話,則返回 attrs.h_ + '-' + attrs.f_
的值。
getVMId()
方法本質上是返回節點上的 attrs.c_ 的值,也就是所在的組件的 id。 Vue 中組件是有層級關係的,在小程序中數據被打平了怎麼表示究竟是修改哪一層的組件的數據呢?
Megalo 經過拼接v
字符來表示組件的層級
0v0
表示是第二層組件,也就是 Vnode 中第一個組件中的第一個子組件
0v1
也表示是第二層組件,也就是 Vnode 中第一個組件中的第二個子組件
0v0v0
表示是第三層組件,也就是 Vnode 中第一個組件中的第一個子組件中的第一個孫子組件
只要能算出正確的相似如$root.0.h.1.t
的 key 路徑,咱們就能夠正確的把 Vnode 樹,更新到微信小程序上。
固然,模板部分也要配合上面扁平化的修改,差值的 `{{}}` 中,應該在編譯的時候替換爲適配扁平化的數據
總結一下,Megalo 小程序實例上數據,既不是 vue 實例上原模原樣的數據,也不是 vue 生成那顆 Vnode 樹,而是 Megao 從 Vnode 樹上摘取後再通過扁平化壓縮後獲得的數據結構。這樣能夠帶來性能上的提高。
類 React 框架存在一個最棘手的問題: 如何把靈活的 jsx 和動態的 react 語法轉爲靜態的小程序模板語法。
爲了解決問題,不一樣的的團隊實踐了不一樣的方案,大致上能夠把全部的類 React 框架分類兩類:
Taro 1/2
, 去哪兒的 Nanachi
,淘寶的rax
Taro Next
,螞蟻的 remax
所謂靜態編譯,就是上面說的這些框架會把用戶寫的業務代碼解析成 AST 樹,而後經過語法分析強行把用戶寫的類 react 的代碼轉換成可運行的小程序代碼。
以下圖所示的Taro 1版本或者2版本的邏輯圖,整個跨端的核心邏輯是落在編譯過程當中的抽象語法樹轉化中作的。
Taro 1/2 在編譯的時候,使用 babel-parser 將 Taro 代碼解析成抽象語法樹,而後經過 babel-types 對抽象語法樹進行一系列修改、轉換操做,最後再經過 babel-generate 生成對應的目標代碼。
有過 Babel 插件開發經驗的同窗應該對上面流程十分熟悉了,無非就是調用 babel 提供的 API 匹配不一樣的狀況,而後修改 AST 樹。
下面咱們來舉一個例子,若是咱們使用 Taro 1/2 框架來寫小程序頁面組件,極可能是長成下面這樣:
能夠看到上面組件很是像一個 React 組件,你須要定義一個 Component
和 render
方法,而且須要返回一段 JSX
。
這段的代碼,會在 Taro1/2 編譯打包的時候,被框架編譯成小程序代碼。具體來講, render
方法中的 JSX
會被提取出來,通過一系列的重重轉換,轉換成小程序的靜態模板,其餘 JS 的部分則會保留成爲小程序頁面的定義,以下圖所示:
這聽上去是一件很美好的事情,可是現實很骨感,爲啥呢?
JSX 的語法過於靈活。
JSX 的靈活是一個雙刃劍,它可讓咱們寫出很是複雜靈活的組件,可是也增長了編譯階段框架去分析和優化的難度。
你在使用 JavaScript 的時候,編譯器不可能hold住全部可能發生的事情,由於 JavaScript 太過於動態化。你想用靜態的方式去分析它是很是複雜一件事情,咱們只要稍微在上面的圖中例子中加入一點動態的寫法,這些框架就可能編譯失敗。
雖然這塊不少框架已經作了不少嘗試,但從本質上來講,框架很難經過這種方式對其提供安全的優化。
這也是 React 團隊花了3 年的時候搞出來 fiber 的意義, React 的優化方案並非在編譯時優化,而是在運行時經過時間分片不阻塞用戶的操做讓頁面感受快起來。
因此,React 解決不了的問題,這些小程序跨端框架一樣也解決不了。
他們都會告訴開發者要去避免不少的動態寫法。好比說 Taro 1 /2 版本的文檔裏面就給出了很是清晰的提示
Taro 發展到了2019年,他們終於意識到了上面問題的緊迫性: JSX 適配工做量大,很難追上 react 的更新。
這些問題歸根到底,很大一部分是 Taro 1/2 的架構問題。Taro 1/2 用 窮舉 的方式對 JSX 可能的寫法進行了一一適配,這一部分工做量很大,徹底就是堆人力去適配 jsx ,實際上 Taro 有大量的 Commit 都是爲了更完善的支持 JSX 的各類寫法。
於此同時,螞蟻金服的@邊柳在第三屆 SEE Conf 介紹了 Remax ,走了不一樣於靜態編譯的一條路,推廣的口號是 『使用真正的 React 來構建小程序』。由於 Taro 1/2是假的 React,只是在開發時遵循了 React 的語法,在代碼編譯以後實際運行時的和 React 並無半毛錢關係,所以也無法支持 React 最新的特性。
Taro 團隊從活躍的社區中受到了啓發,徹底重寫了 Taro 的架構,帶來了 Taro Next 版本。
接下來,咱們會一點點揭開 React 運行時跨端框架的面紗。Taro Next
和 Remax
原理類似,Remax 已經比較穩定了,下面會着重講解 Remax 的原理,Taro Next 放在最後做爲比較。
你須要對 React 的基本原理有必定的瞭解。
在深刻閱讀本文以前,先要確保你可以理解如下幾個基本概念:
經過 JSX
或者 React.createElement
來建立 Element,好比:
<button class='button button-blue'>
<b>
OK!
</b>
</button>
複製代碼
JSX
會被轉義譯爲:
React.createElement(
"button",
{ class: 'button button-blue' },
React.createElement("b", null, "OK!"))
複製代碼
React.createElement
最終構建出相似這樣的對象:
{
type: 'button',
props: {
className: 'button button-blue',
children: {
type: 'b',
props: {
children: 'OK!'
}
}
}
}
複製代碼
React 16版本帶來了全新的 fiber 的架構,代碼拆分也很是清晰,大致上能夠拆分紅這三大塊:
React component API 代碼量比較少
Reconciler 調和器 代碼量很是大,是fiber 調度的核心
**Renderer 渲染器,**負責具體到某一個平臺的渲染,最多見的 ReactDOM 就是 web 瀏覽器平臺的自定義渲染器
Reconciler
和 Renderer
的關係能夠經過下圖縷清楚
Reconciler 調和器的職責是負責 React 的調度和更新,內部實現了 Diff/Fiber 算法,決定何時更新、以及要更新什麼。
**Renderer自定義渲染器,**負責具體到哪個平臺的渲染工做,它會提供宿主組件、處理事件等等。
Renderer 自定義渲染器裏面定義了一堆方法,是提供給 React 的 reconciler
使用的。React 的 reconciler
會調用渲染器中的定義一系列方法來更新最後的頁面。
咱們接下來會重點介紹Renderer自定義渲染器, 暫且先無論 Reconciler 調和器 ,就先認爲它是一個React 提供的黑盒。這個黑盒裏面幫咱們作了時間分片、任務的優先級調度和 fiber 節點 diff 巴拉巴拉一系列的是事情,咱們都不關心。咱們只須要知道 Reconcier 調和器在作完 current fiber tree 和 workIn progress fiber tree 的 diff 工做後,收集到 effects 準備 commit 到真實的 DOM 節點,是調用了的自定義渲染器中提供的方法。
若是在自定義渲染器中,你調用了操做 WEB 瀏覽器 web DOM的方法,諸如咱們很熟悉的 createElement
、appendhild
,那麼就建立/更新瀏覽器中的 web 頁面;若是渲染器中你調用了iOS UI Kit API,那麼則更新 ios ,若是渲染器中調用了 Android UI API, 則更新 Android。
Renderer 自定義渲染器有不少種,咱們最多見的ReactDOM
就是一個渲染器,不一樣的平臺有不一樣的 React 的渲染器,其餘還有不少有意思的自定義渲染器,可讓 React 用在TV 上,Vr 設備上等等,能夠點擊這個連接進行了解: github.com/chentsulin/…
事實上,Remax 和 Taro Next 至關因而本身實現了一套能夠在 React 中用的,且能渲染到小程序頁面的自定義渲染器。
總結來講,React 核心調度工做是在 Reconciler 中完成;『畫』到具體的平臺上,是自定義渲染器的工做。
關於React 渲染器的基本原理,若是對這個話題感興趣的同窗推薦觀看前 React Team 成員 Sophie Alpert 在 React Conf 上分享的《Building a Custom React Renderer》,也特別推薦這個系列的文章 Beginners guide to Custom React Renderers,講解的比較細緻
React 16 版本Fiber 架構以後,更新過程被分爲兩個階段:
協調階段(Reconciliation Phase) 這個階段 Reconciler 調度器會根據事件切片,按照任務的優先級來調度任務,最終會找出須要更新的節點。協調階段是能夠被打斷的,好比有優先級更高的事件要處理時。
提交階段(Commit Phase) 將協調階段計算出來的須要處理的**反作用(Effects)**一次性執行,也就是把須要作出的更改,一會兒應用到 dom 節點上,去修改真實的 DOM 節點。這個階段必須同步執行,不能被打斷
這兩個階段按照render
爲界,能夠將生命週期函數按照兩個階段進行劃分:
constructor
componentWillMount
廢棄componentWillReceiveProps
廢棄static getDerivedStateFromProps
shouldComponentUpdate
componentWillUpdate
廢棄render
getSnapshotBeforeUpdate()
componentDidMount
componentDidUpdate
componentWillUnmount
建立一個自定義渲染器只需兩步:
寫 宿主配置HostConfig,也就是下圖中綠色方框 HostConfig 的配置
實現渲染函數,相似於 ReactDOM.render()
方法
宿主配置 HostConfig,這是react-reconciler
要求宿主平臺提供的一些適配器方法和配置項。這些配置項定義瞭如何建立節點實例、構建節點樹、提交和更新等操做。下文會詳細介紹這些配置項
const Reconciler = require('react-reconciler');
const HostConfig = {
// ... 實現適配器方法和配置項
};
複製代碼
渲染函數就比較套路了,相似於 ReactDOM.render()
方法,本質就是調用了 ReactReconcilerInst
的兩個方法 createContainer
和 updateContainer
// 建立Reconciler實例, 並將HostConfig傳遞給Reconcilerconst
MyRenderer = Reconciler(HostConfig);
// 假設和ReactDOM同樣,接收三個參數
// render(<MyComponent />, container, () => console.log('rendered'))
export function render(element, container, callback) { // 建立根容器
if (!container._rootContainer) { container._rootContainer = ReactReconcilerInst.createContainer(container, false); } // 更新根容器
return ReactReconcilerInst.updateContainer(element, container._rootContainer, null, callback);
}
複製代碼
容器既是 React 組件樹掛載的目標
(例如 ReactDOM 咱們一般會掛載到 #root
元素,#root
就是一個容器)、也是組件樹的 根Fiber節點(FiberRoot)
。根節點是整個組件樹的入口,它將會被 Reconciler 用來保存一些信息,以及管理全部節點的更新和渲染。
HostConfig
支持很是多的參數,這些參數很是多,並且處於 API 不穩定的狀態,你們稍微瞭解一下便可,不用深究。另外,沒有詳細的文檔,你須要查看源代碼或者其餘渲染器實現。
若是感興趣的同窗能夠移步這篇文章 react 渲染器瞭解一下?。常見配置能夠按照下面的階段
來劃分:
經過上面代碼,咱們能夠知道 HostConfig
配置比較豐富,涉及節點操做、掛載、更新、調度、以及各類生命週期鉤子, Reconciler
會在不一樣的階段調用配置方法。好比說在協調階段會新建節點,在提交階段會修改子節點的關係。
爲了思路清晰,咱們按照 【協調階段】——【提交階段】—— 【提交完成】這三個階段來看,咱們接下來先看一下協調階段。
在協調階段, Reconciler
會調用 HostConfig
配置裏面的 createInstance
和createTextInstance
來建立節點。咱們接下倆看看 Remax
源碼是怎麼樣子的
const HostConfig = {
// 建立宿主組件實例
createInstance(type: string, newProps: any, container: Container) {
const id = generate();
// 預處理props, remax會對事件類型Props進行一些特殊處理
const props = processProps(newProps, container, id);
return new VNode({
id,
type,
props,
container,
});
},
// 建立宿主組件文本節點實例
createTextInstance(text: string, container: Container) {
const id = generate();
const node = new VNode({
id,
type: TYPE_TEXT,
props: null,
container,
});
node.text = text;
return node;
},
}
複製代碼
你們能夠回想一下,若是是本來的 ReactDOM 中的話,上面兩個方法應該是經過 javascript 原生的 API document.createElement
和 document.createTextNode
來建立瀏覽器環境的中的DOM節點
。
由於在小程序的環境中,咱們沒有辦法操做小程序的原生節點,因此Remax 在這裏,不是直接去改變 DOM,而建立了本身的 VNode
節點。
你可能會感到驚訝,還能這樣玩,不是說好要操做平臺的節點嘛,這樣不會報錯嗎?
緣由是,React 的 Reconciler 調和器在調度更新時,不關心 hostConifg 裏你新建的一個節點究竟是啥,也不會改寫你在 hostConifg 中定義的節點屬性。
因此自定義渲染器Renderer中一個節點能夠是一個 DOM 節點,也能夠是本身定義的一個普通 javascript 對象,也能夠是 VR 設備上的一個元素。
總而言之,React 的 Reconciler 調度器並不關心自定義渲染器 Renderer 中的節點是什麼形狀的,只會把這個節點透傳到 hostConfig
中定義的其餘方法中,好比說 appendChild
、removeChild
、insertBefore
這些方法中。
上面 Remax 的代碼中建立了本身的 VNode
節點, VNode
的基本結構以下:
interface VNode {
id: number;
container: Container;
children: VNode[];
mounted: boolean;
type: string | symbol;
props?: any;
parent: VNode | null;
text?: string;
appendChild(node: VNode): void;
removeChild(node: VNode): void;
insertBefore(newNode: VNode, referenceNode: VNode): void;
toJSON(): RawNode;
}
複製代碼
友情提示,這裏的
VNode
是 Remax 中本身搞出來的一個對象,和 React 或者 Vue 中的 virtual dom 沒有半毛錢的關係
能夠看到,VNode
其實經過 children
和 parent
組成了一個樹狀結構,咱們把它稱爲一顆鏡像樹
(Mirror Tree),這顆鏡像樹最終會渲染成小程序的界面。 VNode
** 就是鏡像樹中的**虛擬節點
,主要用於保存一些節點信息。
因此, Remax
在 HostConfig 配置的方法中,並無真正的操做 DOM 節點,而是先構成一顆鏡像樹
(Mirror Tree), 而後再同步到渲染進程
中,以下圖綠色的方框所示的那樣,咱們會使用 React 構成一個鏡像樹的 Vnode Tree,而後交給小程序平臺把這個樹給渲染出來。
提交階段也就是 commit 階段,react 會把 effect list 中存在的變動同步到渲染環境的 DOM 節點上去,會分別調用 appendChild
、removeChild
、insertBefore
這些方法
const HostConfig = {
// 用於初始化(首次)時添加子節點
appendInitialChild: (parent: VNode, child: VNode) => {
parent.appendChild(child, false);
},
// 添加子節點
appendChild(parent: VNode, child: VNode) {
parent.appendChild(child, false);
},
// 插入子節點
insertBefore(parent: VNode, child: VNode, beforeChild: VNode) {
parent.insertBefore(child, beforeChild, false);
},
// 刪除節點
removeChild(parent: VNode, child: VNode) {
parent.removeChild(child, false);
},
// 添加節點到容器節點,通常狀況咱們不須要和appendChild特殊區分
appendChildToContainer(container: any, child: VNode) {
container.appendChild(child);
child.mounted = true;
},
// 插入節點到容器節點
insertInContainerBefore(container: any, child: VNode, beforeChild: VNode) {
container.insertBefore(child, beforeChild);
},
// 從容器節點移除節點
removeChildFromContainer(container: any, child: VNode) {
container.removeChild(child);
},
}
複製代碼
下面咱們看,Remax 源碼裏面到底是如何實現這些方法的。
若是是原生的瀏覽器環境中,appendChild 比較簡單,直接調用 javascript 原生操做 DOM 的方法便可。若是是小程序的環境中,你得本身實現 hostConfig 中定義的 VNode 節點上的 appendChild 的方法,源碼實現以下:
VNode.prototype.appendChild = function (node) {
// 把 node 掛載到 child 鏈表上
// firstChild指針指向鏈表的開頭
// lastChild 指針指向鏈表的結尾
if (!this.firstChild) {
this.firstChild = node;
}
if (this.lastChild) {
this.lastChild.nextSibling = node;
node.previousSibling = this.lastChild;
}
this.lastChild = node;
// 若是節點已經掛載了,則調用 requestUpdate 方法,傳入一些參數
if (this.isMounted()) {
this.container.requestUpdate({
type: 'splice',
path: this.path,
start: node.index,
id: node.id,
deleteCount: 0,
children: this.children,
items: [node.toJSON()],
node: this
});
}
};
複製代碼
上面代碼中,並無直接操做小程序的 DOM ,而是操做存內存中的 VNode 組成的鏡像樹:
requestUpdate
這個方法,下面會有詳細的講到。removeChild
方法和上面是同一個套路,先是修改了 VNode 鏡像樹上的節點關係,而後調用了 requestUpdate
這個方法
insertBefore
方法和上面是同一個套路,先是修改了 VNode 鏡像樹上的節點關係,而後調用了 requestUpdate
這個方法
上面介紹的這些方法,都是對節點位置關係的更新,好比說子節點位置的移動啊之類的。
現實中確定也會有一些更新是不涉及到節點移動,而是好比說,節點上的屬性發生了變化、節點的文本發生了變化,Reconciler 就會在協調階段調用下面的這些方法。
commitUpdate: function (node, updatePayload, type, oldProps, newProps) {
// 處理一下 props
node.props = processProps(newProps, node, node.id);
node.update(updatePayload);
},
複製代碼
上面調用了 node.update 方法,定義以下
VNode.prototype.update = function (payload) {
if (this.type === 'text' || !payload) {
this.container.requestUpdate({
type: 'splice',
// root 不會更新,因此確定有 parent
path: this.parent.path,
start: this.index,
id: this.id,
deleteCount: 1,
items: [this.toJSON()],
node: this,
});
return;
}
for (var i = 0; i < payload.length; i = i + 2) {
var _a = __read(toRawProps(payload[i], payload[i + 1], this.type), 2), propName = _a[0], propValue = _a[1];
var path = __spread(this.parent.path, ['nodes', this.id.toString(), 'props']);
if (RuntimeOptions.get('platform') === 'ali') {
path = __spread(this.parent.path, ["children[" + this.index + "].props"]);
}
this.container.requestUpdate({
type: 'set',
path: path,
name: propName,
value: propValue,
node: this,
});
}
};
複製代碼
真神奇鴨,最後仍是調用了 requestUpdate
方法,異曲同工的感受。
上面的方法中,最後都調用了神奇的 requestUpdate
方法,咱們看一下這個方法裏面作了什麼
requestUpdate 方法定義以下:
沒想到吧, 這個requestUpdate方法那麼簡單。
接受一個對象做爲參數
而後把接收的參數 update 推入到 this.updateQueue
這個數組裏面,暫存起來,以後會在【提交完成階段】派上大用場。
在這個階段以前,Remax 構成的 VNode鏡像樹的這個JSON 數據仍是在 Remax 世界中被管理和維護,接下來,咱們會看如何更新 小程序的世界中。
React 會在提交完成階段執行 hostConfig
中定義的 resetAfterCommit
方法,這個方法本來是用React 想來作一些善後的工做。可是Remax 在這個resetAfterCommit
方法作了一個及其重要的工做,那就是同步鏡像樹到小程序**** data。
接下來咱們來看 resetAfterCommit
方法的源碼
resetAfterCommit: function resetAfterCommit(container) {
container.applyUpdate();
},
複製代碼
AppContainer.prototype.applyUpdate = function () {
this.context._pages.forEach(function (page) {
page.container.applyUpdate();
});
};
複製代碼
Container.prototype.applyUpdate = function () {
// 省略了 其餘的邏輯
var updatePayload = this.updateQueue.reduce(function (acc, update) {
// 經過以前緩存的updateQueue 計算出來 updatePayload
return acc;
}, {});
// 小程序的setData 終於出現了!!!! 把 updatePayload 同步到小程序的邏輯層
this.context.setData(updatePayload, function () {
nativeEffector.run();
});
this.updateQueue = [];
};
複製代碼
上面代碼的意思是, **經過以前緩存的updateQueue 計算出來 updatePayload
, **updatePayload
是一個什麼東東呢?咱們能夠經過 debug 斷點來一覽它的風采。
在某一次更新以後的斷點:
updatePayload 是一個 javascript 的對象,對象的 key 是數據在小程序世界中的路徑,對象的 value 就是要更新的值。
小程序的 setData 是支持這樣的寫法: setData({ root.a.b.c: 10 }), key 能夠表達層次關係
在第一次 mount 時的斷點:
咱們能夠在開發者工具中看到小程序實例上的數據,大概長下面這個樣子。
{
"root": {
"children": [
7
],
"nodes": {
"7": {
"id": 7,
"type": "view",
"props": {
"class": "app___2lhPP",
"hover-class": "none",
"hover-stop-propagation": false,
"hover-start-time": 50,
"hover-stay-time": 400
},
"children": [
4,
6
],
"nodes": {
"4": {
"id": 4,
"type": "button",
"props": {
"bindtap": "$$REMAX_METHOD_4_onClick",
"hover-class": "button-hover",
"hover-start-time": 20,
"hover-stay-time": 70
},
"children": [
3
],
"nodes": {
"3": {
"id": 3,
"type": "plain-text",
"text": " click me"
}
}
},
"6": {
"id": 6,
"type": "view",
"props": {
"hover-class": "none",
"hover-stop-propagation": false,
"hover-start-time": 50,
"hover-stay-time": 400
},
"children": [
5
],
"nodes": {
"5": {
"id": 5,
"type": "plain-text",
"text": ""
}
}
}
}
}
}
},
"modalRoot": {
"children": []
},
"__webviewId__": 31
}
複製代碼
在第一次 mount
時,Remax
** 運行時**初始化時會經過小程序的 setData
初始化小程序的 JSON 樹狀數據。
而後,Remax
** 運行時在數據發生更新時,就會經過小程序的 setData
去更新**上面小程序的 JSON 樹狀數據。
那麼,剩下最後一個問題,如今咱們知道了,小程序實例上有了一個 JSON 的樹狀對象,如何渲染成小程序的頁面呢?
若是在瀏覽器環境下,這個問題很是簡單,JavaScript
能夠直接建立 DOM
節點,只要咱們實現使用遞歸,即可完成從 VNode
到 DOM
的還原,渲染代碼以下:
function render(vnode) {
if (typeof vnode === 'string') {
return document.createTextNode(vnode)
}
const props = Object.entries(vnode.props)
const element = document.createElement(vnode.type)
for (const [key, value] of props) {
element.setAttribute(key, value)
}
vnode.children.forEach((node) => {
element.appendChild(render(node))
})
return element
}
複製代碼
但在小程序環境中,不支持直接建立 DOM ,僅支持模板渲染,該如何處理?
上文中,咱們講到類 Vue 的小程序框架的模板是從 Vue 的 template 部分轉成的;
類 React 的運行時小程序框架,jsx 很難轉成模板,只有一個 Vnode 節點組成的鏡像樹。
若是咱們去看 Remax
打包以後的模板代碼,也會發現空空如也,只有三行代碼,第一行引用了一個 **base.wxml **文件,第二行是一個叫 REMAX_TPL 的模板
<template is="REMAX_TPL" data={{root: root}}> </template>
複製代碼
第二行代碼表示使用 REMAX_TPL
模板,傳入的數據是 root
, root
是小程序實例上維護的數據,就是上面咱們提到的小程序的 JSON 樹狀數據,每個節點上保存了一些信息,以下所示:
{
"root": {
"children": [
7
],
"nodes": {
"7": {
"id": 7,
"type": "view",
"props": {
"class": "app___2lhPP",
"hover-class": "none",
"hover-stop-propagation": false,
"hover-start-time": 50,
"hover-stay-time": 400
},
"children": [
4,
6
],
"nodes": {
"4": {
"id": 4,
"type": "button",
"props": {
"bindtap": "$$REMAX_METHOD_4_onClick",
"hover-class": "button-hover",
"hover-start-time": 20,
"hover-stay-time": 70
},
"children": [
3
],
"nodes": {
"3": {
"id": 3,
"type": "plain-text",
"text": " click me"
}
}
},
"6": {
"id": 6,
"type": "view",
"props": {
"hover-class": "none",
"hover-stop-propagation": false,
"hover-start-time": 50,
"hover-stay-time": 400
},
"children": [
5
],
"nodes": {
"5": {
"id": 5,
"type": "plain-text",
"text": "sss"
}
}
}
}
}
}
},
"modalRoot": {
"children": []
},
"__webviewId__": 2
}
複製代碼
咱們來看 base.wxml 裏面是什麼內容,發現 base.wxml 內容超級多,有3000多行。以下圖:
這個 base.wxml
文件是固定的,每一次打包都會生成那麼代碼,代碼中定義了好幾種的小程序的 template
類型,而後重複定義了好幾遍,只是 name 名字的值不一樣。這是爲了兼容某一些小程序平臺不容許 <template>
組件本身嵌套本身,用來模擬遞歸嵌套的。
咱們回到剛纔的那一行代碼,有一個名字是 REMAX_TPL
的模板組件。
<template is="REMAX_TPL" data={{root: root}}> </template>
複製代碼
REMAX_TPL
的模板組件定義在base.wxml
裏面,以下所示:
<template name="REMAX_TPL">
<block wx:for="{{root.children}}" wx:key="*this">
<template is="REMAX_TPL_1_CONTAINER" data="{{i: root.nodes[item], a: ''}}"/>
</block>
</template>
<template name="REMAX_TPL_1_CONTAINER" data="{{i: i}}">
<template is="{{_h.tid(i.type, a)}}" data="{{i: i, a: a + ',' + i.type, tid: 1}}"/>
</template>
複製代碼
上面代碼,首先遍歷了 root 數據中的 children 數組,遍歷到每一項的話,用名字是 REMAX_TPL_1_CONTAINER
的模板組件繼續渲染數據中的 root.
[item] 屬性
REMAX_TPL_1_CONTAINER
的模板組件的定義,實際上是用當前數據的節點的類型——也就是調用 _h.tid(i.type, a)
方法來算出節點類型,多是 text, button ——找到節點類型對應的 template 模板,再次遞歸的遍歷下去。
_h.tid
的方法定義以下,其實就是拼接了兩個值: 1. 遞歸的深度deep的值,2.** 節點的 type**
tid = function (type, ancestor) {
var items = ancestor.split(',');
var depth = 1;
for (var i = 0; i< items.length; i++) {
if (type === items[i]) {
depth = depth + 1;
}
}
var id = 'REMAX_TPL_' + depth + '_' + type;
return id;
}
複製代碼
能夠看到,Remax 會根據每一個子元素的類型選擇對應的模板來渲染子元素,而後在每一個模板中又會去遍歷當前元素的子元素,以此把整個節點樹遞歸遍歷出來。
總結一下:
在第一次 mount
時,Remax
運行時初始化時會經過小程序的 setData
初始化小程序的 JSON 樹狀數據, 在小程序加載完畢後, Remax 經過遞歸模板的形式,把JSON 樹狀數據渲染爲小程序的頁面,用戶就能夠看到頁面啦。
而後,Remax
運行時在數據發生更新時,就會經過小程序的 setData
去更新上面小程序的 JSON 樹狀數據, JSON 樹狀數據被更新了,小程序天然會觸發更新數據對應的那塊視圖的渲染。
Remax 創造性的用遞歸模板的方式,用相對靜態的小程序模板語言實現了動態的模板渲染的特性。
看到這裏,咱們已經對 remax 這種類 react 的跨端框架總體流程有了大概的瞭解
Taro Next 的原理和 Remax 是很像的,這裏我就偷懶一下,直接把 Taro 團隊在 GMTC大會上的 ppt 貼過來了,高清版本的 ppt 能夠點擊這個連接下載:程帥-小程序跨框架開發的探索與實踐-GMTC 終稿.pdf
下面發現和 remax 是很像的。
Taro 團隊實現了 taro-react 包,用來鏈接 react-reconciler
和 taro-runtime
的 BOM/DOM API
Taro-react 就作了兩件事情:
實現 hostConfig
配置,咱們上面已經介紹過了
實現 render 函數(相似於 ReactDOM.render
)方法,咱們上面也已經介紹過了
在更新的過程當中,一樣是在 appendChild、 insertBefore、removeChild 這些方法裏面調用了 enqueueUpdate
方法(人家 remax 叫updateQueue)
渲染的話,和 Remax 的作法同樣,基於組件的 template 動態 「遞歸」 渲染整棵樹。
具體流程爲先去遍歷 Taro DOM Tree
( 對應 Remax 中叫鏡像樹 )根節點的子元素,再根據每一個子元素的類型選擇對應的模板來渲染子元素,而後在每一個模板中又會去遍歷當前元素的子元素,以此把整個節點樹遞歸遍歷出來。
基本上和 remax 同樣,換湯不換藥。
本身寫個React渲染器: 以 Remax 爲例(用React寫小程序)
「2019 JSConf.Asia - 尤雨溪」在框架設計中尋求平衡
Beginners guide to Custom React Renderers
Taro Next 架構揭祕 | GMTC《小程序跨框架開發的探索與實踐》萬字無刪減
Sophie Alpert 在 React Conf 上分享的《Building a Custom React Renderer》