前言
网上冲浪时常被推荐霍春阳 的 《Vue.js设计与实现》 ,Vue.js也是目前自己能吃上一口饭的主要工具,所以前段时间就开始阅读这本书,到目前为止也阅读了差不多三分之二的内容(👽宇宙安全声明:电子书是方便阅读,我确实是买了正版书的)
如果有条件的话非常推荐看这本书的原版,真的写得通俗易懂。
写下文章巩固看过的知识点(其实也是后面的章节有点看不下去了,等会后面不看了前面的又忘掉了,双输!😭),第一篇文章记录非常核心的------响应式系统的基本实现。
可能需要预先了解的概念:
- Proxy
- Set
- Map
- WeakMap
响应式数据和副作用函数概念
副作用函数
什么是副作用函数?例如document.body.innerText是浏览器中全局能够访问到的变量,如果在某个函数中修改了这个值,毫无疑问这个行为会影响到其他使用了这个值的地方,那么这个函数就可以被称为一个副作用函数。当然,对于自己声明的全局变量也是同样的道理。
javascript
function effect() {
document.body.innerText = 'hello vue3'
}
响应式数据
什么是响应式数据?当数据发生变化 时, 与其有联系的副作用函数可以被执行,那么这个数据就可以称为响应式数据。如下面的例子, effectFn中使用到了obj.text,那么如果obj.text发生改变后, effectFn也能够被执行,那么这个数据就是响应式数据。
javascript
let obj = {
text: 'hello responsive data'
}
function effectFn() {
document.body.innerText = obj.text
}
effectFn()
obj.text = 'hello vue3'
响应式数据的基本实现
在上面的例子中,effectFn执行过程中obj.text会被读取(get) ,如果我们可以能实现obj.text被读取时把effectFn收集起来,在obj.text设置(set) 时再执行被收集的函数,那么就基本实现了响应式数据,如下:
javascript
// bucket是用于收集副作用函数的变量
let bucket = new Set()
let obj = { text: 'hello responsive data' }
const proxyObj = new Proxy(obj, {
get: function (target, key) {
// 变量读取时将副作用函数收集下来,目前这里是写死了函数effectFn
bucket.add(effectFn)
return target[key]
},
set: function (target, key, val) {
target[key] = val
// 变量赋值时依次执行收集的副作用函数
for (const item of bucket) {
item()
}
// set中需要返回true
return true
}
})
function effectFn() {
// 这边需要注意,要操作的是代理后的对象proxyObj
document.body.innerText = proxyObj.text
}
// 需要执行一次effectFn,否则你怎么敢说你和proxyObj有关系的,蚝跌油
effectFn()
// 2s后修改数据
setTimeout(() => {
proxyObj.text = 'hello vue3'
}, 2000)
目前实现的效果如下:
上面的实现中存在的不足有:
- 收集副作用函数时是硬编码将effectFn收集起来的,改变了函数名称的话get中的具体实现也需要做更改
- 目前是整个代理对象和副作用函数建立了联系,具体到某一个字段,例如obj的text字段和副作用函数并没有建立联系。试想一下我的obj中有另外的字段例如content,如果我更改了content的值,set的过程很明显也会执行一遍,那么收集到的副作用函数也会执行一遍,但副作用函数中其实没有使用到这个字段,这和我们的期望并不符合。
下面我们尝试解决这两个问题。
实现一个较为完善的响应式系统
响应式系统工作的基本流程:
- 读取变量时,将副作用函数收集
- 设置变量时,执行收集的副作用函数
函数收集时是硬编码的不足
先解决上一节中收集副作用函数是硬编码 的不足,为此我们添加一个变量用于记录当前正在执行的函数,并将副作用函数用watchEffectFn包裹起来,用该函数来执行副作用函数,如下:
javascript
// 全局声明一个变量用于记录当前正在执行的副作用函数
let activeEffectFn = null
function watchEffectFn(fn) {
activeEffectFn = fn
fn()
activeEffectFn = null
}
修改代理规则如下:
javascript
const proxyObj = new Proxy(obj, {
get: function (target, key) {
if (activeEffectFn) {
// 当前正在执行的fn被收集
bucket.add(activeEffectFn)
}
return target[key]
},
set: function (target, key, val) {
target[key] = val
for (const item of bucket) {
item()
}
return true
}
})
watchEffectFn(() => {
document.body.innerText = proxyObj.text
})
setTimeout(() => {
proxyObj.text = 'hello vue3'
}, 2000)
现在整个过程为:
- 执行watchEffectFn,具体的函数fn被传递过来,activeEffectFn被赋值
- 执行传递具体的函数fn,此时触发get,fn被收集
- get执行完毕,fn执行完毕,activeEffectFn置空
- 2s后修改值,set被触发,依次执行收集到的函数
key与函数之间无联系
然后存储函数的时候需要在具体的key和副作用函数之间建立联系 ,为此我们需要建立新的存储副作用函数的数据结构,达到收集时按key收集,触发时按key寻找对应的副作用函数集合的效果。
重新设计后的存储结构如下:
- weakMap中的每一项的key是需要代理的对象(target-1,target-2...)
- key-1,key-2...是对象中的具体的key,具体值是对应的副作用函数集合
javascript
weakMap: {
target-1: {
key-1: Set(effect-1, effect-2, ..., effect-n),
key-2: Set(effect-1, effect-2, ..., effect-n),
...
},
target-2: {
key-1: Set(effect-1, effect-2, ..., effect-n),
key-2: Set(effect-1, effect-2, ..., effect-n),
...
}
}
图源《Vue.js设计与实现》
新增track 函数用来收集副作用函数,新增trigger用来触发副作用函数
javascript
// 所有对象的副作用集合
const targetBucket = new WeakMap()
const proxyObj = new Proxy(obj, {
get: function (target, key, receiver) {
// 需要把副作用函数按照key放进set里
track(target, key, receiver)
return target[key]
},
set: function (target, key, newVal, receiver) {
target[key] = newVal
// 获取key的对应的副作用函数,并执行
trigger(target, key, newVal, receiver)
return true
}
})
/**
* 副作用函数收集
*/
function track(target, key, receiver) {
// 没有正在执行的副作用函数直接返回, 无需收集
if (!activeEffectFn) return
let depsMap = targetBucket.get(target)
if (!depsMap) {
// 以每一个需要代理的对象为key,没有的话就新建
targetBucket.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
// 以对象的key为key,存储该key对应的所有副作用函数
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffectFn)
}
/**
* 副作用函数触发
*/
function trigger(target, key, newVal, receiver) {
const depsMap = targetBucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(fn => {
fn()
})
}
小结
由此我们就实现了一个最基本的响应式系统,当时这个系统自然还有其他的问题,后面我们再继续完善这个系统。有什么写的不对的地方或者什么疑问可以留言,希望文章有帮助到你~