本系列文章,基于vue 2.6.11进行解析,不追究每行代码分析清楚,但求把握大体的重点,比如源码构建流程;如何实现数据双向绑定;如何解析模板;如何解析一个组件的data,method,computed等属性;如何实现在weex,web等多场景下运行。理解有限,有问题希望指正。
我们拿到源码,其实一般有两个着手点
- 从代码执行的入口,比如vue执行,一般是 new Vue() 一个实例出来,所以直接在代码中搜索vue的类的定义,就是入口了。
- 从代码构建的入口,我们一般执行 npm run build进行构建,那么就要看package.json文件。
本篇先看看代码构建:
...
{
"build": "node scripts/build.js", //打包web端vue
"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer", //打包ssr版vue
"build:weex": "npm run build -- weex", //打包weex版vue
}
...
我们看到scripts属性中的build,由于vue支持在web,weex,server几个环境中使用,所以有三种build指令, 都会执行build.js。
let builds = require('./config').getAllBuilds()
if (process.argv[2]) {
const filters = process.argv[2].split(',')
builds = builds.filter(b => {
return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
})
} else {
builds = builds.filter(b => {
return b.output.file.indexOf('weex') === -1
})
}
build.js会先从config文件中读取所有的配置, 然后根据入参过滤出对应的配置,可以看到如果是执行 npm run build, 会取出除weex以外,所有的配置项。
拿到config之后,最终会调用rollup.rollup(config),进行编译,所以vue是采用rollup而不是webpack;
来看看config.js中的配置项:
...
// Runtime+compiler CommonJS build (CommonJS)
'web-full-cjs-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.common.dev.js'),
format: 'cjs',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
'web-full-cjs-prod': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.common.prod.js'),
format: 'cjs',
env: 'production',
alias: { he: './entity-decoder' },
banner
},
// Runtime only ES modules build (for bundlers)
'web-runtime-esm': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.esm.js'),
format: 'es',
banner
},
// Runtime+compiler ES modules build (for bundlers)
'web-full-esm': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.esm.js'),
format: 'es',
alias: { he: './entity-decoder' },
banner
},
...
这里面有非常多的配置项,比如 web-full-cjs-dev用于打包出 Runtime+compiler两部分的符合commonjs规范的vue版本;web-runtime-esm用于打包出只有Runtime部分的ES modules格式的vue版本。 至于Runtime,compiler是什么,我们后面看。
接下来我们选个全一点的配置看,比如web-full-cjs-prod,入口是:web/entry-runtime-with-compiler.js, 最终会生成 dist/vue.common.prod.js。
web/entry-runtime-with-compiler.js实际目录在src/platforms/web/ 下
可以看到存在多种entry,对应的也是不同的config:
对比下:
entry-runtime.js:
/* @flow */
import Vue from './runtime/index'
export default Vue
entry-runtime-with-compiler.js:
/* @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
可以看到entry-runtime-with-compiler.js中对 ./runtime/index.js 中的$mount进行了覆盖包装,$mount函数实际就是对模板进行解析,并生成html插入到页面中。这里也涉及到了非常重要的render方法的生成,后续再回来看。
总结:
1.vue的构建流程,就是根据不同的配置项,采用rollup构建出来。
2.每个构建配置项,对应一个entry.js。 不同的entry.js最终打包出来的vue的功能上会有差别,因为在上面挂载了不同的方法。