聊聊跨端技術(shù)的本質(zhì)與現(xiàn)狀
來(lái)自團(tuán)隊(duì) 匡凌熙 同學(xué)的分享
零、何為跨端
write once, run everywhere
一次編寫(xiě),四處運(yùn)行就是跨端的真諦。因?yàn)榍岸水?dāng)下需要處理的場(chǎng)景實(shí)在是太多了:android、ios、pc、小程序,甚至智能手表、車載電視等,當(dāng)某幾個(gè)場(chǎng)景非常相似的時(shí)候,我們希望能夠用最少的開(kāi)發(fā)成本來(lái)達(dá)到最好的效果,而不是每個(gè)端都需要一套單獨(dú)的人力來(lái)進(jìn)行維護(hù),所以跨端技術(shù)就誕生了。
那么在跨端方案百花齊放的今天,比如現(xiàn)在最為人們所熟知的react native、flutter、electron等,他們之間有沒(méi)有什么共同的特點(diǎn),而我們又是否能夠找到其中的本質(zhì),就是今天這篇文章想講述的問(wèn)題。
一、主流跨端實(shí)現(xiàn)方案
1.1 h5 hybrid 方案
其實(shí),瀏覽器本就是一個(gè)跨端實(shí)現(xiàn)方案,因?yàn)槟阒恍枰斎刖W(wǎng)址,就能在任何端的瀏覽器上打開(kāi)你的網(wǎng)頁(yè)。那么,如果我們把瀏覽器嵌入 app 中,再將地址欄等內(nèi)容隱藏掉,是不是就能將我們的網(wǎng)頁(yè)嵌入原生 app 了。而這個(gè)嵌入 app 的瀏覽器,我們把它稱之為 webview ,所以只要某個(gè)端支持 webview ,那么它就能使用這種方案跨端。
同時(shí)這也是開(kāi)發(fā)成本最小的一種方案,因?yàn)檫@實(shí)際上就是在寫(xiě)前端界面,和我們開(kāi)發(fā)普通的網(wǎng)頁(yè)并沒(méi)有太大區(qū)別。
1.2 框架層+原生渲染
典型的代表是 react-native,它的開(kāi)發(fā)語(yǔ)言選擇了 js,使用的語(yǔ)法和 react 完全一致,其實(shí)也可以說(shuō)它就是 react,這就是我們的框架層。而不同于一般 react 應(yīng)用,它需要借助原生的能力來(lái)進(jìn)行渲染,組件最終都會(huì)被渲染為原生組件,這可以給用戶帶來(lái)比較好的體驗(yàn)。
1.3 框架層+自渲染引擎
這種方案和上面的區(qū)別就是,它并沒(méi)有直接借用原生能力去渲染組件,而是利用了更底層的渲染能力,自己去渲染組件。這種方式顯然鏈路會(huì)比上述方案的鏈路跟短,那么性能也就會(huì)更好,同時(shí)在保證多端渲染一致性上也會(huì)比上一種方案更加可靠。這類框架的典型例子就是 flutter 。
1.4 另類跨端
眾所周知,在最近幾年有一個(gè)東西變得非常火爆:小程序,現(xiàn)在許多大廠都有一套自己的小程序?qū)崿F(xiàn),但相互之間還是有不小差異的,通常可以借助 taro ,remax 這類框架實(shí)現(xiàn)一套代碼,多端運(yùn)行的效果,這也算是一種另類的跨端,它的實(shí)現(xiàn)方式還是比較有意思的,我們后面會(huì)展開(kāi)細(xì)講。
二、react-native 實(shí)現(xiàn)

2.1 rn的三個(gè)線程
rn 包含三個(gè)線程:
native thread:主要負(fù)責(zé)原生渲染和調(diào)用原生能力; js thread:JS 線程用于解釋和執(zhí)行我們的
js代碼。在大多數(shù)情況下,react native使用的js引擎是JSC(JavaScriptCore) ,在使用chrome調(diào)試時(shí),所有的js代碼都運(yùn)行在chrome中,并且通過(guò)websocket與原生代碼通信。此時(shí)的運(yùn)行環(huán)境是v8。shadow thread:要渲染到界面上一個(gè)很重要的步驟就是布局,我們需要知道每個(gè)組件應(yīng)該渲染到什么位置,這個(gè)過(guò)程就是通過(guò)
yoga去實(shí)現(xiàn)的,這是一個(gè)基于flexbox的跨平臺(tái)布局引擎。shadow thread會(huì)維護(hù)一個(gè)shadow tree來(lái)計(jì)算我們的各個(gè)組件在native頁(yè)面的實(shí)際布局,然后通過(guò)bridge通知native thread渲染ui。
2.2 初始化流程
native啟動(dòng)一個(gè)原生界面,比如android會(huì)起一個(gè)新的activity來(lái)承載rn,并做一些初始化的操作。加載 js引擎,運(yùn)行js代碼,此時(shí)的流程和react的啟動(dòng)流程就非常相似了,我們先簡(jiǎn)單觀察調(diào)用棧,

