• 技术文章 >web前端 >Vue.js

    深入聊聊vue3中的reactive()

    青灯夜游青灯夜游2023-01-06 21:21:13原创138

    在vue3的开发中,reactive是提供实现响应式数据的方法。日常开发这个是使用频率很高的api。这篇文章笔者就来探索其内部运行机制。小白一枚,写得不好请多多见谅。

    调试版本为3.2.45

    <script setup>
    import {reactive} from 'vue'
    const data = reactive({ //定义对象
      name:'测试',
      age:10
    })
    const num = reactive(1)//定义基本数据类型
    console.log(data)//便于定位到调试位置
    </script>
    <template>
    <div>
    <h1>{{ data.name }}</h1>
    </div>
    </template>
    
    <style scoped></style>

    设置断点

    image.png

    image.png

    开始调试

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

    复杂数据类型

    我们先调试简单的基本数据类型

    image.png1.

    /*1.初始进来函数,判断目标对象target是否为只读对象,如果是直接返回*/
    function reactive(target) {
        // if trying to observe a readonly proxy, return the readonly version.
        if (isReadonly(target)) {
            return target;
        }
        //创建一个reactive对象,五个参数后续会讲解
        return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
    }
    
    
    /*2.判断是来判断target是否为只读。*/
    function isReadonly(value) {
        return !!(value && value["__v_isReadonly" /* ReactiveFlags.IS_READONLY */]);
    }
    
    /*3.创建一个reactive对象*/
    /*createReactiveObject接收五个参数:
    target被代理的对象,
    isReadonl是不是只读的,
    baseHandlers proxy的捕获器,
    collectionHandlers针对集合的proxy捕获器,
    proxyMap一个用于缓存proxy的`WeakMap`对象*/
    function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
    //如果target不是对象则提示并返回
    /*这里会跳转到如下方法
    判断是否原始值是否为object类型 
    const isObject = (val) => val !== null && typeof val === 'object';
    */
        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;
        }
        //通过使用Proxy函数劫持target对象,返回的结果即为响应式对象了。这里的处理函数会根据target对象不同而不同(这两个函数都是参数传入的):
        //Object或者Array的处理函数是collectionHandlers;
        //Map,Set,WeakMap,WeakSet的处理函数是baseHandlers;
        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的实现

    const mutableHandlers = {
        get,// 获取值的拦截,访问对象时会触发
        set,// 更新值的拦截,设置对象属性会触发
        deleteProperty,// 删除拦截,删除对象属性会触发
        has,// 绑定访问对象时会拦截,in操作符会触发
        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);
    }
    get方法实现
    const get = /*#__PURE__*/ createGetter();
    /*传递两个参数默认都为false
    isReadonly是否为只读
    shallow是否转换为浅层响应,即Reactive---> shallowReactive,shallowReactive监听了第一层属性的值,一旦发生改变,则更新视图;其他层,虽然值发生了改变,但是视图不会进行更新
    */
    function createGetter(isReadonly = false, shallow = false) {
        return function get(target, key, receiver) {
        //1.是否已被reactive相关api处理过;
            if (key === "__v_isReactive" /* ReactiveFlags.IS_REACTIVE */) {
                return !isReadonly;
            }
         //2.是否被readonly相关api处理过
            else if (key === "__v_isReadonly" /* ReactiveFlags.IS_READONLY */) {
                return isReadonly;
            }
            else if (key === "__v_isShallow" /* ReactiveFlags.IS_SHALLOW */) {
                return shallow;
            }
          //3.检测__v_raw属性
            else if (key === "__v_raw" /* ReactiveFlags.RAW */ &&
                receiver ===
                    (isReadonly
                        ? shallow
                            ? shallowReadonlyMap
                            : readonlyMap
                        : shallow
                            ? shallowReactiveMap
                            : reactiveMap).get(target)) {
                return target;
            }
          //4.如果target是数组,且命中了一些属性,则执行函数方法
            const targetIsArray = isArray(target);
            if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
                return Reflect.get(arrayInstrumentations, key, receiver);
            }
          //5.Reflect获取值
            const res = Reflect.get(target, key, receiver);
          //6.判断是否为特殊的属性值
            if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
                return res;
            }
            if (!isReadonly) {
                track(target, "get" /* TrackOpTypes.GET */, key);
            }
            if (shallow) {
                return res;
            }
          //7.判断是否为ref对象
            if (isRef(res)) {
                // ref unwrapping - skip unwrap for Array + integer key.
                return targetIsArray && isIntegerKey(key) ? res : res.value;
            }
          //8.判断是否为对象
            if (isObject(res)) {
                // Convert returned value into a proxy as well. we do the isObject check
                // here to avoid invalid value warning. Also need to lazy access readonly
                // and reactive here to avoid circular dependency.
                return isReadonly ? readonly(res) : reactive(res);
            }
            return res;
        };
    }
    set方法实现

    例子:data.name='2'

    const set = /*#__PURE__*/ createSetter();
      //shallow是否转换为浅层响应,默认为false
    function createSetter(shallow = false) {
        //1.传递四个参数
        return function set(target, key, value, receiver) {
            let oldValue = target[key];
            //首先获取旧值,如果旧值是ref类型,且新值不是ref类型,则不允许修改
            if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
                return false;
            }
         //2.根据传递的shallow参数,来执行之后的操作
            if (!shallow) {
                if (!isShallow(value) && !isReadonly(value)) {
                    oldValue = toRaw(oldValue);
                    value = toRaw(value);
                }
                if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
                    oldValue.value = value;
                    return true;
                }
            }
          //3.检测key是不是target本身的属性
            const hadKey = isArray(target) && isIntegerKey(key)
                ? Number(key) < target.length
                : hasOwn(target, key);
            //利用Reflect.set()来修改值,返回一个Boolean值表明是否成功设置属性
            //Reflect.set(设置属性的目标对象, 设置的属性的名称, 设置的值, 如果遇到 `setter`,`receiver`则为`setter`调用时的`this`值)
            const result = Reflect.set(target, key, value, receiver);
            // 如果目标是原始原型链中的某个元素,则不要触发
            if (target === toRaw(receiver)) {
            //如果不是target本身的属性那么说明执行的是'add'操作,增加属性
                if (!hadKey) {
                    trigger(target, "add" /* TriggerOpTypes.ADD */, key, value);
                }
          //4.比较新旧值,是否触发依赖
                else if (hasChanged(value, oldValue)) {
          //5.触发依赖
                    trigger(target, "set" /* TriggerOpTypes.SET */, key, value, oldValue);
                }
            }
            return result;
        };
    }

    1、以data.name='2'这段代码为例,四个参数分别为:

    target:目标对象,即target={"name": "测试","age": 10}(此处为普通对象)

    key:修改的对应key,即key: "name"

    value:修改的值,即value: "2"

    receiver:目标对象的代理。即receiver=Proxy {"name": "测试","age": 10}

    2、shallow为false的时候。

    第一个判断:如果新值不是浅层响应式并且不是readonly,新旧值取其对应的原始值。

    第二个判断:如果target不是数组并且旧值是ref类型,新值不是ref类型,直接修改oldValue.value为value

    3.检测key是不是target本身的属性。这里的hadKey有两个方法,isArray就不解释,就是判断是否为数组

    isIntegerKey:判断是不是数字型的字符串key值

    //判断参数是否为string类型,是则返回true
    const isString = (val) => typeof val === 'string';
    //如果参数是string类型并且不是'NaN',且排除-值(排除负数),然后将 key 转换成数字再隐式转换为字符串,与原 key 对比
    const isIntegerKey = (key) => isString(key) &&
        key !== 'NaN' &&
        key[0] !== '-' &&
        '' + parseInt(key, 10) === key;

    4.比较新旧值,如果新旧值不同,则触发依赖进行更新

    hasChanged方法

    //Object.is()方法判断两个值是否是相同的值。
    const hasChanged = (value, oldValue) => !Object.is(value, oldValue);

    5.触发依赖,这里太过复杂,笔者也没搞懂,如果有兴趣的读者可自行去调试

    <script setup>
    import { reactive } from "vue";
    const data = reactive({
      name: "测试",
      age: 10,
    });
    
    data.name='1'//这里并未收集依赖,在处理完 createSetupContext 的上下文后,组件会停止依赖收集,并且开始执行 setup 函数。具体原因有兴趣的读者可以自行去了解
    const testClick = ()=>{
      data.name='test'
    }
    </script>
    <template>
      <div>
        <h1>{{ data.name }}</h1>
        <el-button @click="testClick">Click</el-button>
      </div>
    </template>
    
    <style scoped></style>

    基本数据类型

    const num = reactive(2)

    这里比较简单,在createReactiveObject函数方法里面:

    if (!isObject(target)) {
            if ((process.env.NODE_ENV !== 'production')) {
                console.warn(`value cannot be made reactive: ${String(target)}`);
            }
            return target;
        }

    image.png

    因为判断类型不是对象,所以会在控制台打印出警告,并且直接返回原数据

    proxy对象

    <script>
    const data = reactive({
      name: "测试",
      age: 10,
    });
    const num = reactive(data)//定义一个已经是响应式对象
    </script>

    1.调试开始进来reactive函数,然后会经过isReadonly函数,这里跟前面不同的是,target是一个proxy对象,它已经被代理过有set,get等handler。所以在isReadonly函数读取target的时候,target会进行get函数的读取操作。

    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);
    }

    2.可以看到get传入的参数有个key="__v_isReadonly",这里的isReadonly返回是false,接下来进入createReactiveObject函数

    这里说明下,在本次调试中常见的vue里面定义的私有属性有:

    image.png

    3.在createReactiveObject函数中,经过target["__v_isReactive"]的时候会触发target的get函数,这时候get函数传入的参数中key='__v_raw'

        if (target["__v_raw" /* ReactiveFlags.RAW */] &&
            !(isReadonly && target["__v_isReactive" /* ReactiveFlags.IS_REACTIVE */])) {
                    return target;
        }

    image.png

    由上图可知我们检测target即已定义过的proxy对象,被reactiveapi处理过就会有__v_raw私有属性,然后再进行receiver的判断,判断target是否为只读或浅层响应。如果都不是则从缓存proxy的WeakMap对象中获取该元素。最后直接返回target的原始数据(未被proxy代理过)。

    最后回到之前的判断,由下图可知,target__v_raw属性存在,isReadonly为false,__v_isReactive的值为true,可以说明reactive函数需要处理的对象是一个被reactiveAPI处理过的对象,然后直接返回该对象的原始数据。

    image.png

    ref类型

    经过ref函数处理,其本质也是一个对象,所以使用reactive函数处理ref类型就跟处理复杂数据类型一样过程。对于ref函数,如果大家有兴趣可以阅读这篇文章vue3——深入了解ref()。有些内容跟这里差不多,也有对此补充,如果觉得不错请各位帮忙点个赞

    (开发中应该不会有这种嵌套行为吧,这里只是为了测试多样化)。

    <script setup>
    import { reactive,ref } from "vue";
    const data = reactive({
      name: "测试",
      age: 10,
    });
    const numRef = ref(1)
    const dataRef = ref({
      name: "测试2",
      age: 20,
    })
    const num = reactive(numRef)
    const dataReactive = reactive(dataRef)
    console.log('data',data)
    console.log('numRef',numRef)
    console.log('num',num)
    console.log('dataRef',dataRef)
    console.log('dataReactive',dataReactive)
    </script>

    image.png

    Map类型和Set类型

    <script setup>
    import { reactive } from "vue";
    const mapData = new Map();
    mapData.set('name','张三')
    const setData = new Set([1,2,3,1,1])
    console.log(mapData)
    console.log(setData)
    const mapReactive = reactive(mapData)
    console.log(mapReactive)
    </script>

    image.png

    由上图可知Map结构和Set结构使用typeof判断是object,所有流程前面会跟复杂数据类型一样,知道在createReactiveObject函数的getTargetType()函数开始不同。

    getTargetType函数里面toRawType()判断数据类型所用方法为Object.prototype.toString.call()

    const targetType = getTargetType(target);
    
    function getTargetType(value) {
        return value["__v_skip" /* ReactiveFlags.SKIP */] || !Object.isExtensible(value)
            ? 0 /* TargetType.INVALID */
            : targetTypeMap(toRawType(value));
    }
    
    function targetTypeMap(rawType) {//rawType="Map",这里返回值为2
        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 */;
        }
    }

    这时候targetType=2,在createReactiveObject的函数中const proxy = new Proxy(target, targetType === 2 /* TargetType.COLLECTION */ ? collectionHandlers : baseHandlers);的三元表达式中可得知,这里的handlercollectionHandlers

    网上查找可在reactive函数中return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);这条语句找到,当rawType=1handler是用mutableHandlers,rawType=1时是用mutableCollectionHandlers

    mutableCollectionHandlers方法:

    const mutableCollectionHandlers = {
        get: /*#__PURE__*/ createInstrumentationGetter(false, false)
    };
    
    //解构createInstrumentations
    const [mutableInstrumentations, readonlyInstrumentations, shallowInstrumentations, shallowReadonlyInstrumentations] = /* #__PURE__*/ createInstrumentations();
    //传入两个参数,是否为可读,是否为浅层响应
    function createInstrumentationGetter(isReadonly, shallow) {
        const instrumentations = shallow
            ? isReadonly
                ? shallowReadonlyInstrumentations
                : shallowInstrumentations
            : isReadonly
                ? readonlyInstrumentations
                : mutableInstrumentations;
        return (target, key, receiver) => {
            if (key === "__v_isReactive" /* ReactiveFlags.IS_REACTIVE */) {
                return !isReadonly;
            }
            else if (key === "__v_isReadonly" /* ReactiveFlags.IS_READONLY */) {
                return isReadonly;
            }
            else if (key === "__v_raw" /* ReactiveFlags.RAW */) {
                return target;
            }
            return Reflect.get(hasOwn(instrumentations, key) && key in target
                ? instrumentations
                : target, key, receiver);
        };
    }
    //篇幅问题以及这方面笔者并未深入,所以就大概带过
    function createInstrumentations() {
    //创建了四个对象,对象内部有很多方法,其他去掉了,完整可自行去调试查看
        const mutableInstrumentations = {
            get(key) {
                return get$1(this, key);
            },
            get size() {
                return size(this);
            },
            has: has$1,
            add,
            set: set$1,
            delete: deleteEntry,
            clear,
            forEach: createForEach(false, false)
        };
        
        .................
        
        //通过createIterableMethod方法操作keys、values、entries、Symbol.iterator迭代器方法
        const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator];
        iteratorMethods.forEach(method => {
            mutableInstrumentations[method] = createIterableMethod(method, false, false);
            readonlyInstrumentations[method] = createIterableMethod(method, true, false);
            shallowInstrumentations[method] = createIterableMethod(method, false, true);
            shallowReadonlyInstrumentations[method] = createIterableMethod(method, true, true);
        });
        return [
            mutableInstrumentations,
            readonlyInstrumentations,
            shallowInstrumentations,
            shallowReadonlyInstrumentations
        ];
    }

    后续比较复杂,加上笔者技术力还不够,如果想继续深入的读者,可以阅读这篇文章:Vue3响应式原理

    总结:关于reactive的源码调试就到这了,这只是其中一小部分的源码,希望有兴趣的读者可以以此深入,输出文章,共同进步成长。最后,如果这篇文章对你有所收获,请点个赞,如果有写的不对的地方,请大佬们指出(* ̄︶ ̄)。

    (学习视频分享:vuejs入门教程编程基础视频

    以上就是深入聊聊vue3中的reactive()的详细内容,更多请关注php中文网其它相关文章!

    声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn核实处理。
    专题推荐:前端 Vue.js JavaScript
    上一篇:一文详解vue怎么实现v-model(附代码示例) 下一篇:自己动手写 PHP MVC 框架(40节精讲/巨细/新人进阶必看)

    相关文章推荐

    • vue能显示源码吗• vue可以操作本地文件吗• vue怎么实现转换id• vue工程编译sass错误怎么办• 一文带你详解Vue中的组件化编程
    1/1

    PHP中文网