【Mpx】性能优化-part2(尽可能的减少 setData 的调用频次)
CommanderXL opened this issue · 0 comments
每次调用 setData 方法都会完成一次从逻辑层 -> native bridge -> 视图层的通讯,并完成页面的更新。因此频繁的调用 setData 方法势必也会造成视图的多次渲染,用户的交互受阻。所以对于 setData 方法另外一个优化角度就是尽可能的减少 setData 的调用频次,将多个同步的 setData 操作合并到一次调用当中。接下来就来看下 mpx 在这方面是如何做优化的。
还是先来看一个简单的 demo:
<script>
import { createComponent } from '@mpxjs/core'
createComponent({
data: {
msg: 'hello',
obj: {
a: {
c: 1,
d: 2
}
}
}
watch: {
obj: {
handler() {
this.msg = 'world'
},
deep: true
}
},
onShow() {
setTimeout(() => {
this.obj.a = {
c: 1,
d: 'd'
}
}, 200)
}
})
</script>
在示例 demo 当中,msg 和 obj 都作为模板依赖的数据,这个组件开始展示后的 200ms,更新 obj.a 的值,同时 obj 被 watch,当 obj 发生改变后,更新 msg 的值。这里的逻辑处理顺序是:
obj.a 变化 -> 将 renderWatch 加入到执行队列 -> 触发 obj watch -> 将 obj watch 加入到执行队列 -> 将执行队列放到下一帧执行 -> 按照 watch id 从小到大依次执行 watch.run -> setData 方法调用一次(即 renderWatch 回调),统一更新 obj.a 及 msg -> 视图重新渲染
接下来就来具体看下这个流程:由于 obj 作为模板渲染的依赖数据,自然会被这个组件的 renderWatch 作为依赖而被收集。当 obj 的值发生变化后,首先触发 reaction 的回调,即 this.update()
方法,如果是个同步的 watch,那么立即调用 this.run()
方法,即 watcher 监听的回调方法,否则就通过 queueWatcher(this)
方法将这个 watcher 加入到执行队列:
// src/core/watcher.js
export default Watcher {
constructor (context, expr, callback, options) {
...
this.id = ++uid
this.reaction = new Reaction(`mpx-watcher-${this.id}`, () => {
this.update()
})
...
}
update () {
if (this.options.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
而在 queueWatcher 方法中,lockTask 维护了一个异步锁,即将 flushQueue 当成微任务统一放到下一帧去执行。所以在 flushQueue 开始执行之前,还会有同步的代码将 watcher 加入到执行队列当中,当 flushQueue 开始执行的时候,依照 watcher.id 升序依次执行,这样去确保 renderWatcher 在执行前,其他所有的 watcher 回调都执行完了,即执行 renderWatcher 的回调的时候获取到的 renderData 都是最新的,然后再去进行 setData 的操作,完成页面的更新。
// src/core/queueWatcher.js
import { asyncLock } from '../helper/utils'
const queue = []
const idsMap = {}
let flushing = false
let curIndex = 0
const lockTask = asyncLock()
export default function queueWatcher (watcher) {
if (!watcher.id && typeof watcher === 'function') {
watcher = {
id: Infinity,
run: watcher
}
}
if (!idsMap[watcher.id] || watcher.id === Infinity) {
idsMap[watcher.id] = true
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > curIndex && watcher.id < queue[i].id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
lockTask(flushQueue, resetQueue)
}
}
function flushQueue () {
flushing = true
queue.sort((a, b) => a.id - b.id)
for (curIndex = 0; curIndex < queue.length; curIndex++) {
const watcher = queue[curIndex]
idsMap[watcher.id] = null
watcher.destroyed || watcher.run()
}
resetQueue()
}
function resetQueue () {
flushing = false
curIndex = queue.length = 0
}