写点什么

社招前端经典 vue 面试题汇总

作者:bb_xiaxia1998
  • 2022-12-07
    浙江
  • 本文字数:21124 字

    阅读完需:约 69 分钟

用过 pinia 吗?有什么优点?

1. pinia 是什么?


  • Vue3中,可以使用传统的Vuex来实现状态管理,也可以使用最新的pinia来实现状态管理,我们来看看官网如何解释pinia的:PiniaVue 的存储库,它允许您跨组件/页面共享状态。

  • 实际上,pinia就是Vuex的升级版,官网也说过,为了尊重原作者,所以取名pinia,而没有取名Vuex,所以大家可以直接将pinia比作为Vue3Vuex


2. 为什么要使用 pinia?


  • Vue2Vue3都支持,这让我们同时使用Vue2Vue3的小伙伴都能很快上手。

  • pinia中只有stategetteraction,抛弃了Vuex中的MutationVuexmutation一直都不太受小伙伴们的待见,pinia直接抛弃它了,这无疑减少了我们工作量。

  • piniaaction支持同步和异步,Vuex不支持

  • 良好的Typescript支持,毕竟我们Vue3都推荐使用TS来编写,这个时候使用pinia就非常合适了

  • 无需再创建各个模块嵌套了,Vuex中如果数据过多,我们通常分模块来进行管理,稍显麻烦,而pinia中每个store都是独立的,互相不影响。

  • 体积非常小,只有1KB左右。

  • pinia支持插件来扩展自身功能。

  • 支持服务端渲染


3. pinna 使用


pinna文档(opens new window)


  1. 准备工作


我们这里搭建一个最新的Vue3 + TS + Vite项目


npm create vite@latest my-vite-app --template vue-ts
复制代码


  1. pinia基础使用


yarn add pinia
复制代码


// main.tsimport { createApp } from "vue";import App from "./App.vue";import { createPinia } from "pinia";const pinia = createPinia();
const app = createApp(App);app.use(pinia);app.mount("#app");
复制代码


2.1 创建store


//sbinsrc/store/user.tsimport { defineStore } from 'pinia'
// 第一个参数是应用程序中 store 的唯一 idexport const useUsersStore = defineStore('users', { // 其它配置项})
复制代码


创建store很简单,调用 pinia中的defineStore函数即可,该函数接收两个参数:


  • name:一个字符串,必传项,该store的唯一id

  • options:一个对象,store的配置项,比如配置store内的数据,修改数据的方法等等。


我们可以定义任意数量的store,因为我们其实一个store就是一个函数,这也是pinia的好处之一,让我们的代码扁平化了,这和Vue3的实现思想是一样的


2.2 使用store


<!-- src/App.vue --><script setup lang="ts">import { useUsersStore } from "../src/store/user";const store = useUsersStore();console.log(store);</script>
复制代码


2.3 添加state


export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 20,      sex: "男",    };  },});
复制代码


2.4 读取state数据


<template>  <img alt="Vue logo" src="./assets/logo.png" />  <p>姓名:{{ name }}</p>  <p>年龄:{{ age }}</p>  <p>性别:{{ sex }}</p></template><script setup lang="ts">import { ref } from "vue";import { useUsersStore } from "../src/store/user";const store = useUsersStore();const name = ref<string>(store.name);const age = ref<number>(store.age);const sex = ref<string>(store.sex);</script>
复制代码


上段代码中我们直接通过store.age等方式获取到了store存储的值,但是大家有没有发现,这样比较繁琐,我们其实可以用解构的方式来获取值,使得代码更简洁一点


import { useUsersStore, storeToRefs } from "../src/store/user";const store = useUsersStore();const { name, age, sex } = storeToRefs(store); // storeToRefs获取的值是响应式的
复制代码


2.5 修改state数据


<template>  <img alt="Vue logo" src="./assets/logo.png" />  <p>姓名:{{ name }}</p>  <p>年龄:{{ age }}</p>  <p>性别:{{ sex }}</p>  <button @click="changeName">更改姓名</button></template><script setup lang="ts">import child from './child.vue';import { useUsersStore, storeToRefs } from "../src/store/user";const store = useUsersStore();const { name, age, sex } = storeToRefs(store);const changeName = () => {  store.name = "张三";  console.log(store);};</script>
复制代码


2.6 重置state


  • 有时候我们修改了state数据,想要将它还原,这个时候该怎么做呢?就比如用户填写了一部分表单,突然想重置为最初始的状态。

  • 此时,我们直接调用store$reset()方法即可,继续使用我们的例子,添加一个重置按钮


<button @click="reset">重置store</button>// 重置storeconst reset = () => {  store.$reset();};
复制代码


当我们点击重置按钮时,store中的数据会变为初始状态,页面也会更新


2.7 批量更改state数据


如果我们一次性需要修改很多条数据的话,有更加简便的方法,使用store$patch方法,修改app.vue代码,添加一个批量更改数据的方法


<button @click="patchStore">批量修改数据</button>// 批量修改数据const patchStore = () => {  store.$patch({    name: "张三",    age: 100,    sex: "女",  });};
复制代码


  • 有经验的小伙伴可能发现了,我们采用这种批量更改的方式似乎代价有一点大,假如我们state中有些字段无需更改,但是按照上段代码的写法,我们必须要将 state 中的所有字段例举出了。

  • 为了解决该问题,pinia提供的$patch方法还可以接收一个回调函数,它的用法有点像我们的数组循环回调函数了。


store.$patch((state) => {  state.items.push({ name: 'shoes', quantity: 1 })  state.hasChanged = true})
复制代码


2.8 直接替换整个state


pinia提供了方法让我们直接替换整个state对象,使用store$state方法


store.$state = { counter: 666, name: '张三' }
复制代码


上段代码会将我们提前声明的state替换为新的对象,可能这种场景用得比较少


  1. getters属性


  • gettersdefineStore参数配置项里面的另一个属性

  • 可以把getter想象成Vue中的计算属性,它的作用就是返回一个新的结果,既然它和Vue中的计算属性类似,那么它肯定也是会被缓存的,就和computed一样


3.1 添加getter


export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 10,      sex: "男",    };  },  getters: {    getAddAge: (state) => {      return state.age + 100;    },  },})
复制代码


上段代码中我们在配置项参数中添加了getter属性,该属性对象中定义了一个getAddAge方法,该方法会默认接收一个state参数,也就是state对象,然后该方法返回的是一个新的数据


3.2 使用getter


<template>  <p>新年龄:{{ store.getAddAge }}</p>  <button @click="patchStore">批量修改数据</button></template><script setup lang="ts">import { useUsersStore } from "../src/store/user";const store = useUsersStore();// 批量修改数据const patchStore = () => {  store.$patch({    name: "张三",    age: 100,    sex: "女",  });};</script>
复制代码


上段代码中我们直接在标签上使用了store.gettAddAge方法,这样可以保证响应式,其实我们state中的name等属性也可以以此种方式直接在标签上使用,也可以保持响应式


3.3 getter中调用其它getter


export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 20,      sex: "男",    };  },  getters: {    getAddAge: (state) => {      return state.age + 100;    },    getNameAndAge(): string {      return this.name + this.getAddAge; // 调用其它getter    },  },});
复制代码


3.3 getter传参


export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 20,      sex: "男",    };  },  getters: {    getAddAge: (state) => {      return (num: number) => state.age + num;    },    getNameAndAge(): string {      return this.name + this.getAddAge; // 调用其它getter    },  },});
复制代码


<p>新年龄:{{ store.getAddAge(1100) }}</p>
复制代码


  1. actions属性


  • 前面我们提到的stategetters 属性都主要是数据层面的,并没有具体的业务逻辑代码,它们两个就和我们组件代码中的data数据和computed计算属性一样。

  • 那么,如果我们有业务代码的话,最好就是卸载actions属性里面,该属性就和我们组件代码中的methods相似,用来放置一些处理业务逻辑的方法。

  • actions属性值同样是一个对象,该对象里面也是存储的各种各样的方法,包括同步方法和异步方法


4.1 添加actions


export const useUsersStore = defineStore("users", {  state: () => {    return {      name: "test",      age: 20,      sex: "男",    };  },  getters: {    getAddAge: (state) => {      return (num: number) => state.age + num;    },    getNameAndAge(): string {      return this.name + this.getAddAge; // 调用其它getter    },  },  actions: {    // 在实际场景中,该方法可以是任何逻辑,比如发送请求、存储token等等。大家把actions方法当作一个普通的方法即可,特殊之处在于该方法内部的this指向的是当前store    saveName(name: string) {      this.name = name;    },  },});
复制代码


4.2 使用actions


使用actions中的方法也非常简单,比如我们在App.vue中想要调用该方法


const saveName = () => {  store.saveName("poetries");};
复制代码


总结


pinia的知识点很少,如果你有 Vuex 基础,那么学起来更是易如反掌


pinia 无非就是以下 3 个大点:


  • state

  • getters

  • actions

computed 和 watch 区别

  1. 当页面中有某些数据依赖其他数据进行变动的时候,可以使用计算属性computed


Computed本质是一个具备缓存的watcher,依赖的属性发生变化就会更新视图。 适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理



<template>{{fullName}}</template>export default {    data(){        return {            firstName: 'zhang',            lastName: 'san',        }    },    computed:{        fullName: function(){            return this.firstName + ' ' + this.lastName        }    }}
复制代码


  1. watch用于观察和监听页面上的 vue 实例,如果要在数据变化的同时进行异步操作或者是比较大的开销,那么watch为最佳选择


Watch没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听,如果没有写到组件中,不要忘记使用unWatch手动注销



<template>{{fullName}}</template>export default {    data(){        return {            firstName: 'zhang',            lastName: 'san',            fullName: 'zhang san'        }    },    watch:{        firstName(val) {            this.fullName = val + ' ' + this.lastName        },        lastName(val) {            this.fullName = this.firstName + ' ' + val        }    }}
复制代码


computed:


  • computed是计算属性,也就是计算值,它更多用于计算值的场景

  • computed具有缓存性,computed的值在getter执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取computed的值时才会重新调用对应的getter来计算

  • computed适用于计算比较消耗性能的计算场景


watch:


  • 更多的是「观察」的作用,类似于某些数据的监听回调,用于观察props $emit或者本组件的值,当数据变化时来执行回调进行后续操作

  • 无缓存性,页面重新渲染时值不变化也会执行


小结:


  • computedwatch都是基于watcher来实现的

  • computed属性是具备缓存的,依赖的值不发生变化,对其取值时计算属性方法不会重新执行

  • watch是监控值的变化,当值发生变化时调用其对应的回调函数

  • 当我们要进行数值计算,而且依赖于其他数据,那么把这个数据设计为computed

  • 如果你需要在某个数据变化时做一些事情,使用watch来观察这个数据变化


回答范例


思路分析


  • 先看computed, watch两者定义,列举使用上的差异

  • 列举使用场景上的差异,如何选择

  • 使用细节、注意事项

  • vue3变化


computed特点:具有响应式的返回值


const count = ref(1)const plusOne = computed(() => count.value + 1)
复制代码


watch特点:侦测变化,执行回调


const state = reactive({ count: 0 })watch(  () => state.count,  (count, prevCount) => {    /* ... */  })
复制代码


回答范例


  1. 计算属性可以从组件数据派生出新数据,最常见的使用方式是设置一个函数,返回计算之后的结果,computedmethods的差异是它具备缓存性,如果依赖项不变时不会重新计算。侦听器可以侦测某个响应式数据的变化并执行副作用,常见用法是传递一个函数,执行副作用,watch 没有返回值,但可以执行异步操作等复杂逻辑

  2. 计算属性常用场景是简化行内模板中的复杂表达式,模板中出现太多逻辑会是模板变得臃肿不易维护。侦听器常用场景是状态变化之后做一些额外的 DOM 操作或者异步操作。选择采用何用方案时首先看是否需要派生出新值,基本能用计算属性实现的方式首选计算属性.

  3. 使用过程中有一些细节,比如计算属性也是可以传递对象,成为既可读又可写的计算属性。watch可以传递对象,设置deepimmediate等选项

  4. vue3watch选项发生了一些变化,例如不再能侦测一个点操作符之外的字符串形式的表达式; reactivity API中新出现了watchwatchEffect可以完全替代目前的watch选项,且功能更加强大


基本使用


// src/core/observer:45;
// 渲染watcher / computed watcher / watchconst vm = new Vue({ el: '#app', data: { firstname:'张', lastname:'三' }, computed:{ // watcher => firstname lastname // computed 只有取值时才执行
// Object.defineProperty .get fullName(){ // firstName lastName 会收集fullName计算属性 return this.firstname + this.lastname } }, watch:{ firstname(newVal,oldVal){ console.log(newVal) } }});
setTimeout(() => { debugger; vm.firstname = '赵'}, 1000);
复制代码


相关源码


// 初始化statefunction initState (vm: Component) {  vm._watchers = []  const opts = vm.$options  if (opts.props) initProps(vm, opts.props)  if (opts.methods) initMethods(vm, opts.methods)  if (opts.data) {    initData(vm)  } else {    observe(vm._data = {}, true /* asRootData */)  }
// 初始化计算属性 if (opts.computed) initComputed(vm, opts.computed)
// 初始化watch if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) }}
// 计算属性取值函数function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { if (watcher.dirty) { // 如果值依赖的值发生变化,就会进行重新求值 watcher.evaluate(); // this.firstname lastname } if (Dep.target) { // 让计算属性所依赖的属性 收集渲染watcher watcher.depend() } return watcher.value } }}
// watch的实现Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function { const vm: Component = this debugger; if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) } options = options || {} options.user = true const watcher = new Watcher(vm, expOrFn, cb, options) // 创建watcher,数据更新调用cb if (options.immediate) { try { cb.call(vm, watcher.value) } catch (error) { handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`) } } return function unwatchFn () { watcher.teardown() }}
复制代码


Vue 修饰符有哪些

事件修饰符


  • .stop 阻止事件继续传播

  • .prevent 阻止标签默认行为

  • .capture 使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理

  • .self 只当在 event.target 是当前元素自身时触发处理函数

  • .once 事件将只会触发一次

  • .passive 告诉浏览器你不想阻止事件的默认行为


v-model 的修饰符


  • .lazy 通过这个修饰符,转变为在 change 事件再同步

  • .number 自动将用户的输入值转化为数值类型

  • .trim 自动过滤用户输入的首尾空格


键盘事件的修饰符


  • .enter

  • .tab

  • .delete (捕获“删除”和“退格”键)

  • .esc

  • .space

  • .up

  • .down

  • .left

  • .right


系统修饰键


  • .ctrl

  • .alt

  • .shift

  • .meta


鼠标按钮修饰符


  • .left

  • .right

  • .middle

Vue 模板编译原理

Vue 的编译过程就是将 template 转化为 render 函数的过程 分为以下三步


第一步是将 模板字符串 转换成 element ASTs(解析器)第二步是对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)第三步是 使用 element ASTs 生成 render 函数代码字符串(代码生成器)
复制代码

v-model 可以被用在自定义组件上吗?如果可以,如何使用?

可以。v-model 实际上是一个语法糖,如:


<input v-model="searchText">
复制代码


实际上相当于:


<input  v-bind:value="searchText"  v-on:input="searchText = $event.target.value">
复制代码


用在自定义组件上也是同理:


<custom-input v-model="searchText">
复制代码


相当于:


<custom-input  v-bind:value="searchText"  v-on:input="searchText = $event"></custom-input>
复制代码


显然,custom-input 与父组件的交互如下:


  1. 父组件将searchText变量传入 custom-input 组件,使用的 prop 名为value

  2. custom-input 组件向父组件传出名为input的事件,父组件将接收到的值赋值给searchText


所以,custom-input 组件的实现应该类似于这样:


Vue.component('custom-input', {  props: ['value'],  template: `    <input      v-bind:value="value"      v-on:input="$emit('input', $event.target.value)"    >  `})
复制代码

谈谈对 keep-alive 的了解

keep-alive 可以实现组件的缓存,当组件切换时不会对当前组件进行卸载。常用的2个属性 include/exclude ,2个生命周期 activated deactivated


参考 前端进阶面试题详细解答

extend 有什么作用

这个 API 很少用到,作用是扩展组件生成一个构造器,通常会与 $mount 一起使用。


// 创建组件构造器let Component = Vue.extend({ template: "<div>test</div>" });// 挂载到 #app 上new Component().$mount('#app')// 除了上面的方式,还可以用来扩展已有的组件let SuperComponent = Vue.extend(Component);new SuperComponent({  created() {    console.log(1);  },});new SuperComponent().$mount("#app");
复制代码

action 与 mutation 的区别

  • mutation 是同步更新, $watch 严格模式下会报错

  • action 是异步操作,可以获取数据后调用 mutation 提交最终数据

如何保存页面的当前的状态

既然是要保持页面的状态(其实也就是组件的状态),那么会出现以下两种情况:


  • 前组件会被卸载

  • 前组件不会被卸载


那么可以按照这两种情况分别得到以下方法:


组件会被卸载:


(1)将状态存储在 LocalStorage / SessionStorage


只需要在组件即将被销毁的生命周期 componentWillUnmount (react)中在 LocalStorage / SessionStorage 中把当前组件的 state 通过 JSON.stringify() 储存下来就可以了。在这里面需要注意的是组件更新状态的时机。


比如从 B 组件跳转到 A 组件的时候,A 组件需要更新自身的状态。但是如果从别的组件跳转到 B 组件的时候,实际上是希望 B 组件重新渲染的,也就是不要从 Storage 中读取信息。所以需要在 Storage 中的状态加入一个 flag 属性,用来控制 A 组件是否读取 Storage 中的状态。


优点:


  • 兼容性好,不需要额外库或工具。

  • 简单快捷,基本可以满足大部分需求。


缺点:


  • 状态通过 JSON 方法储存(相当于深拷贝),如果状态中有特殊情况(比如 Date 对象、Regexp 对象等)的时候会得到字符串而不是原来的值。(具体参考用 JSON 深拷贝的缺点)

  • 如果 B 组件后退或者下一页跳转并不是前组件,那么 flag 判断会失效,导致从其他页面进入 A 组件页面时 A 组件会重新读取 Storage,会造成很奇怪的现象


(2)路由传值


通过 react-router 的 Link 组件的 prop —— to 可以实现路由间传递参数的效果。


在这里需要用到 state 参数,在 B 组件中通过 history.location.state 就可以拿到 state 值,保存它。返回 A 组件时再次携带 state 达到路由状态保持的效果。


优点:


  • 简单快捷,不会污染 LocalStorage / SessionStorage。

  • 可以传递 Date、RegExp 等特殊对象(不用担心 JSON.stringify / parse 的不足)


缺点:


  • 如果 A 组件可以跳转至多个组件,那么在每一个跳转组件内都要写相同的逻辑。


组件不会被卸载:


(1)单页面渲染


要切换的组件作为子组件全屏渲染,父组件中正常储存页面状态。


优点:


  • 代码量少

  • 不需要考虑状态传递过程中的错误


缺点:


  • 增加 A 组件维护成本

  • 需要传入额外的 prop 到 B 组件

  • 无法利用路由定位页面


除此之外,在 Vue 中,还可以是用 keep-alive 来缓存页面,当组件在 keep-alive 内被切换时组件的 activated、deactivated 这两个生命周期钩子函数会被执行被包裹在 keep-alive 中的组件的状态将会被保留:


<keep-alive>    <router-view v-if="$route.meta.keepAlive"></router-view></kepp-alive>
复制代码


router.js


{  path: '/',  name: 'xxx',  component: ()=>import('../src/views/xxx.vue'),  meta:{    keepAlive: true // 需要被缓存  }},
复制代码

什么是 mixin ?

  • Mixin 使我们能够为 Vue 组件编写可插拔和可重用的功能。

  • 如果希望在多个组件之间重用一组组件选项,例如生命周期 hook、 方法等,则可以将其编写为 mixin,并在组件中简单的引用它。

  • 然后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自已的 hook。

vue2.x 详细

1. 分析


首先找到vue的构造函数


源码位置:src\core\instance\index.js


function Vue (options) {  if (process.env.NODE_ENV !== 'production' &&    !(this instanceof Vue)  ) {    warn('Vue is a constructor and should be called with the `new` keyword')  }  this._init(options)}
复制代码


options是用户传递过来的配置项,如data、methods等常用的方法


vue构建函数调用_init方法,但我们发现本文件中并没有此方法,但仔细可以看到文件下方定定义了很多初始化方法


initMixin(Vue);     // 定义 _initstateMixin(Vue);    // 定义 $set $get $delete $watch 等eventsMixin(Vue);   // 定义事件  $on  $once $off $emitlifecycleMixin(Vue);// 定义 _update  $forceUpdate  $destroyrenderMixin(Vue);   // 定义 _render 返回虚拟dom
复制代码


首先可以看initMixin方法,发现该方法在Vue原型上定义了_init方法


源码位置:src\core\instance\init.js


Vue.prototype._init = function (options?: Object) {    const vm: Component = this    // a uid    vm._uid = uid++    let startTag, endTag    /* istanbul ignore if */    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {      startTag = `vue-perf-start:${vm._uid}`      endTag = `vue-perf-end:${vm._uid}`      mark(startTag)    }
// a flag to avoid this being observed vm._isVue = true // merge options // 合并属性,判断初始化的是否是组件,这里合并主要是 mixins 或 extends 的方法 if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { // 合并vue属性 vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { // 初始化proxy拦截器 initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm // 初始化组件生命周期标志位 initLifecycle(vm) // 初始化组件事件侦听 initEvents(vm) // 初始化渲染方法 initRender(vm) callHook(vm, 'beforeCreate') // 初始化依赖注入内容,在初始化data、props之前 initInjections(vm) // resolve injections before data/props // 初始化props/data/method/watch/methods initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created')
/* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { vm._name = formatComponentName(vm, false) mark(endTag) measure(`vue ${vm._name} init`, startTag, endTag) } // 挂载元素 if (vm.$options.el) { vm.$mount(vm.$options.el) } }
复制代码


仔细阅读上面的代码,我们得到以下结论:


  • 在调用beforeCreate之前,数据初始化并未完成,像dataprops这些属性无法访问到

  • 到了created的时候,数据已经初始化完成,能够访问dataprops这些属性,但这时候并未完成dom的挂载,因此无法访问到dom元素

  • 挂载方法是调用vm.$mount方法


initState方法是完成props/data/method/watch/methods的初始化


源码位置:src\core\instance\state.js


export function initState (vm: Component) {  // 初始化组件的watcher列表  vm._watchers = []  const opts = vm.$options  // 初始化props  if (opts.props) initProps(vm, opts.props)  // 初始化methods方法  if (opts.methods) initMethods(vm, opts.methods)  if (opts.data) {    // 初始化data      initData(vm)  } else {    observe(vm._data = {}, true /* asRootData */)  }  if (opts.computed) initComputed(vm, opts.computed)  if (opts.watch && opts.watch !== nativeWatch) {    initWatch(vm, opts.watch)  }}
复制代码


我们和这里主要看初始化data的方法为initData,它与initState在同一文件上


function initData (vm: Component) {  let data = vm.$options.data  // 获取到组件上的data  data = vm._data = typeof data === 'function'    ? getData(data, vm)    : data || {}  if (!isPlainObject(data)) {    data = {}    process.env.NODE_ENV !== 'production' && warn(      'data functions should return an object:\n' +      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',      vm    )  }  // proxy data on instance  const keys = Object.keys(data)  const props = vm.$options.props  const methods = vm.$options.methods  let i = keys.length  while (i--) {    const key = keys[i]    if (process.env.NODE_ENV !== 'production') {      // 属性名不能与方法名重复      if (methods && hasOwn(methods, key)) {        warn(          `Method "${key}" has already been defined as a data property.`,          vm        )      }    }    // 属性名不能与state名称重复    if (props && hasOwn(props, key)) {      process.env.NODE_ENV !== 'production' && warn(        `The data property "${key}" is already declared as a prop. ` +        `Use prop default value instead.`,        vm      )    } else if (!isReserved(key)) { // 验证key值的合法性      // 将_data中的数据挂载到组件vm上,这样就可以通过this.xxx访问到组件上的数据      proxy(vm, `_data`, key)    }  }  // observe data  // 响应式监听data是数据的变化  observe(data, true /* asRootData */)}
复制代码


仔细阅读上面的代码,我们可以得到以下结论:


  • 初始化顺序:propsmethodsdata

  • data定义的时候可选择函数形式或者对象形式(组件只能为函数形式)


关于数据响应式在这就不展开详细说明


上文提到挂载方法是调用vm.$mount方法


源码位置:


Vue.prototype.$mount = function (  el?: string | Element,  hydrating?: boolean): Component {  // 获取或查询元素  el = el && query(el)
/* istanbul ignore if */ // vue 不允许直接挂载到body或页面文档上 if (el === document.body || el === document.documentElement) { process.env.NODE_ENV !== 'production' && warn( `Do not mount Vue to <html> or <body> - mount to normal elements instead.` ) return this }
const options = this.$options // resolve template/el and convert to render function if (!options.render) { let template = options.template // 存在template模板,解析vue模板文件 if (template) { if (typeof template === 'string') { if (template.charAt(0) === '#') { template = idToTemplate(template) /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !template) { warn( `Template element not found or is empty: ${options.template}`, this ) } } } else if (template.nodeType) { template = template.innerHTML } else { if (process.env.NODE_ENV !== 'production') { warn('invalid template option:' + template, this) } return this } } else if (el) { // 通过选择器获取元素内容 template = getOuterHTML(el) } if (template) { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile') } /** * 1.将temmplate解析ast tree * 2.将ast tree转换成render语法字符串 * 3.生成render方法 */ const { render, staticRenderFns } = compileToFunctions(template, { outputSourceRange: process.env.NODE_ENV !== 'production', shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this) options.render = render options.staticRenderFns = staticRenderFns
/* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile end') measure(`vue ${this._name} compile`, 'compile', 'compile end') } } } return mount.call(this, el, hydrating)}
复制代码


阅读上面代码,我们能得到以下结论:


  • 不要将根元素放到body或者html

  • 可以在对象中定义template/render或者直接使用templateel表示元素选择器

  • 最终都会解析成render函数,调用compileToFunctions,会将template解析成render函数


template的解析步骤大致分为以下几步:


  • html文档片段解析成ast描述符

  • ast描述符解析成字符串

  • 生成render函数


生成render函数,挂载到vm上后,会再次调用mount方法


源码位置:src\platforms\web\runtime\index.js


// public mount methodVue.prototype.$mount = function (  el?: string | Element,  hydrating?: boolean): Component {  el = el && inBrowser ? query(el) : undefined  // 渲染组件  return mountComponent(this, el, hydrating)}
复制代码


调用mountComponent渲染组件


export function mountComponent (  vm: Component,  el: ?Element,  hydrating?: boolean): Component {  vm.$el = el  // 如果没有获取解析的render函数,则会抛出警告  // render是解析模板文件生成的  if (!vm.$options.render) {    vm.$options.render = createEmptyVNode    if (process.env.NODE_ENV !== 'production') {      /* istanbul ignore if */      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||        vm.$options.el || el) {        warn(          'You are using the runtime-only build of Vue where the template ' +          'compiler is not available. Either pre-compile the templates into ' +          'render functions, or use the compiler-included build.',          vm        )      } else {        // 没有获取到vue的模板文件        warn(          'Failed to mount component: template or render function not defined.',          vm        )      }    }  }  // 执行beforeMount钩子  callHook(vm, 'beforeMount')
let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { updateComponent = () => { const name = vm._name const id = vm._uid const startTag = `vue-perf-start:${id}` const endTag = `vue-perf-end:${id}`
mark(startTag) const vnode = vm._render() mark(endTag) measure(`vue ${name} render`, startTag, endTag)
mark(startTag) vm._update(vnode, hydrating) mark(endTag) measure(`vue ${name} patch`, startTag, endTag) } } else { // 定义更新函数 updateComponent = () => { // 实际调⽤是在lifeCycleMixin中定义的_update和renderMixin中定义的_render vm._update(vm._render(), hydrating) } } // we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate (e.g. inside child // component's mounted hook), which relies on vm._watcher being already defined // 监听当前组件状态,当有数据变化时,更新组件 new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { // 数据更新引发的组件更新 callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false
// manually mounted instance, call mounted on self // mounted is called for render-created child components in its inserted hook if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm}
复制代码


阅读上面代码,我们得到以下结论:


  • 会触发boforeCreate钩子

  • 定义updateComponent渲染页面视图的方法

  • 监听组件数据,一旦发生变化,触发beforeUpdate生命钩子


updateComponent方法主要执行在vue初始化时声明的renderupdate方法


render的作用主要是生成vnode


源码位置:src\core\instance\render.js


// 定义vue 原型上的render方法Vue.prototype._render = function (): VNode {    const vm: Component = this    // render函数来自于组件的option    const { render, _parentVnode } = vm.$options
if (_parentVnode) { vm.$scopedSlots = normalizeScopedSlots( _parentVnode.data.scopedSlots, vm.$slots, vm.$scopedSlots ) }
// set parent vnode. this allows render functions to have access // to the data on the placeholder node. vm.$vnode = _parentVnode // render self let vnode try { // There's no need to maintain a stack because all render fns are called // separately from one another. Nested component's render fns are called // when parent component is patched. currentRenderingInstance = vm // 调用render方法,自己的独特的render方法, 传入createElement参数,生成vNode vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { handleError(e, vm, `render`) // return error render result, // or previous vnode to prevent render error causing blank component /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) { try { vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e) } catch (e) { handleError(e, vm, `renderError`) vnode = vm._vnode } } else { vnode = vm._vnode } } finally { currentRenderingInstance = null } // if the returned array contains only a single node, allow it if (Array.isArray(vnode) && vnode.length === 1) { vnode = vnode[0] } // return empty vnode in case the render function errored out if (!(vnode instanceof VNode)) { if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) { warn( 'Multiple root nodes returned from render function. Render function ' + 'should return a single root node.', vm ) } vnode = createEmptyVNode() } // set parent vnode.parent = _parentVnode return vnode}
复制代码


_update主要功能是调用patch,将vnode转换为真实DOM,并且更新到页面中


源码位置:src\core\instance\lifecycle.js


Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {    const vm: Component = this    const prevEl = vm.$el    const prevVnode = vm._vnode    // 设置当前激活的作用域    const restoreActiveInstance = setActiveInstance(vm)    vm._vnode = vnode    // Vue.prototype.__patch__ is injected in entry points    // based on the rendering backend used.    if (!prevVnode) {      // initial render      // 执行具体的挂载逻辑      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)    } else {      // updates      vm.$el = vm.__patch__(prevVnode, vnode)    }    restoreActiveInstance()    // update __vue__ reference    if (prevEl) {      prevEl.__vue__ = null    }    if (vm.$el) {      vm.$el.__vue__ = vm    }    // if parent is an HOC, update its $el as well    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {      vm.$parent.$el = vm.$el    }    // updated hook is called by the scheduler to ensure that children are    // updated in a parent's updated hook.  }
复制代码


2. 结论


  • new Vue的时候调用会调用_init方法

  • 定义 $set$get$delete$watch 等方法

  • 定义 $on$off$emit$off等事件

  • 定义 _update$forceUpdate$destroy生命周期

  • 调用$mount进行页面的挂载

  • 挂载的时候主要是通过mountComponent方法

  • 定义updateComponent更新函数

  • 执行render生成虚拟DOM

  • _update将虚拟DOM生成真实DOM结构,并且渲染到页面中

Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗?

不会立即同步执行重新渲染。Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。


如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环 tick 中,Vue 刷新队列并执行实际(已去重的)工作。

Vue3.0 为什么要用 proxy?

在 Vue2 中, 0bject.defineProperty 会改变原始数据,而 Proxy 是创建对象的虚拟表示,并提供 set 、get 和 deleteProperty 等处理器,这些处理器可在访问或修改原始对象上的属性时进行拦截,有以下特点∶


  • 不需用使用 Vue.$setVue.$delete 触发响应式。

  • 全方位的数组变化检测,消除了 Vue2 无效的边界情况。

  • 支持 Map,Set,WeakMap 和 WeakSet。


Proxy 实现的响应式原理与 Vue2 的实现原理相同,实现方式大同小异∶


  • get 收集依赖

  • Set、delete 等触发依赖

  • 对于集合类型,就是对集合对象的方法做一层包装:原方法执行后执行依赖相关的收集或触发逻辑。

如果让你从零开始写一个 vue 路由,说说你的思路

思路分析:


首先思考vue路由要解决的问题:用户点击跳转链接内容切换,页面不刷新。


  • 借助hash或者 history api实现url跳转页面不刷新

  • 同时监听hashchange事件或者popstate事件处理跳转

  • 根据hash值或者state值从routes表中匹配对应component并渲染


回答范例:


一个SPA应用的路由需要解决的问题是 页面跳转内容改变同时不刷新 ,同时路由还需要以插件形式存在,所以:


  1. 首先我会定义一个createRouter函数,返回路由器实例,实例内部做几件事


  • 保存用户传入的配置项

  • 监听hash或者popstate事件

  • 回调里根据path匹配对应路由


  1. router定义成一个Vue插件,即实现install方法,内部做两件事


  • 实现两个全局组件:router-linkrouter-view,分别实现页面跳转和内容显示

  • 定义两个全局变量:$route$router,组件内可以访问当前路由和路由器实例

Vue 的性能优化有哪些

(1)编码阶段


  • 尽量减少 data 中的数据,data 中的数据都会增加 getter 和 setter,会收集对应的 watcher

  • v-if 和 v-for 不能连用

  • 如果需要使用 v-for 给每项元素绑定事件时使用事件代理

  • SPA 页面采用 keep-alive 缓存组件

  • 在更多的情况下,使用 v-if 替代 v-show

  • key 保证唯一

  • 使用路由懒加载、异步组件

  • 防抖、节流

  • 第三方模块按需导入

  • 长列表滚动到可视区域动态加载

  • 图片懒加载


(2)SEO 优化


  • 预渲染

  • 服务端渲染 SSR


(3)打包优化


  • 压缩代码

  • Tree Shaking/Scope Hoisting

  • 使用 cdn 加载第三方模块

  • 多线程打包 happypack

  • splitChunks 抽离公共文件

  • sourceMap 优化


(4)用户体验


  • 骨架屏

  • PWA

  • 还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启 gzip 压缩等。

v-if 和 v-show 的区别

  • 手段:v-if 是动态的向 DOM 树内添加或者删除 DOM 元素;v-show 是通过设置 DOM 元素的 display 样式属性控制显隐;

  • 编译过程:v-if 切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show 只是简单的基于 css 切换;

  • 编译条件:v-if 是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译; v-show 是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且 DOM 元素保留;

  • 性能消耗:v-if 有更高的切换消耗;v-show 有更高的初始渲染消耗;

  • 使用场景:v-if 适合运营条件不大可能改变;v-show 适合频繁切换。

v-if、v-show、v-html 的原理

  • v-if 会调用 addIfCondition 方法,生成 vnode 的时候会忽略对应节点,render 的时候就不会渲染;

  • v-show 会生成 vnode,render 的时候也会渲染成真实节点,只是在 render 过程中会在节点的属性中修改 show 属性值,也就是常说的 display;

  • v-html 会先移除节点下的所有节点,调用 html 方法,通过 addProp 添加 innerHTML 属性,归根结底还是设置 innerHTML 为 v-html 的值。

谈谈你对 SPA 单页面的理解

SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTMLJavaScriptCSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载


优点:


  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;

  • 基于上面一点,SPA 相对对服务器压力小;

  • 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理


缺点:


  • 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScriptCSS 统一加载,部分页面按需加载;

  • 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;

  • SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势


单页应用与多页应用的区别



实现一个 SPA


  • 监听地址栏中hash变化驱动界面变化

  • pushsate记录浏览器的历史,驱动界面发送变化



  1. hash 模式 :核心通过监听url中的hash来进行路由跳转


// 定义 Router  class Router {      constructor () {          this.routes = {}; // 存放路由path及callback          this.currentUrl = '';  
// 监听路由change调用相对应的路由回调 window.addEventListener('load', this.refresh, false); window.addEventListener('hashchange', this.refresh, false); }
route(path, callback){ this.routes[path] = callback; }
push(path) { this.routes[path] && this.routes[path]() } }
// 使用 router window.miniRouter = new Router(); miniRouter.route('/', () => console.log('page1')) miniRouter.route('/page2', () => console.log('page2'))
miniRouter.push('/') // page1 miniRouter.push('/page2') // page2
复制代码


  1. history 模式history 模式核心借用 HTML5 history apiapi 提供了丰富的 router 相关属性先了解一个几个相关的 api


  • history.pushState 浏览器历史纪录添加记录

  • history.replaceState修改浏览器历史纪录中当前纪录

  • history.popStatehistory 发生变化时触发


// 定义 Router  class Router {      constructor () {          this.routes = {};          this.listerPopState()      }  
init(path) { history.replaceState({path: path}, null, path); this.routes[path] && this.routes[path](); }
route(path, callback){ this.routes[path] = callback; }
push(path) { history.pushState({path: path}, null, path); this.routes[path] && this.routes[path](); }
listerPopState () { window.addEventListener('popstate' , e => { const path = e.state && e.state.path; this.routers[path] && this.routers[path]() }) } }
// 使用 Router
window.miniRouter = new Router(); miniRouter.route('/', ()=> console.log('page1')) miniRouter.route('/page2', ()=> console.log('page2'))
// 跳转 miniRouter.push('/page2') // page2
复制代码


题外话:如何给 SPA 做 SEO


  1. SSR 服务端渲染


将组件或页面通过服务器生成html,再返回给浏览器,如nuxt.js


  1. 静态化


目前主流的静态化主要有两种:


  • 一种是通过程序将动态页面抓取并保存为静态页面,这样的页面的实际存在于服务器的硬盘中

  • 另外一种是通过 WEB 服务器的 URL Rewrite的方式,它的原理是通过 web 服务器内部模块按一定规则将外部的 URL 请求转化为内部的文件地址,一句话来说就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的。这两种方法都达到了实现 URL 静态化的效果


  1. 使用Phantomjs针对爬虫处理


原理是通过Nginx配置,判断访问来源是否为爬虫,如果是则搜索引擎的爬虫请求会转发到一个node server,再通过PhantomJS来解析完整的HTML,返回给爬虫。下面是大致流程图


子组件可以直接改变父组件的数据么,说明原因

这是一个实践知识点,组件化开发过程中有个单项数据流原则,不在子组件中修改父组件是个常识问题


思路


  • 讲讲单项数据流原则,表明为何不能这么做

  • 举几个常见场景的例子说说解决方案

  • 结合实践讲讲如果需要修改父组件状态应该如何做


回答范例


  1. 所有的 prop 都使得其父子之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。另外,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器控制台中发出警告


const props = defineProps(['foo'])// ❌ 下面行为会被警告, props是只读的!props.foo = 'bar'
复制代码


  1. 实际开发过程中有两个场景会想要修改一个属性:


这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data,并将这个 prop 用作其初始值:


const props = defineProps(['initialCounter'])const counter = ref(props.initialCounter)
复制代码


这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop 的值来定义一个计算属性:


const props = defineProps(['size'])// prop变化,计算属性自动更新const normalizedSize = computed(() => props.size.trim().toLowerCase())
复制代码


  1. 实践中如果确实想要改变父组件属性应该emit一个事件让父组件去做这个变更。注意虽然我们不能直接修改一个传入的对象或者数组类型的prop,但是我们还是能够直接改内嵌的对象或属性

Vue 路由 hash 模式和 history 模式

1. hash模式


早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL# 后面的内容。比如下面这个网站,它的 location.hash 的值为 '#search'


https://interview2.poetries.top#search
复制代码


hash 路由模式的实现主要是基于下面几个特性


  • URLhash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送;

  • hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制 hash 的切换;

  • 可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URLhash 值会发生改变;或者使用 JavaScript 来对 loaction.hash 进行赋值,改变 URLhash 值;

  • 我们可以使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)


window.addEventListener("hashchange", funcRef, false);
复制代码


每一次改变 hashwindow.location.hash),都会在浏览器的访问历史中增加一个记录利用 hash 的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了


特点 :兼容性好但是不美观


2. history模式


history采用HTML5的新特性;且提供了两个新方法: pushState()replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更


window.history.pushState(null, null, path);window.history.replaceState(null, null, path);
复制代码


这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。


history 路由模式的实现主要基于存在下面几个特性:


  • pushStaterepalceState 两个 API 来操作实现 URL 的变化 ;

  • 我们可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染);

  • history.pushState()history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。


特点 :虽然美观,但是刷新会出现 404 需要后端进行配置


用户头像

bb_xiaxia1998

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
社招前端经典vue面试题汇总_Vue_bb_xiaxia1998_InfoQ写作社区