Vue3响应式原理初探

news2025/1/16 3:58:14

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 = 11
    obj.age = 22
    obj.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 = 11
    prxoyTarget.age = 22
    prxoyTarget.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 } = Vue
    createApp({
        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 = null
            if (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.name
                    effect(update)
                }

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

	let currentCallback = null
    const effect = (callback) => {
        currentCallback = callback
        callback()
        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] = newValue
         trigger(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 = null
    const effect = (callback) => {
        currentCallback = callback
        callback()
        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 = null
            if (options.setup) {
                dataSource = options.setup()
            }
            return {
                // 在mount中执行初次挂载
                mount(domSelector) {
                    const update = () => document.querySelector(domSelector).innerHTML = dataSource.name
                    effect(update)
                }
            }
        },
        reactive(target) {
            return new Proxy(target, {
                get(target, key) {
                    track(target, key)
                    // 代理对象的get方法
                    return target[key]
                },
                set(target, key, newValue) {
                    // 代理对象的set方法
                    target[key] = newValue
                    trigger(target, key)
                }
            })
        }
    }

    const { createApp, reactive } = Vue

    createApp({
        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.coloradmin.cn/o/1108141.html

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

相关文章

MSQL系列(五) Mysql实战-索引最左侧匹配原则分析及实战

Mysql实战-索引最左侧匹配原则分析及实战 前面我们讲解了索引的存储结构&#xff0c;BTree的索引结构&#xff0c;以及索引最左侧匹配原则&#xff0c;Explain的用法&#xff0c;今天我们来实战一下 最左侧匹配原则 1.联合索引最左侧匹配原则 联合索引有一个最左侧匹配原则 …

一文了解什么是数字孪生

数字孪生&#xff0c;作为数字化时代的新兴技术&#xff0c;正日益引领着未来的发展方向。它并非一种独立的工具或软件&#xff0c;而是一种理念&#xff0c;一种将实体与虚拟世界紧密结合的方法。本文将详细介绍数字孪生的本质、应用领域以及对未来的影响。 数字孪生的本质 …

Redis:Feed流之Timeline的实现

当我们关注了用户后&#xff0c;这个用户发了动态&#xff0c;那么我们应该把这些数据推送给用户&#xff0c;这个需求&#xff0c;其实我们又把他叫做Feed流&#xff0c;关注推送也叫做Feed流&#xff0c;直译为投喂。为用户持续的提供“沉浸式”的体验&#xff0c;通过无限下…

多测师肖sir_高级金牌讲师___python之模块openpyxl

python之模块openpyxl 一、用python读写excel的强大工具&#xff1a;openpyxl &#xff08;一&#xff09;下载openpyxl的方式 方式一&#xff1a;python -m pip install openpyxl 或 pip install openpyxl 方式二&#xff1a;在pycharm中安装 &#xff08;二&#xff09;…

《优化接口设计的思路》系列:第五篇—接口发生异常如何统一处理

系列文章导航 第一篇—接口参数的一些弯弯绕绕 第二篇—接口用户上下文的设计与实现 第三篇—留下用户调用接口的痕迹 第四篇—接口的权限控制 第五篇—接口发生异常如何统一处理 本文参考项目源码地址&#xff1a;summo-springboot-interface-demo 前言 大家好&#xff01;…

VScode运行SVN拉下来的项目

安装依赖包 pnpm install 启动程序 查看package.json文件中的serve&#xff0c;根据这个启动 pnpm dev 在浏览器使用http://localhost:8848/访问

论文阅读:Point-to-Voxel Knowledge Distillation for LiDAR Semantic Segmentation

来源&#xff1a;CVPR 2022 链接&#xff1a;https://arxiv.org/pdf/2206.02099.pdf 0、Abstract 本文解决了将知识从大型教师模型提取到小型学生网络以进行 LiDAR 语义分割的问题。由于点云的固有挑战&#xff0c;即稀疏性、随机性和密度变化&#xff0c;直接采用以前的蒸馏…

【算法设计与分析】第6章02 分支限界法

目录 分支限界法的设计技术 分支限界法&#xff1a;  约束条件  剪枝  分支限界法的设计步骤 思考题&#xff1a; 【例6-6】装载问题。  计算模型 【例6-7】背包  问题分析  问题分析 计算模型  计算模型  算法设计与描述 代码&#xff1a; 思…

文件打包下载excel导出和word导出

0.文件下载接口 请求 GET /pm/prj/menu/whsj/download/{affixId} 文件affixId多个id以逗号隔开。多个文件会以打包得形式。 1.Excel导出 1.0接口 POST 127.0.0.1:8400/pm/io/exportExcel/year-plan-table-workflow/report 参数 [{"org":"011","re…

C#快速排序算法

快速排序实现原理 快速排序&#xff08;Quick Sort&#xff09;是一种常用的排序算法&#xff0c;它基于分治的思想&#xff0c;通过将一个无序的序列分割成两个子序列&#xff0c;并递归地对子序列进行排序&#xff0c;最终完成整个序列的排序。 其基本思路如下&#xff1a; 选…

056:mapboxGL中layer的layout,paint,filter的属性值表达式说明总结

第056个 点击查看专栏目录 本篇文章是mapbox的layer中layout,paint,filter的表达式说明总结。 mapbox中 Expressions 是什么 Expressions:表达式集合(并非 style 的属性,只是 layer 的任何 layout布局属性和 paint绘制属性,以及 filter 属性等,它们的值都可以指定成一…

【NV GPU限制出口】昇腾能否接住滔天富贵

如何评价刘庆峰所言华为GPU已可对标英伟达A100&#xff1f; 国家超级计算济南中心&#xff1a; https://www.nsccjn.cn/

JAVA学习日记1——JAVA简介及第一个java程序

简单记忆 JAVA SE &#xff1a;标准版&#xff0c;核心基础 JAVA EE&#xff1a;企业版&#xff0c;进阶 JDK&#xff1a;Java Development Kit&#xff0c;Java开发工具包&#xff0c;包含JRE JRE&#xff1a;Java Runtime Environment&#xff0c;Java运行时环境&#xff…

Elasticsearch小bug记录:term: XXX was completely eliminated by analyzer

问题&#xff1a; 下面这个报错&#xff0c;是在配置同义词的时候报的错&#xff1a;不能识别南京。 {"error": {"root_cause": [{"type": "illegal_argument_exception","reason": "failed to build synonyms"…

Linux-ssh

文章目录 远程登录服务器配置远程服务器相关信息创建config文件配置config文件 配置密钥登陆先创建密钥配置密钥文件 执行命令scp传文件copy文件copy文件夹配置我们的vim和tmux 远程登录服务器 ssh userhostnameuser:用户名hostname&#xff1a;IP地址或域名 第一次登陆会显示…

Spring MVC(一)【什么是Spring MVC】

重点 Spring&#xff1a;IOC 和 AOP 。 Spring MVC &#xff1a;Spring MVC 的执行流程。 SSM 框架的整合&#xff01; Spring 和 Mybatis 我们不建议使用太多注解&#xff0c;Spring MVC 建议全部使用注解开发&#xff01; 1、MVC 回顾 1.1、什么是MVC MVC是模型(Model)…

音乐播放器VHDL蜂鸣器数码管显示简谱,视频/代码

名称&#xff1a;音乐播放器数码管显示简谱蜂鸣器 软件&#xff1a;Quartus 语言&#xff1a;VHDL 代码功能&#xff1a; 设计音乐播放器&#xff0c;播放一首歌&#xff0c;使用开发板的蜂鸣器播放音乐&#xff0c;使用Quartus内的ROM IP核存储音乐文件&#xff0c;使用数…

编程语言常识

看图区别编程语言 什么是强类型、弱类型语言&#xff1f;哪种更好&#xff1f; 强类型语言 强类型语言是一种强制类型定义的语言&#xff0c;即一旦某一个变量被定义类型&#xff0c;如果不经强制转换&#xff0c;那么它永远就是该数据类型。 在强类型语言中&#xff0c;变量的…

Linux考试复习整理

文章目录 Linux考试整理一.选择题1.用户的密码现象放置在哪个文件夹&#xff1f;2.删除文件或目录的命令是&#xff1f;3.显示一个文件最后几行的命令是&#xff1f;4.删除一个用户并同时删除用户的主目录5.Linux配置文件一般放在什么目录&#xff1f;6.某文件的组外成员的权限…

CUDA编程入门系列(七) GPU内存如何管理

一、内存使用 1.CUDA程序会使用GPU内存与CPU内存 2.CPU内存的分配与释放是标准的,例如new和delete,malloc与free 3.GPU上内存涉及分配和释放使用CUDA提供的库函数实现 4.CUDA/GPU内存与CPU内存的互相传输 这里主要讲全局内存与共享内存的管理 二、CPU内存 栈:有编译器自动分配释…