Vue3-笔记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本质(脏值检测机制)

  1. 接收方法参数并通过getter和setter将参数格式化,参数为getterOrOptions
    1. 判断接收参数,如果是一个函数,则设置为只读。使用了一个onlyGetter,它允许将传过来的函数赋值给getter,但如果进行设置值的操作,就报错。
    2. 判断接收参数,如果是选项式的对象,则使用getter = getterOrOptions.getsetter = getterOrOptions.set进行读取和设置的操作。
  2. 参数格式化后,会得到gettersetter两个变量,然后将变量传入到ComputedRefImpl类中。具体为:const cRef = new ComputedRefImpl(getter,setter,onlyGetter||!setter,isSSR)
    1. 类中定义了_value_dirty等值。、
    2. _dirty用来判断是否发生变化决定是否使用缓存的值。默认为true。
    3. this通过toRaw去脱离proxy代理。然后判断脏值_dirty为true则去读取self.effect.run()来获取到变化后的值。并且将_dirty设置为false,不发生变化,则不调用读取方法直接返回上一次的值。
    4. 当依赖发生变化的时候,才会调用ReactiveEffect,走完将_dirty变为true。

computed手写原理

关于响应式原理见《Vue3响应式原理》。
在此基础上引入 computed.ts ,优化 effect.tsreactive.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>
  • 38
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值