从零实现vue3核心源码 day4

今天主要来学习watch和ref的具体实现。下面代码示例部分只做参考,具体完整代码可以在vue3源码解读: 手写vue3核心源码,内含详细解读 (gitee.com)内查看。

1. watch 和 computed 的区别

  1. computed 目的在于计算新值,有缓存
  2. watch 目的在于监控属性的变化做某一件事

2. watch监听简单类型为什么要包装成方法?

因为如果直接写 state.firstName 的话,取的是字符串,无法监听,所以需要包装成方法,利用effect触发依赖收集。

3. watch 核心实现

代码

js/reactivity/watch.js

scss 复制代码
function traverse(source, seen = new Set()) {
    if (!isObject(source)) {
        return source
    }
    // 对象嵌套互相引用,如果已经有了,直接返回
    if (seen.has(source)) {
        return source
    }
    seen.add(source)
    for (let k in source) {
        // 这里访问了对象中得1所有属性,触发依赖收集
        traverse(source[k], seen)
    }
    return source
}

export function watch(source, cb, options) {
    doWatch(source, cb, options)
}

function doWatch(source, cb, options) {
    let getter
    if (isReactive(source)) {
        // 要进行依赖收集
        getter = () => traverse(source)
    } else if (isFunction(source)) {
        getter = source
    }
    let oldValue
    let clean

    const onCleanup = (fn) => {
        clean = fn
    }
    const job = () => {
        if (cb) {
            if (clean) clean()
            const newValue = effect.run()
            cb(newValue, oldValue, onCleanup)
            oldValue = newValue
        } else {
            effect.run()
        }

    }
    const effect = new ReactiveEffect(getter, job)
    if (options && options.immediate) {
        job()
    }
    oldValue = effect.run()
}

4. 竟态问题

问题场景

考虑这么一个场景,有一个搜索框,如百度搜索框,实时监听输入内容请求接口。我们发送了3次请求,第一个请求接口耗时3s返回, 第二个请求耗时2s返回,第三个请求耗时1s返回,如果我们不作处理,那么必然返回第一个请求接口的内容。而我们实际需要的是第三个请求内容。

watch 的解决方案就是如果有新请求进来就清空上次的操作。这个原理类似于防抖。

自行实现

ini 复制代码
let timer = 4000;

function getData(data) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(data);
        }, (timer -= 1000));
    });
}

let arr = []; // 用于存储上一次的清理操作
// 什么是闭包?
// 我定义函数的作用域与执行函数的作用域不是同一个
watch(
    () => state.age,
    async function (newVal, oldVal, onCleanup) {
        // 自行实现
        // while (arr.length > 0) {
        //   let fn = arr.shift();
        //   fn();
        // }
        let flag = true;
        arr.push(function () {
            flag = false
        })
        let r = await getData(newVal);
        flag && (app.innerHTML = r);
    },
    {
        flush: "sync",
    }
);
state.age = 100; // 请求3s返回 100
state.age = 200; // 请求2s返回 200
state.age = 300; // 请求1s返回 300

首先第一次进来, arr.length 为 0, 不会执行 while, 接着定义 flag 为 true。添加闭包函数放入数组,函数内执行 flag 为 false。 接着等待 getData 异步返回。接着下次请求进来,执行了上次闭包函数将第一次的 flag 变为 false。 等到同步代码执行完成, 异步开始执行 第一次的 flag 已经变为 false, 就不会执行接下来的操作了。

简单来说, 就是每个回调有自己的 flag,当执行下一个回调 改变上一个 flag 状态。

源码实现

js/reactivity/watch.js

ini 复制代码
let clean

const onCleanup = (fn) => {
    clean = fn
}
const job = () => {
    if (cb) {
        if (clean) clean()
        const newValue = effect.run()
        cb(newValue, oldValue, onCleanup)
        oldValue = newValue
    } else {
        effect.run()
    }

}

步骤图

如果 getData 这一步为同步就不行了, 如果实际开发中,请把它包装成异步代码。

5. watchEffect

其实watchEffect 就是 effect。基于 watch 进行实现。

js/reactivity/watch.js

javascript 复制代码
export function watchEffect(effect, options) {
    doWatch(effect, null, options)
}

6. ref 的实现

核心源码

js/reactivity/ref.js

kotlin 复制代码
import {toReactive} from "./reactive.js";
import {activeEffect} from "./effect.js";
import {trackEffects, triggerEffects} from "./handler.js";

export function ref(value) {
    return new RefImpl(value)
}

class RefImpl {
    // 内部采用类的属性访问器 -》 Object.defineProperty
    constructor(rawValue) {
        this.rawValue = rawValue
        this._value = toReactive(rawValue)
        this.dep = new Set()
    }

    get value() {
        if (activeEffect) {
            trackEffects(this.dep)
        }
        return this._value
    }

    set value(newValue) {
        if (newValue !== this.rawValue) {
            this.rawValue = newValue
            this._value = toReactive(newValue)
            triggerEffects(this.dep)
        }

    }
}

// ref 代理的实现
class ObjectRefImpl {
    constructor(object, key) {
        this.object = object
        this.key = key
    }

    get value() {
        return this.object[this.key]
    }

    set value(newValue) {
        this.object[this.key] = newValue
    }
}

export function toRef(object, key) {
    return new ObjectRefImpl(object, key)
}

export function toRefs(object) {
    const ret = {}
    for (const key in object) {
        ret[key] = toRef(object, key)
    }
    return ret
}
相关推荐
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60619 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅10 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment10 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端
爱敲代码的小鱼10 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax