vue的高级特性-插槽,keep-alive,$nextTick等等

948 阅读8分钟

插槽slot

什么是slot?

slot也称作插槽,是分发内容的入口。

默认插槽

使用slot来确定渲染的位置。

父组件:

<div>
 <child>
  哈哈,我是父组件,给默认插槽传递内容
 </child
<div>

模板组件child:

<div>
 <div>这里是模板组件头部</div>
 <slot></slot>
 <div>这里是模板组件尾部</div>
<div>

如果模板组件没有包含一个 元素,则该父组件起始标签和结束标签之间的任何内容都会被抛弃。

  • 后备内容

当父组件没有提供内容时,后备内容将被渲染。

父组件:

<div>
 <child> </child>
<div>

模板组件child:

<div>
 <div>这里是模板组件头部</div>
 <slot>我是后备内容</slot>
 <div>这里是模板组件尾部</div>
<div>

具名插槽

有时,一个模板有多个插槽,如何确定分发内容呢?

  • 给模板中的插槽加上属性name区分。
  • 给父组件中加上指令v-slot,并指定name

注意 v-slot 自2.6更新,只能添加在 <template> 上 (只有一种例外情况:独占默认插槽),这一点和已经废弃的 slot attribute 不同。

父组件:

<div>
 <child>
  <template v-slot:name>我的名字</template>
  <template v-slot:age>我的年龄</template>
  我没有被slot包裹,所以会被传入默认插槽中。
 </child>
<div>

模板组件child:

<div>
 <div>
  <slot name="name"></slot>
 </div>
 <slot>我是后备内容</slot>
 <div>
   <slot name="age"></slot>
 </div>
<div>

结果:

我的名字

我没有被slot包裹,所以会被传入默认插槽中。

我的年龄

作用域插槽

插槽内容是无法访问模板组件中的数据的,那么,如何访问?

  • 给模板组件中的slot绑定数据

父组件:

<div>
 <child>
  <template v-slot:name="slotProps">{{slotProps.user.name}}</template>
  <template v-slot:age>我的年龄</template>
  我没有被slot包裹,所以会被传入默认插槽中。
 </child>
<div>

模板组件child:

<div>
 <div>
  <slot name="name" :user="user"></slot>
 </div>
 <slot>我是后备内容</slot>
 <div>
   <slot name="age"></slot>
 </div>
<div>

user = {
    name:'我的名字'
}

独占默认插槽

当提供的内容只有默认插槽时,组件的标签就可以当做插槽模本来用,就可以不用写template。

父组件:

<div>
 <child v-slot:default="slotProps">
  我是默认插槽。
 </child>
<div>

模板组件child:

<div>
<div>这里是模板组件头部</div>
 <slot :user="user"></slot>
 <div>这里是模板组件尾部</div>
<div>

解构插槽

作用域插槽的内部工作原理是将你的插槽内容包括在一个传入单个参数的函数里:

function (slotProps) {
  // 插槽内容
}

v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。所以在支持的环境下 (单文件组件或现代浏览器),你也可以使用 ES2015 解构来传入具体的插槽 prop

父组件:

<div>
 <child>
  <template v-slot:name="{ user }">{{user.name}}</template>
  <!-- 或者-->
  <!--<template v-slot:name="{ user : name }">{{name}}</template>-->
  <!--<template v-slot:name="{ user = { name : '我的名字' } }">{{ user.name }}</template>-->
  <template v-slot:age>我的年龄</template>
  我没有被slot包裹,所以会被传入默认插槽中。
 </child>
<div>

模板组件child:

<div>
 <div>
  <slot name="name" :user="user"></slot>
 </div>
 <slot>我是后备内容</slot>
 <div>
   <slot name="age"></slot>
 </div>
<div>

user = {
    name:'我的名字'
}

slot本质上是返回VNode的函数,一般情况下,Vue中的组件要渲染到页面上需要经过 template >> render function >> VNode >> DOM 过程。

组件挂载的本质就是执行渲染函数得到VNode,至于data/props/computed这些属性都是给VNode提供数据来源。

在2.5之前,如果是普通插槽就直接是VNode的形式了,而如果是作用域插槽,由于子组件需要在父组件访问子组件的数据,所以父组件下是一个未执行的函数 (slotScope) => return h('div', slotScope.msg) ,接受子组件的slotProps参数,在子组件渲染实例时会调用该函数传入数据。

在2.6之后,两者合并,普通插槽也变成一个函数,只是不接受参数了。

keep-alive

  • keep-alive是什么?

是一个抽象的组件(或称为功能型组件),不会被渲染到dom树种。

下次再渲染组件的时候,还会保持其中的所有状态,并且会触发activated钩子函数。

  • keep-alive的作用?

在内存中缓存组件的状态,不让组件销毁,避免重新渲染

  • keep-alive的使用场景?

比如,有两个tab,当我们在tabA进行浏览,做了一些事,比如阅读滚动条在底部,然后,我们切换到tabB做了一些事情,当我们切换回tabA的时候,我们希望能够保存tabA的状态,依然在我们之前浏览的位置。

因为缓存的需求通常出现在页面切换,所以常和router-view使用:

<keep-alive>
 <router-view />
</keep-alive>

$nextTick

data(){
  return{
      message:'begin'
  }  
},
handleClick () {
    this.message = 'end';
    console.log(this.$refs.message.innerText); //打印“begin”
    this.$nextTick(()=>{
        console.log(this.$refs.message.innerText);//“end”
    })
}

可以看出,虽然改变了数据,但dom中的数据不是最新的。

这是因为,Vue的事件机制是通过队列来调度执行,会等待所有进程执行完成后,再去执行更新,渲染页面,所以,Vue的dom更新是异步的。

当改变一个数据的时候,并不会在视图上立即展示,所以在更新数据后,如果想立即获取dom做一些事情,得到的是未更新前的数据,那么我们要怎样获得最新的视图呢?

$nextTick可以解决这个问题,它的作用是延迟回调,在dom更新循环结束之后,立即调用这个方法,然后执行回调函数,在回调函数中,可以获取到最新的dom,做一些事情。

  • 为什么要异步更新视图?

操作dom是一件非常耗性能的事情,如果一个数据被循环改动1000次,dom就会被更新1000次,那对性能肯定是不友好的。

vue执行Dom异步更新,当检测一个数据发生变化后,vue将开启一个queue队列,然后会统一执行queuewatcher的run。同时,相同id的watcher不会被重复加入到队列中,所以,同一个watcher被触发多次,只会推入到队列中一次,也不会执行1000次watcher的run。避免了很多重复的计算和dom操作。

异步组件

  • 为什么要用异步组件?

当一个页面有多个组件,如果我们一次性就把全部组件加载,那样是会很耗时的而且无用的,我们只需要加载所用到的组件,异步组件就可以很好地解决这个问题,只有在需要使用的时候才去加载组件。

  • 如何使用异步加载?异步加载方案有哪些?

1.vue的异步组件技术

vue可将组件定义成一个异步解析的函数,只有在使用的时候,才会调用函数,此时就会异步的去请求这个组件,并且将结果缓存起来,用于再次渲染。

要使用异步组件,一个比较推荐的方式是配合 webpack 代码分离功能 。

components: {
// 这个特殊的 require 语法
// 将指示 webpack 自动将构建完成的代码,
// 拆分到不同的 bundle 中,然后通过 Ajax 请求加载。
'center':(resolve)=>{require(['./personal-center.vue'],resolve)}//用户信息
}

2.es的import()

  • 使用这种方式(需要webpack > 2.4)
  • 可用于路由配置

// 没有指定webpackChunkName,每个组件打包成一个js文件。
components: () => import('../components/ImportFuncDemo1')
components: () => import('../components/ImportFuncDemo2')
// 指定了相同的webpackChunkName,会合并打包成一个js文件。
components:  () => import(/* webpackChunkName: 'ImportFuncDemo' */ '../components/ImportFuncDemo')
components:  () => import(/* webpackChunkName: 'ImportFuncDemo' */ '../components/ImportFuncDemo2')

2.webpack提供的require.ensure()

用于路由懒加载配置

这种情况下,多个路由指定相同的chunkName,会合并打包成一个js文件。

component: r => require.ensure([], () => r(require('../components/PromiseDemo')), 'demo')

动态组件

  • 目的?作用?

在不同的组件之间切换状态。

使用component标签和is属性

引申提问

组件中的name有什么作用?

  • 当使用keep-alive时,可以搭配组件名字进行缓存过滤
export default {
  name:'Detail'
}
<keep-alive exclude="Detail">
  <router-view/>
</keep-alive>
  • 递归组件调用。组件递归迭代调用自己时,需要用到。
<div>
  <!-- other……thing -->
  <detail-list></detail-list>
</div>
<script>
export default {
  name:'DetailList',//递归组件是指组件自身调用自身
}
</script>
  • 使用vue-devtools调试工具时,里面显示的组件名称就是由name决定的。