4.响应系统的作用与实现

发布于:2025-07-03 ⋅ 阅读:(18) ⋅ 点赞:(0)

1.响应式数据和副作用函数

//副作用函数
function effect(){
  document.body.innerText = '我是王惊涛'
}

effect函数会修改body的文本内容,如果其他函数也会引用body.innerText这一属性,那么effect的执行就会影响其他函数的执行,这个时候就可以说effect函数产生了副作用。

//一个全局变量
let globalA = 10
//副作用函数
const fun1 = ()=>{
    globalA = 100  //修改了全局变量
}
//引用这一变量的函数
const fun2 = ()=>{
    return globalA+1
}

现在当globalA发生变化时,我们希望fun2可以重新执行,用最新的变量去计算,这就是响应式数据的需求所在,一个变量发生变化,引用该变量值的地方都应该触发更新。

2.响应式数据的基本实现

拦截一个属性的读取和修改操作,当这个属性被读取或修改的时候,可以实时的进行一些自定义的操作(写自己的函数),就是响应式数据所要完成的核心功能。

在js中,有两个api可以做到这一点:Object.definePropertyproxy

名称 api版本 vue对应版本 特性
Object.defineProperty ES5 vue2 有缺陷,对于数组和后续添加属性的读写无法直接监控到
proxy ES6 vue3 可以进行各种拦截监听,功能强大
const bucket = new Set()   //一个用于存放副作用函数的桶

let data = {text:'我是王惊涛'}
const data_Proxy = new Proxy(data,{
    get(target,key){   //拦截get方法,只要有人访问data_Proxy中的属性,就会触发这个函数,target代表对象整体,key代表被访问的属性名
        console.log('触发了读取操作')
        bucket.add(fun_rely)  //这里就是解决之前提到的问题,谁用到这个变量了,就会将这个函数保存到存放桶里,修改的时候可以调用这个函数
        return target[key]  //返回该对象的被访问属性值
    },
    set(target,key,newVal){  //拦截set方法,只要有人修改data_Proxy中的属性,就会触发这个函数,newVal代表新的赋值
        console.log('触发了set操作')
        target[key] = newVal  //赋值
        bucket.forEach(fn=>fn())  //调用所有因为修改这个值而被影响的函数
        return true  //代表修改成功,得写
    }
})
let fun_rely = ()=>{  //访问函数
    console.log(data_Proxy.text,'fun_rely函数被调用')
}


fun_rely()  //读
data_Proxy.text = '我还是王惊涛'  //写

在nodejs环境中运行,结果如下:

1724317308955

3.设计一个完善的响应式系统

分析

在上面的代码中,存在一个很明显的缺陷,访问这个对象的任何属性,都会触发桶里的所有回调函数。因为我们没有针对具体某个属性的读写去区分,就好比你收到了A给你发的消息,结果你却给通讯录里的所有都一起回复了。某一属性被修改,应该只触发引用它的回调函数,而不是桶里的所有函数。

所以为了给被操作属性和副作用函数之间建立关联关系,我们得修改一下桶的数据结构。

桶是一个树形的数据结构,一个对象内可能会有多个响应式属性,而每一个属性可能也会有多个副作用函数跟其关联,最后形成了这样的一组数据

target是对象,key是具体的某一个属性,effectFn是关联的副作用函数

--obj1  (target)

    -- key1  (key)

        -- fun1  (effectFn)

        -- fun2  (effectFn)

    -- key2  (key)

        -- fun3  (effectFn)

--obj2  (target)

    -- key1  (key)

         -- fun5  (effectFn)

         -- fun6  (effectFn)

对obj1[key1]进行读取时,fun1和fun2都会添加到这个桶里面,并且和key1建立练习。当obj1[key1]被修改时,就会触发fun1和fun2这两个副作用函数,这样就达到了响应式的效果

很简答的总结:getter中收集依赖(副作用函数),setter中触发依赖

