响应式数据

一、何为响应式

在我们的程序中,总是需要去修改JS变量中的数据,然后使得页面中的内容也跟着变化,如下面的代码

<body><p>姓氏: <span class="lastName"></span></p><p>名字: <span class="firstName"></span></p><p>年龄: <span class="age"></span></p>
</body>
<script>var data = {name: "赵信",age: 18,}// 修改姓氏function getLastName() {let lastName = data.name.substring(1, 0);document.querySelector(".lastName").textContent = lastName;}// 修改名字function getFirstName() {let firstName = data.name.substring(1);document.querySelector(".firstName").textContent = firstName;}// 修改年龄function getAge() {document.querySelector(".age").textContent = data.age;}// 免喷卡:上述函数只是粗略写,各位大神不要那么认真说 获取姓氏名字代码不是这样写...getLastName();getFirstName();getAge();// 希望修改下方属性值后页面也会改变data.name = "杨过";
</script>

由上面代码我们可以看到页面,虽然我们改了data.name和data.age,但是页面中还是显示姓氏:赵;名字:信;年龄:18;,并没有把我们想修改的姓名修改成姓氏:杨;名字:过

其实我们不难看出,下面虽然执行了data.name=杨过,但是却没让Js执行对应的修改dom的函数,页面又怎么可能会修改呢?

所以我们就是应该在data.name = 杨过下面执行getLastName();getFirstName()这两个方法

那么响应式数据就是这样了,只要数据有变化,页面也应该跟着变化

可以粗略的说:如果我们修改了data.name的值,那么只要执行了getLastName、getFirstName函数,那么就是响应式数据了

二、实现响应式

在我们前面的代码中,已经说明了什么是响应式,但是对于该代码,是非常繁琐的:“我每次修改name,我就要调用一下getLastName();getFirstName()这两个函数,如果我修改了10次name,那岂不是要调用10次函数,很显然这种实现是有问题的"

那是否有什么方法或者API让他在每一次修改name的时候就自动调用getLastName();getFirstName()这两个函数呢?

VUE2.0的实现

而Object.defineProperty这个方法,正好可以实现我们想要的效果

我们先来看一下这个Object.defineProperty到底是干嘛的,为什么能实现这个方法

浅尝一下Object.defineProperty

官方定义

Object.defineProperty() 方法会直接作用在一个对象上,或者修改一个对象之后返回这个对象。

官方定义很难让我们看出这个方法能干嘛,不如我们看看他的参数,然后自己动手浅尝一下

  • obj要定义属性的对象。

  • prop要定义或修改的属性的名称或者Symbol。

  • descriptor要定义或修改的属性描述符。

尝试
var obj = {test: "测试"
};
Object.defineProperty(obj,"test",{// 修改当前属性的值,既"测试"改为"测试成功"value : "测试成功",// 该属性为布尔值。默认为true,当改为false时,就不能再修改这个属性的值了;writable : false, // 该属性为布尔值。默认为true,当改为false时,就不能被for in遍历到或者Object.keys()中被枚举;enumerable : false,// 该属性为布尔值。默认为true,当改为false时,表示对象的属性是否可以被删除,以及除value和writable 特性外的其他特性是否可以被修改configurable : true,// get函数,该函数必须要有返回值,在获取监听的属性时自动调用该函数,并且返回值为获取属性get() {return 1;},// set函数,在修改该对象的属性值时,会自动调用该函数,参数为赋值的值set(value) {console.log(value);},
})

阅读上面代码,基本上大概就能明白这个api的使用,可能比较难理解的是get函数和set函数,下面我们具体讲解,这两个方法也是我们Vue2.0实现响应式的重点

var obj = {name: "赵信",
};
var name = "";
// 我们这里去监听对象的name属性
Object.defineProperty(obj,"name",{get(){console.log("获取了对象的name的属性,但我还是给他返回了赵信");return "赵信";},set(value){console.log("修改了name属性值,修改的值为:" + value)name = value;}
});
obj.name = "如来佛祖玉皇大帝观音菩萨指定取西经特派使者花果山水帘洞美猴王齐天大圣孙悟空";
console.log(obj.name);
console.log(name);

我们会发现,浏览器输出控制台居然显示出了四个输出,输出顺序为

  1. 修改了name属性值,修改的值为: 如来佛祖玉皇大帝观音菩萨指定取西经特派使者花果山水帘洞美猴王齐天大圣孙悟空

  1. 获取了对象的name的属性,但我还是给他返回了赵信

  1. 赵信

  1. 如来佛祖玉皇大帝观音菩萨指定取西经特派使者花果山水帘洞美猴王齐天大圣孙悟空

总结

看到这里大概就可以理解了,当去修改这个Object.defineProperty监听的对象的属性的时候,如果外部有人修改了这个属性,则会自动调用set函数;外部有人获取该对象的时候,则会自动调用get函数;

这不就是我们刚刚上面想要的响应式实现的效果吗?下面我们来实现一下响应式

实现

现在我们回到刚刚一开始的代码,并且使用Object.defineProperty试试,看看能不能达到我们想要的效果

<body><p>姓氏: <span class="lastName"></span></p><p>名字: <span class="firstName"></span></p><p>年龄: <span class="age"></span></p>
</body>
<script>var data = {name: "赵信",age: 18,}// 修改姓氏function getLastName() {let lastName = data.name.substring(1, 0);document.querySelector(".lastName").textContent = lastName;}// 修改名字function getFirstName() {let firstName = data.name.substring(1);document.querySelector(".firstName").textContent = firstName;}// 修改年龄function getAge() {document.querySelector(".age").textContent = data.age;}// 免喷卡:上述函数只是粗略写,各位大神不要那么认真说 获取姓氏名字代码不是这样写...getLastName();getFirstName();getAge();// 以下为新增修改的代码;var copyName = data.name; // 备份set的时候的值,不然我在获取的时候怎么知道要给他什么值呢Object.defineProperty(data,"name",{get(){// 读取的时候我直接将备份的名字返回回去return copyName;},set(val) {// 在修改值的时候,我们把值备份一遍copyName = val;// 自动调用渲染姓氏和渲染名字的函数getLastName();getFirstName();}})// 希望修改下方属性值后页面也会改变data.name = "杨过";
</script>

我们在新增了值之后会发现,我只要修改了data.name,不管是在代码修改,还是在浏览器控制台上修改,都可以自动修改页面上的姓氏和名字了;那么我们想要的效果,不就达到了吗,这个时候只需要再给data.age也监听一下属性,那我在修改age的时候,页面上的年龄也会变化了

这就是Vue2的响应式数据实现原理了,但是这样的代码是非常冗余的,我要是有10个属性,那我是不是应该把10个属性都用Object.defineProperty,此时我们就应该把代码优化成一个通用的方法,让每个属性都可以自动的监听到,并且调用渲染的函数。

优化

既然上面说如果有多个属性,那就给多个属性都遍历一遍,然后批量注册,这样不就达到了我们上面说的代码冗余问题。那我们现在就写一个公用方法,直接把data传进来,然后给obj的属性批量注册。具体实现:

function observe(data) {// 遍历对象for (const key in data) {let copyValue = data[key]; // 备份值Object.defineProperty(data,key,{get() {return copyValue;},set(val){copyValue = val;// 执行修改dom的函数(执行依赖这个属性的函数)  ???这里的代码要怎么实现呢},})}
}

我们会发现,set的时候我们没法搞啊。我们要怎么拿到这个值需要修改dom的函数。有人会说,那把函数传进来呢?那我们遍历的对象,怎么知道哪个是对应的属性的函数呢?

通过get函数去收集依赖,收集哪个函数应该执行

那么Vue是怎么去收集依赖的呢?

首先我们先把需要调用的函数收集起来,然后在set的执行的时候批量调用,我们先一步一步来

function observe(data) {// 遍历对象for (const key in data) {let copyValue = data[key]; // 备份值let updateFuns = []; // 收集的函数Object.defineProperty(data,key,{get() {// fun怎么来???if(!updateFuns.includes(fun)){ // 判断是否存在,防止批量执行的时候重复调用updateFuns.push(fun);}return copyValue;},set(val){copyValue = val;// 遍历收集起来的函数,批量执行updateFuns.forEach((updateFun)=>{updateFun();})},})}
}

现在我们就差最后一步了,我们上面的代码中fun这个函数,应该从哪里来呢?

只要解决了这一点,那我们这个响应式就可以实现了,该如何收集?

下面我们直接给代码:

<body><p>姓氏: <span class="lastName"></span></p><p>名字: <span class="firstName"></span></p><p>年龄: <span class="age"></span></p>
</body>
<script>// ...// 修改年龄function getAge() {document.querySelector(".age").textContent = data.age;}// 批量省代码,具体代码看上面observe(data); // 先批量监听对象的属性 // 我们在window上挂载这个函数window.__fun = getLastName;getLastName(); // 调用这个函数的时候,会用到data.name,这样在get的时候就可以收集到这个函数啦;window.__fun = null; // 收集到函数后把window.__fun还原// 每个都来一遍window.__fun = getFirstName;getFirstName();window.__fun = null;window.__fun = getAge;getAge();window.__fun = null;// 然后我们将observe函数改装一下function observe(data) {// 遍历对象for (const key in data) {let copyValue = data[key]; // 备份值let updateFuns = []; // 收集的函数Object.defineProperty(data,key,{get() {// fun怎么来??? 从window上获取if(!updateFuns.includes(window.__fun)){ // 判断是否存在,防止批量执行的时候重复调用updateFuns.push(window.__fun); // 收集起来}return copyValue;},set(val){copyValue = val;// 遍历收集起来的函数,批量执行updateFuns.forEach((updateFun)=>{updateFun();})},})}};   // 希望修改下方属性值后页面也会改变data.name = "杨过";
</script>

