Vue之nextTick理解
前言
一开始就只想搞清楚nextTick的一个原理,谁知道,跟吃了辣条一下,停不下来,从nextTick的源码到Watcher源码再到Dep源码,震惊,然后再结合自己之前看掘金小册理解的双向绑定-响应式系统
,感觉有一种顿悟
的感觉,总之,这是我个人的理解,请大佬们指教,如有转载,请附上原文链接,毕竟我copy源码也挺累的~
多说一句话
因为这篇文章,有挺多源代码的,一般来说,换作是我,我也会一扫而过,一目十行,但是笔者我!<strong>真心!</strong><strong>希望!</strong>你们能够<strong>耐住性子!</strong>去看!源码中,会有一丢丢注释,<strong>一定要看尤大大作者给的注释</strong>
如果有什么地方写错了,恳请大佬们指教,互相进步~
请开始你的表演
那么怎么说nextTick呢?该从何说起,怪难为情的,还是让我们先来看个例子吧
<template>
<div>
<div ref="usernmae">{{ username }}</div>
<button @click="handleChangeName">click</button>
</div>
</template>
export default {
data () {
return {
username: 'PDK'
}
},
methods: {
handleChangeName () {
this.username = '彭道宽'
console.log(this.$refs.username.innerText) // PDK
}
}
}
<storng>震惊!!!</strong>,打印出来的居然的 "PDK",怎么回事,我明明修改了username,将值赋为"彭道宽",为什么还是打印之前的值,而真实获取到DOM结点的innerText并没有得到预期中的“彭道宽”, 为啥子 ?
不方,我们再看一个例子,请看:
export default {
data () {
return {
username: 'PDK',
age: 18
}
},
mounted() {
this.age = 19
this.age = 20
this.age = 21
},
watch: {
age() {
console.log(this.age)
}
}
}
这段脚本执行我们猜测会依次打印:19,20,21。但是实际效果中,只会输出一次:21。为什么会出现这样的情况?
事不过三,所以我们再来看一个例子
export default {
data () {
return {
number: 0
}
},
methods: {
handleClick () {
for(let i = 0; i < 10000; i++) {
this.number++
}
}
}
}
在点击click触发handleClick()事件之后,number会被遍历增加10000次,在vue的双向绑定-响应式系统中,会经过 “setter -> Dep -> Watcher -> patch -> 视图” 这个流水线。那么是不是可以这么理解,每次number++,都会经过这个“流水线”来修改真实的DOM,然后DOM被更新了10000次。
但是身为一位“资深”的前端小白来说,都知道,前端对性能的看中,而频繁的操作DOM,那可是一大“忌讳”啊。Vue.js 肯定不会以如此低效的方法来处理。Vue.js在默认情况下,每次触发某个数据的 setter 方法后,对应的 Watcher 对象其实会被 push 进一个队列 queue 中,在下一个 <strong>tick</strong> 的时候将这个队列 queue 全部拿出来 run一遍。这里我们看看Vue官网的描述 : Vue <strong>异步执行
</strong> DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
Vue在修改数据的时候,不会立马就去修改数据,例如,当你设置 vm.someData = 'new value' ,该组件不会立即重新渲染。当刷新队列时,组件会在事件循环队列清空时的下一个 <strong>tick</strong> 更新, 为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用,下边来自Vue官网中的例子 :
<div id="example">{{message}}</div>
var vm = new Vue({
el: '#example',
data: {
message: '123'
}
})
vm.message = 'new message' // 更改数据
console.log(vm.$el.textContent === 'new message') // false, message还未更新
Vue.nextTick(function () {
console.log(vm.$el.textContent === 'new message') // true, nextTick里面的代码会在DOM更新后执行
})
下一个tick是什么鬼玩意 ?
上面一直扯扯扯,那么到底什么是 下一个tick
?
nextTick函数其实做了两件事情,一是生成一个timerFunc,把回调作为microTask或macroTask参与到事件循环中来。二是把回调函数放入一个callbacks队列,等待适当的时机执行
nextTick在官网当中的定义:
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
在 Vue 2.4 之前都是使用的 microtasks(微任务)
,但是 microtasks 的优先级过高,在某些情况下可能会出现比事件冒泡更快的情况,但如果都使用 macrotasks(宏任务)
又可能会出现渲染的性能问题。所以在新版本中,会默认使用 microtasks,但在特殊情况下会使用 macrotasks。比如 v-on。对于不知道JavaScript运行机制的,可以去看看阮一峰老师的JavaScript 运行机制详解:再谈Event Loop、又或者看看我的Event Loop
哎呀妈,又扯远了,回到正题,我们先去看看vue中的源码 :
/* @flow */
/* globals MessageChannel */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIOS, isNative } from './env'
const callbacks = [] // 定义一个callbacks数组来模拟事件队列
let pending = false // 一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送
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 both microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
let microTimerFunc
let macroTimerFunc
let useMacroTask = false
// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
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)
}
}
// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () => {
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)
}
} else {
// fallback to macro
microTimerFunc = macroTimerFunc
}
/**
* Wrap a function so that if any code inside triggers state change,
* the changes are queued using a (macro) task instead of a microtask.
*/
export function withMacroTask (fn: Function): Function {
return fn._withTask || (fn._withTask = function () {
useMacroTask = true
const res = fn.apply(null, arguments)
useMacroTask = false
return res
})
}
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
if (useMacroTask) {
macroTimerFunc()
} else {
microTimerFunc()
}
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
来来来,我们仔细的扯一扯~
首先因为目前浏览器平台并没有实现 nextTick 方法,所以 Vue.js 源码中分别用 Promise
、setTimeout
、setImmediate
等方式在 microtask(或是macrotasks)中创建一个事件,目的是在当前调用栈执行完毕以后(不一定立即)才会去执行这个事件
<strong>对于实现 macrotasks ,会先判断是否能使用 setImmediate ,不能的话降级为 MessageChannel ,以上都不行的话就使用 setTimeout。</strong> 注意,是对实现<strong>宏任务</strong>的判断
问题来了?为什么要优先定义 setImmediate
和 MessageChannel
创建,macroTasks而不是 setTimeout
呢?
HTML5中规定setTimeout的最小时间延迟是4ms,也就是说理想环境下异步回调最快也是4ms才能触发。Vue使用这么多函数来模拟异步任务,其目的只有一个,就是让回调异步且尽早调用。而 MessageChannel 和 setImmediate 的延迟明显是小于 setTimeout的
// 是否可以使用 setImmediate
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) { // 是否可以使用 MessageChannel
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () => {
port.postMessage(1) // 利用消息管道,通过postMessage方法把1传递给channel.port2
}
} else {
/* istanbul ignore next */
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0) // 利用setTimeout来实现
}
}
setImmediate 和 MessageChannel 都不行的情况下,使用 setTimeout,delay = 0 之后,执行flushCallbacks(),下边是flushCallbacks的代码
// setTimeout 会在 macrotasks 中创建一个事件 flushCallbacks ,flushCallbacks 则会在执行时将 callbacks 中的所有 cb 依次执行。
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
前面说了,nextTick
同时也支持 Promise 的使用,会判断是否实现了 Promise
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 将回调函数整合至一个数组,推送到队列中下一个tick时执行
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) { // pengding = false的话,说明不需要不存在,还没有timerFunc被推送到任务队列中
pending = true
if (useMacroTask) {
macroTimerFunc() // 执行宏任务
} else {
microTimerFunc() // 执行微任务
}
}
// 判断是否可以使用 promise
// 可以的话给 _resolve 赋值
// 回调函数以 promise 的方式调用
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
<img src='https://user-gold-cdn.xitu.io/2018/11/10/166fcad17b34943d?w=580&h=326&f=jpeg&s=23011'>
你以为这就结束了?
ok,上边nextTick的源码比较少,看得大概大概的了,但是呢,还是很懵,所以我又去github看了一下watcher.js的源码,回到开头的第三个例子,就是那个循环10000次的那个小坑逼,来,我们看下源码再说,源码里的代码太多,我挑着copy,嗯,凑合看吧
import {
warn,
remove,
isObject,
parsePath,
_Set as Set,
handleError,
noop
} from '../util/index'
import { traverse } from './traverse'
import { queueWatcher } from './scheduler' // 这个很也重要,眼熟它
import Dep, { pushTarget, popTarget } from './dep' // 眼熟这个,这个是将 watcher 添加到 Dep 中,去看看源码
import type { SimpleSet } from '../util/index'
let uid = 0 // 这个也很重要,眼熟它
/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*/
export default class Watcher {
// 其中的一些我也不知道,我只能从字面上理解,如有大佬,请告知一声
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object, // 我们的options
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watch = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // 看到没有,我们类似于给每个 Watcher对象起个名字,用id来标记每一个Watcher对象
this.active = true
this.dirty = this.lazy
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get() // 执行get()方法
}
get () {
pushTarget(this) // 调用Dep中的pushTarget()方法,具体源码下边贴出
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
if (this.deep) {
traverse(value)
}
popTarget() // 调用Dep中的popTarget()方法,具体源码下边贴出
this.cleanupDeps()
}
return value
}
// 添加到dep中
addDep(dep: Dep) {
const id = dep.id // Dep 中,存在一个id和subs数组(用来存放所有的watcher)
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this) // 调用dep.addSub方法,将这个watcher对象添加到数组中
}
}
}
...
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this) // queueWatcher()方法,下边会给出源代码
}
}
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
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) // 回调函数
}
}
}
}
...
}
太长了?染陌大佬的《剖析 Vue.js 内部运行机制》中给出了一个简单而有利于理解的代码(群主,我不是打广告的,别踢我)
let uid = 0;
class Watcher {
constructor () {
this.id = ++uid;
}
update () {
console.log('watch' + this.id + ' update');
queueWatcher(this);
}
run () {
console.log('watch' + this.id + '视图更新啦~');
}
}
queueWatcher 是个什么鬼
够抽象吧!再看看这个代码,比较一看,你会发现,都出现了一个 <strong>queueWatcher</strong>的玩意,于是我去把源码也看了一下。下边是它的源代码(选择copy)
import {
warn,
nextTick, // 看到没有,我们一开始要讲的老大哥出现了!!!!
devtools
} from '../util/index'
export const MAX_UPDATE_COUNT = 100
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
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() // 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
}
}
}
...
}
/**
* 看注释看注释!!!!!!
* 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 // 获取watcher的id
// 检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验
if (has[id] == null) {
has[id] = true
if (!flushing) {
// 如果没有flush掉,直接push到队列中即可
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 // 标志位,它保证flushSchedulerQueue回调只允许被置入callbacks一次。
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue) // 看到没有,调用了nextTick
// 这里面的nextTick(flushSchedulerQueue)中的flushSchedulerQueue函数其实就是watcher的视图更新。
// 每次调用的时候会把它push到callbacks中来异步执行。
}
}
}
Dep
哎呀妈,我们再来看看Dep中的源码
import type Watcher from './watcher' // 眼熟它
import { remove } from '../util/index'
import config from '../config'
let uid = 0
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
// 将所有的watcher对象添加到数组中
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
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)
}
// 通过循环,来调用每一个watcher,并且 每个watcher都有一个update()方法,通知视图更新
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null
const targetStack = []
export function pushTarget (_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
Dep.target = targetStack.pop()
}
// 说白了,在数据【依赖收集】过程就是把 Watcher 实例存放到对应的 Dep 对象中去
// 这时候 Dep.target 已经指向了这个 new 出来的 Watcher 对象
// get 方法可以让当前的 Watcher 对象(Dep.target)存放到它的 subs 数组中
// 在数据变化时,set 会调用 Dep 对象的 notify 方法通知它内部所有的 Watcher 对象进行视图更新。
最后在扯两句
真的是写这篇文章,花了一下午,也在掘金找了一些文章,但是都不够详细,并且很多时候,感觉很多文章都是千篇一律,借鉴了别人的理解,然后自己同时看染陌大佬的讲解,以及自己去看了源码,才大概看懂,果然,看的文章再多,还不如去看源码来的实在!!!
友情链接
《我的博客》: https://github.com/PDKSophia/blog.io
《我的掘金》 : https://juejin.im/user/594ca8a35188250d892f4139
《剖析 Vue.js 内部运行机制》: https://juejin.im/book/5a36661851882538e2259c0f
《Vue官网之异步更新队列》: https://cn.vuejs.org/v2/guide/reactivity.html#%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0%E9%98%9F%E5%88%97
《MessageChannel API》: https://developer.mozilla.org/zh-CN/docs/Web/API/MessageChannel
《Vue中DOM的异步更新策略以及nextTick机制》: https://funteas.com/topic/5a8dc7c8f7f37aa60a177bb7
《Vue.js 源码之nextTick》: https://github.com/vuejs/vue/blob/dev/src/core/util/next-tick.js
《Vue.js 源码之Watcher》: https://github.com/vuejs/vue/blob/dev/src/core/observer/watcher.js#L31
《Vue.js 源码之Dep》: https://github.com/vuejs/vue/blob/dev/src/core/observer/dep.js