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

Vue中之nextTick函数源码分析详解

程序员文章站 2022-06-17 17:29:50
1. 什么是vue.nexttick()? 官方文档解释如下: 在下次dom更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的dom。...

1. 什么是vue.nexttick()?

官方文档解释如下:

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

2. 为什么要使用nexttick?

<!doctype html>
<html>
 <head>
 <title>演示vue</title>
 <script src="https://tugenhua0707.github.io/vue/vue1/vue.js"></script>
 </head>
 <body>
 <div id="app">
  <template>
  <div ref="list">
   {{name}}
  </div>
  </template>
 </div>
 <script>
  new vue({
  el: '#app',
  data: {
   name: 'aa'
  },
  mounted() {
   this.updatedata();
  },
  methods: {
   updatedata() {
   var self = this;
   this.name = 'bb';
   console.log(this.$el.textcontent); // aa
   this.$nexttick(function(){
    console.log(self.$el.textcontent); // bb
   });
   }
  }
  });
 </script>
 </body>
</html>

如上代码 在页面视图上显示bb,但是当我在控制台打印的时候,获取的文本内容还是 aa,但是使用 nexttick后,获取的文本内容就是最新的内容bb了,因此在这种情况下,我们可以使用nexttick函数了。

上面的代码为什么改变this.name = 'bb';后,再使用console.log(this.$el.textcontent);打印的值还是aa呢?那是因为设置name的值后,dom还没有更新到,所以获取值还是之前的值,但是我们放到nexttick函数里面的时候,代码会在dom更新后执行,因此dom更新后,再去获取元素的值就可以获取到最新值了。

理解dom更新:在vue中,当我们修改了data中的某一个值后,并不会立即反应到该el中,vue将对更改的数据放到watcher的一个异步队列中,只有在当前任务空闲时才会执行watcher队列任务,这就有一个延迟时间,因此放到 nexttick函数后就可以获取该el的最新值了。如果我们把上面的nexttick改成settimeout也是可以的。

3. vue源码详解之nexttick(源码在 vue/src/core/util/env.js)

在理解nexttick源码之前,我们先来理解下 html5中新增的 mutationobserver的api,它的作用是用来监听dom变动的接口,它能监听一个dom对象发生的子节点删除,属性修改,文本内容修改等等。

nexttick源码如下:

export const nexttick = (function () {
 const callbacks = []
 let pending = false
 let timerfunc

 function nexttickhandler () {
 pending = false;
 /*
  之所以要slice复制一份出来是因为有的cb执行过程中又会往callbacks中加入内容,比如$nexttick的回调函数里又有$nexttick,
  那么这些应该放入到下一个轮次的nexttick去执行,所以拷贝一份,遍历完成即可,防止一直循环下去。
  */
 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 */
 /*
 nexttick行为利用了microtask队列, 先使用 promise.resolve().then(nexttickhandler)来将异步回调
 放入到microtask中,promise 和 mutationobserver都可以使用,但是 mutationobserver 在ios9.3以上的
 webview中有bug,因此如果满足第一项的话就可以执行,如果没有原生promise就用 mutationobserver。
 */
 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 (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 ie11, ios7, android 4.4
 /*
  创建一个mutationobserver,observe监听到dom改动之后执行的回调 nexttickhandler 
  */
 var counter = 1
 var observer = new mutationobserver(nexttickhandler)
 var textnode = document.createtextnode(string(counter));
 // 使用mutationobserver的接口,监听文本节点的字符内容
 observer.observe(textnode, {
  characterdata: true
 });
 /*
  每次执行timerfunc函数都会让文本节点的内容在0/1之间切换,切换之后将新赋值到那个我们mutationobserver监听的文本节点上去。
  */
 timerfunc = () => {
  counter = (counter + 1) % 2
  textnode.data = string(counter)
 }
 } else {
 // fallback to settimeout
 /*
  如果上面的两种都不支持的话,我们就使用settimeout来执行
  */
 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)
  }
 });
 /* 如果pending为true,表明本轮事件循环中已经执行过 timerfunc(nexttickhandler, 0) */
 if (!pending) {
  pending = true
  timerfunc()
 }
 if (!cb && typeof promise !== 'undefined') {
  return new promise((resolve, reject) => {
  _resolve = resolve
  })
 }
 }
})()

整体思路理解:首先 nexttick 是一个闭包函数,代码立即执行,在理解整体代码之前,我们先来看个类似的demo,如下代码:

<!doctype html>
<html>
 <head>
 <title>演示vue</title>
 </head>
 <body>
 <div id="app">
  
 </div>
 <script>
  var nexttick = (function(){
  return function queuenexttick(cb, ctx) {
   if (cb) {
   try {
    cb.call(ctx)
   } catch (e) {
    console.log('出错了');
   }
   }
  }
  })();

  // 方法调用
  nexttick(function(){
  console.log(2); // 打印2
  })
 </script>
 </body>
</html>

demo代码和上面的代码很类似。

我们也可以再来抽离使用nexttick做demo代码如下:

var nexttick2 = (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]()
 }
 }
 if (typeof promise !== 'undefined') {
 var p = promise.resolve()
 var logerror = err => { console.error(err) }
 timerfunc = () => {
  p.then(nexttickhandler).catch(logerror)
 }
 } else if (typeof mutationobserver !== 'undefined' ||
 // phantomjs and ios 7.x
 mutationobserver.tostring() === '[object mutationobserverconstructor]'
 ) {
 // use mutationobserver where native promise is not available,
 // e.g. phantomjs ie11, 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, ctx) {
 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
  })
 }
 }
})();
nexttick2(function(){
 console.log(2222);
});

如上代码是nexttick源码的抽离,为了更好的理解nexttick,做了如上的demo。

我们再来理解一下整体的代码的含义;

先定义数组 callbacks = [];来存放所有需要执行的回调函数,定义let pending = false;判断本轮事件是否执行过 timerfunc(nexttickhandler, 0)这个函数,为true说明执行过 timefunc函数,接着定义nexttickhandler函数,该函数的作用是依次遍历数组callbacks保存的函数,依次执行;

请看源代码如下:

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

然后就是三个判断了,代码如下:

if (typeof promise !== 'undefined' && isnative(promise)) {
 var p = promise.resolve();
 var logerror = err => { console.error(err) }
 timerfunc = () => {
 p.then(nexttickhandler).catch(logerror);
} else if (typeof mutationobserver !== 'undefined' && (
 isnative(mutationobserver) ||
 // phantomjs and ios 7.x
 mutationobserver.tostring() === '[object mutationobserverconstructor]'
)){
 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 {
 timerfunc = () => {
 settimeout(nexttickhandler, 0)
 }
}

首先判断是否支持promise对象,如果支持的话,定义了timefunc()函数,为了下一步调用做准备,然后继续判断是否支持该对象 mutationobserver,如果支持的话,创建一个文本节点,监听该节点数据是否发生改变,如果发生改变的话,调用timerfunc函数,counter值会在0/1切换,如果值改变了的话,把该数据值赋值到data属性上面去,那么data属性发生改变了,就会重新渲染页面(因为vue是通过object.defineproperty来监听属性值是否发生改变),如果上面两种情况都不满足的话,那么直接使用settimeout来执行nexttickhandler函数了;

最后nexttick代码返回一个函数,代码如下:

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

代码的含义是:传入的cb是否是函数,ctx参数是否是一个对象,如果cb是一个函数的话,使用cb.call(ctx), 如果timerfunc没有执行过的话,那么pending为false,因此执行 timerfunc()函数。基本的思路就是这样的。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。