聊聊Vue3中Provide和Inject的實現原理

2022-02-16 22:00:17
本篇文章帶大家深入瞭解Vue3,詳細介紹一下Provide / Inject 的實現原理,希望對大家有所幫助!

Vue3 的 Provide / Inject 的實現原理其實就是巧妙利用了原型和原型鏈來實現的,所以在瞭解Vue3 的 Provide / Inject 的實現原理之前,我們先複習一下原型和原型鏈的知識。【相關推薦:】

原型和原型鏈的知識回顧

  • prototype 與 __proto__

prototype 一般稱為顯式原型,__proto__一般稱為隱式原型。 每一個函數在建立之後,在預設情況下,會擁有一個名為 prototype 的屬性,這個屬性表示函數的原型物件。

  • 原型鏈

當我們存取一個JS物件屬性的時候,JS先會在這個物件定義的屬性裡找,找不到就會沿著這個物件的__proto__這個隱式原型關聯起來的鏈條向上一個物件查詢,這個鏈條就叫原型鏈。

function Fn() {}
Fn.prototype.name = 'coboy'
let fn1 = new Fn()
fn1.age = 18
console.log(fn1.name) // coboy
console.log(fn1.age) // 18

fn1是Fn函數new出來的範例物件,fn1.age是這個範例物件上屬性,fn1.name則從Fn.prototype原型物件而來,因為fn1的__proto__隱式原型就是指向Fn這個函數的原型物件Fn.prototype。原型鏈某種意義上是讓一個參照型別繼承另一個參照型別的屬性和方法。

function Fn() {}
Fn.prototype.name = 'coboy'
let fn1 = new Fn()
fn1.name = 'cobyte'
console.log(fn1.name) // cobyte

當存取fn1這個範例物件的屬性name的時候,JS先會在fn1這個範例物件的屬性裡查詢,剛好fn1定義了一個name屬性,所以就直接返回自身屬性的值cobyte,否則就會繼續沿著原型鏈向Fn.prototype上去找,那麼就會返回coboy。

複習完原型和原型鏈的知識之後,我們就開始進入Provide/Inject的實現原理探索。

使用 Provide

setup() 中使用 provide 時,我們首先從 vue 顯式匯入 provide 方法。這使我們能夠呼叫 provide 來定義每個 property。

provide 函數允許你通過兩個引數定義 property

  • name (<String> 型別)

  • value

import { provide } from 'vue'

export default {
  setup() {
    provide('name', 'coboy')
  }
}

provide API實現原理

那麼這個provide API實現原理是什麼呢?

provide 函數可以簡化為

export function provide(key, value) {
    // 獲取當前元件範例
    const currentInstance: any = getCurrentInstance()
    if(currentInstance) {
        // 獲取當前元件範例上provides屬性
        let { provides } = currentInstance
        // 獲取當前父級元件的provides屬性
        const parentProvides = currentInstance.parent.provides
        // 如果當前的provides和父級的provides相同則說明還沒賦值
        if(provides === parentProvides) {
            // Object.create() es6建立物件的另一種方式,可以理解為繼承一個物件, 新增的屬性是在原型下。
            provides = currentInstance.provides = Object.create(parentProvides)
        }
        provides[key] = value
    }
}

綜上所述provide API就是通過獲取當前元件的範例物件,將傳進來的資料儲存在當前的元件範例物件上的provides上,並且通過ES6的新API Object.create把父元件的provides屬性設定到當前的元件範例物件的provides屬性的原型物件上。

元件範例物件初始化時provides屬性的處理

原始碼位置:runtime-core/src/component.ts

1.png

我們通過檢視instance物件的原始碼,可以看到,在instance元件範例物件上,存在parent和provides兩個屬性。在初始化的時候如果存在父元件則把父元件的provides賦值給當前的元件範例物件的provides,如果沒有就建立一個新的物件,並且把應用上下文的provides屬性設定為新物件的原型物件上的屬性。

使用 Inject

setup() 中使用 inject 時,也需要從 vue 顯式匯入。匯入以後,我們就可以呼叫它來定義暴露給我們的元件方式。

inject 函數有兩個引數:

  • 要 inject 的 property 的 name

  • 預設值 (可選)

import { inject } from 'vue'

export default {
  setup() {
    const name = inject('name', 'cobyte')
    return {
      name
    }
  }
}

inject API實現原理

那麼這個inject API實現原理是什麼呢?

inject 函數可以簡化為

export function inject(
  key,
  defaultValue,
  treatDefaultAsFactory = false
) {
  // 獲取當前元件範例物件
  const instance = currentInstance || currentRenderingInstance
  if (instance) {
    // 如果intance位於根目錄下,則返回到appContext的provides,否則就返回父元件的provides
    const provides =
      instance.parent == null
        ? instance.vnode.appContext && instance.vnode.appContext.provides
        : instance.parent.provides

    if (provides && key in provides) {
      return provides[key]
    } else if (arguments.length > 1) {
      // 如果存在1個引數以上
      return treatDefaultAsFactory && isFunction(defaultValue)
        // 如果預設內容是個函數的,就執行並且通過call方法把元件範例的代理物件繫結到該函數的this上
        ? defaultValue.call(instance.proxy) 
        : defaultValue
    }
  }
}

通過inject原始碼分析我們可以知道,inject裡面先獲取當前元件的範例物件,然後判斷是否根元件,如果是根元件則返回到appContext的provides,否則就返回父元件的provides。

如果當前獲取的key在provides上有值,那麼就返回該值,如果沒有則判斷是否存在預設內容,預設內容如果是個函數,就執行並且通過call方法把元件範例的代理物件繫結到該函數的this上,否則就直接返回預設內容。

provide/inject實現原理總結

通過上面的分析,可以得知provide/inject實現原理還是比較簡單的,就是巧妙地利用了原型和原型鏈進行資料的繼承和獲取。provide API呼叫設定的時候,設定父級的provides為當前provides物件原型物件上的屬性,在inject獲取provides物件中的屬性值時,優先獲取provides物件自身的屬性,如果自身查詢不到,則沿著原型鏈向上一個物件中去查詢。

拓展:Object.create原理

方法說明

  • Object.create()方法建立一個新的物件,並以方法的第一個引數作為新物件的__proto__屬性的值(以第一個引數作為新物件的建構函式的原型物件)
  • Object.create()方法還有第二個可選引數,是一個物件,物件的每個屬性都會作為新物件的自身屬性,物件的屬性值以descriptor(Object.getOwnPropertyDescriptor(obj, 'key'))的形式出現,且enumerable預設為false

原始碼模擬

Object.myCreate = function (proto, propertyObject = undefined) {
    if (propertyObject === null) {
        // 這裡沒有判斷propertyObject是否是原始包裝物件
        throw 'TypeError'
    } else {
        function Fn () {}
        // 設定原型物件屬性
        Fn.prototype = proto
        const obj = new Fn()
        if (propertyObject !== undefined) {
            Object.defineProperties(obj, propertyObject)
        }
        if (proto === null) {
            // 建立一個沒有原型物件的物件,Object.create(null)
            obj.__proto__ = null
        }
        return obj
    }
}

定義一個空的建構函式,然後指定建構函式的原型物件,通過new運運算元建立一個空物件,如果發現傳遞了第二個引數,通過Object.defineProperties為建立的物件設定key、value,最後返回建立的物件即可。

範例

// 第二個引數為null時,丟擲TypeError
// const throwErr = Object.myCreate({name: 'coboy'}, null)  // Uncaught TypeError
// 構建一個以
const obj1 = Object.myCreate({name: 'coboy'})
console.log(obj1)  // {}, obj1的建構函式的原型物件是{name: 'coboy'}
const obj2 = Object.myCreate({name: 'coboy'}, {
    age: {
        value: 18,
        enumerable: true
    }
})
console.log(obj2)  // {age: 18}, obj2的建構函式的原型物件是{name: 'coboy'}

拓展:兩個連續賦值的表示式

provides = currentInstance.provides = Object.create(parentProvides) 發生了什麼?

Object.create(parentProvides) 建立了一個新的物件參照,如果只是把 currentInstance.provides 更新為新的物件參照,那麼provides的參照還是舊的參照,所以需要同時把provides的參照也更新為新的物件參照。

來自《JavaScript權威指南》的解析

  • JavaScript總是嚴格按照從左至右的順序來計算表示式
  • 一切都是表示式,一切都是運算
provides = currentInstance.provides = Object.create(parentProvides)

上述的provides是一個表示式,它被嚴格地稱為「賦值表示式的左手端(Ihs)運算元」。 而右側 currentInstance.provides = Object.create(parentProvides) 這一個整體也當做一個表示式,這一個整體賦值表示式的計算結果是賦值給了最左側的providescurrentInstance.provides = Object.create(parentProvides) 這個表示式同時也是一個賦值表示式,Object.create(parentProvides)建立了一個新的參照賦值給了currentInstance這個參照上的provides屬性

currentInstance.provides這個表示式的語意是:

  • 計算單值表示式currentInstance,得到currentInstance的參照
  • 將右側的名字provides理解為一個識別符號,並作為「.」運算的右運算元
  • 計算currentInstance.provides表示式的結果(Result)

currentInstance.provides當它作為賦值表示式的左運算元時,它是一個被賦值的參照,而當它作為右運算元時,則計算它的值。

注意:賦值表示式左側的運算元可以是另一個表示式,而在宣告語句中的等號左邊,絕不可能是一個表示式。 例如上面的如果寫成了let provides = xxx,那麼這個時候,provides只是一個表達名字的、靜態語法分析期作為識別符號來理解的字面文字,而不是一個表示式

(學習視訊分享:)

以上就是聊聊Vue3中Provide和Inject的實現原理的詳細內容,更多請關注TW511.COM其它相關文章!