【大型乾貨】手拉手帶你過一遍vue部分原始碼

JserWang發表於2018-04-25

本文希望可以幫助那些想吃蛋糕,但又覺得蛋糕太大而又不知道從哪下口的人們。

一、如何開始第一步

  • 將原始碼專案clone下來後,按照CONTRIBUTING中的Development Setup中的順序,逐個執行下來
$ npm install 

# watch and auto re-build dist/vue.js
$ npm run dev
複製程式碼
  • 學會看package.json檔案,就像你在使用MVVM去關注它的render一樣。

既然$ npm run dev命令可以重新編譯出vue.js檔案,那麼我們就從scripts中的dev開始看吧。

"dev":"rollup -w -c scripts/config.js --environment TARGET:web-full-dev"
複製程式碼

如果這裡你還不清楚rollup是做什麼的,可以戳這裡,簡單來說就是一個模組化打包工具。具體的介紹這裡就跳過了,因為我們是來看vue的,如果太跳躍的話,基本就把這次主要想做的事忽略掉了,跳跳跳不一定跳哪裡了,所以在閱讀原始碼的時候,一定要牢記這次我們的目的是什麼。

注意上面指令中的兩個關鍵詞scripts/config.jsweb-full-dev,接下來讓我們看看script/config.js這個檔案。

if (process.env.TARGET) {
  module.exports = genConfig(process.env.TARGET)
} else {
  exports.getBuild = genConfig
  exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}

複製程式碼

回憶上面的命令,我們傳入的TARGETweb-full-dev,那麼帶入到方法中,最終會看到這樣一個object

 'web-full-dev': {
 	// 入口檔案
	entry: resolve('web/entry-runtime-with-compiler.js'),
	// 輸出檔案
	dest: resolve('dist/vue.js'),
	// 格式
	format: 'umd',
	// 環境
	env: 'development',
	// 別名
	alias: { he: './entity-decoder' },
	banner
 },
複製程式碼

雖然這裡我們還不知道它具體是做什麼的,暫且通過語義來給它補上註釋吧。既然有了入口檔案,那麼我們繼續開啟檔案web/entry-runtime-with-compiler.js。OK,開啟這個檔案後,終於看到了我們的一個目標關鍵詞

import Vue from './runtime/index'
複製程式碼

江湖規矩,繼續往這個檔案裡跳,然後你就會看到:

import Vue from 'core/index'
複製程式碼

是不是又看到了程式碼第一行中熟悉的關鍵詞Vue

import Vue from './instance/index'
複製程式碼

開啟instance/index後,結束了我們的第一步,已經從package.json中到框架中的檔案,找到了Vue的定義地方。讓我們再回顧下流程:

【大型乾貨】手拉手帶你過一遍vue部分原始碼

二、學會利用demo

切記,在看原始碼時為了防止看著看著看跑偏了,我們一定要按照程式碼執行的順序看。

  • 專案結構中有examples目錄,讓我們也建立一個屬於自己的demo在這裡面吧,隨便copy一個目錄,命名為demo,後面我們的程式碼都通過這個demo來進行測試、觀察。

    index.html內容如下:

    <!DOCTYPE html>
    <html>
      <head>
        <title>Demo</title>
        <script src="../../dist/vue.js"></script>
      </head>
      <body>
        <div id="demo">
          <template>
            <span>{{text}}</span>
          </template>
        </div>
        <script src="app.js"></script>
      </body>
    </html>
    複製程式碼

    app.js檔案內容如下:

    var demo = new Vue({
      el: '#demo',
      data() {
        return {
          text: 'hello world!'
        }
      }
    })
    
    複製程式碼

引入vue.js

上面demo的html中我們引入了dist/vue.js,那麼window下,就會有Vue物件,暫且先將app.js的程式碼修改如下:

console.dir(Vue);
複製程式碼

如果這裡你還不知道console.dir,而只知道console.log,那你就親自試試然後記住他們之間的差異吧。

