文章原意
我们知道Vue2.0原理是利用Object.defineProperty,劫持对象的get与set方法。以及MVVM使用了观察者模式,此次目的就是手把手的看一下到底在哪儿进行的劫持?到底在哪儿创建的观察者?
怕看不懂的童鞋,可先看一下 模拟实现vue的MVVM文章,此文将会手把手展示其内容中对应的Observe,Dep,Watcher,defineRactive的出处,以作进一步讲解
听了很久Vue源码解析。这两天花了几个小时时间参观了一下vue的源码,感受就是语义化写的很好!细节确实比较多,绕也确实绕。
此文有点长,不过耐着性子看完的同学相信已经入门自己动手查看任何一个npm库的源码
介绍
- version:2.6.10
- 源码下载地址:github.com/vuejs/vue/t…
- 类型:runtime
此次观影顺序
读前须知:npm包被引用时,如何找到该包的文件入口呢?其入口的就是 package.json中 "mian"所指向的文件
那我们就从package.json开始,以纯读者角度考虑,纯新手出发,来看一下MVVM实现的机理
具体顺序如下:
- package.json
- scripts\config.js
- src\platforms\web\entry-runtime.js
- src\platforms\web\runtime\index.js
- src\core\index.js
- src\core\instance\index.js
- src\core\instance\init.js
- src\core\instance\state.js --- 最重要的文件了,大部分源码精髓都在此处
- src\core\observer\dep.js
- src\core\observer\watcher.js
package.json
...
"main": "dist/vue.runtime.common.js",
...
"scripts": {
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
"dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-cjs-dev",
"dev:esm": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-esm",
"dev:test": "karma start test/unit/karma.dev.config.js",
"dev:ssr": "rollup -w -c scripts/config.js --environment TARGET:web-server-renderer",
}
从上可知vue源码使用rollup打包,入口文件为 vue.runtime.common.js,rollup使用 scripts/config.js 作为配置文件入口,打开 scripts/config.js
scripts/config.js
...
const builds = {
// Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
'web-runtime-cjs-dev': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.common.dev.js'),
format: 'cjs',
env: 'development',
banner
},
'web-runtime-cjs-prod': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.common.prod.js'),
format: 'cjs',
env: 'production',
banner
},
...
}
其他略过不看,这段可以看到我们的 vue.runtime.common.js 有一个dev版本的** vue.runtime.common.dev.js ** 和一个prod版本** vue.runtime.common.prod.js ** 。
OK,那我们就来看一下dev版本,可以看到他的entry是 'web/entry-runtime.js'
src\platforms\web\entry-runtime.js
/* @flow */
import Vue from './runtime/index'
export default Vue
很简单,就三行,那就进入 ./runtime/index 看看Vue的定义
src\platforms\web\runtime\index.js
import Vue from 'core/index'
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser } from 'core/util/index'
...
Vue.prototype.$mount = function ( //mount方法,执行render函数的入口
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating) //挂在组件的入口函数,内部创建watcher
}
...
export default Vue
又是一个从其他地方骗来的Vue - -0.继续往下找
src\core\index.js
import Vue from './instance/index'
...
发现没?他也白嫖!继续往下找!
src\core\instance\index.js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue) #混入初始化
stateMixin(Vue) // $data,$props,$watch混入
eventsMixin(Vue) // $on,$once,$off,$emit 等被安上原型链
lifecycleMixin(Vue) // $update,$forceUpdate,$destroy
renderMixin(Vue) // $nextTick,_render 上面提到的 mountComponent 所需的内在_render函数
export default Vue
OK,总算看见不白嫖Vue的了。Vue在此申明,他是一个function。之后对Vue的原型链进行了惨无人道的混入。
- 我们看到Vue内部执行了_init函数。此处相当于你代码里写的 new Vue({el:'#app'}) 之后立刻运行的代码。
- {el:'#app'} == _init(options) 中的options
- 接下来看_init,他是initMix赠与的Vue
src\core\instance\init.js
...
export function initMixin (Vue: Class<Component>) {
...
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
...
}
...
摘出重点段落,此处看到了 我们熟悉的生命周期 beforeCreate,created。原来他们是在此处被触发的!同时也有很多的初始化
当然,继续关注我们的目标defineProperty,进入initState函数
src\core\instance\state.js --- 最重要的文件了,大部分源码精髓都在此处
...
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
...
当opts.data为空时 observe 观察者对象 对data默认值空对象{} 进行管控。非空时,具体实现在同文件中的initData里
同文件
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
...
// observe data
observe(data, true /* asRootData */)
}
initData里最后也是使用observe 对data进行观察。
同文件
export function observe (value: any, asRootData: ?boolean): Observer | void {
...
ob = new Observer(value)
...
return ob
}
查看到了observe的实现,他实际就是Observer的对象。
同文件
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.dep = new Dep()
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])
}
}
}
此处,处处是精髓。首先,每一个ob都有一个dep。
- 若data为数组的话,则会对他的所有arrayMethods进行监听,arrayMethods包括 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' 。
- 若data是对象的话则执行walk方法。walk方法内部就是对data的第一层key进行了监听。
defineReactive 同文件
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
if (Dep.target) {
...
dep.depend()
...
}
return value
},
set: function reactiveSetter (newVal) {
...
dep.notify()
}
})
}
在此就找到了我们所期望的 defineProperty 对 传入data内 相应key值的setter及getter的劫持。
- 当我们的数据 data.a.b.c发生变化时,就会走setter,从而执行watcher的update方法得到数据的最新状态(dep.notify内实现)。我们平常用的vm.$watch('a.b.c',cb)和模板中通过指令和插值语法绑定的数据都是基于watcher。
- get中进行了依赖收集dep.depend()
- set中进行了更新通知dep.notify()
- 接下来看一下Dep如何实现依赖收集及
src\core\observer\dep.js
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 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()
}
}
}
Dep.target 是一个watcher类型。对于对象来说,依赖是在getter中收集,在setter中触发执行。依赖存储在哪呢?这里Dep类来管理依赖,对于响应数据对象的每一个key值,都有一个数组来存储依赖。
src\core\observer\watcher.js
export default class Watcher {
cb: Function;
deps: Array<Dep>;
constructor () {
this.cb = cb
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
}
/**
* Add a dependency to this directive.
*/
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)
}
}
}
/**
* Clean up for dependency collection.
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
}
/**
* 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)
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
this.cb.call(this.vm, value, oldValue)
}
}
watcher内部的addDep、update方法实际触发的就是dep.addSub和被收集的依赖事件们~
结尾
至此,看到Vue 对MVVM初始化的全过程,当然中间有的生命周期初始化、事件初始化都有提及,有兴趣的小伙伴们可以继续深入哦。