Vue3学习记录(三)
原理
双向绑定
当修改数据时视图发生改变;在视图发生改变时去修改对应的数据,这就是双向绑定。
- 发布-订阅: 可以使用各种监听事件,当事件去触发时修改数据;
- 数据劫持: 访问或修改属性时,会触发相应的事件。
即双向绑定的实现原理就是 发布-订阅 + 数据劫持。
vue2的双向绑定实现
Object.defineProperty(obj, prop, descriptor)
通过Object.defineProperty
为对象绑定getter
和setter
方法。在__render
中会读取数据。于是将会触发getter
,getter
会将当前的watcher
订阅到这个数据持有的dep
中,完成了依赖收集;而我们修改数据之后会触发setter
方法,触发依赖过程中订阅的所有watcher
的update
,完成了派发更新。
不是所有的对象的改变都可以触发视图改变的:
var vm = new Vue({
data:{
obj: {
a:1,
},
test1:1
}
})
// vm.test2是非响应的
vm.test2 = 1
// vm.obj.b 是非响应的
vm.obj.b = 2
// vm.obj.b 是响应式的
Vue.$set(this.obj,'b', 2)
- 对于已经创建的实例,不能动态增加响应式属性,即添加的属性不是响应式的,可以使用
$set
解决; - 对于数组,
vue
封装了push、pop、shift、unshift、splice、sort、reverse
,其中push、unshift、splice
会将数据转为响应式数据,所以有些数组的操作并不一定会触发视图更新。
vue3的双向绑定实现
Object.defineProperty
只能一个属性一个属性监听,如果对象的属性嵌套的很深,则需要深度遍历;当修改数据的时候,对于这样的数据又得重新遍历,相当的耗性能。
而proxy
可以监听一整个对象,且只有数据被用到的时候才会去监听它,性能得到了提高。
var proxy = new Proxy(target, handler);
主要API
响应性基础API
reactive
创建响应式对象
<template>
<div>
<p>{{obj.count}}</p>
<button @click="add">加1</button>
</div>
</template>
<script>
import { reactive } from 'vue';
export default {
name: 'App',
setup(){
const obj = reactive({count: 0});
function add(){
obj.count += 1;
}
return { obj,add };
}
}
</script>
- 通过
reactive
传入一个对象,返回该对象的副本,实现响应式。当响应对象发生改变时,原对象的值也会发生改变:
setup(){
const tmp = {count: 0};
const obj = reactive(tmp);
function add(){
obj.count += 1;
console.log(obj, tmp) // Proxy {count: 1} {count: 1}
}
return { obj, add };
}
- 之前在
vue2
中直接根据下标修改数组的值,UI界面不会发生改变,但是在vue3
中是可以做到的。
setup(){
let arr = reactive([1,2,3]);
function handleClick(){
arr[1] = 0
}
return { arr, handleClick }
}
readonly
创建只读对象
setup(){
const obj = readonly({ count: 0});
const obj2 = readonly({
f: {
count: 0,
son: {
count: 0
}
}
});
function add(){
obj.count += 1;
obj2.f.count += 1;
obj2.f.son.count += 1;
console.log(obj, obj2); // 0 0 0
}
return { obj, add };
}
通过readonly
传入一个对象,只会对这个对象的属性(含更深层次的属性)只有读的权限,没有修改的权限。
shallowReadonly
创建第一层只读对象
readonly
和shallowReadonly
不一样,前者是所有属性都是只读的,而shallowReadonly
只有第一层属性是只读的。
setup(){
const obj = shallowReadonly({ count: 0});
const obj2 = shallowReadonly({
count: 0,
f: {
son: {
count: 0
}
}
});
function add(){
obj.count += 1;
obj2.f.count += 1;
obj2.f.son.count += 1;
console.log(obj.count, obj2.count, obj2.f.son.count); // 0 0 1 obj2.f.son.count在UI层并没有更新,仍然是0
}
return { obj, obj2, add };
}
isProxy
检测对象是被代理过
setup(){
const obj = reactive({ count: 1});
const obj2 = readonly({ count: 1});
const a = 1;
function add(){
const result = isProxy(obj);
const result2 = isProxy(obj2);
const result3 = isProxy(a);
console.log(result, result2, result3); // true true false
}
return { obj, add };
}
isReactive
是否是 reactive
创建的
setup(){
const obj = reactive({ count: 1});
const obj2 = readonly({ count: 1});
const a = 1;
function add(){
const result = isReactive(obj);
const result2 = isReactive(obj2);
const result3 = isReactive(a);
console.log(result, result2, result3); // true false false
}
return { obj, add };
}
isReadonly
是否是只读
setup(){
const obj = reactive({ count: 1});
const obj2 = readonly({ count: 1});
const obj3 = shallowReadonly({ count: 1});
function add(){
const result = isReadonly(obj);
const result2 = isReadonly(obj2);
const result3 = isReadonly(obj3);
console.log(result, result2, result3);
}
return { obj, add };
}
toRaw
返回 reactive
、 readonly
、ref
代理的原始对象
ref
/reactive
的数据都又一个特点,就是每次修改都会被追踪,即更新界面,但是有的时候我们希望有些操作不要被追踪即,即不会更新UI界面,那我们可以使用toRaw
,toRaw
可以返回reactive/ref
的原始数据,我们可以通过修改原始数据的值进而修改到reactive/ref
的值但不会触发UI界面的更新。
setup(){
const value = {
count: 0,
f: {
son: {
count: 0
}
}
}
const obj = reactive(value);
const obj2 = toRaw(obj);
console.log(value === obj); // false
console.log(value === obj2); // true
function handleClick(){
obj2.count = 1;
console.log(obj); // obj和obj2的值都发生了改变,但是UI界面不会发生更新
console.log(obj2);
}
return { obj, handleClick };
}
对于ref
有点不同,由于ref
会将变量包裹一层value
,所以使用toRaw
的时候应该是:
const obj = ref(value);
const obj2 = toRaw(obj.value);
markRaw
标记一个对象,使其永远不会转换为代理(即不可被追踪)。返回对象本身,一般在编写自己的第三方库时使用。
setup(){
const value = {
count: 0,
f: {
son: {
count: 0
}
}
}
const markRawValue = markRaw(value);
console.log(isReactive(reactive(markRaw))); // false
const obj = reactive(value);
console.log(isReactive(markRaw(obj))); // false
function handleClick(){
obj.count = 2;
console.log(obj); // 值更新,UI不更新
}
return { obj, handleClick };
}
shallowReactive
不对嵌套对象进行深度响应式(Proxy)转换
在默认情况下,reactive
创建的对象都会进行递归监听,即每一层都会包装proxy
:
setup(){
let obj = reactive({
a: 1,
gf: {
b: 2,
f: {
c: 3,
s: {
d: 4
}
}
}
});
function handleClick(){
console.log(obj);
console.log(obj.gf);
console.log(obj.gf.f);
console.log(obj.gf.f.s);
}
return { obj, handleClick }
}
如果数据量特别大,或者数据的层级比较深,都会很消耗性能,vue3
提供了shallowReactive
,将obj
改成shallowReactive
:
let obj = shallowReactive({
a: 1,
gf: {
b: 2,
f: {
c: 3,
s: {
d: 4
}
}
}
});
那么此时打印的内容会发现,只有第一次包裹了proxy
:
是否包裹了proxy
决定了界面是否会更新:
function handleClick(){
// obj.a = 'a'; // 如果改变obj.a即改变了第一层,则UI界面会发生改变
obj.gf.b = 'b';
obj.gf.f.c = 'c';
obj.gf.f.s.d = 'd';
console.log(obj.a);
console.log(obj.gf.b);
console.log(obj.gf.f.c);
console.log(obj.gf.f.s.d);
}
由于第一层数据并没有发生改变,所以值虽然改变了,但是UI视图并不会发生改变:
Refs
ref
返回一个响应式且可变的 ref 对象
之前使用reactive
的时候我们传入的参数都是对象,如果传入某个普通变量也是可以,但是修改这个变量,UI界面并不会发生改变:
setup(){
let count = reactive(123);
function handleClick(){
count++;
console.log(count); // 124 但是UI界面并不会更新
}
return { count, handleClick }
}
如果为了实现响应式,每次都把普通变量包装成对象再使用reactive
,那并不好用。所以可以使用ref
。ref
的底层实现就是reactive
,每次传入普通变量xxx,就会处理为: {value: xxx}
:
setup(){
let count = ref(123); // 这里会转为{value: 123}
function handleClick(){
count.value++;
console.log(count.value); // 124 UI界面会更新
}
return { count, handleClick }
}
在js
中我们读取/修改count
需要使用count.value
;而在vue template
中直接使用count
即可。
小结:Vue通过当前数据的__v_ref来判断,如果有这个私有的属性, 并且取值为true, 那么就代表是一个ref类型的数据,则会自动添加.value
。另外在也可以给ref
传递对象,同样会包裹一层value
。
在vue2中我们可以通过ref
来获得元素,vue3
也可以,但是需要改变写法:
<div ref="box">我是div</div>
setup() {
// console.log(this.$refs.box); // 在setup中无法读取this
let box = ref(null); // reactive({value: null})
onMounted(()=>{
console.log('onMounted',box.value);
});
console.log(box.value); // null
return {box};
}
isRef
判断是否是ref变量
setup(){
let count = ref(123);
let obj = reactive({
name: 'sugarMei'
});
function handleClick(){
console.log(isRef(count)); // true
console.log(isRef(obj)); // false
console.log(isReactive(count)); // false
console.log(isReactive(obj)); // true
}
return { count, handleClick }
}
shallowRef
不会使其值成为响应式的
跟Reactive
,Ref
也是递归监听的:
setup(){
let count = ref(1);
function handleClick(){
console.log(count); // proxy对象
console.log(count.value); // 1
}
return { count, handleClick }
}
之前提过ref
的底层是reactive
,每次会将xx
改为value.xx
,如果使用shallowRef
,则第一层是value
,而不是xx
,那么怎么修改xx
的值将不会引起视图的改变:
setup(){
let count = shallowRef({
count: 1
});
function handleClick(){
count.value.count++;
console.log(count.value); // count的值发生了改变,但是没有触发UI视图改变
}
return { count, handleClick }
}
如果遇到shallowRef
创建的对象,又很想修改除了value
之外的内部值之后触发UI视图改变,可以使用triggerRef
。
triggerRef
手动触发视图更新
function handleClick(){
count.value.count++;
triggerRef(count);
}
这样修改count
值之后就会触发UI视图的改变了。
toRef
创建一个引用对象,且更新之后不会修改UI界面
ref
根据变量创建的对象,就是类似JS中的复制:
-
即如果是普通变量,那么就是简单的复制,
ref
改变或者变量
改变都不会互相影响,且ref
的改变会引起视图变化;let objTemp = 2 const obj = ref(objTemp); objTemp ++ ; // 改变原始值,原始值会发生改变,但是ref值不会,UI界面不会发生更新 console.log(obj.value); // 2 console.log(objTemp); // 3 obj.value ++ ; // 改变ref值,原始值不会发生改变,但是ref值会,UI界面发生更新 console.log(obj.value); // 3 console.log(objTemp); // 2
-
如果变量是对象,那么就是两者指向了同一个内存地址,两者之间变化一个都会互相影响,但是原始数据引起的数据改变不会更新UI视图,而ref的改变会让UI视图发生改变。
let objTemp = { count: 2 } const obj = ref(objTemp); objTemp.count ++ ; // 改变原始值,原始值和ref值会发生改变,但UI界面不会发生更新 console.log(obj.value.count); // 3 console.log(objTemp.count); // 3 obj.value.count ++ ; // 改变ref值,原始值和ref值会发生改变,UI界面发生更新 console.log(obj.value.count); // 3 console.log(objTemp.count); // 3
这时候可以使用toRef
,它创建处理的数据和以前的有关,但是数据变化不会自动更新界面。
let objTemp = {
count: 2
}
const obj = toRef(objTemp, 'count'); // => {value: count}
obj.value ++ ; // 改变ref值,原始值和ref值会发生改变,但是UI界面不发生更新
console.log(obj.value); // 3
console.log(objTemp.count); // 3
小结: 如果利用toRef将某一个对象中的属性变成响应式的数据,我们修改响应式的数据是会影响到原始数据的,但是如果响应式的数据是通过toRef创建的, 那么修改了数据并不会触发UI界面的更新。
toRef
批量创建toRef
对象
由于toRef
每次只能传入一个对象属性,所以Vue提供了传入对象的toRefs
,只是读取每个属性的时候需要使用xxx.value
:
setup(){
let objTemp = {
count: 2
}
const obj = toRefs(objTemp);
function handleClick(){
obj.count.value ++ ; // 改变ref值,原始值和ref值会发生改变,但是UI界面不发生更新
console.log(obj.count.value); // 3
console.log(objTemp.count); // 3
}
return { obj, handleClick };
}
customRef
创建一个自定义的 ref
该函数接收 track
和 trigger
函数作为参数,并应返回一个带有 get
和 set
的对象。
import { customRef } from 'vue';
function myRef(value){
return customRef((track, trigger) => {
return {
get(){
track(); // 告诉vue要追踪这个变量,如果不加,那么修改完之后,UI界面不会显示
console.log('get', value);
return value;
},
set(newVal){
console.log('set', newVal);
value = newVal;
trigger(); // 主动触发ui视图更新
}
}
})
}
export default {
name: 'App',
setup(){
let state = myRef(18); // 使用了自定义ref
function handleClick(){
state.value += 1;
}
return { state, handleClick };
}
}
</script>
get 18 # 界面读取的时候
get 18 # set之后读取了值
set 19 # set修改值,触发视图更新
get 18 # 界面更新,读取值
应用场景: 如果我们在setup
中发起一个异步请求并更新数据时可以的,但是如果想使用async/await
将异步请求变为同步请求是行不通的,UI 界面渲染不出来,但是如果将所有异步请求都写在setup
中并不舒服,可以这样:
function myRef(value){
return customRef(async (track, trigger) => {
await fetch(value)
.then(res => {
return res.json();
})
.then(data => {
console.log(data);
value = data;
trigger(); // 记得更新界面
})
.catch(err => {
console.log(err);
})
return {
get(){
track(); // 告诉vue要追踪这个变量,如果不加,那么修改完之后,UI界面不会显示
console.log('get', value);
return value;
},
set(newVal){
console.log('set', newVal);
value = newVal;
trigger(); // 主动触发ui视图更新
}
}
})
}
export default {
name: 'App',
setup(){
let state = myRef('../public/data.json'); // 使用了自定义ref
function handleClick(){
state.value += 1;
}
return { state, handleClick };
}
}
unRef
则返回内部值
如果参数为ref
,则返回内部值,否则返回参数本身。即 val = isRef(val) ? val.value : val
。
Computed
与Watch
conputed
setup(){
const count = ref(1);
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val + 1
}
})
function handleClick(){
plusOne.value = 1;
console.log(count.value); // 2
console.log(plusOne.value); // 3
}
return { count, handleClick };
}
watchEffect
类似watch
,但是是在setup
中使用
setup(){
const count = ref(0);
watchEffect(() => {
console.log(count.value); // 先打印0 后打印1
})
function handleClick(){
count.value = 1;
}
return { count, handleClick };
}
Watch
与watchEffect
比,有三个特点:
- 惰性地执行副作用,即当值发生改变的时候才会执行(
watchEffect
在初始化的时候也会打印); - 可以具体地说明触发监听器重新运行的状态,即可以指定是那个值改变就触发;
- 访问侦听状态的先前值和当前值。
// 侦听一个getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
// 直接侦听一个ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
// 监听多个源
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
与 watchEffect
共享行为
这个暂时没研究出来,晚点看完所有文档,可能可以连起来,到时候在补充~
上一篇: SpringBoot2.0新特性
下一篇: vue监听属性