【源码&库】 Vue3 的依赖收集和依赖触发是如何工作的

简介: 【源码&库】 Vue3 的依赖收集和依赖触发是如何工作的

前几章完整的介绍了 Vue3 的响应式核心reactiveeffect的实现原理,这一章我们来看看Vue3的依赖收集和依赖触发是如何工作的。


根据之前的分析,我们知道依赖收集是在reactive中的get钩子中完成的(不是所有),而依赖触发是在set钩子中完成的(也不是所有),依赖指的是effect


这里的特点是get是在取值,set是在赋值,那么Vue3是如何正确的收集依赖和触发依赖的呢?接下来我们来一起看看。


依赖收集


在讲解之前的章节的时候,我们知道reactive中的get钩子中会调用track方法,我特意的避开了这一块的内容,讲的很浅,因为讲解这个方法的时候,我们需要先了解Vue3中的effect是如何工作的;


而现在已经讲过了effect的实现原理,我们来看看track方法是如何工作的,会很方便的理解Vue3中的依赖收集,我们还是来回一下get钩子的代码:

function createGetter(isReadonly = false, shallow = false) {
    return function get(target, key, receiver) {
        // 判断是否是数组
        const targetIsArray = isArray(target);
        // 对数组原型上的方法进行特别对待
        if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
            return Reflect.get(arrayInstrumentations, key, receiver);
        }
        // 获取结果
        const res = Reflect.get(target, key, receiver);
        // 收集依赖
        track(target, "get" /* TrackOpTypes.GET */, key);
        // 返回结果
        return res;
    };
}

上述代码解释来自:【源码&库】 Vue3 的依赖收集,这里的依赖指代的是什么?

如果想看详细的具体源码解析可以看这一章:【源码&库】Vue3 的响应式核心 reactive 和 effect 实现原理以及源码分析


可以看到get钩子中会在获取到值之后,调用track方法,其中会传入三个参数,我们来看看track方法的实现:

// 依赖映射表,用来存储对象的依赖
const targetMap = new WeakMap();
/**
 * 
 * @param target    目标对象,指向的是当前操作的对象
 * @param type      操作类型,有 get/has/iterate 三种,会面会讲到区别
 * @param key       操作的 key,指向的是当前操作对象的属性名
 */
function track(target, type, key) {
    // shouldTrack 和 activeEffect 在之前的章节中讲到过
    // shouldTrack 用来判断当前是否需要收集依赖
    // activeEffect 指向的是当前正在执行的 effect,收集依赖收集的就是它
    // 如果 shouldTrack 为 false 或者 activeEffect 为 null,说明不需要收集依赖
    if (shouldTrack && activeEffect) {
        // 获取当前对象的依赖映射表,如果没有则创建一个
        let depsMap = targetMap.get(target);
        if (!depsMap) {
            targetMap.set(target, (depsMap = new Map()));
        }
        // 获取当前对象的依赖集合,如果没有则创建一个
        let dep = depsMap.get(key);
        if (!dep) {
            depsMap.set(key, (dep = createDep()));
        }
        // 创建一个依赖收集的事件信息,只有在开发环境下才会用到
        const eventInfo = {
            effect: activeEffect,
            target,
            type,
            key
        }
        // 收集依赖
        trackEffects(dep, eventInfo);
    }
}

这里的依赖收集的逻辑看着比较简单,但是实际会涉及到很多的细节,我们来一一分析:


首先是shouldTrackactiveEffect,这两个变量在effect中都出现过,但是activeEffect是在effect中赋值的,shouldTrackeffect也有赋值操作,但是值并不是由effect直接控制的。


activeEffect在之前的文章中反复的提到过了,这里就不再赘述,我们来看看shouldTrack是如何工作的;


shouldTrack


这个变量是用来判断当前是否需要收集依赖的,全局搜索一下它的赋值操作,可以找到如下代码:

// 当前执行的 effect 是否需要收集依赖
let shouldTrack = true;
// 调用链栈,用来处理嵌套的 effect 调用情况
// 记录这个链上的 effect 是否需要收集依赖
const trackStack = [];
// 暂停依赖收集
function pauseTracking() {
    // 将当前的 shouldTrack 值压入栈中
    trackStack.push(shouldTrack);
    // 将 shouldTrack 设置为 false,表示暂停收集依赖
    shouldTrack = false;
}
// 恢复依赖收集,没有找打具体的用法,暂时不关心
function enableTracking() {
    trackStack.push(shouldTrack);
    shouldTrack = true;
}
// 重置依赖收集
function resetTracking() {
    // 弹出栈顶的 shouldTrack 值,表示当前的 shouldTrack 值已经失效,需要恢复上一个 shouldTrack 值
    const last = trackStack.pop();
    // 如果 last 为 undefined,说明栈中没有 shouldTrack 值,这时候将 shouldTrack 设置为 true
    shouldTrack = last === undefined ? true : last;
}

这一块具体的使用场景在上一章已经讲过了,是在数组的pushpopshiftunshiftsplice拦截器中使用的,可以去看:代理 Object | get 钩子


这里我们只需要知道,shouldTrack是用来判断当前是否需要收集依赖的,如果shouldTrackfalse,则不会收集依赖,如果shouldTracktrue,则会收集依赖。


依赖映射表 targetMap


首先我们来看看targetMap,这个变量是用来存储对象的依赖的,它是一个WeakMapWeakMap是弱引用的,具体的数据结构如下:

{
    [target]: {
        [key]: [new Set([effect1, effect2])]
    }
}

这里的target指向的是当前操作的对象,key指向的是当前操作对象的属性名,key对应的值是一个SetSet中存储的是当前对象属性的所有依赖,也就是effect


这里说的可能不是很好理解,我们来模拟一下:

// 依赖映射表
const targetMap = new WeakMap();
const track = (target, type, key) => {
    const depsMap = targetMap.get(target);
    if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()));
    }
    // 这里的 key 就是 name,也就是属性名
    let dep = depsMap.get(key);
    if (!dep) {
        // 这里的 dep 就是 Set,也就是依赖集合
        depsMap.set(key, (dep = new Set()));
    }
    // 添加到依赖集合中,这里的 activeEffect 就是当前正在执行的 effect
    dep.add(activeEffect);
}
// 创建一个对象,用来模拟 target
const target = {
    name: '田八',
    age: 18
};
// 这里有两个 effect,用来模拟一个属性用在的两个地方
function effect1(fn) {
    console.log(target.name);
}
function effect2(fn) {
    console.log(target.name);
    console.log(target.age);
}
let activeEffect = null;
// 只有 effect 在执行的时候才会收集依赖
// 实际情况是执行的过程中就会收集依赖,我们这里手动模拟
effect1();
activeEffect = effect1;
// 收集依赖
track(target, 'get', 'name');
activeEffect = null;
// 收集第二个依赖,流程同上
effect2();
activeEffect = effect2;
// 收集依赖
track(target, 'get', 'name');
// 这个会多一个 age 的依赖
track(target, 'get', 'age');
activeEffect = null;

经过上述的流程,最后targetMap的数据结构如下:

{
    [target]: {
        name: [effect1, effect2]
        age: [effect2]
    }
}

依赖收集的逻辑就是这样,只是做收集,真正的执行逻辑是在trigger方法中,这个后面会讲到。


trackEffects


上面已经了解到了shouldTracktargetMap,这里我们来看看trackEffects,这个方法是用来收集依赖的,它的具体实现如下:

// maxMarkerBits 用来记录最大的依赖收集的深度
const maxMarkerBits = 30;
// effectTrackDepth 用来记录当前的依赖收集的深度
let effectTrackDepth = 0;
// trackOpBit 用来记录当前的依赖收集的深度,这个是配合 maxMarkerBits 使用的
let trackOpBit = 1
function trackEffects(dep, debuggerEventExtraInfo) {
    // shouldTrack 含义同上,但是不是全局的,而是局部的
    let shouldTrack = false;
    // 这一步是为了解决递归调用的问题
    if (effectTrackDepth <= maxMarkerBits) {
        // newTracked 用来判断当前的依赖是否新的依赖追踪
        if (!newTracked(dep)) {
            // 设置新的依赖追踪
            dep.n |= trackOpBit; // set newly tracked
            // wasTracked 用来判断当前的依赖是否已经被追踪过了
            shouldTrack = !wasTracked(dep);
        }
    }
    else {
        // Full cleanup mode.
        // 已经到了最大的依赖收集深度,这里就不再对相同的依赖进行收集了
        shouldTrack = !dep.has(activeEffect);
    }
    // 如果 shouldTrack 为 true,说明当前的依赖是新的依赖,需要收集
    if (shouldTrack) {
        // 记录当前的依赖
        dep.add(activeEffect);
        // activeEffect.deps 记录的维度不同,稍后会讲到
        activeEffect.deps.push(dep);
        // 开发环境下才会用到
        if (activeEffect.onTrack) {
            activeEffect.onTrack(Object.assign({ effect: activeEffect }, debuggerEventExtraInfo));
        }
    }
}

这里设计很妙,shouldTrack没啥好介绍的,重要的是shouldTrack的值是如何确定的,这里有两种情况:


  1. effectTrackDepth小于maxMarkerBits,这种情况代表当前的依赖收集的深度还没有达到最大值,这里的深度指的就是递归调用的深度,也可以是effect的嵌套调用的深度,但是通常没人会手写嵌套调用到溢出;
  2. effectTrackDepth大于maxMarkerBits,这种情况代表当前的依赖收集的深度已经达到最大值,就判断当前的依赖是否已经被收集过了,这里的相同依赖指的就是相同的effect


在赖收集的深度还没有到达最大值的情况下,会有两个方法来判断当前的依赖是否是新的依赖,newTrackedwasTracked的实现如下:

function newTracked(dep) {
    return (dep.n & trackOpBit) === 0;
}
function wasTracked(dep) {
    return (dep.n & trackOpBit) > 0;
}

这里使用的是位运算,trackOpBit的值是由effectTrackDepth确定的,在effect执行的过程中,effectTrackDepth会进行自增,当effect执行完成之后,effectTrackDepth会进行自减;


下面就是trackOpBit值的确定过程,在effectrun方法中,会有如下代码:

function run() {
    // ...
    try {
        // ...
        // 在执行 effect 中会对 trackOpBit 进行赋值
        // 1 << ++effectTrackDepth 是位移运算,是将 1 左移 effectTrackDepth 位
        // 这里可以将 1 理解为二进制数据,左移一位就是在二进制数据的最后面添加一个 0
        // 例如:
        //      1 << 0 = 1 = 0001
        //      1 << 1 = 2 = 0010
        //      1 << 2 = 4 = 0100
        //      1 << 10 = 1024 = 10000000000
        // 可自行在控制台进行验证,这里 effectTrackDepth 最大值是 30,由 maxMarkerBits 决定
        trackOpBit = 1 << ++effectTrackDepth;
        // 控制深度
        if (effectTrackDepth <= maxMarkerBits) {
            initDepMarkers(this);
        } else {
            cleanupEffect(this);
        }
        // 执行回调
        return this.fn();
    } finally {
        // 执行完毕确定当前的依赖收集的深度标记
        if (effectTrackDepth <= maxMarkerBits) {
            finalizeDepMarkers(this);
        }
        // effectTrackDepth 自减,空出位置
        trackOpBit = 1 << --effectTrackDepth;
        // ...
    }
}

trackOpBit的值最终会映射到dep.ndep.w上,这两个值在上面已经出现过了,现在来详细扒一扒,在上面的代码中,可以看到有两个方法initDepMarkersfinalizeDepMarkers,这两个方法的实现如下:

const initDepMarkers = ({ deps }) => {
    if (deps.length) {
        for (let i = 0; i < deps.length; i++) {
            deps[i].w |= trackOpBit; // set was tracked
        }
    }
};
const finalizeDepMarkers = (effect) => {
    const { deps } = effect;
    if (deps.length) {
        let ptr = 0;
        for (let i = 0; i < deps.length; i++) {
            const dep = deps[i];
            if (wasTracked(dep) && !newTracked(dep)) {
                dep.delete(effect);
            }
            else {
                deps[ptr++] = dep;
            }
            // clear bits
            dep.w &= ~trackOpBit;
            dep.n &= ~trackOpBit;
        }
        deps.length = ptr;
    }
};

这个东西到底有啥用呢?可以看到的是最终trackOpBit的值会被赋值到dep.wdep.n上,dep.w的作用是用来记录当前的依赖是否被收集过的,dep.n的作用是用来记录当前的依赖收集的深度的;


到这里其实整个流程已经串起来了,画个图来说明一下:

image.png


这个图忽略很多细节,但是可以看到整个流程是怎么走的,到这里我们回顾一下整个流程:


  1. 我们定义一个响应式对象,这个响应式对象会拦截你对属性的操作
  2. 当我们在effect中访问响应式对象的属性时,会触发get方法,这个时候会进行依赖收集
  3. effect在执行的过程中,首先会将全局的activeEffect设置为当前的effecteffect就是依赖收集中的依赖
  4. effect在执行的过程中,会标记嵌套深度,这个可以防止effect的嵌套过深,导致内存溢出
  5. effect在执行的过程中,会将trackOpBit的值赋值到dep.wdep.n上,这个值会记录当前的依赖是否被收集过,以及当前的依赖收集的深度
  6. effect最后会执行用户传入的回调,这个回调就是我们在effect中传入的函数
  7. effect执行完毕,会将effectTrackDepth的值自减,相当于减少层级的深度
  8. 当我们修改响应式对象的属性时,会触发set方法,这个时候会执行effect
  9. 这个时候就会回到第3步


自此整个流程就串起来了,我们已经知道了依赖是如何收集的,依赖收集的细节就是响应式对象的访问一定要在effect中,这样才能收集到依赖;


如果不在effect中访问响应式对象的属性,那么在track过程中是不存在activeEffect的,所以就不会进行依赖收集;


依赖触发


上面已经完整的了解到了依赖收集的流程,那么依赖触发的流程是怎么样的呢?我们先来看一下trigger方法的实现:

function trigger(target, type, key, newValue, oldValue, oldTarget) {
    const depsMap = targetMap.get(target);
    if (!depsMap) {
        // never been tracked
        return;
    }
    let deps = [];
    if (type === "clear" /* TriggerOpTypes.CLEAR */) {
        // collection being cleared
        // trigger all effects for target
        deps = [...depsMap.values()];
    }
    else if (key === 'length' && isArray(target)) {
        const newLength = Number(newValue);
        depsMap.forEach((dep, key) => {
            if (key === 'length' || key >= newLength) {
                deps.push(dep);
            }
        });
    }
    else {
        // schedule runs for SET | ADD | DELETE
        if (key !== void 0) {
            deps.push(depsMap.get(key));
        }
        // also run for iteration key on ADD | DELETE | Map.SET
        switch (type) {
            case "add" /* TriggerOpTypes.ADD */:
                if (!isArray(target)) {
                    deps.push(depsMap.get(ITERATE_KEY));
                    if (isMap(target)) {
                        deps.push(depsMap.get(MAP_KEY_ITERATE_KEY));
                    }
                }
                else if (isIntegerKey(key)) {
                    // new index added to array -> length changes
                    deps.push(depsMap.get('length'));
                }
                break;
            case "delete" /* TriggerOpTypes.DELETE */:
                if (!isArray(target)) {
                    deps.push(depsMap.get(ITERATE_KEY));
                    if (isMap(target)) {
                        deps.push(depsMap.get(MAP_KEY_ITERATE_KEY));
                    }
                }
                break;
            case "set" /* TriggerOpTypes.SET */:
                if (isMap(target)) {
                    deps.push(depsMap.get(ITERATE_KEY));
                }
                break;
        }
    }
    const eventInfo = (process.env.NODE_ENV !== 'production')
        ? { target, type, key, newValue, oldValue, oldTarget }
        : undefined;
    if (deps.length === 1) {
        if (deps[0]) {
            if ((process.env.NODE_ENV !== 'production')) {
                triggerEffects(deps[0], eventInfo);
            }
            else {
                triggerEffects(deps[0]);
            }
        }
    }
    else {
        const effects = [];
        for (const dep of deps) {
            if (dep) {
                effects.push(...dep);
            }
        }
        if ((process.env.NODE_ENV !== 'production')) {
            triggerEffects(createDep(effects), eventInfo);
        }
        else {
            triggerEffects(createDep(effects));
        }
    }
}

代码量比较多,拆解来看,先看参数:

/**
 * @param target    当前响应式对象
 * @param type      触发类型
 * @param key       属性名
 * @param newValue  新值
 * @param oldValue  旧值
 * @param oldTarget 旧的响应式对象
 */
function trigger(target, type, key, newValue, oldValue, oldTarget) {
}

这些参数的大概作用就是用来确定要执行哪些effect,接着往下看:

// 省略 trigger 的定义
// 获取当前响应式对象的依赖
const depsMap = targetMap.get(target);
if (!depsMap) {
    // never been tracked
    return;
}

targetMap在上面已经介绍过了,targetMap会将当前响应式对象作为key,依赖作为value存储起来,所以这里通过target就可以获取到当前响应式对象的依赖;


接着往下看:

// 最终要执行的 effect 都会存储在 deps 中
let deps = [];
if (type === "clear" /* TriggerOpTypes.CLEAR */) {
    // ...
} else if (key === 'length' && isArray(target)) {
    // ...
} else {
   // ...
}

deps是一个数组,最终要执行的effect都会存储在deps中,这里会根据typekey的不同,将要执行的effect存储到deps中;


tpye是操作类型,key是当前操作的属性名,这里可以看到开始会有两个判断,一个是type='clear',一个是key='length'


这两个暂时先不管,先看else中的代码:

// 如果 key 不为 undefined,将 key 对应的依赖存储到 deps 中
if (key !== void 0) {
    deps.push(depsMap.get(key));
}

这一步就将key对应的依赖存储到deps中,例如:

const obj = reactive({
    name: '田八',
    age: 18
})
effect(() => {
    console.log(obj.name)
})
effect(() => {
    console.log(obj.name)
})

这里name有两个effect依赖,所以到这一步的时候,deps中就会有两个effect

接着往下看:

// 根据 type 的不同,将对应的依赖存储到 deps 中
switch (type) {
    // 如果 type 为 add
    case "add" /* TriggerOpTypes.ADD */:
        // 如果 target 不是数组
        if (!isArray(target)) {
            // 将 ITERATE_KEY 对应的依赖存储到 deps 中
            deps.push(depsMap.get(ITERATE_KEY));
            // 如果 target 是 Map
            if (isMap(target)) {
                // 将 MAP_KEY_ITERATE_KEY 对应的依赖存储到 deps 中
                deps.push(depsMap.get(MAP_KEY_ITERATE_KEY));
            }
        }
        // 如果 key 是数字
        else if (isIntegerKey(key)) {
            // new index added to array -> length changes
            // 将 length 对应的依赖存储到 deps 中
            deps.push(depsMap.get('length'));
        }
        break;
    // 如果 type 为 delete
    case "delete" /* TriggerOpTypes.DELETE */:
        // 如果 target 不是数组
        if (!isArray(target)) {
            // 将 ITERATE_KEY 对应的依赖存储到 deps 中
            deps.push(depsMap.get(ITERATE_KEY));
            // 如果 target 是 Map
            if (isMap(target)) {
                // 将 MAP_KEY_ITERATE_KEY 对应的依赖存储到 deps 中
                deps.push(depsMap.get(MAP_KEY_ITERATE_KEY));
            }
        }
        break;
    // 如果 type 为 set
    case "set" /* TriggerOpTypes.SET */:
        // 如果 target 是 Map
        if (isMap(target)) {
            // 将 MAP_KEY_ITERATE_KEY 对应的依赖存储到 deps 中
            deps.push(depsMap.get(ITERATE_KEY));
        }
        break;
}

这里的信息量就比较大了,这里会根据type的不同,将对应的依赖存储到deps中;


这里的type有三种,分别是adddeleteset


这里的adddelete是对数组和Map的操作,set是对Map的操作;


这里的adddelete都会将ITERATE_KEY对应的依赖存储到deps中,如果是Map,还会将MAP_KEY_ITERATE_KEY对应的依赖存储到deps中;


ITERATE_KEYMAP_KEY_ITERATE_KEY见名知意,就是对迭代器的依赖;


思考一个问题,如果对数组执行push或者其他修改数组长度的操作,那么如何触发类似于forEarchmap等迭代器的依赖呢?


这里就是通过ITERATE_KEYMAP_KEY_ITERATE_KEY来实现的,这里会将ITERATE_KEYMAP_KEY_ITERATE_KEY对应的依赖存储到deps中,这样就可以触发迭代器的依赖了;


至于ITERATE_KEYMAP_KEY_ITERATE_KEY是什么时候加入到depsMap中的,这里在之前的章节中都有出现,但是没有深入讲解,这里建议大家可以回顾一下之前的章节,并且自己也跟着在源码中找一下,这样才能更好的理解;


接着往下看:

// 删除开发环境下才会执行的代码
// 如果 deps 中有依赖,并且只有一个
if (deps.length === 1) {
    if (deps[0]) {
        // 直接调用 triggerEffects 触发依赖
        triggerEffects(deps[0]);
    }
} else {
    // 如果 deps 中有多个依赖,将多个依赖合并到一个数组中
    const effects = [];
    for (const dep of deps) {
        if (dep) {
            effects.push(...dep);
        }
    }
    // 调用 triggerEffects 触发依赖,会有深度检测
    triggerEffects(createDep(effects));
}

这里的逻辑就比较简单了,如果deps中只有一个依赖,那么直接调用triggerEffects触发依赖;


如果deps中有多个依赖,那么将多个依赖合并到一个数组中,然后调用triggerEffects触发依赖,createDep就是为当前的依赖添加wn属性,这个就是上面讲到过的;


然后就是triggerEffects了,这个函数就是用来触发依赖的:

function triggerEffects(dep, debuggerEventExtraInfo) {
    // spread into array for stabilization
    // 将 dep 转换为数组
    const effects = isArray(dep) ? dep : [...dep];
    // 先执行 computed 依赖
    for (const effect of effects) {
        if (effect.computed) {
            triggerEffect(effect, debuggerEventExtraInfo);
        }
    }
    // 再执行非 computed 依赖
    for (const effect of effects) {
        if (!effect.computed) {
            triggerEffect(effect, debuggerEventExtraInfo);
        }
    }
}

这里没有什么特别的,就是先执行computed依赖,再执行非computed依赖,主要是为了保证computed依赖的执行顺序;


先执行computed依赖的目的是为了防止后面非computed依赖的执行,可能会修改computed依赖的值,这样就会导致computed依赖的值不正确;


再来看triggerEffect

// 删除开发环境下才会执行的代码
function triggerEffect(effect, debuggerEventExtraInfo) {
    // 如果 effect 不是 activeEffect 或者 effect 允许递归
    if (effect !== activeEffect || effect.allowRecurse) {
        // 如果 effect 有调度器,就执行调度器
        if (effect.scheduler) {
            effect.scheduler();
        }
        // 否则就直接执行
        else {
            effect.run();
        }
    }
}  

这里就非常好理解了,最开头的判断是为了防止死循环,如果effectactiveEffect,那么就不会执行effect


最终执行effect的逻辑就是判断effect是否有scheduler,如果有就执行scheduler,否则就直接执行effect,调度器这一块在之前的章节中也有讲到;


总结


到此为止,我们总算将Vue3的响应式原理讲完了,响应式核心我总共分了 4 篇文章来讲解,这里我再简单的总结一下:


第一篇Vue3 的响应式核心 reactive 和 effect 实现原理以及源码分析:介绍了Vue3的响应式原理,以及Vue3的响应式原理是如何实现的,这一篇是整体将响应式核心过了一遍,但是并不深入;


第二篇跟着 Vue3 的源码学习 reactive 背后的实现原理:详细的介绍了Vue3的响应式拦截是如何实现的,主要介绍Vue3是如何对可观察对象进行拦截的,例如ObjectArrayMapSet等;


第三篇Vue3 的依赖收集,这里的依赖指代的是什么?:主要介绍了effect的调度器是如何使用的,已经effect一些杂项的东西;


然后就是本篇,在之前几篇的铺垫下,这篇主要是将之前的内容串在一起,这样就构成了一个完整的响应式核心的流程;


响应式原理分开来看就是我写的第二篇和第三篇,响应式首先是响应式拦截,这一块就是reactive的核心,然后是依赖收集,这一块就是effect的核心;


最后将这两个核心串在一起,就是响应式核心,这样就构成了一个完整的响应式核心的流程;


目录
相关文章
|
1天前
|
前端开发 JavaScript API
vue3服务端渲染警告解决----DefinePlugin
vue3服务端渲染警告解决----DefinePlugin
7 0
|
1天前
|
JavaScript 前端开发 算法
Vue3与Vue2:对比分析与迁移指南
Vue3与Vue2:对比分析与迁移指南
|
1天前
vue3封装面包屑
vue3封装面包屑
6 0
|
1天前
|
JavaScript Go
VUE3+vite项目中动态引入组件和异步组件
VUE3+vite项目中动态引入组件和异步组件
|
1天前
|
JavaScript 前端开发 API
在VUE3的setup函数中如何引用
在VUE3的setup函数中如何引用
|
1天前
|
JavaScript 定位技术 API
OpenLayers入门-第二篇、在vue3中使用elementplus制作图层控件,图层切换,显示隐藏,图层排序
OpenLayers入门-第二篇、在vue3中使用elementplus制作图层控件,图层切换,显示隐藏,图层排序
|
1天前
|
JavaScript 安全 API
vue3注册添加全局实例属性的方法,如何在setup函数中调用
vue3注册添加全局实例属性的方法,如何在setup函数中调用
|
2天前
|
JavaScript API 开发者
Vue3自定义hooks
Vue3自定义hooks
6 0
|
2天前
|
JavaScript 编译器
Vue3之事件
Vue3之事件
5 0
|
2天前
|
JavaScript
Vue3之Props组件数据传递
Vue3之Props组件数据传递
6 0