Vue源碼翻譯之組件初始化。


廢話不多說。

  

  我們先來看看Vue的入口文件。

 1 import { initMixin } from './init'
 2 import { stateMixin } from './state'
 3 import { renderMixin } from './render'
 4 import { eventsMixin } from './events'
 5 import { lifecycleMixin } from './lifecycle'
 6 import { warn } from '../util/index'
 7 
 8 function Vue (options) {
 9   if (process.env.NODE_ENV !== 'production' &&
10     !(this instanceof Vue)
11   ) {
12     warn('Vue is a constructor and should be called with the `new` keyword')
13   }
14   this._init(options)
15 }
16 
17 initMixin(Vue)
18 stateMixin(Vue)
19 eventsMixin(Vue)
20 lifecycleMixin(Vue)
21 renderMixin(Vue)
22 
23 export default Vue

 

本章先講第17行開始的initMixin方法 —— 組件初始化 

initMixin   

 1 export function initMixin (Vue: Class<Component>) {
 2   Vue.prototype._init = function (options?: Object) {
 3     const vm: Component = this
 4     // a uid
 5     vm._uid = uid++
 6 
 7     let startTag, endTag
 8     /* istanbul ignore if */
 9     if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
10       startTag = `vue-perf-start:${vm._uid}`
11       endTag = `vue-perf-end:${vm._uid}`
12       mark(startTag)
13     }
14 
15     // a flag to avoid this being observed
16     vm._isVue = true
17     // merge options
18     if (options && options._isComponent) {
19       // optimize internal component instantiation
20       // since dynamic options merging is pretty slow, and none of the
21       // internal component options needs special treatment.
22       initInternalComponent(vm, options)
23     } else {
24       vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor), options || {}, vm)
25     }
26     /* istanbul ignore else */
27     if (process.env.NODE_ENV !== 'production') {
28       initProxy(vm)
29     } else {
30       vm._renderProxy = vm
31     }
32     // expose real self
33     vm._self = vm
34     initLifecycle(vm)
35     initEvents(vm)
36     initRender(vm)
37     callHook(vm, 'beforeCreate')
38     initInjections(vm) // resolve injections before data/props
39     initState(vm)
40     initProvide(vm) // resolve provide after data/props
41     callHook(vm, 'created')
42 
43     /* istanbul ignore if */
44     if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
45       vm._name = formatComponentName(vm, false)
46       mark(endTag)
47       measure(`vue ${vm._name} init`, startTag, endTag)
48     }
49 
50     if (vm.$options.el) {
51       vm.$mount(vm.$options.el)
52     }
53   }
54 }

  這里記一下:

  每一個VM對象在實例化的時候,會給一個uid。 然后我們再看后續的代碼:

  這里可以看到有一個屬性是_isComponent,這個屬性在我們整個程序執行第一次的時候,是不存在的,所以這個地方,在Vue首次實例化Vm的時候,肯定是會跳過的,這個怎么理解?其實很簡單,整個項目的根VM對象肯定是非內部組件,你看你項目的main.js,是不是這樣一段代碼:

new Vue({
  el: '#app',
  router,
  template: '<App/>',
  components: {App}
})

  這個Vue對象並非內部組件,而是一個根組件。那上面那段代碼的意思就是,如果options存在,且當前正在執行初始化的是一個組件(根組件_isComponent估計是undefind或者null),那就要進行內部組件的初始化。我們知道,一般一個Vue單頁應用個,是一個根vm,然后根vm實例下面有多個VueComponent,這么一個結構。然后我們現在先把initInternalComponent(vm,options)這個部分先跳過,走else的代碼——options融合(mergeOptions)。在理解這個mergOptions的之前,還得先解決

  這段代碼有點復雜,js的是有一個原型鏈的概念,對象的constructor(構造函數)實際上就是指向這個類(不是對象本身,對象本身是類的實例。就像Java中的類,和你new一個類的實例這樣的區別),還有,需要注意的是,這個方法的入參,有一個:Class<Component>,這個是Flow的語法,然后這個Component連接的類是在/flow/component.js這個文件里定義的。打開這個類,你就能理解這段代碼里面的Ctor.superOptions,Ctor.super這些意思。

  然后這段代碼的意義就是:通過循環遞歸,找到Component這個類的繼承鏈,然后把所有的配置都進行融合。為什么這么做,得后面再說。我們先mark一下這個位置。反正知道這是一個把所有類的繼承鏈的配置屬性進行融合的一個過程。然后回歸上面說的,mergerOptions,這段代碼,

  做的事情也不難理解,就是把你組件屬性中的props、inject,directive等進行規范化,並且還會檢驗你是否按Vue的照規范寫,如果沒有,還會報出提示。然后就開始根據不同的合並策略,進行數據的合並,合並什么呢?合並剛才從類的繼承鏈中獲取的配置對象及你自己在代碼中編寫的配置對象(從第一次合並肯定是new Vue(options)這個options,當然這個mergeOptions方法在很多地方都有用到,也有用來合並Component的Options,所以需要對props等屬性的規范進行檢查)。好了,反正這里,就是進行參數信息的融合。我們進入下一步:

  

