Vue 源碼解讀(2)—— Vue 初始化過程

當學習成為了習慣,知識也就變成了常識。 感謝各位的 點贊收藏評論。

新視頻和文章會第一時間在微信公眾號發(fā)送,歡迎關(guān)注:李永寧lyn

文章已收錄到 github 倉庫 liyongning/blog,歡迎 Watch 和 Star。

封面

image

目標

深入理解 Vue 的初始化過程,再也不怕 面試官 的那道面試題:new Vue(options) 發(fā)生了什么?

找入口

想知道 new Vue(options) 都做了什么,就得先找到 Vue 的構(gòu)造函數(shù)是在哪聲明的,有兩個辦法:

  • 從 rollup 配置文件中找到編譯的入口,然后一步步找到 Vue 構(gòu)造函數(shù),這種方式 費勁

  • 通過編寫示例代碼,然后打斷點的方式,一步到位,簡單

我們就采用第二種方式,寫示例,打斷點,一步到位。

  • /examples 目錄下增加一個示例文件 —— test.html,在文件中添加如下內(nèi)容:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue 源碼解讀</title>
</head>
<body>
  <div id="app">
    {{ msg }}
  </div>
  <script src="../dist/vue.js"></script>
  <script>
    debugger
    new Vue({
      el: '#app',
      data: {
        msg: 'hello vue'
      }
    })
  </script>
</body>
</html>
  • 在瀏覽器中打開控制臺,然后打開 test.html,則會進入斷點調(diào)試,然后找到 Vue 構(gòu)造函數(shù)所在的文件

     [點擊查看演示動圖](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d839ea6f3e5d4adcaf1ea9a8f6ff1a70~tplv-k3u1fbpfcp-watermark.awebp),動圖地址:https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d839ea6f3e5d4adcaf1ea9a8f6ff1a70~tplv-k3u1fbpfcp-watermark.awebp
    

得到 Vue 構(gòu)造函數(shù)在 /src/core/instance/index.js 文件中,接下來正式開始源碼閱讀,帶著目標去閱讀。

在閱讀過程中如遇到看不明白的地方,可通過編寫示例代碼,然后使用瀏覽器的調(diào)試功能進行一步步調(diào)試,配合理解,如果還是理解不了,就做個備注繼續(xù)向后看,也許你看到其它地方,就突然明白這個地方在做什么,或者回頭再來看,就會懂了,源碼這個東西,一定要多看,要想精通,一遍兩遍肯定是不夠的

源碼解讀 —— Vue 初始化過程

Vue

/src/core/instance/index.js

import { initMixin } from './init'

// Vue 構(gòu)造函數(shù)
function Vue (options) {
  // 調(diào)用 Vue.prototype._init 方法,該方法是在 initMixin 中定義的
  this._init(options)
}

// 定義 Vue.prototype._init 方法
initMixin(Vue)

export default Vue

Vue.prototype._init

/src/core/instance/init.js

/**
 * 定義 Vue.prototype._init 方法 
 * @param {*} Vue Vue 構(gòu)造函數(shù)
 */
