常问点
1.自己封装一个指定的派发功能,派发指定组件的方法 (如 elementUI 的$dispatch 实现)
- 实现 $dispatch 方法
- 实现 $broadcast
// 一直向上找parent 触发方法
// 如果我想触发指定组件的 方法
Vue.prototype.$dispatch = function (eventName, conponmentName, value) {
let parent = this.$parent;
while (parent) {
if (parent.$options.name === conponmentName) {
parent.$emit(eventName, value);
return;
}
parent = parent.$parent;
}
};
2. vue 修饰符 sync 的作用
🍁 当一个子组件改变了一个 prop 的值时,这个变化也会同步到父组件中所绑定 🍁
- 🌰 从 2.3.0 起我们重新引入了 .sync 修饰符,但是这次它只是作为一个编译时的语法糖存在。它会被扩展为一个自动更新父组件属性的 v-on 监听器
<comp :foo.sync="bar"></comp>
会被拓展为
<comp :foo="bar" @update:foo="val => bar = val"></comp>00
当子组件需要更新 foo 的值时,它需要显式地触发一个更新事件:
this.$emit("update:foo", newValue);
- 🍊 当一个子组件改变了一个 prop 的值时,这个变化也会同步到父组件中所绑定。如果我们不用.sync,我们想做上面的那个弹窗功能,我们也可以 props 传初始值,然后事件监听,实现起来也不算复杂。这里用 sync 实现,只是给大家提供一个思路,让其明白他的实现原理,可能有其它复杂的功能适用 sync。
这只是一个简单的例子,看完这个不知你是不觉得有个指令跟这个很相似,v-model?对,就是 v-model 在组件上使用的时候。
3. vue keep-alive 常用属性
- include - 字符串或正则表达式。只有名称匹配的组件会被缓存。(用逗号分隔字符串、正则表达式或一个数组)
- exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
- max - 数字。最多可以缓存多少组件实例。
4. vue 运行机制,依赖收集
🍂 Vue 能够实现当一个数据变更时,视图就进行刷新,而且用到这个数据的其他地方也会同步变更;而且,这个数据必须是在有被依赖的情况下,视图和其他用到数据的地方才会变更。 所以,Vue 要能够知道一个数据是否被使用,实现这种机制的技术叫做依赖收集 🍂
- 每个组件实例都有相应的 watcher 实例 - 渲染组件的过程,会把属性记录为依赖 - 当我们操纵一个数据时,依赖项的 setter 会被调用,从而通知 watcher 重新计算,从而致使与之相关联的组件得以更新 🍉
- 🌰 既然模板渲染需要用到某个数据,那么一定会对这个数据进行访问,所以只要拦截 getter,就有时机做出处理
- 🌰 在值变更的时候,也有 setter 可供拦截,那么拦截 setter,也就能做出下一步动作。
所以在 getter 里,我们进行依赖收集(所谓依赖,就是这个组件所需要依赖到的数据),当依赖的数据被设置时,setter 能获得这个通知,从而告诉 render()函数进行重新计算。🍓🍓🍓
5. 一个页面有父子组件,进入之后的渲染顺序触发的生命周期是什么样的
-
组件同步引入时生命周期顺序为
父组件的 beforeCreate、created、beforeMount --> 所有子组件的 beforeCreate、created、beforeMount --> 所有子组件的 mounted --> 父组件的 mounted 🍊
-
组件异步引入时生命周期顺序为
父组件的 beforeCreate、created、beforeMount、mounted --> 子组件的 beforeCreate、created、beforeMount、mounted 🍊
6. keep-alive,如果只想要 router-view 里面的某个组件被缓存,怎么做
-
使用 include/exclude keep-alive 包裹的是 component 对象
router-view 也是一个组件
增加 router.meta 属性
// routes 配置
export default [
{
path: "/",
name: "home",
component: Home,
meta: {
keepAlive: true, // 需要被缓存
},
},
{
path: "/:id",
name: "edit",
component: Edit,
meta: {
keepAlive: false, // 不需要被缓存
},
},
];
<keep-alive>
<router-view v-if="$route.meta.keepAlive">
<!-- 这里是会被缓存的视图组件,比如 Home! -->
</router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive">
<!-- 这里是不被缓存的视图组件,比如 Edit! -->
</router-view>
7. 假设这里有 3 个路由: A、B、C,默认显示 A,当 从 B 跳到 A,A 不刷新,C 跳到 A,A 刷新
- 使用到 组件内守卫 beforeRouteLeave 在跳转下一个路由之前改变 route.meta 属性值
- B 组件内的设置
export default {
data() {
return {};
},
methods: {},
beforeRouteLeave(to, from, next) {
// 设置下一个路由的 meta
to.meta.keepAlive = true; // 让 A 缓存,即不刷新
next();
},
};
- C 组件内设置
export default {
data() {
return {};
},
methods: {},
beforeRouteLeave(to, from, next) {
// 设置下一个路由的 meta
to.meta.keepAlive = false; // 让 A 不缓存,即刷新
next();
},
};
8. 组件通信中的 eventbus 原理是什么
EventBus 又称为事件总线。在 Vue 中可以使用 EventBus 来作为沟通桥梁的概念,就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件,所以组件都可以上下平行地通知其他组件
- 创建 全局 EventBus
const EventBus = new Vue();
Object.defineProperties(Vue.prototype, {
$bus: {
get: function () {
return EventBus;
},
},
});
或者使用 vue.prototype.$bus = new Vue();
- 使用 发布/订阅模式
this.$bus.$emit('nameOfEvent',{ ... pass some event data ...});
this.$bus.$on('nameOfEvent',($event) => {
// ...
})
$once 只执行一次
9. <font color=red>eventbus 的使用缺点</font>
- vue 作为单页面应用,如果在某一个页面刷新了之后,与之相关的 EventBus 会被移除
- 如果业务有反复操作的页面,EventBus 在监听的时候就会触发很多次,也是一个非常大的隐患。这时候我们就需要好好处理 EventBus 在项目中的关系。通常会用到,在 vue 页面销毁时,同时移除 EventBus 事件监听
import EventBus from "../assets/EventBus";
EventBus.$off("sendMessage", 事件函数); // 移除指定事件
EventBus.$off("sendMessage"); // 移除应用内所有对此事件的监听
EventBus.$off(); // 移除应用内所有事件的监听
- 由于是都使用一个 Vue 实例,所以容易出现重复触发的情景,两个页面都定义了同一个事件名,并且没有用$off 销毁(常出现在路由切换时)。
10. class 实现
- es5 通过构造函数(class 只是一个语法糖)
class Mold {
constructor(a, b) {
this.a = a;
this.b = b;
}
count() {
return this.a + this.b;
}
}
let sum = new Mold(1, 2);
console.log(sum.count()); //3
对应的 es5 中的构造函数的写法
function Mold(a, b) {
this.a = a;
this.b = b;
}
Mold.prototype.count = function () {
return this.a + this.b;
};
let sum = new Mold(1, 2);
console.log(sum.count()); //3
11. 实现继承的几种方式
1. 原型链继承
将父类的实例作为子类的原型
function Son(name) {
// Father.call(this);
this.name = name || "->son";
}
Son.prototype = new Father();
var s = new Son("son");
console.log(s.name); // son
s.sayAge(); // 18
s.sayName(); // son
console.log(s.age); // 18
console.log(s instanceof Father); // true
console.log(s instanceof Son); // true
- 优点
- 纯粹的继承关系,实例是子类的实例,也是父类的实例
- 父类新增原型方法、原型属性,子类都能访问到
- 简单,易于实现
- 缺点
- 可以在 Son 构造函数中,为 Son 实例增加实例属性。新增的属性和方法必须放在 new Father()这样的语句之后执行。
- 无法实现多继承,因为原型一次只能被一个实例更改
- 来自原型对象的所有属性被所有实例共享
- 创建子类实例时,无法向父构造函数传参
2. 构造继承(call):复制父类的实例属性给子类
function Son(name) {
Father.call(this);
this.name = name || "->son";
}
var s = new Son("son");
console.log(s.name); // son
//s.sayAge(); // 抛出错误
s.sayName(); // son
console.log(s.age); // undefined
console.log(s instanceof Father); // false
console.log(s instanceof Son); // true
- 优点
- 解决了原型链继承中子类实例共享父类引用属性的问题
- 创建子类实例时,可以向父类传递参数
- 可以实现多继承(call 多个父类对象)
- 缺点
- 实例并不是父类实例,只是子类的实例
- 只能继承父类实例的属性和方法,不能继承其原型上的属性和方法
- 无法实现函数复用,每个子类都有父类实例的副本,影响性能
3. 实例继承:为父类实例添加新特征,作为子类实例返回
function Son(name) {
var f = new Father();
f.name = name || "->son";
return f;
}
var s = new Son("son");
console.log(s.name); // son
s.sayAge(); // 18
s.sayName(); // son
console.log(s.age); // 18
console.log(s instanceof Father); // true
console.log(s instanceof Son); // false
- 优点
- 不限制调用方法,不管是 new 子类()还是子类(),返回的对象具有相同的效果
- 缺点
- 实例是父类的实例,不是子类的实例
- 不支持多继承
4. 组合继承:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
function Son(name) {
Father.call(this);
this.name = name || "->son";
}
Son.prototype = new Father();
// 修复构造函数指向
Son.prototype.constructor = Son;
var s = new Son("son");
console.log(s.name); // son
s.sayAge(); // 18
s.sayName(); // son
console.log(s.age); // 18
console.log(s instanceof Father); // true
console.log(s instanceof Son); // true
- 优点
- 弥补了构造继承的缺点,现在既可以继承实例的属性和方法,也可以继承原型的属性和方法
- 既是子类的实例,也是父类的实例
- 不存在引用属性共享问题
- 可传参
- 函数可以复用
- 缺点
- 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
5. 寄生组合继承:通过寄生方式,砍掉父类的实例属性,避免了组合继承生成两份实例的缺点
function Son(name) {
Father.call(this);
// var f = new Father();
this.name = name || "->son";
}
(function () {
// 创建一个没有实例方法的类
var None = function () {};
None.prototype = Father.prototype;
// 将实例作为子类的原型
Son.prototype = new None();
// 修复构造函数指向
Son.prototype.constructor = Son;
})();
var s = new Son("son");
console.log(s.name); // son
s.sayAge(); // 18
s.sayName(); // son
console.log(s.age); // 18
console.log(s instanceof Father); // true
console.log(s instanceof Son); // true
- 优点
- 堪称完美
- 缺点
- 实现起来较为复杂
6. Class 继承:使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super
class Son extends Father {
constructor(name) {
super(name);
this.name = name || "->father";
}
}
var s = new Son("son");
console.log(s.name); // son
s.sayAge(); // 18
s.sayName(); // son
console.log(s.age); // 18
console.log(s instanceof Father); // true
console.log(s instanceof Son); // true
12. new 的实现
- 创建一个空对象,作为将要返回的实例。
- 将空对象的原型(proto)指向构造函数的 prototype 属性。
- 将空对象赋值给构造函数中的 this。
- 指向构造函数中的代码
function A() {
this.name = 1;
this.age = 2;
// return {} // 如果一个类返回了一个引用空间 那么实例将指向这个空间
}
// let a = new A(); // 1) 创建对象,并且将对象传入到函数中作为this
A.prototype.say = function () {
console.log("say");
};
function mockNew(A) {
let obj = {};
A.call(obj);
obj.__proto__ = A.prototype;
return obj;
}
let o = mockNew(A);
o.say();
console.log(o);
13. 大厂面试题
1. promise.all 异常处理(如何做才能做到 promise.all 中即使一个 promise 程序 reject,promise.all 依然能把其他数据正确返回)
Promise.all 异常/出错一般情况下,all 里传 promise 数组,但是只要有一个出错,就会进入到 catch 里,不会捕获到正常数据的
- 解决方案:
- 全部改为串行调用(会失去并发优势,不推荐)
- 使用 catch 捕获异常
const a = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(111);
}, 1000);
});
const b = new Promise((resolve, reject) => {
setTimeout(() => {
reject(222);
}, 1000);
});
Promise.all(
[a, b].map((p) => {
return p.catch((error) => error);
})
)
.then((res) => {
console.log(res, "res");
})
.catch((error) => {
console.log(error, "error");
});
2. 版本号比较排序
有一组版本号如下['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5']。现在需要对其进行排序,排序的结果为 ['4.3.5','4.3.4.5','2.3.3','0.302.1','0.1.1']
const arr = ["0.1.1", "2.3.3", "0.302.1", "4.2", "4.3.5", "4.3.4.5"];
arr.sort((a, b) => {
const arr1 = a.split(".");
const arr2 = b.split(".");
let i = -1;
while (true) {
i++;
const s1 = arr1[i];
const s2 = arr2[i];
if ((s1 === undefined) | (s2 === undefined)) {
return arr2.length - arr1.length;
}
if (s1 === s2) continue;
return s2 - s1;
}
});
console.log(arr);
3. 数组中第 k 个最大元素
在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素
示例 1:输入: [3,2,1,5,6,4] 和 k = 2,输出: 5
-
示例 2: 输入: [3,2,3,1,2,4,5,5,6] 和 k = 4,输出: 4
如果用快速排序算法(先排序),排序时间复杂度是 O(NlogN),其中 N 表示数组 nums 的长度,想要第 k 大的元素,却给整个数组排序,有点杀鸡用牛刀的感觉,所以这里就有一些小技巧了,可以把时间复杂度降低到 O(NlogK)甚至是 O(N)
4. 二叉树中的所有路径
5. 二叉树中和为某一值的路径
6. node 中 promisify 实现
util 核心模块 中将方法 promise 化 ,以实现 async + await + promise
- 使用示例
const http = require("http");
const url = require("url");
const fs = require('fs');
const { promisify } = require("util");
const readFile = promisify(fs.readFile);
http.createServer((req, res) => {
(async () => {
try {
let data = await readFile('./index2.html');
res.statusCode = 200;
res.setHeader('Content-Type', "text/html;charset=utf-8");
res.end(data);
} catch (err) {
console.log(err);
}
})();
- 实现原理如下
// 实现 promisify 的原理
const promisify1 =
(fn) =>
(...args) => {
return new Promise((resolve, reject) => {
fn(...args, function (err) {
// node 异步的特点,最后一个参数是回调函数 只针对node 因为node是这种error-first的形式
if (err) reject(err);
resolve();
});
});
};
7. fetch 兼容超时重传
实现 fetch 请求超时/错误重新发送一次
- 阻断请求方法:
- AbortController 控制器,可以阻断 web 请求(不兼容 IE)
// IE不支持 const controller = new AbortController(); // AbortController.signal属性获取其关联 AbortSignal对象的引用 const signal = controller.signal; controller.abort();
- 兼容 ie 的 XMLHttpRequest.abort()
- 超时重发 => Promise.race: 同时发送多个请求,返回最先请求到的结果
let newFetch = (url) => {
// 重传次数控制
let requestAgain = 0;
// 创建控制器对象
const controller = new AbortController();
// AbortController.signal属性获取其关联 AbortSignal对象的引用
const signal = controller.signal;
//判断是否为IE 该判断不支持IE11 以上
if (navigator.userAgent.indexOf("MSIE") > -1) {
// 如果是ie 不支持-- AbortController--
// 也不支持fetch
// 此时使用原生xmlHttpRequest
// ie5/6和ie6+创建xmlHttpRequest对象有所不同
return new Promise((resolve, reject) => {
xmlreq(url, "GET", resolve);
})
.then((res) => {
// console.log('success')
// console.log(res)
})
.catch((err) => {
// 重新发送请求一次
if (requestAgain != 0) return;
requestAgain++;
xmlreq(url, "GET", resolve);
});
} else {
// 不是ie
return Promise.race([
requestPromise(url, signal)
.then(async (res) => {
let result = await res.json();
// console.log(result)
})
.catch((err) => {
// 错误时重发
// 使用控制器终止请求
controller.abort();
// 重新发送请求
requestPromise(url).then(async (res) => {
let result = await res.json();
// console.log('请求重发')
// console.log(result)
});
}),
new Promise((resolve, reject) => {
setTimeout(() => {
// 使用控制器终止请求
controller.abort();
// 重新发送请求一次
if (requestAgain != 0) return;
requestAgain++;
requestPromise(url).then(async (res) => {
let result = await res.json();
// console.log('请求重发')
// console.log(result)
});
}, 30000);
}),
]);
}
};
// 封装fetch 请求
let requestPromise = (url, signal = {}) => {
return fetch(url, {
signal,
headers: {
"Content-Type": "application/json;charset=utf-8;",
},
});
};
// 封装xmlHTTPRequest
let xmlreq = (url, methods = "GET", resolve) => {
// 创建xmlHttpRequest 对象
let xmlHttpReq;
if (window.XMLHttpRequest) {
// ie6+
xmlHttpReq = new XMLHttpRequest();
} else {
// ie5/6
xmlHttpReq = new ActiveXObject("Microsoft.XMLHTTP");
}
// 设置超时时间
xmlHttpReq.timeout = 30000;
xmlHttpReq.open("GET", url);
xmlHttpReq.send();
xmlHttpReq.ontimeout = (event) => {
// 终止上一次请求
xmlHttpReq.abort();
// 重新发送请求
xmlHttpReq.send();
};
xmlHttpReq.onreadystatechange = function (event) {
if (event.target.readyState == 4 && event.target.status == 200) {
let resData = JSON.parse(xmlHttpReq.response);
resolve(resData);
}
};
};
newFetch("http://a.com");
8. 观察者模式(高频)
当一个变量值被修改时,可以自动通知所有关注这个变量的其他对象,自动重新更新获取这个变量的新值
- 观察者模式是由具体的目标调用,例如:当事件触发 Dep 就会去调用观察者的方法,所以订阅者和观察者之间是存在依赖的。
- 发布订阅模式是由统一的调度中心(eventBus)调用,因为发布者和订阅者不需要知道对方的存在。
// 发布者-目标
class Dep {
constructor() {
// 记录所有的订阅者
this.subs = [];
}
// 添加订阅者
addSub(sub) {
// 判断订阅者是否存在,并且有update 方法
if (sub && sub.update) {
this.subs.push(sub);
}
}
// 发布通知
notify() {
// 循环发布通知
this.subs.forEach((s) => {
s.update();
});
}
}
// 订阅者-观察者
class Watcher {
update() {
console.log("update --->");
}
}
let dep = new Dep();
let watcher = new Watcher();
// 添加订阅者
dep.addSub(watcher);
// 发布执行
dep.notify();
// 执行结果 update --->
9. String indexOf 实现
10. 扁平化
将一个多维数组变为一维数组
- reduce 实现
function flatten(arr) {
return arr.reduce((result, item) => {
return result.concat(Array.isArray(item) ? flatten(item) : item);
}, []);
}
- ... 拓展运算符实现
function flatten(arr) {
while (arr.some((item) => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
11. 函数柯里化
函数柯里化 把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术
const curry = (fn, args = []) => {
const len = fn.length;
return (...rest) => {
const arr1 = args.concat(rest);
if (arr1.length < len) {
return curry(fn, arr1);
}
return fn(...arr1);
};
};
function add(a, b, c, d, e, f) {
var val = a + b + c + d + e + f;
return val;
}
// let sum = curry(add)(1)(2, 3)(4, 5, 6);
let sum = curry(add)(1)(2)(3)(4)(5, 6);
console.log(sum);
- 如题: 实现一个 add 方法,使计算结果能够满足如下预期:
- add(1)(2)(3) = 6;
- add(1, 2, 3)(4) = 10;
- add(1)(2)(3)(4,5) = 15;
function add() {
// 第一次执行时,定义一个数组专门用来存储所有的参数
var _args = Array.prototype.slice.call(arguments);
// 在内部声明一个函数,利用闭包的特性保存_args并且收集所有的参数值
var _adder = function () {
_args.push(...arguments);
return _adder;
};
// 利用toString的隐式转换的特性
// 当最后执行的时候进行隐式转换,并进行最终的值返回
_adder.toString = function () {
return _args.reduce(function (a, b) {
return a + b;
});
};
return _adder;
}
// 6
console.log(add(1)(2)(3));
// 10
console.log(add(1, 2, 3)(4));
// 15
console.log(add(1)(2)(3)(4)(5));
14. vue 如何实现组件封装
- Vue.extend 创建组件的构造函数
- Vue.component 注册全局组件
- 在实例化 extends 组件构造器时,传入属性必须是 propsData、而不是 props
15. vue 内导航守卫 以及 路由独享守卫、组件内守卫
全局导航守卫
- router.beforeEach 全局前置守卫
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
// ...
})
-
router.beforeResolve 全局解析守卫
- 在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用
router.afterEach 全局后置钩子
- 可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身
路由独享的守卫
- 可以在路由配置上直接定义 beforeEnter 守卫 , 与全局前置守卫的方法参数是一样的
const router = new VueRouter({
routes: [
{
path: "/foo",
component: Foo,
beforeEnter: (to, from, next) => {
// ...
},
},
],
});
组件内的守卫
- beforeRouteEnter
- beforeRouteUpdate
- beforeRouteLeave
const Foo = {
template: `...`,
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
},
};