1. vue3 响应式原理-基础版

139 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

关于 Proxy

众所周知,vue3使用的是Proxy去代理对象。MDN官方是这样说的

image.png ps:在js中,数组和函数也是对象,Proxy也可以进行代理 点击可了解更多--Poxy

具体用法

将obj通过Proxy进行代理 image.png

let obj={
    flag:true,
    count:1,
    name:"zhangsan"
}
const newObj= new Proxy(obj,{
    get(target,key){
    return Reflect.get(target,key)

    },
set(arget,key,value){
    Reflect.set(target,key,value)
    }
    return true
})
console.log(newObj)

newObj打印如下

image.png

vue3响应式原理

vue3响应式原理:使用Proxy进行代理。在代理的get阶段即读取obj属性阶段,收集依赖,在set阶段即设置obj属性阶段,触发依赖。

具体如何去收集依赖和触发依赖呢?举个🌰

目标如下:当我们通过修改 newObj.name=”lisi“的值,页面上也需要更新为lisi

1.现在我们创建一个index.js,代码如下

let obj={
    flag:true,
    count:1,
    name:"zhangsan"
}
const newObj= new Proxy(obj,{
    get(target,key){
    return Reflect.get(target,key)

    },
set(arget,key,value){
    Reflect.set(target,key,value)
    }
    return true
})

function effect(){
    document.getElementById("app").textContent=newObj.name
   }
  effect()
setTimeout(()=>{
    newObj.name="lisi"
},2000)

2.创建一个index.html

<!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>
    <script src="./index.js" async></script>
</head>
<body>
    <div id="app"></div>
</body>
</html>

分析: 在这里,我们执行effect函数

读取:会将newObj.name的值设置为id="app"元素的文本内容,会触发代理的getter,我们可以将我们的effect函数收集起来

设置: newObj.name="lsii",会触发代理的setter,我们可以再次执行effect函数,这样就会再次更新id="app"元素的文本内容 思路:

我们定义一个数组---effects,收集依赖

//1index.js
let obj={
    flag:true,
    count:1,
    name:"zhangsan"
}
let effects=[]
const newObj= new Proxy(obj,{
    get(target,key){
    //新增代码:收集依赖
    effects.push(effect)
    return Reflect.get(target,key)

    },
set(arget,key,value){
    Reflect.set(target,key,value)
    }
    //新增代码:触发依赖
    effects.forEach(effect=>{
    effect()
    })
    return true
})

function effect(){
    document.getElementById("app").textContent=newObj.name
   }
  effect()
 setTimeout(()=>{
    newObj.name="lisi"
},2000)

效果如下

chrome-capture-2022-4-15.gif

问题:

终于实现了! 😄 😄 但是这样收集依赖会有一些问题。

1.effect函数是硬编码,如果不是effect函数该怎么办?

2.目前我是将收集的依赖直接push进effects里面,如果多次读取newObj的不同的属性,触发依赖的时候,我们怎么知道要执行哪个effects呢?

基于以上两个问题

思路:

1.我们可以将要收集的依赖作为fn传入effect里面

2.重新设计要收集依赖的数据结构,使收集依赖时,可以知道收集的是哪个属性的依赖,触发依赖的时候,知道触发的是谁的依赖

//1index.js
...
let activeEffect=null //定义当前正在收集的依赖函数 
function effect(fn){
       activeEffect=fn
       fn()
   
   }
  effect(()=>{
   document.getElementById("app").textContent=newObj.name
  })
 setTimeout(()=>{
    newObj.name="lisi"
},2000)

以上代码解决了effect硬编码的问题

接下来解决第二个问题 我们收集依赖的时候,可以根据target-key-effect 的树状结构去设计数据 比如

erDiagram
target ||--o{ key1 : get
key1 ||--|{ effect1 : get
target ||--o{ key2 : get
key2 ||--|{ effect2 : get

为了代码结构的清晰,我们可以单独把收集依赖和触发依赖的功能,单独抽离出来,分别是track(),trigger()

//1index.js
let obj={
    flag:true,
    count:1,
    name:"zhangsan"
}
let bucket=new weakMap()//
const newObj= new Proxy(obj,{
    get(target,key){
    //新收集依赖
    track(target,key)
    return Reflect.get(target,key)
    },
set(arget,key,value){
    Reflect.set(target,key,value)
    //新触发依赖
   trigger(target,key)
   return true
    }
    
})
//依赖收集
function track(target,key){
    let depMaps=bucket.get(target)
    if(!depMaps){
        bucket.set(target,(depMaps=new Map()))
    }
    let deps=depMaps.get(key)
    if(!deps){
        depMaps.set(key,(deps=new Set()))
    }
    deps.add(activeEffect)
    
}
//触发依赖
function trigger(target,key){
    let depMaps=bucket.get(target)
    let deps=depMaps.get(key)
    deps && deps.forEach(effect => {
        effect()   
    });

}

...

完整代码如下

//1index.js
let obj={
    flag:true,
    count:1,
    name:"zhangsan"
}
let activeEffect=null //定义当前正在收集的依赖函数 
let bucket=new weakMap()//
const newObj= new Proxy(obj,{
    get(target,key){
    //新收集依赖
  track(target,key)
    return Reflect.get(target,key)
    },
set(arget,key,value){
    Reflect.set(target,key,value)
    //新触发依赖
   trigger(target,key)
   return true
    }
    
})
//依赖收集
function track(target,key){
    let depMaps=bucket.get(target)
    if(!depMaps){
        bucket.set(target,(depMaps=new Map()))
    }
    let deps=depMaps.get(key)
    if(!deps){
        depMaps.set(key,(deps=new Set()))
    }
    deps.add(activeEffect)
    
}
//触发依赖
function trigger(target,key){
    let depMaps=bucket.get(target)
    let deps=depMaps.get(key)
    deps && deps.forEach(effect => {
        effect()   
    });

}
//effect相关
function effect(fn){
       activeEffect=fn
       fn()
   
   }
  effect(()=>{
   document.getElementById("app").textContent=newObj.name
  })
 setTimeout(()=>{
    newObj.name="lisi"
},2000)

这样,我们在收集依赖和触发依赖的时候,就知道,收集的是哪个对象,哪个属性的依赖,在触发依赖的时候,依次通过bucket-- target----key -- effect 的方式获取到索要触发的函数啦~