源码上分析Vue2和Vue3的响应式原理

本文节选自我的博客:源码上分析Vue2和Vue3的响应式原理

  • 💖 作者简介:大家好,我是MilesChen,偏前端的全栈开发者。
  • 📝 CSDN主页:爱吃糖的猫🔥
  • 📣 我的博客:爱吃糖的猫
  • 📚 Github主页: MilesChen
  • 🎉 支持我:点赞👍+收藏⭐️+留言📝
  • 💬介绍:The mixture of WEB+DeepLearning+Iot+anything🍁

前言

Vue2和Vue3的响应式原理一直是前端面试中的高频考点,如果你还只知道Vue2通过defineProperty方式实现,Vue3通过代理的方式实现,是不是就太浅显了。那本文带大家从源码去解读他们的实现,响应式实现主要分为三步:数据劫持、收集依赖、派发更新。

Vue2响应式原理

本小节涉及的完整代码github源码链接,这是简化过的源码,添加了注释方便阅读。


整个过程就像上面这张图一样,浏览器会触发’Touch’,这是浏览器在编译文件的过程中完成对所有的HTML中的{{}}、v-text、v-model等涉及响应式的依赖,对每个依赖new Watcher,作为后面的订阅者,因为响应式的目的就是自动完成更新这些订阅者。

  • 数据劫持:在数据劫持阶段将data中的数据添加响应式(对象会以递归的形式去添加)
  • 收集依赖:针对data中每个变量new Dep,在getter中 且 watcher 中依赖这个变量的时候去收集依赖,变量更改的时候触发setter去派发更新
  • 派发更新:watcher对象在创建过程会传入updata用到的cb方法,该方法会去更改Dom上

new Vue的过程中其实就已经完成了数据劫持和依赖收集,

/* vue.js */
class Vue {constructor(options) {// 获取到传入的对象 没有默认为空对象this.$options = options || {}// 获取 elthis.$el =typeof options.el === 'string'? document.querySelector(options.el): options.el// 获取 datathis.$data = options.data || {}// 调用 _proxyData 处理 data中的属性this._proxyData(this.$data)// 使用 Obsever 把data中的数据转为响应式 数据劫持、和收集依赖new Observer(this.$data)// 编译模板 `{{}}`、v-text、v-model等涉及响应式的依赖,对每个依赖`new Watcher`,作为后面的订阅者new Compiler(this)}// 把data 中的属性注册到 Vue_proxyData(data) {Object.keys(data).forEach((key) => {// 进行数据劫持// 把每个data的属性 到添加到 Vue 转化为 getter setter方法Object.defineProperty(this, key, {// 设置可以枚举enumerable: true,// 设置可以配置configurable: true,// 获取数据get() {return data[key]},// 设置数据set(newValue) {// 判断新值和旧值是否相等if (newValue === data[key]) return// 设置新值data[key] = newValue},})})}}

数据劫持

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 完成数据劫持: 把这些 property 全部转为 getter/setter。并且在getter时调用dep.js收集依赖,在setter中调用dep.js的notify方法更新所有依赖的watcher。
注意:对象会以递归的形式去添加响应式

/**
obsever.js 中是把 data 的所有属性 加到 data 自身 变为响应式 转成 getter setter方式
**/
/* observer.js */class Observer {constructor(data) {// 用来遍历 datathis.walk(data)}// 遍历 data 转为响应式walk(data) {// 判断 data是否为空 和 对象if (!data || typeof data !== 'object') return// 遍历 dataObject.keys(data).forEach((key) => {// 转为响应式this.defineReactive(data, key, data[key])})}// 转为响应式// 要注意的 和vue.js 写的不同的是// vue.js中是将 属性给了 Vue 转为 getter setter// 这里是 将data中的属性转为getter setterdefineReactive(obj, key, value) {// 如果是对象类型的 也调用walk 变成响应式,不是对象类型的直接在walk会被returnthis.walk(value)// 保存一下 thisconst self = this// 创建 Dep 对象let dep = new Dep()Object.defineProperty(obj, key, {// 设置可枚举enumerable: true,// 设置可配置configurable: true,// 获取值get() {// 在这里添加观察者对象 Dep.target 表示观察者Dep.target && dep.addSub(Dep.target)return value},// 设置值set(newValue) {// 判断旧值和新值是否相等if (newValue === value) return// 设置新值value = newValue// 赋值的话如果是newValue是对象,对象里面的属性也应该设置为响应式的self.walk(newValue)// 触发通知 更新视图dep.notify()},})}
}

收集依赖

data中每个响应式属性都会new Dep,在getter中 且 watcher 中依赖这个变量的时候去收集依赖,变量更改的时候触发setter去派发更新

/**
每个响应式属性都会创建这样一个 Dep 对象 ,负责收集该依赖属性的Watcher对象 
(是在使用响应式数据的时候做的操作)
**/
/* dep.js */
class Dep {constructor() {// 存储观察者this.subs = []}// 添加观察者addSub(sub) {// 判断观察者是否存在 和 是否拥有update方法if (sub && sub.update) {this.subs.push(sub)}}// 通知方法notify() {// 触发每个观察者的更新方法this.subs.forEach((sub) => {sub.update()})}
}

派发更新

组件在解析 {{}}、v-text、v-model等和依赖相关的内容,每个需要响应式的位置都会创建watcher 实例。派发更新:数据更新过后首先触发setter,接着触发depnotify方法,最后触发watcher的update方法。

Vue 在更新 DOM 时是异步执行的,只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。在下一个的事件循环“tick”中,Vue 在内部对异步队列尝试使用原生的 Promise.then setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
如果要立即访问更新后的数据,直接访问显示是未更新的数据,因为异步任务挂着呢,没执行到
Vue.nextTick(callback):这样回调函数将在 DOM 更新完成后被调用

// 数据更新后 收到通知之后 调用 update 进行更新
//  watcher实例化在compiler文件中 就是编译器中 比如{{}}和v-text和v-model中
/* watcher.js */class Watcher {constructor(vm, key, cb) {// vm 是 Vue 实例this.vm = vm// key 是 data 中的属性this.key = key// cb 回调函数 更新视图的具体方法this.cb = cb// 把观察者的存放在 Dep.targetDep.target = this// 旧数据 更新视图的时候要进行比较// 还有一点就是 vm[key] 这个时候就触发了 get 方法// 之前在 get 把 观察者 通过dep.addSub(Dep.target) 添加到了 dep.subs中this.oldValue = vm[key]// Dep.target 就不用存在了 因为上面的操作已经存好了Dep.target = null}// 观察者中的必备方法 用来更新视图update() {// 获取新值let newValue = this.vm[this.key]// 比较旧值和新值if (newValue === this.oldValue) return// 调用具体的更新方法this.cb(newValue)}
}

问题

对象增删属性检测不到

只能检查到data函数中声明的对象中的所有property,无法检测到添加或移除property;
解决办法:

  1. 实例前在data中声明,为null也行。
  2. 使用this.$delete或Vue.delete进行删除属性。
数组检测问题

Vue2响应式不能检测以下数组的变动:

  1. 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength
    解决办法:1.使用this.$set()或Vue.set();2.使用splice
为什么对象能检测到属性变化,而数组检测不到

本质上Object.defineProperty 也能监听数组变化,但是Vue没采用这个去检测数组,因为要监听数组中的每个元素性能开销大,且使用场景太少。

为什么数组的push和pop等方法会产生响应式?

本来数组的一些方法比如push,pop是不会触发getter/setter的。不会触发的原因是因为这是Array原型上的方法,并没有在Array本身上面。但是vue重写了数组原型上的7个方法,就有了响应式。重写的过程使用拦截器实现,就是和 Array.prototype 一样的对象。

Vue2响应式设计模式的体现

观察者模式:dep.js就是观察者模式,监听到改变就notify所有的观察者
发布订阅模式:dep.js扮演消息中心的角色,observer.js扮演观察者(发布者),watcher.js扮演订阅者

Vue3响应式原理

本小节涉及的完整代码github源码链接,这是简化过的源码,添加了注释方便阅读。

vue3没有vue2那些问题,对象中增删改都可以检测到

Vue3的响应式实现可分为两种:
ref:以ref为代表的基础数据类型的响应式,使用 get/set 存取器实现
reactive:以reactive为代表的引用数据类型的响应式,使用Proxy配合Reflect实现的响应式
其实还以分为更多比如toReftoRefsshallowReactiveshallowRef本文就不展开讨论了

下面主要讲reactive响应式的实现,ref响应式的实现见第四小节 get/set 存取器相关内容


这张图用于辅助理解下面的数据劫持、收集依赖、派发更新理解。

数据劫持

使用Proxy代理的方式实现数据劫持,与Vue2中一样,属性存在引用数据类型会触发递归,在getter中调用track方法收集依赖,trigger方法派发更新。

// 判断是否为对象 ,注意 null 也是对象
const isObject = val => val !== null && typeof val === 'object'
// 判断key是否存在
const hasOwn = (target, key) => Object.prototype.hasOwnProperty.call(target, key)function reactive(target) {// 首先先判断是否为对象if (!isObject(target)) return targetconst handler = {get(target, key, receiver) {console.log(`获取对象属性${key}值`)// 收集依赖track(target, key)const result = Reflect.get(target, key, receiver)// 递归判断的关键, 如果发现子元素存在引用类型,递归处理。if (isObject(result)) {return reactive(result)}return result},set(target, key, value, receiver) {console.log(`设置对象属性${key}值`)// 首先先获取旧值const oldValue = Reflect.get(target, key, reactive)// set 是需要返回 布尔值的let result = true// 判断新值和旧值是否一样来决定是否更新setterif (oldValue !== value) {result = Reflect.set(target, key, value, receiver)// 派发更新trigger(target, key)}return result},deleteProperty(target, key) {console.log(`删除对象属性${key}值`)// 先判断是否有keyconst hadKey = hasOwn(target, key)const result = Reflect.deleteProperty(target, key)if (hadKey && result) {// 派发更新target(target, key)}return result},}return new Proxy(target, handler)
}

收集依赖

结合下图不难理解,在track中完成依赖的收集的过程是,先找targetMap,再找depsMap,最后找actieEffect,没有则创建Map或者添加effect。

  • targetMap:key为响应式对象的引用,value为depsMap
  • depsMap:key响应式对象的属性,value为set类型表示该属性的所有依赖
  • actieEffect:表示触发更新的回调就像Vue2的dp函数

// activeEffect 表示当前正在走的 effect
let actieEffect = null
function effect(callback) {actieEffect = callbackcallback()actieEffect = null
}// targetMap 表里每个key都是一个普通对象 对应他们的 depsMap
let targetMap = new WeakMap()function track(target, key) {// 如果当前没有effect就不执行追踪if (!actieEffect) return// 获取当前对象的依赖图let depsMap = targetMap.get(target)// 不存在就新建if (!depsMap) {targetMap.set(target, (depsMap = new Map()))}// 根据key 从 依赖图 里获取到到 effect 集合let dep = depsMap.get(key)// 不存在就新建if (!dep) {depsMap.set(key, (dep = new Set()))}// 如果当前effectc 不存在,才注册到 dep里if (!dep.has(actieEffect)) {dep.add(actieEffect)}
}

派发更新

派发更新就是去调用触发更新的所有依赖。通过target找到是哪个对象需要更新,再通过key找到是哪个属性需要更新,最后调用该属性的所有依赖的effect更新。

// trigger 响应式触发
function trigger(target, key) {// 拿到 依赖图const depsMap = targetMap.get(target)if (!depsMap) {// 没有被追踪,直接 returnreturn}// 拿到了 视图渲染effect 就可以进行排队更新 effect 了const dep = depsMap.get(key)// 遍历 dep 集合执行里面 effect 副作用方法if (dep) {dep.forEach(effect => {effect()})}
}

问题

直通过索引设置可能隐式导致length问题

当通过索引设置响应式数组的时候,有可能会隐式修改数组的 length 属性,例如设置的索引值大于数组当前的长度时,那么就要更新数组的 length 属性,因此在触发当前的修改属性的响应之外,也需要触发与 length 属性相关依赖进行重新执行。
所以我们要尽量避免这个问题:

  1. 不要去直接修改响应式数组的length属性
  2. 通过数组索引修改响应式数组时,不要将数组索引大于数组的length属性
ref和reactive的响应式区别
  • ref:把一个基础类型包装成一个有value响应式对象(使用get/set 存取器,来进行追踪和触发),如果是普通对象就调用 reactive 来创建响应式对象。
  • reactive:返回proxy对象,这个reactive可以深层次递归,如果发现子元素存在引用类型,递归reactive处理
Object.definePropertyget/set 存取器的区别

Object.defineProperty 是一个较低级别的操作,它只能用于单个属性,并且需要显式地定义每个属性的描述符。这在大量属性定义时可能会显得冗长和繁琐。因此,在 ES6 之后,通常更推荐使用get/set 存取器来创建访问器属性

Object.defineProperty实现响应式

const obj = {};
let _value = 0;Object.defineProperty(obj, 'value', {get() {return _value;},set(newValue) {_value = newValue;},enumerable: true,configurable: true
});console.log(obj.value); // 调用 get 方法,输出: 0
obj.value = 10; // 调用 set 方法,将 _value 设置为 10
console.log(obj.value); // 调用 get 方法,输出: 10

get/set 存取器实现的响应式

const obj = {_value: 0,get value() {return this._value;},set value(newValue) {this._value = newValue;}
};console.log(obj.value); // 调用 get 方法,输出: 0
obj.value = 10; // 调用 set 方法,将 _value 设置为 10
console.log(obj.value); // 调用 get 方法,输出: 10
为什么要使用Reflect(反射)

Reflect是ES6出现的新特性,代码运行期间用来设置或获取对象成员,代替原始的操作,更加安全、语义化;
Object.getPrototypeOf => Reflect.getPrototypeOf
target[propName] => Reflect.get(target,propName)
target[propName] = value => Reflect.set(target,propName,value) 返回true和false
delete target[propName] => Reflect.deleteProperty(target,propName) 返回true和false 表示执行成功还是失败

总结

Vue2的响应式实现可分为三步:

  • 数据劫持:在数据劫持阶段将data中的数据添加响应式(对象会以递归的形式去添加)
  • 收集依赖:针对data中每个变量new Dep,在getter中 且 watcher 中依赖这个变量的时候去收集依赖,变量更改的时候触发setter去派发更新
  • 派发更新:watcher对象在创建过程会传入updata用到的cb方法,该方法会去更改Dom上

Vue2响应式还存在问题:对象增删属性检、数组使用index修改和直接修改length不会触发响应式


Vue3的响应式实现分为两种:

  • 基础数据类型的响应式,使用 get/set 存取器实现
  • 引用数据类型的响应式,使用Proxy配合Reflect实现的响应式,实现过程中也可分为数据劫持、收集依赖、派发更新三步去实现

Vue3响应式还存在问题:直接修改length、将数组索引大于数组的length不会触发响应式


感谢小伙伴们的耐心观看,本文为笔者个人学习记录,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力!

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

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

相关文章

【多线程进阶】synchronized 原理

文章目录 前言1. 基本锁策略2. 加锁工作过程2.1 偏向锁2.2 轻量级锁2.3 重量级锁 3. 其他的优化操作3.1 锁消除3.2 锁粗化 总结 前言 在前面章节中, 提到了多线程中的锁策略, 那么我们 Java 中的锁 synchronized 背后都采取了哪些锁策略呢? 又是如何进行工作的呢? 本节我们就…

全能视频工具 VideoProc Converter 4K for mac中文

VideoProc 4K提供快速完备的4K影片处理方案,您可以透过这款软体调节输出影片格式和大小。能够有效压缩HD/4K影片体积90%以上,以便更好更快地上传到YouTube,或是通过电子邮件附件发送。业界领先的视讯压缩引擎,让你轻松处理大体积视…

《Attention Is All You Need》论文笔记

下面是对《Attention Is All You Need》这篇论文的浅读。 参考文献: 李沐论文带读 HarvardNLP 《哈工大基于预训练模型的方法》 下面是对这篇论文的初步概览: 对Seq2Seq模型、Transformer的概括: 下面是蒟蒻在阅读完这篇论文后做的一…

STM32复习笔记(四):看门狗

目录 (一)简介 (二)IWDG IWDG的CUBEMX工程配置 IWDG相关函数(非常少,所以直接贴上来): (三)WWDG (一)简介 看门狗分为独立看门…

第三课 哈希表、集合、映射

文章目录 第三课 哈希表、集合、映射lc1.两数之和--简单题目描述代码展示 lc30.串联所有单词的子串--困难题目描述代码展示 lc49.字母异位分组--中等题目描述代码展示 lc874.模拟行走机器人--中等题目描述代码展示 lc146.LRU缓存--中等题目描述相关补充思路讲解代码展示图示理解…

mysql双主互从通过KeepAlived虚拟IP实现高可用

mysql双主互从通过KeepAlived虚拟IP实现高可用 在mysql 双主互从的基础上, 架构图: Keepalived有两个主要的功能: 提供虚拟IP,实现双机热备通过LVS,实现负载均衡 安装 # 安装 yum -y install keepalived # 卸载 …

【docker】数据卷和数据卷容器

一、如何管理docker容器中的数据? 二、数据卷 1、数据卷原理 将容器内部的配置文件目录,挂载到宿主机指定目录下 数据卷默认会一直存在,即使容器被删除 宿主机和容器是两个不同的名称空间,如果想进行连接需要用ssh,…

vue、vuex状态管理、vuex的核心概念state状态

每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同: Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候&…

数据结构--Trie字符串统计

1、“Trie树” 作用: 高效地存储和查找字符串集合的数据结构。 2、“Trie树” 存储字符串的形式如下: 用 “0” 来表示 “根节点(root)”。存入一个字符串时,会在字符串最后结尾的那个字符节点打上标记。比如&#x…

Python柱形图

柱形图 柱形图,又称长条图、柱状统计图、条图、条状图、棒形图,是一种以长方形的长度为变量的统计图表。长条图用来比较两个或以上的价值(不同时间或者不同条件),只有一个变量,通常利用于较小的数据集分析…

投资理财:利率下行时代应该怎样存钱?

大家好,我是财富智星,今天跟大家分享一下当下利率下行的时代,钱应该怎样存,存哪里的问题。 一、 银行利率下行 在过去的三十年里,您已经逐渐适应了不断下降的利率,从10%到现在的1.65%。而在未来&#xff0c…

迄今为止丨ChatGPT最强指令,一个可以让机器人生成机器人的Prompt,价值百万!

原文: 【ChatGPT调教】ChatGPT最强指令、让机器人为你生成机器人!-CSDN博客 说明:最好看原文 昨天,发现了一条可能是迄今为止,我见过最牛的,商业价值最高的ChatGPT指令。 通过这条指令,可以…

1400*C. Soldier and Cards(贪心模拟)

Problem - 546C - Codeforces Soldier and Cards - 洛谷 解析&#xff1a; 模拟即可&#xff0c;当循环次数过大的时候跳出循环打印 -1 #include<bits/stdc.h> using namespace std; #define int long long const int N2e55; int n,x,k1,k2,cnt; queue<int>a,b;…

[黑马程序员TypeScript笔记]------一篇就够了

目录&#xff1a; TypeScript 介绍 TypeScript 是什么&#xff1f;TypeScript 为什么要为 JS 添加类型支持&#xff1f;TypeScript 相比 JS 的优势TypeScript 初体验 安装编译 TS 的工具包 编译并运行 TS 代码 简化运行 TS 的步骤 TypeScript 常用类型 概述类型注解常用基础…

MATLAB算法实战应用案例精讲-【优化算法】沙丁鱼优化算法(SOA)(附MATLAB代码实现)

前言 沙丁鱼优化算法(Sardine optimization algorithm,SOA)由Zhang HongGuang等人于2023年提出,该算法模拟沙丁鱼的生存策略,具有搜索能力强,求解精度高等特点。 沙丁鱼主要以浮游生物为食,这些生物包括细菌、腔肠动物、软体动物、原生动物、十足目、幼小藤壶、鱼卵、甲藻…

LeetCode 面试题 08.02. 迷路的机器人

文章目录 一、题目二、C# 题解 一、题目 设想有个机器人坐在一个网格的左上角&#xff0c;网格 r 行 c 列。机器人只能向下或向右移动&#xff0c;但不能走到一些被禁止的网格&#xff08;有障碍物&#xff09;。设计一种算法&#xff0c;寻找机器人从左上角移动到右下角的路径…

【C++设计模式之建造者模式:创建型】分析及示例

简介 建造者模式&#xff08;Builder Pattern&#xff09;是一种创建型设计模式&#xff0c;它将复杂对象的构建过程与其表示分离&#xff0c;使得同样的构建过程可以创建不同的表示。 描述 建造者模式通过将一个复杂对象的构建过程拆分成多个简单的部分&#xff0c;并由不同…

OpenGLES:绘制一个混色旋转的3D圆柱

效果展示 本篇博文会实现两种混色效果的3D圆柱&#xff1a; 一.圆柱体解析 上一篇博文讲解了怎么绘制一个混色旋转的立方体 这一篇讲解怎么绘制一个混色旋转的圆柱 圆柱的顶点创建主要基于2D圆进行扩展&#xff0c;与立方体没有相似之处 圆柱绘制的关键点就是将圆柱拆解成…

【TensorFlow Hub】:有 100 个预训练模型等你用

要访问TensorFlow Hub&#xff0c;请单击此处 — https://www.tensorflow.org/hub 一、说明 TensorFlow Hub是一个库&#xff0c;用于在TensorFlow中发布&#xff0c;发现和使用可重用模型。它提供了一种使用预训练模型执行各种任务&#xff08;如图像分类、文本分析等&#xf…

Docker 配置基础优化

Author&#xff1a;rab 为什么要优化&#xff1f; 你有没有发现&#xff0c;Docker 作为线上环境使用时&#xff0c;Docker 日志驱动程序的日志、存储驱动数据都比较大&#xff08;尤其是在你容器需要增删比较频繁的时候&#xff09;&#xff0c;动不动就好几百 G 的大小&…