万字长文 - Web容器技术解析

489 阅读38分钟

一、前端容器技术概览

1.1 技术范式演进与架构革新

前端容器技术是实现跨平台动态化的核心载体,其本质是通过标准化运行时环境解耦业务代码与宿主平台。根据渲染管线与通信机制的差异,可分为三大技术范式:

1. WebView容器(Hybrid架构)

  • 技术特征:基于系统WebView渲染引擎,通过JSBridge实现原生能力扩展
  • 演进节点:
    • Cordova(2011)建立基础插件体系
    • 微信小程序(2017)定义标准化容器协议
    • 快应用(2018)实现系统级集成优化
  1. 原生渲染容器(React Native架构)
  • 技术突破:引入虚拟DOM与异步通信机制,JavaScript线程与原生UI线程分离
  • 关键创新:
    • React Native(2015)开创声明式UI开发范式
    • Hermes引擎(2019)优化JS执行性能
    • 新架构Fabric(2022)提升渲染管线效率
  1. 自渲染引擎容器(Flutter架构)
  • 颠覆性设计:Skia图形引擎直接操作Canvas,实现像素级渲染控制
  • 演进路线:
    • Flutter 1.0(2018)建立Dart生态基础
    • Impeller引擎(2022)解决Skia卡顿问题
    • WebAssembly支持(2024)突破性能瓶颈
1.2 技术选型决策模型

核心评估维度:

维度WebView方案RN方案Flutter方案
动态化能力⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
渲染性能⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
跨端一致性⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
生态丰富度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

典型应用场景决策树:

2. 主流容器技术架构对比

对比维度WebView容器React Native容器Flutter容器
渲染方式系统WebView渲染原生组件桥接自研Skia引擎绘制
通信机制JSBridge异步通信批量异步Bridge通信PlatformChannel同步通信
UI开发范式HTML/CSS/JSReact声明式JSXDart Widget树
动态化能力支持远程H5热更新CodePush热更新受限(需编译二进制)
性能瓶颈JS解析与DOM操作Bridge通信延迟GPU纹理切换开销
典型应用微信小程序、快应用携程/美团部分业务模块闲鱼/字节系核心场景

二、WebView 容器介绍

架构设计

H5 和小程序架构设计(小程序产生的逻辑)

在在WebView 模式下,主流的技术落地有两种:一种是嵌入 H5 的混合 App,另外一种是小程序。这两种方式在渲染流程和通信流程上有一定区别。

在渲染流程中,WebView H5 方案类似于传统的 Web 应用,先由 Native 打开一个 WebView 容器,WebView 就像浏览器一样,打开 WebView 对应的 URL 地址,然后进行请求资源、加载数据、绘制页面,最终页面呈现在我们眼前。

但是,小程序的 WebView 方案有所不同。小程序采用双线程架构,分为逻辑层和渲染层。

首先也是 Native 打开一个 WebView 页面,渲染层加载 WXML 和 WXSS 编译后的文件,同时逻辑层用于逻辑处理,比如触发网络请求、setData 更新等等。接下来是请求资源,请求到数据之后,数据先通过逻辑层传递给 Native,然后通过 Native 把数据传递给渲染层 WebView,再进行渲染。

WebView H5 的通信流程也很简单,由 DOM 触发事件,像 Vue 或者 React 构建的 Web 应用会响应事件,然后通过数据驱动,更新视图。

但是在小程序中,触发的事件首先需要传递给 Native,再传递给逻辑层,逻辑层处理事件,再把处理好的数据传递给 Native,最后 Native 传递给渲染层,由渲染层负责渲染。

小程序架构模型:

