其他章节请看:

vue 快速入门 系列

Vue 实例的初始化过程

书接上文,每次调用 new Vue() 都会执行 Vue.prototype._init() 方法。倘若你看过 jQuery 的源码,你会发现每次调用 jQuery() 也会执行一个初始化的方法(即 jQuery.fn.init())。两者在执行初始化方法后都会返回一个实例vue 实例jQuery 实例),而且在初始化过程中,都会做许多事情。本篇就和大家一起来看一下 vue 实例的初始化过程。

Tip:本篇亦叫 Vue.prototype._init() 的源码解读。解读顺序和源码的顺序保持一致。

function Vue (options) {
...
this._init(options)
}
// 核心代码
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// 合并参数
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// 初始化生命周期、初始化事件...
initLifecycle(vm)
initEvents(vm)
initRender(vm)
// 触发生命钩子:beforeCreate
callHook(vm, 'beforeCreate')
// resolve injections before data/props
// 我们可以使用的顺序:inject -> data/props -> provide
initInjections(vm)
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') if (vm.$options.el) {
// 挂载
vm.$mount(vm.$options.el)
}
}

initLifecycle(初始化生命周期)

export function initLifecycle (vm: Component) {
const options = vm.$options // locate first non-abstract parent
// 定位第一个非抽象父节点
let parent = options.parent
// 有 parent,并且自己不是抽象的,则找到最近一级的非抽象 parent,并将自己放入其 $children 数组中
if (parent && !options.abstract) {
// 如果 parent 是抽象的(abstract),则继续往上级找 parent,直到 parent 不是抽象的为止
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
// 将 vm 放入 parent 中
parent.$children.push(vm)
} // 初始化实例 property:vm.$parent、vm.$root、$children、vm.$refs
vm.$parent = parent // 父实例,如果当前实例有的话
vm.$root = parent ? parent.$root : vm // 当前组件树的根 Vue 实例
vm.$children = [] // 当前实例的直接子组件
vm.$refs = {} // 一个对象,持有注册过 ref attribute 的所有 DOM 元素和组件实例。 // 以下划线(_)开头的应该是私有实例属性
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}

initLifecycle() 方法做了一下几件事:

  • 找到最近一级非抽象 parent,并将自己放入其 $children 数组中
  • 初始化实例 property:vm.$parent、vm.$root、$children、vm.$refs
  • 给 vm 初始化一些以下划线(_)开头的私有属性

这个方法所做的事太简单了!和我的猜测不一致。

最初我认为初始化生命周期(initLifecycle()),应该和官网的生命周期图相关。现在在来看一下这张图,发现 _init() 中的代码仅仅对应这张图的前一半而已。

graph TD
a("new Vue()") --> b("init Events 和 Lifecycle")
b --> |beforeCreate| c("init injects 和 reactivity")
c --> |created| d("编译模板")
d --> |beforeMount| e("create vm.$el and replace 'el' with it")
e --> |mounted| f("...")

initEvents 初始化事件

export function initEvents (vm: Component) {
// 创建一个没有原型的对象,赋值给 _events
vm._events = Object.create(null)
// 是否有钩子事件
vm._hasHookEvent = false
// init parent attached events
// 初始化父组件附加的事件
const listeners = vm.$options._parentListeners
if (listeners) {
// 更新组件监听器
updateComponentListeners(vm, listeners)
}
}

initEvents() 好像没干啥事。

但一个方法总得干点事,所以如果实在要说这个函数哪里做了点事,应该就和 _parentListeners 有关。

export function updateComponentListeners (
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm
// 更新监听器
// 第一个参数是父组件的监听器,第二个是父组件监听器的老版本
// 之后就是 add、remove,很简单,即注册事件和删除事件
updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
target = undefined
}

initEvents() 做的事情就是,更新父组件给子组件注册的事件。这里有两个关键词:父子组件、注册。

TipupdateListeners() 就两个逻辑:

  • 遍历新的 listeners。如果老的版本上没有定义,说明是新增。里面用 on;新老版本不一致,以新版本为准。
  • 遍历旧的 oldListeners。新的版本没有定义,说明删除了。里面用 remove
export function updateListeners (
on: Object,
oldOn: Object,
add: Function,
remove: Function,
createOnceHandler: Function,
vm: Component
) {
let name, def, cur, old, event
// 遍历新的监听器
for (name in on) {
def = cur = on[name]
old = oldOn[name]
event = normalizeEvent(name)
... if (isUndef(cur)) {
...
// 老的版本上没有定义,说明是新增。里面用 on
} else if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur, vm)
}
if (isTrue(event.once)) {
cur = on[name] = createOnceHandler(event.name, cur, event.capture)
}
add(event.name, cur, event.capture, event.passive, event.params)
// 新老版本不一致,以新版本为准
} else if (cur !== old) {
old.fns = cur
on[name] = old
}
}
// 遍历旧的监听器
for (name in oldOn) {
// 新的版本没有定义,说明删除了。里面用 remove
if (isUndef(on[name])) {
event = normalizeEvent(name)
remove(event.name, oldOn[name], event.capture)
}
}
}

父子组件

我们通过一个实验来了解一下父组件和其中的子组件的创建过程。

定义父子两个组件,并都有4个生命周期钩子函数 beforeCreatecreatedbeforeMountmounted

// WelComeButton.vue - 子组件
<template>
<div>
<button v-on:click="$emit('welcome')">Click me to be welcomed</button>
</div>
</template>
<script>
export default {
beforeCreate () {
console.log('beforeCreate')
},
created () {
console.log('created')
},
beforeMount () {
console.log('beforeMount')
},
mounted () {
console.log('mounted')
}
}
</script>
// About.vue - 父组件
<template>
<div class="about">
<welcome-button></welcome-button>
</div>
</template> <script>
import WelcomeButton from './WelComeButton.vue'
export default {
components: { WelcomeButton },
beforeCreate () {
console.log('parent beforeCreate')
},
created () {
console.log('parent created')
},
beforeMount () {
console.log('parent beforeMount')
},
mounted () {
console.log('parent mounted')
}
}
</script>
// 浏览器输出

parent beforeCreate
parent created
parent beforeMount
beforeCreate
created
beforeMount
mounted
parent mounted

父子组件的创建过程如下:

  1. 父元素的 beforeCreate。会初始化事件和生命周期
  2. 父元素的 created。初始化 inject、data/props、provide
  3. 父元素的 beforeMount。编译模板为渲染函数
  4. 子元素的 beforeCreate
  5. 子元素的 created
  6. 子元素的 beforeMount
  7. 子元素的 mounted
  8. 父元素的 mounted。创建 vm.$el(Vue 实例使用的根 DOM 元素),并挂载视图

于是我们知道包含子组件的组件,它的落地(更新到真实 dom)过程:

  1. 将父组件编译成渲染函数
  2. 创建子组件,并挂载到父组件中
  3. 父组件被挂载

注册

提到注册事件,我们会想到 v-on

v-on 用在普通元素上时,监听原生 DOM 事件。用在自定义元素组件上时,监听子组件触发的自定义事件。

// v-on 用于普通元素
<button v-on:click="greet">Greet</button>
// v-on 用于自定义元素组件
<div id='app'>
<!-- 父组件给子组件注册了事件 chang-count,事件的回调方法是 changCount -->
<button-counter v-on:chang-count='changCount'></button-counter>
</div>

我们通常使用模板,并在其上注册事件,模板会编译生成渲染函数,接着就到了虚拟 DOM,每次执行渲染函数都会生成一份新的 vNode,新的 vNode 和旧的 vNode 对比,查找出需要更新的dom 节点,最后就更新 dom。这个过程会创建一些元素,此时才会去判断到底是组件还是原生的元素(或平台标签)。

为什么得在创建元素的时候才去判断到底是组件还是原生的元素?

笔者猜测:在前面做这个判断从技术上是可以做到的,因为这个逻辑(组件 or 原生的元素)判断不复杂;所以另一种可能就是将这个逻辑放在更新 dom 的时候更加合理。如果是普通元素,直接创建,如果是组件,则创建组件,如果包含子组件,则先创建(或实例化)子组件,并会传递一些参数,其中就包含通过 v-on 注册的事件。

initRender 初始化渲染

export function initRender (vm: Component) {
// 重置子树的根
vm._vnode = null
// 重置 _staticTrees。
// once 只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。
vm._staticTrees = null // v-once cached trees
// 用于当前 Vue 实例的初始化选项
const options = vm.$options
// 父树中的占位符节点
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
// 渲染上下文,即父节点的上下文
const renderContext = parentVnode && parentVnode.context
// vm.$slots,用来访问被插槽分发的内容
vm.$slots = resolveSlots(options._renderChildren, renderContext)
// vm.$scopedSlots,用来访问作用域插槽
vm.$scopedSlots = emptyObject // 定义 vm._c。创建元素类型的 vNode
// 渲染函数中会使用这个方法。
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) // 定义 vm.$attrs、vm.$listeners
// vm.$attrs,包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定
// vm.$listeners,包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器
const parentData = parentVnode && parentVnode.data if (process.env.NODE_ENV !== 'production') {
...
} else {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
}

此函数的功能有些零散,但至少我们知道该方法定义了 6 个实例属性:vm.$slotsvm.$scopedSlotsvm._cvm.$createElementvm.$attrsvm.$listeners

vm._c

此函数将会创建 vnode。这个我们可以通关源码来验证:

vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
...
return _createElement(context, tag, data, children, normalizationType)
}

真正起作用的是 _createElement

export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
if (isDef(data) && isDef((data: any).__ob__)) {
...
return createEmptyVNode()
}
...
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
...
// 定义 vnode
let vnode, ns
if (typeof tag === 'string') {
...
} else {
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}

重点看一下返回值(return),都是 vnode

模板一文中,我们知道模板编译成渲染函数,执行渲染函数就会生成一份 vNode。

callHook

callHook(vm, 'beforeCreate') 会触发 beforeCreated 对应的回调。请看源码:

