为了分析 Vue 的源码,我们需要先从入口文件开始。Vue 的入口文件在 src/platform/web/entry-runtime-with-compiler.js
中。
1. 入口文件的基本结构
入口文件主要做了以下几个事情:
- 定义了 Vue 构造函数。
- 重写了 Vue.prototype._init 方法。
- 定义了 $mount 方法。
- 扩展了 Vue 构造函数的静态属性和方法。
接下来,我们将逐条解析入口文件的代码。
首先是 import 部分:
import Vue from './runtime/index'
import { query } from './util/index'
import { compileToFunctions } from './compiler/index'
import { shouldDecodeNewlines } from './util/compat'
import { shouldDecodeNewlinesForHref } from './util/compat'
其中,import Vue from './runtime/index'
引入了 Vue 的 runtime-only 版本,该版本中没有模板编译器,因此会快一些,但需要在编译时指定渲染函数。接着,引入了一些工具函数和编译相关的方法,可以理解成 Vue 的底层实现。
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
// 处理不能挂载到 html 或 body 元素的情况
if (el === document.body || el === document.documentElement) {
if (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, {
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)
}
接下来,是重写 Vue.prototype._init 方法的代码:
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-init:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
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 {
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)
}
}
该方法会在创建 Vue 实例时被调用,它主要做了以下几个事情:
- 给 Vue 实例分配唯一的
_uid
属性。 - 在非生产模式下,打印出实例初始化的开始和结束标记。
- 判断是否是 Vue 实例对象,并赋值
_isVue
属性为true
。 - 合并 options。
- 在非生产环境下,初始化 Proxy。
- 给实例对象赋值
_self
属性。 - 初始化生命周期。
- 初始化事件。
- 初始化渲染。
- 调用
beforeCreate
钩子函数。 - 解析注入。
- 初始化状态。
- 暴露出正确的
this
。 - 解析 Provider。
- 调用
created
钩子函数。 - 调用
$mount
方法。
接着,是定义 $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) {
if (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, {
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)
}
$mount 方法主要做了以下几个事情:
- 检查是否已经传入参数 el,如果已经传入,则调用 query 方法获取对应的 DOM 元素。
- 检查 el 是否为 body 或 html 标签,如果是则在非生产环境下打印出警告并且返回 Vue 实例对象。
- 获取 Vue 实例对象的 options 属性。
- 如果没有 options.render 方法,则检查是否有 options.template 或 el 属性。
- 如果存在 template 属性,则将 template 编译成 render 和 staticRenderFns 函数,并分别挂载到实例对象的 options.render 和 options.staticRenderFns 上。
- 调用 $mount 方法的原始实现。
最后,是扩展 Vue 构造函数的静态属性和方法:
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.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
// 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)
}
// devtools global hook
/* istanbul ignore next */
if (inBrowser) {
setTimeout(() => {
if (config.devtools) {
if (devtools) {
devtools.emit('init', Vue)
} else {
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' && 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
其中 patch
函数是在 Vue 的运行时版本中导入的,它是将虚拟 DOM 转换成真实 DOM 的核心方法。接着,Vue 在全局注册了一些与平台相关的指令和组件,包括 v-model 和 transition。然后,将浏览器和服务器端的 $mount 方法的不同实现都绑定到 Vue.prototype 上。
最后就是一个调试工具 devtools 和开发模式的提示信息。在浏览器环境下,如果开启了 Vue Devtools,会触发 devtools 的 init
事件,并将 Vue 对象作为参数传递给它,以启用 devtools 的功能。如果开发环境下,Vue 将根据是否关闭了 productionTip 显示不同的提示信息。
2. 示例说明
示例1
在实例化 Vue 对象时,两个字符串类型的参数来初始化该 Vue 实例,其代码如下:
const app = new Vue({
el: '#app',
template: '<h1>Hello, {{ name }}!</h1>',
data() {
return {
name: 'Vue'
}
}
})
此时,Vue 会自动执行编译过程,将 template 属性编译为渲染函数,并将该渲染函数赋值给 options.render。最后,调用 $mount 方法,对 DOM 节点进行渲染。
示例2
在 Vue 实例对象创建完成之后,手动调用 $mount 方法进行DOM渲染。
const app = new Vue({
data() {
return {
message: 'Hello Vue!'
}
}
})
app.$mount('#app')
在该代码中,我们创建了一个 Vue 实例,但是没有传入 el 和 template 参数,因此在创建时被挂载的 DOM 将为空。然后我们手动调用 $mount 方法来将 Vue 实例挂载到 DOM 上。这样,Vue 通过 $mount 方法内部处理,将渲染函数生成真实的 DOM 元素,并且将该 DOM 元素插入到 #app 中。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:vue源码入口文件分析(推荐) - Python技术站