Open CommanderXL opened 4 years ago
由于小程序的双线程的架构设计,逻辑层和视图层之间需要桥接 native bridge。如果要完成视图层的更新,那么逻辑层需要调用 setData 方法,数据经由 native bridge,再到渲染层,这个工程流程为:
小程序逻辑层调用宿主环境的 setData 方法; 逻辑层执行 JSON.stringify 将待传输数据转换成字符串并拼接到特定的JS脚本,并通过evaluateJavascript 执行脚本将数据传输到渲染层; 渲染层接收到后, WebView JS 线程会对脚本进行编译,得到待更新数据后进入渲染队列等待 WebView 线程空闲时进行页面渲染; WebView 线程开始执行渲染时,待更新数据会合并到视图层保留的原始 data 数据,并将新数据套用在WXML片段中得到新的虚拟节点树。经过新虚拟节点树与当前节点树的 diff 对比,将差异部分更新到UI视图。同时,将新的节点树替换旧节点树,用于下一次重渲染。
小程序逻辑层调用宿主环境的 setData 方法;
逻辑层执行 JSON.stringify 将待传输数据转换成字符串并拼接到特定的JS脚本,并通过evaluateJavascript 执行脚本将数据传输到渲染层;
渲染层接收到后, WebView JS 线程会对脚本进行编译,得到待更新数据后进入渲染队列等待 WebView 线程空闲时进行页面渲染;
WebView 线程开始执行渲染时,待更新数据会合并到视图层保留的原始 data 数据,并将新数据套用在WXML片段中得到新的虚拟节点树。经过新虚拟节点树与当前节点树的 diff 对比,将差异部分更新到UI视图。同时,将新的节点树替换旧节点树,用于下一次重渲染。
文章来源
而 setData 作为逻辑层和视图层之间通讯的核心接口,那么对于这个接口的使用遵照一些准则将有助于性能方面的提升。
Mpx 在这个方面所做的工作之一就是基于数据路径的 diff。这也是官方所推荐的 setData 的方式。每次响应式数据发生了变化,调用 setData 方法的时候确保传递的数据都为 diff 过后的最小数据集,这样来减少 setData 传输的数据。
接下来我们就来看下这个优化手段的具体实现思路,首先还是从一个简单的 demo 来看:
<script> import { createComponent } from '@mpxjs/core' createComponent({ data: { obj: { a: { c: 1, d: 2 } } } onShow() { setTimeout(() => { this.obj.a = { c: 1, d: 'd' } }, 200) } }) </script>
在示例 demo 当中,声明了一个 obj 对象(这个对象里面的内容在模块当中被使用到了)。然后经过 200ms 后,手动修改 obj.a 的值,因为对于 c 字段来说它的值没有发生改变,而 d 字段发生了改变。因此在 setData 方法当中也应该只更新 obj.a.d 的值,即:
this.setData('obj.a.d', 'd')
因为 mpx 是整体接管了小程序当中有关调用 setData 方法并驱动视图更新的机制。所以当你在改变某些数据的时候,mpx 会帮你完成数据的 diff 工作,以保证每次调用 setData 方法时,传入的是最小的更新数据集。
这里也简单的分析下 mpx 是如何去实现这样的功能的。在上文的编译构建阶段有分析到 mpx 生成的 Render Function,这个 Render Function 每次执行的时候会返回一个 renderData,而这个 renderData 即用以接下来进行 setData 驱动视图渲染的原始数据。renderData 的数据组织形式是模板当中使用到的数据路径作为 key 键值,对应的值使用一个数组组织,数组第一项为数据的访问路径(可获取到对应渲染数据),第二项为数据路径的第一个键值,例如在 demo 示例当中的 renderData 数据如下:
renderData['obj.a.c'] = [this.obj.a.c, 'obj'] renderData['obj.a.d'] = [this.obj.a.d, 'obj']
当页面第一次渲染,或者是响应式输出发生变化的时候,Render Function 都会被执行一次用以获取最新的 renderData 来进行接下来的页面渲染过程。
// src/core/proxy.js class MPXProxy { ... renderWithData(rawRenderData) { // rawRenderData 即为 Render Function 执行后获取的初始化 renderData const renderData = preprocessRenderData(rawRenderData) // renderData 数据的预处理 if (!this.miniRenderData) { // 最小数据渲染集,页面/组件初次渲染的时候使用 miniRenderData 进行渲染,初次渲染的时候是没有数据需要进行 diff 的 this.miniRenderData = {} for (let key in renderData) { // 遍历数据访问路径 if (renderData.hasOwnProperty(key)) { let item = renderData[key] let data = item[0] let firstKey = item[1] // 某个字段 path 的第一个 key 值 if (this.localKeys.indexOf(firstKey) > -1) { this.miniRenderData[key] = diffAndCloneA(data).clone } } } this.doRender(this.miniRenderData) } else { // 非初次渲染使用 processRenderData 进行数据的处理,主要是需要进行数据的 diff 取值工作,并更新 miniRenderData 的值 this.doRender(this.processRenderData(renderData)) } } processRenderData(renderData) { let result = {} for (let key in renderData) { if (renderData.hasOwnProperty(key)) { let item = renderData[key] let data = item[0] let firstKey = item[1] let { clone, diff } = diffAndCloneA(data, this.miniRenderData[key]) // 开始数据 diff // firstKey 必须是为响应式数据的 key,且这个发生变化的 key 为 forceUpdateKey 或者是在 diff 阶段发现确实出现了 diff 的情况 if (this.localKeys.indexOf(firstKey) > -1 && (this.checkInForceUpdateKeys(key) || diff)) { this.miniRenderData[key] = result[key] = clone } } } return result } ... } // src/helper/utils.js // 如果 renderData 里面即包含对某个 key 的访问,同时还有对这个 key 的子节点访问的话,那么需要剔除这个子节点 /** * process renderData, remove sub node if visit parent node already * @param {Object} renderData * @return {Object} processedRenderData */ export function preprocessRenderData (renderData) { // method for get key path array const processKeyPathMap = (keyPathMap) => { let keyPath = Object.keys(keyPathMap) return keyPath.filter((keyA) => { return keyPath.every((keyB) => { if (keyA.startsWith(keyB) && keyA !== keyB) { let nextChar = keyA[keyB.length] if (nextChar === '.' || nextChar === '[') { return false } } return true }) }) } const processedRenderData = {} const renderDataFinalKey = processKeyPathMap(renderData) // 获取最终需要被渲染的数据的 key Object.keys(renderData).forEach(item => { if (renderDataFinalKey.indexOf(item) > -1) { processedRenderData[item] = renderData[item] } }) return processedRenderData }
其中在 processRenderData 方法内部调用了 diffAndCloneA 方法去完成数据的 diff 工作。在这个方法内部判断新、旧值是否发生变化,返回的 diff 字段即表示是否发生了变化,clone 为 diffAndCloneA 接受到的第一个数据的深拷贝值。
这里大致的描述下相关流程:
相关参阅文档:
由于小程序的双线程的架构设计,逻辑层和视图层之间需要桥接 native bridge。如果要完成视图层的更新,那么逻辑层需要调用 setData 方法,数据经由 native bridge,再到渲染层,这个工程流程为:
文章来源
而 setData 作为逻辑层和视图层之间通讯的核心接口,那么对于这个接口的使用遵照一些准则将有助于性能方面的提升。
Mpx 在这个方面所做的工作之一就是基于数据路径的 diff。这也是官方所推荐的 setData 的方式。每次响应式数据发生了变化,调用 setData 方法的时候确保传递的数据都为 diff 过后的最小数据集,这样来减少 setData 传输的数据。
接下来我们就来看下这个优化手段的具体实现思路,首先还是从一个简单的 demo 来看:
在示例 demo 当中,声明了一个 obj 对象(这个对象里面的内容在模块当中被使用到了)。然后经过 200ms 后,手动修改 obj.a 的值,因为对于 c 字段来说它的值没有发生改变,而 d 字段发生了改变。因此在 setData 方法当中也应该只更新 obj.a.d 的值,即:
因为 mpx 是整体接管了小程序当中有关调用 setData 方法并驱动视图更新的机制。所以当你在改变某些数据的时候,mpx 会帮你完成数据的 diff 工作,以保证每次调用 setData 方法时,传入的是最小的更新数据集。
这里也简单的分析下 mpx 是如何去实现这样的功能的。在上文的编译构建阶段有分析到 mpx 生成的 Render Function,这个 Render Function 每次执行的时候会返回一个 renderData,而这个 renderData 即用以接下来进行 setData 驱动视图渲染的原始数据。renderData 的数据组织形式是模板当中使用到的数据路径作为 key 键值,对应的值使用一个数组组织,数组第一项为数据的访问路径(可获取到对应渲染数据),第二项为数据路径的第一个键值,例如在 demo 示例当中的 renderData 数据如下:
当页面第一次渲染,或者是响应式输出发生变化的时候,Render Function 都会被执行一次用以获取最新的 renderData 来进行接下来的页面渲染过程。
其中在 processRenderData 方法内部调用了 diffAndCloneA 方法去完成数据的 diff 工作。在这个方法内部判断新、旧值是否发生变化,返回的 diff 字段即表示是否发生了变化,clone 为 diffAndCloneA 接受到的第一个数据的深拷贝值。
这里大致的描述下相关流程:
相关参阅文档: