Vue源碼解析四——初始化

我們最開始的列子是:

<div id="app">{{ a }}</div>
var vm = new Vue({
  el: '#app',
  data: { a: 1 }
})

初始化執(zhí)行_init方法俗或,該方法進(jìn)行到vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor), options || {}, vm)的時候, 我們通過mergeOptions方法了解了選項規(guī)范化選項合并在跳。

在我們的例子中,執(zhí)行mergeOptions方法的時候父選項是Vue.options, 子選項就是參數(shù){ el: '#app', data: { a: 1} }决侈。el使用默認(rèn)合并策略進(jìn)行合并,data合并完是一個函數(shù),我們在選項合并中分析過了赖歌⊥髌裕看一下vm.$options的打印結(jié)果

options.png

接著往下看_init方法中的其他代碼

/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
  initProxy(vm)
} else {
  vm._renderProxy = vm
}

這是一個判斷分支,當(dāng)是非生產(chǎn)環(huán)境時執(zhí)行initProxy方法庐冯,生產(chǎn)環(huán)境時在實例上添加_renderProxy屬性孽亲。

再看一下initProxy方法中是如何處理的,該函數(shù)定義在core/instance/proxy.js文件中:

initProxy = function initProxy (vm) {
    if (hasProxy) {
      // determine which proxy handler to use
      const options = vm.$options
      const handlers = options.render && options.render._withStripped
        ? getHandler
        : hasHandler
      vm._renderProxy = new Proxy(vm, handlers)
    } else {
      vm._renderProxy = vm
    }
 }

整體也是一個判斷分支展父,不過不論是哪個分支返劲,最后都在實例上添加了_renderProxy屬性。先看一下hasProxy的定義, 在當(dāng)前文件中:

 const hasProxy =
    typeof Proxy !== 'undefined' && isNative(Proxy)

isNative定義在core/util/env.js文件中:

export function isNative (Ctor: any): boolean {
  return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}

hasProxy的作用就是判斷宿主環(huán)境是否支持原生的 Proxy栖茉。

如果支持Proxy篮绿,則對實例進(jìn)行代理,代理對象賦給vm._renderProxy; 如果不支持吕漂,直接給_renderProxy屬性賦值vm亲配。

重點看一下支持的情況:

const options = vm.$options
const handlers = options.render && options.render._withStripped
    ? getHandler
    : hasHandler
vm._renderProxy = new Proxy(vm, handlers)

攔截目標(biāo)是vm, handles就是攔截行為。如果options.render && options.render._withStripped條件為真惶凝,handlers的值就是getHandler吼虎,否則是hasHandler_withStripped屬性只在測試代碼中被設(shè)置為true苍鲜,所以一般是走hasHandler, 它的定義也在該文件中:

const hasHandler = {
    has (target, key) {
      // key屬性是否存在在target中
      const has = key in target
      // 是全局變量或者是以_開頭的字符串并且沒有在$data中思灰,返回true
      const isAllowed = allowedGlobals(key) ||
        (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
      // target沒有該屬性并且不是全局變量
      if (!has && !isAllowed) {
        // 以 _ 或 $ 開頭的屬性要使用 `$data.${key}`訪問
        if (key in target.$data) warnReservedPrefix(target, key)
        // 屬性沒定義
        else warnNonPresent(target, key)
      }
      return has || !isAllowed
    }
  }

has可以攔截以下操作:

  • in操作
  • Reflect.has()
  • with

這里有兩個警告函數(shù):

  1. warnReservedPrefix, 為了避免和Vue 內(nèi)置的屬性、API 方法沖突混滔,以 _ 或 開頭的屬性不會被 Vue 實例代理官辈。可以通過`data.${key}`訪問
const warnReservedPrefix = (target, key) => {
    warn(
      `Property "${key}" must be accessed with "$data.${key}" because ` +
      'properties starting with "$" or "_" are not proxied in the Vue instance to ' +
      'prevent conflicts with Vue internals' +
      'See: https://vuejs.org/v2/api/#data',
      target
    )
  }

例如

var vm = new Vue({data: {_a: 1}})
vm._a // undefined
vm.$data._a // 1
  1. warnNonPresent. 在渲染的時候用到了${key}屬性遍坟,但是并沒有在實例上定義
const warnNonPresent = (target, key) => {
    warn(
      `Property or method "${key}" is not defined on the instance but ` +
      'referenced during render. Make sure that this property is reactive, ' +
      'either in the data option, or for class-based components, by ' +
      'initializing the property. ' +
      'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
      target
    )
  }

比如:

<div>{{ a }}</div>
var vm = new Vue({data: {_a: 1}})

因為沒有在data中定義a屬性拳亿,就會報上面那段警告,${key}就是a

這是為了在開發(fā)階段給我們一個友好的提示愿伴,幫助我們快速定位問題肺魁。再回到_init函數(shù)中看接下來的代碼

vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

首先在實例對象上添加了_self屬性隔节,其值就是實例本身鹅经。接著執(zhí)行了一系列的initxxx方法,我們來一個個看怎诫。

初始化之 initLifecycle

initLifecycle函數(shù)定義在core/instance/lifecycle.js文件中

export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  // parent指向當(dāng)前實例的父組件
  let parent = options.parent
  // 如果有父組件且當(dāng)前實例不是抽象的
  if (parent && !options.abstract) {
    // while循環(huán)查找當(dāng)前實例的第一個非抽象的父組件
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    // 將vm添加到當(dāng)前第一個非抽象的父實例中
    parent.$children.push(vm)
  }
  // 當(dāng)前實例的$parent屬性指向第一個非抽象的父實例
  vm.$parent = parent
  // 設(shè)置$root實例屬性瘾晃,有父實例就用父實例的$root,沒有就指向當(dāng)前實例
  vm.$root = parent ? parent.$root : vm

  vm.$children = []
  vm.$refs = {}

  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

首先定義了常量options幻妓,它指向當(dāng)前實例的$options屬性蹦误。接著定義了parent,它就是vm.$options.parent的值,也就是當(dāng)前實例的父組件强胰。接著是一個條件判斷parent && !options.abstract(如果有父組件且當(dāng)前實例不是抽象的), 什么是抽象的組件呢舱沧?

官方文檔中介紹的keep-alive就是一個抽象組件,其中有這個一句話:<keep-alive> 是一個抽象組件:它自身不會渲染一個 DOM 元素偶洋,也不會出現(xiàn)在父組件鏈中熟吏。所以抽象組件的特性就是

  • 不渲染真實DOM
  • 不出現(xiàn)在父組件鏈中

再來看這段代碼:

// parent指向當(dāng)前實例的父組件
let parent = options.parent
// 如果有父組件且當(dāng)前實例不是抽象的
if (parent && !options.abstract) {
    // while循環(huán)查找當(dāng)前實例的第一個非抽象的父組件
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    // 將vm添加到當(dāng)前第一個非抽象的父實例中
    parent.$children.push(vm)
}
// 當(dāng)前實例的$parent屬性指向第一個非抽象的父實例
vm.$parent = parent
// 設(shè)置$root實例屬性,有父實例就用父實例的$root玄窝,沒有就指向當(dāng)前實例
vm.$root = parent ? parent.$root : vm

如果if條件為假牵寺,也就是當(dāng)前實例是抽象的,就會跳過if語句塊直接設(shè)置實例屬性$parent$root恩脂,并不會將當(dāng)前實例添加到父組件的$children中帽氓。

如果條件為真,也就是當(dāng)前實例不是抽象的东亦,就會執(zhí)行if語句塊內(nèi)的代碼。通過while循環(huán)查找第一個非抽象的父組件唬渗,因為抽象組件不能作為父級典阵。找到父級(第一個非抽象的父組件)之后,將當(dāng)前組件實例添加到父級的$children中镊逝,再接著設(shè)置$parent$root實例屬性壮啊。

在這之后又設(shè)置了一些實例屬性

vm.$children = []
vm.$refs = {}

vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false

初始化之 initEvents

回到_init函數(shù)中,initLifecycle執(zhí)行完之后撑蒜,就是initEvents, 它定義在core/instance/events.js文件中

export function initEvents (vm: Component) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // init parent attached events
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

在實例上添加_events_hasHookEvent兩個屬性并初始化歹啼。那_events都包含實例的哪些事件呢?我們通過個例子打印看看

<template lang="html">
  <div class="">
    <button-counter @on-change-count="changeCount"></button-counter>
  </div>
</template>

<script>
export default {
  components: {
    'button-counter': {
      data: function () {
        return {
          count: 0
        }
      },
      template: '<button v-on:click="addCount">You clicked me {{ count }} times.</button>',
      mounted () {
        console.log('events', this._events);
      },
      methods: {
        addCount () {
          this.count++;
          this.$emit('on-change-count')
        }
      }
    }
  },
  methods: {
    changeCount () {
      console.log('changeCount');
    }
  }
}
</script>

看一下在子組件中打印_events的結(jié)果:

events.png

結(jié)果是只有在父組件中使用當(dāng)前組件時綁定在當(dāng)前組件上的事件座菠。

// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
    updateComponentListeners(vm, listeners)
}

