Vue的双向绑定(数据劫持)

news2024/12/23 20:09:18

双向绑定

所谓的双向绑定其实就是,ui或者数据有一方做了修改,那么另外一个也会随着改变。简单来说,视图驱动数据,同时数据也能驱动视图。

视图驱动数据,只需要绑定事件。

数据驱动视图,则需要去对数据做监听,我们通常称之为”数据劫持“(在每一次数据改变的时候,去执行更新视图的操作。)

Vue2 - Object.defineProperty

在vue2.x版本中,数据劫持是用过Object.defineProperty这个API来实现

实现原理:

var message = 'abc';
const data = {};
Object.defineProperty(data, 'message', {
    get() {
        return message;
    },
    set(newVal) {
        message = newVal + '123';
    }
});
data.message // 'abc'
data.message = 'test' // 'test123'

读取对象属性,走get方法。修改对象属性。走set方法。

vue中如何对所有属性进行劫持?

只需遍历所有data对象中的所有属性,并对每一个属性使用Object.defineProperty劫持即可,当属性的值发生变化的时候,我们执行一系列的渲染视图的操作。

// 这是数据
const data = {
    text:'abc',
    number:123,
    info: {
    sex: '男',
        nameInfo: {name:'三叠云'}
    }
}
// 函数提升
observer(data);
// 遍历具体对象
function observer(target) {
    if (typeof target !== 'object' || !target) {
        return target;
    }
    for (const key in target) {
        if (target.hasOwnProperty(key)) {
            const value = target[key];
            observerObject(target, key, value);
        }
    }
}
// 递归劫持对象里的属性
function observerObject(target, name, value) {
    if (typeof value === 'object' || Array.isArray(target)) {
        observer(value);
    }
    Object.defineProperty(target, name, {
        get() {
            return value;
        },
        set(newVal) {
            if (newVal !== value) {
                if (typeof value === 'object' || Array.isArray(value)) {
                    observer(value);
                }
                value = newVal;
            }
            // 触发视图渲染
            renderView();
        }
    });
}

遍历这个data对象,对每一个属性都使用observerObject方法进行数据劫持。

observerObject主要做的就是使用Object.defineProperty去监听传入的属性,如果target是一个对象的话,就递归执行observer,确保data中所有的对象中的所以属性都能够被监听到。当我们set的时候,去执行renderView(执行视图渲染相关逻辑)。

但。这只能作用于对象上。如果是数组,我们需要换种思维劫持数据:

在数组中,我们知道能够改变数组本身的方法只有七种:

  • push

  • pop

  • shift

  • unshift

  • splice

  • sort

  • reverse

而我们只需要修改数组的原型方法,往这些方法里添加一些视图渲染的操作。

// 创建一个继承数组原型链的对象
const oldArrayProperty = Array.prototype; // 数组对象原型链上的属性对象 
const newArrayProperty = Object.create(oldArrayProperty); // 创建继承了oldArrayProperty对象的对象

Object.create的用法可以参见【附录:Object.create】

['pop', 'push', 'shift', 'unshift', 'splice'].forEach((method) => {
    newArrayProperty[method] = function() {
        renderView();
        oldArrayProperty[method].call(this, ...arguments);
    };
});
 // 在observer函数中加入数组的判断,如果传入的是数组,则改变数组的原型对象为我们修改过后的原型。
if (Array.isArray(data)) {
   data.__proto__ = newArrayProperty;
}

上面代码就是vue2x版本数据劫持的原理实现。

现在我们可以看出Object.defineProperty的一些问题:

递归遍历所有的对象的属性,这样如果我们数据层级比较深的话,是一件很耗费性能的事情
只能应用在对象上,不能用于数组
只能够监听定义时的属性,不能监听新加的属性,这也就是为什么在vue中要使用Vue.set的原因,删除也是同理

$set的原理可以见附录:Vue.set

Vue3 - Proxy

在Vue3中则是使用Proxy来进行数据劫持,Proxy不同于Object.defineProperty的是,它是对整个数据对象进行数据劫持,而Object.defineProperty是对数据对象的某个属性进行数据劫持(如果是多层需要循环绑定)。

Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。IE不兼容。

