-
Notifications
You must be signed in to change notification settings - Fork 15
Description
Vue源码系列--初始化
最近在看 Vue 的源码架构,打算在公司组织 Vue 源码的分享会,所以准备做一系列关于 Vue 源码的技术输出。
目录结构
先来大致看下 vue 目录结构,这里只列出 src 目录下的文件结构。
├── src
├── compiler -------------------------------- 编译器代码,将 template 模板编译成 render 函数
├── core ------------------------------------ 核心代码,主要包括响应式原理,vdom,全局 API 等
├── platforms ------------------------------- 平台代码
├── web --------------------------------- web 端代码
├── weex -------------------------------- 移动端混合开发代码
├── server ----------------------------------- ssr 服务端渲染
├── sfc -------------------------------------- .vue 单文件组件解析
├── shared ----------------------------------- 通用代码,定义的一些工具函数和常量
Vue 的入口文件
分析 Vue 源码首先要找到 Vue 的入口文件,从入口开始,一步步深入了解实现原理。在跟目录下的 package.json 文件中找到构建配置。
"scripts": {
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
"dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-cjs-dev",
"dev:esm": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-esm",
"dev:test": "karma start test/unit/karma.dev.config.js",
"dev:ssr": "rollup -w -c scripts/config.js --environment TARGET:web-server-renderer",
"dev:compiler": "rollup -w -c scripts/config.js --environment TARGET:web-compiler ",
"dev:weex": "rollup -w -c scripts/config.js --environment TARGET:weex-framework",
"dev:weex:factory": "rollup -w -c scripts/config.js --environment TARGET:weex-factory",
"dev:weex:compiler": "rollup -w -c scripts/config.js --environment TARGET:weex-compiler ",
"build": "node scripts/build.js",
"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
"build:weex": "npm run build -- weex",
"test": "npm run lint && flow check && npm run test:types && npm run test:cover && npm run test:e2e -- --env phantomjs && npm run test:ssr && npm run test:weex",
"test:unit": "karma start test/unit/karma.unit.config.js",
"test:cover": "karma start test/unit/karma.cover.config.js",
"test:e2e": "npm run build -- web-full-prod,web-server-basic-renderer && node test/e2e/runner.js",
"test:weex": "npm run build:weex && jasmine JASMINE_CONFIG_PATH=test/weex/jasmine.js",
"test:ssr": "npm run build:ssr && jasmine JASMINE_CONFIG_PATH=test/ssr/jasmine.js",
"test:sauce": "npm run sauce -- 0 && npm run sauce -- 1 && npm run sauce -- 2",
"test:types": "tsc -p ./types/test/tsconfig.json",
"lint": "eslint src scripts test",
"flow": "flow check",
"sauce": "karma start test/unit/karma.sauce.config.js",
"bench:ssr": "npm run build:ssr && node benchmarks/ssr/renderToString.js && node benchmarks/ssr/renderToStream.js",
"release": "bash scripts/release.sh",
"release:weex": "bash scripts/release-weex.sh",
"release:note": "node scripts/gen-release-note.js",
"commit": "git-cz"
}可以看到,针对不同模块的构建输出设置了不同的构建脚本,这里只看 dev 脚本的,也就是运行 npm run dev。运行脚本的时候,会找到 scripts/config.js 执行,然后在 config.js 中找到 web-full-dev 的配置。
可以看到,构建时的入口文件是 web/entry-runtime-with-compiler.js 。这里的 web 其实是配置的相对路径的别名,相关的配置都写在 scripts/alias.js 中。
顺着路口文件一路找下去,可以看到 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 构造器
* @param {*} options
*/
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)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue这个文件的主要作用是定义了一个 Vue 构造器,里面通过执行 _init 函数进行初始化,这也说明了 Vue 必须通过 new 关键字来初始化。Vue 构造器接收的 options 就是在 new Vue 时传入的配置项。
new Vue 的过程
通过内部定义的 Vue 构造器可以看到,new Vue 时,内部会通过调用 _init 函数进行初始化。_init 函数是挂载在 Vue 原型上的方法,代码定义在 initMixin 中,也就是 src/core/instance/init.js。
// 挂载到 Vue 原型上
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-start:${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)
// beforeCreate 生命周期钩子函数阶段
callHook(vm, 'beforeCreate')
// 初始化 injections
initInjections(vm) // resolve injections before data/props
// 初始化 props,methods,data 等
initState(vm)
// 初始化 provide
initProvide(vm) // resolve provide after data/props
// created 生命周期钩子函数阶段
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)
}
}
}_init 函数的作用主要是合并初始化配置项,初始化事件监听,初始化渲染等操作。可以看到的是,initState 函数也就是初始化配置项是在 beforeCreate 钩子函数阶段后,created 钩子函数阶段前完成的,所以在 beforeCreate 钩子函数阶段是不能访问和操作数据的。
合并配置选项
// 合并配置项
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
)
}这段代码就是用来合并配置选项的。这里只考虑根实例的初始化,组件实例的初始化暂不考虑,也就是 new Vue 初始化。所以代码会进入到 else 代码块,执行 mergeOptions 函数进行合并。mergeOptions 函数接受 3 个参数,第一个参数执行一个函数并得到返回值,其值就相当于是 Vue.options。第二个参数就是传入的配置项,第三个参数是 Vue 实例。Vue.options 定义在 src/core/global-api/index.js 中。
首先给 Vue.options 赋值了一个空对象,然后通过遍历 ASSET_TYPES 数组给 Vue.options 添加属性。ASSET_TYPES 定义在 src/shared/constants.js 中。
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]然后再执行拷贝函数将 KeepAlive 组件赋值给 components 属性值。最终 Vue.options 的值如下:
Vue.options = {
components: {
KeepAlive
},
directives: {},
filters: {}
}到此就明白了 mergeOptions 函数的参数类型了。接下来看下 mergeOptions 函数是怎么合并配置选项的。
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
}
// 规范化 props
normalizeProps(child, vm)
// 规范化 inject
normalizeInject(child, vm)
// 规范化 directives
normalizeDirectives(child)
// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
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)
}
}
}
// 合并配置项代码
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
}mergeOptions 函数首先会对一些配置项进行规范化,比如 props,inject 依赖注入,directives 自定义指令。拿props 为例,这个属性是用来父组件向子组件传参用的,其值的写法有很多种。
// 第 1 种:数组形式
props: ['size', 'disabled']
// 第 2 种
props: {
size: String,
disabled: Boolean
}
// 第 3 种
props: {
size: {
type: String
},
disabled: {
type: Boolean
}
}对于第 1 种和第 2 种写法,在 Vue 内部会统一规范化成第 3 种形式的写法。规范化函数如下:
function normalizeProps (options: Object, vm: ?Component) {
const props = options.props
if (!props) return
const res = {}
let i, val, name
// props 是数组或对象的情况
if (Array.isArray(props)) {
i = props.length
while (i--) {
val = props[i]
if (typeof val === 'string') {
name = camelize(val)
res[name] = { type: null }
} else if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.')
}
}
} else if (isPlainObject(props)) {
for (const key in props) {
val = props[key]
name = camelize(key)
res[name] = isPlainObject(val)
? val
: { type: val }
}
} else if (process.env.NODE_ENV !== 'production') {
warn(
`Invalid value for option "props": expected an Array or an Object, ` +
`but got ${toRawType(props)}.`,
vm
)
}
options.props = res
}对选项的值规范化后,就开始进入到选项合并阶段了,首先会遍历 Vue 内部提供的默认配置项并自行 mergeField 函数,再接着遍历传入的配置项执行 mergeField 函数。mergeField 函数时关键,函数内部会针对不同的配置项进行相应的合并处理。
// 合并配置项代码
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 optionsstrats 是定义的一个空对象,Vue 内部会将配置项合并函数作为属性挂载到这个对象上,包括 data 选项合并函数,生命周期选项合并函数,watch,props,methods 等选项合并函数。Vue 的配置项合并规则在官方文档中解释得很清楚了,合并规则就是:
- 数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
- 同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。
- 值为对象的选项,例如
methods、components和directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
整个 options.js 的作用都是用来合并配置项的。
Vue 的挂载
合并配置项完成后,接着就会进行一些初始化。
// 初始化生命周期
initLifecycle(vm)
// 初始化事件监听
initEvents(vm)
// 初始化渲染
initRender(vm)
// beforeCreate 生命周期钩子函数阶段
callHook(vm, 'beforeCreate')
// 初始化 injections
initInjections(vm) // resolve injections before data/props
// 初始化 props,methods,data 等配置项
initState(vm)
// 初始化 provide
initProvide(vm) // resolve provide after data/props
// created 生命周期钩子函数阶段
callHook(vm, 'created')需要注意的是,对于 props,methods,data 等配置项的初始化是发生在 beforeCreate 钩子函数之后, created 钩子函数之前的,所以,在 beforeCreate 钩子函数阶段是不能访问好操作数据的,必须是至少在 created 钩子函数阶段才能访问和操作。
完成了上面的初始化之后,就会进入到挂载阶段。
// 挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}$mount 函数定义在 src\platforms\web\entry-runtime-with-compiler.js。
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)
}整个函数的作用都是在把模板编译成 render 函数,再把 VDOM 转换成真实的 DOM 元素放入到 document 文档中。这里有一个需要知道的点就是,如果配置项中没有定义 render 配置项,就会选取 template 配置项作为模板编译成 render 函数,如果 template 配置项也不存在,那就直接选取 外部 HTML 作为模板进行编译。优先级是 ender 函数选项-template选项-外部HTML。最后会通过调用定义在 src\core\instance\lifecycle.js 中的 mountComponent 函数转换成真实 DOM。
在 mountComponent 函数内部,会进入 beforeMount 钩子函数阶段和 mounted 钩子函数阶段,需要注意的是,vue 的挂载完成是在 beforeMount 钩子函数之后和 mounted 钩子函数之前发生的,所以 vm.$el 至少是在 mounted 钩子函数阶段才能访问到。
Vue 生命周期
- 在 beforeCreate 阶段也就是实例初始化但是还没创建完成的时候,数据监测和事件初始化还没完成,不能对任何数据访问操作。
- created 阶段,实例创建完成,数据监测和事件初始化也都完成,可以访问和操作 data 数据,但是在这一步,$el 还获取不到,这一步可以开始一些数据请求的操作。
- 实例创建完成后,就进入到挂载阶段,在挂载阶段就可以访问 $el 的值了。要说明的是进入挂载阶段前,会先判断传入的配置项中是否有 el 属性,如果有才会继续初始化,如果没有初始化会暂停,等到执行了 vm.$mount(el) 后才会继续。在 beforeMount 阶段,内部会找到 template 配置项作为模板进行编译成 render 函数,如果没有template 配置项,则使用外部 HTML 作为模板,如果有直接配置 render 函数,则直接使用配置的 render 函数,优先级是:render 函数选项-template选项-外部HTML。并且在 beforeMount 阶段,$el 获取的值还是双大括号的 vue 语法,只是个占位符,内容还没有被替换掉。
- 当执行完 render function之后,到了mounted阶段,el被创建的vm.$el替换掉,挂载完成。到这里为止,初始化过程中会触发上面这些钩子函数,更新和销毁钩子函数都需要手动触发的。
- 当修改 data 选项中的数据时,会触发 beforeUpdate 和 updated,虚拟 DOM 按照 DIFF 算法重新渲染和打补丁,最后 DOM 更新完成后进入到 updated 阶段。
- beforeDestory 是在vue实例销毁之前调用,在这里,实例仍然可以访问操作,一般在这一步中进行:销毁定时器、解绑全局事件、销毁插件对象等操作。
- destoryed 在实例销毁后调用,所有指令都将会解绑,事件监听移除,子实例也会销毁。
总结
new Vue的时候,内部会调用 _init 函数进行初始化,主要是初始化事件监听,初始化渲染,初始化配置项,实例挂载等操作。初始化的时候,会执行 beforeCreate, created, beforeMount, mounted 生命周期钩子函数。