從控制檯我們可以看出,Vue物件以及原型上有一系列屬性,那麼這些屬性是從哪兒來的,做什麼的,就是我們後續去深入的內容。

三、從哪兒來的

是否還記得我們在第一章中找到最終Vue建構函式的檔案?如果不記得了,就再回去看一眼吧,我們在本章會按照那個順序倒著來看一遍Vue的屬性掛載。

instance(src/core/instance/index.js)

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
  	warn('Vue is a constructor and should be 
  	called with the `new` keyword')
  }
  this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

複製程式碼

接下來我們就開始按照程式碼執行的順序,先來分別看看這幾個函式到底是弄啥嘞?

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
複製程式碼
  1. initMixin(src/core/instance/init.js)

    Vue.prototype._init = function (options?: Object) {}
    複製程式碼

    在傳入的Vue物件的原型上掛載了_init方法。

  2. stateMixin(src/core/instance/state.js)

    // Object.defineProperty(Vue.prototype, '$data', dataDef)
    // 這裡$data只提供了get方法,set方法再非生產環境時會給予警告
    Vue.prototype.$data = undefined;
    // Object.defineProperty(Vue.prototype, '$props', propsDef)
    // 這裡$props只提供了get方法,set方法再非生產環境時會給予警告
    Vue.prototype.$props = undefined;
    
    Vue.prototype.$set = set
    Vue.prototype.$delete = del
    
    Vue.prototype.$watch = function() {}
    複製程式碼

    如果這裡你還不知道Object.defineProperty是做什麼的,我對你的建議是可以把物件的原型這部分好好看一眼,對於後面的程式碼瀏覽會有很大的效率提升,不然雲裡霧裡的,你浪費的只有自己的時間而已。

  3. eventsMixin(src/core/instance/events.js)

    Vue.prototype.$on = function() {}
    Vue.prototype.$once = function() {}
    Vue.prototype.$off = function() {}
    Vue.prototype.$emit = function() {}
    複製程式碼
  4. lifecycleMixin(src/core/instance/lifecycle.js)

    Vue.prototype._update = function() {}
    Vue.prototype.$forceUpdate = function () {}
    Vue.prototype.$destroy = function () {}
    複製程式碼
  5. renderMixin(src/core/instance/render.js)

    // installRenderHelpers 
    Vue.prototype._o = markOnce
    Vue.prototype._n = toNumber
    Vue.prototype._s = toString
    Vue.prototype._l = renderList
    Vue.prototype._t = renderSlot
    Vue.prototype._q = looseEqual
    Vue.prototype._i = looseIndexOf
    Vue.prototype._m = renderStatic
    Vue.prototype._f = resolveFilter
    Vue.prototype._k = checkKeyCodes
    Vue.prototype._b = bindObjectProps
    Vue.prototype._v = createTextVNode
    Vue.prototype._e = createEmptyVNode
    Vue.prototype._u = resolveScopedSlots
    Vue.prototype._g = bindObjectListeners
    
    // 
    Vue.prototype.$nextTick = function() {}
    Vue.prototype._render = function() {}
    複製程式碼

將上面5個方法執行完成後,instance中對Vue的原型一波瘋狂輸出後,Vue的原型已經變成了:

【大型乾貨】手拉手帶你過一遍vue部分原始碼

如果你認為到此就結束了?答案當然是,不。讓我們順著第一章整理的圖,繼續回到core/index.js中。

Core(src/core/index.js)

import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { 
	FunctionalRenderContext 
} from 'core/vdom/create-functional-component'

// 初始化全域性API
initGlobalAPI(Vue)

Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})

Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})

// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})

Vue.version = '__VERSION__'

export default Vue

複製程式碼

按照程式碼執行順序,我們看看initGlobalAPI(Vue)方法內容:

// Object.defineProperty(Vue, 'config', configDef)
Vue.config = { devtools: true, …}
Vue.util = {
	warn,
	extend,
	mergeOptions,
	defineReactive,
}
Vue.set = set
Vue.delete = delete
Vue.nextTick = nextTick
Vue.options = {
	components: {},
	directives: {},
	filters: {},
	_base: Vue,
}
// extend(Vue.options.components, builtInComponents)
Vue.options.components.KeepAlive = { name: 'keep-alive' …}
// initUse
Vue.use = function() {}
// initMixin
Vue.mixin = function() {}
// initExtend
Vue.cid = 0
Vue.extend = function() {}
// initAssetRegisters
Vue.component = function() {}
Vue.directive = function() {}
Vue.filter = function() {}
複製程式碼

不難看出,整個Core在instance的基礎上,又對Vue的屬性進行了一波輸出。經歷完Core後,整個Vue變成了這樣:

【大型乾貨】手拉手帶你過一遍vue部分原始碼

繼續順著第一章整理的路線,來看看runtime又對Vue做了什麼。

runtime(src/platforms/web/runtime/index.js)

這裡還是記得先從巨集觀入手,不要去看每個方法的詳細內容。可以通過debugger來暫停程式碼執行,然後通過控制檯的console.dir(Vue)隨時觀察Vue的變化,

  1. 這裡首先針對web平臺,對Vue.config來了一小波方法新增。

    Vue.config.mustUseProp = mustUseProp
    Vue.config.isReservedTag = isReservedTag
    Vue.config.isReservedAttr = isReservedAttr
    Vue.config.getTagNamespace = getTagNamespace
    Vue.config.isUnknownElement = isUnknownElement
    複製程式碼
  2. 向options中directives增加了model以及show指令:

    // extend(Vue.options.directives, platformDirectives)
    Vue.options.directives = {
    	model: { componentUpdated: ƒ …}
    	show: { bind: ƒ, update: ƒ, unbind: ƒ }
    }
    
    複製程式碼
  3. 向options中components增加了Transition以及TransitionGroup

    // extend(Vue.options.components, platformComponents)
    Vue.options.components = {
    	KeepAlive: { name: "keep-alive" …}
    	Transition: {name: "transition", props: {…} …}
    	TransitionGroup: {props: {…}, beforeMount: ƒ, …}
    }
    複製程式碼
  4. 在原型中追加__patch__以及$mount:

    // 虛擬dom所用到的方法
    Vue.prototype.__patch__ = patch
    Vue.prototype.$mount = function() {}
    複製程式碼
  5. 以及對devtools的支援。

entry(src/platforms/web/entry-runtime-with-compiler.js)

  1. 在entry中,覆蓋了$mount方法。

  2. 掛載compile,compileToFunctions方法是將template編譯為render函式

    Vue.compile = compileToFunctions
    複製程式碼

小結

至此,我們完整的過了一遍在web中Vue的建構函式的變化過程:

  • 通過instance對Vue.prototype進行屬性和方法的掛載。
  • 通過core對Vue進行靜態屬性和方法的掛載。
  • 通過runtime新增了對platform === 'web'的情況下,特有的配置、元件、指令。
  • 通過entry來為$mount方法增加編譯template的能力。

四、做什麼的

上一章我們從巨集觀角度觀察了整個Vue建構函式的變化過程,那麼我們本章將從微觀角度,看看new Vue()後,都做了什麼。

將我們demo中的app.js修改為如下程式碼:

var demo = new Vue({
  el: '#demo',
  data() {
    return {
      text: 'hello world!'
    }
  }
})
複製程式碼

