前言
本章,我们将深入 Vue 源码,初步探索其响应式实现原理。如果本篇文章对您有所帮助,请不要吝啬您的点赞和关注。
初始化过程
我们使用 Vue 时都会写一句
new Vue({ ... })
进行初始化,那么现在我们的源码探索之旅也以此为起点去查找 Vue 内部对data
到底做了些什么?
我们打开项目的依赖目录
node_modules
,找到vue
包,定位其下面的vue/src/core/index
文件:
import Vue from './instance/index'
import {
initGlobalAPI } from './global-api/index'
import {
isServerRendering } from 'core/util/env'
import {
FunctionalRenderContext } from 'core/vdom/create-functional-component'
initGlobalAPI(Vue)
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
Object.defineProperty(Vue.prototype, '$ssrContext', {
get () {
/* istanbul ignore next */
return this.$vnode && this.$vnode.ssrContext
}
})
// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
value: FunctionalRenderContext
})
Vue.version = '__VERSION__'
export default Vue
vue/src/core/index
的源码比较简单。根据其变量命名判断,它初始化了 Vue 的一些全局API、挂载了一些和服务器渲染相关的属性以及暴露给我们Vue
进行使用。其中Vue
来自于同目录的./instance/index
,我们顺着去看./instance/index
的源码:
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)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
根据这里的源码,我们知道了在
new Vue()
时,内部会执行this._init()
,而这个_init
方法实际上是在initMixin(Vue)
内定义的,我们现在继续前往./init
文件去找initMixin
方法(因为源码过长,我们后面只展示关键代码):
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
......
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
这个钩子是访问不到我们的data
的,因为只有当执行到initState(vm)
时才会对内部使用到的一些状态,如props
、data
、computed
、watch
、methods
进行初始化。那么我们现在目标显然就是去看看initState(vm)
写了啥,initState
来自于同目录的./state
文件:
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)
}
}
就如上面所说,这里面进行各种状态的初始化,现在我们的目光停留在了
initData(vm)
,看看你对我们的data
做了什么:
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {
}
......
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${
key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${
key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
因为在
initState
方法中,props
和methods
是在data
之前初始化的,所以这里在对循环判断data里的属性有没有重复命名的情况。检测通过后,这里执行了proxy(vm, '_data', key)
将我们data里的属性直接挂载到当前实例上,以下为proxy
方法源码:
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)
}
这里使用了
Object.defineProperty
把这些data
的属性全部转为 getter/setter,这也是为啥我们在data
中定义的属性能直接以this.xxx
的形式使用。
回到
initDate
方法,里面实现响应式最重要的一行代码,便是执行observe(data, true /* asRootData */)
,现在我们终于来到响应式原理的门口。
数据劫持
紧接着,我们定位到
observe
的代码:
export function observe (value: any, asRootData: ?boolean): Observer | void {
......
ob = new Observer(value)
......
return ob
}
看上去
observe
本质上是Observer
类的工厂方法,所以我们继续去看Observer
类的实现:
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
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 through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
可以看到,在
Observer
构造函数内部,进行了数组的判断,如果是数组的话,又要进行hasProto
判断,分别执行protoAugment
和copyAugment
,其实就是把带有副作用的7个数组方法给添加上去,也就是实现之前我们讲到的 Vue 中的数组更新检测。在这里protoAugment
和copyAugment
的调用都传入了arrayMethods
这个参数,那就先来分析这个参数是什么,ctrl+左键点击arrayMethods
,来到src\core\observer\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'
]
/**
* Intercept mutating methods and emit events
*/
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
})
})
可以看到这里通过
def
(实际通过Object.defineProperty)给这7个数组方法设置副作用,针对push、unshift、splice
会增加数组元素的方法,额外的对新增的元素即inserted
不为空时,调用observeArray
循环的进行observe
,之后ob.dep.notify()
通知更新,也就代表着这7个方法执行后,都会通知更新,也就成了响应式。
分析完数组的情况,现在回到
Observer
类,如果是对象则执行walk
,遍历对象所有key值,通过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 () {
......
},
set: function reactiveSetter (newVal) {
......
}
})
}
首先,实例化一个Dep对象,这个
dep
其实是一个闭包引用,在下面的Object.defineProperty
中我们再讲。之前调用defineReactive
的时候shallow
参数是没有传的,则let childOb = !shallow && observe(val)
会对该对象进行深层的监测。defineReactive
内部使用了Object.defineProperty
对所有的key进行设置了getter和setter,就是在此时完成了数据劫持,并添加相应的副作用。现在我们来看getter的关键代码:
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) {
......
childOb = !shallow && observe(newVal)
dep.notify()
}
在getter中,闭包引用的
dep
实际就在做依赖收集的事情,如果存在子对象也进行子对象的依赖收集,是数组就循环收集。而在setter中,对新值同样进行observe
进行监测,然后通过dep.notify()
通知更新完成响应式。
依赖收集
上面说到,在getter中使用
dep.depend()
进行依赖收集,在setter中使用dep.notify()
进行更新通知。那现在我们就需要前往Dep
类的实现,看它如何进行依赖收集和派发更新的。Dep
引用自同目录的./dep
文件,点进行查看其源码:
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 () {
.......
}
}
......
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]
}
这里定义了一个
subs
,看上去是通过addSub、removeSub
进行Watcher
实例的添加删除。dep.depend
方法判断Dep.target
然后执行Dep.target.addDep
。这里的Dep.target
也是个Watcher
实例,现在需要追踪其addDep
方法,看一看Watcher
又是个啥东西:
......
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,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = 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 // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
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()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
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
}
addDep (dep: Dep) {
......
dep.addSub(this)
......
}
......
}
好家伙,面对这密密麻麻的一片代码,我们还是只去关注关键语句。其中
addDep
方法实际上还是通过调用Dep
的addSub
将当前Watcher
实例记录到了Dep
的subs
中。值得注意的是,在get
方法中调用了来自./dep
文件中暴露的pushTarget
方法,往上翻一屏,就能看到pushTarget
中的一句Dep.target = target
。现在,一切都串起来了。还记得在defineReactive
内部使用了Object.defineProperty
对所有的key进行设置了getter的时候,先对Dep.target
进行了判断,即Dep
是否添加了Watcher
实例,现在知道了是这里的get
进行的操作,get
中的value = this.getter.call(vm, vm)
似乎是在记录监测的值。而get
是在Watcher
实例化的时候调用的,this.getter
也是来自实例化的expOrFn
参数。那么,什么时候进行的Watcher
的实例化,是完成整个依赖收集的闭环的关键。
经过查找
new Watcher
的调用位置,发现在src\core\instance\state.js
和src\core\instance\lifecycle.js
代码中进行了Watcher
实例化,而src\core\instance\state.js
中Watcher
的实例化跟计算属性和监听属性有关。现在只需聚焦到src\core\instance\lifecycle.js
的代码,因为这里的Watcher
实例化是跟渲染有关:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
......
callHook(vm, 'beforeMount')
let updateComponent
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
......
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
......
}
这里的
mountComponent
实际上会被vm.$mount
进行调用,也就是在模板挂载时会去进行实例化Watcher
,其第二个参数接收了updateComponent
作为Watcher
的expOrFn
,也就是说Watcher
里的this.getter
实际上是执行的vm._update(vm._render(), hydrating)
。这里就涉及 Vue 中的render
函数了,这里先不做展开,知道这行代码是将虚拟DOM转为真实DOM就行。这个时候如果render
函数内有使用到data
中已经转为了响应式的数据,就会触发get
方法进行依赖的收集,闭环了之前依赖收集的逻辑。
这里给出一个简要的示意流程:
_init
->initState
->initData
->observe
->new Observer
->walk
->defineReactive
->Object.defineProperty定义getter和setter
_init
->vm.$mount
->mountComponent
->new Watcher
->this.getter
->_render
->触发Object.defineProperty定义的getter
->dep.addSub(sub: Watcher)
更新通知
当我们修改定义在
data
中的属性时,该属性的setter
触发,执行dep.notify()
,现在我们看看它的实现:
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
中的subs
是Watcher
数组,那么现在实际就在遍历执行每个Watcher
的update
方法:
......
// 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
}
.....
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
这里实际会去执行
queueWatcher
,因为lazy
和sync
都没有传值进来,如果你有印象,会记得在mountComponent里Watcher
实例化时第四个参数只定义了before
。我们继续去找queueWatcher
的实现,它在同目录的./scheduler.js
文件:
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)
}
}
}
这里看上去先是进行了
Watcher
的去重操作,最终都会去执行flushSchedulerQueue
,只是执行nextTick(flushSchedulerQueue)
再下一次tick
时更新它,还是立即更新的区别:
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
......
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
......
}
......
}
然后就是遍历这个队列,执行传入的
before
方法触发beforeUpdate
钩子。最后执行watcher.run()
方法,执行真正的更新通知:
run () {
if (this.active) {
const value = this.get()
......
}
}
其实就是重新执行一次
this.get()
方法,让vm._update(vm._render())
再走一遍而已。然后生成新旧VNode
,最后进行比对以更新视图。到这整个响应式实现的流程就算是结束了,下面依旧给出简要的示意流程:
数据更改触发Object.defineProperty定义的setter
->dep.notify
->遍历执行watcher.update
->queueWatcher
->flushSchedulerQueue
->watcher.run
->this.getter
->_render
内容预告
本章我们深入 Vue 源码,初步探索其响应式实现原理。在下一章节,我们将对需要进行
http
网络通信的场景,介绍和演示axios
的使用。