聊聊$nextTick源码

这里是咕了一个月终于开了新坑的雪之樱,上午在b站看了个电影《为美好的世界献上祝福!红传说 》,决定水篇博客(所以有什么关系吗)。好吧,言归正传,这次要学习的是$nextTick的源码,相信小伙伴们对$nextTick一定不会陌生,啥,忘了,好吧,笔者在这里贴心地附上vue文档对它的说明。

看~是不是很简单,它用于在组件完成下次更新时执行一个回调,你可以在这里获取更新过的DOM状态,然后做一些操作。然后我们就从源码角度看看他做了什么吧。

$nextTick的混入在vue/src/core/instance/render下的renderMixin方法中


import {nextTick} from '../util/index'
export function renderMixin(Vue: Class) {
	// ...
	Vue.prototype.$nextTick = function (fn: Function) {
		return nextTick(fn, this)
	};
	// ...
}

可以看到,$nextTick是调用了nextTick函数,然后把cb和this作为参数传给了nextTick,那我们就来看看nextTick的实现

nextTick的代码并不多,去掉一些平台相关的代码和注释后,核心逻辑不到一百行的样子,我们先把它贴出来,然后再慢慢分析


/* @flow */
/* globals MessageChannel */

import {handleError} from './error'
import {isNative} from './env'

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 useMacroTask = false;


// 具体实现根据浏览器的兼容实现宏任务
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
	macroTimerFunc = () => {
		setImmediate(flushCallbacks)
	}
} 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 {
	/* istanbul ignore next */
	macroTimerFunc = () => {
		setTimeout(flushCallbacks, 0)
	}
}

// 具体实现根据浏览器的兼容实现微任务
if (typeof Promise !== 'undefined' && isNative(Promise)) {
	const p = Promise.resolve();
	microTimerFunc = () => {
		p.then(flushCallbacks);
	}
} else {
	// fallback to macro
	microTimerFunc = macroTimerFunc
}

export function withMacroTask(fn: Function): Function {
	return fn._withTask || (fn._withTask = function () {
		useMacroTask = true;
		const res = fn.apply(null, arguments);
		useMacroTask = false;
		return res
	})
}

// cb是传入的回调, ctx是this
export function nextTick(cb?: Function, ctx?: Object) {
	let _resolve;
	// 收集cb
	callbacks.push(() => {
		if (cb) {
			try {
				cb.call(ctx)
			} catch (e) {
				handleError(e, ctx, 'nextTick')
			}
		} else if (_resolve) {
			_resolve(ctx)
		}
	});
	if (!pending) {
		pending = true;
		if (useMacroTask) {
			macroTimerFunc()
		} else {
			microTimerFunc()
		}
	}
	// $flow-disable-line
	if (!cb && typeof Promise !== 'undefined') {
		return new Promise(resolve => {
			_resolve = resolve
		})
	}
}

我们可以看到,nextTick中定义了一个变量_resolve,这个变量只有当你没有传入cb,而是打算使用$nextTick().then()这种方式设置回调时才会用到,我们先不分析这个,先按照传入cb的方式看看nextTick做了什么。

在下一行里,nextTick往callbacks里插入了一个匿名函数,这个匿名函数会判断cb是否存在,如果存在就执行,不存在就调用_resolve(ctx),很明显,nextTick并没有马上执行这个回调,而是把它缓存到了一个队列中,那么这个队列什么时候执行呢,我们接着往下看。

然后,nextTick会判断pending的值,pending是一个全局变量,标记着当前的callbacks队列正等待被执行,如果判断pending的值是false,就是不处于等待被执行的状态,就会根据useMacroTask这个标记,执行macroTimerFunc,或者是microTimerFunc,先剧透一下,这两个函数都是用于把callbacks里的回调拿出来执行的,最后,判断如果没有传入cb,那么我们就创建一个new Promise并返回,同时把resolve赋值给_resolve,之前我们已经看到,_resolve会在被推入了callbacks的匿名函数中执行,也就是说,和直接传入cb一样,_resolve也会在callbacks里的函数被取出执行时执行。

然后我们看看 macroTimerFunc microTimerFunc 的实现,这两个函数的作用都是把清空callbacks的任务(flushCallbacks函数)推到执行队列中等待被执行,只不过是推入到宏任务队列还是微任务队列的区别而已,我们先看看看 microTimerFunc


// 具体实现根据浏览器的兼容实现微任务
if (typeof Promise !== 'undefined' && isNative(Promise)) {
	const p = Promise.resolve();
	microTimerFunc = () => {
		p.then(flushCallbacks);
	}
} else {
	// fallback to macro
	// 降级使用宏任务
	microTimerFunc = macroTimerFunc
}

这段代码首先会判断浏览器有没有实现Promise,如果有,就在then里面执行flushCallbacks,也就是说, flushCallbacks被推入到了微任务的执行队列中,等待当前执行栈的任务结束,就可以被执行。如果没有实现Promise(鞭尸IE),就取macroTimerFunc的值

然后我们看看macroTimerFunc的实现,


// 具体实现根据浏览器的兼容实现宏任务
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
	macroTimerFunc = () => {
		setImmediate(flushCallbacks)
	}
} 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 {
	/* istanbul ignore next */
	macroTimerFunc = () => {
		setTimeout(flushCallbacks, 0)
	}
}

可以看到,这里前后依次会尝试使用setImmediateMessageChannelsetTimeout来实现 macroTimerFunc ,他们要做的事情都是把flushCallbacks推入到宏任务队列中。

之前已经说过, 真正把cb从callbacks中取出执行的逻辑在flushCallbacks中,那我们来看看它的实现


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

flushCallbacks函数的实现非常简单,首先把pending置为false,标记着已经清空执行过callbacks队列,接着callbacks复制一份拷贝给copies,然后清空callbacks,依次执行回调。

好啦,这就是nextTick的全部源码解析啦,但我们似乎忘了点什么,没错,还记得为什么$nextTick的作用吗,没错,那就是在dom更新后执行一个回调,那这个又是怎么实现的呢,其实很简单,在修改了数据后,会把修改dom的回调通过nextTick添加到callbacks中,如果在修改数据后在通过$nextTick添加回调,而这次添加的回调会被添加到修改dom的回调后,所以在执行时,也是先执行更新dom的操作,再执行用户通过$nextTick传入的回调。这样就可以在cb里拿到最新的dom,我们来看看源码


export default class Watcher {
	
	/**
	 * Subscriber interface.
	 * Will be called when a dependency changes.
	 * update函数在数据更新时触发
	 */
	update() {
		/* istanbul ignore else */
		// 计算属性的更新
		if (this.computed) {
			// A computed property watcher has two modes: lazy and activated.
			// It initializes as lazy by default, and only becomes activated when
			// it is depended on by at least one subscriber, which is typically
			// another computed property or a component's render function.
			if (this.dep.subs.length === 0) {
				// In lazy mode, we don't want to perform computations until necessary,
				// so we simply mark the watcher as dirty. The actual computation is
				// performed just-in-time in this.evaluate() when the computed property
				// is accessed.
				this.dirty = true
			} else {
				// In activated mode, we want to proactively perform the computation
				// but only notify our subscribers when the value has indeed changed.
				this.getAndInvoke(() => {
					this.dep.notify()
				})
			}
		}
		// 同步更新
		else if (this.sync) {
			this.run()
		} else {
			// 正常的派发更新
			queueWatcher(this)
		}
	}
}

在数据更新后,watcherupdate方法会被触发,然后会触发queueWatcher方法


export function queueWatcher(watcher: Watcher) {
	const id = watcher.id;
	// 判断watcher在不在queue里面
	// 即使一个watcher在一个tick内多次触发update,也不会造成多次更新
	if (has[id] == null) {
		has[id] = true;
		if (!flushing) {
			queue.push(watcher)
		} else {
			// if already flushing, splice the watcher based on its id
			// if already past its id, it will be run next immediately.
			// 如果正在刷新
			let i = queue.length - 1;
			while (i > index && queue[i].id > watcher.id) {
				i--
			}
			queue.splice(i + 1, 0, watcher)
		}
		// queue the flush
		if (!waiting) {
			waiting = true;
			// 在下一个tick去执行
			nextTick(flushSchedulerQueue)
		}
	}
}

看,我们的想法得到了验证,更新dom也是通过nextTick添加的,在这之后通过$nextTick添加的回调自然就在更新dom之后啦。

好了,这就是这篇文章全部的内容了,希望大佬们能给个赞鼓励一下我未来的高产orz,这里是划水咸鱼雪之樱,我们下次见。

点赞

发表评论

昵称和uid可以选填一个,填邮箱必填(留言回复后将会发邮件给你)
tips:输入uid可以快速获得你的昵称和头像

Title - Artist
0:00