Vue详细介绍及使用

一、Vue组件概念

1、基本的概念

    为什么需要组件化?在实际开发中,我们经常也会封装很多公共方法,达到复用的目的,也便于维护。对于前端也是一样,那么什么是组件?

    官方定义:组件(Component)是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素, Vue.js 的编译器为它添加特殊功能。在有些情况下,组件也可以是原生 HTML 元素的形式,以 is 特性扩展。

    组件机制的设计,可以让开发者把一个复杂的应用分割成一个个功能独立组件,降低开发的难度的同时,也提供了极好的复用性和可维护性。组件的出现,就是为了拆分Vue实例的代码量的,能够让我们以不同的组件,来划分不同的功能模块,将来我们需要什么样的功能,就可以去调用对应的组件即可。组件化和模块化的视角不同:

  • 模块化: 是从代码逻辑的角度进行划分的;方便代码分层开发,保证每个功能模块的职能单一;
  • 组件化: 是从UI界面的角度进行划分的;前端的组件化,方便UI组件的重用。

    可复用组件,高内聚、低耦合。Vue中提供了少量的内置组件(keep-alive、component、transition、transition-group等),但可以自定义组件。Vue API中,提供了Vue.extend和Vue.component两个全局方法创建/注册组件,还有一个实例选项components,用来注册局部组件。  

2、学习前准备

2.1、导入项目

    准备好开发工具Visual Studio Code,导入之前生成的项目案例。

项目目录:

目录/文件             说明
build                项目构建(webpack)相关代码
config               配置目录,包括端口号等。我们初学可以使用默认的。
node_modules         npm 加载的项目依赖模块(根据package.json安装时候生成的的依赖安装包)

src                  这里是我们要开发的目录,基本上要做的事情都在这个目录里。里面包含了几个目录及文件:
   assets            脚手架自动会放入一个图片在里面作为初始页面的logo。平常我们使用的时候会在里面建立js,css,img,fonts等文件夹,作为静态资源调用    
   components        用来存放组件,合理地使用组件可以高效地实现复用等功能,从而更好地开发项目。
   router            路由相关的内容。该文件夹下有一个叫index.js文件,用于实现页面的路由跳转
   main.js           入口文件,主要作用是初始化vue实例并使用需要的插件,小型项目省略router时可放在该处
   App.vue           主组件,可通过使用<router-view/>开放入口让其他的页面组件得以显示。也可以直接将组件写这里,而不使用 components 目录。

static               静态资源目录,如图片、字体等。
    .gitkeep         git配置。
.babelrc             es6解析的一个配置
.editorconfig        编辑器的配置文件
.eslintignore        忽略eslint语法规范检查配置文件
.eslintrc.js         eslint(代码格式化检查工具)的配置文件,开启以后要严格遵照它规定的格式进行开发
.gitignore           忽略git提交的一个文件,配置之后提交时将不会加载忽略的文件
.postcssrc.js        文件是postcss-loader包的一个配置
index.html           首页入口文件,经过编译之后的代码将插入到这来。
package.lock.json    锁定安装时的包的版本号,并且需要上传到git,以保证其他人在npm install时大家的依赖能保证一致
package.json         项目配置文件。需要哪些npm包来参与到项目中来,npm install命令根据这个配置文件增减来管理本地的安装包。
README.md            项目的说明文档,markdown 格式

2.2、运行项目

    运行命令npm run dev启动项目(npm run 其实执行了package.json中的script脚本)。更多说明参考:https://blog.csdn.net/xiaoxianer321/article/details/114009647

源码:

webpack-将各项前端代码/资源打包编译成.js、.css、.png等浏览器可以加载的资源。

2.3、Visual Studio Code 插件使用

    在使用过程中经常出现编写不符合eslint规范,空格,缩进,各种括号,导致启动报错,真是头疼。

2.3.1、方式一:安装插件ESLint

然后File(文件)->Preferences(首选项)->settings(设置)

编辑settings.json文件将文件替换为下面的选项,就可以保存时自动格式化。

{
  // vscode默认启用了根据文件类型自动设置tabsize的选项
  "editor.detectIndentation": false,
  // 重新设定tabsize
  "editor.tabSize": 2,
  // #每次保存的时候自动格式化 
  "editor.formatOnSave": true,
  //  #让prettier使用eslint的代码格式进行校验 
  "prettier.eslintIntegration": true,
  //  #去掉代码结尾的分号 
  "prettier.semi": false,
  //  #使用单引号替代双引号 
  "prettier.singleQuote": true,
  //  #让函数(名)和后面的括号之间加个空格
  "javascript.format.insertSpaceBeforeFunctionParenthesis": true,
  // #这个按用户自身习惯选择 
  "vetur.format.defaultFormatter.html": "js-beautify-html",
  // #让vue中的js按编辑器自带的ts格式进行格式化 
  "vetur.format.defaultFormatter.js": "vscode-typescript",
  "vetur.format.defaultFormatterOptions": {
    "js-beautify-html": {
      "wrap_attributes": "force-aligned"
      // #vue组件中html代码格式化样式
    }
  },
  // 格式化stylus, 需安装Manta's Stylus Supremacy插件
  "stylusSupremacy.insertColons": false, // 是否插入冒号
  "stylusSupremacy.insertSemicolons": false, // 是否插入分好
  "stylusSupremacy.insertBraces": false, // 是否插入大括号
  "stylusSupremacy.insertNewLineAroundImports": false, // import之后是否换行
  "stylusSupremacy.insertNewLineAroundBlocks": false, // 两个选择器中是否换行
}

2.3.2、方式二:关闭校验规则

    找到build\webpack.base.conf.js这个文件:

    它是index.js中的一个配置:useEslint并将其值设置为: false,即关闭eslint规则校验。(并不推荐这么做)

二、组件的创建

    关于组件的创建源码分析,可以参考这篇文章:https://blog.csdn.net/xiaoxianer321/article/details/114340405

1、创建组件的构造器

    前面介绍过Vue.extend可以帮助我们对 Vue 实例进行扩展,扩展之后,就可以用此扩展对象创建新的 Vue 实例,Vue.extend是为了创建可复用的组件模板,更主要的是服务于Vue.component组件注册。

案例1:

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=devie-width,initial-scale=1.0">
  <title>vue-test-big</title>
</head>

<body>
  <div id="app"></div>
  <div id="app1"></div>
</body>

</html>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
  var myVue = Vue.extend({
    template: '<div>{{ name }} - {{ age }} - {{ sex }}</div>',
    data: function () {
      return {
        name: '小明',
        age: '19',
        sex: '男'
      };
    }
  });
  var extends1 = new myVue().$mount('#app');
  var extends2 = new myVue().$mount('#app1');

</script>

案例效果:

    new  Vue(),  尽管在Vue官方文档上,在相当多的例子中使用到了创建Vue实例这个操作,实际上它的使用可能并没有你想象的那么频繁,在很多时候,它可能就只在挂载根实例的时候使用到。后面我们接触更多的是组件和路由的使用。

    在Vue 中定义一个组件模板,可以有很多种方式,下面只列出常见的几种:

1)字符串
// 注册
Vue.component('my-component', {
  template: '<div>A custom component!</div>'
})

2)模板字面量
Vue.component('my-component', {
  template: `<div>A custom component!</div>`
})

3)x-template
Vue.component('my-component', {
  template: '#checkbox-template'
})

<script type="text/x-template" id="checkbox-template"> 
   <div>A custom component!</div> 
</script>

4)render 函数
Vue.component('my-component', {
  render(createElement){
      return createElement('div','A custom component!')
  }
})

5)内联模板(inline-template 属性)
<component v-bind:is="currentTabComponent"
               inline-template>
      <div>我是内联模板</div>
    </component>

6)单文件组件(.vue 文件)
<template>
</template>
<script>
</script>
<style>
</style>

1.1、单文件组件

    一般浏览器不能识别.vue的文件,所以需要vue-loader来加载.vue文件,所以需要基于webpack来开发。

    webpack:前端资源模块化加载器和打包工具,他能把各种资源当作模块进行加载,实际上就是通过不同的loader将这些资源加载打包然后输出打包文件。webpack是基于commonJs语法的。

    vue-loader:会解析文件,提取每个语言块,如有必要会通过其它 loader 处理,最后将他们组装成一个 ES Module,它的默认导出是一个 Vue.js 组件选项的对象。支持使用非默认语言,比如 CSS 预处理器,预编译的 HTML 模版语言,通过设置语言块的 lang 属性。

2、组件三要素

    传统前端中一个网页应用是由很多html文件组成,每个html文件又分为三部分:

   1)<html>用于展示视图;

   2)<script type="text/javascript">用于和用户交互

   3)<style>用于控制视图的样式。

    在vue中,每个单文件组件(.vue文件)也可分为三部分(例如:在App.vue中我们可以看到有:<template>、<script>、<style>):

   1)<template>用于展示视图:<template>模板反映了数据和最终展现给用户的DOM之间的映射关系(注:<template></template>一般只有一个<div>根元素,vue初始化之后最终会得到一个vdom树,而树结构必须要求只有一个root节点

   2)<script>用于和用户交互

   3)<style>用于控制视图的样式

1.1、< template >

    html中的template标签中(html5新增)的内容在页面中不会显示。但是在后台查看页面DOM结构存在template标签。

    在Vue中template模板是用于编写视图(DOM)的地方,<template></template>一般只有一个根元素,通常根元素都是div。如果有多个根元素需要使用v-if、v-else、v-else-if设置成只显示其中一个根元素;(template标签不支持v-show指令,v-show="false"对template标签来说不起作用。)

<template>
    <!--一般情况下只有一个根节点,且必须有一个根节点-->
	<div>
		<!-- view -->
	</div>
</template>

1.2、< script >

    vue中的script脚本中包含两部分,导出和导入。

    历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。而ES6 在语言标准的层面上,它实现了模块功能,设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

    关于exportimport命令可以参考这篇文章:https://blog.csdn.net/xiaoxianer321/article/details/113485872

1.3. < style>

    style里和传统的css差不多,不同的是支持了更多的语法,比如scss、less、stylus等,默认是css。

    CSS(层叠样式表):是一门非程序式语言,因为CSS没有变量、函数、SCOPE(作用域),需要书写大量看似没有逻辑的代码,不方便维护及扩展,不利于复用,尤其对于非前端开发工程师来讲,往往会因为缺少 CSS 编写经验而很难写出组织良好且易于维护的 CSS 代码。为了方便前端开发的工作量,出现了sass(sass技术的文件的后缀名有两种形式:.sass和.scss)和less,sass和less由于使用了类似JavaScript的方式去书写,所以必须要经过编译生成css,而html引用只能引用编译之后的css文件,虽然过程多了一层,但是毕竟sass/less在书写的时候就方便很多。

<!--样式默认:lang="css"-->
<!--添加“scoped”-作用域,属性将CSS仅限于此组件-->
<style scoped>
</style>

3、注册组件及使用

    在Vue中使用组件,可能需要用到注册组件,它提供了全局注册局部注册两种方式。

3.1、全局注册组件Vue.component

    在全局api中有个Vue.component方法,提供了三种创建全局组件的方法,还有一种私有组件。

   Vue.component( id, [definition] )

  • 参数

    • {string} id
    • {Function | Object} [definition]
  • 用法:注册或获取全局组件。注册还会自动使用给定的 id 设置组件的名称

// 注册组件,传入一个扩展过的构造器
Vue.component('my-component', Vue.extend({ /* ... */ }))

// 注册组件,传入一个选项对象 (自动调用 Vue.extend)
Vue.component('my-component', { /* ... */ })

// 获取注册的组件 (始终返回构造器)
var MyComponent = Vue.component('my-component')

案例:

    首先我们在main.js中加入我们要注册的组件,然后在App.vue中直接。

main.js 

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'

let head = Vue.extend({
  template: '<p>This is a head component!</p>'
})
Vue.component('tab-home', head)

Vue.component('tab-new', {
  template: '<div>This is a News component</div>'
})

Vue.component('tab-foot', {
  template: '<div>This is a Foot component</div>'
})
/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})

App.vue 

案例效果:

3.2、局部注册组件

    通过某个Vue实例/组件的实例选项 components 注册,使用该选项注册的组件被称为局部注册。继续使用之前的案例,我们在加一个广告组件。我们将HelloWorld.vue中的内容重写一下,然后在APP.vue中使用。

HelloWorld.vue 

<template id="gg">
  <p>This is a advertisement component!</p>
</template>

<script>
export default {
  name: 'HelloWorld',
  components: {
    'my-component': {
      template: '#gg'
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

App.vue

<template>
  <div id="app">
    <tab-home></tab-home>

    <tab-new></tab-new>

    <router-view />

    <tab-foot></tab-foot>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

案例效果:

 

三、组件间的通信

    一个简单的网页,你可能会有页头、侧边栏、内容区等组件,每个组件又包含了其它的像导航链接、博文之类的组件,形成了类似的组件树结构,引用官网一张图:

    组件与组件之间的嵌套使用避免不了数据之间的传递。那么Vue中组件的数据是如何传递的呢?组件间数据传递不同于Vue全局的数据传递,组件实例的数据之间是孤立的,不能在子组件的模板内直接引用父组件的数据。如果要把数据从父组件传递到子组件,就需要使用props属性。在Vue中,父子组件的关系可以总结为:prop向下传递,事件向上传递父组件通过prop给子组件下发数据,子组件通过事件给父组件发送消息。所以我们也会经常遇到组件之间需要传递数据的时候,大致分为四种情况:

  • 父组件向子组件传递数据,通过 props 传递数据。
  • 子组件向父组件传递数据,通过 events(自定义事件-回调参数) 传递数据。
  • 两个同级组件(兄弟组件)之间传递数据,通过 EventBus (事件总线-只适用于极小的项目)、Vuex(官方推荐)传递数据。
  • 其他方式通信-处理边界情况:
  •     1)$parent:父实例,如果当前实例有的话。通过访问父实例也能进行数据之间的交互,但极少情况下会直接修改父组件中的数据。
  •     2)$root:当前组件树的根 Vue 实例。如果当前实例没有父实例,此实例将会是其自己。
  •     3)$children:当前实例的直接子组件。需要注意 $children 并不保证顺序,也不是响应式的。如果你发现自己正在尝试使用 $children 来进行数据绑定,考虑使用一个数组配合 v-for 来生成子组件,并且使用 Array 作为真正的来源。
  •     4)$ref:一个对象,持有注册过 ref attribute 的所有 DOM 元素和组件实例。访问子组件实例或子元素
  •     5)provide / inject。主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。并且这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。

1、父组件向子组件传递数据

1.1、通过 Prop 传递数据

    通过 Prop向子组件传递数据,父子组件之间的数据传递相当于自上而下的下水管子,只能从上往下流,不能逆流。这也正是 Vue 的设计理念之单向数据流(当父组件的属性变化时,将传给子组件,但是反过来不会)。这是为了防止子组件无意间修改了父组件的状态,来避免应用的数据流变得难以理解。

示例:

1)我们先定义好一个父组件(Parent.vue)和一个子组件(Child.vue)

2)Child.vue:我们需要使用父组件的数据定义在props中:props: ['myName', 'myAge']

    HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符,所以在模板中不能直接使用camelCase (驼峰命名法) 的 prop (myName,myAge),需要使用其等价的 kebab-case (短横线分隔命名) 命名。

<template id="child">
  <div class="child">
    <h3>子组件child数据</h3>
    <ul>
      <li>
        <label>姓名:</label>
        <input type="text"
               v-model="myName" />
        <span>您输入的姓名:{{ myName }}</span>
      </li>
      <li>
        <label>年龄:</label>
        <input type="text"
               v-model="myAge" />
        <span>您输入的年龄:{{ myAge }}</span>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  props: ['myName', 'myAge']
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  font-size: 24px;
  font-weight: 400;
  margin-top: 0;
  margin-bottom: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  box-sizing: border-box;
  padding: 0 20px;
  text-align: center;
  color: #fff;
}
ul {
  margin: 10px;
  padding: 0;
  list-style: none outside none;
  border: 1px solid #ccc;
}
input {
  padding: 5px 10px;
  border-radius: 3px;
  border: 1px solid #ccc;
  margin: 10px;
  height: 30px;
}
.child {
  background: #bd6ea2;
  padding: 10px;
  border-radius: 5px;
  color: #fff;
  margin: 0 5px;
}
</style>