// 语法
const p = new Proxy(target, handler)

参数:

  1. target: 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。

  1. handler: 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

handler 对象包含有 Proxy 的各个捕获器:

handler.defineProperty()
handler.has()//in 操作符的捕捉器。
handler.get(target, property)
handler.set(target, property, value)
handler.deleteProperty()//delete 操作符的捕捉器
......

Proxy如何工作?

① 监控数组下标变化:

let arr=[1,2,3]
let handler={
    get(target, key, receiver) {
        console.log('get的key为 ===>' + key);
        return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver){
        console.log('set的key为 ===>' + key, value);
        // 插入
        return Reflect.set(target, key, value, receiver);
    }
}
let p=new Proxy(arr,handler);
p[1]
< get的key为 ===>1
< 2

p[2] = 5
< set的key为 ===>2 3
< 3

reflect也是es6的语法,再proxy使用中常用到reflect。这俩其实没关系,只是搭配着使用,proxy用来拦截,reflect用来操作。

详可见=》附录:reflect对象

① Proxy的receiver是什么呢?

② Vue3的响应式,为什么 Proxy 要配合 Reflect 一起使用,不能直接使用target[key]返回?

先看代码:

const data = [0,1,2];
let proxyData = new Proxy(data,{
    get(target,key,receiver) {
        console.log(proxyData === receiver);
        return target[key]
    }
})
proxyData[0]; // 0
< true

这里返回了true。那么 receiver 是可以表示代理对象的实例。

let parent = {
    name: 'abc'
}
let proxy = new Proxy(parent, {
  get(target, key, receiver) {
    console.log(receiver === proxy);
    console.log(receiver === child);
    return target[key];
  }
})

let child = { text: "123" };
// 设置 child 继承 代理对象 proxy
Object.setPrototypeOf(child, proxy);
child.name // false true
proxy.name // true false

我们可以清楚的看到 receiver 代表的是 继承了 代理对象 proxy 的 child。

到这里,我们明白了 Proxy 中 get 方法的 receiver 参数不仅仅代表 Proxy 的实例,某种情况下,他也会代表继承 Proxy 的那个对象。

let parent = {
  name: "Tom",
  get value() {
    return this.name;
  },
};
 
let proxy = new Proxy(parent, {
  get(target, key, receiver) {
    return Reflect.get(target, key);
  },
});
 
let child = { name: "小Tom" };
// 设置 child 继承 代理对象 proxy
Object.setPrototypeOf(child, proxy);
 
console.log(child.value); //Tom

打印 child.value 控制台输出的是 "Tom"。

我们分析下上面代码:

当我们调用 child.value 的时候,child 本身并不存在 value 属性。

但是它继承的 proxy 对象中存在 value 属性的访问方法。

所以触发 proxy 上的 get value(),同时由于访问了 proxy 上的 value属性,所以触发 proxy 的 get 方法。

get 方法的 target 参数就是 parent,key 参数 就是 value。

然后方法中 return Reflect.get(target, key) 相当于 target[key]。

此时,我们访问的 child 对象的 value 属性,return 的却是 parent 的 value 属性。this.name的指向

不知不觉中 this 指向在 get 方法中被偷偷修改了,原本调用的 child 在 get 方法中 变成了 parent。

所以打印出来的是 parent[value],也就是 "Tom"。

这显然不是我们期望的结果,当我们访问 child.value 时,我们希望输出 child 对象的 name 属性,也就是 "小Tom"。

那么,Reflect 中 get 方法的 receiver 参数该上场了。

let parent = {
  name: "Tom",
  get value() {
    return this.name;
  },
};
 
let proxy = new Proxy(parent, {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver);
  },
});
 
let child = { name: "小Tom" };
// 设置 child 继承 代理对象 proxy
Object.setPrototypeOf(child, proxy);
console.log(child.value); // 小Tom

上面的代码和前面的一样,只是把 get 方法中 returnReflect.get(target, key) 修改成了

return Reflect.get(target, key, receiver) 之后,打印出来的就是我们期望的结果了。

原理很简单:

首先,我们之前提到过 Proxy 中 get 方法的 receiver 参数不仅仅表示代理对象本身,同时也有可能是继承了代理对象的对象,具体区别于调用方。

这里显然他是指向继承了 proxy 的 child。

然后,我们在 Reflect 中的 get 方法第三个参数传入了 Proxy 中的 receiver,也就相当于 child 作为形参,它会修改调用时的 this 指向。

Reflect 中的 receiver 参数可以把属性访问中的 this 指向 receiver 对象。

Vue3中的 Proxy + Reflect 解决了 Vue2 Object.defineProperty中遇到的哪些问题?

  • 一次只能对一个属性进行监听,需要遍历来对所有属性监听。这个我们在上面已经解决了。

  • 在遇到一个对象的属性还是一个对象的情况下,需要递归监听。

  • 对于对象的新增属性,需要手动监听

  • 对于数组通过push、unshift等方法增加的元素,也无法监听


附录:Object.create

Object.create : 创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)

//创建一个Obj对象
  var Obj ={
    name:'mini',
    age:3,
    show:function () {
      console.log(this.name +" is " +this.age);
    }
  }
  //MyObj 继承obj, prototype指向Obj
  var MyObj = Object.create(Obj,{
    like:{
      value:"fish",        // 初始化赋值
      writable:true,       // 是否是可改写的
      configurable:true,   // 是否能够删除,是否能够被修改
      enumerable:true      //是否可以用for in 进行枚举
    },
    hate:{
      configurable:true,
      // 有get就不能有value
      get:function () { console.log(111);  return "mouse" }, 
      // get对象hate属性时触发的方法
      set:function (value) {                                 
      // set对象hate属性时触发的方法 
        console.log(value,2222);
        return value;
      }    
    }
  });

附录:Vue.set

为什么要用Vue.set

主流JavaScript版本的限制 (以及废弃 Object.observe)。Vue2.x 不能检测数组和对象的变化。

#关于对象

Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的

<template>
    ...
    {{data}}
    <a-button @click="myfn">执行</a-button>
    ...
</template>
export default class text extends Vue {
    ...
    data = {a:4}
    myfn() {
        this.data.b = 5
    }
    ...
}

执行结果为:{a:4} 可以看到,在非初始化状态下往this.data上添加b属性,是非响应式的。

myfn() {
   this.data.a = 5
}
// 这时是响应式的
data = {
  a: { d: 5 },
};
myfn() {
  this.data.a = { d: 5, c: 4 };
};
// 这时是响应式的
data = {
  a: { d: 5 },
};
myfn() {
  this.data.a.c = 4
};
// 这时是非响应式的

对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。对于不存在的属性。Vue无法对其进行数据劫持。

但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。

data = {
    a: { d: 5 },
};
myfn() {
    this.$set(this.data.a, 'c', 4);
}
// 此时是响应式的

数组用法:

this.$set(this.dataArray, index, newValue)

Vue.set工作原理

附录:reflect对象

Reflect 是一个window 内置的一个全局对象,它提供拦截 JavaScript 操作的方法

注意:Reflect 不支持 ie浏览器,所以Vue3不支持ie

与大多数全局对象不同,Reflect并非一个构造函数,所以不能通过new 运算符对其进行调用,或者将Reflect对象作为一个函数来调用。Reflect的所有属性和方法都是静态的(就像Math对象)

reflect语法与proxy一样。常见用法:

一:

Reflect.get(target, propertyKey[, receiver])
  • target: 需要取值的目标对象

  • propertyKey: 需要获取的值的键值

  • receiver:receiver则为getter调用时的this值。

const data = ['a','b','c']
Reflect.get(data,2)  // c
const data2 = {name:'dj',age:18}
Reflect.get(data2,'name') //dj

二:

Reflect.set(target, propertyKey, value[, receiver])

为对象设置或修改属性值

const data = [1,2,3]
Reflect.set(data,0,99)
console.log(data) // [99,2,3]
Reflect.set(data,'length', 1);
console.log(data) // [99]
Reflect.set(data,'length', 3);
console.log(data) // [99, 空, 空]

var obj = {};
Reflect.set(obj, "a", 123); // true
obj.a; // 123

三:

Reflect.deleteProperty(target, propertyKey)

用于删除属性

