文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Vue.js设计与实现之设计一个完善的响应系统

2024-12-02 04:32

关注

2.副作用函数

所谓副作用函数,指的是会产生副作用的函数,而副作用指的是函数effect的执行会直接或间接影响到其它函数的执行,那么就说effect函数产生了副作用,effect就是副作用函数。



在上面的代码片段中,副作用函数effect会设置id为app的标签innerHTML属性 app.innerHTML = "hello pingping," + state.name + "," + state.age + "," + state.address;,其中state.address的值为"北京"。而当state.address发生变化时,希望副作用函数effect能够重新执行,state.address的值变为"广州"。

当前在setTimeout函数中代码修改了state.address的值,除了对象的值本身发生变化外,没有其他任何变化,达不到要求的效果。如果希望值变化后副作用函数立即更新,那么state对象数据就必须是响应式的,那么什么是响应式的,应该如何让state实现响应式呢?

3.响应式数据

对上面的要求进行分析,要想让state变成响应式数据,需要满足两个条件:

再次思考,响应式数据的实现就变成了拦截对象进行取值和设值操作。当从state对象中读取address时,就将副作用函数effect存储到容器中,当设置state对象中的address值的时候,从容器中取出effect函数并执行。

取值操作

设置操作

那么,到底应该如何实现对一个对象属性的读取和设置操作呢?在Vue.js2中采用的是Object.defineProperty函数实现的,而在Vue.js3中则是采用Proxy代理对象的方法实现的。我们根据上面的思路和流程图,先简易实现个最low的拦截取值设置操作。


在浏览器中渲染得到:

1s后页面更新渲染为:

看到上面的代码片段,不禁想问为什么要将存储副作用函数的容器类型设置为Set类型,这是因为对于同一个对象属性进行多次代理就会出现死循环的情况,对此使用Set可以用于去重。

state是被代理的原始数据,而obj是采用Proxy进行代理后的对象数据,在其中实现了拦截和取值设值操作,在取值和设置过程中实现了副作用函数effect的存储和取出执行的操作。

4.尚且完善的响应式系统

为什么说是尚且完善的响应式系统,这是因为在本段中将循序渐进介绍,如何实现一个功能尚且完善的响应式系统。可以实现通用式的副作用函数,匿名函数也能够被收集到副作用函数容器中,而非命名的effect函数。

注册副作用函数

要实现这一点,只需要编写一个通用函数,提供注册副作用函数机制即可。

// 全局变量用于存储当前被注册的副作用函数
let activeEffect;
// effect用于注册副作用函数
function effect(fn){
// 当调用effect注册副作用函数时,将副作用函数fn赋值给activeEffect
activeEffect = fn;
// 执行副作用函数
fn();
}

effect(()=>{
app.innerHTML = state.name + "," + state.age + "," + state.address;
})

在上面代码片段中,传递一个闭包即可实现注册副作用函数的功能,当effect函数执行时,先将effect传递的闭包函数暂存到变量activeEffect,作为当前注册的副作用函数。

//原始数据
let state = {
name:"onechuan",
age:18,
address:"北京"
}
// 存储副作用函数的桶
const bucket = new Set();
// 对原始数据的代理
const obj = new Proxy(state,{
// 拦截读取操作
get(target, key){
// 将activeEffect存储的副作用函数收集到桶里
if(activeEffect){
bucket.add(activeEffect)
}
// 返回属性值
return target[key]
},
// 拦截设置操作
set(target, key, newVal){
// 设置属性值
target[key] = newVal
// 把副作用函数从桶里取出并执行
bucket.forEach(fn=>fn())
// 返回true代表设置操作成功
return true
}
})

effect(()=>{
app.innerHTML = state.name + "," + state.age + "," + state.address;
})

setTimeout(()=>{
//修改全局变量,产生副作用
obj.address = "广州";
},1000)

当我们在响应式数据obj上设置一个不存在的属性时,副作用函数并不会去对象上读取这个属性的值,也就是这个不存在的属性并没有与副作用函数建立响应联系。原本不应该触发副作用函数中的匿名函数,但是实际上却触发了effect函数的执行,这也印证了我们当前设计的系统还存在缺陷。

之所以出现上面的问题,这是因为在没有副作用函数与被操作的目标字段之间建立明确的关系,这就是为什么在Vue.js3实际设计中没有简单使用Set类型的原因。为了解决这种问题,我们只需要在副作用函数与被操作字段间建立联系即可,重新设计收集副作用函数的容器数据结构。

依赖收集的数据结构

要重新设计副作用函数的容器数据结构,需要我们分析effect函数的执行机制,这段代码中存在三个重要部分:

三者建立的关系是:

|-target
|- key
|- effectFn

对于上面的分析,我们得先重新设计存储副作用函数的依赖收集容器的数据结构,创建WeakMap用于存储对象,Set用于存储副作用函数。

// 创建存储副作用函数的桶
const bucket = new WeakMap();
// 全局变量用于存储被注册的副作用函数
let activeEffect;

// 响应式函数
const obj = new Proxy(state,{
// 拦截读取操作
get(target, key){
// 没有activeEffect
if(!activeEffect) return
// 根据目标对象从桶中获得副作用函数
let depsMap = bucket.get(target);
// 判断是否存在,不存在则创建一个Map
if(!depsMap) bucket.set(target, depsMap = new Map())
// 根据key从depsMap取的deps,存储着与key相关的副作用函数
let deps = depsMap.get(key);
// 判断key对应的副作用函数是否存在
if(!deps) depsMap.set(key, deps = new Set())
// 最后将激活的副作用函数添加到桶里
deps.add(activeEffect)
// 返回属性值
return target[key]
},
// 拦截设值操作
set(target, key, newVal){
// 设置属性值
target[key] = newVal;
// 根据target从桶中取的depMaps
const depMaps = bucket.get(target);
// 判断是否存在
if(!depMaps) return
// 根据key值取得对应的副作用函数
const effects = depMaps.get(key);
// 执行副作用函数
effects && effects.forEach(fn=>fn())
}
})

在上面的代码片段中,所写WeakMap、Map和Set的数据结构关系如下图所示。三者的具体作用:

为什么使用WeakMap作为存储对象的容器呢?

这是因为WeakMap是弱引用的Map,不会影响到垃圾回收机制的正常工作,WeakMap多引用的对象执行完毕后,会将对象从内存中移除,从而避免内存泄漏。所以WeakMap经常用于存储那些只有当key所引用对象存在时(没有被回收)才有价值的信息。

在前面代码片段中,如果target对象没有任何引用了,说明用户没有使用它,此时垃圾回收机制就可以将其进行清除,从而避免内存溢出。

整理抽取代码

将前面的代码片段进行抽取函数,封装得到track和trigger函数,使得我们的代码逻辑更加清晰明了,也能带给我们更大的灵活性。

// 全局变量用于存储被注册的副作用函数
let activeEffect;
// 创建存储副作用函数的桶
const bucket = new WeakMap();
// 原始数据
const state = {
name:"pingping",
age:18,
address:"北京"
}

// 响应式函数
const obj = new Proxy(state,{
// 拦截读取操作
get(target, key){
// 将副作用函数activeEffect添加到存储副作用函数的WeakMap中
track(target, key)
// 返回属性值
return target[key]
},
// 拦截设值操作
set(target, key, newVal){
// 设置属性值
target[key] = newVal;
// 将副作用函数从WeakMap中取出并执行
trigger(target, key)
}
})

// 在get拦截函数中调用追踪取值函数的变化
function track(target, key){
// 没有activeEffect
if(!activeEffect) return
// 根据目标对象从桶中获得副作用函数
let depsMap = bucket.get(target);
// 判断是否存在,不存在则创建一个Map
if(!depsMap) bucket.set(target, depsMap = new Map())
// 根据key从depsMap取的deps,存储着与key相关的副作用函数
let deps = depsMap.get(key);
// 判断key对应的副作用函数是否存在
if(!deps) depsMap.set(key, deps = new Set())
// 最后将激活的副作用函数添加到桶里
deps.add(activeEffect)
}

// 在set拦截函数中调用trigger函数触发变化
function trigger(target, key){
// 根据target从桶中取的depMaps
const depMaps = bucket.get(target);
// 判断是否存在
if(!depMaps) return
// 根据key值取得对应的副作用函数
const effects = depMaps.get(key);
// 执行副作用函数
effects && effects.forEach(fn=>fn())
}
// effect用于注册副作用函数
function effect(fn){
// 当调用effect注册副作用函数时,将副作用函数fn赋值给activeEffect
activeEffect = fn;
// 执行副作用函数
fn();
}


effect(()=>{
console.log("打印");
document.body.innerText = obj.name + "," + obj.age + "," + obj.address;
})

// 设置一个不存在的属性时
setTimeout(()=>{
obj.address = "广州"
},1000)

5.写在后面

在本文中简单实现了可以进行依赖收集的响应式系统,使用WeakMap配合Map构建了新的存储结构,能够在响应式数据和副作用函数之间建立更加精确的联系。之所以采用WeakMap存储引用对象,是因为其是弱引用的,当某个对象不再被使用时会被垃圾回收机制清除。此外,还对响应式系统的代码进行了功能抽取,对应封装成调用函数track和trigger。

来源:前端一码平川内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