写在前面
(距离上一次写文章已经过去了四个月,羞愧...)这几个月对vue的使用不少,但是自觉地始终停留在比较粗浅的层面,一直无法提升,所以尝试着开始阅读源码。 文中内容仅代表个人理解,如果错误,欢迎指正。
Vue中一个显著特性是数据响应式系统:当数据被修改时,视图会相应更新。从而方便的完成状态管理。官方文档中对此进行了简要的描述,本文将结合vuejs的源码,做出进一步的解析。
基本概念
首先简单介绍一些在响应式系统中重要的概念。
data
vue实例中的数据项
observer
数据属性的观察者,监控对象的读写操作。
dep
(dependence的缩写),字面意思是“依赖”,扮演角色是消息订阅器,拥有收集订阅者、发布更新的功能。
watcher
消息订阅者,可以订阅dep,之后接受dep发布的更新并执行对应视图或者表达式的更新。
dep和watcher
dep
和watcher
的关系,可以理解为:dep
是报纸,watcher
是订阅了报纸的人,如果他们建立了订阅 的关系,那么每当报纸有更新的时候,就会通知对应的订阅者们。
view
暂且认为就是在浏览器中显示的dom(关于virtual dom的内容暂时不在本文讨论)
收集依赖
watcher在自身属性中添加dep的行为,后面会详细介绍
收集订阅者
dep在自身属性中添加watcher的行为,后面会详细介绍
流程简介
首先给出官方文档的流程图
在此基础上,我们根据源码更细一步划分出watcher和data之间的部分,即Dep
和observer
。
总的来说,vue的数据响应式实现主要分成2个部分:
- 把数据转化为getter和setter
- 建立watcher并收集依赖
第一部分是上图中data
、observer
、dep
之间联系的建立过程,第二部分是watcher
、dep
的关系建立
源码解析
本文中采用的源码是vuejs 2.5.0,Git地址
PS:简单的代码直接添加中文注释,所以在关键代码部分做<数字>
标记,在后文详细介绍
首先我们在源码中找到vue进行数据处理的方法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
)
}
// 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)) {
//<1>data属性代理
proxy(vm, `_data`, key)
}
}
// observe data
//对data调用observe
observe(data, true /* asRootData */)
}
这一段代码主要做2件事:
-
代码<1>
在while
循环内调用proxy
函数把data的属性代理到vue实例上。完成之后可以通过vm.key
直接访问data.key
。 -
之后对
data
调用了observe
方法,在这里说明一下,如果是在实例化之前添加的数据,因为被observe
过,所以会变成响应式数据,而在实例化之后使用vm.newKey = newVal
这样设置新属性,是不会自动响应的。解决方法是:- 如果你知道你会在晚些时候需要一个属性,但是一开始它为空或不存在,那么你仅需要设置一些初始值 - 使用`vm.$data`等一些api进行数据操作
接下来来看对应代码:
/* 源码目录 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
//检测当前数据是否被observe过,如果是则不必重复绑定
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
//<1>检测当前的数据是否是对象或者数组,如果是,则生成对应的Observer
observerState.shouldConvert &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
- 在本段代码中,
代码<1>
处,对传入的数据对象进行了判断,只对对象和数组类型生成Observer
实例,然后看Observer
这个类的代码,
Observer
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
// 生成了一个消息订阅器dep实例 关于dep的结构稍后详细介绍
this.dep = new Dep()
this.vmCount = 0
//def函数给当前数据添加不可枚举的__ob__属性,表示该数据已经被observe过
def(value, '__ob__', this)
//<1>对数组类型的数据 调用observeArray方法;对对象类型的数据,调用walk方法
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.
*/
/* 循环遍历数据对象的每个属性,调用defineReactive方法 只对Object类型数据有效 */
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
/**
* Observe a list of Array items.
*/
/* observe数组类型数据的每个值, */
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
/* defineReactive的核心思想改写数据的getter和setter */
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
//<2>生成一个dep实例,注意此处的dep和前文Observer类里直接添加的dep的区别
const dep = new Dep()
//检验该属性是否允许重新定义setter和getter
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
// 获取原有的 getter/setters
const getter = property && property.get
const setter = property && property.set
//<3>此处对val进行了observe
let childOb = !shallow && observe(val)
//<4>下面的代码利用Object.defineProperty函数把数据转化成getter和setter,并且在getter和setter时,进行了一些操作
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
// dep.depend()其实就是dep和watcher进行了互相绑定,而Dep.target表示需要绑定的那个watcher,任何时刻都最多只有一个,后面还会解释
dep.depend()
if (childOb) {
//<5>当前对象的子对象的依赖也要被收集
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
}
//<6>观察新的val并通知订阅者们属性有更新
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
- 在Observer类代码中,首先给当前数据添加了一个dep实例,存放于对象或者数组类型数据的
_![图片描述][2]ob_
属性上,然后把_ob_
挂在该数据上,它是该数据项被observe
的标志,我们可以在控制台看到这个属性,,例如:
//例子 1
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>vue demo</title>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<div id="app">
<div>obj:{{ obj}}</div>
</div>
</body>
<script>
app = new Vue({
el: '#app',
data: {
str: "a",
obj: {
key: "val"
}
}
});
console.log(app._data)
</script>
</html>
在控制台我们可以看到这样的数据
可以看到,首先,data对象上已经有_ob_
属性,这是被observe
的标志;其次,obj
和arr
属性上有_ob_
属性,而str
没有,这个进一步证明了前文提到的:observe
只对对象和数组有效
- 随后,对于数组和对象类型的数据做不同处理:对于数组类型
observe
里面的每个值,对于对象,我们执行walk()
方法,而walk()
就是对于当前数据对象的每个key,执行defineReactive()
方法,所以接下来重点来看defineReactive()
。 -
defineReactive()
中,在代码<2>
处生成了一个dep
实例,并在接下来的代码里,把这个dep
对象放在当前数据对象的key
(比如上面例子1中的str
)的getter
里,这个之前Observer
中的dep
是有区别的:-
Observer
中的dep
挂在Object
或者Array
类型的数据的dep
属性上,可以在控制台直接查看; - 此处添加的
dep
挂在属性的getter/setter上
,存在于函数闭包中,不可直接查看
-
为什么会有2种`Dep`呢?因为对于`Object`或者`Array`类型的数据,可能会有**添加
或者删除成员的操作而其他类型的值只有赋值操作,赋值操作可以在getter/setter上
中检测到。**,
- 接下来
代码<3>
处的是为了处理嵌套的数据对象,比如例子1中,data
是最顶层的Object
,obj
就是data
下的Object
,而obj
里面也可以再继续嵌套数据,有了此处的代码之后,就可以对嵌套的每一层都做observe
处理。 -
代码<4>
处是defineReactive()
的核心:利用Object.defineProperty()
(这个函数建议了解一下mdn地址)
在当前属性的getter和setter中插入操作:
- 在当前数据被get时,当前的
watcher
(也就是Dap.target
)和dep
之间的绑定,这里有个注意点是在代码<5>
处,如果当前数据对象存在子对象,那么子对象的dep
也要和当前watcher
进行绑定,以此类推。 - 在setter时,我们重新观测当前
val
,然后通过dep.notify()
来通知当前dep所绑定的订阅者们数据有更新。
Dep
接下来介绍一下dep
。源码如下:
/* 源码目录 src/core/observer/dep.js */
let uid = 0
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
//添加一个watcher
addSub (sub: Watcher) {
this.subs.push(sub)
}
//移除一个watcher
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
//让当前watcher收集依赖 同时Dep.target.addDep也会触发当前dep收集watcher
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
//通知watcher们对应的数据有更新
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
这个类相对简单很多,只有2个属性:第一个是id
,在每个vue实例中都从0开始计数;另一个是subs
数组,用于存放wacther
,根绝前文我们知道,一个数据对应一个Dep
,所以subs
里存放的也就是依赖该数据需要绑定的wacther
。
这里有个Dep.target
属性是全局共享的,表示当前在收集依赖的那个Watcher,在每个时刻最多只会有一个。
watcher
接下里看watcher的源码,比较长,但是我们只关注其中的几个属性和方法:
/* 源码目录 src/core/observer/watcher.js */
/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*/
/* watcher用来解析表达式,收集依赖,并且当表达式的值改变时触发回调函数
用在$watch() api 和指令中
*/
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: ISet;
newDepIds: ISet;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) {
this.vm = vm
vm._watchers.push(this)
// options
//这里暂时不用关注
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} 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
//deps和newDeps表示现有的依赖和新一轮收集的依赖
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
//<1>解析getter的表达式
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
//<2>获取实际对象的值
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
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.lazy为true是计算属性的watcher,另外处理,其他情况调用get
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()
//<3>清除先前的依赖
this.cleanupDeps()
}
return value
}
/**
* 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)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
/**
* 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 () {
if (this.active) {
//<14>重新收集依赖
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)
}
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
/**
* Remove self from all dependencies' subscriber list.
*/
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
首先看官方文档的英文注释可知,watcher用于watcher用来解析表达式,收集依赖,并且当表达式的值改变时触发回调函数,用在$watch()
api 和指令之中。
watcher函数主要内容是:
- 初始化属性的值,其中和本文相关的主要是
deps
、newDeps
、depIds
、newDepIds
,分别表示现有依赖和新一轮收集的依赖,这里的依赖就是前文介绍的数据对应的dep
。 - 设置getter属性。
<1>
判断传入的表达式类型:可能是函数,也可能是表达式。如果是函数,那么直接设置成getter,如果是表达式,由于代码<2>
处的expOrFn
只是字符串,比如例子1中的obj.key
,在这里仅仅是一个字符串,所以要用parsePath
获取到实际的值 - 执行get()方法,在这里主要做收集依赖,并且获取数据的值,之后要调用
代码<3>
`cleanupDeps`清除旧的依赖。这是必须要做的,因为数据更新之后可能有新的数据属性添加进来,前一轮的依赖中没有包含这个新数据,所以要重新收集。 - update方法主要内容是里面的触发更新之后会触发run方法(虽然这里分了三种情况,但是最终都是触发run方法),而run方法调用
get()
首先重新收集依赖,然后使用this.cb.call
更新模板或者表达式的值。
总结
在最后,我们再总结一下这个流程:首先数据从初始化data开始,使用observe
监控数据:给每个数据属性添加dep
,并且在它的getter过程添加收集依赖操作,在setter过程添加通知更新的操作;在解析指令或者给vue实例设置watch
选项或者调用$watch
时,生成对应的watcher
并收集依赖。之后,如果数据触发更新,会通知watcher
,wacther
在重新收集依赖之后,触发模板视图更新。这就完成了数据响应式的流程。
本文的流程图根据源码的过程画出,而在官方文档的流程图中,没有单独列出dep
和obvserver
,因为这个流程最核心的思路就是将data的属性转化成getter
和setter
然后和watcher
绑定。
然后依然是惯例:如果这篇文章对你有帮助,希望可以收藏和推荐,以上内容属于个人见解,如果有不同意见,欢迎指出和探讨。请尊重作者的版权,转载请注明出处,如作商用,请与作者联系,感谢!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。