微信小程序系列 -- 小程序的 setData

508 阅读6分钟

内容转载自 解剖小程序的 setData

前言

双线程的难题

我们知道,小程序的双线程设计,主要为了管控安全,避免操作 DOM。(可参考《小程序的底层框架》

把开发者的 JS 逻辑代码放到单独的线程去运行,因为不在 Webview 线程里,所以这个环境没有 Webview 任何接口,自然开发者就没法直接操作 DOM,也就没法动态去更改界面。

但是,这样就产生了新的问题。没法操作 DOM,那用户交互需要界面变化的话怎么办呢?

模板数据绑定

模版数据绑定的方案,已经成为前端框架中最基础的功能。

数据绑定的过程其实不复杂:

  1. 解析语法生成 AST。
  2. 根据 AST 结果生成 DOM。
  3. 将数据绑定更新至模板。

浏览器会把 HTML 解析成一棵树,最后渲染出来。整个界面是对应着一棵 DOM 树。

其实浏览器页面的 DOM 结构树,也是 AST 的一种,把 HTML DOM 语法解析并生成最终的页面。而模板引擎中常用的,则是将模板语法解析生成 HTML DOM。

而最容易引发性能问题的,主要是第三点。而关于数据更新的解决方案,React 首先提出了虚拟 DOM 的设计,而现在也基本被大部分框架吸收,小程序也不例外

虚拟 DOM 机制

说到数据更新的 Diff,更多的则是Diff + 更新模板这样一个过程。

虚拟 DOM 解决了常见的局部数据更新的问题,例如数组中值位置的调换、部分更新。

一般来说计算过程如下:

  1. 用 JS 对象模拟 DOM 树。

一个真正的 DOM 元素非常庞大,拥有很多的属性值。而其中很多的属性对于计算过程来说是不需要的,所以我们的第一步就是简化 DOM 对象。 我们用一个 JavaScript 对象结构表示 DOM 树的结构。

  1. 比较两棵虚拟 DOM 树的差异。

当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异。通常来说这样的差异需要记录,最后得到一组差异记录。

  1. 把差异应用到真正的 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 实际上做了两件事:

  1. 改变 this.data 的值
  2. 将修改后的 data 值转换成字符串形式后传给渲染层 image.png

原生组件减少 setData

原生组件的出现,其实与 setData 的机制也有那么点关系,那么就当题外话一块补充下。

频繁交互的性能

我们知道,用户的一次交互,如点击某个按钮,开发者的逻辑层要处理一些事情,然后再通过 setData 引起界面变化。这样的一个过程需要四次通信:

  1. 渲染层 -> Native(点击事件)。
  2. Native -> 逻辑层(点击事件)。
  3. 逻辑层 -> Native(setData)。
  4. Native -> 渲染层(setData)。

在一些强交互的场景(表单、canvas 等),这样的操作流程会导致用户体验卡顿。

引入原生组件

前面也说过,小程序是 Hybrid 应用,除了 Web 组件的渲染体系(上面讲到),还有由客户端原生参与组件(原生组件)的渲染。

引入原生组件的 3 个好处

  1. 绕过 setData、数据通信和重渲染流程,使渲染性能更好。
  2. 扩展 Web 的能力。  比如像输入框组件(input, textarea)有更好地控制键盘的能力。
  3. 体验更好,同时也减轻 WebView 的渲染工作。  比如像地图组件(map)这类较复杂的组件,其渲染工作不占用 WebView 线程,而交给更高效的客户端原生处理。

原生组件可以理解为是微信客户端提供的,原生组件的操作交互就相当于在客户端操作

原生组件的渲染过程

  1. 组件被创建,包括组件属性会依次赋值。
  2. 组件被插入到 DOM 树里,浏览器内核会立即计算布局,此时我们可以读取出组件相对页面的位置(x, y 坐标)、宽高。
  3. 组件通知客户端,客户端在相同的位置上,根据宽高插入一块原生区域,之后客户端就在这块区域渲染界面。
  4. 当位置或宽高发生变化时,组件会通知客户端做相应的调整。

简单来说,就是 原生组件在 WebView 这一层只需要渲染一个占位元素,之后客户端在这块占位元素之上叠了一层原生界面。

有利必有弊,原生组件也是有限制的:

  • 最主要的限制是一些 CSS 样式无法应用于原生组件
  • 由于客户端渲染,原生组件的层级会比所有在 WebView 层渲染的普通组件要高