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

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

是不是看見了一些非常熟悉的函數(shù)名,在上一講的基本原理中已經(jīng)提到過了,這里我們就不再贅述。同時再看一下FiberNode的結構,也和react的保持一致,只不過我們在js層是無法拿到真實結點的,所以stateNode只是一個代號。
js線程通知shadow thread。在react中,走到createInstance以后我們就可以直接調(diào)用createElement來創(chuàng)建真實結點了,但是在rn中我們沒辦法做到這一步,所以我們會通知native層讓它來幫助我們創(chuàng)建一個對應的真實結點。

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

就像這樣的關系,那么我們最直觀的方式肯定是我能夠實現(xiàn)一種通信方式,在開發(fā)層將消息通知到各個系統(tǒng),再由各個系統(tǒng)自己去調(diào)用對應的 api 來實現(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'?})
????}
}
比如這樣,我就能通過判斷平臺來通知對應的端去渲染View組件。這一部分的工作就是跨端框架需要幫助我們做的,它可以把這一步放到 JS 層,也可以把這一步放到c++ 層。我們應該把這部分工作盡量往底層放,也就是我們可以對各個平臺的 api 進行一層封裝,上層只負責調(diào)用封裝的 api,再由這一層封裝層去調(diào)用真正的 api。因為這樣可以復用更多的邏輯,否則像上文中我們在 JS 層去發(fā)送消息給不同的平臺,我們就需要在A\B\C三個平臺寫三個不同方法去渲染組件。
但是,歸根結底就是,一定有一個地方是通過判斷不同平臺來調(diào)用具體實現(xiàn),也就是下面這樣

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

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

前面三個流程都在我們的js層,也就是開發(fā)層,我們寫的代碼經(jīng)歷一遍完整的 react 流程之后,會將最后的結果給到各個小程序,然后再走小程序自己的內(nèi)部流程,將其真正的渲染到界面上。
采用這種做法的典型例子有remax、taro3,他們宣稱用真正的react去開發(fā)小程序,其實并沒有錯,因為真的是把react的整套東西都搬了過來,和react并無差異。我們用taro寫一個非常簡單的例子來看一下:
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>
????)
??}
}
這是一個用taro寫的組件,把它編譯到字節(jié)小程序之后是這樣的效果:

根據(jù)我們之前的分析,在最后生成的文件中,一定包含了一個「小程序渲染器」。它接受的data就是整個ui結構,然后通過小程序的渲染能力渲染到界面上,我們?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文件中找一下,就能找到一個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,所以當我們拿到react傳遞過來的data時,將其傳給template,template就能根據(jù)對應的組件名采用不同的模版進行渲染。隨后再用一個for循環(huán)將其子組件進行遞歸渲染,完成整個頁面的渲染。這個就可以理解為我們針對不同端寫的不同渲染器,如果我們編譯到wx小程序,這里面的內(nèi)容是會不同的。
總之,「在」**react**「對其處理完之后,會把數(shù)據(jù)」**setData**「傳遞給「「小程序」」,小程序再用之前寫好的各種」**template**「將其渲染到頁面上?!?/strong>
下面這張圖就是經(jīng)過react處理之后,能夠拿到頁面的數(shù)據(jù),將其傳遞給小程序之后,就能遞歸渲染出來。

那么這樣的架構有什么問題呢,可以很明顯的看到會走兩遍diff,為什么會走兩遍diff呢?因為在react層為了獲取到我想要什么這個信息,我們必須走一遍diff,這樣才能將最后得到的data交給小程序。
而交給小程序之后,小程序對于之前的流程是無感知的,所以它為了得到需要更新什么這個信息,也需要過一遍diff,或者通過一些其他的方式來拿到這個信息(并沒有深入了解過小程序的渲染流程,所以不確定是否是通過diff拿到的),所以這一整套流程就會走兩遍diff。
為什么我們不能將兩次diff合并為一次?因為小程序的渲染對開發(fā)者而言就是個黑盒,我們不能干擾到其內(nèi)部流程。如果我們能夠直接對接小程序的渲染sdk,那么其實根本沒必要走兩遍diff,因為前置的 react的diff我們已經(jīng)能夠知道需要更新什么內(nèi)容。
這個問題的本質和普通意義上的跨端框架沒有太大的區(qū)別,開發(fā)層也就是 react 知道自己需要什么東西,但是它沒有能力去渲染到界面上,所以需要通過小程序充當渲染層來渲染到真正的界面上。這種開發(fā)方式有一種用 react 去寫 vue 的意思,但是為什么會出現(xiàn)這種詭異的開發(fā)方式,如果這個 vue 做的足夠好的話,誰又想去這樣折騰?
5.4 組件的嵌套
其實還有一個小問題,wx的template是無法支持遞歸調(diào)用的,也就導致了我們想用template遞歸渲染data內(nèi)容是無法實現(xiàn)的,那么這個問題要如何解決呢..我們看一下上面的代碼在wx小程序中編譯出來的結果:

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