還記得instance/init中的Vue建構函式嗎?在程式碼執行了this._init(options),那我們就從_init入手,開始本章的旅途。

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    // 瀏覽器環境&支援window.performance&非生產環境&配置了performance
    if (process.env.NODE_ENV !== 'production' 
    	&& config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      // 相當於 window.performance.mark(startTag)
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      // 將options進行合併
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    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')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' 
    	&& config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
複製程式碼

這個方法都做了什麼?

  1. 在當前例項中,新增_uid,_isVue屬性。
  2. 當非生產環境時,用window.performance標記vue初始化的開始。
  3. 由於我們的demo中,沒有手動處理_isComponent,所以這裡會進入到else分支,將Vue.options與傳入options進行合併。
  4. 為當前例項新增_renderProxy_self屬性。
  5. 初始化生命週期,initLifecycle
  6. 初始化事件,initEvents
  7. 初始化render,initRender
  8. 呼叫生命週期中的beforeCreate
  9. 初始化注入值 initInjections
  10. 初始化狀態 initState
  11. 初始化Provide initProvide
  12. 呼叫生命週期中的 created
  13. 非生產環境下,標識初始化結束,為當前例項增加_name屬性
  14. 根據options傳入的el,呼叫當前例項的$mount

OK,我們又巨集觀的看了整個_init方法,接下來我們結合我們的demo,來細細的看下每一步產生的影響,以及具體呼叫的方法。

mergeOptions(src/core/util/options.js)

vm.$options = mergeOptions(
	resolveConstructorOptions(vm.constructor),
	options || {},
	vm
)

function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      // super option changed,
      // need to resolve new options.
      Ctor.superOptions = superOptions
      // check if there are any late-modified/attached options (#4976)
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // update base extend options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      options = Ctor.options = 
      		mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}
複製程式碼

還記得我們在第三章中,runtime對Vue的變更之後,options變成了什麼樣嗎?如果你忘了,這裡我們再回憶一下:

Vue.options = {
	components: {
   		KeepAlive: { name: "keep-alive" …}
    	Transition: {name: "transition", props: {…} …}
    	TransitionGroup: {props: {…}, beforeMount: ƒ, …}
	},
	directives: {
    	model: { componentUpdated: ƒ …}
		show: { bind: ƒ, update: ƒ, unbind: ƒ }
	},
	filters: {},
	_base: ƒ Vue
}
複製程式碼

我們將上面的程式碼進行拆解,首先將this.constructor傳入resolveConstructorOptions中,因為我們的demo中沒有進行繼承操作,所以在resolveConstructorOptions方法中,沒有進入if,直接返回得到的結果,就是在runtime中進行處理後的options選項。而options就是我們在呼叫new Vue({})時,傳入的options。此時,mergeOptions方法變為:

vm.$options = mergeOptions(
	{
		components: {
	   		KeepAlive: { name: "keep-alive" …}
	    	Transition: {name: "transition", props: {…} …}
	    	TransitionGroup: {props: {…}, beforeMount: ƒ, …}
		},
		directives: {
	    	model: { componentUpdated: ƒ …}
			show: { bind: ƒ, update: ƒ, unbind: ƒ }
		},
		filters: {},
		_base: ƒ Vue
	},
	{
	  el: '#demo',
	  data: ƒ data()
	},
	vm
)
複製程式碼

接下來開始呼叫mergeOptions方法。開啟檔案後,我們發現在引用該檔案時,會立即執行一段程式碼:

// config.optionMergeStrategies = Object.create(null)
const strats = config.optionMergeStrategies
複製程式碼

仔細往下看後面,還有一系列針對strats掛載方法和屬性的操作,最終strats會變為:

【大型乾貨】手拉手帶你過一遍vue部分原始碼

其實這些散落在程式碼中的掛載操作,有點沒想明白尤大沒有放到一個方法裡去統一處理一波?

繼續往下翻,看到了我們進入這個檔案的目標,那就是mergeOptions方法:

function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  debugger;
  if (process.env.NODE_ENV !== 'production') {
	 // 根據使用者傳入的options,檢查合法性
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }
  // 標準化傳入options中的props
  normalizeProps(child, vm)
  // 標準化注入
  normalizeInject(child, vm)
  // 標準化指令
  normalizeDirectives(child)
  const extendsFrom = child.extends
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, 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)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}
複製程式碼

因為我們這裡使用了最簡單的hello world,所以在mergeOptions中,可以直接從30行開始看,這裡初始化了變數options,32行、35行的for迴圈分別根據合併策略進行了合併。看到這裡,恍然大悟,原來strats是定義一些標準合併策略,如果沒有定義在其中,就使用預設合併策略defaultStrat