var obj = { x: 1, y: 2 };
Reflect.deleteProperty(obj, "x"); // true
obj; // { y: 2 }

var arr = [1, 2, 3, 4, 5];
Reflect.deleteProperty(arr, "3"); // true
arr; // [1, 2, 3, , 5]
arr[3]  // undefined 

// 如果属性不存在,返回 true
Reflect.deleteProperty({}, "foo"); // true

// 如果属性不可配置,返回 false
Reflect.deleteProperty(Object.freeze({foo: 1}), "foo"); // false

....还有很多语法应用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/150598.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DC-DC PCB layout经验-含走线宽度和载流量表格

在DC-DC芯片的应用设计中&#xff0c;PCB布板是否合理对于芯片能否表现出其最优性能有着至关重要的影响。不合理的PCB布板会造成芯片性能变差如线性度下降&#xff08;包括输入线性度以及输出线性度&#xff09;、带载能力下降、工作不稳定、EMI辐射增加、输出噪声增加等&#…

不同Nodejs版本的TypeScript的建议配置

Node Target Mapping microsoft/TypeScript Wiki GitHubTypeScript is a superset of JavaScript that compiles to clean JavaScript output. - Node Target Mapping microsoft/TypeScript Wikihttps://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping以上是tsc…

SpringBoot+Vue项目知识管理系统

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7/8.0 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.3.9 浏…

macOS 安装 Frama-C 及使用

操作系统&#xff1a;macOS 12.6 Monterey 官网安装指导&#xff1a;Get Frama-C 一、操作与避坑 &#x1f573;️ 1、macOS 包管理绕不开 Homebrew 工具&#xff0c;确保安装好。 2、安装 Frama-C 的必要依赖 brew install opam gmp gtk gtksourceview libgnomecanvas在安装…

MATLAB-最大值与最小值

在MATLAB中&#xff0c;用于计算最大值的函数是max函数&#xff0c;用于计算最小值的函数是min函数&#xff0c;其调用格式如下。Bmax(A) %计算最大值 &#xff0c;若A为向量&#xff0c;则计算并返回向量中的最大值;若A为矩阵&#xff0c;则计算并返回%一个含有各列最大值的行…

从0到1完成一个Vue后台管理项目(九、引入Breadcrumb面包屑,更改bug)

往期 从0到1完成一个Vue后台管理项目&#xff08;一、创建项目&#xff09; 从0到1完成一个Vue后台管理项目&#xff08;二、使用element-ui&#xff09; 从0到1完成一个Vue后台管理项目&#xff08;三、使用SCSS/LESS&#xff0c;安装图标库&#xff09; 从0到1完成一个Vu…

ansible(第四天)

三&#xff1a;编写playbook 1.Ansible playbook 临时命令可以作为一次性对一组主机运行简单的任务。不过&#xff0c;若要真正发挥Ansible的力量&#xff0c;需要了解如 何使用playbook可以轻松重复的方式对一组主机执行多项复杂的任务。 play是针对对清单中选定的主机运行…

汽车电子系统网络安全组织管理

声明 本文是学习GB-T 38628-2020 信息安全技术 汽车电子系统网络安全指南. 下载地址 http://github5.com/view/764而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 汽车电子系统网络安全组织管理 6.1 组织机构设置 组织应高度重视网络安全&#xff0c…

基于Prometheus+Grafana搭建监控平台(Windows/Linux环境exporter部署)

1. 介绍 1.1 Prometheus是什么?Prometheus&#xff08;普罗米修斯&#xff09;是一个最初在SoundCloud上构建的监控系统。自2012年成为社区开源项目&#xff0c;拥有非常活跃的开发人员和用户社区。为强调开源及独立维护&#xff0c;Prometheus于2016年加入云原生云计算基金会…

【从零开始学习深度学习】43. 算法优化之Adam算法【RMSProp算法与动量法的结合】介绍及其Pytorch实现

Adam算法是在RMSProp算法基础上对小批量随机梯度也做了指数加权移动平均 【可以看做是RMSProp算法与动量法的结合】。 目录1. Adam算法介绍2. 从零实现Adam算法3. Pytorch简洁实现Adam算法--optim.Adam总结1. Adam算法介绍 Adam算法使用了动量变量vt\boldsymbol{v}_tvt​和RMS…

