004-计算(computed)与侦听(watch)-目录
computed
计算属性就是当依赖的属性的值发生变化的时候,才会触发他的更改,如果依赖的值,不发生变化的时候,使用的是缓存中的属性值。
基本使用案例
<template>
<div>
<div>
姓:<input type="text" v-model="firstName">
</div>
<div>
名:<input type="text" v-model="lastName">
</div>
<div>
全名:{{ name }}
</div>
<div>
全名2:{{ name2 }}
</div>
<div>
<button @click="changeName">改名</button>
</div>
</div>
</template>
<script setup lang='ts'>
import { ref, computed } from 'vue'
let firstName = ref('王')
let lastName = ref('程')
// 1.选项式写法:支持一个对象传入get函数以及set函数,自定义操作。
let name = computed<string>({
get() {
return firstName.value + '·' + lastName.value
},
set(newVal) {
[firstName.value, lastName.value] = newVal.split('·')
}
})
const changeName = () => {
name.value = '莉莉娅·雅典娜'
}
// 2.函数式写法:只支持一个getter函数,不允许修改,因为值是只读的。
let name2 = computed(() => firstName.value + '·' + lastName.value)
</script>
购物车案例
<template>
<div>
<div>
<input type="text" placeholder="搜索" v-model="keyWord">
</div>
<div style="margin-top: 20px;">
<table width="500" border>
<thead>
<tr>
<th>物品</th>
<th>单价</th>
<th>数量</th>
<th>总价</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in searchData">
<td>{{ item.name }}</td>
<td>{{ item.price }}</td>
<td>
<button @click="item.num > 1 ? item.num-- : null">-</button>
<input v-model="item.num" type="number">
<button @click="item.num < 99 ? item.num++ : null">+</button>
</td>
<td>{{ item.price * item.num }}</td>
<td>
<button @click="del(index)">删除</button>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="5">
<span>总价:{{ total }}</span>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</template>
<script setup lang='ts'>
import { reactive, ref,computed } from 'vue'
interface Data {
name: string,
price: number,
num: number
}
const data = reactive<Data[]>([
{
name: "手机",
price: 100,
num: 1,
},
{
name: "平板",
price: 200,
num: 1,
},
{
name: "耳机",
price: 300,
num: 1,
}
])
// 筛选查询
let keyWord = ref<string>('')
let searchData = computed(()=>{
return data.filter(item => item.name.includes(keyWord.value))
})
// 计算合计
let total = computed(() => {
return data.reduce((prev: number, next: Data) => {
return prev + next.num * next.price
}, 0)
})
// 删除
const del = (index: number) => {
data.splice(index, 1)
}
</script>
computed本质(脏值检测机制)
- 接收方法参数并通过getter和setter将参数格式化,参数为
getterOrOptions
。- 判断接收参数,如果是一个函数,则设置为只读。使用了一个
onlyGetter
,它允许将传过来的函数赋值给getter,但如果进行设置值的操作,就报错。 - 判断接收参数,如果是选项式的对象,则使用
getter = getterOrOptions.get
和setter = getterOrOptions.set
进行读取和设置的操作。
- 判断接收参数,如果是一个函数,则设置为只读。使用了一个
- 参数格式化后,会得到
getter
和setter
两个变量,然后将变量传入到ComputedRefImpl类
中。具体为:const cRef = new ComputedRefImpl(getter,setter,onlyGetter||!setter,isSSR)
- 类中定义了
_value
,_dirty
等值。、 _dirty
用来判断是否发生变化决定是否使用缓存的值。默认为true。- 对
this
通过toRaw
去脱离proxy代理。然后判断脏值_dirty
为true则去读取self.effect.run()
来获取到变化后的值。并且将_dirty
设置为false,不发生变化,则不调用读取方法直接返回上一次的值。 - 当依赖发生变化的时候,才会调用
ReactiveEffect
,走完将_dirty
变为true。
- 类中定义了
computed手写原理
关于响应式原理见《Vue3响应式原理》。
在此基础上引入 computed.ts
,优化 effect.ts
和 reactive.ts
的脏值检测机制。
具体代码如下:
%% index.html %%
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>vue3-computed实现</title>
</head>
<body>
<div id="app"></div>
<script type="module">
import { computed } from "./computed.js";
import { reactive } from "./reactive.js";
window.a = reactive({ name: "张三", age: 18 });
window.b = computed(() => {
console.log("开始计算->");
return a.age + 10;
});
// 此时的b会永远开始计算,没有脏值机制,不会使用缓存数据
</script>
</body>
</html>
%% computed.ts %%
import { effect } from "./effect.js"
// 以函数式为例
export const computed = (getter: Function) => {
let _value = effect(getter, {
scheduler: () => {
_dirty = true
}
})
// 引入脏值机制,未更新数据则使用缓存
let catchValue: any
let _dirty = true
class ComputedRefImpl {
get value() {
if (_dirty) {
catchValue = _value
_dirty = false
// 为防止一次为false后永远为false
// 需要再effect中引入调度schedule
}
return catchValue
}
}
return new ComputedRefImpl()
}
%% reactive.ts %%
import { track, trigger } from './effect.js'
export const reactive = <T extends object>(target: T) => {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
track(target, key)
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
trigger(target, key)
return res
}
})
}
%% effect.ts %%
// 需要引入调度,来实现脏值机制的重置
interface Options {
scheduler?: Function
}
let activeEffect: any;
export const effect = (fn: Function, options: Options) => {
const _effect = function () {
activeEffect = _effect;
let res = fn()
return res
}
// 进行调度的赋值
_effect.options = options
_effect()
// 当computed使用effect使用时,需要返回计算后的值
return _effect
}
const targetMap = new WeakMap()
export const track = (target: any, key: any) => {
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
deps.add(activeEffect)
}
export const trigger = (target: any, key: any) => {
const depsMap = targetMap.get(target)
const deps = depsMap.get(key)
deps.forEach((effect: any) => {
// 调度判断
if (effect?.options?.scheduler) {
effect?.options?.scheduler?.()
} else {
effect()
}
})
}
%% tsconfig.json %%
{
"compilerOptions": {
"target": "ES2016",
"module": "ESNext",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
watch
侦听器主要是用来监听响应式数据的变化。比如使用ref,reactive的数据源,则可以侦听到数据的变化。
基本使用案例
<template>
<div>
第一个输入框:<input v-model="message" type="text">
<br>
第二个输入框:<input v-model="message2" type="text">
<br>
第三个输入框:<input v-model="obj.one.two.three.value" type="text">
<br>
第四个输入框:<input v-model="obj.one.two.three.label" type="text">
</div>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue'
let message = ref<string>('前端vue')
let message2 = ref<string>('前端react')
// 参数1:source 数据源
// 参数2:cb callback回调函数,包括新值和旧值
// watch(message, (newVal, oldVal) => {
// console.log('newVal', newVal, 'oldVal', oldVal);
// })
// 对多个数据进行侦听
watch([message, message2], (newVal, oldVal) => {
console.log('newVal', newVal, 'oldVal', oldVal);
})
// 对象侦听
let obj = ref({
one: {
two: {
three: {
value: '疯狂星期四 v我50',
label: '肯德基'
}
}
}
})
// 深层次侦听需要加deep属性
// 对于引用类型,新值和旧值是一样的
// 如果是reactive对象,则不用开启deep属性即可实现侦听
watch(obj, (newVal, oldVal) => {
console.log('newVal', newVal, 'oldVal', oldVal);
}, {
deep: true, // 深度侦听
immediate: false, // 默认为false,如果为true,则一开始会将cb回调函数执行一次
flush: "pre" // 默认pre[组件更新前执行],sync[同步执行],post[组件更新后执行]
})
// 侦听单一属性,需要使用函数回调写法
watch(() => obj.value.one.two.three.label, (newVal, oldVal) => {
console.log('newVal', newVal, 'oldVal', oldVal);
}, {
deep: true // 深度侦听
})
</script>
watch本质
*核心是doWatch方法
1.首先格式化传入的souce数据源
,包括ref,reactive,多个属性的数组,单一属性的函数这些方式传入的数据。将这些数据格式化后放入getter函数
。
2.如果是reactive,则将deep默认设置为true。
3.如果是数组,则进行遍历,如果遍历其中有reactive,则使用traverse()
方法进行递归。
4.如果是函数,则会判断cb是否存在,存在就简单的进行封装。如果不存在,就执行watchEffect()
。
5.对旧值进行一个初始化。然后做一个调度,即根据flush
三种模式进行操作。
6.如果是有immediate
属性为true,则立马先调用一次。并且新值会返一个undefined。
7.最后做一次更新。进行赋值,如果是对象就直接引用了。
watchEffect高级侦听器
立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
<template>
<div>
第一个输入框:<input v-model="message" type="text">
<br>
第二个输入框:<input v-model="message2" type="text">
</div>
</template>
<script setup lang="ts">
import { watchEffect, ref } from 'vue';
let message = ref<string>('前端vue')
let message2 = ref<string>('前端react')
// 1.watchEffect()是非惰性的,一开始就会调用
watchEffect(() => {
console.log('message:', message.value);
console.log('message2:', message2.value);
})
// 2.watchEffect()可以加入一个清除副作用函数[在触发监听之前会调用一个函数可以处理逻辑]
watchEffect((oninvalidate) => {
console.log('message', message.value);
oninvalidate(() => {
// 优先执行,但是第一次执行不会触发
console.log('beforeWatch');
})
console.log('message2', message2.value);
}, {
onTrigger(e) { // 用于调试,查看值的变化
debugger
}
})
// 3.停止监听 stopWatch
const stop = watchEffect((oninvalidate) => {
console.log('message', message.value);
oninvalidate(() => {
console.log('beforeWatch');
})
console.log('message2', message2.value);
}, {
flush: "post", // 副作用刷新时机,一般用post[组件更新后执行],还有pre和sync
onTrigger(e) { // 用于调试,查看值的变化
debugger
}
})
stop() // 执行后停止侦听
</script>