nextTick详解

一、循环机制(event loop)之宏任务和微任务

js任务分为同步任务异步任务异步任务又分为宏任务和微任务,其中异步任务属于耗时的任务
在这里插入图片描述

	console.log(1)
	setTimeout(function(){
	  console.log(2);
	  let promise = new Promise(function(resolve, reject) {
	      console.log(7);
	      resolve()
	  }).then(function(){
	    console.log(8)
	  });
	},1000);
	setTimeout(function(){
	  console.log(10);
	  let promise = new Promise(function(resolve, reject) {
	      console.log(11);
	      resolve()
	  }).then(function(){
	    console.log(12)
	  });
	},0);
	let promise = new Promise(function(resolve, reject) {
	    console.log(3);
	    resolve()
	}).then(function(){
	  console.log(4)
	}).then(function(){
	  console.log(9)
	});
	console.log(5)

二、宏任务和微任务有哪些?

1. 宏任务: 整体代码script、setTimeout、setInterval、setImmediate(Node.js)、i/o操作(输入输出,比如读取文件操作、网络请求)、ui render(dom渲染,即更改代码重新渲染dom的过程)、异步ajax等
2. 微任务: Promise(then、catch、finally)、async/await、process.nextTick(Node.js)、Object.observe(⽤来实时监测js中对象的变化,已废弃)、 MutationObserver(监听DOM树的变化)

三、nextTick正文开始

在这里插入图片描述

官方给的解释就是:等待下一次 DOM 更新刷新的工具方法。
nextTick我们经常在项目中用到,简单的理解他就是一个setTimeout函数,将函数放到异步后去处理;将它换成setTImeout也能跑起来,但它仅仅就这么简单吗?那我们为什么不直接用setTimeout呢?

四、nextTick作用

  1. 确保DOM更新完成;
  2. 避免不必要的DOM操作;
  3. 优化性能。
    这里要提示一下:DOM更新和UI渲染是两个东西
    [图片]

在给data中的数据赋值后立马去查看数据导致的。由于 “查看数据” 这个动作是同步操作的,而且都是在赋值之后;因此我们猜测一下,给数据赋值操作是一个异步操作,并没有马上执行,Vue官网对数据操作是这么描述的:
可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserversetImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

五、nextTick源码分析

(源码位置:/src/core/util/next-tick.js)
了解了nextTick的用法和原理之后,我们就来看一下Vue是怎么来实现的。

	const callbacks = []
	let pending = false
	let timerFunc
	export function nextTick (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 的回调,就执行 timeFunc 函数选择当前环境优先支持的异步方法
	  if (!pending) {
	    pending = true
	    timerFunc()
	  }
	  
	  // 如果没有传入回调,并且当前环境支持 promise,就返回一个 promise
	  if (!cb && typeof Promise !== 'undefined') {
	    return new Promise(resolve => {
	      _resolve = resolve
	    })
	  }
	}

我们首先找到nextTick这个函数定义的地方,看看它具体做了什么操作;看到它在外层定义了三个变量,有一个变量看名字就很熟悉:callbacks,就是我们上面说的队列;在nextTick的外层定义变量就形成了一个闭包,所以我们每次调用$nextTick的过程其实就是在向callbacks新增回调函数的过程。
 callbacks新增回调函数后又执行了timerFunc函数,pending用来标识同一个时间只能执行一次。那么这个timerFunc函数是做什么用的呢,我们继续来看代码:

	// 判断当前环境优先支持的异步方法,优先选择微任务
	// 优先级:Promise---> MutationObserver---> setImmediate---> setTimeout
	// setTimeOut 最小延迟也要4ms,而 setImmediate 会在主线程执行完后立刻执行
	// setImmediate 在 IE10 和 node 中支持
	// 多次调用 nextTick 时 ,timerFunc 只会执行一次

	export let isUsingMicroTask = false
	if (typeof Promise !== 'undefined' && isNative(Promise)) {
	  //判断1:是否原生支持Promise
	  const p = Promise.resolve()
	  timerFunc = () => {
	    // 用 promise.then 把 flushCallbacks 函数包裹成一个异步微任务
	    p.then(flushCallbacks)
	    if (isIOS) setTimeout(noop)
	  }
	  isUsingMicroTask = true
	  
	  // 如果不支持 promise,就判断是否支持 MutationObserver
	  // 不是IE环境,并且原生支持 MutationObserver,那也是一个微任务
	} else if (!isIE && typeof MutationObserver !== 'undefined' && (
	  isNative(MutationObserver) ||
	  MutationObserver.toString() === '[object MutationObserverConstructor]'
	)) {
	  //判断2:是否原生支持MutationObserver
	  let counter = 1
	  const observer = new MutationObserver(flushCallbacks)
	  // 监听这个文本节点,当数据发生变化就执行 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)) {
	  //判断3:是否原生支持setImmediate
	  timerFunc = () => {
	    setImmediate(flushCallbacks)
	  }
	} else {
	  //判断4:上面都不行,直接用setTimeout
	  timerFunc = () => {
	    setTimeout(flushCallbacks, 0)
	  }
	}

这里出现了好几个isNative函数,这是用来判断所传参数是否在当前环境原生就支持;例如某些浏览器不支持Promise,虽然我们使用了垫片(polify),但是isNative(Promise)还是会返回false

可以看出这边代码其实是做了四个判断,对当前环境进行不断的降级处理,尝试使用原生的Promise.then、MutationObserversetImmediate,上述三个都不支持最后使用setTimeout;降级处理的目的都是将flushCallbacks函数放入微任务(判断1和判断2)或者宏任务(判断3和判断4),等待下一次事件循环时来执行。MutationObserverHtml5的一个新特性,用来监听目标DOM结构是否改变,也就是代码中新建的textNode;如果改变了就执行MutationObserver构造函数中的回调函数,不过是它是在微任务中执行的。

那么最终我们顺藤摸瓜找到了最终的大boss:flushCallbacksnextTick不顾一切的要把它放入微任务或者宏任务中去执行,它究竟是何方神圣呢?让我们来一睹它的真容:

	/** 作用:用来执行callbacks中的回调函数 */
	/** 为什么要拷贝callbacks?因为nextTick回调中还有可能会调用nextTick,就有可能向callbacks中添加回调,
	就有可能一直循环。nextTick回调中的nextTick应该放在下次的循环里面 */
	/** 为什么使用slice? 因为slice修改原数组或者复制后的数组不会互相影响 */
	function flushCallbacks () {
	  pending = false
	  const copies = callbacks.slice(0)
	  callbacks.length = 0
	  for (let i = 0; i < copies.length; i++) {
	    copies[i]()
	  }
	}

六、总结

到这里,整体nextTick的代码都分析完毕了,总结一下它的流程就是:

  1. 把回调函数放入callbacks等待执行
  2. 将执行函数放到微任务或者宏任务中
  3. 事件循环到了微任务或者宏任务,执行函数依次执行callbacks中的回调
    nextTick中维护了一个callbacks队列,一个pending锁,一个timerFunc
    这个timerFunc就是根据浏览器环境判断得出的一个能够产生微任务或降级为宏任务的api调用,比如promise.then
    callbacks队列的作用是收集当前正在执行的宏任务中所有的nextTick回调,等当前宏任务执行完之后好一次性for循环啪执行完。
    试想如果没有callback队列的话,每次调用nextTick都去创建一个timerFunc微任务(假设支持),那么也就不需要pending锁了。
    现在有了callbacks队列的情况下就只需要创建一个timerFunc微任务,那问题是什么时候创建该微任务呢?
    这里就要讲到pending了,在pendingfalse的时候表示第一次添加cbcallbacks中,这时候创建一个timerFunc微任务,并加锁。
    后面调用nextTick就只是往callbacks添加回调。
    等当前宏任务之后完之后,就会去执行timerFunc清空callbacks队列,并设置pendingfalse,一切归零

七、其他

答案:打印顺序 1、3、 5、 4、 9、 10、 11、 12、 2、 7、 8

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值