Vue深入响应式原理
2022/2/7 23:21:25
本文主要是介绍Vue深入响应式原理,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
参考:
<<深入浅出Vue.js>> 第二章 Object的变化侦测
Vue2.x和Vue3.x官网关于<<深入响应式原理>>的介绍
<<JavaScript权威指南(第七版)>>
1.追踪对象变化的方法
Vue2.x
在组件创建时,Vue2.x系列使用了Object.defineProperty来给组件中的data的每个属性设置为访问器属性。
效果是:访问器属性拥有setter,可以探测到属性值的修改
缺点是:访问器属性具有局限性,无法探测属性的增加或删除。下面是缺点导致的后果—参考Vue2.x官网<<深入响应式原理>>
(1) 嵌套对象添加响应式 property需要调用Vue特殊方法
使用Vue.$set(anyObject, property, value)
或Vue.$delete
let school = { name: "SDU", province: "Shan Dong" } // Vue组件中的data data() { return { name: "Danny", school: school } } // 此变化无法被访问器属性探测 school.city = "Ji Nan" // 需要调用Vue.$set方法 this.$set(this.school, "city", "Ji Nan")
(2 ) Vue 不允许动态添加根级别的响应式 property
不允许在data上添加或删除属性
let school = { name: "SDU", province: "Shan Dong" } // Vue组件中的data data() { return { name: "Danny", school: school } } // 给data添加一个新的属性,这是不允许的 this.gender = "male"
Vue3.x
Vue3.x使用了ES6的proxy来探测对象的变化,详情可参考Vue3.x官网<<深入响应性原理>>
效果是:使用对象代理和对象反射可以探测到对象的所有变化,解决了Vue2.x中的响应式缺陷。
2.通知模板变化的方法
注:在<<深入浅出Vue>>中这里为“收集依赖的方法”
2.1 订阅发布模式
在追踪到对象的改变后,Vue需要通知到所有依赖这些对象的位置(通常指的是Vue模板template),说明对象已经发生改变。这实际上是订阅发布模式的简化版。在这里先了解并实现一个简单的订阅发布模式。参考:https://segmentfault.com/a/1190000019260857
订阅发布模式的设计
- 订阅发布模式关注“订阅过程”,“发布过程”
实现订阅过程和发布过程需要有一个中间人。订阅者将订阅信息提交给中间人,中间人记录下来;发布者把发布信息发布给中间人,中间人查找订阅给类型信息的订阅者,将发布信息依次发送给订阅者
- 订阅发布模式需要的数据结构如下:
用简易代码表示:
// eventType(n)存储第n种事件对应的消息通知队列,队列中存储着订阅者提供的通知函数,执行该通知函数就可通知对应的订阅者 eventType1 = [() => {}, () => {}, ..., () => {}] eventType2 = [() => {}, () => {}, ..., () => {}] eventType3 = [() => {}, () => {}, ..., () => {}] ... eventTypen = [() => {}, () => {}, ..., () => {}] // eventList存储所有可能的事件类型,每种事件类型都指向一个消息通知队列 eventList = { eventType1, eventType2, eventType3, ... eventTypen }
用内存图表示:
订阅发布模式的代码:(JavaScript实现)
class EventEmitter{ constructor() { // 事件类型对象,存储各种不同事件的通知函数队列 this.eventList = {} } // 订阅函数 on(eventType, notifyFunc) { // 如果存在该类型事件,那么直接在其订阅者队列中添加一个通知函数。否则先创建订阅者队列,之后再添加通知函数。 (this.eventList[eventType] || (this.eventList[eventType] = [])).push(notifyFunc) } // 发布函数 emit(eventType, ...content) { // 发布时,先找到该类型事件,然后执行每个订阅者的通知函数,把信息通知给这些订阅者 this.eventList[eventType] && this.eventList[eventType].forEach(notifyFunc => notifyFunc.call(this, ...content)) } // 只订阅一次 once(eventType, notifyFunc) { let that = this // 创建一个新函数on,包装订阅者的通知函数notifyFunc function on(content) { // 当通知订阅者时,执行订阅者传来的通知函数 notifyFunc.call(that, content) // 把新函数on取消订阅 this.off(eventType, on) } // 把新函数on放入通知队列,代替 订阅者的通知函数notifyFunc this.on(eventType, on) } // 取消订阅 off(eventType, notifyFunc) { let notifyQueue = this.eventList[eventType] // 取消订阅时,先判断是否存在这种事件,再判断该事件的通知队列中是否存在该通知函数 if(notifyQueue && notifyQueue.includes(notifyFunc)) // 存在这种函数时则删除通知队列中所有该通知函数 for(let i = 0; i < notifyQueue.length;) if(notifyQueue[i] === notifyFunc) notifyQueue.splice(i, 1) else i ++ } }
测试订阅发布模式
// 创建一个“订阅发布模式”对象 let eventEmitter = new EventEmitter() // 1.测试基础的订阅事件,假设三个同学订阅了“开学事件” eventEmitter.on("开学", console.log) eventEmitter.on("开学", console.log) eventEmitter.on("开学", console.log) // 1秒后发布“开学事件”,发布信息是开学时间 setTimeout(() => { eventEmitter.emit("开学", "2022/2/20") }, 1000) // 2.测试取消订阅 eventEmitter.on("放假", console.log) eventEmitter.on("放假", console.log) eventEmitter.off("放假", console.log) // 1秒后发布“放假事件”,发布信息是放假时间 setTimeout(() => { eventEmitter.emit("放假", "放假时间是2022/1/8") }, 1000) // 3.测试只订阅一次 eventEmitter.once("社会实践", console.log) // 1秒后发布“社会实践事件”,发布信息是社会实践时间 setTimeout(() => { eventEmitter.emit("社会实践", "社会实践时间是2022/1至2022/3") }, 1000) // 2秒后再次发布“社会实践事件”,发布信息是修改后的信息,理论上订阅一次是接收不到此次发布的信息的 setTimeout(() => { eventEmitter.emit("社会实践", "社会实践时间延长至2022/4") }, 2000) // 4.测试发布一个不存在的事件 eventEmitter.emit("放假", "2022/1/8") // 5.测试取消订阅一个不存在的事件 eventEmitter.off("玩电脑", console.log) // 测试结果如下 // 2022/2/20 // 2022/2/20 // 2022/2/20 // 社会实践时间是2022/1至2022/3
2.2 基于订阅发布模式实现Vue的响应式原理
注意:
下述代码思路参考<<深入浅出Vue.js>>,减少了原文中代码的封装性,提高了一些可读性
1.下述代码想要模拟的场景是:
创建一个新的Vue实例,Vue在底层将Vue实例中的data的属性全部设置为访问器属性,检测数据变化。声明一个变量,赋值为data中的某个属性,模拟Vue模板中使用数据绑定。最后改变data中的属性值,观察结果。
2.下述代码在何处使用订阅发布模式:(实现思路)
在将Vue实例中的data的属性设为访问器属性时使用订阅发布模式。访问器get方法中进行订阅,传入一个通知函数,该通知函数能够通知Vue模板中所有绑定该变量的位置,该变量值发生变化。访问器set方法中进行发布,当属性值变化时,发布事件,触发通知函数,通知模板中绑定该变量的位置要更新变量值。
3.下述代码的前提条件:
下述代码将data的属性全部设置为访问器属性,默认是在Vue2.x环境下。如果想模拟Vue3.x环境,可将其自行换为Proxy。
在下述代码中会使用上文已经实现的EventEmitter类,虽然Vue2.x默认不在ES6环境下,没有类的概念,但是方便起见不要计较。
// 假设window.target是如下的通知函数 globalThis.target = function(key, val, newVal) { console.log(key + "属性发生了改变,由" + val + "变为了" + newVal) // 下面的代码省略,是通知Vue模板中使用该变量的位置,该变量值发生了改变,需要更新 } // 将某个属性设置为访问器属性,以做到对属性变化的检测 function defineReactive(data, key, val) { // 不为所有情况创建一个全局的EventEmitter对象的原因是:eventType的表示不方便,两个对象有同名属性时需要考虑对对象进行哈希运算,否则会出现两个对象使用同一个eventType。 // 这里牺牲空间复杂度降低时间复杂度。 const eventEmitter = new EventEmitter() Object.defineProperty(data, key, { enumerable: true, configurable: true, // 注意这里不要使用get()增强型写法,Vue2.x的Object.defineProperty方法在设计时不是在ES6环境,否则直接使用proxy对象完成对象探测 get: function() { // 这里假设一个不存在的window.target属性为通知函数,真正的Vue的通知函数要复杂,这里使用window.target代指之。 if(globalThis.target) // 这里的eventType随便命名即可,订阅时传入通知函数 eventEmitter.on("change", globalThis.target) return val }, set: function(newVal) { if(val === newVal) return // 当属性值发生改变时,先更新属性值,然后发布更新信息 eventEmitter.emit("change", key, val, newVal) val = newVal } }) } // 传入一个对象,使它每个属性变成访问器属性 function makeResponsive(data) { // 简便期间,这里不对data做额外的类型检查,只是检查data是数组还是非数组非函数的普通对象 if(data instanceof Array) { // data是数组,遍历data的每一个属性 data.forEach(property => { // 如果该属性值是非函数的引用类型,那么需要递归使它每个属性变成响应式 if(data[property] && (typeof data[property] !== "function" && data[property] instanceof Object)) { defineReactive(data, property, data[property]) makeResponsive(data[property]) // 注意defineReactive(data, property, data[property]),不能统一写在if之前,因为if中的data[property]相当于调用了get方法,这不是我们希望的 } else defineReactive(data, property, data[property]) }) } else Object.keys(data).forEach(property => { // data是对象的情况同上 if(data[property] && (typeof data[property] !== "function" && data[property] instanceof Object)) { defineReactive(data, property, data[property]) makeResponsive(data[property]) } else defineReactive(data, property, data[property]) }) }
4.测试上述代码
// 测试响应式原理 // 下面的data对象模拟Vue中的data let data = { name: "Danny", gender: "male", school: { name: "SDU", grade: [1, 2, 3], location: { province: "Shandong", city: "WeiHai" } } } // 将data对象的属性全部变为访问器属性 makeResponsive(data) // 模拟Vue模板中使用data中的数据 let gender = data.gender // data中的数据发生变化,将会通知模板中使用该变量的位置。此处使用let模拟模板,无法改变let声明的变量的值,理解意思即可。 data.gender = "female" // 模拟Vue模板中使用data中的数据 let schoolName = data.school.name // data中的数据发生变化,将会通知模板中使用该变量的位置。此处使用let模拟模板,无法改变let声明的变量的值,理解意思即可。 data.school.name = "PKU" // 预计输出结果,输出原因是通知函数 // gender属性发生了改变,由male变为了female // name属性发生了改变,由SDU变为了PKU
2.3 Vue响应式原理实现vm.$watch
注意:
在Vue官网中关于vm. w a t c h 的 介 绍 比 较 简 略 , 但 是 不 影 响 理 解 它 的 含 义 。 在 < < 深 入 浅 出 V u e . j s > > 中 主 要 在 第 四 章 介 绍 v m . watch的介绍比较简略,但是不影响理解它的含义。在<<深入浅出Vue.js>>中主要在第四章介绍vm. watch的介绍比较简略,但是不影响理解它的含义。在<<深入浅出Vue.js>>中主要在第四章介绍vm.watch,但是第二章也有所涉及。
1.回顾:
在2.2中实现了基础的Vue响应式原理,主要是考虑了两个方面:1.如何追踪对象的变化——使用访问器属性 2.如何将对象的变化通知给模板——使用订阅发布模式。
2.vm.$watch:
但是在实际的Vue响应式实现中考虑了更灵活的应用场景,用户应也可以监听对象的变化。Vue提供了更高封装程度的vm. w a t c h 代 替 访 问 器 属 性 , 让 用 户 更 容 易 监 听 对 象 变 化 。 使 用 ‘ ‘ ‘ v m . watch代替访问器属性,让用户更容易监听对象变化。使用```vm. watch代替访问器属性,让用户更容易监听对象变化。使用‘‘‘vm.watch(property, callback)```的效果是,当指定的Vue实例的data中的property发生变化后,会执行callback函数。
3.vm.$watch的实现:
下述代码实现了Watcher类,效果和vm.$watch大致相同,使用方法不同。但效果都是能够让用户监听到对象的变化。将下述代码和2.2中实现的代码结合即可。
class Watcher { // expOrFn为属性表达式,详情参见Vue官网关于vm.$watch的使用,expOrFn对应其第一个参数。在这里的实现中vm指的是Vue实例的data对象。 constructor(vm, expOrFn, callback) { this.vm = vm this.expOrFn = expOrFn this.callback = callback this.value = this.get() } get() { globalThis.target = (key, val, newVal) => { this.callback.call(this.vm, val, newVal) this.value = this.get() } // 访问data.expOrFn对应的属性,此时会触发访问器属性get,get中会加入globalThis.target,此时的globalThis.target已经修改成了用户希望的回调函数 let value = Watcher.parsePath(this.expOrFn).call(this, this.vm) // 将globalThis.target还原,上文globalThis的值就如下 globalThis.target = function(key, val, newVal) { console.log(key + "属性发生了改变,由" + val + "变为了" + newVal) // 下面的代码省略,是通知Vue模板中使用该变量的位置,该变量值发生了改变,需要更新 } return value } // 解析传入的属性expOrFn,比如"a.b.c",结果是obj.a.b.c static parsePath(expOrFn) { let segments = expOrFn.split(".") return function(obj) { for(let i = 0; i < segments.length; i ++) if(!obj) return else obj = obj[segments[i]] return obj } } }
4.代码测试:
在将上述代码和2.1,2.2的代码结合后做下述测试
// 测试响应式原理中的vm.$watch // 下面的data对象模拟Vue中的data let data = { name: "Danny", gender: "male", school: { name: "SDU", grade: [1, 2, 3], location: { province: "Shandong", city: "WeiHai" } } } // 将data对象的属性全部变为访问器属性 makeResponsive(data) // 使用Watcher对象使得用户可以监听到对象的变化 new Watcher(data, "school.name", function(val, newVal) { console.log("使用watcher监听到了对象的变化","老属性值是" + val, "新属性值是" + newVal) }) // 对象发生变化 data.school.name = "PKU" // 预计输出结果 // 使用watcher监听到了对象的变化 老属性值是SDU 新属性值是PKU
3.Vue响应式是异步更新DOM
在Vue官网中介绍<<深入响应式原理>>时提到Vue的DOM更新是异步的,如果想在DOM更新后执行某些回电函数,那么需要使用Vue.$nextTick()。
这里有些困惑,在本人另一篇博客“浏览器事件”中提到了浏览器更新DOM的时机。根据WhatWG官方文档介绍,浏览器先执行task(就是通常讲的宏任务,只不过第一个task是JavaScript同步代码),之后执行microtask,再之后才进行DOM更新。因此我猜想Vue.$nextTick()的实现会用到宏任务,这样会在DOM更新后执行回调。但是在查阅了源码(在node_modules/vue/src/core中)后发现实现中可以使用微任务也可以使用宏任务。在参考了https://segmentfault.com/q/1010000039973370后,本人推测应该与Vue的虚拟DOM有关。此问题将在完成虚拟DOM学习后再详细考虑。
4.总结
总结借用Vue官网关于响应式介绍的图。Watcher就是上面实现的Watcher类,或者说就是vm.$watch()对应上述的2.3。Data的访问器方法实现对象变化侦测对应上文的1。虚线具体的步骤都在代码实现中体现出来。关于虚拟DOM可以暂时忽视。
这篇关于Vue深入响应式原理的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2025-01-04React 19 来了!新的编译器简直太棒了!
- 2025-01-032025年Node.js与PHP大比拼:挑选最适合的后端技术进行现代web开发
- 2025-01-03?? 用 Gemini API、Next.js 和 TailwindCSS 快速搭建 AI 推文生成项目 ??
- 2024-12-31Vue CLI多环境配置学习入门
- 2024-12-31Vue CLI学习入门:一步一步搭建你的第一个Vue项目
- 2024-12-31Vue3公共组件学习入门:从零开始搭建实用组件库
- 2024-12-31Vue3公共组件学习入门教程
- 2024-12-31Vue3学习入门:新手必读教程
- 2024-12-31Vue3学习入门:初学者必备指南
- 2024-12-30Vue CLI多环境配置教程:轻松入门指南