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

vue响应式原理

Object.defineProperty实现vue响应式原理

  • 一、组件化基础
    • 1、“很久以前”的组件化
      • (1)asp jsp php 时代
      • (2)nodejs
    • 2、数据驱动视图(MVVM,setState)
      • (1)数据驱动视图 - Vue MVVM
      • (2)数据驱动视图 - React setState
      • (3)总结
  • 二、Vue响应式
    • 1、vue的响应式是什么
    • 2、Object.defineProperty基本用法
    • 3、Oject.defineProperty实现响应式
      • (1)监听对象
      • (2)监听数组
      • (3)几个缺点
  • 四、结束语

近期在对 vue 的学习到一定阶段之后,在想着自己能不能造些东西。于是身边的小伙伴建议说可以从看 vue 的源码开始,毫无头绪的我原本迟迟不敢迈出这一步……(内心经历了各种自我劝说后)最终,我开启了我的源码学习之路。

于是我搜刮了一些常见的原理来进行学习,我对 vue 源码的第一步从 vue 的响应式原理开始。

下面的这篇文章中,将记录我学习 vue 响应式原理的总结。一起来了解一下吧~🙋‍

一、组件化基础

1、“很久以前”的组件化

(1)asp jsp php 时代

在很久以前,也就是大概第一批接触网页开发的程序员,在他们的那个年代其实就已经有组件化了。

(2)nodejs

nodejs 比起 aspjspphp 来说,起步较晚,但是呢, nodejs 中也有类似的组件化,比如像 js 的模板引擎 ejs ,就可以实现组件化

我们来看下 ejs 是怎么实现组件化的✍:

<!-- 个人信息 -->
<div class = "right-item"><%- include('widgets/user-info', {userInfo:userData.userInfo,isMe:userData.isMe,amIFollowed:userData.amIFollowed,atCount:userData.atCount});%>
</div>
<!-- 用户列表 -->
<%- include('widgets/user-list', {count:userData.count,userList:userData.list
});%>

通过以上代码可以了解到, ejs 通过 <%- %> 的形式来定义一个组件,从而实现数据渲染。

虽说早期也有组件,但是对于传统组件来说,也只是静态渲染,并且它的更新还是要依赖于操作 DOM 。这样子的话,用不用组件开发其实区别也不会差特别多。

因此,为了解决这个问题,就有了现在流行的 vuereact ,基于数据驱动视图的开发。

2、数据驱动视图(MVVM,setState)

(1)数据驱动视图 - Vue MVVM

vue的组件化定义如下所示:

<template><div id="app"><imgalt="Vue logo"src="./assets/logo.png"><HelloWorldmsg="Welcome to your Vue.js App"/></div>
</template>

引用官方的图片,我们来讲下 VueMVVM

mvvm

所谓 MVVM ,即 Model-View-ViewModel

View视图 ,也就是 DOM

Model模型 ,可以理解为 Vue 中组件里面的 data

那么这两者之间,就通过 ViewModel 来做关联。而 ViewModel 可以做的事情有很多,比如说像监听事件,监听指令等。当 Model 层的数据发生修改时,就可以通过 ViewModel ,来把数据渲染到 View 视图层上。反之,当 View 层触发 DOM 事件时,就可以通过 ViewModel ,从而使得 Model 层实现数据的修改。

这就是 Vue 中的数据驱动视图,通过修改 Model 层的数据,来驱动到 View 的视图中来。


了解完基本概念,我们用一段代码来剖析 Vue 中的 MVVM 是怎么样的。

<template><div id="app"><p @click="changeName">{{name}}</p><ul><li v-for="(item, index) in list" :key="index">{{item}}</li></ul><button @click="addItem">添加一项</button></div>
</template>
<script>
export default {name:'app',data(){return{name:'vue',list:['a', 'b', 'c']}},methods:{changeName(){this.name = 'monday';},addItem(){this.list.push(`${Date.now()}`);}}
}
</script>

在上面的代码中, template 部分就表示 view 层,而下面的 data 就表示 Model 层。之后呢,像 @click 这种点击事件,点击完之后触发到具体的 methods ,这一部分就可以视为是 ViewModel 层,这样的话,就可以理解为 ViewModel 层是连接 View 层和 Model 层的一个桥梁。

(2)数据驱动视图 - React setState

React的组件化的定义如下所示:

function App(){return(<div className="App"><header className="AppHeader"><imgsrc={logo}className="App-logo"alt="logo"/><HelloWorldmsg="Welcome to Your React App"/></header></div>);
}

React 通过 setState 去操作数据驱动视图。这里不对 react 的数据驱动视图进行细讲,大家可以根据自身需求进行资料查询~

(3)总结

vuereact 帮助我们通过数据去渲染视图,这也就让我们在做 vuereact 开发时,更多的是关注业务逻辑,而不像传统组件一样要一直去考虑 DOM 更新的问题。

二、Vue响应式

1、vue的响应式是什么

所谓 vue 的响应式,即组件 data 的数据一旦变化,就会立刻触发视图的更新。实现数据驱动视图的第一步,需要了解实现响应式的一个核心 API ,即 Object.defineProperty

2、Object.defineProperty基本用法

我们用一段代码来演示 Object.defineProperty 的用法,如下所示:

const data = {}
const name = 'friday'
Object.defineProperty(data, "name", {get:function () {console.log('get')return name},set: function (newVal) {console.log('set')name = newVal}
})// 测试
console.log(data.name) //get friday
data.name = 'monday' //set

通过上面的代码可以看到,通过 Object.defineProperty ,我们可以实现对数据进行 getset 操作,即获取数据修改数据的操作,从而达到对数据进行响应式的监听。

Object.defineProperty 又是如何实现响应式的呢?接下来一起来一探究竟吧!

3、Oject.defineProperty实现响应式

(1)监听对象

在了解响应式之前,需要大家对 js 的数据类型和深拷贝有一个了解。这里我之前写过一篇文章,如有需要可前往查看~

我们都知道 js 的数据类型有基本数据类型引用数据类型,接下来我们将来实现这两种数据类型的响应式监听。

基本数据类型:

// 触发更新视图
function updateView() {console.log('视图更新')
}// 重新定义属性,监听起来
function defineReactive(target, key, value) {// 深度监听observer(value)// 核心 APIObject.defineProperty(target, key, {get() {return value},set(newValue) {if (newValue !== value) {// 深度监听observer(newValue)// 设置新值// 注意,value 一直在闭包中,此处设置完之后,再次 get 时也是会获取最新的值value = newValue// 触发更新视图updateView()}}})
}// 监听对象属性
function observer(target) {//判断是基本数据类型 or 引用数据类型if (typeof target !== 'object' || target === null) {// 不是对象或数组return target}// 重新定义各个属性(for in 也可以遍历数组)for (let key in target) {defineReactive(target, key, target[key])}
}
// 准备数据
const data = {name: 'monday',age: 20
}// 监听数据
observer(data)// 测试
data.name = 'lisi'
data.age = 18
console.log('name', data.name)
console.log('age', data.age)

此时控制台的打印效果如下:

基本数据类型

从上图可以看到,我们改变了两个数据的值,数据也会实时更新。在控制台中我们可以发现,改变了两个数据的值,同时也显示出两个“视图更新”,至此,则说明这两个数据监听成功

阅读代码我们可以发现,当我们监听的数据是基本数据类型时,会直接返回 target 的值,并且视图进行实时更新。

同时,需要注意的是, Object.defineProperty()新增属性和删除属性时,数据是监听不到的

什么意思呢?我们来演示一下。

依据上面的代码,我们再增加以下两行内容。

data.x = '100' // 新增属性,监听不到 —— 用 Vue.set 解决
delete data.name // 删除属性,监听不到 —— 用 Vue.delete 解决

此时控制台的打印结果如下:

新增属性和删除属性

细心的小伙伴已经发现,加上这两行代码后运行效果跟原来是一样的。所以,我们可以得出结论,在用 Object.defineProperty() 新增和删除属性时,数据是监听不到的,这个时候即使数据修改了,视图也监听不到对应的数据,也就没有办法进行视图更新。


引用数据类型:

同样,依据基本数据类型第一段的代码,我们来监听引用数据类型的数据。测试代码如下:

