欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-10-14 22:22:55
今天做了一个需求,场景是这样的: 在页面拉取一个接口,这个接口返回一些数据,这些数据是这个页面的一个浮层组件要依赖的,然后我在接口一返回数据就展示了这个浮层组件,展示的同...

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

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

父组件:

<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>

子组件

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

问题分析:

原因vue官网上有解析( )

可能你还没有注意到,vue 异步执行 dom 更新。只要观察到数据变化,vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 dom 操作上非常重要。然后,在下一个的事件循环“tick”中,vue 刷新队列并执行实际 (已去重的) 工作。vue 在内部尝试对异步队列使用原生的 promise.then 和 messagechannel,如果执行环境不支持,会采用 settimeout(fn, 0) 代替。

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

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

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

解决方案:

1、 使用nexttick来延迟执行show方法(笼统得说,执行所有需要在数据真正更新后的操作

通过上面的分析我们知道,我们的所有的对vue实例的更新操作,都会先被放入一个队列当中,延迟异步执行,这些异步操作,要么是microtask,要么是macrotask(是microtask还是macroktask取决于环境,nexttick的源码中有所体现),根据事件循环机制,先入队列的先执行,所以如果我们在nexttick当中执行操作就会变成这样。

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

2、 使用settimeout来延迟执行show方法,原理同上

所以我们的解决方法可以是:

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中的所有回调取出来执行,以达到延迟执行的目的。为啥要用闭包呢,我觉得有两个原因:

1、共享变量,比如callbacks、pending和timerfunc。

2、避免反复判断,即是避免反复判断timerfunc是利用promise还是利用mutationobserver或是settimeout来实现异步,这是函数柯里化的一种运用。

这里有两个最主要的方法需要解释下:

1、 nexttickhandler

这个函数,就是把队列中的回调,全部取出来执行,类似于microtask的任务队列。我们通过调用vue.$nexttick就会把回调全部放入这个队列当中,等到要执行的时候,调用nexttickhandler全部取出来执行。

2、 timerfunc

这个变量,它的作用就是通过promise/mutationobserver/settimeout把nexttickhandler放入到真正的任务队列当中,等到事件循环结束,就从任务队列当中取出nexttickhandler来执行,nexttickhandler一执行,callbacks里面的所有回调就会被取出来执行来,这样就达到来延迟执行nexttick传的回调的效果。

通过这个简单的源码分析,我们可以得出两个结论

1、nexttick会根据不同的执行环境,异步任务可能为microtask或者macrotask,而不是固定不变的。所以,如果你想让nexttick里面的异步任务统统看成是microtask的话,你会遇到坑的。

2、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/子组件的

以上所述是小编给大家介绍的vue中的nexttick的使用,希望对大家有所帮助