挖掘隐藏在源码中的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
挖掘隐藏在源码中的Vue技巧!的更多相关文章
- 关于android源码中的APP编译时引用隐藏的API出现的问题
今天在编译android源码中的计算器APP时发现,竟然无法使用系统隐藏的API,比如android.os.ServiceManager中的API,引用这个类时提示错误,记忆中在android源码中的 ...
- jQuery源码中的赌博网站
前言 jQuery源码中有赌博网站? 起因是公司发的一份自查文件,某银行在日常安全运营过程中发现在部分jQuery源码中存在赌博和黄色网站链接. 链接分为好几个: www.cactussoft.cn ...
- 从express源码中探析其路由机制
引言 在web开发中,一个简化的处理流程就是:客户端发起请求,然后服务端进行处理,最后返回相关数据.不管对于哪种语言哪种框架,除去细节的处理,简化后的模型都是一样的.客户端要发起请求,首先需要一个标识 ...
- MMS源码中异步处理简析
1,信息数据的查询,删除使用AsycnQueryHandler处理 AsycnQueryHandler继承了Handler public abstract class AsyncQueryHandle ...
- js-刮刮卡效果,由jquery-eraser源码改的vue组件
vue-eraser 一款用于vue刮刮卡的组件 github地址: vue-eraser npm地址: vue-eraser 在网上有看到过几个版本的组件,都有点问题 1.拉快了,就会断,连不起来( ...
- Vue源码学习1——Vue构造函数
Vue源码学习1--Vue构造函数 这是我第一次正式阅读大型框架源码,刚开始的时候完全不知道该如何入手.Vue源码clone下来之后这么多文件夹,Vue的这么多方法和概念都在哪,完全没有头绪.现在也只 ...
- Vue源码学习二 ———— Vue原型对象包装
Vue原型对象的包装 在Vue官网直接通过 script 标签导入的 Vue包是 umd模块的形式.在使用前都通过 new Vue({}).记录一下 Vue构造函数的包装. 在 src/core/in ...
- Vue源码分析(二) : Vue实例挂载
Vue源码分析(二) : Vue实例挂载 author: @TiffanysBear 实例挂载主要是 $mount 方法的实现,在 src/platforms/web/entry-runtime-wi ...
- Android 网络框架之Retrofit2使用详解及从源码中解析原理
就目前来说Retrofit2使用的已相当的广泛,那么我们先来了解下两个问题: 1 . 什么是Retrofit? Retrofit是针对于Android/Java的.基于okHttp的.一种轻量级且安全 ...
随机推荐
- vue3系列:vue3.0自定义弹框组件V3Popup|vue3.x手机端弹框组件
基于Vue3.0开发的轻量级手机端弹框组件V3Popup. 之前有分享一个vue2.x移动端弹框组件,今天给大家带来的是Vue3实现自定义弹框组件. V3Popup 基于vue3.x实现的移动端弹出框 ...
- VNC使用及其常见问题解决方法
博主之前在博文(https://www.cnblogs.com/kangbazi666/p/14153604.html)中已经介绍了多人VNC的配置方法,下面将简单介绍其使用方法及常见问题的解决方法. ...
- 知乎上看到的一篇讲解Epoll的文章,较形象生动
作者:蓝形参链接:https://www.zhihu.com/question/20122137/answer/14049112来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明 ...
- 数据库事务特性ACID
事务 事务(Transaction),一般是指要做的或所做的事情.在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit).在计算机术语中,事务通常就是指数据库事务. 概念 一 ...
- linq to entity不识别方法"System.String ToString()"
将班级id以字符串形式输入如:"1111,1112,1113".数据库里的id为int型,在数据路里找到匹配的相应班级转换成列表.在这里爆出问题:不识别方法"System ...
- go语言环境搭建以及配置VSCode
Go语言学习笔记(环境安装)-day01 Go语言运行环境安装 下载Go安装包 安装包地址 安装Go语言运行环境 直接在下载好的目录双击运行*.msi的可执行文件,下一步进行安装,安装的目录最好是 ...
- NOIP初赛篇——02计算机系统的基本结构
引言 计算机系统由硬件和软件两部分组成,硬件系统是计算机的"躯干",是物质基础.而软件系统则是建立在这个"躯干"上的"灵魂". 计算机硬件 ...
- 2021年了,`IEnumerator`、`IEnumerable`还傻傻分不清楚?
IEnumerator.IEnumerable这两个接口单词相近.含义相关,傻傻分不清楚. 入行多年,一直没有系统性梳理这对李逵李鬼. 最近本人在怼着why神的<其实吧,LRU也就那么回事> ...
- 最新最简洁Spring Cloud Oauth2.0 Jwt 的Security方式
因为Spring Cloud 2020.0.0和Spring Boot2.4.1版本升级比较大,所以把我接入过程中的一些需要注意的地方告诉大家 我使用的版本是Spring boot 2.4.1+Spr ...
- LeetCode542 01矩阵
给定一个由 0 和 1 组成的矩阵,找出每个元素到最近的 0 的距离. 两个相邻元素间的距离为 1 . 示例 1: 输入: 0 0 0 0 1 0 0 0 0 输出: 0 0 0 0 1 0 0 0 ...