3) Parent.vue:在父组件中导入子组件,并引用子组件<my-child></my-child>

<template id="parent">
  <div id="app">
    <div class="parent">
      <h3>父组件Parent数据</h3>
      <ul>
        <li>
          <label>姓名:</label>
          <input type="text"
                 v-model="name" />
          <span>您输入的姓名:{{ name }}</span>
        </li>
        <li>
          <label>年龄:</label>
          <input type="text"
                 v-model="age" />
          <span>您输入的年龄:{{ age }}</span>
        </li>
      </ul>
    </div>
    <my-child :my-name="name"
              :my-age="age"></my-child>
  </div>
</template>

<script>
import Child from './Child.vue'

export default {
  data () { // data必须是一个函数,如果data不是一个函数,则无法达到每个组件数据都是独立的对象,这个受到javascript的本质影响,如果对象无法独立,则无法做到组件复用。
    return {
      name: 'father',
      age: '28'
    }
  },
  components: {
    'my-child': Child
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  font-size: 24px;
  font-weight: 400;
  margin-top: 0;
  margin-bottom: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  box-sizing: border-box;
  padding: 0 20px;
  text-align: center;
  color: #fff;
}
ul {
  margin: 10px;
  padding: 0;
  list-style: none outside none;
  border: 1px solid #ccc;
}
input {
  padding: 5px 10px;
  border-radius: 3px;
  border: 1px solid #ccc;
  margin: 10px;
  height: 30px;
}
.parent {
  background: #6882b8;
  padding: 10px;
  border-radius: 5px;
  color: #fff;
  margin: 0 5px;
}
</style>

4)在main.js中注册父组件

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import Parent from '@/components/props/Parent'

Vue.component('my-parent', Parent)

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})

 5)在App.vue中使用父组件

<template>
  <div id="app">
    <my-parent></my-parent>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
body {
  height: 100vh;
  width: 100vw;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: #f8f4f4;
}
</style>

示例效果:当父组件的属性变化时,将传导给子组件,但是不会反过来。修改子组件的 prop 值,是不会传回给父组件去更新视图的。

    通过props传递给子组件的myName、myAge,不能在子组件内部修改props中的值。这样的操作破坏了单向数据流的设想,所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。但是如果prop改成对象类型,子组件直接修改数据,不会引起警告(因为当参数是对象的时候,传给子组件的应该是类似于指针的一个东西,指向内存中的一个数据空间,而子组件通过 ObjectName.prop 去操作Object的时候,更改了内存空间的数据,但是并没有更改内存空间的位置,指针仍然是指向这块内存空间的,所以不会有警告报错),但还是不建议这样做。

     官网给出了解决方案:

    1)定义一个局部变量并用prop的值初始化它;

    2)定义一个计算属性,处理prop的值并返回。

我们对子组件进行改造:

<template id="child">
  <div class="child">
    <h3>子组件child数据</h3>
    <ul>
      <li>
        <label>姓名:</label>
        <input type="text"
               v-model="childName" />
        <span>您输入的姓名:{{ childName }}</span>
      </li>
      <li>
        <label>年龄:</label>
        <input type="text"
               v-model="childAge" />
        <span>您输入的年龄:{{ childAge }}</span>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  // 父组件传来的数据,不是响应式的,并且基本类型的数据在子组价中直接修改会报出警告
  props: ['myName', 'myAge'],
  // 为了更新这个data属性,就需要使用侦听器来监听props的变化
  data () {
    return {
      // 如果仅仅是重新定义属性,会导致父组件发生改变而子组件无法响应(注意:只针对基本类型,如果是引用类型,仍旧是变化的)
      childName: this.myName,
      childAge: this.myAge
    }
  },
  computed: { // computed 中不能设置响应式的data数据,更适合多对一的情况
    ismyName () { // 监听父组件的myName,一旦获取到了父组件的值并且发生变化时会触发
      return this.myName
    },
    ismyAge () {
      return this.myAge
    }
  },
  watch: {// 监听到了父组件值发生变化,这时监听器就可以操作响应式data,实现子组件同步父组件的更新
    ismyName (newV, oldV) {
      console.log(newV, oldV)
      this.childName = newV // childName赋值
    },
    ismyAge (newV, oldV) {
      console.log(newV, oldV)
      this.childAge = newV // childName赋值
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  font-size: 24px;
  font-weight: 400;
  margin-top: 0;
  margin-bottom: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  box-sizing: border-box;
  padding: 0 20px;
  text-align: center;
  color: #fff;
}
ul {
  margin: 10px;
  padding: 0;
  list-style: none outside none;
  border: 1px solid #ccc;
}
input {
  padding: 5px 10px;
  border-radius: 3px;
  border: 1px solid #ccc;
  margin: 10px;
  height: 30px;
}
.child {
  background: #bd6ea2;
  padding: 10px;
  border-radius: 5px;
  color: #fff;
  margin: 0 5px;
}
</style>

 案例效果:

    现在修改子组件的,不会影响父组件的传递过来的prop。 

1.2、Prop 类型

    子组件定义 props 有三种方式:

// 第一种数组方式
props: [xxx, xxx, xxx]
// 第二种对象方式
props: { xxx: Number, xxx: String}
// 第三种对象嵌套对象方式
props: {
    xxx: {
        // 类型不匹配会警告
        type: Number,
        // 对象或数组默认值必须从一个工厂函数获取
        default: 0,
        required: true,
        // 自定义验证函数
        validator: function (value) {
            // 这个值必须匹配下列字符串中的一个
            return ['success', 'warning', 'danger'].indexOf(value) !== -1
       }
    }
}

1.2.1、第一种数组方式

    数组方式:可以是静态的传递字符串,也可以是数字、布尔值、数组或者对象,但是后者需要使用到 v-bind指令。前面介绍过(v-bind 主要用于属性绑定,比方你的class属性,style属性,value属性,href属性等等,只要是属性,就可以用v-bind指令进行绑定),在上面(1.1 通过 Prop 传递数据)的案例中我们就用到了v-bind传递一个对象变量。

1)静态字符串

//main.js
Vue.component('v-string', {
  // 声明 props
  props: ['message', 'myMessage'],
  // 就像 data 一样,prop 也可以在模板中使用 myMessage驼峰命名法 在模板中使用 kebab-case (短横线分隔式命名)
  // 同样也可以在 vm 实例中通过 this.message 来使用
  template: '<span>{{ message }} | {{ myMessage }}</span>'
})

//app.vue
<v-string message="hello!"
              my-message="hello world!"></v-string>

运行结果:

2)动态对象

//main.js
Vue.component('v-string', {
  // 声明 props , 注意字面量和使用v-bind指令传递的不同,v-bind传递会当做JavaScript 表达式
  props: ['fatherMsg', 'myAge', 'fatherSz', 'userInfo'],
  template: '<span>{{fatherMsg}} | {{myAge}} {{typeof myAge}} | {{fatherSz.length}} | {{userInfo.names}}</span>'
})

//app.vue
<v-string :father-msg="fatherMsg"
              :my-age="myAge"
              :father-sz="[2,4,6]"
              :user-info="userInfo"></v-string>

export default {
  name: 'App',
  data () {
    return {
      fatherMsg: '我是父组件属性',
      myAge: 18,
      userInfo: { names: '张三', sex: '男' }
    }
  }
}

运行结果:

1.2.2、第二种对象方式

    使用第二种对象的方式,可以验证参数的类型,如果不符合数据规格,Vue 会发出警告。把main.js中的props改成下面的格式,则控制台出现警告。

//main.js
Vue.component('v-string', {
  // 声明 props
  props: {
    fatherMsg: String,
    myAge: String,
    fatherSz: Array,
    userInfo: Object
  },
  template: '<span>{{fatherMsg}} | {{myAge}} {{typeof myAge}} | {{fatherSz.length}} | {{userInfo.names}}</span>'
})

运行结果:

1.2.3、第三种对象嵌套对象方式

    第三种嵌套对象中的属性(type、default、required、validator)均为非必须。

    type可以是以下类型,type 还可以是一个自定义的构造函数(如:author: Person)

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol

将main.js中替换成如下所示:

//main.js
Vue.component('v-string', {
  // 声明 props
  props: {
    // 基础类型检查 (如果`null` 指允许任何类型)
    fatherMsg: String,
    // 可能是多种类型
    myAge: [Number, String],
    // 必传且是数组
    fatherSz: {
      type: Array,
      required: true,
      validator: function (value) {
        console.log(value[0]) // 2 prop 验证失败的时候,(开发环境构建版本的) Vue 将会产生一个控制台的警告。
        return value[0] > 2
      }
    },
    // 数组/对象的默认值应当由一个工厂函数返回,如果没有userInfo对象 则userInfo,names 默认返回defaultName
    userInfo: {
      type: Object,
      default: function () {
        return { names: 'defaultName' }
      }
    }
  },
  template: '<span>{{fatherMsg}} | {{myAge}} {{typeof myAge}} | {{fatherSz.length}} | {{userInfo.names}}</span>'
})

 运行结果:

1.3、非 Prop 的 Attribute

    什么是非Prop 的 Attribute 呢?(props是properties的缩写,attrs是attributes的缩写)在介绍Vue基础的时候,也介绍过properties(属性)和 attributes(特性)。

  • property是DOM中的属性,是JavaScript里的对象;
  • attribute是HTML标签上的特性,它的值只能够是字符串,就比如我们的 DOM 元素自带的属性 classstyleid

    在Vue中一个非 prop 的 attribute 是指传向一个组件,但是该组件并没有相应 prop 定义的 attribute。对于绝大多数 attribute 来说,从外部提供给组件的值会替换掉组件内部设置好的值。但是有两个比较特殊,classstyle 不会替换,但是会合并。其他的属性基本上都会继承或者替换。

1.3.1、Attribute 继承

举个栗子:

在main.js中注册一个全局组件

Vue.component('v-com', {
  props: ['name', 'content', 'b', 'title'],
  template: `<div name='mydiv' id='mydiv' dir='rtl' title='l am div' style='color:blue'>{{name}} | {{ content }} | {{b}} | {{title}}<input type='text' value='male' class='aaa' style="color: red"/></div>`
})

在app.vue中

<template>
  <div id="app">
    <!--props ['name', 'content', 'b', 'title'] 中定义了 不会继承(替换/合并)到根元素上
    class、style 会合并 源码中config.isReservedAttr(hyphenatedKey) --》 makeMap('style,class')单独处理了-->
    <v-com content="hello world"
           a="3"
           :b="fatherMsg"
           c="4"
           class="ddd"
           style="font-size: 16px;"
           title="bt"
           value="fvalue"
           id="hh"
           name="vcom"
           dir="ltr"></v-com>
  </div>
</template>

<script>
export default {
  name: 'App',
  data () {
    return {
      fatherMsg: '我是父组件属性'
    }
  }
}
</script>

案例效果:

    在使用子组件时,我们定义了很多属性:【content="hello world"  a="3" :b="fatherMsg" c="4"  class="ddd" style="font-size: 16px;"  title="bt" value="fvalue" id="hh"  name="vcom" dir="ltr"】

    同时在子组件的根元素上定义了:【name='mydiv' id='mydiv' dir='rtl' title='l am div' style='color:blue' 】

    而子组件中我们只接收props: ['name','content', 'b', 'title']四个属性

    子组件中接收到了 ['name','content', 'b', 'title'],但是这四个属性就没有继承/替换到根元素上。根元素的id、dir被替换,a、c、value继承到了根元素上,而class 和 style进行了合并。

1.3.2、禁用 Attribute 继承

    如果不希望组件上的根元素继承attribute,可以在组件的选项中设置 inheritAttrs: false【inheritAttrs-继承属性的意思

在main.js中加上inheritAttrs: false

Vue.component('v-com', {
  inheritAttrs: false,
  props: ['name', 'content', 'b', 'title'],
  template: `<div name='mydiv' id='mydiv' dir='rtl' title='l am div' style='color:blue'>{{name}} | {{ content }} | {{b}} | {{title}}<input type='text' value='male' class='aaa' style="color: red"/></div>`
})

案例效果:

    加上inheritAttrs: false,根元素的id、dir依旧不变,且a、c、value也没有继承到根元素上了。但是它始终不影响class 和 style的合并。

1.3.3、多个根节点上的 Attribute 继承-$attrs

    当我们知道了inheritAttrs的作用:是否继承来自父组件的属性(默认继承),那如果多个根节点或者不想到根节点继承呢?那就要使用到$attrs。我们再回头来看看官网给我们列举的案例

//main.js
Vue.component('base-input', {
  inheritAttrs: false,
  props: ['label', 'value'],
  template: `
    <label>
      {{ label }} | {{value}} | {{this.$attrs}}
      <input
        v-bind="$attrs"
        v-bind:value="value"
        v-on:input="$emit('input', $event.target.value)"
      >
    </label>
  `
})

//App.vue
<base-input v-model="username"
                required
                placeholder="Enter your username"
                label="name"
                value="username"></base-input>

案例效果

2、子组件向父组件传递数据

    prop实现了把父组件中的数据传递到子组件中,每次父组件更新时,子组件的所有 prop 都会更新为最新值。但是不能在子组件内部改变 prop,当然修改是有效的(可以通过this.$parent.name 访问到父组件中的属性),如果你这么做了,Vue 也会有警告提示。子组件向父组件传递数据,通过events - 自定义事件(回调参数)传递数据。

vm.$emit( eventName, […args] )

  • 参数

    • {string} eventName
    • [...args]

    触发当前实例上的事件。附加参数都会传给监听器回调。

    [每个Vue实例都实现了事件接口:使用$on(evntName)监听事件;使用$emit(eventName,optionalPayload)触发事件。父组件可以在使用子组件的地方直接用v-on来监听子组件触发的事件。]

2.1、通过 events 传递数据

   在上一个案例中其实也使用到了$emit。我们先看官网给出的一个例子。 子组件click事件,调用$emit传入需要触发的父类自定义事件welcome,在父组件中使用子组件时对welcome绑定sayHi方法。

//在main.js 定义组件welcome-button
Vue.component('welcome-button', {
  template: `
    <button @click="$emit('welcome')">
      Click me to be welcomed
    </button>
  `
})

//在App.vue 中使用组件(APP就是welcome-button父组件)
<template>
  <div id="app">
    <welcome-button @welcome="sayHi"></welcome-button>
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    sayHi: function () {
      alert('Hi!')
    }
  }
}
</script>

2.2、子组件向父组件传递一个值

    首先在main.js中定义一个子组件:v-testcom,并在this.$emit('childFn', this.message)中传递一个值this.message

let testCom = Vue.extend({
  template: `
  <div class="testCom">
    <input type="text" v-model="message" />
    <button @click="click">Send</button>
  </div>
  `,
  data () {
    return {
      // 默认
      message: '我是来自子组件的消息'
    }
  },
  methods: {
    click () {
      this.$emit('childFn', this.message)
    }
  }

})
Vue.component('v-testcom', testCom)

在App.vue中使用子组件:<v-testcom @childFn="parentFn($event) 或者 <v-testcom @childFn="parentFn"></v-testcom>  这里的$event默认为childFn的第一个参数

<template>
  <div id="app">
    <!-- 绑定事件处理函数时,可以不带括号,$event 默认是childFn函数的第一个参数-->
    <!-- <v-testcom @childFn="parentFn($event)"></v-testcom> -->
    <v-testcom @childFn="parentFn"></v-testcom>
    <br />
    子组件传来的值 : {{message}}
  </div>
</template>

<script>
export default {
  name: 'App',
  data () {
    return {
      message: ''
    }
  },
  methods: {
    parentFn (payload) {
      this.message = payload
    }
  }
}
</script>

案例效果:

2.3、子组件向父组件传递多个值

    子组件如果只传递一个参数(这个值可以是字符、对象或者数组),接收方也只接收一个参数(这个值可以是字符、对象或者数组)。如果我就是想传递多个参数呢,其实也是可以对应接收的,同时也支持以数组的形式接收。

    首先在子组件中this.$emit('childFn', this.message, this.message1, this.message2)传递三个参数

//main.js
let testCom = Vue.extend({
  template: `
  <div class="testCom">
    <input type="text" v-model="message" />
    <button @click="click">Send</button>
  </div>
  `,
  data () {
    return {
      message: '我是来自子组件的消息1',
      message1: '我是来自子组件的消息2',
      message2: '我是来自子组件的消息3'
    }
  },
  methods: {
    click () {
      this.$emit('childFn', this.message, this.message1, this.message2)
    }
  }
})
Vue.component('v-testcom', testCom)

 如果是多个参数,在App.vue中一种是绑定时默认不写,则按参数一个一个接收。如果加上(),则需以数组的形式接收(并且这个参数只能写arguments),$event默认为childFn的第一参数。

