这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战
前言
我们知道Vue是典型的实现双向数据绑定的MVVM框架,数据变化会更新视图,视图变化会更新数据。视图改变数据我们可以很简单想象到应该是用到了input事件,那数据怎么改变视图呢??
官方文档提到Vue最独特的特性之一就是其非侵入性的响应式系统。当修改普通的Javascript对象时,视图会随之更新。这让Vue的状态管理非常简单,让开发者更容易专注于数据模型。那到底Vue是怎么来实现这个响应式呢??
定义响应式对象
官方文档已经说了Vue2实现响应式的核心是使用Object.defineProperty,把数据对象中的property转化为getter/setter。所以我们可以简单的理解:如果对象拥有getter/setter,就可以称之为响应式对象。getter用来依赖收集,setter用来派发更新。
Object.defineProperty(obj, prop, descriptor)方法可以直接在对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
这里重点关注的就是descriptor参数,它可以用来设置get和set。
定义响应式对象的函数为defineReactive,在src/core/observer/index.js中
/**
* Define a reactive property on an Object.
*/
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
}
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, {
...
})
}
该函数接收5个参数:
- obj:要处理的对象
- key:该对象的key值
- val:要处理的value值
- customSetter:定义的辅助函数
- shallow:判断是否浅复制来处理对象,即是否将对象的子对象也处理为响应式对象
接下来具体看下这个函数的实现:
Dep
首先实例化一个Dep类
const dep = new Dep()
Dep类定义在src/core/observer/dep.js
let uid = 0
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
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()
}
}
}
这段代码写的很清晰,熟悉设计模式的都看的出来这段代码与发布订阅模式/观察者模式非常相似。
它定义了一个subs数组,用来收集所有Watcher类型的订阅者(收集所有的依赖);当调用notify方法时,会依次去执行subs数组(会先根据id从小到大排序)中的每一项,即各个Watcher的update方法。
这里的Watcher对象就是订阅者(观察者)。
depend方法需要配合Watcher来看,后边会说,它负责向Dep中添加Watcher。
具体看一下Dep.target:
Dep.target
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
Dep.target是Dep上的静态数据属性。注释说得很清楚,Dep.target代表当前正在执行的watcher,全局唯一。默认会设置成null,代表没有依赖需要收集,如果Dep.target存在说明有依赖需要被收集。
Object.getOwnPropertyDescriptor
接下来调用Object.getOwnPropertyDescriptor来获取obj对象的key属性对应的属性描述符。如果configurable属性为false,则该对象无法设置为响应式对象。
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
这里也说明了为啥在性能优化的时候推荐使用Object.freeze来冻结那些无需被依赖收集的对象,因为设置了Object.freeze是无法被设置为响应式的,它内部的所有属性的configurable都是false。
预处理getter/setter
如果属性已经定义了getter/setter,则先存下来。
const getter = property && property.get
const setter = property && property.set
预处理val
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
一开始不太理解这个if语句。如果给defineReactive只传了两个参数,则把obj[key]赋值给val,这个可以理解。但是为什么要加(!getter || setter)呢??
上网搜了下,原来!getter是为了解决这个issue。所以当一个对象的某个属性本身具有getter拦截函数,是不会取obj[key]的值,则val为undefined。
那为啥还要判断是否具有setter呢??继续向下看。
childOb
如果shallow为false,说明要对obj深层处理,调用observe函数。observe函数从注释里看到是用来创建一个Observer实例,具体实现后边专门来看,这里大概知道一下Observer类的作用就是为对象添加getter/setter,用来依赖收集和派发更新(Observer内部也是调用defineRective方法)。
let childOb = !shallow && observe(val)
/**
* 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 {
...
}
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
...
if (Array.isArray(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])
}
}
}
Object.defineProperty
接下来就是创建响应式对象的重点了,使用Object.defineProperty函数来对对象的属性值设置getter/setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {},
set: function reactiveSetter() {}
});
getter函数
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
},
-
先判断属性是否本身设置了getter函数,如果设置了直接调用,否则就直接取属性对应的value值。
-
接下来依赖收集,需要联合Watcher来理解。判断Dep.target是否为空,如果不为空,则把当前依赖添加到dep中。
-
如果子对象也是响应式的,则也需要依赖收集添加到子dep中。同时判断属性值是否为数组,如果是则调用dependArray进行处理。
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
setter函数
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
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()
}
})
}
-
首先拿到原来的value值,然后进行新旧值的判断,如果新旧值相同则直接返回。这里有个newVal !== newVal && value !== value判断,一开始没想明白,怎么会有这种场景??后来一想,哦,NaN!!!
-
接下来判断属性是否自带setter,如果设置了则执行,否则直接用新值覆盖
-
如果shadow是false,说明要深度监听,则调用observe将newVal变成响应式。
-
最后调用dep.notify来通知所有的依赖(subs数组中的watchers)进行更新。
为何要判断(!getter || setter)
看完了整个defineReactive函数,再回到中途遇到的那个问题,为啥对setter也进行判断??
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
...
set: function reactiveSetter(newVal){
...
childOb = !shadow && observe(val)
}
})
如果没有setter的判断,当数据对象属性具有getter时,会把val置为undefined。则在后边的深度观测中传给observe的是undefined,即不会被深度监听。但是经过defineProperty后会重新执行setter,并且重新执行深度观测(!shadow && observe(newVal))。这会导致前后不一,一个数据对象定义阶段是非响应式的,修改后又变成响应式的了。
总结
-
Vue2利用Object.defineProperty给数据添加getter和setter来实现响应式对象,getter用来依赖收集,setter用来派发更新。
-
getter依赖收集的核心是Dep。Dep用来管理Watcher,Watcher用来订阅Dep,Dep和Watcher是观察者模式(发布-订阅)的一种体现。