API
的设计new Vue()
开始我们在实际的项目中使用 Vue
的时候 , 一般都是在 main.js
中通过 new Vue({el : '#app , ...options})
生成根组件进行使用的, 相关的配置都通过 options
传入。 Vue
的原型对象会帮我们初始化好很多属性和方法, 我们可以通过 this.property
直接调用即可; 而 Vue
这个类也通过类的静态方法初始化了一些全局的 api, 我们可以通过类名直接调用, 比如 Vue.component()
。 Vue
的原型对象和全局 API 是通过混入的方式融入 Vue
中的。
如下面代码所示, import Vue from './instance/index'
引入 Vue
的构造函数,在用户调用之前, Vue
先做了一些初始化工作, 具体做了哪些工作看 vue/src/core/instance/index.js(点击跳转)
中的代码(下边第二段):
function Vue (options) { ... }
定义了 Vue
构造函数, 我们调用 new Vue
时,只会执行一句代码, 即 this._init(options)
;initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue)
, 从而将 Vue
的初始化函数、状态初始化函数、事件初始化函数、生命周期初始化函数、渲染函数混入到 Vue
的原型对象。这才使得每个组件都有了便捷的功能。初始化函数具体都做了什么工作, 且看后续的分析。vue/src/core/index.js
:
import Vue from './instance/index' // 1. 引入 Vue 构造函数 import { initGlobalAPI } from './global-api/index' // 2. 引入初始化全局 API 的依赖 import { isServerRendering } from 'core/util/env' import { FunctionalRenderContext } from 'core/vdom/create-functional-component' initGlobalAPI(Vue) // 3. 初始化全局 API Object.defineProperty(Vue.prototype, '$isServer', { get: isServerRendering }) Object.defineProperty(Vue.prototype, '$ssrContext', { get () { /* istanbul ignore next */ return this.$vnode && this.$vnode.ssrContext } }) // expose FunctionalRenderContext for ssr runtime helper installation Object.defineProperty(Vue, 'FunctionalRenderContext', { value: FunctionalRenderContext }) Vue.version = '__VERSION__' export default Vue
vue/src/core/instance/index.js
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) // 1. Vue 实例初始化 } initMixin(Vue) // 2 stateMixin(Vue) // 3 eventsMixin(Vue) // 4 lifecycleMixin(Vue) // 5 renderMixin(Vue) // 6 export default Vue
注释 1
处, new Vue()
时, 只执行了一个初始化工作 this._init(options)
; 值得注意的是, 在定义完成构造函数后,此时尚未有 new Vue
的调用, 即在实例创建之前, 会执行注释 2 3 4 5 6
处的初始化工作, 让后初始化全局 API
,至此准备工作已经就绪, 通过调用 new Vue
生成 Vue
实例时,会调用 this._init(options)
。接下来,探索一下 Vue
生成实例前, 依次做了哪些工作。
initMixin
(vue\src\core\instance\init.js
)let uid = 0 export function initMixin (Vue: Class<Component>) { Vue.prototype._init = function (options?: Object) { const vm: Component = this // 1. vm 即 this, 即 Vue 的实例对象 // a uid vm._uid = uid++ // 每个 Vue 实例对象都可以看成一个组件, 每个组件有一个 _uid 属性来标记唯一性 let startTag, endTag /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { startTag = `vue-perf-start:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` mark(startTag) } // a flag to avoid this being observed vm._isVue = true // merge options // 合并参数, options 是我们调用 `new Vue({ el : 'app'chuand, ...args})` 时传入的参数 // 合并完成后将合并结果挂载到当前 `Vue` 实例 if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { vm.$options = mergeOptions( // 合并完成后将合并结果挂载到当前 `Vue` 实例 // 这个函数会检查当前 Vue 实例的否早函数和其父类、祖先类上的 options 选项, 并能监听是否发生了变化, 将 祖先类、父类和当前 Vue 实例的 options 合并到一起 resolveConstructorOptions(vm.constructor), options || {}, vm ) } /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm initLifecycle(vm) // 1. 初始化声明周期 initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { vm._name = formatComponentName(vm, false) mark(endTag) measure(`vue ${vm._name} init`, startTag, endTag) } if (vm.$options.el) { vm.$mount(vm.$options.el) } } }
上边代码给每个实例标记一个唯一的 _uid
属性, 然后标记是否为 Vue
实例, 将用户传入的参数和 Vue
自有参数合并后,挂载到 Vue
的 $options
属性 。
export function initLifecycle (vm: Component) { const options = vm.$options // locate first non-abstract parent // 这个注释已经很明了了, 就是查找当前 vue 实例的第一个非抽象父组件 // 找到后会将当前的组件合并到父组件的 `$children` 数组里 // 从而建立了组件的父子关系 let parent = options.parent if (parent && !options.abstract) { while (parent.$options.abstract && parent.$parent) { parent = parent.$parent } parent.$children.push(vm) } 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 }
如上, 初始化声明周期的时候, 会简历当前组件与其他组件的父子关系, 如果找到父组件, 会将 $root
指针指向父组件,找不到的话, 指向当前 Vue 实例。接下来 vm.$children = []
初始化子组件列表, vm.$refs = {}
初始化引用列表, vm._watcher = null
初始化观察者列表, 此时还没有观察者,无法检测数据变化, vm._isMounted = false
标记当前组件尚未挂载到 DOM
, vm._isDestroyed = false
标记当前组件并不是一个被销毁的实例,这与垃圾回收有关系的, vm._isBeingDestroyed = false
标记当前组件是否正在销毁工作。
至此, 声明周期的初始化已经完成了。
vue/src/core/instance/events.js
:
stateMixin
: 状态初始化vue/src/core/instance/state.js
:
export function stateMixin (Vue: Class<Component>) { // flow somehow has problems with directly declared definition object // when using Object.defineProperty, so we have to procedurally build up // the object here. const dataDef = {} dataDef.get = function () { return this._data } const propsDef = {} propsDef.get = function () { return this._props } if (process.env.NODE_ENV !== 'production') { dataDef.set = function () { warn( 'Avoid replacing instance root $data. ' + 'Use nested data properties instead.', this ) } propsDef.set = function () { warn(`$props is readonly.`, this) } } Object.defineProperty(Vue.prototype, '$data', dataDef) // 1 Object.defineProperty(Vue.prototype, '$props', propsDef) // 2 Vue.prototype.$set = set // 3 Vue.prototype.$delete = del // 4 Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function { const vm: Component = this if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) } options = options || {} options.user = true const watcher = new Watcher(vm, expOrFn, cb, options) if (options.immediate) { try { cb.call(vm, watcher.value) } catch (error) { handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`) } } return function unwatchFn () { watcher.teardown() } } }
上边代码对实例的状态做了初始化。 在注释 1 2
两个地方分别给 Vue
原型对象增加了 $data $props
两个属性, 这两个属性的值分别是当前 vm
的 _data _props
属性, 并且设置这两个属性是不可以修改的。
注释 3 4
处为 vm
添加了 set
和 delete
方法, set
和 delete
是干嘛的就不用介绍了吧, Vue
对象本身也有 Vue.set
和 Vue.delete
这两个方法, 都是来源于下边 set
这个函数, 他的作用体现在下边代码注释的 1 2
处:
参数
target
为对象或者数组,target
有一个__ob__
属性, 这个属性的来源是在Observer
这个类中的构造函数,其中有一句是def(value, '__ob__', this)
,value
是待观测的对象, 也就是我们写代码时传入的data
中的属性, 然后我们传入的data
其实都被代理到__ob__
这个属性上了,以后我们操作data
中的数据或者访问data
中的数据都会被代理到__ob__
这个属性。
之后又在原型对象挂载了 $watcher
方法, 该方法的返回值是一个销毁 watcher
的方法。 至于 watcher
是个啥, 以及 watcher
的作用,后边再谈。
/** * Set a property on an object. Adds the new property and * triggers change notification if the property doesn't * already exist. */ export function set (target: Array<any> | Object, key: any, val: any): any { if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`) } if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key) target.splice(key, 1, val) return val } if (key in target && !(key in Object.prototype)) { target[key] = val return val } const ob = (target: any).__ob__ if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' ) return val } if (!ob) { target[key] = val return val } defineReactive(ob.value, key, val) // 1 ob.dep.notify() // 2 return val }
vue\src\core\util\lang.js
:
/** * Define a property. */ export function def (obj: Object, key: string, val: any, enumerable?: boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) }
其实就是在 Vue 原型对象上挂载了一些方法 ($on $once $off $emit
) , 基于发布订阅模式,实现了一个事件响应系统, 与 nodejs 中的 eventEmitter 是极其相似的。这就是我们常用的事件总线机制的来源。
简单解析一下下面的代码 :
$on
是事件的订阅, 通过他的参数(event: string | Array<string>, fn: Function)
可知, 可以一次订阅多个事件,他们共享一个处理函数, 然后将所有的处理函数以键值对的形式({eventName : handler[]}
)存储在vm._events
对象中,等待事件发布。一旦事件发布, 就会根据事件类型(eventName
)去事件处理函数列表(handler[]
)中,读取处理函数并执行。
$emit
是事件的发布, 生产环境中对事件名称(也就是类型),进行了大小写转换, 不用区分事件名称的大小写了, 当然我们编码不能这样粗狂的去写哈。 然后cbs
是根据事件名称读取的处理函数的列表,const args = toArray(arguments, 1)
是处理事件的参数, 函数toArray
将$emit
函数的参数除掉第一个以后, 最终传入了我们的订阅函数中。 即
vm.$emit('render', 'a',124)
代码最终调用结果是vm._events['render']
列表中所有的函数都以('a', 123)
为参数运行一次。
$off
是将事件的订阅函数从订阅列表中删除, 它提供了两个参数(event?: string | Array<string>, fn?: Function)
, 两个参数都是可选的, 并且不能只穿第二参数。 如果实参列表为空, 则当前vm
上订阅的所有事件和事件的处理函数都将被删除;如果第二参数为空, 则当前vm
的vm._events[event]
中所有的处理函数将被清空; 如果第二个参数fn
不为空, 则只将vm._events[event]
事件处理列表中的fn
函数删除。
$once
表示事件处理只执行一次, 多次发布事件,也只会执行一次处理函数。这个函数有点小技巧。先建立一个on
函数, 然后把事件处理函数fn
挂载到这个函数对象上, 函数也是对象,可以有自己的属性,这个没有疑问吧。on
函数中只有两句代码vm.$off(event, on)
, 让 vm 解除on
函数的订阅, 这就可以保证以后不会再执行on
函数了; 下一句fn.apply(vm, arguments)
调用fn
, 这保证了fn
被执行了一次。 哈哈哈, 666.
事件的初始化就这样讲完了。
export function eventsMixin (Vue: Class<Component>) { const hookRE = /^hook:/ Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component { const vm: Component = this if (Array.isArray(event)) { for (let i = 0, l = event.length; i < l; i++) { vm.$on(event[i], fn) } } else { (vm._events[event] || (vm._events[event] = [])).push(fn) // optimize hook:event cost by using a boolean flag marked at registration // instead of a hash lookup if (hookRE.test(event)) { vm._hasHookEvent = true } } return vm } Vue.prototype.$once = function (event: string, fn: Function): Component { const vm: Component = this function on () { vm.$off(event, on) fn.apply(vm, arguments) } on.fn = fn vm.$on(event, on) return vm } Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component { const vm: Component = this // all if (!arguments.length) { vm._events = Object.create(null) return vm } // array of events if (Array.isArray(event)) { for (let i = 0, l = event.length; i < l; i++) { vm.$off(event[i], fn) } return vm } // specific event const cbs = vm._events[event] if (!cbs) { return vm } if (!fn) { vm._events[event] = null return vm } // specific handler let cb let i = cbs.length while (i--) { cb = cbs[i] if (cb === fn || cb.fn === fn) { cbs.splice(i, 1) break } } return vm } Vue.prototype.$emit = function (event: string): Component { const vm: Component = this if (process.env.NODE_ENV !== 'production') { const lowerCaseEvent = event.toLowerCase() if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) { tip( `Event "${lowerCaseEvent}" is emitted in component ` + `${formatComponentName(vm)} but the handler is registered for "${event}". ` + `Note that HTML attributes are case-insensitive and you cannot use ` + `v-on to listen to camelCase events when using in-DOM templates. ` + `You should probably use "${hyphenate(event)}" instead of "${event}".` ) } } let cbs = vm._events[event] if (cbs) { cbs = cbs.length > 1 ? toArray(cbs) : cbs const args = toArray(arguments, 1) const info = `event handler for "${event}"` for (let i = 0, l = cbs.length; i < l; i++) { invokeWithErrorHandling(cbs[i], vm, args, vm, info) } } return vm } }
lifecycleMixin
生命周期初始化代码如下, 在 Vue 的原型对象上增加了三个方法 _update $forceUpdate $destroy
, 依次来看下都做了什么事吧。
vm._update
通过__patch__
函数把虚拟节点vnode
编译成真实DOM
. 并且, 组件的更新也是在这里完成虚拟节点到真实DOM
的转换。并且父组件更新后, 子组件也会更新。
vm.$forceUpdate
若果当前组件上有观察者, 则直接更细组件。
vm.$destroy
销毁组件, 如果当前组件正在走销毁的流程,则直接返回, 等待继续销毁。 否则, 会触发beforeDestroy
这个声明周期, 并将当前组件标记为正在销毁
的状态。 然后将当前组件从父组件中删除, 然后销毁所有的 watcher, 销毁vm._data__ob__
, 标记组件状态为已销毁
,重新生成真实DOM
, 触发destroyed
生命周期方法, 移除当前组件订阅的事件和事件的处理函数, 将当前组件对父组件的引用清空。
vue/src/core/instance/lifecycle.js
export function lifecycleMixin (Vue: Class<Component>) { Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode const restoreActiveInstance = setActiveInstance(vm) vm._vnode = vnode // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. if (!prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) } restoreActiveInstance() // update __vue__ reference if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } // if parent is an HOC, update its $el as well if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } // updated hook is called by the scheduler to ensure that children are // updated in a parent's updated hook. } Vue.prototype.$forceUpdate = function () { const vm: Component = this if (vm._watcher) { vm._watcher.update() } } Vue.prototype.$destroy = function () { const vm: Component = this if (vm._isBeingDestroyed) { return } callHook(vm, 'beforeDestroy') vm._isBeingDestroyed = true // remove self from parent const parent = vm.$parent if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) { remove(parent.$children, vm) } // teardown watchers if (vm._watcher) { vm._watcher.teardown() } let i = vm._watchers.length while (i--) { vm._watchers[i].teardown() } // remove reference from data ob // frozen object may not have observer. if (vm._data.__ob__) { vm._data.__ob__.vmCount-- } // call the last hook... vm._isDestroyed = true // invoke destroy hooks on current rendered tree vm.__patch__(vm._vnode, null) // fire destroyed hook callHook(vm, 'destroyed') // turn off all instance listeners. vm.$off() // remove __vue__ reference if (vm.$el) { vm.$el.__vue__ = null } // release circular reference (#6759) if (vm.$vnode) { vm.$vnode.parent = null } } }
renderMixin
渲染函数初始化也是向 Vue
的原型对象挂载一些方法。
installRenderHelpers(Vue.prototype)
向 vm 增加了模板的解析编译所需要的一些方法;
$nextTick
即我们在写代码时常用的this.$nextTick()
, 它返回一个Promise
实例p
, 我们可以在p
的then
函数中访问到更新到DOM
元素的数据, 也可以向this.nextTick
传递一个回调函数f
,f
也可以访问更新到DOM
元素的数据。
_render
方法生成虚拟节点。详见后边的代码。
vue/src/core/instance/render.js
export 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 { const vm: Component = this const { render, _parentVnode } = vm.$options if (_parentVnode) { vm.$scopedSlots = normalizeScopedSlots( _parentVnode.data.scopedSlots, vm.$slots, vm.$scopedSlots ) } // set parent vnode. this allows render functions to have access // to the data on the placeholder node. vm.$vnode = _parentVnode // render self let vnode try { // There's no need to maintain a stack because all render fns are called // separately from one another. Nested component's render fns are called // when parent component is patched. currentRenderingInstance = vm vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { handleError(e, vm, `render`) // return error render result, // or previous vnode to prevent render error causing blank component /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) { try { vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e) } catch (e) { handleError(e, vm, `renderError`) vnode = vm._vnode } } else { vnode = vm._vnode } } finally { currentRenderingInstance = null } // if the returned array contains only a single node, allow it if (Array.isArray(vnode) && vnode.length === 1) { vnode = vnode[0] } // return empty vnode in case the render function errored out if (!(vnode instanceof VNode)) { if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) { warn( 'Multiple root nodes returned from render function. Render function ' + 'should return a single root node.', vm ) } vnode = createEmptyVNode() } // set parent vnode.parent = _parentVnode return vnode } }
Vue
全局 API
Vue
中全局 API
一共有一下 12
个。全局 API 是通过构造函数 Vue
直接调用的, 有一些方法在实例上也做了同步, 可以通过实例对象去调用。 比如常用的 Vue.nextTick
, 可以通过 this.$nextTick
进行调用。下面就依次分析一下每个全局 API 的使用和实现思路吧。
src/core/global-api/index.js
Vue.extend