vue异步更新机制

531 阅读3分钟

异步更新队列

vue中更新dom是异步执行的,只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。(先缓冲,缓冲后再将多次触发属性的最后一次作为promise放入队列;而不是一上来就放队列)

关于上面这句话的理解:vue中数据的更新到dom是异步的,数据的异步更新可以理解成一个promise的微任务;并且对同一个属性值进行多次赋值时,只有最后一次赋值会作为一个promise微任务放到更新队列。具体上代码:

场景1结论:dom异步更新,所谓的异步相当于一个promise微任务

// 场景1:

<div id="example">{{message}}</div>
<script>
  var vm = new Vue({
    el: '#example',
    data: {
      message: '123'
    }
  })
  
	// 碰到这句时,把这句理解成一个promise放在promise队列
  vm.message = 'new message' // 更改数据

  Promise.resolve(100).then(value => {
    console.log("如果上面的赋值是相当于promise,那么这句输出结果应该是new message", vm.$el.textContent);		// 第二步输出:new message
  })
  console.log(vm.$el.textContent);	// 第一步输出:123
</script>

场景2结论:赋值改变响应式属性会被当做一个promise,并且执行的顺序也是跟正常promise队列的顺序一样先进先出

//示例1:
var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})

vm.message = 'new' // 更改数据

Promise.resolve(100).then(value => {
  console.log("测试第一次赋值", vm.$el.textContent);		// 第二步输出:new (因为vm.message = 'new' 先放进promise队列,所以先执行,所以本行输出数据已经改变为new)
})
console.log(vm.$el.textContent);		// 第一步输出:123




// 示例2:
var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})

Promise.resolve(100).then(value => {
  console.log("测试第一次赋值", vm.$el.textContent);		// 第二步输出:123 (因为这里vm.message = 'new'这个promise在队列的第二位,所以还没执行,所以此时这里的输出还是123)
})

vm.message = 'new' // 更改数据
console.log(vm.$el.textContent);		// 第一步输出:123

场景3结论:场景3说明了异步更新的执行顺序和原理见代码下方总结

// 示例1:
<div id="example">{{message}}</div>
<script>
  var vm = new Vue({
    el: '#example',
    data: {
      message: '123'
    }
  })

  vm.message = 'msg' // 更改数据

  Promise.resolve(100).then(value => {
    console.log("测试第一次赋值", vm.$el.textContent);		// 第二步输出:new
  })

  vm.message = 'new' // 更改数据
  console.log("==", vm.$el.textContent);		// 第一步123

  Promise.resolve(100).then(value => {
    console.log("测试第二次赋值", vm.$el.textContent);		// 第三步输出:new
  })
</script>



// 示例2:
<div id="example">{{message}}</div>
<script>
  var vm = new Vue({
    el: '#example',
    data: {
      message: '123'
    }
  })

  Promise.resolve(1).then(value => {
    console.log("xxx", vm.$el.textContent);		// 第二步输出:123
  })

  vm.message = 'msg' // 更改数据

  Promise.resolve(2).then(value => {
    console.log("测试", vm.$el.textContent);  // 第三步输出:new
  })

  vm.message = 'new' // 更改数据

  Promise.resolve(3).then(value => {
    console.log("fff", vm.$el.textContent);			// 第四步输出:new
  })
  
  console.log(vm.$el.textContent);			// 第一步输出:123
</script>


// 总结:场景3的示例1和2一起说明vue异步更新原理。以场景3的示例2简单说下流程:主流程顺序执行代码碰到promise1,把promise1放到微任务队列第一位,接着主线程继续执行同步代码,碰到vm.message = 'msg',将vm.message = 'msg'作为promise放到任务队列第二位,并将vm.message属性放在缓冲区中,接着主线程继续执行同步代码发现promise2,将promise2放到微任务队列第三位,主线程继续执行同步代码,碰到 vm.message = 'new',发现vm.message这个属性已经在缓冲区了,所以会把'new'这个值也缓存到缓冲区(但不会将vm.message = 'new'作为微任务放到微任务队列的),接着主线程继续执行同步代码,碰到promise3,将promise3放入微任务队列的第四位,接着主线程继续执行同步代码碰到console.log(vm.$el.textContent);	此时直接执行,则第一步输出123;主线程执行栈为空了,此时会把刚才在缓冲区的vm.message属性的值 'new' 赋值给微任务中排在第二位"vm.message = 'msg'",赋值后vm.message就等于'new';然后主线程按照队列法则先进先出开始执行微任务队列的任务,第一位任务promise1输出xxx123,第二位任务"vm.message = 'new'" 渲染更新到div#example的dom节点上,第三位任务promise2输出 测试new(因为上一个任务已经将new更新到dom节点了)。第四位任务promise3输出 fff new。
主要注意的点是vue会把更新数据的异步任务按照主线程的同步代码执行正常放到队列,但是会将属性缓存到缓冲区,如果执行同步代码时再遇见则更新缓冲区的属性值,而不会再遇见时当做任务放入队列中;最后等到同步代码执行完毕后,会把缓冲区最终的值更新到微任务队列中的异步数据更新任务上, 然后开始从头到尾执行微任务队列。
最后圆满结束。