前言
在之前的文章字节面试题:请你谈谈vue的响应式原理(一)- 掘金中我为各位讲解了一下vue3中reactive的原理并带大家手写了一个简易版的reactive。既然都分了一和二,那么大家肯定也能猜到这篇文章写什么了。相信看完这篇文章之后,各位读者老爷就能理解为什么在vue3中会有两个函数用于创建响应式数据,而为什么ref能把原始类型和引用类型的值都转变为响应式。
正文
在讲明白ref将数据变为响应式原理之前,我们先来简单看看这东西是怎么用的。
js
<template>
<div> </div>
</template>
<script setup>
const name = '浦东彭于晏'
const refName = ref(name)
const obj = {
name: '南昌吴彦祖',
age: 22
}
const refObj = ref(obj)
console.log(refName);
console.log(refObj);
</script>
在代码中我们可以看到,如果我们试图将包装过后的数据直接拿来使用,不管原来的数据是引用类型还是原始类型,我们能拿到的并不是原数据,而是被包裹了一层奇奇怪怪的东西,只有在后面接上.value之后才能拿到"原来的"数据。如果我们展开这个"奇奇怪怪"的东西,会发现上面除了我们给的数据,还有一个名为__v_isRef和_rawValue的键值对,_rawValue很好理解,就是被加工前的数据,而__v_isRef正是ref响应式的标签。代表这个值已经是响应式了。
原始类型
在大致了解的ref的用法之后,我们就可以开始聊聊ref的响应式原理了。这里还是和之前讲reactive一样,会通过手写一个ref 的方式去给各位读者姥爷讲清楚。
废话不多说,直接上代码。首先,来一个函数
js
function createRef(value) {
}
刚刚也提到__v_isRef是ref响应式的标签,而在reactive中我们也讲到过关于避免重复代理的问题。所以这里我们就可以用__v_isRef去做一个判断
js
function createRef(value) {
// 将value变为响应式,同样需要判断是否已经是响应式数据
if (value.__v_isRef) {
return value
}
return new RefImpl(value)
}
RefImpl相信大家也能看出来,首字母大写是一个构造函数。在构造的过程中,首先就是打上__v_isRef的标签用于声明这个值已经是响应式了。随后就是_value赋值。这也正是为什么我们需要通过._value才能看到数据。
js
class RefImpl {
constructor(val) {
// 响应标记,有标记的就是响应式数据
this.__v_isRef = true
this._value = convert(val)
}
}
新语法
接下来的新语法是讲原始类型的值转变为响应式的关键点了。之前我们讲reactive的时候聊到了引用类型是通过proxy代理去做响应式,这里则是用class语法中的"访问器属性"去给原始类型做响应式
首先我们要知道,写在constructor外面的属性是在实例对象的隐式原型上,对原型链不太熟悉的同学可以看看我之前的文章。编程小白的福音:轻松学会JavaScript原型链
js
class RefImpl {
constructor(val) {
// 响应标记,有标记的就是响应式数据
this.__v_isRef = true
this._value = convert(val)
}
// 这一步是在构造函数的现实原型,也就是实例对象的隐式原型上
get value() {
}
set value(newValue) {
}
}
}
这里在函数前面加get的行为属于是在使用js中对象的自读取能力,这种行为就类似下面这种代码。
js
let obj = {
name: 'zhangsan',
age() {
return 18;
},
}
console.log(obj.age());
而加"_"的目的则在于将属性标记为内部使用,避免在实例化对象的时候,属性因为没有setter而导致的程序报错。如此一来,当我们再次访问或读取这个值的时候,就能像被代理过的函数那样触发某些特定需求了。
接下来的事情,各位看过上一篇文章的读者姥爷想必已经门清了,就是在get中做依赖收集以及在set中去触发依赖函数。而之前的依赖收集函数可以直接复用。在set中,当值被修改,我们可以通过对比修改前后的值是否一致,从而选择是否修改。
js
import { track, trigger } from "./effect.js"
class RefImpl {
constructor(val) {
// 响应标记,有标记的就是响应式数据
this.__v_isRef = true
this._value = val
}
// 这一步是在构造函数的现实原型,也就是实例对象的隐式原型上
get value() {
// 为this对象做依赖收集
track(this, 'value')
return this._value
}
set value(newValue) {
console.log("set" + newValue);
// 判断是否相等,相等则不触发更新
if (newValue !== this._value) {
// 这里不能直接复制,要看类型
this._value = val
// 为this对象做依赖触发
trigger(this, 'value')
}
}
}
到这一步,ref中将原始类型变为响应式对象就完成了。但问题在于,如何通过ref将引用类型变为响应式呢?实际上,还是通过我们上次讲的reactive。这里我们优化一下代码,做一个类型判断。如果是原始类型则直接返回,否则返回reactive处理过后的对象。
js
import { track, trigger } from "./effect.js"
import { reactive } from './reactive.js'
class RefImpl {
constructor(val) {
// 响应标记,有标记的就是响应式数据
this.__v_isRef = true
this._value = convert(val)
}
// 这一步是在构造函数的现实原型,也就是实例对象的隐式原型上
get value() {
// 为this对象做依赖收集
track(this, 'value')
return this._value
}
set value(newValue) {
console.log("set" + newValue);
// 判断是否相等,相等则不触发更新
if (newValue !== this._value) {
// 这里不能直接复制,要看类型
this._value = convert(newValue)
// 为this对象做依赖触发
trigger(this, 'value')
}
}
}
function convert(value) {
if (typeof value !== 'object' || value === null) {
// 不是对象,就return value
return value
} else {
return reactive(value)
}
}
总结
其实对于vue3中到底该使用ref还是reactive一直都存在一些争议,有人偏爱reactive的简洁,不用挂个.value的拖油瓶让人神清气爽,写代码都有动力不少,而有人偏爱ref的全能,通篇都是ref。实际上对于二者来说,尤大大的建议是ref,而reactive的出现在我看来更像是一种"被迫无奈"的举动。而在实际开发中,具体使用ref还是reactive属于是个见仁见智的问题,至少在我的开发过程中,统一使用ref的情况占绝大多数。
最后,希望我的文章能够帮助到各位正在学习的朋友们,祝各位读者姥爷0 warning(s),0 error(s)!