Vue3题目
vue3已经使用有一段时间了,偶然间看到一个题目:
<div id="app"></div>
<script>
const $app = document.querySelector('#app')
let state = {
text: 'hello, world'
}
function effect() {
$app.innerText = state.text
}
effect()
setTimeout(() => {
// 2秒后希望app的内容变成hello Vue3
state.text = 'hello Vue3'
}, 2000)
</script>
A:思路很简单,拦截
state对象,在对text取值的时候,收集effect函数依赖,然后text赋值的时候,把收集的effect遍历执行就行。
Q:别口嗨,写出代码。
源码实现
第一版,核心思路两步:
- 收集依赖(effect函数)
- 触发依赖
const $app = document.querySelector('#app')
const dep = new Set()
const state = new Proxy({text: 'hello world'}, {
get (target, key) {
dep.add(effect)
return target[key]
},
set (target, key, newValue) {
target[key] = newValue
dep.forEach((fn) => fn())
}
})
function effect () {
$app.innerText = state.text
}
effect()
setTimeout(() => {
state.text = 'hello Vue3'
}, 2000)
点评
Q:功能是实现了,但是这里收集依赖是写死的
effect函数名,只要稍微变动一下题目,就不行了。
<div id="container">
<div id="app1"></div>
<div id="app2"></div>
</div>
const $app1 = document.querySelector('#app1')
const $app2 = document.querySelector('#app2')
const state = {
text: 'hello world',
text2: 'hello world2'
}
// 改变app1的值
function effect1() {
console.log('执行了effect')
$app1.innerText = state.text
}
// 改变app2的值
function effect2() {
console.log('执行了effect2')
$app2.innerText = state.text2
}
// 2秒钟之后两个div的值要分别改变
setTimeout(() => {
state.text = 'hello Vue3'
state.text2 = 'hello Vue3-2'
}, 2000)
支持多属性响应式的修改和注册
A:大意了,应该把
effect依赖函数通过某种机制主动注册,这样无论是匿名函数或者具名函数都会一视同仁。
const $app1 = document.querySelector('#app1')
const $app2 = document.querySelector('#app2')
const dep = new Set()
let activeEffect
const effect = function (fn) {
activeEffect = fn
fn()
}
const state = new Proxy({text: 'hello world', text2: 'hello world2'}, {
get (target, key) {
dep.add(activeEffect)
return target[key]
},
set (target, key, newValue) {
target[key] = newValue
dep.forEach((fn) => fn())
}
})
effect(function effect1 () {
$app1.innerText = state.text
})
effect(function effect2 () {
$app2.innerText = state.text2
})
setTimeout(() => {
state.text = 'hello Vue3'
state.text2 = 'hello Vue3-2'
}, 2000)
点评
Q:不错,思路灵活,变通很快嘛!不过还有一个问题,给
state上增加一个之前不存在的属性,你的dep却会把收集到的依赖执行一次,是不是有点浪费?能否做到efect中依赖了state中的什么值,其值改变了回调才被执行?
重新设置数据结构
A:苦思冥想,终于明白了第二个版本问题所在:没有在
effect函数与被操作的目标字段之间建立明确的联系。
const state = new Proxy({text: 'hello world'}, {
get (target, key) {
// 无论state什么属性被读取,都会执行get然后收集进dep
dep.add(effect)
return target[key]
},
set (target, key, newValue) {
target[key] = newValue
// 无论state上什么属性被改变,都会触发set,进而收集的依赖被执行
dep.forEach((fn) => fn())
}
})
故思考一番,可以用树状结构:
state
|__key
|__effectFn
画一个数据结构图来理解一下存储关系:
源码实现
const $app = document.querySelector('#app')
const dep = new WeakMap()
let activeEffect
const effect = function (fn) {
activeEffect = fn
fn()
}
const state = new Proxy({text: 'hello world', age: 30}, {
get (target, key) {
// activeEffect无值,意味着没有执行effect,无法收集依赖
if (!activeEffect) {
return
}
// 每个target在dep中都是一个Map类型: key => effects
let depsMap = dep.get(target)
// 第一次先创建联系
if (!depsMap) {
dep.set(target, (depsMap = new Map()))
}
// 根据当前读取的key,尝试读取key的effects函数
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
// 将激活的effectFn存进依赖管理中心
deps.add(activeEffect)
return target[key]
},
set (target, key, newValue) {
target[key] = newValue
// 读取dep
const depsMap = dep.get(target)
if (!depsMap) {
return
}
// 读取依赖当前key的effects
const effects = depsMap.get(key)
effects && effects.forEach((fn) => fn())
}
})
effect(() => {
$app.innerText = `${state.text}, are you ${state.age} years old?`
})
setTimeout(() => {
state.text = 'hello Vue3'
state.age = 18
}, 2000)
点评
Q:能进一步吗,这个只能对
state一个对象进行响应式处理,能再封装一下,像Vue3中reactive一样使用?
A:...
reactive抽象
const $app = document.querySelector('#app')
const dep = new WeakMap()
let activeEffect
const effect = function (fn) {
activeEffect = fn
fn()
}
// 依赖收集
function track (target, key) {
// activeEffect无值,意味着没有执行effect,无法收集依赖
if (!activeEffect) {
return
}
// 每个target在dep中都是一个Map类型: key => effects
let depsMap = dep.get(target)
// 第一次先创建联系
if (!depsMap) {
dep.set(target, (depsMap = new Map()))
}
// 根据当前读取的key,尝试读取key的effects函数
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
// 将激活的effectFn存进依赖管理中心
deps.add(activeEffect)
}
// 依赖触发
function trigger (target, key) {
// 读取dep
const depsMap = dep.get(target)
if (!depsMap) {
return
}
// 读取依赖当前key的effects
const effects = depsMap.get(key)
effects && effects.forEach((fn) => fn())
}
function reactive (state) {
return new Proxy(state, {
get (target, key) {
track(target, key)
return target[key]
},
set (target, key, newValue) {
target[key] = newValue
trigger(target, key)
}
})
}
const state = reactive({
name: '张三',
age: 30
})
effect(() => {
$app.innerText = `${state.name}, are you ${state.age} years old?`
})
setTimeout(() => {
state.name = 'hello Vue3'
state.age = 18
}, 2000)