相信你们在面试的时候没被面试官少问vue的响应式原理,你们可能都会说经过发布订阅模式+数据劫持(Object.defineProperty)把对象里的属性转化为get和set,当属性被修改或访问就通知变化
,然而,大多数人可能只是知道这一层面,并无彻底理解。本文将从一个简单的例子出发,一步步深刻响应式原理。vue
举一个简单的例子,咱们先定义一个对象:react
const hero = {
hp: 1000,
ad: 100
}
复制代码
这里定义了一个英雄,hp为1000,ad为100。面试
如今咱们能够经过hero.hp
和hero.ad
来读写对应的属性值,可是这个英雄的属性被读写时,咱们并不知道。数组
这时候经过Object.defineProperty
就能够在对应的get
和set
来实现了。浏览器
let hero = {}
let val = 1000
Object.defineProperty(hero, 'hp', {
get() {
console.log('hp属性被读取了!')
return val
},
set(newVal) {
console.log('hp属性被修改了!')
val = newVal
}
})
复制代码
经过Object.defineProperty
方法,给hero
定义了一个hp
属性,这个属性在被读写的时候都会触发一段console.log
。如今来尝试一下:markdown
hero.hp
// -> 1000
// -> hp属性被读取了!
hero.hp = 4000
// -> hp属性被修改了!
复制代码
能够看到,英雄已经能够主动告诉咱们其属性的读写状况了,这也意味着,这个英雄的数据对象已是“可观测”的了。为了把英雄的全部属性都变得可观测,咱们能够想一个办法:模块化
/** * 使一个对象转化成可观测对象 * @param { Object } obj 对象 * @param { String } key 对象的key * @param { Any } val 对象的某个key的值 */
function reactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`个人${key}属性被读取了!`)
return val
},
set(newVal) {
console.log(`个人${key}属性被修改了!`)
val = newVal
}
})
}
/** * 把一个对象的每一项都转化成可观测对象 * @param { Object } obj 对象 */
function observable(obj) {
const keys = Object.keys(obj)
keys.forEach((key) => { reactive(obj, key, obj[key]) })
return obj
}
复制代码
如今可使用上面的方法来定义一个响应式的英雄对象。函数
const hero = observable({
hp: 1000,
ad: 100
})
复制代码
你们能够在控制台自行尝试读写英雄的属性,看看它是否是已经变得可观测的。学习
如今,对象已经可观测,任何读写操做他都会主动告诉咱们,若是咱们但愿在修改完对象的属性值以后,他能主动告诉他的其余信息该怎么作?假设有一个watcher
方法优化
watcher(hero, 'type', () => {
return hero.hp <= 1000 ? '后排' : '坦克'
})
复制代码
咱们定义了一个watcher
做为监听器,它监听了hero
的type
属性。这个type
属性的值取决于hero.hp
,换句话来讲,当hero.hp
发生变化时,hero.type
也应该发生变化,前者是后者的依赖。咱们能够把这个hero.type
称为计算属性。
watcher
的三个参数分别是被监听的对象、被监听的属性以及回调函数。回调函数返回一个该被监听属性的值。顺着这个思路,咱们尝试着编写一段代码:
/** * 当计算属性的值被更新时调用 * @param { Any } val 计算属性的值 */
function computed(val) {
console.log(`个人类型是:${val}`);
}
/** * 观测者 * @param { Object } obj 被观测对象 * @param { String } key 被观测对象的key * @param { Function } cb 回调函数,返回“计算属性”的值 */
function watcher(obj, key, cb) {
Object.defineProperty(obj, key, {
get() {
const val = cb()
computed(val)
return val
},
set() {
console.error('计算属性没法被赋值!')
}
})
}
复制代码
如今咱们能够把英雄放在监听器里面,尝试跑一下上面的代码:
watcher(hero, 'type', () => {
return hero.hp <= 1000 ? '后排' : '坦克'
})
hero.type
hero.hp = 4000
hero.type
// -> 个人hp属性被读取了!
// -> 个人类型是:后排
// -> 个人hp属性被修改了!
// -> 个人hp属性被读取了!
// -> 个人类型是:坦克
复制代码
这样看起来确实不错,可是咱们如今是经过hero.type
来获取这个英雄的类型,并非他主动告诉咱们的,若是但愿他的hp修改后能够当即告诉咱们该怎么作? ----依赖收集
当一个可观测的对象被读取后,会触发对应的get
和set
,若是在这里面执行监听器的computed
方法,可让对象发出通知吗?
因为computed
方法须要接受回调函数,而可观测对象内并没有这个函数,因此须要创建一个“中介”把可观测对象和监听器链接起来。
中介用来收集监听器的回调函数的值一级computed()
方法
这个中介就叫“依赖收集器”:
const Dep = {
target: null
}
复制代码
target
用来存放监听器里的computed
方法。
回到监听器,看看在什么地方把computed
赋值给Dep.target
/** * 观测者 * @param { Object } obj 被观测对象 * @param { String } key 被观测对象的key * @param { Function } cb 回调函数,返回“计算属性”的值 */
function watcher(obj, key, cb) {
// 定义一个被动触发函数,当这个“被观测对象”的依赖更新时调用
const onDepUpdated = () => {
const val = cb()
computed(val)
}
Object.defineProperty(obj, key, {
get () {
Dep.target = onDepUpdated
// 执行cb()的过程当中会用到Dep.target,
// 当cb()执行完了就重置Dep.target为null
const val = cb()
Dep.target = null
return val
},
set () {
console.error('计算属性没法被赋值!')
}
})
}
复制代码
咱们在监听器内部定义了一个新的onDepUpdated()
方法,这个方法很简单,就是把监听器回调函数的值以及computed()
给打包到一块,而后赋值给Dep.target
。这一步很是关键,经过这样的操做,依赖收集器就得到了监听器的回调值以及computed()
方法。做为全局变量,Dep.target
理所固然的可以被可观测对象的getter/setter
所使用。
从新看一下咱们的watcher实例:
watcher(hero, 'type', () => {
return hero.hp <= 1000 ? '后排' : '坦克'
})
复制代码
在它的回调函数中,调用了英雄的hp
属性,也就是触发了对应的get
函数。理清楚这一点很重要,由于接下来咱们须要回到定义可观测对象的reactive()
方法当中,对它进行改写:
/** * 使一个对象转化成可观测对象 * @param { Object } obj 对象 * @param { String } key 对象的key * @param { Any } val 对象的某个key的值 */
function reactive(obj, key, val) {
const deps = []
Object.defineProperty(obj, key, {
get() {
if (Dep.target && deps.indexOf(Dep.target) === -1) {
deps.push(Dep.target)
}
return val
},
set(newVal) {
val = newVal
deps.forEach((dep) => {
dep()
})
}
})
}
复制代码
能够看到,在这个方法里面咱们定义了一个空数组deps
,当get
被触发的时候,就会往里面添加一个Dep.target
。回到关键知识点Dep.target
等于监听器的computed()
方法,这个时候可观测对象已经和监听器捆绑到一块。任什么时候候当可观测对象的set
被触发时,就会调用数组中所保存的Dep.target
方法,也就是自动触发监听器内部的computed()
方法。
至于为何这里的deps
是一个数组而不是一个变量,是由于可能同一个属性会被多个计算属性所依赖,也就是存在多个Dep.target
。定义deps
为数组,若当前属性的set
被触发,就能够批量调用多个计算属性的computed()
方法了。
完成了这些步骤,基本上咱们整个响应式系统就已经搭建完成,下面贴上完整的代码:
/** * 定义一个“依赖收集器” */
const Dep = {
target: null
}
/** * 使一个对象转化成可观测对象 * @param { Object } obj 对象 * @param { String } key 对象的key * @param { Any } val 对象的某个key的值 */
function reactive(obj, key, val) {
const deps = []
Object.defineProperty(obj, key, {
get() {
console.log(`个人${key}属性被读取了!`)
if (Dep.target && deps.indexOf(Dep.target) === -1) {
deps.push(Dep.target)
}
return val
},
set(newVal) {
console.log(`个人${key}属性被修改了!`)
val = newVal
deps.forEach((dep) => {
dep()
})
}
})
}
/** * 把一个对象的每一项都转化成可观测对象 * @param { Object } obj 对象 */
function observable(obj) {
const keys = Object.keys(obj)
keys.forEach((key) => { reactive(obj, key, obj[key]) })
return obj
}
/** * 当计算属性的值被更新时调用 * @param { Any } val 计算属性的值 */
function computed(val) {
console.log(`个人类型是:${val}`);
}
/** * 观测者 * @param { Object } obj 被观测对象 * @param { String } key 被观测对象的key * @param { Function } cb 回调函数,返回“计算属性”的值 */
function watcher(obj, key, cb) {
// 定义一个被动触发函数,当这个“被观测对象”的依赖更新时调用
const onDepUpdated = () => {
const val = cb()
computed(val)
}
Object.defineProperty(obj, key, {
get() {
Dep.target = onDepUpdated
// 执行cb()的过程当中会用到Dep.target,
// 当cb()执行完了就重置Dep.target为null
const val = cb()
Dep.target = null
return val
},
set() {
console.error('计算属性没法被赋值!')
}
})
}
const hero = observable({
hp: 1000,
ad: 100
})
watcher(hero, 'type', () => {
return hero.hp <= 1000 ? '后排' : '坦克'
})
console.log(`英雄初始类型:${hero.type}`)
hero.hp = 4000
// -> 个人hp属性被读取了!
// -> 英雄初始类型:后排
// -> 个人hp属性被修改了!
// -> 个人hp属性被读取了!
// -> 个人类型是:坦克
复制代码
上述代码在浏览器控制台可直接执行
在上面的例子中,依赖收集器只是一个简单的对象,其实在reactive()
内部的deps
数组等和依赖收集有关的功能,都应该集成在Dep
实例当中,因此咱们能够把依赖收集器改写一下:
class Dep{
constructor() {
this.deps = []
}
depend() {
if (Dep.target && this.deps.indexOf(Dep.target) === -1) {
this.deps.push(Dep.target)
}
}
notify() {
this.deps.forEach((dep) => {
dep()
})
}
}
Dep.target = null
复制代码
一样的道理,咱们对observable和watcher都进行必定的封装与优化,使这个响应式系统变得模块化:
class Observable{
constructor(obj) {
return this.walk(obj)
}
walk(obj) {
const keys = Object.keys(obj)
keys.forEach((key) => {
this.reactive(obj, key, obj[key])
})
return obj
}
reactive(obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
dep.depend()
return val
},
set(newVal) {
val = newVal
dep.notify()
}
})
}
}
class Watcher{
constructor(obj, key, cb, computed) {
this.obj = obj
this.key = key
this.cb = cb
this.computed = computed
return this.defineComputed()
}
defineComputed() {
const self = this
const onDepUpdated = () => {
const val = self.cb()
this.computed(val)
}
Object.defineProperty(self.obj, self.key, {
get() {
Dep.target = onDepUpdated
const val = self.cb()
Dep.target = null
return val
},
set() {
console.error('计算属性没法被赋值!')
}
})
}
}
复制代码
尝试运做一下:
const hero = new Observable({
hp: 1000,
ad: 100
})
new Watcher(hero, 'type', () => {
return hero.hp <= 1000 ? '后排' : '坦克'
}, (val) => {
console.log(`个人类型是:${hero.type}`)
})
console.log(`英雄初始类型:${hero.type}`)
hero.hp = 4000
// -> 英雄初始类型:后排
// -> 个人类型是:坦克
// -> 4000
复制代码
上述代码在浏览器控制台可直接执行
上述代码,是否是和vue
里的源码很类似?其实思路是同样的,本文把核心部分挑出供你们食用。若是你们在学习vue
源码时,不知如何下手,但愿这篇文章能给你提供帮助。做者也是参考了许多他人的思想和不断的尝试才掌握。
本文是做者蛮早之前的笔记从新整理了一篇供你们食用,若有意见或其余问题欢迎你们指出,若是对你有帮助请记得点赞关注收藏三连击。