<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          聊聊跨端技術(shù)的本質(zhì)與現(xiàn)狀

          共 9775字,需瀏覽 20分鐘

           ·

          2022-03-03 10:23

          來(lái)自團(tuán)隊(duì) 匡凌熙 同學(xué)的分享


          零、何為跨端

          write once, run everywhere

          一次編寫(xiě),四處運(yùn)行就是跨端的真諦。因?yàn)榍岸水?dāng)下需要處理的場(chǎng)景實(shí)在是太多了:androidiospc、小程序,甚至智能手表、車載電視等,當(dāng)某幾個(gè)場(chǎng)景非常相似的時(shí)候,我們希望能夠用最少的開(kāi)發(fā)成本來(lái)達(dá)到最好的效果,而不是每個(gè)端都需要一套單獨(dú)的人力來(lái)進(jìn)行維護(hù),所以跨端技術(shù)就誕生了。

          那么在跨端方案百花齊放的今天,比如現(xiàn)在最為人們所熟知的react nativeflutterelectron等,他們之間有沒(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),但相互之間還是有不小差異的,通常可以借助 taroremax 這類框架實(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 初始化流程

          1. native 啟動(dòng)一個(gè)原生界面,比如android會(huì)起一個(gè)新的activity來(lái)承載rn,并做一些初始化的操作。
          2. 加載 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)。

          1. 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)。
          1. shadow thread 計(jì)算布局,通知native Thread 創(chuàng)建原生組件。
          2. native 在界面上渲染原生組件,呈現(xiàn)給用戶。

          2.3 更新流程

          比如某個(gè)時(shí)候,用戶點(diǎn)擊了屏幕上的一個(gè)按鈕觸發(fā)了一個(gè)點(diǎn)擊事件,此時(shí)界面需要進(jìn)行相應(yīng)的更新操作。

          1. native 獲取到了點(diǎn)擊事件,傳給了js thread
          2. js thread根據(jù) react 代碼進(jìn)行相應(yīng)的處理,比如處理 onClick 函數(shù),觸發(fā)了 setState
          3. react 的更新流程一樣,觸發(fā)了 setState 之后會(huì)進(jìn)行 diff,找到需要更新的結(jié)點(diǎn)
          4. 通知 shadow thread
          5. shadow 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ò)程是這樣的:

          1. native thread監(jiān)聽(tīng)到了滾動(dòng)事件,發(fā)送消息通知js thread
          2. js thread 處理滾動(dòng)事件,如果需要修改 state 需要經(jīng)過(guò)一層js diff,拿到最終需要更新的結(jié)點(diǎn)
          3. js thread 通知 shadow thread
          4. shadow 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)了橋接層的角色,createElementappendChildapi就是給我們封裝好的跨平臺(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) isIosisAndroid的字眼,或者是為了兼容各種奇怪的渲染而產(chǎn)生的非常詭異的hack方式。那我認(rèn)為它絕對(duì)是一個(gè)真正成功的框架。

          但是按我經(jīng)驗(yàn)而言,先后寫(xiě)過(guò)的 h5rn、小程序,他們都沒(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í)選用支持AOTdart作為開(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í)是可以跑在androidIos上的,但是實(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)部流程,將其真正的渲染到界面上。

          采用這種做法的典型例子有remaxtaro3,他們宣稱用真正的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í),將其傳給templatetemplate就能根據(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)榍爸玫?reactdiff我們已經(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)題,wxtemplate是無(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ō),從jsjava其實(shí)是一個(gè)自由度高到自由度低的一個(gè)過(guò)程,肯定是無(wú)法完全一一對(duì)應(yīng)上的,并且由于開(kāi)發(fā)方式、語(yǔ)法完全不一樣,所以想通過(guò)編譯的方式將js編譯到iosandroid上去還是比較難的,但是對(duì)于小程序來(lái)說(shuō),嘗試把jsx編譯到template似乎是一個(gè)可行的方案,實(shí)際上,taro1/2 都是這么干的。不過(guò)從jsxtemplate也是一個(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è):

          1. 開(kāi)創(chuàng)jsx新時(shí)代,函數(shù)式編程思想
          2. 強(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í)支持templatejsx其實(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è)框架能完全吊打所有其他框架,適合自己的才是最好的。


          瀏覽 52
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲欧洲AV | 国产黄片乱伦 | 91无码人妻精品一区二区蜜桃 | 影音先锋三级资源 | 婷婷五月天基地 |