//app.vue
<template>
  <div id="app">
    <v-testcom @childFn="parentFn"></v-testcom>
    <v-testcom @childFn="parentFn2(arguments,$event)"></v-testcom>
    <br />
    子组件传来的值 : {{message}}
  </div>
</template>

<script>
export default {
  name: 'App',
  data () {
    return {
      message: ''
    }
  },
  methods: {
    parentFn (parm1, parm2, parm3) {
      console.log(parm1)
      console.log(parm2)
      console.log(parm3)
    },
    parentFn2 (agr, parm) {
      console.log(agr)
      console.log(parm)
    }
  }
}
</script>

案例效果:

2.4、$listeners的使用

    前面我们介绍了$attrs用来操作父子组件间的属性,与之类似的$listeners用来操作父子组件间的事件。

  $attrs属性收纳:包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。

    $listeners事件收纳:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。

举个栗子:

    首先在components目录下新建一个events/ChildComponent.vue

<template>
  <div class="child-component">
    <h1>我是一个 {{ professional }}</h1>
  </div>
</template>

<script>

export default {
  name: 'ChildComponent',
  props: {
    professional: {
      type: String,
      default: '工程师'
    }
  },
  created () {
    // 调用父组件App.vue中的triggerTwo()方法
    console.log(this.$attrs, this.$listeners)
    this.$listeners.two()
  }
}
</script>

    然后在App.vue中使用局部注册的方式,使用ChildComponent子组件。

<template>
  <div id="app">
    <ChildComponent :professional="professional"
                    :name="name"
                    @one.native="triggerOne"
                    @two="triggerTwo"></ChildComponent>
  </div>
</template>

<script>
import ChildComponent from './components/events/ChildComponent.vue'

export default {
  name: 'App',
  data () {
    return {
      professional: '程序猿',
      name: '穆瑾轩'
    }
  },
  components: {
    ChildComponent
  },
  methods: {
    triggerOne () {
      console.log('one')
    },
    triggerTwo () {
      console.log('two')
    }
  }
}
</script>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
</style>

案例效果:

案例执行后

    console.log(this.$attrs, this.$listeners)  //输出了没有在prop中的name属性 和一个没有被.native修饰的方法:two

    this.$listeners.two()  //执行two绑定的方法triggerTwo 输出two

3、兄弟组件通信-EventBus

    现在,我想两个组件之间进行数据传递,即我A页面点击后跳转到B页面,同时我希望将(组件/页面)A上的某一些参数携带过去给(组件/页面)B。如果咱们的应用程序不需要类似Vuex这样的库来处理组件之间的数据通信,就可以考虑Vue中的事件总线 ,即 EventBus来通信。

    EventBus原本是一个针对Android为了松耦合的基于事件发布/订阅模式(观察者模式)的开源库。EventBus 在vue中适用于跨组件简单通信,不适应用于复杂场景多组件高频率通信,它相当于一个全局的仓库,任何组件都可以去这个仓库里获取事件,确实很方便。但是vue是一个单页应用,当页面刷新了之后,与之相关的EventBus会被移除。如果反复操作的页面,EventBus 在监听的时候就会触发很多次,也是一个非常大的隐患。这时候我们就需要好好处理 EventBus 在项目中的关系,在vue页面销毁时,同时移除EventBus 事件监听。

