Vue3响应式原理初探

vue3响应式原理初探

  • 为什么要使用proxy取代defineProperty
  • 使用proxy如何完成依赖收集呢?

为什么要使用proxy取代defineProperty

原因1:defineproperty无法检测到原本不存在的属性。打个🌰

	new Vue({data(){return {name:'wxs',age:25}}})

在vue2中,底层会通过definproperty来响应式data返回的对象,也就是{name:'wxs',age:25},具体的响应式监听如下。

	const obj = {name:'wxs',age:25}Object.entries(obj).forEach(([key,value]) => {Object.defineProperty(obj,key,{get(){return value},set(newValue){console.log(`监听到属性${key}改变`)value = newValue}})})obj.name = 11obj.age = 22obj.ak47 = 'ak47'

看看控制台输出
请添加图片描述
可以看出,由于初始化中并没有初始化ak47,所以在vue2中对未初始化的对象key值的修改是无法监听到的。
再来看看es6新特性proxy的优越性

 const obj = {name:'wxs',age:25}const prxoyTarget = new Proxy(obj,{get(target,key){return target.key},set(target,key,value){console.log(`监听到${key}需要改成${value}`)target[key] = value}})prxoyTarget.name = 11prxoyTarget.age = 22prxoyTarget.ak47 = 'ak47'

在这里插入图片描述
所以回答这个问题的思路就很清晰了
1、proxy可以完成对未初始化对象key的代理监听,而definproperty不可以,相比较之下,proxy更加优越。
2、由vue2的响应式原理可以看出,vue底层需要对vue实例的返回的每一个key进行get和set操作,无论这个值有没有被用到。所以在vue中定义的data属性越多,那么初始化开销就会越大。而proxy是一个惰性的操作,它只会在用到这个key的时候才会执行get,改值的时候才会执行set。所以在vue3中实现响应式的性能实际上要比vue2实现响应式性能要好。

使用proxy如何完成依赖收集呢?

首先看到vue3官网中给出的初始化一个vue实例的基础用法。
在这里插入图片描述
因为我们需要响应式一个对象,所以我们使用reactive api。

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script><div id="app">{{ message }}</div><script>const { createApp, reactive, toRefs } = VuecreateApp({setup() {const data = reactive({name: 'wxs'})return data}}).mount('#app')
</script>

由这个简单的实例可得知,vue对象内部首先暴露了一个createApp函数用于新建vue实例,那么大致写出Vue内部的执行逻辑,注意看以下代码,重点部分都有注释讲解

const Vue = {// 从官网示例可得,从vue中解构出了createApp函数,那么其内部肯定有createApp的定义createApp(options){// createApp实现细节后文补充return {// 在mount中执行初次挂载mount(domSelector){// 在vue3的源码中也会在mount中执行挂载任务,但是由于此文主要讲解的是vue3的响应式原理,那么AST解析和diff比较就略过,直接显示setup的返回值即可(也就是模拟vue的模版内容为 <div>{{ name }}</div>)。  document.querySelector(domSelector).innerHTML = '页面渲染'}}},// 响应式代理,在前一小节有介绍,此节不做赘述。reactive(target){return new Proxy(target,{get(target,key){// 代理对象的get方法return target[key]},set(target,key,newValue){// 代理对象的set方法target[key] = newValue}})}}

将上述代码执行之后,浏览器应该会出现如下页面
在这里插入图片描述
但是很明显,我们想要将响应式数据呈现在页面上。比如代码中定义到的name。那么我们可以将createApp做如下改写。具体思路是拿出setUp的返回值。然后将返回值渲染到页面。

	 createApp(options) {let dataSource = nullif (options.setup) {dataSource = options.setup()}return {// 在mount中执行初次挂载mount(domSelector) {document.querySelector(domSelector).innerHTML = dataSource.name}}},	

这样就将响应式数据和template模版之间构建了联系。(但是千万不要被这个简化的代码所迷惑,实际上vue底层会执行diff算法来比较最小化更新之后在渲染页面。因为此文是介绍vue3的响应式原理,所以此过程略过)
完成createApp的补充之后,实际上我们已经完成了vue的初次渲染工作。那么剩下的就是需要完成vue组件的更新工作了(其实也就是说,在响应式数据更新的时候,重新执行一下mount里的代码完成页面刷新)
理清思路之后,稍微重构一下mount函数

mount(domSelector) {const update = () => document.querySelector(domSelector).innerHTML = dataSource.nameeffect(update)}

实际上,vue3是通过effect函数来将更新函数和响应式数据来建立链接。所谓的建立链接,也就是通过effect执行的函数中如果包含了响应式对象,如果响应式对象发生改变,函数就会重新执行。
来看看effect函数的实现细节。

	let currentCallback = nullconst effect = (callback) => {currentCallback = callbackcallback()currentCallback = null}

有同学可能会问了,先存一下callback,然后执行一下更新函数,再置空,那么这个赋值不就是脱了裤子放屁-----多此一举吗?那么看看这两行中间夹缝生存的的这一行神奇的代码callback()。执行一下传入的callback,那么也就是update函数。如果执行update函数必然会触发dataSource.name的get方法。那么我们就可以在这里完成依赖收集。将代理对象和key和更新函数之间建立如下的链接关系。
在这里插入图片描述

	// 在get里面触发依赖收集get(target, key) {track(target,key)// 代理对象的get方法return target[key]},
// 从上图出发,定义的track函数const track = (target,key) => {let depMap = reactiveMap.get(target)if(!depMap){depMap = new Map()reactiveMap.set(target,depMap)}let watcherSet = depMap.get(key)if(!watcherSet){watcherMap = new Set();watcherSet.add(currentCallback)depMap.set(key,watcherSet)}}
	// 依赖收集完成,定义触发函数 触发函数其实就是track函数的逆运算,把track存起来的东西全部取出来const trigger = (target, key) => {reactiveMap.get(target).get(key).forEach(cb => cb())}

最后将trigger写到set里面。

	 set(target, key, newValue) {// 代理对象的set方法target[key] = newValuetrigger(target, key)}

大功告成,可以直接改变对象值看看效果。

	createApp({setup() {const data = reactive({name: 'wxs'})setTimeout(() => {data.name = '456'}, 1000)return data}}).mount('#app')

老规矩 贴上全部代码

<!DOCTYPE html>
<html><head><meta charset="UTF-8"><!-- import CSS --><link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"><style>.pagination-container {position: sticky;bottom: 0;z-index: 100;}#box {width: 100%;height: 50%;background: black;}</style>
</head><body><div id="app">{{name}}</div>
</body><!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> --><script>let currentCallback = nullconst effect = (callback) => {currentCallback = callbackcallback()currentCallback = null}let reactiveMap = new Map();const track = (target, key) => {let depMap = reactiveMap.get(target)if (!depMap) {depMap = new Map()reactiveMap.set(target, depMap)}let watcherSet = depMap.get(key)if (!watcherSet) {watcherSet = [];watcherSet.push(currentCallback)depMap.set(key, watcherSet)}}const trigger = (target, key) => {reactiveMap.get(target).get(key).forEach(cb => cb())}const Vue = {createApp(options) {let dataSource = nullif (options.setup) {dataSource = options.setup()}return {// 在mount中执行初次挂载mount(domSelector) {const update = () => document.querySelector(domSelector).innerHTML = dataSource.nameeffect(update)}}},reactive(target) {return new Proxy(target, {get(target, key) {track(target, key)// 代理对象的get方法return target[key]},set(target, key, newValue) {// 代理对象的set方法target[key] = newValuetrigger(target, key)}})}}const { createApp, reactive } = VuecreateApp({setup() {const data = reactive({name: 'wxs'})setTimeout(() => {data.name = '456'}, 1000)return data}}).mount('#app')</script>
</html>

各位看官稍安勿躁,全部代码算上css才100行,当然,vue内部实现肯定比我这个要复杂,比如它内部存更新函数是用的set等等,但是对于响应式原理而言,我们只需要拿出最精华的部分即可。第一遍看不懂没关系,我看视频也是看了七八遍才看懂,各位coder们一定要有耐心,拿下vue3响应式!

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

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

相关文章

【Linux初阶】多线程4 | POSIX信号量,基于环形队列的生产消费模型,线程池,线程安全的单例模式,STL-智能指针和线程安全

文章目录 ☀️一、POSIX信号量&#x1f33b;1.引入&#x1f33b;2.信号量的概念&#x1f33b;3.信号量函数 ☀️二、基于环形队列的生产消费模型&#x1f33b;1.理解环形队列&#x1f33b;2.代码案例 ☀️三、线程池☀️四、线程安全的单例模式&#x1f33b;1.单例模式与设计模…

前端渲染后端返回的HTML格式的数据

在日常开发中&#xff0c;经常有需要前端渲染后端返回页面的需求&#xff0c;对于不同数据结构&#xff0c;前端的渲染方式也不尽相同&#xff0c;本文旨在对各种情况进行总结。 后端返回纯html文件格式 数据包含html标签等元素&#xff0c;数据类型如下图&#xff1a; 前端通…

檀香香料经营商城小程序的作用是什么

檀香香料有安神、驱蚊、清香等作用&#xff0c;办公室或家庭打坐等场景&#xff0c;都有较高的使用频率&#xff0c;不同香料也有不同效果&#xff0c;高品质香料檀香也一直受不少消费者欢迎。 线下流量匮乏&#xff0c;又难以实现全消费路径完善&#xff0c;线上是商家增长必…

PR2023中如何导入字幕

PR中如何导入字幕 方法一&#xff1a; 点开文本&#xff0c;字幕&#xff0c;新建字幕分段&#xff08;点击右上角…三个点&#xff09; 键入调整内容 方法二 点开基本图形&#xff0c;编辑&#xff0c;调整&#xff0c;拖动位置。

功能集成,不占空间,同为科技TOWE嵌入式桌面PDU超级插座

随着现代社会人们生活水平的不断提高&#xff0c;消费者对生活质量有着越来越高的期望。生活中&#xff0c;各式各样的电气设备为我们的生活带来了便利&#xff0c;在安装使用这些用电器时&#xff0c;需要考虑电源插排插座的选择。传统的插排插座设计多暴露于空间之中&#xf…

【JAVA】日志打印java.util.logging.*函数自定义格式,并且显示调用时行号

1、JAVA自带的这样&#xff1a; 代码如下&#xff1a; import java.util.logging.*; Logger logger Logger.getLogger(MyLogger.class.toString()); logger.info("123");显示效果&#xff1a; 这样的格式&#xff0c;看起来不太好看&#xff0c;比如&#xff1a;会…

BI零售数据分析,当代零售企业的核心竞争力

在数字化转型中&#xff0c;BI智能零售数据分析成为了极其重要的核心竞争力之一。通过对大数据的采集和分析&#xff0c;零售企业可以更好地了解消费者的需求和行为模式&#xff0c;从而做出更准确的决策。例如&#xff0c;通过分析消费者的购物历史、浏览记录等数据&#xff0…

美妆品牌如何有效利用软文推广引流获客

近年来随着美妆品牌的转型升级和居民消费观念的转变&#xff0c;美妆行业取得了更大发展空间&#xff0c;新产品不断涌现&#xff0c;消费者拥有更多选择&#xff0c;那么在竞争激烈的市场中美妆品牌如何才能突破重围&#xff0c;找出新的价值增长点呢&#xff1f; 一、 细分消…

Confluence 解决PDF导出乱码问题

1.原因 PDF导出乱码是因为由于服务器缺少必要字体 2.解决办法 下载字体文件将字体文件重命名为simhei.ttf Confluence→管理→PDF导出语言支持&#xff0c;导入字体即可

在UniApp中使用uni.makePhoneCall方法调起电话拨打功能

目录 1.在manifest.json文件中添加权限 2. 组件中如何定义 3.如何授权 4.相关知识点总结 1.在manifest.json文件中添加权限 {"permissions": {"makePhoneCall": {"desc": "用于拨打电话"}} }2. 组件中如何定义 <template>…

阶段性总结

uart协议&#xff1a; 通用异步收发器 UART&#xff08;Universal Asynchronous Receiver/Transmitter)&#xff0c;是一种串行、异步、全双工的通信协议&#xff0c;将所需传输的数据一位接一位地传输&#xff0c;在UART通讯协议中信号线上的状态位高电平代表’1’&#xff0…

PRCV 2023:语言模型与视觉生态如何协同?合合信息瞄准“多模态”技术

近期&#xff0c;2023年中国模式识别与计算机视觉大会&#xff08;PRCV&#xff09;在厦门成功举行。大会由中国计算机学会&#xff08;CCF&#xff09;、中国自动化学会&#xff08;CAA&#xff09;、中国图象图形学学会&#xff08;CSIG&#xff09;和中国人工智能学会&#…