一篇文章,带你快速回顾: Vue 面试题的关键知识点

臭长警告!

尽管文章看起来很长,其实大部分都是结构性的代码。文章会很详细地为各个关键的知识点举例。相信很快就可以让我们回忆起每个关键的知识点及其用法。

一、

1. 插值语法

<template>
  <div id="app">
    {{ name }}  <!-- 这里就是插值语法 -->
  </div>
</template>

<script>

export default {
  data () {
    return {
      name: '弼马温'
    }
  }
}
</script>

<style>
</style>

2. 指令

v-html、v-if、v-for、v-show、v-model

3. 动态属性

<template>
  <div id="app">
    {{ name }}
    <p id="test">test</p>
    <!-- 动态属性 -->
    <p :id="dynamicId">test</p>
  </div>
</template>

<script>

export default {
  data () {
    return {
      name: '弼马温',
      dynamicId: Math.random()
    }
  }
}
</script>

<style>
</style>

  

 

刷新一下页面,就可以看到第二个p标签的id发生了变化 

 

4. 表达式

我们可以在 {{ }} 中写一些JS表达式,

什么是JS表达式?

可以赋值到一个变量上去的JS语句

<template>
  <div id="app">
    {{ monkeyName }}
    <p id="test">test</p>
    <p :id="dynamicId">test</p>
    <!-- 表达式 -->
    <p :id="dynamicId">{{isAMonkey ? monkeyName : pigName}}</p>  <!-- {{}}中可以写一些JS表达式 -->
  </div>
</template>

<script>

export default {
  data () {
    return {
      monkeyName: '弼马温',
      pigName: '猪悟能',
      dynamicId: Math.random(),
      isAMonkey: false
    }
  }
}
</script>

<style>
</style>

5. v-html

作用:以下方代码为例,作用就是用p1对应的标签替换掉div的子标签

<template>
  <div id="app" v-html="p1">
    {{ monkeyName }}
    <p id="test">test</p>
    <p :id="dynamicId">test</p>
    <p :id="dynamicId">{{isAMonkey ? monkeyName : pigName}}</p>
  </div>
</template>

<script>

export default {
  data () {
    return {
      monkeyName: '弼马温',
      pigName: '猪悟能',
      dynamicId: Math.random(),
      isAMonkey: false,
      p1: '<p>呆子!</p>'
    }
  }
}
</script>

<style>
</style>

注意!v-html可能会造成XSS攻击,将p1转化为标签会有一个编译的过程,如果内部有恶意的js代码,可能就会被同时执行。

<template>
  <div id="app" v-html="img1">
    {{ monkeyName }}
    <p id="test">test</p>
    <p :id="dynamicId">test</p>
    <p :id="dynamicId">{{isAMonkey ? monkeyName : pigName}}</p>
  </div>
</template>

<script>

export default {
  data () {
    return {
      monkeyName: '弼马温',
      pigName: '猪悟能',
      dynamicId: Math.random(),
      isAMonkey: false,
      p1: '<p>呆子!</p>',
      img1: `<img src="123" onerror="alert('兄弟!你已经没了')"/>`
    }
  }
}
</script>

<style>
</style>

因此我们需要借助一些库将p1转化为一段纯净的html标签。

 二、计算属性

<template>
  <div id="app">
    <p>{{number}}</p>
    <p>number: {{computedNumber1}}</p>
    <!-- 计算属性:数据的双向绑定 -->
    <input v-model="computedNumber2">
  </div>
</template>

<script>

export default {
  data () {
    return {
      number: 5
    }
  },
  computed: {
    computedNumber1 () {
      return this.number * 2
    },
    computedNumber2: {
      <!-- 借助v-model绑定计算属性,实现数据的双向绑定,需要我们主动添加一个setter,
           computedNumber1 中的return 本质上就相当于getter,getter只能让页面的数据跟随data的变化而变化
           我们如果需要让data中的数据根据页面的变化而变化,就需要手动设定一个setter -->
      get () {
        return this.number * 2
      },
      set (value) {
        this.number = value / 2
      }
    }
  }
  
}
</script>

<style>
</style>

计算属性有一大优点,就是计算属性是否再次计算,取决于其绑定的data中的值是否发生变化,只有发生了变化,才会再次计算,

<template>
  <div id="app">
    <p>{{number}}</p>
    <p>number: {{computedNumber1}}</p>
    <input v-model="computedNumber2">
  </div>
</template>

<script>

export default {
  data () {
    return {
      number: 5,
      number2: 5

    }
  },
  computed: {
    computedNumber1 () {
      return this.number * 2
    },
    computedNumber2: {
      get () {
        console.log('???????')
        return this.number * 2
      },
      set (value) {
        console.log('!!!!!!!')
        this.number = value / 2
      }
    }
  }
  
}
</script>

<style>
</style>

我们修改了三次number2,但是两个计算属性绑定的都是number1,所以并不会导致computedNumber2被再次被执行。

??????是页面初始化时,执行的依次计算属性。

改变几次number试一下:

印证了上面的话,确实是没有问题的。

 三、watch:检测数据的变化

<template>
  <div id="app">
    <input type="text" v-model="name">
    <p>{{name}}</p>
    <input type="text" v-model="info.hobby">
    <p>{{info}}</p>
  </div>
</template>

<script>

export default {
  data () {
    return {
      name: '八戒',
      info: {
        hobby: '高老庄的小娘们'
      }

    }
  },
  watch: {
    name(curValue, preValue){
      console.log( 'name', curValue, preValue )
    },
    info(curValue, preValue){
      console.log( 'name', curValue, preValue )
    }
  }
  
}
</script>

<style>
</style>

 

 我们更改name的值,被watch检测到,触发name对应的函数,但是hobby的值发生更改,却无法被检测到。

原因很简单,采用的是值传递。而info是一个对象,存储的仅仅只是对象的地址。对象内的属性hobby发生改变,是不会影响对象所在的地址的。

监听属性值为基本数据类型的对象时,可以使用函数,但是,监听的如果是引用类型,就必须使用一个对象,我们调整一下代码:

<template>
  <div id="app">
    <input type="text" v-model="name">
    <p>{{name}}</p>
    <input type="text" v-model="info.hobby">
    <p>{{info}}</p>
  </div>
</template>

<script>

export default {
  data () {
    return {
      name: '八戒',
      info: {
        hobby: '高老庄的小娘们'
      }

    }
  },
  watch: {
    name(curValue, preValue){
      console.log( 'name', curValue, preValue )
    },
    // 引用类型属性值的监听写法
    info: {
      handler: function(curValue, preValue){
        console.log( 'info', curValue, preValue )
      },
      // 深度监听属性值
      deep: true
    }
  }
  
}
</script>

<style>
</style>

任务完成!

 四、动态绑定class和style

<template>
  <ul class="list clearfix">
    <li class="item" :class="{'active': isActive}">项目1</li>
    <li class="item" :class="['active']">项目2</li>
    <li class="item" :style="styleDate">项目3</li>
  </ul>
</template>

五、v-show与v-if的区别

v-if不渲染未命中的元素,而v-show依旧存在于渲染树中,只不过display属性为none。

元素渲染后不会再发生改变,应该使用v-if,频繁发生改变的使用v-show

六、v-for遍历数组

<template>
  <div>
    <ul class="list clearfix">
      <li v-for="(item, index) in arr" :key="item.id">
        {{index+1}}-{{item.value}}
      </li>
    </ul>
    <ul class="list clearfix">
      <li v-for="(value, key, index) in obj" :key="key">
        {{index}}: {{key}}: {{value}}
      </li>
    </ul>
  </div>
</template>

<script>

export default {
  data () {
    return {
      arr: [
        {id: 1, value: '项目1'},
        {id: 2, value: '项目2'},
        {id: 3, value: '项目3'}
      ],
      obj: {
        name: '小马哥',
        info: '好帅!'
      }

    }
  }
}
</script>

注意:v-if与v-for不要联用,因为再Vue中,v-for的优先级要高于v-if。就可能出现v-for渲染了数据,但是v-if的值为false,又不允许数据被渲染。我们可以将v-if放到ul上,也可以放到 li 的内容的span上

七、事件

<template>
  <div>
    <button @click="incrementBy10(10, $event)">点击加10</button>
    {{number}}
  </div>
</template>

<script>

export default {
  data () {
    return {
      number: 0
    }
  },
  methods:{
    incrementBy10(number, e){
      this.number += number;
      console.log( e )
    }
  }
}
</script>

八、父子附件通信

props & $emit

1. props:

<!-- 父:App.vue -->
<template>
  <div id="app">
    <MyInput />
    <!-- 父组件向子组件传递数据 -->
    <MyList :list="list" />
  </div>
</template>

