vuejs的核心原理就是响应式。在了解vue.js的响应式之前,我们需要先认识一下vue实现响应式的基本方法Object.defineProperty()
Object.defineProperty
/**
obj: 对象
prop: 对象的属性名
descriptor: 描述符,是一个对象,包括一些属性
*/
Object.defineProperty(obj, prop, descriptor)
其中,descriptor对象中我们用到的属性包括如下,了解更多请参考文档
get
方法:当读取obj上的prop时调用该方法set
方法:当修改或设置obj上的prop时调用该方法- configurable,属性是否可以被修改或者删除,默认 false。
- enumerable,属性是否可枚举,默认 false。
Observer
我们看如下代码,这就是实现响应式的关键之处
//
// defineReactive方法,省略部分处理逻辑
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 创建一个dep
// get时用来进行依赖收集
// set时用来通知watcher更新dom
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) {
// 修改value的值
val = newVal
// 通知watcher更新
dep.notify()
}
})
}
从上面代码可以看到每执行一次defineReactive
方法,都会存在一个dep
,即每存在一个data,都会有一个dep与之对应,这句话需要牢记,这对理解下面的依赖收集和视图更新过程至关重要。
当然,光有这个是不够的,我们从之前的 内部运行机制总览 中知道,在new Vue()
后,我们会执行一个init()
方法,在init()
方法内部我们会执行observe()
// src/core/observer/index.js
// observe方法 为方便说明,省略了部分源码
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 不是对象或是VNode则退出
const ob = new Observer(value)
return ob
}
// src/core/observer/index.js
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()
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)
}
}
// 如果value是对象,执行walk方法,对每一个key进行defineReactive
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
// 如果是数组,则执行observeArray,对每一项进行observe
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
这样通过以上代码,然后配合defineReactive
为每一项data
设置get
和set
。当我们获取vue中data的时候,会执行get,并进行依赖收集;修改data的时候,会执行set,并执行update方法来更新视图。
ok,我们现在具体来分析,它是怎么依赖收集的呢
依赖收集阶段
那么,我们现在有个问题就是,为什么要进行依赖收集?
举个例子来说明
new Vue({
template:
`<div>
<span>{{text1}}</span>
<span>{{text2}}</span>
<div>`,
data: {
text1: 'text1',
text2: 'text2',
text3: 'text3'
}
});
比如这时候我们执行了如下代码
this.text3 = newText3
显然,当我们修改这个text3
的时候,会触发set
方法,并更新视图。但是,在这种情况下,由于我们在视图中并未用到text3
这个变量,那么我们就不需要执行视图更新的方法。那么,这时候我们就需要在执行get的时候来确定哪些数据应该执行更新,哪些数据不应该执行更新,这个过程就叫依赖收集。
再举个例子
let p = new Vue({
template:
`<div>
<o1 :text1={text1}></o1>
<o2 :text1={text1}></o2>
<div>`,
data: {
text1: 'text1'
};
});
let o1 = new Vue({
template:
`<div>
<span>{{text1}}</span>
<div>`,
props: ['text1']
});
let o2 = new Vue({
template:
`<div>
<span>{{text1}}</span>
<div>`,
props: ['text1']
});
我们假设在p父组件内部有两个子组件 o1 , o2
都用到了父组件p
的数据text1
,这时候我们在p
组件内部执行以下代码
this.text1 = 'newText1'
这个时候我们会通知o1 , o2
执行视图更新。那么我们依赖收集的作用就是让text1知道,当自己变化的时候,需要通知依赖自己的组件更新视图。最后形成如下图的一种关系
简单来理解,data表示数据, Dep表示存放子组件(watcher对象)的盒子
,watcher表示依赖data的子组件
(或者说存放子组件更新方法的对象)。
ok,了解了这些,我们来看看vue具体怎么实现Dep,和Watcher的吧。
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)
}
// 依赖收集,当存在Dep.target的时候添加观察者对象
// 这里的Dep.target其实是Watcher实例
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知所有观察者,执行update
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
这里重点介绍几个方法:
depend()
: 这是依赖收集的入口,这里的Dep.target
指的是watcher
对象,addDep()
方法就是把当前的watcher
添加到对应的dep
对象的subs
中。形成如下图的关系
addSub()
: 向subs
中添加watcher
removeSub()
: 移除watcher
notify()
: 通知视图更新的方法
Watcher
我们再来看watcher的实现
// 省略部分源码
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// new Watcher时把this赋值给Dep.target,get时使用
Dep.target = this;
}
// 添加依赖,将当前watcher添加到dep的subs中去
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)
}
}
}
// 当依赖发生改变的时候进行回调。
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
// 执行watcher回调
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) //
}
}
}
}
}
addDep
: 就是调用了dep的addSub方法将,watcher和dep关联。update
: 当依赖发生改变的时候进行回调,更新视图。
这里简要说一下queueWatcher
这个方法,在vue
中,当数据更新时,我们会维护一个quene
队列,这个队列里存放着变化的数据所依赖的所有watcher
,这个quene
队列会放在nextTick
里执行每一个watcher
上的run
方法。这个run
方法会重新执行render()
方法并进行diff dom
。
queueWatcher方法简介
总结
首先在 observer
的过程中会注册 get
方法,该方法用来进行依赖收集
。在它的闭包中会有一个 Dep
对象,这个对象用来存放 Watcher
对象的实例。其实依赖收集
的过程就是把 Watcher
实例存放到对应的 Dep
对象中去。get
方法可以让当前的 Watcher
对象(Dep.target)存放到它的 subs
中(addSub)方法,在数据变化时,set
会调用 Dep 对象的 notify
方法通知它内部所有的 Watcher
对象进行视图更新。