前言

最近关于Vue的技巧文章大热,我自己也写过一篇(vue开发中的"骚操作"),但这篇文章的技巧是能在Vue的文档中找到蛛丝马迹的,而有些文章说的技巧在Vue文档中根本找不到踪迹!这是为什么呢?

当我开始阅读源码的时候,我才发现,其实这些所谓的技巧就是对源码的理解而已。

下面我分享一下我的收获。

隐藏在源码中的技巧

我们知道,在使用Vue时,要使用new关键字进行调用,这就说明Vue是一个构造函数。所以源头就是定义Vue构造函数的地方!

src/core/instance/index.js中找到了这个构造函数

  1. function Vue (options) {
  2. if (process.env.NODE_ENV !== 'production' &&
  3. !(this instanceof Vue)
  4. ) {
  5. warn('Vue is a constructor and should be called with the `new` keyword')
  6. }
  7. this._init(options)
  8. }
  9. initMixin(Vue)
  10. stateMixin(Vue)
  11. eventsMixin(Vue)
  12. lifecycleMixin(Vue)
  13. renderMixin(Vue)

在构造函数中,只做一件事——执行this._init(options)

_init()函数是在initMixin(Vue)中定义的

  1. export function initMixin (Vue: Class<Component>) {
  2. Vue.prototype._init = function (options?: Object) {
  3. // ... _init 方法的函数体,此处省略
  4. }
  5. }

以此为主线,来看看在这过程中有什么好玩的技巧。

解构赋值子组件data的参数

按照官方文档,我们一般是这样写子组件data选项的:

  1. props: ['parentData'],
  2. data () {
  3. return {
  4. childData: this.parentData
  5. }
  6. }

但你知道吗,也是可以这么写:

  1. data (vm) {
  2. return {
  3. childData: vm.parentData
  4. }
  5. }
  6. // 或者使用解构赋值
  7. data ({ parentData }) {
  8. return {
  9. childData: parentData
  10. }
  11. }

通过解构赋值的方式将props里的变量传给data函数中,也就是说 data 函数的参数就是当前实例对象。

这是因为data函数的执行是用call()方法强制绑定了当前实例对象。这发生在data合并的阶段,接下来去看看,说不定还有一些别的收获!

_init()函数中主要是执行一系列的初始化,其中options选项的合并是初始化的基础。

  1. vm.$options = mergeOptions(
  2. resolveConstructorOptions(vm.constructor),
  3. options || {},
  4. vm
  5. )

Vue实例上添加了$options属性,在那些初始化方法中,无一例外的都使用到了实例的$options属性,即vm.$options

其中合并data就是在mergeOption中进行的。

  1. strats.data = function (
  2. parentVal: any,
  3. childVal: any,
  4. vm?: Component
  5. ): ?Function {
  6. if (!vm) {
  7. if (childVal && typeof childVal !== 'function') {
  8. process.env.NODE_ENV !== 'production' && warn(
  9. 'The "data" option should be a function ' +
  10. 'that returns a per-instance value in component ' +
  11. 'definitions.',
  12. vm
  13. )
  14.  
  15. return parentVal
  16. }
  17. return mergeDataOrFn(parentVal, childVal)
  18. }
  19.  
  20. return mergeDataOrFn(parentVal, childVal, vm)
  21. }

上面代码是data选项的合并策略函数,首先通过判断是否存在vm,来判断是否为父子组件,存在vm则为父组件。不管怎么,最后都是返回mergeDataOrFn的执行结果。区别在于处理父组件时,透传vm

接下来看看mergeDataOrFn函数。

  1. export function mergeDataOrFn (
  2. parentVal: any,
  3. childVal: any,
  4. vm?: Component
  5. ): ?Function {
  6. if (!vm) {
  7. // in a Vue.extend merge, both should be functions
  8. if (!childVal) {
  9. return parentVal
  10. }
  11. if (!parentVal) {
  12. return childVal
  13. }
  14. // when parentVal & childVal are both present,
  15. // we need to return a function that returns the
  16. // merged result of both functions... no need to
  17. // check if parentVal is a function here because
  18. // it has to be a function to pass previous merges.
  19. return function mergedDataFn () {
  20. return mergeData(
  21. typeof childVal === 'function' ? childVal.call(this, this) : childVal,
  22. typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
  23. )
  24. }
  25. } else {
  26. return function mergedInstanceDataFn () {
  27. // instance merge
  28. const instanceData = typeof childVal === 'function'
  29. ? childVal.call(vm, vm)
  30. : childVal
  31. const defaultData = typeof parentVal === 'function'
  32. ? parentVal.call(vm, vm)
  33. : parentVal
  34. if (instanceData) {
  35. return mergeData(instanceData, defaultData)
  36. } else {
  37. return defaultData
  38. }
  39. }
  40. }
  41. }

