ref: ref
是用于监听原始值的, 因为 js 原始值不能引用内存地址,就算修改了也无从知晓,因而可以将其包装成一个对象,这样就可以获取到这个变量的引用,监听修改。 ref
只有一个 value 属性。(为了方便监听才包装成value对象里面)
// 以下按序号阅读,可以复制全部运行。
// 4.2 新建一个保存订阅的 WeakMap<object, Set>,使用 WeakMap 防止内存泄漏
const subscription = new WeakMap()
// 7.2 当前需要加入订阅列表的回调
let currentSub
// ---- 例子 ----
const count = ref(0)
const double = computed(() => {console.log('computed'); return count.value * 2}) // log computed #这里没做 lazy
watch(() => console.log(count.value, double.value)) // log 0 0
// log computed
count.value++ // log 1 2
console.log(double.value) // log 2 # 没有 log computed 证明缓存了
// -------------
// 1. 先写 ref 函数,已知使用 proxy
function ref(value) {
const innerObj = { value }
return new Proxy(innerObj, {
get(obj, key, receiver) {
if (key !== 'value') { return }
// 9.1 收集依赖,即 将当前订阅放入到每个触发了 getter 的变量的订阅列表中
track(obj, key)
return Reflect.get(obj, key, receiver)
},
// 2. 暂不知道如何收集依赖,先写 set,修改变量就要通知订阅
set(obj, key, v, receiver) {
if (key !== 'value') { return false }
const res = Reflect.set(obj, key, v, receiver)
// 3.1 通知该变量订阅的修改
trigger(obj, key)
return res
}
})
}
// 9.2 收集依赖
function track(obj, key) {
if (!currentSub) { return }
let subList = subscription.get(obj)
if (!subList) {
subList = new Set()
subscription.set(obj, subList)
}
subList.add(currentSub)
}
// 3.2 通知
function trigger(obj, key) {
// 4.1 所有订阅应该在一个列表上才能通知到
// 5. 获取当前监听变量的订阅列表
const subList = subscription.get(obj) // Set<Function>
if (!subList) { return }
// subList.forEach((cb) => cb())
// 13. 先执行 computed 再执行 watch
Array.from(subList)
.sort((a, b) => Number(!!b.computed) - Number(!!a.computed))
.forEach(cb => cb())
}
// 6. 既然发现有一个订阅列表了,那么 watch 的时候就是将订阅放入对应的列表
function watch(cb, opt = {}) {
// 12. 标记 computed
cb.computed = opt.computed
// 7.1 怎么放? 可以使用一个全局变量来标记当前的订阅
currentSub = cb
// 8. 执行一下,这样可以触发变量的 getter
cb()
// 10. 收集完成,清理
currentSub = null
}
// 11. 最后实现 computed,其实就是 watch 的 lazy 版,触发订阅时注意要先 computed 再到 watch
function computed(getter, setter) {
// 数值要缓存起来,不要每次都算
let value
// 这里将订阅放到 computed 所依赖的变量的订阅列表,就是 count
watch(() => { value = getter() }, { computed: true })
return new Proxy({}, {
get(obj, key, receiver) {
if (key !== 'value') { return }
return value
},
set(obj, key, v, receiver) {
if (key !== 'value' || !setter) { return false }
return setter(obj, key, v, receiver)
}
})
}
reactive:
reactive 需要实现对象的遍历监听以及属性增删的监听。
const subMap = new WeakMap()
let currentCb
// 保存已经 reactive 的对象
const alreadyReactive = new WeakMap()
// --- 例子 ---
const state = reactive({
count: 0,
obj: {a: 0},
arr: [1,2,3]
})
watch(() => { console.log('count:', state.count) })
watch(() => { console.log('obj.a:', state.obj.a) })
// 这里有问题了,watch 的时候只收集到子对象的,没收集到子对象的属性,那么就监听不到了其属性修改
watch(() => { console.log('obj:', state.obj) })
watch(() => { console.log('arr:', state.arr) })
state.count++ // log count: 1
state.obj.a++ // log obj.a: 1
// 这里就监听不到了,要查看源码才知道什么做
state.arr.push(4)
state.arr.pop()
state.arr[0] = 11
// ---------
function reactive(target) {
const cache = alreadyReactive.get(target)
if (cache) { return cache }
const proxy = new Proxy(target, {
get(obj, key, receiver) {
track(obj, key)
const res = Reflect.get(obj, key, receiver)
// 如果属性是对象,就返回一个 reactive 包装的对象,递归遍历
// 由于频发触发 reacitve 函数有性能问题,因此可以缓存起来
// 使用 alreadyReactive 保存已包装过的对象
return typeof res === 'object' ? reactive(res) : res
},
set(obj, key, value, receiver) {
const res = Reflect.set(obj, key, value, receiver)
trigger(obj, key)
return res
}
})
alreadyReactive.set(target, cache)
return proxy
}
// 由于对象有多个属性,每个属性都有对应的订阅列表
// 因此容器 subMap 的数据结构为 WeakMap<object, Map<string, Set>
function trigger(obj, key) {
const target = subMap.get(obj)
if (!target) { return }
const sub = target.get(key)
if (!sub) { return }
sub.forEach(cb => cb())
}
function track(obj, key) {
if (!currentCb) { return }
let target = subMap.get(obj)
if (!target) {
target = new Map()
subMap.set(obj, target)
}
let sub = target.get(key)
if (!sub) {
sub = new Set()
target.set(key, sub)
}
sub.add(currentCb)
}
function watch(cb) {
currentCb = cb
cb()
currentCb = null
}
总结:
1.computed 计算属性具有缓存性质,会先在内部定义顶层一个value变量用作闭包赋值,
然后又调用watch函数,把computed的第一个参数赋值给刚刚在顶层定义的value变量,
并且放进watch函数,并且第2个参数标记为true表示属于计算属性。
最后返回一个new Proxy,这样调用的时候就可以.value获取值
简单形容:利用闭包性质达到缓存目的,再调用watch函数,接着返回new Proxy
2.watch:实际接收2个参数,第1个是函数,第2个参数是computed计算属性里面调用watch函数时候的标记。
先把标记赋值给第一个参数,再把当前需要监听观察的(就是第1个参数)赋值给外层的一个全局变量保存。
再直接执行第一个参数,主要为了触发ref或者reactive里面的get函数,最后把全局变量清空。
只有在watch的时候,内部才会赋值给全局变量再触发变量ref、reactive的get函数,进行依赖收集。
再简单点的形容:其实就是触发变量里面get函数
3.ref: 把传进来的参数变成key为value的对象,并直接返回一个new Proxy,监听的就是当前的value对象。
读取时触发get函数,进行track收集依赖,最后使用Reflect.get返回对应的值。
重点时track收集依赖:把当前obj,key传递2个参数过去,
前提是通过watch函数进入的才会进行依赖收集,否则时不会进行收集,因为只有在watch的时候才会赋值全局变量。
track:1.首先会在函数外部顶层定义使用map的数据结构变量subscription。
2.以obj为key,new Set为value,放进定义map的全局变量。
3.对上面的new Set value值进行add新增当前全局变量
trigger:当对ref值设置的时候会触发set函数:直接Reflect.set返回值。
并且也会trigger携带2个参数通知变量被修改。
trigger:里面做了什么?因为get的时候进行了依赖收集,
所以首先用map的get获取key值,里面就是存放的set函数,
再对获取的依赖进行遍历运行里面的值