百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

React 代码如何跑在小程序上?

bigegpt 2024-08-24 23:08 2 浏览

标题中我们提出一个问题:react 代码如何跑在小程序上?目前看来大致两种思路:


1. 把 react 代码编译成小程序代码,这样我们可以开发用 react,然后跑起来还是小程序原生代码,结果很完美,但是把 react 代码编译成各个端的小程序代码是一个力气活,而且如果想用 vue 来开发的话,那么还需要做一遍 vue 代码的编译,这是 taro 1/2 的思路。


2. 我们可以换个问题思考,react 代码是如何跑在浏览器里的?

  • 站在浏览器的角度来思考:无论开发用的是什么框架,React 也好,Vue 也罢,最终代码经过运行之后都是调用了浏览器的那几个 BOM/DOM 的 API ,如:createElement、appendChild、removeChild 等。
  • Taro 3 主要通过在小程序端模拟实现 DOM、BOM 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,把虚拟 DOM setData 成小程序 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 在组件挂载到页面中前调用,更新时不会调用。


这个方法我们下面事件注册时还会提到。

finalizeInitialChildren (dom, type: string, props: any) {
  updateProps(dom, {}, props) 
  // 提前执行更新属性操作,Taro 在 Page 初始化后会立即从 dom 读取必要信息


  // ....
},


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的createContainr``updateContainer和 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#39;, '@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接口中,有一个方法 finalizeInitialChildren,在这个方法里会调用updateProps。这是挂载页面阶段时间的注册时机。updateProps 会调用 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)
},


进一步的调用方法: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 也提供了预渲染和虚拟列表等功能和组件。


但从长远来看,计算机硬件的性能越来越冗余,如果在牺牲一点可以容忍的性能的情况下换来整个框架更大的灵活性和更好的适配性,并且能够极大的提升开发体验,也是值得的。

作者:孟祥辉

来源:微信公众号:哈啰技术

出处:https://mp.weixin.qq.com/s/134VAXPJczElvdYzNFcHhA

相关推荐

Docker篇(二):Docker实战,命令解析

大家好,我是杰哥上周我们通过几个问题,让大家对于Docker有了一个全局的认识。然而,说跟练往往是两个概念。从学习的角度来说,理论知识的学习,往往只是第一步,只有经过实战,才能真正掌握一门技术所以,本...

docker学习笔记——安装和基本操作

今天学习了docker的基本知识,记录一下docker的安装步骤和基本命令(以CentOS7.x为例)一、安装docker的步骤:1.yuminstall-yyum-utils2.yum-con...

不可错过的Docker完整笔记(dockerhib)

简介一、Docker简介Docker是一个开源的应用容器引擎,基于Go语言并遵从Apache2.0协议开源。Docker可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,...

扔掉运营商的 IPTV 机顶盒,全屋全设备畅看 IPTV!

其实现在看电视节目的需求确实大大降低了,折腾也只是为了单纯的让它实现,享受这个过程带来的快乐而已,哈哈!预期构想家里所有设备直接接入网络随时接收并播放IPTV直播(电信点播的节目不是太多,但好在非常稳...

第五节 Docker 入门实践:从 Hello World 到容器操作

一、Docker容器基础运行(一)单次命令执行通过dockerrun命令可以直接在容器中执行指定命令,这是体验Docker最快捷的方式:#在ubuntu:15.10容器中执行ech...

替代Docker build的Buildah简单介绍

Buildah是用于通过较低级别的coreutils接口构建OCI兼容镜像的工具。与Podman相似,Buildah不依赖于Docker或CRI-O之类的守护程序,并且不需要root特权。Builda...

Docker 命令大全(docker命令大全记录表)

容器生命周期管理run-创建并启动一个新的容器。start/stop/restart-这些命令主要用于启动、停止和重启容器。kill-立即终止一个或多个正在运行的容器rm-于删除一个或...

docker常用指令及安装rabbitMQ(docker安装rabbitmq配置环境)

一、docker常用指令启动docker:systemctlstartdocker停止docker:systemctlstopdocker重启docker:systemctlrestart...

使用Docker快速部署Storm环境(docker部署confluence)

Storm的部署虽然不是特别麻烦,但是在生产环境中,为了提高部署效率,方便管理维护,使用Docker来统一管理部署是一个不错的选择。下面是我开源的一个新的项目,一个配置好了storm与mono环境的D...

Docker Desktop安装使用指南:零基础教程

在之前的文章中,我多次提到使用Docker来安装各类软件,尤其是开源软件应用。鉴于不少读者对此有需求,我决定专门制作一期关于Docker安装与使用的详细教程。我主要以Macbook(Mac平台)为例进...

Linux如何成功地离线安装docker(linux离线安装httpd)

系统环境:Redhat7.2和Centos7.4实测成功近期因项目需要用docker,所以记录一些相关知识,由于生产环境是不能直接连接互联网,尝试在linux中离线安装docker。步骤1.下载...

Docker 类面试题(常见问题)(docker面试题目)

Docker常见问题汇总镜像相关1、如何批量清理临时镜像文件?可以使用sudodockerrmi$(sudodockerimages-q-fdanging=true)命令2、如何查看...

面试官:你知道Dubbo怎么优雅上下线的吗?你:优雅上下线是啥?

最近无论是校招还是社招,都进行的如火如荼,我也承担了很多的面试工作,在一次面试过程中,和候选人聊了一些关于Dubbo的知识。Dubbo是一个比较著名的RPC框架,很多人对于他的一些网络通信、通信协议、...

【Docker 新手入门指南】第五章:Hello Word

适合人群:完全零基础新手|学习目标:30分钟掌握Docker核心操作一、准备工作:先确认是否安装成功打开终端(Windows用户用PowerShell或GitBash),输入:docker--...

松勤软件测试:详解Docker,如何用portainer管理Docker容器

镜像管理搜索镜像dockersearch镜像名称拉取镜像dockerpullname[:tag]列出镜像dockerimages删除镜像dockerrmiimage名称或id删除...