前提
- 当我们初始化一个Vue对象的时候, Vue的内部是如何运行的。 我们带着问题去探索;
- Vue的源码版本为:
2.6.14 - 贴出的所有源码都是 经过 删减后的简化代码
示例代码
使用官网的入门教程的例子。
<div id="app">
{{ message }}
</div>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
Vue的构造函数
源码定位: src/core/instance/index.js
function Vue (options) {
this._init(options) // 初始化时 调用 _init原型方法
}
_init原型方法
源码定位: src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
vm._uid = uid++
// a flag to avoid this being observed
vm._isVue = true
// merge options
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
vm._renderProxy = vm
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate') // 生命周期
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created') // 生命周期
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
我们逐行进行分析:
const vm: Component = this // 存储当前的实例对象
vm._uid = uid++ // 当前对象的uid, vm._uid = 0
vm._isVue = true
合并选项
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
resolveConstructorOptions
在本文中, 直接返回 构造函数的 options
mergeOptions 合并options
源码定位: src/util/options.js
function mergeOptions (
parent: Object,
child: Object,
vm?: Component
) {
normalizeProps(child, vm) // 统一格式 props
normalizeInject(child, vm) // 统一格式 inject
normalizeDirectives(child) // 统一格式 Directives
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
}
normalizeProps
根据文档 props 可以有几种格式:
- 字符串数组:
['a', 'b', 'c'] - 纯对象指定类型:
{
a: Number,
b: String,
c: Boolean
}
- 带有验证格式
props: {
// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
propA: Number,
// 多个可能的类型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 100
},
// 带有默认值的对象
propE: {
type: Object,
// 对象或数组默认值必须从一个工厂函数获取
default: function () {
return { message: 'hello' }
}
},
// 自定义验证函数
propF: {
validator: function (value) {
// 这个值必须匹配下列字符串中的一个
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
该函数 就是将 props属性 ,变成统一格式
{
props: {
a: {
type: xxxx
}
}
}
function normalizeProps (options: Object, vm: ?Component) {
const props = options.props
if (!props) return
const res = {}
let i, val, name
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 }
}
}
options.props = res
}
- 如果props是字符串数组, 那么遍历数组 将 数组转为 对 带有 type: any 的对象格式
// props: ['a', 'b', 'c'] props: { a: { type: null }, b: { type: null }, c: { type: null } }
- 否则 ,如果是纯对象, 那么 则遍历 所有属性
- 如果 属性值是 纯对象, 那么则不做处理
// 不做处理 props: { a: { type: String, required: true } }
- 否则 将属性值 转为
{type: 属性值}props: { a: String } // ===> props: { a: { type: String } }
- 如果都不是 则报错
normalizeInject
统一Inject格式, Provide 和 Inject 一般情况下用不到, 故作了解
function normalizeInject (options: Object, vm: ?Component) {
const inject = options.inject
if (!inject) return
const normalized = options.inject = {}
if (Array.isArray(inject)) {
for (let i = 0; i < inject.length; i++) {
normalized[inject[i]] = { from: inject[i] }
}
} else if (isPlainObject(inject)) {
for (const key in inject) {
const val = inject[key]
normalized[key] = isPlainObject(val)
? extend({ from: key }, val)
: { from: val }
}
}
}
- 如果 inject 不存在 ,则直接返回
- 否则 如果是 字符串数组, 则 转为
{from: ''}格式{ inject: ['a'] } // =====> { inject: { a: { from: a } } }
- 如果 是 纯对象, 则遍历对象
- 如果 属性值 是对象, 则 进行浅拷贝
extend({ from: key }, val)- 否则 转为
{from: ''}格式
normalizeDirectives
统一Directives格式
{
directives: {
"a": function() {}
}
}
// =====>
{
directives: {
"a": {
bind: function() {},
update: function() {}
}
}
}
function normalizeDirectives (options: Object) {
const dirs = options.directives
if (dirs) {
for (const key in dirs) {
const def = dirs[key]
if (typeof def === 'function') {
dirs[key] = { bind: def, update: def }
}
}
}
}
- 如果 含有指令 并且 属性值为 函数的, 统一为:
{ bind: def, update: def }
至此, mergeOptions 分析完毕, 继续回到 _init 函数。
initLifecycle(vm)
顾名思义, 初始化生命周期, 但是看了源码,只是新增了一些属性
function initLifecycle (vm: Component) {
const options = vm.$options
let parent = options.parent
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
}
initEvents(vm)
初始化 事件
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)
}
}
initRender(vm)
初始化渲染
function initRender (vm: Component) {
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options // 获取 当前实例的 $option
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
const parentData = parentVnode && parentVnode.data
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
resolveSlots()
处理 插槽
function resolveSlots (
children: ?Array<VNode>,
context: ?Component
): { [key: string]: Array<VNode> } {
if (!children || !children.length) {
return {}
}
const slots = {}
for (let i = 0, l = children.length; i < l; i++) {
const child = children[i]
const data = child.data
// remove slot attribute if the node is resolved as a Vue slot node
if (data && data.attrs && data.attrs.slot) {
delete data.attrs.slot
}
// named slots should only be respected if the vnode was rendered in the
// same context.
if ((child.context === context || child.fnContext === context) &&
data && data.slot != null
) {
const name = data.slot
const slot = (slots[name] || (slots[name] = []))
if (child.tag === 'template') {
slot.push.apply(slot, child.children || [])
} else {
slot.push(child)
}
} else {
(slots.default || (slots.default = [])).push(child)
}
}
// ignore slots that contains only whitespace
for (const name in slots) {
if (slots[name].every(isWhitespace)) {
delete slots[name]
}
}
return slots
}
vm.$createElement
提供给 初始化的时候 render 属性
$attrs 和 $listeners
高阶组件, 不做讲解
callHook(vm, 'beforeCreate')
调用 beforeCreate 钩子, 面试经常问到的, 读了上面的源码,可以看出, beforeCreate 之前 只是做了一些准备工作, 此时 你无法 拿到data里的数据 以及 $el, 无法拿到 挂载点, 不要在这里 对 节点 进行操作
initInjections(vm)
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)
}
}
function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
// inject is :any because flow is not smart enough to figure out cached
const result = Object.create(null)
// 获取所有键名
const keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject)
// 遍历 键名
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
// #6574 in case the inject object is observed...
if (key === '__ob__') continue
const provideKey = inject[key].from
let source = vm
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
source = source.$parent
}
if (!source) {
if ('default' in inject[key]) {
const provideDefault = inject[key].default
result[key] = typeof provideDefault === 'function'
? provideDefault.call(vm)
: provideDefault
} else if (process.env.NODE_ENV !== 'production') {
warn(`Injection "${key}" not found`, vm)
}
}
}
return result
}
}
inject: ['foo'],
inject: {
foo: {
from: 'xxx',
default: !function || function
}
}
- 获取所有的
inject键名,开始遍历- 如果 已经是 响应式 则跳过
- 如果 祖先级别
provide提供了 对应的键名的值, 则进行 赋值- 否则 如果 含有
default属性, 如果 是函数 则 调用 函数, 否则直接赋值- 否则 报错
- defineReactive 最后 进行响应式 处理
initState(vm)
处理 props,methods, data, computed, watch
function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
// 初始化 props
if (opts.props) initProps(vm, opts.props)
// 初始化 methods
if (opts.methods) initMethods(vm, opts.methods)
// 初始化 data
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
// 初始化 computed
if (opts.computed) initComputed(vm, opts.computed)
// 初始化 watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initProps(vm, opts.props)
// 初始化props
function initProps (vm: Component, propsOptions: Object) {
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)
}
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
defineReactive(props, key, value)
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
// 检验 prop
function validateProp (
key: string,
propOptions: Object,
propsData: Object,
vm?: Component
): any {
const prop = propOptions[key]
const absent = !hasOwn(propsData, key)
let value = propsData[key]
// boolean casting
const booleanIndex = getTypeIndex(Boolean, prop.type)
if (booleanIndex > -1) {
if (absent && !hasOwn(prop, 'default')) {
value = false
} else if (value === '' || value === hyphenate(key)) {
// only cast empty string / same name to boolean if
// boolean has higher priority
const stringIndex = getTypeIndex(String, prop.type)
if (stringIndex < 0 || booleanIndex < stringIndex) {
value = true
}
}
}
// check default value
if (value === undefined) {
value = getPropDefaultValue(vm, prop, key)
// since the default value is a fresh copy,
// make sure to observe it.
const prevShouldObserve = shouldObserve
toggleObserving(true)
observe(value)
toggleObserving(prevShouldObserve)
}
return value
}
- 验证
props的 所有属性 的合法性- 类型检查 是否含有
Boolean类型, 如果是, 那么 是否有default属性, 没有的话 直接 赋值false- 如果是空字符串, 则
Boolean的优先级最高, 直接 为true- 如果 不含有
Boolean类型, 并且 为undefined, 则获取default默认值,getPropDefaultValue- 如果没有
default属性, 则返回undefined- 如果 是
function并且type不含有Function, 则调用 函数方法 7.否则 直接返回值- 对返回的值 进行 观察
new Observer(value)- 对
prop进行响应式
简单来说, 就是 对 props的 属性进行响应式
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 $.`
)
}
}
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
}
}
这边还是比较好理解, 就是 对 methods属性进行遍历,
- 判断 是否为函数, 不是则 抛出异常
- 判断 是否在
props中存在, 如果是, 则抛出异常- 判断 是否是保留关键字, 如果是, 则抛出异常
- 否则 就绑定 上下文
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 */)
}
同 initMethods
data是否是 函数,(子组件必须是函数),如果是 则执行函数,获取返回值, 否则 直接赋值- 如果
data的值 不是 对象, 则依旧抛出异常- 遍历 所有的 属性
- 判断 是否在
methods和props中 初始化过, 如果是 则抛出异常- 否则 进行 响应式
initComputed
删除 服务端渲染
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
} else if (vm.$options.methods && key in vm.$options.methods) {
warn(`The computed property "${key}" is already defined as a method.`, vm)
}
}
}
}
- 创建一个空的
watchers对象 - 遍历
computed的属性, 如果 属性值 是function,则 直接 赋值给getter变量,否则 获取 属性值的get属性 - 如果
getter不存在 则直接抛出异常 - 否则 对
computed的属性,进行观察 - 判断 是否在
props,methods,data中初始化过, 如果是 则抛出异常
initWatch
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
- 遍历
watch, 如果是数组, 则进行遍历,createWatcher- 否则 直接
createWatcher- 在
createWatcher中, 如果 参数handler是一个 对象 则 获取handler.handler属性- 如果
handler是一个字符串, 则从 当前实例中获取 方法, 其实就是 在methods中获取- 调用
$watch进行, 细节会在 后续文章
initProvide(vm)
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
相对简单, 初始化 provide, 如果是 函数, 则运行函数, 否则直接赋值 给 vm._provided, 共 子组件的 inject使用。
回到 initInject 中,我们看到 这样一行代码:
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
callHook(vm, 'created')
调用 created 钩子, 此时 此刻, 可以拿到 data, inject, props, computed, watch, methods 内的数据
最后 挂载
如果 选项中 有 el , 则调用 挂载, 否则需要 主动 调用 $mount 方法
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
挂载 和 更新 , 会有新的一篇文章 进行描述
总结
上面陈述的 只是一个流程, 关于 如何 依赖收集, 响应式,挂载 和 更新 这些 深层次的 源码, 会写在后续文章