vue源码解读–深入响应式原理
前言
其实前端开发最重要的 2 个⼯作,⼀个是把数据渲染到⻚⾯,另⼀个是处理⽤户交互。Vue 把数据渲到⻚⾯的能⼒我们已经通过源码分析出其中的原理了,但是由于⼀些⽤户交互或者是其它⽅⾯导致数据发⽣变化重新对⻚⾯渲染的原理我们还未分析
当我们不使用Vue的时候,使用原生进行开发的时候,我们需要手动修改DOM,但是我们会遇到这几个问题:
-
我需要修改哪块的 DOM?
-
我的修改效率和性能是不是最优的?
-
我需要对数据每⼀次的修改都去操作 DOM 吗?
-
我需要 case by case 去写修改 DOM 的逻辑吗?
如果我们使⽤了 Vue,那么上⾯⼏个问题 Vue 内部就帮你做了,那么 Vue 是如何在我们对数据修改后⾃动做这些事情呢,接下来我们将进⼊⼀些 Vue 响应式系统的底层的细节。
响应式对象
我们都知道Vue2的响应式是基于ES5的Object.defineProperty,Vue3的是利用浏览器自带的属性Proxy,都知道Proxy的效果更好,但是为了兼容性,Vue2还是选择了Object.defineProperty,但即使是这样,Vue还是无法兼容IE8及其一下的版本
MVVM模式
数据变化,视图也会变化
Object.defineProperty
Object.defineProperty ⽅法会直接在⼀个对象上定义⼀个新属性,或者修改⼀个对象的现有属性, 并返回这个对象,先来看⼀下它的语法:Object.defineProperty(obj, prop, descriptor)obj 是要在其上定义属性的对象; prop 是要定义或修改的属性的名称; descriptor 是将被定义或修改的属性描述符。
⽐较核⼼的是 descriptor ,它有很多可选键值,具体的可以去参阅它的⽂档。这⾥我们最关⼼的是get 和 set , get 是⼀个给属性提供的 getter ⽅法,当我们访问了该属性的时候会触发 getter ⽅法; set 是⼀个给属性提供的 setter ⽅法,当我们对该属性做修改的时候会触发 setter ⽅法。⼀旦对象拥有了 getter 和 setter,我们可以简单地把这个对象称为响应式对象。那么 Vue.js 把哪些对象变成了响应式对象了呢,接下来我们从源码层⾯分析。
initState
前面我们在new Vue发生什么提到,function vue里面有一个_init方法在这个方法中会执行initState这个方法
作用
initState函数主要的作用就是初始化props,data。methods,computed,watched
初始化Props initProps
function initProps(vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = [] const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key) const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key) if (isReservedAttribute(hyphenatedKey) || config.isReservedAttr(hyphenatedKey)) {
warn(`"${hyphenatedKey}"is a reserved attribute and cannot be used as compone nt prop.`, vm)
}
defineReactive(props, key, value, () = >{
if (vm.$parent && !isUpdatingChildComponent) {
warn(`Avoid mutating a prop directly since the value will be` + `overwritten whenever the parent component re - renders.` + `Instead, use a data or computed property based on the prop 's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
defineReactive(props, key, value)
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (! (key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
这里对props遍历主要做了两件事情
-
对遍历出的每一个props的参数调用方法defineReactive,将每一个prop的属性变为响应式
-
通过Proxy将每一个数据访问代理到vm原型上,意思是我们可以通过this进行访问
初始化data initData
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)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true
/* asRootData */
)
}
initData也做了两件事情
- 遍历data中的数据,通过proxy对每个数据进行代理,然后我们可以通过this进行访问
- observe方法观测data中的数据,让data中的数据变为响应式
proxy
刚刚initData,initProps中都用到了proxy进行代理,那么代理究竟是什么,代理的作⽤是把 props 和 data 上的属性代理到 vm 实例上,这也就是为什么⽐如我们定义了如下 props,却可以通过 vm 实例访问到它。
let comP = {
props: {
msg: 'hello'
},
methods: {
say() {
console.log(this.msg)
}
}
}
observe
observe主要作用就是用来监测数据的变化
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
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方法就是给每一个非VNode的对象数据添加一个observer属性,如果已经添加了,则直接返回,然后在一定条件下创建一个Observer的实例
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)
}
Observer(观察者)是干什么的
observer是一个类,他的作用就是给对象的属性添加getter和setter方法
在observer中
/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
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
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
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.
*/
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])
}
}
}
在observer类中,最重要的是def函数,def函数其实就是对Object.definePropty的封装,
/**
* Define a property.
*/
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
这就是为什么读取data上对象类型的属性会多了一个 _ob_
属性(因为observer类的构造函数中会调用def函数,然后给val传入‘ _ ob _ ’,方便对数据进行监测,进行响应式处理)
ex:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue CDN Demo</title>
<!-- 引入 Vue.js CDN -->
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
</head>
<body>
<div id="app">
<h1>{{ greeting }}</h1>
<p>{{ message }}</p>
</div>
<!-- 编写 Vue 代码 -->
<script>
let test = new Vue({
el: '#app',
data: {
greeting: 'Hello, Vue!',
message: 'This is a simple Vue.js demo using CDN.',
obj: {
a: 1,
b: 2
}
}
});
console.log(test.obj)
</script>
</body>
</html>
打印obj就会发现里面存在了 _ ob _
依赖收集
我们知道响应式属性中的getter属性就是对依赖进行收集,实现getter最重要的函数就是Dep函数
getter部分的逻辑:
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
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
},
// ...
})
}
这里最重要的是 const dep=new Dep()
还有就是在get函数中通过dep.depend做依赖的收集
Dep
(Dep函数实现的源码这里就不展示了),Dep 是一个class,是整个 getter 依赖收集的核⼼,Dep是对Watcher的一个管理,失去Watcher后Dep单独存在是没有意义的
Dep的主要作用:
- 依赖收集(Dependence Collection): 在响应式数据的 getter 操作中,
Dep
会判断是否有正在进行的计算(例如渲染视图的计算),如果有,它会将这个计算的目标Watcher
添加到自己的依赖列表中,建立起依赖关系。 - 触发更新(Notify): 当响应式数据发生变化时,它的 setter 操作会通知对应的
Dep
,而Dep
会遍历自己的依赖列表,通知所有依赖(Watcher
)进行更新操作。
Watcher
Watcher有什么用:
它的主要作用是建立对响应式数据的依赖关系,并在数据变化时执行相应的回调函数,通常用于更新视图。
当我们的数据发生改变时,会触发数据的 setter 方法,setter 方法通知与之关联的 Dep
对象。Dep
对象负责通知依赖于该数据的 Watcher
对象,使其执行更新操作
Watcher也是一个类,他会创建两个Dep实例数组,一个是新添加的Dep实例数组,一个是以前的实例数组,为什么会这样设计?这里有个场景:
我们的模板会根据 v-if 去渲染不同⼦模板 a 和 b,当我们满⾜某种条件的时候渲染 a 的时候,会访问到 a 中的数据,这时候我们对 a 使⽤的数据添加了 getter,做了依赖收集,那么当我们去修改 a 的数据的时候,理应通知到这些订阅者。那么如果我们⼀旦改变了条件渲染了 b 模板,⼜会对 b 使⽤的数据添加了 getter,如果我们没有依赖移除的过程,那么这时候我去修改 a 模板的数据,会通知 a 数据的订阅的回调,这显然是有浪费的
Vue在这里设计的非常好
总结
收集依赖的⽬的是为了当这些响应式数据发生变化,触发它们的 setter 的时候,能知道应该通知哪些订阅者去做相应的逻辑处理,我们把这个过程叫派发更新,其实 Watcher 和 Dep 就是⼀个⾮常经典的观察者设计模式的实现
派发更新
class Dep {
// ...
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
在响应式数据的 setter 中,如果新值和旧值不相等,说明数据发生了变化。此时,Dep 对象会调用自己的 notify
方法,遍历依赖列表,通知所有相关的 Watcher 执行更新操作。
notify
这⾥的逻辑⾮常简单,遍历所有的 subs ,也就是 Watcher 的实例数组,然后调⽤每⼀个watcher 的 update ⽅法
总结
实际上就是当数据发⽣变化的时候,触发 setter 逻辑,把在依赖过程中订阅的的所有观察者,也就是 watcher ,都触发它们的update 过程,这个过程⼜利⽤了队列做了进⼀步优化,在 nextTick 后执⾏所有 watcher 的run ,最后执⾏它们的回调函数。 nextTick 是 Vue ⼀个⽐较核⼼的实现了,下⼀节我们来重点分析它的实现。
补充
Vue.set
当我们对于一个对象添加属性的时候,Vue2的响应式是无法监听到的,vue2提供了一个解决方法,使用vue.set或者vm.$set添加,这样添加的数据就是响应式,set方法在源码中主要是触发ob.dep.notify() 手动进行依赖通知
数组
接着说⼀下数组的情况,Vue 也是不能检测到以下变动的数组:
1.当你利⽤索引直接设置⼀个项时,例如: vm.items[indexOfItem] = newValue
2.当你修改数组的⻓度时,例如: vm.items.length = newLength
对于第⼀种情况,可以使⽤: Vue.set(example1.items, indexOfItem, newValue) ;⽽对于第⼆种情况,可以使⽤ vm.items.splice(newLength) 。
总结
简单来说,vue2响应式的原理就是经过这几个步骤
- 创建响应式对象(initState,proxy,observe)
- 依赖收集(Def,Watcher)
- 派发更新(def.notify())