最近一直忙于找实习,一直奔波于北京和学校两地之间,很荣幸能加入一家小公司,因为以前一直使用的React,如今的公司用VUE,因此花费了两天时间学习了VUE的基本使用,这两天心血来潮准备看看VUE的源码,我只是一个大二的初生牛犊,有错误的地方还望你们指出来,咱们共同窗习。喜欢的请点赞html
今天开始,我准备浅谈一下本身对于VUE源码的理解,同时配套源码的注释,具体地址过两天将会发布 ,同时文章里面所用到的代码都会有一个单独的文章进行汇总 ,今天先说说Vue这个 构造函数vue
使用VUE的时候咱们须要new一下,故追本寻源,在 ./instance/index
文件中找到定义VUE构造函数的代码node
// 从五个文件导入五个方法(不包括 warn)
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'
// 定义 Vue 构造函数
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)
}
// 将 Vue 做为参数传递给导入的五个方法
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
// 导出 Vue
export default Vue
复制代码
能够看出使用率安全模式提醒你使用new操做符来调用VUE,接着将VUE做为参数,传递给了五个引入的方法,最后导出VUE。ios
那么这五个方法又作了什么?git
initMixin
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
// ... _init 方法的函数体,此处省略
}
}
复制代码
原来是在VUE的原型上添加了_init
方法,这个方法应该是内部初始化的一个方法,在上面咱们看到过这个方法。github
也就是说当咱们调用new VUE()
的时候会执行this._init(options)
web
stateMixin
const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }
if (process.env.NODE_ENV !== 'production') {
dataDef.set = function (newData: Object) {
warn(
'Avoid replacing instance root $data. ' +
'Use nested data properties instead.',
this
)
}
propsDef.set = function () {
warn(`$props is readonly.`, this)
}
}
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
复制代码
最后面两句很熟悉,使用Object.definePropety
在Vue.prototype
上定义了两个属性,分别是$data
和$props
,这两个属性的定义分别写在了dataDef
和propsDef
这两个对象上 ,仔细看上面的代码,首先分别是get
,能够能够看到$data
属性实际上代理的是_data
这个属性,而$props
代理的是_props
这个实力属性,而后有一个生产环境的判断,若是不是生产环境的话,就为$data
和$props
这两个属性设置了set,实际上就是想提醒一下你:别想修改我,也就是说$data
和$props
是两个只读的属性。(又get到新的知识点,开心不😄)npm
接下来,stateMixin
又在Vue.prototype
上定义了三个方法api
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
// ...
}
复制代码
分别是$set
,$delete
,$watch
,实际上你都见过这些东西数组
eventsMixin
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {}
Vue.prototype.$once = function (event: string, fn: Function): Component {}
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {}
Vue.prototype.$emit = function (event: string): Component {}
复制代码
这个方法又在Vue.prototype
上添加了四个方法,如上
lifecycleMixin
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
Vue.prototype.$forceUpdate = function () {}
Vue.prototype.$destroy = function () {}
复制代码
这个方法在Vue.prototype
上添加了三个方法,是否是感受很熟悉 ?
renderMixin
这个方法的一开始以Vue.prototype
为参数调用了installRenderHelpers
函数,这个函数来自于与render.js
文件相同目录下的render-helpers/index.js
文件,找到这个函数
export function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
}
复制代码
不难发现,这个函数的做用就是在Vue.prototype
上添加一系列的方法
renderMixin
方法在执行完installRenderHelpers
函数以后,又在Vue.prototype
上添加了两个方法,分别是$nextTick
和_render
,最终通过renderMixin
以后,Vue.prototype
又被添加了以下方法:
// 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 (fn: Function) {}
Vue.prototype._render = function (): VNode {}
复制代码
至此,instance/index.js
文件中的代码就运行完毕了(具体指npm run dev
命令时构建的运行).大概了解了每一个 Mixin
方法的做用骑士就是包装Vue.prototype
,在其上挂载一些属性和方法,下面我会把代码合并在一块,以便于之后查看
依旧按照追本溯源的原则,咱们找到前一个文件core/index.js
,下面是其所有代码 ,一样高效简短
// 从 Vue 的出生文件导入 Vue
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'
// 将 Vue 构造函数做为参数,传递给 initGlobalAPI 方法,该方法来自 ./global-api/index.js 文件
initGlobalAPI(Vue)
// 在 Vue.prototype 上添加 $isServer 属性,该属性代理了来自 core/util/env.js 文件的 isServerRendering 方法
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
// 在 Vue.prototype 上添加 $ssrContext 属性
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 存储了当前 Vue 的版本号
Vue.version = '__VERSION__'
// 导出 Vue
export default Vue
复制代码
上面的代码中,首先从Vue
的出生文件,也就是instance/index.js
文件导入Vue
,而后分别从三个文件导入了三个变量 ,
其中initGlobalAPI
是一个函数,而且以Vue
构造函数做为参数进行调用
initGlobalAPI(Vue)
而后在Vue.prototype
上分别添加了两个只读的属性,分别是:$isServer
和$ssrContext
。接着在Vue
构造函数上定义了FunctionalRenderContext
静态属性,而且FunctionalRenderContext
属性的值来自于core/vdom/create-functional-component
文件,从命名来看,这个属性是为了在SSR中使用它。
最后,在Vue
构造函数上添加了一个静态属性version
,存储了当前Vue
的版本值,可是这里的'_VERSION'
是什么东西呢?找了半天打开scripts.config.js
文件,找到了getConfig
方法,其中有这么一句话:_VERSION_:version
。也就是说最后的_VERSION_
最终将被version
的值替换,而version
的值就是Vu
的版本号
回头继续看看 initGlobalAPI(Vue)
这段代码,貌似是要给Vue
上添加一些全局的API
,实际上就是这样的,这些全局API
以静态属性和方法的形式被添加到Vue
构造函数上,找到这个方法,看看主要作了什么
// config
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
Object.defineProperty(Vue, 'config', configDef)
复制代码
首先是这样 一段代码,意思是给vue···添加一个
config```属性,也是一个只读属性,你修改它会在非生产模式下给你一个友好的提示。
那么Vue.config
的值是什么呢?在src/core/global-api/index/js
文件开头有这样一行代码
import config from '../config'
因此是从这个文件导出的对象。
接着是下面这段代码
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
复制代码
在Vue
中添加了util
属性,这是一个对象,这个对象拥有四个属性分别是: warn
,extend
,ergeOptions
以及defineReactive
,这四个属性来自于core/util/index.js
文件 大概意思就是Vue.util
以及util
下的四个方法都是不被公认是公共API的一部分,要避免依赖他们,可是你依然能够用,不过你仍是不要用的好
而后是这样一段代码
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
复制代码
接着给Vue
添加了三个属性
// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}
复制代码
这段代码给Vue
添加了observable
方法,这个方法先是调用了observe
这个方法,而后返回了obj
(传入的参数)
上面的注释说明这个API是添加Vue
2.6,接着是下面的代码
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios. Vue.options._base = Vue extend(Vue.options.components, builtInComponents) 复制代码
这段代码先是经过object.create()
建立了一个新的空对象,添加到Vue
的options
属性上,接着给options
属性添加了值(ASSET_TYPES
是一个数组),这段代码执行完其实就是这样
Vue.options = {
components: Object.create(null),
directives: Object.create(null),
filters: Object.create(null),
_base: Vue
}
复制代码
接着就是将builtInComponents
的属性混合到Vue.options.components
中
最终Vue.options.components
的值以下:
Vue.options.components = {
KeepAlive
}
复制代码
那么到如今为止,Vue.options
已经变成了这个亚子
Vue.options = {
components: {
KeepAlive
},
directives: Object.create(null),
filters: Object.create(null),
_base: Vue
}
复制代码
咱们继续看代码
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
复制代码
这四个方法来自于四个文件 ,咱们分别来看看
initUse
/* @flow */
import { toArray } from '../util/index'
export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
// ...
}
}
复制代码
其实很简单,就是在Vue
函数上添加use
方法,也就是咱们常常在main.js
文件中用的全局API,用来安装VUE
插件。
initMixin
/* @flow */
import { mergeOptions } from '../util/index'
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
}
复制代码
这段代码就是在Vue
中添加mixin
这个全局API
initExtend
export function initExtend (Vue: GlobalAPI) {
/**
* Each instance constructor, including Vue, has a unique
* cid. This enables us to create wrapped "child * constructors" for prototypal inheritance and cache them.
*/
Vue.cid = 0
let cid = 1
/**
* Class inheritance
*/
Vue.extend = function (extendOptions: Object): Function {
// ...
}
}
复制代码
initExtend
方法在Vue
中添加了Vue.cid
静态属性,和Vue.extend
静态方法
initAssetRegisters
export function initAssetRegisters (Vue: GlobalAPI) {
/**
* Create asset registration methods.
*/
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
// ......
}
})
}
复制代码
其中某个东西咱们以前见过了 ,因此最终initAssetRegisters
方法,Vue
又多了三个静态方法
Vue.component
Vue.directive
Vue.filter
复制代码
这三个方法你们确定不陌生,分别用来全局注册组件,指令和过滤器。
这样 咱们大概了解了上述几个文件的做用
如今,咱们弄清了Vue
构造函数的过程当中的两个主要的文件,分别是:core/instance/index.js
,core/index.js
文件,咱们知道core
目录下的文件存放的是与平台无关的代码,可是,Vue
是一个Multi-platform
的项目,不一样平台可能会内置不一样的组件,指令或者一些平台特有的功能等,那么就须要根据不一样的平台进行平台化地包装。
咱们打开platforms
目录,能够发现有两个子目录web
和weex
。这两个子目录的做用就是分别为相应的平台对核心的Vue
进行包装的。咱们先去看看web
的根文件
/* @flow */
import Vue from 'core/index'
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser } from 'core/util/index'
import {
query,
mustUseProp,
isReservedTag,
isReservedAttr,
getTagNamespace,
isUnknownElement
} from 'web/util/index'
import { patch } from './patch'
import platformDirectives from './directives/index'
import platformComponents from './components/index'
// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
复制代码
首先依旧是导入了不少文件,而后对core/config.js
文件进行一些修改吧(本来文件里的对象大部分属性都是初始化了一个初始值),注释的意思是这个配置是与平台有关的,极可能会被覆盖掉,这个时候咱们回来再看看代码,其实就是在覆盖默认导出的config
对象的属性,至于这些东西的做用,暂时还不知道
接着是下面两句代码
/ install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
复制代码
安装特定平台运行时的指令和组件,以前咱们已经看到过Vue.options
是什么样的,通过这样一番折腾,变成啥了?
咱们先看看platformDirectives
和platformComponents
长什么样,顺着导入的文件地址,咱们看到platformDirectives
实际是这样
platformDirectives = {
model,
show
}
复制代码
也就是通过 extend(Vue.options.directives, platformDirectives)
以后,Vue.options
将变成:
Vue.options = {
components: {
KeepAlive
},
directives: {
model,
show
},
filters: Object.create(null),
_base: Vue
}
复制代码
一样的道理,变化以后是下面这样的
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: Object.create(null),
_base: Vue
}
复制代码
如今咱们搞清楚了做用,就是Vue.options
上添加web
平台运行时特定的指令和组件。
接着往下看看
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
复制代码
首先在Vue.prototype
上添加了_patch_
方法,若是浏览器环境运行的话这个方法的值为patch
函数,不然是一个空函数noop
,而后又在Vue.prototype
上添加了$mount
方法,暂时不须要关注方法的做用和内容吧。
再往下的一段代码
if (inBrowser) {
setTimeout(() => {
if (config.devtools) {
if (devtools) {
devtools.emit('init', Vue)
} else if (
process.env.NODE_ENV !== 'production' &&
process.env.NODE_ENV !== 'test'
) {
console[console.info ? 'info' : 'log'](
'Download the Vue Devtools extension for a better development experience:\n' +
'https://github.com/vuejs/vue-devtools'
)
}
}
if (process.env.NODE_ENV !== 'production' &&
process.env.NODE_ENV !== 'test' &&
config.productionTip !== false &&
typeof console !== 'undefined'
) {
console[console.info ? 'info' : 'log'](
`You are running Vue in development mode.\n` +
`Make sure to turn on production mode when deploying for production.\n` +
`See more tips at https://vuejs.org/guide/deployment.html`
)
}
}, 0)
}
export default Vue
复制代码
这段代码是vue-tools
的全局钩子,它被包裹在setTimeout
中,最后导出了Vue
在看完上面这个文件以后,其实运行
时版本的Vue
构造函数已经"成型",咱们看到entry-runtime.js
文件只有两行代码,
import Vue from './runtime/index'
export default Vue
复制代码
能够发现,运行时
版的入口文件,导出的Vue
就在./runtime/index.js
为止。而后咱们须要了解完整的Vue
,入口文件是entry-runtime-with-compiler.js
,因此完整版和运行版差异就在compiler,因此咱们要看的这个文件做用就是在运行时版本的基础上添加compiler
,先看看文件的代码
/* @flow */
import config from 'core/config'
import { warn, cached } from 'core/util/index'
import { mark, measure } from 'core/util/perf'
import Vue from './runtime/index'
import { query } from './util/index'
import { compileToFunctions } from './compiler/index'
import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
return mount.call(this, el, hydrating)
}
/**
* Get outerHTML of elements, taking care
* of SVG elements in IE as well.
*/
function getOuterHTML (el: Element): string {
if (el.outerHTML) {
return el.outerHTML
} else {
const container = document.createElement('div')
container.appendChild(el.cloneNode(true))
return container.innerHTML
}
}
Vue.compile = compileToFunctions
export default Vue
复制代码
看的出来先是导出了不少文件以及运行时的Vue,还从./compiler/index.js
文件中导入compileToFunctions
,后边根据id获取元素的innerHTML
,接着使用mount
变量缓存了Vue.prototype.$mount
方法,而后重写了Vue.prototype.$mount
方法,后续接着获取元素的outerHTML
,最终在Vue
上添加了一个全局的API:compileToFunctions
,导出了Vue
。
看完是否是有点懵逼?这个文件运行下来对Vue的影响有两个,第一个影响是它重写了Vue.prototype.$mount
方法;第二个是添加了Vue.compile
全局API,至于具体作什么,咱们一步一步看!
首先,它待遇el
作了限制,Vue不能挂载在body
,html
这样的根节点上,接下来的是很关键的逻辑:若是没有定义redner
方法,则会把el
或者template
字符串转换成render
方法。刚学习Vue不了解之前什么状况,在Vue2.0版本上,全部的Vue的组件的渲染最终都须要render
方法,不管咱们是用单文件的.vue
文件仍是写了el
或者template
属性,最终都会转换成render
方法,这个过程是一个"在线编译"的过程,它是调用compileToFunctions
方法实现的,这个后续介绍。最后,调用原先原型上的$mount
方法挂载。