函数整体是由if判断分支语句块组成,对vm进行判断,也使得mergeDataOrFn也能区分父子组件。

  1. return function mergedDataFn () {
  2. return mergeData(
  3. typeof childVal === 'function' ? childVal.call(this, this) : childVal,
  4. typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
  5. )
  6. }

来看这一段,当父子组件的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 操作符创建实例时的情况。

  1. if (!vm) {
  2. ...
  3. } else {
  4. return function mergedInstanceDataFn () {
  5. // instance merge
  6. const instanceData = typeof childVal === 'function'
  7. ? childVal.call(vm, vm)
  8. : childVal
  9. const defaultData = typeof parentVal === 'function'
  10. ? parentVal.call(vm, vm)
  11. : parentVal
  12. if (instanceData) {
  13. return mergeData(instanceData, defaultData)
  14. } else {
  15. return defaultData
  16. }
  17. }
  18. }

如果走else分支的话那么就直接返回mergedInstanceDataFn函数。其中父子组件data选项函数的执行也是用了call(vm, vm)方法,强制绑定当前实例对象。

  1. const instanceData = typeof childVal === 'function'
  2. ? childVal.call(vm, vm)
  3. : childVal
  4. const defaultData = typeof parentVal === 'function'
  5. ? parentVal.call(vm, vm)
  6. : parentVal

注意此时的mergedInstanceDataFn函数同样还没有执行。所以mergeDataFn函数永远返回一个函数。

为什么这么强调返回的是一个函数呢?也就是说strats.data最终结果是一个函数?

这是因为,通过函数返回的数据对象,保证了每个组件实例都要有一个唯一的数据副本,避免了组件间数据互相影响。

这个mergeDataFn就是后面的初始化阶段处理执行的。mergeDataFn返回是mergeData(childVal, parentVal)的执行结果才是真正合并父子组件的data选项。也就是到了初始化阶段才是真正合并,这是因为propsinject这两个选项的初始化是先于data选项的,这就保证了能够使用props初始化data中的数据。

这才能在data选项中调用props或者inject的值!

生命周期钩子可以写成数组形式

生命周期钩子可以写成数组形式,不信你可以试试!

  1. created: [
  2. function () {
  3. console.log('first')
  4. },
  5. function () {
  6. console.log('second')
  7. },
  8. function () {
  9. console.log('third')
  10. }
  11. ]

这啥能这么写?来看看生命周期钩子的合并处理!

mergeHook是用于合并生命周期钩子。

  1. /**
  2. * Hooks and props are merged as arrays.
  3. */
  4. function mergeHook (
  5. parentVal: ?Array<Function>,
  6. childVal: ?Function | ?Array<Function>
  7. ): ?Array<Function> {
  8. return childVal
  9. ? parentVal
  10. ? parentVal.concat(childVal)
  11. : Array.isArray(childVal)
  12. ? childVal
  13. : [childVal]
  14. : parentVal
  15. }
  16.  
  17. LIFECYCLE_HOOKS.forEach(hook => {
  18. strats[hook] = mergeHook
  19. })

其实从注释中也能发现Hooks and props are merged as arrays.

使用forEach遍历LIFECYCLE_HOOKS常量,说明LIFECYCLE_HOOKS是一个数组。LIFECYCLE_HOOKS来自于shared/constants.js文件。

  1. export const LIFECYCLE_HOOKS = [
  2. 'beforeCreate',
  3. 'created',
  4. 'beforeMount',
  5. 'mounted',
  6. 'beforeUpdate',
  7. 'updated',
  8. 'beforeDestroy',
  9. 'destroyed',
  10. 'activated',
  11. 'deactivated',
  12. 'errorCaptured'
  13. ]

所以那段forEach语句,它的作用就是在strats策略对象上添加用来合并各个生命周期钩子选项的函数。

  1. return childVal
  2. ? parentVal
  3. ? parentVal.concat(childVal)
  4. : Array.isArray(childVal)
  5. ? childVal
  6. : [childVal]
  7. : parentVal

函数体由三组三目运算符组成,在经过 mergeHook 函数处理之后,组件选项的生命周期钩子函数被合并成一个数组。

在第一个三目运算符中,首先判断是否有 childVal,即组件的选项是否写了生命周期钩子函数,如果没有则直接返回了 parentVal,这里有一个预设的假定,就是如果有 parentVal 那么一定是个数组,如果没有 parentVal 那么 strats[hooks] 函数根本不会执行。以 created 生命周期钩子函数为例:

  1. new Vue({
  2. created: function () {
  3. console.log('created')
  4. }
  5. })