<script>
import MyInput from './components/Input';
import MyList from './components/List';


export default {
  data () {
    return {
      list: [
        {
          id: 1,
          title: '如何卷才能保持真正地高效卷?'
        },
        {
          id: 2,
          title: '我们应该停下成为卷王之王的脚步吗?'
        },
        {
          id: 3,
          title: '是什么让我们越来越卷?是求生欲?还是我乐意?'
        },
        {
          id: 4,
          title: '越卷真的可以越快乐?'
        },
      ]
    }
  },
  components: {
    MyInput,
    MyList
  }
}
</script>

<style>
</style>
<!-- 子:Input.vue -->

<template>
  <div>
    <input type="text" v-model="title">
    <button @click="add">添加</button>
  </div>
</template>

<script>

export default {
  data () {
    return {
      title: ''
    }
  },
  methods:{
    add(){

    }
  }
}
</script>

<style>
</style>

<!-- 子:List.vue -->

<template>
  <div class="list">
    <ul>
      <!-- 同时将数据渲染到页面 -->
      <li v-for="item in list" :key="item.id">
        {{item.title}}
      </li>
    </ul>
  </div>
</template>

<script>

export default {
  // 子组件接收数据
  props: {
    list: Array
  }
}
</script>

<style>
</style>

2. $emit

<!-- App.vue -->
<template>
  <div id="app">
    <!--订阅addItem消息(这种方式,只能订阅子组件发布的消息)-->
    <MyInput @addItem="add"/>
    <MyList :list="list" />
  </div>
</template>
<!-- Item.vue -->

<template>
  <div>
    <input type="text" v-model="title">
    <!-- 为按钮绑定事件 -->
    <button @click="add">添加</button>
  </div>
</template>

<script>

export default {
  data () {
    return {
      title: ''
    }
  },
  methods:{
    add(){
      // 在点击时,发布消息(这种发布方式只能由父组件监听)
      this.$emit('addItem', this.title);
    }
  }
}
</script>

<style>
</style>

 List.vue不变,然后在输入框中输入内容,点击添加进行测试:

 成功!

九、兄弟组件通信:事件总线eventBus

eventBus其实就是一个Vue的实例

// eventBus.js

import Vue from 'vue';

const eventBus = new Vue();

export default eventBus;

我们借助Vue实例身上的$emit方法来讲数据广播出去,需要接收数据的地方,只需要$on来监听广播即可。

<!-- Input.vue -->

<template>
  <div>
    <input type="text" v-model="title">
    <button @click="add">添加</button>
  </div>
</template>

<script>
// 引入事件总线
import eventBus from '../event-bus'

export default {
  data () {
    return {
      title: ''
    }
  },
  methods:{
    add(){
      this.$emit('addItem', this.title);

      // 通过事件总线,讲数据广播出去
      eventBus.$emit('broadcastAResource', this.title);
    }
  }
}
</script>

<style>
</style>
<!-- List.vue -->

<template>
  <div class="list">
    <ul>
      <li v-for="item in list" :key="item.id">
        {{item.title}}
      </li>
    </ul>
  </div>
</template>

<script>
import eventBus from '../event-bus';

export default {
  props: {
    list: Array
  },
  methods: {
    // 处理对应的参数
    handleAddTitle(title){
      console.log( '拿到了广播的数据:', title );
    }
  },
  mounted(){
    // 监听事件总线的广播,获取同名广播内的数据,然后调用对应的函数,数据位于函数参数中
    eventBus.$on('broadcastAResource', this.handleAddTitle);
  }
}
</script>

<style>
</style>

 测试一下:

就在刚刚,突然注意到了 之前在学习Vue的时候,忽略的一点:

为什么不直接用$emit去将数据发出去,然后通过@xxx去订阅对应数据呢?

原因很简单,回顾之前的例子,简单的$emit,使用的场景是父组件中引用了子组件,父组件可以获取$emit中对应的事件,所以@addItem就直接拿到了。但是现在是兄弟组件,他们是通过一个公共的父组件建立的联系,彼此之间并不能直接联系。因此,我们就不能单纯地采用$emit了。

于是,事件总线eventBus的效果就凸显出来了。

 十、父子组件声明周期执行顺序

Vue实例创建完毕时,执行created;渲染完毕后,执行mounted;数据发生改变时,执行updated;销毁时,执行destroyed函数 

单个组件中的生命周期,可以参考这篇文章:

移动端项目总结 - Vue 生命周期函数_拯救世界的光太郎-CSDN博客

上面这篇文章言简意赅,只分析重点,看完之后大致就能让你回忆起各个声明周期函数的特点了。

接下来,主要分析父子组件声明周期函数的执行顺序

<!-- App.vue -->

<template>
  <div id="app">
    <MyInput @addItem="add"/>
    <MyList :list="list" />
  </div>
</template>

<script>
import MyInput from './components/Input';
import MyList from './components/List';

export default {
  data () { ...
  },
  methods: { ...
  },
  created(){
    console.log( 'parent: App created' );
  },
  mounted(){
    console.log( 'parent: App mounted' );
  },
  beforeUpdate(){
    console.log( 'parent: App before update' );
  },
  updated(){
    console.log( 'parent: App updated' );
  },
  components: { ...
  }
}
</script>

<style>
</style>
<!-- List.vue -->

<template>
  <div class="list">
    <ul>
      <li v-for="item in list" :key="item.id">
        {{item.title}}
      </li>
    </ul>
  </div>
</template>

<script>
import eventBus from '../event-bus';

export default {
  props: { ...
  },
  methods: { ...
  },
  created(){
    console.log( 'child: List created' );
  },
  mounted(){
    eventBus.$on('broadcastAResource', this.handleAddTitle);
    console.log( 'child: List mounted' );
  },
  beforeUpdate(){
    console.log( 'child: List before update' );
  },
  updated(){
    console.log( 'child: List updated' );
  }
}
</script>

<style>
</style>

看一下初次加载的打印结果:

先创建父组件实例,然后创建子组件实例,子组件先渲染,然后父组件渲染

在输入框中输入一个数据,然后添加进入,看一下执行结果:

为什么是父组件先执行before update呢?

原因很简单,数据存储在父组件。我们点击按钮,然后在Input组件中通过$emit将数据传递给父组件,父组件触发对应事件将数据添加进date中,父组件的数据发生变化,于是父组件的before update就先触发了,这个数据会被传递到List组件中,于是接下来,List也触发了before update。

为什么子组件的update先执行呢?

这是因为,我们将数据传递给子组件,然后子组件去渲染数据,子组件渲染完毕了,父组件调用子组件才算是渲染完毕。如果子组件没有渲染完成,那么父组件的update也就会一直等待着子组件。

十一、$nextTick

看一个打印异常的例子:

<template>
  <div id="app">
    <ul ref="ulRef">
      <li v-for="item in list" :key="item.id">
        {{item.title}}
      </li>
    </ul>
    <button @click="add">添加</button>
  </div>
</template>

<script>
export default {
  data () {
    return {
      list: [
        {
          id: 1,
          title: '如何卷才能保持真正地高效卷?'
        },
        {
          id: 2,
          title: '我们应该停下成为卷王之王的脚步吗?'
        },
        {
          id: 3,
          title: '是什么让我们越来越卷?是求生欲?还是我乐意?'
        },
        {
          id: 4,
          title: '越卷真的可以越快乐?'
        },
      ]
    }
  },
  methods: {
    add(){
      this.list.push({
        id: Math.random(),
        title: Math.random()
      })

      const ulElem = this.$refs.ulRef;
      const length = ulElem.childNodes.length;
      console.log( 'length: ', length );
    }
  }
}
</script>

<style>
</style>

点击添加按钮:

 命名是五个数据,而且显示数据长度的代码,也在渲染追加 li 的下方,为什么length却是4呢?

这是因为,在Vue中,渲染是异步的。也就是说,我们写的打印长度的代码已经执行完了,这个时候,新的li其实还没有被追加到ul中去,所以求得的length还依旧是4。

怎么解决呢?

很简单,借助$nextTick。

  methods: {
    add(){
      this.list.push({
        id: Math.random(),
        title: Math.random()
      })
      this.$nextTick(()=>{
        const ulElem = this.$refs.ulRef;
        const length = ulElem.childNodes.length;
        console.log( 'length: ', length );
      })

    }
  }

this.$nextTick接收一个箭头函数,这个箭头函数在页面最终渲染完毕(多次更新节点数据时,只在全部都更新完后才会执行)后执行,在这里处理数据,就可以保证数据是最新的了!

十二、插槽slot

基本使用:

1. 定义一个插槽

<!-- slotTest.vue -->

<template>
  <div class="slot-test">
    <!-- 相当于一个槽,我们可以将这个槽安装到任意位置 -->
    <slot></slot>
  </div>
