从零实现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
}
相关推荐
定栓3 分钟前
Webpack迁到Vite第二节:JSX 到 Render 函数
vue.js·webpack·vite
小羊没烦恼!19 分钟前
T-SQL查询高级—SQL Server索引中的碎片和填充因子
开发语言·前端·javascript·数据库·后端·python·web安全
Liu.77438 分钟前
Vue3 中如何使用 GET 请求传递复杂参数
vue.js·vue3
定栓1 小时前
Webpack迁到Vite第一节:模块系统迁移:require → import
运维·前端·webpack
小熊在忙fpga1 小时前
N32G430芯片的flash表示
前端·javascript·html
Data analyse4561 小时前
App埋点版本兼容的字段迁移校验
大数据·运维·服务器·前端·数据库·数据分析
NC 王炸1 小时前
AI 做的便签,换个浏览器就没了?加一个导出按钮,把数据带走
javascript·ai编程·数据备份·localstorage·txt导出
todoitbo1 小时前
本来只想给内网开个口子,Natapp 却多给了一道防护
前端·javascript·vue.js·内网穿透·网络防护·natapp
会灭火的程序员1 小时前
SuperMap AgentX Server智能体接入WEB应用技术文档
前端·supermap·agentx·agentx server
CRMEB1 小时前
商品 SKU 设计,从简单规格到多规格踩过什么
java·前端·数据库·开源