对于 strats.created 策略函数来讲,childVal 就是例子中的 created 选项,它是一个函数。parentVal 应该是 Vue.options.created,但 Vue.options.created 是不存在的,所以最终经过 strats.created 函数的处理将返回一个数组:

  1. options.created = [
  2. function () {
  3. console.log('created')
  4. }
  5. ]

再看下面的例子:

  1. const Parent = Vue.extend({
  2. created: function () {
  3. console.log('parentVal')
  4. }
  5. })
  6.  
  7. const Child = new Parent({
  8. created: function () {
  9. console.log('childVal')
  10. }
  11. })

其中 Child 是使用 new Parent 生成的,所以对于 Child 来讲,childVal 是:

  1. created: function () {
  2. console.log('childVal')
  3. }

而 parentVal 已经不是 Vue.options.created 了,而是 Parent.options.created,那么 Parent.options.created 是什么呢?它其实是通过 Vue.extend 函数内部的 mergeOptions 处理过的,所以它应该是这样的:

  1. Parent.options.created = [
  2. created: function () {
  3. console.log('parentVal')
  4. }
  5. ]

经过mergeHook函数处理,关键在那句:parentVal.concat(childVal),将 parentVal 和 childVal 合并成一个数组。所以最终结果如下:

  1. [
  2. created: function () {
  3. console.log('parentVal')
  4. },
  5. created: function () {
  6. console.log('childVal')
  7. }
  8. ]

另外注意第三个三目运算符:

  1. : Array.isArray(childVal)
  2. ? childVal
  3. : [childVal]

它判断了 childVal 是不是数组,这说明了生命周期钩子是可以写成数组的。这就是开头所说的原理!

生命周期钩子的事件侦听器

大家可能不知道什么叫做「生命周期钩子的事件侦听器」?,其实Vue组件是可以这么写的:

  1. <child
  2. @hook:created="childCreated"
  3. @hook:mounted="childMounted"
  4. />

在初始化中,使用callhook(vm, 'created')函数执行created生命周期函数,接下来瞧一瞧callhook()的实现方法:

  1. export function callHook (vm: Component, hook: string) {
  2. // #7573 disable dep collection when invoking lifecycle hooks
  3. pushTarget()
  4. const handlers = vm.$options[hook]
  5. if (handlers) {
  6. for (let i = 0, j = handlers.length; i < j; i++) {
  7. try {
  8. handlers[i].call(vm)
  9. } catch (e) {
  10. handleError(e, vm, `${hook} hook`)
  11. }
  12. }
  13. }
  14. if (vm._hasHookEvent) {
  15. vm.$emit('hook:' + hook)
  16. }
  17. popTarget()
  18. }

callhook()函数接收两个参数:

  • 实例对象;
  • 要调用的生命周期钩子的名称;

首先缓存生命周期函数:

  1. const handlers = vm.$options[hook]

如果执行 callHook(vm, created),那么就相当于:

  1. const handlers = vm.$options.created

刚刚介绍过,对于生命周期钩子选项最终会被合并处理成一个数组,所以得到的handlers就是一个生命周期钩子的数组。接着执行的是这段代码:

  1. if (handlers) {
  2. for (let i = 0, j = handlers.length; i < j; i++) {
  3. try {
  4. handlers[i].call(vm)
  5. } catch (e) {
  6. handleError(e, vm, `${hook} hook`)
  7. }
  8. }
  9. }

最后注意到 callHook 函数的最后有这样一段代码:

  1. if (vm._hasHookEvent) {
  2. vm.$emit('hook:' + hook)
  3. }

其中 vm._hasHookEvent 是在initEvents函数中定义的,它的作用是判断是否存在「生命周期钩子的事件侦听器」,初始化值为 false 代表没有,当组件检测到存在生命周期钩子的事件侦听器时,会将vm._hasHookEvent设置为 true

生命周期钩子的事件侦听器,就是开头说的:

  1. <child
  2. @hook:created="childCreated"
  3. @hook:mounted="childMounted"
  4. />

使用hook:加生命周期钩子名称的方式来监听组件相应的生命周期钩子。

总结

1、子组件data选项函数是有参数的,而且是当前的实例对象;

2、生命周期钩子是可以写成数组形式,按顺序执行;

3、可以使用生命周期钩子的事件侦听器来注册生命周期函数

「不过没在官方文档中写明的方法,不建议使用」。

作者: zhangwinwin
链接:挖掘隐藏在源码中的Vue技巧!
来源:github

挖掘隐藏在源码中的Vue技巧!的更多相关文章

  1. 关于android源码中的APP编译时引用隐藏的API出现的问题

    今天在编译android源码中的计算器APP时发现,竟然无法使用系统隐藏的API,比如android.os.ServiceManager中的API,引用这个类时提示错误,记忆中在android源码中的 ...

  2. jQuery源码中的赌博网站

    前言 jQuery源码中有赌博网站? 起因是公司发的一份自查文件,某银行在日常安全运营过程中发现在部分jQuery源码中存在赌博和黄色网站链接. 链接分为好几个: www.cactussoft.cn ...

  3. 从express源码中探析其路由机制

    引言 在web开发中,一个简化的处理流程就是:客户端发起请求,然后服务端进行处理,最后返回相关数据.不管对于哪种语言哪种框架,除去细节的处理,简化后的模型都是一样的.客户端要发起请求,首先需要一个标识 ...

  4. MMS源码中异步处理简析

    1,信息数据的查询,删除使用AsycnQueryHandler处理 AsycnQueryHandler继承了Handler public abstract class AsyncQueryHandle ...

  5. js-刮刮卡效果,由jquery-eraser源码改的vue组件

    vue-eraser 一款用于vue刮刮卡的组件 github地址: vue-eraser npm地址: vue-eraser 在网上有看到过几个版本的组件,都有点问题 1.拉快了,就会断,连不起来( ...

  6. Vue源码学习1——Vue构造函数

    Vue源码学习1--Vue构造函数 这是我第一次正式阅读大型框架源码,刚开始的时候完全不知道该如何入手.Vue源码clone下来之后这么多文件夹,Vue的这么多方法和概念都在哪,完全没有头绪.现在也只 ...

  7. Vue源码学习二 ———— Vue原型对象包装

    Vue原型对象的包装 在Vue官网直接通过 script 标签导入的 Vue包是 umd模块的形式.在使用前都通过 new Vue({}).记录一下 Vue构造函数的包装. 在 src/core/in ...

  8. Vue源码分析(二) : Vue实例挂载

    Vue源码分析(二) : Vue实例挂载 author: @TiffanysBear 实例挂载主要是 $mount 方法的实现,在 src/platforms/web/entry-runtime-wi ...

  9. Android 网络框架之Retrofit2使用详解及从源码中解析原理

    就目前来说Retrofit2使用的已相当的广泛,那么我们先来了解下两个问题: 1 . 什么是Retrofit? Retrofit是针对于Android/Java的.基于okHttp的.一种轻量级且安全 ...

随机推荐

  1. Code-Review-Maven编译(第三方jar包引用)

    Code-Review-SpringBoot-Maven编译(第三方jar包引用) 在使用maven编译项目时,有时候咱们可能会使用一些第三方的jar包依赖库,比如第三方支付类的接入,大多出于安全考虑 ...

  2. JAVA读取EXCEL_自动生成实体类

    代码实现PropertyAnno.java import java.lang.annotation.ElementType; import java.lang.annotation.Retention ...

  3. [leetcode712] Minimum ASCII Delete Sum for Two Strings

    public int minimumDeleteSum(String s1, String s2) { /* 标准的动态规划题目,难点在于想出将两个字符串删除到相同的过程 这里从两个字符串的开头字符考 ...

  4. niceyoo的2020年终总结-2021年Flag

    碎碎念,向本命年说再见! 又到了一年一度立 Flag 的时间了,怎么样,去年的 Flag 大家实现的怎么样?还有信心立下 2021 年的 Flag 吗~ 今年我算比较背的,年初的一次小意外,直接在床上 ...

  5. 详解CSS布局

    CSS页面布局允许我们拾取网页中的元素,并且控制它们相对正常布局流.周边元素.父容器或者主视口/窗口的位置.主要对文档流的改变进行布局.假设你已经掌握了CSS的选择器.属性和值,并且可能对布局有一定了 ...

  6. Oracle RedoLog-基本概念和组成

    Oracle 数据库恢复操作最关键的依据就是 redo log,它记录了对数据库所有的更改操作.在研究如何提取 redolog 中 DML 操作的过程可谓一波三折,因为介绍 redolog 结构细节的 ...

  7. 【分享】wdcp服务器管理系统常用维护工具

    wdcp (WDlinux Control Panel) 是一套用PHP开发的Linux服务器管理系统,类似国外流行的cpanel,旨在易于使用和管理Linux服务器,可以在线通过网页管理服务器和虚拟 ...

  8. IntelliJ IDEA实用插件

    Free MyBatis plugin 插件效果 Save Actions 插件设置 勾选后Ctrl + S就会执行格式化操作,等价于格式化快捷键Alt + Ctrl + L

  9. try catch finally语句块中存在return语句时的执行情况剖析

    2种场景 (1) try中有return,finally中没有return(注意会改变返回值的情形);(2) try中有return,finally中有return; 场景代码分析(idea亲测) 场 ...

  10. js 必须为字母或下划线, 一旦创建不能修改

    <div class="form-group"> <label class="col-lg-2 control-label" for=&quo ...