对源码的分析是根据这篇http://hcysun.me/vue-design/ 来学习的,毕竟是第一次读源码。
先把vue的源码从尤大的github上clone到本地,然后开始分析。
首先需要了解一下vue的构建工具rollup和类型检查工具flow。(官网)
然后正式开始看源码吧,
1、首先是构建输出,有很多版本,cjs、es、umd,和运行版、完整版,cjs是结合构建工具browserify 和 webpack 1 使用的,es是webpack2+ 以及 Rollup 使用,而umd是直接使用< script>标签就可以引用的版本
为什么要分 运行时版 与 完整版?首先你要知道一个公式:运行时版 + Compiler = 完整版。也就是说完整版比运行时版多了一个 Compiler,一个将字符串模板编译为 render 函数的家伙,大家想一想:将字符串模板编译为 render 函数的这个过程,是不是一定要在代码运行的时候再去做?当然不是,实际上这个过程在构建的时候就可以完成,这样真正运行的代码就免去了这样一个步骤,提升了性能。同时,将 Compiler 抽离为单独的包,还能减小了库的体积。
但是为什么需要完整版呢?说白了就是允许你在代码运行的时候去现场编译模板,在不配合构建工具的情况下可以直接使用,但是更多的时候推荐你配合构建工具使用运行时版本。
2、Vue的构造函数
在使用vue的时候,我们都是new Vue,说明vue是个构造函数,那么先来找到它,根据构建的时候的npm run dev,找到dev,发现执行的是
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
然后找到web-full-dev
'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及输出文件dist/vue.js,根据alias找一下web别名
web: resolve('src/platforms/web'),
然后找到入口文件entry-runtime-with-compiler.js,发现
import Vue from './runtime/index'
是从这个文件导过来的vue,然后再找这个文件,层层嵌套,最后找到了./instance/index.js,最终找到了vue的真正源头:
// 从五个文件导入五个方法(不包括 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
3、当我们使用vue的时候,一般会这样定义:
var vm = new Vue({
el: '#app',
data: {
test: 1
}
})
<div id="app">{{test}}</div>
分析这个例子,讲清楚vue的运行机制。
首先构造函数new Vue创建一个实例,传入一个options,我们回头看构造函数,调用的是this._init方法,传入options,也就是上面那个对象
{
el: '#app',
data: {
test: 1
}
}
那么这个_init方法在哪定义的?之前一步一步找到Vue构造函数的时候看过这个方法,在src/core/instance/init.js中定义
_init 方法的一开始,是这两句代码:
const vm: Component = this
// a uid
vm._uid = uid++
首先声明了常量 vm,其值为 this 也就是当前这个 Vue 实例啦,然后在实例上添加了一个唯一标示:_uid,其值为 uid,uid 这个变量定义在 initMixin 方法的上面,初始化为 0,可以看到每次实例化一个 Vue 实例之后,uid 的值都会 ++。
后面是性能分析的代码,暂且不看,到这段:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
定义了实例的$options属性,这个mergeOptions函数是合并用的,先看resolve这个函数,传进去的是构造函数,里面的代码大概如下
function resolveConstructorOptions (Ctor: Class<Component>) {
let options = Ctor.options
return options
}
也就是把构造函数的options返回(构造函数本身定义了options属性,写死的但可以覆盖)
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives:{
model,
show
},
filters: Object.create(null),
_base: Vue
}
然后这个mergeOptions第二个参数是options就是我们传进去的对象,第三个是实例。
好现在来看mergeOptions这个函数,这个函数的意思大概就是跟名称一样,把传进去的参数merge到一个对象中,也就是把Vue.options和options合并成一个新的对象,但这个函数还不止做了合并这一件事,在合并之前会先检查你传入的options是否合乎要求,然后将你传进去的options规范化,也就是将你写的简单化的语法都变成规范的,比如
props: {
someData1: Number,
someData2: {
type: String,
default: ''
}
}
那么经过这个函数,props 将被规范为:
props: {
someData1: {
type: Number
},
someData2: {
type: String,
default: ''
}
}
还规范化了inject、directives这两个选项。
后面就是合并这些东西了,合并也需要一定的策略(比如如果是子组件需要和父组件的某些属性合并),比如说如果是自定义的options,就采用默认策略直接在$options属性中加上,如果是data就会采用data的策略将它初始化成一个函数(执行才得到值),如果是el采用el的合并策略,props采用props的合并策略,create生命周期函数也有自己的合并策略,反正最后都会加在实例的$options属性上。也就是说合并这一步是将vue实例加上$options属性。
4、合并完传给Vue的选项之后,继续看初始化vue实例时候使用的_init方法
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
这个是在实例上加一个proxy代理(ES6 has),主要作用是当访问一个实例上不存在的属性的时候,在开发阶段给我们一个友好而准确的提示(warning)
5、之后初始化一些生命周期函数
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')
都是初始化一些变量,在实例上添加一些属性,callhook(vm,beforeCreate)在初始化其他变量之前调用自己定义的beforecreate函数,callHook(vm, ‘created’)就是在初始化完了之后调用created函数
6、数据响应系统
在上面的initState函数里初始化了很多选项,有使用 initProps 函数初始化 props 属性;使用 initMethods 函数初始化 methods 属性;使用 initData 函数初始化 data 选项;使用 initComputed 函数和 initWatch 函数初始化 computed 和 watch 选项
我们直接看initData函数,了解响应式系统
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
在initData函数前半部分(observe之前)做了这么几件事
根据 vm.$options.data 选项获取真正想要的数据(注意:此时 vm.$options.data 是函数)
校验得到的数据是否是一个纯对象
检查数据对象 data 上的键是否与 props 对象上的键冲突
检查 methods 对象上的键是否与 data 对象上的键冲突
在 Vue 实例对象上添加代理访问数据对象的同名属性
最后调用 observe 函数开启响应式之路
响应式先占坑
7、回到init方法,初始化生命周期、事件、 props、 methods、 data、 computed 与 watch 。其中最重要的是通过 Object.defineProperty 设置 setter 与 getter 函数,用来实现「响应式」以及「依赖收集」。当这些都完成之后会调用 $mount 挂载 组件,如果是运行时编译,即不存在 render function 但是存在 template 的情况,需要进行「编译」步骤。
实际上完整版 Vue 的 $mount 函数要做的核心事情就是编译模板(template)字符串为渲染函数,并将渲染函数赋值给 vm.$options.render 选项,这个选项将会在真正挂载组件的 mountComponent 函数中,然后解析模板并生成虚拟dom然后渲染成实际dom