# v-show和v-if的区别
v-show:通过css display控制元素显示和隐藏,元素依然保留在DOM节点中
v-if:元素真正的渲染和销毁,会在DOM节点中把元素移除和新增,而不是显示和隐藏
频繁切换显示状态用v-show,否则用v-if,利于性能优化
# 为何在v-for中使用key
diff算法中通过tag和key来判断,是否是通一个节点,减少渲染次数,提升渲染性能
# 描述vue组件的生命周期(包括父子组件)
| 生命周期 | 描述 |
|---|---|
| beforeCreate | 在实例初始化之后,进行数据侦听和事件/侦听器的配置之前同步调用。 |
| created | 在实例创建完成后被立即同步调用。在这一步中,实例已完成对选项的处理,意味着以下内容已被配置完毕:数据侦听、计算属性、方法、事件/侦听器的回调函数。然而,挂载阶段还没开始,且 $el property 目前尚不可用。 |
| beforeMount | 在挂载开始之前被调用:相关的 render 函数首次被调用。 |
| mounted | 实例被挂载后调用,这时 el 被新创建的 vm.$el 替换了。如果根实例挂载到了一个文档内的元素上,当 mounted 被调用时 vm.$el 也在文档内。注意 mounted 不会保证所有的子组件也都被挂载完成。如果你希望等到整个视图都渲染完毕再执行某些操作,可以在 mounted 内部使用 vm.$nextTick |
| beforeUpdate | 在数据发生改变后,DOM 被更新之前被调用。这里适合在现有 DOM 将要被更新之前访问它,比如移除手动添加的事件监听器。 |
| updated | 在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。注意,updated 不会保证所有的子组件也都被重新渲染完毕。如果你希望等到整个视图都渲染完毕,可以在 updated 里使用 vm.$nextTick |
| beforeDestroy | 实例销毁之前调用。在这一步,实例仍然完全可用。 |
| destroyed | 实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。 |

父子组件生命周期关系:
// src/App.vue
<template>
{{ count }}
<child v-if="isShow" :count1="count1" />
</template>
<script>
import Child from "./Child.vue";
export default {
components: { Child },
data() {
return {
count: 1,
count1: 2,
isShow: true,
};
},
beforeCreate() {
console.log("parent ~ beforeCreate");
},
created() {
console.log("parent ~ created");
},
beforeMount() {
console.log("parent ~ beforeMount");
},
mounted() {
console.log("parent ~ mounted");
setTimeout(() => {
this.count = 2;
}, 3000);
setTimeout(() => {
this.count1 = 10;
}, 6000);
setTimeout(() => {
this.isShow = false;
}, 7000);
},
beforeUpdate() {
console.log("parent ~ beforeUpdate");
},
updated() {
console.log("parent ~ updated");
},
beforeDestroy() {
console.log("parent ~ beforeDestroy");
},
destroyed() {
console.log("parent ~ destroyed");
},
};
</script>
<template>
<div>child-{{ count }} || {{ count1 }}</div>
</template>
<script>
export default {
name: "child",
props: ["count1"],
data() {
return {
count: 1,
};
},
beforeCreate() {
console.log("child ~ beforeCreate");
},
created() {
console.log("child ~ created");
},
beforeMount() {
console.log("child ~ beforeMount");
},
mounted() {
console.log("child ~ mounted");
setTimeout(() => {
this.count = 2;
}, 4000);
},
beforeUpdate() {
console.log("child ~ beforeUpdate");
},
updated() {
console.log("child ~ updated");
},
beforeDestroy() {
console.log("parent ~ beforeDestroy");
},
destroyed() {
console.log("parent ~ destroyed");
},
};
</script>
首次加载时生命周期执行的顺序:
运行结果:

只是子组件中的数据更新:
只是父组件中的数据更新,不影响子组件:
父组件中的数据更新,影响到了子组件:

组件销毁:

# 错误捕获
# errorCaptured
详细含义请查阅官网 errorCaptured (opens new window) 。
在捕获一个来自后代组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传播。
示例:
src/Child.vue
<template></template>
<script>
export default {
mounted() {
console.log(a);
},
};
</script>
src/App.vue
<template>
<child />
</template>
<script>
import Child from "./Child.vue";
export default {
components: {
Child,
},
errorCaptured(err, vm, info) {
console.log(err, vm, info);
},
};
</script>
运行代码后,可以看到控制台打印出了报错信息:

# errorHandler
errorHandler (opens new window) 指定组件的渲染和观察期间未捕获错误的处理函数。这个处理函数被调用时,可获取错误信息和 Vue 实例。
还是上面的代码,我们把 App.vue 中捕获错误的代码删掉,在 main.js 中加入 errorHandler 方法,用于捕获错误。
import Vue from 'vue';
import App from './App.vue';
Vue.config.productionTip = false;
Vue.config.errorHandler = function(err, vm, info) {
console.log(err, vm, info);
};
new Vue({
render: (h) => h(App)
}).$mount('#app');
可以看到浏览器控制台打印出了捕获的错误信息:

