挖掘隐藏在源码中的Vue技巧!
前言
最近关于Vue
的技巧文章大热,我自己也写过一篇(vue开发中的"骚操作"),但这篇文章的技巧是能在Vue
的文档中找到蛛丝马迹的,而有些文章说的技巧在Vue
文档中根本找不到踪迹!这是为什么呢?
当我开始阅读源码的时候,我才发现,其实这些所谓的技巧就是对源码的理解而已。
下面我分享一下我的收获。
隐藏在源码中的技巧
我们知道,在使用Vue
时,要使用new
关键字进行调用,这就说明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)
在构造函数中,只做一件事——执行this._init(options)
。
而_init()
函数是在initMixin(Vue)
中定义的
export function initMixin (Vue: Class<Component>) { Vue.prototype._init = function (options?: Object) { // ... _init 方法的函数体,此处省略 } }
以此为主线,来看看在这过程中有什么好玩的技巧。
解构赋值子组件data的参数
按照官方文档,我们一般是这样写子组件data
选项的:
props: ['parentData'], data () { return { childData: this.parentData } }
但你知道吗,也是可以这么写:
data (vm) { return { childData: vm.parentData } } // 或者使用解构赋值 data ({ parentData }) { return { childData: parentData } }
通过解构赋值的方式将props
里的变量传给data
函数中,也就是说 data
函数的参数就是当前实例对象。
这是因为data
函数的执行是用call()
方法强制绑定了当前实例对象。这发生在data
合并的阶段,接下来去看看,说不定还有一些别的收获!
在_init()
函数中主要是执行一系列的初始化,其中options
选项的合并是初始化的基础。
vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm )
在Vue
实例上添加了$options
属性,在那些初始化方法中,无一例外的都使用到了实例的$options
属性,即vm.$options
。
其中合并data
就是在mergeOption
中进行的。
strats.data = function ( parentVal: any, childVal: any, vm?: Component ): ?Function { if (!vm) { if (childVal && typeof childVal !== 'function') { process.env.NODE_ENV !== 'production' && warn( 'The "data" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.', vm ) return parentVal } return mergeDataOrFn(parentVal, childVal) } return mergeDataOrFn(parentVal, childVal, vm) }
上面代码是data
选项的合并策略函数,首先通过判断是否存在vm
,来判断是否为父子组件,存在vm
则为父组件。不管怎么,最后都是返回mergeDataOrFn
的执行结果。区别在于处理父组件时,透传vm
。
接下来看看mergeDataOrFn
函数。
export function mergeDataOrFn ( parentVal: any, childVal: any, vm?: Component ): ?Function { if (!vm) { // in a Vue.extend merge, both should be functions if (!childVal) { return parentVal } if (!parentVal) { return childVal } // when parentVal & childVal are both present, // we need to return a function that returns the // merged result of both functions... no need to // check if parentVal is a function here because // it has to be a function to pass previous merges. return function mergedDataFn () { return mergeData( typeof childVal === 'function' ? childVal.call(this, this) : childVal, typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal ) } } else { return function mergedInstanceDataFn () { // instance merge const instanceData = typeof childVal === 'function' ? childVal.call(vm, vm) : childVal const defaultData = typeof parentVal === 'function' ? parentVal.call(vm, vm) : parentVal if (instanceData) { return mergeData(instanceData, defaultData) } else { return defaultData } } } }
函数整体是由if
判断分支语句块组成,对vm
进行判断,也使得mergeDataOrFn
也能区分父子组件。
return function mergedDataFn () { return mergeData( typeof childVal === 'function' ? childVal.call(this, this) : childVal, typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal ) }
来看这一段,当父子组件的data
选项同时存在,那么就返回mergedDataFn
函数。mergedDataFn
函数又返回mergeData
函数。
在mergeData函数中,执行父子组件的data
选项函数,注意这里的 childVal.call(this, this)
和 parentVal.call(this, this)
,关键在于 call(this, this)
,可以看到,第一个 this
指定了 data
函数的作用域,而第二个 this
就是传递给 data
函数的参数。这就是开头能用解构赋值的原理。
接着往下看!
注意因为函数已经返回了(return)
,所以mergedDataFn
函数还没有执行。
以上就是处理子组件的data
选项时所做的事,可以发现在处理子组件选项时返回的总是一个函数。
说完了处理子组件选项的情况,再看看处理非子组件选项的情况,也就是使用 new
操作符创建实例时的情况。
if (!vm) { ... } else { return function mergedInstanceDataFn () { // instance merge const instanceData = typeof childVal === 'function' ? childVal.call(vm, vm) : childVal const defaultData = typeof parentVal === 'function' ? parentVal.call(vm, vm) : parentVal if (instanceData) { return mergeData(instanceData, defaultData) } else { return defaultData } } }
如果走else
分支的话那么就直接返回mergedInstanceDataFn
函数。其中父子组件data
选项函数的执行也是用了call(vm, vm)
方法,强制绑定当前实例对象。
const instanceData = typeof childVal === 'function' ? childVal.call(vm, vm) : childVal const defaultData = typeof parentVal === 'function' ? parentVal.call(vm, vm) : parentVal
注意此时的mergedInstanceDataFn
函数同样还没有执行。所以mergeDataFn
函数永远返回一个函数。
为什么这么强调返回的是一个函数呢?也就是说strats.data
最终结果是一个函数?
这是因为,通过函数返回的数据对象,保证了每个组件实例都要有一个唯一的数据副本,避免了组件间数据互相影响。
这个mergeDataFn
就是后面的初始化阶段处理执行的。mergeDataFn
返回是mergeData(childVal, parentVal)
的执行结果才是真正合并父子组件的data
选项。也就是到了初始化阶段才是真正合并,这是因为props
和inject
这两个选项的初始化是先于data
选项的,这就保证了能够使用props
初始化data
中的数据。
这才能在data
选项中调用props
或者inject
的值!
生命周期钩子可以写成数组形式
生命周期钩子可以写成数组形式,不信你可以试试!
created: [ function () { console.log('first') }, function () { console.log('second') }, function () { console.log('third') } ]
这啥能这么写?来看看生命周期钩子的合并处理!
mergeHook是用于合并生命周期钩子。
/** * Hooks and props are merged as arrays. */ function mergeHook ( parentVal: ?Array<Function>, childVal: ?Function | ?Array<Function> ): ?Array<Function> { return childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal } LIFECYCLE_HOOKS.forEach(hook => { strats[hook] = mergeHook })
其实从注释中也能发现
Hooks and props are merged as arrays
.使用forEach
遍历LIFECYCLE_HOOKS
常量,说明LIFECYCLE_HOOKS
是一个数组。LIFECYCLE_HOOKS
来自于shared/constants.js
文件。
export const LIFECYCLE_HOOKS = [ 'beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'beforeDestroy', 'destroyed', 'activated', 'deactivated', 'errorCaptured' ]
所以那段forEach
语句,它的作用就是在strats
策略对象上添加用来合并各个生命周期钩子选项的函数。
return childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal
函数体由三组三目运算符组成,在经过 mergeHook
函数处理之后,组件选项的生命周期钩子函数被合并成一个数组。
在第一个三目运算符中,首先判断是否有 childVal
,即组件的选项是否写了生命周期钩子函数,如果没有则直接返回了 parentVal
,这里有一个预设的假定,就是如果有 parentVal
那么一定是个数组,如果没有 parentVal
那么 strats[hooks]
函数根本不会执行。以 created
生命周期钩子函数为例:
new Vue({ created: function () { console.log('created') } })
对于 strats.created
策略函数来讲,childVal
就是例子中的 created
选项,它是一个函数。parentVal
应该是 Vue.options.created
,但 Vue.options.created
是不存在的,所以最终经过 strats.created
函数的处理将返回一个数组:
options.created = [ function () { console.log('created') } ]
再看下面的例子:
const Parent = Vue.extend({ created: function () { console.log('parentVal') } }) const Child = new Parent({ created: function () { console.log('childVal') } })
其中 Child
是使用 new Parent
生成的,所以对于 Child
来讲,childVal
是:
created: function () { console.log('childVal') }
而 parentVal
已经不是 Vue.options.created
了,而是 Parent.options.created
,那么 Parent.options.created
是什么呢?它其实是通过 Vue.extend
函数内部的 mergeOptions
处理过的,所以它应该是这样的:
Parent.options.created = [ created: function () { console.log('parentVal') } ]
经过mergeHook函数处理,关键在那句:parentVal.concat(childVal)
,将 parentVal
和 childVal
合并成一个数组。所以最终结果如下:
[ created: function () { console.log('parentVal') }, created: function () { console.log('childVal') } ]
另外注意第三个三目运算符:
: Array.isArray(childVal) ? childVal : [childVal]
它判断了 childVal
是不是数组,这说明了生命周期钩子是可以写成数组的。这就是开头所说的原理!
生命周期钩子的事件侦听器
大家可能不知道什么叫做「生命周期钩子的事件侦听器」?,其实Vue
组件是可以这么写的:
<child @hook:created="childCreated" @hook:mounted="childMounted" />
在初始化中,使用callhook(vm, 'created')
函数执行created
生命周期函数,接下来瞧一瞧callhook()
的实现方法:
export function callHook (vm: Component, hook: string) { // #7573 disable dep collection when invoking lifecycle hooks pushTarget() const handlers = vm.$options[hook] if (handlers) { for (let i = 0, j = handlers.length; i < j; i++) { try { handlers[i].call(vm) } catch (e) { handleError(e, vm, `${hook} hook`) } } } if (vm._hasHookEvent) { vm.$emit('hook:' + hook) } popTarget() }
callhook()
函数接收两个参数:
- 实例对象;
- 要调用的生命周期钩子的名称;
首先缓存生命周期函数:
const handlers = vm.$options[hook]
如果执行 callHook(vm, created)
,那么就相当于:
const handlers = vm.$options.created
刚刚介绍过,对于生命周期钩子选项最终会被合并处理成一个数组,所以得到的handlers
就是一个生命周期钩子的数组。接着执行的是这段代码:
if (handlers) { for (let i = 0, j = handlers.length; i < j; i++) { try { handlers[i].call(vm) } catch (e) { handleError(e, vm, `${hook} hook`) } } }
最后注意到 callHook
函数的最后有这样一段代码:
if (vm._hasHookEvent) { vm.$emit('hook:' + hook) }
其中 vm._hasHookEvent
是在initEvents
函数中定义的,它的作用是判断是否存在「生命周期钩子的事件侦听器」,初始化值为 false
代表没有,当组件检测到存在生命周期钩子的事件侦听器时,会将vm._hasHookEvent
设置为 true
。
生命周期钩子的事件侦听器,就是开头说的:
<child @hook:created="childCreated" @hook:mounted="childMounted" />
使用hook:
加生命周期钩子名称的方式来监听组件相应的生命周期钩子。
总结
1、子组件data选项函数是有参数的,而且是当前的实例对象;
2、生命周期钩子是可以写成数组形式,按顺序执行;
3、可以使用生命周期钩子的事件侦听器来注册生命周期函数
「不过没在官方文档中写明的方法,不建议使用」。
作者: zhangwinwin
链接:挖掘隐藏在源码中的Vue技巧!
来源:github