vue 是数据驱动的,意思是数据改变会驱动视图更新,要实现这个首先要对数据进行监测,下面看看 Vue 初始化时是如何监测数据的
初始化
vue 在创建实例时,会调用 _init 进行初始化,在 _init 中,会调用 initState 对数据进行初始化
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
// ... 略
initState(vm)
}
}
initState
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); // 初始化 data
} else {
observe((vm._data = {}), true /* asRootData */);
}
if (opts.computed) initComputed(vm, opts.computed);
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
可以看到该方法中对 props methods data computed watch 进行了初始化,由于数据驱动主要是针对 data 中的数据,所以下面看一下 initData
initData
initData 定义在 src/core/instance/state.js
function initData(vm: Component) {
let data = vm.$options.data;
data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};
if (!isPlainObject(data)) {
data = {};
process.env.NODE_ENV !== "production" &&
warn(
"data functions should return an object:\n" +
"https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function",
vm
);
}
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 */);
}
平时使用 Vue 时,data 一般会定义为方法(为了 data 的指针不同),所以 initData 开头有一个判断,如果 data 是函数,则调用一下获取到 data。
如果 data 不是一个对象,则初始化为对象。
然后进行了重名判断,如果与 methods props 中的属性重名了,则给出警告
最后 initData 主要做了如下两件事
1. proxy - 代理属性
什么是代理属性,举例来说,假设在 data 中定义了一个 msg 属性,在引用时,为什么可以直接使用 this.msg 而不需 this.data.msg 呢?原因就是 initData 时,Vue 将 data 的属性代理到了 this 上,其他 props computed methods 等等也是这样处理过的,所以也可以直接通过 this 访问
{
data () {
return {
msg: 'hello'
}
},
methods: {
say() {
console.log(this.msg)
}
}
}
这个实现很简单,可以看到 initData
中一行关键代码
proxy(vm, `_data`, key);
proxy 其实是通过 Object.defineProperty
拦截 this 的 getter读取 和 setter设置 操作,在读取时,读取 this._data
上的属性,设置时,设置 this._data
上的属性
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
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)
}
2. observe - 监测数据
可以看到 initData 最后执行了
observe(data, true /* asRootData */);
observe 定义在 src/core/observer/index.js
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
}
如果数据是被监测过的,那么会有一个 __ob__
属性,这时直接赋值给 ob 即可,如果未监测过则实例化一个 Observer 类,并且把 data 通过参数传进去
Observer 类定义在 src/core/observer/index.js
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through each property 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 给数据加了一个 __ob__
属性,指向自身,这里就和上面 observe 中的判断对应上了
后面对于数组则调用 observeArray
监测数组的方法,其实是遍历数组,对每个 item 进行监测
对于对象,则直接调用 walk 方法,可以看到 walk 方法中遍历对象调用了 defineReactive
defineReactive 定义在 src/core/observer/index.js
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
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
可以看到 defineReactive 开头实例化了 Dep 观察者系统,由此可知 data 中每个属性都会实例化一个 Dep,这个是用于给当前属性收集当前 watcher 的,watcher 的作用是初始化视图,并且它包含一个更新视图的 update 方法,这里把 watcher 收集起来,等到数据更新时,调用这个 update 方法即可实现驱动视图更新
后面又对属性调用了 observe 方法,这样相当于递归把 data 中的对象属性也进行了监测
最后关键代码来了,defineReactive 中利用 Object.defineProperty 拦截了属性的 读取getter 和 设置setter 操作,读取时订阅 watcher,设置时通过 watcher 更新视图
在 getter 中收集当前 watcher 这里的关键代码是 dep.depend()
,这一行代码就是使用该属性的观察者系统订阅当前 watcher ,这里判断了 Dep.target ,Dep.target 是当前 watcher ,关于这个值是什么时候赋上的,请看下面数据驱动流程分析,这里只需先了解 getter 订阅当前 watcher,setter 更新视图
在 setter 中更新视图,这里的关键代码是 dep.notify()
这一行代码用于通知观察者队列中的 watcher 进行视图更新
数据驱动流程分析
上面说到,defineReactive 拦截了属性的 getter 和 setter,读取属性时订阅 watcher,设置属性时通过 watcher 更新视图,我们设置属性时就会触发 setter ,从而更新视图,那 getter 是什么时候触发的呢? getter 中判断的 Dep.target 又是什么时候设置的呢? 了解了 vue 初始化的整体流程就能明白这个问题。
可以先看一下 vue源码之初始化 - new Vue() 之后发生了什么
<div id="app">
{{msg}}
<button @click="changeMsg">改变message</button>
</div>
var app = new Vue({
el: '#app',
data: {
msg: 'Hello Vue!'
},
methods: {
changeMsg () {
this.msg = 'Hello word'
}
}
})
以上面代码为例,new Vue 时,Vue 首先会进行一系列初始化,然后通过 mountComponent
函数进行挂载,在挂载时,会组装 updateComponent
函数, 这个函数会传入 watcher 中,提供给 watcher 进行视图更新,它通过 _render
生成 vnode 然后通过 _update
渲染 dom
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
Watcher 类的关键代码如下
// core/observer/watcher.js
export default class Watcher {
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.value = this.lazy
? undefined
: this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
}
}
}
// dep.js
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
可以看到在实例化 watcher 时,会调用 get 方法给 value 赋值,在 get 方法中会调用 pushTarget
, 在 pushTarget
中就把当前 watcher 赋值到了 Dep.target 中
接着又调用了 value = this.getter.call(vm, vm)
,这里的 getter 实际上是 updateComponent
方法,所以这里实际上是调用 updateComponent
初始化视图
在初始化视图生成 Vnode 时,必定会读取到 msg 这个值,于是就触发了 msg 这个值的 getter, 在 msg 的 getter 中初始化观察者系统 Dep,然后将刚刚设置的 Dep.target 推进观察者系统中。
接着点击按钮,调用 changeMsg 方法,这个方法会设置 msg ,这时就进入到了 msg 的 setter 中,setter 通过
dep.notify()
通知 watcher 调用 updateComponent 方法进行视图更新
如图所示:render 触发 getter,收集 watcher, 设置时触发 setter 通知 watcher 更新视图