看完這篇文章,Vue3一點也不難!

前言

感受時間好快啊,也有一個月多沒更文了。這圖片仍是2020年產的呢~html

開啓2021年之旅,這篇文章是2021年首發...但願讀者掘友多多支持哈~vue

整理了一下這段日子裏學習的Vue3,我想以後Vue3會成爲一種趨勢。那就捉急學起來吧~react

setup

這個api的調用時機:建立組件實例,而後初始化 props ,緊接着就調用setup 函數。從生命週期鉤子的視角來看,它會在 beforeCreate 鉤子以前被調用。ios

咱們能夠在這個函數寫大部分的業務邏輯,在Vue2中咱們是經過每一個選項的形式將代碼邏輯分離開,好比methods,data,watch選項。Vue3如今 改變了這樣的模式(ps:也不能說改變吧,由於它也是能兼容Vue2的寫法)ajax

這難道不是三國的理論嗎?分久必合,合久並分axios

setup 是有2個可選的參數。api

  • props --- 屬性 (響應式對象 且 能夠監聽(watch))
  • context上下文對象---使用這個對象不再用擔憂this到底指向哪裏啦

生成響應式對象

如何理解響應式對象?我在另一篇文章中使用例子講述過一個場景,可查看:【Vue.js進階】總結我從Vue源碼學到了什麼(上)數組

如今來看看Vue3是如何生成響應式數據markdown

reactive

該函數接收一個對象做爲參數,並返回一個代理對象。reactive函數生成的對象若是沒有合理的使用會丟失響應式。數據結構

先來看看什麼狀況下會失去響應式:

setup(){
    const obj=reactive({
      name:'金毛',
      age:10
    });
    return{
      ...obj, //失去響應式了 由於obj失去了引用了
    }
  },
複製代碼

如何解決這個失去響應式的問題?

return {
	 obj, //這樣寫那麼模板就要都基於obj來調取, 類型{{obj.age}}
 	...toRefs(obj)   //用toRefs包裝,必須是reactive生成的對象, 普通對象不能夠, 它把每一項都拿出來包了一下, 這樣就能夠在模板中使用 {{age}}。
}
複製代碼

至於toRefs是什麼接下來會講述,如今先知道它是能夠解決失去響應式的問題。

ref

由於reactive函數能夠代理一個對象,但沒法代理基本數據類型,因此須要使用ref函數來間接對基本數據類型進行處理。該函數對基本數據類型數據進行裝箱操做使得成爲一個響應式對象,能夠跟蹤數據變化。

<span @click="addN">快來點擊我唄~~</span>
 <span>{{n}}</span> 
複製代碼
setup(){
    
    const n=ref(1); //生成的n是一個對象, 這樣方便vue去監控它
    function addN(){
      console.log(n.value,n,'.....')
      n.value++;  //注意這裏要使用.value的形式, 由於n是對象, value纔是他的值
    }
    return {
      n,      //返回出去頁面的template纔可使用它, {{n}} 不須要.value
      addN,
    }
 }
複製代碼

如何實現ref?

在實現ref以前,咱們先來了解認識Vue3源碼中提供的tracktrigger這個兩個api。

track 和 trigger是依賴收集的核心

track 是用來跟蹤收集依賴 (收集 effect):接收三個參數。那trigger 用來觸發響應 (執行 effect)。(ps:文章的後續會講述如何去實現這兩個api,如今先留個疑問...)

這裏是利用js是單線程的,那就能夠在獲取值的時候進行攔截依賴收集,在設置更新值時觸發依賴的更新。因此ref的實現大體實現思路就能夠寫成:

function myRef(val: any) {
  let value = val
  const ref = {
    get value() {
      // 收集依賴
      track(r, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newVal: any) {
      if (newVal !== value) {
        value = newVal
        // 觸發響應
        trigger(r, TriggerOpTypes.SET, 'value')
      }
    }
  }
  return ref
}
複製代碼

toRef 與 toRefs

如上在reactive模塊的內容中經過使用toRefs來包裝生成的對象,那麼所生成的對象是指向了對象相應 property 的ref

利用Vue3官網的🌰來加深理解:

const state = reactive({
  foo: 1,
  bar: 2
})

const stateAsRefs = toRefs(state)
// ref 和 原始property 「連接」
state.foo++
console.log(stateAsRefs.foo.value) // 2

stateAsRefs.foo.value++
console.log(state.foo) // 3
複製代碼

toRef 和toRefs 實現原理是一致的。toRef用來把一個響應式對象的的某個 key 值轉換成 ref。而toRefs函數是把一個響應式對象的全部的key都轉成了ref。

如何實現toRef

由於target目標對象自己就是一個響應式數據,已經經歷過依賴收集,響應觸發的這個攔截了,因此在實現toRef時就不須要了。

這裏實現了toRef,那麼toRefs也天然而來就能夠實現了...(ps:經過遍歷獲得便可)

function toRef(target, key) {
    return {
        get value() {
            return target[key]
        },
        set value(newVal){
            target[key] = newVal
        }
    }
}
複製代碼

思考

在使用關於ref相關的內容時,咱們看到了在模板中沒有.value去獲取數據,可是在js代碼塊中使用.value去訪問屬性。

能夠總結爲Vue3的自動拆裝箱:

JS :須要經過.value訪問包裝對象

模板: 自動拆箱,就是在模板中不須要使用.value訪問

反作用

在Vue3中有一個反作用的概念,那什麼是反作用呢?

接下來說述的api就是和這個反作用相關!

effect & watchEffect

effect該函數用於定義反作用,它的參數就是反作用函數,這個函數可能會產生反作用。默認狀況下,這個反作用會先執行。

如何理解這個反作用?

能夠經過下面的代碼來知曉:

import { effect,reactive } from '@vue/reactivity';
// import {watchEffect} from '@vue/runtime-core';
// 使用 reactive() 函數定義響應式數據
const obj = reactive({ text: 'hello' })
// 使用 effect() 函數定義反作用函數
effect(() => {
     document.body.innerText = obj.text
})

// watchEffect(() => {
// document.body.innerText = obj.text
// })

// 一秒後修改響應式數據,這會觸發反作用函數從新執行
setTimeout(() => {
  obj.text += ' world'
}, 1000)
複製代碼

也就是說effect接收的回調函數cb就是一個反作用,當數據發生變化的時候,這個cb就會被觸發...

思考

import {effect,reactive } from '@vue/reactivity';
const obj = reactive({ a: 1 })
effect(() => {
   console.log(obj.foo)
}
obj.a++
obj.a++
obj.a++
//結果:2,3,4
複製代碼

當effect函數和響應式數據創建了聯繫,那麼只要響應式數據一發生改變,那麼effect函數回調就會被執行。也就是說變幾回執行幾回。這樣的性能是否是很差??

effect能夠傳遞第二個參數 { scheduler: XXX }, 指定調度器:XXX。

所謂調度器就是用來指定如何運行反作用函數的。

watchEffect函數就是基於這個調度器的原理來優化實現反作用。

import {reactive } from '@vue/reactivity';
 import {watchEffect} from '@vue/runtime-core';
const obj = reactive({ a: 1 })
watchEffect(() => {
   console.log(obj.foo)
}
obj.a++
obj.a++
obj.a++
//結果:4
複製代碼

那這個實現思路是什麼呢? 至關於用一個隊列queue來收集這些cb,在收集以前作一個判斷看看隊列是否已經存在這個cb。而後經過while循環執行隊列裏的cb便可。 僞代碼:

const queue=  [];
let dirty = false;
function queueHandle(job) {
  if (!queue.includes(job)) queue.push(job)
  if (!dirty) {
    dirty = true
    Promise.resolve().then(() => {
      let fn;
      while(fn = queue.shift()) {
        fn();
      }
    })
  }
}
複製代碼

通常在開發環境下不使用effect而是使用watchEffect。

異步反作用

剛剛上面講述的是在同步的狀況下,那麼異步的反作用好比數據發生變動時又會發生一次ajax請求。咱們沒辦法判斷哪一次的請求更快,這無形當中就給咱們帶來了不肯定性...

那如何解決這種不肯定呢?

想辦法清理失效時的回調,我所想的是經過在執行這一次的反作用時,清理上一次的異步反作用,使得以前掛起的異步操做無效。

Vue3經過在watchEffect傳入的回調函數中能夠接收一個 onInvalidate 函數做入參。

能夠基於effect實現大體的原理:

import { effect } from '@vue/reactivity'

function watchEffect(fn: (onInvalidate: (fn: () => void) => void) => void) {
  let cleanup: Function
  function onInvalidate(fn: Function) {
    cleanup = fn
  }
  // 封裝一下 effect
  // 在執行反作用函數以前,先使上一次無做用無效
  effect(() => {
    cleanup && cleanup();
    fn(onInvalidate)
  })
}
複製代碼

如何中止反作用

Vue3提供了一個stop函數,用來中止反作用。

在effect 函數會返回一個值,這個值其實就是 effect 自己。將這個返回值傳入到stop函數中,那麼後續在更改數據,都沒法實現effect 的回調函數被調用。

區別

watchEffect會維護與組件實例以及組件狀態 (是否被卸載等) 的關係,若是一個組件被卸載,那麼 watchEffect 也將被 stop,但 effect不會。

effect 是須要咱們收的去清楚反作用的,要否則它自己是不會主動被清除的。

watch

再來談談watch,它等同於組件的偵聽器。watch 須要偵聽特定的數據源,並在回調函數中執行反作用。默認狀況下,它也是惰性的,即只有當被偵聽的源發生變化時才執行回調。

這個api的實現和Vue2沒什麼很大的理解區別,關鍵Vue3在Vue2的基礎上又擴展了一些功能,相對於Vue2更加完美了...

// 特定響應式對象監聽
// 也可開啓immediate: true, 這個和2.0沒什麼區別
watch(
  text,
  () => {
    console.log("watch text:");
  }
);

// 特定響應式對象監聽 能夠獲取新舊值
watch(
  text,
 (newVal, oldVal) => {
    console.log("watch text:", newVal, oldVal);
  },
);

// 多響應式對象監聽
watch(
  [firstName,lastName],
 ([newFirst,newLast], [oldFirst,oldlast]) => {
   console.log(newFirst,'新的first值',newLast,'新的last值')
  },
  
);

複製代碼

與 watchEffect 比較,watch 容許咱們:

  • 懶執行反作用;
  • 更具體地說明什麼狀態應該觸發偵聽器從新運行;
  • 訪問偵聽狀態變化先後的值。

triggerRef

還記得Vue2的$forceUpdate這個強制性刷新嗎? 在Vue3中也有一個強制去觸發反作用的api。先來看看下面的代碼:

//shallowRef它只代理 ref 對象自己,也就是說只有 .value 是被代理的,而 .value 所引用的對象並無被代理
const shallow = shallowRef({
  greet: 'Hello, world'
})

// 第一次運行時記錄一次 "Hello, world"
watchEffect(() => {
  console.log(shallow.value.greet)
})

// 這不會觸發做用,由於 ref 很淺層
shallow.value.greet = 'Hello, universe'

// 手動觸發,記錄 "Hello, universe"
triggerRef(shallow)

複製代碼

生命週期這件事

2.x與 3.0的對照

beforeCreate -> 使用 setup()
  created -> 使用 setup()
  beforeMount -> onBeforeMount  ---只能在setup裏面使用
  mounted -> onMounted		---只能在setup裏面使用
  beforeUpdate -> onBeforeUpdate	---只能在setup裏面使用
  updated -> onUpdated		---只能在setup裏面使用
  beforeDestroy -> onBeforeUnmount		---只能在setup裏面使用
  destroyed -> onUnmounted		---只能在setup裏面使用
  errorCaptured -> onErrorCaptured		---只能在setup裏面使用
複製代碼

獲取真實的Dom元素

Vue2的ref獲取真實的dom元素this.$refs.XXX,而Vue3也是經過ref獲取真實的dom元素,可是寫法上發生更改。

<div v-for="item in list" :ref="setItemRef"></div>
  <p ref="content"></p>
複製代碼
import { ref, onBeforeUpdate, onUpdated } from 'vue'

export default {
setup() {
	//定義一個變量接收dom
  let itemRefs = [];
  let content=ref(null);
  const setItemRef = el => {
    itemRefs.push(el)
  }
  onBeforeUpdate(() => {
    itemRefs = []
  })
  onUpdated(() => {
    console.log(itemRefs)
  })
  //返出去的名稱要與dom的ref相同, 這樣就能夠接收到dom的回調
  return {
    itemRefs,
    setItemRef,
    content
  }
}
}
複製代碼

提供/注入--開發插件

以前開發一個公共組件或者是要封裝一個公共的插件時會將功能掛在原型上或者是使用mixins。

其實這樣子很差,掛在原型上使得Vue顯得臃腫並且還會命名衝突的可能,mixins混入會使得代碼跳躍,讓閱讀者的邏輯跳躍。

如今有個新的方案CompositionAPI,來實現插件的開發

1.能夠將插件的公共功能使用provide函數和inject函數包裝。

import {provide, inject} from 'vue';
// 這裏使用symbol就不會形成變量名的衝突了, 這個命名權交給用戶纔是真正合理的架構設計
const StoreSymbol = Symbol()

export function provideString(store){
  provide(StoreSymbol, store)  //
}

//目標插件
export function useString() {
  const store = inject(StoreSymbol)  
  /*拿到具體的值了,能夠作相關的功能了*/
  return store
}
複製代碼

2.在根組件進行數據的初始化,引入provideString函數,並進行數據傳輸

export default {
  setup(){
    // 一些初始化'配置/操做'能夠在這裏進行
    // 須要放在對應的根節點, 由於依賴provide 和 inject
     provideString({
       a:'可能我是axios',
       b:'可能我是一個message彈框'
     })
  }
}
複製代碼

3.在想要的組件中引入插件,完成相關的功能的使用

import { useString } from '../插件';

export default {
  setup(){
    const store = useString(); //可使用這個插件了
  }
}
複製代碼

Vue3響應式原理

vue2的響應式缺點:

  • 默認就會遞歸
  • 不支持數組改變長度來響應式
  • 對象不存在的屬性不會被攔截

如何實現

上述留下來的疑問,如今在這裏實現。

咱們先來分析一下Vue3響應式原理是如何實現的:

  • Vue3不在使用Object.defineProperty進行攔截了,相反替代的方案是ES6的Proxy
  • 依賴收集不是經過Dep類和Watch類,而是經過track函數將目標對象和反作用進行相關聯,經過trigger進行依賴的響應。
  • 反作用的以棧形式的方式進行存儲,先進後出的思想。

這只是大致的思路,實現過程當中還有挺多細節的分析,接下來一步一步講解...

第一步:

先來實現一個reactivity,實現這個函數要思考的問題是:

1.{a:{b:2}}像這種多層嵌套的對象,如何進行響應更新?

2.一個對象屢次調用reactivity函數,那麼應該怎麼處理?

3.一個對象的代理對象調用了reactivity函數,又應該怎麼處理?

4.如何去判斷對象是新增屬性仍是修改屬性?

//工具類函數
function isObject(obj){
  return typeof obj==='object' && obj!==null?true:false;
}
function isOwnKey(target,key){
  return Object.hasOwnProperty(target,key)?true:false;
}

複製代碼
function reactivity(target){
 return  createReactivity(target);
}
let toProxy=new WeakMap();   //用來鏈接目標對象(key)與代理對象(value)的關係
let toRaw=new WeakMap();   //用來鏈接代理對象(key)與目標對象(value)的關係

function createReactivity(target){
  if(!isObject(target)){
    return ;
  }
  let mapProxy=toProxy.get(target); //處理目標對象被響應式處理屢次的狀況
  if(mapProxy){
    return mapProxy;
  }
  if(toRaw.has(target)){  //處理目標對象的代理對象被響應式處理 let proxy=reactivity(obj);reactivity(proxy);
    return target;
  }
  let proxy=new Proxy(target,{
    get(target,key,receiver){
      let res=Reflect.get(target,key);
      //在獲取屬性的時候,收集依賴
      track(target,key);  ++++
      return isObject(res)?reactivity(res):res;  //遞歸實現多層嵌套的對象
    },
    set(target,key,value,receiver){
      let res=Reflect.set(target, name, value,receiver);  
      let oldValue=target[key];  //獲取老值,用於比較新值和老值的變化,改變了才修改
      
      /**經過判斷key是否已經存在來判斷是新增屬性仍是修改屬性,而且在新增的時候可能會改變原有老的屬性,這一點大多數人都不會被考慮到 */
      if(!isOwnKey(target,key)){
        console.log('新增屬性');
        //在設置屬性的時候發佈
        trigger(target,'add',key); +++
      }else if(oldValue!==value){
        console.log('修改屬性');
        trigger(target,'set',key); +++
      }
      return res;
    },
  })
  toProxy.set(target,proxy);
  toRaw.set(proxy,target);
  return proxy;
}
複製代碼
第二步:

如今來實現一個反作用effect函數,這個函數咱們考慮最簡單的方式,就是傳入一個fn做爲入參。

咱們用一個全局的隊列來存儲effect,這個隊列的存儲的方式正如我剛剛所說的是棧隊列的方式。

那麼在一上來effect就會默認執行一次,那麼先收集effect,而後利用js單線程的原理進行目標對象和effect進行相關聯。

let effectStack=[];  //棧隊列,先進後出

function effect(fn){
  let effect=createEffect(fn);
  effect();  //默認先執行一遍
}
function createEffect(){
  let effect=function(){
    run(effect,fn);
  }
  return effect;
}
//run執行函數,功能1.收集effect,2.執行fn
function run(effect,fn){
  //利用try-finally來防止當發生錯誤的時候也會執行finally的代碼
  //利用js是單線程執行的。先收集再關聯
  try{
    effectStack.push(effect);
    fn();
  }finally{
    effectStack.pop();
  }
}
複製代碼
第三步:

這一步的關鍵是在收集依賴的時候如何讓目標對象的key和effect產生聯繫。 在這裏有個特殊的數據結構:

{
   target:{
     key1:[effect1,effect12],
     key2:[effect3,effect4],
     key3:[effect5],
   }
 }
複製代碼

每一個目標對象(做爲key)都有對應的value(是個對象),而後對象(value)又映射了key和effect。

那麼在響應依賴的時候,由於咱們獲得了effect和目標對象所對應的key的關係了,那麼遍歷觸發便可。

let targetMap=new WeakMap();
//收集依賴
function track(target,key){
  let effect=effectStack[effectStack.length-1]; //從棧獲取effect看看是否有反作用
  if(effect){  //有關係才建立依賴
    let depMap=targetMap.get(target);
    if(!mapRarget){
      targetMap.set(target,(depMap=new Map()));
    }
    let deps=depMap.get(key);
    if(!deps){
      mapDep.set(key,(deps=new Set()));
    }
    if(!deps.has(effect)){
      deps.add(effect)
    }
  }
}
//響應依賴
function trigger(target,type,key){
  let depMap=targetMap.get(target);
  if(depMap){
   let deps= depMap.get(key);  //當前的key所對應的effect
   if(deps){
     deps.forEach(effect=>effect());
   }
  }
}

複製代碼

好了,到這裏Vue3的響應式原理基本上是實現了,固然你能夠看完本篇文章再去看看源碼,我相信你會更容易理解源碼了~

總結

在學習的過程當中,翻過Vue3的源碼看了3天,說實話還真挺頭疼。後來我決定不先看源碼了,先學會如何去使用Vue3開始,而後再想一想爲什麼會這麼用,最後到如何實現。

求知的路上真的是路漫漫其修遠兮,吾將上下而求索

參考資料

Vue3中文文檔

【Vue3官方教程】🎄萬字筆記 | 同步導學視頻

相關文章
相關標籤/搜索