【大型干货】手拉手带你过一遍vue部分源码

25,075 阅读5分钟

本文希望可以帮助那些想吃蛋糕,但又觉得蛋糕太大而又不知道从哪下口的人们。

一、如何开始第一步

  • 将源码项目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的定义地方。让我们再回顾下流程:

二、学会利用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的原型已经变成了:

如果你认为到此就结束了?答案当然是,不。让我们顺着第一章整理的图,继续回到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变成了这样:

继续顺着第一章整理的路线,来看看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会变为:

其实这些散落在代码中的挂载操作,有点没想明白尤大没有放到一个方法里去统一处理一波?

继续往下翻,看到了我们进入这个文件的目标,那就是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之后,变为了如下:

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来继续分析一波。

这里你可以先留意到几个关键词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完美的串在了一起。也希望经历这个过程后,你能对真正的对这张图,有一定的理解。

当然,$mount中还有一步被我轻描淡写了,那就是这部分,将template转换为render,render实际调用时,会经历_render, $createElement, __patch__, 方法,有兴趣可以自己浏览下'src/core/vdom/'目录下的文件,来了解vue针对虚拟dom的使用。

最后

如果你喜欢,可以继续浏览笔者关于vue template转换部分的文章《Vue对template做了什么》