掘友和我说:尤雨溪推荐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...

相关推荐
Lei_zhen961 分钟前
记录一次electron-builder报错ENOENT: no such file or directory, rename xxxx的问题
前端·javascript·electron
咖喱鱼蛋4 分钟前
Electron一些概念理解
前端·javascript·electron
yqcoder5 分钟前
Vue3 + Vite + Electron + TS 项目构建
前端·javascript·vue.js
长风清留扬12 分钟前
一篇文章了解何为 “大数据治理“ 理论与实践
大数据·数据库·面试·数据治理
鑫宝Code22 分钟前
【React】React Router:深入理解前端路由的工作原理
前端·react.js·前端框架
Mr_Xuhhh2 小时前
重生之我在学环境变量
linux·运维·服务器·前端·chrome·算法
永乐春秋2 小时前
WEB攻防-通用漏洞&文件上传&js验证&mime&user.ini&语言特性
前端
鸽鸽程序猿2 小时前
【前端】CSS
前端·css
ggdpzhk2 小时前
VUE:基于MVVN的前端js框架
前端·javascript·vue.js
学不会•5 小时前
css数据不固定情况下,循环加不同背景颜色
前端·javascript·html