欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

vue3源碼分析reactivity實(shí)現(xiàn)原理

 更新時(shí)間:2023年01月18日 15:38:00   作者:豬豬愛前端  
這篇文章主要為大家介紹了vue3源碼分析reactivity實(shí)現(xiàn)原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

引言

上一章中我們解析了createApp到底發(fā)生了什么? 本來我們應(yīng)該繼續(xù)向下解析mount方法的,但是后面很多地方涉及到了響應(yīng)式的api也就是reactivity的api,所以我們必須要單獨(dú)將這一章拎出來做單獨(dú)的講解。本章主要分析內(nèi)容:

第一部分:簡單版reactivity

  • 響應(yīng)式主要是為了實(shí)現(xiàn)如下效果
//設(shè)置響應(yīng)式對(duì)象
const proxy = reactive({a:1})
//當(dāng)proxy.a發(fā)生變化的時(shí)候,調(diào)用effect中的回調(diào)函數(shù)
effect(()=>{
 console.log(proxy.a)
})
proxy.a++

下面我們先來設(shè)計(jì)一個(gè)方案實(shí)現(xiàn)這樣的效果

  • 在effect中有一個(gè)回調(diào)函數(shù),當(dāng)回調(diào)函數(shù)第一次執(zhí)行的時(shí)候我們需要監(jiān)聽到這個(gè)函數(shù)內(nèi)部有哪些響應(yīng)式對(duì)象
  • 如何讓內(nèi)部響應(yīng)式和當(dāng)前執(zhí)行的這個(gè)函數(shù)產(chǎn)生關(guān)聯(lián)呢?我們可以在即將執(zhí)行這個(gè)回調(diào)函數(shù)的時(shí)候設(shè)置一個(gè)全局變量activeEffect,讓當(dāng)前即將執(zhí)行的副作用函數(shù)為activeEffect,然后在響應(yīng)式內(nèi)部的get中收集到這個(gè)函數(shù),執(zhí)行完這個(gè)函數(shù)立刻設(shè)置activeEffect為null,這樣就不會(huì)影響其他收集依賴的執(zhí)行。
  • 當(dāng)這個(gè)proxy發(fā)生變化的時(shí)候立刻找到這個(gè)收集到的依賴項(xiàng)觸發(fā)就實(shí)現(xiàn)了這樣的效果。
  • 思考一下這是一個(gè)怎樣的結(jié)構(gòu)?首先對(duì)象和對(duì)象的建對(duì)應(yīng)一個(gè)依賴,依賴可能有多個(gè),考慮到如果對(duì)象失去引用,那么依賴將不可能被調(diào)用,我們 采用weakMap結(jié)構(gòu),一個(gè)對(duì)象對(duì)應(yīng)一個(gè)depsMap,而對(duì)象含有多個(gè)key,一個(gè)對(duì)象加一個(gè)key對(duì)應(yīng)dep依賴集合,一個(gè)對(duì)象和一個(gè)key可能被多次使用在不同effect中,可能有多個(gè)依賴,所以dep類型為Set,也就是如下結(jié)構(gòu)
reactiveMap = {
  [object Object]:{
     [key]:new Set()
  }
}

了解了整個(gè)設(shè)計(jì)流程我們開始書寫代碼:

(1).實(shí)現(xiàn)reactive和effect

1.當(dāng)effect函數(shù)即將開始執(zhí)行的時(shí)候設(shè)置全局變量

let activeEffect = null;
const reactiveMap = new WeakMap();
function effect(fn) {
  const reactEffect = new ReactiveEffect(fn);
  reactEffect.run();//第一次立刻執(zhí)行副作用函數(shù)
}
//存放副作用的類
class ReactiveEffect {
  constructor(fn, scheduler) {
    this.fn = fn;
    this.scheduler = scheduler;//調(diào)度器
  }
  run() {
    try {
      //執(zhí)行之前先修改activeEffect
      activeEffect = this;
      return this.fn();
    } finally {
      //執(zhí)行完設(shè)置activeEffect為null
      activeEffect = null;
    }
  }
}

2.創(chuàng)建響應(yīng)式函數(shù)reactive

function reactive(obj) {
  //獲取值的時(shí)候收集依賴
  const getter = function (object, key, receiver) {
    const res = Reflect.get(object, key, receiver); //獲取真實(shí)的值
    track(object, key, res);
    if (typeof res === "object") {
      return reactive(res);
    }
    return res;
  };
  //當(dāng)設(shè)置值的時(shí)候觸發(fā)依賴
  const setter = function (object, key, value, receiver) {
    const res = Reflect.set(object, key, value, receiver);
    trigger(object, key, value, res);
    return res;
  };
  const mutations = {
    get: getter,
    set: setter,
  };
  const proxy = new Proxy(obj, mutations);
  return proxy;
}

3.實(shí)現(xiàn)track和trigger函數(shù)

function track(object, key, oldValue) {
  //首先看看之前是否有這個(gè)對(duì)象的depsMap
  //如果沒有表示是第一次收集創(chuàng)建一個(gè)new Map
  let depsMap = reactiveMap.get(object);
  if (!depsMap) {
    reactiveMap.set(object, (depsMap = new Map()));
  }
  //如果是第一次收集這個(gè)key,則創(chuàng)建一個(gè)新的dep依賴
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  找到這個(gè)target和key對(duì)應(yīng)的依賴之后進(jìn)行副作用收集
  trackEffects(dep);
}
function trackEffects(dep) {
  //因?yàn)樵O(shè)置的對(duì)象是響應(yīng)式的所以只要
  //響應(yīng)式對(duì)象改變都會(huì)收集,但是只有
  //在effect執(zhí)行的時(shí)候activeEffect才有值
  //才能收集到依賴并且dep采用了集合防止
  //重復(fù)收集同一個(gè)依賴
  if (activeEffect) {
    dep.add(activeEffect);
  }
}
//當(dāng)修改值的時(shí)候觸發(fā)依賴函數(shù)
function trigger(object, key, newVal, oldVal) {
  const depsMap = reactiveMap.get(object);
  const dep = depsMap.get(key);//找到target和key對(duì)應(yīng)的dep
  //執(zhí)行依賴函數(shù)
  if (dep.size > 0) {
    for (const effect of dep) {
      if (effect.scheduler) {
        effect.scheduler();
      } else effect.run();
    }
  }
}

