我们在使用 vue-cli 或者看 vue 源码的时候,通常会看到两种不同的包,我们首先看一下源码生成的 dist 目录
主要分为两种:runtime(运行时)和 compiler(编译时)
我们来说一下 runtime 和 compiler 的区别
在我们编写代码使用了 template 模板时,我们就需要使用 compiler,将模板解析成 render 函数,那么这个编译过程会发生在运行时,所以需要带有编译器的版本。很显然,这个编译过程对性能会有一定损耗,而我们 runtime 没有> compiler,包体积会减少大约 30%,而且已经编译成 render 函数,不需要再依赖 compiler
而我们在线上版本,一般都是使用了 vue-loader 进行了编译,所以上线就是 runtime
如下图,这是 src/core 里的目录结构
components:模板编译的代码
global-api:最上层的文件接口
instance:声明周期
observer:数据收据收集与订阅
util:常用工具方法类
vdom:虚拟 dom
MVVM 是一种架构模式,也被称为 model-view-binder。它由微软架构师 Ken Cooper 和 Ted Peters 开发,通过利用 WPF(微软 .NET 图形系统)和 Silverlight(WPF 的互联网应用派生品)的特性来简化用户界面的事> 件驱动程序设计。微软的 WPF 和 Silverlight 架构师之一 John Gossman 于 2005 年在他的博客上发表了 MVVM。
接下来,我们看一下,vue 中的 MVVM 是如何实现的,我们先看核心目录结构

