Vue原始碼: 關於vm.$delete()/Vue.use() 內部原理

小諾哥發表於2019-05-01

vm.$delete()

vm.$delete用法見官網

為什麼需要Vue.delete()?

在ES6之前, JS沒有提供方法來偵測到一個屬性被刪除了, 因此如果我們通過delete刪除一個屬性, Vue是偵測不到的, 因此不會觸發資料響應式。

見下面的demo。


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Vue Demo</title>
    <script src="https://cdn.jsdelivr.net/npm/vue"></script>
  </head>
  <body>
    <div id="app">
      名字: {{ user.name }} 年紀: {{ user.age }}
      <button @click="addUserAgeField">刪除一個年紀欄位</button>
    </div>
    <script>
      const app = new Vue({
        el: "#app",
        data: {
          user: {
            name: "test",
            age: 10
          }
        },
        mounted() {},
        methods: {
          addUserAgeField() {
            // delete this.user.age; // 這樣是不起作用, 不會觸發資料響應式更新
            this.$delete(this.user, 'age') // 應該使用
          }
        }
      });
    </script>
  </body>
</html>

複製程式碼

原始碼分析內部實現

原始碼位置vue/src/core/instance/state.js的stateMixin方法

export function stateMixin (Vue: Class<Component>) {
    ...
    
    Vue.prototype.$set = set
    Vue.prototype.$delete = del
    
    ...

}
複製程式碼

然後檢視del函式位置, vue/src/core/observer/index.js。

/**
 * Delete a property and trigger change if necessary.
 * target: 將被刪除屬性的目標物件, 可以是物件/陣列
 * key: 刪除屬性
 */
export function del (target: Array<any> | Object, key: any) {
  // 非生產環境下, 不允許刪除一個原始資料型別, 或者undefined, null
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // 如果target是陣列, 並且key是一個合法索引,通過陣列的splcie方法刪除值, 並且還能觸發資料的響應(陣列攔截器擷取到變化到元素, 通知依賴更新資料)
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  // 獲取ob
  const ob = (target: any).__ob__
  // target._isVue: 不允許刪除Vue例項物件上的屬性
  // (ob && ob.vmCount): 不允許刪除根資料物件的屬性,觸發不了響應
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
    return
  }
  // 如果屬性壓根不在物件上, 什麼都不做處理
  if (!hasOwn(target, key)) {
    return
  }
  // 走到這一步說明, target是物件, 並且key在target上, 直接使用delete刪除
  delete target[key]
  // 如果ob不存在, 說明target本身不是響應式資料,
  if (!ob) {
    return
  }
  // 存在ob, 通過ob裡面儲存的Dep例項的notify方法通知依賴更新
  ob.dep.notify()
}

複製程式碼

工具函式

// 判斷是否v是未定義
export function isUndef (v: any): boolean %checks {
  return v === undefined || v === null
}

// 判斷v是否是原始資料型別(基本資料型別)
export function isPrimitive (value: any): boolean %checks {
  return (
    typeof value === 'string' ||
    typeof value === 'number' ||
    // $flow-disable-line
    typeof value === 'symbol' ||
    typeof value === 'boolean'
  )
}

// 判斷物件上是否有屬性
const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn (obj: Object | Array<*>, key: string): boolean {
  return hasOwnProperty.call(obj, key)
}

複製程式碼

關於__ob__屬性, 在很多原始碼地方我們都會看到類似這樣獲取ob(Observer例項)

const ob = (target: any).__ob__
複製程式碼

牢記只要資料被observe過就會打上這個私有屬性, 是在Observer類的構造器裡面發生的

export class Observer {
    constructor (value: any) {
    this.value = value
    // 依賴是存在Observe上的dep屬性, 再次通知依賴更新時候我們一般使用__ob__.dep.notify()
    this.dep = new Dep()
    this.vmCount = 0
    // 定義__ob__
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
   }
    ...

}
複製程式碼

Vue.use()

大家都知道這個方法是用來安裝外掛的, 是全域性api。 具體使用見官網

通過Vue.use()原始碼+Vuex部分原始碼分析外掛的安裝過程

Vue.use()什麼時候被綁在Vue原型上

原始碼位置: vue/src/core/index.js

Vue原始碼: 關於vm.$delete()/Vue.use() 內部原理
Vue

initGlobalAPI()

原始碼位置: vue/src/core/global-api/index.js

export function initGlobalAPI (Vue: GlobalAPI) {
    ...
    // 初始化use()
    initUse(Vue)
    ...

}
複製程式碼

initUse()

原始碼位置: vue/src/core/global-api/use.js

export function initUse (Vue: GlobalAPI) {
  // 這裡的Vue是構造器函式.
  // 通過以下原始碼:
  // vue-dev/src/core/global-api/index.js  initGlobalAPI()中
  // vue-dev/src/core/index.js 這裡執行了initGlobalAPI() => 初始化一些全域性api
  // Vue.use(): 安裝Vue.js的外掛
  // 如果外掛是一個物件,必須提供 install 方法
  // 如果外掛是一個函式,它會被作為 install 方法
  // install 方法呼叫時,會將 Vue 作為引數傳入
  Vue.use = function (plugin: Function | Object) {
    // installedPlugins儲存install後的外掛
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {
      // 同一個外掛只會安裝一次
      return this
    }
    // additional parameters
    // 除了外掛外的其他引數 Vue.use(MyPlugin, { someOption: true })
    const args = toArray(arguments, 1)
    // 往args儲存Vue構造器, 供外掛的install方法使用
    args.unshift(this)
    // 分情況執行外掛的install方法, 把this(Vue), 引數拋回給install方法
    // 所以我們常說, install這個方法的第一個引數是 Vue 構造器,第二個引數是一個可選的選項物件:
    if (typeof plugin.install === 'function') {
      // plugin是一個物件
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      // plugin是一個函式
      plugin.apply(null, args)
    }
    // install之後會儲存該外掛避免重複安裝
    installedPlugins.push(plugin)
    return this
  }
}

複製程式碼

Vuex原始碼

我們都知道開發一個Vue.js 的外掛應該暴露一個 install 方法。這個方法的第一個引數是 Vue 構造器,第二個引數是一個可選的選項物件:

那麼我們首先就是看Vuex的install方法是怎麼實現的

原始碼位置: vuex-dev/src/store.js

let Vue // bind on install

// install: 裝載vuex到vue, Vue.use(Vuex)也是執行install方法
// 關於Vue.use()原始碼. vue-dev/src/core/global-api/use.js
export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  // 首次安裝外掛, 會把區域性的Vue快取到全域性的window.Vue. 主要為了避免重複呼叫Vue.use()
  Vue = _Vue
  applyMixin(Vue)
}
複製程式碼
applyMixin()

原始碼位置: vuex/src/mixin.js

export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    // 如果是2.x.x以上版本,注入一個全域性mixin, 執行vueInit方法
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    // 重寫Vue原型上的_init方法, 注入vueinit方法 _init方法見 vue-dev/src/core/instance/init.js
    const _init = Vue.prototype._init // 作為快取變數
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      // 重新執行_init
      _init.call(this, options)
    }
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   */
  // 注入store到Vue構造器
  function vuexInit () {
    // 這裡的this. 指的是Vue構造器
    /**
     *  new Vue({
     *    ...,
     *    store,
     *    route
     * })
     */
    // options: 就是new Vue(options)
    // 原始碼見 vue-dev/src/core/instance/init.js initMixin方法
    const options = this.$options
    // store injection
    // store是我們使用new Vuex.Store(options)的例項
    // 注入store到Vue建構函式上的$store屬性上, 所以我們在Vue元件裡面使用this.$store來使用
    if (options.store) {
      // options.store為真說明是根節點root
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      // 子元件直接從父元件中獲取$store,這樣就保證了所有元件都公用了全域性的同一份store
      this.$store = options.parent.$store
    }
  }
}

複製程式碼

至於install方法Vuex是如果執行的?

export class Store {
  constructor (options = {}) {
    // 瀏覽器環境下安裝vuex
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }
    ...
  }
}
複製程式碼

相關文章