是不是看見(jiàn)了一些非常熟悉的函數(shù)名,在上一講的基本原理中已經(jīng)提到過(guò)了,這里我們就不再贅述。同時(shí)再看一下FiberNode的結(jié)構(gòu),也和react的保持一致,只不過(guò)我們?cè)?code style="overflow-wrap: break-word;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">js層是無(wú)法拿到真實(shí)結(jié)點(diǎn)的,所以stateNode只是一個(gè)代號(hào)。
js線程通知shadow thread。在react中,走到createInstance以后我們就可以直接調(diào)用createElement來(lái)創(chuàng)建真實(shí)結(jié)點(diǎn)了,但是在rn中我們沒(méi)辦法做到這一步,所以我們會(huì)通知native層讓它來(lái)幫助我們創(chuàng)建一個(gè)對(duì)應(yīng)的真實(shí)結(jié)點(diǎn)。

shadow thread計(jì)算布局,通知native Thread創(chuàng)建原生組件。native在界面上渲染原生組件,呈現(xiàn)給用戶。
2.3 更新流程
比如某個(gè)時(shí)候,用戶點(diǎn)擊了屏幕上的一個(gè)按鈕觸發(fā)了一個(gè)點(diǎn)擊事件,此時(shí)界面需要進(jìn)行相應(yīng)的更新操作。
native獲取到了點(diǎn)擊事件,傳給了js threadjs thread根據(jù)react代碼進(jìn)行相應(yīng)的處理,比如處理onClick函數(shù),觸發(fā)了setState。和 react的更新流程一樣,觸發(fā)了setState之后會(huì)進(jìn)行diff,找到需要更新的結(jié)點(diǎn)通知 shadow threadshadow thread計(jì)算布局之后通知native thread進(jìn)行真正的渲染。
2.4 特點(diǎn)
我們上述說(shuō)的通知,都是通過(guò) bridge 實(shí)現(xiàn)的,bridge本身是用實(shí)現(xiàn)C++的,就像一座橋一樣,將各個(gè)模塊關(guān)聯(lián)起來(lái),整個(gè)通信是一個(gè)「異步」的過(guò)程。這樣做好處就是各自之間不會(huì)有阻塞關(guān)系,比如 不會(huì)native thread因?yàn)?code style="overflow-wrap: break-word;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">js thread而阻塞渲染,給用戶良好的體驗(yàn)。但是這種「異步」也存在一個(gè)比較明顯的問(wèn)題:因?yàn)橥ㄐ胚^(guò)程花費(fèi)的時(shí)間比較長(zhǎng),所以在一些時(shí)效性要求較高場(chǎng)景上體驗(yàn)較差。
比如長(zhǎng)列表快速滾動(dòng)的時(shí)候或者需要做一些跟手的動(dòng)畫(huà),整個(gè)過(guò)程是這樣的:
native thread監(jiān)聽(tīng)到了滾動(dòng)事件,發(fā)送消息通知js threadjs thread處理滾動(dòng)事件,如果需要修改state需要經(jīng)過(guò)一層js diff,拿到最終需要更新的結(jié)點(diǎn)js thread通知shadow threadshadow thread通知native渲染
當(dāng)用戶操作過(guò)快的時(shí)候,就會(huì)導(dǎo)致界面來(lái)不及更新,進(jìn)而導(dǎo)致在快速滑動(dòng)的時(shí)候會(huì)出現(xiàn)白屏、卡頓的現(xiàn)象。
2.5 優(yōu)化
我們很容易看出,這是由rn的架構(gòu)引出的問(wèn)題,其實(shí)小程序的架構(gòu)也會(huì)有這個(gè)問(wèn)題,所以在rn和小程序上出現(xiàn)一些需要頻繁通信的場(chǎng)景時(shí),就會(huì)導(dǎo)致頁(yè)面非常差,流暢度降低。那么如果想解決這個(gè)問(wèn)題,勢(shì)必要從架構(gòu)上去進(jìn)行修改。
三、從rn看本質(zhì)
那么既然我們知道了rn是如何實(shí)現(xiàn)的跨端,那么我們就可以來(lái)探究一下它本質(zhì)上是在干什么。首先,跨端可以分為「邏輯跨端」和「渲染跨端」。
「邏輯跨端」通常通過(guò) vm來(lái)實(shí)現(xiàn),例如利用 v8 引擎,我們就能在各個(gè)平臺(tái)上運(yùn)行我們的 js 代碼,實(shí)現(xiàn)「邏輯跨端」。
那么第二個(gè)問(wèn)題就是「渲染跨端」,我們把業(yè)務(wù)代碼的實(shí)現(xiàn)抽象為開(kāi)發(fā)層,比如 react-native 中我們寫(xiě)的 react 代碼就屬于開(kāi)發(fā)層,再把具體要渲染的端稱為渲染層。作為開(kāi)發(fā)層來(lái)說(shuō),我一定知道我想要的ui長(zhǎng)什么樣,但是我沒(méi)有能力去渲染到界面上,所以當(dāng)我聲明了一個(gè)組件之后,我們需要考慮的問(wèn)題是如何把我想要什么告訴渲染層。

就像這樣的關(guān)系,那么我們最直觀的方式肯定是我能夠?qū)崿F(xiàn)一種通信方式,在開(kāi)發(fā)層將消息通知到各個(gè)系統(tǒng),再由各個(gè)系統(tǒng)自己去調(diào)用對(duì)應(yīng)的 api 來(lái)實(shí)現(xiàn)最終的渲染。
function?render()?{
????if(A)?{
????????message.sendA('render',?{?type:?'View'?})
????}
????
????
????if(B)?{
????????message.sendB('render',?{?type:?'View'?})
????}
????
????
????if(C)?{
????????message.sendC('render',?{?type:?'View'?})
????}
}
比如這樣,我就能通過(guò)判斷平臺(tái)來(lái)通知對(duì)應(yīng)的端去渲染View組件。這一部分的工作就是跨端框架需要幫助我們做的,它可以把這一步放到 JS 層,也可以把這一步放到c++ 層。我們應(yīng)該把這部分工作盡量往底層放,也就是我們可以對(duì)各個(gè)平臺(tái)的 api 進(jìn)行一層封裝,上層只負(fù)責(zé)調(diào)用封裝的 api,再由這一層封裝層去調(diào)用真正的 api。因?yàn)檫@樣可以復(fù)用更多的邏輯,否則像上文中我們?cè)?JS 層去發(fā)送消息給不同的平臺(tái),我們就需要在A\B\C三個(gè)平臺(tái)寫(xiě)三個(gè)不同方法去渲染組件。
但是,歸根結(jié)底就是,一定有一個(gè)地方是通過(guò)判斷不同平臺(tái)來(lái)調(diào)用具體實(shí)現(xiàn),也就是下面這樣

有一個(gè)地方會(huì)對(duì)系統(tǒng)進(jìn)行判斷,再通過(guò)某種通信方式通知到對(duì)應(yīng)的端,最后執(zhí)行真正的方法。其實(shí),所有跨端相關(guān)操作其實(shí)都在做上圖中的這些事情。所有的跨端也可以總結(jié)為下面這句話:
「我知道我想要什么,但是我沒(méi)有能力去渲染,我要通知有能力渲染的人來(lái)幫助我渲染」
比如hybrid跨端方案中,webview其實(shí)就充當(dāng)了橋接層的角色,createElement,appendChild等api就是給我們封裝好的跨平臺(tái)api,底層最終調(diào)用到了什么地方,又是如何渲染到界面上的細(xì)節(jié)都被屏蔽掉了。所以我們利用這些api就能很輕松的實(shí)現(xiàn)跨端開(kāi)發(fā),寫(xiě)一個(gè)網(wǎng)頁(yè),只要能夠加載 webview 的地方,我們的代碼就能跑在這個(gè)上面。
又比如flutter的方案通過(guò)研發(fā)一個(gè)自渲染的引擎來(lái)實(shí)現(xiàn)跨端,這種思路是不是相當(dāng)于另外一個(gè)瀏覽器?但是不同的點(diǎn)在于 flutter 是一個(gè)非常新的東西,而 webview 需要遵循大量的 w3c 規(guī)范和背負(fù)一堆歷史包袱。flutter 并沒(méi)有什么歷史包袱,所以它能夠從架構(gòu),設(shè)計(jì)的方面去做的更好更快,能夠做更多的事情。
四、跨端目前有什么問(wèn)題
4.1 一致性
對(duì)于跨端來(lái)說(shuō),如何屏蔽好各端的細(xì)節(jié)至關(guān)重要,比如針對(duì)某個(gè)端特有的api如何處理,如何保證渲染細(xì)節(jié)上各個(gè)端始終保持一致。如果一個(gè)跨端框架能夠讓開(kāi)發(fā)者的代碼里面不出現(xiàn) isIos、isAndroid的字眼,或者是為了兼容各種奇怪的渲染而產(chǎn)生的非常詭異的hack方式。那我認(rèn)為它絕對(duì)是一個(gè)真正成功的框架。
但是按我經(jīng)驗(yàn)而言,先后寫(xiě)過(guò)的 h5、rn、小程序,他們都沒(méi)有真正做到這一點(diǎn),所以項(xiàng)目里面會(huì)出現(xiàn)為了解決不同端不一致問(wèn)題而出現(xiàn)的各種奇奇怪怪的代碼。而這個(gè)問(wèn)題其實(shí)也是非常難解決的,因?yàn)楦鞫说牟町愡€是比較大的,所以說(shuō)很難去完全屏蔽這些細(xì)節(jié)。
比如說(shuō)h5中磨人的垂直居中問(wèn)題,我相信只要開(kāi)發(fā)過(guò)移動(dòng)端頁(yè)面的都會(huì)遇見(jiàn),就不用我多說(shuō)了。
4.2 為什么出現(xiàn)了這么多框架
為什么大家其實(shí)本質(zhì)上都是在干一件事情,卻出現(xiàn)了這么多的解決方案?其實(shí)大家都覺(jué)得某些框架沒(méi)能很好的解決某個(gè)問(wèn)題,所以想自己去造一套。其中可能很多開(kāi)發(fā)者最關(guān)心的就是性能問(wèn)題,比如:
rn因?yàn)榧軜?gòu)上的原因?qū)е履承﹫?chǎng)景性能差,所以它就想辦法從架構(gòu)上去進(jìn)行修改。flutter直接自己搞了一套渲染引擎,同時(shí)選用支持AOT的dart作為開(kāi)發(fā)語(yǔ)言。
但是其實(shí)我們?cè)谶x擇框架的時(shí)候性能并不是唯一因素,開(kāi)發(fā)體驗(yàn)、框架生態(tài)這些也都是關(guān)鍵因素,我個(gè)人感受是,目前rn的生態(tài)還是比其他的要好,所以在開(kāi)發(fā)過(guò)程中你想要的東西基本都有。
五、小程序跨端
ok,說(shuō)了這么多,對(duì)于跨端部分的內(nèi)容其實(shí)我想說(shuō)的已經(jīng)說(shuō)的差不多了,還記得上文提到的 Taro、Uni-app 一類跨小程序方案么。為什么說(shuō)它是另類的跨端,因?yàn)樗鋵?shí)并沒(méi)有實(shí)際跨端,只是為了解決各個(gè)小程序語(yǔ)法之間不兼容的問(wèn)題。但是它又確實(shí)是一個(gè)跨端解決方案,因?yàn)樗?「write once, run everything。」
下面我們先來(lái)了解下小程序的背景。
5.1 什么是小程序
小程序是各個(gè)app廠商對(duì)外開(kāi)放的一種能力。通過(guò)廠商提供的框架,就能在他們的app中運(yùn)行自己的小程序,借助各大app的流量來(lái)開(kāi)展自己的業(yè)務(wù)。同時(shí)作為廠商如果能吸引到更多的人加入到開(kāi)發(fā)者大軍中來(lái),也能給app帶來(lái)給多的流量,這可以看作一個(gè)雙贏的業(yè)務(wù)。那么最終呈現(xiàn)在app中的頁(yè)面是以什么方式進(jìn)行渲染的呢?其實(shí)還是通過(guò)webview,但是會(huì)嵌入一些原生的組件在里面以提供更好的用戶體驗(yàn),比如video組件其實(shí)并不是h5 video,而是native video。
5.2 什么是小程序跨端
那么到了這里,我們就可以來(lái)談一談關(guān)于小程序跨端的東西了。關(guān)于小程序跨端,核心并不是真正意義上的跨端,雖然小程序也做到了跨端,例如一份代碼其實(shí)是可以跑在android和Ios上的,但是實(shí)際上這和hybrid跨端十分相似。
在這里我想說(shuō)的其實(shí)是,市面上現(xiàn)在有非常多的小程序:字節(jié)小程序、百度小程序、微信小程序、支付寶小程序等等等等。雖然他們的dsl十分相似,但是終歸還是有所不同,那么就意味著如果我想在多個(gè)app上去開(kāi)展我的業(yè)務(wù),我是否需要維護(hù)多套十分相似的代碼?我又能否通過(guò)一套代碼能夠跑在各種小程序上?
5.3 怎么做
想通過(guò)一套代碼跑在多個(gè)小程序上,和想通過(guò)一套代碼跑在多個(gè)端,這兩件事到底是不是一件事呢?我們?cè)倩氐竭@張圖