以下为一段自己模拟的源码和测试用例,请耐心观看…

代码
/*桶的结构
  /.../ 表示数据的类型

  bucket/weakMap结构/ :{
  0:{
       key/object结构/:{text:'我是王惊涛'} 
       value/Map结构/: {
                         key/属性名,字符串结构/:'text',
                         value/函数,副作用函数/:  ()=>{console.log('副作用函数执行') return data.text}
                       }
    }
  }

这就是桶的结构,方便大家去看下面的代码

*/

const data = {text:'我是王惊涛'}


//完善响应式的代码(模拟源码)

let activeEffect = undefined    //用一个全局变量存储副作用函数
const bucket = new WeakMap()    //用来收集依赖的容器
function effect(fn){   //注册副作用函数
    activeEffect = fn  
    fn()   //假定这里的fn就是用户fun函数,通过调用想获取data.text,从而触发了getter操作,进行收集依赖
}


const data_Proxy = new Proxy(data,{
    //读取
    get(target,key){
      if(!activeEffect) {
        //副作用函数没有注册,如果注释掉上面的effect(fun),这里就会是一个普通的对象属性查询,只会return对应的值,然后结束函数的执行
        return target[key]
      }
      //定义一个变量,从桶中获取当前访问对象所对应的值
      let depsMap = bucket.get(target)  
      if(!depsMap){
        //depsMap有效时,在桶中插入值,key为target对象,value是一个map容器
        bucket.set(target,(depsMap = new Map()))  
      }
      //定义deps,map结构中对应属性名称值
      let deps = depsMap.get(key)  
      if(!deps){
        //deps无效时,map容器插入值,key为键,值是一个Set,可用来存放多个副作用函数
        depsMap.set(key,(deps = new Set()))  
      }
      //将副作用函数插入set列表中,从而形成与target[key]的依赖
      deps.add(activeEffect)  
      return target[key]
      /*
      名称             数据类型            具体含义
      target          对象                data
      key             属性名              text
      bucket          weakMap            最外层容器,key为target,value为depsMap
      depsMap         Map                第二层容器,key为key(属性值),value为deps
      deps            Set                最后一层容器,每个元素都是一个副作用函数
      */

    },
    //写入
    set(target,key,newVal){
      //赋上最新的值
      target[key] = newVal
      //根据对象,去查该对象在桶中所对应的值
      const depsMap = bucket.get(target)
      if(!depsMap){
        //没有查找到,说明并没有进行响应式的注册
        return
      }
      //查询key所对应的Set
      const effects = depsMap.get(key)
      //执行和该key有关的所有依赖函数,触发完成所有位置的更新
      effects && effects.forEach(fn => fn())
    }
})


//测试代码
const fun1 = ()=>{
    console.log('副作用函数1执行')
   return data_Proxy.text
}
const fun2 = ()=>{
   console.log('副作用函数2执行')
   return data_Proxy.text + ',哈哈哈'
}
effect(fun1)  //注册fun1
effect(fun2)  //注册fun2
console.log(data_Proxy.text,'读')
data_Proxy.text = '我还是王惊涛'
console.log(data_Proxy.text,'读')
console.log(bucket,'桶结构')
执行效果

1724392954778

依赖图形结构(官方)

1724393104293

weakMap和Map的使用区别

在上面的容器中使用了weakMap和Map,区别在于如果两个容器都插入一个以对象为key的键值对,Map是强引用类型,即时这个对象消失了,不会再调用了,垃圾回收机制也不会对其进行回收,会一直存在内存中;而weakMap是弱引用类型,如果这个对象不会再引用,垃圾机制就会将其回收,不会造成内存溢出

无注释简练代码(增加track和trigger函数)
//创建track和trigger优化之前代码,没有功能上的新增

