CommanderXL / Biu-blog

个人博客
431 stars 39 forks source link

【Mpx】性能优化-part1 (尽可能的减少 setData 传输的数据) #46

Open CommanderXL opened 4 years ago

CommanderXL commented 4 years ago

由于小程序的双线程的架构设计,逻辑层和视图层之间需要桥接 native bridge。如果要完成视图层的更新,那么逻辑层需要调用 setData 方法,数据经由 native bridge,再到渲染层,这个工程流程为:

小程序逻辑层调用宿主环境的 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 接受到的第一个数据的深拷贝值。

这里大致的描述下相关流程:

  1. 响应式的数据发生了变化,触发 Render Function 重新执行,获取最新的 renderData;
  2. renderData 的预处理,主要是用以剔除通过路径访问时同时有父、子路径情况下的子路径的 key;
  3. 判断是否存在 miniRenderData 最小数据渲染集,如果没有那么 Mpx 完成 miniRenderData 最小渲染数据集的收集,如果有那么使用处理后的 renderData 和 miniRenderData 进行数据的 diff 工作(diffAndCloneA),并更新最新的 miniRenderData 的值;
  4. 调用 doRender 方法,进入到 setData 阶段

相关参阅文档: