CommanderXL / Biu-blog

个人博客
432 stars 39 forks source link

【Mpx】性能优化-part2(尽可能的减少 setData 的调用频次) #47

Open CommanderXL opened 4 years ago

CommanderXL commented 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 加入到执行队列:

// 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
}