vue.$nextTick源码浅析

282 阅读2分钟

最近有人问我一个问题:

在这里插入图片描述
这是一个点击触发的函数,start绑定的是class为info的dom的v-if,但是把这句话放到下面就获取不到dom了是为什么
其实之前也遇到过这个问题,不过没有深究,因为赶时间完成功能,不过现在闲下来了,正好有人问问题,我就回过头去 好好再研究一下vue的nextTick。 以下源码分析都是基于 "vue": "^2.6.10" 版本。 这是我debugger用的测试页面 就很简单的一个vue页面,通过按钮点击事件来控制div元素的显示与消失。
在这里插入图片描述

<template>
  <div id="app">
    <div class="info" v-if="start">123456</div>
    <button @click="test">show</button>
  </div>
</template>

<script>
export default {
  name: 'app',
  data () {
    return {
      start:false,
    }
  },
  methods:{
    test(){
      this.$nextTick(()=>{
        let el=document.querySelector('.info');
        console.log(el);
      })
      this.start=!this.start;
    }
  },
  watch:{
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

首先我们先来看下nextTick的源码:

在这里插入图片描述
里面有个timerFunc函数 是关键,我们来找一下timerFunc是在哪初始化的

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    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.
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // 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.
  // Techinically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

可见timerFunc是想模拟微任务,首先判断浏览器是否支持promise,如果不支持改用MutationObserver如果MutationObserver也不支持就改用setImmediate或者setTimeout来模拟。

timerFunc = () => {
    p.then(flushCallbacks)
	...
  }

这是关键代码 timerFunc是一个函数 会以微任务的方式去执行flushCallbacks函数,flushCallbacks函数是一个清空回调队列的函数,其实这个队列就是刚刚nextTick函数里的callbacks数组,每次nextTick都会往里push你传入的函数,而且这个队列其实就是vue官方文档里所谓的 异步更新队列

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

好了,好像似乎没有什么问题,可是。。。他们是在哪被调用的呢? 现在就要debugger了!

this.$nextTick(()=>{
        let el=document.querySelector('.info');
        console.log(el);
      })

首先这段代码 打断点,然后 没有任何问题 进入了 nextTick push了我们写的函数(我们的函数是写在App.vue页面第23行的),并且调用了timerFunc函数 注册了微任务flushCallbacks

在这里插入图片描述
在这里插入图片描述
然后微任务被注册以后又会回到 App.vue中继续往下走,然后 走到了第27行代码

this.start=!this.start;

这是一个响应式属性,被绑定在了v-if上,这次赋值的话一定会触发watcher去通知vue进行重新渲染 即调用update函数 通过diff算法 比对新旧node进行更新(这部分内容很多,要讲的话篇幅会比较长,而且我也没有深入研究过,,所以要是不知道的话可以去网上查看他人的博客了解大致过程是怎样的) 所以我们在watcher的update方法里打断点

在这里插入图片描述
update方法会进入queueWatcher函数,我们来看下这个函数做了什么
在这里插入图片描述
注意 这里调用了nextTick!!! 所以第一次调用queueWatcher函数的时候会调用nextTick函数,而nextTick函数会往异步更新队列队列里push你的函数,这里的话就是push flushSchedulerQueue函数,而这个函数 会去调用 vm._update 进行渲染更新,所以这就能解释,为什么把nextTick写在前面 就不能获取到本应该出现的div元素了,因为nextTick写在前面 他就在异步更新队列的队首,dom更新就被放在了后面,所以当然这里console.log 出来的结果就会是null拉!