写在前面
有句话说的好,知其然知其所以然。从代码底层理解机制能使我们更好的理解框架,以下是我对Vue中响应式系统的拙见。既为整合知识,又为以后学习有个参考。
什么叫做双向绑定?
v-model
指令在表单 <input>
、<textarea>
及 <select>
元素上创建双向数据绑定。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。->官方文档中的详细描述
简单来说,整个过程通过视图改变数据,数据更新视图。
视图更新数据不难实现,那么如何实现数据更新视图呢? Vue的响应式系统解决这一问题。
当一个Vue实例被创建时,它向Vue的响应式系统中加入其data对象中能找到的所有的属性。当数据发生改变时,视图就会产生“响应”,即匹配更新为新的值。
源码解析
本文Vue版本为2.6.11,Vue3.0利用了proxy
来替换Obejct.defineProperty
来实现对data
中变量赋予get/set
属性,笔者不是很了解,所以这里本文只讨论Vue2.0。
Object类
首先我们知道Vue实例创建的过程中有一个生命周期,其中created
阶段就是会初始化data
和methods
。
首先在vue/src/core/instance/state.js
中initState
初始化函数中
会判断当前实例是否有data
属性,如果有则进入initData()
方法中,否则生成一个空对象data
。接下来进入到initData()
方法中,首先判断data
属性的类型,如果是‘function’
那么会执行getData()
方法将获取data
函数并遍历data
里面的所有属性,initData()
方法的源码定义如下:
vue/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)
····//some code
// observe data
observe(data, true /* asRootData */)
}
在最后将执行observe()
方法监听data
中的所有属性。observe()
方法源码如下:
vue/src/core/observe/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
}
首先observe
方法会判断每个value是否有'__ob__'
属性,它是一个Observe
对象的实例。如果有就直接使用,否则会根据一些条件(是否是数组或对象,可拓展,非Vue
组件等),创建一个Observe
实例对象。然后再来看Observe
对象的源码
export class Observer {
···//some code
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) // 给对象添加set和get属性以达到监听的目的
}
}
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])
}
}
}
首先会判断value
是否是数组,如果是则使用observeArray()
递归遍历,最终都会执行walk()
方法,并执行最重要的defineReactive()
方法。这个方法会利用Object.defineProperty()
方法给每一value
设置get
函数和set
函数以达到数据监听的功能,这里贴出具体的Object.defineProperty()
方法实现,源码如下:
export function defineReactive (
····//some code
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
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify() //数据更新时通知dep更新对应的依赖
}
})
}
在get
函数中执行dep.depend()
进行依赖收集,以便更快的查找到发生改变的数据。在set
函数中,当数据发生改变时就会调用dep.notify()
函数会遍历所有的订阅Watcher
,调用它们的updata
方法,这样就达到了给data
对象添加Observe
并监听数据的过程。
Watcher类
上面说到dep.depend()
和dep.notify()
,其实这个dep
实例是一个Dep
类,这个类就是用来订阅所有的Watcher
并在Observe
监听到数据变化时通知Watcher
的,具体的源码如下:
depend () {
if (Dep.target) {
Dep.target.addDep(this) //添加依赖
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
这里顺便提一下,Watcher对象的updata方法,这里贴上updata()源码:
update () {
// 默认情况下都会进入else的分支,异步则直接调用watcher的run方法
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run() //通过新的虚拟DOM和老的虚拟DOM进行前后diff算法对比以得到新的DOM
} else {
queueWatcher(this) //推入队列
}
}
其中有一个queueWatcher()
方法,会将Watcher
实例推入队列,通过nextTick
方法在下一个事件循环周期处理Watcher
队列,这样做是为了减少多次连续更新DOM减少DOM操作的一种性能优化手段。在queueWatcher()
中使用nextTick(flushSchedulerQueue)
执行异步操作,flushSchedulerQueue()
是一个函数为了调用queue
中所有watcher
的run
方法,然后更新DOM。此时nextTick()
使得此过程为异步更新队列,更多关于nextTick
的原理(这里涉及Vue处理绑定事件的原理)请查找其他相关文章。
总结:Watcher
的作用就是收到Dep
的通知,然后异步去更新DOM。
写到最后
总结一下:vue中Observe通过Objcet.defineProperty()给data属性添加set和get方法,通过get方法中的dep.depend收集依赖并监听数据,当数据发生改变时触发set函数中的dep.notify()通知对应的Watcher实例,再调用watcher.updata进行更新视图,这就是Vue中的响应式系统。而Vue的双向绑定就是基于这一过程实现的:input绑定v-model事件,当输入数据之后,就做到了数据更新(第一步,视图更新数据)。监听到data中属性数据改变后,触发属性set方法然后通过Watcher实例的updata方法更新视图(第二步,数据更新视图),那麽这样就做到了数据的双向绑定,get it!
我是集唱跳rap于一身的前端程序猿Tzyito,关注我,教你打(shuo)篮(sao)球(hua)。