Open CommanderXL opened 4 years ago
每次调用 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 加入到执行队列:
this.update()
this.run()
queueWatcher(this)
// 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 }
每次调用 setData 方法都会完成一次从逻辑层 -> native bridge -> 视图层的通讯,并完成页面的更新。因此频繁的调用 setData 方法势必也会造成视图的多次渲染,用户的交互受阻。所以对于 setData 方法另外一个优化角度就是尽可能的减少 setData 的调用频次,将多个同步的 setData 操作合并到一次调用当中。接下来就来看下 mpx 在这方面是如何做优化的。
还是先来看一个简单的 demo:
在示例 demo 当中,msg 和 obj 都作为模板依赖的数据,这个组件开始展示后的 200ms,更新 obj.a 的值,同时 obj 被 watch,当 obj 发生改变后,更新 msg 的值。这里的逻辑处理顺序是:
接下来就来具体看下这个流程:由于 obj 作为模板渲染的依赖数据,自然会被这个组件的 renderWatch 作为依赖而被收集。当 obj 的值发生变化后,首先触发 reaction 的回调,即
this.update()
方法,如果是个同步的 watch,那么立即调用this.run()
方法,即 watcher 监听的回调方法,否则就通过queueWatcher(this)
方法将这个 watcher 加入到执行队列:而在 queueWatcher 方法中,lockTask 维护了一个异步锁,即将 flushQueue 当成微任务统一放到下一帧去执行。所以在 flushQueue 开始执行之前,还会有同步的代码将 watcher 加入到执行队列当中,当 flushQueue 开始执行的时候,依照 watcher.id 升序依次执行,这样去确保 renderWatcher 在执行前,其他所有的 watcher 回调都执行完了,即执行 renderWatcher 的回调的时候获取到的 renderData 都是最新的,然后再去进行 setData 的操作,完成页面的更新。