主讲:云隐
理论
面试题:简单聊聊对于 MVVM 的了解
发展史已经旁支:
- 语义化模板;
MVC:Model - View - Controller;MVVM:Model - View - ViewModel;- 数据会绑定在
viewModel层,并且自动将数据渲染到页面上; - 视图发生变化时,会通知
viewModel层更新数据;
- 数据会绑定在
写法
vue 是如何利用 MVVM 思想进行项目开发
数据双向绑定:
- 利用花括号,构筑饿了数据与视图的双向绑定 —— 若干正则;
- 通过视图绑定事件,来处理数据;
生命周期
面试题:Vue 的生命周期
beforeCreate => created创建阶段beforeCreate:new Vue()- 实例挂载功能,此时还是一个空实例;created:data、props、method、computed- 数据操作,不涉及到vdom和dom;
beforeMount => mounted挂载阶段beforeMount:vdom- 数据操作,但是不涉及dom;mounted:dom- 任何操作;
beforeUpdate => updated更新阶段beforeUpdate:vdom更新了的,dom未更新还是旧的 - 可以更新数据;updated:dom已经更新了 - 谨慎操作数据(操作的数据和更新的数据是同一个,可能会发生死循环)
beforeDestory => destoryed销毁阶段beforeDestory:实例vm尚未被销毁 - 清空eventBus、reset store、clear计时器;destoryed:实例已经被销毁 - 收尾;
定向监听
面试题:computed 和 watch
相同点:
- 都是基于
vue的依赖收集机制; - 都是被依赖的变化触发,进行改变,进而进行处理计算;
不同点:
- 入和出
computed:多入单出 - 多个值变化,组成一个值的变化;watch:单入多出 - 单个值的变化,进而影响一系列的状态变更;
- 性能
computed:会自动diff依赖,若依赖没有变化,会改从缓存中读取当前计算值;watch:无论监听值变化与否,都会执行回调;
- 写法上
computed:必须有return返回值;watch:不一定;
- 时机上
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 的作用
- 模板编译原理:
template => domtemplate=> 正则匹配语法 - 生成AST(静态 + 动态)=> 转换AST为可执行方法 =>render()=>dom;
dom diff- 层级:只考虑单层复用,多层级遍历实现;
- 顺序:双向指针,首尾向中间移动;
- 替换:移动、新增、删除;优先复用 -
key=> 快速识别顺序;
key的作用 - 尽可能复用节点- 常见问题:
index做key、随机数做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、渲染/更新、渲染/卸载 等一系列过程,称之为生命周期。
二、各个生命周期的作用
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生命周期执行的时候,data和methods中的数据都还没有初始化,不能在这个阶段使用data中的数据都还没有初始化,不能在这个阶段使用data和methods中的方法;created:data和methods都已经初始化好了,如果要调用methods中的方法,或者操作data中的数据,最早可以在这个阶段中操作;beforeMount:执行到这个钩子的时候,在内存中已经编译好了模板,但是还没有挂在到页面中,此时,页面还是旧的;mounted:执行到这个钩子的时候,就表示Vue实例已经初始化完成了,此时组件脱离了创建阶段,进入到了运行阶段,如果我们想要通过插件操作页面上DOM节点,最早可以在这个阶段中进行;beforeUpdate:当执行这个钩子时,页面中的显示数据还是旧的,data中的数据是更新后的,页面还没有和最新的数据保持同步;updated:页面显示的数据和data中的数据已经保持同步了,都是最新的;beforeDestory:Vue实例从运行阶段进入到了销毁阶段,这个时候data、methods、指令、过滤器等等都是处于可用状态,还没有真正的被销毁;destoryed:这个时候data、methods、指令、过滤器等等都处于不可用状态,组件已经被销毁了;
五、created 和 mounted 的区别
created:在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图;mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的DOM节点进行一些需要的操作;
六、Vue 请求异步数据在哪个周期函数
在 created、beforeMount、mounted 中进行调用。因为在这三个钩子函数中,data 已经创建了,可以将服务器端返回的数据进行赋值。
推荐在 created 钩子函数中获取,优点:
- 能更快获取到服务端数据,减少页面加载时间,用户体验更好;
SSR不支持beforeMount和mounted钩子函数,放在created中有助于一致性;
七、keep-alive 中的生命周期有哪些
keep-alive 是 Vue 提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染 DOM。
如果问一个组件包裹了 keep-alive,那么它会多出两个生命周期:deactivated 和 activated。同时,beforeDestory 和 destoryed 就不会再被触发了,因为组件不会被真正的销毁。
当组件被换掉时,会被缓存到内存中,触发 deactivated 生命周期;当组件被切回来时,再去缓存里找这个组件,触发 activated 钩子函数。
八、请详细说下对 Vue 生命周期的理解
总共分为 8 个阶段:创建前/后、载入前/后、更新前/后、销毁前/后;
- 创建前/后:在
beforeCreated阶段,Vue实例的挂载元素$el和数据对象data都为undefined,还未初始化。在created阶段,Vue实例的数据对象data都有了,$el还没有; - 载入前/后:在
beforeMounted阶段,Vue实例的$el和data都初始化了,但还是挂载之前为虚拟DOM节点,data.message还未替换。在mounted阶段,Vue实例挂载完成,data.message成功渲染; - 更新前/后:当
data数据变化时,会触发beforeUpdate和updated钩子函数; - 销毁前/后:在执行
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/injectAPI完成该任务;provide/inject:能够实现祖先给后代传值
// ancestor(祖代)
provide() {
return {
foo: 'foo'
}
}
// descendant(后代)
inject: ['foo']
注意:
provide和inject主要为高阶组件/组件库提供用例,并不推荐直接用于应用程序代码中,我们更多会在开源组件库中见到。但是,反过来想要后代给祖代传值,这种方案就不行了!!!
- 官方提示:
provide和inject绑定并不是响应式的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的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,通过它管理数据并通知组件状态变更。