实现 Vue异步更新队列

236 阅读2分钟

首先我们先了解下Vue的视图更新机制,可以总结为以下流程

1.数据改变 --> 2.生成新的 virtual dom --> 3.新旧 virtual dom 对比 patch --> 4.真实dom更新 

思考以下代码

new Vue({
  el: '#example',
  data(){
      return{
          num:1
      }
  },
  methods: {
    changeData: function () {
    for (let i = 0; i < 100; i++) {
         this.num += 1
        }
    }
  }
})

根据我们上面总结的 vue 视图更新机制,那么执行 changeData时,将会触发 100 次视图更新流程(步骤2 到步骤5),显然易见,这会导致很大的性能问题,所以 vue 内部使用了 异步更新队列来优化。

异步更新队列

  • 数据更新不会立即触发新的virtual dom生成以及diff patch,而是开启一个队列,并缓冲在同一事件循环中发生的所有数据变更
  • 下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

自己实现一个简单的异步更新队列

以下代码会执行100次 setState 方法 ,而每次 执行 setState将会调用一次 render函数,实际情况中,render函数将会包含大量的计算(diff、 patch),我们要做的优化就是将 render函数的执行次数降低 为 1 次

const state = {
  num: 1
}

for (let i = 0; i < 100; i++) {
  setState({
    num: num + 1
  })
}

function setState(stateChange) {
  Object.assign(state, stateChange)
  render(state)
}

function render(state) {
  console.log('state 的值是:', state);
}

源码

const state = {
  num: 1
}

const setStateQueue = [];//状态改变队列,每次状态改变时候将 stateChange push 进来

for (let i = 0; i < 100; i++) {
  setState({
    num: state.num + 1
  })
}

function setState(stateChange) {
  enqueueSetState(stateChange)
}

/* 
在 Vue 2.4 之前都是使用的 microtasks,
但是 microtasks 的优先级过高,在某些情况下可能会出现比事件冒泡更快的情况,
但如果都使用 macrotasks 又可能会出现渲染的性能问题。
所以在新版本中,会默认使用 microtasks,但在特殊情况下会使用 macrotasks,比如 v-on。
为了简单起见, 我们先使用 promise 来生成  microtasks
*/
function defer(fn) {
  return Promise.resolve().then(fn);
}

function enqueueSetState(stateChange) {
  if (setStateQueue.length === 0) { 
    defer(flush)
  }
  setStateQueue.push({ stateChange });
}

function flush() {
  let item

  while (item = setStateQueue.shift()) { //执行队列内的每一次数据变更
    const { stateChange } = item;
    Object.assign(state, stateChange);
  }
  render(state)
}

function render(state) {
  console.log('render 函数中获取 state 的值是:', state);
}


执行下看看,render 确实只执行了一次

参考资料: