<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
const data = {name:'阿锋'}
const obj = new Proxy(data,{
get(target,key){
track(target,key)
return target[key]
},
set(target,key,newVal){
target[key] = newVal
trigger(target,key)
}
})
let bucket = new WeakMap()
function track(target,key) {
if(!activeEffect)return
let targetMap = bucket.get(target)
if(!targetMap){
bucket.set(target,(targetMap = new Map()))
}
let deps = targetMap.get(key)
if(!deps){
targetMap.set(key,(deps = new Set()))
}
deps.add(activeEffect)
activeEffect.deps.push(deps)
}
function trigger(target,key){
const targetMap = bucket.get(target)
if(!targetMap) return
const effects = targetMap.get(key)
const cloneDeps = new Set()
effects.forEach(effect=>{
if(effect!==activeEffect){
cloneDeps.add(effect)
}
})
cloneDeps.forEach(effect => {
if(effect.options.scheduler){
effect.options.scheduler(effect)
}else{
effect()
}
})
}
let activeEffect
let effectStack = []
function createEffect(fn,options={}){
const effectFn = () =>{
cleanup(effectFn)
activeEffect= effectFn
effectStack.push(effectFn)
const res = fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length -1]
return res
}
effectFn.deps = []
effectFn.options = options
if(!options.lazy){
return effectFn()
}
return effectFn
}
function cleanup(effectFn){
effectFn.deps.forEach(deps=>{
deps.delete(effectFn)
})
effectFn.deps.length = []
}
function traverse(source,seen = new Set()){
if(typeof source !=="object" || source === null || seen.has(source)){
return
}
seen.add(source)
for(let key in source){
traverse(source[key],seen)
}
return source
}
function watch(source,cb,options={}){
let newVal, oldVal,getter
if(typeof source === "function"){
getter = source
}else{
getter = () => traverse(source)
}
const job = () =>{
newVal = effectFn()
cb(oldVal,newVal)
oldVal = newVal
}
const effectFn = createEffect(()=> getter(),{
lazy:true,
scheduler:() =>{
if(options.flusg ==="post"){
const p = Promise.resolve()
p.then(job)
}else{
job()
}
}
})
if(options.immediate){
job()
}else{
oldVal = effectFn()
}
}
watch(()=>obj.name,(oldVal,newVal)=>{
document.body.innerText = `旧值:${oldVal}, 新值:${newVal}`
},{
immediate:true,
flush:"post"
})
setTimeout(()=>{
obj.name="细锋"
},3000)
</script>
</body>
</html>
总结:
本质上利用了副作用函数重新执行时的可调度性。一个 watch 本身会创建一个 effect,当这个 effect 依赖的响应式数据发生变化时,会执行该 effect 的调度器函数,即 scheduler。这里的 scheduler 可以理解为“回调”,所以我们只需要在 scheduler 中执行用户通过 watch 函数注册的回调函数即可。此外,我们还讲解了立即执行回调的 watch,通过添加新的 immediate 选项来实现,还讨论了如何控制回调函数的执行时机,通过 flush 选项来指定回调函数具体的执行时机,本质上是利用了调用器和异步的微任务队列。