top

Vue异步更新队列nextTick原理

异步更新队列

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

 

例如,当你设置 vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。例如:

 

<div id="example">{{message}}</div>
var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})
vm.message = 'new message' // 更改数据
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
})

 

再如:

for(let i=0;i<n;i++){ this.someData+=i }

如上面的例子,如果我们多次修改someDate的值,并不是每次修改的结果都会渲染到页面上。因为我们只需要看到最终的值。vue是在执行完一次事件循环后把缓冲的数据更改更新到页面上

vm.$nextTick(callback),在下一次dom更新之后执行对应的回调,这样我们获取到的dom就是更新过的dom

下面了解下nextTick的主要应用的场景及原因。

  • 在Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中

created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳,所以此处一定要将DOM操作的js代码放进Vue.nextTick()的回调函数中。

  • 在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的DOM结构的时候,这个操作都应该放进Vue.nextTick()的回调函数中。

msg1和msg3显示的内容还是变换之前的,而msg2显示的内容是变换之后的。其根本原因是因为Vue中DOM更新是异步的

获取this.Message的两种情况:

在nextick中可以获取更新后的dom

nextTick实现原理:

环境判断:

判断支持哪个宏任务或微任务,顺序如下

  • Promise
  • MutationObserver
  • setImmediate
  • setTimeout
export let isUsingMicroTask = false // 是否启用微任务开关
const callbacks = [] // 回调队列
let pending = false // 异步控制开关,标记是否正在执行回调函数

// 该方法负责执行队列中的全部回调
function flushCallbacks () {
  // 重置异步开关
  pending = false
  // 防止nextTick里有nextTick出现的问题
  // 所以执行之前先备份并清空回调队列
  const copies = callbacks.slice(0)
  callbacks.length = 0
  // 执行任务队列
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
let timerFunc // 用来保存调用异步任务方法
// 判断当前环境是否支持原生 Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // 保存一个异步任务
  const p = Promise.resolve()
  timerFunc = () => {
    // 执行回调函数
    p.then(flushCallbacks)
    // ios 中可能会出现一个回调被推入微任务队列,但是队列没有刷新的情况
    // 所以用一个空的计时器来强制刷新任务队列
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // 不支持 Promise 的话,在支持MutationObserver的非 IE 环境下
  // 如 PhantomJS, iOS7, Android 4.4
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // 使用setImmediate,虽然也是宏任务,但是比setTimeout更好
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 以上都不支持的情况下,使用 setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

用timerfunc保存支持的宏任务或微任务,调用此函数会执行并清空任务回调队列callbacks

执行清空回调队列,

nextTick()

把传入的回调函数放进callbacks中

执行timerfunc,就会遍历callbacks队列执行相应的回调函数

/*存放异步执行的回调*/
const callbacks = [] 
/*一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/
let pending = false
/*一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用*/
let timerFunc

/*
  推送到队列中下一个tick时执行
  cb 回调函数
  ctx 上下文
*/
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
   // 第一步 传入的cb会被push进callbacks中存放起来
  callbacks.push(() => {
    if (cb) {      
        try {
            cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })  
  // 检查上一个异步任务队列(即名为callbacks的任务数组)是否派发和执行完毕了。pending此处相当于一个锁
  if (!pending) {
  // 若上一个异步任务队列已经执行完毕,则将pending设定为true(把锁锁上)
    pending = true
    // 调用判断Promise,MutationObserver,setTimeout的优先级
    timerFunc()
  }
  // 第三步执行返回的状态
  if (!cb && typeof Promise !== 'undefined') {   
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

原理总结:

环境判断,判断支持哪个宏微任务,promise、mutationobserver、settimeout等,把timerfunc执行函数放到支持的宏或微任务中,

把nexttick传入的回调函数放进callbacks回调队列中,用pending检查上一个任务队列是否执行完毕,执行完毕则调用timerfunc执行函数,来遍历执行callbacks

阅读剩余
THE END
icon
0
icon
分享
发表评论
评论列表

赶快来坐沙发