我的编程空间,编程开发者的网络收藏夹
学习永远不晚

深入探讨Vue3的响应式机制

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

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

深入探讨Vue3的响应式机制

什么是响应式?Vue是怎么实现响应式的?下面本篇文章带大家深入了解下Vue3的响应式原理,希望对大家有所帮助!

深入探讨Vue3的响应式机制

Vue这个框架相信大家都不陌生了,提到Vue我相信面试官首先会问的问题之一就是Vue的响应式原理是如何实现的,之前也写过一篇Vue2的响应式原理的文章,那么我们今天就来聊聊Vue3的响应式机制。【相关推荐:vuejs视频教程、web前端开发】

一、什么是响应式?

在javascript中的变量是没有响应式这么一个概念的,代码的执行逻辑都是自上而下的,而在Vue框架中,响应式是特色功能之一。我们先看个例子

let num = 1;
let double = num * 2;
console.log(double); // 2
num = 2;
console.log(double); // 2

可以很明显看出来double这个变量和num这个变量的关系并不是响应式的,如果我们将计算double的逻辑封装成一个函数,当num这个变量的值改变,我们就重新执行这个函数,这样double的值就会随着num的改变而改变,也就是我们俗称的响应式的。

let num = 1;
// 将计算过程封装成一个函数
let getDouble = (n) => n * 2;
let double = getDouble(num);
console.log(double); // 2

num = 2;
// 重新计算double,这里当然也没有实现响应式,只是说明响应式实现的时候这个函数应该再执行一次
double = getDouble(num);
console.log(double); // 4

虽然实际开发的过程中会比现在这样简单的情况复杂很多,但是就是可以封装成一个函数去实现,现在的问题就在于我们如何使得double的值会根据num变量的改变而重新计算呢?

如果每一次修改num变量的值,getDouble这个函数都能知道并且执行,根据num变量的改变而给double也相应的发生改变,这样就是一个响应式的雏形了。

二、响应式原理

在Vue中使用过三种响应式解决方案,分别是definePropertyProxyvalue setter。在Vue2中是使用了 defineProperty API,在这之前的文章中有过较为详细的描述,想了解Vue2响应式的小伙伴戳这里--->vue响应式原理 | vue2篇

defineProperty API

在 Vue2 中核心部分就在于 defineProperty 这个数据劫持 API ,当我们定义一个对象obj,使用 defineProperty 代理 num 属性,读取 num 属性时执行了 get 函数,修改num属性时执行了 set 函数,我们只需要将计算 double 的逻辑写在 set 函数中,就可以使得每次 num 改变时, double 被相应的赋值,也就是响应式。

let num = 1;
let detDouble = (n) => n * 2;
let obj = {}
let double = getDouble(num)

Object.defineProperty(obj,'num',{
    get() {
        return num;
    }
    set(val){
        num = val;
        double = getDouble(val)
    }
})
console.log(double); // 2
obj.num = 2;
console.log(double); // 4

defineProperty缺陷:当我们删除obj.num属性时,set函数不会执行,所以在Vue2中我们需要一个$delete 一个专门的函数去删除数据。并且obj对象中不存在的属性无法被劫持,并且修改数组上的length属性也是无效的。

Proxy

单从 Proxy 的名字我们可以看出它是代理的意思,而 Proxy 的重要意义是解决了 Vue2 响应式的缺陷。

Proxy用法:

var proxy = new Proxy(target, handler);

Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中,new Proxy() 表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。

var proxy = new Proxy({}, {
  get: function(target, propKey) {
    return 35;
  }
});

proxy.time // 35
proxy.name // 35
proxy.title // 35

在 Proxy 身上支持13种定制拦截

  • get(target, propKey, receiver) :拦截对象属性的读取,比如proxy.fooproxy['foo']
  • set(target, propKey, value, receiver) :拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = v,返回一个布尔值。
  • has(target, propKey) :拦截propKey in proxy的操作,返回一个布尔值。
  • deleteProperty(target, propKey) :拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target) :拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey) :拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc) :拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target) :拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target) :拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target) :拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto) :拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args) :拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)
  • construct(target, args) :拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)

Reflect

在ES6中官方新定义了 Reflect 对象,在ES6之前对象上的所有的方法都是直接挂载在对象这个构造函数的原型身上,而未来对象可能还会有很多方法,如果全部挂载在原型上会显得比较臃肿,而 Reflect 对象就是为了分担 Object的压力。

(1) 将Object对象的一 些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上现阶段,某些方法同时在ObjectReflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。

(2) 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false

// 老写法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}

// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}

(3) 让Object操作都变成函数行为。某些Object操作是命令式,比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为。

