掘友和我说:尤雨溪推荐ref

浅聊一下

在我写的到底该用ref还是reactive???这篇文章的评论区,有掘友说尤雨溪推荐ref,在上一篇文章中,我们实现了reactive的手写 (对不起reactive,这次一定好好手写你) ,那么本篇文章我们将来完成ref的手写,并且通过源码来分析尤雨溪推荐ref的理由...

开始

在之前的文章中,我们提到了一个数据经过ref代理以后变成了一个RefImpl对象(到底该用ref还是reactive??? - 掘金)

今天我们就从这里开始手写一个ref,其实步骤也分三步

  1. 创建RefImpl对象代理
  2. 对使用的属性进行副作用函数收集
  3. 对收集的副作用函数进行触发

创建RefImpl对象代理

与reactive一样,本着一个函数完成一件事情的原则,将函数拆分

js 复制代码
export function ref(val) {//将原始类型的数据变成响应式
    return createRef(val)
}

function createRef(val) {
    return new RefImpl(val)
}

在createRef中,与reactive一样,要判断这个val是否已经代理过,那么如何才能知道是否已经代理过呢?那就要来看RefImpl类了,我们可以在RefImpl类中定义一个属性,如果被代理,就让他为true

js 复制代码
class RefImpl {
    constructor(val) {
        this.__v__isRef = true//给每一个被ref操作过的属性值都添加标记
        this._value = val
    }
    get value() {
        return this._value
    }
}

这里的value函数是什么呢?我们要拿到ref对象的值,要通过.value访问,但是.value其实是一个函数, 在这里的get就是让我们不用手动调用函数,而是直接访问

js 复制代码
class a {
    value(){
        console.log("123465")
    }
}

如果是这种形式,则要调用 value 需要 通过 XX.value()来调用

js 复制代码
class a {
    get value(){
        console.log("123465")
    }
}

这种形式只要 XX.value就可以调用了

接下来该做什么呢?还记得我们的Ref可以代理基本数据类型和引用数据类型吗?不过如果ref代理的是基本数据类型,那么返回值是一个RefImpl对象,如果代理的是引用数据类型,那么返回的就是一个Proxy代理对象...那么我们先进行类型判断

js 复制代码
function conver(val){
    if(typeof val !== 'object'||val === null){
        return val
    }else{
        return reactive(val)
    }
}

这样就满足我们的要求了,基本数据类型返回RefImpl对象,引用类型返回Proxy代理对象,把conver拿上去用,并且在createRef中判断是否代理过

js 复制代码
function createRef(val) {
    //判断val是否已经是响应式
    //将val变为响应式
    if(val.__v__isRef){
        return val
    }
    return new RefImpl(val)
}
class RefImpl {
    constructor(val) {
        this.__v__isRef = true//给每一个被ref操作过的属性值都添加标记
        this._value = conver(val)
    }
    get value() {
        //为this对象做依赖收集
        return this._value
    }
}

这里只有一个get,写完reactive我们知道在修改属性的时候还有一个set方法

js 复制代码
class RefImpl {
    constructor(val) {
        this.__v__isRef = true//给每一个被ref操作过的属性值都添加标记
        this._value = conver(val)
    }
    get value() {
        //为this对象做依赖收集
        track(this, 'value')
        return this._value
    }
    set value(newVal) {
        if (newVal !== this._value) {
            this._value = conver(newVal)
        }
    }

}

在set方法中,同样的要使用conver判断是否传入的是对象

对使用的属性进行副作用函数收集

副作用函数的收集在上一篇文章中已经写过了,这里就不过多赘述,直接上代码,不知道的掘友去看看(对不起reactive,这次一定好好手写你 - 掘金 (juejin.cn))

js 复制代码
const targetMap = new WeakMap();
let activeEffect = null;//一个副作用函数
export function track(target,key){
    // targetMap = {
    //     target:{
    //         key:[Effect1,Effect2]
    //     }
    // }

    let depsMap = targetMap.get(target);
    if (!depsMap){//初次读取到值 收集effect
        targetMap.set(target,depsMap = new Map())
    }
    let deps = depsMap.get(key);

    if (!deps){//该属性还未添加过副作用
        depsMap.set(key,deps = new Set());
    }

    if(!deps.has(activeEffect) && activeEffect){
        //存入一个effect函数
        deps.add(activeEffect);
    }
    depsMap.set(key,deps);
}

与reactive相同的是,在get的时候调用这个trank函数收集副作用函数

js 复制代码
    get value() {
        //为this对象做依赖收集
        track(this, 'value')
        return this._value
    }

由于trank要传入两个参数,而ref处理的基本数据类型的数据并没有与之对应的key,所以我们这里约定俗成放一个'value'充当key

对收集的副作用函数进行触发

触发过程也是与reactive一样的,在set里触发

js 复制代码
export function trigger(target,key){
    const depsMap = targetMap.get(target);
    if(!depsMap){//当前对象中所有的key都没有副作用函数(从来没有使用过)
        return
    }
    const deps = depsMap.get(key);
    if(!deps){
        return
    }
    deps.forEach(effectFn => {
        if(effectFn.scheduler){
            effectFn.scheduler();
        }else{
            effectFn();
        }
    });
}
js 复制代码
    set value(newVal) {
        if (newVal !== this._value) {
            this._value = conver(newVal)
            trigger(this, 'value')//触发'value'上的副作用函数
        }
    }

最后不要忘了我们的effect副作用函数

js 复制代码
export function effect(fn,options = {}){ //watch 和 computed 的核心逻辑

    const effectFn = ()=>{
        try{
            activeEffect = effectFn
            return fn()
        }finally{
            activeEffect = null
        }
    }
    if(!options.lazy){
        effectFn();
    }
    effectFn.scheduler = options.scheduler;
    return effectFn;
}

效果

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script type="module">
        import {ref} from './ref.js'
        import {effect} from './effect.js'
        const age = ref(18)
        age.value = 19
        effect(() => {
            console.log(age.value);
        })
        setInterval(() => {
            age.value = age.value + 1
        }, 1000)
    </script>
</body>
</html>

总结一下

ref 和 reactive 的区别

reactive使用Proxy代理了各种操作行为,在读取值,修改值的时候分别进行收集副作用函数和触发副作用函数的操作来实现响应效果

ref处理对象时,依然是使用Proxy代理,而在处理基本数据类型的时候,是给原始值添加value()函数和原生JS的set和get来实现为属性添加副作用函数和触发副作用函数的效果,来实现响应式

为什么尤雨溪推荐ref?

ref既可以处理简单数据类型,又可以处理复杂数据类型,而reactive只能处理引用数据类型的数据,存在局限性,还有一个我们这里没有聊到的东西,就是reactive使用不当会失去响应,有时间可以来聊一聊...

结尾

终于,写完了ref和reactive...

相关推荐
向前看-22 分钟前
验证码机制
前端·后端
燃先生._.1 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖2 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235242 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240253 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar3 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人4 小时前
前端知识补充—CSS
前端·css
GISer_Jing4 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试
m0_748245524 小时前
吉利前端、AI面试
前端·面试·职场和发展