持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第13天,点击查看活动详情
前言
大家好,上篇文章watch的原理中我们分享了vue中侦听器的使用方法,使用场景以及实现原理,到这里关于Vue中一些常用的全局API的原理基本就分享完了。今天我们再来学习一下Vue2中数据响应式的实现原理,其实关于数据响应式我们再分享MVVM原理时已经涉及到一部分了,今天我们再来从头到尾详细分析一下。
initState
在我们通过new Vue去创建vue实例时,在Vue的内部首先会调用一个_init函数,而在这个_init函数中会做一堆的init操作,比如initLefecycle、initEvents、initRender、initInjections、initState、initProvide。其中initState就是我们要找的数据响应式的入口,先来看下initState的源码
export function initState(vm: Component) {
vm._watchers = [];
const opts = vm.$options;
if (opts.props) initProps(vm, opts.props);
if (opts.methods) initMethods(vm, opts.methods);
if (opts.data) {
initData(vm);
} else {
observe((vm._data = {}), true /* asRootData */);
}
if (opts.computed) initComputed(vm, opts.computed);
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
可以看到在该方法中也是一堆init操作,对应的就是vue中的props、methods、data、computed和watch,其中initComputed和initWatch在前两篇分享中已经分析过了,本次我们以data为例分析一下响应式数据的原理。
initData
function initData(vm: Component) {
let data: any = vm.$options.data;
data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};
if (!isPlainObject(data)) {
data = {};
...
}
// 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--) {
...
}
// observe data
observe(data, true /* asRootData */);
}
以上是initData函数的核心代码,其中省去了一些信息提示代码。
- 首先在vue实例的$options上拿到data
- 然后检测data是否是一个函数类型(data可以是一个对象也可以是一个函数),如果是函数则调用getData函数让data函数执行并将返回结果(一个对象)重新赋值给data
- 继续检测data是不是一个纯对象,如果不是纯对象则将data赋为空对象,并给出错误提示信息
- 接着利用Object.keys获取到data中所有的属性,并在vue实例的$options中拿到props和methods,目的是为了检测data中的属性是否已经在props或methods中被定义
- 下面就是利用wihile循环遍历data中所有的属性,并检测该属性是否在props或methods中已经存在,如果存在则给出错误提示,因为这三者中的属性名是不能重复的
- 最后如果检测都没问题则调用observe函数对data进行数据劫持
observe
//源码位置:src/core/observer/index.js 42行
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函数比较简单,其核心目的就是利用data创建一个Oberver的实例。
- 在该方法中首先要确保data是一个纯对象,并且不是虚拟DOM的实例
- 然后判断data中是否存在__ob__属性,并且这个属性是Observer的实例,则证明已经为data对象创建过Observer实例了(也就是说已经被劫持过了)则直接将data中的__ob__赋值给ob变量
- 否则就直接new一个Observer,并且把data作为参数传递进去
Observer
//源码位置:src/core/observer/index.js 135行
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(obj: object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
在创建Observer(new Observer
)实例时会执行到Observer的构造函数,在函数中主要做了两件事:就是分别对数组和对象的属性进行数据劫持
- 首先这个参数value就是我们外面传进来的data
- 紧接着创建了一个Dep实例,这个Dep实际时一个发布订阅,在
new Dep
时会给dep实例添加一个subs数组,该数组用于存放所有属性的watchers - 然后判断data是数组还是对象,如果是数组调用observeArray方法对data进行劫持,如果data是一个对象则带调用walk方法
- 在walk方法中主要就是遍历data中所有的属性,为每个属性调用
defineReactive
进行数据劫持
defeinReactive
下面我们还是走对象这条线,从walk函数走起分析一下defeinReactive的代码,这个方法也是响应式的核心所在。
//源码位置:src/core/observer/index.js 135行
export function defineReactive(obj: object, key: string,val?: any,customSetter?: Function | null, shallow?: boolean
) {
const dep = new Dep()
...
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 (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
},
})
}
//源码位置:src/core/observer/dep.js
addSub(sub: Watcher) {
this.subs.push(sub)
}
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()
}
}
上面是defineReactive函数节选的一部分核心代码:
- 首先在函数体内会创建一个Dep实例,关于Dep我们在前面几篇的分享中也有提到过。在Vue中每个响应式属性都会有一个dep属性,它的主要作用就是
利用发布订阅模式来管理每个属性的watcher
- 接着下面一句Object.defineProperty也就是实现响应式的关键了,这里主要做了3件事:
- 首先就是把key属性添加到obj对象上
- 然后对该属性的get和set进行拦截
- 在get中会调用dep.depend函数进行依赖收集
- get函数中用到了个Dep.target,实际上这个target是Dep的一个静态属性,其值是一个watcher的实例,会通过两个函数
pushTarget
和popTarget
来设置和移除。也就是说当涉及到该key属性读取时就会触发这了的get函数,在该函数中调用dep.depend()进行依赖收集,依赖收集后又将Dep.target设置为null,避免重复收集。- dep.depend()函数中调用了watcher中的addDep函数,而在addDep函数中又通过dep实例调用了dep中的addSub函数,绕来绕去其最终目的就是给dep的subs事件池中添加一个watcher实例。总结来说就是只要有用到
key
属性的地方,都会相应的添加一个watcher实例(利用Dep来管理),主要目的就是将来当该key值有更新的时候,通过dep来通过各个watchers进行同步更新,从而达到响应式的目的
- dep.depend()函数中调用了watcher中的addDep函数,而在addDep函数中又通过dep实例调用了dep中的addSub函数,绕来绕去其最终目的就是给dep的subs事件池中添加一个watcher实例。总结来说就是只要有用到
- set函数中除了给属性设置新值外,还有一句实现响应式的核心代码,就是dep.notify()。在上面我们刚刚说过当属性key值发生变化时就会通过dep来通过各个watcher进行更新。dep.notify就是这个通知操作,在该函数中主要就是循环dep的subs事件池然后调用update方法更新,其中subs中保存的就是所有的watcher实例,最终调用的其实就是watcher实例上的update方法
总结
本次分享我们主要梳理了数据响应式的原理,简单总结如下:
通过Observe进行数据劫持的时候给每个被劫持的属性都添加了一个dep实例(new Dep),在dep实例中有个数组类型的subs属性,在这个数组中存储的都是使用当前属性时创造的Watcher的实例。
当对应的属性被访问时就会触发对应的get函数,在get函数中通过dep.depend进行依赖收集,同时会创建一个观察者Watcher实例,实例创建完成后就会触发watcher的addDep方法,从而就会调用dep实例的addSub方法,最终将当期watcher实例添加的dep的subs数组中
当被劫持的那个属性数据更新时,就会触发这个属性的set函数,set在执行的时候会触发dep实例的notify方法,notify执行会遍历dep的subs数组,让数组中的每个watcher实例执行update方法,从而更新视图