介绍:
本篇文章我们将粗略的去了解,在 Vue 实例创建的过程中,分别执行了什么操作,为了照顾功底不深的小伙伴我们省去了部分方法实现的详细源码,有兴趣的可以自己去下载源码文档查看,或者关注我的后续文章
本节学习将让大家彻底了解 Vue
的结构和其执行顺序流程,让我们对 Vue 的使用更加行云流水
1. 首先我们来看下源码中,定义 Vue 这个文件夹中发生了什么
① 首先定义了 Vue 构造函数,在构造函数中加入了判断,是否使用 new Vue()
的样式来调用,然后调用了该原型下的 _init()
方法,并且将我们传入的一系列数据传入,像 data, methods, computed, watch, 还有生命周期钩子等等
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
② 我们在 new Vue()
执行前,先执行了以下这一系列函数,将该方法传入,我们分别看看他们干了什么
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
ifecycleMixin(Vue)
renderMixin(Vue)
2. 在new Vue 之前做了什么
① initMixin(Vue)
为Vue函数原型上添加了在构造器中执行的_init
函数
Vue.prototype._init = function (options) {
// 其中的代码我们稍后在研究
}
② stateMixin(Vue)
为Vue 添加了一系列实例方法和参数
涉及到了 观察者Watcher,将在下一篇文章中专门讲解
- $data: 返回的是
new Vue
参数中传入的 data - $props: 返回的是当前组件接收到的 props 对象
- $set: 弥补后期向
data
中的新添加数据不支持响应式的不足,由于响应式数据只发生在初始化 data 数据中 - $delete: 删除 data 中的数据
- $watch: 用于监听一个值的变化
export function stateMixin (Vue) {
const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function (expOrFn,cb,options){
}
}
③ eventsMixin 函数
- emit `一同使用,相当与发布订阅模式
- on` 上定义的事件
- on ` 上的事件
- $once:定义在实例中只执行一次的自定义事件
Vue.prototype.$on = function (event, fn): Component {
}
Vue.prototype.$once = function (event, fn){
}
Vue.prototype.$off = function (event, fn) {
}
Vue.prototype.$emit = function (event) {
}
④ lifecycleMixin(Vue)
这个阶段只干了三件事情,为Vue原型上增加两个生命周期方法和一个没有暴露的方法
其中涉及到了callHook钩子函数的执行,我们在下文中讲解
- $forceUpdate: 主动使
Vue
实例上的所有 watcher 更新 - $destroy: 先调用了
beforeDestroy
钩子函数,解绑所有 Vue 实例中的连接,最后调用destroyed
钩子函数。 - _update: 添加一个方法,用来更新 dom ,参数为一个虚拟
node
,通过 diff 算法,更新新的dom 节点
export function lifecycleMixin (Vue) {
Vue.prototype.$forceUpdate = function () {
const vm = this
if (vm._watcher) {
vm._watcher.update()
}
}
Vue.prototype.$destroy = function () {
const vm = this
callHook(vm, 'beforeDestroy')
// 删除所有 Vue 实例相关绑定
callHook(vm, 'destroyed')
}
Vue.prototype._update = function (vnode, hydrating) {
}
}
⑤ renderMixin
- $nextTick:暴露一个实例方法,参数为一个方法,将该方法添加到全局定义的
callback
队列中,在dom
更新之后自动执行 - _render: 返回一个虚拟节点准备更新到 dom 树上
- installRenderHelpers : 为 Vue 上绑定了一系列渲染所需要的方法
function renderMixin (Vue: Class<Component>) {
// install runtime convenience helpers
installRenderHelpers(Vue.prototype)
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
Vue.prototype._render = function (): VNode {
}
}
3. this._init(options)
在执行 Vue 构造方法之前,我们先初始化了一系列Vue 原型上的方法和数据,现在我们进入下一步来了解初始化的过程。
① 进行合并
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
② 执行初始化
- 通过以下代码一目了然,在
beforeCreate
钩子执行前,data 中的数据还没有初始化为响应式,也没有添加到 vue 实例中,因此通过 this 访问不到
initLifecycle(vm) // 初始化生命周期
initEvents(vm) // 初始化事件
initRender(vm) // 初始化渲染树
callHook(vm, 'beforeCreate') // 执行钩子函数
initInjections(vm) // 初始化 inject,用于子组件接收父组件的传参
initState(vm) // 初始化 data 响应式,method,computed 等等
initProvide(vm) // 负责给子组件提供参数
callHook(vm, 'created') // 执行 created 钩子函数
4. 在上一节中讲的初始化中, 我们来具体看每一个初始化函数中做了什么
① initLifecycle(vm)
在实例上定义了新的参数
- $parent :当前实例的父实例
- $root: 当前实例的根实例,如果没有则为自己本身
- $children : 当前实例下的子实例,也就是子组件
- $refs: 当前实例下所有有
ref
属性的 dom 节点
const options = vm.$options
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)
初始化了一个 _event对象,用来存放通过 $on
调用的事件
vm._events = Object.create(null)
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
③ initRender(vm)
初始化渲染函数
- $slots: 访问当前实例下的 slot 标签,返回的是一个具名对象,默认问
default
- $scopedSlots: 用来访问作用域插槽。对于包括 默认 slot 在内的每一个插槽,该对象都包含一个返回相应 VNode 的函数。
- $attrs: 用来访问父作用域传入子组件上的所有
prop
,除了class和style - $listeners: 访问父作用域中的 (不含 .native 修饰器的) v-on 事件监听器
- 定义了
_c
,$createElement
函数来创建一个虚拟node
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
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)
const parentData = parentVnode && parentVnode.data
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
④ callHook(vm, 'beforeCreate');
我们来看下钩子函数内部的实现
- 这里使用了策略模式,将我们对应的钩子函数赋值给 handlers,然后执行
invokeWithErrorHandling
函数
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)
}
- 我们来进一步了解一下
invokeWithErrorHandling
里边发生了什么 在这里调用了传过来的钩子函数,并把该函数this 指向 vm 实例
function invokeWithErrorHandling (handler,context,args,vm,info)
{
args ? handler.apply(context, args) : handler.call(context)
}
⑤ initInjections(vm)
- resolveInject :该函数将遍历该实例的父实例,去查找
injec
t 的值 - toggleObserving:控制是否将值定义为 响应式的,因此
inject/provide
提供的值为非响应式的 - defineReactive:将值添加到 vm 实例上,可直接通过实例访问
if (result) {
const result = resolveInject(vm.$options.inject, vm)
toggleObserving(false)
Object.keys(result).forEach(key => {
defineReactive(vm, key, result[key])
})
toggleObserving(true)
}
⑥ initState(vm)
这个函数很简单,就是对传入的参数,方法,数据,computed,watch进行初始化,让我们看看他们内部是怎么一回事
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
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)
}
- initProps(vm, opts.props)
该方法中,将父给子组件传入的参数解析出来,并保存到实例的
_props
下边,如果该实例不是根实例,则该参数不是响应式的
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
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)
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
- initMethods(vm, opts.methods)
初始化函数中,将我们传入的每一个
methods
的方法的执行上下文绑定问vm
,也就是this
指向method
function initMethods (vm: Component, methods: Object) {
const props = vm.$options.props
for (const key in methods) {
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
}
}
- initData(vm)
这个函数中主要做了一件事情,就是将
Data
中的数据变为响应式的,并且判断该名字是否与methods
和props
中的重复,重复将报错
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
//判断名字是否重复
observe(data, true /* asRootData */)
}
- initComputed(vm, opts.computed)
为每一个
computed
中的键值生成一个观察者,并且将每一个computed
中的函数作为getter
和setter
绑定在vm
实例对应的键值上,获取该值时,将自身watcher
放置到Dep.targer
的位置,然后触发对应依赖值的getter
,将该watch放入自身依赖中,就形成了一个完整的观察者模式
,此处不做解释,我们将在之后文章中专门去讲观察者模式
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
}
}
- initWatch(vm, opts.watch)
1. 遍历watch中的方法,为其创建一个
观察者
,并添加到对应的依赖上
2. 在createWatcher
中,又继续调用了vm.$watch
,而在vm.$watch中有immediate
的判断,是否立即调用
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)
}
}
⑦ initProvide(vm)
这个方法中将我们传入的provide放置在实例的_provided
中,方便 inject
向上查找该值
function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
⑧ callHook(vm, 'created')
在上边我们讲过了 callHook
的运行机制,这里我们就不多说,调用了 created
钩子函数
5. 在所有初始化结束之后,我们就进入了挂载阶段
① 最后一步,将 el
挂载到 dom 树中
我们将在下一篇文章中详细讲述
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
至此,初始化阶段就全部结束了,下一步,让我们来学习挂载阶段。