计算机系统应用教程网站

网站首页 > 技术文章 正文

1.1万字深入细品Vue3.0源码响应式系统笔记「下」

btikc 2024-09-09 01:53:12 技术文章 13 ℃ 0 评论


作者:hkc52 前端巅峰

转发链接:https://mp.weixin.qq.com/s/A6WgCjQj3KsaKC6kSLy-1A

原文作者:KC

原文链接:https://hkc452.github.io/slamdunk-the-vue3/

前言

上一篇 1.1万字深入细品Vue3.0源码响应式系统笔记「上」 讲解了effect 是响应式系统的核心,而响应式系统又是 vue3 中的核心。接下来我们继续讲解 collectionHandlers

collectionHandlers 主要是对 set、map、weakSet、weakMap 四种类型的对象进行劫持。 主要有下面三种类型的 handler,当然照旧,我们拿其中的 mutableCollectionHandlers 进行讲解。剩余两种结合理解。

export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: createInstrumentationGetter(false, false)
}

export const shallowCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: createInstrumentationGetter(false, false)(false, true)
}

export const readonlyCollectionHandlers: ProxyHandler<CollectionTypes> = {
  get: createInstrumentationGetter(true, false)
}
  • mutableCollectionHandlers 主要是对 collection 的方法进行劫持,所以主要是对 get 方法进行代理,接下来对 createInstrumentationGetter(false, false) 进行研究。
  • instrumentations 是代理 get 访问的 handler,当然如果我们访问的 key 是 ReactiveFlags,直接返回存储的值,否则如果访问的 key 在 instrumentations 上,在由 instrumentations 进行处理。
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  const instrumentations = shallow
    ? shallowInstrumentations
    : isReadonly
      ? readonlyInstrumentations
      : mutableInstrumentations

  return (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
  ) => {
    if (key === ReactiveFlags.isReactive) {
      return !isReadonly
    } else if (key === ReactiveFlags.isReadonly) {
      return isReadonly
    } else if (key === ReactiveFlags.raw) {
      return target
    }

    return Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
  }
}
  • 接下来看看 mutableInstrumentations ,可以看到 mutableInstrumentations 对常见集合的增删改查以及 迭代方法进行了代理,我们就顺着上面的 key 怎么进行拦截的。注意 this: MapTypes 是 ts 上对 this 类型进行标注
const mutableInstrumentations: Record<string, Function> = {
  get(this: MapTypes, key: unknown) {
    return get(this, key, toReactive)
  },
  get size() {
    return size((this as unknown) as IterableCollections)
  },
  has,
  add,
  set,
  delete: deleteEntry,
  clear,
  forEach: createForEach(false, false)
}
const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
iteratorMethods.forEach(method => {
  mutableInstrumentations[method as string] = createIterableMethod(
    method,
    false,
    false
  )
  readonlyInstrumentations[method as string] = createIterableMethod(
    method,
    true,
    false
  )
  shallowInstrumentations[method as string] = createIterableMethod(
    method,
    true,
    true
  )
})
  • get 方法 首先获取 target ,对 target 进行 toRaw, 这个会被 createInstrumentationGetter 中的 proxy 拦截返回原始的 target,然后对 key 也进行一次 toRaw, 如果两者不一样,说明 key 也是 reative 的, 对 key 和 rawkey 都进行 track ,然后调用 target 原型上面的 has 方法,如果 key 为 true ,调用 get 获取值,同时对值进行 wrap ,对于 mutableInstrumentations 而言,就是 toReactive。
function get(
  target: MapTypes,
  key: unknown,
  wrap: typeof toReactive | typeof toReadonly | typeof toShallow
) {
  target = toRaw(target)
  const rawKey = toRaw(key)
  if (key !== rawKey) {
    track(target, TrackOpTypes.GET, key)
  }
  track(target, TrackOpTypes.GET, rawKey)
  const { has, get } = getProto(target)
  if (has.call(target, key)) {
    return wrap(get.call(target, key))
  } else if (has.call(target, rawKey)) {
    return wrap(get.call(target, rawKey))
  }
}
  • has 方法 跟 get 方法差不多,也是对 key 和 rawkey 进行 track。
function has(this: CollectionTypes, key: unknown): boolean {
  const target = toRaw(this)
  const rawKey = toRaw(key)
  if (key !== rawKey) {
    track(target, TrackOpTypes.HAS, key)
  }
  track(target, TrackOpTypes.HAS, rawKey)
  const has = getProto(target).has
  return has.call(target, key) || has.call(target, rawKey)
}
  • size 和 add 方法 size 最要是返回集合的大小,调用原型上的 size 方法,同时触发 ITERATE 类型的 track,而 add 方法添加进去之前要判断原本是否已经存在了,如果存在,则不会触发 ADD 类型的 trigger。
function size(target: IterableCollections) {
  target = toRaw(target)
  track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
  return Reflect.get(getProto(target), 'size', target)
}

function add(this: SetTypes, value: unknown) {
  value = toRaw(value)
  const target = toRaw(this)
  const proto = getProto(target)
  const hadKey = proto.has.call(target, value)
  const result = proto.add.call(target, value)
  if (!hadKey) {
    trigger(target, TriggerOpTypes.ADD, value, value)
  }
  return result
}

set 方法

  • set 方法是针对 map 类型的,从 this 的类型我们就可以看出来了, 同样这里我们也会对 key 做两个校验,第一,是看看现在 map 上面有没有存在同名的 key,来决定是触发 SET 还是 ADD 的 trigger, 第二,对于开发环境,会进行 checkIdentityKeys 检查
function set(this: MapTypes, key: unknown, value: unknown) {
  value = toRaw(value)
  const target = toRaw(this)
  const { has, get, set } = getProto(target)

  let hadKey = has.call(target, key)
  if (!hadKey) {
    key = toRaw(key)
    hadKey = has.call(target, key)
  } else if (__DEV__) {
    checkIdentityKeys(target, has, key)
  }

  const oldValue = get.call(target, key)
  const result = set.call(target, key, value)
  if (!hadKey) {
    trigger(target, TriggerOpTypes.ADD, key, value)
  } else if (hasChanged(value, oldValue)) {
    trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  }
  return result
}
  • checkIdentityKeys 就是为了检查目标对象上面,是不是同时存在 rawkey 和 key,因为这样可能会数据不一致。
function checkIdentityKeys(
  target: CollectionTypes,
  has: (key: unknown) => boolean,
  key: unknown
) {
  const rawKey = toRaw(key)
  if (rawKey !== key && has.call(target, rawKey)) {
    const type = toRawType(target)
    console.warn(
      `Reactive ${type} contains both the raw and reactive ` +
        `versions of the same object${type === `Map` ? `as keys` : ``}, ` +
        `which can lead to inconsistencies. ` +
        `Avoid differentiating between the raw and reactive versions ` +
        `of an object and only use the reactive version if possible.`
    )
  }
}
  • deleteEntry 和 clear 方法
  • deleteEntry 主要是为了触发 DELETE trigger ,流程跟上面 set 方法差不多,而 clear 方法主要是触发 CLEAR track,但是里面做了一个防御性的操作,就是如果集合的长度已经为0,则调用 clear 方法不会触发 trigger。
function deleteEntry(this: CollectionTypes, key: unknown) {
  const target = toRaw(this)
  const { has, get, delete: del } = getProto(target)
  let hadKey = has.call(target, key)
  if (!hadKey) {
    key = toRaw(key)
    hadKey = has.call(target, key)
  } else if (__DEV__) {
    checkIdentityKeys(target, has, key)
  }

  const oldValue = get ? get.call(target, key) : undefined
  // forward the operation before queueing reactions
  const result = del.call(target, key)
  if (hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}

function clear(this: IterableCollections) {
  const target = toRaw(this)
  const hadItems = target.size !== 0
  const oldTarget = __DEV__
    ? target instanceof Map
      ? new Map(target)
      : new Set(target)
    : undefined
  // forward the operation before queueing reactions
  const result = getProto(target).clear.call(target)
  if (hadItems) {
    trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
  }
  return result
}
  • forEach 方法 在调用 froEach 方法的时候会触发 ITERATE 类型的 track,需要注意 Size 方法也会同样类型的 track,毕竟集合整体的变化会导致整个两个方法的输出不一样。顺带提一句,还记得我们的 effect 时候的 trigger 吗,对于 SET | ADD | DELETE 等类似的操作,因为会导致集合值的变化,所以也会触发 ITERATE_KEY 或则 MAP_KEY_ITERATE_KEY 的 effect 重新收集依赖。
  • 在调用原型上的 forEach 进行循环的时候,会对 key 和 value 都进行一层 wrap,对于我们来说,就是 reactive。
function createForEach(isReadonly: boolean, shallow: boolean) {
  return function forEach(
    this: IterableCollections,
    callback: Function,
    thisArg?: unknown
  ) {
    const observed = this
    const target = toRaw(observed)
    const wrap = isReadonly ? toReadonly : shallow ? toShallow : toReactive
    !isReadonly && track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
    // important: create sure the callback is
    // 1. invoked with the reactive map as `this` and 3rd arg
    // 2. the value received should be a corresponding reactive/readonly.
    function wrappedCallback(value: unknown, key: unknown) {
      return callback.call(thisArg, wrap(value), wrap(key), observed)
    }
    return getProto(target).forEach.call(target, wrappedCallback)
  }
}
  • createIterableMethod 方法 主要是对集合中的迭代进行代理,['keys', 'values', 'entries', Symbol.iterator] 主要是这四个方法。
const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
iteratorMethods.forEach(method => {
  mutableInstrumentations[method as string] = createIterableMethod(
    method,
    false,
    false
  )
  readonlyInstrumentations[method as string] = createIterableMethod(
    method,
    true,
    false
  )
  shallowInstrumentations[method as string] = createIterableMethod(
    method,
    true,
    true
  )
})
  • 可以看到,这个方法也会触发 TrackOpTypes.ITERATE 类型的 track,同样也会在遍历的时候对值进行 wrap,需要主要的是,这个方法主要是 iterator protocol 进行一个 polyfill, 所以需要实现同样的接口方便外部进行迭代。
function createIterableMethod(
  method: string | symbol,
  isReadonly: boolean,
  shallow: boolean
) {
  return function(this: IterableCollections, ...args: unknown[]) {
    const target = toRaw(this)
    const isMap = target instanceof Map
    const isPair = method === 'entries' || (method === Symbol.iterator && isMap)
    const isKeyOnly = method === 'keys' && isMap
    const innerIterator = getProto(target)[method].apply(target, args)
    const wrap = isReadonly ? toReadonly : shallow ? toShallow : toReactive
    !isReadonly &&
      track(
        target,
        TrackOpTypes.ITERATE,
        isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
      )
    // return a wrapped iterator which returns observed versions of the
    // values emitted from the real iterator
    return {
      // iterator protocol
      next() {
        const { value, done } = innerIterator.next()
        return done
          ? { value, done }
          : {
              value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
              done
            }
      },
      // iterable protocol
      [Symbol.iterator]() {
        return this
      }
    }
  }
}
  • 总的来说对集合的代理,就是对集合方法的代理,在集合方法的执行的时候,进行不同类型的 key 的 track 或者 trigger。

ref 其实就是 reactive 包了一层,读取值要要通过 ref.value 进行读取,同时进行 track ,而设置值的时候,也会先判断相对于旧值是否有变化,有变化才进行设置,以及 trigger。话不多说,下面就进行 ref 的分析。

  • 通过 createRef 创建 ref,如果传入的 rawValue 本身就是一个 ref 的话,直接返回。
  • 而如果 shallow 为 false, 直接让 ref.value 等于 value,否则对 rawValue 进行 convert 转化成 reactive。可以看到 __v_isRef 标识 一个对象是否是 ref,读取 value 触发 track,设置 value 而且 newVal 的 toRaw 跟 原先的 rawValue 不一致,则进行设置,同样对于非 shallow 也进行 convert。
export function ref(value?: unknown) {
  return createRef(value)
}
const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val
function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  let value = shallow ? rawValue : convert(rawValue)
  const r = {
    __v_isRef: true,
    get value() {
      track(r, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newVal) {
      if (hasChanged(toRaw(newVal), rawValue)) {
        rawValue = newVal
        value = shallow ? newVal : convert(newVal)
        trigger(
          r,
          TriggerOpTypes.SET,
          'value',
          __DEV__ ? { newValue: newVal } : void 0
        )
      }
    }
  }
  return r
}
  • triggerRef 手动触发 trigger ,对 shallowRef 可以由调用者手动触发。 unref 则是反向操作,取出 ref 中的 value 值。
export function triggerRef(ref: Ref) {
  trigger(
    ref,
    TriggerOpTypes.SET,
    'value',
    __DEV__ ? { newValue: ref.value } : void 0
  )
}

export function unref<T>(ref: T): T extends Ref<infer V> ? V : T {
  return isRef(ref) ? (ref.value as any) : ref
}
  • toRefs 是将一个 reactive 对象或者 readonly 转化成 一个个 refs 对象,这个可以从 toRef 方法可以看出。
export function toRefs<T extends object>(object: T): ToRefs<T> {
  if (__DEV__ && !isProxy(object)) {
    console.warn(`toRefs() expects a reactive object but received a plain one.`)
  }
  const ret: any = {}
  for (const key in object) {
    ret[key] = toRef(object, key)
  }
  return ret
}

export function toRef<T extends object, K extends keyof T>(
  object: T,
  key: K
): Ref<T[K]> {
  return {
    __v_isRef: true,
    get value(): any {
      return object[key]
    },
    set value(newVal) {
      object[key] = newVal
    }
  } as any
}
  • 需要提到 baseHandlers 一点的是,对于非 shallow 模式中,对于 target 不是数组,会直接拿 ref.value 的值,而不是 ref。
 if (isRef(res)) {
      if (targetIsArray) {
        !isReadonly && track(target, TrackOpTypes.GET, key)
        return res
      } else {
        // ref unwrapping, only for Objects, not for Arrays.
        return res.value
      }
    }

而 set 中,如果对于 target 是对象,oldValue 是 ref, value 不是 ref,直接把 vlaue 设置给 oldValue.value

if (!shallow) {
      value = toRaw(value)
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
}
  • 需要注意的是, ref 还支持自定义 ref,就是由调用者手动去触发 track 或者 trigger,就是通过工厂模式生成我们的 ref 的 get 和 set
export type CustomRefFactory<T> = (
  track: () => void,
  trigger: () => void
) => {
  get: () => T
  set: (value: T) => void
}

export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
  const { get, set } = factory(
    () => track(r, TrackOpTypes.GET, 'value'),
    () => trigger(r, TriggerOpTypes.SET, 'value')
  )
  const r = {
    __v_isRef: true,
    get value() {
      return get()
    },
    set value(v) {
      set(v)
    }
  }
  return r as any
}
  • 这个用法,我们可以在测试用例找到,
 const custom = customRef((track, trigger) => ({
  get() {
    track()
    return value
  },
  set(newValue: number) {
    value = newValue
    _trigger = trigger
  }
}))

computed 就是计算属性,可能会依赖其他 reactive 的值,同时会延迟和缓存计算值,具体怎么操作。show the code。需要注意的是,computed 不一定有 set 操作,因为可能是只读 computed。

  • 首先我们会对传入的 getterOrOptions 进行解析,如果是方法,说明是只读 computed,否则从 getterOrOptions 解析出 get 和 set 方法。
  • 紧接着,利用 getter 创建 runner effect,需要注意的 effect 的三个参数,第一是 lazy ,表明内部创建 effect 之后,不会立即执行。第二是 coumputed, 表明 computed 上游依赖改变的时候,会优先 trigger runner effect,而 runner 也不会在这时被执行的,原因看第三。第三,我们知道,effect 传入 scheduler 的时候, effect 会 trigger 的时候会调用 scheduler 而不是直接调用 effect。而在 computed 中,我们可以看到 trigger(computed, TriggerOpTypes.SET, 'value') 触发依赖 computed 的 effect 被重新收集依赖。同时因为 computed 是缓存和延迟计算,所以在依赖 computed effect 重新收集的过程中,runner 会在第一次计算 value,以及重新让 runner 被收集依赖。这也是为什么要 computed effect 的优先级要高的原因,因为让 依赖的 computed的 effect 重新收集依赖,以及让 runner 最早进行依赖收集,这样才能计算出最新的 computed 值。
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  let dirty = true
  let value: T
  let computed: ComputedRef<T>

  const runner = effect(getter, {
    lazy: true,
    // mark effect as computed so that it gets priority during trigger
    computed: true,
    scheduler: () => {
      if (!dirty) {
        dirty = true
        trigger(computed, TriggerOpTypes.SET, 'value')
      }
    }
  })
  computed = {
    __v_isRef: true,
    // expose effect so computed can be stopped
    effect: runner,
    get value() {
      if (dirty) {
        value = runner()
        dirty = false
      }
      track(computed, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newValue: T) {
      setter(newValue)
    }
  } as any
  return computed
}
  • 从上面可以看出,effect 有可能被多次调用,像下面中 value.foo++,会导致 effectFn 运行两次,因为同时被 effectFn 同时被 effectFn 和 c1 依赖了。PS: 下面这个测试用例是自己写的,不是 Vue 里面的。
it('should trigger once', () => {
    const value = reactive({ foo: 0 })
    const getter1 = jest.fn(() => value.foo)
    const c1 = computed(getter1)
    const effectFn = jest.fn(() => {
        value.foo
        c1.value
    })
    effect(effectFn)
    expect(effectFn).toBe(1)
    value.foo++
    // 原本以为是 2
    expect(effectFn).toHaveBeenCalledTimes(3)
  })
  • 对于 computed 暴露出来的 effect ,主要为了调用 effect 里面 stop 方法停止依赖收集。至此,响应式模块分析完毕。

本篇文章完结

推荐Vue学习资料文章:

1.1万字深入细品Vue3.0源码响应式系统笔记「上」

「实践」Vue 数据更新7 种情况汇总及延伸解决总结

尤大大细说Vue3 的诞生之路「译」

提高10倍打包速度工具Snowpack 2.0正式发布,再也不需要打包器

大厂Code Review总结Vue开发规范经验「值得学习」

Vue3 插件开发详解尝鲜版「值得收藏」

带你五步学会Vue SSR

记一次Vue3.0技术干货分享会

Vue 3.x 如何有惊无险地快速入门「进阶篇」

「干货」微信支付前后端流程整理(Vue+Node)

带你了解 vue-next(Vue 3.0)之 炉火纯青「实践」

「干货」Vue+高德地图实现页面点击绘制多边形及多边形切割拆分

「干货」Vue+Element前端导入导出Excel

「实践」Deno bytes 模块全解析

细品pdf.js实践解决含水印、电子签章问题「Vue篇」

基于vue + element的后台管理系统解决方案

Vue仿蘑菇街商城项目(vue+koa+mongodb)

基于 electron-vue 开发的音乐播放器「实践」

「实践」Vue项目中标配编辑器插件Vue-Quill-Editor

基于 Vue 技术栈的微前端方案实践

消息队列助你成为高薪 Node.js 工程师

Node.js 中的 stream 模块详解

「干货」Deno TCP Echo Server 是怎么运行的?

「干货」了不起的 Deno 实战教程

「干货」通俗易懂的Deno 入门教程

Deno 正式发布,彻底弄明白和 node 的区别

「实践」基于Apify+node+react/vue搭建一个有点意思的爬虫平台

「实践」深入对比 Vue 3.0 Composition API 和 React Hooks

前端网红框架的插件机制全梳理(axios、koa、redux、vuex)

深入Vue 必学高阶组件 HOC「进阶篇」

深入学习Vue的data、computed、watch来实现最精简响应式系统

10个实例小练习,快速入门熟练 Vue3 核心新特性(一)

10个实例小练习,快速入门熟练 Vue3 核心新特性(二)

教你部署搭建一个Vue-cli4+Webpack移动端框架「实践」

2020前端就业Vue框架篇「实践」

详解Vue3中 router 带来了哪些变化?

Vue项目部署及性能优化指导篇「实践」

Vue高性能渲染大数据Tree组件「实践」

尤大大细品VuePress搭建技术网站与个人博客「实践」

10个Vue开发技巧「实践」

是什么导致尤大大选择放弃Webpack?【vite 原理解析】

带你了解 vue-next(Vue 3.0)之 小试牛刀【实践】

带你了解 vue-next(Vue 3.0)之 初入茅庐【实践】

实践Vue 3.0做JSX(TSX)风格的组件开发

一篇文章教你并列比较React.js和Vue.js的语法【实践】

手拉手带你开启Vue3世界的鬼斧神工【实践】

深入浅出通过vue-cli3构建一个SSR应用程序【实践】

怎样为你的 Vue.js 单页应用提速

聊聊昨晚尤雨溪现场针对Vue3.0 Beta版本新特性知识点汇总

【新消息】Vue 3.0 Beta 版本发布,你还学的动么?

Vue真是太好了 壹万多字的Vue知识点 超详细!

Vue + Koa从零打造一个H5页面可视化编辑器——Quark-h5

深入浅出Vue3 跟着尤雨溪学 TypeScript 之 Ref 【实践】

手把手教你深入浅出vue-cli3升级vue-cli4的方法

Vue 3.0 Beta 和React 开发者分别杠上了

手把手教你用vue drag chart 实现一个可以拖动 / 缩放的图表组件

Vue3 尝鲜

总结Vue组件的通信

Vue 开源项目 TOP45

2020 年,Vue 受欢迎程度是否会超过 React?

尤雨溪:Vue 3.0的设计原则

使用vue实现HTML页面生成图片

实现全栈收银系统(Node+Vue)(上)

实现全栈收银系统(Node+Vue)(下)

vue引入原生高德地图

Vue合理配置WebSocket并实现群聊

多年vue项目实战经验汇总

vue之将echart封装为组件

基于 Vue 的两层吸顶踩坑总结

Vue插件总结【前端开发必备】

Vue 开发必须知道的 36 个技巧【近1W字】

构建大型 Vue.js 项目的10条建议

深入理解vue中的slot与slot-scope

手把手教你Vue解析pdf(base64)转图片【实践】

使用vue+node搭建前端异常监控系统

推荐 8 个漂亮的 vue.js 进度条组件

基于Vue实现拖拽升级(九宫格拖拽)

手摸手,带你用vue撸后台 系列二(登录权限篇)

手摸手,带你用vue撸后台 系列三(实战篇)

前端框架用vue还是react?清晰对比两者差异

Vue组件间通信几种方式,你用哪种?【实践】

浅析 React / Vue 跨端渲染原理与实现

10个Vue开发技巧助力成为更好的工程师

手把手教你Vue之父子组件间通信实践讲解【props、$ref 、$emit】

1W字长文+多图,带你了解vue的双向数据绑定源码实现

深入浅出Vue3 的响应式和以前的区别到底在哪里?【实践】

干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)

基于Vue/VueRouter/Vuex/Axios登录路由和接口级拦截原理与实现

手把手教你D3.js 实现数据可视化极速上手到Vue应用

吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【上】

吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【中】

吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【下】

Vue3.0权限管理实现流程【实践】

后台管理系统,前端Vue根据角色动态设置菜单栏和路由

作者:hkc52 前端巅峰

转发链接:https://mp.weixin.qq.com/s/A6WgCjQj3KsaKC6kSLy-1A

原文作者:KC

原文链接:https://hkc452.github.io/slamdunk-the-vue3/

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表