# 数据通信与渲染

# setData

# setData 的流程

setData 的过程,大致可以分成几个阶段:

  • 逻辑层虚拟 DOM 树的遍历和更新,触发组件生命周期和 observer 等;
  • 将 data 从逻辑层传输到视图层;
  • 视图层虚拟 DOM 树的更新、真实 DOM 元素的更新并触发页面渲染更新,完成后触发setData回调。

# 数据通信

setData 将 data 从逻辑层传输到视图层。由于小程序的逻辑层和视图层是两个独立的运行环境、分属不同的线程或进程,不能直接进行数据共享,需要进行数据的序列化、跨线程/进程的数据传输、数据的反序列化,因此数据传输过程是异步的、非实时的。iOS/iPadOS/MacOS 上,数据传输是通过 evaluateJavascript 实现的,还会有额外 JS 脚本解析和执行的耗时。数据传输的耗时与数据量的大小正相关,如果对端线程处于繁忙状态,数据会在消息队列中等待

# 使用建议

  1. data 应只包括渲染相关的数据,避免每次 setData 都传递大量新数据。setData 应只用来进行渲染相关的数据更新。用 setData 的方式更新渲染无关的字段,会触发额外的渲染流程,或者增加传输的数据量,影响渲染耗时。页面或组件渲染无关的数据,应挂在非 data 的字段下;页面或组件渲染间接相关的数据可以设置为「纯数据字段 (opens new window)」,可以使用 setData 设置并使用 observers 监听变化;避免使用 data 在页面或组件方法间进行数据共享

  2. 控制 setData 的频率,避免频繁的去 setData。仅在需要进行页面内容更新时调用 setData;对连续的 setData 调用尽可能的进行合并。每次 setData 都会触发逻辑层虚拟 DOM 树的遍历和更新,也可能会导致触发一次完整的页面渲染流程。过于频繁(毫秒级)的调用 setData,会导致以下后果:

    • 逻辑层 JS 线程持续繁忙,无法正常响应用户操作的事件,也无法正常完成页面切换;
    • 视图层 JS 线程持续处于忙碌状态,逻辑层 -> 视图层通信耗时上升,视图层收到消息的延时较高,渲染出现明显延迟;
    • 视图层无法及时响应用户操作,用户滑动页面时感到明显卡顿,操作反馈延迟,用户操作事件无法及时传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层。
  3. 选择合适的 setData 范围。组件的 setData 只会引起当前组件和子组件的更新,可以降低虚拟 DOM 更新时的计算开销。对于需要频繁更新的页面元素(例如:秒杀倒计时),可以封装为独立的组件,在组件内进行 setData 操作。必要时可以使用 CSS contain 属性 (opens new window)限制计算布局、样式和绘制等的范围。

  4. setData 应只传发生变化的数据。setData 的数据量会影响数据拷贝和数据通讯的耗时,增加页面更新的开销,造成页面更新延迟。setData 应只传入发生变化的字段;建议以数据路径形式改变数组中的某一项或对象的某个属性。

  5. **后台态页面进行 setData。**页面切后台后的更新操作,应尽量避免,或延迟到页面 onShow 后延迟进行。由于小程序逻辑层是单线程运行的,后台态页面去 setData 也会抢占前台页面的运行资源,且后台态页面的的渲染用户是无法感知的,会产生浪费。在某些平台上,小程序渲染层各 WebView 也是共享同一个线程,后台页面的渲染和逻辑执行也会导致前台页面的卡顿。

# 双线程通信

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

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

当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。**即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。**而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。所以我们的setData函数将数据从逻辑层发送到视图层,是异步的。

# 双线程渲染机制

双线程的渲染,其实是结合了模版绑定、虚拟 DOM、线程通信,最后整合的一个执行步骤:

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

就是我们常见的这些:

<view> {{ message }} </view>
<view wx:if="{{condition}}"> </view>
<checkbox checked="{{false}}"> </checkbox>
1
2
3
  1. 小程序在逻辑层提供了设置页面数据的 api - setData
this.setData({
  key: value
});
1
2
3

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

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

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

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

# 原生组件

# 原生组件的引入

我们知道,用户的一次交互,如点击某个按钮,开发者的逻辑层要处理一些事情,然后再通过 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 - 官方 5 条 setData 优化建议 (opens new window)

性能优化之 setData (opens new window)

被删的前端游乐场 (opens new window)