// 老写法
'assign' in Object // true

// 新写法
Reflect.has(Object, 'assign') // true

(4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。

Proxy(target, {
  set: function(target, name, value, receiver) {
    var success = Reflect.set(target, name, value, receiver);
    if (success) {
      console.log('property ' + name + ' on ' + target + ' set to ' + value);
    }
    return success;
  }
});

所以我们在这里会使用到 Proxy 和 Reflect 对象的方与 Proxy 一一对应这一特性,来实现Vue3的响应式原理。

三、Vue3响应式原理的实现

在Vue3中响应式的核心方法是

function reactive (target){
    // 返回一个响应式对象
    return createReactiveObject(target); 
}

根据我们前面所做的铺垫,所以我们会使用 Proxy 代理我们所需要的相应的对象,同时使用 Reflect 对象来映射。所以我们先初步实现一下,再慢慢优化,尽可能全面。

判断是否为对象(方法不唯一,有多种方法)

function isObject(val){
    return typeof val === 'object' && val !== null
}

尽可能采用函数式编程,让每一个函数只做一件事,逻辑更加清晰。

初步实现

function createReactiveObject (target) {
    // 首先由于Proxy所代理的是对象,所以我们需要判断target,若是原始值直接返回
    if(!isObject(target)) {
        return target;
    }
    
    let handler = {
        get(target, key, receiver) {
            let res = Reflect.get(target, key, receiver); // 使用Reflect对象做映射,不修改原对象
            console.log('获取');
            return res;
        },
        set(target, key, value, receiver) {
            let res = Reflect.set(target, key, value, receiver);
            console.log('修改');
            return res
        },
        deleteProperty(target, key) {
            let res = Reflect.deleteProperty(target, key)
            console.log('删除');
            return res;
        }
    }
    let ProxyObj = new Proxy(target,handler); // 被代理过的对象
    return ProxyObj;
}

image.png

但是这样会有一个问题,如果我需要代理的对象是深层嵌套的对象呢?我们先看看效果

image.png

当我们深层代理时,我们直接修改深层对象中的属性并不会触发 Proxy 对象中的 set 方法,那为什么我们可以修改呢?其实就是直接访问原对象中深层对象的值并修改了,那我们如何优化这个问题呢?

那也需要用到递归操作,判断深层对象是否被代理了,如果没有再执行reactive将内部未被代理的对象代理。

那么我们在 get 方法内部就不能直接将映射之后的 res 返回出去了

解决代理对象内部有嵌套对象

get(target, key, receiver) {
            let res = Reflect.get(target, key, receiver); // 使用Reflect对象做映射,不修改原对象
            console.log('获取');
            // 判断代理之后的对象是否内部含有对象,如果有的话就递归一次
            return isObject(res) ? reactive(res) : res;
        }

解决对象重复代理(多次代理、多层代理)

这样我们就实现了对象的深层代理,并且只有当我们访问到内部嵌套的对象时我们才 会去递归调用reactive ,这样不仅可以实现深层代理,并且节约了性能,但是其实我们还没有彻底完善,我们来看看下面这段代码

let proxy = reactive({name: '寒月十九', message: { like: 'coding' }});
reactive(proxy);
reactive(proxy);
reactive(proxy);

这样是不是合法的,当然是合法的,但是没有必要也没有意义,所以为了避免被代理过的对象,再次被代理,太浪费性能,所以我们需要将被代理的对象打上标记,这样当带被代理过的对象访问到时,直接将被代理过的对象返回,不需要再次代理。

在 Vue3 中,使用了hash表做映射,来记录是否已经被代理了。

// WeakMap-弱引用对象,一旦弱引用对象未被使用,会被垃圾回收机制回收
let toProxy = new WeakMap();  // 存放形式 { 原对象(key): 代理过的对象(value)}
let toRow = new WeakMap();  // 存放形式 { 代理过的对象(key): 原对象(value)}
let ProxyObj = new Proxy(target,handler); // 被代理过的对象
toProxy.set(target,ProxyObj);
toRow.set(ProxyObj.target);
return ProxyObj;
let ByProxy = toProxy.get(target);
// 防止多次代理
if(ByProxy) { // 如果在WeakMap中可以取到值,则说明已经被代理过了,直接返回被代理过的对象
    return ByProxy;
}
// 防止多层代理
if(toRow.get(target)) {
    return target
}
// 为了防止下面这种写法(多层代理)
// let proxy2 = reactive(proxy);
// let proxy3 = reactive(proxy2);
// 其实本质上与下面这种写法没有区别(多次代理)
// reactive(proxy);
// reactive(proxy);
// reactive(proxy);

数组相应问题

let arr = [1 ,2 ,3 ,4];
let proxy = reactive(arr);
proxy.push(5);
// 在set内部其实会干两件事,首先会将5这个值添加到数组下标4的地方,并且会修改length的值

image.png

与 Vue2 的数据劫持相比,Vue3 中的 Proxy 可以直接修改数组的长度,但是这样我们需要在 set 方法中判断我们是要在代理对象身上添加属性还是修改属性。

因为更新视图的函数会在set函数中调用,我们向数组中进行操作会触发两次更新视图,所以我们需要做一些优化。

// 判断属性是否原本存在
function hasOwn(target,key) {
    return target.hasOwnProperty(key);
}

set(target, key, value, receiver) {
    let res = Reflect.set(target, key, value, receiver);
    // 判断是新增属性还是修改属性
    let hadKey = hasOwn(target,key);
    let oldValue = target[key];
    if(!hadKey) { // 新增属性
        console.log('新增属性');
    }else if(oldValue !== value){
        console.log('修改属性');
    }
    return res
 },

避免多次更新视图,比如修改的值与原来一致就不更新视图,在上面两个判断条件中添加更新视图的函数,就不会多次更新视图。

完整版代码

function isObject(val) {
  return typeof val === 'object' && val !== null
}

function reactive(target) {
  // 返回一个响应式对象
  return createReactiveObject(target);
}

// 判断属性是否原本存在
function hasOwn(target, key) {
  return target.hasOwnProperty(key);
}

// WeakMap-弱引用对象,一旦弱引用对象未被使用,会被垃圾回收机制回收
let toProxy = new WeakMap();  // 存放形式 { 原对象(key): 代理过的对象(value)}
let toRow = new WeakMap();  // 存放形式 { 代理过的对象(key): 原对象(value)}

function createReactiveObject(target) {
  // 首先由于Proxy所代理的是对象,所以我们需要判断target,若是原始值直接返回
  if (!isObject(target)) {
    return target;
  }

  let ByProxy = toProxy.get(target);
  // 防止多次代理
  if (ByProxy) { // 如果在WeakMap中可以取到值,则说明已经被代理过了,直接返回被代理过的对象
    return ByProxy;
  }
  // 防止多层代理
  if (toRow.get(target)) {
    return target
  }

  let handler = {
    get(target, key, receiver) {
      let res = Reflect.get(target, key, receiver); // 使用Reflect对象做映射,不修改原对象
      console.log('获取');
      return isObject(res) ? reactive(res) : res;
    },
    set(target, key, value, receiver) {
      let res = Reflect.set(target, key, value, receiver);
      // 判断是新增属性还是修改属性
      let hadKey = hasOwn(target, key);
      let oldValue = target[key];
      if (!hadKey) { // 新增属性
        console.log('新增属性');
      } else if (oldValue !== value) {
        console.log('修改属性');
      }
      return res
    },
    deleteProperty(target, key) {
      let res = Reflect.deleteProperty(target, key)
      console.log('删除');
      return res;
    }
  }
  let ProxyObj = new Proxy(target, handler); // 被代理过的对象
  return ProxyObj;
}

// let proxy = reactive({name: '寒月十九'});
// proxy.name = '十九';
// console.log(proxy.name);
// delete proxy.name;
// console.log(proxy.name);

// let proxy = reactive({name: '寒月十九', message: { like: 'coding' }});
// proxy.message.like = 'writing';
// console.log('====================================');
// console.log(proxy.message.like);
// console.log('====================================');

let arr = [1, 2, 3, 4];
let proxy = reactive(arr);
proxy.push(5)

Proxy的缺陷

在IE11以下的浏览器都不兼容,所以如果使用 Vue3 开发一个单页应用的项目,需要考虑到兼容性问题,需要我们做额外的很多操作,才能使得IE11 以下的版本能够兼容。

(学习视频分享:vuejs入门教程、编程基础视频)

以上就是深入探讨Vue3的响应式机制的详细内容,更多请关注编程网其它相关文章!

免责声明:

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

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

深入探讨Vue3的响应式机制

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

深入探讨Vue3的响应式机制

什么是响应式?Vue是怎么实现响应式的?下面本篇文章带大家深入了解下Vue3的响应式原理,希望对大家有所帮助!
2023-05-14

深入探讨Java SPI机制及其应用场景

本文深入介绍了Java SPI机制的原理和实现方式,以及在Java中如何使用SPI实现可插拔的组件化架构,包括示例代码和应用场景分析,帮助读者更好地理解和应用该机制
2023-05-17

深入探讨MySQL InnoDB引擎的锁机制

MySQL InnoDB 锁的深入解析在MySQL数据库中,锁是保证数据完整性和一致性的重要机制。而InnoDB存储引擎作为MySQL中最常用的存储引擎之一,其锁机制更是备受关注。本文将深入解析InnoDB存储引擎的锁机制,包括锁的类型、加
深入探讨MySQL InnoDB引擎的锁机制
2023-12-21

深入探讨Golang中的文件锁机制

Golang(Go语言)是一门越来越受欢迎的编程语言,其简洁高效的特点吸引了众多开发者的喜爱。在Golang中,文件锁机制是一种常用的同步技术,用于管理和保护文件或共享资源的访问。本文将深入探讨Golang中的文件锁机制,介绍其原理、应用场
深入探讨Golang中的文件锁机制
2024-02-29

深入探讨Golang中Map的修改机制

Golang中Map的修改机制是指在使用Map类型的数据结构时,对Map中的键值对进行修改操作时所涉及到的一系列规则和机制。本文将通过详细介绍Golang中Map的基本概念、操作方法以及修改时的机制,通过具体的代码示例来帮助读者更深入地了解
深入探讨Golang中Map的修改机制
2024-03-03

深入理解Vue3响应式原理

响应式就是当对象本身(对象的增删值)或者对象属性(重新赋值)发生变化时,将会运行一些函数,最常见的就是render函数,下面这篇文章主要给大家介绍了关于Vue3响应式原理的相关资料,需要的朋友可以参考下
2022-12-19

Vue3的响应式机制怎么实现

这篇文章主要介绍了Vue3的响应式机制怎么实现的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Vue3的响应式机制怎么实现文章都会有所收获,下面我们一起来看看吧。一、什么是响应式?在javascript中的变量是
2023-07-04

Vue3响应式机制源码分析

本篇内容介绍了“Vue3响应式机制源码分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!什么是响应式响应式一直都是 Vue 的特色功能之一;
2023-07-06

深入探讨:Go语言主函数的等待机制

标题:深入探讨:Go语言主函数的等待机制Go语言作为一门高效、并发性强的编程语言,其特有的主函数等待机制在编写多线程程序时显得尤为重要。本文将深入探讨Go语言中主函数的等待机制,结合具体的代码示例来说明其实现原理。在Go语言中,主函数的
深入探讨:Go语言主函数的等待机制
2024-03-11

深入探讨 PHP 中防抖机制的实现方法

防抖机制是一种常用于避免函数频繁触发的技术,特别是在用户交互操作中。在 PHP 中,防抖机制可以用来处理用户连续点击或频繁触发的函数调用,从而有效地降低服务器的压力和提升用户体验。本文将深入探讨 PHP 中防抖机制的实现方法,并提供具体的代
2023-10-21

深入探讨PyCharm中自动换行的机制及实现

在软件开发中,PyCharm是一款被广泛使用的集成开发环境。它能够为开发者提供丰富的功能和工具,使得编程变得更加高效和愉快。其中,自动换行功能是PyCharm中的一个重要特性,本文将深度解读自动换行的原理和实现,并提供具体的代码示例。自动
深入探讨PyCharm中自动换行的机制及实现
2024-02-24

一文带你深入理解Vue3响应式原理

响应式就是当对象本身(对象的增删值)或者对象属性(重新赋值)发生变化时,将会运行一些函数,最常见的就是render函数,下面这篇文章主要给大家介绍了关于Vue3响应式原理的相关资料,需要的朋友可以参考下
2022-11-13

深入探讨Go语言同步机制的原理与实现

Go语言作为一种面向并发编程的语言,在其同步机制设计中引入了goroutine、channel以及select语句等特性,使得并发编程变得更加容易和高效。本文将深入探讨Go语言同步机制的原理与实现,并结合具体的代码示例进行讲解。1. Go
深入探讨Go语言同步机制的原理与实现
2024-03-02

如何进行Java多线程同步机制的深入探讨

今天就跟大家聊聊有关如何进行Java多线程同步机制的深入探讨,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。在很多程序员的脑海中Java多线程都会占据一定的位置,众所周知,在Java多
2023-06-17

深入探讨Go语言中的类型转换机制及实践

本篇文章给大家分享《深入探讨Go语言中的类型转换机制及实践》,覆盖了Golang的常见基础知识,其实一个语言的全部知识点一篇文章是不可能说完的,但希望通过这些问题,让读者对自己的掌握程度有一定的认识(B 数),从而弥补自己的不足,更好的掌握
深入探讨Go语言中的类型转换机制及实践
2024-04-04

编程热搜

目录