本文但愿能够帮助那些想吃蛋糕,但又以为蛋糕太大而又不知道从哪下口的人们。javascript
clone
下来后,按照CONTRIBUTING中的Development Setup
中的顺序,逐个执行下来$ npm install # watch and auto re-build dist/vue.js $ npm run dev 复制代码
既然$ npm run dev
命令能够从新编译出vue.js
文件,那么咱们就从scripts
中的dev
开始看吧。html
"dev":"rollup -w -c scripts/config.js --environment TARGET:web-full-dev" 复制代码
若是这里你还不清楚
rollup
是作什么的,能够戳这里,简单来讲就是一个模块化打包工具。具体的介绍这里就跳过了,由于咱们是来看vue的,若是太跳跃的话,基本就把此次主要想作的事忽略掉了,跳跳跳不必定跳哪里了,因此在阅读源码的时候,必定要牢记此次咱们的目的是什么。vue
注意上面指令中的两个关键词scripts/config.js
和web-full-dev
,接下来让咱们看看script/config.js
这个文件。java
if (process.env.TARGET) { module.exports = genConfig(process.env.TARGET) } else { exports.getBuild = genConfig exports.getAllBuilds = () => Object.keys(builds).map(genConfig) } 复制代码
回忆上面的命令,咱们传入的TARGET
是web-full-dev
,那么带入到方法中,最终会看到这样一个object
node
'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,打开这个文件后,终于看到了咱们的一个目标关键词git
import Vue from './runtime/index' 复制代码
江湖规矩,继续往这个文件里跳,而后你就会看到:github
import Vue from 'core/index' 复制代码
是否是又看到了代码第一行中熟悉的关键词Vue
web
import Vue from './instance/index' 复制代码
打开instance/index
后,结束了咱们的第一步,已经从package.json中到框架中的文件,找到了Vue
的定义地方。让咱们再回顾下流程:npm
切记,在看源码时为了防止看着看着看跑偏了,咱们必定要按照代码执行的顺序看。json
项目结构中有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!' } } }) 复制代码
上面demo的html中咱们引入了dist/vue.js,那么window下,就会有Vue
对象,暂且先将app.js的代码修改以下:
console.dir(Vue); 复制代码
若是这里你还不知道
console.dir
,而只知道console.log
,那你就亲自试试而后记住他们之间的差别吧。
从控制台咱们能够看出,Vue
对象以及原型上有一系列属性,那么这些属性是从哪儿来的,作什么的,就是咱们后续去深刻的内容。
是否还记得咱们在第一章中找到最终Vue
构造函数的文件?若是不记得了,就再回去看一眼吧,咱们在本章会按照那个顺序倒着来看一遍Vue
的属性挂载。
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)
复制代码
initMixin(src/core/instance/init.js)
Vue.prototype._init = function (options?: Object) {} 复制代码
在传入的Vue
对象的原型上挂载了_init
方法。
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
是作什么的,我对你的建议是能够把对象的原型这部分好好看一眼,对于后面的代码浏览会有很大的效率提高,否则云里雾里的,你浪费的只有本身的时间而已。
eventsMixin(src/core/instance/events.js)
Vue.prototype.$on = function() {} Vue.prototype.$once = function() {} Vue.prototype.$off = function() {} Vue.prototype.$emit = function() {} 复制代码
lifecycleMixin(src/core/instance/lifecycle.js)
Vue.prototype._update = function() {} Vue.prototype.$forceUpdate = function () {} Vue.prototype.$destroy = function () {} 复制代码
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中。
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
作了什么。
这里仍是记得先从宏观入手,不要去看每一个方法的详细内容。能够经过
debugger
来暂停代码执行,而后经过控制台的console.dir(Vue)
随时观察Vue
的变化,
这里首先针对web平台,对Vue.config来了一小波方法添加。
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
复制代码
向options中directives增长了model
以及show
指令:
// extend(Vue.options.directives, platformDirectives) Vue.options.directives = { model: { componentUpdated: ƒ …} show: { bind: ƒ, update: ƒ, unbind: ƒ } } 复制代码
向options中components增长了Transition
以及TransitionGroup
:
// extend(Vue.options.components, platformComponents) Vue.options.components = { KeepAlive: { name: "keep-alive" …} Transition: {name: "transition", props: {…} …} TransitionGroup: {props: {…}, beforeMount: ƒ, …} } 复制代码
在原型中追加__patch__
以及$mount
:
// 虚拟dom所用到的方法 Vue.prototype.__patch__ = patch Vue.prototype.$mount = function() {} 复制代码
以及对devtools的支持。
在entry中,覆盖了$mount
方法。
挂载compile,compileToFunctions
方法是将template
编译为render
函数
Vue.compile = compileToFunctions
复制代码
至此,咱们完整的过了一遍在web中Vue的构造函数的变化过程:
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) } } 复制代码
这个方法都作了什么?
_uid
,_isVue
属性。_renderProxy
,_self
属性。initLifecycle
initEvents
initRender
beforeCreate
initInjections
initState
initProvide
created
_name
属性options
传入的el
,调用当前实例的$mount
OK,咱们又宏观的看了整个_init
方法,接下来咱们结合咱们的demo,来细细的看下每一步产生的影响,以及具体调用的方法。
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进行了合并便可。接下来,咱们继续回到_init
对options
合并处理完以后作了什么?
在merge完options后,会判断若是是非生产环境时,会进入initProxy方法。
if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } vm._self = vm 复制代码
带着雾水,进入到方法定义的文件,看到了Proxy
这个关键字,若是这里你还不清楚,能够看下阮老师的ES6,上面有讲。
vm._renderProxy = new Proxy(vm, handlers)
,这里的handlers
,因为咱们的options中没有render,因此这里取值是hasHandler。这部分具体是作什么用的,暂且知道有这么个东西,主线仍是不要放弃,继续回到主线吧。
初始化了与生命周期相关的属性。
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 } 复制代码
function initEvents (vm) { vm._events = Object.create(null) vm._hasHookEvent = false // 省去部分与本次demo无关代码 ... } 复制代码
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 = {} } 复制代码
调用生命周期函数beforeCreate
因为本demo没有用到注入值,对本次vm并没有实际影响,因此这一步暂且忽略,有兴趣能够自行翻阅。
本次的只针对这最简单的demo,分析
initState
,可能忽略了不少过程,后续咱们会针对更复杂的demo来继续分析一波。
这里你能够先留意到几个关键词Observer
,Dep
,Watcher
。每一个Observer
都有一个独立的Dep
。关于Watcher
,暂时没用到,可是请相信,立刻就能够看到了。
因为本demo没有用到,对本次vm并没有实际影响,因此这一步暂且忽略,有兴趣能够自行翻阅。
这里知道为何在
created
时候,无法操做DOM了吗?由于在这里,尚未涉及到实际的DOM渲染。
这里前面有个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
函数挂载至vm
的options
,而后调用调用原有的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 } 复制代码
Watcher
的构造函数中,本次传入的updateComponent
做为Wather
的getter
。get
方法调用时,又经过pushTarget
方法,将当前Watcher
赋值给Dep.target
getter
,至关于调用vm._update
,先调用vm._render
,而这时vm._render
,此时会将已经准备好的render
函数进调用。render
函数中又用到了this.text
,因此又会调用text
的get
方法,从而触发了dep.depend()
dep.depend()
会调回Watcher
的addDep
,这时Watcher
记录了当前dep
实例。dep.addSub(this)
,dep
又记录了当前Watcher
实例,将当前的Watcher
存入dep.subs
中。demo
尚未使用的,也就是当this.text
发生改变时,会触发Observer
中的set
方法,从而触发dep.notify()
方法来进行update
操做。最后这段文字太干了,能够本身经过断点,耐心的走一遍整个过程。若是没有耐心看完这段描述,能够看看笔者这篇文章100行代码带你玩vue响应式。
就这样,Vue
的数据响应系统,经过Observer
、Watcher
、Dep
完美的串在了一块儿。也但愿经历这个过程后,你能对真正的对这张图,有必定的理解。
固然,$mount
中还有一步被我轻描淡写了,那就是这部分,将template转换为render,render实际调用时,会经历_render
, $createElement
, __patch__
, 方法,有兴趣能够本身浏览下'src/core/vdom/'目录下的文件,来了解vue
针对虚拟dom的使用。
若是你喜欢,能够继续浏览笔者关于vue template转换部分的文章《Vue对template作了什么》。