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

src/core/index.js
。
看到一大推第一次见并且不熟的代码,谁都会感动头疼。所以我看源码的基本方针是
- 不清楚应用方法的具体实现,先靠他的命名猜一下(所以英文好很关键,哭)。
- 如果有一大堆
if..else-if..else
,先找到按正常流程走的代码,其他分支先放一放...- 不用钻牛角尖,看的懂的代码就好好理解,看不懂的了解个大概足已!看源码的目的是更好的理解框架的实现原理,并不是要把整个框架吃透(
关键也吃不透啊,vue源代码那么多,咱也不是啥大神,难道看不懂去问尤雨溪吗,咱也不敢问讷)
// 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
里写了啥。
// src/core/instance/index
import { initMixin } from './init'
...
initMixin(Vue)
...
export default Vue
其他初始化函数我们先不看,从initMixin
这个名字和第一个引入的骄傲位置来说,他应该和我们要找的data
属性有一腿。所以我们打开./init
看一下。
// src/core/instance/init
import { initState } from './state'
...
initState(vm)
...
从命名上来讲,state
应该是与data
联系更多的,也许是因为在react里,初始化数据就叫作state
吧,所以我们打开./state
找到initState
方法
// 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
的方法定义。
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
的类
// 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
方法来定义响应式数据。
// 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()
}
})
}
省略了部分代码后,我们注意到在get
和set
里分别执行了dep.depend()
和dep.notify()
,而Dep
就是我们常说的订阅发布管理中心,这时候我们来看一张,vue实现数据双向绑定的示例图。

**1.**从
init Data
说起,比如我们在vue实例中定义了初始化的data
属性,接着会触发new Observer()
,data
里所有的数据都会通过上面介绍的那样,通过defineReactive
这个方法为每一个属性挂载Object.defineProperty
(也可以说在get
里为每一个属性都添加了一个订阅,在set
里做一个通知订阅者的操作),如果触发了setter
,也就是在业务代码里改变了data
里的值,会通知Watcher
,Wathcer
更新指令系统对应绑定的data
值 **2.**从编译侧说起,Dom 上通过指令或者双大括号绑定的数据,经过编译以后,会为数据进行添加观察者Watcher
,当实例化Watcher
的时候 会触发属性的getter
方法,此时会调用dep.depend()
,并且会将Watcher
的依赖收集起来。
那么我们可以看一下dep.depend()
和dep.depend()
// 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
的文件下找到的。
// 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)
}
}
一系列操作的主要作用就是让Dep
与Wathcer
建立双向的联系。
代码真是太多了,解释不完,感觉要烂尾了
最后vue有一个很关键的指令解析系统,在src/compiler/directives
文件中可以找到v-bind
,v-on
,v-model
相应的源码。能力有限,看不下去了。越挖越深。
说的我自己都乱了
言简意赅的总结一下,Observer
就是对data
里到所有值进行一个数据劫持,强行给每个数据注入set
(能监听到数据改变,没有return
)与get
(该数据具体呈现出来的值,能return
出数据)方法,Observer
操作完以后,data
可以理解成房子资源。然后Dep
是个订阅器(订阅管理中心,可以理解成房地产中介),Watcher
是订阅者(有钱买房的人),Watcher
把需求和联系方式通过dep.depend()
告诉中介dep
,dep
中介找到了合适的房子通过dep.notify()
打电话通知我们忽悠买房。那Wathcer
没有钱之前就是被绑定在dom上的一些数据,通过了v-model
,v-test
,双大括号等途径赚到了钱(也就是vue的compile编译系统),升级成了一个Wathcer
,赚钱和买房总是无穷无尽的,dom发生了更新(比如input事件),赚到钱了就去问中介dep
有没有房,同时如果房源发生了变化(data发生了更新),中介dep
会通知Wathcer
买房不?