initLifecycle

  這里呢,可以看到,就是vm實例進行初始化,並且開始執行寫生命周期函數。好了,來看看initLifecycle這個方法,這個方法就有點好玩了。

 1 export function initLifecycle (vm: Component) {
 2   const options = vm.$options
 3 
 4   // locate first non-abstract parent
 5   let parent = options.parent
 6   if (parent && !options.abstract) {
 7     while (parent.$options.abstract && parent.$parent) {
 8       parent = parent.$parent
 9     }
10     parent.$children.push(vm)
11   }
12 
13   vm.$parent = parent
14   vm.$root = parent ? parent.$root : vm
15 
16   vm.$children = []
17   vm.$refs = {}
18 
19   vm._watcher = null
20   vm._inactive = null
21   vm._directInactive = false
22   vm._isMounted = false
23   vm._isDestroyed = false
24   vm._isBeingDestroyed = false
25 }

  我們看看,截出的代碼第六行開始,這個函數的入參是一個Component —— 組件,所以你要這樣去理解。如果組件的parent存在,並且組件還不是抽象的(其實抽象組件目前好像就只有keep-alive,這個也先mark,回頭遇到說這個概念,或者看Vue文檔,里面有解釋抽象組件的意義。),然后做一件什么事兒呢,就是找到這個Component最上級的,非抽象的父組件,然后讓這個父組件添加自己到父組件的children數組當中。並且看第13行代碼,這個vm對象做了一個與父vm的關聯,從而完善這個tree型的組件樹。這樣其實在每個組件在初始化的時候,都會給自己的父組件進行關聯。從父組件的children中可以找到該父組件下所有的子組件,這就是一顆樹形結構的數據對象,或者說是一個金字塔。

  其實對這個vm的理解,我個人是這么理解的,vm自然是一個Vue的實例,就是一個VueComponent的實例,但是這個VueComponent與我們自己編碼的Component可不是同一個東西,我們自己編碼的那個Component只是一個模板,框架加載了這個模板,然后,根據組件使用的次數實例化對應個數的VueComponent實例。

  為什么這么做呢?現在解釋還太早,往后看。這個方法的后續代碼就是初始化一些vm的參數,現在也沒啥可解釋的。反正記住這里給了這些值一個初始化的值,以后有遇到再回頭就知道是在這里進行了初始賦值。

initEvents

  好了,進入下一個方法,initEvents(vm)

1 export function initEvents (vm: Component) {
2   vm._events = Object.create(null)
3   vm._hasHookEvent = false
4   // init parent attached events
5   const listeners = vm.$options._parentListeners
6   if (listeners) {
7     updateComponentListeners(vm, listeners)
8   }
9 }

  從這段代碼可以看出,首先是為vm的一些參數設置了值,這邊可能會有讓我們困惑的地方就是_parentListeners這個屬性特么又是從哪里來的?不要着急,我們之所以錯過了這個方法,是因為我們前面不是有一個_isComponent的判斷,然后跳過了initInternalComponent方法么?其實這個listeners是針對父子組件的事件通知的,就是你可能經常會在html標簽上寫 v-on。那其實這部分代碼就是對組件的事件監聽,及更新組件的監聽事件,並且對v-on設置的參數進行一些規范化,比如你會有一些方法后綴如.capture || .once,把它們從你的配置,轉成js對象,這樣也才方便后續對事件進行一些操作。  