let activeEffect = undefined   
const bucket = new WeakMap()  
function effect(fn){  
    activeEffect = fn  
    fn()   
}
const data = {text:'我是王惊涛'}
const data_Proxy = new Proxy(data,{
    get(target,key){
      track(target,key)
      return target[key]

    },
    set(target,key,newVal){
      target[key] = newVal
      trigger(target,key)
    }
})

function track(target,key){
    if(!activeEffect) {
        return target[key]
      }
      let depsMap = bucket.get(target)  
      if(!depsMap){
        bucket.set(target,(depsMap = new Map()))  
      }
      let deps = depsMap.get(key)  
      if(!deps){
        depsMap.set(key,(deps = new Set()))  
      }
      deps.add(activeEffect) 
}
function trigger(target,key){
    const depsMap = bucket.get(target)
    if(!depsMap){
      return
    }
    const effects = depsMap.get(key)
    effects && effects.forEach(fn => fn())
}

将get和set中的逻辑分裂到其他函数中,也是为了日后的方便维护,将逻辑拆分成功能单独的函数,可以有效降低重复逻辑的数量,极大提升灵活性。

4.分支切换与cleanup

有如下场景

const data = {ok:true,text:'我是王惊涛'}
const obj = new Proxy(data,{...})
effect(function effectFn(){
    document.body.innerText = obj.ok?obj.text:'not'
})

如果obj.ok初始值为true,会触发obj.text的读取,副作用函数effectFn分别被data.ok和data.text所对应的依赖集合收集。当字段obj.ok修改为false,触发了副作用函数的调用,因为三元表达式的原因,obj.text不符合判断条件,就不会被读取,只会触发obj.ok的操作,所以,理想情况下,副作用函数effectFn不应该被obj.text所对应的依赖收集。

但是按照之前的代码来看,obj.ok修改为false,effectFn会执行。如果修改data.text,effectFn函数还是会执行,这个就不应该了。在obj.ok为false时,data.text修改不应该触发依赖函数。

解决思路:

所以,每次执行副作用函数时,可以先把他从所有与之关联的依赖集合中删除。

当副作用函数执行完毕后,会重新建立联系,但是在新的联系中不会包含遗留的副作用函数。而这个操作前提是要知道那些依赖集合中包含了这个副作用函数。

/**
 * 场景
 * const data = {ok:true,text:'我是王惊涛'}
const obj = new Proxy(data,{...})
effect(function effectFn(){
    document.body.innerText = obj.ok?obj.text:'not'
})
 */

//增强
let activeEffect = undefined   
const bucket = new WeakMap() 
function effect(fn){  
    const effectFn = ()=>{
        //清除原有的副作用函数
        cleanup(effectFn)
        activeEffect = effectFn
        fn()
    }
    //添加一个用来存储所有与该副作用函数相关的依赖集合
    effectFn.deps = []
    effectFn()   
}

const data = {ok:true,text:'我是王惊涛'}
const data_Proxy = new Proxy(data,{
    get(target,key){
      track(target,key)
      return target[key]

    },
    set(target,key,newVal){
      target[key] = newVal
      trigger(target,key)
    }
})

function track(target,key){
    if(!activeEffect) {
        return target[key]
      }
      let depsMap = bucket.get(target)  
      if(!depsMap){
        bucket.set(target,(depsMap = new Map()))  
      }
      let deps = depsMap.get(key)  
      if(!deps){
        depsMap.set(key,(deps = new Set()))  
      }
      deps.add(activeEffect) 
      activeEffect.deps.push(deps)   //新增
}
function trigger(target,key){
    const depsMap = bucket.get(target)
    if(!depsMap){
      return
    }
    const effects = depsMap.get(key)
    //为了防止刚删除又创建,再删除再创建的无限死循环
    const effectsToRun = new Set(effects)
    effectsToRun.forEach(effectFn => effectFn())
    // effects && effects.forEach(fn => fn())
}
//清除副作用函数的函数
function cleanup(effectFn){
   for(let i = 0;i<effectFn.deps.length;i++){
      const deps = effectFn.deps[i]
      deps.delete(effectFn)
   }
   effectFn.deps.length = 0
}