LVGL官方UI设计软件——SquareLine Studio micropython 使用简单测评

经常去LVGL官网逛的人一定都知道这个软件&#xff0c;作为官方的亲儿子&#xff0c;使用体验如何呢&#xff0c;我简单体验了一周左右&#xff0c;简单做个测评&#xff0c;本测评仅代表我个人意见&#xff0c;并且仅限micropython的使用体验&#xff01; 首先是价格&#xff0…

TCP报文段(segment)首部格式

TCP传给IP的数据单元称作TCP报文段或简称为TCP段&#xff08;TCP segment&#xff09;。 IP传给链路层的数据单元称作IP数据报(IP datagram)。 通过以太网传输的比特流称作帧(Frame)。 逐层封装&#xff1a; 源端口号发送端端口号&#xff0c;字段长16位&#xff08;2字节&…

计算机网络第二章

物理层的基本概念物理层的作用&#xff1a;物理层解决如何在连接各种计算机的传输媒体上传输数据比特流&#xff0c;而不是指具体的传输媒体。物理层的主要任务&#xff1a;确定与传输媒体接口有关的一些特性 &#x1f51c;本质&#xff1a;定义一些固定标准物理层的四大特性&a…

Word怎么转PDF?8个Word转PDF工具分析

Word 到 PDF 转换工具是用于将 Microsoft Word&#xff08;DOC 或 DOCX&#xff09;文档转换为 PDF 格式的程序。根据操作模式&#xff0c;它可以是在线或离线软件。当然&#xff0c;考虑到市场上充斥着此类工具&#xff0c;获得最好的 DOCX 到 PDF 转换器可能会让人头疼。正是…

MySQL基础篇第12章(MySQL数据类型)

1. MySQL中的数据类型 常见的数据类型的属性&#xff1a; 2. 整数介绍 2.1 类型介绍 整数类型一共有 5 种&#xff0c;包括 TINYINT、SMALLINT、MEDIUMINT、INT&#xff08;INTEGER&#xff09;和 BIGINT。 它们的区别如下表所示&#xff1a; 2.2 可选属性 整数类型的可选…

javaweb-异步请求AjaxaxiosJSON

1&#xff0c;Ajax 1.1 概述 AJAX (Asynchronous JavaScript And XML)&#xff1a;异步的 JavaScript 和 XML。 我们先来说概念中的 JavaScript 和 XML&#xff0c;JavaScript 表明该技术和前端相关&#xff1b;XML 是指以此进行数据交换。而这两个我们之前都学习过。 ####…

JavaWeb基础——从入门到超神(笔记,持续更新)

day00综述 需要学习SpringBoot&#xff0c;但是JavaWeb是基础&#xff0c;来补一下 JavaWeb就是将数据库中的数据用好看的样式在网页上呈现出来 day01MySQL基础 接下来就是MySQL的安装什么的 mysqld --initialize-insecure mysqld -install net start mysql至此我的电脑上已…

【蓝桥杯-筑基篇】基础入门

&#x1f353;系列专栏:蓝桥杯 &#x1f349;个人主页:个人主页 目录 1.数位翻转 2.三个数求最大值的写法 3.两数交换的几种方法 4.身份证第18位合法性校验 5.黑洞数&#xff08;陷阱数&#xff09; 1.数位翻转 如: 整数 12345 返回结果为整数: 54321 当第一次看到这个题…

【零基础】学python数据结构与算法笔记7

文章目录前言41.查找排序部分习题42.查找排序习题143.查找排序习题244.查找排序习题345.查找排序习题4总结前言 学习python数据结构与算法&#xff0c;学习常用的算法&#xff0c; b站学习链接 41.查找排序部分习题 选题部分来自leetcode 42.查找排序习题1 242. 有效的…

蓝桥杯备赛Day6——链表

目录 数组的缺点 链表 单向链表 双向链表 Python链表的实现 手写链表 数组的缺点 1)需要占用连续的空间 若某个数组很大&#xff0c;可能没有这么大的连续空间给它用。 2〉不方便删除和插入 例如删除数组中间的一个数据&#xff0c;需要把后面所有的数据往前挪填补这个空…