// 准备数据
const data = {name: 'monday',age: 20,info: {address: '福州' // 需要深度监听},nums: ['打篮球', '出来玩', '打乒乓球']
}// 监听数据
observer(data)// 测试
data.info.address = '上海' // 深度监听
data.nums.push('神游') // 监听数组

此时浏览器的打印结果如下:

引用数据类型

我们可以发现,只出现了一个视图更新,没有出现两个。原因在于,对象 info 监听到了,但是数组 nums 并没有监听到。这是为什么呢?

其实,从某种意义上来讲, nums 虽然可以走到深度遍历里面,但是呢, Object.defineProperty() 这个 API 本身是不具备监听数组能力的,所以我们需要加工一层,让其可以拥有监听数组的能力。

(2)监听数组

要想让 Object.defineProperty() 这个 API 拥有监听数组的能力,我们可以这么做。具体代码如下:

// 触发更新视图
function updateView() {console.log('视图更新')
}// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {arrProto[methodName] = function () {updateView() // 触发视图更新oldArrayProperty[methodName].call(this, ...arguments)// Array.prototype.push.call(this, ...arguments)}
})// 重新定义属性,监听起来
function defineReactive(target, key, value) {// 深度监听observer(value)// 核心 APIObject.defineProperty(target, key, {get() {return value},set(newValue) {if (newValue !== value) {// 深度监听observer(newValue)// 设置新值// 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值value = newValue// 触发更新视图updateView()}}})
}// 监听对象属性
function observer(target) {if (typeof target !== 'object' || target === null) {// 不是对象或数组return target}// 污染全局的 Array 原型(如果直接定义在这里面,会直接污染全局)// Array.prototype.push = function () {//     updateView()//     ...// }if (Array.isArray(target)) {target.__proto__ = arrProto}// 重新定义各个属性(for in 也可以遍历数组)for (let key in target) {defineReactive(target, key, target[key])}
}// 准备数据
const data = {name: 'monday',age: 20,info: {address: '福州' // 需要深度监听},nums: ['打篮球', '出来玩', '打乒乓球']
}// 监听数据
observer(data)// 测试
data.info.address = '上海' // 深度监听
data.nums.push('神游') // 监听数组

此时浏览器的打印效果如下:

监听数组

我们可以看到,两个数据对应的视图都更新了。通过对数组原型的重新定义,我们就让 Object.defineProperty() 实现了监听数组的能力。

(3)几个缺点

在让 Object.defineProperty() 实现响应式功能以后,我们来总结下其存在的几个缺点:

1)深度监听,需要递归到底,一次性计算量大

在遍历对象或数组时,需要进行深度监听,即需要递归到底,这会使得一次性计算量非常大。(这个问题在 vue3.0 中已经解决,其解决原理是不一定要一次性递归,而是可以我们什么时候用,什么时候再递归。这个将放在后面的文章中讲解)

2)无法监听新增属性/删除属性

Object.defineProperty() 在进行新增属性和删除属性时,视图是无法进行更新的,也就是数据监听不到,这一点在平常的开发中需要特别注意!否则有时候我们在取数据时总会莫名其妙地都不知道自己错在哪里。通常解决这个问题的方法是,使用 Vue.setVue.delete 来进行新增属性删除属性,这样就可以解决数据无法监听的问题。

3)无法原生监听数组,需要特殊处理

Object.defineProperty() 这个 API 本身无法监听原生数组,需要通过重新定义数组原型的方式,来对数组进行数据监听。

四、结束语

对于 vue2.x 的响应式原理讲到这里就结束啦!从上面的分析中我们可以发现, Object.defineProperty() 有它一定的好用之处,但同时也有一些缺点存在。因此 Vue3.0 用了 Proxy 来解决上述缺点中存在的问题,但是呢, proxy 到现在其实也还没有推广开来,因为 proxy 有兼容性的问题存在,如无法兼容 IE11 等问题,且 proxy 无法 polyfill ,所以 vue2.x 很长一段时间内应该还会存在。因此,对于 vue2.xvue3.0 来说,这两者都是得学的,而不是说出了 vue3.0 就不学 vue2.x 了,对于这两者来说,更多的是相辅相成的一个结果。

闲谈到此结束,对于 vue 原理的学习有深深感受到造轮子的快乐,但是啃源码在开始学习时确实会比较枯燥。希望再接再厉,争取啃下更多 vue 的源码,读懂更多原理!

  • 关注公众号 星期一研究室 ,不定期分享学习干货,学习路上不迷路~
  • 如果这篇文章对你有用,记得点个赞加个关注再走哦~

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

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

相关文章

leetcode:203. 移除链表元素(两种方法)

一:题目 二:上码 1:方法一&#xff1a;(虚拟一个首结点) class Solution { public:ListNode* removeElements(ListNode* head, int val) {//1.虚拟一个头结点 这样就不用单独处理了ListNode * virtuals new ListNode(0);//给其开辟个空间并且赋初值virtuals->next head…

面试中的网红虚拟DOM,你知多少呢?深入解读diff算法

深入浅出虚拟DOM和diff算法一、虚拟DOM&#xff08;Vitual DOM&#xff09;1、虚拟DOM&#xff08;Vitual DOM&#xff09;和diff的关系2、真实DOM的渲染过程3、虚拟DOM是什么&#xff1f;4、解决方案 - vdom&#xff08;1&#xff09;问题引出&#xff08;2&#xff09;vdom如…

Blazor带我重玩前端(六)

本文主要讨论Blazor事件内容&#xff0c;由于blazor事件部分很多&#xff0c;所以会分成上下两篇&#xff0c;本文为第二篇。双向绑定概述如图所示当点击单项绑定的时候&#xff0c;MyOnewayComponent里的属性值会发生变化&#xff0c;这种变化是单项的&#xff0c;仅仅只是本地…

leetcode707:设计链表(增删差)