//测试代码
const fun = ()=>{
   return data_Proxy.ok?data_Proxy.text:'无文本'
}

effect(fun)

console.log(fun(),'第一次执行')
setTimeout(()=>{
    data_Proxy.ok = false
    console.log(fun(),'第二次执行')
},3000)

5.嵌套的effect与effect栈

effect函数是可以嵌套执行的,例如每个vue文件的内容都是一个render(),组件之间的嵌套关系,也会让effect去嵌套执行

//伪代码,effect嵌套

let data = {foo:'foo值',bar:'bar值'}
let effect = (fn)=>{
    /** */
}
let temp1,temp2
effect (function effectFn1(){
    console.log('effectFn1执行')
    effect(function effectFn2(){
        console.log('effectFn2执行')
        temp2 = data.bar
    })
    temp1 = data.foo
})

理想状态下,副作用函数和对象属性之间的关系为

data

– foo

-- effectFn1

– bar

-- effectFn2

但是实际执行结果为

effectFn1执行
effectFn2执行
effectFn2执行

原因:用全局变量activeEffect来存储通过effect函数注册的副作用函数,这意味着同一时刻activeEffect所存储的副作用函数只能由一个。当副作用函数发生嵌套时,内层副作用函数的执行会覆盖activeEffect的值,并且永远不会恢复到原来的值。如果这个时候再有响应式数据进行依赖收集,即时这个响应式数据是在外层副作用函数中读取的,他们收集到的副作用函数也都会是内层副作用函数,这就是问题所在。

**解决思路:**创建一个函数栈effectStack,在副作用函数执行时,将当前副作用函数压入栈中,待副作用函数执行完毕后从栈中弹出,并始终让activeEffect指向栈顶的副作用函数。这样就能做到一个响应式数据只会收集直接读取其值的副作用函数,而不会出现互相影响的情况

let data = {foo:'foo值',bar:'bar值'}

let activeEffect
const effectStack = []   //effect栈

//注册响应式的函数
let effect = (fn)=>{
    const effectFn = ()=>{
        //将副作用函数赋值给activeEffect
        activeEffect = effectFn
        //调用副作用函数之前将当前副作用函数压入栈中
        effectStack.push(effectFn)
        //执行副作用函数
        fn()
        //当副作用函数执行完毕后,将当前副作用函数弹出栈,并把activeEffect还原为之前的值
        effectStack.pop()
        activeEffect = effectStack[effectStack.length -1]
    }
    //用来存储与该副作用函数相关的依赖集合
    effectFn.deps = []
    effectFn()
}
let temp1,temp2
effect (function effectFn1(){
    console.log('effectFn1执行')
    effect(function effectFn2(){
        console.log('effectFn2执行')
        temp2 = data.bar
    })
    temp1 = data.foo
})

6.避免无限递归循环

场景:
const data = {foo:1}
const obj = new Proxy(data,{/**/})
effect(()=>obj.foo++)
原因:

obj.foo++可以理解为 obj.foo = obj.foo + 1 既进行了读取操作,也进行了修改操作。

先读取obj.foo的值,触发了track操作,将当前副作用函数收集到桶中,接着+1后再赋值给obj.foo,会触发trigger操作,把桶中的副作用函数取出并执行。问题在于该副作用函数还在执行中,还没有执行完毕,又要开始下一次的执行,就会无限调用自己,形成了栈溢出。

解决方法

设置和读取操作都在同一个副作用函数中进行,无论是track收集的还是trigger执行的,都是activeEffect。可以在trigger时增加守卫条件,如果trigger触发执行的副作用函数与当前执行的副作用函数相同,则不触发执行。

