这里是咕了一个月终于开了新坑的雪之樱,上午在 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) | |
} | |
} |
可以看到,这里前后依次会尝试使用 setImmediate,MessageChannel, setTimeout
来实现 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) | |
} | |
} | |
} |
在数据更新后, watcher
的 update
方法会被触发,然后会触发 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,这里是划水咸鱼雪之樱,我们下次见。