從 Vue 源碼學 provide/inject

一直對 Vue 的 provide/inject 的實現原理不是很清楚,致使平時工做中用的時候迷迷糊糊、模棱兩可的。今天決定看一下源碼,搞懂其中的機制,方便在工做中更流暢地使用。html

API用法

正如官網所述, 使用很簡單:vue

  • provide:Object | () => Object
  • inject:Array<string> | { [key: string]: string | Symbol | Object }

父組件使用 provide 來向子組件提供值, provide 能夠是對象,也能夠是返回對象的方法:git

// 父組件
export default {
  provide: {
    name: '張三'
  }
}
複製代碼

子組件使用 inject 來獲取父組件提供的值並注入到組件內, inject 能夠是字符串的數組,也能夠是對象:github

// 子組件
export default {
  inject: {
    providedName: { from: 'name' }
  }
}
複製代碼

而後就可使用該屬性了:web

// 子組件
export default {
  inject: {
    providedName: { from: 'name' }
  },
  // 能夠做爲屬性默認值
  props: {
    propsName: {
      default() { return this.providedName; }
    }
  },
  // 能夠做爲 data 的默認值
  data() {
    return {
      localName: this.providedName,
    }
  },
  created() {
    console.log(this.providedName);
  }
}
複製代碼

用法其實很簡單。可是有幾點疑問:api

  • 父組件經過 provide 能夠提供自身的屬性和方法給後代嗎?該怎麼作呢?
  • 父組件經過 provide 提供的自身屬性具備響應式嗎?
  • 子組件經過 inject 注入的屬性是在哪一個生命週期階段注入的?

帶着問題能夠去閱讀源碼。數組

源碼解析

我尚未系統地完整的閱讀過 Vue 的源碼,因此從工程中查找 provide/inject 相關的關鍵字找到了相關文件。markdown

格式化 inject

src/core/instance/index.js 文件中能夠看到, 在調用 new Vue(options) 的時候會調用 this._init(options):ide

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
複製代碼

而這個 _init 方法就是在 initMixin 添加到 Vue 原型上的方法。函數

src/core/instance/init.js 中能夠看到源碼,忽略其它信息能夠看到以下代碼:

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor), options || {}, vm
)
複製代碼

順藤摸瓜看一下 mergeOptions 方法裏面的代碼, 發現有一個 normalizeInject 的方法:

function normalizeInject (options: Object, vm: ?Component) {
  const inject = options.inject
  const normalized = options.inject = {}
  if (Array.isArray(inject)) {
    for (let i = 0; i < inject.length; i++) {
      normalized[inject[i]] = { from: inject[i] }
    }
  } else if (isPlainObject(inject)) {
    for (const key in inject) {
      const val = inject[key]
      normalized[key] = isPlainObject(val)
        ? extend({ from: key }, val)
        : { from: val }
    }
  } else if (process.env.NODE_ENV !== 'production' && inject) {
    warn(
      `Invalid value for option "inject": expected an Array or an Object, ` +
      `but got ${toRawType(inject)}.`,
      vm
    )
  }
}
複製代碼

這個方法就是格式化 inject 的方法了,邏輯很簡單,就是作以下幾種轉換:

  • inject 爲 Array<string> 類型, 例如:
    {
        inject: [ 'name' ]
      }
    複製代碼
    直接轉換爲:
    {
        inject: {
          name: {
            from: 'name',
          }
        }
      }
    複製代碼
  • inject 爲 Object 類型,且屬性值爲非普通對象,例如:
    {
        inject: {
          name: 'name',
          name2: { default: '' }
        }
      }
    複製代碼
    轉換爲:
    {
        inject: {
          name: {
            from: 'name'
          },
          name2: {
            from: 'name2',
            default: '',
          }
        }
      }
    複製代碼

綜上,inject 最終會被格式化爲以下格式,這個格式也是 inject 的標準格式

{
  inject: {
    [injectKey]: {
      from: 'providedKey',
      default: '默認值',
    }
  }
}
複製代碼

初始化 inject 和 provide

接着看 src/core/instance/init.js 中的代碼, 會發現初始化的代碼:

initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm) // resolve injections before data/props
  initState(vm) // 初始化 data、props、methods、computed、watch等
  initProvide(vm) // resolve provide after data/props
  callHook(vm, 'created')
複製代碼

忽略其它信息,能夠看到 provideinject 都是beforeCreatecreated 之間初始化的。因此解答了 inject 是在哪一個階段注入 這個問題。因此,若是咱們平常開發中能夠在 created 鉤子中獲取注入的值,可是不能在 beforeCreate 中獲取。

再看一下各個類型數據的初始化順序:

  • initInject: 首先初始化 inject 的注入內容
  • initState: 而後初始化 vue 實例的各個資源,data、props、methods、computed、watch等
  • initProvide: 最後初始化 provide 信息

因此咱們能夠獲得另外一個問題的答案: 父組件經過 provide 是能夠提供自身的屬性和方法給後代的

初始化 inject 的具體邏輯

繼續點進 initInjections 方法看一下具體邏輯:

export function initInjections (vm: Component) {
  const result = resolveInject(vm.$options.inject, vm)
  toggleObserving(false)
  Object.keys(result).forEach(key => {
    if (process.env.NODE_ENV !== 'production') {
      defineReactive(vm, key, result[key], () => {
        warn(
          `Avoid mutating an injected value directly since the changes will be ` +
          `overwritten whenever the provided component re-renders. ` +
          `injection being mutated: "${key}"`,
          vm
        )
      })
    } else {
      defineReactive(vm, key, result[key])
    }
  })
  toggleObserving(false)
}
複製代碼

首先從咱們格式化過的 $options.inject 中解析出 inject 對象,即 result,這個沒啥問題

而後關閉了 observe 的選項。這個是幹什麼的呢? 經過點進 toggleObserving 函數能夠看到是重置了 全局的一個變量:shouldObserve

關閉它幹啥呢?能夠看一下 defineReactive 代碼,在代碼一開始的時候就會調用 observe 方法來觀察一個對象:

// src/core/observer/index.js
export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) {
  const dep = new Dep()
  // 其它代碼
  let childOb = !shallow && observe(val)
}
複製代碼

再看下 observe 的代碼:

// src/core/observer/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (shouldObserve) {
    ob = new Observer(value)
  }
  return ob
}
複製代碼

建立 observer 的時候回檢查 shouldObserve 的值,因此這裏關閉了 shouldObserve, 咱們在用 defineReactive 給 Vue 實例定義響應式屬性的時候,就沒法觀察一個對象了。

不具備響應式的 provide

根據以上,咱們能夠得出結論: 經過 inject 注入的一個普通對象是不具有響應式的。 下面這個示例中,經過 changeName 改變 person.name 的值,是不會觸發視圖更新的。 由於對於 person 對象,沒有使用 observe 方法爲其建立 Observer

// 父組件
const person = { name: '張三' }
export default {
  name: 'parent',
  provide: {
    person,
  },
  methods: {
    changeName() {
      person.name = '李四'
    }
  }
}
// 子組件
export default {
  name: 'child',
  inject: [ 'person' ]
}
複製代碼

具備響應式的 provide

再看下面這個例子:

// 父組件
export default {
  name: 'parent',
  provide() {
    return {
      person: this.person,
    }
  },
  data() {
    return {
      person: { name: '張三' }
    }
  },
  methods: {
    changeName() {
      this.person.name = '李四'
    }
  }
}
// 子組件
export default {
  name: 'child',
  inject: [ 'person' ]
}
複製代碼

而父組件提供一個具備響應式的對象給子組件,子組件獲取到的值就是響應式的。經過 changeName 改變 person.name 的值,是會觸發視圖更新的。

因此這裏也回答了另一個問題: 父組件經過 provide 提供的自身的響應式屬性傳給子組件後具備響應式的,可是提供的普通對象,是不具有響應式的

其它API相關的具體邏輯

有興趣的話,繼續看一下 resolveInject 中的邏輯:

export function resolveInject (inject: any, vm: Component): ?Object {
  const result = Object.create(null)
  const keys = Object.keys(inject);
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const provideKey = inject[key].from
    let source = vm
    while (source) {
      if (source._provided && provideKey in source._provided) {
        result[key] = source._provided[provideKey]
        break
      }
      source = source.$parent
    }
    if (!source) {
      if ('default' in inject[key]) {
        const provideDefault = inject[key].default
        result[key] = typeof provideDefault === 'function'
          ? provideDefault.call(vm)
          : provideDefault
      } else if (process.env.NODE_ENV !== 'production') {
        warn(`Injection "${key}" not found`, vm)
      }
    }
  }
  return result
}
複製代碼

遍歷 inject 中的全部key,每一個 key 值的 from 屬性表示要從父級組件注入的屬性。查找過程是逐級網上的,找到提供了 provide 的父級以後就再也不繼續尋找,因此始終會注入最近一級的 provide 屬性。

另外,從這裏也可獲得兩個API用法:

  • 若是提供了 default ,在沒有尋找到 provide 值時會使用 default 提供的值。
  • default 能夠是函數,在函數中能夠經過 this 訪問組件實例。

可是,須要注意的是在 default 函數中經過 this 是訪問不到 propsdata 中的屬性的,緣由上面也說了,inject 的初始化在 data等以前(因此這裏的 this 貌似沒什麼用)。

初始化 provide 的具體邏輯

代碼在 src/core/instance/inject.js 中:

export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}
複製代碼

很是簡單,就是把咱們寫的 provide 最終都轉成對象存儲起來,與上文的 result[key] = source._provided[provideKey] 相對應。 同時能夠看出,若是給 provide 提供了一個方法的話,在方法裏面是能夠經過 this 來訪問實例中的屬性和方法的。這也就解決了 若是把實例中的數據經過 provide 提供給子組件 這個問題:

export default {
  provide() {
    return {
      name: this.name;
    }
  },
  data() {
    return {
      name: '',
    }
  }
}
複製代碼

總結

下面整理一下具體的問題。

  1. 父組件經過 provide 能夠提供自身的屬性和方法給後代嗎? 能夠。給 provide 設置一個方法,在方法中就能夠經過 this 來訪問 props,data,methods 等資源。

  2. 父組件經過 provide 提供的自身屬性具備響應式嗎? 父組件提供的具備響應式的屬性,注入子組件後是具備響應式的,可是提供的普通對象,不具有響應式功能。

  3. 子組件經過 inject 注入的屬性是在哪一個生命週期階段注入的? 是在 beforeCreatecreated 之間注入的。全部的順序以下:

    1. 先初始化 injection
    2. 在初始化 data,props等,所以在 data,props中可使用 injection
    3. 而後在初始化 provide, 因此組件能夠將自身的屬性和數據提供給後代組件。

以上就是本文的所有內容了,感謝各位閱讀,若是有任何疑問,歡迎留言。

轉載請註明來源從 Vue 源碼學 provide/inject

相關文章
相關標籤/搜索