vue队列

vue通常鼓励开发人员沿着“数据驱动”的方式思考,避免直接接触 DOM。Vue的dom更新是异步的,当数据发生变化,vue并不是里面去更新dom,而是开启一个队列。跟JavaScript原生的同步任务和异步任务相同。

比如我们调用一个方法,同时涉及多个数据的操作改变,vue会把这一些列操作推入到一个队列中,相当于JavaScript的同步任务,在执行过程中可能会出现一些产生任务队列的异步任务,比如定时器、回调等。

在vue里面任务队列也叫事件循环队列。我们都知道JavaScript是循环往复的执行任务队列。Vue也一样,在一个同步任务过程中是不会去更新渲染视图,而是在同步任务(事件循环队列)执行完毕之后,在主线程的同步执行完毕,读取任务队列时更新视图。

这个机制对于页面性能是非常重要的,试想一下,我们要是每操作一个数据就更新一次视图,那么在性能上会非常差。而如果是在一次任务执行完毕之后更新视图,可以避免特别多的重复操作。

在开发过程中,我们很容易遇见需要先渲染数据然后操作dom,这时候就要使用vue提供的nextTick函数。

对于这个函数有这样两句话:

Vue.nextTick(callback),当数据发生变化,更新后执行回调。

Vue.$nextTick(callback),当dom发生变化,更新后执行的回调。

我测试了很多例子,最后还是只实现了dom的变化,对于第一句没有实现:

<p >{{msg}}</p>
<button @click="add">加</button>

add(){
  this.msg = 'have';
  console.log(document.getElementById('msg').innerHTML);  这个不能实现
  this.$nextTick(() => {
      console.log(document.getElementById('msg').innerHTML);  可以实现
  });
}

=========================

一般来讲,任务队列分为macro-task和micro-task,这两者是什么自己百度。事件循环会执行任务队列中的任务,当执行任务队列时,会先执行一个macro-task,再执行所有的micro-task,然后浏览器根据情况判断是否渲染,这是一次完整的事件循环。然后会再执行一个macro-task,执行此时所有的micro-task,再渲染,又是一次事件循环,一直重复。

那么,同一事件循环,一般指的是在macro-task执行时所发生的数据变更,因为vue异步更新用到了nextTick,这个方法内部就是调用了js异步方法的回调,比如MutationObserver的回调,或者setTimeout的回调,MutationObserver属于micro-task,setTimeout属于macro-task。

所以,如果异步更新是在MutationObserver(有的浏览器不支持,此时会用setTimeout)中,那么更新就是在此次macro-task执行完之后的micro-task,因为MutationObserver属于micro-task。这个时候,数据还都同在一次事件循环中,而且是本次事件循环还没结束。

如果是setTimeout,因为setTimeout属于macro-task,其实就是下一次事件循环了,因为一次事件循环只执行一个macro-task(这个有待商榷,不同浏览器好像不同,我们暂且认为一次循环只执行一个),那么更新就是在下一次事件循环了,这个时候的数据,也都是一次事件循环的数据,只不过是上一次的。人家的描述没毛病,是同一事件循环内的。

源码

#batcher.js
import config from './config'
import {
  warn,
  nextTick,
  devtools
} from './util/index'

// we have two separate queues: one for directive updates
// and one for user watcher registered via $watch().
// we want to guarantee directive updates to be called
// before user watchers so that when user watchers are
// triggered, the DOM would have already been in updated
// state.

var queue = [] //这个应该用于指令异步更新的队列
var userQueue = [] //这个应该是用于用户定义的data异步更新的队列
var has = {} 
var circular = {}
var waiting = false

/**
 * Reset the batcher's state.
 */
// 重置队列状态
function resetBatcherState () {
  queue.length = 0
  userQueue.length = 0
  has = {}
  circular = {}
  waiting = false
}

/**
 * Flush both queues and run the watchers.
 */
//执行这2个队列里的异步更新
function flushBatcherQueue () {
  runBatcherQueue(queue)
  runBatcherQueue(userQueue)
  // user watchers triggered more watchers,
  // keep flushing until it depletes
  if (queue.length) { //如果执行的时候又有异步更新被查到队列中,就继续执行
    return flushBatcherQueue() 
  }
  // dev tool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
  resetBatcherState() 执行完后重置异步更新队列
}

/**
 * Run the watchers in a single queue.
 *
 * @param {Array} queue
 */
//这里就是循环更新异步队列中的watcher
function runBatcherQueue (queue) {
  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (let i = 0; i < queue.length; i++) {
    var watcher = queue[i]
    var id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > config._maxUpdateCount) {
        warn(
          'You may have an infinite update loop for watcher ' +
          'with expression "' + watcher.expression + '"',
          watcher.vm
        )
        break
      }
    }
  }
  queue.length = 0
}

/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 *
 * @param {Watcher} watcher
 *   properties:
 *   - {Number} id
 *   - {Function} run
 */
//这个开放的接口,把需要更新的watcher添加到异步队列中
export function pushWatcher (watcher) {
  const id = watcher.id
  if (has[id] == null) {
    // push watcher into appropriate queue
    const q = watcher.user
      ? userQueue
      : queue
    has[id] = q.length
    q.push(watcher)
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushBatcherQueue)
    }
  }
}

 =====================

 还有这个文章

