前言:最近准备跳槽了,所以开始整理面试题,欢迎大家一起学习和指出不对的地方
1、为什么 data是一个函数
组件中data写成一个函数,数据以函数返回形式定义,这样复用一次组件,就会返回一份新的data。这样就就给每个组件创建了一个私有的数据,不会造成数据的污染。
如果单纯的写对象形式,就会使组件实例共用了一份data,就会造成一个变了全都会变的结果。
2、组件通信有哪几种方式
- prop是 和 $emit 父组件向子组件传递数据是通过 prop 传递的,子组件传递给父组件是通过 $emit 触发事件来做到的
- $parent:获取当前组件的父组件 $children:获取当前的组件的子组件
- 父组件中通过 provide 来提供变量,然后在子组件中通过 inject 来注入变量。(官方不推荐在实际业务中使用,但是写组件库时很常用)
- $refs 获取组件实例
- eventBus 兄弟组件数据传递 这种情况下可以使用事件总线的方式(创建一个vue 实例 通过这个vue 实例去传递)
- vuex 状态管理
3、vue的生命周期和作用
beforeCreate:在实例初始化之后data、methods、computed 以及 watch 上的数据和方法都不能被访问到
created:实例已经创建完成之后,data watch 属性和方法的运算 没有$el 可以使用vm.$nextTick访问 DOM
beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用
mounted 在挂载完成后发生,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom 节点
beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁(patch)之前。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程
updated 发生在更新完成之后,当前阶段组件 Dom 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新,该钩子在服务器端渲染期间不被调用
beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。我们可以在这时进行善后收尾工作,比如清除计时器。
destroyed Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。
activated keep-alive 专属,组件被激活时调用
deactivated keep-alive 专属,组件被销毁时调用
不需要 DOM 数据的接口 一般都是放在 created 请求的
4、v-show 和 v-if 的区别
- v-if 会把DOM节点,设置为注释节点
- v-show 通过 display:bolck; 和 display:none; 控制显示隐藏
使用场景 - v-if切换不频繁或者一次判断的情况
- v-show 切换频繁
扩展
display:none、visibility:hidden 和 opacity:0 之间的区别?
共同点:都是用来隐藏的
不同点: - 是否占据空间
- display:none 隐藏之后不占据位置
- visibility:hidden、opacity:0 隐藏后占据位置
- 子元素是否继承
- display:none 不会被子元素继承
- visibility:hidden 会被子元素继承 可以通过 visibility:visible 来显示子元素
- opacity:0 会被子元素继承,不能通过设置子元素 opacity:1 来显示子元素
- 事件绑定
- display:none 无效
- visibility:hidden 无效
- opacity:0 有效
- 过渡动画
- display:none 无效
- visibility:hidden 无效
- opacity:0 有效
5、vue的内置指令
指令名 | 作用 |
---|---|
v-once | 定义它的元素或组件值渲染一次 包括元素或组件的所有子节点 首次渲染后 不再随数据的变化 重新渲染 将被视为静态内容 |
v-cloak | 保持在元素上直到关联实例结束编译 解决初始化慢导致页面闪动的最佳实践 |
v-bind | 绑定数据,动态更新HTML元素上的属性 |
v-on | 用户DOM元素的事件绑定 |
v-html | 赋值html |
v-text | 赋值text |
v-bind | 绑定数据,动态更新HTML元素上的属性 |
v-model | 绑定数据 绑定的数据是响应式的 |
v-if | 控制显示隐藏 |
v-show | 控制显示隐藏 |
v-for | 遍历渲染数据 优先级比 v-if 高 建议不要一起使用 |
v-pre | 跳过这个元素的和它子元素的编译过程 |
扩展内容 v-model语法糖
<input v-model="sth" />
// 等同于
<input
v-bind:value="message"
v-on:input="message=$event.target.value"
>
//$event 指代当前触发的事件对象;
//$event.target 指代当前触发的事件对象的dom;
//$event.target.value 就是当前dom的value值;
//在@input方法中,value => sth;
//在:value中,sth => value;
6、v-for、v-if 为什么不建议一起使用
v-for 和 v-if 不要在同一个标签中使用,因为解析时先解析 v-for 再解析 v-if。如果遇到需要同时使用时可以考虑写成计算属性的方式
7、computed 和 watch 的区别
computed 是计算属性,依赖其他属性计算值,并且computed 的值有缓存,只有当计算值发生变化才会返回内容
watch 监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作。
计算属性一般用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑
扩展内容 计算属性处理v-for和v-if 不能连用
<ul>
<li
v-for="user in activeUsers"
:key="user.id">
{{ user.name }}
</li>
</ul>
computed: {
activeUsers: function () {
// 过滤除了isActive的其他
return this.users.filter(function (user) {
return user.isActive
})
}
}
8、常见的修饰符
事件修饰符
- .stop:防止事件冒泡 跟JS中的 event.stopPropagation() 一样
- .prevent:防止事件传播 跟JS中的 event.preventDefault() 一样
- .capture:与冒泡的方向相反,事件捕获由外到内
- .self:只会触发自己范围内的事件,不包含子元素
- .once:只会触发一次
v-model 的修饰符
- .lazy 通过这个修饰符,转变为在 change 事件再同步
- .number 自动将用户的输入值转化为数值类型
- .trim 自动过滤用户输入的首尾空格
键盘事件的修饰符
- .enter
- .tab
- .delete (捕获“删除”和“退格”键)
- .esc
- .space
- .up
- .down
- .left
- .right
系统修饰键 - .ctrl
- .alt
- .shift
- .meta
鼠标按钮修饰符 - .left
- .right
- .middle
9、Vue 的单向数据流
数据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
10、vue响应式原理
核心实现类:
Observer : 它的作用是给对象的属性添加 getter 和 setter,用于依赖收集和派发更新
Dep : 用于收集当前响应式对象的依赖关系,每个响应式对象包括子对象都拥有一个 Dep 实例(里面 subs 是 Watcher 实例数组),当数据有变更时,会通过 dep.notify()通知各个 watcher。
Watcher : 观察者对象 , 实例分为渲染 watcher (render watcher),计算属性 watcher (computed watcher),侦听器 watcher(user watcher)三种
Watcher 和 Dep 的关系:
watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者,dep 通过 notify 遍历了 dep.subs 通知每个 watcher 更新。
依赖收集:
initState 时,对 computed 属性初始化时,触发 computed watcher 依赖收集
initState 时,对侦听属性初始化时,触发 user watcher 依赖收集
render()的过程,触发 render watcher 依赖收集
re-render 时,vm.render()再次执行,会移除所有 subs 中的 watcer 的订阅,重新赋值。
派发更新:
组件中对响应的数据进行了修改,触发 setter 的逻辑
调用 dep.notify()
遍历所有的 subs(Watcher 实例),调用每一个 watcher 的 update 方法。
原理:
当创建 Vue 实例时,vue 会遍历 data 选项的属性,利用 Object.defineProperty 为属性添加 getter 和 setter 对数据的读取进行劫持(getter 用来依赖收集,setter 用来派发更新),并且在内部追踪依赖,在属性被访问和修改时通知变化。
每个组件实例会有相应的 watcher 实例,会在组件渲染的过程中记录依赖的所有数据属性(进行依赖收集,还有 computed watcher,user watcher 实例),之后依赖项被改动时,setter 方法会通知依赖与此 data 的 watcher 实例重新计算(派发更新),从而使它关联的组件重新渲染。
一句话总结:
vue.js 采用数据劫持结合发布-订阅模式,通过 Object.defineproperty 来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发响应的监听回调
11、vue双向数据绑定
Vue.js 2.0 采用数据劫持(Proxy 模式)结合发布者-订阅者模式(PubSub 模式)的方式,通过 Object.defineProperty()来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
每个组件实例都有相应的watcher程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。
vue.js 3.0 待补充
vuex
Vuex 是专门为 vue 提供的全局状态管理系统,用于多个组件中数据共享、数据缓存等。(无法持久化、内部核心原理是通过创造一个全局实例 new Vue)
- State:定义了应用状态的数据结构,可以在这里设置默认的初始状态
- Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的getter 映射到局部计算属性
- Mutation:是唯一更改 store 中状态的方法,且必须是同步方法
- Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作
- Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中
Vuex 页面刷新数据丢失怎么解决
需要做 vuex 数据持久化 一般使用本地存储的方案来保存数据 可以自己设计存储方案 也可以使用第三方插件
推荐使用 vuex-persist 插件,它就是为 Vuex 持久化存储而生的一个插件。不需要你手动存取 storage ,而是直接将状态保存至 cookie 或者 localStorage 中
Vuex 为什么要分模块并且加命名空间
**模块:**由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。
**命名空间:**默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。
困难
Vue.mixin 的使用场景和原理
在日常开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或相似的代码,这些代码的功能相对独立,可以通过Vue的mixin功能抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化会调用mergeOptions方法进行合并,采用策略模式针对不同的属性进行合并。当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
export default function initMixin(Vue){
Vue.mixin = function (mixin) {
// 合并对象
this.options=mergeOptions(this.options,mixin)
};
}
};
// src/util/index.js
// 定义生命周期
export const LIFECYCLE_HOOKS = [
"beforeCreate",
"created",
"beforeMount",
"mounted",
"beforeUpdate",
"updated",
"beforeDestroy",
"destroyed",
];
// 合并策略
const strats = {};
// mixin核心方法
export function mergeOptions(parent, child) {
const options = {};
// 遍历父亲
for (let k in parent) {
mergeFiled(k);
}
// 父亲没有 儿子有
for (let k in child) {
if (!parent.hasOwnProperty(k)) {
mergeFiled(k);
}
}
//真正合并字段方法
function mergeFiled(k) {
if (strats[k]) {
options[k] = strats[k](parent[k], child[k]);
} else {
// 默认策略
options[k] = child[k] ? child[k] : parent[k];
}
}
return options;
}
使用场景
// minins.js
let mixin = {
data() {
return {
msg: '我是爱你的'
}
},
created() {
console.log("我是mixin里面的created!")
},
methods: {
hello() {
console.log(msg);
}
}
}
export default mixin
// vue文件
<template>
<div>{{ msg }}</div>
</template>
<script>
import mixins from "../mixins";
export default {
mixins: [mixins],
data() {
return {};
},
created() {
console.log(this.msg);
this.msg = this.msg + ",你要是这样想我也没办法!";
},
};
</script>
<style>
</style>
自定义指令
自定义指令
自定义指令分为全局指令和局部指令,在vue3中可以通过应用实例身上的 directive() 注册一个全局自定义指令,如果注册局部指令,可以在 diretives 选项来注册局部指令
全局自定义指令
自定义指令的钩子
- bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
- inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
- update:被绑定于元素所在的模板更新时调用,而无论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。
- componentUpdated:被绑定元素所在模板完成一次更新周期时调用。
- unbind:只调用一次,指令与元素解绑时调用。
// copy.ts
const copyDirective = {
mounted(el, binding) {
el.target = binding.value;
el.addEventListener("click", () => {
// 创建 textarea 元素
const textarea = document.createElement("textarea");
// 移出视野
textarea.style.position = "fixed";
textarea.style.top = "-99999px";
// 插入 DOM
document.body.appendChild(textarea);
// 定义值
textarea.value = el.target;
// 自动选中文本 用于复制的 API 只能复制选中的值
textarea.select();
// 浏览器复制 API 显示废弃的原因是没有纳入标准 但仍被各大主流浏览器支持
const res = document.execCommand("Copy");
res && console.log(`success: ${el.target}`);
// 移除 textarea
document.body.removeChild(textarea);
});
},
updated(el, binding) {
// 实时更新内容
el.target = binding.value;
},
};
export function setCopyDrective(app) {
app.directive("copy", copyDirective);
}
// index.ts
import type { App } from "vue";
import { setCopyDrective } from "./copy";
export function setDirectives(app: App) {
// 加载需要的指令
setCopyDrective(app);
}
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import { setDirectives } from "./directives";
const app = createApp(App);
// 注册所有自定义指令
setDirectives(app);
app.mount('#app')
nextTick 使用场景和原理
nextTick 中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法。
let callbacks = [];
let pending = false;
function flushCallbacks() {
pending = false; //把标志还原为false
// 依次执行回调
for (let i = 0; i < callbacks.length; i++) {
callbacks[i]();
}
}
let timerFunc; //定义异步方法 采用优雅降级
if (typeof Promise !== "undefined") {
// 如果支持promise
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
};
} else if (typeof MutationObserver !== "undefined") {
// MutationObserver 主要是监听dom变化 也是一个异步方法
let counter = 1;
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true,
});
timerFunc = () => {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
} else if (typeof setImmediate !== "undefined") {
// 如果前面都不支持 判断setImmediate
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// 最后降级采用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
export function nextTick(cb) {
// 除了渲染watcher 还有用户自己手动调用的nextTick 一起被收集到数组
callbacks.push(cb);
if (!pending) {
// 如果多次调用nextTick 只会执行一次异步 等异步队列清空之后再把标志变为false
pending = true;
timerFunc();
}
}
keep-alive 使用场景和原理
keep-alive 是 Vue 内置的一个组件,可以实现组件缓存,当组件切换时不会对当前组件进行卸载。
- 常用的两个属性 include/exclude,允许组件有条件的进行缓存。
- 两个生命周期 activated/deactivated,用来得知当前组件是否处于活跃状态。
- keep-alive 的中还运用了 LRU(最近最少使用) 算法,选择最近最久未使用的组件予以淘汰。
export default {
name: "keep-alive",
abstract: true, //抽象组件
props: {
include: patternTypes, //要缓存的组件
exclude: patternTypes, //要排除的组件
max: [String, Number], //最大缓存数
},
created() {
this.cache = Object.create(null); //缓存对象 {a:vNode,b:vNode}
this.keys = []; //缓存组件的key集合 [a,b]
},
destroyed() {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys);
}
},
mounted() {
//动态监听include exclude
this.$watch("include", (val) => {
pruneCache(this, (name) => matches(val, name));
});
this.$watch("exclude", (val) => {
pruneCache(this, (name) => !matches(val, name));
});
},
render() {
const slot = this.$slots.default; //获取包裹的插槽默认值
const vnode: VNode = getFirstComponentChild(slot); //获取第一个子组件
const componentOptions: ?VNodeComponentOptions =
vnode && vnode.componentOptions;
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions);
const { include, exclude } = this;
// 不走缓存
if (
// not included 不包含
(include && (!name || !matches(include, name))) ||
// excluded 排除里面
(exclude && name && matches(exclude, name))
) {
//返回虚拟节点
return vnode;
}
const { cache, keys } = this;
const key: ?string =
vnode.key == null
? // same constructor may get registered as different local components
// so cid alone is not enough (#3269)
componentOptions.Ctor.cid +
(componentOptions.tag ? `::${componentOptions.tag}` : "")
: vnode.key;
if (cache[key]) {
//通过key 找到缓存 获取实例
vnode.componentInstance = cache[key].componentInstance;
// make current key freshest
remove(keys, key); //通过LRU算法把数组里面的key删掉
keys.push(key); //把它放在数组末尾
} else {
cache[key] = vnode; //没找到就换存下来
keys.push(key); //把它放在数组末尾
// prune oldest entry //如果超过最大值就把数组第0项删掉
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
vnode.data.keepAlive = true; //标记虚拟节点已经被缓存
}
// 返回虚拟节点
return vnode || (slot && slot[0]);
},
};
LRU 的核心思想是如果数据最近被访问过,那么将来被访问的几率也更高,所以我们将命中缓存的组件 key 重新插入到 this.keys 的尾部,这样一来,this.keys 中越往头部的数据即将来被访问几率越低,所以当缓存数量达到最大值时,我们就删除将来被访问几率最低的数据,即 this.keys 中第一个缓存的组件。
动态组件、异步组件
动态组件
动态组件就是 component 组件,组件身上可以绑定一个is属性,用来表示某一个组件
通过使用保留的元素,动态地绑定到它的is特性,我们可以让多个组件可以使用同一个挂载点,并动态切换。根据v-bind:is="组件名"中的组件名去自动匹配组件,如果匹配不到则不显示。
// 局部注册
// 方式一:
<template>
<component :is="myComponent"></component>
<button @click="switchCmp">切换组件</button>
</template>
<script>
import myComponent1 from '../my-component1'; // 引入方式1
import myComponent2 from '../my-component2';
export default {
components: { myComponent1, myComponent2 },
data() {
return {
myComponent: myComponent1
};
},
methods: {
switchCmp() {
this.myComponent = myComponent2;
}
}
};
</script>
// 方式二:
<template>
<component :is="myComponent"></component>
<button @click="switchCmp">切换组件</button>
</template>
<script>
export default {
components: {
myComponent1: () => import('./my-component1'), // 引入方式二
myComponent2: () => import('./my-component2')
},
data() {
return {
myComponent: myComponent1
};
},
methods: {
switchCmp() {
this.myComponent = myComponent2;
}
}
};
</script>
// 全局注册
// main.js
Vue.component('myComponent1', () => import('./my-component1')); // 引入方式3
Vue.component('myComponent2', () => import('./my-component2')); // 组件使用
<template>
<component :is="myComponent"></component>
<button @click="switchCmp">切换组件</button>
</template>
<script>
export default {
components: {
myComponent1,
myComponent2
},
data() {
return {
myComponent: myComponent1
};
},
methods: {
switchCmp() {
// 切换组件
this.myComponent = myComponent2;
}
}
};
</script>
异步组件
// 全局注册 - 工厂函数需要返回Promise对象, vue最终会将其转换为上面的对象形式
// 方式一
Vue.component('async-example', function(resolve, reject) {
// 返回一个promise对象
resolve({
template: '<div>I am async!</div>'
});
});
// 方式二
Vue.component(
'async-webpack-example',
// 这个动态导入会返回一个 `Promise` 对象。
() => import('./my-async-component')
);
// 局部
new Vue({
// 当使用局部注册的时候,你也可以直接提供一个返回 Promise 的函数
components: {
'my-component': () => import('./my-async-component')
}
});
Vue2跟Vue3的区别
web前端面试题(JavaScript)
Vue 性能优化
Vue项目关于webpack的优化