vueJS源码之响应式原理

179 阅读4分钟

响应式是vue非常重要的一个功能,页面的动态更新,computed属性,watch函数都用到响应式系统来实现。

一个比较简单的响应式由两部分组成,第一部分是用proxy代理响应式数据,第二部分是添加副作用函数。

proxy代理响应式数据就是使用proxy去代理需要响应式的对象,访问对象的属性会触发get方法,get方法会把依赖收集起来,如下图所示的weakMap-map-set三级依赖,weakMap以对象为key,指向一个Map,Map会以属性为key指向一个Set,set里面存的是副作用函数,set要把副作用函数存起来,还需要一个全局变量,这个全局变量在第二部分添加副作用函数的时候,把副作用函数赋值给全局变量,最后执行副作用函数,如果在副作用函数内部访问代理对象的属性值就会把副作用函数添加到对应的set中

2f631869f47ef2d6adb95df0c4945ba.png

image.png

这个是第一部分

       let activeEffect=null
       let data={a:12}
       let bucket=new WeakMap()
       let obj=new Proxy(data,{
            get(target,key){
                if(!activeEffect) return target[key]
                let depMap=bucket.get(target)
                if(!depMap){
                    depMap=new Map()
                    bucket.set(target,depMap)
                }
                let depSet=depMap.get(key)
                if(!depSet){
                    depSet=new Set()
                    depMap.set(key,depSet)
                }
                depSet.add(activeEffect)
                return target[key]
            },
            set(target,key,newValue){
                target[key]=newValue
                let depMap = bucket.get(target)
                if(depMap){
                    let depSet=depMap.get(key)
                    if(depSet){
                        depSet.forEach((fn)=>{
                            fn()
                        })
                    }
                }
                
                // return true
            }
        })

这个是第二部分,把副作用fn函数赋值给全局变量activeEffect

   function effect(fn){
            activeEffect=fn
            fn()
        }

执行这个effect传入一个函数,这个函数就是副作用函数,在副作用函数里面访问obj对象的属性,就会把触发get方法,从而把副作用函数当作依赖收集到set中

 effect(()=>{
            console.log("value1="+obj.a)
        })
  //output:value1=12

当修改属性值的时候这个副作用函数就会触发

    obj.a++
    //output:value1=13

如果在副作用函数里面操作dom的话,每次数据变更,dom也会变化

 effect(()=>{
     documeng.body.innerText=obj.a
  })

以上是一个最基本的响应式,如果有动态条件语句,上面的写法不能满足。下面的示例中

let data={a:12,b:true}
 effect(()=>{
        console.log("value="+(obj.b?obj.a:"none"))
  })
  //output:value=12
  //obj.b=false
  //output:value=none
  //obj.a++
  //output:value=none

b默认为true,第一次输出为value=12,然后修改b为false,输出value=none,目前输出和预期一样。再修改obj.a++,输出value=none,这不是我们想要的,b为false,在条件语句中a是不执行的,在修改a的值时,不需要响应式

要解决这个问题,就需要每一次副作用函数执行,删除属性对当前副作用函数的依赖并且重新收集依赖

  function effect(fn){
            function effectFn(){
                effectFn.deps.forEach((depSet)=>{
                    depSet.delete(effectFn)
                })
                effectFn.deps=[]
                activeEffect=effectFn
                fn()
            }
            effectFn.deps=[]
            effectFn()
        }

     let obj=new Proxy(data,{
            get(target,key){
                if(!activeEffect) return target[key]
                let depMap=bucket.get(target)
                if(!depMap){
                    depMap=new Map()
                    bucket.set(target,depMap)
                }
                let depSet=depMap.get(key)
                if(!depSet){
                    depSet=new Set()
                    depMap.set(key,depSet)
                }
                
                depSet.add(activeEffect)
                //新增
                activeEffect.deps.push(depSet)
                return target[key]
            },
            set(target,key,newValue){
                target[key]=newValue
                let depMap = bucket.get(target)

                if(depMap){
                    let depSet=depMap.get(key)
                    if(depSet){
                    //避免死循环,创建set副本
                       new Set(depSet).forEach((effectFn)=>{
                            effectFn()
                        })
                    }
                }
                
                return true
            }
        })

重新对副作用函数做了一个包装,增加deps属性赋值一个空数组,在收集依赖的时候把包含当前副作用函数的Set压入到deps数组中,在副作用函数触发时,通过遍历deps拿到Set,然后删除掉Set中当前的副作用函数,deps重置为空数组,执行fn重新收集依赖。

上面这些过程总结一下,在每一次触发副作用函数的时候删除老的依赖,注入新的依赖。到此为止动态依赖的问题已经基本解决,另外需要提到的一点就是,在遍历set触发依赖函数的时候会发生死循环,因为删除set的副作用函数和收集set的副作用函数在同一个遍历函数发生的,所以需要在遍历触发副作用函数的时候创建一个set副本避免死循环。

在vue中,当子组件被父组件引用的时候,会发生副作用函数的嵌套,上面的功能并不能实现嵌套的副作用函数

     effect(()=>{
            effect(()=>{
                console.log("valueb="+obj.b)
            })
            console.log("valuea="+obj.a)
        })
        //output:valueb=100
        //output:valuea=12
        //input:obj.b++
        //output:valueb=101
        //input:obj.a++
        //output:valueb=101

上面的代码中obj.a++,输出valueb=101是不对的,a属性收集的是b属性的副作用函数,也就是外层属性收集的是内层副作用函数。发生这种情况的原因是外层副作用函数执行