H5 模式我们再熟悉不过了,不再赘述,重点介绍小程序逻辑层和渲染层的架构设计。注意,这里的小程序不限于微信小程序,市面上小程序的方案都是架构上大体一致的,包括支付宝小程序、京东小程序、美团小程序等。(天下文章一大抄 or 好的设计,心有灵犀~

逻辑层处理

小程序的逻辑层,就是在小程序 js 文件中写的业务逻辑。它和单页面应用 SPA 类似,以 React 框架为例,JSX 语法能够形象地表述出页面的结构,但其本质仍是 JS。页面即组件,组件本质上是函数,如果不做代码分割,所有的代码都会打包成一个 js 文件。
小程序也是这样,如果想在小程序中开发一个页面,那么首先在 app.json 中注册页面。

{
  "pages":[
    "pages/index/index", 
  ],
    "window":{
    "backgroundTextStyle":"light",
      "navigationBarBackgroundColor": "#fff",
      },
}

如上在 pages 属性下加入 pages/index/index ,就注册了第一个页面,然后我们在项目结构中创建对应的文件,如下所示:

在 index.js 中简单写一个 demo:

Page({
  data:{
    message:'hello,world',
    context:['小程序''React Native']
  },
  onLoad(){
    console.log('===小程序页面 onLoad 执行==>')
    console.log(window) // undefined

  },
  onReady() {
    console.log('===小程序页面 onReady 执行==>')
  },
  handleClick(){
    console.log('点击事件')
  }
})

在 WXML 中这么写:

<view bind:tap="handleClick" >hello,world</view>

在浏览器环境下是不存在 Page 等函数的,并且如上在 onLoad 函数中,打印 window 对象为 undefined,这就说明逻辑层的 runtime 运行时并不是浏览器环境提供的。

小程序会有很多页面,一般情况下主包内容会被打包到一起,形成一个 js 文件,我们先称之为 app-service.js。这样一来,像 WebView 打开 pages/index/index 页面,逻辑层就会执行 app-service.js 中对应的代码,打开一个 webview,大致如下实现方式:

/* 存放对应的页面 */
self.source_code.pages = [
  {
    name: 'pages/index/index', //对应的页面路径
    source: { // js 逻辑资源
      jsCode: function (exports, require, module) {
        module.exports = function (wx, App, Page, Component, getApp, global) {
          // 编译后小程序业务代码,这样就可以获取 wx,Page,Component 属性。
          // 业务代码
        }
      },
      jsJson: {...}
    }
  },
  {
    name: '/app' // 小程序 app 文件
      source: {
  jsCode: function (exports, require, module) {
    module.exports = function (wx, App, Page, Component, getApp, global) {
      // 业务代码 
      App({})
    }
  },
}

];
/* 存放对应的组件 */
self.source_code.components = []
/* 存放正常的 js 文件 */
self.source_code.modules = []

根据小程序逻辑层编译之后的代码,可以看到对于页面(Page)层面的结构存放到了一个数组 pages 里。比如 pages/index/index:

  • 对于 index.js 里面的代码,可以用 jsCode 函数包装;
  • 对于组件(Component)层面的文件放在 components 数组里面;
  • 对于一些其他的 js,可以用 modules 数组来保存。

在小程序中,像是页面文件 pages/index/index.js,暴露出一个方法:

exports.a = function(){}
Page({ ... })

如果在另外的一个组件文件中引入 a:

const index = require('./index')
console.log(index.a) //  function a

这会让 index 页面渲染不出来,原因是此时的 index.js 是按照 module 维度编译的,而不是按照 page 维度编译的。

原因是:

  • 页面文件(如 index.js)需要按照页面维度编译,以确保它们能够遵循小程序的生命周期和结构。
  • 如果页面文件被当作模块维度编译,那么它们可能无法正确地遵循小程序的页面生命周期和结构,从而导致页面无法渲染。

驱动逻辑层

逻辑层的运行离不开 js 引擎,以 JavascriptCore 为例,本质上就是执行 js 代码,也就是我们在小程序写的业务代码,因为此时的运行环境并不是浏览器内核提供的,这也就说明为什么我们不能在小程序中使用 DOM 相关的 API 了。

而 js 引擎是通过客户端宿主环境运行的,比如微信小程序,那么运行小程序的宿主环境就是微信客户端。

小程序基础库

整个逻辑层,除了写在业务层的代码,还有小程序在逻辑层注入的基础库,比如在微信中的 wx 对象,以及上面提到的 Page、Component、Behaivor 等方法。总结一下小程序的基础库做了些什么:

  • 小程序基础库负责驱动整个业务逻辑 js 的执行、运行,并维持整个小程序应用;
  • 提供小程序运行时所需的各种 API。
多页面架构

小程序模式采用了多页面架构,一个小程序可以存在多个 WebView 页面,可以对比原生应用来理解。

在原生应用中,以 iOS App 为例,页面之间的导航是以页面为单位的堆栈,注意这里不同于 web 堆栈,iOS 应用的堆栈保存的是当前页面,页面切换仍然存在,除非出栈,可以简单理解为添加了 keppalive 的 web 堆栈,所以能做到页面间随意跳转也能很丝滑,

小程序页面导航相对 web 更加丝滑的原因也在于此,小程序存在多个 WebView,页面跳转时就会重新创建一个 WebView。这样一来,两个 WebView 页面会共存到小程应用中,返回时就会有原生应用中的丝滑效果了。

但是这样的页面堆栈逻辑也会最大内存开销,开辟过多堆栈会导致应用卡顿,所以会存在堆栈数量限制。以微信小程序为例子,页面栈最大数量为 10 个,所以此时需要小程序的开发者控制页面栈的层数。

小程序常用的路由跳转方法有:navigateTo、redirectTo、navigateBack,我们一一来看。

  • navigateTo:打开新页面,新页面入栈。
  • redirectTo:页面重定向,当前页面出栈,而后新页面入栈。
  • navigateBack:页面回退,页面一直出栈,到达指定页面停止。

虽然渲染层采用多个 WebView 页面,但是逻辑层还是在同一个线程处理多个页面栈的。在小程序中,用 Navigator 去控制页面状态,用 pageStack 来保存页面栈的信息。

视图层处理

WebView 视图层

首先,我们来说下视图层。我们在小程序写的 WXML 结构最后会被转变成 HTML 结构,写的 WXSS 结构,最后被转变成 CSS 结构。

渲染层大致逻辑

拿上面的 pages/index/index 为例子,视图层 WebView 最终的产物是一个 html 文件,如下所示:

<!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>page/index/index</title>
        </head>
        <body>
          <!-- 小程序基础库 -->
            <script src="/page-frame/app.js" ></script>
            <script>
              function bootstrap(){
                /* 页面逻辑  */
                window._pageName  = 'page/index/index'
                var script = document.createElement('script')
              /* 加载视图层的 js */
              script.setAttribute('type','text/javascript')
              script.setAttribute('src','/_app/app-view.js')
              document.body.append(script)
              }
              bootstrap()
            </script>
          </body>
        </html>

每当小程序打开一个页面,本质上就是打开了一个新的 HTML 文件,接下来依次加载小程序的基础库 JS 和视图层的 JS 代码。那么,这两个 JS 是做什么的?

小程序基础库负责渲染、通信、底层基建等工作,包括怎么把代码渲染到页面上,怎么和逻辑层做通信。我们在传统 SPA 应用中,如果是用 React 框架构建的应用,那么 React 框架本身就类似于小程序的基础库。

视图层的 JS 就很好理解了,可以理解成我们写的模版结构,比如微信的 WXML。在 WebView 环境下,只能识别 HTML、CSS 和 JS,不能够直接识别 WXML,需要先将 WXML 转成语法树结构,不过这些工作在小程序编译上传阶段就已经完成了。

视图层渲染

语法树(Syntax Tree)是源代码语法结构的一种抽象表示,它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构

通过以上的 script 加载视图层,在 app-view 中会保留小程序编译后的语法树信息,这样就可以通过基础库来识别语法树绘制页面。比如我们在 page/index/index 对应的 WXML 这么写:

<view class="container {{ show ? 'in' : 'out' }}"  >
  <view bind:tap="handleClick" >{{ message }}</view>
</view>

如上是一段小程序模版结构,它最后会变成什么? 在正式回答这个问题之前,我们先来看下 React 中的 jsx 结构,本质上会被编译成jsx(之前是 createElement 方案)形式。如下:

babel

<div class="container" >hello,world</div>
// 之前是 createElement 方案
import { jsx as _jsx } from "react/jsx-runtime";
/*#__PURE__*/_jsx("div", {
  class: "container",
  children: "hello,world"
});

以上代码在 React 应用 render 阶段执行,返回的对象结构会直观描述出元素的结构与层次。

同样的,小程序也符合这个流程,在编译阶段把 WXML 转成语法树结构,再在小程序运行时执行语法树,得到页面元素结构并且渲染。

把 WXML 结构通过一个 render 函数来承载,在渲染层渲染页面的时候执行 render 函数,同时传入 context,context,那它主要做了些什么呢?

  • 通过 view、text、image 等标签渲染对应的元素,可以通过 renderNode 来实现。renderNode 和 createElement 类似,接受多个属性,第一个属性为类型,第二个属性为标签属性,比如绑定的 props 等;
  • 如果我们想把逻辑层 data 或者是 props 中的数据渲染到视图层,比如我们在 WXML 中获取 data 中的 message 属性,或者通过 data 中 show 属性判断 class 是 in 还是 out,那么需要 getPropsData 去获取逻辑层的数据;
  • 如果要把多个 class 合并,可以通过 mergeClass 来实现。

我们再看渲染层的流程:首先通过 WebView 加载 html js 等资源,然后加载基础库和视图层的代码,假如是 pages/index/index 页面,那么找到对应 render 函数。接下来通过 render 函数,以及 context 提供的各种方法 就可以把页面结构表现出来,再通过基础库转变成真实的 DOM 结构并渲染出来,这样就完成了渲染流程。

 小程序通信方式以及通信细节

setData 介绍及原理

setData 是一个用于更新小程序页面数据的方法,使用的时候传入一个对象作为参数,更改数据的同时,小程序会把数据同步通信到渲染层,实现了页面的动态更新。

同时,setData 的第二个参数,在渲染视图完毕后执行,执行的时候状态已经更新,并且视图已经渲染完毕。这一点和 React 中 Class 组件的 setData 很类似,可以说小程序的涉及参考了 React、Vue 等很有优秀框架的思想。

setData(object,callback)

setData 传递数据注意事项: 在 setData 中并不是所有的数据类型都能通过视图层传递给渲染层。打个比方,我们想通过 setData 把函数传递给子组件。

// js 
this.setData({
  message:{
    name:'大前端跨端开发指南',
    fn:()=>{},
    setProps:new Set()
  }
})
  //wxml 
  <children message="{{ message }}"  />

如上我们将 message 属性传递给 children 组件,message 属性中有一个函数 fn 和一个特殊的数据类型 Set setProps。

子组件中,正常情况下是拿不到函数 fn 和 setProps 。

本质原因是小程序采用的是双线程架构,当通过 setData 将数据通过逻辑层传递到渲染层的时候,底层实现是使用了 JSON 序列化和反序列化的方式来更新数据,而函数不是合法的 JSON 数据类型,无法被正确处理,而 set 会被处理成对象结构。这一点和 React Native 很相似,不过在最新中,RN 视图层和 Native 的通信已经是 C++的模块了,大大减少了转换开销。

setData 原理简述 了解一下 setData 的底层实现。 一次 setData 主要做了哪些事情:

  • 首先在 Component 或者 Page 实例上合并 data;
  • 然后把第一个参数 callback 放到回调函数队列 queue;
  • 接下来发起数据通信,把数据通过桥的方式,由逻辑层 -> Native -> 渲染层,完成视图层更新;
  • 最后执行对应的 callback 函数,完成整个 setData 流程。

流程图如下:

setData 优化注意事项

不合理运用 setData ,会严重影响小程序的性能。如下所示:

  • 当用 setData 渲染一个很大的数据结构的时候,需要将整个数据由逻辑层传递到渲染层,这样就会传递一个很大的数据结构,会存在一个比较大的转换开销,导致渲染时间变长,比如小程序应用在初始化的时候,整个页面用 setData 一次性渲染很多模块。
  • 频繁使用 setData 的时候也会造成性能问题。其中最重要的一个原因就是 setData 阻塞,在 setData 数据传递过程中浪费了大量的时间,导致渲染层没有空余时间完成视图的绘制与响应。比如当一个列表存在很多 item 的时候,如果改变一次状态需要更新每一个 item,就会导致列表中的每一个 item 都触发一次 setData ,那么短时间内会触发大量的 setData,从而造成阻塞,进而影响性能。
  • 一次性 setData 影响范围广、渲染范围广,这是一个比较特殊的场景。比如有一个状态 state,它有很多组件在使用。当这个状态改变的时候,就会有很多组件需要更新,这也会造成渲染任务非常大。

其中重要的性能瓶颈点有:

  • 架构设计上的影响:受到小程序双线程通信架构的影响,从 setData 原理上,我们能明确出一次 setData 到视图的改变,是由业务逻辑层 -> 宿主 Native -> 视图渲染层。相比于单纯的 H5 应用可以直接通过 DOM api 去改变视图,小程序的这种方式会让数据流程更加复杂、更长,存在更大的开销。因此,小程序的架构设计决定了 setData 是性能瓶颈。
  • setData 的:频繁使用 setData,或者 setData 大量的数据都会造成性能问题。

setData 优化手段:控制 setData 的频率

明白了 setData 影响性能的原因之后,我们来看看 setData 的性能优化手段,具体还要从频和量两个方面入手。

1. 合并 setData :减少 setData 的调用次数,尽可能合并多个 setData 操作,比如:

this.setData({
  name: 'alien',
})
this.setData({
  age:18
})

可以合并成如下的样子:

this.setData({
  name: 'alien',
  age:18
})

2. 状态提升数据预处理:这种场景也比较多,比如当页面中有很多列表子组件 item 的场景,并且每一个元素可能通过 props 状态属性判断来决定展示内容,这样就要在 item 中触发 setData ,结果就是在短时间内触发大量的 setData 。举一个例子:

list = [ {
  'id' : '1',
  'giftName' : '约斯夫家庭校园多功能创可贴卡通女少女可爱超弹防水透气弹力小面积开放性创伤创口贴 超弹防水透气型 100贴/盒',
  'giftImage' : 'https://img14.360buyimg.com/n1/jfs/t1/117043/23/16493/438028/5f50a682E96819e0d/a3678e5c4fb5a3cf.jpg',
  'price' : '19.90',
  status: 1
}, {
  'id' : '2',
  'giftName' : '【MaincareBio】医用外科口罩一次性无菌三层透气成人挂耳式防细菌病毒飞沫防护医用口罩 儿童医用外科口罩50只【10只/包*5包】',
  'giftImage' : 'https://img14.360buyimg.com/n1/jfs/t1/133614/39/16312/128620/5fb3a1b8E02fec0c6/0b7d82a132932f35.jpg',
  'price' : '39.90',
  status: 1
}, {
  'id' : '3',
  'giftName' : '乐樊一次性医用外科口罩医生专用成人通用三层医疗口罩透气单片防护 医用外科口罩100只蓝色【非独立包装/2包】',
  'giftImage' : 'https://img14.360buyimg.com/n1/jfs/t1/151889/33/15018/129441/6008e066Ee813ef0d/1f1a8218fa30a05f.jpg',
  'price' : '31.90',
  status: 1
}, {
  'id' : '4',
  'giftName' : '俏东方 一次性医用口罩白色 轻薄透气 三层防护含熔喷过滤成人男女适用冬季防护面罩 50只医用口罩白色整包(工厂特惠)',
  'giftImage' : 'https://img14.360buyimg.com/n1/jfs/t1/164271/11/7365/212791/6032be25E162107e3/df794675c5095edf.jpg',
  'price' : '9.90',
  status: 2
}, {
  'id' : '5',
  'giftName' : '【7仓隔日达】咔贝爱(KABEIAI)一次性医用防护口罩防尘防雾霾防颗粒物 三层防护透气医用口罩 医用口罩50只(1包)',
  'giftImage' : 'https://img14.360buyimg.com/n1/jfs/t1/156216/4/9112/168310/601e5d2aE4ad9ee3b/65a25f358d136a20.jpg',
  'price' : '19.90',
  status: 0
}]

function requestData(){
  return new Promise(reslove=>{
    reslove(list)
  })
}

Page({
  data:{
    dataList:[]
  },
  onLoad(){
    this.getInitDataSource()
  },
  onReady() {
  },
  async getInitDataSource(){
    const dataList = await requestData() 
    this.setData({
      dataList
    })
  }
})

wxml:

<view>
  <block wx:for="{{ dataList }}"  >
    <item dataItem="{{ item }}"  />
  </block>
</view>

如上,我们简单模拟了一个数据请求列表数据的流程,并且把数据的每一项传递给每一个 item 组件。这里有一个特殊的属性 status ,每一个 item 组件需要根据 status 值来展示不同的文本颜色,那么子组件如下这么写:

js:

Component({
  properties: {
    dataItem:{
      type:Object
    }
  },
  lifetimes: {
    ready(){
      const { status } = this.properties.dataItem
      let color = '#ccc'
      if(status === 0){
        color = 'pink'
      }else if(status === 1){
        color = 'rgb(193, 192, 255)'
      }
      this.setData({
        color
      })
    }
  },
  data:{
    color:''
  },
})
<view class="item" >
  <image src="{{dataItem.giftImage}}" class="item-image" />
  <text class="item-name" style="color:{{color}}"  >{{ dataItem.giftName }}</text>
</view>

从上面我们可以看到,需要 status 来控制展示的文本的颜色,有一个通用的处理逻辑就是在每一个 item 组件的 ready 生命周期函数中通过 properties 属性中 dataItem 的 status 属性来触发 setData 让文本渲染不同的颜色。我们先来看看效果:

这种更新模式就会造成短时间触发大量的 setData ,进而影响性能。其中一种解决方案就是状态提升,将 item 中的 color 属性,提升到父级中。接下来修改一下代码,page 中的 js 修改 getInitDataSource 方法。

async getInitDataSource(){
  const dataList = await requestData() 
  this.setData({
    /*  预处理状态,把每一个 item 加入 color 属性 */  
    dataList:dataList.map(item=>{
      const { status } = item
      let color = '#ccc'
      if(status === 0){
        color = 'pink'
      }else if(status === 1){
        color = 'rgb(193, 192, 255)'
      }
      item.color = color
      return item
    })
  })
}

如上我们在这个函数中预处理数据,遍历 item 加入 color 属性。这样在 item 组件中就不需要再 setData 数据。

在子组件 item 的 wxml 中,可以直接这么写,来达到相同的效果:

<view class="item" >
  <image src="{{dataItem.giftImage}}" class="item-image" />
  <text class="item-name" style="color:{{dataItem.color}}"  >{{ dataItem.giftName }}</text>
</view>

这样就不需要每一个 item 再触发 setData ,大幅度减少了 setData 的数量。

出现这种问题的本质原因是:小程序 WXML 相比于 React 那种 Render + JSX 语法来说,数据预处理的能力比较弱。在小程序中,WXML 在编译阶段转化成语法树 AST 了,并且将数据进入到解析语法树对应的处理函数中,在此期间缺乏对源数据处理的能力。而在 Render + JSX 模式中,JSX 会被 Babel 编译成 createElement 形式,Element 直接描述了页面或者组件的整体结构,并且开发者可以在 Render 函数中对数据源进行预处理。

3. WXS 状态预处理数据: 如果不想用状态提升的方式,我们也可以用 WXS 对数据进行预处理。如上案例中我们不需要在父组件和子组件的 JSX 中做些什么,只需要在子组件的 WXML 中这么写:

<wxs module="module" >
  function handleStatusColor(status){
    var color = '#ccc';
  if(status === 0){
    color = 'pink';
  }else if(status === 1){
    color = 'rgb(193, 192, 255)';
  };
  return color;
  };
  module.exports = {
    handleStatusColor:handleStatusColor,
  };
</wxs>
  <view class="item" >
  <image src="{{dataItem.giftImage}}" class="item-image" />
  <text class="item-name" style="color:{{module.handleStatusColor(dataItem.status)}}"  >{{ dataItem.giftName }}</text>
  </view>

也能达到相同的目的,因此用 WXS 做数据预处理也是一个非常不错的选择,在一定程度上减少了 setData 的频率。

不过,使用 WXS 有一些注意事项:

  • 首先 WXS 的执行并不在逻辑层,所以我们无法在 WXS 引入逻辑层的状态;
  • WXS 只是 JS 的一个子集,只有一些基本数据的处理能力,像 Object 构造函数在 WXS 就不存在,使用的时候应该重点注意;
  • WXS 会有一些特殊的语法限制,这在 JS 中是不存在的。

4. 非渲染的数据不用 setData : 在微信小程序中,有很多状态没必要用 setData 来处理,所以我们可以将数据进行分类,只有用于视图渲染的数据才保存在 data 中,用来做渲染或者传递给子组件。对于一些不需要渲染或者传递的数据,直接绑定在 this 实例上就可以了。比如如下代码逻辑:

async onLoad(){
  /* 获取初始化数据 */
  const data = await this.getInitCacheData()
  this.cacheData = data
},

我们通过 getInitCacheData 获取到数据源,但是这些数据用于处理业务逻辑,不用渲染,这样我们就把数据直接绑定在 this 上就可以了。

5. setData 的防抖与节流:在一些场景下,可能频繁触发 setData,比如 scrollview 组件滑动的过程中频繁使用 setData 改变状态。这个时候,就可以用防抖函数来处理。代码片段如下所示:

Component({
  lifetimes:{
    ready(){
      /* 立即执行的防抖函数,时间为 500 毫秒 */
      this.handleScroll = immediateDebounce(this.handleScroll.bind(this),500)
    }
  },
  methods:{
    handleScroll(){
      /* 修改状态 */
      this.setData({...})
    }
  },
})

setData 优化手段-控制渲染优先级

控制元素渲染的优先级: 还有一些 setData 的场景也会影响性能和用户体验。比如下面这种:

如上当点击清空购物车的时候,要触发两个状态:

  • 需要 setData 让弹窗消失;
  • 需要 setData 让每一条数据购物车数量重置为 0。

如果我们让这两个 setData 更新任务一起进行,就会更新大量的元素,也就会让弹窗消失变得卡顿,是很不好的用户体验。所以我们要给更新任务设置优先级,当点击 清空购物车 的时候,我们更期望弹窗立即消失,然后每个商品元素清空购物车数据。

因此,弹窗消失就有一个更高的优先级。我们都知道,在 React 框架中这个操作可以通过 transition 来完成,但是在小程序中没有对应的 api,不过我们可以通过 setTimeout 来模拟实现。代码如下所示:

/* 清空购物车 */
clearShoppingCart(){
  /* 隐藏弹窗 */
  this.setData({
    modelShow:false
  })
  setTimeout(()=>{
    /* 改变 item 的购物车的数量 */
    this.setData(()=>{
      cartList:this.data.cartList.map(item=>{
        item.count = 0
        return item
      })
    })
  },200)
},

如上,通过 setTimeout 将 setData 进行任务优先级的区分,保证了良好的用户体验。

一些小小的经验

重写 小程序 page 和 component 类,拦截相应的生命周期以及方法,比如在 onLoad 的时候做一些埋点、生命周几劫持的操作,

亦或者对setData 进行重新,添加一些批处理机制,完善小程序没有的优化逻辑、等等

function WrapPage (options,originPage = Page){
  originPage({
    onLoad(){
      const _this = this 
      this.setData = function (data,callback){
        /* 这里可以对 setData 进行了包装处理,比如一定时间内统计 setData 的数量 ,或者计算已从 setData 传输数量 */
        const startTime = new Date().getTime()
        _this.setData(data,()=>{
          const endTime = new Date().getTime()
          console.log('渲染时间:',endTime - startTime)
          callback()
        })
      }
    }

  })
}
export default WrapPage

三、 React Native 容器介绍

RN 是我司 App 核心方案,携程的技术栈就是以 RN 出名的,核心就是以原生 App 为基座,或者容器,通过渲染引擎,将 js 代码动态渲染成原生组件,类似于 SPA 应用中将 JS -> 虚拟 DOM ->真实 Dom 的过程,有点是即有原生组件的页面性能,又有 JS 的动态化,可以实现增量更新之类的动态操作。在很多大厂都有很好的实践。

1. Native 渲染 vs. WebView 渲染

RN 与传统 WebView 模式的核心差异在于渲染方式:

对比维度WebView 渲染Native 渲染(RN)
技术本质基于浏览器内核渲染 HTML/CSS/JS通过 Native 组件(如 Android 的 View、iOS 的 UIView)直接渲染
性能表现首屏加载依赖网络,白屏时间长;交互流畅度较低(如长列表滚动)首屏加载更快;交互体验接近原生
动态化能力支持在线资源动态更新,但受限于网络JS Bundle 可动态下发,支持热更新
原生能力调用需通过桥接(如 JSBridge)与 Native 通信,流程复杂通过 JSI(JavaScript Interface)直接调用 Native 方法,性能更优

2. 为什么选择 React Native?

1. Web 方案的局限性
  • 性能瓶颈:H5 首屏加载慢,动画/手势交互卡顿。
  • 原生能力受限:调用摄像头、传感器等需复杂桥接,流程繁琐。
  • 体验割裂:UI 风格与原生应用不一致。
2. 原生开发的不足
  • 开发成本高:需为 iOS/Android 双端独立开发,维护成本翻倍。
  • 迭代效率低:依赖应用商店审核,紧急修复难以及时生效。
  • 包体积膨胀:原生代码直接打包,影响用户下载体验。
3. RN 的核心优势
  • 跨端复用:一套代码兼容 iOS/Android,降低开发成本。
  • 动态化能力:JS Bundle 独立于原生包,支持热更新。
  • 生态成熟:基于 React 生态,社区活跃,学习成本低。

3. RN 的构成

RN 本质上是用 JS 作为逻辑层,用 Native 作为渲染层,还需要一个中间层作为逻辑层与渲染层的通信。所以 RN 本质上由逻辑层 JS,渲染层 Java (Android)和 iOS (Objective-C),以及通信层 C++ 组成。

其中需要 JS 引擎来运行 RN 的 JS 代码,那么我们来看一下 RN 中的 JS 引擎。

4. JS 引擎演进与 JSI

4.1. 主流 JS 引擎对比

引擎特点适用场景
V8支持 JIT 编译,执行速度快;内存占用较高Chrome、Node.js
JavaScriptCoreiOS 默认引擎,轻量级;Android 适配差RN 旧版(iOS 平台)
Hermes专为移动端优化,支持预编译字节码;启动快,内存占用低RN 新版(Android/iOS)

4.2. JSI(JavaScript Interface)的作用

JSI(JavaScript Interface)通过C++层实现JS与原生直接内存访问,核心改进包括:

  • 同步调用:JS线程可直接调用原生方法,无需消息队列。

  • 共享内存:JS与C++对象共享同一内存空间,避免数据拷贝。

  • 类型安全:通过C++模板自动转换数据类型(如jsi::String → std::string)。

在 iOS 应用中默认为 JSCore 引擎,这使得 RN 也用 JSCore,但是 JSCore 没有对 Android 机型做好适配。

基于这个背景,RN 团队提供了 JSI (JavaScript Interface)框架,JSI 并不是 RN 的一部分,JSI 可以视作一个兼容层,意在磨平不同 JS 引擎中的差异性。

JSI 实现了引擎切换,比如在 iOS 平台运行的 JSCore,在 Andriod 中运行的是 Hermes 引擎。

JSI 作为引擎统一的通用层,JSI 会定义与 JS 引擎交互的接口以及数据转化的方法。比如在 JSI 中定义了一个执行 JS 的方法叫做 evaluateJavaScript()。具体如何执行 JS 代码其实是由各引擎实现的,通过这种方式屏蔽不同引擎间的差异,可以方便地实现 JS 引擎切换。

JSI 一个主要功能就是提供了 C++ 和 JS 数据转化的能力,比如在 React Native 通信过程中,涉及数据由 JS 层向 C++ 桥层的通信。那么,在没有 JSI 之前,React Native 是如何进行 C++ 和 JS 层通信的呢?
在之前没有 JSI 之前,是需要 JS 序列化成 JSON 串,然后通过反序列化 JSON 成 C++ 数据来完成通信的,这样的序列化和反序列化通信效率很差,JSI 提供了数据深拷贝的能力,也就是说从序列化变成了深拷贝,整体提升了性能。

JSI 是 RN 新架构的核心,主要就是:

  • 引擎无关性:通过抽象层屏蔽不同引擎(如 JSCore/Hermes)的差异。
  • 高效通信:JS 与 Native 直接共享内存,无需序列化。
  • 同步调用:支持 JS 直接调用 Native 方法,减少延迟。

JSI 核心功能:

  1. 数据互通:JS 与 C++ 对象双向访问(如 HostObject)。
  2. 类型转换:自动处理 NumberStringObject 等类型的深拷贝。
  3. 引擎抽象:统一接口(如 evaluateJavaScript()),适配不同引擎。

4.3. JS Bundle 的管理模式

4.3.1. 单 Bundle vs. 多 Bundle 简述
模式优点缺点适用场景
单 Bundle共享全局状态;开发简单包体积大;业务耦合度高中小型应用,页面关联紧密
多 Bundle按需加载;独立更新状态隔离;需管理多 Bundle 依赖大型应用,模块独立性高
4.3.2. 单 Bundle 和多 Bundle 模式

每次运行一个 bundle 时,RN 需要外层容器提供 JS 引擎来运行当前的jsbundle(比如钱包的增量)。在 RN 中,可以通过路由方案来实现页面跳转,通常情况下一个 jsbundle 对应一个或多个页面。
在业务开发中,需要根据具体情况选择单 bundle 或者多个 bundle 的方式。

比如有一些独立的页面,需要不同的发版流程、上线周期,或者不同的团队维护,这种情况下一个页面对应一个 bundle 比较适合。但是有一些上下关联密切的页面,或者是上下级父子页面,也可以采用一个 bundle 对应多个页面,当然现实场景下:一对一、一对多的复杂情况都有可能发生,或者共存在一个项目中。

在同一 bundle 下,文件运行在相同的 JS 上下文中,因此可以使用 Redux 等状态管理工具在 React Native 中实现组件或页面状态的共享。

4.3.3. RN 在 Native 中的形态

RN 在 Native 中,可以是独立的页面,也就是整个页面都是 RN,也有可能是局部动态化方案,只有一部分是 RN,其他部分是 Native 或者其他的技术栈。
在 RN 中每一个应用都有一个入口文件,RN 中提供了注册根本应用的方法,那就是 AppRegistry,这一点和 React Web 应用会有一些区别。Web 应用中,主要依赖于 react-dom 中提供的 api ,但是在 RN 项目中,无需再下载 react-dom,取而代之的是 react-native 包。

import {AppRegistry} from 'react-native'
/* 根组件 */
import App from './app' 

AppRegistry.registerComponent('MyReactNativeApp', () => <App />)

5. React Native 运行时原理

5.1. RN 应用架构概览

JSX 代码会被 Babel 编译成 JS 代码,然后 Native 启动 RN 应用,通过 createElement 形成 element 树结构,然后在 React Reconciler 调和节点形成虚拟 DOM 树。
Native 根据形成的虚拟 DOM fiber 树形成一个中间态的 Shadow Tree 。对于 Shadow Tree 的形成,由 JSI ,Fabric 和 TurboModules 构成的新架构与老 RN 架构会有一定的区别。Shadow Tree 的形成主要是用于做布局计算,描绘出整个页面的视图信息。

RN 应用的整体架构分为四部分:

层级功能描述
JS 运行时运行业务代码,生成虚拟 DOM(Fiber 树)
JS 引擎提供 JavaScript 执行环境(如 Hermes 或 JSC)
Native 层负责 Android/iOS 原生渲染
通信层通过 C++ 桥接 JS 与 Native,支持跨线程通信(老架构)或直接调用(新架构)

整个渲染的大致流程:

5.2. 初始化流程解析

5.2.1. 应用注册

RN 应用的起点是通过 AppRegistry.registerComponent 注册根组件:

import {AppRegistry} from 'react-native'
/* 根组件 */
import App from './app' 

AppRegistry.registerComponent('MyReactNativeApp', () => <App />)
  • 核心逻辑:将组件注册到全局 runnables 对象,等待 Native 调用 runApplication 启动。在主体程序完成注册之后,RN 应用会被 Native 应用启动,客户端就可以通过 AppName (如上的 MyReactNativeApp)在 runnables 上找到对应的 RN 程序,然后就可以调用 run 方法来运行应用。
5.2.2. 应用启动

在初始化阶段,会创建一个容器组件 renderable , 其中 RootComponent 组件就是就是注册的时候,传入 registerComponent 的第二个参数,接下来就会调用 Renderer.renderElement 进入正式渲染流程了。随便说一句,这里的 renderElement 对于整个 RN 相当于 React web 中的 ReactDOM.render。

function renderApplication(RootComponent, initialProps) {
  const renderable = <AppContainer><RootComponent {...initialProps} /></AppContainer>;
  Renderer.renderElement({ element: renderable, rootTag });
}
  • 关键方法renderElement 根据架构类型(Fabric 或老架构)选择渲染方式。
5.2.3. Render 阶段:虚拟 DOM 的构建
5.2.3.1. Render整个流程大致流程:
  • 第一个就是 render 阶段,会调和整个 fiber 树,这个期间包括整个 fiber 虚拟 DOM 的创建,给每一个虚拟 DOM 节点打上不同的 flags 比如创建,删除,更新。
  • 第二个就是 commitRoot 是在 commit 阶段执行的,这个过程中会通过不同的 API 向 Native 发送不同的指令,接下来 Native 会渲染真实视图。
  • 第三个就是通过 ensureRootIsScheduled,确定是否有其他的更新任务,如果有其他的更新任务,那么进行更新。

Fiber 节点结构

每一个 fiber 可以看作一个执行的单元,在调和过程中,每一个发生更新的 fiber 都会作为一次 workInProgress。 workLoopSync 会遍历一遍 fiber 树,执行 performUnitOfWork ,这个函数包括两个阶段 beginWork 和 completeWork 。

属性描述
return父节点
child第一个子节点
sibling兄弟节点
memoizedProps组件上一次渲染的 props

示例
以下 JSX 代码生成的 Fiber 树结构:

<View>
  <Text>Hello</Text>
  <View>
    <Text>World</Text>
  </View>
</View>

对应上边代码的图示:

beginWork:是向下调和的过程。就是由 fiberRoot 按照 child 指针逐层向下调和,期间会执行函数组件,实例类组件,diff 调和子节点,打不同effectTag。在 beginWork 中,如果是类组件,那么会实例化类组件,如果是函数组件,那么会通过 renderWithHooks 运行我们的函数组件。

function beginWork(current,workInProgress, renderLanes){
    switch(workInProgress.tag){
        // case1:如果是函数组件,那么通过 renderWithHooks 执行
        // case2:如果是类组件,那么初始化或者更新我们的类组件。
    }
}

completeUnitOfWork:是向上归并的过程,如果有兄弟节点,会返回 sibling兄弟,没有返回 return 父级,一直返回到 fiebrRoot,在遍历节点的过程,会触发 completeWork ,如果是新节点,那么会通过createNode创建新的节点,通过createTextInstance创建文本节点 ,在 web 端 React 中,这里会创建真实的 DOM 节点。

function completeUnitOfWork(unitOfWork) {
  var completedWork = unitOfWork;
  do {
    /*  */  
    var current = completedWork.alternate;
    unitOfWork = completedWork.return;
    if (0 === (completedWork.flags & 32768)) {
      if (
       completeWork(current, completedWork, subtreeRenderLanes)
      ) {
        workInProgress = current;
        return;
      }
    } else {
      current = unwindWork(current, completedWork);
    }
    /* 遍历兄弟节点  */
    completedWork = completedWork.sibling;
  } while (null !== completedWork);
}

这么一上一下,构成了整个 fiber 树的调和。最初的例子在整个 workLoop 过程中,整体调和过程如下所示:

5.2.4. Commit 阶段:从虚拟 DOM 到原生渲染
5.2.4.1. Commit 阶段流程
function commitRootImpl() {
  commitBeforeMutationEffects(); // 处理副作用前的回调(如 getSnapshotBeforeUpdate)
  commitMutationEffects();       // 执行 DOM 更新(创建/删除节点)
  commitLayoutEffects();         // 处理布局后的回调(如 useEffect)
}

新老架构的渲染差异Commit 阶段流程

特性老架构(Bridge)新架构(Fabric + JSI)
通信方式通过桥接异步通信(JSON 序列化)直接调用 C++ 方法(JSI 实现同步通信)
Shadow TreeNative 端生成在 C++ 层生成,减少跨线程开销
性能瓶颈高频更新易阻塞通信支持同步操作,提升渲染性能

代码对比

  • 老架构:通过 UIManager 桥接
ReactNativePrivateInterface.UIManager.createView(tag, viewName, props);
  • Fabric 架构:直接调用 JSI 方法
nativeFabricUIManager.createNode(tag, viewName, props);

5.2.5. 新老架构核心原理对比

  1. 老架构
    • 流程:JS -> Bridge -> Native 主线程 -> 渲染
    • 缺点:JSON 序列化与异步通信导致性能损耗。
  1. Fabric 架构
    • 流程:JS -> JSI -> C++ 层 -> 直接操作 Shadow Tree
    • 优势:跨线程通信减少,支持同步渲染。

6. RN 组件

RN 移动端应用和 web 移动端应用最大的区别就是渲染模式的不同,RN 是采用 Native 原生渲染的方式,所以在 RN 中是不能像 webview 一样使用 DOM 元素标签的,比如 div,span 等,因为需要做一些特殊处理,和 web 端的逻辑是不一样的。
取而代之的是, RN 提供了对应的视图组件,比如 View 组件,Text 组件等。
RN 中的组件在 Libraries/Components 文件夹下面,如下可以看到 RN 中的原生组件:

这里以 最常见的 View 组件为例:View 组件

其中大致逻辑就是

const View = React.forwardRef((props,forwardedRef)=>{
  return <TextAncestor value={false}>{actualView}</TextAncestor>;
})

如上就是 View 的基本底层实现,其中 ViewNativeComponent 就是 viewName 名称为RCTView的原生组件。
这样在 RN 的调和阶段,被标记成 HostComponent 的 fiber ,然后可以通过 createNode 传递 RCTView 组件的绘制指令,这样 Native 就可以渲染对应的视图了。

7. RN 事件处理和更新逻辑

7.1. web 端示例

在 RN 中,比如有一段代码如下所示:

function Demo (){
  const [ number, setNumber ] = useState(0)
  const handleClickAdd = () => {
    setNumber(number + 1)
  }
  return <TouchableOpacity onPress={handleClickAdd} >
    <View ><Text>{ number }</Text></View>
  </TouchableOpacity> 
}

如上在 Demo 组件里绑定一个点击事件 handleClickAdd,当触发点击事件的时候,会更新 number 状态触发更新,那么整体交互流程是什么样的呢?

7.2. RN 中的事件处理

在 web 应用中,可以用事件监听器,监听浏览器事件,在 RN 中也是通过监听 Native 事件的方式。

在 RN 中,点击原生组件图示会存在一个监听的过程。

以 Fabric 架构为例子,在 _nativeFabricUIManage 上有一个函数registerEventHandler 用来注册 React JS 线程事件处理函数 dispatchEvent。核心流程如下所示:

var registerEventHandler = _nativeFabricUIManage.registerEventHandler

if (registerEventHandler) {
  /* 通过 native 桥的方式注册事件处理函数 dispatchEvent */
  registerEventHandler(dispatchEvent);
}

这样注册之后,当 Native 感知到点击事件之后,就会触发 dispatchEvent 函数,会把当前触发事件的元素信息,以参数的形式传递给 dispatchEvent 函数。在 RN 中,事件处理都要经过 dispatchEvent 函数。

在 dispatchEvent 中,会找到触发事件对应的 fiber 元素节点,然后通过 batchedUpdates 执行批量更新逻辑。

批量更新逻辑:批量更新指的是防止在同一个上下文中,执行多次 setState 或者是 useState 的 dispatchAction 而造成的多次重复更新。比如如下一段代码:

handleClick(){
  this.setState({ number:1 })
  this.setState({ name:'Alien' })
}

如上在 handleClick 中有两个 setState, 通过 batchedUpdates 会合称为一次更新作用,其原理和 web 端如出一辙。

批量更新的逻辑,本质上就是通过一个开关 executionContext ,这个开关为 BatchedContext,证明此次更新发生在事件处理函数内部,那么就可以不着急发生更新,而是等到事件处理完成(比如上面的 handleClick)执行完毕,再统一触发更新。这样就保障了在事件内部多次更新会合并成一次更新。

回到主流程 runExtractedPluginEventsInBatch 上来,找到了发生事件的 fiber 虚拟节点,接下来构建事件源对象,找到对应的事件处理函数(如上的 handleClick,然后分别执行事件处理函数就可以了。

7.3. RN 中的更新逻辑

当 RN 组件中触发 setState 的时候,本质上是触发 React.Component 上的 setState 方法,来看一下具体实现。

function Component(props, context, updater) {
  this.props = props;      //绑定props
  this.context = context;  //绑定context
  this.refs = emptyObject; //绑定ref
  this.updater = updater || ReactNoopUpdateQueue; //上面所属的updater 对象
}
/* 绑定setState 方法 */
Component.prototype.setState = function(partialState, callback) {
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
}

如上可以看出 Component 底层 React 的处理逻辑是,类组件执行构造函数过程中会在实例上绑定 props 和 context (这一点和刚刚提到的小程序的逻辑很像),初始化置空 refs 属性,原型链上绑定setState、forceUpdate 方法。对于 updater,React 在实例化类组件之后会单独绑定 update 对象。

那么在 RN 中 React 组件是什么时间节点被实例化的呢?

原来在初始化的过程中,最后会执行到 beginWork 中,在 beginWork 会根据不同的 fiber 类型执行不同的处理逻辑。

function beginWork(){
  switch (workInProgress.tag) {
    case FunctionComponent: {} // 函数组件逻辑
    case ClassComponent: { //类组件逻辑
      return updateClassComponent(...)
    }
  }
}

可以清晰的看到,如果是类组件的话,会触发 updateClassComponent 方法,在这个函数中,如果是第一次渲染的时候,会实例化类组件,然后给类组件传递 updater 对象。updater 的样子如下所示:

var classComponentUpdater = {
  enqueueReplaceState: function(inst, payload, callback) {
    /* 获取当前 fiber  */  
    var fiber = get(inst);
    /* 创建一个 update */
    var update = createUpdate(eventTime, lane);
    /* 触发更新 */
    scheduleUpdateOnFiber(root, fiber, lane, eventTime);
  }
    ...
    }

如上 classComponentUpdater 就是组件实例上的 updater 对象。当触发 setState 的时候,会获取当前 fiber 虚拟节点,然后创建一个 update 对象,最后调用 scheduleUpdateOnFiber 触发更新。

function scheduleUpdateOnFiber(){
  ensureRootIsScheduled(root, eventTime);
}

如上保存了最核心的流程,scheduleUpdateOnFiber 本质上就是调用 ensureRootIsScheduled 开始整体更新。在上一章节中,已经介绍了 ensureRootIsScheduled 为整个 fiber 树构建更新的入口函数,这里就不重复介绍了。

8. React Native 通信逻辑解析

8.1. 核心架构

采用分层架构图示说明,自上而下为:

  1. JS逻辑层 - 负责业务逻辑与虚拟DOM生成
  2. C++桥接层 - JSI实现双向通信枢纽
  3. Native渲染层 - 处理原生视图渲染与系统交互)

8.2. 初始化流程全链路

8.3. Native层启动引擎

关键组件协作表

组件名称职责描述生命周期阶段
ReactRootView承载RN视图的容器组件视图挂载阶段
ReactInstanceManager管理JS运行时环境与模块注册应用启动阶段
CatalystInstanceImpl通信中枢,桥接Native/C++/JS交互全生命周期

初始化序列

  1. ReactRootView触发startReactApplication
  2. 创建JS线程加载Bundle
  3. 生成CatalystInstance通信实例
  4. 建立三端模块注册表

8.4. JS层消息队列

MessageQueue核心机制

class MessageQueue {
  private _lazyCallableModules: {[name: string]: () => Object}; // 可调用模块注册
  private _successCallbacks: Map<number, Function>; // 回调函数池
  
  // 关键方法
  registerCallableModule(name: string, module: Object) {
    this._lazyCallableModules[name] = () => module;
  }
  
  enqueueNativeCall(moduleId: number, methodId: number, params: any[]) {
    // 序列化调用请求
    this._queue.push([moduleId, methodId, params]);
    global.nativeFlushQueueImmediate(this._queue);
  }
}

8.5. 双向通信机制详解

8.5.1. Native→JS指令下发

典型场景:启动根组件

调用链路径

Java: catalystInstance.getJSModule(AppRegistry.class)
      ↓ 动态代理
C++: Instance::callJSFunction("AppRegistry", "runApplication")
      ↓ JSI通信
JS: MessageQueue.__callFunction → AppRegistry.runApplication()

性能优化点

  • 代理模式实现懒加载
  • 调用参数序列化优化
  • 跨线程批量消息处理
8.5.2. 3JS→Native功能调用

两种调用模式对比

特性同步调用异步调用
执行方式阻塞JS线程直到返回非阻塞,通过回调处理结果
适用场景轻量级设备信息获取耗时的原生功能操作
实现原理callNativeSyncHookenqueueNativeCall+回调ID管理
典型APIgetCurrentActivityfetch网络请求

回调处理流程

  1. JS侧生成唯一callbackId存入Map
  2. Native执行完成后触发invokeCallback
  3. C++桥接层通过JSI定位回调函数
  4. 执行回调并清理引用