vue 快速入门 系列 —— Vue 实例的初始化过程

vue

其他章节请看:

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 实例的初始化过程 的全部内容, 来源链接: utcz.com/z/378743.html

回到顶部