渣新一枚,近期打算離職,去好點的城市發展,而後發現寫了很多業務代碼,真正有用的又一問三不知。 雖然一直都在用Vue,但總以爲好像只是背了背文檔,成了一個Api調用師。基於職業發展焦慮, 打算好好學習學習,把看到的東西總結下,即使快過期了,面試也可能會用到。既方便之後複習,也分享給你們,有錯還請指正,寫的很差望海涵。。vue
咱們再來看看源碼中是如何一步一步建立一個Vue實例的node
new Vue(options)
顯示建立一個Vue實例,傳入實例配置屬性options,咱們就來看看Vue構造函數作了什麼?react
function Vue ( options ) {
if ( !( this instanceof Vue ) ) { warn( 'Vue is a constructor and should be called with the `new` keyword' ); } this._init( options ); } 複製代碼
構造函數就只作了一件事,調用內部方法_init()
, _init
是混入Vue
原型對象上的一個方法,咱們繼續往下看。web
_init簡要代碼以下(已省略於本文無關代碼)面試
Vue.prototype._init = function ( options ) {
var vm = this; vm.$options = mergeOptions( resolveConstructorOptions( vm.constructor ), options || {}, vm ); vm._self = vm; initLifecycle( vm ); initEvents( vm ); initRender( vm ); callHook( vm, 'beforeCreate' ); initInjections( vm ); // resolve injections before data/props initState( vm ); // init props methods data computed watch initProvide( vm ); // resolve provide after data/props callHook( vm, 'created' ); if ( vm.$options.el ) { vm.$mount( vm.$options.el ); } } 複製代碼
_init
函數中,mergeOptions
將傳入的配置和諸如Mixin
配置按照默認或用戶自定義的合併策略進行合併。 配置項合併完成後,就能夠開始初始化組件數據了。緩存
function initLifecycle ( vm ) {
var options = vm.$options; // locate first non-abstract parent var parent = options.parent; if ( parent && !options.abstract ) { while ( parent.$options.abstract && parent.$parent ) { parent = parent.$parent; } parent.$children.push( vm ); } vm.$parent = parent; 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; } 複製代碼
initLifecycle
中首先循環查找當前組件的父級(非抽象),並掛在到$parent
屬性上,而後初始化了其餘的一些屬性的默認值。 說明beforeCreate
此時已經能夠訪問$parent
和$root
編輯器
function initEvents ( vm ) {
vm._events = Object.create( null ); vm._hasHookEvent = false; // init parent attached events var listeners = vm.$options._parentListeners; if ( listeners ) { updateComponentListeners( vm, listeners ); } } 複製代碼
initEvents
也不復雜,初始化內部屬性並獲取父組件註冊到該組件上的事件並初始化(updateComponentListeners)。 其中_parentListeners
是父組件傳入的事件監聽,_parentListeners
存在時將會在子組件內進行事件註冊。ide
「這裏須要注意的是,initEvents
初始化的是父組件傳入的事件監聽。」函數
function initRender ( vm ) {
var options = vm.$options; var parentVnode = vm.$vnode = options._parentVnode; // the placeholder node in parent tree var renderContext = parentVnode && parentVnode.context; vm.$slots = resolveSlots( options._renderChildren, renderContext ); vm.$scopedSlots = emptyObject; vm._c = function ( a, b, c, d ) { return createElement( vm, a, b, c, d, false ); }; vm.$createElement = function ( a, b, c, d ) { return 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 var parentData = parentVnode && parentVnode.data; { defineReactive$$1( vm, '$attrs', parentData && parentData.attrs || emptyObject, function () { !isUpdatingChildComponent && warn( "$attrs is readonly.", vm ); }, true ); defineReactive$$1( vm, '$listeners', options._parentListeners || emptyObject, function () { !isUpdatingChildComponent && warn( "$listeners is readonly.", vm ); }, true ); } } 複製代碼
這裏能夠看到,initRender
初始化一些屬性,其中主要的函數$createElemen
t已經能在實例中訪問。代碼的最後幾行中, 還將父組件的attrs
和listeners
(都作了響應式處理)掛載到了子組件實例中,主要是爲了高階組件的使用。oop
在調用beforeCreate
前,vue實例已經能訪問$parent
, $root
, $listeners
, $attrs
, $createElement
, $slots
屬性了。 這些屬性大部分都是從父組件中傳入的。
function initInjections ( vm ) {
var result = resolveInject( vm.$options.inject, vm ); if ( result ) { toggleObserving( false ); Object.keys( result ).forEach( function ( key ) { /* istanbul ignore else */ { defineReactive$1( vm, key, result[key], function () { warn( "Avoid mutating an injected value directly since the changes will be " + "overwritten whenever the provided component re-renders. " + "injection being mutated: \"" + key + "\"", vm ); } ); } } ); toggleObserving( true ); } } 複製代碼
initInjections
僅僅對inject
選項作處理,resolveInject
函數內部會層層遍歷父節點,查找全部注入的屬性並將inject
相關屬性轉換成鍵值對的形式。 拿到鍵值對後,再逐一將這些注入屬性掛載到當前實例下。
須要注意的是,掛載前執行了toggleObserving
函數,傳入false
時,後續綁定的屬性將不會主動設置爲響應式,也就是說,inject
屬性一般都並不是響應的(除非它自己就是響應式)。 inject
初始化後再恢復響應式綁定=>toggleObserving( true )
。
function initState ( vm ) {
vm._watchers = []; var opts = vm.$options; if ( opts.props ) { initProps( vm, opts.props ); } if ( opts.methods ) { initMethods( vm, opts.methods ); } if ( opts.data ) { initData( vm ); } else { observe( vm._data = {}, true /* asRootData */ ); } if ( opts.computed ) { initComputed( vm, opts.computed ); } if ( opts.watch && opts.watch !== nativeWatch ) { initWatch( vm, opts.watch ); } } 複製代碼
從代碼中就能夠看出來initState
都作了什麼,看到這裏,咱們能夠記住created
前已經初始化了props methods data computed watch
屬性。
function initProvide ( vm ) {
var provide = vm.$options.provide; if ( provide ) { vm._provided = typeof provide === 'function' ? provide.call( vm ) : provide; } } 複製代碼
initProvide只作了一件事,將組件提供的Provide選項保存到私有屬性_provided中。
至此,咱們已經能訪問絕大部分的組件屬性了,如data, props, methods, inject
等
_init
函數最後一步就是掛在實例到DOM中,用到的就是$mount
函數。
$mount
涉及到的代碼可能有些多,篇幅問題,咱們就看看主要步驟。
Vue.prototype.$mount = function (el, hydrating) {
if ( !options.render ) { var template = options.template; if ( template ) { // 對template模板進行校驗處理 } else if ( el ) { template = getOuterHTML( el ); } if ( template ) { var ref = compileToFunctions( template, { outputSourceRange: "development" !== 'production', shouldDecodeNewlines: shouldDecodeNewlines, shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this ); var render = ref.render; var staticRenderFns = ref.staticRenderFns; options.render = render; options.staticRenderFns = staticRenderFns; } } return mount.call( this, el, hydrating ) }; 複製代碼
$mount
根據手寫render函數
或template
選項和el
選項決定如何渲染掛在實例。 其中比較主要的就是compileToFunctions
函數。 咱們看到該函數傳入了template
並返回了render
,staticRenderFns
等函數。 很容易猜到Vue在這裏對模板進行了解析。返回的render也就是咱們常說的渲染函數。
模板轉換爲渲染函數有三個步驟:
有渲染函數後,咱們就能掛在到真實節點上了mount.call( this, el, hydrating )
。
mount function ( el, hydrating ) {
el = el && inBrowser ? query( el ) : undefined; return mountComponent( this, el, hydrating ) }; function mountComponent ( vm, el, hydrating ) { vm.$el = el; if ( !vm.$options.render ) { vm.$options.render = createEmptyVNode; } callHook( vm, 'beforeMount' ); ... } 複製代碼
mountComponent
函數中,確保了render函數存在。緊隨其後調用了beforeMount
生命週期。 咱們回顧如下created
和beforeMount
,它們之間進行了模板編譯,優化,轉換爲渲染函數。
// 緊接剛纔的代碼
var updateComponent; updateComponent = function () { vm._update( vm._render(), hydrating ); }; new Watcher( vm, updateComponent, noop, { before: function before () { if ( vm._isMounted && !vm._isDestroyed ) { callHook( vm, 'beforeUpdate' ); } } }, true /* isRenderWatcher */ ); hydrating = false; if ( vm.$vnode == null ) { vm._isMounted = true; callHook( vm, 'mounted' ); } return vm 複製代碼
這裏主要是初始化了Watcher
實例,實例中傳入了updateComponent
函數。 該函數後續會對比虛擬Dom並更新視圖。初始化了Watcher
時就會執行一次updateComponent
。 而後callHook( vm, 'mounted' )
。 因此mounted階段已經能訪問真實DOM了。
new Watcher( vm, updateComponent, noop, {
before: function before () { if ( vm._isMounted && !vm._isDestroyed ) { callHook( vm, 'beforeUpdate' ); } } }, true /* isRenderWatcher */ ); 複製代碼
new Watche 的before中調用了beforeUpdate,順着before,我找到了flushSchedulerQueue函數。
function flushSchedulerQueue () {
currentFlushTimestamp = getNow(); flushing = true; var watcher, id; ... // do not cache length because more watchers might be pushed // as we run existing watchers for ( index = 0; index < queue.length; index++ ) { watcher = queue[index]; if ( watcher.before ) { watcher.before(); } ... watcher.run(); ... } ... // call component updated and activated hooks callActivatedHooks( activatedQueue ); callUpdatedHooks( updatedQueue ); ... } 複製代碼
當須要更新依賴/狀態時($forceUpdate
, 數據讀取變更),flushSchedulerQueue
就會被觸發。
flushSchedulerQueue
先遍歷了watcher
,調用了before
(也就是生命週期beforeUpdate), 而後執行callActivatedHook
函數,該函數調用其「子組件」的activated
鉤子, 最後再調用callUpdatedHooks
也就是updated
鉤子。
來看看activated, keep-alive緩存的組件激活時調用
componentVNodeHooks: {
insert: function insert ( vnode ) { var context = vnode.context; var componentInstance = vnode.componentInstance; if ( !componentInstance._isMounted ) { componentInstance._isMounted = true; callHook( componentInstance, 'mounted' ); } if ( vnode.data.keepAlive ) { if ( context._isMounted ) { queueActivatedComponent( componentInstance ); } else { activateChildComponent( componentInstance, true /* direct */ ); } } }, ... } 複製代碼
以上代碼能夠看到,mounted調用後會判斷是否爲緩存組件並調用activated。
調用$destory
時觸發beforeDestroy
或deactivated
,完成數據銷燬後調用destoryed
。
好記性不如爛筆頭,看了很快就忘了,仍是記一記能有個好的思路,也方便複習。
本文使用 mdnice 排版