1. 入口
由于我本地调试代码是通过 npm run dev
构建的,所以可以在 package.json
找到真实运行的命令
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"
复制代码
通过我们之前开篇分析,我们可以透过 TARGET:web-full-dev
在 scripts/config.js
中找到编译入口
// 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
}
复制代码
我们就此来到 web/entry-runtime-with-compiler.js
文件,在最后一行我们找到
export default Vue
复制代码
这个 Vue
便是我们在 Vue
中最终输出的构造函数了,通过溯源我们可以找到它是哪里来的
entry-runtime-with-compiler.js -> web/runtime/index -> core/index -> core/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
的真实面目,是个构造函数,但是这个函数有点简单,就一句代码初始化 this._init(options)
,我们甚至没看到 _init
方法是在哪里定义的。
这样做的好处是将构造函数的逻辑拆分成不同的部分,按照逻辑解耦分离,不同的 Mixin 函数为 Vue 提供不同的功能方法,其中 _init
方法就是在 initMixin
定义的。
2. 初始化函数
我们接着看看 _init
函数做了些什么,代码比较多,我就直接在其上面写注释了,我们主要关注主流程即可。
let uid = 0
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// 组件实例 暂时跳过
initInternalComponent(vm, options)
} else {
// resolveConstructorOptions 是从构造函数的父类继承方法
// 这边一般返回 vm.constructor.options {components: {}, directives: {}, filters: {}}
// 其中vm.constructor.options => Vue.options 的定义是在 core/index.js 中的 initGlobalAPI(Vue)
// initGlobalAPI 详见2.1
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
// proxy代理vm,主要用于提示错误和键名规范,如果 key is not defined on the instance but
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
// 初始化生命周期及相关数据 如vm._isMounted vm._isDestroyed vm._inactive vm.$parent vm.$children 等
// 详见2.2
initLifecycle(vm)
// 初始化事件 注册在组件中定义的事件如 <hello @confirm="xx" />
// 详见2.3
initEvents(vm)
// 初始化渲染函数 暂不分析
initRender(vm)
// 调用组件的beforeCreate钩子
callHook(vm, 'beforeCreate')
// inject 暂时跳过
initInjections(vm) // resolve injections before data/props
// 初始化数据 详见2.4
initState(vm)
// provide 暂时跳过
initProvide(vm) // resolve provide after data/props
// 调用组件created钩子 可以看出来 beforeCreate 和 created 的差别在于中间几个数据初始化函数 特别是 initSate
callHook(vm, 'created')
// 挂载节点
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
复制代码
现在我们对构造函数中的唯一调用方法 this._init
方法进行了学习,基本可以知道实例化的大致逻辑流程了。因为函数中调用的方法已经高度封装,所以代码量虽少,但逻辑还是挺多的其实,下面我们将对上述流程中的部分方法进行学习,进一步搞清初始化逻辑。
2.1 initGlobalAPI
我们在前面的初始化函数中有提到 vm.contructor.options
,但是我们没看到在何处有定义它的地方,实际上我们在前面找构造函数 Vue
的定义的时候,曾经溯源了几个文件 entry-runtime-with-compiler.js -> web/runtime/index -> core/index -> core/instance/index
,各个文件不是单纯的去引入输出,而是在各自的范围内进行了一系列操作,其中在 core/index
就有这么一行代码
initGlobalAPI(Vue)
复制代码
我们来看看 core/global-api/index.js
中 initGlobalAPI
的具体逻辑
export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
configDef.get = () => config
Object.defineProperty(Vue, 'config', configDef)
// 定义了Vue的静态工具函数 但不属于开放API 我们尽量不去使用
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
// 原来set delete nextTick 在这边初始化
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}
// 我们的主角options出场
Vue.options = Object.create(null)
// ASSET_TYPES的定义在 share/constants 中 ASSET_TYPES = ['component', 'directive', 'filter']
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
// 不知 先跳过
Vue.options._base = Vue
// KeepAlive逻辑 先跳过
extend(Vue.options.components, builtInComponents)
// 一些常用的方法定义,通过解耦思想在函数中实现 use mixin extend 方法
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
// 比较特殊 通过遍历ASSET_TYPES分别定义 Vue.directive Vue.component Vue.filter
initAssetRegisters(Vue)
}
复制代码
2.2 initLifecycle
我们再来看看在 initLifecycle
究竟是如何初始化生命周期的
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)
}
// 初始化实例属性$parent $root $children $refs 等
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
函数比我们想的简单一些,它就是定义了实例和父子节点相关的属性及生命周期描述相关属性。这其实是可以理解的,因为生命周期钩子函数我们在组件中定义,在初始化和渲染更新中触发,所以理论上调用钩子函数也应该在实际渲染的各个过程中才对,就如我们前面在 _init
函数中调用的 callHook(vm, 'created')
,在 created
钩子调用中再去修改生命周期相关属性及调用函数。
2.3 initEvents
initEvents
函数本身比较简单,主要是为组件上定义的函数调用 updateComponentListeners
进行注册处理,关于 updateComponentListeners
我们就先不分析了,后面有机会再进行分析
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)
}
}
复制代码
2.4 initState
我们最后再来分析下 initState
,这是个比较重要的函数,我们可以从中分析得出框架在 beforeCreate
和 created
之间做了哪些数据处理
export function initState (vm: Component) {
vm._watchers = []
// 这段代码真是短小精悍 短短的几行代码 分别初始化了 props methods data computed 我们来分别看看
const opts = vm.$options
// 详见2.4.1
if (opts.props) initProps(vm, opts.props)
// 详见2.4.2
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
// 详见2.4.3
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
复制代码
2.4.1 initProps
initProps
到底是如何初始化的,我们来看看
function initProps (vm: Component, propsOptions: Object) {
// propsOptions为组件Props定义形如{name: String}
// propsData为组件接收到的数据形如{name: 'Joke'}
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
// 遍历prop定义
for (const key in propsOptions) {
keys.push(key)
// validateProp用于获取prop的值(propsData数据或默认值)
const value = validateProp(key, propsOptions, propsData, vm)
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
// 这边能遇见我们的错误提示老朋友 不允许重新父组件传来的prop值
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
// 为props添加响应式 其中前面toggleObserving(false)的作用就在这边
// 当value为对象时不会进一步递归添加响应式
defineReactive(props, key, value)
}
// vm代理_props值 也就是我们的props
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
复制代码
2.4.2 initMethods
相比之下 initMethods
就简单很多了
function initMethods (vm: Component, methods: Object) {
const props = vm.$options.props
for (const key in methods) {
if (process.env.NODE_ENV !== 'production') {
if (typeof methods[key] !== 'function') {
warn(
`Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
`Did you reference the function correctly?`,
vm
)
}
if (props && hasOwn(props, key)) {
warn(
`Method "${key}" has already been defined as a prop.`,
vm
)
}
if ((key in vm) && isReserved(key)) {
warn(
`Method "${key}" conflicts with an existing Vue instance method. ` +
`Avoid defining component methods that start with _ or $.`
)
}
}
// 上面都是开发环境的校验和错误提示
// 正式环境只有这句实际逻辑 将methods下的方法都复制给vm 并将this指定为vm
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
}
}
复制代码
2.4.3 initData
我们继续看 initData
的逻辑
function initData (vm: Component) {
let data = vm.$options.data
// 如果是函数则将this指向vm并执行函数
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--) {
// 校验是否和methods和props键重复
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)) {
// vm代理_data
proxy(vm, `_data`, key)
}
}
// 响应式数据 后面文章单独分析
// observe data
observe(data, true /* asRootData */)
}
复制代码
2.4.4 initComputed和initWatch
涉及watch,我们后面再讲
3. 结语
前面我们大致分析了 Vue
函数实例化的流程。
-
通过
entry-runtime-with-compiler.js
找到入口,再通过溯源找到 Vue 函数的定义 -
_init
函数的流程
初始化生命周期数据 -> 初始化组件事件 -> 初始化渲染 -> 调用beforeCreate -> 初始化数据 -> 调用created -> 挂载节点
-
Vue静态方法的初始化
initGlobalAPI
逻辑 -
实例化流程相关函数
initLifecycle initEvents initState
逻辑 -
initState
中props methods data
的初始化逻辑
还有些流程没有分析的,我将在后面的文章继续
-
initRender
初始化渲染 -
initState
初始化数据中的computed watch
初始化 -
initData
中的observe(data)
数据监测
最后啰嗦一句,贴的代码比较多。分析的有不对的地方希望帮忙指正,有不清楚的地方也可以提出来,大家一起交流~