先解释一下这几个文件主要的作用:
我们先看一下 vue MVVM 的流程图,便于理解
首先看一下我们 index.js,这个文件就是 observer,主要就是监听数据变化,为每个数据对象绑定 get,set 方法等
export class Observer {
value: any;
dep: Dep;
vmCount: number;
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
看上面的图,我们来说一下具体的流程
我们在这里插一下,先把处理数组说一下
//--------index.js
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
//上面提到的两个方法protoAugment、copyAugment
function protoAugment (target, src: Object) {
target.__proto__ = src;
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i];
def(target, key, src[key]);
}
}
//---------array.js
import { def } from '../util/index'
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
};
if (inserted) ob.observeArray(inserted);
// notify change
ob.dep.notify()
return result;
})
})
//---------util/lang.js
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
array.js 就是将可以改变数组的方法,通过 Object.defineProperty 进行监听
我们可以看到,observer 一进来执行了 Object.getOwnPropertyNames,里面传入了 arrayMethods,arrayMethods 就是数组的原型,我们又接着调用 def 方法,监听操作数组的方法,def 方法里,value 就是传入的 mutator 方法,最后调用 dep 的 notify 方法,notify 方法看上面图我们知道里面会去更新 watcher,一会说到 watcher,然后返回 result,result 就是数组原型的方法,vue 做了缓存,返回的 result 也被挂在带了 Object 原型上,便于以后对数据的操作
接着,我们看一下这个很重要的方法 observe
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
我们可以看到,observe 方法主要就是执行了 new Observer,将实例化 Observer 并传入 value,方法里也判断了ob存不存在,避免循环创建,浪费内存
我们再看一下上面 walk 方法里循环执行的 defineReactive 方法
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
可以看到 defineReactive 这个方法里做了数据的监听,使用的就是 Object.defineProperty,因为 Object.defineProperty 不能监听数组变化,所以我们上面有了 array.js 处理数组,在 vue3 里,使用 proxy 代替了> Object.defineProperty,兼容 IE11 及以上,之后 vue3.0 新特性会讲到为什么使用 proxy,我们说说 defineReactive 方法里属性的作用
在 observer 这个文件里,还导出了 set 和 del 方法,这个我们这里不做解释,主要就是给 Vue 提供的方法,Vue 可以删除或者更新原型上的属性,我们还可以看到,方法里引入了 Dep,并且实例了 Dep,get 的时候执行了 dep.depend 方法,set 的时候执行了 dep.notify 方法,我们可以参照上面的图,会知道,notify 会触发 Dep,Dep 里会执行 update 更新 Directive,而触发 get 的时候,执行 depend 方法,我们紧接着阑槛 dep 里做了什么
dep 就比较简单了,我们先来看它的源码
let uid = 0
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
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.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
先简单说一些变量的作用,也很容易看得出来
从上面的来看,我们大概清楚了 dep 是干啥的,至于 Dep.target=null,就可以知道同一时间,只能有一个 watcher 被执行,上面的 if 条件不符合,那么在什么地方修改了 target 呢,那一定是 pushTarget 和 popTarget 这两个方法了,我们可以看到这两个方法被导出,而我们发现,在 watcher 里使用了这两个方法
在 watcher 里,定义了很多属性,其中包括重要的几个属性:
我们可以在 watcher 的 constructor 里看到这样一行代码
this.value = this.lazy
? undefined
: this.get()
lazy 是个修饰符,我们先不管,我们看接下来的 get 方法
get () {
pushTarget(this)
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 {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
我们可以看到,get 方法中首先执行 pushTarget,将 Dep.target 值设置为当前 watcher
紧接着,使用了 try…catch…finally,会根据 deep 来判断是否触发当前数据子属性的 getter,这里先不看,我们一会看 traverse.js
然后执行了 popTarget 方法,这个方法就是删除 Dep.target,还执行了 cleanupDeps,这个方法就是清空依赖,因为我们已经实现了对当前 watcher 的收集
我们发现上面执行了 this.getter.call,这里的 getter 就是 lifecycle.js 里的 updateComponent 方法,执行了挂载在原型上的_update 和_render 方法
经过上面的流程,我们发现最后又回到了 observer 的 get 方法里,再列举一下代码
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
}
我们发现又执行 dep.depend 方法, 而且调用了 Dep.target.addDep(this),由于 Dep.target 指向的是 Watcher,我们再看 watcher 里的 addDep 方法
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
我们发现,最后还是调用了 dep.addSub 方法,这样又回到了 dep 里,dep 里的 addSub,将 watcher 添加到了数组中,最终 data 的对应 watcher 都会被放到 deps 里,这样就完成了收集
我们结合之前看到的 dep,当数据变化时,set 方法会触发 notify 方法,notify 方法里调用了 update 方法,我们知道 subs 里存放的是 watcher,那么我们也就知道 watcher 的 update 会触发
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
这里我找了资料,因为没有仔细去看 lazy 和 sync 具体的用处,借鉴于别人的知识,知道 lazy 和 computed 有关,this.sync 跟 watch 有关,如果 sync 是 true,那么要求 watcher 更新要在当前 Tick 一并执行,不需要放到下一个 Tick
我们直接看 queueWatcher 方法,queueWatcher 方法在 scheduler.js 中,接下来我们看 scheduler.js
紧接着上面的,我们先看一下 queueWatcher 做了什么
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)
}
}
}
queueWatcher 首先根据 id,避免重复的执行的 watcher,然后 push 到一个 queue 数组里,最后调用了 nextTick 方法,然后传入了 flushSchedulerQueue 方法,我们紧接着看 flushSchedulerQueue 干了什么
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true;
let watcher, id;
queue.sort((a, b) => a.id - b.id);
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;
}
}
}
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice();
resetSchedulerState();
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);
if (devtools && config.devtools) {
devtools.emit('flush');
}
}
我们发现,这个方法主要就是排序,根据 id 从大到小,因为我们前面也看了,id 是一个 number 从 0 开始,接着使用 for 循环,执行 watcher.run 方法,我们再回到 watcher.js 里,看一下这个 run 方法
//watcher.js
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)
}
}
}
}
我们发现,又回到了 get 方法,也就是要更新 dom,这篇文章里先不说 dom 的更新,将会在之后的 vue 章节里具体说明
遗留问题:nextTick,将会在之后的章节里说一下,nextTick 就是在下次 DOM 更新循环结束之后执行延迟回调