Vue 基础

128 阅读9分钟

主讲:云隐

理论

面试题:简单聊聊对于 MVVM 的了解

发展史已经旁支:

  • 语义化模板;
  • MVC:Model - View - Controller
  • MVVM:Model - View - ViewModel
    • 数据会绑定在 viewModel 层,并且自动将数据渲染到页面上;
    • 视图发生变化时,会通知 viewModel 层更新数据;

写法

vue 是如何利用 MVVM 思想进行项目开发

数据双向绑定:

  • 利用花括号,构筑饿了数据与视图的双向绑定 —— 若干正则;
  • 通过视图绑定事件,来处理数据;

生命周期

面试题:Vue 的生命周期

  • beforeCreate => created 创建阶段
    • beforeCreatenew Vue() - 实例挂载功能,此时还是一个空实例;
    • createddata、props、method、computed - 数据操作,不涉及到 vdomdom
  • beforeMount => mounted 挂载阶段
    • beforeMountvdom - 数据操作,但是不涉及 dom
    • mounteddom - 任何操作;
  • beforeUpdate => updated 更新阶段
    • beforeUpdatevdom 更新了的,dom 未更新还是旧的 - 可以更新数据;
    • updateddom 已经更新了 - 谨慎操作数据(操作的数据和更新的数据是同一个,可能会发生死循环)
  • beforeDestory => destoryed 销毁阶段
    • beforeDestory:实例 vm 尚未被销毁 - 清空 eventBusreset storeclear 计时器;
    • destoryed:实例已经被销毁 - 收尾;

定向监听

面试题:computed 和 watch

相同点:

  1. 都是基于 vue 的依赖收集机制;
  2. 都是被依赖的变化触发,进行改变,进而进行处理计算;

不同点:

  1. 入和出
    • computed:多入单出 - 多个值变化,组成一个值的变化;
    • watch:单入多出 - 单个值的变化,进而影响一系列的状态变更;
  2. 性能
    • computed:会自动 diff 依赖,若依赖没有变化,会改从缓存中读取当前计算值;
    • watch:无论监听值变化与否,都会执行回调;
  3. 写法上
    • computed:必须有 return 返回值;
    • watch:不一定;
  4. 时机上
    • computed:从首次生成赋值,就开始计算运行了;
    • watch:首次不会运行,除非 Immediate: true

条件

v-if & v-show & v-else & e-else-if

  • v-if:无 dom,不会渲染实际节点及其子节点;
  • v-show:存在实际节点及其子节点,但不展示,不占据位置;

循环

面试题:v-for 和 v-if 优先级

优先级:v-for 大于 v-if - 先循环再判断;

面试题:key 的作用

  1. 模板编译原理:template => dom
    • template => 正则匹配语法 - 生成 AST(静态 + 动态)=> 转换 AST 为可执行方法 => render() => dom
  2. dom diff
    • 层级:只考虑单层复用,多层级遍历实现;
    • 顺序:双向指针,首尾向中间移动;
    • 替换:移动、新增、删除;优先复用 - key => 快速识别顺序;
  3. key 的作用 - 尽可能复用节点
    • 常见问题:indexkey、随机数做 key - 不可以;

指令

默认指令

  • v-once:只渲染一次;

  • v-text:渲染字符串;

  • v-html:渲染 html

  • v-bind: 绑定赋值;

  • v-on@ 监听;

  • v-model:双向绑定 - 语法糖;

    • 默认::value + @input
    • 重配置:
    model: {
      prop: 'selected',
      event: 'change'
    }
    

自定义指令

directives: {
  zhuawa: {
    update: function() {
      // ...
    }
  }
}
<!-- 使用 -->
<div v-zhuawa></div>

事件

事件修饰符

  • .stop:阻止单击事件继续传播;
  • .prevent:提交事件不再重载页面;
  • .capture:添加事件监听器时使用事件捕获模式;
  • .self:只当在 event.target 是当前元素自身时触发处理函数;
  • .once:点击事件将只会触发一次;
  • .passive

按钮修饰符

  • .enter
  • .delete
  • ...

事件设计

面试题:为何 vue 把事件写在模板上,而不是 js 中
  • 模板定位事件触发源 + 触发源寻找触发事件逻辑 —— 更方便定位问题;
  • js 与事件本身解耦 —— 更便于测试隔离;
  • viewModel 销毁,自动解绑事件 —— 更便于回收;

组件化

一般组件 + 动态组件

<template>
  <div class="hello">
    <h3>一般组件</h3>
    <zhuawa></zhuawa>

    <h3>动态组件</h3>
    <component :is="currentComponent"></component>
  </div>
</template>

<script>
import zhuawa from '@/components/zhuawa';
import zhuawa2 from '@/components/zhuawa2';
import zhuawa3 from '@/components/zhuawa3';

export default {
  name: 'HelloWorld',
  data() {
    return {
      number: 100,
      number2: 0,
    };
  },
  watch: {
    number() {
      this.number2++;
    },
  },
  computed: {
    currentComponent() {
      return this.number2 > 3 ? 'zhuawa3' : 'zhuawa2';
    },
  },
  components: {
    zhuawa,
    zhuawa2,
    zhuawa3,
  },
};
</script>

扩展

Vue 声明周期

一、什么是 vue 生命周期?

Vue 实例有一个完整的生命周期,也就是从 开始创建、初始化数据、编译模板、挂载 DOM、渲染/更新、渲染/卸载 等一系列过程,称之为生命周期。

lifecycle.png

二、各个生命周期的作用

  • beforeCreate(创建前):组件实例被创建之初,组件的属性生效之前;
  • created(创建后):组件实例已经完全挂载,属性也绑定,但是真实 DOM 还没有生成,$el 还不可以使用;
  • beforeMount(挂载前):在挂载开始之前被调用,相关的 render 函数首次被调用;
  • mounted(挂载后):在 el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子;
  • beforeUpdate(更新前):组件数据更新之前调用,真实 DOM 还没有被渲染;
  • updated(更新后):组件数据更新之后;
  • activated(激活前):keep-alive 专属,组件被激活时调用;
  • deactivated(激活后):keep-alive 专属,组件被销毁时调用;
  • beforeDestory(销毁前):组件销毁前调用;
  • destoryed(销毁后):组件销毁后调用;

三、Vue 子组件和父组件的执行顺序

  • 加载渲染过程:
    • beforeCreate
    • created
    • beforeMount
    • beforeCreate
    • created
    • beforeMount
    • mounted
    • mounted
  • 更新过程
    • beforeUpdate
    • beforeUpdate
    • updated
    • updated
  • 销毁过程
    • beforeDestory
    • beforeDestory
    • destoryed
    • destoryed

四、简述每个周期具体适合哪些场景

  • beforeCreate:在 new 一个 vue 实例后,只有一些默认的生命周期钩子和默认事件,其他的东西都还没有创建,在 beforeCreate 生命周期执行的时候,datamethods 中的数据都还没有初始化,不能在这个阶段使用 data 中的数据都还没有初始化,不能在这个阶段使用 datamethods 中的方法;
  • createddatamethods 都已经初始化好了,如果要调用 methods 中的方法,或者操作 data 中的数据,最早可以在这个阶段中操作;
  • beforeMount:执行到这个钩子的时候,在内存中已经编译好了模板,但是还没有挂在到页面中,此时,页面还是旧的;
  • mounted:执行到这个钩子的时候,就表示 Vue 实例已经初始化完成了,此时组件脱离了创建阶段,进入到了运行阶段,如果我们想要通过插件操作页面上 DOM 节点,最早可以在这个阶段中进行;
  • beforeUpdate:当执行这个钩子时,页面中的显示数据还是旧的,data 中的数据是更新后的,页面还没有和最新的数据保持同步;
  • updated:页面显示的数据和 data 中的数据已经保持同步了,都是最新的;
  • beforeDestoryVue 实例从运行阶段进入到了销毁阶段,这个时候 datamethods、指令、过滤器等等都是处于可用状态,还没有真正的被销毁;
  • destoryed:这个时候 datamethods、指令、过滤器等等都处于不可用状态,组件已经被销毁了;

五、created 和 mounted 的区别

  • created:在模板渲染成 html 前调用,即通常初始化某些属性值,然后再渲染成视图;
  • mounted:在模板渲染成 html 后调用,通常是初始化页面完成后,再对 htmlDOM 节点进行一些需要的操作;

六、Vue 请求异步数据在哪个周期函数

createdbeforeMountmounted 中进行调用。因为在这三个钩子函数中,data 已经创建了,可以将服务器端返回的数据进行赋值。

推荐在 created 钩子函数中获取,优点:

  • 能更快获取到服务端数据,减少页面加载时间,用户体验更好;
  • SSR 不支持 beforeMountmounted 钩子函数,放在 created 中有助于一致性;

七、keep-alive 中的生命周期有哪些

keep-aliveVue 提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染 DOM

如果问一个组件包裹了 keep-alive,那么它会多出两个生命周期:deactivatedactivated。同时,beforeDestorydestoryed 就不会再被触发了,因为组件不会被真正的销毁。

当组件被换掉时,会被缓存到内存中,触发 deactivated 生命周期;当组件被切回来时,再去缓存里找这个组件,触发 activated 钩子函数。

八、请详细说下对 Vue 生命周期的理解

总共分为 8 个阶段:创建前/后、载入前/后、更新前/后、销毁前/后;

  • 创建前/后:在 beforeCreated 阶段,Vue 实例的挂载元素 $el 和数据对象 data 都为 undefined,还未初始化。在 created 阶段,Vue 实例的数据对象 data 都有了,$el 还没有;
  • 载入前/后:在 beforeMounted 阶段,Vue 实例的 $eldata 都初始化了,但还是挂载之前为虚拟 DOM 节点,data.message 还未替换。在 mounted 阶段,Vue 实例挂载完成,data.message 成功渲染;
  • 更新前/后:当 data 数据变化时,会触发 beforeUpdateupdated 钩子函数;
  • 销毁前/后:在执行 destoryed 方法后,对 data 的改变不会再触发周期函数,说明此时 Vue 实例已经解除了事件监听以及和 DOM 的绑定,但是 DOM 结构依然存在;

组件化

  • Vue 的核心思想:数据驱动组件化

  • 组件化是 Vue 的核心思想,主要目的是为了代码重用;

组件传值、通信

一、父组件 => 子组件

1、属性 props

  • 父组件 - 传递数据:
<HelloWorld msg="我是通过 prop 传递的数据" />
  • 子组件 - 接收数据:
props: {
  msg: {
    type: String,
    default: ''
  }
}

2、引用 refs

此方法(this.$refs.propRef.xxx = 'xxx')要在 mounted 生命周期中使用,因为组件的创建顺序是【先父后子】,在 created 中使用的话,不会获取到。

组件的加载渲染过程:

  • beforeCreate
  • created
  • beforeMount
  • beforeCreate
  • created
  • beforeMount
  • mounted
  • mounted
// parent
<HelloWorld ref="propRef" />

this.$refs.propRef.xxx = 'xxx';

this.$refs 如果在 created 中使用的话,可以写在 this.$nextTick 的回调函数中。

此时的执行顺序是:created 中的方法 => mounted 中的方法 => created 中的 nextTick

created() {
  console.log("第一个执行");
  console.log(this.$refs.propRef); // undefined
  this.$nextTick(() => {
    console.log("第三个执行");
    console.log(this.$refs.propRef);
  });
}

mounted() {
  console.log("第二个执行");
  this.$refs.propRef.foo = "bar";
}

3、子元素 $children

// parent
this.$children[0].xx = "xxx";

官方解释:当前实例的直接子组件。需要注意 $children 并不保证顺序,也不是响应式的。

二、子组件 => 父组件

1、自定义事件 - $emit /@xxx

  • 在子组件派发,在父组件监听
    • 注意:【事件的派发者是谁,事件的监听者就是谁 】,只不过声明的时候声明在父组件中了;
1、传递一个参数:
  • 子组件:
this.$emit('add', good);
  • 父组件:
<Cart @add="cartAdd($event)"></Cart>

<!-- 或者 -->
<Cart @add="cartAdd"></Cart>
cartAdd(e) {
  console.log(e);
}

// 或者
cartAdd(args) {
  console.log(args);
}
2、传递多个参数:父组件可以使用 arguments 获取参数
  • 子组件:
this.$emit('add', 'data1', 'data2');
  • 父组件:
<Cart @add="cartAdd(arguments)"></Cart>

<!-- 或者 -->
<Cart @add="cartAdd"></Cart>
cartAdd(msg) {
  console.log(msg[0]);
  console.log(msg[1]);
	...
}
  
// 或者
cartAdd(...args) {
  console.log(args);
}

三、兄弟组件:通过共同父辈组件

  • 通过共同的父辈组件搭桥:$parent$root
// brother1
this.$parent.$on('foo', handle);

// brother2
this.$parent.$emit('foo');

四、祖先和后代之间

1、provide/inject

  • 由于嵌套层数过多,传递 props 不切实际,vue 提供了 provide/inject API 完成该任务;
    • provide/inject:能够实现祖先给后代传值
// ancestor(祖代)
provide() {
	return {
    foo: 'foo'
  }
}

// descendant(后代)
inject: ['foo']

注意:provideinject 主要为高阶组件/组件库提供用例,并不推荐直接用于应用程序代码中,我们更多会在开源组件库中见到。

但是,反过来想要后代给祖代传值,这种方案就不行了!!!

  • 官方提示:provideinject 绑定并不是响应式的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应式的。
// App
provide() {
  return {
    dong: this.home
  };
},
data() {
	return {
		home: ["App home"]
	};
}

// HelloWorld
inject: ["dong"],

// this.dong = ["App2 data"]; // 值会有变化,但是报警告,Avoid mutating an injected value directly since the changes will be overwritten whenever the provided component re-renders
this.dong.push("App2 data"); // 可以修改成功

五、任意两个组件之间:事件总线 或 vuex

1、事件总线 bus

  • 事件总线:创建一个 Bus 类负责事件派发、监听和回调管理;

  • src/plugins/bus.js

// Bus:事件派发、监听和回调管理
class Bus {
  constructor() {
    this.callbacks = {};
  }

  $on(name, fn) {
    this.callbacks[name] = this.callbacks[name] || [];
    this.callbacks[name].push(fn);
  }

  $emit(name, args) {
    if (this.callbacks[name]) {
      this.callbacks[name].forEach(cb => cb(args));
    }
  }
}

export default Bus; 
  • main.js
import Bus from "./plugins/bus";

Vue.prototype.$bus = new Bus();
  • 组件使用:
// child1
this.$bus.$on('foo', handle);

// child2
this.$bus.$emit('foo');

2、vuex

  • vuex:创建唯一的全局数据管理者 store,通过它管理数据并通知组件状态变更。