Vue2源码分析
文章目录
资源
vue源码
当前版本:2.6.14
迁出项目:git clone https://github.com/vuejs/vue.git
文件架构
dist —> 发布目录,存放不同Vue版本,如完整版或运行时版
examples —> 范例,存放测试代码,直接用浏览器打开即可
flow —> Vue源码的类型检查是flow,存放flow的类型声明文件
types —> Vue新版支持typescript,此目录是ts的类型声明文件
packages —> 核心代码之外的独立库
scripts ----> 构建脚本
src ----> 源码
>> compiler ----> 编译器相关
>>> codegen —> 把AST转换为Render函数
>>> parser —> 解析模板成AST
>> platforms ----> 架构相关
>>> weex
>>> web
>>>> runtime ----> web端运行时相关代码
>>>> compiler ----> web编译器相关代码,将模板编译成render函数
>>>> server ----> 服务端渲染
>>>> util —> 工具类
>> core ----> 核心代码
>>> components —> 通用组件,如keep-alive
>>> global-api —> 全局api
>>> instance —> 构造函数等
>>> observer —> 响应式相关
>>> util —> 工具方法
>>> vdom —> 虚拟dom相关
调试环境搭建
- 安装依赖:
npm i
—> 安装phantom.js时即可终止 - 安装rollup:
npm i -g rollup
- 打开package.json,修改dev脚本,添加sourcemap --> 源码映射,可以在浏览器中使用源码调试
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
-
运行开发命令:
npm run dev
----> 打包文件放在dist目录下,文件名为vue.js -
在examples目录下的测试案例中,引入前面创建的vue.js
入口
执行npm run dev
命令,其实际执行的是package.json中scripts下的dev脚本:
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
dev脚本中-c scripts/config.js
指明配置文件的位置;TARGET:web-full-dev
指明了输出文件的配置项
'web-full-dev': {
// 入口: src/platforms/web/entry-runtime-with-compiler.js
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.js'), // 输出文件的目录dist/vue.js
format: 'umd',// umd版本
env: 'development',
alias: {
he: './entity-decoder' },
banner
},
在dist目录下有很多Vue.js的构建版本:
- 完整版:包含编译器和运行时的版本;
- 编译器:将template编译成javascript渲染函数的代码;
- 运行时:用来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码。基本上就是除去编译器的其它一切;
- UMD:UMD 版本可以通过
<script>
标签直接用在浏览器中,默认文件就是运行时 + 编译器的UMD版本 (vue.js
); - CommonJS:CJS 版本用来配合老的打包工具比如webpack 1,只包含运行时的版本 (
vue.runtime.common.js
); - ES Module:ESM版本用于webpack2+,默认文件是只有运行时版本 (
vue.runtime.esm.js
),我们使用vue-cli创建的项目使用的就是这个版本。
// vue.runtime.esm.js:由于cli使用的vue版本不带编译器,因此无法使用template或el,只能用render
new Vue({
// template: '<div>template</div>',
render(h) {
return h(App)
}
}).$mount('#app')
// examples/test/init.html:测试代码
<!DOCTYPE html>
<html>
<head>
<title>Vue源码剖析</title>
<!-- 这里使用的vue.js是带编译器、umd格式的版本 -->
<!-- 平时cli创建项目,使用的vue.runtime.esm.js,不带编译器 -->
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="demo">
<h1>初始化流程</h1>
<p>{
{
foo }}</p>
<p>{
{
obj.foo}}</p>
<div v-for="a in arr" :key="a">
{
{
a }}
</div>
<!--普通事件-->
<p @click="onClick">this is p</p>
<!--自定义事件-->
<comp @comp-click="onCompClick"></comp>
</div>
<script>
Vue.component('comp', {
template: `
<div @click="onClick">this is comp</div>
`,
methods: {
onClick() {
this.$emit('comp-click')
}
}
})
// render > template > el
// 创建vue实例
// 省略了$mount()
const app = new Vue({
el: '#demo',
// template: '<div>template</div>',
// template: '#app',
// render(h){return h('div','render')},
data:{
foo:'foo',
obj: {
foo: 'obj_foo' },
arr: ['arr_foo', 'arr_bar']
},
methods: {
onClick() {
console.log('普通事件');
},
onCompClick() {
console.log('自定义事件');
}
},
mounted() {
setTimeout(() => {
this.foo = 'fooooo'
}, 1000);
},
beforeCreate(){
console.log('beforeCreate ');
}
})
// 由于cli使用的vue版本不带编译器,因此无法使用template或el,只能用render
// new Vue({
// render(h) {
// return h(App)
// }
// }).$mount('#app')
console.log(app.$options.render);
</script>
</body>
</html>
// src/platforms/web/entry-runtime-with-compiler.js
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
})
// 扩展$mount:解析初始化选项
// new Vue().$mount(el)
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// el:#demo ---> 获取el对应的dom对象
el = el && query(el)
// this指的是vue实例或VueComponent实例,$options挂在在Vue实例上的属性,会经过this._init(options)进行合并
const options = this.$options
// 若不存在render属性
if (!options.render) {
// 获取options中的template属性值
let template = options.template
// 若存在template
if (template) {
// 若template为string,如 template: '#app' 或 template: '<div>template</div>'
if (typeof template === 'string') {
// 若template第一个字符为#,如 #app
// 若首字母不是#,则template = '<div>template</div>'
if (template.charAt(0) === '#') {
// 拿到template中id对应的dom对象
template = idToTemplate(template)
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,将el对象的dom对象中的outerHTML拿出,作为template
template = getOuterHTML(el)
}
// 若template或el生成的template存在
if (template) {
// 编译器函数compileToFunctions将拿到的template编译成render函数
// render() ---> vdom
const {
render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
// 将render重新设置到options上
options.render = render
options.staticRenderFns = staticRenderFns
}
}
// 默认工作:挂载
return mount.call(this, el, hydrating)
}
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
// src/platforms/web/runtime/index.js
import Vue from 'core/index'
import {
extend, noop } from 'shared/util'
import {
mountComponent } from 'core/instance/lifecycle'
import {
devtools, inBrowser } from 'core/util/index'
import {
patch } from './patch'
import platformDirectives from './directives/index'
import platformComponents from './components/index'
// 安装平台运行时指令和组件
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
// 安装平台特有的patch函数:执行diff算法
Vue.prototype.__patch__ = inBrowser ? patch : noop
// public mount method
// Vue原型上定义$mount方法:实际上调用mountComponent, vdom => dom,并添加到页面上dom树上
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
// 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:定义Vue类属性,如Vue.set/delete/nextTick/observable/use/mixin/extend
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
// 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'
// 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')
}
// 初始化: _init()在哪?
this._init(options)
}
// mixin做什么的?声明vue实例属性和方法
initMixin(Vue) // _init()
stateMixin(Vue) // $attrs/$listeners/$set / $delete/$watch
eventsMixin(Vue) // $on()/$emit()/$once()/$off()
lifecycleMixin(Vue) // _update()/$forceUpdate()/$destroy()
renderMixin(Vue) // $nextTick()/_render()
export default Vue
文件加载顺序:
src/core/instance/index.js
- initMixin:Vue原型上(Vue.prototype),定义_init()
- stateMixin:Vue原型上,定义 d a t a ( 只 读 ) / data(只读)/ data(只读)/props(只读)/ s e t ( ) / set()/ set()/delete()/$watch()
- eventsMixin:Vue原型上,定义 o n ( ) / on()/ on()/emit()/ o n c e ( ) / once()/ once()/off()
- lifecycleMixin:Vue原型上,定义_update()/ f o r c e U p d a t e ( ) / forceUpdate()/ forceUpdate()/destroy()
- renderMixin:Vue原型上,定义$nextTick()/_render()
src/core/index.js
-
initGlobalAPI:定义Vue类属性和方法
–> Vue.config(只读)
–> Vue.util
–> Vue.set
–> Vue.delete
–> Vue.nextTick
–> Vue.observable
—> Vue.options.components/directives/filters/_base
–> initUse:Vue.use
–> initMixin:Vue.mixin
–> initExtend:Vue.extend
–> initAssetRegisters:Vue.component / Vue.directive / Vue.filter
src/platforms/web/runtime/index.js
- 安装web平台特有指令和组件
- 在Vue.prototype上定义__patch__:补丁函数,执行patching算法进行更新
- 在Vue.prototype上定义$mount:挂载vue实例到指定宿主元素(vdom => dom)
src/platforms/web/entry-runtime-with-compiler.js
- 扩展默认$mount方法:处理template或el选项
初始化流程
创建Vue实例时,只执行this._init(options)
函数,因此从_init函数开始解析。
// 测试demo
<!DOCTYPE html>
<html>
<head>
<title>Vue源码剖析</title>
<!-- 这里使用的vue.js是带编译器、umd格式的版本 -->
<!-- 平时cli创建项目,使用的vue.runtime.esm.js,不带编译器 -->
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="demo">
<h1>初始化流程</h1>
<p>{
{
foo }}</p>
<p>{
{
obj.foo}}</p>
<div v-for="a in arr" :key="a">
{
{
a }}
</div>
<!--普通事件-->
<p @click="onClick">this is p</p>
<!--自定义事件-->
<comp @comp-click="onCompClick"></comp>
</div>
<script>
Vue.component('comp', {
template: `
<div @click="onClick">this is comp</div>
`,
methods: {
onClick() {
this.$emit('comp-click')
}
}
})
// render > template > el
// 创建vue实例
// 省略了$mount()
const app = new Vue({
el: '#demo',
// template: '<div>template</div>',
// template: '#app',
// render(h){return h('div','render')},
data:{
foo:'foo',
obj: {
foo: 'obj_foo' },
arr: ['arr_foo', 'arr_bar']
},
methods: {
onClick() {
console.log('普通事件');
},
onCompClick() {
console.log('自定义事件');
}
},
mounted() {
setTimeout(() => {
this.foo = 'fooooo'
}, 1000);
},
beforeCreate(){
console.log('beforeCreate ');
}
})
// 由于cli使用的vue版本不带编译器,因此无法使用template或el,只能用render
// new Vue({
// render(h) {
// return h(App)
// }
// }).$mount('#app')
console.log(app.$options.render);
</script>
</body>
</html>
// src/core/instance/init.js:执行Vue原型上的_init方法
Vue.prototype._init = function (options?: Object) {
// 原型方法中的this表示实例,这里指的是vue实例;这里的作用是防止this指向改变,存为vm
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-start:${
vm._uid}`
endTag = `vue-perf-end:${
vm._uid}`
mark(startTag)
}
vm._isVue = true
// 代码分析点一:合并选项
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
)
}
// 代码分析点二:代理
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// 代码分析点三:挂载实例属性
vm._self = vm
initLifecycle(vm) // $parent/$root/$refs/..
// <comp @click>
initEvents(vm) // event
// <comp>xxxxxx</comp>
initRender(vm) // $slots/$scopSlots/$createElement()
// 代码分析点四:执行生命周期钩子
callHook(vm, 'beforeCreate')
// 代码分析点五:注入组件状态相关的属性
initInjections(vm) // 注入祖辈传下来的数据
initState(vm) // 组件数据初始化,包括了props/methods/data/computed/watch
initProvide(vm) // 给后代传递数据
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)
}
// 代码分析点六:执行$mount
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
代码分析点一:合并选项
// 合并选项: 默认选项和用户选项
if (options && options._isComponent) {
// 1.1 若是Vue组件,即VueComponent实例
initInternalComponent(vm, options)
} else {
// 1.2 Vue实例
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {
},
vm
)
}
// 参数分析start
// 参数1:options --> 执行new Vue时,传入构造函数的对象
options = {
beforeCreate: ƒ beforeCreate(),
data:{
arr: ["arr_foo", "arr_bar"],
obj: {
foo: "obj_foo"},
foo: "foo"
},
el: "#demo"
methods: {
onClick: ƒ onClick(),
onCompClick: ƒ onCompClick()
},
mounted: ƒ mounted()
}
// 参数2:vm.constructor ---> Vue的构造函数,可以访问Vue的类属性
// 如initGlobalAPI中定义的Vue.util/Vue.set/Vue.delete/Vue.nextTick/Vue.observable/Vue.options
// 函数3:resolveConstructorOptions(vm.constructor) --> 函数的作用是将Ctor中的options和其父类的options合并并返回
// 由于这里是根组件,没有父类,因此直接返回Vue.options
export function resolveConstructorOptions (Ctor: Class<Component>) {
let options = Ctor.options
// 若Ctor是Vue.extend构建的子类,则存在super属性
if (Ctor.super) {
//对Ctor的父类进行递归遍历,获取其父类-类属性options
const superOptions = resolveConstructorOptions(Ctor.super)
// 也可以通过Ctor来获取父类的options
const cachedSuperOptions = Ctor.superOptions
// 若不相等,则说明父类中的options发生了改变
if (superOptions !== cachedSuperOptions) {
// 重新挂载父类的options
Ctor.superOptions = superOptions
// 检查是否有后期修改或附加的option
const modifiedOptions = resolveModifiedOptions(Ctor)
// 更新子类的options
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions)
}
// 父类和子类的options进行合并
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
return options
}
// 函数3执行结果如下:
{
components: {
KeepAlive: {
name: "keep-alive", abstract: true, props: {
…}, created: ƒ, destroyed: ƒ, …},
Transition: {
name: "transition", props: {
…}, abstract: true, render: ƒ},
TransitionGroup: {
props: {
…}, methods: {
…}, beforeMount: ƒ, render: ƒ, updated: ƒ},
comp: ƒ VueComponent(options)
},
directives: {
model: {
inserted: ƒ, componentUpdated: ƒ},
show: {
bind: ƒ, update: ƒ, unbind: ƒ}
},
filters: {
},
_base: ƒ Vue(options)
}
// 函数4:mergeOptions:参数1是Vue类属性中的options,命名为parent,参数2是执行Vue构造函数传入的options,命名为child,参数3是Vue实例
export function mergeOptions (parent: Object,child: Object,vm?: Component): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
// 由于child传入的props、inject、directives可以是数组或对象,因此对其格式化处理,统一转换为对象
// 举例:props:['foo'] 或 props:{ foo: {type:String,required: true}}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
// 将child中的extends和mixins合并到parent中去
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
// 将parent和child进行合并,对于不同的key,其合并策略不同,有的是替换有的是组合
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
}
// 举例:钩子函数合并策略:将parentVal和childVal合并成数组并返回
function mergeHook (parentVal: ?Array<Function>,childVal: ?Function | ?Array<Function>): ?Array<Function> {
const res = childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
return res ? dedupeHooks(res) : res
}
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured'
];
LIFECYCLE_HOOKS.forEach(function (hook) {
strats[hook] = mergeHook;
})
// 举例:components/directives/filters合并策略:返回{...childVal,__proto__: parentVal}
function mergeAssets (
parentVal: ?Object,
childVal: ?Object,
vm?:</