這些平臺(tái)是否可以對(duì)應(yīng)上不同的小程序?
再回到那句話:「我知道我想要什么,但是我沒(méi)有能力去渲染,我要通知有能力渲染的人來(lái)幫助我渲染。」
現(xiàn)在來(lái)理一下我們的需求:
小程序的語(yǔ)法不好用,我希望用 react 開(kāi)發(fā); 我希望盡可能低的成本讓小程序跑在多個(gè)平臺(tái)上。
那么從這句話來(lái)看:「我」代表了什么,「有能力渲染的人」又代表了什么?
第二個(gè)很容易對(duì)應(yīng)上,「有能力渲染的人」就是小程序本身,只有它才能幫助我們把內(nèi)容真正渲染到界面上。
而「我」又是什么呢?其實(shí)這個(gè)「我」可以是很多東西,不過(guò)這里我們的需求是想用react進(jìn)行開(kāi)發(fā),所以我們回想一下第一講中react的核心流程,當(dāng)它拿到vdom的時(shí)候,是不是就已經(jīng)知道【我想要什么】了?所以我們把react拿到vdom之前的流程搬過(guò)來(lái),這樣就能獲取到「我知道我想要什么」的信息,但是「我沒(méi)有能力去渲染」,因?yàn)檫@不是web,沒(méi)有dom api,所以我需要通知小程序來(lái)幫助我渲染,我還可以根據(jù)不同的端來(lái)通知不同的小程序幫助我渲染。
所以整個(gè)流程就是下面這樣的:

前面三個(gè)流程都在我們的js層,也就是開(kāi)發(fā)層,我們寫(xiě)的代碼經(jīng)歷一遍完整的 react 流程之后,會(huì)將最后的結(jié)果給到各個(gè)小程序,然后再走小程序自己的內(nèi)部流程,將其真正的渲染到界面上。
采用這種做法的典型例子有remax、taro3,他們宣稱用真正的react去開(kāi)發(fā)小程序,其實(shí)并沒(méi)有錯(cuò),因?yàn)檎娴氖前?code style="overflow-wrap: break-word;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">react的整套東西都搬了過(guò)來(lái),和react并無(wú)差異。我們用taro寫(xiě)一個(gè)非常簡(jiǎn)單的例子來(lái)看一下:
import?{?Component?}?from?'react'
import?{?View,?Text,?Button?}?from?'@tarojs/components'
import?'./index.css'
export?default?class?Index?extends?Component?{
??state?=?{
????random:?Math.random()
??}
??componentWillMount?()?{?}
??componentDidMount?()?{?}
??componentWillUnmount?()?{?}
??componentDidShow?()?{?}
??componentDidHide?()?{?}
??handleClick?=?()?=>?{
????debugger;
????console.log("Math.random()",?Math.random());
????this.setState({random:?Math.random()})
??}
??render?()?{
????return?(
??????<View?className='index'>
????????<Text>Hello?world!?{this.state.random}Text>
????????<Button?onClick={this.handleClick}>clickButton>
??????View>
????)
??}
}
這是一個(gè)用taro寫(xiě)的組件,把它編譯到字節(jié)小程序之后是這樣的效果:

根據(jù)我們之前的分析,在最后生成的文件中,一定包含了一個(gè)「小程序渲染器」。它接受的data就是整個(gè)ui結(jié)構(gòu),然后通過(guò)小程序的渲染能力渲染到界面上,我們?nèi)?code style="overflow-wrap: break-word;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">dist文件中找一下,就能找到一個(gè)base.ttml的文件,里面的內(nèi)容是這樣的
<template?name="taro_tmpl">
??<block?tt:for="{{root.cn}}"?tt:key="uid">
????<template?is="tmpl_0_container"?data="{{i:item}}"?/>
??block>
template>
<template?name="tmpl_0_catch-view">
??<view?hover-class="{{i.hoverClass===undefined?'none':i.hoverClass}}"?hover-stop-propagation="{{i.hoverStopPropagation===undefined?false:i.hoverStopPropagation}}"?hover-start-time="{{i.hoverStartTime===undefined?50:i.hoverStartTime}}"?hover-stay-time="{{i.hoverStayTime===undefined?400:i.hoverStayTime}}"?animation="{{i.animation}}"?bindtouchstart="eh"?bindtouchend="eh"?bindtouchcancel="eh"?bindlongpress="eh"?bindanimationstart="eh"?bindanimationiteration="eh"?bindanimationend="eh"?bindtransitionend="eh"?style="{{i.st}}"?class="{{i.cl}}"?bindtap="eh"?catchtouchmove="eh"??id="{{i.uid}}">
????<block?tt:for="{{i.cn}}"?tt:key="uid">
??????<template?is="tmpl_0_container"?data="{{i:item}}"?/>
????block>
??view>
template>
<template?name="tmpl_0_static-view">
??<view?hover-class="{{i.hoverClass===undefined?'none':i.hoverClass}}"?hover-stop-propagation="{{i.hoverStopPropagation===undefined?false:i.hoverStopPropagation}}"?hover-start-time="{{i.hoverStartTime===undefined?50:i.hoverStartTime}}"?hover-stay-time="{{i.hoverStayTime===undefined?400:i.hoverStayTime}}"?animation="{{i.animation}}"?style="{{i.st}}"?class="{{i.cl}}"??id="{{i.uid}}">
????<block?tt:for="{{i.cn}}"?tt:key="uid">
??????<template?is="tmpl_0_container"?data="{{i:item}}"?/>
????block>
??view>
template>
<template?name="tmpl_0_pure-view">
??<view?style="{{i.st}}"?class="{{i.cl}}"??id="{{i.uid}}">
????<block?tt:for="{{i.cn}}"?tt:key="uid">
??????<template?is="tmpl_0_container"?data="{{i:item}}"?/>
????block>
??view>
template>
<template?name="tmpl_0_view">
??<view?hover-class="{{i.hoverClass===undefined?'none':i.hoverClass}}"?hover-stop-propagation="{{i.hoverStopPropagation===undefined?false:i.hoverStopPropagation}}"?hover-start-time="{{i.hoverStartTime===undefined?50:i.hoverStartTime}}"?hover-stay-time="{{i.hoverStayTime===undefined?400:i.hoverStayTime}}"?animation="{{i.animation}}"?bindtouchstart="eh"?bindtouchmove="eh"?bindtouchend="eh"?bindtouchcancel="eh"?bindlongpress="eh"?bindanimationstart="eh"?bindanimationiteration="eh"?bindanimationend="eh"?bindtransitionend="eh"?style="{{i.st}}"?class="{{i.cl}}"?bindtap="eh"??id="{{i.uid}}">
????<block?tt:for="{{i.cn}}"?tt:key="uid">
??????<template?is="tmpl_0_container"?data="{{i:item}}"?/>
????block>
??view>
template>
<template?name="tmpl_0_static-text">
??<text?selectable="{{i.selectable===undefined?false:i.selectable}}"?space="{{i.space}}"?decode="{{i.decode===undefined?false:i.decode}}"?style="{{i.st}}"?class="{{i.cl}}"??id="{{i.uid}}">
????<block?tt:for="{{i.cn}}"?tt:key="uid">
??????<template?is="tmpl_0_container"?data="{{i:item}}"?/>
????block>
??text>
template>
<template?name="tmpl_0_text">
??<text?selectable="{{i.selectable===undefined?false:i.selectable}}"?space="{{i.space}}"?decode="{{i.decode===undefined?false:i.decode}}"?style="{{i.st}}"?class="{{i.cl}}"?bindtap="eh"??id="{{i.uid}}">
????<block?tt:for="{{i.cn}}"?tt:key="uid">
??????<template?is="tmpl_0_container"?data="{{i:item}}"?/>
????block>
??text>
template>
<template?name="tmpl_0_button">
??<button?size="{{i.size===undefined?'default':i.size}}"?type="{{i.type}}"?plain="{{i.plain===undefined?false:i.plain}}"?disabled="{{i.disabled}}"?loading="{{i.loading===undefined?false:i.loading}}"?form-type="{{i.formType}}"?open-type="{{i.openType}}"?hover-class="{{i.hoverClass===undefined?'button-hover':i.hoverClass}}"?hover-stop-propagation="{{i.hoverStopPropagation===undefined?false:i.hoverStopPropagation}}"?hover-start-time="{{i.hoverStartTime===undefined?20:i.hoverStartTime}}"?hover-stay-time="{{i.hoverStayTime===undefined?70:i.hoverStayTime}}"?name="{{i.name}}"?bindtouchstart="eh"?bindtouchmove="eh"?bindtouchend="eh"?bindtouchcancel="eh"?bindlongpress="eh"?bindgetphonenumber="eh"?data-channel="{{i.dataChannel}}"?style="{{i.st}}"?class="{{i.cl}}"?bindtap="eh"??id="{{i.uid}}">
????<block?tt:for="{{i.cn}}"?tt:key="uid">
??????<template?is="tmpl_0_container"?data="{{i:item}}"?/>
????block>
??button>
template>
<template?name="tmpl_0_scroll-view">
??<scroll-view?scroll-x="{{i.scrollX===undefined?false:i.scrollX}}"?scroll-y="{{i.scrollY===undefined?false:i.scrollY}}"?upper-threshold="{{i.upperThreshold===undefined?50:i.upperThreshold}}"?lower-threshold="{{i.lowerThreshold===undefined?50:i.lowerThreshold}}"?scroll-top="{{i.scrollTop}}"?scroll-left="{{i.scrollLeft}}"?scroll-into-view="{{i.scrollIntoView}}"?scroll-with-animation="{{i.scrollWithAnimation===undefined?false:i.scrollWithAnimation}}"?enable-back-to-top="{{i.enableBackToTop===undefined?false:i.enableBackToTop}}"?bindscrolltoupper="eh"?bindscrolltolower="eh"?bindscroll="eh"?bindtouchstart="eh"?bindtouchmove="eh"?bindtouchend="eh"?bindtouchcancel="eh"?bindlongpress="eh"?bindanimationstart="eh"?bindanimationiteration="eh"?bindanimationend="eh"?bindtransitionend="eh"?style="{{i.st}}"?class="{{i.cl}}"?bindtap="eh"??id="{{i.uid}}">
????<block?tt:for="{{i.cn}}"?tt:key="uid">
??????<template?is="tmpl_0_container"?data="{{i:item}}"?/>
????block>
??scroll-view>
template>
<template?name="tmpl_0_static-image">
??<image?src="{{i.src}}"?mode="{{i.mode===undefined?'scaleToFill':i.mode}}"?lazy-load="{{i.lazyLoad===undefined?false:i.lazyLoad}}"?style="{{i.st}}"?class="{{i.cl}}"??id="{{i.uid}}">
????<block?tt:for="{{i.cn}}"?tt:key="uid">
??????<template?is="tmpl_0_container"?data="{{i:item}}"?/>
????block>
??image>
template>
<template?name="tmpl_0_image">
??<image?src="{{i.src}}"?mode="{{i.mode===undefined?'scaleToFill':i.mode}}"?lazy-load="{{i.lazyLoad===undefined?false:i.lazyLoad}}"?binderror="eh"?bindload="eh"?bindtouchstart="eh"?bindtouchmove="eh"?bindtouchend="eh"?bindtouchcancel="eh"?bindlongpress="eh"?style="{{i.st}}"?class="{{i.cl}}"?bindtap="eh"??id="{{i.uid}}">
????<block?tt:for="{{i.cn}}"?tt:key="uid">
??????<template?is="tmpl_0_container"?data="{{i:item}}"?/>
????block>
??image>
template>
<template?name="tmpl_0_#text"?data="{{i:i}}">
??<block>{{i.v}}block>
template>
<template?name="tmpl_0_container">
??<template?is="{{'tmpl_0_'?+?i.nn}}"?data="{{i:i}}"?/>
template>
從名字可以看出,這是用于渲染各種組件的template,所以當(dāng)我們拿到react傳遞過(guò)來(lái)的data時(shí),將其傳給template,template就能根據(jù)對(duì)應(yīng)的組件名采用不同的模版進(jìn)行渲染。隨后再用一個(gè)for循環(huán)將其子組件進(jìn)行遞歸渲染,完成整個(gè)頁(yè)面的渲染。這個(gè)就可以理解為我們針對(duì)不同端寫(xiě)的不同渲染器,如果我們編譯到wx小程序,這里面的內(nèi)容是會(huì)不同的。
總之,「在」**react**「對(duì)其處理完之后,會(huì)把數(shù)據(jù)」**setData**「?jìng)鬟f給「「小程序」」,小程序再用之前寫(xiě)好的各種」**template**「將其渲染到頁(yè)面上。」
下面這張圖就是經(jīng)過(guò)react處理之后,能夠拿到頁(yè)面的數(shù)據(jù),將其傳遞給小程序之后,就能遞歸渲染出來(lái)。

