内容转载自 解剖小程序的 setData
前言
双线程的难题
我们知道,小程序的双线程设计,主要为了管控安全,避免操作 DOM。(可参考《小程序的底层框架》)
把开发者的 JS 逻辑代码放到单独的线程去运行,因为不在 Webview 线程里,所以这个环境没有 Webview 任何接口,自然开发者就没法直接操作 DOM,也就没法动态去更改界面。
但是,这样就产生了新的问题。没法操作 DOM,那用户交互需要界面变化的话怎么办呢?
模板数据绑定
模版数据绑定的方案,已经成为前端框架中最基础的功能。
数据绑定的过程其实不复杂:
- 解析语法生成 AST。
- 根据 AST 结果生成 DOM。
- 将数据绑定更新至模板。
浏览器会把 HTML 解析成一棵树,最后渲染出来。整个界面是对应着一棵 DOM 树。
其实浏览器页面的 DOM 结构树,也是 AST 的一种,把 HTML DOM 语法解析并生成最终的页面。而模板引擎中常用的,则是将模板语法解析生成 HTML DOM。
而最容易引发性能问题的,主要是第三点。而关于数据更新的解决方案,React 首先提出了虚拟 DOM 的设计,而现在也基本被大部分框架吸收,小程序也不例外
虚拟 DOM 机制
说到数据更新的 Diff,更多的则是Diff + 更新模板这样一个过程。
虚拟 DOM 解决了常见的局部数据更新的问题,例如数组中值位置的调换、部分更新。
一般来说计算过程如下:
- 用 JS 对象模拟 DOM 树。
一个真正的 DOM 元素非常庞大,拥有很多的属性值。而其中很多的属性对于计算过程来说是不需要的,所以我们的第一步就是简化 DOM 对象。 我们用一个 JavaScript 对象结构表示 DOM 树的结构。
- 比较两棵虚拟 DOM 树的差异。
当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异。通常来说这样的差异需要记录,最后得到一组差异记录。
- 把差异应用到真正的 DOM 树上。
对差异记录要应用到真正的 DOM 树上,例如节点的替换、移动、删除,文本内容的改变等。
小程序里,由于无法直接操作 DOM,主要也是通过数据传递的方式来进行相关的模版更新。模版绑定的机制、数据更新的机制,都可以参照上面的说明,想更具体理解也可以参考《前端模板引擎》。
那么既然不在一个线程,数据的通信是怎么做的呢?
双线程渲染机制
双线程的渲染,其实是结合了前面的一系列机制(模版绑定、虚拟 DOM、线程通信),最后整合的一个执行步骤。
1. 通过模版数据绑定和虚拟 DOM 机制,小程序提供了带有数据绑定语法的 DSL 给到开发者,用来在渲染层描述界面的结构。
就是我们常见的这些:
<view> {{ message }} </view>
<view wx:if="{{condition}}"> </view>
<checkbox checked="{{false}}"> </checkbox>
噢,这里顺便吐个槽,wx:if竟然不支持[].indexOf(xx) > -1等等相关的函数运算(摔!)。
2. 小程序在逻辑层提供了设置页面数据的 api。
不用问就是setData了:
this.setData({
key: value
});
setData函数用于将数据从逻辑层发送到视图层(异步),同时改变对应的this.data的值(同步)
3. 逻辑层需要更改界面时,只要把 setData 修改的内容数据传到渲染层。
传输的数据,会转换为字符串形式传递,故应尽量避免传递大量数据。
4. 渲染层会根据前面提到的渲染机制重新生成 VD(虚拟 DOM)树,并更新到对应的 DOM 树上,引起界面变化。
总的来说: setData 这个 API 实际上做了两件事:
- 改变 this.data 的值
- 将修改后的 data 值转换成字符串形式后传给渲染层
原生组件减少 setData
原生组件的出现,其实与 setData 的机制也有那么点关系,那么就当题外话一块补充下。
频繁交互的性能
我们知道,用户的一次交互,如点击某个按钮,开发者的逻辑层要处理一些事情,然后再通过 setData 引起界面变化。这样的一个过程需要四次通信:
- 渲染层 -> Native(点击事件)。
- Native -> 逻辑层(点击事件)。
- 逻辑层 -> Native(setData)。
- Native -> 渲染层(setData)。
在一些强交互的场景(表单、canvas 等),这样的操作流程会导致用户体验卡顿。
引入原生组件
前面也说过,小程序是 Hybrid 应用,除了 Web 组件的渲染体系(上面讲到),还有由客户端原生参与组件(原生组件)的渲染。
引入原生组件的 3 个好处
- 绕过 setData、数据通信和重渲染流程,使渲染性能更好。
- 扩展 Web 的能力。 比如像输入框组件(input, textarea)有更好地控制键盘的能力。
- 体验更好,同时也减轻 WebView 的渲染工作。 比如像地图组件(map)这类较复杂的组件,其渲染工作不占用 WebView 线程,而交给更高效的客户端原生处理。
原生组件可以理解为是微信客户端提供的,原生组件的操作交互就相当于在客户端操作
原生组件的渲染过程
- 组件被创建,包括组件属性会依次赋值。
- 组件被插入到 DOM 树里,浏览器内核会立即计算布局,此时我们可以读取出组件相对页面的位置(x, y 坐标)、宽高。
- 组件通知客户端,客户端在相同的位置上,根据宽高插入一块原生区域,之后客户端就在这块区域渲染界面。
- 当位置或宽高发生变化时,组件会通知客户端做相应的调整。
简单来说,就是 原生组件在 WebView 这一层只需要渲染一个占位元素,之后客户端在这块占位元素之上叠了一层原生界面。
有利必有弊,原生组件也是有限制的:
- 最主要的限制是一些 CSS 样式无法应用于原生组件
- 由于客户端渲染,原生组件的层级会比所有在 WebView 层渲染的普通组件要高