这是 Vue2.x 源码分析的第一篇,介绍在 vue.js 中如何对 Object
和 Array
进行变化侦测的
Vue.js 的渲染过程是声明式的,当内部的状态发生了变化,需要不断进行重新渲染,变化侦测就是来监听内部状态的变化。
一、对 Object 的监测
在 JS 中可以使用 Object.defineProperty
和 Proxy
这两个方法来监测变化,Vue 2.x 版本考虑到兼容性使用的是 Object.defineProperty
方法,而在 Vue 3.x 中使用的是 Proxy
,后续介绍
在 Vue 源码中,对 Object.defineProperty
封装如下
/*
@filePath: src/core/observer/index.js
*/
function defineReactive(obj, key, val){
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
return value
},
set: function reactiveSetter (newVal) {
if (newVal === val) {
return
}
val = newVal
}
})
}
所以,当从data
的key
中读取数据时,get
函数被触发;当往data
的key
中设置数据,set
函数被触发
1.1、如何收集依赖?
在上面使用 Object.defineProperty
对数据监测,是因为当数据发生变化的时候,需要通知到那些使用到该数据的地方,例如👇
<template>
<div>{{ title }}<div>
</template>
在组件的 tempalte
中使用来数据 title
, 所以当 title
变化了,需要通知到 tempalte
总结就是:在getter
中收集依赖,在setter
中触发(通知)依赖
1.2、收集的依赖存在哪?
在 Vue 源码中,使用了一个 Dep
类来封装依赖收集的代码,通过这个类,可以收集依赖、删除依赖或者向依赖发通知
/*
@filePath: src/core/observer/dep.js
*/
export default class Dep {
constructor () {
// 用了存储依赖
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
removeSub (sub) {
remove(this.subs, sub)
}
depend () {
if (window.target) {
this.addSub(window.target)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
在 defineReactive
函数中,就可以使用 Dep 类了
/*
@filePath: src/core/observer/index.js
*/
function defineReactive(obj, key, val){
// 1、每个 key 都创建一个实例来存储该属性的依赖
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 2、当获取属性时,收集依赖
dep.depend()
return value
},
set: function reactiveSetter (newVal) {
if (newVal === val) {
return
}
val = newVal
// 3、当属性发生变化,通知依赖
dep.notify()
}
})
}
在收集依赖前,我们假设了依赖保存在 window.target
上了。此时,我们知道了收集的依赖是存储到 Dep
中的
1.3、依赖为何物?
依赖,也就是使用到数据的地方,同时也是数据发生变化时,需要通知到的地方。以 data
中的数据为例,在 template
、computed
、watch
等多个地方都可以使用到,所以,Vue.js 内部抽象出一个能集中处理这些情况的类,收集依赖时,只需要收集这个类的实例,同时通知也只需要通知到它一个,它再通知到其他地方,这个类叫做 Watcher
所以,收集依赖,就是收集 Watcher 实例
/*
@filePath: src/core/observer/watcher.js
*/
export class Watcher{
constructor(vm, expOrFn, cb){
this.vm = vm
this.getter = parsePath(expOrFn)
this.cb = cb
// 1、初始化,调用 this.get() => this.getter()
this.value = this.get()
}
get(){
// 2、当前 Watcher 实例保存在 window.target 上
window.target = this
let value = this.getter.call(this.vm, this.vm)
window.target = undefined
return value
}
update(){
// 3、属性变化,通知依赖会调用 update 方法, 再去执行回调函数cb
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
所以,无论是 vm.$watch('obj.name',(value, oldValue) => {})
还是模版中使用obj.name
, 当数据发生变化时,都需要通过 Watcher
来进行通知,执行各种的回调函数
1.4、监测所有的 key
defineReactive
方法是监测数据中的单个属性,而我们需要监测到所有的属性(包括子属性),将其都转换成 getter/setter
的形式
/*
@filePath: src/core/observer/index.js
*/
export class Observer {
constructor (value: any) {
this.value = value
if (!Array.isArray(value)) {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
function defineReactive(obj, key, val){
/*省略*/
}
1.5、局限性
Vue.js 通过 Object.defineProperty
来将对象的 key 转换成 getter/setter
的形式来追踪变化,但 getter/setter
只能追踪一个数据是否被修改,无法追踪新增的属性已经删除的属性,为了解决这问题,Vue.js 提供了 $set
和 $delete
两个API
1.6、小结
关于如何监测Object,汇总如下图👇
二、对 Array 的监测
对 Array 的监测不能使用 getter/setter
方法,是因为我们常通过 Array 原型上的方法来改变数组的内容
通过汇总,Array 原型上可以改变数组自身内容的方法共 7 个,分别是:push
, pop
, shift
, unshift
, slice
, sort
, reverse
2.1、拦截器
想要监测数组的变化,只需用一个拦截器覆盖 Array原型,当使用上述 7 种方法修改数组时,执行的时拦截器中的方法
/*
@filePath: src/core/observer/array.js
*/
import { def } from '../util/index'
// 1、基于 Array.prototype 创建一个拦截器
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
// 2、在拦截器上重新定义上述 7 个方法
def(arrayMethods, method, function mutator (...args) {
// 3、执行方法本身的逻辑,返回执行结果
const result = original.apply(this, args)
return result
})
})
2.2、如何收集依赖?
对数组而言,通知依赖肯定是在拦截器中,那么,数组在哪里收集依赖呢?看个例子
{
list: [1,2,3]
}
以上面数据为例,只有通过 list
属性,才能获取这个数组,那么读取 list
属性,一定为触发list
属性的 getter
方法,所以数组也是在 getter
中收集依赖
所以,Array 在 getter 中收集依赖,在拦截器中触发(通知)依赖
2.3、存储依赖
Array 的依赖也是通过 Dep 实例来进行保存,但与 Object 不同的是,Vue.js 把 Array 的 dep 放到 Observer 中了,而对象属性对应的 dep 则在 defineReactive 函数中
之所以放在 Observer 中, 是因为在 getter 和拦截器中都需要访问到这个 dep 实例
/*
@filePath: src/core/observer/index.js
*/
export class Observer {
constructor (value: any) {
this.value = value
this.dep = new Dep()
if (Array.isArray(value)) {
this.observeArray(value)
} else {
this.walk(value)
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
// 2、可能是对象数组,需要监测数组每一项
observe(items[i])
}
}
}
2.4、收集依赖
我们需要在 getter 中访问到 dep 实例,才能收集依赖,代码如下
/*
@filePath: src/core/observer/index.js
*/
function defineReactive(obj, key, val){
// 1、此处假设 val 是个数组
let childOb = observer(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 2、收集依赖
if (childOb) {
childOb.dep.depend()
}
return value
},
set: function reactiveSetter (newVal) {
if (newVal === val) {
return
}
val = newVal
}
})
}
export function observe (value){
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__ // 当数据已经检测过,直接返回
} else {
ob = new Observer(value) // 进行检测
}
return ob
}
以数据 list: [1,2,3]
为例,当访问属性 list, 会对数组 [1,2,3]
进行监测
2.5、通知依赖
Array 发生变化时,需要在拦截器中访问到 Observer 实例,通过其 dep 属性通知依赖
/*
@filePath: src/core/observer/index.js
*/
export class Observer {
constructor (value: any) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this)
if (Array.isArray(value)) {
this.observeArray(value)
} else {
this.walk(value)
}
}
//...
}
在 value
上增加 __ob__
属性即可访问到 Observer
实例,继而可以访问 dep
属性
接下来,只需要在拦截器中通知依赖, 同时,当我们使用 push
, unshift
, splice
时会往数组中添加新元素,新增的元素也需要被监测到。
/*
@filePath: src/core/observer/array.js
*/
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
2.6、局限性
通过 Array 下标修改元素的值,或者设置 list.length = 0 来清空数组等,Vue.js 都无法监测数组的变化