function trigger (target,key){
    const depsMap = bucket.get(target)
    if(!depsMap) return
    const effects = depsMap.get(key)

    const effectsToRun = new Set()
    effects && effects.forEach(effectFn=>{
        if(effectFn !== activeEffect){  //如果trigger执行的副作用函数与当前执行的副作用函数相同,则不触发执行
            effectsToRun.add(effectFn)
        }
    })
    effectsToRun.forEach(effectFn => effectFn())
}

7.调度执行

可调度性是响应式系统非常重要的特性。

其实就是通过事件循环,宏微任务的机制,可以自定义控制事件和函数的执行顺序

在vue中,多次修改响应式数据只会触发一次更新,就是用的这个机制

effect(()=>{
    console.log(obj.foo)
},
//options
{
  scheduler(fn){
     setTimeout(fn)
  }
}
)


function effect(fn,options){
    const effectFn = ()=>{
        /** */
    }
    //将options挂载到effectFn上
    effectFn.options = options
    effectFn.deps = []
    effectFn()
}

function trigger(targrt,key){
    const effectsToRun = new Set()
    effectsToRun.forEach(effectFn=>{
        //如果一个副作用函数存在调度器,则使用该调度器,并将副作用函数作为参数传递
        if(effectFn.options.scheduler){
            effectFn.options.scheduler(effectFn)
        }else{
            effectFn()
        }
    })
}


8.计算属性computed和lazy

lazy

lazy懒执行,就是effect函数执行不会立刻传递给副作用函数

//立即执行
effect(()=>{
console.log('副作用函数')
})

//通过设置lazy字段为true,不立即执行
effect(()=>{
console.log('副作用函数')
},{lazy:true})


function effect(fn,options={}){
  const effectFn =()=>{
    /** */
  }
  effectFn.options = options
  effectFn.deps = []

  if(!options.lazy){   //立即执行
    effectFn()
  }
  return effectFn
}

但如果仅仅是这样,意义也不大,因为我们后续还需要去执行副作用函数,所有就需要拿到副作用函数

function effect(fn,options = {}){
    const effectFn = ()=>{
        cleanup(effectFn)
        activeEffect = effectFn
        effectStack.push(effectFn)
        //将fn的执行结果存储到res中
        const res = fn()
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
        //将res做为effectFn的返回值
        return res
    }
    effectFn.options = options
    effectFn.deps = []
    //未检出lazy:true,立即执行
    if(!options.lazy){
        effectFn()
    }
    return effectFn
}

传递给effect函数的参数fn才是真正的副作用函数,而effectFn是我们包装后的副作用函数。为了effectFn得到真正的副作用函数fn的执行结果,将res变量(副作用函数执行结果)作为返回值。

computed功能实现

下面的代码比较完整,有注释,可以直接引用到html中,或者在nodejs中运行

//计算属性函数
function computed(getter){
  let value
  let dirty = true
    //把getter作为副作用函数,创建一个lazy的effect
   const effectFn = effect(getter,{
    lazy:true,
   })
   const obj = {
    get value(){
     return effectFn()
    }
   }
   return obj
}

//定义一个全局变量,保存副作用函数
let activeEffect = undefined   
//effect栈
const effectStack = []
//桶,用来保存对象=>属性=>依赖之间的连接关系
const bucket = new WeakMap() 
//注册副作用函数
function effect(fn,options={}){  
    //effectFn是经过封装后的副作用函数,响应式数据的依赖其实就是这个经过封装的函数
    const effectFn = ()=>{
        //清除原有的副作用函数
        cleanup(effectFn)
        activeEffect = effectFn
        //栈末尾压入一个副作用函数
        effectStack.push(effectFn)
        //执行这个函数
        const res =  fn()
        //effects栈清除最后一个函数
        effectStack.pop()
        //动态的副作用函数赋值为栈中的最后一位
        activeEffect = effectStack[effectStack.length - 1]
        //res是副作用函数执行完毕的值,保存在res变量中,通过一层层返回,effect返回副作用函数effectFn,effectFn函数被调用返回res的值,最后作为obj中的value值,obj就是最后新生成的数据
        return res
    }
    //保存副作用函数的配置
    effectFn.options = options
    //添加一个用来存储所有与该副作用函数相关的依赖集合
    effectFn.deps = []
    //根据lazy属性判断,是否为立即执行副作用函数
    if(!options.lazy){
      effectFn()  
    }
    //返回值就是依赖
    return effectFn
}

//原始数据
const data = {foo:1,bar:2}
//使用元编程Proxy重写和自定义getter和setter
const data_Proxy = new Proxy(data,{
  //重写getter,收集依赖,返回值
    get(target,key){
      track(target,key)
      return target[key]
    },
    //重写setter,触发依赖,修改值
    set(target,key,newVal){
      target[key] = newVal
      trigger(target,key)
    }
})
//追踪
function track(target,key){
     //activeEffect无效,说明没有注册副作用函数,直接返回对象.属性值
    if(!activeEffect) {
        return target[key]
      }
      //depsMap:桶中第一层数据,键是对象,值是map
      let depsMap = bucket.get(target)  
      if(!depsMap){
        //在bucket,没有找到这个值,就新增一个
        bucket.set(target,(depsMap = new Map()))  
      }
      //获取桶中对象=>属性的依赖集合
      let deps = depsMap.get(key)  
      if(!deps){
        //不存在就给赋值一个,Set类型,第二层数据
        depsMap.set(key,(deps = new Set()))  
      }
      //将副作用函数加入deps容器中
      deps.add(activeEffect) 
      //同时副作用函数的deps容器中加入刚才容器,类似于发布订阅模式,回调函数的相互存储和引用
      activeEffect.deps.push(deps)   
}
//触发
function trigger(target,key){
  //获取桶中对应对象的值
    const depsMap = bucket.get(target)
    if(!depsMap){
      return
    }
    //获取对应属性的值
    const effects = depsMap.get(key)
    //为了防止刚删除又创建,再删除再创建的无限死循环
    const effectsToRun = new Set(effects)
    effects && effects.forEach(effectFn=>{
      if(effects !== activeEffect){
        effectsToRun.add(effectFn)
      }
    })
    effectsToRun.forEach(effectFn => {
      //自定义的调度
if(effectFn.options && effectFn.options.scheduler){
  effectFn.options.scheduler(effectFn)
}else{
  effectFn()
}   
    })
}
//清除副作用函数的函数
function cleanup(effectFn){
   for(let i = 0;i<effectFn.deps.length;i++){
      const deps = effectFn.deps[i]
      deps.delete(effectFn)
   }
   effectFn.deps.length = 0
}



//一个需要其他变量计算的值,传参为一个有return值的函数
const sumRes = computed(()=>data_Proxy.foo + data_Proxy.bar)

console.log(sumRes.value,'计算获取的值:第一次')
data_Proxy.bar  = 10
console.log(sumRes.value,'计算获取的值:第二次')

9.watch的实现原理

watch的本质就是观测一个响应式数据,当数据变化时通知并执行相应的回调函数。

watch本质上就是利用effect以及options.scheduler选项。

effect(()=>{
  console.log(obj.foo)
},{
 scheduler(){
   //当obj.foo值变化时,会执行scheduler调度函数
}
})

当一个副作用函数中访问响应式数据obj.foo,会在副作用函数与响应式数据之间建立联系,当响应式数据变化时,会触发副作用函数重新执行。但是如果副作用函数存在scheduler选项,当响应式数据发生变化时,会触发scheduler调度函数执行,而不是直接触发副作用函数执行。从这个角度看,scheduler调度函数相当于一个回调函数,而watch的实现就是利用了这个特点特点。

简单版

可直接测试