一:题目 二:上码 class MyLinkedList { public://定义链表节点结构体struct LinkedNode {int val;LinkedNode* next;LinkedNode(int val):val(val), next(nullptr){}};// 初始化链表MyLinkedList() {node new LinkedNode(0); // 这里定义的头结点 是一个虚拟头结点&#xff0…

深入探究.Net Core Configuration读取配置的优先级

前言在之前的文章.Net Core Configuration源码探究一文中我们曾解读过Configuration的工作原理&#xff0c;也.Net Core Configuration Etcd数据源一文中探讨过为Configuration自定义数据源需要哪些操作。由于Configuration配置系统也是.Net Core的核心&#xff0c;其中也包含了…

TypeScript,从0到入门带你进入类型的世界

从0到入门进入TS的世界一、什么是TypeScript&#xff1f;1、编程语言的类型2、TypeScript究竟是什么&#xff1f;二、为什么要学习TypeScript&#xff1f;1、程序更容易理解2、效率更高3、更少的错误4、非常好的包容性5、一点小缺点三、typescript入门1、如何安装TypeScript2、…

编写第一个 .NET 微服务

介绍本文的目的是&#xff1a;通过创建一个返回列表的简单服务&#xff0c;并在 Docker 容器中运行该服务&#xff0c;让您熟悉使用 .NET 创建微服务的构建过程。安装 .NET SDK要开始构建 .NET 应用程序&#xff0c;首先下载并安装 .NET Core SDK&#xff08;软件开发工具包&am…

模板编译template的背后,究竟发生了什么事?带你了解template的纸短情长

解析模板编译template的背后发生了什么一、&#x1f4d1;初识模板编译1、vue组件中使用render代替template2、模板编译总结二、✏️感受模板编译的美1、with语法&#xff08;1&#xff09;例子展示&#x1f330;&#xff08;2&#xff09;知识点归纳三、&#x1f4c8;编译模板1…

leetcode24. 两两交换链表中的节点(思路+解析)

一:题目 二:思路 思路: 1.分析题意 这是相邻结点进行交换 如果是4个结点 那么1和2交换 3和4交换 如果是3个结点 那么就1和2进行交换 3不动 2.这里我们定义一个虚拟头节点方便操作&#xff0c;我们只需三步实现结点的交换 <1>:让虚拟结点指向第二个结点(进行交换的结点我…

把Autofac玩的和java Spring一样6

大家好&#xff0c;今天来介绍我开源的一个autofac.Annotation项目 源码&#xff1a;https://github.com/yuzd/Autofac.Annotation本项目是autofa的一个扩展组件&#xff0c;autofac是一个老牌的DI容器框架 &#xff0c;支持netframework和netcoreAnnotdation是注解的意思&…

『软件测试5』测开岗只要求会黑白盒测试?NO!还要学会性能测试!

浅谈软件测试中的性能测试一、&#x1f92a;性能测试概念1、为什么要有性能测试&#xff1f;2、性能测试是什么&#xff1f;3、性能测试的目的二、&#x1f910;性能测试指标1、响应时间2、吞吐量3、并发用户数4、TPS(Transaction Per Second)5、点击率6、资源利用率三、&#…

CLR的简单理解

CLR加载程序生成进程&#xff0c;一个进程中可以存在多个线程&#xff0c;当创建一个线程时&#xff0c;会分配1Mb的空间&#xff0c;也就是线程的栈空间&#xff0c;对应jvm的虚拟机堆栈&#xff0c;是线程执行过程中用到的工作内存。这片内存用于方法传递实参&#xff0c;并存…

『软件测试6』bug一两是小事,但安全漏洞是大事!

详解软件测试中的安全测试一、&#x1f4bf;安全测试概念1、安全测试概述2、安全测试与软件生命周期的关系3、常规测试与安全测试的不同&#xff08;1&#xff09;测试目标不同&#xff08;2&#xff09;假设条件不同&#xff08;3&#xff09;思考域不同&#xff08;4&#xf…

我们真的需要JWT吗?

JWT&#xff08;JSON Web Token&#xff09;是目前最流行的认证方案之一。博客园、各种技术公众号隔三差五就会推一篇JWT相关的文章&#xff0c;真的多如牛毛。但我对JWT有点困惑&#xff0c;今天写出来跟大家探讨探讨&#xff0c;不要喷哈。JWT原理本文默认读者已经对JWT有所了…

leetcode面试题 02.07. 链表相交

一:题目 二:思路 1.这道题我们是需要找到一个结点&#xff0c;并且从这个结点往后的结点都相等 2.我们需要将两个链表 右对齐 3.然后将长链表的指针移动到和短链表头结点相同的位置 4.接下来就是比较指针&#xff0c;当一个指针相同也就意味着往后的结点的数值也相等 三:上码…

详解队列在前端的应用,深剖JS中的事件循环Eventloop,再了解微任务和宏任务

队列在前端中的应用一、队列是什么二、应用场景三、前端与队列&#xff1a;事件循环与任务队列1、event loop2、JS如何执行3、event loop过程4、 DOM 事件和 event loop5、event loop 总结四、宏任务和微任务1、引例2、宏任务和微任务&#xff08;1&#xff09;常用的宏任务和微…

终于弄明白了 Singleton,Transient,Scoped 的作用域是如何实现的

一&#xff1a;背景1. 讲故事前几天有位朋友让我有时间分析一下 aspnetcore 中为什么向 ServiceCollection 中注入的 Class 可以做到 Singleton&#xff0c;Transient&#xff0c;Scoped&#xff0c;挺有意思&#xff0c;这篇就来聊一聊这一话题&#xff0c;自从 core 中有了 S…

leetcode142. 环形链表 II(暴力+双链表)

一:题目 二:思路 1.双指针 快慢指针(快指针一次一个结点&#xff0c;慢指针一次两个结点) 2.如果有环的话&#xff0c;那么快慢指针肯定会相遇 3.那么相遇的地点一定在环中 因为如果没有环的话慢指针是永远追不到快指针的 4.接下来就是判断出口在那里&#xff0c;我们定义一个…

动态 Restful API 生成

介绍通常在DDD开发架构中&#xff0c;我们写完服务层需要在控制器中写API&#xff0c;今天介绍一个组件 Plus.AutoApi 可以用它来动态生成 Restful 风格的 WebApi&#xff0c;不用写 Controller。快速使用在你的应用服务层中添加组件Install-Package Plus.AutoApi在 Startup 中…

卷死了!再不学vue3就没有人要你了!速来围观vue3新特性

一文全面了解vue3新特性一、&#x1f636;vue3比vue2有什么优势&#xff1f;二、&#x1f9d0;Vue3升级了哪些重要的功能1、createApp2、emits(父子组件间的通信)&#xff08;1&#xff09;通信方式&#xff08;2&#xff09;举个例子&#x1f330;3、多事件处理4、Fragment5、…