這就是reactivity最核心的邏輯,是不是覺得非常簡單呢?目前我們代理的是對(duì)象,那如果我們代理的時(shí)候是一個(gè)值呢?那就要使用到ref,下面我們來寫寫極簡版的ref實(shí)現(xiàn)吧!

(2).實(shí)現(xiàn)ref

  • 我們可以采用把值包裝成一個(gè)對(duì)象的方法,利用類自帶的攔截器當(dāng)get的時(shí)候收集依賴,那么收集依賴需要target和key,顯然target就是RefImpl實(shí)例,而key就是value,同樣在set的時(shí)候觸發(fā)依賴就實(shí)現(xiàn)了ref
function ref(value) {
  return createRef(value);
}
function createRef(value) {
  return new RefImpl(value);
}
class RefImpl {
  constructor(value) {
    this.__v_isRef = true;
    this._value = value;
  }
  get value() {
    track(this, "value");
    return this._value;
  }
  set value(value) {
    this._value = value;
    trigger(this, "value", value, this._value);
    return true;
  }
}

(3).實(shí)現(xiàn)computed

說到computed,他是如何實(shí)現(xiàn)的呢?我們先來說說他的要求,computed接受一個(gè)getter,必須有返回值,當(dāng)內(nèi)部收集到的響應(yīng)式發(fā)生改變的時(shí)候我們?nèi)プx取compute.value也會(huì)發(fā)生相應(yīng)的變化,并且computed返回的對(duì)象也是響應(yīng)式的例如:

//設(shè)置響應(yīng)式
const proxy = reactive({ a: 1, b: { a: 1 } });
//設(shè)置計(jì)算屬性
const comp = computed(() => {
  return proxy.a + 1;
});
effect(() => {
  console.log(comp.value);
});
//當(dāng)proxy.a發(fā)生變化,讀取comp的value也會(huì)發(fā)生變化,并且因?yàn)閏omp是響應(yīng)式
//在effect中被收集了,所以當(dāng)proxy.a發(fā)生變化也會(huì)導(dǎo)致effect中的函數(shù)執(zhí)行
proxy.a++;

下面我們來看看他的實(shí)現(xiàn)

這里必須要說一個(gè)scheduler,ReactiveEffect接受兩個(gè)參數(shù)如果有第二個(gè)參數(shù),那么就不會(huì)調(diào)用run方法而是調(diào)用scheduler方法。

所以computed的實(shí)現(xiàn)原理就是,當(dāng)執(zhí)行computed這個(gè)函數(shù)的時(shí)候創(chuàng)建ComputedRefImpl,而構(gòu)造器中會(huì)自動(dòng)創(chuàng)建ReactiveEffet,這個(gè)時(shí)候會(huì)傳遞一個(gè)schduler,也就是說以后這個(gè)effect不會(huì)調(diào)用run方法而是調(diào)用schduler方法,我們只需要在在shcduler方法中設(shè)置dirty為true表示修改了值,然后在進(jìn)行調(diào)度,通過comp.value收集到的依賴就可以了,這里的響應(yīng)式其實(shí)有兩個(gè)地方,第一個(gè)地方是computed內(nèi)部有一個(gè)響應(yīng)式,第二是comp本身也是響應(yīng)式需要收集依賴,當(dāng)computed內(nèi)部響應(yīng)式發(fā)生變化會(huì)導(dǎo)致this._effect.scheduler執(zhí)行,那么dirty會(huì)設(shè)置為true,當(dāng)comp.value在其他effect中的時(shí)候會(huì)觸發(fā)track收集依賴,所以當(dāng)computed內(nèi)部響應(yīng)式發(fā)生改變就會(huì)觸發(fā)get時(shí)候收集到的effect。

class ComputedRefImpl {
  constructor(getter) {
    //調(diào)度器
    this._effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true;
        //修改了getter中監(jiān)聽到的值
        //引起對(duì)dep內(nèi)的更新
        for (const effect of this.dep) {
          if (effect.scheduler) {
            effect.scheduler();
          } else effect.run();
        }
      }
    });
    this.dep = new Set(); //依賴
    this._dirty = true; //是否需要更新
    this._value = null;
  }
  get value() {
    trackEffects(this.dep); //收集依賴
    if (this._dirty) {
      this._dirty = false;
      this._value = this._effect.run();
    }
    return this._value;
  }
}

最后我們來試試效果吧

const proxy = reactive({ a: 1, b: { a: 1 } });
const comp = computed(() => {
  return proxy.a + 1;
});
const proxyRef = ref(100);
effect(() => {
  console.log(proxy.b.a);
  console.log(comp.value);
});
effect(() => {
  console.log(proxyRef.value);
});
proxy.a++;
proxy.b.a++;
proxyRef.value++;
//log:1 2 100 1 3 2 3 101

好啦! 看了reactivity的建議版本實(shí)現(xiàn),相信你已經(jīng)基本了解了reactivety,我們開始分析源碼吧!

第二部分:深入分析對(duì)于object、array的響應(yīng)式代理

  • 我們重reactivity包最常用的api,reactive開始進(jìn)行分析,因?yàn)椴捎昧斯S函數(shù),所以對(duì)應(yīng)的shallow,readonly,shallowReadonly也會(huì)分析到。
  • 我們先來看看reactive函數(shù)
//深度代理
export function reactive(target) {
  //如果被代理的是readonly返回已經(jīng)被readonly代理過的target
  if (isReadonly(target)) {
    return target;
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  );
}
//只代理第一層
export function shallowReactive(target) {
  return createReactiveObject(
    target,
    false,
    shallowReactiveHandlers,
    shallowCollectionHandlers,
    shallowReactiveMap
  );
}
//代理只讀屬性
export function readonly(target) {
  return createReactiveObject(
    target,
    true,
    readonlyHandlers,
    readonlyCollectionHandlers,
    readonlyMap
  );
}
//只代理只讀的第一層
export function shallowReadonly(target) {
  return createReactiveObject(
    target,
    true,
    shallowReadonlyHandlers,
    shallowReadonlyCollectionHandlers,
    shallowReadonlyMap
  );
}

我們發(fā)現(xiàn)這四個(gè)api本質(zhì)都是調(diào)用了createReactiveObject,但是他們傳遞的參數(shù)是不同的,對(duì)于不同的代理handlers處理是不同的,而其中還有對(duì)于map set等的代理就需要使用到collectionHandlers,對(duì)于代理過的對(duì)象我們?cè)俅螌?duì)這個(gè)對(duì)象進(jìn)行代理是不必要的,需要reactiveMap進(jìn)行緩存。已經(jīng)代理過的對(duì)象讀取緩存就可以了。

接下來我們深入createReactiveObject,先來看看源代碼

export function createReactiveObject(
  target,
  isReadonly,
  baseHandlers,
  collectionHandlers,
  proxyMap
) {
  //不能夠代理非對(duì)象
  if (!isObject(target)) {
    {
      console.warn(`value cannot be made reactive: ${String(target)}`);
    }
    return target;
  }
  //已經(jīng)代理過的對(duì)象不需要在進(jìn)行二次代理
  if (target[RAW] && !(isReadonly && target[IS_REACTIVE])) {
    return target;
  }
  //防止重復(fù)代理
  const existingProxy = proxyMap.get(target);
  if (existingProxy) {
    return existingProxy;
  }
  //獲取當(dāng)前被代理對(duì)象的類型
  //為0表示被代理對(duì)象為不可拓展對(duì)象
  //或者當(dāng)前對(duì)象含有__v_skip屬性
  //為1表示Array,Object類型用baseHandlers處理
  //為2表示map set weakMap weakSet 用collectionHandlers處理
  const targetType = getTargetType(target);
  //不可代理 返回原對(duì)象
  if (targetType === 0) {
    console.warn(`current target:${target} can not been proxy!`);
    return target;
  }
  //進(jìn)行代理
  const proxy = new Proxy(
    target,
    //判斷當(dāng)前代理對(duì)象的類型,如果是array object采用baseHandlers
    //如果是map set weakMap weakSet采用collectionHandlers
    targetType === 2 ? collectionHandlers : baseHandlers
  );
  proxyMap.set(target, proxy);
  //返回代理成功的對(duì)象
  return proxy;
}
  • 這個(gè)函數(shù)比較簡單,首先是第一種情況,調(diào)用了 reactive(target) 然后再次調(diào)用 reactive(target) 會(huì)返回同一個(gè)proxy代理對(duì)象,因?yàn)閮?nèi)部建立了reactiveMap緩存
  • 第二種情況是得到了 proxy = reactive(target) 然后再對(duì)proxy進(jìn)行代理reactive(proxy) 這樣的為了防止二次代理,最終會(huì)選擇返回proxy。
  • 當(dāng)然還會(huì)判斷對(duì)于不是對(duì)象的,是不能夠進(jìn)行代理的
  • 之后還通過targetType判斷了當(dāng)前代理的類型,對(duì)于不同的類型使用不同的代理方式,我們順便來看看getTargetType函數(shù)
//如果對(duì)象帶有__v_skip或則對(duì)象不可拓展則不可代理
//然后根據(jù)類型判斷需要哪種函數(shù)進(jìn)行代理
export function getTargetType(value) {
  return value[SKIP] || !Object.isExtensible(value)
    ? 0
    : targetTypeMap(toRawType(value));
}
//對(duì)截取的類型判斷 如果是object array返回1
//如果是set map weakMap weakSet返回2
export function targetTypeMap(rawType) {
  switch (rawType) {
    case "Object":
    case "Array":
      return 1;
    case "Map":
    case "Set":
    case "WeakMap":
    case "WeakSet":
      return 2;
    default:
      return 0;
  }
}
//截取類型
export const toRawType = (value) => {
  //截取[object Object]中的"Object"
  return Object.prototype.toString.call(value).slice(8, -1);
};

本部分我們僅討論對(duì)于object和array類型的代理,所以我們跳過collectionHandlers的實(shí)現(xiàn),現(xiàn)在我們來看看baseHandlers,baseHandlers顯然是根據(jù)shallow readonly不同傳遞的不同的handlers,其中包含:

  • mutableHandlers
  • shallowReadonlyHandlers
  • readonlyHandlers
  • shallowReactiveHandlers 我們看看他是如何創(chuàng)建這四個(gè)handlers的吧!
//reactive的proxy handlers
//這個(gè)便是new Proxy()中的第二個(gè)參數(shù),可以攔截get
//set deleteProperty has ownKeys等進(jìn)行處理
const mutableHandlers = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys,
};
//處理readonly的proxy handler
const readonlyHandlers = {
  get: readonlyGet,
  //對(duì)于readonly的handlers不需要set值
  //打印警告,但是不修改值
  set(target, key) {
    {
      warn(
        `Set operation on key "${String(key)}" failed: target is readonly.`,
        target
      );
    }
    return true;
  },
  //對(duì)于只讀屬性,不能刪除值
  deleteProperty(target, key) {
    {
      warn(
        `Delete operation on key "${String(key)}" failed: target is readonly.`,
        target
      );
    }
    return true;
  },
};
//處理只代理第一層的proxy handler
const shallowReactiveHandlers = shared.extend({}, mutableHandlers, {
  get: shallowGet,
  set: shallowSet,
});
//處理只對(duì)第一層做只讀代理的proxy handler
const shallowReadonlyHandlers = shared.extend({}, readonlyHandlers, {
  get: shallowReadonlyGet,
});
//這里的shared.extend就是Object的assign方法
//shared.extend = Object.assgin

顯然,在以上代碼中出現(xiàn)了幾個(gè)代理函數(shù)分別是getter setter deleteProperty ownKeys has,接下來我們便對(duì)每一個(gè)進(jìn)行分析。

(1).handlers中的getter

  • 我們發(fā)現(xiàn)對(duì)于getter,有shallowGet、readonlyGet、shallowReadonlyGet以及get,我們看看是如何得到這些方法的。
const get = createGetter();
const shallowGet = createGetter(false, true);
const readonlyGet = createGetter(true, false);
const shallowReadonlyGet = createGetter(true, true);

他們都調(diào)用了createGetter方法,這是一個(gè)工廠函數(shù),通過傳遞isReadonly isShallow來判斷是哪種類型的getter,然后創(chuàng)建不同的get。所以接下來我們自然而然需要分析createGetter函數(shù)。

//創(chuàng)造getter的工廠函數(shù),通過是否是只讀和
//是否只代理第一層創(chuàng)造不同的getter函數(shù)
export function createGetter(isReadonly = false, shallow = false) {
  //傳遞進(jìn)入Proxy的get函數(shù)
  //例如const obj = {a:2}
  //    const proxy = new Proxy(obj,{
  //  get(target,key,receiver){
  //    當(dāng)通過proxy.a對(duì)obj進(jìn)行訪問的時(shí)候,會(huì)先進(jìn)入這個(gè)函數(shù)
  //     返回值將會(huì)作為proxy.a獲得的值
  //   }
  // })
  return function get(target, key, receiver) {
    //1.對(duì)isReadonly isShallow等方法的處理
    //以下前面幾個(gè)判斷都是為了通過一些關(guān)鍵key判斷
    //當(dāng)前的對(duì)象是否是被代理的,或者是否是只讀的
    //是否是只代理第一層的。
    //假設(shè)當(dāng)前我們的代理是reactive類型
    //如果我們?cè)L問__v_isReactive那么返回值應(yīng)該為true
    //同理訪問readonly類型則返回false
    //故而這里取反
    if (key === IS_REACTIVE) {
      return !isReadonly;
    }
    //訪問__v_isReadonly返回isReadonly真實(shí)值即可
    else if (key === IS_READONLY) {
      return isReadonly;
    }
    //訪問__v_isShallow 返回shallow真實(shí)值即可
    else if (key === IS_SHALLOW) {
      return shallow;
    }
    //當(dāng)訪問__v_raw的時(shí)候,根據(jù)當(dāng)前的readonly和shallow屬性
    //訪問不同的map表,通過map表獲得代理前的對(duì)象
    else if (
      key === RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {
      return target;
    }
    //判斷當(dāng)前target是否是數(shù)組
    const targetIsArray = isArray(target);
    //如果調(diào)用的push pop shift unshift splice includes indexOf lastIndexOf
    //攔截這個(gè)方法
    if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver);
    }
    //獲取訪問的真實(shí)值
    const res = Reflect.get(target, key, receiver);
    //判斷當(dāng)前訪問的key是否是內(nèi)置的Symbol屬性或則是否是不需要track的key
    //例如__proto__ , __v_isRef , __isVue 如果是這些屬性則直接返回
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res;
    }
    //如果不是只讀屬性 開始收集依賴 只讀屬性不需要收集依賴
    if (!isReadonly) {
      track(target, trackOpTypes.get, key);
    }
    //只需要代理一層,不用再進(jìn)行代理了返回即可
    if (shallow) {
      return res;
    }
    //如果是訪問到的value是ref類型,返回res.value
    //訪問的是數(shù)組的數(shù)字屬性則返回res
    if (isRef(res)) {
      return targetIsArray && isIntegerKey(key) ? res : res.value;
    }
    //如果得到的結(jié)果依然是對(duì)象繼續(xù)進(jìn)行深度代理
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res);
    }
    return res;
  };
}
  • 首先對(duì)于已經(jīng)進(jìn)行了代理的對(duì)象,可以通過判斷key=__v_isReactive,__v_isShallow,__v_isReadonly判斷是否是 reactive,shallow,readonly, 當(dāng)然這也是isReactive、isReadonly等api實(shí)現(xiàn)基礎(chǔ)。
  • 之后對(duì)于某些特殊屬性的訪問我們也不需要去收集依賴?yán)?[Symbol.iterator]。
  • 如果不是只讀的代理,就需要收集依賴方便后續(xù)effect調(diào)用。
  • 如果訪問到的value還是一個(gè)對(duì)象我們還需要進(jìn)行深度代理。

isNonTrackableKeys函數(shù)、builtInSymbols、如果數(shù)組調(diào)用了push pop includes方法該怎么處理呢?

//這里貼上源碼,感興趣的仔細(xì)閱讀,不在進(jìn)行講解
const isNonTrackableKeys = makeMap(`__proto__,__v_isRef,__isVue`);
function makeMap(str, expectsLowerCase) {
  const map = Object.create(null); //創(chuàng)造一個(gè)空對(duì)象
  const list = str.split(","); //["__proto__","__isVUE__"]
  for (let i = 0; i < list.length; i++) {
    map[list[i]] = true;
    //{"__proto__":true,"__isVUE__":true}
  }
  //返回一個(gè)函數(shù),用于判斷是否是傳遞的str分割出來的某一個(gè)值
  //可以通過expectsLowerCase指定是否需要將分隔值轉(zhuǎn)化為小寫
  return expectsLowerCase
    ? (val) => !!map[val.toLowerCase()]
    : (val) => !!map[val];
}
//Symbol的所有屬性值
export const builtInSymbols = new Set(
  //首先獲取所有的Symbol的key
  Object.getOwnPropertyNames(Symbol)
     //過濾掉arguments和caller
    .filter((key) => key !== "arguments" && key !== "caller")
     //拿到所有的Symbol值
    .map((key) => Symbol[key])
    //過濾掉不是symbol的值
    .filter(shared.isSymbol)
);
  • buildInSymbols就是Symbol的所有內(nèi)置屬性key例如Symbol.iterator等。
  • 再來看看如何處理數(shù)組特殊方法的調(diào)用。
//當(dāng)前代理的對(duì)象是數(shù)組,且訪問了pop等8個(gè)方法中的一個(gè)
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
      //進(jìn)行代理
      return Reflect.get(arrayInstrumentations, key, receiver);
}
const arrayInstrumentations = createArrayInstrumentations();
function createArrayInstrumentations() {
  const instrumentations = {};
  //攔截?cái)?shù)組的方法 arr.includes()
  ["includes", "indexOf", "lastIndexOf"].forEach((key) => {
    instrumentations[key] = function (...args) {
      //這里的this指向調(diào)用當(dāng)前方法的數(shù)組
      const arr = toRaw(this);
      //將當(dāng)前數(shù)組中的所有元素收集依賴
      for (let i = 0, l = this.length; i < l; i++) {
        track(arr, trackOpTypes.get, i + "");
      }
      //執(zhí)行函數(shù)
      const res = arr[key](...args);
      if (res === -1 || res === false) {
        return arr[key](...args.map(toRaw));
      } else {
        return res;
      }
    };
  });
  //如果使用這些方法取消收集依賴
  ["push", "pop", "shift", "unshift", "splice"].forEach((key) => {
    instrumentations[key] = function (...args) {
      //停止收集依賴 將shouldTrack變?yōu)閒alse
      pauseTracking();
      //這里toRaw是為了防止二次執(zhí)行g(shù)etter,執(zhí)行數(shù)組對(duì)應(yīng)的方法
      const res = toRaw(this)[key].apply(this, args);
      //重新收集依賴,將shouldTrack變?yōu)閠rue
      resetTracking();
      return res;
    };
  });
  return instrumentations;
}
//中斷追蹤
export function pauseTracking() {
  trackStack.push(shouldTrack);
  shouldTrack = false;
}
//重設(shè)追蹤
export function resetTracking() {
  //獲取之前的shouldTrack值
  const last = trackStack.pop();
  //如果trackStack中沒有值shouldTrack設(shè)置為true
  shouldTrack = last === undefined ? true : last;
}
  • 首先,對(duì)于includes、indexOf、lastIndexOf會(huì)遍歷數(shù)組中的所有元素并且會(huì)有獲取的操作,也就是說數(shù)組所有元素都可能進(jìn)行訪問執(zhí)行g(shù)et,所以整個(gè)數(shù)組中的所有元素都必須要進(jìn)行track操作。
  • 對(duì)于pop等五個(gè)方法,依賴收集是混亂的,例如我執(zhí)行shift操作,對(duì)于底層來說就需要對(duì)元素進(jìn)行移動(dòng),這顯然會(huì)導(dǎo)致getter和setter的多次觸發(fā),所以我們必須要停止收集依賴。

好啦,接下來我們進(jìn)行track函數(shù)進(jìn)行分析,看看是如何收集依賴的。

export function track(target, type, key) {
  //當(dāng)調(diào)用了effect方法,會(huì)給activeEffect賦值
  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()));
    }
    //傳遞入生命周期鉤子的當(dāng)前effect的信息
    const eventInfo = { effect: activeEffect, target, type, key };
    trackEffects(dep, eventInfo);
  }
}
  • 這個(gè)方法相信大家已經(jīng)相當(dāng)?shù)氖煜ち税?!跟我們寫的簡易版reactivity是一樣的,就是通過target,key獲取依賴,沒有就創(chuàng)建。
  • 那么activeEffect是什么時(shí)候賦值的呢?相信在簡易版reactivity中大家已經(jīng)知道啦,就是在調(diào)用effect之前賦值,調(diào)用完成后變?yōu)閚ull,但是源碼的實(shí)現(xiàn)更加復(fù)雜,考慮的問題更加全面。
export class ReactiveEffect {
  constructor(fn, scheduler = null, scope) {
    this.fn = fn; //副作用函數(shù)
    //調(diào)度器(如果有調(diào)用器就不在執(zhí)行run方法而是執(zhí)行調(diào)度器)
    this.scheduler = scheduler;
    this.active = true;
    /**
     * 當(dāng)前副作用被那些變量所依賴
     * 例如:
     *  effect(()=>{
     *   console.log(proxy.a)
     *  })
     *  effect(()=>{
     *   console.log(proxy.a)
     *  })
     *
     *  每一個(gè)effect的回調(diào)函數(shù)都會(huì)產(chǎn)生一個(gè)ReactiveEffect實(shí)例
     *  第一個(gè)effect中有proxy.a被讀取,那么就會(huì)被收集依賴,則
     *  對(duì)于第一個(gè)ReactiveEffect實(shí)例來說deps中就有有proxy.a
     *  也就是target key 指向的dep,這個(gè)dep是一個(gè)集合,代表的是
     *  target key對(duì)應(yīng)的dep
     */
    this.deps = [];
    this.parent = undefined;
    //TODO recordEffectScope
    recordEffectScope(this, scope);
  }
  //開始執(zhí)行
  run() {
    if (!this.active) {
      return this.fn();
    }
    let parent = activeEffect;
    let lastShouldTrack = shouldTrack;
    while (parent) {
      if (parent === this) {
        return;
      }
      parent = parent.parent;
    }
    try {
      //可能有嵌套的effect,當(dāng)執(zhí)行到effect回調(diào)函數(shù)中有effect的時(shí)候
      //現(xiàn)在的activeEffect相當(dāng)于最新創(chuàng)建的effect的父級(jí)effect
      /*
        例如:effect(()=>{
            現(xiàn)在指向外部的effect
            console.log(proxy.a)
            effect(()=>{
              在這里面的時(shí)候activeEffect指向內(nèi)部effect
              console.log(proxy.b)
            })
            現(xiàn)在需要將activeEffect恢復(fù)為外部effect
            console.log(proxy.b)
        })
        當(dāng)然對(duì)應(yīng)的parent也應(yīng)該改變,這就是try finally的作用
      */
      this.parent = activeEffect;
      //讓當(dāng)前的activeEffect為當(dāng)前effect實(shí)例
      activeEffect = this;
      shouldTrack = true;
      //設(shè)置嵌套深度
      trackOpBit = 1 << ++effectTrackDepth;
      if (effectTrackDepth <= maxMarkerBits) {
        initDepMarkers(this);
      } else {
        cleanupEffect(this);
      }
      //執(zhí)行effect副作用
      return this.fn();
    } finally {
      //退出當(dāng)前effect回調(diào)函數(shù)的執(zhí)行,要將全局變量退回到當(dāng)前
      //effect的父級(jí)effect(回溯)
      if (effectTrackDepth <= maxMarkerBits) {
        finalizeDepMarkers(this);
      }
      //全部進(jìn)行回溯
      trackOpBit = 1 << --effectTrackDepth; //恢復(fù)trackOpBit
      activeEffect = this.parent;
      shouldTrack = lastShouldTrack;
      this.parent = undefined;
      if (this.deferStop) {
        this.stop();
      }
    }
  }
  stop() {
    if (activeEffect === this) {
      this.deferStop = true;
    } else if (this.active) {
      cleanupEffect(this);
      if (this.onStop) {
        this.onStop();
      }
      this.active = false;
    }
  }
}
  • 通過try finally解決了嵌套的effect activeEffect指向不明確問題。
  • 設(shè)置了effectOpBit表示當(dāng)前深度,超過30層則不能再嵌套了。
  • stop方法用于停止執(zhí)行副作用執(zhí)行。
  • 接下來我們繼續(xù)看trackEffects執(zhí)行。
//收集副作用
export function trackEffects(dep, debuggerEventExtraInfo) {
  let shouldTrack = false;
  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      dep.n |= trackOpBit;
      shouldTrack = !wasTracked(dep);
    }
  } else {
    //如果已經(jīng)收集過就不收集了
    shouldTrack = !dep.has(activeEffect);
  }
  //通過上面的判斷是否需要收集
  if (shouldTrack) {
    //往當(dāng)前target key對(duì)應(yīng)的dep中添加effect
    dep.add(activeEffect);
    //當(dāng)前effect中有哪些被代理的變量的dep
    activeEffect.deps.push(dep);
    //生命周期,當(dāng)真正執(zhí)行track的時(shí)候調(diào)用函數(shù)
    if (activeEffect.onTrack) {
      activeEffect.onTrack({
        effect: activeEffect,
        ...debuggerEventExtraInfo,
      });
    }
  }
}
  • 顯然這個(gè)函數(shù)就是用于收集effect到dep,同時(shí)構(gòu)建effect的deps(代表當(dāng)前effect中有哪些被代理過的變量指向的dep,例如proxy.a能指向一個(gè)dep,同時(shí)proxy.a在當(dāng)前effect回調(diào)函數(shù)中執(zhí)行,那么對(duì)于當(dāng)前effect來說deps中應(yīng)該包含代表proxy.a的dep)
  • 完成依賴收集我們就可以進(jìn)入setter的學(xué)習(xí)了!觸發(fā)依賴更新。

(2).handlers中的setter

//創(chuàng)造setter的工廠函數(shù)
export function createSetter(shallow) {
  return function set(target, key, value, receiver) {
    let oldValue = target[key]; //獲取代理對(duì)象之前的value
    //舊值是ref,新值不是ref
    if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
      return false;
    }
    //深度代理的情況
    if (!shallow) {
      if (!isShallow(value) && !isReadonly(value)) {
        //防止如果后面操作了value 引起二次setter
        oldValue = toRaw(oldValue);
        value = toRaw(value);
      }
      //target是對(duì)象且值為ref類型,當(dāng)對(duì)這個(gè)值修改的時(shí)候應(yīng)該修改ref.value
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value;
        return true;
      }
    }
    //判斷當(dāng)前訪問的key是否存在,不存在則是設(shè)置新的值
    const hadKey =
      //當(dāng)前的target為數(shù)組且訪問的是數(shù)字
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key);
    //設(shè)置value
    const result = Reflect.set(target, key, value, receiver);
    if (target === toRaw(receiver)) {
      //設(shè)置新的值
      if (!hadKey) {
        trigger(target, triggerOpTypes.add, key, value);
      }
      //修改老的值
      else if (hasChanged(value, oldValue)) {
        trigger(target, triggerOpTypes.set, key, value, oldValue);
      }
    }
    return result;
  };
}
  • 根據(jù)hadKey判斷當(dāng)前是修改值還是新增值,傳遞不同的類型進(jìn)行trigger(觸發(fā)更新)所以我們接下來繼續(xù)分析trigger
//根據(jù)不同的類型添加必要的副作用到deps中
export function trigger(target, type, key, newValue, oldValue, oldTarget) {
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return;
  }
  let deps = []; //當(dāng)前要處理的所有依賴
  if (type === triggerOpTypes.clear) {
    //清空,相當(dāng)于所有的元素都發(fā)生改變
    //故而全部都需要添加進(jìn)依賴
    deps = [...depsMap.values()];
  }
  //攔截修改數(shù)組長度的情況
  else if (key === "length" && isArray(target)) {
    //放入key為length或者數(shù)組下標(biāo)大于設(shè)置值的所以依賴
    //例如:const a = [1,2,3] a.length=1
    //那么數(shù)組長度發(fā)生了變化,2,3的依賴都應(yīng)該被放入
    depsMap.forEach((dep, key) => {
      if (key === "length" || key >= newValue) {
        deps.push(dep);
      }
    });
  }
  //其他的情況獲取之前在getter收集的依賴到deps中
  else {
    //將target key 指向的依賴放入deps中
    if (key !== void 0) {
      deps.push(depsMap.get(key));
    }
    //根據(jù)不同type添加不同的必要依賴到deps
    switch (type) {
      //處理添加新的值
      case triggerOpTypes.add:
        if (!isArray(target)) {
          //set或map
          deps.push(depsMap.get(ITERATE_KEY));
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY));
          }
        } else if (isIntegerKey(key)) {
          //當(dāng)前修改的是數(shù)組且是新增值
          //例如 arr.length = 3 arr[4] = 8
          //此時(shí)數(shù)組長度會(huì)發(fā)生改變所以當(dāng)前數(shù)組的
          //length屬性依然需要被放入依賴
          deps.push(depsMap.get("length"));
        }
        break;
      case triggerOpTypes.delete:
        //處理delete...
        break;
      case triggerOpTypes.set:
        //處理map類型...
    }
  }
  //當(dāng)前effect的信息
  const eventInfo = { target, type, key, newValue, oldValue, oldTarget };
  if (deps.length === 1) {
    if (deps[0]) {
      {
        triggerEffects(deps[0], eventInfo);
      }
    }
  } else {
    const effects = [];
    //扁平化所有的effect
    for (const dep of deps) {
      if (dep) {
        effects.push(...dep);
      }
    }
    //執(zhí)行所有的副作用
    triggerEffects(createDep(effects), eventInfo);
  }
}
//創(chuàng)建dep
export const createDep = (effects) => {
  const dep = new Set(effects);
  dep.w = 0;
  dep.n = 0;
  return dep;
};
  • 這個(gè)函數(shù)顯然就是處理邊際情況,收集所有的deps并調(diào)用triggerEffects進(jìn)行觸發(fā)。
  • triggerOpTypes一共有 "clear"、"set"、"delete"、"add",其中只有 "add" 是處理object和array的代理的。
  • "clear":當(dāng)觸發(fā)了clear表示清除當(dāng)前代理對(duì)象所有的元素,所有元素都被修改了,所以所有的dep都需要被添加到deps中。
  • "add":代表當(dāng)前是新增的值,對(duì)于數(shù)組來說如果訪問了比自身長度大的屬性,那么length屬性將被修改所以這種情況屬性 "length" 對(duì)應(yīng)的dep也應(yīng)該被放入 deps。
  • 對(duì)于數(shù)組假設(shè)數(shù)組長度是10,然后修改了數(shù)組length屬性例如arr.length = 3,那么相當(dāng)于刪除了7個(gè)元素,那么這7個(gè)元素對(duì)應(yīng)的dep應(yīng)當(dāng)放入deps中。
  • 接下來繼續(xù)調(diào)用triggerEffects觸發(fā)收集到的所有dep。
//根據(jù)trigger最終組成的deps觸發(fā)所有副作用執(zhí)行
function triggerEffects(dep, debuggerEventExtraInfo) {
  //拿到所有的effects 包裝成數(shù)組
  const effects = isArray(dep) ? dep : [...dep];
  //含有computed屬性先執(zhí)行
  for (const effect of effects) {
    if (effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo);
    }
  }
  //不含有computed屬性后執(zhí)行
  for (const effect of effects) {
    if (!effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo);
    }
  }
}
function triggerEffect(effect, debuggerEventExtraInfo) {
  if (effect !== activeEffect || effect.allowRecurse) {
    //生命周期,對(duì)于這個(gè)effect在進(jìn)行trigger的時(shí)候調(diào)用
    if (effect.onTrigger) {
      effect.onTrigger(shared.extend({ effect }, debuggerEventExtraInfo));
    }
    //如果有調(diào)度器則執(zhí)行調(diào)度器否則執(zhí)行run
    if (effect.scheduler) {
      effect.scheduler();
    } else {
      effect.run();
    }
  }
}
  • 含有computed屬性的先執(zhí)行,沒有的后執(zhí)行,有scheduler調(diào)用scheduler否則調(diào)用run。這樣就完成了觸發(fā)。

(3).handlers的deleteProperty

//處理刪除屬性的邏輯(統(tǒng)一處理)
//target:要?jiǎng)h除屬性的對(duì)象 key:要?jiǎng)h除對(duì)象值的鍵
export function deleteProperty(target, key) {
  const hadKey = hasOwn(target, key); //判斷刪除的屬性是否在
  const oldValue = target[key]; //獲得舊值
  //刪除屬性返回值為是否刪除成功
  const result = Reflect.deleteProperty(target, key);
  if (result && hadKey) {
    //觸發(fā)副作用
    trigger(target, triggerOpTypes.delete, key, undefined, oldValue);
  }
  return result;
}

當(dāng)調(diào)用delete obj.xxx的時(shí)候deleteProperty就會(huì)監(jiān)聽到,這顯然是修改值的情況所以我們執(zhí)行trigger,類型自然就是 "delete" ,還記得trigger中對(duì)于 "delete" 類型我們并沒有講解,下面我們看看這部分如何處理。

//將target key 指向的依賴放入deps中
    if (key !== void 0) {
      deps.push(depsMap.get(key));
}
//省略部分代碼...
case triggerOpTypes.delete:
  if (!isArray(target)) {
    //添加key為iterate的依賴,后面講這個(gè)依賴來自于哪里
    deps.push(depsMap.get(ITERATE_KEY));
    if (isMap(target)) {
      deps.push(depsMap.get(MAP_KEY_ITERATE_KEY));
    }
}
break;
//省略部分代碼...
  • 首先把刪除的那個(gè)元素的依賴放入deps中。
  • 如果刪除的是對(duì)象那么會(huì)添加key為ITERATE_KEY的依賴。這個(gè)key來自于ownKeys的攔截,當(dāng)在收集依賴的時(shí)候也就是在effect中寫了Object.keys、Object.getOwnPropertyNames、Object.getOwnPropertySymbols、又或者調(diào)用了Reflect.ownKeys。這樣的代碼就會(huì)觸發(fā)ownKeys的攔截這個(gè)時(shí)候其實(shí)就是track的類型就是ITERATE_KEY,也就是說如果你寫了Object.keys那么就會(huì)收集依賴,某一天你刪除了proxy上的屬性,同樣會(huì)觸發(fā)依賴更新。
const {reactive,effect} = require('./reactivity.cjs')
const proxy = reactive({a:1})
effect(()=>{
    Object.keys(proxy)
    console.log(111)
})
effect(()=>{
    proxy.a
    console.log(111)
})
delete proxy.a
//log: 111 111 111 111 

(4).handlers的ownKeys

//攔截Object.keys getOwnPropertyNames等
export function ownKeys(target) {
  track(target, "iterate", isArray(target) ? "length" : ITERATE_KEY);
  return Reflect.ownKeys(target);
}
  • track我們已經(jīng)分析過了,如果target是非數(shù)組元素,那么追蹤的key就是ITERATE_KEY這就是上面delete哪里的來源。

(5).handlers的has

//攔截foo in proxy foo in Object.create(proxy)
//with(proxy){foo} Reflect.has
export function has(target, key) {
  const result = Reflect.has(target, key); //判斷是否有這個(gè)屬性
  //不是Symbol或內(nèi)置Symbol屬性
  if (!isSymbol(key) || !builtInSymbols.has(key)) {
    track(target, "has", key);
  }
  return result;
}
  • has 同樣是判斷是否存在元素,不涉及修改,所以是track,傳遞類型為 "has",收集依賴即可,特殊的是has只能攔截注釋中的情況,getOwnProperty是不能攔截的。

好啦! 第二部分們已經(jīng)完成了所有的分析,但是本文還沒有完!因?yàn)槠^長,第三部分和第四部分我放在下一章節(jié)。我們最后再來總結(jié)一下吧!

本文總結(jié):

本文我們寫了一個(gè)簡單版本的reactivity,便于大家后續(xù)理解真正的源碼,然后我們分析了如何攔截array和object類型的數(shù)據(jù),總體來說就是在effect執(zhí)行的時(shí)候修改當(dāng)前activeEffect的指向,然后執(zhí)行effect的時(shí)候收集依賴通過proxy原生api攔截get has ownKeys的操作,完成依賴的收集,然后在set和delete的時(shí)候進(jìn)行觸發(fā),并且對(duì)邊際情況也進(jìn)行了處理、例如數(shù)組訪問修改length、使用pop push includes方法的處理等。

下文我們將會(huì)繼續(xù)分析對(duì)于map set weakMap weakSet的攔截以及ref computed等api的實(shí)現(xiàn),更多關(guān)于vue3源碼分析reactivity的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • vue中beforeRouteLeave實(shí)現(xiàn)頁面回退不刷新的示例代碼

    vue中beforeRouteLeave實(shí)現(xiàn)頁面回退不刷新的示例代碼

    這篇文章主要介紹了vue中beforeRouteLeave實(shí)現(xiàn)頁面回退不刷新的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-11-11
  • vue循環(huán)中點(diǎn)擊選中再點(diǎn)擊取消(單選)的實(shí)現(xiàn)

    vue循環(huán)中點(diǎn)擊選中再點(diǎn)擊取消(單選)的實(shí)現(xiàn)

    這篇文章主要介紹了vue循環(huán)中點(diǎn)擊選中再點(diǎn)擊取消(單選)的實(shí)現(xiàn),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2020-09-09
  • vue在.js文件中引入store和router問題

    vue在.js文件中引入store和router問題

    這篇文章主要介紹了vue在.js文件中引入store和router問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-03-03
  • 使用Vue-cli 3.0搭建Vue項(xiàng)目的方法

    使用Vue-cli 3.0搭建Vue項(xiàng)目的方法

    這篇文章主要介紹了使用Vue-cli 3.0搭建Vue項(xiàng)目的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2018-06-06
  • vue清除地址欄路由參數(shù)方式

    vue清除地址欄路由參數(shù)方式

    這篇文章主要介紹了vue清除地址欄路由參數(shù)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-01-01
  • vue3.0中的watch偵聽器實(shí)例詳解

    vue3.0中的watch偵聽器實(shí)例詳解

    雖然計(jì)算屬性在大多數(shù)情況下更合適,但有時(shí)也需要一個(gè)自定義的偵聽器,這就是為什么Vue通過watch選項(xiàng)提供了一個(gè)更通用的方法,來響應(yīng)數(shù)據(jù)的變化,這篇文章主要給大家介紹了關(guān)于vue3.0中watch偵聽器的相關(guān)資料,需要的朋友可以參考下
    2021-10-10
  • vue 倒計(jì)時(shí)結(jié)束跳轉(zhuǎn)頁面實(shí)現(xiàn)代碼

    vue 倒計(jì)時(shí)結(jié)束跳轉(zhuǎn)頁面實(shí)現(xiàn)代碼

    在商場(chǎng)大家經(jīng)常看到停車收費(fèi)倒計(jì)時(shí)支付頁面,今天小編通過本文給大家分享vue 倒計(jì)時(shí)結(jié)束跳轉(zhuǎn)頁面功能,感興趣的朋友一起看看吧
    2023-10-10
  • Vue.js實(shí)現(xiàn)在下拉列表區(qū)域外點(diǎn)擊即可關(guān)閉下拉列表的功能(自定義下拉列表)

    Vue.js實(shí)現(xiàn)在下拉列表區(qū)域外點(diǎn)擊即可關(guān)閉下拉列表的功能(自定義下拉列表)

    這篇文章主要介紹了Vue.js實(shí)現(xiàn)在下拉列表區(qū)域外點(diǎn)擊即可關(guān)閉下拉列表的功能(自定義下拉列表) ,需要的朋友可以參考下
    2017-05-05
  • Vue中Number方法將字符串轉(zhuǎn)換為數(shù)字的過程

    Vue中Number方法將字符串轉(zhuǎn)換為數(shù)字的過程

    這篇文章主要介紹了Vue中Number方法將字符串轉(zhuǎn)換為數(shù)字,本文通過示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2023-06-06
  • vue?ui的安裝步驟以及使用詳解

    vue?ui的安裝步驟以及使用詳解

    最近公司開發(fā)一個(gè)項(xiàng)目,采用的前后端分離的方式,前端采用vue,但是再項(xiàng)目開發(fā)過程中遇到一個(gè)比較麻煩的問題,下面這篇文章主要給大家介紹了關(guān)于vue?ui的安裝步驟以及使用的相關(guān)資料,需要的朋友可以參考下
    2022-08-08

最新評(píng)論