vue:手动实现一个相对完善的响应式系统(一)

77 阅读5分钟

这篇单纯为本人学习《vue设计与实现》做的笔记,写得并不好,建议直接阅读这本书

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

要想理解vue中的响应式系统原理,就必须知道什么是副作用函数和响应式数据,如下所示

    const obj = {text:'hello world'}
    function effect(){
        document.body.innerText = obj.text
    }

函数effect执行会设置元素文本,读取文本内容,其他函数也会读取和设置文本内容,effect会直接或间接影响到其他函数执行,这样的函数就称为副作用函数. 那什么是响应式数据呢,如上,我们希望我们修改obj中的值,副作用函数自动重新执行,那么这个对象就是响应式数据

2.响应式数据基本实现

实现响应式数据我们要做的就是 1.当副作用函数执行时,触发obj.text的读取操作,并将该副作用函数收集起来 2.当修改obj.text时,触发obj.text的设置操作,将副作用函数拿出来执行 这里定义一个叫bucket(桶)的数据集合

vue3是用Proxy来代理对象实现响应式数据的,这里也用Proxy

    const bucket = new Set()//存放副作用函数
    
    //数据
    const obj = {text:'hello world'}
    //对数据进行代理
    const obj = new Proxy(data,{
        get(target,key){
            bucket.add(effect) //将副作用函数收集到桶中
            return target[key]
        },
        set(target,key,newVal){
            target[key] = newVal
            bucket.forEach(fn=>fn())//将副作用函数依次执行
            return true
        }
    )
    
    function effect(){ //副作用函数
        document.body.innerText = obj.text
    }
    effect()//执行副作用函数,读取obj.text,将副作用函数收集
    setTimeout(()=>{ //2s后修改obj.text,触发set操作,bucket中副作用函数依次执行
        obj.text = "hello vue3" 
    },2000)
    

3.相对完善的响应式系统

按照上面编码,只要函数名不叫effect我们这个响应式系统就失效了,还是有很多缺点,很不完善,我们要让即使副作用函数为匿名函数,也可以触发,也可以被收集,所以我们需要提供一个注册副作用函数的机制

    //全局变量存储被注册的副作用函数
    let activeEffect
    //effect函数用于注册副作用函数
    function effect(fn){
        //当effect注册副作用函数时,将副作用函数fn赋值给activeEffect
        activeEffect = fn
        //执行一下副作用函数
        fn()
    }
    //这样,即使副作用函数为匿名函数,我们也可以收集起来
    effect(()=>{
         document.body.innerText = obj.text
    })
     const obj = new Proxy(data,{
        get(target,key){
            if(activeEffect){
                bucket.add(activeEffect) //将activeEffect收集到桶中
            }
            
            return target[key]
        },
        set(target,key,newVal){
            target[key] = newVal
            bucket.forEach(fn=>fn())//将副作用函数依次执行
            return true
        }
    )

这样我们注册副作用函数的机制就创建成功了,但是发生了以下操作呢

    effect(()=>{
        console.log("触发啦")
        document.body,innerText = obj.text
    })
    setTimeout(()=>{
        obj.notExist = "hello vue3"
    },2000)

请问,会打印几次“触发啦”,答案是两次,这是因为我们在读取时把副作用函数加入到了bucket里面,此时设置obj.notExist会触发obj的set操作,会把bucket中的副作用函数拿出来执行,所以要解决这个问题,就必须重新设置bucket的结构,让obj中每个属性和对应的副作用函数一一对应 那么,bucket的结构该怎么设计呢 观察一下下面的代码,这段代码有三个角色 1.被读取代理对象obj 2.被读取字段名text 3.被注册副作用函数effectFn

    effect(function effectFn (){
        document.body,innerText = obj.text
    })

那么,就可以为这3个角色创建一种树型结构

image.png 如果当两个副作用函数访问同一个对象的属性值

    effect(function effectFn1(){
        obj.text
    })
    effect(function effectFn2(){
        obj.text
    })

那么,他们的关系对应如下

image.png 如果,一个副作用欢呼声读取同一个代理对象不同的属性

     effect(function effectFn(){
        obj.text
        obj.foo
    })

那么,他们的关系应该为

image.png

如果,不同的副作用函数访问代理对象不同的属性

        effect(function effectFn1(){
        obj.text
    })
        effect(function effectFn2(){
        obj.foo
    })

他们关系为

image.png 接下来,就是按照这数据结构实现这个bucket 我们的bucket是这样的结构

image.png

    const bucket = new WeakMap() //这里为什么用WeakMap,因为这里我们用对象作为key,WeakMap只能用引用数据类型做键名,WeakMap对键名中的引用数据类型是弱引用,键所指的对象可以被垃圾回收
     const obj = new Proxy(data,{
        get(target,key){
           track(target,key)
            
            return target[key]
        },
        set(target,key,newVal){
            target[key] = newVal
           trigger(target,key)//将副作用函数从bucket中取出来执行
            return true
        }
    )
    
    //这里封装两个函数,分别为读取操作时将副作用函数添加到bucket中,另一个为将副作用函数取出执行的函数
    
    //在get函数中调用track函数追踪
    function track(target,key){
        //没有activeEffect直接返回
        if(!activeEffect) return 
        let depsMap = bucket.get(target)
        if(!depsMap){ //在bucket中代理对象的map集合,没有就新建与代理对象关联
            bucket.set(target, (depsMap = new Map()))
        }
        //根据key从map集合中找到对应的set集合
        let deps = depsMap.get(key)
        if(!deps){ //没有则新建与key对应的set集合添加到depsMap中
            depsMap.set(key,(deps = new Set()))
        }
        deps.add(activeEffect)//将副作用函数添加至该对应mset集合中
        retuen target[key]
    }

    //设置操作中的函数
    function trigger(target,key){
        const depsMap = bucket(target)//取出target的map集合
        if(!depsMap) return  //没有直接返回
        const effects = depsMap.get(key)
        effects && effects.forEach(fn=>fn()) //将对应的所有副作用函数全部执行
    } 

到这里,代理数据和他的属性与副作用函数就一一对应,bucket是每一个代理对象(target)作为键的WeakMap类型, bucket中的每一个Map类型的对象是由target的key作为键的value为存储着副作用函数的set类型的值.