细心的读者可能看到了,上面的代码我们只测试了同步代码报错,如果是异步代码呢?
src/Child.vue
<template></template>
<script>
export default {
mounted() {
setTimeout(() => {
console.log(a);
}, 3000);
},
};
</script>
src/App.vue
<template>
<child />
</template>
<script>
import Child from "./Child.vue";
export default {
components: {
Child,
},
errorCaptured(err, vm, info) {
console.log(err, vm, info);
},
};
</script>
src/main.js
import Vue from 'vue';
import App from './App.vue';
Vue.config.productionTip = false;
Vue.config.errorHandler = function(err, vm, info) {
console.log(err, vm, info);
};
new Vue({
render: (h) => h(App)
}).$mount('#app');
运行代码,可以看到,这两个方法都捕获不到定时器中的错误:

来看下接口请求:
<template>
<div></div>
</template>
<script>
import axios from "axios";
export default {
mounted() {
axios
.get("http://192.168.0.104:8083/index.html1")
.then((res) => {
console.log("res", res);
})
.catch((err) => {
// console.log(err);
});
}
};
</script>
控制台输出:

可以看到捕获不到 Promise 里的报错。
改成 async 和 await 形式的接口请求:
<template>
<div></div>
</template>
<script>
import axios from "axios";
export default {
mounted() {
const res = await axios.get("http://192.168.0.104:8083/index.html1");
console.log("res", res);
},
};
</script>
控制台输出结果如下:

可以看到可以捕获 async、await 方法里的报错。
errorHandler 从 2.6.0 起,这个钩子也会捕获 v-on DOM 监听器内部抛出的错误。另外,如果任何被覆盖的钩子或处理函数返回一个 Promise 链 (例如 async 函数),则来自其 Promise 链的错误也会被处理。
所以,如果我们要在代码中要上报项目错误的时候,不能完全依赖这两个函数,还需配合其他的方式,一起处理上报。
# 父组件怎么监听子组件生命周期
可以在调用的组件上添加 @hook:[生命周期方法]= “[回调函数]” 来实现,具体代码如下:
src/Child.vue
<template>
<div>child</div>
</template>
<script>
export default {
mounted() {},
};
</script>
src/App.vue
<template>
<div>
<child @hook:mounted="callback" />
</div>
</template>
<script>
import Child from "./Child.vue";
export default {
components: {
Child,
},
methods: {
callback() {
console.log("子组件生命周期执行了");
},
},
};
</script>
可以看到控制台输出了:
# Vue组件如何通信
Vue 组件间的通信大概可以分为三类:
- 父子组件通信
props、 $parent / $children、 provide / inject 、 ref 、$attrs / $listeners
- 兄弟组件通信
eventBus ; vuex
- 隔代组件通信
eventBus、Vuex;provide / inject 、$attrs / $listeners
下面我们来详细介绍下:
# props 和 $emit
父组件通过 props 将数组传给子组件;子组件通过 $emit 给父组件发送消息
父组件:
<template>
<div>
<child :name="name" @changeAge="changeAge" />
{{ age }}
</div>
</template>
<script>
import Child from "./Child.vue";
export default {
components: {
Child,
},
data() {
return {
name: "f",
age: 17,
};
},
methods: {
changeAge(ev) {
this.age = ev;
},
},
};
</script>
子组件:
<template>
<div @click="changeAge">{{ name }}</div>
</template>
<script>
export default {
props: ["name"],
methods: {
changeAge() {
this.$emit("changeAge", 18);
},
},
};
</script>
# EventBus
事件总线的方式适合父子、兄弟、跨级组件的通信。
下面是一个简单的 demo:
父组件:
// src/App.vue
<template>
<div>
<child-1/>
<child-2/>
</div>
</template>
<script>
import Child1 from "./Child.vue";
import Child2 from "./Child2.vue";
export default {
components: {
Child1,
Child2,
}
};
</script>
子组件1:
// src/Child1.vue
<template>
<div @click="send">child1:{{ info }}</div>
</template>
<script>
import EventBus from "./bus/index";
export default {
data() {
return {
info: "",
};
},
mounted() {
EventBus.$on("receiveInfoChild2", (info) => (this.info = info));
},
methods: {
send() {
EventBus.$emit("receiveInfoChild1", "I love you");
},
},
};
</script>
子组件2:
// src/Child2.vue
<template>
<div @click="send">child2: {{ info }}</div>
</template>
<script>
import EventBus from "./bus/index";
export default {
data() {
return {
info: "",
};
},
mounted() {
EventBus.$on("receiveInfoChild1", (info) => (this.info = info));
},
methods: {
send() {
EventBus.$emit("receiveInfoChild2", "I love you, too.");
},
},
};
</script>
效果如下:

使用 EventBus 代码可读性、维护性都比较低。
# $attrs 和 $listeners
- $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。
- $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件
demo 如下:
父组件:
// src/App.vue
<template>
<div>
<child
:name="name"
:age="age"
:score="100"
sex="man"
@click="handleClick"
/>
</div>
</template>
<script>
import Child from "./Child.vue";
export default {
components: {
Child,
},
data() {
return {
name: "f",
age: 18,
socre: 100,
};
},
methods: {
handleClick(value) {
console.log("来自child2传递过来的数据:", value);
},
},
};
</script>
子组件1:
// src/Child.vue
<template>
<div>
<child2 v-bind="$attrs" v-on="$listeners" />
</div>
</template>
<script>
import Child2 from "./Child2.vue";
export default {
components: {
Child2,
},
inheritAttrs: false,
mounted() {
console.log("child1 $attrs ~ ", this.$attrs);
console.log("child1 $listeners ~ ", this.$listeners);
},
};
</script>
子组件2:
// src/Child2.vue
<template>
<div @click="handleClick">child2 {{ $attrs.name }}</div>
</template>
<script>
export default {
props: ["sex"],
inheritAttrs: false,
mounted() {
console.log("child2 $attrs ~ ", this.$attrs);
console.log("child2 $listeners ~ ", this.$listeners);
},
methods: {
handleClick() {
this.$listeners.click("你好呀,爷爷组件");
},
},
};
</script>
运行效果如下:

从上图可以看到由于 child2 设置的 props 属性中含有 sex,所以 child2 并没有在控制台输出 sex,child1 输出了 sex ,这也就印证了 $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 。
# provide 和 inject
祖先组件中通过provider来提供变量,然后在子孙组件中通过inject来注入变量。示例代码如下:
父组件:
// src/App.vue
<template>
<div>
<child />
</div>
</template>
<script>
import Child from "./Child.vue";
export default {
components: {
Child,
},
provide: {
name: "fang",
},
};
</script>
子组件1:
// src/Child.vue
<template>
<div>
<child2 />
</div>
</template>
<script>
import Child2 from "./Child2.vue";
export default {
components: {
Child2,
},
};
</script>
子组件2:
// src/Child2.vue
<template>
<div>child2接收到的数据 - {{ name }}</div>
</template>
<script>
export default {
inject: ["name"],
};
</script>
效果如下:
# $children 和 $parent
这个方法只能在父子组件中使用,且并不推荐使用。
父组件:
// src/App.vue
<template>
<div><child ref="child1" /></div>
</template>
<script>
import Child from "./Child.vue";
export default {
components: {
Child,
},
data() {
return {
age: 18,
};
},
mounted() {
console.log("父组件获取子组件的name属性", this.$children[0].name);
this.$children[0].handleClick();
},
};
</script>
子组件:
// src/Child.vue
<template>
<div></div>
</template>
<script>
export default {
data() {
return {
name: "f",
};
},
created() {
console.log("子组件获取父组件的age属性", this.$parent.age);
},
methods: {
handleClick() {
console.log("父组件执行了子组件的方法");
},
},
};
</script>
运行效果如下:

# ref
ref 用在普通的 DOM 元素上,指代的是 DOM 节点;用在组件上,指代的是组件实例,这种方式一般也不推荐使用。
父组件:
// src/App.vue
<template>
<div><child ref="child1" /></div>
</template>
<script>
import Child from "./Child.vue";
export default {
components: {
Child,
},
mounted() {
// 获取子组件的方法和事件
const child1 = this.$refs.child1;
console.log("子组件的name属性值为", child1.name);
child1.handleClick();
},
};
</script>
子组件:
// src/Child.vue
<template>
<div></div>
</template>
<script>
export default {
data() {
return {
name: "f",
};
},
methods: {
handleClick() {
console.log("执行了子组件的方法");
},
},
};
</script>
效果展示如下:
# Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
我们先来简要介绍下 Vuex 中核心概念:
- state:数据来源
- mutaions:改变 state 的唯一途径,不能用来处理异步操作
- action:用来处理异步操作,如需修改状态,还是要在内部调用 mutation 才行,不能直接修改。
- getter:类似 Vue 中的计算属性
- module:相当于命名空间
Vuex 的具体使用可以查阅 官方文档 (opens new window) ,这里不再赘述。
下面我们来看下通过 Vuex 怎么实现组件间的通信,这里以兄弟组件为例:
父组件:
// src/App.vue
<template>
<div>
<child-1 />
<child-2 />
</div>
</template>
<script>
import Child1 from "./Child1.vue";
import Child2 from "./Child2.vue";
export default {
components: {
Child1,
Child2,
},
};
</script>
子组件1:
// src/Child1.vue
<template>
<div>来自child2的打招呼: {{ $store.state.fromChild2Msg }}</div>
</template>
<script>
export default {
mounted() {
this.$store.commit("sayHiToChild2", "hi child2");
},
};
</script>
子组件2:
// src/Child2.vue
<template>
<div>来自child1的打招呼: {{ $store.state.fromChild1Msg }}</div>
</template>
<script>
export default {
mounted() {
this.$store.commit("sayHiToChild1", "hi child1");
},
};
</script>
在 store 文件夹中新建 index.js 文件,输入以下内容:
// src/store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const state = {
fromChild1Msg: '',
fromChild2Msg: ''
};
const mutations = {
sayHiToChild1(state, payload) {
state.fromChild2Msg = payload;
},
sayHiToChild2(state, payload) {
state.fromChild1Msg = payload;
}
};
export default new Vuex.Store({ state, mutations });
在 main.js 中引入 store:
// src/main.js
import Vue from 'vue';
import App from './App.vue';
import store from './store/index';
Vue.config.productionTip = false;
Vue.config.errorHandler = function(err, vm, info) {
console.log(err, vm, info);
};
new Vue({
store,
render: (h) => h(App)
}).$mount('#app');
运行效果:
# 描述组件渲染和更新过程
# 虚拟DOM的优缺点
优点:
- 极大的减少了对真实 DOM 的操作
框架会帮助我们更新视图,不用我们自己频繁操作 DOM,可以极大的提升开发效率。
- 更好的跨平台
# 真实的 DOM 依赖于平台,而虚拟 DOM 的本质是 JS 对象,相对而言可以更方便的进行跨平台操作,如服务端渲染、混合开发等;
缺点:
虚拟 DOM 在某些极端情况下不一定比真实 DOM 快,举一个例子,就比如我就想在页面上写一个 hello world在,直接使用 document.write('hello world')即可,要是使用框架就繁琐了很多。既然它能够如此流行,对于大多数情况下表现肯定是不错的,能应对我们大多数的开发,个人感觉至于虚拟 DOM 的缺点一般情况根本不用关心。
# 如何将组件所有的props传递给子组件
通过$props
<Login v-bind="$props"/>
# 如何自己实现v-model
v-model 是一个语法糖,在我们使用一些表单输入元素的时候帮我们实现双向数据绑定。v-model 对不同的输入元素绑定不同的属性和方法:
- text 和 textarea 元素使用 value 属性和 input 事件;
- checkbox 和 radio 使用 checked 属性和 change 事件;
- select 字段将 value 作为 prop 并将 change 作为事件。
这里以 input 为例:
<template>
<div>
<input :value="value" @input="value = $event.target.value" />
{{ value }}
</div>
</template>
<script>
export default {
data() {
return {
value: "",
};
},
};
</script>
展示效果:
# 多个组件有相同的逻辑,如何抽离
mixin
# 何时使用异步组件
加载大组件
路由异步加载
# 什么时候使用keep-alive
缓存组件, 不需要重复渲染
多个静态tab页的切换
优化性能
# 什么时候使用beforeDestory
解绑自定义事件event.$off
清除定时器
解绑自定义DOM事件,如window scroll等
# 什么是作用域插槽
# vuex中action和mutation的区别
action中可以处理异步,mutation中不可以
mutation一般做原子操作,就是每次做一个操作
action可以整合多个mutation
# Vue-router常用的路由模式
路由模式主要分为两类:hash 模式和 H5 history 模式。
hash 模式: 使用 URL hash 值来作路由,兼容性好;
history 模式: 依赖 HTML5 History API 和服务器配置,兼容性略差,但用户体验好点;
# hash 模式的实现原理
location.hash 的值就是 url 中 # 后面的值,它有以下特性:
- hash 值的变化会在浏览器中增加访问记录,可以通过浏览器的前进、后退实现;
- 可以通过 hashchange 事件监听 hash 值的变化,控制页面的渲染
- hash 值的变化不会向服务器发送请求,不会刷新页面
# history 模式的实现原理
history.pushState() 和 history.repalceState()。这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录。
# 如何配置vue-router异步加载
export default new VueRouter {
routes: [
{
path: '/',
component: ()=>import(/* webpackChunkName: "cart" */'./comoponents/Cart')
}
]
}
# 用Vnode描述一个DOM结构
<div id="app" class="box">
<p>hello</p>
<ul style="color:red;">
<li>f</li>
</ul>
</div>
对应的Vnode
{
tag: 'div'
props: {
className: 'box'
id: 'app'
},
children: [
{
tag: 'p'
children: 'hello'
},
{
tag: 'ul',
props: {
style: 'color:red'
},
children: [
{
tag: 'li',
children: 'f'
}
]
}
]
}
# 监听data变化的核心API是什么
Object.defineProperty
深度监听和监听数组
有何缺点
# Vue如何监听数组变化
vu2中重新定义原型,重写push、pop等方法,实现监听
vue3中Proxy可以原生监听数组变化
# 描述响应式原理
监听data变化
组件渲染和更新流程
# diff算法的时间复杂度
O(n)
在O(n^3)基础上做了一些调整
# 简述diff算法过程
# Vue为什么是异步渲染,$nextTick有什么用
异步渲染,以提高渲染性能
$nextTick在DOM更新后触发回调
# Vue常见性能优化
合理使用v-show和v-if
合理使用computed
v-for中使用key,避免和v-if同时使用,因为v-for的优先级高些,每次v-for的时候v-if都要重新计算一遍
自定义事件、DOM事件要及时销毁
合理使用异步组件
data层级不要太深
使用vue-loader在开发环境做模板编译
合理使用keep-alive
前端通用的一些性能优化,如图片懒加载等
webpack层面的一些优化
# 双向数据绑定v-model的实现原理
input元素 value=this.name
绑定input事件this.name=$event.target.value
data更新触发re-render
# 对MVVM的理解
很久以前就有了组件化的概念asp、jsp时就有了,传统组件只是静态渲染,更新还需要依赖于操作DOM。Vue通过MVVM实现数据驱动视图,React 使用 setState 来实现数据驱动视图。我们不在具体操作 DOM,直接修改 Vue中的数据就行了。Vue 框架本身根据数据重新渲染视图。使用 Vue 开发的时候让我们更加关注数据,更加关注业务逻辑。
MVVM分为Model、View、ViewModel三者。
- Model:代表数据模型,数据和业务逻辑都在Model层中定义;
- View:代表UI视图,负责数据的展示;
- ViewModel:就是与界面(view)对应的Model。因为,数据库结构往往是不能直接跟界面控件一一对应上的,所以,需要再定义一个数据对象专门对应view上的控件。而 ViewModel 的职责就是把 model 对象封装成可以显示和接受输入的界面数据对象。
Model 和 View 并无直接关联,而是通过 ViewModel 来进行联系的,Model 和 ViewModel 之间有着双向数据绑定的联系。因此当 Model 中的数据改变时会触发 View 层的刷新,View 中由于用户交互操作而改变的数据也会在 Model 中同步。
简单的说,ViewModel 就是 View 与 Model 的连接器,View 与 Model 通过 ViewModel 实现双向绑定。View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。
# computed有什么特点
具备缓存功能,只要当依赖的属性发生变化时才会重新计算有利于提高性能
# 为何data必须是一个函数
如果data是对象,每个组件的data 都会是内存的同一个地址,一个数据改变了其他也改变了; 如果data是一个函数时,每个组件实例都有自己的作用域,每个实例相互独立不会相互影响;
# ajax请求应该放在哪个生命周期
created 和 mounted 是在同一个 tick 中执行的,而ajax 请求的时间一定会超过一个 tick。所以即便ajax的请求耗时是 0ms, 那么也是在 nextTick 中更新数据到 DOM 中。所以说在不依赖 DOM 节点的情况下一点区别都没有
如果需要操作 DOM 则放在 mounted 中。
# 如何理解ref toRef 和 toRefs
# ref
生成值类型的响应数据
可用于模板和reactive
通过.value修改值
<template>
<p>ref demo {{ageRef}} {{state.name}}</p>
</template>
<script>
import { ref, reactive } from 'vue'
export default {
name: 'Ref',
setup() {
const ageRef = ref(20) // 值类型 响应式
const nameRef = ref('fang')
const state = reactive({
name: nameRef
})
setTimeout(() => {
console.log('ageRef', ageRef.value)
ageRef.value = 25 // .value 修改值
nameRef.value = 'fei'
}, 1500);
return {
ageRef,
state
}
}
}
</script>
模板引用
<template>
<div ref="root">This is a root element</div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
const root = ref(null)
onMounted(() => {
// DOM元素将在初始渲染后分配给ref
console.log(root.value) // <div>这是根元素</div>
})
return {
root
}
}
}
</script>
# toRef和toRefs如何使用
针对一个响应式对象(reactive封装)的prop
创建一个ref,具有响应式
两者保持引用关系
<template>
<p>toRef demo - {{ageRef}} - {{state.name}} {{state.age}}</p>
</template>
<script>
import { ref, toRef, reactive } from 'vue'
export default {
name: 'ToRef',
setup() {
const state = reactive({
age: 20,
name: 'fang'
})
const age1 = computed(() => {
return state.age + 1
})
// // toRef 如果用于普通对象(非响应式对象),产出的结果不具备响应式
// const state = {
// age: 20,
// name: 'fei'
// }
const ageRef = toRef(state, 'age')
setTimeout(() => {
state.age = 25
}, 1500)
setTimeout(() => {
ageRef.value = 30 // .value 修改值
}, 3000)
return {
state,
ageRef
}
}
}
</script>
toRefs
将响应式对象转换为普通对象
对象的每个props都对应ref
两者保持引用关系
<template>
<p>toRefs demo {{age}} {{name}}</p>
</template>
<script>
import { ref, toRef, toRefs, reactive } from 'vue'
export default {
name: 'ToRefs',
setup() {
const state = reactive({
age: 20,
name: '双越'
})
const stateAsRefs = toRefs(state) // 将响应式对象,变成普通对象
// const { age: ageRef, name: nameRef } = stateAsRefs // 每个属性,都是 ref 对象
// return {
// ageRef,
// nameRef
// }
setTimeout(() => {
state.age = 25
}, 1500)
return stateAsRefs
// 这样结构出来state的值,虽然模板中可以正常使用,但是失去了响应式
// return {
// ...state
// }
}
}
</script>
# ref toRef和toRefs的最佳使用方式
function useFn () {
const state = reactive({
x: 1,
y: 2
})
// 返回时转换为ref
return toRefs(state)
}
export default {
setup() {
// 可以在不失去响应式的情况下进行结构
const { x, y } = useFn()
return {
x,
y
}
}
}
最佳使用方式:
- 用reactive做对象的响应式, 用ref做值类型的响应式
- setup中返回toRefs(state)或者toRef(state, key)
- ref变量命名都用xxxRef的方式
- 合成函数返回响应式对象时,使用toRefs
# 为什么需要用 ref
为什么需要ref?
返回值类型,会丢失响应式,如在setup、computed、合成函数,都有可能返回值类型。如果Vue不定义ref用户将自造ref,返回更混乱。
<template>
<p>{{age}} {{state.age}} - {{age1}}</p>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'WhyRef',
setup() {
let age = 20
setTimeout(() => {
age = 30
}, 1500)
// Proxy不能设置值类型
return {
// 此时age并不具备响应式
age
}
}
}
</script>
为什么需要.value?
ref是一个对象(不丢失响应式),value存储值,通过.value属性的get、set实现响应式,用于模板、reactive时不需要.value,其他情况都需要
为什么需要toRef和toRefs?
在不丢失响应式的情况下,把对象数据分解、扩散。前提是针对响应式对象非普通对象它们不创造响应式,而是延续响应式。
# vue3升级了哪些重要功能
createApp
// vue2
const app = new Vue({/*选项*/})
// vue3
const app = Vue.createApp({/*选项*/})
import { createApp } from 'vue'
import App from './App'
createApp(App).mount('#app')
// vue2
Vue.use()
Vue.mixin()
Vue.component()
Vue.directive()
// vue3
app.use()
app.mixin()
app.component()
app.directive()
emits属性
生命周期
多事件
<button @click="one(), two()"></button>
Fragment
// vue2
<template>
<div>
<h1>测试</h1>
<p>测试结果</p>
<div>
</template>
// vue3
<template>
<h1>测试</h1>
<p>测试结果</p>
</template>
移除.sync
// vu2
<MyCmp v-bind:title.sync="title"></MyCmp>
// vue3
<MyCmp v-model:title="title"></MyCmp>
异步组件的写法
new Vue({
components: {
'my-component': () => import('./home.vue')
}
})
// vue3
import { createApp, defineAsyncComponent } from 'vue'
createApp({
components: {
AsyncComponent: defineAsyncComponent(() => import('./home.vue'))
}
})
移除filter
// 在花括号中
{{message | capitalize}}
// 在v-bindz中
<div v-bind="rawId | formatId"></div>
Teleport
// 伪代码
<button @click="modalOpen = true"></button>
<teleport to="body">
<div v-if="modalOpen" class="modal">
<div>
<button @click="modalOpen = false"></button>
<div>
</div>
</teleport>
Suspense
CompositionAPI
# Composition API 如何实现逻辑复用
抽离逻辑代码到一个函数
函数命名以use开头,如:useXXX
在setup中引用useXXX函数
useMousePosition.js
import { reactive, ref, onMounted, onUnmounted } from 'vue'
function useMousePosition() {
const x = ref(0)
const y = ref(0)
function update(e) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
console.log('useMousePosition mounted')
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
console.log('useMousePosition unMounted')
window.removeEventListener('mousemove', update)
})
return {
x,
y
}
}
export default useMousePosition
MousePosition.vue
<template>
<p>mouse position {{x}} {{y}}</p>
</template>
<script>
import { reactive } from 'vue'
import useMousePosition from './useMousePosition'
export default {
name: 'MousePosition',
setup() {
const { x, y } = useMousePosition()
return {
x,
y
}
}
}
</script>
# Proxy 和 Object.defineProperty 对比
优点:
- 可以直接实现对整个对象的监听,无需通过遍历监听每个属性;
- 可以直接监听对象属性的新增、删除;
- 可以直接监听数组的变化;
- 提供了更多拦截方法,可以实现更多操作;
缺点:
兼容性不是很好,且不能通过 polyfill 抹平;
# Object.defineProperty
优点:
兼容性相对较好,Proxy 存在浏览器兼容问题,并且无法被 polyfill 抹平;
缺点:
- 它只能劫持对象的属性,如果想实现对对象的监控,需要逐个遍历对象的属性;
- 不能监听数组的变化,如果想实现对数组的监听,需要重写数组的方法,但即便重写后直接通过数组索引修改数组元素也检测不到;
- 它不能对 Set、Map 进行监听;
- 不能监听对象属性的新增和删除,只能通过 Vue.set 和 Vue.delete 实现监听;
# Vue3如何实现响应式
// const data = {
// name: 'zhangsan',
// age: 20,
// }
const data = ['a', 'b', 'c']
const proxyData = new Proxy(data, {
get(target, key, receiver) {
// 只处理本身(非原型的)属性
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('get', key) // 监听
}
const result = Reflect.get(target, key, receiver)
return result // 返回结果
},
set(target, key, val, receiver) {
// 重复的数据,不处理
if (val === target[key]) {
return true
}
const result = Reflect.set(target, key, val, receiver)
console.log('set', key, val)
// console.log('result', result) // true
return result // 是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
// console.log('result', result) // true
return result // 是否删除成功
}
})
# Vite 为什么启动非常快
Vite是一个前端打包工具,Vue作者发起的项目,借助Vue的影响力,发展较快和Webpack竞争
优势:开发环境下无需打包,启动快
开发环境下使用ES6 Module,无需打包-非常快,无需像webpack打包成ES5再引入
生产环境使用rollup并不会快很多
浏览器中怎么直接使用ES Module呢?
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">app</div>
<!-- 基本使用 -->
<script type="module">
import { add } from './index.js'
console.log('基本使用', add(1, 2))
</script>
<!-- 外联的方式 -->
<script type="module" src="./index.js"></script>
<!-- 远程引入 -->
<script type="module">
import { createStore } from 'https://unpkg.com/redux@latest/es/redux.mjs'
console.log('远程引入', createStore)
</script>
<!-- 动态引入 -->
<script>
app.onclick = async function() {
const add = await import('./index.js')
const multi = await import('./index.js')
console.log('add', multi)
console.log('动态引入', add.add(1, 2), multi.default(34, 45))
}
</script>
</body>
</html>
index.js
export function add(a, b) {
return a + b
}
export default function multi(a, b) {
return a * b
}
console.log('外链引入')
# watch和watchEffect的区别
两者都可以监听data属性变化
watch需要明确监听哪个属性
watchEffect会根据其中的属性,自动监听其变化
<template>
<p>watch vs watchEffect</p>
<p>{{numberRef}}</p>
<p>{{name}} {{age}}</p>
</template>
<script>
import { reactive, ref, toRefs, watch, watchEffect } from 'vue'
export default {
name: 'Watch',
setup() {
const numberRef = ref(100)
const state = reactive({
name: '双越',
age: 20
})
watchEffect(() => {
// 初始化时,一定会执行一次(收集要监听的数据)
console.log('hello watchEffect')
})
watchEffect(() => {
console.log('state.name', state.name)
})
watchEffect(() => {
console.log('state.age', state.age)
})
watchEffect(() => {
console.log('state.age', state.age)
console.log('state.name', state.name)
})
setTimeout(() => {
state.age = 25
}, 1500)
setTimeout(() => {
state.name = '双越A'
}, 3000)
// watch(numberRef, (newNumber, oldNumber) => {
// console.log('ref watch', newNumber, oldNumber)
// }
// // , {
// // immediate: true // 初始化之前就监听,可选
// // }
// )
// setTimeout(() => {
// numberRef.value = 200
// }, 1500)
// watch(
// // 第一个参数,确定要监听哪个属性
// () => state.age,
// // 第二个参数,回调函数
// (newAge, oldAge) => {
// console.log('state watch', newAge, oldAge)
// },
// // 第三个参数,配置项
// {
// immediate: true, // 初始化之前就监听,可选
// // deep: true // 深度监听
// }
// )
// setTimeout(() => {
// state.age = 25
// }, 1500)
// setTimeout(() => {
// state.name = '双越A'
// }, 3000)
return {
numberRef,
...toRefs(state)
}
}
}
</script>
# setup中如何获取组件实例
在setup中和其他的Composition API中是没有this的,通过getCurrentInstance获取当前实例。如果使用的是Options API可照常使用this。
<template>
<p>get instance</p>
</template>
<script>
import { onMounted, getCurrentInstance } from 'vue'
export default {
name: 'GetInstance',
data() {
return {
x: 1,
y: 2
}
},
setup() {
console.log('this', this) // undefined
onMounted(() => {
console.log('this in onMounted', this) // undefined
console.log('x', instance.data.x) // 1
})
const instance = getCurrentInstance()
// setup是created和beforeCreate的合集,这个时候组件还没有正式的初始化,所以打印输出undefined
console.log('x', instance.data.x) // undefined
},
mounted() {
console.log('this2', this)
console.log('y', this.y)
}
}
</script>
# Composition API 和 React Hooks 的对比
前者setup只会被调用一次,后者函数(函数组件)会被调用多次
前者无需使用useMemo、useCallback,因为setup只调用一次
前者无需顾虑调用顺序,后者必须保证hooks顺序一致
前者reactive+ref比后者useState难理解
# Vue3为什么比Vue2快
Vue原理大体分为
组件化
响应式
vdom和diff
模板编译
渲染过程
前端路由
# 监听data变化的核心API是什么
组件data的数据一旦变化,立刻触发视图更新。
核心API Object.defineProperty
Proxy兼容性不太好,且无法polyfill
怎么监听数组,怎么监听复杂对象
Object.defineProperty的缺点:
深度监听需要一次性递归到底,一次性计算量大
无法监听新增属性/删除属性(Vue.set、Vue.delete)
无法原生监听数组,需要特殊处理
const data = {
name: 'f',
info: {
name:''
}
}
function updateView() {
console.log('更新视图')
}
function defineReactive(target, key, value) {
// 深度监听
observe(value)
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue !== value) {
observe(value)
value = newValue
updateView()
}
}
})
}
function observe(target) {
if (target == null || typeof target !== 'object') return target
for (let key in target)
defineReactive(target, key, target[key])
}
observe(data)
data.name
// 赋值相同的值不会重新设置
data.name = 'f'
data.name = 'ff'
data.info.name = 1 // 深度监听
delete data.name //
data.age = 18
# vue如何监听数组变化
const data = {
arr: []
}
function updateView() {
console.log('更新视图')
}
const oldArrPrototype = Array.prototype;
const arrProto = Object.create(oldArrPrototype);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function () {
updateView()
oldArrPrototype[methodName].call(this, ...arguments)
}
})
function defineReactive(target, key, value) {
// 深度监听
observe(value)
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue !== value) {
observe(value)
value = newValue
updateView()
}
}
})
}
function observe(target) {
if (target == null || typeof target !== 'object') return target
if (Array.isArray(target))
target.__proto__ = arrProto
for (let key in target)
defineReactive(target, key, target[key])
}
observe(data)
data.arr.push(1)
# 如何用JS实现hash路由
url组成部分
以这个链接为例:http://192.168.0.103:8080/books/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BAVue.js/01.html?a=1&b=2#%E4%BB%80%E4%B9%88%E6%98%AFvue-js
location.protocol -> "http:"
location.hostname -> "192.168.0.103"
location.host -> "192.168.0.103:8080"
location.port -> "8080"
location.pathname -> "/books/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BAVue.js/01.html"
location.search -> "?a=1&b=2"
location.hash -> "#%E4%BB%80%E4%B9%88%E6%98%AFvue-js"
hash变化会触发页面跳转,即浏览器的前进、后退,hash变化不会刷新页面,SPA的特点不需要刷新页面,hash不会提交到server端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>hash test</title>
</head>
<body>
<p>hash test</p>
<button id="btn1">修改 hash</button>
<script>
// hash 变化,包括:
// a. JS 修改 url
// b. 手动修改 url 的 hash
// c. 浏览器前进、后退
window.onhashchange = (event) => {
console.log('old url', event.oldURL)
console.log('new url', event.newURL)
console.log('hash:', location.hash)
}
// 页面初次加载,获取 hash
document.addEventListener('DOMContentLoaded', () => {
console.log('hash:', location.hash)
})
// JS 修改 url
document.getElementById('btn1').addEventListener('click', () => {
location.href = '#/user'
})
</script>
</body>
</html>
hash变化,包括:
- JS修改url // location.href = "#/user"
- 手动修改url的hash //在浏览器地址栏手动修改hash,但是注意如果修改了url的其它部分可能会触发页面刷新,而且会触发前进后退
- 浏览器前进、后退 //在浏览器操作前进后退,会导致hash变化
# 如何用JS实现H5 history路由
url规范的路由,跳转时不刷新页面,主要通过history.pushState和window.onpopstate这两个方式实现
history需要后端支持
toB的情况推荐用hash,简单易用,对url规范不敏感,C端 不考虑 SEO ,搜索引擎优化的话,也可以不用 H5 history
popstate事件在浏览器的history对象的当前记录发生显式切换时触发。注意,调用history.pushState()或history.replaceState(),并不会触发popstate事件。该事件只在用户在history记录之间显式切换时触发,比如鼠标点击“后退/前进”按钮,或者在脚本中调用history.back()、history.forward()、history.go()时触发。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>history API test</title>
</head>
<body>
<p>history API test</p>
<button id="btn1">修改 url</button>
<script>
// 页面初次加载,获取 path
document.addEventListener('DOMContentLoaded', () => {
console.log('load', location.pathname)
})
// 打开一个新的路由
// 【注意】用 pushState 方式,浏览器不会刷新页面
document.getElementById('btn1').addEventListener('click', () => {
const state = { name: 'page1' }
console.log('切换路由到', 'page1')
history.pushState(state, '', 'page1') // 重要!!
})
// 监听浏览器前进、后退
window.onpopstate = (event) => { // 重要!!
console.log('onpopstate', event.state, location.pathname)
}
// 需要 server 端配合,可参考
// https://router.vuejs.org/zh/guide/essentials/history-mode.html#%E5%90%8E%E7%AB%AF%E9%85%8D%E7%BD%AE%E4%BE%8B%E5%AD%90
</script>
</body>
</html>
# vue组件是如何渲染和更新的
初次渲染过程
- 解析模板为render函数(开发环境已通过vue-loader完成)
- 触发响应式,监听data属性getter setter
- 执行render函数,生成vnode,执行patch
更新过程
- 修改data,触发setter
- 重新执行render函数,生成newVnode
- patch
render函数在生成vnode时会触发data里面的getter,并收集依赖,当data被修改时去通知watcher重新render
# vue组件异步渲染
汇集data的修改,一次性更新视图,减少DOM操作次数提高性能。
# Vdom和diff算法
vdom是实现Vue的重要基石,diff算法是vdom中最核心、最关键部分
DOM操作非常消耗性能,以前用JQ可以自行控制DOM操作的时机手动调整。
Vue、React都是数据驱动视图
vdom - 用JS模拟DOM结构,计算出最小的变更,操作DOM
树diff的时间复杂度为O(n^3) 1.遍历tree1 2.遍历tree2 3.排序
优化时间复杂度到O(n):
只比较同一层级,不跨级比较
tag不相同则直接删掉重建,不再深度比较
tag和key两者都相同,则认为是相同节点,不再深度比较。
# 模板编译前置知识点-with语法
vue template complier将模板编译成render函数,执行render函数生成vnode
使用with,改变{}内自由变量的查找方式,将{}内自由变量,当做obj的属性来查找,如果找不到匹配的属性就会报错 ,with要慎用,它打破了作用域规则,易读性变差。
let person = {
name: 'f'
}
console.log(person.name) // f
console.log(person.age) // undefined
with(person) {
console.log(name) // f
console.log(age) // Uncaught ReferenceError: age is not defined
}
参考: