1.介绍
本内容是为了方便自己以后查阅同时让基础比较薄弱的人也能看懂vue的源码,所以采用的方式比较啰嗦,勿喷。不喜欢直接划走即可!!!
这里的代码和vue的源码不是完全一样的。但是核心的代码以及思路是一样的。当你学会当前的代码再去阅读源码会容易很多
这里为了编写文档时方便 且方便理解 我将所有的内容全部写到一个文件中了,后续会在main分支将所有代码抽离
effect 这个API 做的事情就是 当获取数据(get)时,收集触发get的key和对应的函数的依赖关系,当修改数据(set)时,执行触发set的key对应的函数
不过在此之前你应该学会使用effect effect默认会先执行一次
这章节很重要 真的很重要 理解这个后续你就事半功倍了
2.编写代码
2.1.effect
首先我们要创建且导出effect 函数, vue3使用了类来创建响应式的Effect,下面是基本的代码结构 很简单对吧
class ReactiveEffect {
constructor(private fn) {}
run() {
return this.fn()
}
}
export function effect(fn) {
const _effect = new ReactiveEffect(fn)
_effect.run()
}
现在 我们来明确一件事 effect这个API的最终目的就是为了收集依赖,那么我们接下来的核心目的就是 当执行effect的时候让外界拿到当前正在执行的effect
let activeEffect = undefined
class ReactiveEffect {
constructor(private fn) {}
run() {
activeEffect = this
return this.fn()
}
}
现在我们来分析一下会有哪些极端的情况
// 情况一
effect(() => {
app.innerHTML += proxyObj1.age
})
proxyObj1.address
/** 这种情况 proxyObj1.address也会触发get 但是这个并没有被effect函数包裹,所以他并没有对应的effect函数
解决:执行当前的fn后让activeEffect为undefined 这样proxyObj1.address触发get 执行track函数的时候 activeEffect就是undefined了 就不会执行后续逻辑了
run() {
try {
activeEffect = this
return this.fn()
} finally {
activeEffect = undefined
}
}
*/
// 情况二 effect嵌套
effect(() => { // e1
app.innerHTML = proxyObj1.name
effect(() => { // e2
app.innerHTML += proxyObj1.age
})
proxyObj1.address
})
/**
*
上面这段代码 如果使用情况一的解决方案 还是会有问题
name 对应的effect是 e1
age 对应的effect是 e2
address 对应的effect是undefined 这就不对了 address应该对应e1
解决: vue2和早期的vue3使用的是栈来解决 但是这会多出一个全局的变量 不太好
现在的解决方案是 使用树结构来解决
原理是:给每个effect指定一个parent,指向的是当前effect的父effect,等到子effect执行完毕 在让activeEffect执行父effect 具体看上面完整代码
*/
解决方案
let activeEffect = undefined
class ReactiveEffect {
constructor(private fn) {}
parent = undefined
run() {
// try包裹是为了防止执行fn函数报错 影响后续代码
// 下面的代码解决了 情况一和情况二的问题
try {
this.parent = activeEffect
activeEffect = this
return this.fn()
} finally {
activeEffect = this.parent
}
}
}
至此 我们就能让外界拿到当前正在执行的effect了
2.2.分别在set和get设置对应的方法
现在我们需要在响应式对象的get方法中调用一个tarck函数用来收集依赖,在set方法中调用一个trigger方法来执行依赖
export const mutableHandles = {
get(target, key, receiver) {
// 解决情况二
if (!target[ReactiveFlags.IS_REACTIVE])
target[ReactiveFlags.IS_REACTIVE] = true
// 收集依赖
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const oldValue = target[key]
const r = Reflect.set(target, key, value, receiver)
// 判断两次更新的值是否相同 如果相同就不用二次执行 提高性能
if (oldValue !== value) trigger(target, key, value, oldValue)
return r
},
}
2.3.track
这个函数是为了保存key和当前执行的effect直接的关系的,将这个两者的依赖存储成如下的结构
weakMap => map => set
e g:{name:'zs'} => name => [effect1,effect2]
const targetMap = new WeakMap()
export function track(target, key) {
// 只用是effect中的才跟踪收集依赖
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 虽然set是不会重复的 但是内部肯定会做逻辑的 那么如果我们判断一下在加入会提升性能
let shouldTrack = !dep.has(activeEffect)
if (shouldTrack) {
dep.add(activeEffect)
}
}
至此我们已经能获取一张依赖表了
2.4.trigger
// 此方法目的是等到修改数据了从依赖表中拿到对应的effect 然后执行
export function trigger(target, key, newValue, oldValue) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
dep &&
dep.forEach((effect) => {
effect.run()
})
}
现在来看一下极端的情况
// 情况三
/**
* 当定时器更改obj.name 那么就会触发effect函数 执行obj.name = Math.random() 此时又会触发effect...如此往复成了死循环 因为random是随机数 */
effect(() => {
obj.name = Math.random()
app.innerHTML = obj.name
})
setTimeout(() => {
obj.name = 'wzng'
}, 2000)
解决方案
export function trigger(target, key, newValue, oldValue) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
dep &&
dep.forEach((effect) => {
if (effect !== activeEffect) effect.run()
})
}
2.5.cleanupEffect
看下面这个情况
// 情况四
// 如下代码 当我们执行定时器的内容 flag 已经是false了 然后在执行obj.name=‘xxx' 这时候不应该执行console.log
// 换句话来说就是 因为flag是false了就不应该再去收集name的依赖了,只用收集flag和age的依赖就行 这样会节约性能
// 做法 当我们在收集依赖的时候 收集当前effect和触发的key之间的关系 每次执行run的时候先将其清空
// 然后等到更新时再次收集 eg:更新的时候会触发get和set
// 说句白话就是 在当前的effect.deps中存储key对应的effect
effect(() => {
app.innerHTML = obj.flag ? obj.name : obj.age
})
setTimeout(() => {
obj.flag = false
setTimeout(() => {
console.log('name')
obj.name = 'xxx'
}, 1000)
}, 2000)
解决方案
function cleanupEffect(effect) {
const { deps } = effect
console.log(deps)
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
effect.deps.length = 0
}
export class ReactiveEffect {
constructor(private fn) {}
// 解决情况四
deps = []
...
run() {
// try包裹是为了防止执行fn函数报错 影响后续代码
// 下面的代码解决了 情况一和情况二的问题
try {
this.parent = activeEffect
activeEffect = this
// 解决情况四
cleanupEffect(this)
return this.fn()
} finally {
activeEffect = this.parent
// 复原parent 可要可不要 最好写上 保持好习惯
this.parent = undefined
}
}
}
export function track(target, key) {
...
if (shouldTrack) {
dep.add(activeEffect)
// 解决情况四
activeEffect.deps.push(dep)
}
}
// 此方法目的是等到修改数据了从依赖表中拿到对应的effect 然后执行
export function trigger(target, key, newValue, oldValue) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
dep &&
dep.forEach((effect) => {
// 做个判断 为了防止情况三 的发生
if (effect !== activeEffect) effect.run()
})
}
但是使用该解决方案后代码会进入死循环,原因如下
/*
跟新数据触发 trigger 会执行
dep &&
dep.forEach((effect) => {
if (effect !== activeEffect) effect.run()
})
接着执行 effect.run()
run中会执行cleanupEffect() 该还是会将当前的dep中的内容清空
然后 会执行fn fn中又会收集依赖 会对dep中继续add
此时代码还在forEach中 因为add了所以会继续往后遍历 继续执行run 然后就形成了死循环
下面是模仿上面的代码
*/
// 这个就是dep
const set = new Set([1])
// 遍历
set.forEach((item) => {
// 执行run方法
run()
})
function run() {
// 简单模仿
set.delete(1)
set.add(1)
}
解决方案
/*触发死循环的是因为在同一个set中添加 删除导致的 所以我们可以将其set中的内容放到一个数组 然后遍历数组即可*/
// 此方法目的是等到修改数据了从依赖表中拿到对应的effect 然后执行
export function trigger(target, key, newValue, oldValue) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
// 加个判断 看dep是否有值
if (!dep) return
// 解决情况四
const effects = [...dep]
effects &&
effects.forEach((effect) => {
// 做个判断 为了防止情况三 的发生
if (effect !== activeEffect) {
// 解决情况六
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
})
}
2.6.stop
官网的effect还提供了一种能力就是当数据变化时,不自动的更新,而是手动的更新,看下面的案列,这是官网提供的api$forceUpdate,底层就是stop
下面的effect是官方的effect 做了一件这样的事情 就是当数据变化时我们不希望自动更新,而是我们调用方法手动跟新
const runner = effect(() => {
app.innerHTML = obj.name
})
runner.effect.stop()
setTimeout(() => {
obj.name = 'lisi'
runner()
}, 2000)
我们来分析一下结构 可以看出effect返回的是一个函数,这个函数有effect属性(函数也是对象),effect中有stop方法,那么我们按照官网的effect来改造我们自己写的effect
export function effect(fn) {
// 创建一个响应式effect
const _effect = new ReactiveEffect(fn)
_effect.run()
// 绑定this 不然外界调用this会有问题
const runner = _effect.run.bind(_effect)
runner.effect = _effect
return runner
}
现在我们来分析一下怎么实现上述说的功能,说白了就是当调用stop方法的时候,清空当前的依赖关系即可(上面已经编写过了该方法cleanupEffect),这样等到set的时候去依赖表里找依赖关系找不到 就不会更新了 但是会有一个问题 虽然清空了依赖 但是当修改数据的时候又会重新的搜集依赖,所以我们需要一个标识来表示是否是活跃的 如果不是活跃的那么就代表了调用了stop 只有激活的时候才执行run中的逻辑 非激活状态直接调用fn即可 (当用户手动调用runner的时候要执行fn 这样就做到了手动跟新)
export class ReactiveEffect {
constructor(private fn) {}
// 解决情况四
deps = []
// 解决情况二 嵌套effect的问题
parent = undefined
// 标识是否是激活状态
active = true
run() {
// 如果是失活(调用了stop) 直接调用fn 不自动更新
if (!this.active) return this.fn()
// try包裹是为了防止执行fn函数报错 影响后续代码
// 下面的代码解决了 情况一和情况二的问题
try {
this.parent = activeEffect
activeEffect = this
// 解决情况四
cleanupEffect(this)
return this.fn()
} finally {
activeEffect = this.parent
// 复原parent 可要可不要 最好写上 保持好习惯
this.parent = undefined
}
}
stop() {
// 只有是激活状态下才执行代码 言外之意就是调用了stop就不执行逻辑 节省性能
if (this.active) {
this.active = false
// 清空依赖
cleanupEffect(this)
}
}
}
2.7.scheduler
effect还提供了一个功能就是 当数据修改之后,不使用原来的effect跟新而是使用我们自己提供的方法来跟新
这其实有点和watch类似,某个数据变化后调用回调函数
const runner = effect(() => {
app.innerHTML = obj.name
}, {
scheduler() {
app.innerHTML = 123
}
})
// 当数据修改 不使用effect中的函数来更新 而使用提供的scheduler来更新
setTimeout(() => {
obj.name = 'lisi'
}, 2000)
那么现在我们来分析一下实现的原理,其实原理是比较简单的,我们在使用effect时传入自己的方法,格式按照上面的来(官方的),那么当修改的数据的时候 调用trigger时 判断当前effect是否有scheduler 如果有就调用。没有就调用原来的effect
export class ReactiveEffect {
// 添加scheduler
constructor(private fn, public scheduler) {}
...
}
export function effect(fn, options: any = {}) {
// 创建一个响应式effect 这里传入scheduler
const _effect = new ReactiveEffect(fn, options.scheduler)
_effect.run()
// 绑定this 不然外界调用this会有问题
const runner = _effect.run.bind(_effect)
runner.effect = _effect
return runner
}
export function trigger(target, key, newValue, oldValue) {
...
effects &&
effects.forEach((effect) => {
// 做个判断 为了防止情况三 的发生
if (effect !== activeEffect) {
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
})
}