写在前面
来了来了,我来填坑了之前响应式文章中,关于最后Dep的notify后通知Watcher的run去更新数据的中间环节省略内容,以及nextTick的内容,这篇文章讲会大致说一下。大致的会分为一下两个部分
- 怎么走到了nextTick
- 来看看nextTick
注:这次源码内容基于"vue@^2.6.10"版本,因为会和大佬PDF的版本(Vue.js 2.5.17-beta.0,)有部分不同,虽然我再关键处会贴出两个部分进行对比, 但是我还是建议看这个文章的时候也能打开自己Vue项目里的Vue文件大致看一下。
ok 让我们开始。
1、怎么走到了nextTick
这里我们不再已响应式那个流程走,而是以nextTick为源头看看,来搜源码。
- 定义nextTick 在 src/core/util/next-tick.js 中,
- 那里用了?全局搜,除了一些runtime和全局混入中的Vue.prototype.$nextTick(),我们找到了src/core/observer/scheduler.js 中queueWatcher函数调用了nextTick(flushSchedulerQueue)。
- 来 我们继续,flushSchedulerQueue干啥了 ,看里面 它主要各种操作走了watcher.run()的方法。继续
- watcher.run()我们来到src/core/observer/watcher.js中看一下run的方法。好了 完事了,这里调用了cb然后就去更新数据了。
- 好了好了,完事了,逆推完美撒花,结束,,,,,,哈哈,当然不可能。大致走完了,我们接下来将源码贴上,再从run开始推回去,我们再屡一下。
1.1 Dep.notify => Watcher.updata => queueWatcher
- src/core/observer/dep.js
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
看到了,发布订阅的通知就是从这里开始,继续走update,当然这里还对是否有全局异步进行了判断,这个地方我们后面大致说一下吧,因为这个地方2.5版本中并没有。。。而且后面有不少地方都能见到这个process.env.NODE_ENV !== 'production' && !config.async判断。
- src/core/observer/watcher.js
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
我们在watcher的update中可以看到,还是判断,正常没有乱七八糟的配置的话,就会走queueWatcher。
- src/core/observer/scheduler.js
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
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
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
这⾥引⼊了⼀个队列的概念,这也是 Vue在做派发更新的时候的⼀个优化的点,它并不会每次数据改 变都触发 watcher 的回调,⽽是把这些 watcher 先添加到⼀个队列⾥,然后在 nextTick 后执 ⾏ flushSchedulerQueue 。 这⾥有⼏个细节要注意⼀下,⾸先⽤ has 对象保证同⼀个 Watcher 只添加⼀次;接着对 flushing 的判断;最后通过 wating 保证对 nextTick(flushSchedulerQueue) 的调⽤逻辑只有⼀次,另外 nextTick 的实现我之后会抽⼀⼩节 专门去讲,⽬前就可以理解它是在下⼀个 tick,也就是异步的去执⾏ flushSchedulerQueue 。
- src/core/observer/scheduler.js
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}
...do other thing....
}
- 队列排序 来看注释。queue.sort((a, b) => a.id - b.id) 对队列做了从⼩到⼤的排序,这么做主要有以下要确保以下 ⼏点:
1.1. 组件的更新由⽗到⼦;因为⽗组件的创建过程是先于⼦的,所以 watcher 的创建也是先⽗后⼦,执⾏顺序也应该保持先⽗后⼦。
1.2. ⽤户的⾃定义 watcher 要优先于渲染 watcher 执⾏;因为⽤户⾃定义 watcher 是在渲染 watcher 之前创建的。
1.3. 如果⼀个组件在⽗组件的 watcher 执⾏期间被销毁,那么它对应的 watcher 执⾏都可以被跳过,所以⽗组件的 watcher 应该先执⾏。
- 队列遍历(不细究了,代码都有注释,注意 queueWatcher 中else中的注释
else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
) 3. 状态恢复(resetSchedulerState)
- src/core/observer/watcher.js
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
来到了run()这就很明显了。就是执行回调,这里你如果是看2.5版本的话会看到
run () {
if (this.active) {
this.getAndInvoke(this.cb)
}
}
然后getAndInvoke这个方法和2.6的版本执行的一样,这里也可以看到,Vue也是在会不断地优化自己的代码,当然也不排除之前有想再run中加别的东西的可能。
- 小结:
- 再正式开始说nextTick前,我们算是把之前响应式派发的债补了,但是这也是我看nextTick后才回过劲迷糊过来的,之前不敢写也是因为自己也是糊里糊涂。
- 大致梳理一下,就是我们派发更新会调用notify然后走watcher的update然后呢走queueWatcher这个又是一个调用nextTick传递flushSchedulerQueue函数的方法,flushSchedulerQueue中又是走了watcher的run,。我们会再nextTick中依次调用flushSchedulerQueue也就是已经拍好对的run的方法,也就是依次执行数据更新。,语言文字够清晰了吧 ,奥利给。。。。。后面给个图吧,
- 下面我们来说一下nextTick吧
来看看nextTick
代码不多直接上源码吧(2.6.10)
- src/core/util/next-tick.js
/* @flow */
/* globals MutationObserver */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
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]()
}
}
// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc
(
我卑微的添加
let microTimerFunc
let macroTimerFunc
2.5版本没有timerFunc而是以上两个定义
)
// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
...进行 MutationObserver的处理,用不到为了短点,就删了
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Techinically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
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)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
这里其实和大佬PDF中有点不一样大佬PDF 分析的是2.5版,不同代码我就不贴出去了,。我再我这个地方的加注释吧,凑活看,不行的话,有兴趣的自己下载2.5去看、2.5的时候Vue在这里会声明 microTimerFunc 和 macroTimerFunc 2 个变量,它们分别对应的是 microtask 的函数和 macro task 的函数,但是后面也是会加判断,最后⼀次性地根据 useMacroTask 条件执⾏ macroTimerFunc 或者是 microTimerFunc 。
我们既然自己已经看了2.6+版,那么我们就不在去学习2.5的了,已上面2.6+为主, 来开始
- 读注释: 上面有两大段注释,我彩笔翻译(加机翻)大致如下
- 这里我们有异步延迟使用microtasks包装。在2.5我们使用(宏观)任务(结合microtasks)。然而,当状态发生改变且重绘之前会发生一些微妙的问题,同时,在事件处理程序中使用(宏观)任务会导致一些奇怪的行为,因此现在我们选择再任何地方都使用microtasks。这个权衡的一个主要缺点是,有一些场景比如,microtasks过高的优先级。。。。我去他XXX 遍不下去了,,,求大佬指点,反正人尤大肯定有考虑选择了这个方案。,
- nextTick行为利用了microtask队列,可以直接使用原生的promise或者是 MutationObserver.MutationObserver更广泛的支持,然而它是严重困扰着/ 在iOS UIWebView > = 9.3.3事件处理程序触发后联系。。。。。大致意思就是告诉你怎么在这判断吧MutationObserver这玩意,不太了解,这里还是判断了setImmediate这是⼀个⾼版本 IE 和 Edge 才⽀持的特性,不⽀持的话再去检测是否⽀持原⽣的 MessageChannel , 如果也不⽀持的话就会降级为 setTimeout 0 ;
哎反正乱七八糟翻译了一下,大致意思就是在判断选用什么样的异步方式,当然我们只看我们最关键的Web的。
走流程吧
先来看 nextTick ,这就是我们在上⼀节执⾏ nextTick(flushSchedulerQueue) 所⽤到的函数。它的逻辑也很简单,把传⼊的回调函数 cb 压⼊ callbacks 数组,它们都会在下⼀个 tick 执⾏ flushCallbacks , flushCallbacks 的逻辑⾮常简单,对callbacks遍历,然后执⾏相应的回调函数。这⾥使⽤ callbacks⽽不是直接在nextTick中执⾏回调函数的原因是保证在同⼀个 tick内多次执⾏nextTick,不会开启多个异步任务,⽽把这些异步任务都压成⼀个同步任务,在下⼀个 tick 执⾏完毕。
然后就很清晰了啊, 回去调用watcher的run去更新数据了,哈哈 完事。。
但是这里还有两个细节我想再说一下
- slice(0)
const copies = callbacks.slice(0)
我们可以看到上面有个这样的一段代码。我专门搜了搜,说是有两个功能。 我们使用slice(0)对原始数组进行一个深拷贝和将类数组对象转化为真正的数组对象,当时我看到这的时候确实不太了解,看了看,然后才发现Vue中有很多类似的写法,哈哈,有兴趣的可以再stackoverflow看看 注:我的第一个评论,开心,先声明一下啊。关于slice(0)的使用。我最开始是百度搜索,唯一搜到的是这个文章"blog.csdn.net/i042416/art…",里面给了两种说法和stackoverflow的链接,这个深浅拷贝很好验证,控制台稍微打印一下,可以验证,如下图,但是关于这个知识点需要好好梳理一下,这里算是在给自己加个坑把。
再次注:我先道歉,上面是错的。晚上再看一个课程视频中正好讲了slice的深浅拷贝问题具体表现如下
我们可以看到,对于数组中的引用类型修改,这种拷贝并没有完成深拷贝,我不再乱言。推荐"www.cnblogs.com/echolun/p/7…"这篇文章,里面讲到了关于slice拷贝的部分。 最后还是谢谢我第一个文章评论者left_泽鑫的指正,也让我进一步学习了。
- 对于config.sync的判断
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
我们上面留的问题,这里我们需要去找一下config.async
- 源码地址src/core/config.js 对于全局配置可以看我的API学习文档
/**
* Perform updates asynchronously. Intended to be used by Vue Test Utils
* This will significantly reduce performance if set to false.
*/
async: true,
这就是async的注释,翻译如下 执行异步更新。为了Vue的单元测试使用,,如果设置为false。这将显著降低性能。默认是true 拿回去看,就很明白了。上面的判断那就是如果有async的专门配置的话,就会让所有的数据更新进行单个流程的跑,
- 总结:
nextTick算是完事了,。但是我觉得还缺点什么应该是实际的分析和操作吧,后续有空补上,毕竟Vue的开发一大利器,不能仅限于源码看。。。
后记
- 没啥说的,希望多交流,也希望看到的你能有收获,谢谢。