电脑基础 · 2023年3月25日

【前端面试专题】【3】Vue2

Vue 2.x

Vue 基本使用

指令、插值

  • 插值、表达式
  • 指令、动态属性
  • v-html:会有 XSS 风险,会覆盖子组件
<template>
  <div>
    <p>文本插值 {{ mesage }}</p>
    <p>JS表达式 {{ flag ? 'yes' : 'no' }}</p> <!-- 只能是表达式,不能是 js 语句 -->
    <p :id="dynamicId">动态属性 id</p>
    <p v-html="rawHtml">
      <span>【注意】使用 v-html 之后,将会覆盖子元素</span>
    </p>
  </div>
</template>
<script>
export default {
  data() {
    return {
      message: 'hello',
      flag: true,
      rawHtml: '<b>这是加粗</b><i>这是斜体</i>',
      dynamicId: `id-${Date.now()}`
    }
  }
}
</script>
<style scoped></style>

computed 和 watch

  • computed 有缓存,data 不变则不会重新计算
  • watch 如何深度监听?
  • watch 监听引用类型,拿不到 oldVal
<template>
  <div>
    <p>num {{ num }}</p>
    <p>double1 {{ double1 }}</p>
    <input v-model="double2" />
  </div>
</template>
<script>
export default {
  data() {
    return {
      num: 20
    }
  },
  computed: {
    double1() {
      return this.num * 2
    },
    // v-model 绑定的话需要写 get set
    double2: {
      get() {
        return this.num * 2
      },
      set(val) {
        return this.num = val / 2
      }
    }
  }
}
</script>
<style scoped></style>
<template>
  <div>
    <input v-model="name" />
    <input v-model="info.city" />
  </div>
</template>
<script>
export default {
  data() {
    return {
      name: 'Jae',
      info: {
        city: 'xx'
      }
    }
  },
  computed: {},
  watch: {
    name(oldVal, val) {
      console.log('watch name', oldVal, val) // 值类型,可以正常拿到 oldVal 和 val
    },
    info: {
      handler(oldVal, val) {
        console.log('watch info', oldVal, val) // 引用类型,拿不到 oldVal。因为指针相同,此时已经指向了新的 val
      },
      deep: true // 深度监听
    }
  }
}
</script>
<style scoped></style>

class 和 style

  • 使用动态属性
  • 使用驼峰式写法
<template>
  <div>
    <p :class="{ black: isBack, yellow: isYellow }">使用 class</p>
    <p :class="[ black, yellow ]">使用 class(数组)</p>
    <p :style="styleData">使用 style</p>
  </div>
</template>
<script>
export default {
  data() {
    return {
      isBack: true,
      isYellow: true,
      black: 'black',
      yellow: 'yellow',
      styleData: {
        fontSize: '40px', // 驼峰
        color: 'red',
        backgroundColor: '#ccc' // 驼峰
      }
    }
  }
}
</script>
<style scoped></style>

条件渲染

  • v-if v-else 的用法,可使用变量,也可以使用 === 表达式
  • v-if 和 v-show 的区别
  • v-if 和 v-show 的使用场景
<template>
  <div>
    <p v-if="type === 'a'">A</p>
    <p v-else-if="type === 'b'">B</p>
    <p v-else>other</p>
    <p v-show="type === 'a'">A by v-show</p>
    <p v-show="type === 'b'">B by v-show</p>
  </div>
</template>
<script>
export default {
  data() {
    return {
      type: 'a'
    }
  }
}
</script>
<style scoped></style>

【前端面试专题】【3】Vue2
如果页面上需要频繁切换渲染,用 v-show;如果渲染的次数不频繁,用 v-if

循环(列表)渲染

  • 如何遍历对象?—— 也可以用v-for
  • key 的重要性
  • v-for 和 v-if 不能一起使用(v-for 优先级高,会执行 n 次循环后再进行 n 次 v-if 判断)
<template>
  <div>
    <p>遍历数组</p>
    <ul>
      <li v-for="(item, index) in arrList" :key="item.id">
        {{ index }} - {{ item.id }} - {{ item.title }}
      </li>
    </ul>
    <p>遍历对象</p>
    <ul>
      <li v-for="(val, key, index) in objList" :key="key">
        {{ index }} - {{ key }} - {{ val.title }}
      </li>
    </ul>
  </div>
</template>
<script>
export default {
  data() {
    return {
      flag: false,
      arrList: [
        { id: '1', title: 'title1' },
        { id: '2', title: 'title2' },
        { id: '3', title: 'title3' }
      ],
      objList: {
        a: { title: 'title1' },
        b: { title: 'title2' },
        c: { title: 'title3' }
      }
    }
  }
}
</script>
<style scoped></style>

