深入浅出vue.js----实例方法与全局API的实现原理---生命周期相关的实例方法-vm.$nextTick

一、vm.$nextTick

(1)nextTick接收一个回调函数作为参数,它的作用是将回调延迟到下次DOM更新周期之后执行

(2)它与全局Vue.nextTick一样,不同是是回调的this自动绑定到调用它的实例上

(3)如果没有提供回调且在支持Promise的环境中,则返回一个Promise

(4)在Vue.js中,当状态发生改变时,watcher会得到通知,然后触发虚拟DOM的渲染过程。而watcher触发渲染这个操作并不是同步的,而是异步的。Vue.js中有一个队列,每当需要渲染时,会将watcher推送到这个队列中,在下一次事件循环中再让watcher触发渲染的过程。

二、为什么Vue.js使用异步更新队列

(1)Vue.js2.0开始使用虚拟DOM进行渲染,变化侦测的通知只发送到组件,组件内用到的所有状态的变化都会通知到同一个watcher,然后虚拟DOM会对整个组件进行“比对(diff)”并更改DOM。

(2)也就是说,如果在同一轮事件循环中有两个数据发生了变化,那么组件的watcher会受到两份通知,从而进行两次渲染。事实上,并不需要渲染两次,虚拟DOM会对整个组件进行渲染,所有只需要等所有状态都修改完毕后,一次性将整个组件的DOM渲染到最新即可。

(3)要解决整个问题,Vue.js的实现方式是将收到通知的watcher实例添加到队列中缓存起来,并且在添加到队列之前检查其中是否已经存在相同的watcher,只有不存在时,才将watcher实例添加到队列中。然后在下一次事件循环(event loop)中,Vue.js会让队列中的watcher触发渲染流程并清空队列。这样就可以保证即使在同一事件循环中有两个状态发生改变,watcher最后也只执行一次渲染流程。

三、什么是事件循环

(1)Javascript是一门单线程且非阻塞的脚本语言,这意味着Javascript代码在执行的任何时候都只有一个主线程来处理所有任务。

(2)而非阻塞是指当代码需要处理异步任务时,主线程会挂起这个任务,当异步任务处理完毕后,主线程再根据一定规则去执行相应回调。

(3)当任务处理完毕后,Javascript会将这个事件加入一个队列中,我们称这个队列为事件队列。被放入事件队列中的事件不会立刻执行其回调,而是等待当前执行栈中的所有任务执行完毕后,主线程会去查找事件队列中是否有任务。

(4)异步任务有两种,不同类型的任务会被分配到不同的任务队列中。

  • 微任务
  • 宏任务

(5)当执行栈中的所有任务都执行完毕后,会去检查微任务队列中是否还有事件存在,如果存在,则会依次执行微任务队列中事件对应的回调,直到为空。然后去宏任务队列中取出一个事件,把对应的回调加入当前执行栈,当执行栈中的所有任务都执行完毕后,检查微任务队列中是否有事件存在。无限重复循环此过程,就形成了一个无限循环,这个循环就叫做事件循环

(6)属于微任务的事件包括但不限于以下几种

  • Promise.then
  • MutationObserver
  • Object.observe
  • process.nextTick

(7)属于宏任务的事件包括但不限于以下几种

  • setTimeOut
  • setInterval
  • setImmediate
  • MessageChannel
  • requestAnimationFrame
  • I/O
  • UI交互事件

四、什么是执行栈

(1)当执行一个方法时,Javascript会生成一个与这个方法对应的执行环境,又叫执行上下文。这个执行环境中有这个方法的私有作用域、上层作用域的指向、方法的参数、私有作用域中定义的变量以及this对象。这个执行环境会被添加到一个栈中,这个栈就是执行栈。

(2)如果在这个方法的代码中执行到了一行函数调用语句,那么Javascript会生成这个函数的执行环境并将其添加到执行栈中,然后进入这个执行环境继续执行其中的代码。执行完毕并返回结果后,Javascript会退出执行环境并把这个执行环境从栈中销毁,回到上一个方法的执行环境。这个过程反复进行,直到执行栈中的代码全部执行完毕。这个执行环境的栈就是执行栈

(3)下次DOM更新周期的意思其实是下次微任务执行时更新DOM。而vm.$nextTick其实是将回调添加到微任务中。只有在特殊情况下才会降级成宏任务,默认会添加到微任务中。

(4)因此,如果使用vm.$nextTick来获取更新后的DOM,则需要注意顺序的问题。因为不论是更新DOM的回调还是使用vm.$nextTick注册的回调,都说向微任务队列中添加任务,所以哪个任务先添加到队列中,就先执行哪个任务。

(5)事实上,更新DOM的回调也是使用vm.$nextTick来注册到微任务中的。

(6)如果想要在vm.$nextTick中获取更新后的DOM,则一定要在更改数据的后面使用vm.$nextTick注册回调。

new Vue({
	methods:{
		example:function(){
			<!-- 先修改数据 -->
			this.message = 'changed';
			<!-- 然后使用nextTick注册回调 -->
			this.$nextTick(function(){
				<!-- DOM现在更新了 -->
			})
		}
	}
})

如果先使用vm.$nextTick注册回调,然后修改数据,则在微任务对垒中先执行使用vm.$nextTick注册的回调,然后执行更新DOM的回调。所以在回调中得不到最新的DOM,因为此时DOM还没有更新。

(7)在事件循环中,必须当微任务队列中的事件都执行完之后,才会从宏任务队列中取出一个事件执行下一轮,所以添加到微任务队列中的任务的执行时机优先于向宏任务队列中添加的任务。

new Vue({
	methods:{
		example:function(){
			<!-- 先使用setTimeout向宏任务中注册回调 -->
			setTimeout(_=>{
				<!-- DOM现在更新了 -->
			},0)
			<!-- 然后修改数据向微任务中注册回调 -->
			this.message = 'changed';
	}
})

(8)setTimeout属于宏任务,使用它注册的回调会加入到宏任务中。宏任务的执行要比微任务晚,所以即便是先注册,也是先更新DOM后执行setTimeout中设置的回调,

五、vm.$nextTick原理

(1)vm.$nextTick和全局Vue.nextTick是相同的,所以nextTick的具体实现并不是在Vue原型上的$nextTick方法中,而是抽象成了nextTick方法供两个方法共用。

import { nextTick } from '../util/index'

Vue.prototype.$nextTick = function(fn){
	return nextTick(fn,this);
}

(2)Vue原型上的$nextTick方法只是调用了nextTick方法,具体实现其实在nextTick中。

(3)由于vm.$nextTick会将回调添加到任务队列中延迟执行,所以在回调执行前,如果反复调用vm.$nextTick,Vue.js并不会反复将回调添加到任务队列中,只会向任务队列中添加一个任务。

(4)Vue.js内部有一个列表用来存储vm.$nextTick参数中提供的回调。在一轮事件循环中,vm.$nextTick只会向任务队列添加一个任务,多次使用vm.$nextTick只会将回调添加到回调列表中缓存起来。当任务触发时,依次执行列表中的所有回调并清空列表。

(5)nextTick方法的实现方式

<!-- 回调列表 -->
const callbacks = [];
let pending = false;

<!-- 执行所有回调并清空列表 -->
function flushCallbacks(){
	pending = false;
	const copies = callbacks.slice(0);
	callbacks.length = 0;
	for(let i = 0;i<copies.length;i++){
		copies[i]();
	}
}

let microTimerFunc;
const p = Promise.resolve();
<!-- 添加微任务 -->
microTimerFunc = () =>{
	p.then(flushCallbacks)
}

export function nextTick(cb,ctx){
	<!-- 将回调加入回调队列 -->
	callbacks.push(()=>{
		if(cb){
			cb.call(ctx);
		}
	})
	<!-- 第一次进入,添加微任务 -->
	if(!pending){
		pending = true;
		microTimerFunc();
	}
}
<!-- 测试一下 -->
nextTick(function(){
	console.log(this.name);//Berwin
},{name:'Berwin'})

1、通过数组callbacks来存储用户注册的回调。

2、声明了变量pending来标记是否已经向队列中添加一个任务了。每当向任务队列中插入任务时,将pending设置为true,每当任务被执行时将pending设置为false,这样就可以通过pending的值来判断是否需要向任务队列中添加任务。

3、函数flushCallbacks,即被注册的那个任务。当这个函数被触发时,会将callbacks中的所有函数依次执行,然后清空callbacks,并将pending设置为false。即一轮事件循环中,flushCallbacks只会执行一次。

4、microTimerFunc函数,它的作用是使用Promise.then将flushCallbacks添加到微任务队列中。

5、执行nextTick函数注册回调时,首先将回调函数添加到callbacks中,然后使用pending判断是否需要向任务队列中新增任务。

(6)在Vue.js2.4版本之前,nextTick方法在任何地方都使用微任务,但是微任务的优先级太高,在某些场景下可能会出现问题。所以Vue.js提供了在特殊场合下可以强制使用宏任务的方法。

<!-- 回调列表 -->
const callbacks = [];
let pending = false;

<!-- 执行所有回调并清空列表 -->
function flushCallbacks(){
	pending = false;
	const copies = callbacks.slice(0);
	callbacks.length = 0;
	for(let i = 0;i<copies.length;i++){
		copies[i]();
	}
}
<!-- 微任务 -->
let microTimerFunc;
<!-- 宏任务 -->
let macroTimerFunc = function(){...}
<!-- 使用宏任务标识 -->
let userMacroTask = false;
const p = Promise.resolve();
<!-- 添加微任务 -->
microTimerFunc = () =>{
	p.then(flushCallbacks)
}

export function withMacroTask(fn){
	return fn._withTask || (fn_withTask = function({
		userMacroTask = true;
		const res = fn.apply(null,arguments);
		userMacroTask = false;
		retrun res;
	}))
}

export function nextTick(cb,ctx){
	<!-- 将回调加入回调队列 -->
	callbacks.push(()=>{
		if(cb){
			cb.call(ctx);
		}
	})
	<!-- 第一次进入,添加微任务 -->
	if(!pending){
		pending = true;
		<!-- 添加宏任务代码 -->
		if(userMacroTask){
			macroTimerFunc();
		}else{
			microTimerFunc();
		}
		
	}
}

1、新增了withMacroTask函数,它的作用是给回调函数做一层包装,保证在整个回调函数执行过程中,如果修改了状态(数据),那么更新DOM的操作会被推到宏任务队列中,也就是说,更新DOM的执行时间会晚于回调函数的执行时间。

2、withMacroTask先将变量userMacroTask设置为true,然后执行回调,如果这时候回调中修改了数据(触发了更新DOM的操作),而userMacroTask是true,那么更新DOM的操作会被推送到宏任务队列中。当回调执行完毕后,将userMacroTask恢复为false。

3、被withMacroTask包裹的函数所使用的所有vm.$nextTick方法都会将回调添加到宏任务队列中,其中包括状态被修改后触发的更新DOM的回调和用户自己使用vm.$nextTick注册的回调等。

(7)macroTimerFunc如何将回调添加到宏任务队列中

1、 Vue.js优先使用setImmediate,但是它存在兼容性问题,只能在IE中使用,所以使用MessageChannel作为备选方案。如果浏览器也不支持MessageChannel,那么最后会使用setTimeout来将回调添加到宏任务队列中。

<!-- setImmediate -->
if(typeof setImmediate !=='undefined' && isNative(setImmediate)){
	macroTimerFunc = () =>{
		setImmediate(flushCallbacks);
	}
<!-- MessageChannel -->
}else if(typeof MessageChannel !== 'undefined' &&(isNative(MessageChannel)||
		MessageChannel.toString()==='[Object MessageChannelConstructor]')){
			const channel = new MessageChannel();
			const port = channel.port2;
			channel.port1.onmessage = flushCallbacks;
			macroTimerFunc = () =>{
				port.postMessage(1);
			}
}else{
<!-- setTimeout -->
	macroTimerFunc = () =>{
		setTimeout(flushCallbacks,0);
	}
}

(8)microTimerFunc的实现原理是使用Promise.then,但并不是所有浏览器都支持Promise,当不支持时,会被降级成macroTimerFunc。

if(typeof Promise !== 'undefined' && isNative(Promise)){
	const p = Promise.resolve();
	microTimerFunc = () =>{
		p.then(flushCallbacks);
	}
}else{
	microTimerFunc = macroTimerFunc;
}

(9)官方文档中有一句话:如果没有提供回调且在支持Promise的环境中,则返回一个Promise。

this.$nextTick()
 .then(function({
	 //DOM更新了
 }))

要实现这个功能,需要在nextTick中进行判断,如果没有提供回调且当前环境支持Promise,那么返回Promsie,并且在callbacks中添加一个函数,当这个函数执行时,执行Promise 的resolve即可。

export function nextTick(cb,ctx){
	let _resolve;
	<!-- 将回调加入回调队列 -->
	callbacks.push(()=>{
		if(cb){
			cb.call(ctx);
		}else if(_resolve){
			_resolve(ctx);
		}
	})
	<!-- 第一次进入,添加微微任务 -->
	if(!pending){
		pending = true;
		<!-- 添加宏任务代码 -->
		if(userMacroTask){
			macroTimerFunc();
		}else{
			microTimerFunc();
		}
	}
	if(!cb && typeof Promise !== 'undefined'){
		return new Promise(resolve =>{
			_resolve = resolve;
		})
	}
}

六、完整的代码

<!-- 回调列表 -->
const callbacks = [];
let pending = false;

<!-- 执行所有回调并清空列表 -->
function flushCallbacks(){
	pending = false;
	const copies = callbacks.slice(0);
	callbacks.length = 0;
	for(let i = 0;i<copies.length;i++){
		copies[i]();
	}
}
<!-- 添加微任务的函数 -->
let microTimerFunc;
<!-- 添加宏任务的函数 -->
let macroTimerFunc;
<!-- 使用宏任务标识 -->
let userMacroTask = false;
<!-- 添加宏任务macroTimerFunc实现 -->
<!-- setImmediate -->
if(typeof setImmediate !=='undefined' && isNative(setImmediate)){
	macroTimerFunc = () =>{
		setImmediate(flushCallbacks);
	}
<!-- MessageChannel -->
}else if(typeof MessageChannel !== 'undefined' &&(isNative(MessageChannel)||
		MessageChannel.toString()==='[Object MessageChannelConstructor]')){
			const channel = new MessageChannel();
			const port = channel.port2;
			channel.port1.onmessage = flushCallbacks;
			macroTimerFunc = () =>{
				port.postMessage(1);
			}
}else{
<!-- setTimeout -->
	macroTimerFunc = () =>{
		setTimeout(flushCallbacks,0);
	}
}
<!-- 添加微任务microTimerFunc实现 -->
<!-- 支持Promise -->
if(typeof Promise !== 'undefined' && isNative(Promise)){
	const p = Promise.resolve();
	microTimerFunc = () =>{
		p.then(flushCallbacks);
	}
}else{
<!-- 不支持Promise降级为宏任务 -->
	microTimerFunc = macroTimerFunc;
}
<!-- 将回调包在这个函数中,将任务加入到宏任务中 -->
export function withMacroTask(fn){
	return fn._withTask || (fn_withTask = function(){
		userMacroTask = true;
		const res = fn.apply(null,arguments);
		userMacroTask = false;
		retrun res;
	})
}

export function nextTick(cb,ctx){
	let _resolve;
	<!-- 将回调加入回调队列 -->
	callbacks.push(()=>{
		if(cb){
			cb.call(ctx);
		}else if(_resolve){
			_resolve(ctx);
		}
	})
	<!-- 第一次进入,添加微任务 -->
	if(!pending){
		pending = true;
		<!-- 添加宏任务代码 -->
		if(userMacroTask){
			macroTimerFunc();
		}else{
			microTimerFunc();
		}
	}
	<!-- nextTick无回调且支持Promise返回Promise -->
	if(!cb && typeof Promise !== 'undefined'){
		return new Promise(resolve =>{
			_resolve = resolve;
		})
	}
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值