likes
comments
collection
share

vue3——深入了解ref()

作者站长头像
站长
· 阅读数 16

前言:自从入门vue3之后有一段时间之后能够比较熟练使用基本的api。然后就开始打算调试简单的api的源码,以此来加深对api的理解。

调试版本为3.2.45

ref()接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value

<script setup>
import {ref} from 'vue'

const num = ref(0)//定义基本的数据类型
const numRef = ref(num)//定义已经是ref类型
const obj = ref({name:'zhangsan',age:12})//定义复杂数据类型
console.log("num", num);//用来快速定位需要调试的文件
</script>

设置断点

我们启动项目后,需要先找到需要调试的文件,然后设置断点。

vue3——深入了解ref()

vue3——深入了解ref()

开始调试

基础数据类型

接下来我们可以开始调试了,设置好断点后,只要重新刷新页面就可以进入调试界面。

想要详细了解控制台各个功能请点击此处

vue3——深入了解ref()

1.最开始的入口,会直接返回一个创建ref的函数,会携带两个参数,一个是所定义的值;一个是false,用于后面判断是否需要赋予ref各个相关的属性

function ref(value) {
    return createRef(value, false);
}

2.通过isRef函数来判断所定义的值是否已经是一个ref对象,如果是就直接返回该值。否则实例化RefImpl类,给该值赋予各个关于ref的属性,将其转化为一个ref对象

function createRef(rawValue, shallow) {
    if (isRef(rawValue)) {
        return rawValue;
    }
    return new RefImpl(rawValue, shallow);
}
function isRef(r) {
/*判断该值的__v_isRef属性是否为true,因为前面r=0,并没有所谓__v_isRef属性,所以这里r.__v_isRef是为undefined。这里的返回值是为false
*/
    return !!(r && r.__v_isRef === true);
}

3.RefImpl的构造器接收两个值,一个value所定义的原始值,一个__v_isShallow是否为浅层响应式

class RefImpl {
    constructor(value, __v_isShallow) {
    //是否为浅层响应式
        this.__v_isShallow = __v_isShallow;
    //dep 是一个 Set 类型的数据,它的作用是用来存储当前的 ref 值收集的依赖
        this.dep = undefined;
    //前面说到这个属性用来判断是否转化为ref对象
        this.__v_isRef = true;
    //用于保存当前 ref 值对应的原始值,当传递的参数是基本数据类型时,_value 和 _rawValue 相同。如果传递的参数是一个对象的话,_rawValue 用于保存转化前的原始值。
        this._rawValue = __v_isShallow ? value : toRaw(value);
    //用于保存 ref 当前的值,如果传递的参数是基本数据类型的话,_value 和 _rawValue 相同。如果传递的参数是一个对象的话,_value 就是经过 reactive 函数转化过的值。
        this._value = __v_isShallow ? value : toReactive(value);
    }
    //当我们使用 .value 读取属性的时候,会执行 get value(){} 函数,并将函数的返回值作为 .value 读取到的值
    get value() {
        trackRefValue(this);
        return this._value;
    }
    //在 set value() {} 函数中,判断新值和旧值有没有发生改变,如果有变化的是,则重新赋值 _rawValue 和 _value 属性,并触发当前 ref 值的依赖重新执行。
    set value(newVal) {
    //判断原始值是否为浅层响应式或是否为readonly对象
        const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal);
    //如果该值不是响应式或readonly对象,则返回原来的值,否则将该值通过toRaw转化为原来的target对象
        newVal = useDirectValue ? newVal : toRaw(newVal);
        //检测两个值是否相等,如果不同了就赋值为新的
        if (hasChanged(newVal, this._rawValue)) {
            this._rawValue = newVal;
        //原始值如果为响应式则直接返回,否则将其转化为响应式
            this._value = useDirectValue ? newVal : toReactive(newVal);
        //触发依赖,更新视图
            triggerRefValue(this, newVal);
        }
    }
}

关于get value(){}的有关函数

function trackRefValue(ref) {
    // 判断是否需要收集依赖
    // shouldTrack 全局变量,代表当前是否需要 track 收集依赖
    // activeEffect 全局变量,代表当前的副作用对象 ReactiveEffect
    if (shouldTrack && activeEffect) {
        ref = toRaw(ref);
    // 如果没有 dep 属性,则初始化 dep,dep 是一个 Set<ReactiveEffect>,存储副作用函数
    // trackEffects 收集依赖
        if ((process.env.NODE_ENV !== 'production')) {
            trackEffects(ref.dep || (ref.dep = createDep()), {
                target: ref,
                type: "get" /* TrackOpTypes.GET */,
                key: 'value'
            });
        }
        else {
            trackEffects(ref.dep || (ref.dep = createDep()));
        }
    }
}

关于set value(){}的有关函数,这里涉及很多函数调用,篇幅有点长,笔者就不一一列出来了,有兴趣的读者可以自己改变两个值,一个基本数据类型,一个复杂类型来作为断点进行调试。

vue3——深入了解ref()

function isShallow(value) {
    return !!(value && value["__v_isShallow" /* ReactiveFlags.IS_SHALLOW */]);//判断原始值是否为浅层响应式
}
function isReadonly(value) {
    return !!(value && value["__v_isReadonly" /* ReactiveFlags.IS_READONLY */]);//判断原始值是否为只读
}

4.经过RefImpl类的实例化后,我们所定义的原始值赋值许多属性

vue3——深入了解ref()

ref类型

这次我们调试的是已经定义过的ref对象

const num = ref(0)
const numRef = ref(num)
console.log("numRef", numRef);

vue3——深入了解ref()

这里是在isRef函数判断开始不同

vue3——深入了解ref()

从调试的截图可以看出,这里isRef()返回是true,那么就会直接返回原始值。

复杂数据类型

const objRef = ref({name:'张三',age:18})
console.log('objRef',objRef)

vue3——深入了解ref()

这里调试所运行的函数跟基础类型前三步一样,当实例RefImpl类的时候,执行下面这条语句的时候

this._value = __v_isShallow ? value : toReactive(value);
//从这一条语句开始不同
/*这条语句主要判断原始值是否为基础数据类型复杂数据类型。基础数据类型会直接返回原值。复杂数据类型会走reactive函数进行proxy代理*/
const toReactive = (value) => isObject(value) ? reactive(value) : value;

//判断是否原始值是否为object类型
const isObject = (val) => val !== null && typeof val === 'object';
function reactive(target) {
    //如果该对象是只读的,就直接返回
    // if trying to observe a readonly proxy, return the readonly version.
    if (isReadonly(target)) {
        return target;
    }
    return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
}

createReactiveObject函数调用

/*createReactiveObject接收五个参数:
target被代理的对象,
isReadonl是不是只读的,
baseHandlers proxy的捕获器,
collectionHandlers针对集合的proxy捕获器,
proxyMap一个用于缓存proxy的`WeakMap`对象*/
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
//如果target不是对象则提示并返回
    if (!isObject(target)) {
        if ((process.env.NODE_ENV !== 'production')) {
            console.warn(`value cannot be made reactive: ${String(target)}`);
        }
        return target;
    }
    // 如果target已经是proxy是代理对象则直接返回.
    if (target["__v_raw" /* ReactiveFlags.RAW */] &&
        !(isReadonly && target["__v_isReactive" /* ReactiveFlags.IS_REACTIVE */])) {
        return target;
    }
    // 从proxyMap中获取缓存的proxy对象,如果存在的话,直接返回proxyMap中对应的proxy。否则创建proxy。
    const existingProxy = proxyMap.get(target);
    if (existingProxy) {
        return existingProxy;
    }
    // 并不是任何对象都可以被proxy所代理。这里会通过getTargetType方法来进行判断。
    const targetType = getTargetType(target);
    //当类型值判断出是不能代理的类型则直接返回
    if (targetType === 0 /* TargetType.INVALID */) {
        return target;
    }
    //进行target代理操作
    const proxy = new Proxy(target, targetType === 2 /* TargetType.COLLECTION */ ? collectionHandlers : baseHandlers);
    proxyMap.set(target, proxy);
    return proxy;
}

getTargetType方法调用流程

//1.进入判断如果value有__v_skip属性且为true或对象是可拓展则返回0,否则走类型判断函数
function getTargetType(value) {
//Object.isExtensible() 方法判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)。
    return value["__v_skip" /* ReactiveFlags.SKIP */] || !Object.isExtensible(value)
        ? 0 /* TargetType.INVALID */
        : targetTypeMap(toRawType(value));
}
//2.这里通过Object.prototype.toString.call(obj)来判断数据类型
const toRawType = (value) => {
    // extract "RawType" from strings like "[object RawType]"
    return toTypeString(value).slice(8, -1);
};
const toTypeString = (value) => objectToString.call(value);
//3.这里rawType是为'Object'所以会返回1
function targetTypeMap(rawType) {
    switch (rawType) {
        case 'Object':
        case 'Array':
            return 1 /* TargetType.COMMON */;
        case 'Map':
        case 'Set':
        case 'WeakMap':
        case 'WeakSet':
            return 2 /* TargetType.COLLECTION */;
        default:
            return 0 /* TargetType.INVALID */;//返回0说明除前面的类型外其他都不能被代理,如Date,RegExp,Promise等
    }
}

createReactiveObject方法中const proxy = new Proxy(target, targetType === 2 /* TargetType.COLLECTION */ ? collectionHandlers : baseHandlers);这一条语句中,第二个参数判断target是否为Map或者Set类型。从而使用不同的handler来进行依赖收集。

在调试的文件node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js中,我们从reactive函数的createReactiveObject函数调用的其中两个参数mutableHandlersmutableCollectionHandlers开始往上查询

这里就讲解mutableHandlers的实现,一般来说我们很少用到Map和Set类型数据(其实是mutableCollectionHandlers复杂多了,调试好久,查阅了许多资料也没怎么搞懂o(╥﹏╥)o)

createGetter函数解析

const mutableHandlers = {
    get,// 获取值的拦截
    set,// 更新值的拦截
    deleteProperty,// 删除拦截
    has,// 绑定访问对象时会拦截
    ownKeys// 获取属性key列表
};

function deleteProperty(target, key) {
    // key是否是target自身的属性
    const hadKey = hasOwn(target, key);
    // 旧值
    const oldValue = target[key];
    // 调用Reflect.deleteProperty从target上删除属性
    const result = Reflect.deleteProperty(target, key);
    // 如果删除成功并且target自身有key,则触发依赖
    if (result && hadKey) {
        trigger(target, "delete" /* TriggerOpTypes.DELETE */, key, undefined, oldValue);
    }
    return result;
}
//
function has(target, key) {
    //检查目标对象是否存在此属性。
    const result = Reflect.has(target, key);
    // key不是symbol类型或不是symbol的内置属性,进行依赖收集
    if (!isSymbol(key) || !builtInSymbols.has(key)) {
        track(target, "has" /* TrackOpTypes.HAS */, key);
    }
    return result;
}
/*ownKeys可以拦截以下操作:
1.Object.keys()
2.Object.getOwnPropertyNames()
3.Object.getOwnPropertySymbols()
4.Reflect.ownKeys()操作*/
function ownKeys(target) {
    track(target, "iterate" /* TrackOpTypes.ITERATE */, isArray(target) ? 'length' : ITERATE_KEY);
    return Reflect.ownKeys(target);
}

写到这对于ref的内部运行了解起码有了基本的认知。后续应该会出关于其他比较简单的api调试过程,比如reactive。等水平上去后,准备调试理解依赖的收集以及触发加上虚拟DOM的算法。

总结:这次调试ref源码就到了。虽不完整,但是磕磕碰碰还是弄出大概了。希望这篇文章能对你有所帮助,如果可以请给个赞,谢谢大家(* ̄︶ ̄)。如果文章有写的不对的地方,请帮忙指出,促进笔者的成长(* ̄︶ ̄)

转载自:https://juejin.cn/post/7181673853502881848
评论
请登录