</template>

<script>
export default {
  
}
</script>

<style scoped>

</style>

2. 安装插槽、将配件插入插槽 

<!-- App.vue -->

<template>
  <div id="app">
    <!-- 相当于将槽安装到了电脑上,我们需要用这个槽时,就可以将对应的配件插件去 -->
    <slotTest>
      <!-- 向槽中插入对应的配件 -->
      <p>人生最爱:小树林</p>
      <!--  ---------------  -->
    </slotTest>
    <!--     ----------------------------------------------------     -->
  </div>
</template>

<script>
// 引入插槽
import slotTest from './slotTest.vue'
export default {
  data () {
  },
  // 将插槽声明成组件,就可以使用
  components: {
    slotTest
  }
}
</script>

<style>
</style>

再来看一个稍微复杂一丢丢的用法:

结构上是插槽套了一个插槽,不过也很容易理解,我们将插槽看作它的父级元素即可,来看一下

<!-- App.vue -->

<template>
  <div id="app">
    <list-slot>
      <item-slot>娘子!</item-slot>
      <item-slot>啊哈~</item-slot>
      <item-slot>ku~liu~liu~liu~dei~hei~</item-slot>
    </list-slot>
  </div>
</template>

<script>
import listSlot from './components/listSlot.vue';
import itemSlot from './components/itemSlot.vue';
export default {
  data () {
  },
  components: {
    'list-slot': listSlot,
    'item-slot': itemSlot
  }
}
</script>

<style>
</style>
<!-- listSlot.vue -->

<template>
  <ul class="list-slot">
    <slot></slot>
  </ul>
</template>

<script>
export default {
  
}
</script>

<style scoped>
  
</style>
<!-- itemSlot.vue -->

<template>
  <li>
    <slot></slot>
  </li>
</template>

<script>
export default {
  
}
</script>

<style scoped>
  
</style>

3. 插槽默认值

这个很简单,只需要在<slot>默认值</slot>中间写上对应的默认值即可。 

十三、插槽传参

date数据位于CurrentUser插槽中,但我们想在App.vue中使用这些数据,怎么办呢?

很简单,只需要在使用数据的地方,外面套一层template标签,然后给标签添加v-slot:default属性,即可拿到被引用的插槽传来的数据,看下面的例子

<!-- App.vue -->

<template>
  <div id="app">
    <current-user>
      <!-- 在数据的外面,套一层template标签,然后添加一个属性即可获取到传递来的数据 -->
      <template v-slot:default="{ user }">
        {{user.name}}: {{user.skill}} biu~ biu~ biu~
      </template>
    </current-user>
  </div>
</template>

<script>
  import CurrentUser from './components/CurrentUser.vue';

  export default{
    components: {
      CurrentUser
    }
  }
</script>

<style>
</style>
<!-- CurrentUser.vue -->

<template>
  <h1>
    <slot :user="user">
      {{ user.name }}
    </slot>
  </h1>
</template>

<script>
export default {
  data() {
    return {
      user: {
        name: '动感超人',
        skill: '动感光波!'
      }
    }
  }
}
</script>

<style scoped>
  
</style>

一定要注意,"v-slot:defalult" v与default之间,不要习惯性地加一个空格,这里整体是一个属性,被空格分隔开就无法识别了,然后就会报下面的错误:

 十四、具名插槽

具名插槽,就是我们可以定义多个插槽。在使用插槽时,通过v-slot指定具体插槽的名字,将内容放至对应的插槽中,而没有指定名字的部分,都被放置默认插槽中

其次,页面显示的顺序,取决于我们在插槽组件中,插槽定义的顺序,而非使用插槽的顺序,我们一起来看一下:

<!-- App.vue -->

<template>
  <div id="app">
    <layout>
      <p>文本1</p>
      <p>文本2</p>

      <template v-slot:footer>
        <h1>footer</h1>
      </template>
      <template v-slot:header>
        <h1>header</h1>
      </template>

      <p>文本3</p>
    </layout>
  </div>
</template>

<script>
  import Layout from './components/Layout.vue';

  export default{
    components: {
      Layout
    }
  }
</script>

<style>
</style>
<!-- Layout.vue -->

<template>
  <div id="app">
    <header>
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot>
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div> 
</template>

<script>
export default {
  
}
</script>

<style scoped>
  
</style>

 十五、动态组件

<!-- App.vue -->

