聊聊跨端技術的本質與現狀
來自團隊 匡凌熙 同學的分享
零、何爲跨端
write once, run everywhere
一次編寫,四處運行就是跨端的真諦。因爲前端當下需要處理的場景實在是太多了:android、ios、pc、小程序,甚至智能手錶、車載電視等,當某幾個場景非常相似的時候,我們希望能夠用最少的開發成本來達到最好的效果,而不是每個端都需要一套單獨的人力來進行維護,所以跨端技術就誕生了。
那麼在跨端方案百花齊放的今天,比如現在最爲人們所熟知的react native、flutter、electron等,他們之間有沒有什麼共同的特點,而我們又是否能夠找到其中的本質,就是今天這篇文章想講述的問題。
一、主流跨端實現方案
1.1 h5 hybrid 方案
其實,瀏覽器本就是一個跨端實現方案,因爲你只需要輸入網址,就能在任何端的瀏覽器上打開你的網頁。那麼,如果我們把瀏覽器嵌入 app 中,再將地址欄等內容隱藏掉,是不是就能將我們的網頁嵌入原生 app 了。而這個嵌入 app 的瀏覽器,我們把它稱之爲 webview ,所以只要某個端支持 webview ,那麼它就能使用這種方案跨端。
同時這也是開發成本最小的一種方案,因爲這實際上就是在寫前端界面,和我們開發普通的網頁並沒有太大區別。
1.2 框架層 + 原生渲染
典型的代表是 react-native,它的開發語言選擇了 js,使用的語法和 react 完全一致,其實也可以說它就是 react,這就是我們的框架層。而不同於一般 react 應用,它需要藉助原生的能力來進行渲染,組件最終都會被渲染爲原生組件,這可以給用戶帶來比較好的體驗。
1.3 框架層 + 自渲染引擎
這種方案和上面的區別就是,它並沒有直接借用原生能力去渲染組件,而是利用了更底層的渲染能力,自己去渲染組件。這種方式顯然鏈路會比上述方案的鏈路跟短,那麼性能也就會更好,同時在保證多端渲染一致性上也會比上一種方案更加可靠。這類框架的典型例子就是 flutter 。
1.4 另類跨端
衆所周知,在最近幾年有一個東西變得非常火爆:小程序,現在許多大廠都有一套自己的小程序實現,但相互之間還是有不小差異的,通常可以藉助 taro ,remax 這類框架實現一套代碼,多端運行的效果,這也算是一種另類的跨端,它的實現方式還是比較有意思的,我們後面會展開細講。
二、react-native 實現
2.1 rn 的三個線程
rn 包含三個線程:
-
native thread:主要負責原生渲染和調用原生能力;
-
js thread:JS 線程用於解釋和執行我們的
js代碼。在大多數情況下,react native使用的js引擎是 JSC(JavaScriptCore) ,在使用chrome調試時,所有的js代碼都運行在chrome中,並且通過websocket與原生代碼通信。此時的運行環境是v8。 -
shadow thread:要渲染到界面上一個很重要的步驟就是佈局,我們需要知道每個組件應該渲染到什麼位置,這個過程就是通過
yoga去實現的,這是一個基於flexbox的跨平臺佈局引擎。shadow thread會維護一個shadow tree來計算我們的各個組件在native頁面的實際佈局,然後通過bridge通知native thread渲染ui。
2.2 初始化流程
-
native啓動一個原生界面,比如android會起一個新的activity來承載rn,並做一些初始化的操作。 -
加載
js引擎,運行js代碼,此時的流程和react的啓動流程就非常相似了,我們先簡單觀察調用棧,
是不是看見了一些非常熟悉的函數名,在上一講的基本原理中已經提到過了,這裏我們就不再贅述。同時再看一下FiberNode的結構,也和react的保持一致,只不過我們在js層是無法拿到真實結點的,所以stateNode只是一個代號。
js線程通知shadow thread。在react中,走到createInstance以後我們就可以直接調用createElement來創建真實結點了,但是在rn中我們沒辦法做到這一步,所以我們會通知native層讓它來幫助我們創建一個對應的真實結點。
-
shadow thread計算佈局,通知native Thread創建原生組件。 -
native在界面上渲染原生組件,呈現給用戶。
2.3 更新流程
比如某個時候,用戶點擊了屏幕上的一個按鈕觸發了一個點擊事件,此時界面需要進行相應的更新操作。
-
native獲取到了點擊事件,傳給了js thread -
js thread根據react代碼進行相應的處理,比如處理onClick函數,觸發了setState。 -
和
react的更新流程一樣,觸發了setState之後會進行diff,找到需要更新的結點 -
通知
shadow thread -
shadow thread計算佈局之後通知native thread進行真正的渲染。
2.4 特點
我們上述說的通知,都是通過 bridge 實現的,bridge本身是用實現C++的,就像一座橋一樣,將各個模塊關聯起來,整個通信是一個**「異步」**的過程。這樣做好處就是各自之間不會有阻塞關係,比如 不會native thread因爲js thread而阻塞渲染,給用戶良好的體驗。但是這種**「異步」**也存在一個比較明顯的問題:因爲通信過程花費的時間比較長,所以在一些時效性要求較高場景上體驗較差。
比如長列表快速滾動的時候或者需要做一些跟手的動畫,整個過程是這樣的:
-
native thread監聽到了滾動事件,發送消息通知js thread -
js thread處理滾動事件,如果需要修改state需要經過一層js diff,拿到最終需要更新的結點 -
js thread通知shadow thread -
shadow thread通知native渲染
當用戶操作過快的時候,就會導致界面來不及更新,進而導致在快速滑動的時候會出現白屏、卡頓的現象。
2.5 優化
我們很容易看出,這是由rn的架構引出的問題,其實小程序的架構也會有這個問題,所以在rn和小程序上出現一些需要頻繁通信的場景時,就會導致頁面非常差,流暢度降低。那麼如果想解決這個問題,勢必要從架構上去進行修改。
三、從 rn 看本質
那麼既然我們知道了rn是如何實現的跨端,那麼我們就可以來探究一下它本質上是在幹什麼。首先,跨端可以分爲**「邏輯跨端」**和**「渲染跨端」**。
「邏輯跨端」通常通過 vm來實現,例如利用 v8 引擎,我們就能在各個平臺上運行我們的 js 代碼,實現「邏輯跨端」。
那麼第二個問題就是**「渲染跨端」**,我們把業務代碼的實現抽象爲開發層,比如 react-native 中我們寫的 react 代碼就屬於開發層,再把具體要渲染的端稱爲渲染層。作爲開發層來說,我一定知道我想要的ui長什麼樣,但是我沒有能力去渲染到界面上,所以當我聲明瞭一個組件之後,我們需要考慮的問題是如何把我想要什麼告訴渲染層。
就像這樣的關係,那麼我們最直觀的方式肯定是我能夠實現一種通信方式,在開發層將消息通知到各個系統,再由各個系統自己去調用對應的 api 來實現最終的渲染。
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 進行一層封裝,上層只負責調用封裝的 api,再由這一層封裝層去調用真正的 api。因爲這樣可以複用更多的邏輯,否則像上文中我們在 JS 層去發送消息給不同的平臺,我們就需要在 A\B\C 三個平臺寫三個不同方法去渲染組件。
但是,歸根結底就是,一定有一個地方是通過判斷不同平臺來調用具體實現,也就是下面這樣
有一個地方會對系統進行判斷,再通過某種通信方式通知到對應的端,最後執行真正的方法。其實,所有跨端相關操作其實都在做上圖中的這些事情。所有的跨端也可以總結爲下面這句話:
「我知道我想要什麼,但是我沒有能力去渲染,我要通知有能力渲染的人來幫助我渲染」
比如hybrid跨端方案中,webview其實就充當了橋接層的角色,createElement,appendChild等api就是給我們封裝好的跨平臺api,底層最終調用到了什麼地方,又是如何渲染到界面上的細節都被屏蔽掉了。所以我們利用這些api就能很輕鬆的實現跨端開發,寫一個網頁,只要能夠加載 webview 的地方,我們的代碼就能跑在這個上面。
又比如flutter的方案通過研發一個自渲染的引擎來實現跨端,這種思路是不是相當於另外一個瀏覽器?但是不同的點在於 flutter 是一個非常新的東西,而 webview 需要遵循大量的 w3c 規範和揹負一堆歷史包袱。flutter 並沒有什麼歷史包袱,所以它能夠從架構,設計的方面去做的更好更快,能夠做更多的事情。
四、跨端目前有什麼問題
4.1 一致性
對於跨端來說,如何屏蔽好各端的細節至關重要,比如針對某個端特有的api如何處理,如何保證渲染細節上各個端始終保持一致。如果一個跨端框架能夠讓開發者的代碼裏面不出現 isIos、isAndroid的字眼,或者是爲了兼容各種奇怪的渲染而產生的非常詭異的hack方式。那我認爲它絕對是一個真正成功的框架。
但是按我經驗而言,先後寫過的 h5、rn、小程序,他們都沒有真正做到這一點,所以項目裏面會出現爲了解決不同端不一致問題而出現的各種奇奇怪怪的代碼。而這個問題其實也是非常難解決的,因爲各端的差異還是比較大的,所以說很難去完全屏蔽這些細節。
比如說h5中磨人的垂直居中問題,我相信只要開發過移動端頁面的都會遇見,就不用我多說了。
4.2 爲什麼出現了這麼多框架
爲什麼大家其實本質上都是在幹一件事情,卻出現了這麼多的解決方案?其實大家都覺得某些框架沒能很好的解決某個問題,所以想自己去造一套。其中可能很多開發者最關心的就是性能問題,比如:
-
rn因爲架構上的原因導致某些場景性能差,所以它就想辦法從架構上去進行修改。 -
flutter直接自己搞了一套渲染引擎,同時選用支持AOT的dart作爲開發語言。
但是其實我們在選擇框架的時候性能並不是唯一因素,開發體驗、框架生態這些也都是關鍵因素,我個人感受是,目前rn的生態還是比其他的要好,所以在開發過程中你想要的東西基本都有。
五、小程序跨端
ok,說了這麼多,對於跨端部分的內容其實我想說的已經說的差不多了,還記得上文提到的 Taro、Uni-app 一類跨小程序方案麼。爲什麼說它是另類的跨端,因爲它其實並沒有實際跨端,只是爲了解決各個小程序語法之間不兼容的問題。但是它又確實是一個跨端解決方案,因爲它符合 「write once, run everything。」
下面我們先來了解下小程序的背景。
5.1 什麼是小程序
小程序是各個app廠商對外開放的一種能力。通過廠商提供的框架,就能在他們的app中運行自己的小程序,藉助各大app的流量來開展自己的業務。同時作爲廠商如果能吸引到更多的人加入到開發者大軍中來,也能給app帶來給多的流量,這可以看作一個雙贏的業務。那麼最終呈現在app中的頁面是以什麼方式進行渲染的呢?其實還是通過webview,但是會嵌入一些原生的組件在裏面以提供更好的用戶體驗,比如video組件其實並不是h5 video,而是native video。
5.2 什麼是小程序跨端
那麼到了這裏,我們就可以來談一談關於小程序跨端的東西了。關於小程序跨端,核心並不是真正意義上的跨端,雖然小程序也做到了跨端,例如一份代碼其實是可以跑在android和Ios上的,但是實際上這和hybrid跨端十分相似。
在這裏我想說的其實是,市面上現在有非常多的小程序:字節小程序、百度小程序、微信小程序、支付寶小程序等等等等。雖然他們的dsl十分相似,但是終歸還是有所不同,那麼就意味着如果我想在多個app上去開展我的業務,我是否需要維護多套十分相似的代碼?我又能否通過一套代碼能夠跑在各種小程序上?
5.3 怎麼做
想通過一套代碼跑在多個小程序上,和想通過一套代碼跑在多個端,這兩件事到底是不是一件事呢?我們再回到這張圖
這些平臺是否可以對應上不同的小程序?
再回到那句話:「我知道我想要什麼,但是我沒有能力去渲染,我要通知有能力渲染的人來幫助我渲染。」
現在來理一下我們的需求:
-
小程序的語法不好用,我希望用 react 開發;
-
我希望儘可能低的成本讓小程序跑在多個平臺上。
那麼從這句話來看:**「我」代表了什麼,「有能力渲染的人」**又代表了什麼?
第二個很容易對應上,**「有能力渲染的人」**就是小程序本身,只有它才能幫助我們把內容真正渲染到界面上。
而**「我」**又是什麼呢?其實這個**「我」**可以是很多東西,不過這裏我們的需求是想用react進行開發,所以我們回想一下第一講中react的核心流程,當它拿到vdom的時候,是不是就已經知道【我想要什麼】了?所以我們把react拿到vdom之前的流程搬過來,這樣就能獲取到**「我知道我想要什麼」**的信息,但是**「我沒有能力去渲染」**,因爲這不是web,沒有dom api,所以我需要通知小程序來幫助我渲染,我還可以根據不同的端來通知不同的小程序幫助我渲染。
所以整個流程就是下面這樣的:
前面三個流程都在我們的js層,也就是開發層,我們寫的代碼經歷一遍完整的 react 流程之後,會將最後的結果給到各個小程序,然後再走小程序自己的內部流程,將其真正的渲染到界面上。
採用這種做法的典型例子有remax、taro3,他們宣稱用真正的react去開發小程序,其實並沒有錯,因爲真的是把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}>click</Button>
</View>
)
}
}
這是一個用taro寫的組件,把它編譯到字節小程序之後是這樣的效果:
根據我們之前的分析,在最後生成的文件中,一定包含了一個**「小程序渲染器」**。它接受的data就是整個ui結構,然後通過小程序的渲染能力渲染到界面上,我們去dist文件中找一下,就能找到一個base.ttml 的文件,裏面的內容是這樣的
<template >
<block tt:for="{{root.cn}}" tt:key="uid">
<template is="tmpl_0_container" data="{{i:item}}" />
</block>
</template>
<template >
<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 >
<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 >
<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 >
<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 >
<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 >
<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 >
<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 >
<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 >
<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 >
<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 {{i:i}}">
<block>{{i.v}}</block>
</template>
<template >
<template is="{{'tmpl_0_' + i.nn}}" data="{{i:i}}" />
</template>
從名字可以看出,這是用於渲染各種組件的template,所以當我們拿到react傳遞過來的data時,將其傳給template,template就能根據對應的組件名採用不同的模版進行渲染。隨後再用一個for循環將其子組件進行遞歸渲染,完成整個頁面的渲染。這個就可以理解爲我們針對不同端寫的不同渲染器,如果我們編譯到wx小程序,這裏面的內容是會不同的。
總之,「在」**react**「對其處理完之後,會把數據」**setData**「傳遞給「「小程序」」,小程序再用之前寫好的各種」**template**「將其渲染到頁面上。」
下面這張圖就是經過react處理之後,能夠拿到頁面的數據,將其傳遞給小程序之後,就能遞歸渲染出來。
那麼這樣的架構有什麼問題呢,可以很明顯的看到會走兩遍diff,爲什麼會走兩遍diff呢?因爲在react層爲了獲取到我想要什麼這個信息,我們必須走一遍diff,這樣才能將最後得到的data交給小程序。
而交給小程序之後,小程序對於之前的流程是無感知的,所以它爲了得到需要更新什麼這個信息,也需要過一遍diff,或者通過一些其他的方式來拿到這個信息 (並沒有深入瞭解過小程序的渲染流程,所以不確定是否是通過diff拿到的),所以這一整套流程就會走兩遍diff。
爲什麼我們不能將兩次diff合併爲一次?因爲小程序的渲染對開發者而言就是個黑盒,我們不能干擾到其內部流程。如果我們能夠直接對接小程序的渲染sdk,那麼其實根本沒必要走兩遍diff,因爲前置的 react的diff我們已經能夠知道需要更新什麼內容。
這個問題的本質和普通意義上的跨端框架沒有太大的區別,開發層也就是 react 知道自己需要什麼東西,但是它沒有能力去渲染到界面上,所以需要通過小程序充當渲染層來渲染到真正的界面上。這種開發方式有一種用 react 去寫 vue 的意思,但是爲什麼會出現這種詭異的開發方式,如果這個 vue 做的足夠好的話,誰又想去這樣折騰?
5.4 組件的嵌套
其實還有一個小問題,wx的template是無法支持遞歸調用的,也就導致了我們想用template遞歸渲染data內容是無法實現的,那麼這個問題要如何解決呢.. 我們看一下上面的代碼在wx小程序中編譯出來的結果:
我們可以看到各種template之間多了 0、1、2、3 這種標號.. 就是爲了解決無法遞歸調用的問題,提前多造幾個名字不同功能相同的template,不就能跨過遞歸調用的限制了麼...
六、另一種粗暴的跨端
上述的這些跨端都是通過某種架構方式去實現的,那如果我們粗暴一點的想,我能不能直接把一套代碼通過編譯的方式去編譯到不同的平臺。比如我把js代碼編譯成java代碼、object-c代碼,其實,個人感覺也不是不行,但是因爲這些的差異實在太大,所以在寫js代碼的時候,可能需要非常強的約束性、規範性,把開發者限制在某個區域內,才能很好的編譯過去。也就是說,從js到java其實是一個自由度高到自由度低的一個過程,肯定是無法完全一一對應上的,並且由於開發方式、語法完全不一樣,所以想通過編譯的方式將js編譯到ios和android上去還是比較難的,但是對於小程序來說,嘗試把jsx編譯到template似乎是一個可行的方案,實際上,taro1/2 都是這麼幹的。不過從jsx到template也是一個自由度從高到低的一個過程,所以是沒辦法絕對完美地將把所有語法都編譯到template...
這裏可以給大家分享一個很有意思的例子,最近很火的 SolidJS 框架也支持用 JSX 寫代碼,但是它完全沒有react這麼重的runtime,因爲它的JSX最終會被編譯成一些原生的操作... 我們看一個簡單的例子:https://playground.solidjs.com/
在 react 語境下,我們在input框裏面輸入內容的時候,上面的文案應該跟着改變,但是實際上並沒有。這是因爲這個東西最後被編譯完之後是一些原生的操作,它其實只會運行一遍,最後你觸發的各種click並不會導致函數重新運行,而是直接通過原生操作操作到對應的DOM上來修改視圖,也就導致了上面問題的產生。
其實我覺得這樣挺反人類的,雖然是JSX的語法,但是卻缺少了最核心的東西:函數式的思維。(還不如寫template)。
七、virtual dom
7.1 對於跨端的意義
提到跨端,可能很多人第一個想到的東西就是 virtual dom,因爲它是對於ui的抽象,脫離了平臺,所以可能很多人會覺得virtual dom和跨平臺已經是綁定在一起的東西了。但是其實個人感覺並不是。
首先我們回想一下,我們之前說到的跨平臺的本質是什麼?開發層知道自己想要什麼,然後告訴渲染層自己想要什麼,就這麼簡單。那對於react-native來說,是通過virtual dom來判斷自己需要更新什麼結點的嗎?其實並不是,單靠一個virtual dom還不足以獲取到這個信息,必須還要加上diff,所以是virtual dom+diff獲取到了自己想要什麼的信息,再通過通信的方式告訴native去更新真正的結點。
所以virtual dom在這個裏面只扮演了一個獲取方法的角色,是通過virtual dom+diff這個方法拿到了我們想要的東西。換言之,我們也可以通過其他的方法來拿到我們想要什麼。比如之前分享的san框架,這是一個沒有virtual dom的框架,但是它爲什麼能夠跨平臺,我們先不管它內部是如何實現的,但是在更新階段,如果它在某個時刻調用了 createElement,那麼它一定是知道了:自己想要什麼。對應上跨端的內容,這個時候就能通過某種手段去告訴native,渲染某個東西。
「所以,當我們通過其他手段獲取到了:我們想要什麼這個信息之後,就能通知」**native**「去渲染真正的內容。」
7.2 virtual dom 的優勢
那麼vdom的優勢在於什麼地方?我認爲主要是下面兩個:
-
開創
jsx新時代,函數式編程思想 -
強大的表達力。能夠使用
template獲取更多優化信息,又能夠支持jsx
首先,jsx 簡直開創了一個新時代,讓我們能夠以函數式編程思想去寫ui,之前誰能想到一個切圖仔還能用這樣的方式去寫ui。
其次,我們知道,vue雖然是使用的template作爲dsl,但是實際上我們也是可以寫jsx的,jsx所提供的靈活能力是template無法比擬的。而之所以能夠同時支持template和jsx其實就是因爲vdom的存在,如果vue不引入vdom,是沒辦法說去支持jsx的語法的,或者說,是沒辦法去支持真正的jsx。
八、結語
還是那句話,跨端就是:「我知道我想要什麼,但是我沒有能力去渲染,我要通知有能力渲染的人來幫助我渲染。」
他們的本質都非常簡單,但是細節卻非常難處理,同時對於目前市面上的多種跨端框架,也需要大家根據自己的項目去權衡利弊選擇一個最有方案,畢竟目前沒有一個框架能完全吊打所有其他框架,適合自己的纔是最好的。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/G_2eBJyrr6FHm7WjI_MIPw