initRender:

  好,接下來,初始化render:

 1 export function initRender (vm: Component) {
 2   vm._vnode = null // the root of the child tree
 3   vm._staticTrees = null // v-once cached trees
 4   const options = vm.$options
 5   const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
 6   const renderContext = parentVnode && parentVnode.context
 7   vm.$slots = resolveSlots(options._renderChildren, renderContext)
 8   vm.$scopedSlots = emptyObject
 9   // bind the createElement fn to this instance
10   // so that we get proper render context inside it.
11   // args order: tag, data, children, normalizationType, alwaysNormalize
12   // internal version is used by render functions compiled from templates
13   vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
14   // normalization is always applied for the public version, used in
15   // user-written render functions.
16   vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
17 
18   // $attrs & $listeners are exposed for easier HOC creation.
19   // they need to be reactive so that HOCs using them are always updated
20   const parentData = parentVnode && parentVnode.data
21 
22   /* istanbul ignore else */
23   if (process.env.NODE_ENV !== 'production') {
24     defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
25       !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
26     }, true)
27     defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
28       !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
29     }, true)
30   } else {
31     defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
32     defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
33   }
34 }

  在initRender當中,我們可以看到,對vm的一些關於界面繪制相關的屬性和方法都會被進行初始化,比如:vm._c及vm.$createElement,這個方法的具體內容,暫時不看,先看主線任務。代碼第23行開始,可以看到$attrs及$listeners這兩個對象的數據從父Vnode中來。而這兩個屬性的解釋,也可以從Vue的Api文檔中找到:$attrs && $listeners

  好了這里我來插播一個概念:我們剛才在initLifecycle的地方有接觸到Component有一個樹形結構,但是我們也都知道一個問題,就是Vue里的組件,是可以復用的,如果在一個頁面中同時出現兩個相同的組件,這兩個組件的數據是相互隔離的,並不會互相干涉,這是因為實際上在Vue當中,真正跟數據相關聯的,不是咱們自己寫的那個Component(*.vue),而是通過Component(*.vue)組件創建的VueComponent實例。所以你寫的組件,其實就是相當於一個VueComponent的模板,框架根據模板,創建一個或多個VueComponent,而這些VueComponent相互之間都是獨立的,否則當你使用v-for並且循環體是一個組件的話,你就會發現,循環后的每個組件值都是一樣的,而且改一個組件的數據,其他的也都會跟着改。這里非常的有意思。你編寫的Component,其實只是一個模板,實際被創建出來的是Vdom,這個概念一定要分清楚。要理解到Component(*.vue)與VueComponent的關系其實真的不太容易,我是按照Vue的原理自己實現一個框架走到這一步的時候,才理解了其中的深意,而這部分,可以從/src/core/vdom/create-component.js這個文件export出的createComponent()方法中看出細節。

  還有VueComponent是包含一個或多個Vnode(一個HTML節點就會對應生成一個Vnode)組成的,這段代碼中的vm代表的是一個組件,一個組件是有好多個Vnode組成的一個Vdom,你可能會問,為什么是從parentVnode當中獲取data的attrs?所謂attrs其實就是html'標簽上的attributes(元素),我們回想一下,當我們制作好一個組件后,是不是使用類似<component-name/>的自定義標簽來代表在html文檔中的某個地方使用組件?然后你要給這個組件所配置的所有屬性,都是在這個<component-name/>標簽上去寫,比如傳入一個title屬性:<component-name title="testAttrs"/>所以其實這個自定義標簽,就是你自己編寫的組件的父節點,而這個attrs屬性,也只可能從你的父節點中獲取。簡單講一下這個$attr的作用,比如你要寫一個列表組件,肯定是先有一個<custom-ul/>然后再來<custom-li/>,但是我們希望這兩個組件要配合一起用,然后我們又希望,只需要把數組數據設置給<custom-ul/>這個標簽而省去寫<custom-li/>,那你就要在外層ul組件上傳遞給li組件的數據,為了區分這個數據其實是給內部li使用的,而ul這個外層組件其實是不需要用的,不需要自然不用定義props,而我們又想要把數據傳遞進去給內部的li組件用,那就可以使用$attrs來實現了。當然這個例子不一定恰當,但是應該能解釋的清除這個特性。

  繼續我們看31行和32行,這地方開始對這兩個參數進行響應式配置。這里是整個Vue響應式原理的核心。我已經迫不及待的想要解析這部分的代碼,但是這個要講還挺占篇幅,所以放到后面,反正同學們只要知道,通過了這個defineReacttive方法之后,這個數據就會具有響應式的特性,數據變動就會觸發頁面重繪及變更。好了,這個render的初始化就告一段落,我們進入下一段,下面緊接着,是一個生命周期函數,就是beforeCreate,我們從Vue的文檔中可以看到,在這個生命周期函數當中,其實數據觀測是尚未設定的,從源碼中我們也可以看到,執行到beforeCreate這個步驟的時候,其實Vue只是做了一些數據、實例初始化操作,這也是為什么在beforeCreate方法中如果對數據進行更改,你會發現數據並沒有如你所願的變更。

