学习vue中的nextTick() 的实现原理

119 阅读3分钟

nextTick()是什么?

定义:在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后立即使用这个方法,获取更新后的DOM。

Vue就会开启一个任务队列,然后把在同一个事件循环 (Event loop) 中观察到数据变化的 Watcher(Vue源码中的Wacher类是用来更新Dep类收集到的依赖的)推送进这个队列。

如果这个watcher被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和DOM操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。

nextTick的作用是为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback),JS是单线程的,拥有事件循环机制,nextTick的实现就是利用了事件循环的宏任务和微任务。

先解读nextTick源码

(1)创建了一个callbacks数组和 一个flushCallbacks函数,将nextTick传入得到回调push到数组里,然后flushCallbacks循环执行

export let isUsingMicroTask = false //是否启用微任务开个
// 1.创建了一个callbacks数组和 一个flushCallbacks函数,将nextTick传入得到回调push到数组里,然后flushCallbacks循环执行
 //callbacks是定义一个回调队列
const callbacks = []
//pending是执行异步的开关,标记是否正在执行回调函数
let pending = false 
// flushCallbacks函数是负责执行队列中的全部回调
function flushCallbacks() {
  // 重置异步开关
  pending = false
  // 防止nextTick里面有nextTick出现的问题
  // 在执行之前先进行存储然后清空回调队列
  const copies = callbacks.slice(0)
  callbacks.length = 0
  //循环执行队列任务
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

(2)判断采用了哪种异步回调方法

因为微任务优先级高,首先尝试微任务

《1》尝试使用promise.then(微任务)

《2》使用MutationObserver(微任务)回调

《3》使用setImmediate(宏任务)回调

《4》使用setTimeout(宏任务)回调

let timerFunc //定义一个执行异步任务的方法

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
// 判断当前的队列中是否支持原生的promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // 保存执行一个异步任务
  const p = Promise.resolve()
  timerFunc = () => {
    // 执行回调函数flushCallbacks
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    // ios 中可能会出现一个回调被推入微任务队列,但是队列没有刷新的情况
    // 所以用一个空的计时器来强制刷新任务队列
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'

  
)) {
  // 不支持promise的话,在支持MutationObserver的非IE浏览器下
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  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)) {
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  // 使用setImmediate,虽然也是宏任务,但是比setTimeout更好
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  // 上面都不支持的情况下,使用setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
// 抛出nextTick方法
export function nextTick(cb?: Function, ctx?: Object) {
  let _resolve
  // callbacks是维护微任务的数组
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    // 将维护的队推送到微任务队列中维护
    timerFunc()
  }
  // $flow-disable-line
  // 判断nextTick 没有参数,浏览器支持promise,就返回一个promise对象
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

nextTick的调用方法

1.回调函数 vue.nextTick(callback)

2.promise方法:vue.nextTick().then(callback)

3.实例方式:vm.$nextTick(callback)

vue.nextTick的应用

1.Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中,原因是在created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳,所以此处一定要将DOM操作的js代码放进Vue.nextTick()的回调函数中。与之对应的就是mounted钩子函数,因为该钩子函数执行时所有的DOM挂载已完成。

created(){
    let that=this;
    that.$nextTick(function(){  //不使用this.$nextTick()方法会报错
        that.$refs.aa.innerHTML="created中更改了按钮内容";  //写入到DOM元素
    });
  },

2.2、当项目中你想在改变DOM元素的数据后基于新的dom做点什么,对新DOM一系列的js操作都需要放进Vue.nextTick()的回调函数中; 通俗的理解是:更改数据后当你想立即使用js操作新的视图的时候需要使用它

<template>
  <div class="hello">
    <h3 id="h">{{testMsg}}</h3>
  </div>
</template>
 
<script>
export default {
  name: 'HelloWorld',
  data () {
    return {
      testMsg:"原始值",
    }
  },
  methods:{
    changeTxt:function(){
      let that=this;
      that.testMsg="修改后的文本值";  //vue数据改变,改变dom结构
      let domTxt=document.getElementById('h').innerText;  //后续js对dom的操作
      console.log(domTxt);  //输出可以看到vue数据修改后DOM并没有立即更新,后续的dom都不是最新的
      if(domTxt==="原始值"){
        console.log("文本data被修改后dom内容没立即更新");
      }else {
        console.log("文本data被修改后dom内容被马上更新了");
      }
    },
 
  }
}
</script>
 

总结

nextTick是Vue提供的一个全局API,由于Vue的异步更新策略导致对数据的修改不会立即体现在DOM变化上,此时如果我们需要立即获取到变化后的DOM状态,就需要使用API(定义+场景)

Vue在更新DOM时是异步执行的。只要侦听到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作是非常重要的。nextTick方法会在队列中加入一个回调函数,确保该函数在前面的DOM操作完成之后在调用(必要性)

callbacks里面加入了我们传入的函数,就是nextTick的 ()=>{} 这部分,然后用timeFunc的异步方式调用他们,首选是promise方式。