从源码里面了解vue的nextTick的使用

今天作了一个需求,场景是这样的:html

在页面拉取一个接口,这个接口返回一些数据,这些数据是这个页面的一个浮层组件要依赖的,而后我在接口一返回数据就展现了这个浮层组件,展现的同时,上报一些数据给后台(这些数据就是父组件从接口拿的),这个时候,神奇的事情发生了,虽然我拿到数据了,可是浮层展示的时候,这些数据还未更新到组件上去。vue

父组件:react

<template>
    .....
    <pop ref="pop" :name="name"/>
</template>
<script>
export default {
    .....
    created() {
        ....
        // 请求数据,并从接口获取数据
        Data.get({
            url: xxxx,
            success: (data) => {
                // 问题出如今这里,咱们赋值之后直接调用show方法,去展示,show方法调用的同时上报数据,而上报的数据这个时候还未更新到子组件
                this.name = data.name
                this.$refs.pop.show()
            }
        })
    }
}

</script>
复制代码

子组件bash

<template>
   <div v-show="isShow">
        ......
   </div>
</template>
<script>
export default {
    .....
    props: ['name'],
    methods: {
       show() {
           this.isShow = true
           // 上报
           Report('xxx', {name: this.name})
       }
    }
}

</script>
复制代码

问题分析:

缘由vue官网上有解析(cn.vuejs.org/v2/guide/re…)闭包

可能你尚未注意到,Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的全部数据改变。若是同一个 watcher 被屡次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免没必要要的计算和 DOM 操做上很是重要。而后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工做。Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MessageChannel,若是执行环境不支持,会采用 setTimeout(fn, 0) 代替。app

这句话就是说,当咱们在父组件设置this.name=name的时候,vue并不会直接更新到子组件中(dom的更新也同样未当即执行),而是把这些更新操做所有放入到一个队列当中,同个组件的全部这些赋值操做,都做为一个watcher的更新操做放入这个队列当中,而后等到事件循环结束的时候,一次性从这个队列当中获取全部的wathcer执行更新操做。在咱们这个例子当中,就是咱们在调用show的时候,实际上,咱们的this.name=name并未真正执行,而是被放入队列中。vue的这种作法是基于优化而作的,毋庸置疑,否则咱们若是有n多个赋值vue就执行n多个dom更新,那效率将会很是的低效和不可取的。dom

image

下文中的更新操做指对data的值进行更新的操做,在vue中,都会被放入队列异步执行。异步

解决方案:

一、使用nextTick来延迟执行show方法(笼统得说,执行全部须要在数据真正更新后的操做
经过上面的分析咱们知道,咱们的全部的对vue实例的更新操做,都会先被放入一个队列当中,延迟异步执行,这些异步操做,要么是microtask,要么是macrotask(是microtask仍是macroktask取决于环境,nextTick的源码中有所体现),根据事件循环机制,先入队列的先执行,因此若是咱们在nextTick当中执行操做就会变成这样。 imageasync

二、使用setTimeout来延迟执行show方法,原理同上ide

因此咱们的解决方法能够是:

this.name = data.name
setTimeout(() => {
 this.$refs.pop.show()
})
复制代码

或者

this.name = data.name
this.$nextTick(() => {
 this.$refs.pop.show()
})
复制代码

nextTick的实现原理

其实nextTick的实现原理是挺简单的,简单点说,就是实现异步,经过不一样的执行环境,用不一样的方式来实现,保证nextTick里面的回调函数可以异步执行。为何要这么作呢?由于vue对dom的更新也是异步的呀。

下面贴出源码:

/**
 * Defer a task to execute it asynchronously.
 */
export const nextTick = (function () {
  const callbacks = []
  let pending = false
  let timerFunc

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

  // 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 if */
  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve()
    var logError = err => { console.error(err) }
    timerFunc = () => {
      p.then(nextTickHandler).catch(logError)
      // 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)
    }
  } 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
    var counter = 1
    var observer = new MutationObserver(nextTickHandler)
    var textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
      characterData: true
    })
    timerFunc = () => {
      counter = (counter + 1) % 2
      textNode.data = String(counter)
    }
  } else {
    // fallback to setTimeout
    /* istanbul ignore next */
    timerFunc = () => {
      setTimeout(nextTickHandler, 0)
    }
  }

  return function queueNextTick (cb?: Function, ctx?: Object) {
    let _resolve
    callbacks.push(() => {
      if (cb) {
        try {
          cb.call(ctx)
        } catch (e) {
          handleError(e, ctx, 'nextTick')
        }
      } else if (_resolve) {
        _resolve(ctx)
      }
    })
    if (!pending) {
      pending = true
      timerFunc()
    }
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        _resolve = resolve
      })
    }
  }
})()

复制代码

首先咱们看到这个是利用了闭包的特性,返回queueNextTick,因此咱们实际调用的nextTick其实就是调用queueNextTick,一调用这个方法,就会把nextTick的回调放入队列callbacks当中,等到合适的时机,会将callbacks中的全部回调取出来执行,以达到延迟执行的目的。为啥要用闭包呢,我以为有两个缘由:
一、共享变量,好比callbacks、pending和timerFunc。
二、避免反复判断,便是避免反复判断timerFunc是利用Promise仍是利用MutationObserver或是setTimeout来实现异步,这是当即执行函数的一种运用。

这里有两个最主要的方法须要解释下:
一、nextTickHandler
这个函数,就是把队列中的回调,所有取出来执行,相似于microtask的任务队列。咱们经过调用Vue.$nextTick就会把回调所有放入这个队列当中,等到要执行的时候,调用nextTickHandler所有取出来执行。

二、timerFunc
这个变量,一执行就会经过Promise/Mutationobserver/Settimeout实现异步,把nextTickHandler放入到真正的异步任务队列当中,等到事件循环结束,就从任务队列当中取出nextTickHandler来执行,nextTickHandler一执行,callbacks里面的全部回调就会被取出来执行来,这样就达到来延迟执行nextTick传的回调的效果。

经过这个简单的源码分析,咱们能够得出两个结论
一、nextTick会根据不一样的执行环境,异步任务可能为microtask或者macrotask,而不是固定不变的。因此,若是你想让nextTick里面的异步任务通通当作是microtask的话,你会遇到坑的。
二、nextTick的并不能保证必定能获取获得更新后的dom,这取决于你是先进行数据赋值仍是先调用nextTick。好比:

new Vue({
      el: '#app',
      data() {
        return {
          id: 2
        }
      },
      created() {
        
      },
      mounted() {
        this.$nextTick(() => {
          console.log(document.getElementById('id').textContent) // 这里打印出来的是2,由于先调用了nextTick
        })
        this.id = 3
      }
  })
复制代码

结论

若是想要获取更新后的DOM或者子组件(依赖父组件的传值),能够在更新操做以后当即使用Vue.nextTick(callback),注意这里的前后顺序,先进行更新操做,再调用nextTick获取更新后的DOM/子组件,源码里面咱们知道nextTick是没法保证必定是可以获取获得更新后的DOM/子组件的

相关文章
相关标签/搜索