这样我们就实现了一个vue的数据响应式功能啦,最后只需要再来一个封装,把下面的代码再封装一遍基本上就完成了。

function runFun(fn) {window.__fun = fn;fn();window.__fun = null;
}

以上就是Vue实现响应式的基本过程啦,但是其实这样的实现是有缺陷的,具体下面会讲解

Vue3.0的实现

上面我们实现了响应式的基本过程,如果感兴趣的朋友可以通过阅读Vue2.0源码会发现,我们仅仅通过Object.defineProperty去实现其实是有很大缺陷的,下面列举几个常见的

1.我们如果对一个对象进行删除与添加属性操作,是无法劫持到的;因为我们是通过遍历对象每个属性去监听的,如果突然加了个属性或者少了个属性,这个对象是完全无感知的

const obj = {foo: "foo",bar: "bar"
}
observe(obj)
delete obj.foo // 这样页面是没反应的
obj.jar = 'xxx' // 如果页面用到了obj?.jar,但是一开始是没有,后续加上页面也是没响应的

2.当我们对一个数组进行监听的时候,并不那么好使

const arrData = [1,2,3,4,5];
observe(arrData);
arrData.push() // 没反应
arrData.pop()  // 没反应
arrDate[6] = 99 // 没反应

上面2个我们可以看到Object.defineProperty无法劫持到,从而无法实现数据响应式,

所以在Vue2源码中,增加了set、delete ,并且对数组api方法进行一个重写

大概代码如下,有兴趣的可以了解一下,没兴趣的直接看下面

// 数组重写
const originalProto = Array.prototype;
const arrayProto = Object.create(originalProto);
['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(method => {arrayProto[method] = function () {originalProto[method].apply(this.arguments)dep.notice()}
});// set、delete
Vue.set(obj,'bar','newbar')
Vue.delete(obj),'bar')

3.如果存在深层的嵌套对象关系,需要深层的进行监听,造成了性能的极大问题

所以在Vue3.0中就放弃了Object.defineProperty实现数据响应式,从而该为另外一种proxy

浅尝一下Proxy

官方定义

用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)

这个描述起码比Object.defineProperty好一点,起码知道他是劫持对象的,具体参数如下:

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

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

尝试
var obj = {test: "测试"
};
const handler = {get: function(obj, prop) {return prop in obj ? obj[prop] : "没有这个属性哦";}
};
const proxyObj = new Proxy(obj, handler); // 代理了的对象
proxyObj.test = "测试成功";
proxyObj.a = "我新增了a";console.log(proxyObj.test, proxyObj.a);      // "测试成功", "我新增了a"
console.log(proxyObj.c); // "没有这个属性哦"
// 然后我们输出obj看一下;
console.log(obj.test, obj.a);      // "测试成功", "我新增了a"
console.log(obj.c); // "没有这个属性哦"
// 我们会发现,我们操作的被代理对象也会自动修改

我们会发现,这样实现比我们在 Object.defineProperty上实现更加的方便,只需要直接代理一下当前这个对象就好了,不需要再去遍历代理了

Proxy还有很多截取对象的方法,这里就不说明了,有兴趣的可以去看一下官方文档

Proxy官方文档

下面我们来测试一下到底有没有我们想要的效果

function observe(obj) {if (typeof obj !== 'object' && obj != null) {return obj}// Proxy代理逻辑const observed = new Proxy(obj, {get(target, key, receiver) {const res = Reflect.get(target, key, receiver)console.log(`获取${key}:${res}`)return res},set(target, key, value, receiver) {const res = Reflect.set(target, key, value, receiver)console.log(`设置${key}:${value}`)return res},deleteProperty(target, key) {const res = Reflect.deleteProperty(target, key)console.log(`删除${key}:${res}`)return res}})return observed
}

测试一下

const obj = [1,2,3]
const proxyObj = observe(obj)
obj.psuh(4) // 控制台输出劫持到了

实现

其他实现方式跟VUE2就差不多了,有兴趣的可以自己去试一下

缺点

Proxy 不兼容IE,也没有 polyfill, defineProperty 能支持到IE9

总结

  • Object.defineProperty只能遍历对象属性进行劫持

  • Proxy直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的

  • Proxy可以直接监听数组的变化(push、shift、splice)

以上就是Vue实现响应式数据的基本理解啦,End

感谢大家耐心看到这里!

