Vue3響應(yīng)式對(duì)象Reactive和Ref的用法解讀
一、內(nèi)容簡(jiǎn)介
本篇文章著重結(jié)合源碼版本V3.2.20介紹Reactive和Ref。前置技能需要了解Proxy對(duì)象的工作機(jī)制,以下貼出的源碼均在關(guān)鍵位置備注了詳細(xì)注釋。
備注:本篇幅只講到收集依賴和觸發(fā)依賴更新的時(shí)機(jī),并未講到如何收集依賴和如何觸發(fā)依賴。響應(yīng)式原理快捷通道。
二、Reactive
1. 關(guān)鍵源碼
/*源碼位置:/packages/reactivity/src/reactive.ts*/ /** ?* 創(chuàng)建響應(yīng)式代理對(duì)象 ?* @param target 被代理對(duì)象 ?* @param isReadonly 是否只讀 ?* @param baseHandlers 普通對(duì)象的攔截操作 ?* @param collectionHandlers 集合對(duì)象的攔截操作 ?* @param proxyMap 代理Map ?* @returns? ?*/ function createReactiveObject( ? target: Target, ? isReadonly: boolean, ? baseHandlers: ProxyHandler<any>, ? collectionHandlers: ProxyHandler<any>, ? proxyMap: WeakMap<Target, any> ) { ? //如果不是對(duì)象,則警告,Proxy代理只支持對(duì)象 ? if (!isObject(target)) { ? ? if (__DEV__) { ? ? ? console.warn(`value cannot be made reactive: ${String(target)}`) ? ? } ? ? return target ? } ? //如果被代理對(duì)象已經(jīng)是一個(gè)proxy對(duì)象且是響應(yīng)式的并且此次創(chuàng)建的新代理對(duì)象不是只讀的,則直接返回被代理對(duì)象 ? //這兒存在一種情況需要重新創(chuàng)建,即被代理對(duì)象已經(jīng)是一個(gè)代理對(duì)象了,且可讀可寫。但新創(chuàng)建的代理對(duì)象是只讀的 ? //那么,本次生成的那個(gè)代理對(duì)象最終是只讀的。響應(yīng)式必須可讀可寫,只讀的代理對(duì)象是非響應(yīng)式的。 ? if ( ? ? target[ReactiveFlags.RAW] && ? ? !(isReadonly && target[ReactiveFlags.IS_REACTIVE]) ? ) { ? ? return target ? } ? //從map中找,如果對(duì)象已經(jīng)被代理過,則直接從map中返回,否則生成代理 ? const existingProxy = proxyMap.get(target) ? if (existingProxy) { ? ? return existingProxy ? } ? // 獲取代理類型,即采用集合類型的代理還是普通對(duì)象類型的代理 ? const targetType = getTargetType(target) ? if (targetType === TargetType.INVALID) { ? ? return target ? } ? // 生成代理對(duì)象并存入map中 ? const proxy = new Proxy( ? ? target, ? ? targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers ? ) ? proxyMap.set(target, proxy) ? return proxy }
2. 源碼流程分析
Vue中創(chuàng)建響應(yīng)式代理對(duì)象都是通過createReactiveObject方法創(chuàng)建。這個(gè)方法里面的主要邏輯很簡(jiǎn)單,就是生成一個(gè)目標(biāo)對(duì)象的代理對(duì)象,代理對(duì)象最為核心的操作攔截則由外部根據(jù)是否只讀和是否淺響應(yīng)傳入,然后將這個(gè)代理對(duì)象存起來以備下次快捷獲取。
三、代理攔截操作
1. 數(shù)組操作
(1).關(guān)鍵源碼
//源碼位置: /packages/reactivity/src/baseHandlers.ts function createArrayInstrumentations() { ? const instrumentations: Record<string, Function> = {} ? // instrument identity-sensitive Array methods to account for possible reactive ? // values ? ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => { ? ? instrumentations[key] = function (this: unknown[], ...args: unknown[]) { ? ? ? //獲取原始數(shù)組 ? ? ? const arr = toRaw(this) as any ? ? ? for (let i = 0, l = this.length; i < l; i++) { ? ? ? ? //收集依賴 鍵值為索引 i ? ? ? ? track(arr, TrackOpTypes.GET, i + '') ? ? ? } ? ? ? // 調(diào)用數(shù)組的原始方法 ? ? ? const res = arr[key](...args) ? ? ? if (res === -1 || res === false) { ? ? ? ? // 如果不存在,則將參數(shù)參數(shù)轉(zhuǎn)換為原始數(shù)據(jù)在試一次(這兒可能是防止傳入的是代理對(duì)象導(dǎo)致獲取失敗) ? ? ? ? return arr[key](...args.map(toRaw)) ? ? ? } else { ? ? ? ? return res ? ? ? } ? ? } ? }) ? // instrument length-altering mutation methods to avoid length being tracked ? // which leads to infinite loops in some cases (#2137) ? ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => { ? ? instrumentations[key] = function (this: unknown[], ...args: unknown[]) { ? ? ? //由于上面的方法會(huì)改變數(shù)組長(zhǎng)度,因此暫停收集依賴,不然會(huì)導(dǎo)致無(wú)限遞歸 ? ? ? pauseTracking() ? ? ? //調(diào)用原始方法 ? ? ? const res = (toRaw(this) as any)[key].apply(this, args) ? ? ? //復(fù)原依賴收集 ? ? ? resetTracking() ? ? ? return res ? ? } ? }) ? return instrumentations }
(2).源碼流程分析
上述源碼其實(shí)就是重寫了對(duì)于數(shù)組方法的操作,在通過數(shù)組的代理對(duì)象訪問以上數(shù)組方法時(shí),就會(huì)執(zhí)行重寫后的數(shù)組方法。
內(nèi)部邏輯很簡(jiǎn)單,對(duì)于改變了數(shù)組長(zhǎng)度的方法,先暫停依賴收集,調(diào)用原始數(shù)組方法,然后復(fù)原依賴收集。
對(duì)于判斷元素是否存在的數(shù)組方法,執(zhí)行依賴收集并調(diào)用數(shù)組原始方法。
總結(jié)來說最終都是調(diào)用了數(shù)組的原始方法,只不過在調(diào)用前后添加了關(guān)于依賴收集相關(guān)的行為。
2.Get操作
(1).關(guān)鍵源碼
//源碼位置: /packages/reactivity/src/baseHandlers.ts /** ?* 創(chuàng)建并且返回一個(gè)Get方法 ?* @param isReadonly 是否只讀 ?* @param shallow 是否淺響應(yīng) ?* @returns? ?*/ function createGetter(isReadonly = false, shallow = false) { ? return function get(target: Target, key: string | symbol, receiver: object) { ? ? //這兒不重要,其實(shí)就是通過代理對(duì)象訪問這幾個(gè)特殊屬性時(shí),返回相應(yīng)的值,和響應(yīng)式無(wú)關(guān) ? ? if (key === ReactiveFlags.IS_REACTIVE) { ? ? ? return !isReadonly ? ? } else if (key === ReactiveFlags.IS_READONLY) { ? ? ? return isReadonly ? ? } else if ( ? ? ? key === ReactiveFlags.RAW && ? ? ? receiver === ? ? ? ? (isReadonly ? ? ? ? ? ? shallow ? ? ? ? ? ? ? shallowReadonlyMap ? ? ? ? ? ? : readonlyMap ? ? ? ? ? : shallow ? ? ? ? ? ? shallowReactiveMap ? ? ? ? ? : reactiveMap ? ? ? ? ).get(target) ? ? ) { ? ? ? return target ? ? } ?? ? ? const targetIsArray = isArray(target) ? ? //如果是調(diào)用的數(shù)組方法,則調(diào)用重寫后的數(shù)組方法,前提不是只讀的 ? ? if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) { ? ? ? return Reflect.get(arrayInstrumentations, key, receiver) ? ? } ? ? //調(diào)用原始行為獲取值 ? ? const res = Reflect.get(target, key, receiver) ? ? //訪問Symbol對(duì)象上的屬性和__proto__,__v_isRef,__isVue這3個(gè)屬性,直接返回結(jié)果值 ? ? if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { ? ? ? return res ? ? } ? ? if (!isReadonly) { ? ? ? //不是只讀,則收集依賴 ? ? ? track(target, TrackOpTypes.GET, key) ? ? } ? ? if (shallow) { ? ? ? //如果對(duì)象是淺響應(yīng)的 則返回結(jié)果 ? ? ? return res ? ? } ? ? if (isRef(res)) { ? ? ? //如果值是Ref對(duì)象且是通過數(shù)組代理對(duì)象的下標(biāo)訪問的,則不做解包裝操作,否則返回解包裝后的值 ? ? ? // ref unwrapping - does not apply for Array + integer key. ? ? ? const shouldUnwrap = !targetIsArray || !isIntegerKey(key) ? ? ? return shouldUnwrap ? res.value : res ? ? } ? ? if (isObject(res)) { ? ? ? //走到這兒需要滿足非淺響應(yīng)。如果結(jié)果是一個(gè)對(duì)象,則將改對(duì)象轉(zhuǎn)換為只讀代理對(duì)象或者響應(yīng)式代理對(duì)象返回 ? ? ? //e.g.? ? ? ? // test:{ ? ? ? // ? a:{ ? ? ? // ? ? c:10 ? ? ? // ? } ? ? ? // } ? ? ? //以上測(cè)試對(duì)象當(dāng)訪問屬性a時(shí),此時(shí)res是一個(gè)普通對(duì)象,如果不轉(zhuǎn)換為代理對(duì)象,則對(duì)a.c的操作不會(huì)被攔截處理,導(dǎo)致無(wú)法響應(yīng)式處理 ? ? ? // 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 ? } }
(2).源碼流程分析
上述Get方法是在通過代理對(duì)象獲取某一個(gè)值時(shí)觸發(fā)的。流程很簡(jiǎn)單,就是對(duì)幾個(gè)特殊屬性做了特殊返回。
如果是數(shù)組方法,則調(diào)用重寫后的數(shù)組方法,不是則調(diào)用原始行為獲取值。
如果不是只讀,則收集依賴,對(duì)返回結(jié)果進(jìn)行判斷特殊處理。其中最關(guān)鍵的地方在于收集依賴和將獲取到的嵌套對(duì)象轉(zhuǎn)換為響應(yīng)式對(duì)象。
3. Set操作
(1).關(guān)鍵源碼
//源碼位置: /packages/reactivity/src/baseHandlers.ts /** ?* 創(chuàng)建并返回一個(gè)Set方法 ?* @param shallow 是否淺響應(yīng) ?* @returns? ?*/ function createSetter(shallow = false) { ? return function set( ? ? target: object, ? ? key: string | symbol, ? ? value: unknown, ? ? receiver: object ? ): boolean { ? ? //獲取改變之前的值 ? ? let oldValue = (target as any)[key] ? ? if (!shallow) { ? ? ? value = toRaw(value) ? ? ? oldValue = toRaw(oldValue) ? ? ? //對(duì)Ref類型值的特殊處理 ? ? ? //比較2個(gè)值,如果舊值是Ref對(duì)象,新值不是,則直接變Ref對(duì)象的value屬性 ? ? ? if (!isArray(target) && isRef(oldValue) && !isRef(value)) { ? ? ? ? //這兒看似沒有觸發(fā)依賴更新,其實(shí)Ref對(duì)象的value進(jìn)行賦值會(huì)觸發(fā)Ref對(duì)象的寫操作,在那個(gè)操作里面會(huì)觸發(fā)依賴更新 ? ? ? ? oldValue.value = value ? ? ? ? return true ? ? ? } ? ? } else { ? ? ? // in shallow mode, objects are set as-is regardless of reactive or not ? ? } ? ? const hadKey = ? ? ? isArray(target) && isIntegerKey(key) ? ? ? ? ? Number(key) < target.length ? ? ? ? : hasOwn(target, key) ? ? const result = Reflect.set(target, key, value, receiver) ? ? // don't trigger if target is something up in the prototype chain of original ? ? // 這個(gè)判斷其實(shí)是處理一個(gè)代理對(duì)象的原型也是代理對(duì)象的情況,以下是測(cè)試代碼 ? ? // let hiddenValue: any ? ? // const obj = reactive<{ prop?: number }>({}) ? ? // const parent = reactive({ ? ? // ? set prop(value) { ? ? // ? ? hiddenValue = value ? ? // ? }, ? ? // ? get prop() { ? ? // ? ? return hiddenValue ? ? // ? } ? ? // }) ? ? // Object.setPrototypeOf(obj, parent) ? ? // obj.prop = 4 ? ? // 當(dāng)存在上述情形,第一次設(shè)置值時(shí),由于子代理沒有prop屬性方法,會(huì)觸發(fā)父代理的set方法。父代理的這個(gè)判斷此時(shí)是false,算是一個(gè)優(yōu)化,避免2個(gè)觸發(fā)更新 ? ? if (target === toRaw(receiver)) { ? ? ? if (!hadKey) { ? ? ? ? //觸發(fā)add類型依賴更新 ? ? ? ? trigger(target, TriggerOpTypes.ADD, key, value) ? ? ? } else if (hasChanged(value, oldValue)) { ? ? ? ? //觸發(fā)set類型依賴更新 ? ? ? ? trigger(target, TriggerOpTypes.SET, key, value, oldValue) ? ? ? } ? ? } ? ? return result ? } }
(2).源碼流程分析
當(dāng)設(shè)置時(shí),首先對(duì)舊值是Ref類型對(duì)象做了個(gè)特殊處理,如果滿足條件,則走Ref對(duì)象的set方法邏輯觸發(fā)依賴更新。
否則根據(jù)是否存在key值,判斷是新增屬性,還是修改屬性,觸發(fā)不同類型的依賴更新。
之所以要區(qū)分依賴類型,是因?yàn)槟承傩詴?huì)連帶別的屬性更改,比如數(shù)組直接設(shè)置下標(biāo),會(huì)導(dǎo)致length的更改,這個(gè)時(shí)候需要收集length為鍵值的依賴,以便連帶更新依賴的length屬性的地方。
4. 其余行為攔截操作
(1).關(guān)鍵源碼
//源碼位置: /packages/reactivity/src/baseHandlers.ts /** ?* delete操作符時(shí)觸發(fā) ?* @param target 目標(biāo)對(duì)象 ?* @param key 鍵值 ?* @returns? ?*/ function deleteProperty(target: object, key: string | symbol): boolean { ? const hadKey = hasOwn(target, key) ? const oldValue = (target as any)[key] ? const result = Reflect.deleteProperty(target, key) ? if (result && hadKey) { ? ? //觸發(fā)依賴更新 ? ? trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue) ? } ? return result } /** ?* in 操作符時(shí)觸發(fā) ?* @param target 目標(biāo)對(duì)象 ?* @param key 鍵值 ?* @returns? ?*/ function has(target: object, key: string | symbol): boolean { ? const result = Reflect.has(target, key) ? if (!isSymbol(key) || !builtInSymbols.has(key)) { ? ?? ?//收集依賴 ? ? track(target, TrackOpTypes.HAS, key) ? } ? return result } /** ?* Object.keys()等類似方法時(shí)調(diào)用 ?* @param target 目標(biāo)對(duì)象 ?* @returns? ?*/ function ownKeys(target: object): (string | symbol)[] { ? //收集依賴 ? track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY) ? return Reflect.ownKeys(target) }
(2).源碼流程分析
上述源碼其實(shí)就是在對(duì)一些特殊操作符或者特定API時(shí)的特殊處理,本質(zhì)還是收集依賴和觸發(fā)依賴更新,沒什么好講的。
四、Ref對(duì)象
1. 思考一個(gè)問題
為什么存在了Reactive代理對(duì)象后,已經(jīng)可以進(jìn)行依賴收集和依賴更新了,還要設(shè)計(jì)一個(gè)Ref類型。
測(cè)試一:針對(duì)以下測(cè)試代碼,Ref對(duì)象值的改變正確觸發(fā)了更新
?? ?//源碼位置 /packages/reactivity/__test__/ref.spec.ts ? ? const a = ref(1) ? ? let dummy ? ? let calls = 0 ? ? effect(() => { ? ? ? calls++ ? ? ? dummy = a.value; ? ? }) ? ? a.value = 2; ? ? //此時(shí)dummy = 2,a對(duì)象值的改變觸發(fā)了依賴更新
測(cè)試二:修改以上代碼,更新失敗
? ? const a = 1 ? ? let dummy ? ? let calls = 0 ? ? effect(() => { ? ? ? calls++ ? ? ? dummy = a; ? ? }) ? ? a= 2; ? ? //此時(shí)dummy = 1,a的改變沒有觸發(fā)依賴更新
上述2個(gè)示例很明顯的表明出了,對(duì)于非響應(yīng)式對(duì)象的改變,不會(huì)觸發(fā)依賴更新。Reactive是通過代理實(shí)現(xiàn)的,代理只支持對(duì)象,不支持非對(duì)象的基礎(chǔ)類型。所以需要設(shè)計(jì)一個(gè)Ref類型來包裝這些類型數(shù)據(jù),以便擁有響應(yīng)式狀態(tài)
2. 簡(jiǎn)要說明
既然設(shè)計(jì)了Ref來支持非對(duì)象屬性,那么也一定需要兼容對(duì)象屬性。內(nèi)部其實(shí)很簡(jiǎn)單,如果是對(duì)象,則直接轉(zhuǎn)為Reactive代理對(duì)象。
3. 關(guān)鍵源碼
class RefImpl<T> { ? private _value: T ? private _rawValue: T ? public dep?: Dep = undefined ? public readonly __v_isRef = true ? constructor(value: T, public readonly _shallow: boolean) { ? ? //原始數(shù)據(jù) ? ? this._rawValue = _shallow ? value : toRaw(value) ? ? //外部訪問到的數(shù)據(jù),轉(zhuǎn)換為響應(yīng)式 ? ? this._value = _shallow ? value : toReactive(value) ? } ? get value() { ? ? //跟蹤依賴 ? ? trackRefValue(this) ? ? return this._value ? } ? set value(newVal) { ? ? newVal = this._shallow ? newVal : toRaw(newVal) ? ? if (hasChanged(newVal, this._rawValue)) { ? ? ? //如果原始數(shù)據(jù)之間的比較不一樣,則賦值 ? ? ? this._rawValue = newVal ? ? ? //把新值轉(zhuǎn)換為響應(yīng)式對(duì)象 ? ? ? this._value = this._shallow ? newVal : toReactive(newVal) ? ? ? //觸發(fā)依賴 ? ? ? triggerRefValue(this, newVal) ? ? } ? } } //轉(zhuǎn)換響應(yīng)式對(duì)象方法 export const toReactive = <T extends unknown>(value: T): T => ? isObject(value) ? reactive(value) : value type CustomRefFactory<T> = ( ? track: () => void, ? trigger: () => void ) => { ? get: () => T ? set: (value: T) => void } //收集依賴 export function trackRefValue(ref: RefBase<any>) { ? //是否可以收集 ? if (isTracking()) { ? ? //獲取原始數(shù)據(jù) ? ? ref = toRaw(ref) ? ? if (!ref.dep) { ? ? ? //如果不存在依賴,就創(chuàng)建一個(gè)依賴對(duì)象 ? ? ? ref.dep = createDep() ? ? } ? ? //收集依賴 ? ? if (__DEV__) { ? ? ? trackEffects(ref.dep, { ? ? ? ? target: ref, ? ? ? ? type: TrackOpTypes.GET, ? ? ? ? key: 'value' ? ? ? }) ? ? } else { ? ? ? trackEffects(ref.dep) ? ? } ? } } //觸發(fā)依賴更新 export function triggerRefValue(ref: RefBase<any>, newVal?: any) { ? ref = toRaw(ref) ? if (ref.dep) { ? ? if (__DEV__) { ? ? ? triggerEffects(ref.dep, { ? ? ? ? target: ref, ? ? ? ? type: TriggerOpTypes.SET, ? ? ? ? key: 'value', ? ? ? ? newValue: newVal ? ? ? }) ? ? } else { ? ? ? triggerEffects(ref.dep) ? ? } ? } } /** ?* 自定義響應(yīng)式對(duì)象 ?*/ class CustomRefImpl<T> { ? public dep?: Dep = undefined ? private readonly _get: ReturnType<CustomRefFactory<T>>['get'] ? private readonly _set: ReturnType<CustomRefFactory<T>>['set'] ? public readonly __v_isRef = true ? constructor(factory: CustomRefFactory<T>) { ? ? const { get, set } = factory( ? ? ? () => trackRefValue(this), ? ? ? () => triggerRefValue(this) ? ? ) ? ? this._get = get ? ? this._set = set ? } ? get value() { ? ? return this._get() ? } ? set value(newVal) { ? ? this._set(newVal) ? } }
四. 源碼解析
Ref對(duì)象實(shí)際是代理的簡(jiǎn)化版,針對(duì)value設(shè)置了一個(gè)getter,setter讀取器。
這個(gè)讀取器可以對(duì)讀寫操作進(jìn)行攔截,因此可以進(jìn)行依賴的收集和更新。
同時(shí)又巧妙了對(duì)reactive做了一層封裝,假如傳入的是一個(gè)多層嵌套的復(fù)雜對(duì)象,最終是類似ref.value.a其實(shí)操作的已經(jīng)是reactive代理對(duì)象上的屬性,已經(jīng)和ref無(wú)關(guān)了。對(duì)于CustomRefImpl類型,其實(shí)核心和RefImpl是一樣的,更加精簡(jiǎn),只不過將Get方法和Set方法交給程序員自己去實(shí)現(xiàn)了。
只需要在這個(gè)Get方法里面調(diào)用track方法進(jìn)行依賴收集和在Set方法里面調(diào)用依賴更新即可。
示例代碼如下:
? ? let value = 1 ? ? const custom = customRef((track, trigger) => ({ ? ? ? get() { ? ? ? ? track() ? ? ? ? return value ? ? ? }, ? ? ? set(newValue: number) { ? ? ? ? value = newValue ? ? ? ? trigger() ? ? ? } ? ? })) ? ? let dummy ? ? effect(() => { ? ? ? dummy = custom.value? ? ? }) ? ? custom.value = 2 ? ? //此時(shí)dummy = 2;
五、總結(jié)
1. 收集依賴和觸發(fā)依賴的本質(zhì)
export const enum TrackOpTypes { ? GET = 'get', ? HAS = 'has', ? ITERATE = 'iterate' } export const enum TriggerOpTypes { ? SET = 'set', ? ADD = 'add', ? DELETE = 'delete', ? CLEAR = 'clear' }
以上時(shí)源碼中定義的收集依賴的和觸發(fā)依賴的類型。其實(shí)也就是當(dāng)涉及讀操作時(shí)收集依賴,當(dāng)設(shè)計(jì)寫操作時(shí)觸發(fā)依賴更新。
2. 響應(yīng)式對(duì)象本質(zhì)是對(duì)數(shù)據(jù)進(jìn)行了包裝,攔截了讀寫操作。
3. 上述篇幅并未講到集合類型代理的處理,原理其實(shí)一樣,有興趣的可以自行翻閱源碼。
4. 本篇幅只講到收集依賴和觸發(fā)依賴的時(shí)機(jī),并未講到如何收集和如何觸發(fā)。
這些僅為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
vue?跳轉(zhuǎn)頁(yè)面$router.resolve和$router.push案例詳解
這篇文章主要介紹了vue?跳轉(zhuǎn)頁(yè)面$router.resolve和$router.push案例詳解,這樣實(shí)現(xiàn)了既跳轉(zhuǎn)了新頁(yè)面,又不會(huì)讓后端檢測(cè)到頁(yè)面鏈接不安全之類的,需要的朋友可以參考下2023-10-10vue.js實(shí)現(xiàn)的經(jīng)典計(jì)算器/科學(xué)計(jì)算器功能示例
這篇文章主要介紹了vue.js實(shí)現(xiàn)的經(jīng)典計(jì)算器/科學(xué)計(jì)算器功能,具有基本四則運(yùn)算計(jì)算器以及科學(xué)計(jì)算器的功能,可實(shí)現(xiàn)開方、乘方、三角函數(shù)以及公式運(yùn)算等功能,需要的朋友可以參考下2018-07-07Vue移動(dòng)端實(shí)現(xiàn)pdf/excel/圖片在線預(yù)覽
這篇文章主要為大家詳細(xì)介紹了Vue移動(dòng)端實(shí)現(xiàn)pdf/excel/圖片在線預(yù)覽功能的相關(guān)方法,文中的示例代碼講解詳細(xì),有需要的小伙伴可以參考下2024-04-04VUE使用axios調(diào)用后臺(tái)API接口的方法
這篇文章主要介紹了VUE使用axios調(diào)用后臺(tái)API接口的方法,文中講解非常細(xì)致,代碼幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下2020-08-08