React 代码跑在小程序上-- taro3 运行时
标题中我们提出一个问题:react 代码如何跑在小程序上? 目前看来大致两种思路:
-
把 react 代码编译成小程序代码,这样我们可以开发用 react,然后跑起来还是小程序原生代码,结果很完美,但是把 react 代码编译成各个端的小程序代码是一个力气活,而且如果想用 vue 来开发的话,那么还需要做一遍 vue 代码的编译,这是 taro 1/2 的思路。
-
我们可以换个问题思考,react 代码是如何跑在浏览器里的?
- 站在浏览器的角度来思考:无论开发用的是什么框架,React 也好,Vue 也罢,最终代码经过运行之后都是调用了浏览器的那几个 BOM/DOM 的 API ,如:
createElement、appendChild、removeChild
等。 - Taro 3 主要通过在小程序端模拟实现 DOM、BOM API 来让前端框架直接运行在小程序环境中。
- 站在浏览器的角度来思考:无论开发用的是什么框架,React 也好,Vue 也罢,最终代码经过运行之后都是调用了浏览器的那几个 BOM/DOM 的 API ,如:
下面我们具体看看各自的实现。
Taro 1/2
Taro 1/2 的架构主要分为:编译时 和 运行时。
其中编译时主要是将 Taro 代码通过 Babel
转换成 小程序的代码,如:JS、WXML、WXSS、JSON。
运行时主要是进行一些:生命周期、事件、data 等部分的处理和对接。
Taro 编译时
Taro 的编译,使用 babel-parser
将 Taro 代码解析成抽象语法树,然后通过 babel-types
****对抽象语法树进行一系列修改、转换操作,最后再通过 babel-generate
生成对应的目标代码。
整个编译时最复杂的部分在于 JSX 编译。
我们都知道 JSX 是一个 JavaScript 的语法扩展,它的写法千变万化,十分灵活。这里我们是采用 穷举 的方式对 JSX 可能的写法进行了一个一个地适配,这一部分工作量很大,实际上 Taro 有大量的 Commit 都是为了更完善的支持 JSX 的各种写法。
Taro 运行时
接下来,我们可以对比一下编译后的代码,可以发现,编译后的代码中,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
等进行了替换和重写,结合前面编译后 render
方法被替换,大家不难猜出:Taro 当前架构只是在开发时遵循了 React 的语法,在代码编译之后实际运行时,和 React 并没有关系。
而 createComponent
主要作用是调用 Component()
构建页面;对接事件、生命周期等;进行 Diff Data 并调用 setData
方法更新数据。
这样的实现过程有三⼤缺点:
- JSX ⽀持程度不完美。Taro 对 JSX 的⽀持是通过编译时的适配去实现的,但 JSX ⼜⾮常之灵活,因此还不能做到 100% ⽀持所有的 JSX 语法。 JSX 是一个 JavaScript 的语法扩展,它的写法千变万化,十分灵活。之前Taro团队是采用穷举的方式对 JSX 可能的写法进行了一个一个地适配,这一部分工作量很大。
- 不⽀持 source-map。Taro 对源代码进⾏了⼀系列的转换操作之后,就不⽀持 source-map 了,⽤户 调试、使⽤这个项⽬就会不⽅便。
- 维护和迭代⼗分困难。Taro 编译时代码⾮常的复杂且离散,维护迭代都⾮常的困难。
Taro 3
Taro 3 则可以大致理解为解释型架构(相对于 Taro 1/2 而言),主要通过在小程序端模拟实现 DOM、BOM API 来让前端框架直接运行在小程序环境中,从而达到小程序和 H5 统一的目的。
而对于生命周期、组件库、API、路由等差异,依然可以通过定义统一标准,各端负责各自实现的方式来进行抹平。
而正因为 Taro 3 的原理,在 Taro 3 中同时支持 React、Vue 等框架,甚至还支持了 jQuery,还能支持让开发者自定义地去拓展其他框架的支持,比如 Angular,Taro 3 整体架构如下:
模拟实现 DOM、BOM API
Taro 3 创建了 taro-runtime
****的包,然后在这个包中实现了 一套 高效、精简版的 DOM/BOM API(下面的 UML 图只是反映了几个主要的类的结构和关系):
TaroEventTarget
类,实现addEventListener
和removeEventListener
。TaroNode
类继承TaroEventTarget
类,主要实现insertBefore
、appendChild
等操作 Dom 节点的方法。下面在页面渲染我们会具体看这几个方法的实现。TaroElement
类继承TaroNode
类,主要是节点属性相关的方法和dispatchEvent
方法,dispatchEvent
方法在下面讲事件触发的时候也会涉及到。TaroRootElement
类继承TaroElement
类,其中最主要是enqueueUpdate
和performUpdate
,把虚拟 DOMsetData
成小程序 data 的操作就是这两个函数。
然后,我们通过 Webpack 的 ProvidePlugin
****插件,注入到小程序的逻辑层。
Webpack ProvidePlugin 是一个 webpack 自带的插件,用于在每个模块中自动加载模块,而无需使用 import/require 调用。该插件可以将全局变量注入到每个模块中,避免在每个模块中重复引用相同的依赖。
// trao-mini-runner/src/webpack/build.conf.ts
plugin.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.ts
export const getProviderPlugin = args => {
return partial(getPlugin, webpack.ProvidePlugin)([args])
}
这样,在小程序的运行时,就有了 一套高效、精简版的 DOM/BOM API。
taro-react:小程序版的 react-dom
在 DOM/BOM 注入之后,理论上来说,react 就可以直接运行了。
但是因为 React-DOM
包含大量浏览器兼容类的代码,导致包太大。Taro 自己实现了 react 的自定义渲染器,代码在taro-react
包里。
在 React 16 ,React 的架构如下:
最上层是 React 的核心部分 react-core
,中间是 react-reconciler
,其的职责是维护 VirtualDOM
树,内部实现了 Diff/Fiber
算法,决定什么时候更新、以及要更新什么。
而 Renderer
负责具体平台的渲染工作,它会提供宿主组件、处理事件等等。例如 React-DOM
就是一个渲染器,负责 DOM 节点的渲染和 DOM 事件处理。
Taro实现了taro-react
包,用来连接 react-reconciler
和 taro-runtime
的 BOM/DOM API。是基于 react-reconciler
的小程序专用 React 渲染器,连接 @tarojs/runtime
的DOM 实例,相当于小程序版的react-dom
,暴露的 API 也和react-dom
保持一致。
这里涉及到一个问题:如何自定义 React 渲染器?
第一步: 实现宿主配置( 实现react-reconciler
的hostConfig
配置)
这是react-reconciler
要求宿主提供的一些适配器方法和配置项。这些配置项定义了如何创建节点实例、构建节点树、提交和更新等操作。即在 hostConfig
的方法中调用对应的 Taro BOM/DOM 的 API。
1. 创建形操作
createInstance(type, newProps, rootContainerInstance, _currentHostContext, workInProgress)。
react-reconciler 使用该方法可以创建对应目标平台的UI Element实例。 比如 document.createElement 根据不同类型来创建 div、img、h2等DOM节点,并使用 newProps参数给创建的节点赋予属性。而在 Taro 中:
import { document } from '@tarojs/runtime'
// 在 ReactDOM 中会调用 document.createElement 来生成 dom,
// 而在小程序环境中 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
如果目标平台允许创建纯文本节点。那么这个方法就是用来创建目标平台的文本节点。
import { document } from '@tarojs/runtime'
// Taro: 模拟的 document 支持创建 text 节点, 返回 `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树创建。
// 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
在组件挂载到页面中前调用,更新时不会调用。
Taro: 遍历 props,更新到 instance 中即可,同时返回false,即不需要调用 commitMount
方法。
finalizeInitialChildren (dom, type: string, props: any) {
updateProps(dom, {}, props)
// 提前执行更新属性操作,Taro 在 Page 初始化后会立即从 dom 读取必要信息
if (type === 'input' || type === 'textarea') {
track(dom)
}
return false
},
prepareUpdate(domElement, oldProps, newProps)
这里是比较oldProps
,newProps
的不同,用来判断是否要更新节点。
prepareUpdate (instance, _, oldProps, newProps) {
return getUpdatePayload(instance, oldProps, newProps)
},
// ./props.ts
export 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)
此函数用于更新domElement属性,下文要讲的事件注册就是在这个方法里。
// Taro: 根据 updatePayload,将属性更新到 instance 中,
// 此时 updatePayload 是一个类似 `[prop1, value1, prop2, value2, ...]` 的数组
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 成对出现
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
里必要的回调函数的实现,源码里还有很多回调函数的实现,详见trao-react
源码。
第二步:实现渲染函数,类似于ReactDOM.render() 方法。可以看成是创建 Taro DOM Tree 容器的方法。
源码实现详见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
类最后调用TaroReconciler
的createContain``pdateContainer
和 getPublicRootInstance
方法,实际上就是react-reconciler
包里面对应的方法。
渲染函数是在什么时候被调用的呢?
在编译时,会引入插件 taro-plugin-react
, 插件内会调用 modifyMiniWebpackChain
=> setAlias
。
// taro-plugin-react/src/webpack.mini.ts
function setAlias (ctx: IPluginContext, framework: Frameworks, chain) {
if (framework === 'react') {
alias.set('react-dom$', '@tarojs/react')
}
}
这样ReactDOM.createRoot
和ReactDOM.render
实际上调用的就是trao-react
的createRoot
和render
方法。
经过上面的步骤,React 代码实际上就可以在小程序的运行时正常运行了,并且会生成 Taro DOM Tree
。那么偌大的 Taro DOM Tree 怎样更新到页面呢?
从虚拟 Dom 到小程序页面渲染
因为⼩程序并没有提供动态创建节点的能⼒,需要考虑如何使⽤相对静态的 wxml 来渲染相对动态的 Taro DOM 树。Taro使⽤了模板拼接的⽅式,根据运⾏时提供的 DOM 树数据结构,各 templates 递归地 相互引⽤,最终可以渲染出对应的动态 DOM 树。
模版化处理
首先,将小程序的所有组件挨个进行模版化处理,从而得到小程序组件对应的模版。如下图就是小程序的 view 组件模版经过模版化处理后的样子。⾸先需要在 template ⾥⾯写⼀个 view,把它所有的属性全部列出来(把所有的属性都列出来是因为⼩程序⾥⾯不能去动态地添加属性)。
模板化处理的核心代码在 packages/shared/src/template.ts
文件中。会在编译工程中生成 base.wxml
文件,这是我们打包产物之一。
// 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>
打包产生的页面代码是这样的:
// pages/index/index.wxml
<import src="../../base.wxml"/>
<template is="taro_tmpl" data="{{root:root}}" />
接下来是遍历渲染所有⼦节点,基于组件的 template,动态 “递归” 渲染整棵树。
具体流程为先去遍历 Taro DOM Tree 根节点的子元素,再根据每个子元素的类型选择对应的模板来渲染子元素,然后在每个模板中我们又会去遍历当前元素的子元素,以此把整个节点树递归遍历出来。
hydrate Data
而动态递归时需要获取到我们的 data,也就是 root。
首先,在 createPageConfig
中会对 config.data
进行初始化,赋值 {root:{cn:[]}}
。
export function createPageConfig (component: any, pageName?: string, data?: Record<string, unknown>, pageConfig?: PageConfig) {
// .......
if (!isUndefined(data)) {
config.data = data
}
// .......
}
React在commit
阶段会调用HostConfig
里的appendInitialChild
方法完成页面挂载,在Taro中则继续调用:appendInitialChild
—> appendChild
—> insertBefore
—> enqueueUpdate
。
// taro-react/src/reconciler.ts
appendInitialChild (parent, child) {
parent.appendChild(child)
},
appendChild (parent, child) {
parent.appendChild(child)
},
// taro-runtime/src/dom/node.ts
public 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
}
这里看到最终调用enqueueUpdate
方法,传入一个对象,值为 path
和 value
,而 value
值是hydrate
方法的结果。
hydrate
方法我们可以翻译成“注水”,函数 hydrate
用于将虚拟 DOM(TaroElement 或 TaroText)转换为小程序组件渲染所需的数据格式(MiniData)。
回想一下小程序员生的 data 里都是我们页面需要的 state,而 taro 的hydrate
方法返回的 miniData 是把 state 外面在包裹上我们页面的 node 结构值。举例来看,我们一个 helloword 代码所hydrate
的 miniData 如下(可以在小程序IDE中的 ”AppData“ 标签栏中查看到完整的data数据结构):
{
"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的内容更小。)
Container = 'container',
Childnodes = 'cn',
Text = 'v',
NodeType = 'nt',
NodeName = 'nn',
// Attrtibutes
Style = 'st',
Class = 'cl',
Src = 'src'
我们获取到以上的 data 数据,去执行enqueueUpdate
函数,enqueueUpdate
函数内部执行performUpdate
函数,performUpdate
函数最终执行 ctx.setData
,ctx
是小程序的实例,也就是执行我们熟悉的 setData
方法把上面hydrate
的 miniData
赋值给 root,这样就渲染了小程序的页面数据。
// taro-runtime/src/dom/root.ts
public 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)
}
整体流程可以概括为:当在React中调用 this.setState
时,React内部会执行reconciler
,进而触发 enqueueUpdate
方法,如下图:
事件处理
事件注册
在HostConfig
接口中,有一个方法 commitUpdate
,用于在react的commit阶段更新属性:
commitUpdate (dom, updatePayload, _, oldProps, newProps) {
updatePropsByPayload(dom, oldProps, updatePayload)
updateFiberProps(dom, newProps)
},
进一步的调用方法:updatePropsByPayload
=> setProperty
=> setEvent
。
// taro-react/src/props.ts
function 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)
}
}
进一步的看看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 内部变量 __handlers
中保存。
事件触发
// 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.ts
function createPageConfig(){
const config = {...} // config会作为小程序 Page() 的入参
config.eh = eventHandler
config.data = {root:{cn:[]}}
return config
}
eventHandler
最终会触发 dom.dispatchEvent(e)
。
// taro-runtime/src/dom/element.ts
class TaroElement extends TaroNode {
dispatchEvent(event){
const listeners = this.__handlers[event.type] // 取出回调函数数组
for (let i = listeners.length; i--;) {
result = listener.call(this, event) // event是TaroEvent实例
}
}
}
至此,react 代码终于是可以完美运行在小程序环境中。
还要提到一点的是,Taro3 在 h5 端的实现也很有意思,Taro在 H5 端实现一套基于小程序规范的组件库和 API 库,在这里就不展开说了。
总结
Taro 3从之前的重编译时,到现在的重运行时,解决了架构问题,可以用 react、vue 甚至 jQuery 来写小程序,但也带来了一些性能问题。
为了解决性能问题,Taro 3 也提供了预渲染和虚拟列表等功能和组件。
但从长远来看,计算机硬件的性能越来越冗余,如果在牺牲一点可以容忍的性能的情况下换来整个框架更大的灵活性和更好的适配性,并且能够极大的提升开发体验,也是值得的。
这篇好文章是转载于:学新通技术网
- 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
- 本站站名: 学新通技术网
- 本文地址: /boutique/detail/tanhgaifka
-
photoshop保存的图片太大微信发不了怎么办
PHP中文网 06-15 -
Android 11 保存文件到外部存储,并分享文件
Luke 10-12 -
word里面弄一个表格后上面的标题会跑到下面怎么办
PHP中文网 06-20 -
《学习通》视频自动暂停处理方法
HelloWorld317 07-05 -
photoshop扩展功能面板显示灰色怎么办
PHP中文网 06-14 -
微信公众号没有声音提示怎么办
PHP中文网 03-31 -
excel下划线不显示怎么办
PHP中文网 06-23 -
怎样阻止微信小程序自动打开
PHP中文网 06-13 -
excel打印预览压线压字怎么办
PHP中文网 06-22 -
TikTok加速器哪个好免费的TK加速器推荐
TK小达人 10-01