這裡有個小細節,就是在迴圈子options時,僅合併父options中不存在的項,來提高合併效率。

讓我們繼續來用最直白的方式,回顧下上面的過程:

// 初始化合並策略
const strats = config.optionMergeStrategies
strats.el = strats.propsData = function (parent, child, vm, key) {}
strats.data = function (parentVal, childVal, vm) {}
constants.LIFECYCLE_HOOKS.forEach(hook => strats[hook] = mergeHook)
constants.ASSET_TYPES.forEach(type => strats[type + 's'] = mergeAssets)
strats.watch = function(parentVal, childVal, vm, key) {}
strats.props = 
strats.methods = 
strats.inject = 
strats.computed = function(parentVal, childVal, vm, key) {}
strats.provide = mergeDataOrFn

// 預設合併策略
const defaultStrat = function (parentVal, childVal) {
  return childVal === undefined
    ? parentVal
    : childVal
}

function mergeOptions (parent, child, vm) {
	// 本次demo沒有用到省略前面程式碼
	...
	
	const options = {}
  	let key
  	for (key in parent) {
    	mergeField(key)
  	}
  	for (key in child) {
    	if (!hasOwn(parent, key)) {
      		mergeField(key)
    	}
  	}
  	function mergeField (key) {
    	const strat = strats[key] || defaultStrat
    	options[key] = strat(parent[key], child[key], vm, key)
  	}
  	return options
}
複製程式碼

怎麼樣,是不是清晰多了?本次的demo經過mergeOptions之後,變為了如下:

【大型乾貨】手拉手帶你過一遍vue部分原始碼

OK,因為我們本次是來看_init的,所以到這裡,你需要清除Vue通過合併策略,將parent與child進行了合併即可。接下來,我們繼續回到_initoptions合併處理完之後做了什麼?

initProxy(src/core/instance/proxy.js)

在merge完options後,會判斷如果是非生產環境時,會進入initProxy方法。

if (process.env.NODE_ENV !== 'production') {
  initProxy(vm)
} else {
  vm._renderProxy = vm
}
vm._self = vm
複製程式碼

帶著霧水,進入到方法定義的檔案,看到了Proxy這個關鍵字,如果這裡你還不清楚,可以看下阮老師的ES6,上面有講。

  • 這裡在非生產環境時,對config.keyCodes的一些關鍵字做了禁止賦值操作。
  • 返回了vm._renderProxy = new Proxy(vm, handlers),這裡的handlers,由於我們的options中沒有render,所以這裡取值是hasHandler。

這部分具體是做什麼用的,暫且知道有這麼個東西,主線還是不要放棄,繼續回到主線吧。

initLifecycle(src/core/instance/lifecycle.js)

初始化了與生命週期相關的屬性。

function initLifecycle (vm) {
  const options = vm.$options
  // 省去部分與本次demo無關程式碼
  ...
  vm.$parent = undefined
  vm.$root = vm

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

  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}
複製程式碼

initEvents(src/core/instance/events.js)

function initEvents (vm) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // 省去部分與本次demo無關程式碼
  ...
}
複製程式碼

initRender(src/core/instance/render.js)

function initRender (vm: Component) {
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // v-once cached trees
  vm.$slots = {}
  vm.$scopedSlots = {}
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  vm.$createElement= (a, b, c, d) => createElement(vm, a, b, c, d, true)
  vm.$attrs = {}
  vm.$listeners = {}
}
複製程式碼

