看懂vue源码-理解数据双向绑定的实现

如果你是一名前端从业者,并且在简历上写了会使用vue框架,那么在拿着这份简历去面试的时候,面试官有很大的概率会问你vue的数据双向绑定是如何实现的。
打开goole,输入vue双向绑定,有非常多优秀的博主已经对vue数据双向绑定作了一个全方位的刨析,阅读之后,你会大概了解,双向绑定涉及到javascript的核心api是Object.defineProperty,通过setget这俩个存取描述符来监听数据的实时改变,并且在对模版作出相应改变。
那么为了更加了解vue是如何实现数据双向绑定的,我花了一下午的时间阅读vue的源码,并将我的对vue实现数据双向绑定的方式理解记录了下来。

打开vue源码目录

vue源码目录
这几个文件夹都是分别负责什么的,我们暂且不管(其实是我不知道),我们找到入口文件src/core/index.js
看到一大推第一次见并且不熟的代码,谁都会感动头疼。所以我看源码的基本方针是

  • 不清楚应用方法的具体实现,先靠他的命名猜一下(所以英文好很关键,哭)。
  • 如果有一大堆if..else-if..else,先找到按正常流程走的代码,其他分支先放一放…
  • 不用钻牛角尖,看的懂的代码就好好理解,看不懂的了解个大概足已!看源码的目的是更好的理解框架的实现原理,并不是要把整个框架吃透(关键也吃不透啊,vue源代码那么多,咱也不是啥大神,难道看不懂去问尤雨溪吗,咱也不敢问讷)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// src/core/index.js
import Vue from './instance/index' //从Vue这个关键词来看,这个应该是vue的核型方法
import { initGlobalAPI } from './global-api/index' // 初始化全局API?
import { isServerRendering } from 'core/util/env' // 判断是不是ssr?
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
// 调用方法咯,初始化全局变量
initGlobalAPI(Vue)
// 给vue原型添加$isServer属性 --当前 Vue 实例是否运行于服务器。
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
// 给vue原型添加$ssrContext 不认识这玩意
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

我就是以上面这种方式来一点点看源码的。根据上面得到的提示,我们应该去看看./instance/index里写了啥。

1
2
3
4
5
6
// src/core/instance/index
import { initMixin } from './init'
...
initMixin(Vue)
...
export default Vue

其他初始化函数我们先不看,从initMixin这个名字和第一个引入的骄傲位置来说,他应该和我们要找的data属性有一腿。所以我们打开./init看一下。

1
2
3
4
5
// src/core/instance/init
import { initState } from './state'
...
initState(vm)
...

从命名上来讲,state应该是与data联系更多的,也许是因为在react里,初始化数据就叫作state吧,所以我们打开./state找到initState方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/core/instance/state
export function initState (vm: Component) {
vm._watchers = [] // 看起来像清空一个观察者队列
const opts = vm.$options
if (opts.props) initProps(vm, opts.props) // 初始化props参数
if (opts.methods) initMethods(vm, opts.methods) // 初始化methods参数
if (opts.data) {
initData(vm) // 如果有data参数,初始化data参数
} else {
observe(vm._data = {}, true /* asRootData */) // 如果没有,触发observe方法(这个方法很关键!),给一个{}作为默认值并且作为rootdata
}
if (opts.computed) initComputed(vm, opts.computed) // 初始化computed参数
if (opts.watch && opts.watch !== nativeWatch) {
// watch存在并且 这个watch不是Firefox(火狐浏览器)在Object.prototype上有一个“监视”功能,初始化
initWatch(vm, opts.watch)
}
}

从上面的代码中,我们看到很多脸熟的代码了,并且终于找到我们想找的data属性,顺水推舟继续往下走吧,找到initData的方法定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// 判断data是不是个函数,如果时执行getData(往一个targetStack push进去?)
if (!isPlainObject(data)) {
// 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--) {
// 判断data里定义的key是否与methods和props的冲突
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 */)
}

到这里,我们已经很接近实现数据双向绑定的函数了,那就observe,接下来去../observer/index里看看,observe函数到底写了些什么东西。
export function observe()的函数里,return出来的是一个名为Observer的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 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)
}
}

/**
* 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])
}
}
}

当我们调用new Oberver(value)的时候,会执行this.walk(value)这个方法,看方法里的作用应该是,遍历value,执行defineReactive方法,而在defineReactive方法里主要就是通过Object.defineProperty方法来定义响应式数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/core/observer/index.js
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
...
Object.defineProperty(obj, key, {
...
get: function reactiveGetter () {
...
dep.depend()
...
return value
},
set: function reactiveSetter (newVal) {
...
dep.notify()
}
})
}

省略了部分代码后,我们注意到在getset里分别执行了dep.depend()dep.notify(),而Dep就是我们常说的订阅发布管理中心,这时候我们来看一张,vue实现数据双向绑定的示例图。
数据双向绑定的示例图
大概解释一下上图,上图实现的设计模式为 订阅-发布 模式。可以从俩个入口分别说起

1.init Data说起,比如我们在vue实例中定义了初始化的data属性,接着会触发new Observer(),data里所有的数据都会通过上面介绍的那样,通过defineReactive这个方法为每一个属性挂载Object.defineProperty(也可以说在get里为每一个属性都添加了一个订阅,在set里做一个通知订阅者的操作),如果触发了setter,也就是在业务代码里改变了data里的值,会通知WatcherWathcer更新指令系统对应绑定的data
2.从编译侧说起,Dom 上通过指令或者双大括号绑定的数据,经过编译以后,会为数据进行添加观察者Watcher,当实例化Watcher的时候 会触发属性的getter方法,此时会调用dep.depend(),并且会将Watcher的依赖收集起来。

那么我们可以看一下dep.depend()dep.depend()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/core/observer/dep.js
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里的一般都是Watcher类,像Dep.target.addDep(this)subs[i].update()这俩个方法是可以在定义Watcher的文件下找到的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/core/observer/watcher.js
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)
}
}

一系列操作的主要作用就是让DepWathcer建立双向的联系。

代码真是太多了,解释不完,感觉要烂尾了

最后vue有一个很关键的指令解析系统,在src/compiler/directives文件中可以找到v-bind,v-on,v-model相应的源码。能力有限,看不下去了。越挖越深。

说的我自己都乱了

言简意赅的总结一下,Observer就是对data里到所有值进行一个数据劫持,强行给每个数据注入set(能监听到数据改变,没有return)与get(该数据具体呈现出来的值,能return出数据)方法,Observer操作完以后,data可以理解成房子资源。然后Dep是个订阅器(订阅管理中心,可以理解成房地产中介),Watcher是订阅者(有钱买房的人),Watcher把需求和联系方式通过dep.depend()告诉中介depdep中介找到了合适的房子通过dep.notify()打电话通知我们忽悠买房。那Wathcer没有钱之前就是被绑定在dom上的一些数据,通过了v-model,v-test,双大括号等途径赚到了钱(也就是vue的compile编译系统),升级成了一个Wathcer,赚钱和买房总是无穷无尽的,dom发生了更新(比如input事件),赚到钱了就去问中介dep有没有房,同时如果房源发生了变化(data发生了更新),中介dep会通知Wathcer买房不?

最后祝大家早日能买到房。

自闭
我想吃鸡腿!