我们学习Vue都是从下面这个例子开始的
new Vue({
render: h => h(App),
}).$mount('#app')
事实上,所有的Vue项目的组成组件都是一个Vue的实例,最后由根部的Vue实例去挂载到DOM上,当然这个"挂载"的操作可以针对不同的平台而有不同的行为,比如挂载到移动设备上就成了weex,挂载到小程序上就成了mpvue。所以首先我们要知道Vue实例里面含有哪些东西。
实际上,这个$mount就等同于React里的ReactDOM.render。
Vue构造函数
我是全局搜索关键词"function Vue"来定位到Vue类的定义的,位于/src/core/instance/index.js里。
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
代码很简单,就是用function的方式定义了Vue类,之后只要new Vue一下就可以生成Vue的实例了。在构造函数里我们看到调用了_init( )方法,但是上下文中并没有看到_init( )方法的定义,往下看,有一堆的mixin方法,这些方法其实就是在Vue的prototype上增加各种方法,_init( )也就是在这些mixin调用过后添加的。
列举一下Vue里约定俗成前缀
_ 表示私有,这个和一般的约定一致。
$ 表示实例上的属性或方法,比如$mount。
另外还有一些方法是定义在全局的,也就是Vue构造函数上的,比如Vue.component。
initMixin
刚才看到的_init( )方法就是在这里定义出来的,抽掉一些不重要的代码后
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
vm._uid = uid++
vm._isVue = true
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// expose real self
vm._self = vm
initLifecycle(vm)
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')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
这一段执行完成后,相当于生命周期的
大致干了这些事情
对于每一个生成的Vue实例在内部都用_uid来跟踪
合并option对象
initLifecycle方法里定义了许多和生命周期有关的变量,有些是内部的,有些是暴露在实例上的
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
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
}
initEvents针对配置里传进来事件,做了一些操作。
initRender里初始化了一些跟虚拟DOM渲染有关的属性和方法,比如后面会登场的赫赫有名的_c方法和实例上的$createElement方法,而把方法连接到实例上其实就是为了在渲染的时候能获得实例的上下文
export function initRender (vm: Component) {
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 // the placeholder node in parent tree
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)
}
接着callHook方法第一次登场,从名字就可以看出来callHook就是用来调用钩子函数。
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
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)
}
popTarget()
}
它还是通过$emit来调用的,所以在后面的章节要睁大眼睛看一下Vue里事件的实现。
下面的Injection和Provider有点陌生,查了下官网上说
provide
和inject
主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。
而夹在中间的initState( )又是重中之重,初始化了Vue的几大响应式组成部分,计划下一篇文章就是探究这方面的原理的。
最后如果找到配置项里有el,就执行挂载的方法。当然也可以如之前的代码所示,调用Vue实例上的$mount来执行挂载,对应到生命周期图里的这个部分。
stateMixin
这里面定义了一个响应式里很重要的方法——$watch,也会放到后文来详述。
eventsMixin
实例上事件的四大方法
- vm.$on
- vm.$once
- vm.$off
- vm.$emit
先看$on
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)
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
其实就是把传入的方法回调,放在Vue的实例上,而事件的名字就作为键,对应的值是一个数组,表明同一个事件可以调用多个回调方法,传入的event也能是一个数组,不过这种不同的事件上调用同一个回调的事情现实中应该不多吧。
可以做一下实验
有$on就必有$off
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
}
原理也很简单,就是寻找——删除。
而$once本质就是在这个事件回调上做了替换,先调用一下这个传入的回调方法,执行完成后调用一下$off把它删除,达到只调用一次的目的。
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
}
$emit的话就是寻找——调用。
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
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
}
注意一下这几个方法调用返回的都是Vue实例本身,说明实际中可以写成链式调用的形式。
lifecycleMixin
定义了$forceUpdate,强行在Vue实例上的所有Watcher对象都调用一把更新,后面讲响应式原理的时候会谈到什么是Watcher对象。
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}
又定义了$destroy
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
}
}
对应了生命周期图上的这个部分
清理工作还是挺多的
- 和父组件的引用关系中拆分出来
- 清理所有Watcher对象
- 清理所有Observer对象
- 移除所有事件
- 清理挂载的DOM元素
renderMixin
主要是定义了跟虚拟DOM渲染有关的属性($vnode)和私有方法(_render),在后面讲到虚拟DOM的时候再来讲。
下一篇文章要讲Vue响应式原理了,想想都激动。