小程序中,一个重要的性能环节就是同步 worker 进程数据到渲染进程。对于使用响应式来管理状态的状况,搜索社区实现,能够发现不少只是粗暴地递归遍历一下复杂对象,从而监听到数据变化。javascript
在 Goldfish 中,一样使用了响应式引擎来管理状态数据。响应式天生的好处是:可以精确监听状态数据变化,而后生成最小化的数据更新对象。java
举个例子,假如如今有一个响应式对象:node
const observableObj = { name: '禺疆', address: { city: 'ChengDu', }, };
若是将 city
修改成 'HangZhou'
,那么很容易生成小程序中 setData
能直接使用的以下数据更新对象:react
const updateObj = { 'address.city': 'HangZhou', };
固然,咱们不可能数据每次变化的时候,就当即调用 setData
去更新数据,毕竟频繁更新是很耗性能的。因此,咱们须要使用 setData
、 $spliceData
、 $batchedUpdates
批量更新。git
要作批量更新,第一步就是划分什么时间段内的更新算是一个批量。github
很天然地,咱们想到使用 setTimeout
:在监听到数据更新请求时,使用 setTimeout
计时,搜集时间段内全部的数据更新需求,在计时结束时统一更新。typescript
实际上,在移动端应当谨慎使用 setInterval
、setTimeout
计时,因为移动设备节省电量,很容易不许。好比 setInterval
设置时间间隔为 8 分钟,在移动设备上很容易出现时间间隔变长为 16 分钟左右。小程序
既然 setTimeout
不行,那么咱们第二个想到的多是 requestAnimationFrame
。很遗憾,小程序 worker 进程里面没有 requestAnimationFrame
。数组
最后,只剩下 Microtask 了。在小程序的 worker 进程里,咱们能够借助 Promise.resolve()
来生成 Microtask,参考以下伪代码:数据结构
setData request 1 setData request 2 setData request 3 await Promise.resolve() combine request 1 2 3 setData
实际上,因为响应式引擎的监听回调触发作了 Promise.resolve()
批量处理的逻辑,而且在咱们的业务代码中,也很容出现 Microtask,数据更新请求(setData Request)并非上述规规矩矩从上到下同步执行的,极可能在若干个 Microtask 中穿插请求。所以,上述搜集到的数据更新请求是不完整的,咱们须要搜集到当前同步代码块及同步代码块中产生的全部 Microtask 生成的数据更新请求:
export class Batch { private segTotalList: number[] = []; private counter = 0; private cb: () => void; public constructor(cb: () => void) { this.cb = cb; } // 每次有数据请求的时候,都调用一下 set。 public async set() { const segIndex = this.counter === 0 ? this.segTotalList.length : (this.segTotalList.length - 1); if (!this.segTotalList[segIndex]) { this.segTotalList[segIndex] = 0; } this.counter += 1; this.segTotalList[segIndex] += 1; await Promise.resolve(); this.counter -= 1; // 同步块中最后一个 set 调用对应的 Microtask if (this.counter === 0) { const segLength = this.segTotalList.length; // 看看下一个 Microtask 触发前,是否还有新的更新请求进来。 // 若是没有,说明更新请求稳定了,当即触发更新逻辑(this.cb) await Promise.resolve(); if (this.segTotalList.length === segLength) { this.cb(); this.counter = 0; this.segTotalList = []; } } } }
搞定更新时机以后,咱们只须要在合适的时机,将积累的更新逻辑放置在 $batchedUpdates
中执行就行了。
可是在项目中发现,页面初始数据格式化的时候,若是数据结构很复杂,就很容易产生具备大量扁平 key 的更新对象,相似这样:
setData({ 'state.key1': 'xxx', 'state.key2.key21': 'xx', 'state.key3': 'xxx', ... });
虽然更新对象看起来都很“最小化”,可是传递给渲染进程并还原成正常对象的过程当中,确定少不了耗时的 key 恢复处理。咱们也实际测试过,若是直接调用 setData
去更新复杂数据对象,小程序仍是比较流畅的,可是换成“最小化”更新对象以后,小程序有明显的卡滞。
所以,在构造更新数据时,应当设置一个 key 数量上限,若是超出上限,应当合并,造成 key 数量更小的更新对象。好比上述示例,能够合并成:
setData({ state: { ...this.data.state, ...{ key1: 'xxx', key2: { key21: 'xx', }, key3: 'xxx', }, }, ... });
咱们能够把更新对象当作一棵树,好比上述例子,对应的树形结构以下:
state / | \ key1 key2 key3 | key21
有多少个叶子节点,就会生成多少个 key。
在搜集更新请求阶段,能够顺手构造对应的树形结构。在更新时,按照深度优先的顺序遍历树,生成更新对象。遍历过程当中,记录已生成的 key 数量。可能遍历到树中某个节点时,发现加上直接子节点数量,已经超过 key 数量限制了,此时就不要向下遍历了,直接在该节点处生成更新对象。代码参考:
class UpdateTree { private root = new Ancestor(); private view: View; private limitLeafTotalCount: LimitLeafCounter; public constructor(view: View, limitLeafTotalCount: LimitLeafCounter) { this.view = view; this.limitLeafTotalCount = limitLeafTotalCount; } // 构造树 public addNode(keyPathList: (string | number)[], value: any) { let curNode = this.root; const len = keyPathList.length; keyPathList.forEach((keyPath, index) => { if (curNode.children === undefined) { if (typeof keyPath === 'number') { curNode.children = []; } else { curNode.children = {}; } } if (index < len - 1) { const child = (curNode.children as any)[keyPath]; if (!child || child instanceof Leaf) { const node = new Ancestor(); node.parent = curNode; (curNode.children as any)[keyPath] = node; curNode = node; } else { curNode = child; } } else { const lastLeafNode: Leaf = new Leaf(); lastLeafNode.parent = curNode; lastLeafNode.value = value; (curNode.children as any)[keyPath] = lastLeafNode; } }); } private getViewData(viewData: any, k: string | number) { return isObject(viewData) ? viewData[k] : null; } private combine(curNode: Ancestor | Leaf, viewData: any): any { if (curNode instanceof Leaf) { return curNode.value; } if (!curNode.children) { return undefined; } if (Array.isArray(curNode.children)) { return curNode.children.map((child, index) => { return this.combine(child, this.getViewData(viewData, index)); }); } const result: Record<string, any> = isObject(viewData) ? viewData : {}; for (const k in curNode.children) { result[k] = this.combine(curNode.children[k], this.getViewData(viewData, k)); } return result; } private iterate( curNode: Ancestor | Leaf, keyPathList: (string | number)[], updateObj: Record<string, any>, viewData: any, availableLeafCount: number, ) { if (curNode instanceof Leaf) { updateObj[generateKeyPathString(keyPathList)] = curNode.value; this.limitLeafTotalCount.addLeaf(); } else { const children = curNode.children; const len = Array.isArray(children) ? children.length : Object.keys(children || {}).length; if (len > availableLeafCount) { updateObj[generateKeyPathString(keyPathList)] = this.combine(curNode, viewData); this.limitLeafTotalCount.addLeaf(); } else if (Array.isArray(children)) { children.forEach((child, index) => { this.iterate( child, [ ...keyPathList, index, ], updateObj, this.getViewData(viewData, index), this.limitLeafTotalCount.getRemainCount() - len, ); }); } else { for (const k in children) { this.iterate( children[k], [ ...keyPathList, k, ], updateObj, this.getViewData(viewData, k), this.limitLeafTotalCount.getRemainCount() - len, ); } } } } // 生成更新对象 public generate() { const updateObj: Record<string, any> = {}; this.iterate( this.root, [], updateObj, this.view.data, this.limitLeafTotalCount.getRemainCount(), ); return updateObj; } public clear() { this.root = new Ancestor(); } }
到此为止,咱们已经能在合适的时机,针对某个页面或组件生成限定数量的 key 去同步数据了。
还有个问题须要解决:更新顺序。上述更新过程,咱们会针对普通对象,使用 setData
,针对数组,使用 $spliceData
。在这两个方法以前,会分别准备好两个方法的对象参数。假设以下场景:
// page 的 data.list 中已经存在一个元素 pageInstance.data = { list: ['0'], }; // 某个时刻,调用 setData 和 $spliceData 更新数据 pageInstance.setData({ 'list[1]': '1', }); pageInstance.$spliceData({ list: [1, 0, '2'], });
更新完成以后,pageInstance.data.list
变为 ['0', '2', '1']
,若是调换 setData
和 $spliceData
的顺序,那么 pageInstance.data.list
将会变为 ['0', '1']
。
所以,咱们不能打乱批量更新中 setData
和 $spliceData
的调用顺序。
此时,咱们构造的批量更新逻辑必须知足:
为了保持顺序,在批量更新块中,好比:
setData request setData request spliceData request spliceData request setData request
前两个合并成一个 setData
更新对象,中间两个合并成一个 $spliceData
更新对象,最后一个是单独的 setData
更新对象。
先后两个 setData 更新对象的 key 数量,统一受 key 数量的限制。
绝大多数状况下,$spliceData 更新对象会比较小,所以不限制该更新对象的 key 数量。
至此,全部已知问题处理完毕,完整代码参考此处。