這段代碼應(yīng)該是初始化父組件添加的事件狸眼,沒看太懂,先略過浴滴。

初始化之 initRender

接下來執(zhí)行的是initRender函數(shù)拓萌,該函數(shù)存在于core/instance/render.js文件中,函數(shù)體如下:

export function initRender (vm: Component) {
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // v-once cached trees
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject 
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

  // $attrs & $listeners are exposed for easier HOC creation.
  // they need to be reactive so that HOCs using them are always updated
  const parentData = parentVnode && parentVnode.data

  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
    }, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
    }, true)
  } else {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
  }
}

這么一大段代碼一眼望去讓人頭大升略,我們一點點來看微王,不懂的就暫時略過,不能丟了主線^o^

首先在Vue實例上添加了兩個屬性:_vnode品嚣、_staticTrees

vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees

它們都被初始化為null, 會在合適的地方進(jìn)行賦值并使用

接下來是這樣一段代碼:

const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject

這段代碼是解析處理slot的炕倘,內(nèi)容比較復(fù)雜,先略過~

// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

這是添加了兩個實例屬性:_c$createElement, 它們都是對createElement方法的再封裝翰撑。

我們都寫過渲染函數(shù)

render: function (createElement) {
    return createElement(
      'h',
      'test'
    )
}

其中createElement也可以替換成this.$createElement

render: function () {
    return this.$createElement(
      'h',
      'test'
    )
}

最后是這樣一段代碼:

// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
const parentData = parentVnode && parentVnode.data

/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
    }, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
    }, true)
} else {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}

這里是在Vue實例上添加了兩個屬性$attrs$listeners, 可以用來創(chuàng)建高階組件罩旋,官方文檔上對這兩個屬性有說明

這兩個對象定義的時候用了defineReactive函數(shù),該函數(shù)的作用是定義響應(yīng)式屬性瘸恼,具體實現(xiàn)在后面看雙向數(shù)據(jù)綁定的時候再看劣挫。也就是說這兩個屬性是響應(yīng)式的,并且是兩個只讀屬性帐我。

生命周期鉤子

剩余的函數(shù)調(diào)用包括這些:

callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

callHook就是調(diào)用鉤子函數(shù),在beforeCreatecreated鉤子函數(shù)調(diào)用的中間芬为,調(diào)用了initInjections媚朦、initStateinitProvide三個方法日戈,在initState函數(shù)里面又調(diào)用了initProps initMethods initData initComputed initWatch, 所以在beforeCreate鉤子被調(diào)用時询张,所有與inject、provide props浙炼、methods份氧、data、computed 以及 watch 相關(guān)的內(nèi)容都不能使用弯屈。在created鉤子函數(shù)中可以使用蜗帜,但是不能訪問dom,因為這時候還沒掛載呢资厉。

我們看一下callHook的實現(xiàn)方式, 該函數(shù)存在于lifecycle.js文件中:

export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  const handlers = vm.$options[hook]
  const info = `${hook} hook`
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}

函數(shù)體的開頭和結(jié)尾分別調(diào)用了pushTargetpopTarget, 這是為了在調(diào)用生命周期時禁止收集依賴钮糖。

在看選項合并的時候我們知道生命周期鉤子最終被處理成了數(shù)組,所以handlers如果有值的話就是一個數(shù)組酌住。

所以下面判斷如果handlers存在的話循環(huán)調(diào)用每一個函數(shù)

if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
}

invokeWithErrorHandling定義在core/util/error.js文件中店归,里面調(diào)用了handlers[i]函數(shù)

export function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  try {
    res = args ? handler.apply(context, args) : handler.call(context)
    if (isPromise(res)) {
      res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
    }
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}

為了捕獲可能出現(xiàn)的錯誤,使用try...catch包裹了函數(shù)調(diào)用酪我。如果函數(shù)調(diào)用返回Promise消痛,用catch捕獲錯誤。

最后是這樣一段代碼:

if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
}

這是因為我們的生命周期還可以這樣調(diào)用:

<component
  @hook:beforeCreate="handleChildBeforeCreate"
  @hook:created="handleChildCreated" />

使用@hook:生命周期名來監(jiān)聽組件的生命周期都哭,當(dāng)有這種方式時秩伞,_hasHookEvent屬性就被設(shè)置為true逞带。

因為initInjectionsinitState都有涉及數(shù)據(jù)雙向綁定的內(nèi)容,所以接下來我們先看數(shù)據(jù)雙向綁定纱新。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末展氓,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子脸爱,更是在濱河造成了極大的恐慌遇汞,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件簿废,死亡現(xiàn)場離奇詭異空入,居然都是意外死亡,警方通過查閱死者的電腦和手機族檬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進(jìn)店門歪赢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人单料,你說我怎么就攤上這事埋凯。” “怎么了扫尖?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵白对,是天一觀的道長。 經(jīng)常有香客問我藏斩,道長躏结,這世上最難降的妖魔是什么却盘? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任狰域,我火速辦了婚禮,結(jié)果婚禮上黄橘,老公的妹妹穿的比我還像新娘兆览。我一直安慰自己,他們只是感情好塞关,可當(dāng)我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布抬探。 她就那樣靜靜地躺著,像睡著了一般帆赢。 火紅的嫁衣襯著肌膚如雪小压。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天椰于,我揣著相機與錄音怠益,去河邊找鬼。 笑死瘾婿,一個胖子當(dāng)著我的面吹牛蜻牢,可吹牛的內(nèi)容都是我干的烤咧。 我是一名探鬼主播,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼抢呆,長吁一口氣:“原來是場噩夢啊……” “哼煮嫌!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起抱虐,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤昌阿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后梯码,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體宝泵,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年轩娶,在試婚紗的時候發(fā)現(xiàn)自己被綠了儿奶。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡鳄抒,死狀恐怖闯捎,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情许溅,我是刑警寧澤瓤鼻,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站贤重,受9級特大地震影響茬祷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜并蝗,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一祭犯、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧滚停,春花似錦沃粗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至起惕,卻和暖如春涡贱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背惹想。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工问词, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人勺馆。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓戏售,卻偏偏與公主長得像侨核,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子灌灾,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內(nèi)容

  • 回憶 首先搓译,render函數(shù)中手寫h=>h(app),new Vue()實例初始化init()和原來一樣锋喜。$mou...
    LoveBugs_King閱讀 2,274評論 1 2
  • 前言 您將在本文當(dāng)中了解到,往網(wǎng)頁中添加數(shù)據(jù),從傳統(tǒng)的dom操作過渡到數(shù)據(jù)層操作,實現(xiàn)同一個目標(biāo),兩種不同的方式....
    itclanCoder閱讀 25,764評論 1 12
  • 這篇筆記主要包含 Vue 2 不同于 Vue 1 或者特有的內(nèi)容些己,還有我對于 Vue 1.0 印象不深的內(nèi)容。關(guān)于...
    云之外閱讀 5,046評論 0 29
  • Vue 實例 屬性和方法 每個 Vue 實例都會代理其 data 對象里所有的屬性:var data = { a:...
    云之外閱讀 2,204評論 0 6
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,093評論 1 32