標(biāo)題中我們提出一個(gè)問(wèn)題:react 代碼如何跑在小程序上?目前看來(lái)大致兩種思路:
1. 把 react 代碼編譯成小程序代碼,這樣我們可以開(kāi)發(fā)用 react,然后跑起來(lái)還是小程序原生代碼,結(jié)果很完美,但是把 react 代碼編譯成各個(gè)端的小程序代碼是一個(gè)力氣活,而且如果想用 vue 來(lái)開(kāi)發(fā)的話,那么還需要做一遍 vue 代碼的編譯,這是 taro 1/2 的思路。
2. 我們可以換個(gè)問(wèn)題思考,react 代碼是如何跑在瀏覽器里的?
- 站在瀏覽器的角度來(lái)思考:無(wú)論開(kāi)發(fā)用的是什么框架,React 也好,Vue 也罷,最終代碼經(jīng)過(guò)運(yùn)行之后都是調(diào)用了瀏覽器的那幾個(gè) BOM/DOM 的 API ,如:createElement、appendChild、removeChild 等。
- Taro 3 主要通過(guò)在小程序端模擬實(shí)現(xiàn) DOM、BOM API 來(lái)讓前端框架直接運(yùn)行在小程序環(huán)境中。
下面我們具體看看各自的實(shí)現(xiàn)。
Taro 1/2
Taro 1/2 的架構(gòu)主要分為:編譯時(shí) 和 運(yùn)行時(shí)。
其中編譯時(shí)主要是將 Taro 代碼通過(guò) Babel 轉(zhuǎn)換成 小程序的代碼,如:JS、WXML、WXSS、JSON。
運(yùn)行時(shí)主要是進(jìn)行一些:生命周期、事件、data 等部分的處理和對(duì)接。
Taro 編譯時(shí)
Taro 的編譯,使用 babel-parser 將 Taro 代碼解析成抽象語(yǔ)法樹(shù),然后通過(guò) babel-types 對(duì)抽象語(yǔ)法樹(shù)進(jìn)行一系列修改、轉(zhuǎn)換操作,最后再通過(guò) babel-generate 生成對(duì)應(yīng)的目標(biāo)代碼。
整個(gè)編譯時(shí)最復(fù)雜的部分在于 JSX 編譯。
我們都知道 JSX 是一個(gè) JavaScript 的語(yǔ)法擴(kuò)展,它的寫法千變?nèi)f化,十分靈活。這里我們是采用 窮舉 的方式對(duì) JSX 可能的寫法進(jìn)行了一一適配,這一部分工作量很大,實(shí)際上 Taro 有大量的 Commit 都是為了更完善的支持 JSX 的各種寫法。
Taro 運(yùn)行時(shí)
接下來(lái),我們可以對(duì)比一下編譯后的代碼,可以發(fā)現(xiàn),編譯后的代碼中,React 的核心 render 方法 沒(méi)有了。同時(shí)代碼里增加了 BaseComponent 和 createComponent ,它們是 Taro 運(yùn)行時(shí)的核心。
// 編譯前import Taro, { Component } from '@tarojs/taro'import { View, Text } from '@tarojs/components'import './index.scss'export default class Index extends Component { config = { navigationBarTitleText: '首頁(yè)' } 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 主要是對(duì) React 的一些核心方法:setState、forceUpdate 等進(jìn)行了替換和重寫,結(jié)合前面編譯后 render 方法被替換,大家不難猜出:Taro 當(dāng)前架構(gòu)只是在開(kāi)發(fā)時(shí)遵循了 React 的語(yǔ)法,在代碼編譯之后實(shí)際運(yùn)行時(shí),和 React 并沒(méi)有關(guān)系。
而 createComponent 主要作用是調(diào)用 Component() 構(gòu)建頁(yè)面;對(duì)接事件、生命周期等;進(jìn)行 Diff Data 并調(diào)用 setData 方法更新數(shù)據(jù)。
這樣的實(shí)現(xiàn)過(guò)程有三?缺點(diǎn):
- JSX ?持程度不完美。Taro 對(duì) JSX 的?持是通過(guò)編譯時(shí)的適配去實(shí)現(xiàn)的,但 JSX ??常之靈活,因此還不能做到 100% ?持所有的 JSX 語(yǔ)法。JSX 是一個(gè) JavaScript 的語(yǔ)法擴(kuò)展,它的寫法千變?nèi)f化,十分靈活。之前Taro團(tuán)隊(duì)是采用窮舉的方式對(duì) JSX 可能的寫法進(jìn)行了一一適配,這一部分工作量很大。
- 不?持 source-map。Taro 對(duì)源代碼進(jìn)?了?系列的轉(zhuǎn)換操作之后,就不?持 source-map 了,?戶 調(diào)試、使?這個(gè)項(xiàng)?就會(huì)不?便。
- 維護(hù)和迭代?分困難。Taro 編譯時(shí)代碼?常的復(fù)雜且離散,維護(hù)迭代都?常的困難。
Taro 3
Taro 3 則可以大致理解為解釋型架構(gòu)(相對(duì)于 Taro 1/2 而言),主要通過(guò)在小程序端模擬實(shí)現(xiàn) DOM、BOM API 來(lái)讓前端框架直接運(yùn)行在小程序環(huán)境中,從而達(dá)到小程序和 H5 統(tǒng)一的目的。
而對(duì)于生命周期、組件庫(kù)、API、路由等差異,依然可以通過(guò)定義統(tǒng)一標(biāo)準(zhǔn),各端負(fù)責(zé)各自實(shí)現(xiàn)的方式來(lái)進(jìn)行抹平。
而正因?yàn)?Taro 3 的原理,在 Taro 3 中同時(shí)支持 React、Vue 等框架,甚至還支持了 jQuery,還能支持讓開(kāi)發(fā)者自定義地去拓展其他框架的支持,比如 Angular,Taro 3 整體架構(gòu)如下:
模擬實(shí)現(xiàn) DOM、BOM API
Taro 3 創(chuàng)建了 taro-runtime 的包,然后在這個(gè)包中實(shí)現(xiàn)了 一套 高效、精簡(jiǎn)版的 DOM/BOM API(下面的 UML 圖只是反映了幾個(gè)主要的類的結(jié)構(gòu)和關(guān)系):
- TaroEventTarget類,實(shí)現(xiàn)addEventListener和removeEventListener。
- TaroNode類繼承TaroEventTarget類,主要實(shí)現(xiàn)insertBefore、appendChild等操作 Dom 節(jié)點(diǎn)的方法。下面在頁(yè)面渲染我們會(huì)具體看這幾個(gè)方法的實(shí)現(xiàn)。
- TaroElement類繼承TaroNode類,主要是節(jié)點(diǎn)屬性相關(guān)的方法和dispatchEvent方法,dispatchEvent方法在下面講事件觸發(fā)的時(shí)候也會(huì)涉及到。
- TarorootElement類繼承TaroElement類,其中最主要是enqueueUpdate和performUpdate,把虛擬 DOM setData 成小程序 data 的操作就是這兩個(gè)函數(shù)。
然后,我們通過(guò) Webpack 的 ProvidePlugin 插件,注入到小程序的邏輯層。
Webpack ProvidePlugin 是一個(gè) webpack 自帶的插件,用于在每個(gè)模塊中自動(dòng)加載模塊,而無(wú)需使用 import/require 調(diào)用。該插件可以將全局變量注入到每個(gè)模塊中,避免在每個(gè)模塊中重復(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])}
這樣,在小程序的運(yùn)行時(shí),就有了 一套高效、精簡(jiǎn)版的 DOM/BOM API。
taro-react:小程序版的 react-dom
在 DOM/BOM 注入之后,理論上來(lái)說(shuō),react 就可以直接運(yùn)行了。
但是因?yàn)?React-DOM 包含大量瀏覽器兼容類的代碼,導(dǎo)致包太大。Taro 自己實(shí)現(xiàn)了 react 的自定義渲染器,代碼在taro-react包里。
在 React 16 ,React 的架構(gòu)如下:
最上層是 React 的核心部分 react-core ,中間是 react-reconciler,其的職責(zé)是維護(hù) VirtualDOM 樹(shù),內(nèi)部實(shí)現(xiàn)了 Diff/Fiber 算法,決定什么時(shí)候更新、以及要更新什么。
而 Renderer 負(fù)責(zé)具體平臺(tái)的渲染工作,它會(huì)提供宿主組件、處理事件等等。例如 React-DOM 就是一個(gè)渲染器,負(fù)責(zé) DOM 節(jié)點(diǎn)的渲染和 DOM 事件處理。
Taro實(shí)現(xiàn)了taro-react 包,用來(lái)連接 react-reconciler 和 taro-runtime 的 BOM/DOM API。是基于 react-reconciler 的小程序?qū)S?React 渲染器,連接 @tarojs/runtime的DOM 實(shí)例,相當(dāng)于小程序版的react-dom,暴露的 API 也和react-dom 保持一致。
這里涉及到一個(gè)問(wèn)題:如何自定義 React 渲染器?
第一步: 實(shí)現(xiàn)宿主配置( 實(shí)現(xiàn)react-reconciler的hostConfig配置)
這是react-reconciler要求宿主提供的一些適配器方法和配置項(xiàng)。這些配置項(xiàng)定義了如何創(chuàng)建節(jié)點(diǎn)實(shí)例、構(gòu)建節(jié)點(diǎn)樹(shù)、提交和更新等操作。即在 hostConfig 的方法中調(diào)用對(duì)應(yīng)的 Taro BOM/DOM 的 API。
1. 創(chuàng)建形操作
createInstance(type,newProps,rootContainerInstance,_currentHostContext,workInProgress)。
react-reconciler 使用該方法可以創(chuàng)建對(duì)應(yīng)目標(biāo)平臺(tái)的UI Element實(shí)例。比如 document.createElement 根據(jù)不同類型來(lái)創(chuàng)建 div、img、h2等DOM節(jié)點(diǎn),并使用 newProps參數(shù)給創(chuàng)建的節(jié)點(diǎn)賦予屬性。而在 Taro 中:
import { document } from '@tarojs/runtime'// 在 ReactDOM 中會(huì)調(diào)用 document.createElement 來(lái)生成 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)平臺(tái)允許創(chuàng)建純文本節(jié)點(diǎn)。那么這個(gè)方法就是用來(lái)創(chuàng)建目標(biāo)平臺(tái)的文本節(jié)點(diǎn)。
import { document } from '@tarojs/runtime'// Taro: 模擬的 document 支持創(chuàng)建 text 節(jié)點(diǎn), 返回 `document.createTextNode(text)` 即可.createTextInstance (text: string, _rootContainerInstance: any, _hostContext: any, internalInstanceHandle: Fiber) { const textNode = document.createTextNode(text) precacheFiberNode(internalInstanceHandle, textNode) return textNode},
2. UI樹(shù)操作
appendInitialChild(parent, child)
初始化UI樹(shù)創(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 在組件掛載到頁(yè)面中前調(diào)用,更新時(shí)不會(huì)調(diào)用。
這個(gè)方法我們下面事件注冊(cè)時(shí)還會(huì)提到。
finalizeInitialChildren (dom, type: string, props: any) { updateProps(dom, {}, props) // 提前執(zhí)行更新屬性操作,Taro 在 Page 初始化后會(huì)立即從 dom 讀取必要信息 // ....},
prepareUpdate(domElement, oldProps, newProps)
這里是比較oldProps,newProps的不同,用來(lái)判斷是否要更新節(jié)點(diǎn)。
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屬性,下文要講的事件注冊(cè)就是在這個(gè)方法里。
// Taro: 根據(jù) updatePayload,將屬性更新到 instance 中,// 此時(shí) updatePayload 是一個(gè)類似 `[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 成對(duì)出現(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ù)的實(shí)現(xiàn),源碼里還有很多回調(diào)函數(shù)的實(shí)現(xiàn),詳見(jiàn)trao-react源碼。
第二步:實(shí)現(xiàn)渲染函數(shù),類似于ReactDOM.render() 方法??梢钥闯墒莿?chuàng)建 Taro DOM Tree 容器的方法。
源碼實(shí)現(xiàn)詳見(jià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 方法,實(shí)際上就是react-reconciler包里面對(duì)應(yīng)的方法。
渲染函數(shù)是在什么時(shí)候被調(diào)用的呢?
在編譯時(shí),會(huì)引入插件taro-plugin-react, 插件內(nèi)會(huì)調(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實(shí)際上調(diào)用的就是trao-react的createRoot和render方法。
經(jīng)過(guò)上面的步驟,React 代碼實(shí)際上就可以在小程序的運(yùn)行時(shí)正常運(yùn)行了,并且會(huì)生成 Taro DOM Tree。那么偌大的 Taro DOM Tree 怎樣更新到頁(yè)面呢?
從虛擬 Dom 到小程序頁(yè)面渲染
因?yàn)?程序并沒(méi)有提供動(dòng)態(tài)創(chuàng)建節(jié)點(diǎn)的能?,需要考慮如何使?相對(duì)靜態(tài)的 wxml 來(lái)渲染相對(duì)動(dòng)態(tài)的 Taro DOM 樹(shù)。Taro使?了模板拼接的?式,根據(jù)運(yùn)?時(shí)提供的 DOM 樹(shù)數(shù)據(jù)結(jié)構(gòu),各 templates 遞歸地 相互引?,最終可以渲染出對(duì)應(yīng)的動(dòng)態(tài) DOM 樹(shù)。
模版化處理
首先,將小程序的所有組件挨個(gè)進(jìn)行模版化處理,從而得到小程序組件對(duì)應(yīng)的模版。如下圖就是小程序的 view 組件模版經(jīng)過(guò)模版化處理后的樣子。?先需要在 template ??寫?個(gè) view,把它所有的屬性全部列出來(lái)(把所有的屬性都列出來(lái)是因?yàn)?程序??不能去動(dòng)態(tài)地添加屬性)。
模板化處理的核心代碼在 packages/shared/src/template.ts 文件中。會(huì)在編譯工程中生成 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)生的頁(yè)面代碼是這樣的:
// pages/index/index.wxml<import src="../../base.wxml"/><template is="taro_tmpl" data="{{root:root}}" />
接下來(lái)是遍歷渲染所有?節(jié)點(diǎn),基于組件的 template,動(dòng)態(tài) “遞歸” 渲染整棵樹(shù)。
具體流程為先去遍歷 Taro DOM Tree 根節(jié)點(diǎn)的子元素,再根據(jù)每個(gè)子元素的類型選擇對(duì)應(yīng)的模板來(lái)渲染子元素,然后在每個(gè)模板中我們又會(huì)去遍歷當(dāng)前元素的子元素,以此把整個(gè)節(jié)點(diǎn)樹(shù)遞歸遍歷出來(lái)。
hydrate Data
而動(dòng)態(tài)遞歸時(shí)需要獲取到我們的 data,也就是 root。
首先,在 createPageConfig 中會(huì)對(duì) 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階段會(huì)調(diào)用HostConfig里的appendInitialChild方法完成頁(yè)面掛載,在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方法,傳入一個(gè)對(duì)象,值為 path 和 value,而 value 值是hydrate方法的結(jié)果。
hydrate方法我們可以翻譯成“注水”,函數(shù) hydrate 用于將虛擬 DOM(TaroElement 或 TaroText)轉(zhuǎn)換為小程序組件渲染所需的數(shù)據(jù)格式(MiniData)。
回想一下小程序員生的 data 里都是我們頁(yè)面需要的 state,而 taro 的hydrate方法返回的 miniData 是把 state 外面在包裹上我們頁(yè)面的 node 結(jié)構(gòu)值。舉例來(lái)看,我們一個(gè) 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 是小程序的實(shí)例,也就是執(zhí)行我們熟悉的 setData 方法把上面hydrate的 miniData賦值給 root,這樣就渲染了小程序的頁(yè)面數(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) { // 初次渲染,使用頁(yè)面級(jí)別的 setData normalUpdate = data } // ........ ctx.setData(normalUpdate, cb)}
整體流程可以概括為:當(dāng)在React中調(diào)用 this.setState 時(shí),React內(nèi)部會(huì)執(zhí)行reconciler,進(jìn)而觸發(fā) enqueueUpdate 方法,如下圖:
事件處理
事件注冊(cè)
在HostConfig接口中,有一個(gè)方法 finalizeInitialChildren,在這個(gè)方法里會(huì)調(diào)用updateProps。這是掛載頁(yè)面階段時(shí)間的注冊(cè)時(shí)機(jī)。updateProps 會(huì)調(diào)用 updatePropsByPayload 方法。
finalizeInitialChildren (dom, type: string, props: any) { updateProps(dom, {}, props) //....},
在HostConfig接口中,有一個(gè)方法 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' } // 通過(guò)addEventListener將事件注冊(cè)到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] } }}
可以看到事件會(huì)注冊(cè)到dom對(duì)象上,最終會(huì)放入到 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其中的一個(gè)模板,可以看到,所有組件中的事件都會(huì)由 eh 代理。在createPageConfig時(shí),會(huì)將 config.eh 賦值為 eventHandler。
// taro-runtime/src/dsl/common.tsfunction createPageConfig(){ const config = {...} // config會(huì)作為小程序 Page() 的入?yún)? config.eh = eventHandler config.data = {root:{cn:[]}} return config}
eventHandler 最終會(huì)觸發(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實(shí)例 } }}
至此,react 代碼終于是可以完美運(yùn)行在小程序環(huán)境中。
還要提到一點(diǎn)的是,Taro3 在 h5 端的實(shí)現(xiàn)也很有意思,Taro在 H5 端實(shí)現(xiàn)一套基于小程序規(guī)范的組件庫(kù)和 API 庫(kù),在這里就不展開(kāi)說(shuō)了。
總結(jié)
Taro 3從之前的重編譯時(shí),到現(xiàn)在的重運(yùn)行時(shí),解決了架構(gòu)問(wèn)題,可以用 react、vue 甚至 jQuery 來(lái)寫小程序,但也帶來(lái)了一些性能問(wèn)題。
為了解決性能問(wèn)題,Taro 3 也提供了預(yù)渲染和虛擬列表等功能和組件。
但從長(zhǎng)遠(yuǎn)來(lái)看,計(jì)算機(jī)硬件的性能越來(lái)越冗余,如果在犧牲一點(diǎn)可以容忍的性能的情況下?lián)Q來(lái)整個(gè)框架更大的靈活性和更好的適配性,并且能夠極大的提升開(kāi)發(fā)體驗(yàn),也是值得的。
作者:孟祥輝
來(lái)源:微信公眾號(hào):哈啰技術(shù)
出處:https://mp.weixin.qq.com/s/134VAXPJczElvdYzNFcHhA
版權(quán)聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶自發(fā)貢獻(xiàn),該文觀點(diǎn)僅代表作者本人。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如發(fā)現(xiàn)本站有涉嫌抄襲侵權(quán)/違法違規(guī)的內(nèi)容, 請(qǐng)發(fā)送郵件至 舉報(bào),一經(jīng)查實(shí),本站將立刻刪除。