3.1、EventBus的使用方式($emit$on$off

3.1.1、单独使用一个Vue实例作为中央事件总线

    新建一个EventBus.js,方式一:

import Vue from 'vue'
export const EventBus = new Vue()

   导出使用

import { EventBus } from './EventBus.js'


 //EventBus.$on()...

 新建一个EventBus.js,方式二:

// 你也可以这么定义
export default (Vue) => {
  const EventBus = new Vue()
  Vue.prototype.$bus = {
    /**
     *  @param {any} event 第一个参数是事件对象,第二个参数是接收到消息信息,可以是任意类型
     *  @method $on 事件订阅, 监听当前实例上的自定义事件。
     *  @method $off 取消事件订阅,移除自定义事件监听器。
     *  @method $emit 事件广播, 触发当前实例上的事件。
     *  @method $once 事件订阅, 监听一个自定义事件,但是只触发一次,在第一次触发之后移除监听器。
     */
    $on (...event) {
      EventBus.$on(...event)
    },
    $off (...event) {
      EventBus.$off(...event)
    },
    $once (...event) {
      EventBus.$emit(...event)
    },
    $emit (...event) {
      EventBus.$emit(...event)
    }
  }
  return EventBus
}

  导出使用:

import Vue from 'vue'
import EventBus from './EventBus.js'

//EventBus(Vue).$on()...

3.1.2、在根组件中注册bus

const bus = new Vue()

Vue.prototype.$bus = bus

//绑定到原型上,这样我们就不需要再自己写bus.js引入了,可以直接使用this.$bus.on(),this.$bus.$emit(),this.$bus.$off()

3.1.3、使用插件vue-bus

安装  npm install vue-bus --save

//如果安装不成功
//出现npm ERR! code ERR_TLS_CERT_ALTNAME_INVALID...错误

//试试(取消npm的https认证):npm config set strict-ssl false

//main.js中引入   

import VueBus from 'vue-bus'

Vue.use(VueBus)


//然后就可以使用this.$bus.on()...

3.2、EventBus的使用

    这里使用安装插件vue-bus的方式,来使用EventBus。

    安装好了vue-bus,在main.js中使用插件。

import VueBus from 'vue-bus'

Vue.use(VueBus)

   在components下新建目录eventBus,然后新建一个A.vue 和 一个B.vue

    A.vue

<template>
  <button @click="sendMsg">发送MsgA</button>
</template>

<script>
export default {
  data () {
    return {
      MsgA: 'A组件中的Msg'
    }
  },
  methods: {
    sendMsg () {
      /* 调用全局Vue实例中的$bus事件总线中的emit属性,发送事
      件"aMsg",并携带A组件中的Msg */
      this.$bus.emit('aMsg', this.MsgA)
      console.log('我是' + this.MsgA)
    }
  },
  beforeCreate: function () {
    console.group('A组件 beforeCreate 创建前状态===============》')
  },
  created: function () {
    console.group('A组件 created 创建完毕状态===============》')
  },
  beforeMount: function () {
    console.group('A组件 beforeMount 挂载前状态===============》')
  },
  mounted: function () {
    console.group('A组件 mounted 挂载结束状态===============》')
  },
  beforeUpdate: function () {
    console.group('A组件 beforeUpdate 更新前状态===============》')
  },
  updated: function () {
    console.group('A组件 updated 更新完成状态===============》')
  },
  beforeDestroy: function () {
    console.group('A组件 beforeDestroy 销毁前状态===============》')
  },
  destroyed: function () {
    console.group('A组件 destroyed 销毁完成状态===============》')
  }
}
</script>

    B.vue

<template>
  <!-- 展示msgB -->
  <div>
    <p>{{msgB}}</p>
  </div>
</template>

<script>
export default {
  data () {
    return {
      // 初始化一个msgB
      msgB: ''
    }
  },
  beforeCreate: function () {
    console.group('B组件 beforeCreate 创建前状态===============》')
  },
  created: function () {
    console.group('B组件 created 创建完毕状态===============》')
  },
  beforeMount: function () {
    console.group('B组件 beforeMount 挂载前状态===============》')
  },
  mounted: function () {
    /* 调用全局Vue实例中的$bus事件总线中的on属性,监听A组件发送
    到事件总线中的aMsg事件 */
    this.$bus.on('aMsg', (data) => {
      // 将A组件传递过来的参数data赋值给msgB
      this.msgB = data
      console.log('我是B组件的mounted')
    })
    console.group('B组件 mounted 挂载结束状态===============》')
  },
  beforeUpdate: function () {
    console.group('B组件 beforeUpdate 更新前状态===============》')
  },
  updated: function () {
    console.group('B组件 updated 更新完成状态===============》')
  },
  beforeDestroy: function () {
    // 组件销毁前销毁移除事件监听
    this.$bus.off('aMsg')
    console.log('B组件 $bus.off移除事件监听')
    console.group('B组件 beforedestroy 销毁前状态===============》')
  },
  destroyed: function () {
    console.group('B组件 destroyed 销毁完成状态===============》')
  }
}
</script>

    App.vue

<template>
  <div id="app">
    <!--v-show 的切换组件始终保持在 mounted 钩子  -->
    <A v-show="swa"></A>
    <!-- v-if 的切换true => false 执行 beforedestroy destroyed 钩子 -->
    <B v-if="swb"></B>

    <button @click="ycb">我是B检察官</button>
  </div>
</template>

<script>

import A from './components/eventBus/A.vue'
import B from './components/eventBus/B.vue'

export default {
  name: 'App',
  data () {
    return {
      'swa': true,
      'swb': true
    }
  },
  components: {
    A,
    B
  },
  methods: {
    ycb () {
      if (this.swb) {
        this.swb = false
      } else {
        this.swb = true
      }
    }
  }
}
</script>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
</style>

案例效果:

    A组件bus事件总线通过emit,触发当前实例上的事件aMsg,并附加参数this.MsgA传给监听器。B组件bus事件总线通过on,监听当前实例上的自定义事件aMsg,并将传递过来的参数赋值给:msgB (this.msgB = data ),当B组价销毁时应当要销毁事件总线上的监听,避免被其他人劫走或者多次触发。

4、其他通信方式

4.1、vm.$parentvm.$rootvm.$children   

    在官网实例property我们可以看到Vue实例,提供了vm.$parentvm.$rootvm.$children,分别可以获取父类实例、根实例、和子类实例,并访问其实例对象的某些属性和方法,但是很少会直接去修改他们的属性。

    vm.$parent 类型Vue instance

 vm.$root  类型Vue instance

 vm.$children  类型Array<Vue instance>

    我们用之前的Child.vue 和 Parent.vue组件,输出一下。

Child.vue

<template id="child">
  <div class="child">
    <h3>子组件child数据</h3>
    <ul>
      <li>
        <label>姓名:</label>
        <input type="text"
               v-model="childName" />
        <span>您输入的姓名:{{ childName }}</span>
      </li>
      <li>
        <label>年龄:</label>
        <input type="text"
               v-model="childAge" />
        <span>您输入的年龄:{{ childAge }}</span>
      </li>
    </ul>
    <button @click="getFaMethods">获取父组件信息</button>
    <button @click="getRootMethods">获取根组件信息</button>
  </div>
</template>

<script>
export default {
  // 父组件传来的数据,不是响应式的,并且基本类型的数据在子组价中直接修改会报出警告
  props: ['myName', 'myAge'],
  // 为了更新这个data属性,就需要使用侦听器来监听props的变化
  data () {
    return {
      // 如果仅仅是重新定义属性,会导致父组件发生改变而子组件无法响应(注意:只针对基本类型,如果是引用类型,仍旧是变化的)
      childName: this.myName,
      childAge: this.myAge
    }
  },
  computed: { // computed 中不能设置响应式的data数据,更适合多对一的情况
    ismyName () { // 监听父组件的myName,一旦获取到了父组件的值并且发生变化时会触发
      return this.myName
    },
    ismyAge () {
      return this.myAge
    }
  },
  watch: {// 监听到了父组件值发生变化,这时监听器就可以操作响应式data,实现子组件同步父组件的更新
    ismyName (newV, oldV) {
      // console.log(newV, oldV)
      this.childName = newV // childName赋值
    },
    ismyAge (newV, oldV) {
      // console.log(newV, oldV)
      this.childAge = newV // childName赋值
    }
  },
  mounted () {
    console.group('%c%s', 'color:blue', '我是Child组件' + '我输出的内容:')
    console.log('我的父组件:')
    console.log(this.$parent) // 获取父组件
  },
  methods: {
    getChildName () {
      console.log(this.childName)
    },
    getFaMethods () {
      // vue 里 this.$parent 作用
      // $parent在子组件中可以调用父组件的方法,也可以获得其数据,也可以修改
      console.group('%c%s', 'color:blue', '我是Child组件' + '我输出父组件的内容:')
      console.log('我执行父组件的方法-------------')
      this.$parent.getParentName()
      console.log('我获取父组件的属性--------------')
      console.log(this.$parent.name)
      this.$parent.name = 'mjx' // 改变父组件的属性
      console.log(this.$parent.name)
    },
    getRootMethods () {
      console.group('%c%s', 'color:blue', '我是Child组件' + '我输出根组件的内容:')
      console.log('输出根组件')
      console.log(this.$root) // 获取根组件
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  font-size: 24px;
  font-weight: 400;
  margin-top: 0;
  margin-bottom: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  box-sizing: border-box;
  padding: 0 20px;
  text-align: center;
  color: #fff;
}
ul {
  margin: 10px;
  padding: 0;
  list-style: none outside none;
  border: 1px solid #ccc;
}
input {
  padding: 5px 10px;
  border-radius: 3px;
  border: 1px solid #ccc;
  margin: 10px;
  height: 30px;
}
.child {
  background: #bd6ea2;
  padding: 10px;
  border-radius: 5px;
  color: #fff;
  margin: 0 5px;
}
</style>

Parent.vue

<template id="parent">
  <div id="app">
    <div class="parent">
      <h3>父组件Parent数据</h3>
      <ul>
        <li>
          <label>姓名:</label>
          <input type="text"
                 v-model="name" />
          <span>您输入的姓名:{{ name }}</span>
        </li>
        <li>
          <label>年龄:</label>
          <input type="text"
                 v-model="age" />
          <span>您输入的年龄:{{ age }}</span>
        </li>
      </ul>
      <button @click="getChildMethod">获取子组件信息</button>
    </div>
    <my-child :my-name="name"
              :my-age="age"></my-child>
  </div>
</template>

<script>
import Child from './Child.vue'

export default {
  data () {
    return {
      name: 'father',
      age: '28'
    }
  },
  components: {
    'my-child': Child
  },
  methods: {
    getParentName () {
      console.log('我是父类的方法:' + this.name)
    },
    getParentAge () {
      console.log('我是父类的方法:' + this.age)
    },
    getChildMethod () {
      console.log('我是父组件,我执行子组件的方法-------------')
      this.$children[0].getChildName()
      console.log('我获取子组件的属性--------------')
      console.log(this.$children[0].childAge)
      this.$children[0].childAge = '20'
      console.log(this.$children[0].childAge)
    }
  },
  mounted () {
    console.group('%c%s', 'color:red', '我是Parent组件' + '我输出的内容:')
    console.log('我的子组件:')
    console.log(this.$children) // 获取子组件,获取的是一个数组
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  font-size: 24px;
  font-weight: 400;
  margin-top: 0;
  margin-bottom: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  box-sizing: border-box;
  padding: 0 20px;
  text-align: center;
  color: #fff;
}
ul {
  margin: 10px;
  padding: 0;
  list-style: none outside none;
  border: 1px solid #ccc;
}
input {
  padding: 5px 10px;
  border-radius: 3px;
  border: 1px solid #ccc;
  margin: 10px;
  height: 30px;
}
.parent {
  background: #6882b8;
  padding: 10px;
  border-radius: 5px;
  color: #fff;
  margin: 0 5px;
}
</style>

输出结果:

 

    vm.$parentvm.$children 是不是比较简单粗暴, 但是他对需要通信的组件绑定性强,你必须得知道你的子或父是什么,或者说你必须知道你的子组件或者父组件有什么东西的时候才可以调用,这样会让你的子组件和父组件耦合,子组件不能单独使用。

    vm.$children返回的是一个数组,如果我在中间再插入一个组件,这样通过数组下标获取子组件,确实比较不方便。那么Vue中也提供了另外一种方式,让我们去访问子组件实例或者元素,他就是vm.$refs

4.2、扩展vm.$refs代替vm.$children获取子组件

    vm.$refs:一个对象,持有注册过 ref attribute 的所有 DOM 元素和组件实例。它和原生JS中的document.querySelector('xxx')功能一样,它可以在vue中获取元素匹配组件。

    在Parent.vue中多引入几个<my-child></my-child>,给中间的子组件加上ref="usethis",在父组件中使用this.$refs.usethis获取

<template id="parent">
  <div id="app">
    <div class="parent">
      <h3>父组件Parent数据</h3>
      <ul>
        <li>
          <label>姓名:</label>
          <input type="text"
                 v-model="name" />
          <span>您输入的姓名:{{ name }}</span>
        </li>
        <li>
          <label>年龄:</label>
          <input type="text"
                 v-model="age" />
          <span>您输入的年龄:{{ age }}</span>
        </li>
      </ul>
      <button @click="getChildMethod">获取子组件信息</button>
    </div>
    <my-child :my-name="name"
              :my-age="age"></my-child>
    <my-child :my-name="name"
              :my-age="age"
              ref="usethis"></my-child>
    <my-child :my-name="name"
              :my-age="age"></my-child>
  </div>
</template>

<script>
import Child from './Child.vue'

export default {
  data () {
    return {
      name: 'father',
      age: '28'
    }
  },
  components: {
    'my-child': Child
  },
  methods: {
    getParentName () {
      console.log('我是父类的方法:' + this.name)
    },
    getParentAge () {
      console.log('我是父类的方法:' + this.age)
    },
    getChildMethod () {
      console.log('我是父组件-------------')
      console.log(this.$refs)
      console.log(this.$refs.usethis)
    }
  },
  mounted () {
    console.group('%c%s', 'color:red', '我是Parent组件' + '我输出的内容:')
    console.log('我的子组件:')
    console.log(this.$children) // 获取子组件,获取的是一个数组
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  font-size: 24px;
  font-weight: 400;
  margin-top: 0;
  margin-bottom: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  box-sizing: border-box;
  padding: 0 20px;
  text-align: center;
  color: #fff;
}
ul {
  margin: 10px;
  padding: 0;
  list-style: none outside none;
  border: 1px solid #ccc;
}
input {
  padding: 5px 10px;
  border-radius: 3px;
  border: 1px solid #ccc;
  margin: 10px;
  height: 30px;
}
.parent {
  background: #6882b8;
  padding: 10px;
  border-radius: 5px;
  color: #fff;
  margin: 0 5px;
}
</style>

案例效果:

4.3、多层级组件通信provide/inject

  在组件式开发中,最大的痛点就在于组件之间的通信。Vue 提供了各种各样的组件通信方式,从基础的 props/$emit 到用于兄弟组件通信的 EventBus,还有vm.$parentvm.$rootvm.$children ,再到用于全局数据管理的 Vuex(Vuex相对更复杂一点,后面再介绍)。也许人会问:使用 $root 都能获得根节点,也可做全局共享,那么我们何必使用 provide/inject 呢?不过 provide/inject 也是有它用武之地的。

    provide/inject翻译为:提供/注入,是 Vue 在 2.2.0 版本新增的 API。官网介绍如下:

    这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。

  provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的 property。在该对象中你可以使用 ES2015 Symbols 作为 key,但是只在原生支持 Symbol 和 Reflect.ownKeys 的环境下可工作。

  inject 选项应该是一个字符串数组,或一个对象,对象的 key 是本地的绑定名,value 是:在可用的注入内容中搜索用的 key (字符串或 Symbol),或一个对象,该对象的:from property 是在可用的注入内容中搜索用的 key (字符串或 Symbol)default property 是降级情况下使用的 value。

  provide 和 inject 主要在开发高阶插件/组件库时使用。并不推荐用于普通应用程序代码中。provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。

  看了官网的解释感觉有点迷惑,大致意思是:

  1)只要一个组件使用了 provide 向下提供数据,那其下所有的子组件都可以通过 inject 来注入,不管中间隔了多少代,而且可以注入多个来自不同父级提供的数据(不一定要从根组件开始);

     2)provide/inject可以像import/export一样提供数据共享(Symbol起到一个句柄的作用,解决了有可能出现的依赖命名冲突问题),但是他的的价值更在于它能够提供一个多实例,并且在组件树上能按照规则覆盖的机制,主要用于组件封装。

     3)Vue 不会对 provide 中的变量进行响应式处理。所以,要想 inject 接受的变量是响应式的,provide 提供的变量本身就需要是响应式的。

4.3.1、provide/inject语法格式

    provide / inject(2.2.0 新增)

类型

  • provideObject | () => Object
  • injectArray<string> | { [key: string]: string | Symbol | Object }

方式一:provide为对象(provideObject

// 父级组件提供 'foo' 
var Provider = {
  provide: {
    foo: 'bar'
  },
  // ...
}


// 子组件注入 'foo' inject以数组的形式注入
var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
  // ...
}


//利用ES2015 Symbols foo 可以改写成
const foo = Symbol()
const Provider = {
  provide () {
    return {
      [foo]: 'foo'
    }
  }
}

方式二:provide为函数() => Object

    如果需要用到实例对象,方式一,使用foo: this.bar, 访问不到Vue实例,我们需要使用函数的形式。

// 方式二:
provide () { // 函数形式
    return{
        tos: this.money // 返回实例的一个对象
    }
}


provide () {
    return { 
      fms: () => { // 返回一个匿名函数
          return this.famsg
        }
    }
}

// provide 2.5.0以前,以数组的形式接收,2.5.0+ 也可以通过设置默认值使其变成可选项
const Child = {
  inject: {
    foo: {
      from: 'bar',
      default: () => [1, 2, 3]
    }
  }
}

4.3.2、provide/inject案例

    首先,我们在components/pd目录下新建grandfather.vue、father.vue、son.vue

grandfather.vue

<template>
  <div class="kx">
    康熙
    <A-f></A-f>
  </div>
</template>

<script>
import father from './father'
export default {
  name: 'kangxi',
  // 康熙给乾隆提供‘200元’
  // 康熙给雍正提供‘100元’
  provide: { // provide写法一:Object,不能访问实例this
    tos: '200元',
    tof: '100元'
  },
  components: {
    'A-f': father
  }
}
</script>

<style>
.kx {
  width: 200px;
  height: 140px;
  text-align: center;
  background-color: #ffb599;
}
</style>

father.vue

<template>
  <div class="yz">
    雍正
    <A-son></A-son>
  </div>
</template>

<script>
import son from './son'

export default {
  name: 'yongzheng',
  data () {
    return {
      famsg: '雍正说拿我的',
      money: '300元'
    }
  },
  components: {
    'A-son': son
  }
  // ,
  // provide () { // 方式二:函数形式
  //   return { // 返回一个匿名函数
  //     fms: () => {
  //       return this.famsg
  //     },
  //     // tos: this.money // 函数直接返回一个对象
  //   }
  // }
}
</script>

<style>
.yz {
  width: 160px;
  height: 100px;
  margin-left: 20px;
  text-align: center;
  background-color: #84b5ff;
}
</style>

son.vue

<template>
  <div class="ql">
    乾隆要到的钱:{{tos}}
  </div>
</template>

<script>
export default {
  name: 'qianlong',
  // 乾隆要200块
  inject: ['tos']
}
</script>

<style>
.ql {
  width: 120px;
  height: 60px;
  /* margin-top: -80px; */
  margin-left: 20px;
  text-align: center;
  background-color: blanchedalmond;
}
</style>

    然后在main.js中注册组件:Vue.component('A-grandfather', gd),并在App.vue中使用<A-grandfather></A-grandfather>,然后看效果,grandfather.vue因为比较宠爱孙子,准备给乾隆200元,并顺利接收到。

    突然这事情被雍正知道了,对乾隆说,怎么能要爷爷的钱呢?我给你,你还给爷爷。雍正赶紧得提供啊,于是,打开了被注释的代码provide那部分。乾隆高兴的很,开始接收(顺便调整下宽高)son.vue:

<template>
  <div class="ql">
    乾隆要到的钱:{{tos}}
    <br />
    红包签名:{{fmsfunc()}}
    <br />
    <br />
    {{toather}}
  </div>
</template>

<script>
export default {
  name: 'qianlong',
  // inject对象形式,这次是300块
  inject: {
    fmsfunc: {
      from: 'fms' // 函数形式,使用时以函数使用
    },
    tos: {
      from: 'tos'
    },
    toather: {
      default: '我是未接收设置的默认值'
    }
  }
}
</script>

<style>
.ql {
  width: 240px;
  height: 200px;
  margin-left: 40px;
  text-align: center;
  background-color: blanchedalmond;
}
</style>

father.vue 

<template>
  <div class="yz">
    雍正{{money}}
    <button @click="change">变更按钮</button>
    <A-son></A-son>
  </div>
</template>

<script>
import son from './son'

export default {
  name: 'yongzheng',
  data () {
    return {
      famsg: '雍正说拿我的',
      money: '300元'
    }
  },
  components: {
    'A-son': son
  },
  provide () { // 方式二:函数形式
    return { // 返回一个匿名函数
      fms: () => { // 箭头函数中的this在定义函数时绑定的
        console.log(this)
        return this.famsg
      },
      tos: this.money // 函数直接返回一个对象
    }
  },
  methods: {
    change () {
      this.money = '400元'
      this.famsg = '雍正说拿我的加200'
    }
  }
}
</script>

<style>
.yz {
  width: 320px;
  height: 240px;
  margin-left: 40px;
  text-align: center;
  background-color: #84b5ff;
}
</style>

案例效果:

    father提供的数据覆盖了grandfather提供的数据,注入的值可以来自不同的父类或者祖先,也就是说provide/inject在组件树中会有更好的体现,注入的不必管从哪里来的,提供的可以在任意父类层中改变,而不用改子类。变更按钮体现则是注入使用普通函数和箭头函数的区别。provide/inject它解决一些循环组件比如tree, menu, list等, 传参困难, 并且难以管理的问题, 主要用于组件封装, 常见于一些ui组件库。

    前面介绍了很多关于组件间的通信:props events、$parent、$root、$children、provide / inject都只适用于关系型组件之间通信,非关系型使用EventBus(通过 vm.$on 或 vm.$once 进行订阅,vm.$emit 发布,vm.off 取消订阅),当然还有Vuex,这个最后面再介绍。

四、vue插槽(slot分发内容)

1、插槽简介

    Vue组件的另一个重要概念是插槽,其主要参照了当前Web Components规范草案,是组件的一块HTML模板,而这块模板显示不显示,以及怎么显示由父组件来决定。不过,插槽显示的位置由子组件自身决定,slot写在组件template的哪块,父组件传过来的模板将来就显示在哪块。插槽提供了一个将内容放置到新位置或使组件更通用的出口。在实际项目开发当中,时常会把父组件的内容与子组件自己的模板混合起来使用,而这样的一个过程在Vue中被称为内容分发

    简单的说:使用组件的时候,经常需要在父组件中为子组件中插入一些标签等。当然其实可以通过属性等操作,但是比较麻烦,直接写标签还是方便很多。 那么Vue提供了slot协助子组件对父容器写入的标签进行管理。插槽就是子组件中的提供给父组件使用的一个占位符,用<slot></slot> 表示,父组件可以在这个占位符中填充任何模板代码,如 HTML、组件等,填充的内容会替换子组件的<slot></slot>标签。

    从 vue@2.6.x 开始,Vue 为具名和范围插槽引入了一个全新的语法,即我们今天要讲的主角:v-slot 指令。目的就是想统一之前slot 和 slot-scope 语法,使代码更加规范和清晰。官方推荐我们使用 v-slot 来替代后两者。

    为了理解插槽,我们先看Vue2.6之前的写法:新建一个子组件Date.vue

<template>
  <div>
    <h1>天气状况:</h1>
    <!-- <slot></slot>标签内可以不写内容,写了会被当做默认内容显示 -->
    <slot>我是后备内容:默认值</slot>
  </div>
</template>
<script>
export default {
  name: 'child'
}
</script>

    新建一个父组件Weather.vue,注册Vue.component('v-weather', weather),在App.vue中使用

<template>
  <div>
    <div>使用slot分发内容</div>
    <div>
      <date>
        <div>20210301 多云,最高气温18度,最低气温6度,微风</div>
      </date>
      <date>
        <div>20210302 晴天,最高气温20度,最低气温8度,微风</div>
      </date>
      <date>
        <!-- 我不传内容,则使用<slot>标签内的默认内容,否则就会替换 -->
      </date>
    </div>
  </div>
</template>
<script>
import date from './Date.vue'
export default {
  name: 'Weather',
  components: {
    date
  }
}
</script>

案例效果:

    插槽<slot></slot> 中的内容,被父组件中写在子组件内部的内容(可以是字符、也可以是标签)替换了。如果插槽内有内容,会是默认内容,父组件提供了覆盖,未提供则默认。slot 和 slot-scope 这两个目前已被废弃但未被移除,后面都只介绍v-slot的使用。

2、匿名/具名插槽

    有时候,也许子组件内的slot不止一个,那么我们如何在父组件中,精确的在想要的位置,插入对应的内容呢?那就取个名字,一个不带 name 的 <slot> 出口会带有隐含的名字“default”。

    跟 v-on 和 v-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 #。例如 v-slot:header 可以被重写为 #header

    具名语法:<template v-slot:slotname>插入内容(字符或标签)</template>,<slot name='slotname'></slot>

    匿名语法:<template v-slot:default>插入内容(字符或标签)</template> 或者(default可省略)  <template v-slot>插入内容(字符或标签)</template>

    具名缩写:<template #slotname>插入内容(字符或标签)</template>,<slot name='slotname'></slot>  (匿名缩写不能省略default)

//在main.js中注册组件,带有两个插槽,我们先不给插槽指定名称
Vue.component('news-content', {
  template: `<div>
               <slot></slot>
               <div class='news'>content</div>
               <slot></slot>
             </div>`
})


//在App.vue里使用组件
<template>
  <div id="app">
    <news-content>
      <div class="header">header</div>
      <div class="footer">footer</div>
    </news-content>
  </div>
</template>

案例效果:

    如果没有指定名称,子组件接收多个插入内容,就会被一个slot占用,这时我们可以使用具名插槽,给插槽起一个名字,对传递内容进行唯一性的标识。

//main.js
Vue.component('news-content', {
  template: `<div>
               <slot name='header'></slot>
               <div class='news'>content</div>
               <slot name='footer'></slot>
             </div>`
})

//App.vue
<template>
  <div id="app">
    <news-content>
      <!-- v-slot 一般只能添加在 <template> 标签上-->
      <template v-slot:header>
        <div class="header">header</div>
      </template>
      <template v-slot:footer>
        <div class="footer">footer</div>
      </template>
    </news-content>
  </div>
</template>

案例效果:

3、作用域插槽

    vue2.6以前slot-scope作用域插槽:为了解决父组件模板的所有东西都会在父级作用域内编译,子组件模板的所有东西都会在子级作用域内编译,父组件的模板是无法使用到子组件模板中的数据,slot-scope的出现却实现了父组件调用子组件内部的数据,子组件的数据通过slot-scope属性传递到了父组件。

    语法:  v-slot:slotName="slotProps"    [slotProps --->  vue作用域的映射会映射子组件v-bind的数据]

    支持解构插槽 Prop语法:  v-slot:slotName="{vue作用域的映射:别名}"   

 语法一:
<!--绑定在<slot> 元素上的 attribute 被称为插槽 prop-->
<current-user v-slot:default="slotProps">
  {{ slotProps.user.firstName }}
</current-user>

<!--v-slot将包含所有插槽 prop 的对象命名为 slotProps(名字自定义)-->
 <template v-slot:slotName="slotProps">
    {{ slotProps.user.firstName }}
 </template>

 语法二:支持解构
<!--{{接受插槽 prop对象}}-->
<template v-slot:slotName="{user}">
    {{ user.firstName }}
 </template>

<!--{{user 重命名为 person}}-->
<template v-slot:slotName="{user: person}">
    {{ person.firstName }}
 </template>

<!--插槽名也支持动态名,语法:-->
<template v-slot:[dynamicSlotName]>
    ...
</template>

案例:在main.js中注册组件s-child和s-user

Vue.component('s-child', {
  data: function () {
    return {
      'user': [
        { id: 1, name: '张三', age: 20 },
        { id: 2, name: '李四', age: 22 },
        { id: 3, name: '王五', age: 27 },
        { id: 4, name: '张龙', age: 27 },
        { id: 5, name: '赵虎', age: 27 }
      ]
    }
  },
  template: `<div>
               <ul>
                <li class="list" v-for="item of user">
                    <slot name="itname" :items="item">{{item.name}}</slot>
                </li>
              </ul>
            </div>`
})

Vue.component('s-user', {
  data: function () {
    return {
      'user': { id: 1, name: '张三', age: 20 }
    }
  },
  template: `<div>
              <slot name="slotname" :users="user">{{user.name}}</slot>
            </div>`
})

在App.vue中使用

<template>
  <div id="app">
    <s-child>
      <template v-slot:itname="{items:uit}">
        <h3>{{uit.id+'-'+uit.name}}</h3>
      </template>
    </s-child>

    <s-user>
      <!--缩写 <template #slotname="us"> -->
      <template v-slot:slotname="us">
        <h3>{{us.users.id+'-'+us.users.name}}</h3>
      </template>
    </s-user>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
</style>

案例效果:

五、动态组件和异步组件

1、动态组件

1.1、内置组件component的使用

    组件的模板不会永远是固定的。应用可能会需要在运行期间加载一些新的组件或者内容。前面在介绍v-if指令的时候,对于切换简单组件或dom,直接使用v-if会更加直观,但是如果是复杂一点的组件,如果写一堆v-if就会显得有些臃肿。

    vue提供了一个内置组件component

component

  • Props

    • is - string | ComponentDefinition | ComponentConstructor
    • inline-template - boolean
  • 用法渲染一个“元组件”为动态组件。依 is 的值,来决定哪个组件被渲染。

<!-- 动态组件由 vm 实例的 `componentId` property 控制 -->
<component :is="componentId"></component>

<!-- 也能够渲染注册过的组件或 prop 传入的组件 -->
<component :is="$options.components.child"></component>

案例1:

    1)在components目录下新建dtzj/Acom.vue和dtzj/Bcom.vue,然后在App.vue中使用。

Acom.vue

<template>
  <span class="aclass">{{msg}}</span>
</template>

<script>
export default {
  name: 'Acom',
  data () {
    return {
      msg: '我是A组件'
    }
  }
}
</script>

<style scoped>
.aclass {
  color: red;
}
</style>

Bcom.vue

<template>
  <span class="bclass">{{msg}}</span>
</template>

<script>
export default {
  name: 'Acom',
  data () {
    return {
      msg: '我是B组件'
    }
  }
}
</script>

<style scoped>
.bclass {
  color: blue;
}
</style>

App.vue

<template>
  <div id="app">
    <component v-bind:is="currentTabComponent"></component><br />
    <button @click="changeACp">组件A</button>
    <button @click="changeBCp">组件B</button>
  </div>
</template>

<script>
import Acom from './components/dtzj/Acom'
import Bcom from './components/dtzj/Bcom'

export default {
  name: 'App',
  data () {
    return {
      currentTabComponent: Acom
    }
  },
  components: {
    Acom,
    Bcom
  },
  methods: {
    changeACp () {
      this.currentTabComponent = Acom
    },
    changeBCp () {
      this.currentTabComponent = Bcom
    }
  }
}
</script>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
</style>

案例效果:

案例2:

    在vue中,模板的位置有两种,一种是在组件内部定义,一种是在组件外部定义。前面我们使用的都是内部定义:在创建组件的时候定义template的。Vue提供了一种内联模板的功能,在使用组件时,给标签加上inline-complate特性,组件就会把它的内容当作模板,而不是当内容分发。

    我们对App.vue修改一下:

<template>
  <div id="app">
    <component v-bind:is="currentTabComponent"
               inline-template>
      <div>我是内联模板{{msg}}</div>
    </component><br />
    <button @click="changeACp">组件A</button>
    <button @click="changeBCp">组件B</button>
  </div>
</template>

<script>
import Acom from './components/dtzj/Acom'
import Bcom from './components/dtzj/Bcom'

export default {
  name: 'App',
  data () {
    return {
      currentTabComponent: Acom
    }
  },
  components: {
    Acom,
    Bcom
  },
  methods: {
    changeACp () {
      this.currentTabComponent = Acom
    },
    changeBCp () {
      this.currentTabComponent = Bcom
    }
  }
}
</script>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
</style>

案例效果:

    上诉案例中可见,内联的模板优先级更高。这种方式虽然灵活,但是给让我们组件的模版与其他属性难以分离。

1.2、内置组件keep-alive的使用

    我们先来看一个案例:在本章1.1的案例基础上,在A组件中新增一个方法,用于变更数据。

<template>
  <span class="aclass">{{msg}}
    <button @click="changemsg">变更数据</button>
  </span>
</template>

<script>
export default {
  name: 'Acom',
  data () {
    return {
      msg: '我是A组件'
    }
  },
  methods: {
    changemsg () {
      this.msg = '我是A组件变化后的数据'
    }
  }
}
</script>

<style scoped>
.aclass {
  color: red;
}
</style>

案例效果: 

    上诉案例中,当我们将A组件的数据变更后,在切换到B组件,但是当我们再回到A组件时,发现他的状态又复原了。如果在切回A组件的时候,我们要使其保持之前的状态呢?内置组件keep-alive就是用来做这个的。

keep-alive

  • Props

    • include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
    • exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
    • max - 数字。最多可以缓存多少组件实例。
  • 用法<keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 <transition> 相似,<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。

    当组件在 <keep-alive> 内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行。

<!-- 逗号分隔字符串 -->
<keep-alive include="a,b">
  <component :is="view"></component>
</keep-alive>

<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
  <component :is="view"></component>
</keep-alive>

<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
  <component :is="view"></component>
</keep-alive>

匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值)。匿名组件不能被匹配。

案例:修改App.vue,对A组件缓存

<template>
  <div id="app">
    <keep-alive include='Acom'>
      <component v-bind:is="currentTabComponent"></component>
    </keep-alive>
    <br />
    <button @click="changeACp">组件A</button>
    <button @click="changeBCp">组件B</button>
  </div>
</template>

<script>
import Acom from './components/dtzj/Acom'
import Bcom from './components/dtzj/Bcom'

export default {
  name: 'App',
  data () {
    return {
      currentTabComponent: Acom
    }
  },
  components: {
    Acom,
    Bcom
  },
  methods: {
    changeACp () {
      this.currentTabComponent = Acom
    },
    changeBCp () {
      this.currentTabComponent = Bcom
    }
  }
}
</script>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
</style>

案例效果:

    只对A组件缓存,当从B组件切回A组件时,A组件的状态一直都保持在在切换前,而B组件被复原。include='Acom' Acom为组件A中的name属性。

2、异步组件

    Vue作为单页面应用遇到最棘手的问题是首屏加载时间的问题,单页面应用会把页面脚本打包成一个文件,这个文件包含着所有业务和非业务的代码,而脚本文件过大也是造成首页渲染速度缓慢的原因。如果一个页面有成百上千个组件,倘若这些组件都要一起加载的话,进入页面就会显得很慢。我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。

案例:

    当我们首次运行1.2中的案例,首次加载时间是193ms

    现在使用异步加载的方式,修改App.vue,将组件B的加载方式改为:const Bcom = () => import('./components/dtzj/Bcom')

<template>
  <div id="app">
    <!-- include='Acom' 为组件中定义的name属性,这里只对组件A缓存 -->
    <keep-alive include='Acom'>
      <component v-bind:is="currentTabComponent"></component>
    </keep-alive>
    <br />
    <button @click="changeACp">组件A</button>
    <button @click="changeBCp">组件B</button>
  </div>
</template>

<script>
import Acom from './components/dtzj/Acom'
// import Bcom from './components/dtzj/Bcom'
const Bcom = () => import('./components/dtzj/Bcom')

export default {
  name: 'App',
  data () {
    return {
      currentTabComponent: Acom
    }
  },
  components: {
    Acom,
    Bcom
  },
  methods: {
    changeACp () {
      this.currentTabComponent = Acom
    },
    changeBCp () {
      this.currentTabComponent = Bcom
    }
  }
}
</script>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
</style>

案例效果:

    由于内容不多,时间上相差不大,但是异步的生成了一个1.js,这个就是在使用B组件时加载的Bcom.vue,由webpack帮我们处理。

    异步组件也可以分为全局异步和局部异步。2.3.0+ 新增的异步组件工厂函数也可以返回一个如下格式的对象:

const AsyncComponent = () => ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  component: import('./MyComponent.vue'),
  // 异步组件加载时使用的组件
  loading: LoadingComponent,
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
})

六、过渡&动画

1、过渡和动画的由来

    过渡(transition)和动画(animation)原是CSS3中具有颠覆性的特征之一,我们可以在不使用 Flash 动画或 JavaScript 的情况下,当元素从一种样式变换为另一种样式时为元素添加效果,他们都是随着时间改变元素的属性值。

    过渡:就是使瞬间的样式变化,按照一定方式变得缓慢平缓(有时候平缓一些看着还是比较舒适的);过渡主要描绘的是transtion:过渡属性 过渡所需要时间 过渡动画函数 过渡延迟时间。

img{
    transition: height 0.5s linear 0.5s;
}

    动画:就是一帧一帧图片连续切换实现的效果,关键帧就是里面主要的一些帧;(CSS3的动画是个很不错的技术,基本能取代一些gif,javascript,flash等)。动画主要描绘的是@keyframes(关键帧),通过控制关键帧来控制动画的每一步,实现更为复杂的动画效果。

img{
  animation mymove 1s infinite; //animation 所有动画属性的简写属性,除了 animation-play-state 属性。
}

@keyframes mymove  //@keyframes 规定动画。一个动画中可以有很多个帧(创建多个百分比,从而达到一种不断变化的效果)
{
0% {top:0px;}
75% {top:300px;}
84% {top:240px;}
100% {top:300px;}
}
  
//0% 100%的百分号都不能省略,0%可以由from代替,100%可以由to代替。要让mymove动画有效果,就必须要通过CSS3 animation属性来调用它。

    过渡(transition)和动画(animation)的主要区别在于:1)transition需要触发一个事件才会随着时间改变其CSS属性;animation在不需要触发任何事件的情况下,也可以显式的随时间变化来改变元素CSS属性,达到一种动画的效果。2)过渡只有一组(两个:开始-结束) 关键帧,动画可以设置多个(多个百分比)。

    而在Vue中,插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果。包括以下工具:

  • 在 CSS 过渡和动画中自动应用 class
  • 可以配合使用第三方 CSS 动画库,如 Animate.css
  • 在过渡钩子函数中使用 JavaScript 直接操作 DOM
  • 可以配合使用第三方 JavaScript 动画库,如 Velocity.js

    在Vue中动画和过渡类似,正如官方所说,唯一的区别是:动画与过渡的区别是在动画中 v-enter 类名在节点插入 DOM 后不会立即删除,而是在 animationend 事件触发时删除。

    其次就是动画和过渡transition对应的的css属性不同。

1.1、CSS3过渡的语法及属性

    过渡的语法:transition: property duration timing-function delay; 即:transition: 过渡属性 过渡所需要时间 过渡动画函数 过渡延迟时间 

描述语法
transition-property指定CSS属性的name,transition效果默认值为:all   transition: none|all| property; 可具体制定css属性
transition-durationtransition效果需要指定多少秒或毫秒才能完成默认值为:0     transition:time;
transition-timing-function指定transition效果的转速曲线

默认值为:ease

transition: linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(n,n,n,n);

ease 规定慢速开始,然后变快,然后慢速结束的过渡效果 (等于cubic-bezier(0.25,0.1,0.25,1))。

linear 规定以相同速度开始至结束的过渡效果(等于 cubic-bezier(0,0,1,1))

transition-delay定义transition效果开始的时候默认值:0    transition:time

1.2、CSS3动画的语法及属性

    动画语法:animation: name duration timing-function delay iteration-count direction fill-mode play-state;即:animation:关键帧名 动画完成时间 完成周期 延迟时间 动画播放次数 方向 不播放时的样式  制定播放状态

说明语法
animation-name指定要绑定到选择器的关键帧的名称默认值:none   animation:keyframename|none;
animation-duration动画指定需要多少秒或毫秒完成默认值:0  animation:time;
animation-timing-function设置动画将如何完成一个周期

默认值:ease 

animation: linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(n,n,n,n);

animation-delay设置动画在启动前的延迟间隔。默认值:0   animation:time
animation-iteration-count定义动画的播放次数。默认值:1 animation:value
animation-direction指定是否应该轮流反向播放动画。

默认值:normal 

animation: normal|reverse|alternate|alternate-reverse|initial|inherit;

animation-fill-mode规定当动画不播放时(当动画完成时,或当动画有一个延迟未开始播放时),要应用到元素的样式。

默认值:none

animation: none|forwards|backwards|both|initial|inherit;

animation-play-state指定动画是否正在运行或已暂停。

默认值:running 

animation: paused|running;

2、内置组件transition

  • Props

    • name - string,用于自动生成 CSS 过渡类名。例如:name: 'fade' 将自动拓展为 .fade-enter.fade-enter-active 等。默认类名为 "v"
    • appear - boolean,是否在初始渲染时使用过渡。默认为 false
    • css - boolean,是否使用 CSS 过渡类。默认为 true。如果设置为 false,将只通过组件事件触发注册的 JavaScript 钩子。
    • type - string,指定过渡事件类型,侦听过渡何时结束。有效值为 "transition" 和 "animation"默认 Vue.js 将自动检测出持续时间长的为过渡事件类型。
    • mode - string,控制离开/进入过渡的时间序列。有效的模式有 "out-in" 和 "in-out";默认同时进行。
    • duration - number | { enter: number, leave: number } 指定过渡的持续时间。默认情况下,Vue 会等待过渡所在根元素的第一个 transitionend 或 animationend 事件。
    • enter-class - string
    • leave-class - string
    • appear-class - string
    • enter-to-class - string
    • leave-to-class - string
    • appear-to-class - string
    • enter-active-class - string
    • leave-active-class - string
    • appear-active-class - string
  • 事件

    • before-enter
    • before-leave
    • before-appear
    • enter
    • leave
    • appear
    • after-enter
    • after-leave
    • after-appear
    • enter-cancelled
    • leave-cancelled (v-show only)
    • appear-cancelled
  • 用法

    <transition> 元素作为单个元素/组件的过渡效果。<transition> 只会把过渡效果应用到其包裹的内容上,而不会额外渲染 DOM 元素,也不会出现在可被检查的组件层级中。

    扩展:<transtion> 组件的实现是 export 出 一个对象,它会将预先设定好的 props 绑定到 transition 上,可以对transitionProps中定义的样式进行任意形式的重写。对于CSS过渡的,通过在适当的时机添加和移除transition class实现,对于js过渡的,只需要在适当的时间调用绑定注册的事件回调即可。

export default {
  name: 'transition',
  props: transitionProps,
  abstract: true,
  render (h: Function) {
    //render ...处理
  }
}

export const transitionProps = {
  name: String,
  appear: Boolean,
  css: Boolean,
  mode: String,
  type: String,
  enterClass: String,
  leaveClass: String,
  enterToClass: String,
  leaveToClass: String,
  enterActiveClass: String,
  leaveActiveClass: String,
  appearClass: String,
  appearActiveClass: String,
  appearToClass: String,
  duration: [Number, String, Object]
}

3、内置组件transition-group

  • Props

    • tag - string,默认为 span
    • move-class - 覆盖移动过渡期间应用的 CSS 类。
    • 除了 mode,其他 attribute 和 <transition> 相同。
  • 事件

    • 事件和 <transition> 相同。
  • 用法

    <transition-group> 元素作为多个元素/组件的过渡效果。<transition-group> 渲染一个真实的 DOM 元素。默认渲染 <span>,可以通过 tag attribute 配置哪个元素应该被渲染。

    注意,每个 <transition-group> 的子节点必须有独立的 key,动画才能正常工作

    <transition-group> 支持通过 CSS transform 过渡移动。当一个子节点被更新,从屏幕上的位置发生变化,它会被应用一个移动中的 CSS 类 (通过 name attribute 或配置 move-class attribute 自动生成)。如果 CSS transform property 是“可过渡”property,当应用移动类时,将会使用 FLIP 技术使元素流畅地到达动画终点。

4、单元素/组件的过渡

    Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡:

  • 条件渲染 (使用 v-if)
  • 条件展示 (使用 v-show)
  • 动态组件
  • 组件根节点

4.1、过渡的类名

    Vue为过渡(Transitions)定义了6个class类名(进入/离开):

    进入:

    1)v-enter:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。--》"出现"开始的样子

    2)v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。--》"出现"过程的定义(时间,延迟和曲线函数)

    3)v-enter-to2.1.8 版及以上定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter 被移除),在过渡/动画完成之后移除。-》"出现"最后的样子

    离开:

    1)v-leave:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。--》"消失"开始的样子

    2)v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。--》 "消失"过程的定义(时间,延迟和曲线函数)

    3)v-leave-to2.1.8 版及以上定义离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave 被删除),在过渡/动画完成之后移除。--》"消失"最后的样子

