Vue3底层响应式原理 一(基础篇)

7 篇文章 0 订阅
3 篇文章 0 订阅


最近在看Vue3的设计与实现,更多关注的是底层的实现,重点内容做个记录,并且与大家分享一下。
这一篇我们先了解一下响应式系统的一些基本概念以及怎么去实现一个比较简单响应式系统。

一、响应式系统的作用与实现

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

在开始设计一个响应式系统之前,我们应该了解什么是响应式数据,以及什么是副作用函数。

  • 副作用函数
    副作用函数就是会产生副作用的函数
    例如:
function effect () {
	document.body.innerText = "Hello Vue3"
}

解释:当effect函数执行的时候,会改变body的innerText,但是body的innerText可能在其他任何地方都会被使用。这样effect函数的执行影响了其他函数。我们认为effect函数产生了副作用。
我们希望的是,所改变的数据都是响应式的,所谓响应式数据,就是在数据发生变化时候,它的副作用函数会重新执行。
例如:

const obj = {text: "Hello Vue3"}
function effect () {
	document.body.innerText = obj.text;
}

解释:如上代码所写,obj.text在副作用函数effect中引用,我们希望的是,当obj.text一旦发生变化,我们就执行副作用effect函数,这样会永远保证document.body.innerText的值是我们最新改变的那个值。

2、响应式数据的基本实现

在了解了什么是响应式数据以及副作用函数之后,现在的问题变成了我们如何才能让上文的obj变成响应式数据呢?我们有两点线索可以发现。

  • 1、当副作用函数执行的时候,会触发obj.text的读取操作;
  • 2、当修改了obj.text的值,会触发obj.,text的设置操作;
    这样就变成了问题就变成了如何来拦截一个对象的读写操作。
    拦截读取操作:
    思路:当我们读取obj.text的时候,我们可以将它的副作用函数effect放在一个桶里面
    在这里插入图片描述
    接着,当我们设置obj.text的值的时候,会触发obj.text的写操作,我们将桶中的effect函数取出执行即可。
    在这里插入图片描述
    这样我们就保证了在obj.text修改的时候,刷新了一遍obj.text的值,保证其最新的状态。
    那么具体怎么样拦截一个对象的读写操作呢?Vue2是采用ES5的Object.defineProperty方法。Vue3则采用了ES6的Proxy来实现。
    接下来,我们用Proxy来实现上面的思路:
    // 原始数据
    const data = {
        text: "Hello Vue"
    }

    // 设置一个用来存放副作用函数的桶
    const bucket = new Set();

    // 将原始数据用proxy来进行代理
    const obj = new Proxy(data, {
        // 拦截obj的读取操作
        get(target, key) {
            // 读取obj的值的时候,将副作用函数存放进bucket桶中
            bucket.add(effect);
            // 返回属性值
            return target[key];
        },

        // 拦截obj的设置操作
        set(target, key, newVal) {
            // 将原始值设置为最新的值
            target[key] = newVal;
            // 设置完之后将bucket桶中拿出effect()执行
            bucket.forEach(fn => fn());
            // true代表设置成功
            return true;
        }
    })

    // 实验
    // effect副作用函数
    const effect = () => {
        document.body.innerText = obj.text;
    }

    // 执行副作用函数effect,触发obj.text的读取操作(get)
    effect();

    // 设置一个定时器,在三秒之后修改obj.text的值,触发它的设置操作(set)
    setTimeout(() => {
        obj.text = "Hello Vue3"
    }, 3000)
    // 浏览器会在三秒之后从原来的Hello Vue改为Hello Vue3

3、设计一个完善的响应系统

在实现了一个最基本的响应式数据之后,其实上面的代码还存在着很多问题,接下来我们一一解决这些问题,同时完善我们的响应式系统。

  • 问题一
    在上面的代码中,我们所使用的副作用函数采用的是硬编码的方式,但是如果我们的副作用函数一旦改变名字,那么上面的响应式系统将无法正常工作。
    • 解决方法
      提供一个能够注册副作用函数的机制
    // 原始数据
    const data = {
        text: "Hello Vue"
    }
    
    // 设置一个用来存放副作用函数的桶
    const bucket = new Set();
    
    // 将原始数据用proxy来进行代理
    const obj = new Proxy(data, {
        // 拦截obj的读取操作
        get(target, key) {
            // 读取obj的值的时候,将副作用函数存放进bucket桶中
            // bucket.add(effect);  废弃
    
            // 如果activityEffect存在
            if (activityEffect) {
                // 读取obj的值的时候,将副作用函数存放进bucket桶中
                bucket.add(activityEffect);
            }
            // 返回属性值
            return target[key];
        },
    
        // 拦截obj的设置操作
        set(target, key, newVal) {
            // 将原始值设置为最新的值
            target[key] = newVal;
            // 设置完之后将bucket桶中拿出effect()执行
            bucket.forEach(fn => fn());
            // true代表设置成功
            return true;
        }
    })
    
    // 定义一个全局变量,用来接收副作用函数
    let activityEffect;
    
    // effect用来注册副作用函数,接收一个副作用函数fn作为参数
    const effect = (fn) => {
        // 保存副作用函数
        activityEffect = fn;
        // 执行副作用函数
        fn()
    }
    // newEffect新的副作用函数,名字不再局限于effect
    const newEffect = () => {
        // 触发obj的读取操作
        document.body.innerText = obj.text;
    }
    
    // 执行注册副作用函数effect
    effect(newEffect);
    
    // 设置一个定时器,在三秒之后修改obj.text的值,触发它的设置操作(set)
    setTimeout(() => {
        obj.text = "Hello Vue3"
    }, 3000)
    // 浏览器会在三秒之后从原来的Hello Vue改为Hello Vue3
    
  • 问题二
    上面虽然对effect硬编码的方式进行了优化,但是我们进一步测试会发现,当在obj上新添加一个不存在的值的时候,这时候会执行两次副作用函数,例如:
effect( () => {
	console.log("我被打印了!")
    document.body.innerText = obj.text;
});

setTimeout(() => {
    obj.newText = "Hello Vue3"
}, 3000)

