[TOC]
- Vue 学习笔记
- Vue 源码解析 - 主线流程
- Vue 源码解析 - 模板编译
- Vue 源码解析 - 组件挂载
- Vue 源码解析 - 数据驱动与响应式原理
前言
前面我们对 Vue 的基本使用进行了归纳:Vue 学习笔记,接下来我们对 Vue 的源码进行解读。
注:本文所使用的 Vue 源码版本为:Vue 2.6.10
源码调试环境构建
在进行源码阅读之前,可以先搭建下 Vue 的源码调试环境:
- Vue 调试环境搭建:搭建 Vue 调试环境,可以让我们在浏览器中对 Vue 源码进行单步调试,方便阅读理解源码,具体搭建步骤如下:
- 下载 Vue 源码,这里使用当前最新源码:Vue 2.6.10
- 在源码
package.json
中的dev
脚本最后添加--sourcemap
,用于生成.map
文件:
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev --sourcemap",
- 在源码根目录下,执行
npm install
,安装package.json
指定的依赖。 - 执行
npm run dev
, 这一步 将在dist
目录下生成vue.js
和vue.js.map
两个文件。 - 找到样例项目,随便打开一个项目,比如:
examples/commits/index.html
,将vue.min.js
修改为vue.js
:
- 浏览器打开
examples/commits/index.html
,运行项目。 -
F12
打开浏览器控制台,点击Sources
标签,跳转到源码界面。 - Vue 源码的入口位置为:
src/core/instance/index.js
,随意在该文件内打上一个断点,刷新即可进入调试模式,如下图所示:
Vue 源码目录设计
Vue 的源码都存放在src
目录下:
vue/src
├─compiler
│ ├─codegen
│ ├─directives
│ └─parser
├─core
│ ├─components
│ ├─global-api
│ ├─instance
│ │ └─render-helpers
│ ├─observer
│ ├─util
│ └─vdom
│ ├─helpers
│ └─modules
├─platforms
│ ├─web
│ │ ├─compiler
│ │ │ ├─directives
│ │ │ └─modules
│ │ ├─runtime
│ │ │ ├─components
│ │ │ ├─directives
│ │ │ └─modules
│ │ ├─server
│ │ │ ├─directives
│ │ │ └─modules
│ │ └─util
│ └─weex
│ ├─compiler
│ │ ├─directives
│ │ └─modules
│ │ └─recycle-list
│ ├─runtime
│ │ ├─components
│ │ ├─directives
│ │ ├─modules
│ │ └─recycle-list
│ └─util
├─server
│ ├─bundle-renderer
│ ├─optimizing-compiler
│ ├─template-renderer
│ └─webpack-plugin
├─sfc
└─shared
src
目录主要包含 6 大类功能代码:
目录 | 描述 |
---|---|
compiler | 模板编译(即template 转化成render 函数) |
core | 存放通用的,平台无关的运行时代码(核心代码) |
server | 服务端渲染代码 |
platforms | 平台相关代码 |
sfc | 单文件组件(*.vue )编译解析代码 |
shared | 通用的工具方法 |
Vue 源码构建
Vue 源码是基于 Rollup 进行构建的,下面我们对其构建过程进行分析。
注:Rollup 和 Webpack 都是前端构建工具,Webpack 功能相对更强大,但 Rollup 更轻量。Vue 之所以采用 Rollup 构建发布文件,据尤雨溪本人的回答,是因为最终打包出来的文件会更小,且初始化速度更快。
首先,查看下package.json
设置的构建命令:
"scripts": {
...
"build": "node scripts/build.js",
"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
"build:weex": "npm run build -- weex",
...
}
可以看到,当我们执行构建命令npm run build
的时候,其实就是执行node scripts/build.js
文件,因此,我们来看下构建脚本scripts/build.js
的具体内容:
...
// 读取构建配置文件
let builds = require('./config').getAllBuilds()
// 过滤构建版本
// filter builds via command line arg
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 {
// filter out weex builds by default
builds = builds.filter(b => {
return b.output.file.indexOf('weex') === -1
})
}
// 进行构建
build(builds)
function build (builds) {
...
buildEntry(builds[built])
...
}
function buildEntry (config) {
...
return rollup.rollup(config)
...
return write(file, code)
...
}
function write (dest, code, zip) {
return new Promise((resolve, reject) => {
...
fs.writeFile(dest, code, err => {
...
})
...
}
scripts/build.js
构建脚本主要做了 3 件事:
- 读取配置文件:具体代码为:
// build.js
let builds = require('./config').getAllBuilds()
// config.js
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
通过config.js
提供的getAllBuilds
函数,即可获取到配置信息。而getAllBuilds
获取的信息来自builds
,我们看下builds
的具体内容:
const builds = {
// Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
'web-runtime-cjs-dev': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.common.dev.js'),
format: 'cjs',
env: 'development',
banner
},
'web-runtime-cjs-prod': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.common.prod.js'),
format: 'cjs',
env: 'production',
banner
},
// Runtime+compiler CommonJS build (CommonJS)
'web-full-cjs-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.common.dev.js'),
...
},
'web-full-cjs-prod': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.common.prod.js'),
...
},
// Runtime only ES modules build (for bundlers)
'web-runtime-esm': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.esm.js'),
...
},
// Runtime+compiler ES modules build (for bundlers)
'web-full-esm': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.esm.js'),
...
},
// Runtime+compiler ES modules build (for direct import in browser)
'web-full-esm-browser-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.esm.browser.js'),
...
},
// Runtime+compiler ES modules build (for direct import in browser)
'web-full-esm-browser-prod': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.esm.browser.min.js'),
...
},
// runtime-only build (Browser)
'web-runtime-dev': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.js'),
...
},
// runtime-only production build (Browser)
'web-runtime-prod': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.min.js'),
...
},
// Runtime+compiler development build (Browser)
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.js'),
...
},
// Runtime+compiler production build (Browser)
'web-full-prod': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.min.js'),
...
},
// Web compiler (CommonJS).
'web-compiler': {
entry: resolve('web/entry-compiler.js'),
dest: resolve('packages/vue-template-compiler/build.js'),
...
},
// Web compiler (UMD for in-browser use).
'web-compiler-browser': {
entry: resolve('web/entry-compiler.js'),
dest: resolve('packages/vue-template-compiler/browser.js'),
...
},
// Web server renderer (CommonJS).
'web-server-renderer-dev': {
entry: resolve('web/entry-server-renderer.js'),
dest: resolve('packages/vue-server-renderer/build.dev.js'),
...
},
'web-server-renderer-prod': {
entry: resolve('web/entry-server-renderer.js'),
dest: resolve('packages/vue-server-renderer/build.prod.js'),
...
},
'web-server-renderer-basic': {
entry: resolve('web/entry-server-basic-renderer.js'),
dest: resolve('packages/vue-server-renderer/basic.js'),
...
},
'web-server-renderer-webpack-server-plugin': {
entry: resolve('server/webpack-plugin/server.js'),
dest: resolve('packages/vue-server-renderer/server-plugin.js'),
...
},
'web-server-renderer-webpack-client-plugin': {
entry: resolve('server/webpack-plugin/client.js'),
dest: resolve('packages/vue-server-renderer/client-plugin.js'),
...
},
// Weex runtime factory
'weex-factory': {
weex: true,
entry: resolve('weex/entry-runtime-factory.js'),
dest: resolve('packages/weex-vue-framework/factory.js'),
...
},
// Weex runtime framework (CommonJS).
'weex-framework': {
weex: true,
entry: resolve('weex/entry-framework.js'),
dest: resolve('packages/weex-vue-framework/index.js'),
...
},
// Weex compiler (CommonJS). Used by Weex's Webpack loader.
'weex-compiler': {
weex: true,
entry: resolve('weex/entry-compiler.js'),
dest: resolve('packages/weex-template-compiler/build.js'),
...
}
}
可以看到,builds
提供了所有版本的构建信息,而genConfig
函数只是对builds
提供的构建信息转换成 Rollup 所需的参数格式而已。
注:上述配置信息build
中的entry
字段表示构建入口的JS
文件路径,dest
字段表示构建完成的JS
文件路径,字段format
表示构建文件的格式,其值有如下可选:
- cjs:表示构建出来的文件遵循 CommonJS 规范。
- es:表示构建文件遵循 ES Module 规范。
- umd:表示构建文件遵循 UMD 规范。
- 过滤构建文件:可以通过命令行传入参数指定构建版本,否则默认构建除
weex
以外的所有版本。 - 进行构建:使用 Rollup 进行构建,最终构建的版本存放于
dist
目录和package
目录中。
以上,就是 Vue 的整个构建过程。
Vue 源码入口文件
在 Vue 构建完成后,会生成两种 Vue.js 版本:Runtime Only 和 Runtime + Compiler:
Runtime Only:在编译阶段,将
.vue
等文件编译成.js
文件的时候,通常借助如 webpack 的vue-loader
工具进行操作,因此,Runtime Only 版本的 Vue.js 无须包含编译部分代码,其体积会更轻量。Runtime + Compiler:如果没有对模板进行预编译,但代码中又使用如
template
等需要进行编译的模板,则需要在运行时进行编译(在线编译)。因此,Runtime + Compiler 版本的 Vue.js 不仅包含了必须的运行时代码,也包含了编译代码。
// 需要编译器的版本:Runtime + Compiler
new Vue({
template: '{{ hi }}'
})
// 不需要运行时编译:Runtime Only
new Vue({
render (h) {
return h('div', this.hi)
}
})
以下我们的分析采用的是 Runtime + Compiler 版本。
在上面讲解 Vue 的构建过程中,可以知道,对于 Web 应用,其 Runtime + Compiler 构建出来的 Vue.js 的入口文件为:src/platforms/web/entry-runtime-with-compiler.js
,具体代码如下:
// scripts/alias.js
const path = require('path')
const resolve = p => path.resolve(__dirname, '../', p)
module.exports = {
vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
compiler: resolve('src/compiler'),
core: resolve('src/core'),
shared: resolve('src/shared'),
web: resolve('src/platforms/web'),
weex: resolve('src/platforms/weex'),
server: resolve('src/server'),
sfc: resolve('src/sfc')
}
// scripts/config.js
const aliases = require('./alias')
const resolve = p => {
const base = p.split('/')[0]
if (aliases[base]) {
return path.resolve(aliases[base], p.slice(base.length + 1))
} else {
return path.resolve(__dirname, '../', p)
}
}
const builds = {
...
// Runtime+compiler development build (Browser)
'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
},
// Runtime+compiler production build (Browser)
'web-full-prod': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.min.js'),
format: 'umd',
env: 'production',
alias: { he: './entity-decoder' },
banner
},
...
}
注:scripts/alias.js
文件对一些路径进行了映射,比如web
对应真实路径为src/platforms/web
,这样在其他文件中可简化路径书写。
下面来看下src/platforms/web/entry-runtime-with-compiler.js
源码内容:
// src/platforms/web/entry-runtime-with-compiler.js
import Vue from './runtime/index'
import { compileToFunctions } from './compiler/index'
...
// 缓存 Vue 原型上的 mount 函数
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
...
// 对模板进行编译
const { render, staticRenderFns } = compileToFunctions(template, {...}
...
return mount.call(this, el, hydrating)
}
function getOuterHTML (el: Element): string {
...
}
Vue.compile = compileToFunctions
export default Vue
entry-runtime-with-compiler.js
主要做了以下三件事:
- 导入
Vue
- 在
Vue
原型上重新定义了一个$mount
函数,用于对模板进行编译,最后会重新调用原先定义的$mount
函数,进行组件挂载操作。 - 让
Vue.compile
指向函数compileToFunctions
下面依次对上述事件进行阐述。
Vue 源码整体流程
entry-runtime-with-compiler.js
首先会导入Vue
实例:
import Vue from './runtime/index'
我们循着代码进入./runtime/index.js
进行查看:
import Vue from 'core/index'
...
// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
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) {
...
if (config.devtools) {
if (devtools) {
...
}
export default Vue
代码很清晰,主要就是做了以下几件事:
- 安装一些平台相关的工具函数
- 安装一些平台相关的运行时指令和组件
- 为浏览器安装
patch
函数 - 为
Vue
定义挂载函数:这里是第一次在Vue
的原型上定义$mount
函数,其功能就是进行组件挂载mountComponent
。该函数会在entry-runtime-with-compiler.js
中被重新定义,增加模板编译功能,成功编译出render
函数后,再经由该函数进行组件挂载。 - 浏览器环境下
hook
测试工具devtools
注:./runtime/index.js
最重要的功能就是定义了$mount
函数进行组件挂载功能:mountComponent
,其具体详情参见:Vue 源码解析 - 组件挂载
回到开头,我们可以看到,Vue
实例由core/index
导入而来,其具体路径为src/core/index.js
,我们进入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'
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
这里主要是对Vue
进行了一些全局初始化操作:主要包含以下两方面初始化内容:
- 为
Vue
定义全局静态方法:查看initGlobalAPI
源码即可看到定义的静态方法,这里就不深入源码,只列举出initGlobalAPI
总共定义的静态方法:Vue.util = { warn, extend, mergeOptions, defineReactive } Vue.set = set Vue.delete = del Vue.nextTick = nextTick Vue.observable =
(obj: T): T => {...} Vue.use = function (plugin: Function | Object) {...} Vue.mixin = function (mixin: Object) {...} Vue.extend = function (extendOptions: Object): Function {..} - 在
Vue
原型上定义了$isServer
,$ssrContext
,FunctionalRenderContext
等属性。
再次回到开头,可以看到src/core/index.js
从./instance/index
导入Vue
,那我们继续查看./instance/index
文件:
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'
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
的源码定义,并且./instance/index
中除了定义Vue
之外,还做了很多其他事情:initMixin(Vue)
,stateMixin(Vue)
,eventsMixin(Vue)
,lifecycleMixin(Vue)
和renderMixin(Vue)
。这些方法都以Mixin
结尾,表明其都是通过 Mixin 方式为Vue
添加扩展功能(这也是为什么Vue
采用函数定义,而不是class
的原因),其实质就是在Vue
的原型上添加扩展方法,如下所示:
// initMixin(Vue)
Vue.prototype._init = function (...) {...}
// stateMixin(Vue)
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function (...){...}
// eventsMixin(Vue)
Vue.prototype.$on = function (...): Component {...}
Vue.prototype.$once = function (...): Component {...}
Vue.prototype.$off = function (...): Component {...}
Vue.prototype.$emit = function (...): Component {...}
// lifecycleMixin(Vue)
Vue.prototype._update = function (...) {...}
Vue.prototype.$forceUpdate = function () {...}
Vue.prototype.$destroy = function () {...}
// renderMixin(Vue)
Vue.prototype.$nextTick = function (...) {...}
Vue.prototype._render = function (): VNode {...}
到这里,Vue
源码的粗略完整流程已经分析完毕。其总体流程如下图所示:
以下将对 Vue 整体流程中相对重要的模块依序进行源码分析。
Vue 实例创建流程
当我们使用new Vue()
时,我们来看下整个Vue
实例的创建过程:
// src/core/instance/index.js
function Vue(options) {
...
this._init(options)
}
new Vue()
时,Vue 内部就只进行了this._init(options)
操作,前面我们分析过,this._init
函数是在initMixin(Vue)
中定义的:
// src/core/instance/init.js
export function initMixin (Vue: Class) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
...
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) { // 组件对象
initInternalComponent(vm, options)
} else { // Vue 实例
// 将传递进来的选项和 Vue 自带的系统相关的选项进行合并
vm.$options = mergeOptions(
// Vue 的内置选项,定义于 web/runtime/index.js 中,Vue.options.directives,Vue.options.components
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
...
// expose real self
vm._self = vm
// 初始化生命周期
initLifecycle(vm)
// 初始化事件处理
initEvents(vm)
// 初始化 render
initRender(vm)
// 触发 beforeCreate 钩子函数
callHook(vm, 'beforeCreate')
// 解析 options.inject 注入
initInjections(vm) // resolve injections before data/props
// 初始化 props、methods、data、computed 与 watch
initState(vm)
// 解析 options.provide
initProvide(vm) // resolve provide after data/props
// 触发 created 钩子函数
callHook(vm, 'created')
...
// 如果有传入 el,则进行挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
可以看到,_init
函数内部主要是对传递进来的Options
对象和Vue
自带的指令directives
和组件components
(这些指令和组件的定义位于web/runtime/index.js
中)进行合并,以及很多的初始化操作与钩子函数触发,最后还进行了挂载操作。
_init
函数做了很多的事情,我们主要对以下事件进行分析:
-
initLifecycle(vm)
:见名知意,该函数用于初始化Vue
的生命周期,其源码如下所示:
// core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
// 获取合并后的选项
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
可以看到,initLifecycle
函数就是对Vue
的$parent
,$root
,$children
,$refs
,_watcher
,_inactive
,_directInactive
,_isMounted
,_isDestroyed
,_isBeingDestroyed
属性进行了复位操作。
-
initEvents(vm)
:见名知意,该函数用于初始化Vue
的事件,其源码如下所示:
// core/instance/event.js
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
主要是对Vue
组件的_events
,_hasHookEvent
和$options._parentListeners
进行复位操作。
-
initRender(vm)
:初始化Vue
组件的渲染功能。其源码如下所示:
export function initRender (vm: Component) {
...
// 将子元素解析到一个 slot 对象中
vm.$slots = resolveSlots(options._renderChildren, renderContext)
...
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
...
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
...
}
可以看到,initRender
函数主要是为Vue
组件设置了_c
和$createElement
函数,可用于创建虚拟节点,其中:
-
vm._c
是内部版本,主要用于渲染模板代码。 -
vm.$createElement
是公有版本,用于用户自定义渲染函数。
这两个函数的底层实现均为createElement
函数,该函数主要用于创建虚拟节点,关于该函数相关内容,请参考:Vue 源码解析 - 组件挂载
-
callHook(vm, 'beforeCreate')
:见名知意,该函数会触发beforeCreate
钩子。其源码如下所示:
// core/instance/lifecycle.js
export function callHook(vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget();
const handlers = vm.$options[hook];
const info = `${hook} hook`;
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info);
}
}
if (vm._hasHookEvent) {
vm.$emit("hook:" + hook);
}
popTarget();
popTarget()
}
// core/util/error.js
export function invokeWithErrorHandling(
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res;
try {
res = args ? handler.apply(context, args) : handler.call(context);
...
}
从源码中可以看到,callHook(vm,"beforeCreate")
就是通过vm.$option["beforeCreate"]
取出我们设置的beforeCreate
钩子函数,最后在invokeWithErrorHandling
中进行回调,这样,我们的钩子函数就生效了。
-
initInjections(vm)
:初始化注入inject
事件。其源码如下:
// core/instance/inject.js
export function initInjections(vm: Component) {
const result = resolveInject(vm.$options.inject, vm);
if (result) {
toggleObserving(false);
Object.keys(result).forEach(key => {
...
defineReactive(vm, key, result[key]);
});
toggleObserving(true);
}
}
export function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
...
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
...
let source = vm
while (source) {
...
result[key] = source._provided[provideKey]
...
}
source = source.$parent
}
...
}
return result
}
}
Vue 提供provide
/inject
事件,允许祖先组件向其子孙组件传递数据,无论子孙组件嵌套多深,都能进行传递。而initInjections
就是完成这个功能的。
从源码中可以看到,initInjections
首先会取出options.inject
字段,然后取出该字段的键值Object.get(key)
,依次从祖先的_provided
字段中取出该键值对应的值,存储到一个新的对象中result
,直至遍历结束。最后还会为这些inject
的键值进行响应式设置defineReactive
,如此便完成了provide
/inject
功能。
注:响应式设置defineReactive
的具体详情请参考:Vue 源码解析 - 数据驱动与响应式原理
-
initState(vm)
:对一些状态的初始化。其源码如下:
// core/instance/state.js
export function initState(vm: Component) {
vm._watchers = [];
const opts = vm.$options;
// 初始化 options.props
if (opts.props) initProps(vm, opts.props);
// 初始化 options.methods
if (opts.methods) initMethods(vm, opts.methods);
if (opts.data) {
// 初始化 options.data
initData(vm);
} else {
// 没有 options.data 时,绑定为一个空对象
observe((vm._data = {}), true /* asRootData */);
}
// 初始化 options.computed
if (opts.computed) initComputed(vm, opts.computed);
if (opts.watch && opts.watch !== nativeWatch) {
// 初始化 options.watcher
initWatch(vm, opts.watch);
}
}
从源码中可以看到,initState
函数主要对Vue
组件的props
,methods
,data
,computed
和watch
等状态进行初始化,各初始化具体内容如下:
-
props
:由函数initProps
进行初始化,其源码如下所示:
// src/core/instance/state.js
function initProps(vm: Component, propsOptions: Object) {
...
// propsOptions 就是 Vue.$options.props
for (const key in propsOptions) {
...
// 对键进行检测,并返回其对应的值
const value = validateProp(key, propsOptions, propsData, vm);
...
defineReactive(props, key, value);
}
...
}
// src/core/util/props.js
export function validateProp (
key: string,
propOptions: Object,
propsData: Object,
vm?: Component
): any {
const prop = propOptions[key]
...
// boolean casting
// 检测是否为 Boolean 类型
const booleanIndex = getTypeIndex(Boolean, prop.type)
if (booleanIndex > -1) {
...
value = true
...
}
}
// check default value
if (value === undefined) {
// 获取 default 值
value = getPropDefaultValue(vm, prop, key)
...
}
...
return value
}
initProps
其实就是对Vue.$options.props
进行解析并设置到Vue
的实例属性上,且这些属性具备响应式功能defineReactive
。
具体的实现步骤就是对Vue.$options.props
进行遍历,获取其每个键值,对每个键值的值进行检测validateProp
并返回其值,然后进行响应式设置。
-
methods
:由函数initMethods
进行初始化,其源码如下所示:
// core/instance/state.js
function initMethods(vm: Component, methods: Object) {
...
for (const key in methods) {
...
// 绑定 methods 中的函数到 vm 中
vm[key] = typeof methods[key] !== "function" ? noop : bind(methods[key], vm);
}
}
// src/shared/util.js
export function noop (a?: any, b?: any, c?: any) {}
function nativeBind (fn: Function, ctx: Object): Function {
return fn.bind(ctx)
}
export const bind = Function.prototype.bind ? nativeBind : polyfillBind
initMethods
源码比较好读,就是遍历Vue.$options.methods
,将每个方法都绑定到Vue
实例上(如果methods
内的键对应不是一个函数,就绑定到一个空函数noop
,否则,就绑定到Vue
上:bind(methods[key],vm)
)。
-
data
:由函数initData
进行初始化,其内部主要是对Options.data
进行了代理(使得Vue
实例具备与Options.data
相同的键值)和对Options.data
的键值进行了响应式设置,具体详情请参考:Vue 源码解析 - 数据驱动与响应式原理
-
initProvide(vm)
:解析options.provide
,其源码如下所示:
// src/core/instance/inject.js
export function initProvide(vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
其实就是如果定义了Options.provide
遍历,就将其赋值到vm._provided
上。
callHook(vm, 'created')
:触发created
钩子函数。-
vm.$mount(vm.$options.el)
:前面我们讲过,Vue 有两种版本:Runtime Only 和 Runtime + Compiler。对于不同的 Vue 版本,
mount
函数有不同的实现:- 对于 Runtime Only 版本,
mount
函数只提供 组件挂载 功能 - 对于 Runtime + Compiler 版本,
mount
函数提供 模板编译 + 组件挂载 功能
更多详细内容,请参考:Vue 源码解析 - 模板编译,Vue 源码解析 - 组件挂载
- 对于 Runtime Only 版本,
到此,整个Vue
实例的创建过程就简略
参考
- 深入vue - 源码目录及构建过程分析
- Vue.js 技术揭秘