准备工作
去github上把vue2的仓库clone下来。下载依赖,给dev脚本加上--sourcemap参数。
然后执行dev脚本。
再然后我们可以打开一个测试案例,然后进行调试。比如todoMVC。
vue\examples\classic\todomvc\index.html
注意要引入dist目录下的vue.js文件,因为这个文件是加上--sourcemap参数才有的,可以进行源码映射方便调试。
简单的代码 这是我经过简化todoMVC后的代码。。。
<!doctype html>
<section class="todoapp">
{{count}}
</section>
<script src="../../../dist/vue.js"></script>
<script src="app.js"></script>
</body>
</html>
同目录下的app.js
// app Vue instance
var app = new Vue({
data: {
count: 0
},
mounted() {
this.count = 1 // 等下在浏览器调试时,这行代码打上断点。
},
})
// mount
app.$mount('.todoapp')
开始调试
当在mounted钩子里给count赋值为1的时候,会触发set函数
export function proxy(target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter(val) {
// ===================
this[sourceKey][key] = val
// ===================
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
进入set函数,看看里面是怎么做的。
我的理解是dep相当于一个响应式数据,watcher相当于响应式数据在模板上的监视者。一个dep对应一个或多个watcher。当然一个watcher也只对应视图模板上的某一个地方。
export function defineReactive(
obj: object,
key: string,
val?: any,
customSetter?: Function | null,
shallow?: boolean,
mock?: boolean
) {
const dep = new Dep()
// ...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
if (__DEV__) {
dep.depend({
target: obj,
type: TrackOpTypes.GET,
key
})
} else {
dep.depend()
}
if (childOb) {
childOb.dep.depend()
if (isArray(value)) {
dependArray(value)
}
}
}
return isRef(value) && !shallow ? value.value : value
},
// 执行这里的set函数
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if (!hasChanged(value, newVal)) {
return
}
if (__DEV__ && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else if (getter) {
return
} else if (!shallow && isRef(value) && !isRef(newVal)) {
value.value = newVal
return
} else {
// 将新值赋值给dep的val属性上
val = newVal
}
childOb = !shallow && observe(newVal, false, mock)
if (__DEV__) {
// 响应式数据去通知它的watcher们更新视图了。在这里打断点进入代码看看。
dep.notify({
type: TriggerOpTypes.SET,
target: obj,
key,
newValue: newVal,
oldValue: value
})
} else {
dep.notify()
}
}
})
return dep
}
dep.notify函数内部:
export default class Dep {
static target?: DepTarget | null
id: number
subs: Array<DepTarget>
constructor() {
this.id = uid++
this.subs = []
}
addSub(sub: DepTarget) {
this.subs.push(sub)
}
removeSub(sub: DepTarget) {
remove(this.subs, sub)
}
depend(info?: DebuggerEventExtraInfo) {
if (Dep.target) {
Dep.target.addDep(this)
if (__DEV__ && info && Dep.target.onTrack) {
Dep.target.onTrack({
effect: Dep.target,
...info
})
}
}
}
// 响应式数据去通知它的watcher们更新视图了,但不是立即同步执行更新(run方法),而是执行update方法
notify(info?: DebuggerEventExtraInfo) {
// subs是一个watcher数组,这里复制一个副本
const subs = this.subs.slice()
if (__DEV__ && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
// for循环,执行watcher的update函数,可以简单的理解为更新视图的函数。
for (let i = 0, l = subs.length; i < l; i++) {
if (__DEV__ && info) {
const sub = subs[i]
// 触发将要更新的钩子函数
sub.onTrigger &&
sub.onTrigger({
effect: subs[i],
...info
})
}
// 依次执行数组中watcher的update方法。
// 重点就在这里了,update方法内部并不是同步执行的,而是异步执行,那它到底是怎么实现的呢?
subs[i].update()
}
}
}
subs[i].update()执行方法内部:
即会去执行watcher实例的update方法。
export default class Watcher implements DepTarget {
// ...
constructor(
vm: Component | null,
expOrFn: string | (() => any),
cb: Function,
options?: WatcherOptions | null,
isRenderWatcher?: boolean
) {
//
}
get() {
// ...
}
// ...
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// 会去执行这个方法,把watcher放入队列中。传入参数为watcher实例this。
queueWatcher(this)
}
}
run() {
if (this.active) {
// 真正的更新视图
// ...
}
}
// ...
}
queueWatcher方法内部:
可以看到,
接收获取到watcher实例,
const queue: Array<Watcher> = []
let waiting = false
// ...
// 这个方法的作用就是将要更新的watcher放入定义的queue数组中,
// 并且执行一次nextTick方法。这应该是异步更新的关键。
export function queueWatcher(watcher: Watcher) {
const id = watcher.id
if (has[id] != null) {
return
}
if (watcher === Dep.target && watcher.noRecurse) {
return
}
has[id] = true
if (!flushing) { // 没有执行flushSchedulerQueue方法时
// 往queue数组中push watcher
queue.push(watcher)
} else {
// 动态往数组中添加watcher
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// wait默认为false
if (!waiting) {
// 然后就置为true了。
waiting = true
if (__DEV__ && !config.async) {
flushSchedulerQueue()
return
}
// 在这里会执行nextTick方法。没错,源码内部也使用了,也给我们暴露可以使用。
// 传入参数是一个名为flushSchedulerQueue的函数。
nextTick(flushSchedulerQueue)
}
}
// ==========================
// flushSchedulerQueue方法定义
// ==========================
function flushSchedulerQueue() {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
queue.sort(sortCompareFn)
// ===========================触发watcher的run方法========================
// 同步for顺序循环queue数组,执行watcher的run方法,触发视图更新。
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
if (__DEV__ && 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
}
}
}
// ===========================触发watcher的run方法end========================
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
function resetSchedulerState() {
index = queue.length = activatedChildren.length = 0
has = {}
if (__DEV__) {
circular = {}
}
waiting = flushing = false
}
在nextTick所在的文件,会有一个数组来存放传入nextTick的函数入参。
nextTick的作用就是把参数push进名为callbacks的数组中,并且执行一次timeFunc方法。
而timeFunc的作用就是使用Promise.then来异步执行flushCallbacks方法,把存入callbacks中的数组统统flush掉(执行一遍)
看到这里,是不是感觉和很多个watcher放入queue数组有点相像?
// 这个文件里会有一个数组,专门保存传入nextTick的回调参数
const callbacks: Array<Function> = []
// ...
let pending = false
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
let _resolve
// 把回调参数push进callbacks数组中。
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e: any) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
// 然后执行在同一文件里的timerFunc方法。
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
// 因为有些浏览器不执行Promise,所以需要做处理。。ie,你知道的。。这里删减一些代码,太长了
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
// 原来timerFunc方法就是使用Promise的微任务,异步执行flushCallbacks方法。
timerFunc = () => {
p.then(flushCallbacks)
}
} else if (
// ...
) {
// ...
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// ...
} else {
// ...
}
// 执行这里
// callbacks数组中存的是传入nextTick参数的函数。
// for循环执行。
function flushCallbacks() {
pending = false
// 保存一个副本
const copies = callbacks.slice(0)
// 将原本的函数数组清空、初始化。
callbacks.length = 0
// 循环执行副本的每一个函数。
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
总结
当vue的响应式数据更新时,会去触发对应的set方法,进而执行dep.notify方法,通知所有的watcher去更新视图。
每个watcher会执行update方法,将自己的实例this作为参数调用queueWatcher(this)方法,然后把自己添加到队列数组queue中。
queueWatcher方法第一次还会调用nextTick(flushSchedulerQueue)方法。
flushSchedulerQueue会for循环依次执行queue中保存的watcher的run方发。
而nextTick方法也是将传入的函数参数添加到一个数组callbacks中。
并且第一次还会执行timeFunc方法,在兼容的情况下也就是执行Promise.then(flushCallbacks)方法。
flushCallbacks()会for循环依次执行callbacks中保存的函数。