Vue3响应式原理初探

vue3响应式原理初探

为什么要使用proxy取代defineProperty

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

js 复制代码
	new Vue({
		data(){
			return {
				name:'wxs',
				age:25
			}
		}
	})

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

js 复制代码
	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的优越性

js 复制代码
 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。

js 复制代码
<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内部的执行逻辑,注意看以下代码,重点部分都有注释讲解

js 复制代码
    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的返回值。然后将返回值渲染到页面。

js 复制代码
	 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函数

js 复制代码
mount(domSelector) {
                    const update = () => document.querySelector(domSelector).innerHTML = dataSource.name
                    effect(update)
                }

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

来看看effect函数的实现细节。

js 复制代码
	let currentCallback = null
    const effect = (callback) => {
        currentCallback = callback
        callback()
        currentCallback = null
    }

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

js 复制代码
	// 在get里面触发依赖收集
	get(target, key) {
	  	track(target,key)
	  	// 代理对象的get方法
	  	return target[key]
	},
js 复制代码
// 从上图出发,定义的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)
        }
    }
js 复制代码
	// 依赖收集完成,定义触发函数 触发函数其实就是track函数的逆运算,把track存起来的东西全部取出来
	const trigger = (target, key) => {
        reactiveMap.get(target).get(key).forEach(cb => cb())
    }

最后将trigger写到set里面。

js 复制代码
	 set(target, key, newValue) {
         // 代理对象的set方法
         target[key] = newValue
         trigger(target, key)
     }

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

js 复制代码
	createApp({
        setup() {
            const data = reactive({
                name: 'wxs'
            })

            setTimeout(() => {
                data.name = '456'
            }, 1000)
            return data
        }
    }).mount('#app')

老规矩 贴上全部代码

html 复制代码
<!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响应式!

相关推荐
道爷我悟了几秒前
Vue入门-指令学习-v-html
vue.js·学习·html
无咎.lsy1 分钟前
vue之vuex的使用及举例
前端·javascript·vue.js
fishmemory7sec9 分钟前
Electron 主进程与渲染进程、预加载preload.js
前端·javascript·electron
fishmemory7sec11 分钟前
Electron 使⽤ electron-builder 打包应用
前端·javascript·electron
工业互联网专业42 分钟前
毕业设计选题:基于ssm+vue+uniapp的校园水电费管理小程序
vue.js·小程序·uni-app·毕业设计·ssm·源码·课程设计
豆豆1 小时前
为什么用PageAdmin CMS建设网站?
服务器·开发语言·前端·php·软件构建
计算机学姐1 小时前
基于SpringBoot+Vue的在线投票系统
java·vue.js·spring boot·后端·学习·intellij-idea·mybatis
JUNAI_Strive_ving1 小时前
番茄小说逆向爬取
javascript·python
看到请催我学习2 小时前
如何实现两个标签页之间的通信
javascript·css·typescript·node.js·html5
twins35202 小时前
解决Vue应用中遇到路由刷新后出现 404 错误
前端·javascript·vue.js