export function initMixin (Vue: Class<Component>) {
  // 負責 Vue 的初始化過程
  Vue.prototype._init = function (options?: Object) {
    // vue 實例
    const vm: Component = this
    // 每個 vue 實例都有一個 _uid,并且是依次遞增的
    vm._uid = uid++

    // a flag to avoid this being observed
    vm._isVue = true
    // 處理組件配置項
    if (options && options._isComponent) {
      /**
       * 每個子組件初始化時走這里,這里只做了一些性能優(yōu)化
       * 將組件配置對象上的一些深層次屬性放到 vm.$options 選項中,以提高代碼的執(zhí)行效率
       */
      initInternalComponent(vm, options)
    } else {
      /**
       * 初始化根組件時走這里,合并 Vue 的全局配置到根組件的局部配置,比如 Vue.component 注冊的全局組件會合并到 根實例的 components 選項中
       * 至于每個子組件的選項合并則發(fā)生在兩個地方:
       *   1、Vue.component 方法注冊的全局組件在注冊時做了選項合并
       *   2、{ components: { xx } } 方式注冊的局部組件在執(zhí)行編譯器生成的 render 函數(shù)時做了選項合并,包括根組件中的 components 配置
       */
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      // 設置代理,將 vm 實例上的屬性代理到 vm._renderProxy
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    // 初始化組件實例關(guān)系屬性,比如 $parent、$children、$root、$refs 等
    initLifecycle(vm)
    /**
     * 初始化自定義事件,這里需要注意一點,所以我們在 <comp @click="handleClick" /> 上注冊的事件,監(jiān)聽者不是父組件,
     * 而是子組件本身,也就是說事件的派發(fā)和監(jiān)聽者都是子組件本身,和父組件無關(guān)
     */
    initEvents(vm)
    // 解析組件的插槽信息,得到 vm.$slot,處理渲染函數(shù),得到 vm.$createElement 方法,即 h 函數(shù)
    initRender(vm)
    // 調(diào)用 beforeCreate 鉤子函數(shù)
    callHook(vm, 'beforeCreate')
    // 初始化組件的 inject 配置項,得到 result[key] = val 形式的配置對象,然后對結(jié)果數(shù)據(jù)進行響應式處理,并代理每個 key 到 vm 實例
    initInjections(vm) // resolve injections before data/props
    // 數(shù)據(jù)響應式的重點,處理 props、methods、data、computed、watch
    initState(vm)
    // 解析組件配置項上的 provide 對象,將其掛載到 vm._provided 屬性上
    initProvide(vm) // resolve provide after data/props
    // 調(diào)用 created 鉤子函數(shù)
    callHook(vm, 'created')

    // 如果發(fā)現(xiàn)配置項上有 el 選項,則自動調(diào)用 $mount 方法,也就是說有了 el 選項,就不需要再手動調(diào)用 $mount,反之,沒有 el 則必須手動調(diào)用 $mount
    if (vm.$options.el) {
      // 調(diào)用 $mount 方法,進入掛載階段
      vm.$mount(vm.$options.el)
    }
  }
}

resolveConstructorOptions

/src/core/instance/init.js

/**
 * 從組件構(gòu)造函數(shù)中解析配置對象 options,并合并基類選項
 * @param {*} Ctor 
 * @returns 
 */
export function resolveConstructorOptions (Ctor: Class<Component>) {
  // 配置項目
  let options = Ctor.options
  if (Ctor.super) {
    // 存在基類,遞歸解析基類構(gòu)造函數(shù)的選項
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      // 說明基類構(gòu)造函數(shù)選項已經(jīng)發(fā)生改變,需要重新設置
      Ctor.superOptions = superOptions
      // 檢查 Ctor.options 上是否有任何后期修改/附加的選項(#4976)
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // 如果存在被修改或增加的選項,則合并兩個選項
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      // 選項合并,將合并結(jié)果賦值為 Ctor.options
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}

resolveModifiedOptions

/src/core/instance/init.js

/**
 * 解析構(gòu)造函數(shù)選項中后續(xù)被修改或者增加的選項
 */
function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
  let modified
  // 構(gòu)造函數(shù)選項
  const latest = Ctor.options
  // 密封的構(gòu)造函數(shù)選項,備份
  const sealed = Ctor.sealedOptions
  // 對比兩個選項,記錄不一致的選項
  for (const key in latest) {
    if (latest[key] !== sealed[key]) {
      if (!modified) modified = {}
      modified[key] = latest[key]
    }
  }
  return modified
}

mergeOptions

/src/core/util/options.js

/**
 * 合并兩個選項,出現(xiàn)相同配置項時,子選項會覆蓋父選項的配置
 */
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  // 標準化 props、inject、directive 選項,方便后續(xù)程序的處理
  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  // 處理原始 child 對象上的 extends 和 mixins,分別執(zhí)行 mergeOptions,將這些繼承而來的選項合并到 parent
  // mergeOptions 處理過的對象會含有 _base 屬性
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options = {}
  let key
  // 遍歷 父選項
  for (key in parent) {
    mergeField(key)
  }

  // 遍歷 子選項,如果父選項不存在該配置,則合并,否則跳過,因為父子擁有同一個屬性的情況在上面處理父選項時已經(jīng)處理過了,用的子選項的值
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }

  // 合并選項,childVal 優(yōu)先級高于 parentVal
  function mergeField (key) {
    // strats = Object.create(null)
    const strat = strats[key] || defaultStrat
    // 值為如果 childVal 存在則優(yōu)先使用 childVal,否則使用 parentVal
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

initInjections

/src/core/instance/inject.js

/**
 * 初始化 inject 配置項
 *   1、得到 result[key] = val
 *   2、對結(jié)果數(shù)據(jù)進行響應式處理,代理每個 key 到 vm 實例
 */
export function initInjections (vm: Component) {
  // 解析 inject 配置項,然后從祖代組件的配置中找到 配置項中每一個 key 對應的 val,最后得到 result[key] = val 的結(jié)果
  const result = resolveInject(vm.$options.inject, vm)
  // 對 result 做 數(shù)據(jù)響應式處理,也有代理 inject 配置中每個 key 到 vm 實例的作用。
  // 不不建議在子組件去更改這些數(shù)據(jù),因為一旦祖代組件中 注入的 provide 發(fā)生更改,你在組件中做的更改就會被覆蓋
  if (result) {
    toggleObserving(false)
    Object.keys(result).forEach(key => {
      /* istanbul ignore else */
      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(true)
  }
}

resolveInject

/src/core/instance/inject.js

/**
 * 解析 inject 配置項,從祖代組件的 provide 配置中找到 key 對應的值,否則用 默認值,最后得到 result[key] = val
 * inject 對象肯定是以下這個結(jié)構(gòu),因為在 合并 選項時對組件配置對象做了標準化處理
 * @param {*} inject = {
 *  key: {
 *    from: provideKey,
 *    default: xx
 *  }
 * }
 */
export function resolveInject (inject: any, vm: Component): ?Object {
  if (inject) {
    // inject is :any because flow is not smart enough to figure out cached
    const result = Object.create(null)
    // inject 配置項的所有的 key
    const keys = hasSymbol
      ? Reflect.ownKeys(inject)
      : Object.keys(inject)

    // 遍歷 key
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      // 跳過 __ob__ 對象
      // #6574 in case the inject object is observed...
      if (key === '__ob__') continue
      // 拿到 provide 中對應的 key
      const provideKey = inject[key].from
      let source = vm
      // 遍歷所有的祖代組件,直到 根組件,找到 provide 中對應 key 的值,最后得到 result[key] = provide[provideKey]
      while (source) {
        if (source._provided && hasOwn(source._provided, provideKey)) {
          result[key] = source._provided[provideKey]
          break
        }
        source = source.$parent
      }
      // 如果上一個循環(huán)未找到,則采用 inject[key].default,如果沒有設置 default 值,則拋出錯誤
      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
  }
}

initProvide

/src/core/instance/inject.js

/**
 * 解析組件配置項上的 provide 對象,將其掛載到 vm._provided 屬性上 
 */
export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

總結(jié)

Vue 的初始化過程(new Vue(options))都做了什么?

  • 處理組件配置項

    • 初始化根組件時進行了選項合并操作,將全局配置合并到根組件的局部配置上

    • 初始化每個子組件時做了一些性能優(yōu)化,將組件配置對象上的一些深層次屬性放到 vm.$options 選項中,以提高代碼的執(zhí)行效率

  • 初始化組件實例的關(guān)系屬性,比如 $parent、$children、$root、$refs 等

  • 處理自定義事件

  • 調(diào)用 beforeCreate 鉤子函數(shù)

  • 初始化組件的 inject 配置項,得到 ret[key] = val 形式的配置對象,然后對該配置對象進行淺層的響應式處理(只處理了對象第一層數(shù)據(jù)),并代理每個 key 到 vm 實例上

  • 數(shù)據(jù)響應式,處理 props、methods、data、computed、watch 等選項

  • 解析組件配置項上的 provide 對象,將其掛載到 vm._provided 屬性上

  • 調(diào)用 created 鉤子函數(shù)

  • 如果發(fā)現(xiàn)配置項上有 el 選項,則自動調(diào)用 $mount 方法,也就是說有了 el 選項,就不需要再手動調(diào)用 $mount 方法,反之,沒提供 el 選項則必須調(diào)用 $mount

  • 接下來則進入掛載階段

鏈接

感謝各位的:點贊、收藏評論,我們下期見。


當學習成為了習慣,知識也就變成了常識。 感謝各位的 點贊、收藏評論

新視頻和文章會第一時間在微信公眾號發(fā)送,歡迎關(guān)注:李永寧lyn

文章已收錄到 github 倉庫 liyongning/blog,歡迎 Watch 和 Star。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容