<template>
  <div id="app">
    <div v-for="item in productData" :key="item.id">
      <!-- 可以看到,:is的值,应该与我们注册组件时,对组件命的名相同 -->
      <component :is="`My${item.type}`"></component>
    </div>
  </div>
</template>

<script>
  import Image from './components/Image.vue';
  import Text from './components/Text.vue';
  import Video from './components/Video.vue';

  export default{
    // 模拟从后台获取的数据
    data() {
      return {
        productData: [
          {
            id: 1,
            type: 'Text'
          },
          {
            id: 2,
            type: 'Image'
          },
          {
            id: 3,
            type: 'Video'
          }
        ]
      }
    },
    components: {
      // 其实这里可以不去重命名,这样在使用时,就不需要在:is中去拼接My了
      MyImage: Image,
      MyText: Text,
      MyVideo: Video
    }
  }
</script>

<style>
</style>
<!-- Video、Text、Image -->

<template>
  <div id="video">
    <h1>
      视频
    </h1>
  </div> 
</template>

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

<style scoped>
  
</style>

 页面中渲染的数据,就取决于后台返回的数据。可以根据后台返回的数据,任意顺序、任意数量地使用组件。

十六、异步组件

当我们的组件中,引入着一些非常大的组件时,又不确定用户是否为激活这些组件,若直接将这些组件加载下来,会造成很大的资源浪费,因此我们就可以选择异步加载。只有当用户激活这些组件了,才去加载,不激活就不加载,可以在一定程度上节省资源的开销。

<!-- 异步引入方式 -->

<template>
  <div id="app">
    <test v-if="show"></test>
    <button @click="show = true">显示Test组件</button>
  </div>
</template>

<script>
import Test from './components/Test.vue'
export default{
  data() {
    return {
      show: false
    }
  },
  components: {
    // Test: () => import('./components/Test')
    Test
  }
}
  
</script>

<style>
</style>
<!-- Test.vue -->

<template>
  <div id="app">
    Test组件
  </div>
</template>

<script>
export default {
  
}
</script>

<style scoped>
  
</style>

先来看一下同步import引入,由components注册组件的结果是什么样的:

 可以看到,虽然我们没有点击按钮,但是Test组件其实已经被加载出来了,只是还没有渲染到页面,我们如果在是这个时候点击按钮,是不可能再去发送请求的,因为Test资源已经被请求过了。

对比一下异步引入Test组件的结果:

 可以看到,在我们还没有点击按钮时,Test组件并没有被加载到本地。

点击按钮:

 可以看到,成功加载至本地。

这就是同步与异步的差别,当一些资源,很有可能不会被用户浏览,或者说被用户浏览的概率非常非常小,就可以选择用异步的方式。减少页面初次加载时发送的请求数量,来降低请求时间,提升页面响应速度,给用带来更流畅的使用体验。

十七、用keep-alive实现组件缓存

ABC分别对应三个组件,点击不同的按钮,可以控制触发不同的组件

<!-- App.vue -->

<template>
  <div id="app">
    <button @click="state = 'A'">A</button>
    <button @click="state = 'B'">B</button>
    <button @click="state = 'C'">C</button>

    <comp-a v-if="state === 'A'"></comp-a>
    <comp-b v-if="state === 'B'"></comp-b>
    <comp-c v-if="state === 'C'"></comp-c>
  </div>
</template>

<script>
import CompA from './components/CompA.vue'
import CompB from './components/CompB.vue'
import CompC from './components/CompC.vue'
export default{
  data() {
    return {
      state: 'A'
    }
  },
  components: {
    CompA,
    CompB,
    CompC
  }
}
  
</script>

<style>
</style>
<!-- CampA.vue、CampB.vue、CampC.vue -->

<template>
  <div id="comp-a">
    组件A
  </div>
</template>

<script>
export default {
  mounted(){
    console.log( '组件A渲染' );
  },
  destroyed(){
    console.log( '组件A销毁' );
  }
}
</script>

<style scoped>
  
</style>

"组件A渲染" 触发于页面的初次渲染

然后我们依次点击B、C、A按钮,可以看到: 

如果每个组件的代码量都非常地大,组件不断地渲染、销毁,会十分消耗性能。

这个时候,我么就可以借助keep-alive对Dom结构进行缓存,使用方式很简单,只需要将需要缓存的组件放到keep-alive中即可:

<!-- App.vue -->

<template>
  <div id="app">
    <button @click="state = 'A'">A</button>
    <button @click="state = 'B'">B</button>
    <button @click="state = 'C'">C</button>

    <keep-alive>
      <comp-a v-if="state === 'A'"></comp-a>
      <comp-b v-if="state === 'B'"></comp-b>
      <comp-c v-if="state === 'C'"></comp-c>
    </keep-alive>
  </div>
</template>

 这个时候我们再去点击三个按钮,就会发现,仅仅只会去执行重新渲染的操作,而不再去销毁、创建。

即使我们再次点击A按钮,也不会再次去执行组件A渲染,因为这些都可以从缓存中获取

 

易混知识点对比:v-show于keep-alive

<!-- App.vue -->

<template>
  <div id="app">
    <button @click="state = 'A'">A</button>
    <button @click="state = 'B'">B</button>
    <button @click="state = 'C'">C</button>

    <comp-a v-show="state === 'A'"></comp-a>
    <comp-b v-show="state === 'B'"></comp-b>
    <comp-c v-show="state === 'C'"></comp-c>
  </div>
</template>

 

 我们发现,页面一加载,组件ABC就全部被加载出来了。这显然是不符合我们的需求的,我们一开始的目的,就是在寻找针对较大组件的解决方案。

一下子就把全部的大组件都加载出来,这得消耗多少性能,给用户带来的体验,恐怕就只有:这软件太卡了,启动地这么慢!

十八、使用mixin抽离公共逻辑

我们书写代码时,难免会遇到很多公共的属性、方法、生命周期函数,如果我们在每个组件都写入这些代码,第一代码冗余,组件间的数据维护十分困难

我们可以定义一个mixin文件,来专门存放这些公共的数据,这样就很大程度地降低了开发难度

<!-- App.vue -->

<template>
  <div id="app">
    <comp-a></comp-a>
    <hr>
    <comp-b></comp-b>
  </div>
</template>

<script>
import CompA from './components/CompA.vue';
import CompB from './components/CompB.vue';

export default{
  components: {
    CompA,
    CompB
  }
}
  
</script>

<style>
</style>
<!-- CompA.vue、CompB.vue -->

<template>
  <div id="comp-a">
    <p>{{ aData }}</p>
    <p>{{ commonData }}</p>
    <button @click="aMethod">aMethod</button>
    <button @click="commonMethod">commonMethod</button>
  </div>
</template>

<script>
export default {
  data(){
    return {
      aData: '组件A的数据',
      commonData: '公共的数据'
    }
  },
  methods: {
    aMethod() {
      console.log( '组件A的方法' );
    },
    commonMethod() {
      console.log( '公共的方法' );
    }
  },
  mounted() {
    console.log( '组件A mounted' );
    console.log( 'common mounted' );
  }
}
</script>

<style scoped>
  
</style>

 可以看到,其实组件A、B之间有很多重复的公共代码,虽然这样写起来十分复杂,但是只要功底够硬,运行起来还是没有问题的:

不过,对代码的维护,可能就是个十分困难的问题。

既然有这么多公共的代码,我们为什么不将这些代码维护到一起呢?

来,我们一起看一下如果借助mixin,将公共的代码给维护起来:

先创建一个mixin.js文件

// mixin.js

export default{
  data(){
    return {
      commonData: '公共的数据'
    }
  },
  methods: {
    commonMethod() {
      console.log( '公共的方法' );
    }
  },
  mounted() {
    console.log( 'common mounted' );
  }
}

 去掉组件A中公共的数据,然后将mixin引入进去

<template>
  <div id="comp-a">
    <p>{{ aData }}</p>
    <p>{{ commonData }}</p>
    <button @click="aMethod">aMethod</button>
    <button @click="commonMethod">commonMethod</button>
  </div>
</template>

<script>
import mixin from './mixin';

export default {
  mixins: [mixin],
  data(){
    return {
      aData: '组件A的数据',
    }
  },
  methods: {
    aMethod() {
      console.log( '组件A的方法' );
    }
  },
  mounted() {
    console.log( '组件A mounted' );
  }
}
</script>

<style scoped>
  
</style>

测试,一切正常,这样对于公共数据的维护,减轻了极大的负担。 

十九、Vue3与Vue2声明周期的区别

1. vue2中的beforeDestroy和destroyed,在Vue3中变成了beforeUmount、unmounted。

2. Vue3提供的Composition API写法

<!-- App.vue -->

<template>
  <div id="app">
    {{ message }}
  </div>
</template>

<script>
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted
} from 'vue';

export default{

  // Vue3
  setup() {  // setup 相当于 beforeCreate + created 两个生命周期函数
    onBeforeMount(()=>{ })
    onMounted(()=>{ })
    onBeforeUpdate(()=>{ })
    onUpdated(()=>{ })
    onBeforeUnmount(()=>{ })
    onUnmounted(()=>{ })
  },

  // Vue2
  beforeCreate(){ },
  created(){ },
  beforeMount(){ },
  mounted(){ },
  beforeUpdate(){ },
  updated(){ },
  onBeforeUnmount(){ },
  unmounted(){ }
}
  
</script>

<style>
</style>

二十、Composition API之 代码优化

// UserRepositories.vue

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: { 
      type: String,
      required: true
    }
  },
  data () {
    return {
      repositories: [], // 1
      filters: { ... }, // 3
      searchQuery: '' // 2
    }
  },
  computed: {
    filteredRepositories () { ... }, // 3
    repositoriesMatchingSearchQuery () { ... }, // 2
  },
  watch: {
    user: 'getUserRepositories' // 1
  },
  methods: {
    getUserRepositories () {
      // 使用 `this.user` 获取用户仓库
    }, // 1
    updateFilters () { ... }, // 3
  },
  mounted () {
    this.getUserRepositories() // 1
  }
}

该组件可能拥有三个任务:1. 渲染一个仓库列表 2. 支持对仓库数据的过滤 3. 以及筛选

我们会在这其中写很多属性方法,以提供对上述功能的支持。

可以看到,每个任务的不同不同操作,被分割在了不同的选项中,于是,代码看起来就会是这种感觉:

 这个时候,对于代码的维护,就会变得十分困难,因为同一个功能,方法被写的七零八散,寻找对应的代码,就十分地麻烦

这时,Composition API就可以发挥它的作用了

它地目的就会对代码按功能进行分割,一个组件包含了三个功能,那么,每个功能都可以对应一个setup

用户列表有关的功能:

// UserRepositories.vue
import { fetchUserRepositories } from '@/api/repositories'

// 在我们的组件内
setup (props) {
  let repositories = []
  const getUserRepositories = async () => {
    repositories = await fetchUserRepositories(props.user)
  }

  return {
    repositories,
    getUserRepositories // 返回的函数与方法的行为相同
  }
}

可以看到,存放仓库数据的代码都在一个setup中,以及根据不同的用户请求不同仓库的方法

// useUserRepositories.js

import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch } from 'vue'

export default function useUserRepositories(user) {
  const repositories = ref([])
  const getUserRepositories = async () => {
    repositories.value = await fetchUserRepositories(user.value)
  }

  onMounted(getUserRepositories)
  watch(user, getUserRepositories)

  return {
    repositories,
    getUserRepositories
  }
}

同时,对搜索功能也进行拆分:

// userRepositoryNameSearch.js

import { ref, computed } from 'vue'

export default function useRepositoryNameSearch(repositories) {
  const searchQuery = ref('')
  const repositoriesMatchingSearchQuery = computed(() => {
    return repositories.value.filter(repository => {
      return repository.name.includes(searchQuery.value)
    })
  })

  return {
    searchQuery,
    repositoriesMatchingSearchQuery
  }
}

过滤也是如此,

然后在文件中将每个不同的功能都引入进去

import useUserRepositories from './composables/userRepositoryName';
import useRepositoryNameSearch from './composables/useRepositoryNameSearch';
import useRepositoryFilters from './composables/useRepositoryFilters';

export default{
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: { 
      type: String,
      required: true
    }
  },
  setup(props){
    const {user} = toRefs(props);

    // 请求用户仓库
    const {
      repositories, 
      getUserRepositories
    } = useUserRepositories(user);

    // 搜索
    const {
      searchQuery,
      repositoriesMatchingSearchQuery
    } = useRepositoryNameSearch(repositories);

    // 过滤
    const {
      filters,
      updateFilters,
      filteredRepositories
    } = useRepositoryFilters(repositoriesMatchingSearchQuery);

    return {
      // 因为我们并不关心未经过滤的仓库
      // 我们可以在`repositories`名称下暴露过滤后的结果
      repositories: filteredRepositories,
      getUserRepositories,
      searchQuery,
      filters,
      updateFilters
    }
  }
}

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

麦田里的POLO桔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值