# 数据通信与渲染
# setData
# setData 的流程
setData
的过程,大致可以分成几个阶段:
- 逻辑层虚拟 DOM 树的遍历和更新,触发组件生命周期和 observer 等;
- 将 data 从逻辑层传输到视图层;
- 视图层虚拟 DOM 树的更新、真实 DOM 元素的更新并触发页面渲染更新,完成后触发
setData
回调。
# 数据通信
setData 将 data 从逻辑层传输到视图层。由于小程序的逻辑层和视图层是两个独立的运行环境、分属不同的线程或进程,不能直接进行数据共享,需要进行数据的序列化、跨线程/进程的数据传输、数据的反序列化,因此数据传输过程是异步的、非实时的。iOS/iPadOS/MacOS 上,数据传输是通过 evaluateJavascript
实现的,还会有额外 JS 脚本解析和执行的耗时。数据传输的耗时与数据量的大小正相关,如果对端线程处于繁忙状态,数据会在消息队列中等待。
# 使用建议
data 应只包括渲染相关的数据,避免每次 setData 都传递大量新数据。setData 应只用来进行渲染相关的数据更新。用 setData 的方式更新渲染无关的字段,会触发额外的渲染流程,或者增加传输的数据量,影响渲染耗时。页面或组件渲染无关的数据,应挂在非 data 的字段下;页面或组件渲染间接相关的数据可以设置为「纯数据字段 (opens new window)」,可以使用 setData 设置并使用 observers 监听变化;避免使用 data 在页面或组件方法间进行数据共享。
控制 setData 的频率,避免频繁的去 setData。仅在需要进行页面内容更新时调用 setData;对连续的 setData 调用尽可能的进行合并。每次 setData 都会触发逻辑层虚拟 DOM 树的遍历和更新,也可能会导致触发一次完整的页面渲染流程。过于频繁(毫秒级)的调用
setData
,会导致以下后果:- 逻辑层 JS 线程持续繁忙,无法正常响应用户操作的事件,也无法正常完成页面切换;
- 视图层 JS 线程持续处于忙碌状态,逻辑层 -> 视图层通信耗时上升,视图层收到消息的延时较高,渲染出现明显延迟;
- 视图层无法及时响应用户操作,用户滑动页面时感到明显卡顿,操作反馈延迟,用户操作事件无法及时传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层。
选择合适的 setData 范围。组件的 setData 只会引起当前组件和子组件的更新,可以降低虚拟 DOM 更新时的计算开销。对于需要频繁更新的页面元素(例如:秒杀倒计时),可以封装为独立的组件,在组件内进行 setData 操作。必要时可以使用 CSS contain 属性 (opens new window)限制计算布局、样式和绘制等的范围。
setData 应只传发生变化的数据。setData 的数据量会影响数据拷贝和数据通讯的耗时,增加页面更新的开销,造成页面更新延迟。setData 应只传入发生变化的字段;建议以数据路径形式改变数组中的某一项或对象的某个属性。
**后台态页面进行 setData。**页面切后台后的更新操作,应尽量避免,或延迟到页面
onShow
后延迟进行。由于小程序逻辑层是单线程运行的,后台态页面去setData
也会抢占前台页面的运行资源,且后台态页面的的渲染用户是无法感知的,会产生浪费。在某些平台上,小程序渲染层各 WebView 也是共享同一个线程,后台页面的渲染和逻辑执行也会导致前台页面的卡顿。
# 双线程通信
小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。
一个小程序存在多个界面,所以渲染层存在多个 WebView 线程。 逻辑层和渲染层的通信会经由微信客户端(Native)做中转,逻辑层发送网络请求也经由 Native 转发 。
当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。**即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。**而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。所以我们的setData
函数将数据从逻辑层发送到视图层,是异步的。
# 双线程渲染机制
双线程的渲染,其实是结合了模版绑定、虚拟 DOM、线程通信,最后整合的一个执行步骤:
- 通过模版数据绑定和虚拟 DOM 机制,小程序提供了带有数据绑定语法的 DSL 给到开发者,用来在渲染层描述界面的结构。
就是我们常见的这些:
<view> {{ message }} </view>
<view wx:if="{{condition}}"> </view>
<checkbox checked="{{false}}"> </checkbox>
2
3
- 小程序在逻辑层提供了设置页面数据的 api - setData
this.setData({
key: value
});
2
3
setData
函数用于将数据从逻辑层发送到视图层(异步),同时改变对应的this.data
的值(同步)。
- 逻辑层需要更改界面时,只要把修改后的 data 通过 setData 传到渲染层。
传输的数据,会转换为字符串形式传递,故应尽量避免传递大量数据。
- 渲染层会根据前面提到的渲染机制重新生成 VD(虚拟 DOM)树,并更新到对应的 DOM 树上,引起界面变化。
# 原生组件
# 原生组件的引入
我们知道,用户的一次交互,如点击某个按钮,开发者的逻辑层要处理一些事情,然后再通过 setData 引起界面变化。这样的一个过程需要四次通信:
- 渲染层 -> Native(点击事件)。
- Native -> 逻辑层(点击事件)。
- 逻辑层 -> Native(setData)。
- Native -> 渲染层(setData)。
在一些强交互的场景(表单、canvas 等),这样的操作流程会导致用户体验卡顿。
小程序是 Hybrid 应用,除了 Web 组件的渲染体系(上面讲到),还有由客户端原生参与组件(原生组件)的渲染。引入原生组件的有 3 个好处:
- 绕过 setData、数据通信和重渲染流程,使渲染性能更好。
- 扩展 Web 的能力。 比如像输入框组件(input, textarea)有更好地控制键盘的能力。
- 体验更好,同时也减轻 WebView 的渲染工作。 比如像地图组件(map)这类较复杂的组件,其渲染工作不占用 WebView 线程,而交给更高效的客户端原生处理。
# 原生组件的渲染过程
- 组件被创建,包括组件属性会依次赋值。
- 组件被插入到 DOM 树里,浏览器内核会立即计算布局,此时我们可以读取出组件相对页面的位置(x, y 坐标)、宽高。
- 组件通知客户端,客户端在相同的位置上,根据宽高插入一块原生区域,之后客户端就在这块区域渲染界面。
- 当位置或宽高发生变化时,组件会通知客户端做相应的调整。
简单来说,就是 原生组件在 WebView 这一层只需要渲染一个占位元素,之后客户端在这块占位元素之上叠了一层原生界面。
有利必有弊,原生组件也是有限制的:
- 最主要的限制是一些 CSS 样式无法应用于原生组件
- 由于客户端渲染,原生组件的层级会比所有在 WebView 层渲染的普通组件要高
合理使用 setData - 官方 5 条 setData 优化建议 (opens new window)