//watch函数
function watch(source,callBack){
  effect(
    ()=>traverse(source),
    {
        scheduler(){
            callBack()
        }
    }
  )
}
//递归回调函数
function traverse(value,seen = new Set()){
    if(typeof value !== 'object' || value === null || seen.has(value)){
        return
    }
    seen.add(value)
    for(const k in value){
        traverse(value[k],seen)
    }
    return value
}

//watch测试代码
//简单版本
watch(data_Proxy,()=>{
    console.log('变化了',data_Proxy.foo)
})
data_Proxy.foo = 10
data_Proxy.foo = 20
函数版

除了常规的响应式数据,还可以接收一个getter函数

/**附带getter版本  start*/
function watch(source, callBack) {
  let getter
  if (typeof source === 'function') {
    //如果source为函数类型
    getter = source
  } else {
    getter = () => traverse(source)
  }
  //getter()执行完毕后返回值为被监测的对象
  effect(() => getter(),
    {
      scheduler() {
        callBack()
      }
    })
}
newValue和oldValue版本

通常,在数据监视时回去根据newValue和oldValue去做一些逻辑处理



//计算属性函数


/**监听对象属性 start*/
//watch函数

/**newValue和oldValue版本  start*/
function watch(source, callBack) {
  let getter
  if (typeof source === 'function') {
    //如果source为函数类型
    getter = source
  } else {
    //如果是初始对象类型
    getter = () => traverse(source)
  }
  //定义新旧值变量
  let oldValue , newValue
  //getter()执行完毕后返回值为被监测的对象
  const effectFn =  effect(() => getter(),
    {
      lazy:true,  //使用effect注册副作用函数时,开启lazt选项,并把返回值存储到effectFn中以便后续手动调用
      scheduler() {
        //执行effectFn,得到的是新值
        newValue = effectFn()
        callBack(newValue,oldValue)
        //更新旧值,不然下一次得到的会是错误的旧值
  
        oldValue = newValue
      }
    })
    //手动调用,得到就是旧值
    oldValue = effectFn()
}
/**newValue和oldValue版本  end*/


//递归回调函数
function traverse(value, seen = new Set()) {
  //判断value的类型,正常情况下,value都是监视某一个对象,比如obj,如果递归传递回来的seen中有这个obj,也是不成立的,唯一性
  if (typeof value !== 'object' || value === null || seen.has(value)) {
    return
  }
  //set中添加这个对象作为元素
  seen.add(value)
  //遍历对象
  for (const k in value) {
    //递归回调,将对象的所有属性都遍历一遍
    traverse(value[k], seen)
  }
  //返回的还是这个对象
  return value
}




//定义一个全局变量,保存副作用函数
let activeEffect = undefined
//effect栈
const effectStack = []
//桶,用来保存对象=>属性=>依赖之间的连接关系
const bucket = new WeakMap()
//注册副作用函数
function effect(fn, options = {}) {
  //effectFn是经过封装后的副作用函数,响应式数据的依赖其实就是这个经过封装的函数
  const effectFn = () => {
    //清除原有的副作用函数
    cleanup(effectFn)
    activeEffect = effectFn
    //栈末尾压入一个副作用函数
    effectStack.push(effectFn)
    //执行这个函数
    const res = fn()
    //effects栈清除最后一个函数
    effectStack.pop()
    //动态的副作用函数赋值为栈中的最后一位
    activeEffect = effectStack[effectStack.length - 1]
    //res是副作用函数执行完毕的值,保存在res变量中,通过一层层返回,effect返回副作用函数effectFn,effectFn函数被调用返回res的值,最后作为obj中的value值,obj就是最后新生成的数据
    return res
  }

  effectFn.options = options
  //创建一个容器,effectFn
  effectFn.deps = []
  //根据lazy属性判断,是否为立即执行副作用函数
  if (!options.lazy) {
    effectFn()
  }
  //返回值就是依赖
  return effectFn
}