Vue响应式数据基本讲解(2.0和3.0)相关推荐

  1. Vue响应式数据: Observer模块实现

    前言 首先欢迎大家关注我的Github博客,也算是对我的一点鼓励,毕竟写东西没法获得变现,能坚持下去也是靠的是自己的热情和大家的鼓励.接下来的日子我应该会着力写一系列关于Vue与React内部原理的文 ...

  2. Vue响应式数据: Observer模块实现 1

    前言   首先欢迎大家关注我的Github博客,也算是对我的一点鼓励,毕竟写东西没法获得变现,能坚持下去也是靠的是自己的热情和大家的鼓励.接下来的日子我应该会着力写一系列关于Vue与React内部原理 ...

  3. Vue响应式原理详细讲解

    面试官:请你简单的说说什么是Vue的响应式. 小明:mvvm就是视图模型模型视图,只有数据改变视图就会同时更新. 面试官:说的很好,回去等通知吧. 小明:.... Vue响应式原理 先看官方的说法 简 ...

  4. 【vue3 Api - watchEffect 的讲解 使用】- 侦听响应式数据执行副作用(effect)函数

    在了解 `watchEffect` api之前,需要了解在vue中,副作用函数的定义是什么: 字面意义的讲,副作用函数指的是会产生副作用的函数,例如下面该函数: var num = 10 functi ...

  5. vue源码之响应式数据

    分析vue是如何实现数据响应的. 前记 现在回顾一下看数据响应的原因. 之前看了vuex和vue-i18n的源码, 他们都有自己内部的vm, 也就是vue实例. 使用的都是vue的响应式数据特性及$w ...

  6. 深入了解 Vue 响应式原理(数据拦截)

    前言 在上一章节我们已经粗略的分析了整个的Vue 的源码(还在草稿箱,需要梳理清楚才放出来),但是还有很多东西没有深入的去进行分析,我会通过如下几个重要点,进行进一步深入分析. 深入了解 Vue 响应 ...

  7. Vue响应式原理Vue中数据的监听

    文章目录 Observer理解如上图 Dep「依赖管理」 Watcher理解如上图 总结:Vue响应式原理的核心就是Observer.Dep.Watcher. Vue响应式原理理解以后就知道Vue是怎 ...

  8. vue如何在style标签中使用data响应式数据?

    文章目录 一.面临到的需求? 二.如何在css中使用变量? 三.vue如何在style标签中使用data响应式数据? 一.面临到的需求? 首先我们知道css对应是有伪类的,如:hover, :acti ...

  9. 手把手教你剖析vue响应式原理,监听数据不再迷茫

    Object.defineProperty实现vue响应式原理 一.组件化基础 1."很久以前"的组件化 (1)asp jsp php 时代 (2)nodejs 2.数据驱动视图( ...

最新文章

  1. 五本必读的深度学习圣经书籍,入门 AI 从 深度学习 开始
  2. 图解CSS3----5-否定伪类选择器
  3. SpringBoot默认的错误处理机制
  4. Linux后门入侵检测工具 rkhunter 安装使用
  5. 文本串加密和解密程序
  6. Unity项目在pc和ios设备上黑屏的原因探究
  7. 单片机89C51最小系统讲解
  8. 如何科学地评价妹子身材?三围符合黄金比例是审美标准?你错了!
  9. MacOS技巧|Mac如何自定义触控栏Touch Bar?显示Touch Bar教程
  10. Azure核心服务(VM)——>创建windows虚拟机并在此之上部署一个cms网站
  11. 互联网+背景下给旅行社的重大挑战
  12. 华为手机开发人员选项哪里去了
  13. Permission denied: user=dr.who, access=READ_EXECUTE, inode=/user/root:root:supergroup:drwx------
  14. [最新]Myeclipse 10.7.1 激活工具及过程详解 亲测
  15. 小程序利用canvas实现波浪动态图,原生canvas的部分限制
  16. 计算机等级考试公共知识大纲,二级计算机等级考试大纲
  17. 威佐夫博弈算法C++
  18. 如何将弹幕嵌入视频中,合成一个文件
  19. Windows Azure Platform (三)云计算的特点
  20. 一文读懂裸金属服务器是什么意思,和物理机有啥区别

热门文章

  1. idea注释模板:类注释模板、方法注释模板(带参数获取以及参数换行)
  2. 牛客网sql练习笔记(三)
  3. 互联网早报:字节跳动布局新消费:将推出香水品牌“Emotif”;闲鱼上线“校园圈”频道
  4. 模数转换器(ADC)的两种架构:SAR和Σ-Δ
  5. v-model的理解
  6. 用u盘计算机安装系统安装教程,教你如何用U盘重装系统
  7. 计算机网络:期中考试2020年
  8. 【三星固态】980pro 0e 健康度
  9. python遍历数组获取下标
  10. WPS和Office的自动备份目录