vue nextTick原理

  • Post author:
  • Post category:vue




nextTick 什么时候用?



出现的前提

修改数据时,视图不会即时更新;

这是因为vue采用的是异步更新策略; 修改数据后并不会立即更新dom,dom的更新是异步的。



什么是nextTick

官方文档解释:


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



nextTick的使用场景

  • 更改数据后,进行节点dom操作。
  • 在created,mounted生命周期中进行DOM操作。

```javascript
<template>
  <div class="box" ref="list">{{msg}}</div>
</template>
export default {
  data () {
    return {
      msg: 'hello'
    }
  },
  created() {
    console.log(this.$refs.msg); 
    // 打印undefined,这是因为在当前生命周期,dom还没挂载
    this.$nextTick(() => {
      console.log(this.$refs.msg);
    });
   },
  mounted () {
  	//在mounted阶段其实也无法保证全部挂载完毕,所以最好还是加上nextTick
  	this.$nextTick(() => {
      console.log(this.$refs.msg);
    });
    //当我们修改this.msg数据后,想要操作dom;这个时候拿到的是旧值
    this.msg = 'world'
    let box = document.getElementsByClassName('box')[0]
    console.log(box.innerHTML) // hello
  }
}
//还有输入框当前是隐藏的;我们修改v-show让其显示的同时让 输入框 获取焦点
//获取元素 宽高 等



nextTick的本质



nextTick.js源码

//平时的写法
//this.$nextTick(() => {
//      console.log(this.$refs.msg);
//    });
//() => {
//      console.log(this.$refs.msg);
//    }就是传入的cb;

let callbacks = [];
let pending = false;
let timerFun

function nextTick (cb, ctx) {
  var _resolve;
   // 把传进来的回调函数cb放到callbacks队列里
  callbacks.push(function () {
    if (cb) {
      try {
        cb.call(ctx);
        
      } catch (e) {
        handleError(e, ctx, 'nextTick');
      }
    } else if (_resolve) {
      _resolve(ctx);
    }
  });
  // pending代表一个等待状态 等这个tick执行
  if (!pending) {// 同一个事件循环(tick) 这里面只会执行一次
    pending = true;
    timerFunc();//重点看这个
  }

  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(function (resolve) {
      _resolve = resolve;
    })
  }
}

/**----- timerFunc ----*/降级策略

// 1、优先考虑Promise实现
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
  }
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]')) {
// 2、降级到MutationObserver实现
  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)
  }
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 3、降级到setImmediate实现  setImmediate在宏任务中优先级比setTimeout高
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
// 4、如果以上都不支持就用setTimeout来兜底了
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
// 将callbacks中的cb依次执行
function flushCallbacks () {
  //设置 pending 为 false, 说明该 函数已经被推入到任务队列或主线程中。
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()// 执行回调函数
   }
 }

//总结  本质 往任务队列 追加微任务或者宏任务



MutationObserver API(监视DOM变动的接口)

上面提到了MutationObserver API 是浏览器自带api,还可以用它做兴趣埋点

let mo = new MutationObserver(callback)  //传入一个回调 ,得到实例
var domTarget = 你想要监听的dom节点
mo.observe(domTarget, {
      characterData: true //说明监听文本内容的修改。
      childList:true//子节点变动
})

分析

 let counter = 1
  const observer = new MutationObserver(flushCallbacks)  //创建实例
  const textNode = document.createTextNode(String(counter)) //创建一个TextNode节点
  observer.observe(textNode, {
    characterData: true
  })//监听
  //当我们调用timerFunc  counter 改变了,
  //导致TextNode节点文本改变,所以触发observe,执行回调flushCallbacks![在这里插入图片描述](https://img-blog.csdnimg.cn/2021071614481365.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2JpZ19Db2NreQ==,size_16,color_FFFFFF,t_70#pic_center)

  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }

在这里插入图片描述

讲到这里,大家就知道nextTick 其实就是利用了事件循环机制。



nextTick,下一个tick?

  • JS分为同步任务和异步任务。
  • 同步任务都在主线程上执行,形成一个执行栈。
  • 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  • 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。

    在这里插入图片描述

    总结:每执行一个宏任务,就是一个tick。那么可理解为“在下一个宏任务执行之前的回调函数”。

    问题:为什么优先宏任务?

    microtasks(微任务)的执行速度是快于宏任务的;并且宏任务之间还穿插着UI渲染。因此优先选择microtasks。

    那这和dom更新有啥关系呢?nextTick如何知道DOM什么时候更新呢?带着疑惑,继续往下看。



nextTick如何知道DOM什么时候更新呢?


```javascript
<template>
  <div class="box" ref="list">{{msg}}</div>
</template>
export default {
  data () {
    return {
      msg: 'hello'
    }
  },
  mounted () {
    this.msg = 'world'
    let box = document.getElementsByClassName('box')[0]
    console.log(box.innerHTML) // hello
    this.$nextTick(() => {
      console.log(box.innerHTML) // world
    });
  }
}

分析这个代码;this.msg = ‘world’执行后;直接打印是hello;而nextTick下是最新的值。如何做到的呢?

我们可以从this.msg = ‘world’ 开始说起。msg是响应式数据,在vue中当某个响应式数据发生变化的时候,他的setter函数会通知dep,dep会调用他管理的所有的watch对象。触发watch对象的update实现。这部分的知识可以后续了解下。反正最终this.msg = ‘world’这个代码,触发watch对象的update。

<!--观察者Watcher类--> 
class Watcher { 
    constructor () { 
        Dep.target = this // new Watcher的时候把观察者存放到Dep.target里面 
    } 
    update () { 
        queueWatcher(this) // 异步更新策略 
    } 
    run () { 
        // dom在这里执行真正的更新 
      } 
    }
    

/** queueWatcher函数*/
let has = {};
let queue = [];
let waiting = false;

function queueWatcher (watcher: Watcher) {
   /*获取watcher的id*/
  const id = watcher.id
  // 防止queue队列wachter对象重复
  if (has[id] == null) {
    has[id] = true
    queue.push(watcher)
    
    // 传递本次的更新任务
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

/** flushSchedulerQueue函数 */
function flushSchedulerQueue () {
    let watcher, id;
    for (index = 0; index < queue.length; index++) {
        watcher = queue[index];
        id = watcher.id;
        has[id] = null;
        // 执行更新
        watcher.run();
    }
    // 更新完毕恢复标志位
    waiting = false;
}

1.queue里面存放着我们本次要更新的watcher对象,queueWatcher函数做了一个判重操作,相同的watcher对象只会被加入到queue队列一次。就比如写个for循环去改变msg变量。虽然会出发update100次,但是之后进入queue一次。

2.flushSchedulerQueue函数依次调用了wacther对象的run方法执行更新。并作为回调传递给了nextTick函数。

3.waiting这个标记位代表我们是否已经向nextTick函数传递了更新任务,nextTick会在当前task结束后再去处理传入的回掉,只需要传递一次,更新完毕再重置这个标志位。

也就是说:

this.msg=‘hello’ =>update=>queueWatcher=>quene=>nextTick(flushSchedulerQueue );

最终调用nextTick,并将quene中的watch的run作为回调传入。当然只是传入,没调用哈。

再看上面写的代码

this.msg='hello' //执行后 callbacks里面【flushSchedulerQueue 】
this.$nextTick(() => {
      console.log(box.innerHTML) // world
    });
 //执行后 callbacks里面【flushSchedulerQueue,() => {
      console.log(box.innerHTML) // world
    }

由此可见,先执行flushSchedulerQueue后,dom更新了,这个时候去执行用户写的回调,当然是能够获取到更新后的DOM。



demo

<template>
  <div class="hello">
    <div class='test' >{{msg}}</div>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  },
  mounted () {
    let x = document.querySelector('.test')
    this.$nextTick(() => {
      console.log(x.innerHTML, 1111)//Welcome to Your Vue.js App
    })
    this.msg = '新数据'

    this.$nextTick(() => {
      console.log(x.innerHTML, 222)//新数据111
    })
    this.msg = '新数据1111'
  }
}
</script>

那我这样写,打印出来的是什么呢?

解释
1
this.$nextTick(() => {
      console.log(x.innerHTML, 1111)//Welcome to Your Vue.js App
  })
callbacks () => {
      console.log(x.innerHTML, 1111)//Welcome to Your Vue.js App
  }2
  this.msg = '新数据'
  callbacks =[
	  () => {
	      console.log(x.innerHTML, 1111)//Welcome to Your Vue.js App
	  },
	  flushSchedulerQueue 
   ]
  3
  this.$nextTick(() => {
      console.log(x.innerHTML, 222)//新数据111
    })
    执行后如下:
    
   callbacks =[
	   () => {
	      console.log(x.innerHTML, 1111)//Welcome to Your Vue.js App
	  	},
  		flushSchedulerQueue ,
	  	() => {
	      console.log(x.innerHTML, 222)//新数据111
	    }
  ]
  4.
  this.msg = '新数据1111'
  是同一个watch;判重,所以调用了update方法,但是没有进入quene数组中。
  但是为啥打印的是新数据111,而不是新数据呢。因为watch实例的value是已经更新完毕的。
  
  也就是说:
  第二步执行后:watch实例的value是'新数据'。然后向nextTick抛出一个回调。
  第三步执行后:向nextTick抛出一个回调。
  第四步执行后:watch实例的value是'新数据111'。
  然后代码(第一个宏任务)跑完,开始执行所有微任务(现在的nextTick中的回调)。当执行第二个回调的时候,watch.run方法拿到的就是watch实例的value'新数据111'



版权声明:本文为big_Cocky原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。