https://www.cnblogs.com/DevinnZ/p/11065645.html

=================

异步更新队列指的是当状态发生变化时,Vue异步执行DOM更新。

我们在项目开发中会遇到这样一种场景:当我们将状态改变之后想获取更新后的DOM,往往我们获取到的DOM是更新前的旧DOM,我们需要使用vm.$nextTick方法异步获取DOM,例如:

Vue. component( 'example', { template :'<span>{{ message }}</span>', data:function() {

return{ message :'没有更新'} }, methods :{ updateMessage:function() {

this. message='更新完成'console. log( this. $el. textContent) //=> '没有更新'this. $nextTick( function() {

console. log( this. $el. textContent) //=> '更新完成'}) } }})

我们都知道这样做很麻烦,但为什么Vue还要这样做呢?

首先我们假设Vue是同步执行DOM更新,会有什么问题?

如果同步更新DOM将会有这样一个问题,我们在代码中同步更新数据N次,DOM也会更新N次,伪代码如下:

this. message='更新完成'//DOM更新一次

this. message='更新完成2'//DOM更新两次

this. message='更新完成3'//DOM更新三次

this. message='更新完成4'//DOM更新四次

但事实上,我们真正想要的其实只是最后一次更新而已,也就是说前三次DOM更新都是可以省略的,我们只需要等所有状态都修改好了之后再进行渲染就可以减少一些无用功。

而这种无用功在Vue2.0开始变得更为重要,Vue2.0开始引入了Virtualdom,每一次状态发生变化后,状态变化的信号会发送给组件,组件内部使用VirtualDOM进行计算得出需要更新的具体的DOM节点,然后对DOM进行更新操作,每次更新状态后的渲染过程需要更多的计算,而这种无用功也将浪费更多的性能,所以异步渲染变得更加至关重要。

组件内部使用VIrtualDOM进行渲染,也就是说,组件内部其实是不关心哪个状态发生了变化,它只需要计算一次就可以得知哪些节点需要更新。也就是说,如果更改了N个状态,其实只需要发送一个信号就可以将DOM更新到最新。例如:

this. message='更新完成'

this. age=23

this. name=berwin

代码中我们分三次修改了三种状态,但其实Vue只会渲染一次。因为VIrtualDOM只需要一次就可以将整个组件的DOM更新到最新,它根本不会关心这个更新的信号到底是从哪个具体的状态发出来的。

那如何才能将渲染操作推迟到所有状态都修改完毕呢?很简单,只需要将渲染操作推迟到本轮事件循环的最后或者下一轮事件循环。也就是说,只需要在本轮事件循环的最后,等前面更新状态的语句都执行完之后,执行一次渲染操作,它就可以无视前面各种更新状态的语法,无论前面写了多少条更新状态的语句,只在最后渲染一次就可以了。

将渲染推迟到本轮事件循环的最后执行渲染的时机会比推迟到下一轮快很多,所以Vue优先将渲染操作推迟到本轮事件循环的最后,如果执行环境不支持会降级到下一轮。

当然,Vue的变化侦测机制决定了它必然会在每次状态发生变化时都会发出渲染的信号,但Vue会在收到信号之后检查队列中是否已经存在这个任务,保证队列中不会有重复。如果队列中不存在则将渲染操作添加到队列中。

之后通过异步的方式延迟执行队列中的所有渲染的操作并清空队列,当同一轮事件循环中反复修改状态时,并不会反复向队列中添加相同的渲染操作。

所以我们在使用Vue时,修改状态后更新DOM都是异步的。

说到这里简单介绍下什么是事件循环。

事件循环机制

JS中存在一个叫做执行栈的东西。JS的所有同步代码都在这里执行,当执行一个函数调用时,会创建一个新的执行环境并压到栈中开始执行函数中的代码,当函数中的代码执行完毕后将执行环境从栈中弹出,当栈空了,也就代表执行完毕。

这里有一个问题是代码中不只是同步代码,也会有异步代码。当一个异步任务执行完毕后会将任务添加到任务队列中。例如:

setTimeout( _=>{}, 1000)

代码中setTimeout会在一秒后将回调函数添加到任务队列中。事实上异步队列也分两种类型:微任务、宏任务。

微任务和宏任务的区别是,当执行栈空了,会检查微任务队列中是否有任务,将微任务队列中的任务依次拿出来执行一遍。当微任务队列空了,从宏任务队列中拿出来一个任务去执行,执行完毕后检查微任务队列,微任务队列空了之后再从宏任务队列中拿出来一个任务执行。这样持续的交替执行任务叫做事件循环。

属于微任务(microtask)的事件有以下几种:

  • Promise.then
  • MutationObserver
  • Object.observe
  • process.nextTick

属于宏任务(macrotask)的事件有以下几种:返回搜狐,查看更多

  • setTimeout
  • setInterval
  • setImmediate
  • MessageChannel
  • requestAnimationFrame
  • I/O
  • UI交互事件