4.2、CSS 过渡基本使用

    新建一个trans.vue,然后在App.vue中注册并使用。

<template>
  <div><button v-on:click="show = !show">{{ show ? '隐藏' : '显示'}}</button>
    <!-- name="fade"CSS过渡类名,默认类名为"v"-->
    <transition name="fade">
      <p v-show="show">hello !</p>
    </transition>
  </div>
</template>

<script>
export default {
  data () {
    return {
      show: true
    }
  }
}
</script>

<style scoped>
/* 过渡类名fade的进入过程的定义(时间,延迟和曲线函数)  过渡的属性为:opacity 透明度*/
.fade-enter-active {
  transform: translateY(10px); /*从下面10px的地方开始 */
  transition: opacity 0.9s;
}
/* 过渡类名fade的离开过程的定义(时间,延迟和曲线函数)  过渡的属性为:opacity 透明度*/
.fade-leave-active {
  transition: opacity 0.8s cubic-bezier(1, 0.5, 0.8, 1);
}
/* 定义:进入和离开开始的样子 过渡的属性为:opacity 透明度*/
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
</style>

案例效果:

4.3、CSS 动画基本使用

    新建一个Anim1.vue,然后在App.vue中注册并使用。

<template>
  <div><button v-on:click="show = !show">{{ show ? '隐藏' : '显示'}}</button>
    <transition name="bounce">
      <p v-show="show">hello !</p>
    </transition>
  </div>
</template>

<script>
export default {
  data () {
    return {
      show: true
    }
  }
}
</script>

<style scoped>
/* 过渡类名bounce的进入过程的定义(时间,延迟和曲线函数) bounce-in 弹跳*/
.bounce-enter-active {
  animation: bounce-in 0.5s;
}
.bounce-leave-active {
  animation: bounce-in 0.5s reverse;
}
@keyframes bounce-in {
  /* 弹跳关键帧动画*/
  0% {
    transform: scale(0);
  }
  50% {
    transform: scale(1.5);
  }
  100% {
    transform: scale(1);
  }
}
</style>

案例效果: 

4.4、自定义过渡的类名

我们可以通过以下 attribute 来自定义过渡类名:

  • enter-class
  • enter-active-class
  • enter-to-class (2.1.8+)
  • leave-class
  • leave-active-class
  • leave-to-class (2.1.8+)

他们的优先级高于普通的类名,这对于 Vue 的过渡系统和其他第三方 CSS 动画库,如 Animate.css 结合使用十分有用。

案例:

<template>
  <div><button v-on:click="show = !show">{{ show ? '隐藏' : '显示'}}</button>
    <link href="https://cdn.jsdelivr.net/npm/animate.css@3.5.1"
          rel="stylesheet"
          type="text/css">
    <!-- https://animate.style/ 可以在这里看animate.css的特效 -->
    <transition name="custom-classes-transition"
                enter-active-class="animated tada"
                leave-active-class="animated bounceOutRight">
      <p v-if="show">hello</p>
    </transition>
  </div>
</template>

<script>
export default {
  data () {
    return {
      show: true
    }
  }
}
</script>

<style scoped>
</style>

案例效果:

4.5、同时使用过渡和动画

    如果在一个组件或者标签中我们要同时使用过度和动画应该怎么做呢?

案例一:Anim1.vue,不指定类型,也不显示指定时间

<template>
  <div>
    <link href="https://cdn.jsdelivr.net/npm/animate.css@3.5.1"
          rel="stylesheet"
          type="text/css">
    <!-- https://animate.style/ 可以在这里看animate.css的特效
    animate.css执行动画的默认时间是1s
    appear元素出现时执行的动画animation
    显示的指定动画时间:duration="2000" 或者 :duration="{enter:2000,leave:2000}"
    声明你需要 Vue 监听的类型type="transition" type="animation" -->
    <transition name="fade"
                appear
                appear-active-class="animated swing"
                enter-active-class="animated swing fade-enter-active"
                leave-active-class="animated swing fade-leave-active">
      <div v-show="show">Hello World</div>
    </transition>
    <button @click="handleClick">toggle</button>
  </div>
</template>

<script>
export default {
  data () {
    return {
      show: true
    }
  },
  methods: {
    handleClick () {
      this.show = !this.show
    }
  }
}
</script>

<style scoped>
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
.fade-enter-active {
  transition: opacity 3s;
}

.fade-leave-active {
  transition: opacity 5s;
}
</style>

案例效果:

    首次进入有抖动效果。当我点击隐藏的时候,渐变执行5秒后隐藏,当我执行显示的时候抖动后渐变3秒显示。

案例二:Anim1.vue,指定动画时间

<template>
  <div>
  <link href="https://cdn.jsdelivr.net/npm/animate.css@3.5.1"
          rel="stylesheet"
          type="text/css">
    <!-- https://animate.style/ 可以在这里看animate.css的特效
    animate.css执行动画的默认时间是1s
    appear元素出现时执行的动画animation
    显示的指定动画时间:duration="2000" 或者 :duration="{enter:2000,leave:2000}单位ms"
    声明你需要 Vue 监听的类型type="transition" type="animation"
    :duration="{enter:2000,leave:1000} 指定时间两秒 则过渡2秒 离开动画过渡1s-->
    <transition name="fade"
                appear
                :duration="{enter:2000,leave:1000}"
                appear-active-class="animated swing"
                enter-active-class="animated swing fade-enter-active"
                leave-active-class="animated bounceOutRight fade-leave-active">
      <div v-show="show">Hello World</div>
    </transition>
    <button @click="handleClick">toggle</button>
  </div>
</template>

<script>
export default {
  data () {
    return {
      show: true
    }
  },
  methods: {
    handleClick () {
      this.show = !this.show
    }
  }
}
</script>

<style scoped>
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
.fade-enter-active {
  transition: opacity 3s;
}

.fade-leave-active {
  transition: opacity 5s;
}
</style>

案例效果:

    指定时间动画因为是animate.css执行动画的默认时间1s,指定动画时间,则覆盖了过渡的时间。

案例三:Anim1.vue,指定类型。官网说:给同一个元素同时设置两种过渡动效,比如 animation 很快的被触发并完成了,而 transition 效果还没结束。在这种情况中,你就需要使用 type attribute 并设置 animation 或 transition 来明确声明你需要 Vue 监听的类型。什么意思呢?其实就是用type 类型的时间作为默认时间。

    我们将Anim1.vue,<transition type="animation"></transition>指定为动画类型,则过渡类型的时间被覆盖为1s。

    我们案例中指定类型为type="transition"呢?我们发现首次抖动不再执行了,是因为appear-active-class后面并没有指定过渡css,则使用其默认值0

<template>
  <div>
  <link href="https://cdn.jsdelivr.net/npm/animate.css@3.5.1"
          rel="stylesheet"
          type="text/css">
    <!-- https://animate.style/ 可以在这里看animate.css的特效
    animate.css执行动画的默认时间是1s
    appear元素出现时执行的动画animation
    显示的指定动画时间:duration="2000" 或者 :duration="{enter:2000,leave:2000}单位ms"
    声明你需要 Vue 监听的类型type="transition" type="animation"
    :duration="{enter:2000,leave:1000} 指定时间两秒 则过渡2秒 离开动画过渡1s
    若指定为type="transition"则appear-active-class发现不会执行了,因为其后面没有定义transition的css 其值被默认为0-->
    <transition name="fade"
                appear
                type="transition"
                appear-active-class="animated swing"
                enter-active-class="animated swing fade-enter-active"
                leave-active-class="animated bounceOutRight fade-leave-active">
      <div v-show="show">Hello World</div>
    </transition>
    <button @click="handleClick">toggle</button>
  </div>
</template>

<script>
export default {
  data () {
    return {
      show: true
    }
  },
  methods: {
    handleClick () {
      this.show = !this.show
    }
  }
}
</script>

<style scoped>
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
.fade-enter-active {
  transition: opacity 3s;
}

.fade-leave-active {
  transition: opacity 5s;
}
</style>

4.6、JavaScript 钩子

    内置组件transition除了提供了很多的props,支持 CSS 过渡和动画中自动应用 class,同时也提供了不少事件,即过渡钩子函数中使用 JavaScript 直接操作 DOM。这个和css的类似,就直接引用官网的例子。

案例:使用第三方 JavaScript 动画库,如 Velocity.js,没安装的可以先安装:

    安装:npm install velocity-animate --save-dev 引入: import  Velocity from 'velocity-animate'

<template>
  <div>
    <button @click="show = !show">
      Toggle
    </button>
    <transition v-on:before-enter="beforeEnter"
                v-on:enter="enter"
                v-on:leave="leave"
                v-bind:css="false">
      <p v-if="show">
        Demo
      </p>
    </transition>
  </div>
</template>
<script>
import Velocity from 'velocity-animate'
export default {
  data () {
    return {
      show: false
    }
  },
  methods: {
    beforeEnter (el) {
      el.style.opacity = 0
      el.style.transformOrigin = 'left'
    },
    enter (el, done) {
      Velocity(el, { opacity: 1, fontSize: '1.4em' }, { duration: 300 })
      Velocity(el, { fontSize: '1em' }, { complete: done })
    },
    leave (el, done) {
      Velocity(el, { translateX: '15px', rotateZ: '50deg' }, { duration: 600 })
      Velocity(el, { rotateZ: '100deg' }, { loop: 2 })
      Velocity(el, {
        rotateZ: '45deg',
        translateY: '30px',
        translateX: '30px',
        opacity: 0
      }, { complete: done })
    }
  }
}
</script>

<style scoped>
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
.fade-enter-active {
  transition: opacity 3s;
}

.fade-leave-active {
  transition: opacity 5s;
}
</style>

案例效果:

5、多个元素的过渡

    官网上有一句话:当有相同标签名的元素切换时,需要通过 key attribute 设置唯一的值来标记以让 Vue 区分它们,否则 Vue 为了效率只会替换相同标签内部的内容。即使在技术上没有必要,给在 <transition> 组件中的多个元素设置 key 是一个更好的实践。

案例一:新建一个MoreCon.vue,并在App.vue中使用

<template>
  <div>
    <transition name="fade">
      <!-- Vue 在两个元素进行切换的时候,会尽量复用dom,如果删除key则过渡效果无效
      只需要给这两个div不同的key值就不会复用 -->
      <div v-if="show"
           key="hello">hello world</div>
      <div v-else
           key="bye">bye world</div>
    </transition>
    <button @click="handleClick">切换</button>
  </div>
</template>

<script>
export default {
  data () {
    return {
      show: true
    }
  },
  methods: {
    handleClick () {
      this.show = !this.show
    }
  }
}
</script>

<style scoped>
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
.fade-enter-active,
.fade-leave-active {
  transition: opacity 3s;
}
</style>

案例效果:

5.1、过渡模式

案例一:使用transform(元素的2D或3D转换。这个属性允许你将元素旋转,缩放,移动,倾斜等)来实现平滑移动效果。

<template>
  <div>
    <link href="https://cdn.jsdelivr.net/npm/animate.css@3.5.1"
          rel="stylesheet"
          type="text/css">
    <transition name="fade">
      <!-- Vue 在两个元素进行切换的时候,会尽量复用dom,如果删除key则过渡效果无效
      只需要给这两个div不同的key值就不会复用 -->
      <div v-if="show"
           key="hello"
           class="hello">hello world</div>
      <div v-else
           key="bye"
           class="bye">bye world</div>
    </transition>
    <br>
    <button @click="handleClick">切换</button>
  </div>
</template>

<script>
export default {
  data () {
    return {
      show: true
    }
  },
  methods: {
    handleClick () {
      this.show = !this.show
    }
  }
}
</script>

<style scoped>
.fade-enter {
  opacity: 0;
  transform: translateX(-20px);
}
.fade-leave-to {
  opacity: 0;
}
.fade-enter-active {
  transition: all 1s ease-in;
}
.fade-leave-active {
  transform: translateX(20px);
  transition: all 1s ease-in-out;
}
div {
  position: fixed;
}
</style>

案例效果:     

    案例一中的过渡,看着感觉总有一点不和谐。同时生效的进入和离开的过渡不能满足所有要求,所以 Vue 提供了过渡模式。

    in-out:新元素先进行过渡,完成之后当前元素过渡离开。

  out-in:当前元素先进行过渡,完成之后新元素过渡进入。

    我们加上过渡模式再试下效果:<transition name="fade" mode="out-in">  out-in先隐藏再出现

<!-- in-out先出现再隐藏 out-in先隐藏再出现mode="out-in"-->
    <transition name="fade"
                mode="out-in">
      <!-- Vue 在两个元素进行切换的时候,会尽量复用dom,如果删除key则过渡效果无效
      只需要给这两个div不同的key值就不会复用 -->
      <div v-if="show"
           key="hello"
           class="hello">hello world</div>
      <div v-else
           key="bye"
           class="bye">bye world</div>
    </transition>

案例效果:

6、多个组件的过渡

    多个组件的过渡简单很多 - 我们不需要使用 key attribute。相反,我们只需要使用动态组件

案例:

<template>
  <div>
    <input type="radio"
           name="only"
           v-model="view"
           value="v-a">A
    <input type="radio"
           name="only"
           value="v-b"
           v-model="view">B<br>
    <transition name="component-fade"
                mode="out-in">
      <component v-bind:is="view"></component>
    </transition>
  </div>
</template>

<script>
export default {
  data () {
    return {
      view: 'v-a'
    }
  },
  methods: {
    handleClick () {
      this.show = !this.show
    }
  },
  components: {
    'v-a': {
      template: '<div>Component A</div>'
    },
    'v-b': {
      template: '<div>Component B</div>'
    }
  }
}
</script>

<style scoped>
.component-fade-enter-active,
.component-fade-leave-active {
  transition: opacity 0.3s ease;
}
.component-fade-enter, .component-fade-leave-to
/* .component-fade-leave-active for below version 2.1.8 */ {
  opacity: 0;
}
</style>

案例效果:

 7、列表过渡

    我们主要介绍了 <transition> 组件,它针对单一元素的 enter 以及 leave 阶段进行了过渡效果的封装处理,使得我们只需关注 css 和 js 钩子函数的业务实现即可。

    那么怎么同时渲染整个列表,比如使用 v-for?在这种场景中,使用 <transition-group> 组件。在我们深入例子之前,先了解关于这个组件的几个特点:

  • 不同于 <transition>,它会以一个真实元素呈现:默认为一个 <span>。你也可以通过 tag attribute 更换为其他元素。
  • 过渡模式不可用,因为我们不再相互切换特有的元素。
  • 内部元素总是需要提供唯一的 key attribute 值。
  • CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身。

7.1、列表的进入/离开/排序过渡

    <transition-group> 元素作为多个元素/组件的过渡效果。可以通过 tag attribute 配置哪个元素应该被渲染。简单的说:tag就是表示用何总元素进行包裹,默认使用span进行包裹。

  <transition-group> 组件还有一个特殊之处。不仅可以进入和离开动画,还可以改变定位。使动画显得更平缓,其内部的实现,Vue 使用了一个叫 FLIP 简单的动画队列,
使用 transforms 将元素从之前的位置平滑过渡到新的位置。要使用这个新功能只需了解新增的 v-move class,它会在元素的改变定位的过程中应用。像之前的类名一样,可以通过 name attribute 来自定义前缀,也可以通过 move-class attribute 手动设置。v-move 对于设置过渡的切换时机和过渡曲线非常有用。

    需要注意的是使用 FLIP 过渡的元素不能设置为 display: inline 。作为替代方案,可以设置为 display: inline-block 或者放置于 flex 中。

扩展:

    FLIP是一种记忆设备和技术,最早是由@Paul Lewis提出的,FLIP是First、Last、Invert和Play四个单词首字母的缩写。FLIP 将一些性能低下的动画映射为 transform 动画。通过记录元素的两个快照,一个是元素的初始位置(First – F),另一个是元素的最终位置(Last – L),然后对元素使用一个 transform 变换来反转(Invert – I),让元素看起来还在初始位置,最后移除元素上的 transform 使元素由初始位置运动(Play – P)到最终位置。它就是通过这样一种高性能的方式来动态的改变DOM元素的位置和尺寸,而不需要管它的布局是如何计算或渲染的(比如,height、width、float、绝对定位、Flexbox和Grid等)。

