🔥Vue2响应式原理
🏢核心:数据劫持
我们想实现响应式,我们就需要知道数据在什么时候发生了变化,变化后又需要更改哪些地方
js提供了Object.DefineProperty方法,我们通过它可以为数据定义satter和getter
通过setter,我们可以知道数据什么时候发生了变化
通过getter,我们可以知道数据什么时候被调用,以及被谁调用(后续会讲到方式)
调用方法为数据添加getter和setter:
let obj = {
num:1
}
//为obj的num添加getter和setter
Object.defineProperty(obj, 'num', {
//定义属性是否可枚举、可配置
enumerable: true,
configurable: true,
//定义getter,数据被读取时就会执行此函数
get() {
console.log('有人读取num啦,快看看是谁!')
},
//定义setter,数据被修改时就会执行此函数
set(newVal) {
console.log('有人设置num啦,快对调用num的地方做更新!')
}
})
let num = obj.num//输出:'有人读取num啦,快看看是谁!'
obj.num = 2//输出:'有人设置num啦,快对调用num的地方做更新!'
但是要注意,由于我们在getter中没有返回任何值,所以num中接收到的是undefined,并且由于我们没有在setter中对其进行赋值,所以obj.num也没有赋值为2,这就是数据劫持,如果getter和setter没有松手,数据是无法被拿到或修改的
那我们如何在getter中返回值,在setter中修改值呢?
如果我们直接在getter中返回obj.num,就会在返回其值时再次触发getter,造成死循环,同样,在setter中直接为obj.num进行赋值,也会无限触发setter,导致错误
这时候我们就需要一个中间变量来帮我们进行值传递
let obj = {
num:1
}
let temp = obj.num //temp就成了obj.num的化身,任何对obj.num的操作都会转移到它身上
//为obj的num添加getter和setter
Object.defineProperty(obj, 'num', {
enumerable: true,
configurable: true,
//定义getter,数据被读取时就会执行此函数
get() {
return temp
},
//定义setter,数据被修改时就会执行此函数
set(newVal) {
temp = newVal
}
})
let num = obj.num//得到的是temp的值
obj.num = 2//temp替obj.num修改为了2
这样我们就完成了数据的劫持,为属性添加了getter和setter,并使其可以正常工作
现在我们要进行一点改进
在vue中,我们需要为每个数据添加响应式,那么就要执行若干次Object.defineProperty方法,而每次执行都要为它指定一个中间人temp,会使代码显得凌乱又不好维护,所以我们要将其封装为defineReactive方法👇
function defineReactive(obj, key, value) {
//value是传入的obj.num的值
//为obj的num添加getter和setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
//定义getter,数据被读取时就会执行此函数
get() {
return value
},
//定义setter,数据被修改时就会执行此函数
set(newVal) {
value = newVal
}
})
}
我们以value的形式将要定义响应式的数据的值传了进来,利用非引用对象会在函数作用域中创建一个副本的特性,将其作为我们要用的中间人temp。
在这个方法中,get和set函数会被绑定到obj的属性描述符上,可以理解为保留了引用,所以不会被清除,这种形式有没有让你联想到闭包!
只要obj上的num属性还在,getter和setter作为num的属性描述符就在,这两个方法在,它们引用的value就不会被清除,也就是形成了闭包(关于闭包不理解的请看:小何的世界 (theluckyone.top)),我们每次调用get和set函数,进行操作的都是同一个value
🏢递归劫持
现在我们手握defineReactive,谁来了都要被我们劫持,但如果对象内又有对象呢?
let obj = {
son: {
name: '小明'
}
}
这样我们调用defineReactive函数时,不仅要调用defineReactive(obj),还要调用defineReactive(son)才能将这个obj里里外外劫持完毕。
最简单的方法是我们直接调用两次,但在对象内部情况不确定的情况下,我们就无法直接写出所有情况了。
于是我们这一节要实现一个递归侦测,也就是传入一个obj,管他里面有多少子对象,统统被我们的defineReactive劫持
我们先用简单的递归来实现,在vue底层并不是这样做的,但为了理解,我们一步一步来👇
let obj = {
son: {
name: '小明'
}
}
function defineReactive(obj, key, value) {
//在这里判断obj[key]的值(即value)是不是对象
//如果是对象,则遍历这个对象,为其每个子元素调用defineReactive为其添加响应式
if(typeof value === 'object'&&value) {
for(let key in value) {
defineReactive(value, key, value[key]);
}
}
Object.defineProperty(obj, key, {
//这里不变,此处省略
})
}
defineReactive(obj, 'son', obj.son)
我们在defineReactive函数中对obj[key]的值进行了判断,如果其值是对象,且不为null,那我们就对他再执行defineReactive,如果他里面还有对象,那就再判断,再调用,直到把其中所有对象都进行属性劫持。
📕依赖容器与Watcher
我们现在对所有属性劫持完毕,就等着数据被调用啦
我们的想法是:当数据被调用时,我们记录下它在哪里被调用,以便在数据发生变化时,我们对调用的地方进行通知修改。
记录到哪里呢?记录到依赖容器里,记录谁呢?记录watcher
当一个地方调用了数据,就会生成一个叫watcher的东西,并且会将watcher实例放到这个数据对应的依赖容器中
当数据发生变动时,我们就从依赖容器中取出所有watcher,一个一个进行通知修改
🌙定义依赖容器
我们以类的形式定义,这样就可以在每个数据身上绑定一个专属于它的依赖容器,专门用来收集对它的依赖
class Dep {
constructor() {
this.subs = []//这里就是我们的容器,以后收集的watcher实例就放在这里面
}
depend() {
//此处进行依赖收集
}
notify() {
//属性发生改变时,在这里通知依赖容器中的watcher
}
}
🌙定义Watcher
当数据发生改变时,依赖容器中通过notify方法通知Watcher,Watcher收到通知后,就去执行对应的操作以完成响应式任务
我们同样以类的形式定义
class Watcher {
constructor(obj, key) {}
update() {
console.log(`${obj}[${key}]的内容发生更新啦`)
}
}
现在我们定义了它构造方法的参数,我们通过传入对应对象及其key来生成这个属性专有的watcher
我们还定义了一个update方法,用于在数据更新后调用,也是响应式的最后一步(更新数据或执行回调)
🌙为属性绑定容器
要为属性绑定专有容器,在为它定义响应式的时候进行再好不过了
所以我们在defineReactive中操作👇
function defineReactive(obj, key, value) {
let dep = new Dep()//生成依赖容器
//...
}
由于我们是在defineReactive作用域中定义的依赖容器,所以它的作用域只有这个函数,即每个属性只会有它专有的容器
🌙依赖收集
我们现在绑定了容器,有了Watcher,如何进行依赖收集呢?
先前说过,哪里调用响应式属性,哪里就会生成一个对应的watcher实例
所以我们要在依赖容器中存储调用者,就是要存储对应的watcher实例
所以我们现在的问题就是:如何在watcher生成后将它放到属性的Dep容器中呢?
我们想到了通过getter,当watcher实例化时必然会执行构造函数,那么在执行构造函数时,我们将watcher传入,getter里不就能获取到我们的watcher了吗
但是问题又来了,getter定义参数我们怎么传参呢?因为我们只需要读取对象就可以触发getter,完全没有传参的机会呀
我们另辟蹊径,想到js中不是还有个全局对象吗?浏览器中是window,node环境下是global,那我在生成watcher时,将它放到全局对象中,在getter中再去全局对象中读取watcher,不就获取到了吗
说干就干!
我们将语句封装到get方法中,注意这个和getter不是一个性质,这里的get只是一个普通函数
class Watcher {
constructor(obj, key) {
this.obj = obj
this.key = key
this.get()
}
get() {
window.target = this //我们将this,也就是watcher实例,绑定到window.target属性上
this.obj[this.key]//调用数据,触发getter
window.target = null //别忘了调用完毕后释放window.target哦
}
update() {
console.log(`${this.key}的内容发生更新啦`)
}
}
Dep容器中封装收集依赖的流程:
Dep() {
constructor() {
this.subs = []
}
depend() {
if(window.target && window.target instanceof Watcher) {
//如果window.target上有watcher的话,就进行收集
this.subs.push(window.target)
}
}
}
getter改造:
get() {
dep.depend()//执行容器的depend方法进行依赖收集
return value
}
这样一个流程走下来,watcher实例就成功加入了属性的依赖容器中
🌙更新通知
接下来就是响应式的最后一步,如果发生数据更新,我们就要进行通知,告诉所有的watcher:你观测的数据更新啦,快做出响应!
数据变化要触发setter,所以我们在setter中进行通知:
set(newVal) {
if(value === newVal) {
//如果新旧值相同,就不进行操作
return
}
dep.notify() //执行notify进行通知
value = newVal
}
那么notify函数中做了什么呢?
notify() {
//遍历subs数组,挨个通知里面的watcher实例,触发其update方法
this.subs.forEach(watcher => {
watcher.update()
})
}
这样我们就完成了一个简单的对象响应式实现,完整代码👇
let obj = {
son: {
name: '小明'
}
}
function defineReactive(obj, key, value) {
let dep = new Dep()//生成依赖容器
//在这里判断obj[key]的值(即value)是不是对象
//如果是对象,则遍历这个对象,为其每个子元素调用defineReactive为其添加响应式
if(typeof value === 'object'&&value) {
for(let key in value) {
defineReactive(value, key, value[key]);
}
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
//定义getter,数据被读取时就会执行此函数
get() {
dep.depend()//执行容器的depend方法进行依赖收集
return value
},
//定义setter,数据被修改时就会执行此函数
set(newVal) {
if(value === newVal) {
//如果新旧值相同,就不进行操作
return
}
dep.notify() //执行notify进行通知
value = newVal
}
})
}
class Dep {
constructor() {
this.subs = []//这里就是我们的容器,以后收集的watcher实例就放在这里面
}
depend() {
if(window.target && window.target instanceof Watcher) {
//如果window.target上有watcher的话,就进行收集
this.subs.push(window.target)
}
}
notify() {
//遍历subs数组,挨个通知里面的watcher实例,触发其update方法
this.subs.forEach(watcher => {
watcher.update()
})
}
}
class Watcher {
constructor(obj, key) {
this.obj = obj
this.key = key
this.get()
}
get() {
window.target = this //我们将this,也就是watcher实例,绑定到window.target属性上
this.obj[this.key]//调用数据,触发getter
window.target = null //别忘了调用完毕后释放window.target哦
}
update() {
console.log(`${this.key}的内容发生更新啦`)
}
}
defineReactive(obj, 'son', obj.son) //进行响应式转换
new Watcher(obj.son,'name') //数据调用
obj.son.name = '小王'//输出:name的内容发生更新啦
当然,这只是一个非常简陋的响应式实现,我们现在要对其进行改造,考虑更多的情况
📕进阶改造
以上的简陋实现还存在很多问题,如无法实现数组的响应式,无法侦测到对象新增或删除属性等
所以我们要进行深入改造,使其能应对更多情况
🌙Observer对象和observe函数
在之前我们是直接通过在defineReactive函数内递归进行内层响应式处理的
现在我们要引入Obeserver对象和observe函数来替代我们进行观测,完成其内所有的响应式转换
具体逻辑:
- defineReactive为对象创建响应式
- 在defineReactive内调用observe方法
- observe方法中判断传入的是值是对象还是基本数据类型
- 如果是对象,就为其创建Observer对象
- Observer实例化时,会作为__ob__属性绑定在响应式对象身上
- 同样也是在Observer实例化时,会遍历对象中的所有属性,执行defineReactive函数,这就回到了第一步,如此循环
既然是改造从defineReactive作为切入口,所以我们先对它进行改写:
/**
* 为元素添加响应式(getter/setter)
* @param {Object} obj 外层对象
* @param {*} key 要添加响应式的元素的key
* @param {*} value 元素对应的值
*/
defineReactive(obj, key, value) {
//获取元素的ob实例,如果没有则进行创建
let childOb = this.observe(obj[key])
Object.defineProperty(obj, key, {
//...
})
}
我们这里先砍掉定义依赖容器的部分,因为后期会在其他地方对其进行初始化,这里先不展开说
我们在这里做的事情很简单,我们为传入的值调用observe进行观测
⭐observe函数
我们通过observe函数来查看元素是否已经具备响应式,若不具备响应式,就为其创建专有的Observer实例:
/**
* 返回传入对象的Observer实例
* @param {Object} obj 被检查对象
* @returns 对象的Observer实例
*/
observe(obj) {
//判断是否为对象,不是对象则返回,不为其添加observer实例
if(typeof obj !== 'object') {
return
}
//如果obj上已经有了__ob__属性,说明obj已经是响应式对象了,直接返回它的__ob__属性
if(obj.__ob__ && obj.__ob__ instanceof Observer) {
return obj.__ob__
}else {
//如果obj不是响应式属性,那么就为其创建Observer实例
let ob = new Observer(obj)
return ob
}
}
⭐Observer类
在实例化Observer类时,我们将当前实例绑定到传入对象的__ob__属性上,然后再调用walk对obj进行遍历,为其下的每个属性进行defineReactive操作(因为我们定义的__ob__属性的enumerable为false,所以不会遍历它)
export const class Observer {
constructor(obj) {
this.value = obj
//创建依赖容器
let ob = this
//每个对象上存放一个ob
this.def(obj, '__ob__', ob)
this.walk()
}
/**
* 递归为Object类型上的每个元素添加响应式
* @param {Object} obj 需要添加响应式的对象
*/
walk() {
//遍历所有key,执行defineReactive为其添加响应式
Object.keys(this.value).forEach(key => {
this.defineReactive(this.value, key, this.value[key])
})
}
/**
* 工具方法,封装Object.definProperty
* @param {Object} obj 待添加属性对象
* @param {String} key 待添加的key
* @param {*} value 待添加属性的值
*/
def(obj, key, value) {
Object.defineProperty(obj, key, {
value:value,
enumerable:false,
configurable:true,
writable:true
})
}
}
现在我们完成了变换侦测的进阶改造,每一个响应式对象身上都会绑有一个__ob__实例,在这一小节中,这个属性只能用作证明这个属性已经被转换为响应式,之后我们再谈它还有其他哪些用处
🌙数组响应式的侦听方式
我们上面实现的响应式只是针对对象来实现,而没有完成数组的响应式,两者之间还是有一定差别的
在vue2中,数组的响应式实现还要从数组的方法入手
我们对数组进行操作时,要通过数组的各种方法来进行增删改,所以我们就可以在方法中动手脚,让它在执行数组方法时,进行响应通知
具体实现:
定义一个对象,在对象内实现数组的所有基本方法,我们将这个对象叫做拦截器👇
const original = Array.prototype
export const arrayMethods = Object.create(original)//基于数组原型创建一个新对象
;[
'pop',
'push',
'shift',
'unshift',
'splice',
'sort',
'reverse'
].forEach(method => {
//缓存原始方法
const originalMethod = original[method]
//方法替换增强
Object.defineProperty(arrayMethods, method, {
value:function mutator(...args) {
//执行原始方法
return originalMethod.call(this, ...args)
},
enumerable: false,
writable: true,
configurable: true
})
});
拦截器,顾名思义,我们利用它来拦截数组原型,将原本的Array.prototype设为我们自己创建的拦截器
⭐原型替换
我们将原型替换放在Observer中进行
改造后的Observer类:
//检查__proto__是否可用
const hasProto = '__proto__' in {}
//获取所有key(包括不可枚举的)
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
export default class Observer {
//给构造方法传入要添加响应式的值
constructor(value) {
this.value = value
this.dep = new Dep()
let ob = this
//将ob绑到数组上,没有搞懂这里为什么写在这里,这样不是普通对象也会存在ob吗?
//经过查阅,普通对象上也会有ob,好像和后续$delete等方法有关
Object.defineProperty(value, '__ob__', {
enumerable:false,
get() {
return ob
},
set(newVal) {
ob = newVal
}
})
if(Array.isArray(value)) {
//修改原型/直接添加方法
const augment = hasProto?this.protoAugment : this.copyAugment
augment(value, arrayMethods, arrayKeys)
//遍历添加响应式
for(let i = 0;i < value.length;i++) {
observe(value[i])
}
}else {
this.walk()
}
}
walk() {
Object.keys(this.value).forEach(key => {
defineReactive(this.value, key, this.value[key])
})
}
protoAugment(target, src) {
console.log('完成原型替换')
target.__proto__ = src
}
copyAugment(target, src, keys) {
keys.forEach(key => {
Object.defineProperty(target, key, {
value:src[key],
enumerable:false,
writable:true,
configurable:true
})
});
}
}
根据浏览器是否支持隐式原型,我们分别通过protoAugment和copyAugment进行数组函数的增强
在不支持隐式原型的情况下,我们直接将增强后的方法放到数组身上,而不是修改原型
而在上面也通过for循环为数组每个元素执行observe,实现递归观测
⭐为数组绑定容器
在绑定容器前,我们先考虑一下,我们要在哪里进行更新通知?
数组无法通过定义setter来更新内容,而是通过数组方法来进行修改,所以我们要在增强的方法中动手脚
那么我们的依赖容器该定义在哪里呢?
如果我们和之前一样,定义到defineReactive的闭包中,数组方法就拿不到容器咯
所以我们需要另想其他方法
这里__ob__属性就派上用场啦!
我们将__ob__属性绑定到了数组对象身上,那么数组方法是不是就能访问到数组对象身上的__ob__属性了呢?
在原型上是无法直接访问对象属性的,但我们可以通过在原型上的方法中获取this来访问实例
修改后的拦截器:
const original = Array.prototype
export const arrayMethods = Object.create(original)
;[
'pop',
'push',
'shift',
'unshift',
'splice',
'sort',
'reverse'
].forEach(method => {
//缓存原始方法
const originalMethod = original[method]
//方法替换增强
Object.defineProperty(arrayMethods, method, {
value:function mutator(...args) {
this.__ob__.dep.notify()//通过__ob__获取dep容器
//执行原始方法
return originalMethod.call(this, ...args)
},
enumerable: false,
writable: true,
configurable: true
})
});
这样我们就实现了数组的响应式
🌙Watcher补充
- 我们在Watcher中可以接收一个回调函数,在数据更新时进行调用
- 我们可以通过传入对象表达式来生成getter,就可以很方便地为其传入复杂表达式(通过下述的parsePath函数完成)
export default class Watcher {
constructor(obj, expOrfn, cb) {
this.data = obj
this.callback = cb
this.getter = this.parsePath(expOrfn)
this.get()
}
get() {
global.target = this
this.value = this.getter(this.data)
global.target = null
return this.value
}
update() {
let oldVal = this.value
this.value = this.get()
this.callback()
}
parsePath(expOrfn) {
const bailRE = /[^\w.$]/
if(bailRE.test() || typeof expOrfn === 'function') {
return expOrfn
}
return (obj) => {
expOrfn.split('.').forEach(key => {
if(!obj) return
obj = obj[key]
});
return obj
}
}
}
以上我们就简单完成了vue响应式的实现,当然源码考虑的情况更多会有出入,但大体思想都囊括了,还有部分内容没有完善,比如$delete方法等,后期我有空了再补,下面附上Vue的源码👇
🏢源码
🌙DefineReactive
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 声明 dep,每个声明为响应式的值都有一个自己的 dep
const dep = new Dep()
// 获取属性描述符
const property = Object.getOwnPropertyDescriptor(obj, key)
// configurable 为 false 则 return
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
// 获取 getter 和 setter
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 设置 getter 和 setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// getter
get: function reactiveGetter () {
// 获取 value
const value = getter ? getter.call(obj) : val
// 在 pushTarget 函数中会赋值 Dep.target(初始值为 null,赋值为一个 watcher)
// 可以使用 popTarget 函数移除当前 Dep.target
// 保证只同时处理一个 watcher
// 在声明组件的 watcher 时会调用 watcher.get,将当前 watcher push进 targetStack,且 Dep.target = watcher
if (Dep.target) {
// 收集依赖
dep.depend()
if (childOb) {
// object 收集依赖
childOb.dep.depend()
if (Array.isArray(value)) {
// 数组收集依赖
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
// 获取当前 value
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
// 值并未变化,或都为 NaN
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
}
// 重新观察 newVal
childOb = !shallow && observe(newVal)
// 通知所有依赖更新
dep.notify()
}
})
}
🌙Observe
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 不是 object 或 是VNode则 return
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 已经被观察过了,直接使用 __b__
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve && // 需要被观察
!isServerRendering() && // 不是服务端渲染
(Array.isArray(value) || isPlainObject(value)) && // value 是 array 或 纯object
Object.isExtensible(value) && // 当前 value 是否可扩展
!value._isVue // 不是 Vue 实例
) {
// 创建新的观察者
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
🌙Observer
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 不是 object 或 是VNode则 return
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 已经被观察过了,直接使用 __b__
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve && // 需要被观察
!isServerRendering() && // 不是服务端渲染
(Array.isArray(value) || isPlainObject(value)) && // value 是 array 或 纯object
Object.isExtensible(value) && // 当前 value 是否可扩展
!value._isVue // 不是 Vue 实例
) {
// 创建新的观察者
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
🌙Dap
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()
}
}
}
源码及注释来源:juejin.cn/post/700697…