那么這樣的架構(gòu)有什么問(wèn)題呢,可以很明顯的看到會(huì)走兩遍diff,為什么會(huì)走兩遍diff呢?因?yàn)樵?code style="overflow-wrap: break-word;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">react層為了獲取到我想要什么這個(gè)信息,我們必須走一遍diff,這樣才能將最后得到的data交給小程序。
而交給小程序之后,小程序?qū)τ谥暗牧鞒淌菬o(wú)感知的,所以它為了得到需要更新什么這個(gè)信息,也需要過(guò)一遍diff,或者通過(guò)一些其他的方式來(lái)拿到這個(gè)信息(并沒(méi)有深入了解過(guò)小程序的渲染流程,所以不確定是否是通過(guò)diff拿到的),所以這一整套流程就會(huì)走兩遍diff。
為什么我們不能將兩次diff合并為一次?因?yàn)樾〕绦虻匿秩緦?duì)開(kāi)發(fā)者而言就是個(gè)黑盒,我們不能干擾到其內(nèi)部流程。如果我們能夠直接對(duì)接小程序的渲染sdk,那么其實(shí)根本沒(méi)必要走兩遍diff,因?yàn)榍爸玫?react的diff我們已經(jīng)能夠知道需要更新什么內(nèi)容。
這個(gè)問(wèn)題的本質(zhì)和普通意義上的跨端框架沒(méi)有太大的區(qū)別,開(kāi)發(fā)層也就是 react 知道自己需要什么東西,但是它沒(méi)有能力去渲染到界面上,所以需要通過(guò)小程序充當(dāng)渲染層來(lái)渲染到真正的界面上。這種開(kāi)發(fā)方式有一種用 react 去寫(xiě) vue 的意思,但是為什么會(huì)出現(xiàn)這種詭異的開(kāi)發(fā)方式,如果這個(gè) vue 做的足夠好的話,誰(shuí)又想去這樣折騰?
5.4 組件的嵌套
其實(shí)還有一個(gè)小問(wèn)題,wx的template是無(wú)法支持遞歸調(diào)用的,也就導(dǎo)致了我們想用template遞歸渲染data內(nèi)容是無(wú)法實(shí)現(xiàn)的,那么這個(gè)問(wèn)題要如何解決呢..我們看一下上面的代碼在wx小程序中編譯出來(lái)的結(jié)果:

我們可以看到各種template之間多了0、1、2、3這種標(biāo)號(hào)..就是為了解決無(wú)法遞歸調(diào)用的問(wèn)題,提前多造幾個(gè)名字不同功能相同的template,不就能跨過(guò)遞歸調(diào)用的限制了么...
六、另一種粗暴的跨端
上述的這些跨端都是通過(guò)某種架構(gòu)方式去實(shí)現(xiàn)的,那如果我們粗暴一點(diǎn)的想,我能不能直接把一套代碼通過(guò)編譯的方式去編譯到不同的平臺(tái)。比如我把js代碼編譯成java代碼、object-c代碼,其實(shí),個(gè)人感覺(jué)也不是不行,但是因?yàn)檫@些的差異實(shí)在太大,所以在寫(xiě)js代碼的時(shí)候,可能需要非常強(qiáng)的約束性、規(guī)范性,把開(kāi)發(fā)者限制在某個(gè)區(qū)域內(nèi),才能很好的編譯過(guò)去。也就是說(shuō),從js到java其實(shí)是一個(gè)自由度高到自由度低的一個(gè)過(guò)程,肯定是無(wú)法完全一一對(duì)應(yīng)上的,并且由于開(kāi)發(fā)方式、語(yǔ)法完全不一樣,所以想通過(guò)編譯的方式將js編譯到ios和android上去還是比較難的,但是對(duì)于小程序來(lái)說(shuō),嘗試把jsx編譯到template似乎是一個(gè)可行的方案,實(shí)際上,taro1/2 都是這么干的。不過(guò)從jsx到template也是一個(gè)自由度從高到低的一個(gè)過(guò)程,所以是沒(méi)辦法絕對(duì)完美地將把所有語(yǔ)法都編譯到template...
這里可以給大家分享一個(gè)很有意思的例子,最近很火的 SolidJS 框架也支持用 JSX 寫(xiě)代碼,但是它完全沒(méi)有react這么重的runtime,因?yàn)樗?code style="overflow-wrap: break-word;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">JSX最終會(huì)被編譯成一些原生的操作...我們看一個(gè)簡(jiǎn)單的例子:https://playground.solidjs.com/
在 react 語(yǔ)境下,我們?cè)?code style="overflow-wrap: break-word;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">input框里面輸入內(nèi)容的時(shí)候,上面的文案應(yīng)該跟著改變,但是實(shí)際上并沒(méi)有。這是因?yàn)檫@個(gè)東西最后被編譯完之后是一些原生的操作,它其實(shí)只會(huì)運(yùn)行一遍,最后你觸發(fā)的各種click并不會(huì)導(dǎo)致函數(shù)重新運(yùn)行,而是直接通過(guò)原生操作操作到對(duì)應(yīng)的DOM上來(lái)修改視圖,也就導(dǎo)致了上面問(wèn)題的產(chǎn)生。
其實(shí)我覺(jué)得這樣挺反人類的,雖然是JSX的語(yǔ)法,但是卻缺少了最核心的東西:函數(shù)式的思維。(還不如寫(xiě)template)。
七、virtual dom
7.1 對(duì)于跨端的意義
提到跨端,可能很多人第一個(gè)想到的東西就是 virtual dom,因?yàn)樗菍?duì)于ui的抽象,脫離了平臺(tái),所以可能很多人會(huì)覺(jué)得virtual dom和跨平臺(tái)已經(jīng)是綁定在一起的東西了。但是其實(shí)個(gè)人感覺(jué)并不是。
首先我們回想一下,我們之前說(shuō)到的跨平臺(tái)的本質(zhì)是什么?開(kāi)發(fā)層知道自己想要什么,然后告訴渲染層自己想要什么,就這么簡(jiǎn)單。那對(duì)于react-native來(lái)說(shuō),是通過(guò)virtual dom來(lái)判斷自己需要更新什么結(jié)點(diǎn)的嗎?其實(shí)并不是,單靠一個(gè)virtual dom還不足以獲取到這個(gè)信息,必須還要加上diff,所以是virtual dom+diff獲取到了自己想要什么的信息,再通過(guò)通信的方式告訴native去更新真正的結(jié)點(diǎn)。
所以virtual dom在這個(gè)里面只扮演了一個(gè)獲取方法的角色,是通過(guò)virtual dom+diff這個(gè)方法拿到了我們想要的東西。換言之,我們也可以通過(guò)其他的方法來(lái)拿到我們想要什么。比如之前分享的san框架,這是一個(gè)沒(méi)有virtual dom的框架,但是它為什么能夠跨平臺(tái),我們先不管它內(nèi)部是如何實(shí)現(xiàn)的,但是在更新階段,如果它在某個(gè)時(shí)刻調(diào)用了 createElement,那么它一定是知道了:自己想要什么。對(duì)應(yīng)上跨端的內(nèi)容,這個(gè)時(shí)候就能通過(guò)某種手段去告訴native,渲染某個(gè)東西。
「所以,當(dāng)我們通過(guò)其他手段獲取到了:我們想要什么這個(gè)信息之后,就能通知」**native**「去渲染真正的內(nèi)容。」
7.2 virtual dom的優(yōu)勢(shì)
那么vdom的優(yōu)勢(shì)在于什么地方?我認(rèn)為主要是下面兩個(gè):
開(kāi)創(chuàng) jsx新時(shí)代,函數(shù)式編程思想強(qiáng)大的表達(dá)力。能夠使用 template獲取更多優(yōu)化信息,又能夠支持jsx
首先,jsx 簡(jiǎn)直開(kāi)創(chuàng)了一個(gè)新時(shí)代,讓我們能夠以函數(shù)式編程思想去寫(xiě)ui,之前誰(shuí)能想到一個(gè)切圖仔還能用這樣的方式去寫(xiě)ui。
其次,我們知道,vue雖然是使用的template作為dsl,但是實(shí)際上我們也是可以寫(xiě)jsx的,jsx所提供的靈活能力是template無(wú)法比擬的。而之所以能夠同時(shí)支持template和jsx其實(shí)就是因?yàn)?code style="overflow-wrap: break-word;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">vdom的存在,如果vue不引入vdom,是沒(méi)辦法說(shuō)去支持jsx的語(yǔ)法的,或者說(shuō),是沒(méi)辦法去支持真正的jsx。
八、結(jié)語(yǔ)
還是那句話,跨端就是:「我知道我想要什么,但是我沒(méi)有能力去渲染,我要通知有能力渲染的人來(lái)幫助我渲染。」
他們的本質(zhì)都非常簡(jiǎn)單,但是細(xì)節(jié)卻非常難處理,同時(shí)對(duì)于目前市面上的多種跨端框架,也需要大家根據(jù)自己的項(xiàng)目去權(quán)衡利弊選擇一個(gè)最有方案,畢竟目前沒(méi)有一個(gè)框架能完全吊打所有其他框架,適合自己的才是最好的。