多出来的一次打印是因为在obj上设置了newText导致触发了obj的set操作,而set操作回把桶中的副作用函数拿出来执行一次。

  • 解决办法
    导致该问题的根本原因就是我们没有对副作用函数和被操作的目标字段之间建立明确的联系。那么这样,我们就需要对桶的结构重新进行设计。
    我们仔细观察副作用函数,其实就存在三个角色的联系关系。
    在这里插入图片描述
    • 1、代理对象obj
    • 2、obj的属性text
    • 3、副作用函数newEffect
      原来单一的Set满足不了这样的关系,我们可以采用WeakMap、Map、Set三者结合来重新设计桶结构。至于WeakMap与Map的区别以及各自的应用场景,在这里就不多说了,还不了解的可以自己去查一下。
      在这里插入图片描述
      接下来,我们具体用代码来实现一下:
    // 原始数据
    const data = {
        text: "Hello Vue"
    }

    // 设置一个用来存放副作用函数的桶--WeakMap
    const bucket = new WeakMap();

    // 将原始数据用proxy来进行代理
    const obj = new Proxy(data, {
        // 拦截obj的读取操作
        get(target, key) {
            // 如果activityEffect不存在,我们不需要收集副作用函数
            if (!activityEffect) return;

            // 如果存在 我们需要从桶中取出target对应的Map,这个Map中存放的是key
            let depsMap = bucket.get(target);

            // 如果depsMap不存在,说明是第一次读取,我们需要将其添加为响应式数据,并且将副作用函数存在桶中
            if (!depsMap) {
                bucket.set(target, (depsMap = new Map()));
            }

            // 我们需要从Map中取出key对应的Set,这个Set中存放的是effectFn
            let deps = depsMap.get(key);

            // 如果deps不存在,说明该属性还没有副作用函数,将该effectFn添加到Set当中
            if (!deps) {
                depsMap.set(key, (deps = new Set()));
            }
            deps.add(activityEffect);

            // 返回属性值
            return target[key];
        },

        // 拦截obj的设置操作
        set(target, key, newVal) {
            // 将原始值设置为最新的值
            target[key] = newVal;

            // 将WeakMap中取出target对应的key
            const depsMap = bucket.get(target);

            // 如果不存在,说明没有副作用函数,直接返回就行
            if (!depsMap) return true;

            // 如果存在,将读取的key对应的存放effecfFn从Set中取出来
            const effectFns = depsMap.get(key);

            // 如果Set中存在副作用函数,就执行副作用函数
            effectFns && effectFns.forEach(fn => fn());
        }
    })

    // 定义一个全局变量,用来接收副作用函数
    let activityEffect;

    // effect用来注册副作用函数,接收一个副作用函数fn作为参数
    const effect = (fn) => {
        // 保存副作用函数
        activityEffect = fn;
        // 执行副作用函数
        fn()
    }
    // newEffect新的副作用函数,名字不再局限于effect
    const newEffect = () => {
        console.log("我被打印了")
        // 触发obj的读取操作
        document.body.innerText = obj.text;
    }

    // 执行注册副作用函数effect
    effect(newEffect);

    // 设置一个定时器,在三秒之后设置一个新属性
    setTimeout(() => {
        obj.newtext = "Hello Vue3"
    }, 3000)
    // "我被打印了" 只打印了一次

这样我们就简单的实现了一个微响应式系统,其实上面的代码get和set中可以做一些灵活性的封装,有兴趣的可以试试。
有了这篇的基础,下一周可以写一下Vue3的非基本类型的响应式原理!

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue3.0来了,你还学的动吗? 2020年9月底,Vue3.0正式版终于发布了。Vue在全球拥有 130 多万用户 ,它被应用于各种不同的场景中。而在国内,更是最火热的前端框架,2.0与3.0的开发模式有了很大的改变,所以3.0的全新版本势必会引发新的一波学习潮流。对于前端开发者来说,不管你嘴上如何“学不动”,注定离不开“真相定律”,Vue3.0是你提升自身技术能力,晋升中级工程师一定要掌握的。  本课程将基于 Vue3.0 正式版,从Vue基础语法入手,全面掌握 Vue3.0 全家桶技术栈,并结合实战项目开发,让你拥有Vue3.0项目开发经验,更好的掌握Vue开发企业项目的流程 。 本课程共包括三个模块 一、Vue基础 本模块将讲解Vue3.0基本用法、插值表达式、常用指令等知识点,还会精讲Vue 3.0核心语法,如计算属性、过滤器、监视器、模板、生命周期等内容。会带你深入理解Vue组件化技术,讲解全局组件和局部组件的定义,组件间数据传递,以及Vue内置组件等知识点。让你掌握模块化的概念,会通过Vue脚本架搭建项目,并掌握使用Axios发送AJAX请求,让你快速入门Vue3.0。 二、Vue核心 这个模块会带你讲解Vue3.0全家桶的知识点(Vue Router路由+Vuex状态管理),涉及Vue路由的用法、包括路由嵌套、路由模式、编程式导航、路由守卫等,还会全面讲解Vuex状态管理机制及使用,理解state、mutation、action等核心概念,让你轻松掌握Vue全家桶。 三、项目实战 实战项目会贴近企业流程,按照企业级别代码质量和工程开发流程进行授课,让你理解这套技术在企业中被使用的真实流程,更好的掌握Vue各个基础知识点。项目开发流程包括项目需求分析->环境搭建与配置->搭建远程Git仓库->接口分析->项目开发->打包上线。实战项目涉及内容

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值