从零实现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
}
相关推荐
一只欢喜18 分钟前
uniapp使用uview2上传图片功能
前端·uni-app
程序员大金28 分钟前
基于SpringBoot+Vue+MySQL的养老院管理系统
java·vue.js·spring boot·vscode·后端·mysql·vim
尸僵打怪兽31 分钟前
后台数据管理系统 - 项目架构设计-Vue3+axios+Element-plus(0920)
前端·javascript·vue.js·elementui·axios·博客·后台管理系统
customer0839 分钟前
【开源免费】基于SpringBoot+Vue.JS网上购物商城(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
ggome39 分钟前
Uniapp低版本的安卓不能用解决办法
前端·javascript·uni-app
Ylucius1 小时前
JavaScript 与 Java 的继承有何区别?-----原型继承,单继承有何联系?
java·开发语言·前端·javascript·后端·学习
前端初见1 小时前
双token无感刷新
前端·javascript
、昔年1 小时前
前端univer创建、编辑excel
前端·excel·univer
emmm4591 小时前
前端中常见的三种存储方式Cookie、localStorage 和 sessionStorage。
前端
Q186000000001 小时前
在HTML中添加视频
前端·html·音视频