標(biāo)題中我們提出一個問題:react 代碼如何跑在小程序上?目前看來大致兩種思路:
1. 把 react 代碼編譯成小程序代碼,這樣我們可以開發(fā)用 react,然后跑起來還是小程序原生代碼,結(jié)果很完美,但是把 react 代碼編譯成各個端的小程序代碼是一個力氣活,而且如果想用 vue 來開發(fā)的話,那么還需要做一遍 vue 代碼的編譯,這是 taro 1/2 的思路。
2. 我們可以換個問題思考,react 代碼是如何跑在瀏覽器里的?
- 站在瀏覽器的角度來思考:無論開發(fā)用的是什么框架,React 也好,Vue 也罷,最終代碼經(jīng)過運行之后都是調(diào)用了瀏覽器的那幾個 BOM/DOM 的 API ,如:createElement、appendChild、removeChild 等。
- Taro 3 主要通過在小程序端模擬實現(xiàn) DOM、BOM API 來讓前端框架直接運行在小程序環(huán)境中。
下面我們具體看看各自的實現(xiàn)。
Taro 1/2
Taro 1/2 的架構(gòu)主要分為:編譯時 和 運行時。
其中編譯時主要是將 Taro 代碼通過 Babel 轉(zhuǎn)換成 小程序的代碼,如:JS、WXML、WXSS、JSON。
運行時主要是進(jìn)行一些:生命周期、事件、data 等部分的處理和對接。
Taro 編譯時
Taro 的編譯,使用 babel-parser 將 Taro 代碼解析成抽象語法樹,然后通過 babel-types 對抽象語法樹進(jìn)行一系列修改、轉(zhuǎn)換操作,最后再通過 babel-generate 生成對應(yīng)的目標(biāo)代碼。
整個編譯時最復(fù)雜的部分在于 JSX 編譯。
我們都知道 JSX 是一個 JavaScript 的語法擴展,它的寫法千變?nèi)f化,十分靈活。這里我們是采用 窮舉 的方式對 JSX 可能的寫法進(jìn)行了一一適配,這一部分工作量很大,實際上 Taro 有大量的 Commit 都是為了更完善的支持 JSX 的各種寫法。
Taro 運行時
接下來,我們可以對比一下編譯后的代碼,可以發(fā)現(xiàn),編譯后的代碼中,React 的核心 render 方法 沒有了。同時代碼里增加了 BaseComponent 和 createComponent ,它們是 Taro 運行時的核心。
// 編譯前import Taro, { Component } from '@tarojs/taro'import { View, Text } from '@tarojs/components'import './index.scss'export default class Index extends Component { config = { navigationBarTitleText: '首頁' } componentDidMount () { } render () { return ( <View className=‘index' onClick={this.onClick}> <Text>Hello world!</Text> </View> ) }}// 編譯后import {BaseComponent, createComponent} from '@tarojs/taro-weapp'class Index extends BaseComponent {// ... _createDate(){ //process state and props }}export default createComponent(Index)
BaseComponent 主要是對 React 的一些核心方法:setState、forceUpdate 等進(jìn)行了替換和重寫,結(jié)合前面編譯后 render 方法被替換,大家不難猜出:Taro 當(dāng)前架構(gòu)只是在開發(fā)時遵循了 React 的語法,在代碼編譯之后實際運行時,和 React 并沒有關(guān)系。
而 createComponent 主要作用是調(diào)用 Component() 構(gòu)建頁面;對接事件、生命周期等;進(jìn)行 Diff Data 并調(diào)用 setData 方法更新數(shù)據(jù)。
這樣的實現(xiàn)過程有三?缺點:
- JSX ?持程度不完美。Taro 對 JSX 的?持是通過編譯時的適配去實現(xiàn)的,但 JSX ??常之靈活,因此還不能做到 100% ?持所有的 JSX 語法。JSX 是一個 JavaScript 的語法擴展,它的寫法千變?nèi)f化,十分靈活。之前Taro團(tuán)隊是采用窮舉的方式對 JSX 可能的寫法進(jìn)行了一一適配,這一部分工作量很大。
- 不?持 source-map。Taro 對源代碼進(jìn)?了?系列的轉(zhuǎn)換操作之后,就不?持 source-map 了,?戶 調(diào)試、使?這個項?就會不?便。
- 維護(hù)和迭代?分困難。Taro 編譯時代碼?常的復(fù)雜且離散,維護(hù)迭代都?常的困難。
Taro 3
Taro 3 則可以大致理解為解釋型架構(gòu)(相對于 Taro 1/2 而言),主要通過在小程序端模擬實現(xiàn) DOM、BOM API 來讓前端框架直接運行在小程序環(huán)境中,從而達(dá)到小程序和 H5 統(tǒng)一的目的。
而對于生命周期、組件庫、API、路由等差異,依然可以通過定義統(tǒng)一標(biāo)準(zhǔn),各端負(fù)責(zé)各自實現(xiàn)的方式來進(jìn)行抹平。
而正因為 Taro 3 的原理,在 Taro 3 中同時支持 React、Vue 等框架,甚至還支持了 jQuery,還能支持讓開發(fā)者自定義地去拓展其他框架的支持,比如 Angular,Taro 3 整體架構(gòu)如下:
模擬實現(xiàn) DOM、BOM API
Taro 3 創(chuàng)建了 taro-runtime 的包,然后在這個包中實現(xiàn)了 一套 高效、精簡版的 DOM/BOM API(下面的 UML 圖只是反映了幾個主要的類的結(jié)構(gòu)和關(guān)系):
- TaroEventTarget類,實現(xiàn)addEventListener和removeEventListener。
- TaroNode類繼承TaroEventTarget類,主要實現(xiàn)insertBefore、appendChild等操作 Dom 節(jié)點的方法。下面在頁面渲染我們會具體看這幾個方法的實現(xiàn)。
- TaroElement類繼承TaroNode類,主要是節(jié)點屬性相關(guān)的方法和dispatchEvent方法,dispatchEvent方法在下面講事件觸發(fā)的時候也會涉及到。
- TarorootElement類繼承TaroElement類,其中最主要是enqueueUpdate和performUpdate,把虛擬 DOM setData 成小程序 data 的操作就是這兩個函數(shù)。
然后,我們通過 Webpack 的 ProvidePlugin 插件,注入到小程序的邏輯層。
Webpack ProvidePlugin 是一個 webpack 自帶的插件,用于在每個模塊中自動加載模塊,而無需使用 import/require 調(diào)用。該插件可以將全局變量注入到每個模塊中,避免在每個模塊中重復(fù)引用相同的依賴。
// trao-mini-runner/src/webpack/build.conf.tsplugin.providerPlugin = getProviderPlugin({ window: ['@tarojs/runtime', 'window'], document: ['@tarojs/runtime', 'document'], navigator: ['@tarojs/runtime', 'navigator'], requestAnimationFrame: ['@tarojs/runtime', 'requestAnimationFrame'], cancelAnimationFrame: ['@tarojs/runtime', 'cancelAnimationFrame'], Element: ['@tarojs/runtime', 'TaroElement'], SVGElement: ['@tarojs/runtime', 'SVGElement'], MutationObserver: ['@tarojs/runtime', 'MutationObserver'], history: ['@tarojs/runtime', 'history'], location: ['@tarojs/runtime', 'location'], URLSearchParams: ['@tarojs/runtime', 'URLSearchParams'], URL: ['@tarojs/runtime', 'URL'],})// trao-mini-runner/src/webpack/chain.tsexport const getProviderPlugin = args => { return partial(getPlugin, webpack.ProvidePlugin)([args])}
這樣,在小程序的運行時,就有了 一套高效、精簡版的 DOM/BOM API。
taro-react:小程序版的 react-dom
在 DOM/BOM 注入之后,理論上來說,react 就可以直接運行了。
但是因為 React-DOM 包含大量瀏覽器兼容類的代碼,導(dǎo)致包太大。Taro 自己實現(xiàn)了 react 的自定義渲染器,代碼在taro-react包里。
在 React 16 ,React 的架構(gòu)如下:
最上層是 React 的核心部分 react-core ,中間是 react-reconciler,其的職責(zé)是維護(hù) VirtualDOM 樹,內(nèi)部實現(xiàn)了 Diff/Fiber 算法,決定什么時候更新、以及要更新什么。
而 Renderer 負(fù)責(zé)具體平臺的渲染工作,它會提供宿主組件、處理事件等等。例如 React-DOM 就是一個渲染器,負(fù)責(zé) DOM 節(jié)點的渲染和 DOM 事件處理。
Taro實現(xiàn)了taro-react 包,用來連接 react-reconciler 和 taro-runtime 的 BOM/DOM API。是基于 react-reconciler 的小程序?qū)S?React 渲染器,連接 @tarojs/runtime的DOM 實例,相當(dāng)于小程序版的react-dom,暴露的 API 也和react-dom 保持一致。
這里涉及到一個問題:如何自定義 React 渲染器?
第一步: 實現(xiàn)宿主配置( 實現(xiàn)react-reconciler的hostConfig配置)
這是react-reconciler要求宿主提供的一些適配器方法和配置項。這些配置項定義了如何創(chuàng)建節(jié)點實例、構(gòu)建節(jié)點樹、提交和更新等操作。即在 hostConfig 的方法中調(diào)用對應(yīng)的 Taro BOM/DOM 的 API。
1. 創(chuàng)建形操作
createInstance(type,newProps,rootContainerInstance,_currentHostContext,workInProgress)。
react-reconciler 使用該方法可以創(chuàng)建對應(yīng)目標(biāo)平臺的UI Element實例。比如 document.createElement 根據(jù)不同類型來創(chuàng)建 div、img、h2等DOM節(jié)點,并使用 newProps參數(shù)給創(chuàng)建的節(jié)點賦予屬性。而在 Taro 中:
import { document } from '@tarojs/runtime'// 在 ReactDOM 中會調(diào)用 document.createElement 來生成 dom,// 而在小程序環(huán)境中 Taro 中模擬了 document,// 直接返回 `document.createElement(type)` 即可createInstance (type, props: Props, _rootContainerInstance: any, _hostContext: any, internalInstanceHandle: Fiber) { const element = document.createElement(type) precacheFiberNode(internalInstanceHandle, element) updateFiberProps(element, props) return element},
createTextInstance
如果目標(biāo)平臺允許創(chuàng)建純文本節(jié)點。那么這個方法就是用來創(chuàng)建目標(biāo)平臺的文本節(jié)點。
import { document } from '@tarojs/runtime'// Taro: 模擬的 document 支持創(chuàng)建 text 節(jié)點, 返回 `document.createTextNode(text)` 即可.createTextInstance (text: string, _rootContainerInstance: any, _hostContext: any, internalInstanceHandle: Fiber) { const textNode = document.createTextNode(text) precacheFiberNode(internalInstanceHandle, textNode) return textNode},
2. UI樹操作
appendInitialChild(parent, child)
初始化UI樹創(chuàng)建。
// Taro: 直接 parentInstance.appendChild(child) 即可appendInitialChild (parent, child) { parent.appendChild(child)},
appendChild(parent, child)
此方法映射為 domElement.appendChild 。
appendChild (parent, child) { parent.appendChild(child)},
3. 更新prop操作
finalizeInitialChildren
finalizeInitialChildren 在組件掛載到頁面中前調(diào)用,更新時不會調(diào)用。
這個方法我們下面事件注冊時還會提到。
finalizeInitialChildren (dom, type: string, props: any) { updateProps(dom, {}, props) // 提前執(zhí)行更新屬性操作,Taro 在 Page 初始化后會立即從 dom 讀取必要信息 // ....},
prepareUpdate(domElement, oldProps, newProps)
這里是比較oldProps,newProps的不同,用來判斷是否要更新節(jié)點。
prepareUpdate (instance, _, oldProps, newProps) { return getUpdatePayload(instance, oldProps, newProps)},// ./props.tsexport Function getUpdatePayload (dom: TaroElement, oldProps: Props, newProps: Props){ let i: string let updatePayload: any[] | null = null for (i in oldProps) { if (!(i in newProps)) { (updatePayload = updatePayload || []).push(i, null) } } const isFormElement = dom instanceof FormElement for (i in newProps) { if (oldProps[i] !== newProps[i] || (isFormElement && i === 'value')) { (updatePayload = updatePayload || []).push(i, newProps[i]) } } return updatePayload}
commitUpdate(domElement, updatePayload, type, oldProps, newProps)
此函數(shù)用于更新domElement屬性,下文要講的事件注冊就是在這個方法里。
// Taro: 根據(jù) updatePayload,將屬性更新到 instance 中,// 此時 updatePayload 是一個類似 `[prop1, value1, prop2, value2, ...]` 的數(shù)組commitUpdate (dom, updatePayload, _, oldProps, newProps) { updatePropsByPayload(dom, oldProps, updatePayload) updateFiberProps(dom, newProps)},export function updatePropsByPayload (dom: TaroElement, oldProps: Props, updatePayload: any[]){ for(let i = 0; i < updatePayload.length; i = 2){ // key, value 成對出現(xiàn) const key = updatePayload[i]; const newProp = updatePayload[i 1]; const oldProp = oldProps[key] setProperty(dom, key, newProp, oldProp) }}function setProperty (dom: TaroElement, name: string, value: unknown, oldValue?: unknown) { name = name === 'className' ? 'class' : name if ( name === 'key' || name === 'children' || name === 'ref') { // skip } else if (name === 'style') { const style = dom.style if (isString(value)) { style.cssText = value } else { if (isString(oldValue)) { style.cssText = '' oldValue = null } if (isObject<StyleValue>(oldValue)) { for (const i in oldValue) { if (!(value && i in (value as StyleValue))) { setStyle(style, i, '') } } } if (isObject<StyleValue>(value)) { for (const i in value) { if (!oldValue || value[i] !== (oldValue as StyleValue)[i]) { setStyle(style, i, value[i]) } } } } } else if (isEventName(name)) { setEvent(dom, name, value, oldValue) } else if (name === 'dangerouslySetInnerHTML') { const newHtml = (value as DangerouslySetInnerHTML)?.__html ?? '' const oldHtml = (oldValue as DangerouslySetInnerHTML)?.__html ?? '' if (newHtml || oldHtml) { if (oldHtml !== newHtml) { dom.innerHTML = newHtml } } } else if (!isFunction(value)) { if (value == null) { dom.removeAttribute(name) } else { dom.setAttribute(name, value as string) } }}
上面是hostConfig里必要的回調(diào)函數(shù)的實現(xiàn),源碼里還有很多回調(diào)函數(shù)的實現(xiàn),詳見trao-react源碼。
第二步:實現(xiàn)渲染函數(shù),類似于ReactDOM.render() 方法。可以看成是創(chuàng)建 Taro DOM Tree 容器的方法。
源碼實現(xiàn)詳見trao-react/src/render.ts。
export function render (element: ReactNode, domContainer: TaroElement, cb: Callback) { const root = new Root(TaroReconciler, domContainer) return root.render(element, cb)}export function createRoot (domContainer: TaroElement, options: CreateRootOptions = {}) { // options should be an object const root = new Root(TaroReconciler, domContainer, options) // ...... return root}
class Root { public constructor (renderer: Renderer, domContainer: TaroElement, options?: CreateRootOptions) { this.renderer = renderer this.initInternalRoot(renderer, domContainer, options) } private initInternalRoot (renderer: Renderer, domContainer: TaroElement, options?: CreateRootOptions) { // ..... this.internalRoot = renderer.createContainer( containerInfo, tag, null, // hydrationCallbacks isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, onRecoverableError, transitionCallbacks ) } public render (children: ReactNode, cb: Callback) { const { renderer, internalRoot } = this renderer.updateContainer(children, internalRoot, null, cb) return renderer.getPublicRootInstance(internalRoot) }}
而 Root 類最后調(diào)用TaroReconciler的createContainr“updateContainer和 getPublicRootInstance 方法,實際上就是react-reconciler包里面對應(yīng)的方法。
渲染函數(shù)是在什么時候被調(diào)用的呢?
在編譯時,會引入插件taro-plugin-react, 插件內(nèi)會調(diào)用 modifyMiniWebpackChain=> setAlias。
// taro-plugin-react/src/webpack.mini.tsfunction setAlias (ctx: IPluginContext, framework: Frameworks, chain) { if (framework === 'react') { alias.set('react-dom$', '@tarojs/react') }}
這樣ReactDOM.createRoot和ReactDOM.render實際上調(diào)用的就是trao-react的createRoot和render方法。
經(jīng)過上面的步驟,React 代碼實際上就可以在小程序的運行時正常運行了,并且會生成 Taro DOM Tree。那么偌大的 Taro DOM Tree 怎樣更新到頁面呢?
從虛擬 Dom 到小程序頁面渲染
因為?程序并沒有提供動態(tài)創(chuàng)建節(jié)點的能?,需要考慮如何使?相對靜態(tài)的 wxml 來渲染相對動態(tài)的 Taro DOM 樹。Taro使?了模板拼接的?式,根據(jù)運?時提供的 DOM 樹數(shù)據(jù)結(jié)構(gòu),各 templates 遞歸地 相互引?,最終可以渲染出對應(yīng)的動態(tài) DOM 樹。
模版化處理
首先,將小程序的所有組件挨個進(jìn)行模版化處理,從而得到小程序組件對應(yīng)的模版。如下圖就是小程序的 view 組件模版經(jīng)過模版化處理后的樣子。?先需要在 template ??寫?個 view,把它所有的屬性全部列出來(把所有的屬性都列出來是因為?程序??不能去動態(tài)地添加屬性)。
模板化處理的核心代碼在 packages/shared/src/template.ts 文件中。會在編譯工程中生成 base.wxml文件,這是我們打包產(chǎn)物之一。
// base.wxml<wxs module="xs" src="./utils.wxs" /><template name="taro_tmpl"> <block wx:for="{{root.cn}}" wx:key="sid"> // tmpl_' 0 '_' 2 <template is="{{xs.a(0, item.nn, '')}}" data="{{i:item,c:1,l:''}}" /> </block></template>....<template name="tmpl_0_2"> <view style="{{i.st}}" class="{{i.cl}}" id="{{i.uid||i.sid}}" data-sid="{{i.sid}}"> <block wx:for="{{i.cn}}" wx:key="sid"> <template is="{{xs.a(c, item.nn, l)}}" data="{{i:item,c:c 1,l:xs.f(l,item.nn)}}" /> </block> </view></template>
打包產(chǎn)生的頁面代碼是這樣的:
// pages/index/index.wxml<import src="../../base.wxml"/><template is="taro_tmpl" data="{{root:root}}" />
接下來是遍歷渲染所有?節(jié)點,基于組件的 template,動態(tài) “遞歸” 渲染整棵樹。
具體流程為先去遍歷 Taro DOM Tree 根節(jié)點的子元素,再根據(jù)每個子元素的類型選擇對應(yīng)的模板來渲染子元素,然后在每個模板中我們又會去遍歷當(dāng)前元素的子元素,以此把整個節(jié)點樹遞歸遍歷出來。
hydrate Data
而動態(tài)遞歸時需要獲取到我們的 data,也就是 root。
首先,在 createPageConfig 中會對 config.data 進(jìn)行初始化,賦值 {root:{cn:[]}}。
export function createPageConfig (component: any, pageName?: string, data?: Record<string, unknown>, pageConfig?: PageConfig) { // ....... if (!isUndefined(data)) { config.data = data } // .......}
React在commit階段會調(diào)用HostConfig里的appendInitialChild方法完成頁面掛載,在Taro中則繼續(xù)調(diào)用:appendInitialChild —> appendChild —> insertBefore —> enqueueUpdate。
// taro-react/src/reconciler.tsappendInitialChild (parent, child) { parent.appendChild(child)},appendChild (parent, child) { parent.appendChild(child)},// taro-runtime/src/dom/node.tspublic appendChild (newChild: TaroNode) { return this.insertBefore(newChild)}public insertBefore<T extends TaroNode> (newChild: T, refChild?: TaroNode | null, isReplace?: boolean): T { // 忽略了大部分代碼 this.enqueueUpdate({ path: newChild._path, value: this.hydrate(newChild) }) return newChild}
這里看到最終調(diào)用enqueueUpdate方法,傳入一個對象,值為 path 和 value,而 value 值是hydrate方法的結(jié)果。
hydrate方法我們可以翻譯成“注水”,函數(shù) hydrate 用于將虛擬 DOM(TaroElement 或 TaroText)轉(zhuǎn)換為小程序組件渲染所需的數(shù)據(jù)格式(MiniData)。
回想一下小程序員生的 data 里都是我們頁面需要的 state,而 taro 的hydrate方法返回的 miniData 是把 state 外面在包裹上我們頁面的 node 結(jié)構(gòu)值。舉例來看,我們一個 helloword 代碼所hydrate的 miniData 如下(可以在小程序IDE中的 ”AppData“ 標(biāo)簽欄中查看到完整的data數(shù)據(jù)結(jié)構(gòu)):
{ "root": { "cn": [ { "cl": "index", "cn": [ { "cn": [ { "nn": "8", "v": "Hello world!" } ], "nn": "4", "sid": "_AH" }, { "cn": [ { "nn": "8", "v": "HHHHHH" } ], "nn": "2", "sid": "_AJ" }, { "cl": "blue", "cn": [ { "nn": "8", "v": "Page bar: " }, { "cl": "red", "cn": [ { "nn": "8", "v": "red" } ], "nn": "4", "sid": "_AM" } ], "nn": "4", "sid": "_AN" } ], "nn": "2", "sid": "_AO" } ], "uid": "pages/index/index?$taroTimestamp=1691064929701" }, "__webviewId__": 1}
這里的字段含義解釋一下 :(我想這里縮寫是可能盡可能讓每一次setData的內(nèi)容更小。)
Container = 'container',Childnodes = 'cn',Text = 'v',NodeType = 'nt',NodeName = 'nn',// AttrtibutesStyle = 'st',Class = 'cl',Src = 'src
我們獲取到以上的 data 數(shù)據(jù),去執(zhí)行enqueueUpdate函數(shù),enqueueUpdate函數(shù)內(nèi)部執(zhí)行performUpdate函數(shù),performUpdate函數(shù)最終執(zhí)行 ctx.setData,ctx 是小程序的實例,也就是執(zhí)行我們熟悉的 setData 方法把上面hydrate的 miniData賦值給 root,這樣就渲染了小程序的頁面數(shù)據(jù)。
// taro-runtime/src/dom/root.tspublic enqueueUpdate (payload: UpdatePayload): void { this.updatePayloads.push(payload) if (!this.pendingUpdate && this.ctx) { this.performUpdate() }}public performUpdate (initRender = false, prerender?: Func) { // ..... while (this.updatePayloads.length > 0) { const { path, value } = this.updatePayloads.shift()! if (path.endsWith(Shortcuts.Childnodes)) { resetPaths.add(path) } data[path] = value } // ....... if (initRender) { // 初次渲染,使用頁面級別的 setData normalUpdate = data } // ........ ctx.setData(normalUpdate, cb)}
整體流程可以概括為:當(dāng)在React中調(diào)用 this.setState 時,React內(nèi)部會執(zhí)行reconciler,進(jìn)而觸發(fā) enqueueUpdate 方法,如下圖:
事件處理
事件注冊
在HostConfig接口中,有一個方法 finalizeInitialChildren,在這個方法里會調(diào)用updateProps。這是掛載頁面階段時間的注冊時機。updateProps 會調(diào)用 updatePropsByPayload 方法。
finalizeInitialChildren (dom, type: string, props: any) { updateProps(dom, {}, props) //....},
在HostConfig接口中,有一個方法 commitUpdate,用于在react的commit階段更新屬性:
commitUpdate (dom, updatePayload, _, oldProps, newProps) { updatePropsByPayload(dom, oldProps, updatePayload) updateFiberProps(dom, newProps)},
進(jìn)一步的調(diào)用方法:updatePropsByPayload => setProperty => setEvent。
// taro-react/src/props.tsfunction setEvent (dom: TaroElement, name: string, value: unknown, oldValue?: unknown) { const isCapture = name.endsWith('Capture') let eventName = name.toLowerCase().slice(2) if (isCapture) { eventName = eventName.slice(0, -7) } const compName = capitalize(toCamelCase(dom.tagName.toLowerCase())) if (eventName === 'click' && compName in internalComponents) { eventName = 'tap' } // 通過addEventListener將事件注冊到dom中 if (isFunction(value)) { if (oldValue) { dom.removeEventListener(eventName, oldValue as any, false) dom.addEventListener(eventName, value, { isCapture, sideEffect: false }) } else { dom.addEventListener(eventName, value, isCapture) } } else { dom.removeEventListener(eventName, oldValue as any) }}
進(jìn)一步的看看dom.addEventListener做了什么?addEventListener是類TaroEventTarget的方法:
export class TaroEventTarget { public __handlers: Record<string, EventHandler[]> = {} public addEventListener (type: string, handler: EventHandler, options?: boolean | AddEventListenerOptions) { type = type.toLowerCase() // 省略很多代碼 const handlers = this.__handlers[type] if (isArray(handlers)) { handlers.push(handler) } else { this.__handlers[type] = [handler] } }}
可以看到事件會注冊到dom對象上,最終會放入到 dom 內(nèi)部變量 __handlers 中保存。
事件觸發(fā)
// base.wxml<template name="tmpl_0_7"> <view hover-class="{{xs.b(i.p1,'none')}}" hover-stop-propagation="{{xs.b(i.p4,!1)}}" hover-start-time="{{xs.b(i.p2,50)}}" hover-stay-time="{{xs.b(i.p3,400)}}" bindtouchstart="eh" bindtouchmove="eh" bindtouchend="eh" bindtouchcancel="eh" bindlongpress="eh" animation="{{i.p0}}" bindanimationstart="eh" bindanimationiteration="eh" bindanimationend="eh" bindtransitionend="eh" style="{{i.st}}" class="{{i.cl}}" bindtap="eh" id="{{i.uid||i.sid}}" data-sid="{{i.sid}}" > <block wx:for="{{i.cn}}" wx:key="sid"> <template is="{{xs.a(c, item.nn, l)}}" data="{{i:item,c:c 1,l:xs.f(l,item.nn)}}" /> </block> </view></template>
上面是base.wxml其中的一個模板,可以看到,所有組件中的事件都會由 eh 代理。在createPageConfig時,會將 config.eh 賦值為 eventHandler。
// taro-runtime/src/dsl/common.tsfunction createPageConfig(){ const config = {...} // config會作為小程序 Page() 的入?yún)? config.eh = eventHandler config.data = {root:{cn:[]}} return config}
eventHandler 最終會觸發(fā) dom.dispatchEvent(e)。
// taro-runtime/src/dom/element.tsclass TaroElement extends TaroNode { dispatchEvent(event){ const listeners = this.__handlers[event.type] // 取出回調(diào)函數(shù)數(shù)組 for (let i = listeners.length; i--;) { result = listener.call(this, event) // event是TaroEvent實例 } }}
至此,react 代碼終于是可以完美運行在小程序環(huán)境中。
還要提到一點的是,Taro3 在 h5 端的實現(xiàn)也很有意思,Taro在 H5 端實現(xiàn)一套基于小程序規(guī)范的組件庫和 API 庫,在這里就不展開說了。
總結(jié)
Taro 3從之前的重編譯時,到現(xiàn)在的重運行時,解決了架構(gòu)問題,可以用 react、vue 甚至 jQuery 來寫小程序,但也帶來了一些性能問題。
為了解決性能問題,Taro 3 也提供了預(yù)渲染和虛擬列表等功能和組件。
但從長遠(yuǎn)來看,計算機硬件的性能越來越冗余,如果在犧牲一點可以容忍的性能的情況下?lián)Q來整個框架更大的靈活性和更好的適配性,并且能夠極大的提升開發(fā)體驗,也是值得的。
作者:孟祥輝
來源:微信公眾號:哈啰技術(shù)
出處:https://mp.weixin.qq.com/s/134VAXPJczElvdYzNFcHhA
版權(quán)聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶自發(fā)貢獻(xiàn),該文觀點僅代表作者本人。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如發(fā)現(xiàn)本站有涉嫌抄襲侵權(quán)/違法違規(guī)的內(nèi)容, 請發(fā)送郵件至 舉報,一經(jīng)查實,本站將立刻刪除。