解剖小程序的 setData

2018-11-14 16:26:19 浏览数 (1)

作者:微信支付前端工程师 王贝珊

原文链接:https://godbasin.github.io/2018/10/05/wxapp-set-data/

小程序的双线程,之前也有详细讲过了。而双线程的设计,使得逻辑层和渲染层无法直接进行数据传输。那双线程的渲染机制、通信机制,setData 的出现、工作原理、使用建议等,应该要怎么去理解呢?

无处不在的 setData


几乎每个开发者都会用到setData,要是在复杂的页面中,写了很多的setData,然后我们会发现页面真的是延迟严重,甚至卡顿、假死。

官方在性能优化中有提到:

  1. 避免频繁的去 setData。
  2. 避免每次 setData 都传递大量新数据。
  3. 后台态页面进行 setData。

但是到底是为什么呢?setData的出现、设计方案是怎样的,又为何要这么设计呢?一切都还是要从双线程说起。

小程序的虚拟 DOM

双线程的难题

我们知道,小程序的双线程设计,主要为了管控安全,避免操作 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,主要也是通过数据传递的方式来进行相关的模版更新。模版绑定的机制、数据更新的机制,都可以参照上面的说明,想更具体理解也可以参考《前端模板引擎》。

那么既然不在一个线程,数据的通信是怎么做的呢?

小程序的数据通信与渲染机制

双线程通信方式

小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。

一个小程序存在多个界面,所以渲染层存在多个 WebView 线程。 逻辑层和渲染层的通信会经由微信客户端(Native)做中转,逻辑层发送网络请求也经由 Native 转发 ,小程序的通信模型如图:

小程序的通信模型图(官方)小程序的通信模型图(官方)

当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。

而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。所以我们的setData函数将数据从逻辑层发送到视图层,是异步的。

有了线程之间的通信,我们来看看小程序的渲染机制。

双线程渲染机制

双线程的渲染,其实是结合了前面的一系列机制(模版绑定、虚拟 DOM、线程通信),最后整合的一个执行步骤。

1. 通过模版数据绑定和虚拟 DOM 机制,小程序提供了带有数据绑定语法的 DSL 给到开发者,用来在渲染层描述界面的结构。

就是我们常见的这些:

代码语言:txt复制
<view> {{ message }} </view>
<view wx:if="{{condition}}"> </view>
<checkbox checked="{{false}}"> </checkbox>

噢,这里顺便吐个槽,wx:if竟然不支持[].indexOf(xx) > -1等等相关的函数运算(摔!)。

2. 小程序在逻辑层提供了设置页面数据的 api。

不用问就是setData了:

代码语言:txt复制
this.setData({
  key: value
})

setData函数用于将数据从逻辑层发送到视图层(异步),同时改变对应的this.data的值(同步)。

3. 逻辑层需要更改界面时,只要把修改后的 data 通过 setData 传到渲染层。

传输的数据,会转换为字符串形式传递,故应尽量避免传递大量数据。

4. 渲染层会根据前面提到的渲染机制重新生成 VD(虚拟 DOM)树,并更新到对应的 DOM 树上,引起界面变化。

原生组件的出现

原生组件的出现,其实与 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 层渲染的普通组件要高

参考

  • setData
  • 《小程序开发指南--6.3 原生组件》

结束语


总而言之,这一节内容主要是围绕 setData 展开,包括双线程的渲染机制、通信机制,setData 的出现(逻辑层通知渲染层)、工作原理(evaluateJavascript 字符串传递)、使用建议(setData 交互性能)、性能优化(原生组件出现)。

小程序乍一看是简单的双线程设计,但仔细研究就会发现设计过程中也遇到了不少问题,不断探索解决才有了现在的美好样子。我们在开发过程中会踩的一些坑,其实在理解原理之后便很容易懂了。

现在再来看,官方在性能优化中说到的优化建议,你都能深刻理解了吗?

0 人点赞