// 将钩子拿出来,触发
export function callHook(vm: Component, hook: string) {
// #7573 调用生命周期钩子时禁用 dep 收集
pushTarget()
// 是一个数组。比如可以通过 Vue.mixin 注入一个 created,这样就能有两个 created。
const handlers = vm.$options[hook]
const info = `${hook} 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()
}

callHook() 真正调用钩子的方法是 invokeWithErrorHandling()。请看源码:

// handler 指回调
export function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
try {
// res,指回调的结果
res = args ? handler.apply(context, args) : handler.call(context) // {1}
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
// issue #9511
// avoid catch triggering multiple times when nested calls
res._handled = true
}
} catch (e) {
handleError(e, vm, info)
}
return res
}

其中 回调的结果(行{1})放在 try...catch 中,如果报错,则会进入处理错误逻辑,即 handleError()。看父组件、父父组件...(一直往上找),如果没能捕获错误,则进入全局错误处理(globalHandleError)。请看源码:

export function handleError (err: Error, vm: any, info: string) {
pushTarget()
try {
if (vm) {
let cur = vm
// 依次找父组件,父父组件...,如果定义了错误捕获(errorCaptured),并能捕获错误,则退出函数
// 否则进入全局错误处理
while ((cur = cur.$parent)) {
const hooks = cur.$options.errorCaptured
if (hooks) {
// 依次迭代错误捕获
for (let i = 0; i < hooks.length; i++) {
try {
// 如果错误捕获返回 false,则视为已捕获,结束函数
const capture = hooks[i].call(cur, err, vm, info) === false
if (capture) return
} catch (e) {
globalHandleError(e, cur, 'errorCaptured hook')
}
}
}
}
}
// 全局处理错误
globalHandleError(err, vm, info)
} finally {
popTarget()
}
}

initInjections

inject 就是给子孙组件注入属性或方法。就像这样:

// 父级组件提供 'foo'
var Provider = {
provide: {
foo: 'bar'
},
// ...
} // 子组件注入 'foo'
var Child = {
inject: ['foo'],
created () {
console.log(this.foo) // => "bar"
}
// ...
}

执行 initInjections() 方法,首先获取 vm 中注入的 inject(包含注入的 key 和对应的属性或方法),然后将 inject 绑定到 vm 上,期间会关闭响应。

// 初始化注入
export function initInjections(vm: Component) {
// 拿到注入的key以及对应的属性或方法。数据结构是 [{key: provideProperyOrFunction},...]
const result = resolveInject(vm.$options.inject, vm)
// 若有注入
if (result) {
// 官网:provide 和 inject 绑定并不是可响应的。
// 关闭响应
toggleObserving(false)
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
...
} else {
// 访问 key 时,其实就会访问 result[key],即调用注入的函数
defineReactive(vm, key, result[key])
}
})
toggleObserving(true)
}
}

TipresolveInject() 会返回一个包含对象的数组,里面是 inject 属性以及对应的值:

export function resolveInject(inject: any, vm: Component): ?Object {
if (inject) {
const result = Object.create(null)
const keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject)
// 依次遍历 inject 的每个 key,从当前 vm 开始找 provide,若没有则依次往上一级找
// 如果找到,则注册到一个空对象(result)中
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)) {
// 找到inject对应的 provide,存入 result 对象中
// _provided 在下文的 initProvide 中被初始化
result[key] = source._provided[provideKey]
break
}
// 找上一级
source = source.$parent
}
// source 为假值,说明一直找到顶部,都找到
if (!source) {
...
}
}
return result
}
}

initState 初始化状态

初始化状态,即初始化 props、methods、data、computed 和 watch。

export function initState(vm: Component) {
vm._watchers = []
// 用于当前 Vue 实例的初始化选项
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

定义组件时,我们可以通过 props 定义父组件传来的属性。就像这样:

// 接收父组件传来的 title 属性
Vue.component('blog-post', {
props: ['title'],
template: '<h3>{{ title }}</h3>'
})

initProps() 会将 prop 和对应的属性或方法加入 vm._props 中,并将 prop 代理到 vm._props。如果访问 props,例如 vm.titlexx,其实访问的是 vm._props.titlexx。请看源码:

function initProps(vm: Component, propsOptions: Object) {
// propsData,创建实例时传递 props
const propsData = vm.$options.propsData || {}
// 下面会将 prop 和对应的属性或方法绑定到此对象中
const props = vm._props = {}
const keys = vm.$options._propKeys = []
...
// propsOptions 是 vm.$options.props
for (const key in propsOptions) {
keys.push(key)
// value 是 prop 对应的属性或方法
const value = validateProp(key, propsOptions, propsData, vm)
// Tip:直接看生成环境的逻辑即可
if (process.env.NODE_ENV !== 'production') {
...
} else {
// defineReactive 将数据转为响应式。给 props 添加 key 和对应的 value。
defineReactive(props, key, value)
}
// 如果 key 不在 vm 中,则将 key 代理到 _props
// 就是说,如果访问props,例如 vm.titlexx,其实访问的是 vm._props.titlexx
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}

initMethods

initMethods() 会将我们定义的方法放到 vm 中。开发环境下会检查方法名,比如不能和 prop 中重复,不能和现有 Vue 实例方法冲突。

function initMethods (vm: Component, methods: Object) {
const props = vm.$options.props
for (const key in methods) {
if (process.env.NODE_ENV !== 'production') {
...
if (props && hasOwn(props, key)) {
// `方法 "${key}" 已经被定义为一个 prop。`,
warn(
`Method "${key}" has already been defined as a prop.`,
vm
)
}
if ((key in vm) && isReserved(key)) {
// `方法 "${key}" 与现有的 Vue 实例方法冲突。 ` +
// `避免定义以_或$开头的组件方法。`
warn(
`Method "${key}" conflicts with an existing Vue instance method. ` +
`Avoid defining component methods that start with _ or $.`
)
}
}
// bind(methods[key], vm),将 methods[key] 方法绑定到 vm 中
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
}
}

initData

initData() 首先会取得 data,并放入 vm._data 中。依次将 data 中的 key 代理到 vm._data 中,期间会检查 key 是否与 methods 或 props 中 key 相同。如果访问 data,例如访问 vm.age,其实访问的是 vm._data.age。请看源码:

function initData(vm: Component) {
// 取得数据 vm.$options.data
let data = vm.$options.data
// 如果数据是函数,则调用 getData(即 data.call(vm, vm))返回数据,每个实例都有一份
// 如果data不是函数,则每个实例公用这份 data
// vm._data 指向 data,后面会做一个代理。访问 vm.age,其实访问的是 vm._data.age
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// data 如果不是一个对象,开发环境则发出警告:数据函数应该返回一个对象
if (!isPlainObject(data)) {
...
}
// 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]
// key 不能和 methods 中相同
if (process.env.NODE_ENV !== 'production') {
...
}
// key 不能和 props 中相同
if (props && hasOwn(props, key)) {
...
// 没有被预定,则将 key 代理到 _data
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}

initComputed

computed 用法如下:

computed: {
aDouble: vm => vm.a * 2
}

initComputed() 会依次迭代我们定义的 computed,给每一个 key 都会创建一个 Watcher,并给 Watcher 传入 key 对应的回调方法,最后在 vm 上定义计算属性(defineComputed(vm, key, userDef))。请看源码:

function initComputed(vm: Component, computed: Object) {
// 创建一个空对象给 vm._computedWatchers,是计算属性的 watcher
const watchers = vm._computedWatchers = Object.create(null)
// 是否是服务端渲染
const isSSR = isServerRendering() // 迭代 computed
for (const key in computed) {
// 取得 key 对应的方法
const userDef = computed[key]
// computed 还支持 get、set
const getter = typeof userDef === 'function' ? userDef : userDef.get
... // 非服务端渲染
if (!isSSR) {
// create internal watcher for the computed property.
// 为计算属性创建内部观察者。访问 vm['计算属性'] 时会使用
watchers[key] = new Watcher(
vm,
// 取值(watcher.value)时会用到
getter || noop, // {1}
noop,
computedWatcherOptions
)
}
// 定义计算属性
if (!(key in vm)) {
// userDef,即 key 对应的回调
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
// 计算属性不能在 data、props和methods中
...
}
}
}

如果你想知道 defineComputed(vm, key, userDef) 做了什么?请继续看。

defineComputed() 的核心功能在最后一句:

// 定义计算属性
export function defineComputed(
target: any,
key: string,
userDef: Object | Function
) {
// 不是服务端渲染,则需要缓存
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop } else {
// 计算属性可以有 get、set
...
}
...
// target 是 vm
// 访问 vm[key] 就会访问 sharedPropertyDefinition
Object.defineProperty(target, key, sharedPropertyDefinition)
}

我们这里不是服务端渲染,所以进入 createComputedGetter()

function createComputedGetter(key) {
return function computedGetter() {
// 取得在 initComputed() 中定义的 watcher
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 计算属性是有缓存的(官网:计算属性是基于它们的响应式依赖进行缓存的)
// 脏的(比如说计算属性依赖的某个数据值变了,就是脏的),则重新求值
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
// 取得 watcher 的值。会访问 initComputed() 方法中的 getter(行{1})
return watcher.value
}
}
}

defineComputed(vm, key, userDef) 做什么事情,它的名字其实已经告诉我们了(即定义计算属性)。比如访问一个计算属性,会取得对应计算属性的 Watcher,然会从 watcher 中取得对应的值。其中 watcher 的 dirty 与缓存有关。

Tip:有关 Water 的介绍可以看 侦测数据的变化

initWatch

用法如下:

watch: {
a: function (val, oldVal) {
console.log('new: %s, old: %s', val, oldVal)
}
}

initWatch() 会依次迭代我们传入的 watch,并通过 createWatcher 创建 Watcher。请看源码:

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)
}
}
}

createWatcher() 的本质是 vm.$watch()

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]
}
// hander 是函数
// vm.$watch() 方法赋予我们监听实例上数据变化的能力
return vm.$watch(expOrFn, handler, options)
}

initProvide

provide 就是提供给子孙组件注入属性或方法。就像这样:

// 父级组件提供 'foo'
var Provider = {
provide: {
foo: 'bar'
},
// ...
}

initProvide 与上文的 initInjections 对应。

initProvide() 主要就是将用户传入的 provide 保存到 vm._provided,后续给 inject 使用。请看源码:

export function initProvide(vm: Component) {
const provide = vm.$options.provide
// 存起来,供子孙组件使用
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}

vm.$mount

_init() 的末尾就是挂载(vm.$mount()):

if (vm.$options.el) {
vm.$mount(vm.$options.el)
}

扩展

props、data、methods、computed 的 key 为什么不能相同

因为这些 key 最后都绑定在 vm 上,所以不能相同。请看源码:

// props
proxy(vm, `_props`, key) // data
proxy(vm, `_data`, key) // methods
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm) // compued
defineComputed(vm, key, userDef)

data 中可以使用 props吗

initState() 中有如下代码:

// 初始化 props
if (opts.props) initProps(vm, opts.props)
...
// 初始化 data
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}

由于 props 先初始化,所以在 data 中可以使用 props。请看示例:

// 父组件
<welcome-button name="peng"></welcome-button> // WelcomeButton.vue
<template>
<div>
name={{ name }} <br />
myName={{ myName }}
</div>
</template>
<script>
export default {
props: ['name'],
data () {
return {
myName: this.name + 'jiali'
}
}
}
</script>

浏览器输出:

name=peng
myName=pengjiali

Tip:props 中使用 data 却是不可以的,因为 data 初始化在 props 后面。

computed 和 watch 谁先执行

请问下面这段代码,控制台输出什么:

<template>
<div>
<!-- 读取三个属性 -->
{{ doubleAge }} {{ age }} {{ name }}
</div>
</template>
<script>
export default {
data () {
return {
age: 18,
name: 'peng'
}
},
computed: {
doubleAge: function (vm) {
const result = this.age * 2
console.log('computed')
return result
}
},
watch: {
age: {
handler: function (val, oldVal) {
console.log('watch age')
},
immediate: !true
},
name: {
handler: function (val, oldVal) {
console.log('watch name')
},
// 立即执行
immediate: true
}
},
created () {
setTimeout(() => this.age++, 5000)
}
}
</script>
watch name
computed
// 过5秒
watch age
computed

虽然在 initState() 中先初始化 computed,再初始化 watch,但在这个例子中,却是先执行 watch,后执行 computed。

// computed
if (opts.computed) initComputed(vm, opts.computed)
// watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}

其他章节请看:

vue 快速入门 系列

vue 快速入门 系列 —— Vue 实例的初始化过程的更多相关文章

  1. vue 快速入门 系列 —— vue 的基础应用(上)

    其他章节请看: vue 快速入门 系列 vue 的基础应用(上) Tip: vue 的基础应用分上下两篇,上篇是基础,下篇是应用. 在初步认识 vue一文中,我们已经写了一个 vue 的 hello- ...

  2. vue 快速入门 系列 —— vue 的基础应用(下)

    其他章节请看: vue 快速入门 系列 vue 的基础应用(下) 上篇聚焦于基础知识的介绍:本篇聚焦于基础知识的应用. 递归组件 组件是可以在它们自己的模板中调用自身的.不过它们只能通过 name 选 ...

  3. vue 快速入门 系列 —— vue loader 上

    其他章节请看: vue 快速入门 系列 vue loader 上 通过前面"webpack 系列"的学习,我们知道如何用 webpack 实现一个不成熟的脚手架,比如提供开发环境和 ...

  4. vue 快速入门 系列 —— vue loader 下

    其他章节请看: vue 快速入门 系列 vue loader 下 CSS Modules CSS Modules 是一个流行的,用于模块化和组合 CSS 的系统.vue-loader 提供了与 CSS ...

  5. vue 快速入门 系列 —— vue loader 扩展

    其他章节请看: vue 快速入门 系列 vue loader 扩展 在vue loader一文中,我们学会了从零搭建一个简单的,用于单文件组件开发的脚手架.本篇将在此基础上继续引入一些常用的库:vue ...

  6. vue 快速入门 系列 —— Vue(自身) 项目结构

    其他章节请看: vue 快速入门 系列 Vue(自身) 项目结构 前面我们已经陆续研究了 vue 的核心原理:数据侦测.模板和虚拟 DOM,都是偏底层的.本篇将和大家一起来看一下 vue 自身这个项目 ...

  7. vue 快速入门 系列 —— vue-cli 下

    其他章节请看: vue 快速入门 系列 Vue CLI 4.x 下 在 vue loader 一文中我们已经学会从零搭建一个简单的,用于单文件组件开发的脚手架:本篇,我们将全面学习 vue-cli 这 ...

  8. vue 快速入门 系列 —— vue-router

    其他章节请看: vue 快速入门 系列 Vue Router Vue Router 是 Vue.js 官方的路由管理器.它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌. 什么是路由 ...

  9. vue 快速入门 系列 —— vue-cli 上

    其他章节请看: vue 快速入门 系列 Vue CLI 4.x 上 在 vue loader 一文中我们已经学会从零搭建一个简单的,用于单文件组件开发的脚手架:本篇,我们将全面学习 vue-cli 这 ...

随机推荐

  1. Linux Cgroups详解(一)

    [转载]http://blog.chinaunix.net/uid-23253303-id-3999432.html Cgroups是什么? Cgroups是control groups的缩写,是Li ...

  2. 以简御繁介绍IOC

    1.IOC的理论背景 大家开发理念,一直都是奔着架构稳定.低耦合性.而IOC初衷,就是为了解决模块依赖问题,理解<六大设计原则(SOLID)> 如图所示,在我们开发中,业务的实现,就是靠着 ...

  3. Golang项目的配置管理——Viper简易入门配置

    Golang项目的配置管理--Viper简易入门配置 What is Viper? From:https://github.com/spf13/viper Viper is a complete co ...

  4. Java EE数据持久化框架作业目录(作业笔记)

    第1章 MyBatis入门>>> 1.1.4 在Eclipse中搭建MyBatis基本开发环境 1.2.5 使用MyBatis查询所有职员信息 1.3.3 获取id值为1的角色信息. ...

  5. C#/.NET之WebAPI(从入门到放弃一)

    1.怎么理解WebApi,他究竟是什么? 关于这一篇,视频学习可参照B站up主:全栈ACE,全栈ACE的个人空间,社区QQ群如下,有什么问题也可加群咨询. 首先使用Visual Studio创建一个新 ...

  6. Eclipse启动SpringCloud微服务集群的方法

    1.说明 下面这篇文章介绍了Eureka Server集群的启动方法, SpringCloud创建Eureka模块集群 是通过jar包启动时指定配置文件的方式实现的. 现在只有Eureka Serve ...

  7. SpringCloud创建Config多客户端公共配置

    1.说明 基于已经创建好的Spring Cloud配置中心, 在配置中心仅保存一套配置文件, 多个客户端可以通过配置中心读取到相同的配置, 而不需要在每个客户端重复配置一遍, 下面以一个Config ...

  8. POI导入导出Excel(HSSF格式,User Model方式)

    1.POI说明 Apache POI是Apache软件基金会的开源代码库, POI提供对Microsoft Office格式档案读和写的功能. POI支持的格式: HSSF - 提供读写Microso ...

  9. 基于GO语言实现的固定长度邀请码

    1. 选取数字加英文字母组成32个字符的字符串,用于表示32进制数. 2. 用一个特定的字符比如`G`作为分隔符,解析的时候字符`G`后面的字符不参与运算. 3. LEN表示邀请码长度,默认为6. g ...

  10. centos6.5-DNS搭建

    在RHEL6.5中,系统光盘自带了BIND服务的安装文件 安装步骤 准备工作: Service iptables stop    #关闭防火墙    Setenforce 0    关闭selinux ...