//原始数据
const data = { foo: 1, bar: 2 }
//使用元编程Proxy重写和自定义getter和setter
const data_Proxy = new Proxy(data, {
  //重写getter,收集依赖,返回值
  get(target, key) {
    track(target, key)
    return target[key]
  },
  //重写setter,触发依赖,修改值
  set(target, key, newVal) {
    target[key] = newVal
    trigger(target, key)
  }
})
//追踪
function track(target, key) {
  //activeEffect无效,说明没有注册副作用函数,直接返回对象.属性值
  if (!activeEffect) {
    return target[key]
  }
  //depsMap:桶中第一层数据,键是对象,值是map
  let depsMap = bucket.get(target)
  if (!depsMap) {
    //在bucket,没有找到这个值,就新增一个
    bucket.set(target, (depsMap = new Map()))
  }
  //获取桶中对象=>属性的依赖集合
  let deps = depsMap.get(key)
  if (!deps) {
    //不存在就给赋值一个,Set类型,第二层数据
    depsMap.set(key, (deps = new Set()))
  }
  //将副作用函数加入deps容器中
  deps.add(activeEffect)
  //同时副作用函数的deps容器中加入刚才容器,类似于发布订阅模式,回调函数的相互存储和引用
  activeEffect.deps.push(deps)
}
//触发
function trigger(target, key) {
  //获取桶中对应对象的值
  const depsMap = bucket.get(target)
  if (!depsMap) {
    return
  }
  //获取对应属性的值
  const effects = depsMap.get(key)
  //为了防止刚删除又创建,再删除再创建的无限死循环
  const effectsToRun = new Set(effects)
  effects && effects.forEach(effectFn => {
    if (effects !== activeEffect) {
      effectsToRun.add(effectFn)
    }
  })
  effectsToRun.forEach(effectFn => {
    //自定义的调度
    if (effectFn.options && effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn)
    } else {
      effectFn()
    }
  })
}
//清除副作用函数的函数
function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i]
    deps.delete(effectFn)
  }
  effectFn.deps.length = 0
}


//computed测试代码
//一个需要其他变量计算的值,传参为一个有return值的函数
//   const sumRes = computed(()=>data_Proxy.foo + data_Proxy.bar)

//   console.log(sumRes.value,'计算获取的值:第一次')
//   data_Proxy.bar  = 10
//   console.log(sumRes.value,'计算获取的值:第二次')


// watch测试代码

watch(()=>data_Proxy.foo,(newVal,oldVal)=>{
  console.log(newVal,oldVal,'数据监视')
})

data_Proxy.foo = 20  // 20 1 '数据监视'
data_Proxy.foo = 100   //100 20  '数据监视'

10.立即执行的watch与回调执行时机

在watch函数传参时,第三个参数:immediate设置为true,可以立即执行一次

function watch(source, callBack,options) {
    let getter
    if (typeof source === 'function') {
      //如果source为函数类型
      getter = source
    } else {
      //如果是初始对象类型
      getter = () => traverse(source)
    }
    //定义新旧值变量
    let oldValue , newValue
    //提取scheduler调度函数为一个独立的job函数
  const job = ()=>{
    newValue = effectFn()
    callBack(newValue,oldValue)
    oldValue = newValue
  }

  const effectFn = effect(
    ()=>getter(),
    {
        lazy:true,
        scheduler:job  //使用job函数作为调度器函数
    }
  )

  if(options.immediate){
    job()   //当immediate为true时立即执行,从而触发回调执行
  }else{
    oldValue = effectFn()
  }


  watch(()=>data_Proxy.foo,(newVal,oldVal)=>{
    console.log(newVal,oldVal,'数据监视')
  },{immediate:true})
  
  data_Proxy.foo = 20
  data_Proxy.foo = 100

  //执行结果
  //1 undefined  '数据监视'
  //20   1   '数据监视'
  //100  20  '数据监视'

11.过期的副作用

12.总结

1724987706705