案例一:新建一个List.vue

<template>
  <div id="list-complete-demo"
       class="demo">
    <button v-on:click="shuffle">Shuffle</button>
    <button v-on:click="add">Add</button>
    <button v-on:click="remove">Remove</button>
    <transition-group name="list-complete"
                      tag="p">
      <span v-for="item in items"
            v-bind:key="item"
            class="list-complete-item">
        {{ item }}
      </span>
    </transition-group>
  </div>
</template>

<script>
// npm i --save lodash 没安装先安装
// 使用方式使用import 或者 let _ = require('lodash')
import _ from 'lodash'

export default {
  data () {
    return {
      items: [1, 2, 3, 4, 5, 6, 7, 8, 9],
      nextNum: 10
    }
  },
  methods: {
    randomIndex () {
      return Math.floor(Math.random() * this.items.length)
    },
    add () {
      this.items.splice(this.randomIndex(), 0, this.nextNum++)
    },
    remove () {
      this.items.splice(this.randomIndex(), 1)
    },
    shuffle () {
      this.items = _.shuffle(this.items)
    }
  }
}
</script>

<style scoped>
.list-complete-item {
  transition: all 1s;
  display: inline-block;
  margin-right: 10px;
}
.list-complete-enter, .list-complete-leave-to
/* .list-complete-leave-active for below version 2.1.8 */ {
  opacity: 0;
  transform: translateY(30px);
}
.list-complete-leave-active {
  position: absolute;
}
/* v-move --> list-complete-move 是当:key对应的元素位置发生变化时添加的样式,如果key值不发生变化则认为没有移动,只更新DOM*/
.list-complete-move {
  color: red;
}
</style>

案例效果:

    v-bind:key="item",绑定的key值如果未发生变化,Vue是不会改变位置的,案例中的有些颜色不变的就是位置就是key值没变,只会更新DOM,这个在Vue详细介绍及使用第一篇文章中有介绍到。

案例二:

<template>
  <div id="list-complete-demo"
       class="demo">
    <button v-on:click="shuffle">Shuffle</button>
    <button v-on:click="add"
            style="display:none">Add</button>
    <button v-on:click="remove"
            style="display:none">Remove</button>
    <br>
    <transition-group name="list-complete"
                      tag="div">
      <span v-for="item in items"
            v-bind:key="item"
            class="list-complete-item">
        {{ item }}
      </span>
    </transition-group>
  </div>
</template>

<script>
// npm i --save lodash 没安装先安装
// 使用方式使用import 或者 let _ = require('lodash')
import _ from 'lodash'

export default {
  data () {
    return {
      items: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30],
      nextNum: 10
    }
  },
  methods: {
    randomIndex () {
      return Math.floor(Math.random() * this.items.length)
    },
    add () {
      this.items.splice(this.randomIndex(), 0, this.nextNum++)
    },
    remove () {
      this.items.splice(this.randomIndex(), 1)
    },
    shuffle () {
      this.items = _.shuffle(this.items)
    }
  }
}
</script>

<style scoped>
.list-complete-item {
  transition: all 1s;
  display: inline-block;
  margin-right: 0px;
}
.list-complete-enter, .list-complete-leave-to
/* .list-complete-leave-active for below version 2.1.8 */ {
  opacity: 0;
  transform: translateY(30px);
}
.list-complete-leave-active {
  position: absolute;
}
/* v-move --> list-complete-move 是当:key对应的元素位置发生变化时添加的样式,如果key值不发生变化则认为没有移动,只更新DOM*/
.list-complete-move {
  color: red;
}
span {
  border: blue 1px solid;
  height: 30px;
  width: 30px;
}
.demo div {
  height: 162px;
  width: 192px;
  border: tomato 1px dashed;
  margin-left: 4px;
  margin-top: 4px;
}
.demo {
  height: 192px;
  width: 202px;
  border: tomato 1px dashed;
}
</style>

案例效果:

7.2、列表的交错过渡

    通过 data attribute 与 JavaScript 通信,就可以实现列表的交错过渡。

<template>
  <div id="staggered-list-demo">
    <input v-model="query">
    <transition-group name="staggered-fade"
                      tag="ul"
                      v-bind:css="false"
                      v-on:before-enter="beforeEnter"
                      v-on:enter="enter"
                      v-on:leave="leave">
      <li v-for="(item, index) in computedList"
          v-bind:key="item.msg"
          v-bind:data-index="index">{{ item.msg }}</li>
    </transition-group>
  </div>
</template>

<script>
// npm install velocity-animate@beta 没安装先安装
import Velocity from 'velocity-animate'

export default {
  data () {
    return {
      query: '',
      list: [
        { msg: 'Bruce Lee' },
        { msg: 'Jackie Chan' },
        { msg: 'Chuck Norris' },
        { msg: 'Jet Li' },
        { msg: 'Kung Fury' }
      ]
    }
  },
  computed: {
    computedList () {
      var vm = this
      return this.list.filter(function (item) {
        return item.msg.toLowerCase().indexOf(vm.query.toLowerCase()) !== -1
      })
    }
  },
  methods: {
    beforeEnter (el) {
      el.style.opacity = 0
      el.style.height = 0
    },
    enter (el, done) {
      var delay = el.dataset.index * 150
      setTimeout(function () {
        Velocity(
          el,
          { opacity: 1, height: '1.6em' },
          { complete: done }
        )
      }, delay)
    },
    leave (el, done) {
      var delay = el.dataset.index * 150
      setTimeout(function () {
        Velocity(
          el,
          { opacity: 0, height: 0 },
          { complete: done }
        )
      }, delay)
    }
  }
}
</script>

案例效果:

8、动态过渡和可复用过渡

    在 Vue 中即使是过渡也是数据驱动的!动态过渡最基本的例子是通过 name attribute 来绑定动态值。当你想用 Vue 的过渡系统来定义的 CSS 过渡/动画在不同过渡间切换会非常有用。

    过渡可以通过 Vue 的组件系统实现复用。要创建一个可复用过渡组件,你需要做的就是将 <transition> 或者 <transition-group> 作为根组件,然后将任何子组件放置在其中就可以了。关于这部分可以在官网中查看(可复用的过渡动态过渡

9、状态过渡

    官网对于状态过渡的描述:Vue 的过渡系统提供了非常多简单的方法设置进入、离开和列表的动效。那么对于数据元素本身的动效呢,比如:

  • 数字和运算
  • 颜色的显示
  • SVG 节点的位置
  • 元素的大小和其他的 property

    这些数据要么本身就以数值形式存储,要么可以转换为数值。有了这些数值后,我们就可以结合 Vue 的响应式和组件系统,使用第三方库来实现切换元素的过渡状态。那么什么是过渡状态呢?这还是很抽象,不过大致可以理解为从一个状态到另一种状态变化的过程。

    通常我们将用户界面描述为一种状态。一个状态定义了一组属性的改变,并且会在一定的条件下被触发。另外在这些状态转化的过程中可以有一个过渡,定义了这些属性的动画或者一些附加的动作(变化过程),当进入一个新的状态时,动作也可以被执行。

9.1、状态动画与侦听器

    官网使用了第三方动画库GSAP 来实现状态的过渡,GSAP JS是GreenSock公司新出的一个2D动画引擎,是一个JavaScript库,用于创建高性能、零依赖、跨浏览器动画,可以与React、Vue、Angular和vanilla JS协同工作。

    GSAP有几大核心模块,它们是:

  • TweenLite:GSAP的基础,一个轻量级和快速的HTML5动画库
  • TweenMax:TweenLite的扩展,除了包括TweenLite本身之外,还包括TimelineLite、TimelineMax、CSSPlugin、AttrPlugin、RoundPropsPlugin、DirectionalRotationPlugin、BezierPlugin和EasePack
  • TimelineLite:一种轻量级的Timeline,用于控制多个Tween和(或)其他Timeline
  • TimelineMax:一个增强版的TimelineLite,它提供了额外的、非必要的功能,如repeatrepeatDelayyoyo等等

    要了解什么是GSAP,就要了解Tweens(补间)和Timelines(时间轴)。前面我们接触的CSS Animations 和 Transitions,控制动画时序主要依赖的是动画的持续时间(Duration Timeline)和 延迟时间(Delay Timeline)。

   Tweens:翻译成补间,是描述一帧一帧序列的术语是指在两个独立对象之间创建过渡帧的过程。TweenLite包含了一些方法,这些方法可让您精确控制每个补间。随时播放,暂停,倒退和调整(速度)。

   Timelines:主要是用来控制动画对象对应的动画效果的播放时间和持续时间。在GSAP的世界中,时间轴就非常的强大,除了GSAP自带的时间轴API(即gsap.timeline())之外,还有 TimelineLite()TimelineMax()、 TweenLite 和 TweenMax

案例一:

<template>
  <div id="animated-number-demo">
    <input v-model.number="number"
           type="number"
           step="20">
    <p>{{ animatedNumber }}</p>
  </div>
</template>

<script>
// 使用了第三方动画库:GSAP 是一个JavaScript库,用于创建高性能、零依赖、跨浏览器动画,可以与React、Vue、Angular和vanilla JS协同工作。
// npm install gsap 没安装先安装
import gsap from 'gsap'

export default {
  data () {
    return {
      number: 0,
      tweenedNumber: 0
    }
  },
  computed: {
    animatedNumber: function () {
      return this.tweenedNumber.toFixed(0)
    }
  },
  watch: {
    number: function (newValue) {
      gsap.to(this.$data, { duration: 0.5, tweenedNumber: newValue })
    }
  }
}
</script>

案例效果:

    正常我们设置值是响应一个结果,而案例中通过第三方动画库,展示给我们的是整个过程,从0-20.这就是官网所说的状态过渡。

9.2、动态状态过渡

    就像 Vue 的过渡组件一样,数据背后状态过渡会实时更新,这对于原型设计十分有用。当你修改一些变量,即使是一个简单的 SVG 多边形也可实现很多难以想象的效果。

案例:

<template>
  <div class="box">
    <svg width="200"
         height="200">
      <polygon :points="points"></polygon>
      <circle cx="100"
              cy="100"
              r="90"></circle>
    </svg>
    <label>Sides: {{ sides }}</label>
    <input type="range"
           min="3"
           max="500"
           v-model.number="sides" />
    <label>Minimum Radius: {{ minRadius }}%</label>
    <input type="range"
           min="0"
           max="90"
           v-model.number="minRadius" />
    <label>Update Interval: {{ updateInterval }} milliseconds</label>
    <input type="range"
           min="10"
           max="2000"
           v-model.number="updateInterval" />
  </div>
</template>

<script>
// 使用了第三方动画库:GSAP 是一个JavaScript库,用于创建高性能、零依赖、跨浏览器动画,可以与React、Vue、Angular和vanilla JS协同工作。
// npm install gsap 没安装先安装
import { TweenLite } from 'gsap'

export default {
  data () {
    var defaultSides = 10
    var stats = Array.apply(null, { length: defaultSides }).map(
      function () {
        return 100
      }
    )
    return {
      stats: stats,
      points: generatePoints(stats),
      sides: defaultSides, // 控制边数
      minRadius: 50, // 最小半径
      interval: null,
      updateInterval: 500
    }
  },
  watch: {
    sides: function (newSides, oldSides) {
      var sidesDifference = newSides - oldSides
      if (sidesDifference > 0) {
        for (let i = 1; i <= sidesDifference; i++) {
          this.stats.push(this.newRandomValue())
        }
      } else {
        var absoluteSidesDifference = Math.abs(sidesDifference)
        for (let i = 1; i <= absoluteSidesDifference; i++) {
          this.stats.shift()
        }
      }
    },
    stats: function (newStats) {
      TweenLite.to(this.$data, this.updateInterval / 1000, {
        points: generatePoints(newStats)
      })
    },
    updateInterval: function () {
      this.resetInterval()
    }
  },
  mounted: function () {
    this.resetInterval()
  },
  methods: {
    randomizeStats: function () {
      var vm = this
      this.stats = this.stats.map(function () {
        return vm.newRandomValue()
      })
    },
    newRandomValue () {
      return Math.ceil(
        this.minRadius + Math.random() * (100 - this.minRadius)
      )
    },
    resetInterval () {
      var vm = this
      clearInterval(this.interval)
      this.randomizeStats()
      this.interval = setInterval(function () {
        vm.randomizeStats()
      }, this.updateInterval)
    }
  }
}

function valueToPoint (value, index, total) {
  var x = 0
  var y = -value * 0.9
  var angle = ((Math.PI * 2) / total) * index
  var cos = Math.cos(angle)
  var sin = Math.sin(angle)
  var tx = x * cos - y * sin + 100
  var ty = x * sin + y * cos + 100
  return { x: tx, y: ty }
}

function generatePoints (stats) {
  var total = stats.length
  return stats
    .map(function (stat, index) {
      var point = valueToPoint(stat, index, total)
      return point.x + ',' + point.y
    })
    .join(' ')
}
</script>

<style scoped>
svg {
  display: block;
}
polygon {
  fill: #41b883;
}
circle {
  fill: transparent;
  stroke: #35495e;
}
input[type="range"] {
  display: block;
  width: 100%;
  margin-bottom: 15px;
}
.box {
  width: 300px;
  height: 300px;
}
</style>

案例效果:

9.3、把过渡放到组件里

    把过渡放到组件里赋予设计以生命可以在官网中查看。这里就不再介绍。

七、可复用性 & 组合

    混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。换句话说,mixins是对vue组件的一种扩展,将一些公用的常用数据或者方法,构建一个可被混入的数据结构,被不同的vue组件进行合并,就可以在不同的vue组件中使用相同的方法或者基础数据。

    如果只是提取公用的数据或者通用的方法,并且这些数据或者方法,不需要组件间进行维护,就可以使用mixins。(类似于js中封装的一些公用的方法)

7.1、混入

    先新建MixA.vue、MixB.vue,两个组件

<template>
  <div>
    <h3 v-if="show">hello!我是组件A</h3>
    <button @click="toggleShow">切换</button>
  </div>
</template>

<script>
export default {
  data () {
    return {
      show: true
    }
  },
  methods: {
    toggleShow () {
      this.show = !this.show
    }
  }
}
</script>
<template>
  <div>
    <h3 v-if="show">hello!我是组件B</h3>
    <button @click="toggleShows">切换</button>
  </div>
</template>

<script>
export default {
  data () {
    return {
      show: true
    }
  },
  methods: {
    toggleShows () {
      this.show = !this.show
    }
  }
}
</script>

     我们发现MixA.vue、MixB.vue两个组件都有相同的逻辑。如何对相同的逻辑做提取,达到组件间代码复用的效果?

7.1.1、基础

    我们对MixA.vue、MixB.vue相同的部分做一个提取。新建一个mymixin.js文件。

// 定义一个mixin 复用的代码 mixin
export const showMixin = {
  data () {
    return {
      show: true,
      msg: '我是mixin',
      ol: '公共属性'
    }
  },
  methods: {
    toggleShow () {
      this.show = !this.show
    }
  }
}

MixA.vue

<template>
  <div>
    <h3 v-if="show">hello!我是组件A</h3>
    <button @click="toggleShow">切换</button>
  </div>
</template>

<script>
import { showMixin } from './mymixin'
export default {
  mixins: [showMixin]
}
</script>

MixB.vue

<template>
  <div>
    <h3 v-if="show">hello!我是组件B</h3>
    <button @click="toggleShow">切换</button>
  </div>
</template>

<script>
import { showMixin } from './mymixin'

export default {
  mixins: [showMixin]
}
</script>

案例效果:

7.1.2、选项合并

    修改MixA.vue。

案例:

<template>
  <div>
    <h3 v-if="show">hello!我是组件A</h3>
    <button @click="toggleShow">切换</button>
    {{this.$data}}
  </div>
</template>

<script>
import { showMixin } from './mymixin'
export default {
  data () {
    return {
      msg: '我是A组件'
    }
  },
  mixins: [showMixin]
}
</script>

 案例效果:

    当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”,如果发生冲突时以组件数据优先。上面案例中的msg组件中的优先级更高。

7.1.3、全局混入

    混入也可以进行全局注册。使用时格外小心!一旦使用全局混入,它将影响每一个之后创建的 Vue 实例。我们在main.js中使用全局注册:

// 全局的mixin
Vue.mixin({
  created () {
    const myoption = this.$data.msg
    if (myoption) {
      console.log(myoption)
    }
  }
})

案例效果:

8、渲染函数 & JSX

8.1、渲染函数

    在前面我们说过,vue定义模板的方式有很多种,但是最终都是会通过render函数生成Virtual DOM。然而在一些场景中,需要使用JavaScript的编程能力和创建HTML,这就是render函数,它比template更接近编译器。Vue推荐使用模板来构建我们的应用界面,在底层实现中Vue会将模板编译成渲染函数,当然我们也可以不写模板,直接写渲染函数,以获得更好的控制。

    官网也给我们举了个栗子。大致描述一下他想表达的意思:比如当我们有一个新闻组件,在一个页面中出现了多条新闻,版式都差不多,只是内容有所不同。我们可能会想到应用插槽的形式去展示不同的新闻内容。

<newscomponent>A新闻</newscomponent>
<newscomponent>B新闻</newscomponent>
<newscomponent>C新闻</newscomponent>
<newscomponent>D新闻</newscomponent>

    假如我们要着重突出某个新闻甚至想改字体变颜色大小等就显得没有那么灵活了。我们可以在渲染函数的帮助下做到这一点,渲染函数有助于使组件变得动态,并通过保持它的通用性和使用相同的组件传递参数来使用它。

案例:新建一个Myrender.vue

<script>
export default {
  render: function (createElement) {
    var a = this.elementtype.split(',')
    // createElement 返回虚拟DOM
    // 接收参数类型:createElement({String | Object | Function},{Object},{String | Array})
    // 接收参数解释:createElement('HTML 标签名|组件选项对象|async 函数 --必填',attribute 对应的数据对象,子级虚拟节点)
    return createElement(a[0], {
      attrs: { // 设置id和style
        id: a[3],
        style: 'color:' + a[1] + ';font-size:' + a[2] + 'px;'
      }
    },
      this.$slots.default // 匿名插槽获取内容
    )
  },
  props: {
    elementtype: {
      attributes: String,
      required: true
    }
  }
}
</script>

    在App.vue中使用组件

<template>
  <div id="app">
    <newscomponent :elementtype="'div,red,25,div1'">A news</newscomponent>
    <newscomponent :elementtype="'h3,green,25,h3tag'">B news</newscomponent>
    <newscomponent :elementtype="'p,blue,25,ptag'">C news</newscomponent>
    <newscomponent :elementtype="'div,green,25,divtag'">D news</newscomponent>
  </div>
</template>

<script>
import newscomponent from './components/temp/Myrender'

export default {
  name: 'App',
  components: {
    newscomponent
  }
}
</script>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
  margin-left: 20px;
}
</style>

案例效果:

    渲染函数更接近于底层,虽然很灵活,但是相对于模板的写法要复杂多了。这个时候JSX 就派上用场了。

8.2、JSX

    JSX 是一种 Javascript 的语法扩展,JSX = Javascript + XML,即在 Javascript 里面写 XML,因为 JSX 的这个特性,所以他即具备了 Javascript 的灵活性,同时又兼具 html 的语义化和直观性。这就是为什么会有一个 Babel 插件,用于在 Vue 中使用 JSX 语法,它可以让我们回到更接近于模板的语法上。

案例一:新建一个MyJsx.vue

<script>
export default {
  render () {
    let a = this.myelements.split(',')
    const mystyle = 'color:' + a[0] + ';' + 'font-size:' + a[1] + 'px;'
    return <div style={mystyle}>{this.$slots.default}</div>
  },
  props: {
    myelements: {
      attributes: String,
      required: true
    }
  }
}
</script>

在App.vue中使用

<template>
  <div id="app">
    <jsxcomponent :myelements="'red,30'">A news</jsxcomponent>
    <jsxcomponent :myelements="'green,25'">A news</jsxcomponent>
  </div>
</template>

<script>
import jsxcomponent from './components/temp/MyJsx'

export default {
  name: 'App',
  components: {
    jsxcomponent
  }
}
</script>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
  margin-left: 20px;
}
</style>

案例效果:

    关于jsx的语法可以在这里学习https://github.com/vuejs/jsx

8.3、函数式组件

    Vue 提供了一种称为函数式组件的组件类型,又叫非渲染组件,React中也有这个概念:用来定义那些没有响应数据,也不需要有任何生命周期的场景,它只接受一些props 来显示组件。我们可以把函数式组件想像成组件里的一个函数,入参是渲染上下文(render context),返回值是HTML。

    函数组件特点:1)无状态(Stateless),组件自身是没有状态的;2、无实例(Instanceless),组件自身没有实例,也就是没有this。

    由于函数式组件拥有的这两个特性,我们就可以把它用作高阶组件(High order components),所谓高阶,就是可以生成其它组件的组件。

语法格式:

Vue.component('my-component', {
  functional: true,
  // Props 是可选的
  props: {
    // ...
  },
  // 为了弥补缺少的实例
  // 提供第二个参数作为上下文:data、props、slots、children 以及 parent 都可以通过 context 来访问
  render: function (createElement, context) {
    // ...
  }
})

    content包含的属性如下:

  • props:提供所有 prop 的对象
  • children:VNode 子节点的数组
  • slots:一个函数,返回了包含所有插槽的对象
  • scopedSlots:(2.6.0+) 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。
  • data:传递给组件的整个数据对象,作为 createElement 的第二个参数传入组件
  • parent:对父组件的引用
  • listeners:(2.3.0+) 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是 data.on 的一个别名。
  • injections:(2.3.0+) 如果使用了 inject 选项,则该对象包含了应当被注入的 property。

8.3.1、函数式组件与普通组件的区别

    1)函数式组件需要在声明组件是指定 functional

    2)没有this,this通过render函数的第二个参数来代替

    3)没有生命周期钩子函数,不能使用计算属性,watch

    4)不能通过$emit 对外暴露事件,调用事件只能通过context.listeners.click的方式调用外部传入的事件

    5)函数式组件的props可以不用显示声明,所以没有在props里面声明的属性都会被自动隐式解析为prop,而普通组件所有未声明的属性都解析到$attrs里面,并自动挂载到组件根元素上面(可以通过inheritAttrs属性禁止)

    函数式组件的部分源码:

function createComponent (
    Ctor,
    data,
    context,
    children,
    tag
  ) {
    if (isUndef(Ctor)) {
      return
    }

    var baseCtor = context.$options._base;

    // ...中间省略N行
    // functional component
    if (isTrue(Ctor.options.functional)) { // 在此判断是否是函数式组件,如果是return 自定义render函数返回的Vnode,跳过底下初始化的流程
      return createFunctionalComponent(Ctor, propsData, data, context, children)
    }
    // ...中间省略N行
    // install component management hooks onto the placeholder node
    installComponentHooks(data); // 正常的组件是在此进行初始化方法(包括响应数据和钩子函数的执行)

    // return a placeholder vnode
    var name = Ctor.options.name || tag;
    var vnode = new VNode(
      ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
      data, undefined, undefined, undefined, context,
      { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
      asyncFactory
    );

    return vnode
  }

8.3.1、函数式组件的使用

    由于函数式组件不需要实例化,无状态,没有生命周期,所以渲染性能要好于普通组件。主要运用场景:比如一些详情页面,列表界面等,它们有一个共同的特点是只需要将外部传入的数据进行展现,不需要有内部状态,不需要在生命周期钩子函数里面做处理,这时候你就可以考虑使用函数式组件

    案例:实现一个登陆头像,有照片就使用照片,无照片使用默认照片。

案例一:新建一个函数式组件tx.vue

<script>
export default {
  // 通过functional属性指定组件为函数式组件
  functional: true,
  // 组件接收的外部属性
  props: {
    avatar: {
      type: String,
      required: true
    }
  },
  // 通过通过JSX的方式声明
  /**
   * 渲染函数
   * @param {*} h
   * @param {*} context 函数式组件没有this, props, slots等都在context上面挂着
   */
  render (h, context) {
    const { props } = context
    return <img src={props.avatar} style="width:50px;height:50px"></img>
  }
}
</script>

在App.vue中使用

<template>
  <div id="app">
    <!-- 按照约定俗成的习惯,不经过webpack处理的放在static,需要经过处理的放assets -->
    <!-- 默认路径如果是放在assets目录中的图片,不能直接使用路径去获取,绝对路径和相对路径都无法获取-->
    <!-- vue在打包时只会将static下面的图片保留,assets目录下的图片会转换成base64,直接打包到js文件中,所以用路径是读取不到图片的 -->
    无法正常显示:<tx :avatar="pgpath0 ? pgpath0 : '/assets/default.jpg'"></tx><br>
    默认头像:<tx :avatar="pgpath0 ? pgpath0 : '/static/default.jpg'"></tx><br>
    <!-- 使用ES6中的import -->
    正常头像(方式一):<tx :avatar="pgpath ? pgpath : '/static/default.jpg'"></tx><br>
    <!-- 使用require -->
    正常头像(方式二):<tx :avatar="pgpath ? pgpath : '/static/default.jpg'"></tx>
  </div>
</template>

<script>
import tx from './components/temp/tx'
import txjpg from './assets/avatar.jpg'

export default {
  name: 'App',
  components: {
    tx
  },
  data () {
    return {
      pgpath0: '',
      pgpath: txjpg,
      pgpath2: require('./assets/avatar.jpg')
    }
  }
}
</script>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
  margin-left: 20px;
}
</style>

案例效果:

    关于图片的路径,有两种方式:一种是不经过webpack处理,直接写绝对路径拿static里的文件,直接写/static/xx/xxx.png;一种是经过使用webpack处理,不管是require还是import,声明为一个变量,后续使用这个变量。

// 在 2.5.0 及以上版本中,如果你使用了单文件组件,那么基于模板的函数式组件可以这样声明:
<template functional>
  <div class="igsize">
    <img :src="props.avatar ? props.avatar : '../../static/default.jpg'" />
  </div>
</template>

9、插件

    什么是Vue插件,它和Vue组件有什么区别?通常插件是一种遵循一定规范的应用程序接口编写出来的程序,是一个库。而组件则更倾向于一个单一的功能,一个控件或对象。Vue官网给我们的解释是:插件通常用来为 Vue 添加全局功能、组件是可复用的 Vue 实例。似乎还是有点懵。

    其实, Vue 插件 和 Vue组件 只是在 Vue.js 中包装的两个概念而已,不管是插件还是组件,最终目的都是为了实现逻辑复用。它们的本质都是对代码逻辑的封装,只是封装方式不同而已。在必要时,组件也可以封装成插件,插件也可以改写成组件,就看实际哪种封装更方便使用了。除此之外,插件是全局的,组件可以全局注册也可以局部注册。插件就是对Vue功能的增强或补充。

    官网说到,插件的功能范围没有严格的限制——一般有下面几种:

    1)添加全局方法或者 property。如:vue-custom-element

    2)添加全局资源:指令/过滤器/过渡等。如 vue-touch

    3)通过全局混入来添加一些组件选项。如 vue-router

    4)添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。

    5)一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router

9.1、认识插件

    官网给了我们一个模板:Vue.js 的插件应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象。

    一个Vue插件可以是一堆Vue组件的集合(插件干的事就是把内部的组件帮你倒入到vue全局下),也可以是用来扩展Vue功能的,比如 Vuex, Vue-Router。你也可以写一个插件,在Vue原型上扩展方法、添加指令、注入选项......

MyPlugin.install = function (Vue, options) {
  // 1. 添加全局方法或 property
  Vue.myGlobalMethod = function () {
    // 逻辑...
  }

  // 2. 添加全局资源
  Vue.directive('my-directive', {
    bind (el, binding, vnode, oldVnode) {
      // 逻辑...
    }
    ...
  })

  // 3. 注入组件选项
  Vue.mixin({
    created: function () {
      // 逻辑...
    }
    ...
  })

  // 4. 添加实例方法
  Vue.prototype.$myMethod = function (methodOptions) {
    // 逻辑...
  }
}

9.2、插件的使用

    通过全局方法 Vue.use() 使用插件。它需要在你调用 new Vue() 启动应用之前完成。语法:

// 调用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin)

    例如:我们做一个图片轮播,找到一个可以使用的插件:Vue Carousel 3d,对应的使用说明:https://github.com/Wlada/vue-carousel-3d

//步骤1:安装
npm install -S vue-carousel-3d

//步骤2:注册插件
import Vue from 'vue'
import Carousel3d from 'vue-carousel-3d'

Vue.use(Carousel3d)

//步骤3:使用插件
<carousel-3d>
      <slide :index="0">
        <img src="./assets/avatar.jpg">
      </slide>
      <slide :index="1">
        <img src="./assets/default.jpg">
      </slide>
      <slide :index="2">
        <img src="./assets/logo.png">
      </slide>
</carousel-3d>

案例效果:

9.3、插件的开发

    前面说到插件是对Vue功能的增强或补充,可以添加全局方法、全局资源、混入选项,也可以同时注册多个组件或功能。

案例一:写一个加载中插件

    1)下载一张加载中图片cj.gif

    2)loading.vue

<template>
  <div class='wrapper'
       v-if="isshow">
    <div class='loading'>
      <img src="./cj.gif"
           alt=""
           width="115"
           height="100"
           style="border: blue 1px solid;">
    </div>
  </div>
</template>

<script>
export default {
  props: {
    isshow: {
      type: Boolean,
      default: false
    }
  }
}
</script>

<style scoped>
</style>

   3)index.js

import LoadingComponent from './loading.vue'

let Loading = {}

Loading.install = (Vue) => {
  // 当然这里也可以做多个全局组件注册
  Vue.component('loading', LoadingComponent)
}

export default Loading

 4)注册插件

import load from './components/aloading/index'

Vue.use(load)

案例二:写一个提示插件

    1)新建一个mytoast.vue

<template>
  <transition name="fade">
    <div class="toast"
         v-show="show">
      {{message}}
    </div>
  </transition>
</template>

<script>
export default {
  data () {
    return {
      show: false,
      message: ''
    }
  }
}
</script>

<style scoped>
.toast {
  position: fixed;
  top: 10%;
  left: 40%;
  margin-left: -15vw;
  padding: 2vw;
  width: 30vw;
  font-size: 4vw;
  color: #fff;
  text-align: center;
  background-color: rgba(0, 0, 0, 0.8);
  border-radius: 5vw;
  z-index: 999;
}

.fade-enter-active,
.fade-leave-active {
  transition: 0.3s ease-out;
}
.fade-enter {
  opacity: 0;
  transform: scale(1.2);
}
.fade-leave-to {
  opacity: 0;
  transform: scale(0.8);
}
</style>

    2)新建一个index.js

import ToastComponent from './mytoast.vue'

const Toast = {}

// 注册Toast
Toast.install = function (Vue) {
  // 生成一个Vue的子类
  // 同时这个子类也就是组件
  const ToastConstructor = Vue.extend(ToastComponent)
  // 生成一个该子类的实例
  const instance = new ToastConstructor()
  // 将这个实例挂载在我创建的div上
  // 并将此div加入全局挂载点内部
  instance.$mount(document.createElement('div'))
  document.body.appendChild(instance.$el)
  // 通过Vue的原型注册一个方法
  // 让所有实例共享这个方法
  Vue.prototype.$toast = (msg, duration = 2000) => {
    instance.message = msg
    instance.show = true

    setTimeout(() => {
      instance.show = false
    }, duration)
  }
}
export default Toast

    3)注册插件

import Toast from './components/acj/index'

Vue.use(Toast)

    4)App.vue中使用插件

<template>
  <div id="app">
    <loading :isshow='show'></loading>
    <button @click="show = !show">显示/隐藏loading</button>

    <button @click="toast">显示taost弹出框</button>

  </div>
</template>

<script>
export default {
  name: 'App',
  data () {
    return {
      show: false
    }
  },
  methods: {
    toast () {
      this.$toast('你好')
    }
  }
}
</script>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
  margin-left: 20px;
}
</style>

案例效果:

    插件一其实也是一个全局组件,插件二在注册完成后就已经存在了,直接在js中操作元素。

10、过滤器

    关于过滤器,在这篇文章(https://blog.csdn.net/xiaoxianer321/article/details/111560355)中就已经介绍过了。

八、路由

    由于文章过长,关于路由将在这篇文章(https://blog.csdn.net/xiaoxianer321/article/details/116114007)中介绍。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