前言
Vue3用久了,你会不会对自己手下的代码发生一些疑问?比如说:
- 有了
reactive
,那还需求ref
干啥? - 同样是呼应式署理,为什么只需
ref
能够署理原始数据,reactive
却不可? - 为什么运用
ref
的值要加上一个麻烦的.value
? - 为什么
ref
能够一同署理原始数据和方针数据? - 为什么只需修正呼应式数据,视图就主动改动了呢?Vue它怎样就知道我修正了数据呢?
诸如此类的问题,是否令你感到有些困惑?不必忧虑,这些问题其实是多是因为Vue3的新特性和语法带来的改动导致的。下面会将上述问题一一捋顺,让咱们开发的时分能够做到知其所以然。
本文旨在收拾思路,让读者了解的整体流程,帮助读者对 Vue 的作业原理树立一个整体的知道和了解。而并非谈论详细的源码。因此,本文不会涉及到实际中大段的 Vue 源码。请安心阅览(恶作剧的!)
运用差异
reactive 的运用
在Vue3中,reactive
是一个用于创立呼应式方针的API。它承受一个一般的 JavaScript方针 作为参数,并回来一个呼应式的署理。只需运用简略的几行代码就能够创立一个呼应式数据,并轻松的运用它,一般版运用如下:
<script setup>
// 引入
import { reactive } from 'vue';
// 创立呼应式方针
let reactiveInfo = reactive({
name: '饿肚子游侠'
});
</script>
<template>
<div>
// 页面运用
{{ reactiveInfo }}
</div>
</template>
好,很棒,完全没有问题。但当咱们想往reactive
里装一个原始数据类型的时分,就出问题了。Vue会给出提示:它无法取得呼应式。
这个提示是什么意思呢?其实也便是:后续假如再想对reactiveInfo
的值进行更新赋值的时分,从头赋值后的值现已不能再呼应式的更新到页面上了。那该怎样办呢?假定现在有个参数,一定要界说为number
类型或者其它任何的原始数据类型怎样办?凡是有运用Vue3经历的小伙伴必定都知道,该ref
上场了。
ref 的运用
在Vue3中,ref
是一个创立呼应式的API,它能够将一般的 JavaScript数据 变为呼应式数据。ref
函数承受一个初始值作为参数,并回来一个呼应式的引证方针。与reactive
一样,ref
运用起来也很简略。区别就在于,在script
标签里对其进行获取的时分,需求加上一个.value
,而在template里运用的时分则不必:
<script setup>
// 引入
import { ref } from 'vue';
// 创立呼应式方针
let refTest = ref(0);
// 修正测验
setTimeout(() => {
refTest.value = 2;
}, 1000);
</script>
<template>
<div>
// 页面运用
{{ refTest }}
</div>
</template>
差异背面
在简略了解了reactive
和ref
的运用之后,现在,不论是想要完成方针仍是原始数据类型的呼应式转换,都能够轻松做到了。但是却不由会对它们的运用差异发生一些猎奇,为什么!到底为什么ref
能够署理原始数据,reactive
不可!为什么运用ref
的值还非要加上.value
!为什么修正了我的呼应式方针,视图也跟着变!不必忧虑,下面就来把这些问题一个一个的解决。
前置常识了解(简略了解)
在JavaScript中,Proxy
是一个内置的方针,用于创立一个署理方针,能够阻拦并定制方针方针上的操作。Proxy
的构造函数承受两个参数:方针方针(Target)和处理器方针(Handler)。
参数解说如下:
- 方针方针(Target):即被署理的方针,能够是任何JavaScript方针(包括数组、函数、一般方针等)。
Proxy
会在方针方针上创立一个署理,阻拦对方针方针的操作。 - 处理器方针(Handler):一个包含各种阻拦办法的方针。处理器方针能够界说一系列阻拦器(也称为圈套或署理办法),这些阻拦器在对署理方针进行操作时会被主动调用。处理器方针能够重写或定制这些操作,以完成自界说的行为。
处理器方针的常见阻拦器办法包括:
-
get(target, property, receiver)
:阻拦对署理方针特点的读取操作。 -
set(target, property, value, receiver)
:阻拦对署理方针特点的赋值操作。 -
has(target, property)
:阻拦in
操作符的操作。 -
deleteProperty(target, property)
:阻拦delete
操作符的操作。 -
apply(target, thisArg, argumentsList)
:阻拦对署理方针的函数调用操作。 -
construct(target, argumentsList, newTarget)
:阻拦对署理方针的new
操作符的操作。
为什么 reactive 不能监听原始数据类型?
要了解为什么reactive
无法监听原始数据类型,咱们需求提到前置常识里的Proxy
。实际上,reactive
底层是经过Proxy
来完成数据劫持的。因此,只需了解了Proxy
的特性,就能明白为什么reactive
不支持原始数据类型了(代码能够跟着敲一下,便于了解也能够加深印象)。
先来试验一下运用Proxy
监听方针,能够留意看代码注释:
// 界说一个方针
let testObj = {
name: '11'
};
// 将方针传入Proxy中,并传入处理器方针来重写它的get和set办法
const proxyObj = new Proxy(testObj, {
get: () => {
// 获取数据的时分输出‘get’
console.log('get');
},
set: () => {
// 设置数据的时分输出‘set’
console.log("set");
return 'set';
}
});
// 试验数据是否正常被监听了
proxyObj.name = '22';
proxyObj.name
假如你运行了上述代码,会发现控制台正常输出了” set “和” get “,表明咱们成功运用Proxy
对数据进行了监听。接下来,假如咱们将testObj
改成原始数据类型,并再次测验,你会发现控制台输出了如下报错信息:无法运用非方针作为方针或处理程序创立署理。这意味着Proxy
是无法对原始数据类型进行署理的。而reactive
正是基于Proxy
封装而成的,所以reactive
不能监听原始数据类型也就不难了解了。
那么下一个问题紧接着就来了:ref
,为什么能够监听原始数据类型?
为什么 ref 能够监听原始数据类型?
在探求这个问题之前,让咱们先来了解一下ref
这个API的封装思路是什么,以及它与reactive
之间的异同。一同也会解答为什么ref
需求运用.value
。
其实经过前文的试验,咱们现已观察到:Proxy
是只能监听方针,而无法直接处理原始数据类型的数据。考虑到这一点,咱们是不是能够测验将原始数据放置在方针中,以便Proxy
进行监听呢?接下来咱们就按照这一思路,模拟完成对原始数据类型的监听。
ref
函数的封装原理如下:
- 首要,创立一个方针,这个方针包含一个名为
value
的特点,用于存储咱们传入的初始值(方针类型或者原始数据类型皆可)。 - 接下来,就和
reactive
API的封装根本共同了,运用Proxy
来创立一个署理方针。署理方针会阻拦对其特点的读取和赋值操作。 - 在署理方针的阻拦器办法中,对于读取操作,会回来方针的
value
特点的值。而对于赋值操作,会将新的值赋给呼应式方针的value
特点。 - 最后,将署理过的方针回来。这样,
ref
函数回来的值实际上便是由署理方针包装过的方针了。
function ref(value) {
// 将传入的初始值存在reactiveObj中
const reactiveObj = { value };
// 回来一个Proxy署理
return new Proxy(reactiveObj, {
// 读取数据
get(target, property, receiver) {
console.log('触发了获取操作');
return target.value;
},
// 更新数据
set(target, property, value, receiver) {
target.value = value;
console.log('触发了更新操作');
return 'set';
},
});
}
// 测验
const count = ref(100000);
count.value // 输出'get'
count.value = 1; // 输出'set'
这样操作之后,控制台现已能够正常输出了” 触发了获取操作 “和” 触发了更新操作 “了。
在ref
的封装进程中,咱们将值包装在一个方针的value
特点中,这个进程就解说了为什么咱们需求运用.value
来访问和修正ref
封装回来的值。同样的,这儿不论传入的数据是方针类型仍是原始数据类型,都会经过这个包装将其放在一个方针里,然后再运用Proxy
监听包装后的方针,所以才让ref
具有一同署理原始数据和方针数据的才能。
为什么只需修正呼应式数据 视图就会主动改动
当修正数据后,视图会当即更新,这是Vue的常见特性。然而,你是否曾思考过其间的原理?Vue是如何知道咱们修正了哪个数据的呢?
实际在Vue源码中,这个进程涉及到了更多的内容和考虑,还要考虑模板编译等内容。因为这些内容并非本文的要点,咱们不会过多谈论。为了更明晰地展现原理而不涉及源码细节,本文采用了下面这样一个简化的办法来代替杂乱的进程。
从上面小节中现已了解到,在Vue中,咱们运用Proxy
来署理数据方针,使其成为可观察的。也便是当咱们修正署理方针的特点时,能够在Proxy
中感知到这种改动并触发相关操作,然后来做一些咱们想做的事的,比如:数据改动的时分,是不是能够将对应的视图也一同改掉?
所以接下来要对上面的监听做一点点改造,具体便是在setter
函数中经过id
获取到对应的DOM元素,并直接替换更新后的数据,然后到达修正数据后视图改动的效果:
<div id="app"></div>
<button onclick="count.value ">加一</button>
<div id="updateTest"></div>
<script>
function ref(value) {
const reactiveObj = { value };
return new Proxy(reactiveObj, {
// 读取数据
get(target, property, receiver) {
console.log('get', property);
return target.value;
},
// 更新数据
set(target, property, value, receiver) {
target.value = value;
console.log('set');
document.querySelector("#updateTest").innerHTML = value;
return 'set';
},
});
}
const count = ref(100000);
document.querySelector("#updateTest").innerHTML = count.value;
</script>
现在,点击加一按钮,页面就能够跟着数据改动更新啦!接下来只需求增加一套依靠搜集来盯梢count
特点的改动,就能够在在数据更新时主动更新相关的视图啦。
注释写的较全,所以代码不过多解说,能够多留意注释哦!
首要,咱们需求创立一个Dep
类来办理依靠的搜集和告诉。
// 创立依靠办理类 Dep
class Dep {
constructor() {
// 用于存储依靠的订阅者
this.subscribers = new Set();
}
// 增加订阅者
depend() {
if (activeWatcher) {
this.subscribers.add(activeWatcher);
}
}
// 告诉所有订阅者进行更新
notify() {
this.subscribers.forEach((watcher) => watcher.update());
}
}
然后,咱们需求将Dep
实例与count
特点相关起来,再对上面的ref
函数进行一下改造。
// 先将activeWatcher设为null 确保在初始化阶段没有活动的Watcher方针
let activeWatcher = null;
// 创立 ref 函数
function ref(value) {
const reactiveObj = { value };
// 创立依靠办理实例
const dep = new Dep();
return new Proxy(reactiveObj, {
// 读取数据
get(target, property, receiver) {
console.log('get', property);
dep.depend(); // 把依靠搜集一下
return target.value;
},
// 更新数据
set(target, property, value, receiver) {
target.value = value;
console.log('set');
dep.notify(); // 告诉依靠更新
return 'set';
},
});
}
最后,咱们在按钮的点击事情监听器中创立了一个Watcher
实例,在数据改动时更新视图。
// 调用ref函数
const count = ref(100000);
// 将初始值放到页面上
document.querySelector("#updateTest").innerHTML = count.value;
// 创立 Watcher 类
class Watcher {
constructor(updateFn) {
this.updateFn = updateFn;
}
// 执行更新操作
update() {
this.updateFn();
}
}
activeWatcher = new Watcher(() => {
// 更新视图
document.querySelector("#updateTest").innerHTML = count.value;
});
至此,咱们现已完成了依靠的搜集。下面是整个依靠搜集触发的具体流程,能够跟着再收拾一下:
- 当用户点击按钮时,按钮的点击事情被触发。在点击事情中,每点击一次就将
count
的值加一。 -
count
的值改动了,在数据改动后更新的进程中,Proxy
的监听起效了,count
的set
函数被调用,而其间包括了告诉依靠更新的逻辑,于是触发了告诉依靠更新的操作。 - 而在告诉依靠更新的进程中,
activeWatcher
又被触发,调用了它的update
办法。 -
update
办法中执行了更新视图的函数,将count.value
的值更新到#updateTest
元素中,然后更新了视图。
总结
实际在Vue3的源码中必定不会封装的如此简略,实际上要考虑的事情还有很多,比如Vue怎样知道哪里运用了数据,更新后的数据该更新到哪里?Vue是怎样把script里界说的呼应式数据正确的放到页面上的?等诸如此类的问题。本文这儿只是提供一个整体流程和思考路径,假如有对详尽的源码感兴趣的小伙伴,能够在谈论区提出,后续会连续更新数据劫持、模板编译等相关的源码解读,到时也会本文留传的疑惑解开。