这里是咕了一个月终于开了新坑的雪之樱,上午在 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 里的函数被取出执行时执行。

然后我们看看 macroTimerFuncmicroTimerFunc 的实现,这两个函数的作用都是把清空 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,这里是划水咸鱼雪之樱,我们下次见。