callHook(vm, 'beforeCreate)

呼叫生命週期函式beforeCreate

initInjections(src/core/instance/inject.js)

由於本demo沒有用到注入值,對本次vm並無實際影響,所以這一步暫且忽略,有興趣可以自行翻閱。

initState(src/core/instance/state.js)

本次的只針對這最簡單的demo,分析initState,可能忽略了很多過程,後續我們會針對更復雜的demo來繼續分析一波。

【大型乾貨】手拉手帶你過一遍vue部分原始碼

這裡你可以先留意到幾個關鍵詞ObserverDepWatcher。每個Observer都有一個獨立的Dep。關於Watcher,暫時沒用到,但是請相信,馬上就可以看到了。

initProvide(src/core/instance/inject.js)

由於本demo沒有用到,對本次vm並無實際影響,所以這一步暫且忽略,有興趣可以自行翻閱。

callHook(vm, 'created')

這裡知道為什麼在created時候,沒法操作DOM了嗎?因為在這裡,還沒有涉及到實際的DOM渲染。

vm.$mount(vm.$options.el)

這裡前面有個if判斷,所以當你如果沒有在new Vue中的options沒有傳入el的話,就不會觸發實際的渲染,就需要自己手動呼叫了$mount

這裡的$mount最終會調向哪裡?還記得我們在第三章看到的compiler所做的事情嗎?就是覆蓋Vue.prototype.$mount,接下來,我們一起進入$mount函式看看它都做了什麼吧。

// 只保留與本次相關程式碼,其餘看太多會影響視線
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  const options = this.$options
  if (!options.render) {
    let template = getOuterHTML(el)
    if (template) {
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  return mount.call(this, el, hydrating)
}
複製程式碼

這裡在覆蓋$mount之前,先將原有的$mount保留至變數mount中,整個覆蓋後的方法是將template轉為render函式掛載至vmoptions,然後呼叫呼叫原有的mount。所以還記得mount來自於哪嘛?那就繼續吧runtime/index,方法很簡單,呼叫了生命週期中mountComponent

// 依然只保留和本demo相關的內容
function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  callHook(vm, 'beforeMount')

  let updateComponent = () => {
  	vm._update(vm._render(), hydrating)
  }

  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}
複製程式碼

OK,精彩的部分來了,一個Watcher,盤活了整個我們前面鋪墊的一系列東西。開啟src/core/observer/watcher.js,讓我們看看Watcher的建構函式吧。為了清楚的看到Watcher的流程。依舊只保留方法我們需要關注的東西:

  constructor (vm, expOrFn, cb, options, isRenderWatcher) {
  	this.vm = vm
  	vm._watcher = this
    vm._watchers.push(this)
    this.getter = expOrFn
    this.value = this.get()
  }
  
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    value = this.getter.call(vm, vm)
    popTarget()
    this.cleanupDeps()
    return value
  }
複製程式碼
  1. Watcher的建構函式中,本次傳入的updateComponent作為Wathergetter
  2. get方法呼叫時,又通過pushTarget方法,將當前Watcher賦值給Dep.target
  3. 呼叫getter,相當於呼叫vm._update,先呼叫vm._render,而這時vm._render,此時會將已經準備好的render函式進呼叫。
  4. render函式中又用到了this.text,所以又會呼叫textget方法,從而觸發了dep.depend()
  5. dep.depend()會調回WatcheraddDep,這時Watcher記錄了當前dep例項。
  6. 繼續呼叫dep.addSub(this)dep又記錄了當前Watcher例項,將當前的Watcher存入dep.subs中。
  7. 這裡順帶提一下本次demo還沒有使用的,也就是當this.text發生改變時,會觸發Observer中的set方法,從而觸發dep.notify()方法來進行update操作。

最後這段文字太乾了,可以自己通過斷點,耐心的走一遍整個過程。如果沒有耐心看完這段描述,可以看看筆者這篇文章100行程式碼帶你玩vue響應式

就這樣,Vue的資料響應系統,通過ObserverWatcherDep完美的串在了一起。也希望經歷這個過程後,你能對真正的對這張圖,有一定的理解。

【大型乾貨】手拉手帶你過一遍vue部分原始碼

當然,$mount中還有一步被我輕描淡寫了,那就是這部分,將template轉換為render,render實際呼叫時,會經歷_render, $createElement, __patch__, 方法,有興趣可以自己瀏覽下'src/core/vdom/'目錄下的檔案,來了解vue針對虛擬dom的使用。

最後

如果你喜歡,可以繼續瀏覽筆者關於vue template轉換部分的文章《Vue對template做了什麼》

相關文章