事件

  • event 参数,自定义参数
  • 事件修饰符,按键修饰符
  • 事件被绑定到哪里?
<template>
  <div>
    <p>{{ num }}</p>
    <button @click="handleClickIncre1">+1</button>
    <button @click="handleClickIncre2(2, $event)">+2</button>
	<!-- 事件修饰符 -->
	<!-- 阻止单击事件继续传播 -->
    <a @click.stop="doSomeThing"></a>
    <!-- 提交事件不再重载页面 -->
    <form @submit.prevent="onSubmit"></form>
    <!-- 修饰符的串联 -->
    <a @click.stop.prevent="doSomeThing"></a>
    <!-- 只有修饰符 -->
    <form @submit.prevent></form>
    <!-- 添加事件监听器时使用事件捕获模式,即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
    <div @click.capture="doSomeThing"></div>
    <!-- 只当在 event.target 是当前元素自身时触发处理函数,即事件不是从内部元素触发的 -->
    <div @click.self="doSomeThing"></div>
	<!-- 按键修饰符 -->
	<button @click.ctrl="onClick">A</button>
    <!-- 有且只有 Ctrl 被按下时才触发 -->
    <button @click.ctrl.exact="onClick">A</button>
    <!-- 没有任何系统修饰符被按下时才触发 -->
    <button @click.exact="onClick">A</button>
  </div>
</template>
<script>
export default {
  data() {
    return {
      num: 0
    }
  },
  methods: {
    handleClickIncre1(event) {
      // 原生的 event 对象 
      console.log('event', event, event.__proto__.constructor) // event PointerEvent {...} PointerEvent() { [native code] }
      console.log(event.target) // <button>+1</button>
      console.log(event.currentTarget) // 事件是被注册到当前元素的
      this.num ++
    },
    handleClickIncre2(val, event) {
      console.log(event.target) // <button>+2</button>
      this.num = this.num + val
    }
  }
}
</script>
<style scoped></style>

表单

  • v-model
  • 常见表单项 textarea checkbox radio select
  • 修饰符 lazy number trim
<template>
  <div>
    <p>输入框:{{ name }}</p>
    <input type="text" v-model.trim="name" />
    <input type="text" v-model.lazy="name" />
    <input type="text" v-model.number="age" />
    <p>多行文本:{{ desc }}</p>
    <!-- 注意,不是<textarea>{{ desc }}</textarea> -->
    <textarea v-model="desc"></textarea>
    <p>复选框 {{ checked }}</p>
    <input type="checkbox" v-model="checked" />
    <p>多个复选框 {{ checkedNames }}</p>
    <input type="checkbox" id="jae" value="Jae" v-model="checkedNames" />
    <label for="jae">Jae</label>
    <input type="checkbox" id="tom" value="Tom" v-model="checkedNames" />
    <label for="tom">Tom</label>
    <input type="checkbox" id="amy" value="Amy" v-model="checkedNames" />
    <label for="amy">Amy</label>
    <p>单选 {{ gender }}</p>
    <input type="radio" id="male" value="male" v-model="gender" />
    <label for="male"></label>
    <input type="radio" id="female" value="female" v-model="gender" />
    <label for="female"></label>
    <p>下拉列表选择 {{ selected }}</p>
    <select v-model="selected">
      <option disabled value="">请选择</option>
      <option>A</option>
      <option>B</option>
      <option>C</option>
    </select>
    <p>下拉列表多选 {{ selectedList }}</p>
    <select v-model="selectedList" multiple>
      <option disabled value="">请选择</option>
      <option>A</option>
      <option>B</option>
      <option>C</option>
    </select>
  </div>
</template>
<script>
export default {
  data() {
    return {
      name: 'Jae',
      age: 22,
      desc: 'hello',
      checked: true,
      checkedNames: [],
      gender: 'male',
      selected: '',
      selectedList: []
    }
  }
}
</script>
<style scoped></style>

【前端面试专题】【3】Vue2

Vue 组件使用

props 和 $emit

代码示例:

<!-- index.vue -->
<template>
  <div>
    <Input @add="addHandler" />
    <List :list="list" @delete="deleteHandler" />
  </div>
</template>
<script>
import Input from './input'
import List from './List'
export default {
  name: 'ComponentsDemo',
  components: {
    Input,
    List
  },
  data() {
    return {
      list: [
        {
          id: 'id-1',
          title: '标题1'
        }
      ]
    }
  },
  methods: {
    addHandler(title) {
      this.list.push({
        id: `id-${Date.now()}`,
        title
      })
    },
    deleteHandler(id) {
      console.log(1);
      this.list = this.list.filter(item => item.id !== id)
    }
  }
}
</script>
<!-- Input.vue -->
<template>
  <div>
    <input type="text" v-model="title">
    <button @click="addTitle">add</button>
  </div>
</template>
<script>
export default {
  name: 'InputName',
  data() {
    return {
      title: ''
    }
  },
  methods: {
    addTitle() {
      this.$emit('add', this.title)
      this.title = ''
    }
  }
}
</script>
<!-- List.vue -->
<template>
  <div>
    <ul>
      <li v-for="item in list" :key="item.id">
        {{ item.title }}
        <button @click="deleteItem(item.id)">删除</button>
      </li>
    </ul>
  </div>
</template>
<script>
export default {
  name: 'ListName',
  props: {
    list: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {}
  },
  methods: {
    deleteItem(id) {
      this.$emit('delete', id)
    }
  }
}
</script>

【前端面试专题】【3】Vue2

组件间通讯 - 自定义事件

// event.js 
import Vue from 'vue'
export default new Vue()
<!-- Input.vue -->
<script>
import event from './event'
export default {
  name: 'InputName',
  data() {
    return {
      title: ''
    }
  },
  methods: {
    addTitle() {
      event.$emit('inputAddTitle', this.title)
    }
  }
}
</script>
<!-- List.vue -->
<script>
import event from './event'
export default {
  mounted() {
    event.$on('inputAddTitle', this.addTitleHandler)
  },
  beforeDestroy() {
    // 及时销毁,否则可能造成内存泄漏
    event.$off('inputAddTitle', this.addTitleHandler)
  },
  methods: {
    addTitleHandler(title) {
      console.log('input组件emit过来的:', title)
    }
  }
}
</script>

【前端面试专题】【3】Vue2

组件生命周期

1)单个组件

  • 挂载阶段(beforeCreate、created、beforeMount、mounted)
  • 更新阶段(beforeUpdate、updated)
  • 销毁阶段(beforeDestroy、destroyed)
    【前端面试专题】【3】Vue2
    2)父子组件
    参考上方 《prop 和 $emit》中的代码示例:
    【前端面试专题】【3】Vue2
<!-- index.vue -->
created() {
  console.log('index.vue created')
},
mounted() {
  console.log('index.vue mounted')
}
<!-- List.vue -->
created() {
  console.log('List.vue created')
},
mounted() {
  console.log('List.vue mounted')
}

输出为:

index.vue created
List.vue created
List.vue mounted
index.vue mounted

即:父组件比子组件先创建,但是父组件要等子组件渲染完才能渲染

Vue 高级特性

自定义 v-model

<!-- index.vue -->
<template>
  <div>
    <!-- 自定义 v-model -->
    <p>{{ text }}</p>
    <CustomVModel v-model="text" />
  </div>
</template>
<script>
import CustomVModel from './CustomVModel.vue'
export default {
  name: 'AdvancedUse',
  components: {
    CustomVModel
  },
  data() {
    return {
      text: 'Jae'
    }
  }
}
</script>
<!-- CustomVModel.vue -->
<template>
  <div>
    <input type="text" :value="text" @input="$emit('change', $event.target.value)">
  </div>
</template>
<script>
export default {
  model: {
    prop: 'text', // 对应 props 中的 text
    event: 'change'
  },
  props: {
    text: {
      type: String,
      default: ''
    }
  }
}
</script>

【前端面试专题】【3】Vue2

$nextTick

  • Vue 是异步渲染
  • data 改变之后,DOM 不会立刻渲染
  • $nextTick 会在 DOM 渲染之后被触发,以获取最新 DOM 节点

代码示例:

<template>
  <div>
    <ul ref="ul">
      <li v-for="(item, index) in list" :key="index">{{ item }}</li>
    </ul>
    <button @click="add">添加</button>
  </div>
</template>
<script>
export default {
  name: 'NextTick',
  data() {
    return {
      list: [1, 2, 3]
    }
  },
  methods: {
    add() {
      this.list.push(`${Date.now()}`)
      this.list.push(`${Date.now()}`)
      this.list.push(`${Date.now()}`)
      const ulEle = this.$refs.ul // 获取 DOM 元素
      console.log(ulEle.childNodes.length)
    }
  }
}
</script>

【前端面试专题】【3】Vue2
可以看出这个输出结果不太对,一开始是3个,点击添加后添加了3个,怎么输出长度还是3呢?这就是因为此时输出的 DOM 节点还是上一次的,还未更新,如果需要得到正常的结果,就需要加上 $nextTick 确保 DOM 渲染完成:

this.$nextTick(() => {
  const ulEle = this.$refs.ul
  console.log(ulEle.childNodes.length)
})

【前端面试专题】【3】Vue2

slot

基本使用

代码示例:

<!-- index.vue -->
<template>
  <div>
    <SlotDemo :url="website.url">
      {{ website.title }}
    </SlotDemo>
  </div>
</template>
<script>
import SlotDemo from './SlotDemo.vue'
export default {
  name: 'AdvancedUse',
  components: {
    SlotDemo
  },
  data() {
    return {
      website: {
        url: 'https://www.bing.com',
        title: '搜索引擎',
        subTitle: '比百度好用的搜索引擎'
      }
    }
  }
}
</script>
<!-- SlotDemo.vue -->
<template>
  <div>
    <a :href="url">
      <slot>
        默认内容,即父组件没设置内容时,显示的东西
      </slot>
    </a>
  </div>
</template>
<script>
export default {
  name: 'SlotDemo',
  props: {
    url: {
      type: String,
      defalut: ''
    }
  },
  data() {
    return {}
  }
}
</script>

【前端面试专题】【3】Vue2
如果将 index.vue 中 SlotDemo 组件修改一下:

<!-- SlotDemo.vue -->
<SlotDemo :url="website.url" />

【前端面试专题】【3】Vue2

作用域插槽

代码示例:

<!-- index.vue -->
<template>
  <div>
    <ScopedSlotDemo :url="website.url">
      <!-- 我想在这里显示子组件中 website 中的 title,怎么获取得到? -->
    </ScopedSlotDemo>
  </div>
</template>
<script>
import ScopedSlotDemo from './ScopedSlotDemo.vue'
export default {
  name: 'AdvancedUse',
  components: {
    ScopedSlotDemo
  },
  data() {
    return {}
  }
}
</script>
<!-- ScopedSlotDemo.vue -->
<template>
  <div>
    <a :href="url">
      <slot :slotData="website"> <!-- 将 website 放在 slotData 中,这个名字可以随便取 -->
        {{ website.subTitle }} <!-- 默认值显示 subTitle,即父组件不传值 -->
      </slot>
    </a>
  </div>
</template>
<script>
export default {
  props: {
    url: {
      type: String,
      defalut: ''
    }
  },
  data() {
    return {
      website: {
        url: 'https://www.baidu.com',
        title: '某搜索引擎',
        subTitle: '比bing不好用的搜索引擎'
      }
    }
  }
}
</script>

在父组件中的插槽接收即可:

<!-- index.vue -->
<template>
  <div>
    <ScopedSlotDemo :url="website.url">
      <template v-slot="{ slotData }"> <!-- 这里是解构写法,原来是 v-slot="slotProps",这个 slotProps 也可以自己取名 -->
        {{ slotData.title }} <!-- 如果上面是用 slotProps 接收,那这里就是写成 slotProps.slotData.title  -->
      </template>
    </ScopedSlotDemo>
  </div>
</template>
<script>
import ScopedSlotDemo from './ScopedSlotDemo.vue'
export default {
  name: 'AdvancedUse',
  components: {
    ScopedSlotDemo
  },
  data() {
    return {
      website: {
        url: 'https://www.bing.com',
        title: '搜索引擎',
        subTitle: '比百度好用的搜索引擎'
      }
    }
  }
}
</script>

【前端面试专题】【3】Vue2

具名插槽
<!-- NamedSlot.vue -->
<div>
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
  	<slot name="footer"></slot>
  </footer>
</div>
<!-- index.vue -->
<NamedSlot>
  <template #header>
    <h1>将插入 header slot 中</h1>
  </template>
  <p>将插入 main slot 中,即默认的未命名的 slot</p>
  <template #header>
    <h2>将插入 footer slot 中</h2>
  </template>
</NamedSlot>

动态、异步组件

动态组件
  • :is="componentName" 用法
  • 需要根据数据,动态渲染的场景。即组件类型不确定
<template>
  <div>
  	<!-- 动态组件 -->
    <component :is="componentName" />
  </div>
</template>
<script>
import NextTick from './NextTick.vue'
export default {
  name: 'AdvancedUse',
  components: {
    NextTick
  },
  data() {
    return {
      componentName: 'NextTick'
    }
  }
}
</script>
异步组件
  • import() 函数
  • 按需加载,异步加载大组件
<template>
  <div>
  	<!-- 异步组件 -->
    <NextTick v-if="showCom" />
    <button @click="showCom = true">show Component</button>
  </div>
</template>
<script>
// import NextTick from './NextTick.vue' 不要同步引进来
export default {
  name: 'AdvancedUse',
  components: {
    NextTick: () => import ('../components/NextTick') // 动态引入
  },
  data() {
    return {
      showCom: false
    }
  }
}
</script>

keep-alive

  • 缓存组件
  • 频繁切换,不需要重复渲染(比如 tabs 标签页)
  • Vue 常见性能优化
<!-- index.vue -->
<template>
  <div>
    <button @click="changeState('A')">A</button>
    <button @click="changeState('B')">B</button>
    <button @click="changeState('C')">C</button>
    <keepAliveStateA  v-if="state === 'A'" />
    <keepAliveStateB  v-if="state === 'B'" />
    <keepAliveStateC  v-if="state === 'C'" />
  </div>
</template>
<script>
import keepAliveStateA from './keepAliveStateA'
import keepAliveStateB from './keepAliveStateB'
import keepAliveStateC from './keepAliveStateC'
export default {
  name: 'KeepAlive',
  components: {
    keepAliveStateA,
    keepAliveStateB,
    keepAliveStateC
  },
  data() {
    return {
      state: 'A'
    }
  },
  methods: {
    changeState(state) {
      this.state = state
    }
  }
}
</script>
<!-- keepAliveStateA.vue -->
<template>
  <div>state A</div>
</template>
<script>
export default {
  data() {
    return {}
  },
  computed: {},
  watch: {},
  created() {},
  mounted() {
    console.log('A mounted')
  },
  destroyed() {
    console.log('A destroyed')
  },
}
</script>
<!-- keepAliveStateB.vue -->
<template>
  <div>state B</div>
</template>
<script>
export default {
  data() {
    return {}
  },
  computed: {},
  watch: {},
  created() {},
  mounted() {
    console.log('B mounted')
  },
  destroyed() {
    console.log('B destroyed')
  },
}
</script>
<!-- keepAliveStateC.vue -->
<template>
  <div>state C</div>
</template>
<script>
export default {
  data() {
    return {}
  },
  computed: {},
  watch: {},
  created() {},
  mounted() {
    console.log('C mounted')
  },
  destroyed() {
    console.log('C destroyed')
  },
}
</script>

【前端面试专题】【3】Vue2
如果不需要每次切换的时候都销毁组件,该怎么做呢?使用 keep-alive

<template>
  <div>
    <button @click="changeState('A')">A</button>
    <button @click="changeState('B')">B</button>
    <button @click="changeState('C')">C</button>
	<keep-alive>
      <keepAliveStateA  v-if="state === 'A'" />
      <keepAliveStateB  v-if="state === 'B'" />
      <keepAliveStateC  v-if="state === 'C'" />
    </keep-alive>
  </div>
</template>
</script>

【前端面试专题】【3】Vue2

mixin

  • 多个组件有相同的逻辑,抽离出来
  • mixin并不是完美的解决方案,会有一些问题
  • Vue3 提出的 Composition API 旨在解决这些问题
<!-- index.vue -->
<template>
  <div>
    <p>{{ name }} {{ age }} {{ city }}</p>
    <button @click="showName">显示姓名</button>
  </div>
</template>
<script>
import myMixin from './mixin'
export default {
  mixins: [myMixin], // 可添加多个
  data() {
    return {
      name: 'Jae',
      age: 22
    }
  },
  mounted() {
    console.log('index mounted')
  },
}
</script>
// mixin.js
export default {
  data() {
    return {
      city: 'XXX'
    }
  },
  mounted() {
    console.log('mixin mounted')
  },
  methods: {
    showName() {
      console.log(this.name)
    }
  }
}

【前端面试专题】【3】Vue2
mixin 的问题

  • 变量来源不明确,不利于阅读
  • 多 mixin 可能造成命名冲突
  • mixin 和组件可能出现多对多的关系,复杂度较高

Vuex 的使用

Vuex 基本概念

  • state
  • getters
  • action
  • mutation

用于 Vue 组件

  • dispatch
  • commit
  • mapState
  • mapGetters
  • mapActions
  • mapMutations

【前端面试专题】【3】Vue2

Vue-router 的使用

路由模式(hash、H5 history)

  • hash 模式(默认),如 http://xxx.com/#/detail/10
  • H5 history 模式,如 http://xxx.com/detail/20
  • 后者需要 server 端支持,一般无特殊需求选则前者

路由配置(动态路由、懒加载)

const router = new VueRouter({
  routes: [
    // 动态路径参数,以冒号开头
    { path: '/user/:id', component: User }
  ]
})
export default new VueRouter({
  routes: [
    {
      path: '/',
      component: () => import('./../components/home')
    }
  ]
})