initInjections/initProvide

  這兩個函數要一起說,因為這兩個函數所對應的inject和provider是成對出現的。這兩個函數其實內容也不會很復雜,來看第一個

 1 export function initInjections (vm: Component) {
 2   const result = resolveInject(vm.$options.inject, vm)
 3   if (result) {
 4     observerState.shouldConvert = false
 5     Object.keys(result).forEach(key => {
 6       /* istanbul ignore else */
 7       if (process.env.NODE_ENV !== 'production') {
 8         defineReactive(vm, key, result[key], () => {
 9           warn(
10             `Avoid mutating an injected value directly since the changes will be ` +
11             `overwritten whenever the provided component re-renders. ` +
12             `injection being mutated: "${key}"`,
13             vm
14           )
15         })
16       } else {
17         defineReactive(vm, key, result[key])
18       }
19     })
20     observerState.shouldConvert = true
21   }
22 }

  這部分的代碼,開始就是resolveInject,這個方法其實也就是從配置當中讀取inject配置,

  

  這段代碼的意思是,是否是ES2015的Symbol?如果是則通過Reflect來獲取key,Object.getOwnPropertyDescriptor的方法時獲取對象某個值的屬性描述,這一小段的意思是,通過ownKeys得到了一個key數組,但是這個數組要使用filter過濾,只留下“可枚舉”的特性的Key,不可枚舉的就不需要留下了,至於這么做的原因,嗯,如果有興趣,可以去看看相關知識,我就不拓展了,否則要跑題了。至於Object.keys就沒什么好解釋了,這個方法直接就返回的就是具有可枚舉特性的key值數組。

  這段代碼的意思是,從當前的vm向上查找,如果從父級vm上找到了對應的provide那就取對應的值,然后跳出循環,我們觀察一下,這個while只有在兩種情況下回跳出循環,一個是source為undefind或null,另一個就是找到了對應provide的值,那如果跳出循環的時候,發現父級的vm都undefind了(就是代碼中if(!source)這個判斷)那說明這個值還沒找到,那就要看inject的配置當中,是否有默認配置,其實這個邏輯可以看Vue的文檔 provide/inject 就能理解這個邏輯的意義,provide是能注入到所有子組件當中,這里循環向上查找provide就是這個所謂的注入到所有子組件的道理。好了,回到上層代碼,剩下的,其實就是把數據進行響應化,上面講過就不多說了。至於provide,文檔里說了,他必須是Object或者返回Object的函數,所以initProvide其實也沒有什么特別的,基本就是一看就懂。

  至於兩個函數一個在initState前初始化,一個在后初始化,按照我的理解,因為provide不是跟自己組件使用,而是給子組件使用,而inject是給當前組件自己用的,並且provide的數據還有可能是從其他props或data傳入,這些數據都是需要經過initState進行可響應化。

initState

  接下來說說這個initState

 

 1 export function initState (vm: Component) {
 2   vm._watchers = []
 3   const opts = vm.$options
 4   if (opts.props) initProps(vm, opts.props)
 5   if (opts.methods) initMethods(vm, opts.methods)
 6   if (opts.data) {
 7     initData(vm)
 8   } else {
 9     observe(vm._data = {}, true /* asRootData */)
10   }
11   if (opts.computed) initComputed(vm, opts.computed)
12   if (opts.watch && opts.watch !== nativeWatch) {
13     initWatch(vm, opts.watch)
14   }
15 }

 

  其實這個方法也不會很復雜,首先就是初始化Props,其實就是對Props數據進行一些校驗或賦值,來看一下initProps

  在這段代碼中,我們可以看到,如果vm是根節點或根vm,則此vm的props,需要進行響應化轉換,但是如果他不是根vm,則不需要進行響應化轉換。這是為什么呢?因為,我們的props都是從父級組件傳進來的,而父級組件傳進來的值大多是定義在父級組件的data屬性,而data屬性是必須響應化,所以到了子vm自然就不需要再做一次響應化處理。(就算傳入子組件的值是來自父組件的props,並且向上依然如此,當追溯到根vm時,根vm的props是經過了響應化的,所以最終依然還是會成為可響應的屬性。)

  這剩下initState剩下的部分倒還真的沒啥好說,都很容易就能看明白。

    接下來,就是 stateMixin了。

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM