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

更新日期: 2022-04-06阅读: 836标签: 响应式

1.写在前面

响应系统是vue.js的重要组成部分,我们要实现一个简易的响应式系统,必须先要了解什么是响应式数据和副作用函数。在实现过程中,我们需要考虑如何避免无限递归,为什么需要嵌套副作用函数,以及多个副作用函数之间会产生什么影响?

2.副作用函数

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

<div id="app"></div>

<script>
//全局变量
let state = {
    name:"onechuan",
    age:18,
    address:"北京"
}
function effect(){
    app.innerhtml = "hello pingping," + state.name + "," + state.age + "," + state.address;
}
effect();

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

在上面的代码片段中,副作用函数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变成响应式数据,需要满足两个条件:

  • 在副作用函数effect执行时,从对象state中读取address的值,触发读取操作 。
  • 当修改state.address的值时,把对象state中的address的值进行修改,触发设置操作 。

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


取值操作


设置操作

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

<div id="app"></div>
<script>
//全局变量
let state = {
    name:"onechuan",
    age:18,
    address:"北京"
}

// 存储副作用函数的桶
const bucket = new Set();

// 对原始数据的代理
const obj = new Proxy(state,{
    // 拦截读取操作
    get(target, key){
        // 将副作用函数effect添加到存储副作用函数的桶中
        bucket.add(effect)
        // 返回属性值
        return target[key]
    },
    // 拦截设置操作
    set(target, key, newVal){
        // 设置属性值
        target[key] = newVal
        // 把副作用函数从桶里取出并执行
        bucket.forEach(fn=>fn())
        // 返回true代表设置操作成功
        return true
    }
})

function effect(){
    const app = document.querySelector("#app");
    app.innerHTML = obj.name + "," + obj.age + "," + obj.address;
}
    
effect();

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

浏览器中渲染得到:


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函数的执行机制,这段代码中存在三个重要部分:

  • 被操作(读取)的代理对象obj (target对象) 。
  • 被操作(读取)的属性名称address (target对象的键名) 。
  • 使用effect函数注册的副作用函数effectFn 。

三者建立的关系是:

|-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用于存储代理对象target,用于存储和判断当前对象是否已经被Proxy进行代理过。如果被代理过则直接返回WeakMap中的代理对象,如果没有被代理过则使用Proxy进行代理后存储,从而避免同一个对象被代理多次。
  • Map用于存储经过Proxy代理的对象的属性名 。
  • Set用于存储Map中对应的每个属性的副作用函数,可以用于去重,避免多次调用 。


为什么使用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。

来源: 前端一码平川


链接: https://www.fly63.com/article/detial/11315

移动端web app要使用rem实现自适应布局:font-size的响应式

rem是相对于根元素html,这样就意味着,我们只需要在根元素确定一个px字号,则可以来算出元素的宽高。

使用现代CSS的响应式版面

通过模块化缩放,使用传统属性和calc()来动态缩放你的字体大小.为字体大小使用百分比.给文本内容和媒体查询使用em,针对不同视口尺寸使用不同缩放值.视口越小,缩放比例越小,使用媒体查询或者media()函数基于视口来改变比例和基础字号

web响应式图片的5种实现

在目前的前端开发中,我们经常需要进行响应式的网站开发。本文着重介绍一下弹性图片,也就是响应式图片的解决方案:js或服务端、srcset 、sizes 、picture标签、svg图片

HTML5+CSS3响应式垂直时间轴,高端,大气

HTML5+CSS3响应式垂直时间轴,使用了HTML5标签<section>,时间轴中所有的内容包括标题、简介、时间和图像都放在.cd-timeline-block的DIV中,多个DIV形成一个序列,并把这些DIV放在<section>中。

实现响应式_CSS变量

CSS 变量是 CSS 引入的一个新特性,目前绝大多数浏览器已经支持了,它可以帮助我们用更少的代码写出同样多的样式,大大提高了工作效率,本篇文章将教你如何使用 CSS 变量(css variable)。CSS中原生的变量定义语法是:--*,变量使用语法是:var(--*),其中*表示变量名称

vue响应式原理及依赖收集

Vue通过设定对象属性的setter/getter方法来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图。

vue响应式系统--observe、watcher、dep

Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的JavaScript 对象,而当你修改它们时,视图会进行更新,这使得状态管理非常简单直接,我们可以只关注数据本身

Responsive Web Design 响应式网页设计

常见的布局方案:固定布局:以像素作为页面的基本单位,不管设备屏幕及浏览器宽度,只设计一套尺寸;可切换的固定布局:同样以像素作为页面单位,参考主流设备尺寸

响应式布局的实现

响应式布局,即 Responsive design,在实现不同屏幕分辨率的终端上浏览网页的不同展示方式。通过响应式设计能使网站在手机和平板电脑上有更好的浏览阅读体验。响应式布局的关键不仅仅在于布局

深入响应式原理

说到响应式原理其实就是双向绑定的实现,说到 双向绑定 其实有两个操作,数据变化修改dom,input等文本框修改值的时候修改数据1. 数据变化 -> 修改dom;2. 通过表单修改value -> 修改数据

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!