回调函数经常会因为调用环境的变化而导致 this
的指向性变化。除此之外,使用回调函数来处理多个继发的异步任务时容易导致回调地狱(Callback Hell):
fs.readFile(fileA, ‘utf-8’, function (err, data) {
fs.readFile(fileB, ‘utf-8’, function (err, data) {
fs.readFile(fileC, ‘utf-8’, function (err, data) {
fs.readFile(fileD, ‘utf-8’, function (err, data) {
// 假设在业务中 fileD 的读写依次依赖 fileA、fileB 和 fileC
// 或者经常也可以在业务中看到多个 HTTP 请求的操作有前后依赖(继发 HTTP 请求)
// 这些异步任务之间纵向嵌套强耦合,无法进行横向复用
// 如果某个异步发生变化,那它的所有上层或下层回调可能都需要跟着变化(比如 fileA 和 fileB 的依赖关系倒置)
// 因此称这种现象为 回调地狱
// …
});
});
});
});
复制代码
回调函数不能通过 return
返回数据,比如我们希望调用带有回调参数的函数并返回异步执行的结果时,只能通过再次回调的方式进行参数传递:
// 希望延迟 3s 后执行并拿到结果
function getAsyncResult(result: number) {
setTimeout(() => {
return result * 3;
}, 1000);
}
// 尽管这是常规的编程思维方式
const result = getAsyncResult(3000);
// 但是打印 undefined
console.log('result: ', result);
function getAsyncResultWithCb(result: number, cb: (result: number) => void) {
setTimeout(() => {
cb(result * 3);
}, 1000);
}
// 通过回调的形式获取结果
getAsyncResultWithCb(3000, (result) => {
console.log('result: ', result); // 9000
});
复制代码
对于 JavaScript 中标准的异步 API 可能无法通过在外部进行 try...catch...
的方式进行错误捕获:
try {
setTimeout(() => {
// 下述是异常代码
// 你可以在回调函数的内部进行 try…catch…
console.log(a.b.c)
}, 1000)
} catch(err) {
// 这里不会执行
// 进程会被终止
console.error(err)
}
复制代码
上述示例讲述的都是 JavaScript 中标准的异步 API ,如果使用一些三方的异步 API 并且提供了回调能力时,这些 API 可能是非受信的,在真正使用的时候会因为执行反转(回调函数的执行权在三方库中)导致以下一些问题:
-
使用者的回调函数设计没有进行错误捕获,而恰恰三方库进行了错误捕获却没有抛出错误处理信息,此时使用者很难感知到自己设计的回调函数是否有错误
-
使用者难以感知到三方库的回调时机和回调次数,这个回调函数执行的权利控制在三方库手中
-
使用者无法更改三方库提供的回调参数,回调参数可能无法满足使用者的诉求
-
…
举个简单的例子:
interface ILib {
params: T;
emit(params: T): void;
on(callback: (params: T) => void): void;
}
// 假设以下是一个三方库,并发布成了npm 包
export const lib: ILib = {
params: ‘’,
emit(params) {
this.params = params;
},
on(callback) {
try {
// callback 回调执行权在 lib 上
// lib 库可以决定回调执行多次
callback(this.params);
callback(this.params);
callback(this.params);
// lib 库甚至可以决定回调延迟执行
// 异步执行回调函数
setTimeout(() => {
callback(this.params);
}, 3000);
} catch (err) {
// 假设 lib 库的捕获没有抛出任何异常信息
}
},
};
// 开发者引入 lib 库开始使用
lib.emit(‘hello’);
lib.on((value) => {
// 使用者希望 on 里的回调只执行一次
// 这里的回调函数的执行时机是由三方库 lib 决定
// 实际上打印四次,并且其中一次是异步执行
console.log(value);
});
lib.on((value) => {
// 下述是异常代码
// 但是执行下述代码不会抛出任何异常信息
// 开发者无法感知自己的代码设计错误
console.log(value.a.b.c)
});
复制代码
Promise
Callback 的异步操作形式除了会造成回调地狱,还会造成难以测试的问题。ES6 中的 Promise (基于 Promise A +[21] 规范的异步编程解决方案)利用有限状态机[22]的原理来解决异步的处理问题,Promise 对象提供了统一的异步编程 API,它的特点如下:
-
Promise 对象的执行状态不受外界影响。Promise 对象的异步操作有三种状态:
pending
(进行中)、fulfilled
(已成功)和rejected
(已失败) ,只有 Promise 对象本身的异步操作结果可以决定当前的执行状态,任何其他的操作无法改变状态的结果 -
Promise 对象的执行状态不可变。Promise 的状态只有两种变化可能:从
pending
(进行中)变为fulfilled
(已成功)或从pending
(进行中)变为rejected
(已失败)
温馨提示:有限状态机提供了一种优雅的解决方式,异步的处理本身可以通过异步状态的变化来触发相应的操作,这会比回调函数在逻辑上的处理更加合理,也可以降低代码的复杂度。
Promise 对象的执行状态不可变示例如下:
const promise = new Promise((resolve, reject) => {
// 状态变更为 fulfilled 并返回结果 1 后不会再变更状态
resolve(1);
// 不会变更状态
reject(4);
});
promise
.then((result) => {
// 在 ES 6 中 Promise 的 then 回调执行是异步执行(微任务)
// 在当前 then 被调用的那轮事件循环(Event Loop)的末尾执行
console.log('result: ', result);
})
.catch((error) => {
// 不执行
console.error('error: ', error);
});
复制代码
假设要实现两个继发的 HTTP 请求,第一个请求接口返回的数据是第二个请求接口的参数,使用回调函数的实现方式如下所示(这里使用 setTimeout
来指代异步请求):
// 回调地狱
const doubble = (result: number, callback: (finallResult: number) => void) => {
// Mock 第一个异步请求
setTimeout(() => {
// Mock 第二个异步请求(假设第二个请求的参数依赖第一个请求的返回结果)
setTimeout(() => {
callback(result * 2);
}, 2000);
}, 1000);
};
doubble(1000, (result) => {
console.log('result: ', result);
});
复制代码
温馨提示:继发请求的依赖关系非常常见,例如人员基本信息管理系统的开发中,经常需要先展示组织树结构,并默认加载第一个组织下的人员列表信息。
如果采用 Promise 的处理方式则可以规避上述常见的回调地狱问题:
const firstPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
// Mock 异步请求
// 将 resolve 改成 reject 会被 catch 捕获
setTimeout(() => resolve(result), 1000);
});
};
const nextPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
// Mock 异步请求
// 将 resolve 改成 reject 会被 catch 捕获
setTimeout(() => resolve(result * 2), 1000);
});
};
firstPromise(1000)
.then((result) => {
return nextPromise(result);
})
.then((result) => {
// 2s 后打印 2000
console.log('result: ', result);
})
// 任何一个 Promise 到达 rejected 状态都能被 catch 捕获
.catch((err) => {
console.error('err: ', err);
});
复制代码
Promise 的错误回调可以同时捕获 firstPromise
和 nextPromise
两个函数的 rejected
状态。接下来考虑以下调用场景:
const firstPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
// Mock 异步请求
setTimeout(() => resolve(result), 1000);
});
};
const nextPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
// Mock 异步请求
setTimeout(() => resolve(result * 2), 1000);
});
};
firstPromise(1000)
.then((result) => {
nextPromise(result).then((result) => {
// 后打印
console.log('nextPromise result: ', result);
});
})
.then((result) => {
// 先打印
// 由于上一个 then 没有返回值,这里打印 undefined
console.log('firstPromise result: ', result);
})
.catch((err) => {
console.error('err: ', err);
});
复制代码
首先 Promise 可以注册多个 then
(放在一个执行队列里),并且这些 then
会根据上一次返回值的结果依次执行。除此之外,各个 Promise 的 then
执行互不干扰。我们将示例进行简单的变换:
const firstPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
// Mock 异步请求
setTimeout(() => resolve(result), 1000);
});
};
const nextPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
// Mock 异步请求
setTimeout(() => resolve(result * 2), 1000);
});
};
firstPromise(1000)
.then((result) => {
// 返回了 nextPromise 的 then 执行后的结果
return nextPromise(result).then((result) => {
return result;
});
})
// 接着 nextPromise 的 then 执行的返回结果继续执行
.then((result) => {
// 2s 后打印 2000
console.log('nextPromise result: ', result);
})
.catch((err) => {
console.error('err: ', err);
});
复制代码
上述例子中的执行结果是因为 then
的执行会返回一个新的 Promise 对象,并且如果 then
执行后返回的仍然是 Promise 对象,那么下一个 then
的链式调用会等待该 Promise 对象的状态发生变化后才会调用(能得到这个 Promise 处理的结果)。接下来重点看下 Promise 的错误处理:
const promise = new Promise((resolve, reject) => {
// 下述是异常代码
console.log(a.b.c);
resolve(‘hello’);
});
promise
.then((result) => {
console.log('result: ', result);
})
// 去掉 catch 仍然会抛出错误,但不会退出进程终止脚本执行
.catch((err) => {
// 执行
// ReferenceError: a is not defined
console.error(err);
});
setTimeout(() => {
// 继续执行
console.log(‘hello world!’);
}, 2000);
复制代码
从上述示例可以看出 Promise 的错误不会影响其他代码的执行,只会影响 Promise 内部的代码本身,因为Promise 会在内部对错误进行异常捕获,从而保证整体代码执行的稳定性。Promise 还提供了其他的一些 API 方便多任务的执行,包括
-
Promise.all
:适合多个异步任务并发执行但不允许其中任何一个任务失败 -
Promise.race
:适合多个异步任务抢占式执行 -
Promise.allSettled
:适合多个异步任务并发执行但允许某些任务失败
Promise 相对于 Callback 对于异步的处理更加优雅,并且能力也更加强大, 但是也存在一些自身的缺点:
-
无法取消 Promise 的执行
-
无法在 Promise 外部通过
try...catch...
的形式进行错误捕获(Promise 内部捕获了错误) -
状态单一,每次决断只能产生一种状态结果,需要不停的进行链式调用
温馨提示:手写 Promise 是面试官非常喜欢的一道笔试题,本质是希望面试者能够通过底层的设计正确了解 Promise 的使用方式,如果你对 Promise 的设计原理不熟悉,可以深入了解一下或者手动设计一个。
Generator
Promise 解决了 Callback 的回调地狱问题,但也造成了代码冗余,如果一些异步任务不支持 Promise 语法,就需要进行一层 Promise 封装。Generator 将 JavaScript 的异步编程带入了一个全新的阶段,它使得异步代码的设计和执行看起来和同步代码一致。Generator 使用的简单示例如下:
const firstPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
};
const nextPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 3), 1000);
});
};
// 在 Generator 函数里执行的异步代码看起来和同步代码一致
function* gen(result: number): Generator<Promise, Promise, number> {
// 异步代码
const firstResult = yield firstPromise(result)
console.log('firstResult: ', firstResult) // 2
// 异步代码
const nextResult = yield nextPromise(firstResult)
console.log('nextResult: ', nextResult) // 6
return nextPromise(firstResult)
}
const g = gen(1)
// 手动执行 Generator 函数
g.next().value.then((res: number) => {
// 将 firstPromise 的返回值传递给第一个 yield 表单式对应的 firstResult
return g.next(res).value
}).then((res: number) => {
// 将 nextPromise 的返回值传递给第二个 yield 表单式对应的 nextResult
return g.next(res).value
})
复制代码
通过上述代码,可以看出 Generator 相对于 Promise 具有以下优势:
-
丰富了状态类型,Generator 通过
next
可以产生不同的状态信息,也可以通过return
结束函数的执行状态,相对于 Promise 的resolve
不可变状态更加丰富 -
Generator 函数内部的异步代码执行看起来和同步代码执行一致,非常利于代码的维护
-
Generator 函数内部的执行逻辑和相应的状态变化逻辑解耦,降低了代码的复杂度
next
可以不停的改变状态使得 yield
得以继续执行的代码可以变得非常有规律,例如从上述的手动执行 Generator 函数可以看出,完全可以将其封装成一个自动执行的执行器,具体如下所示:
const firstPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
};
const nextPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 3), 1000);
});
};
type Gen = Generator<Promise, Promise, number>
function* gen(): Gen {
const firstResult = yield firstPromise(1)
console.log('firstResult: ', firstResult) // 2
const nextResult = yield nextPromise(firstResult)
console.log('nextResult: ', nextResult) // 6
return nextPromise(firstResult)
}
// Generator 自动执行器
function co(gen: () => Gen) {
const g = gen()
function next(data: number) {
const result = g.next(data)
if(result.done) {
return result.value
}
result.value.then(data => {
// 通过递归的方式处理相同的逻辑
next(data)
})
}
// 第一次调用 next 主要用于启动 Generator 函数
// 内部指针会从函数头部开始执行,直到遇到第一个 yield 表达式
// 因此第一次 next 传递的参数没有任何含义(这里传递只是为了防止 TS 报错)
next(0)
}
co(gen)
复制代码
温馨提示:TJ Holowaychuk[23] 设计了一个 Generator 自动执行器 Co[24],使用 Co 的前提是
yield
命令后必须是 Promise 对象或者 Thunk 函数。Co 还可以支持并发的异步处理,具体可查看官方的 API 文档[25]。
需要注意的是 Generator 函数的返回值是一个 Iterator 遍历器对象,具体如下所示:
const firstPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
};
const nextPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 3), 1000);
});
};
type Gen = Generator<Promise>;
function* gen(): Gen {
yield firstPromise(1);
yield nextPromise(2);
}
// 注意使用 next 是继发执行,而这里是并发执行
Promise.all([…gen()]).then((res) => {
console.log('res: ', res);
});
for (const promise of gen()) {
promise.then((res) => {
console.log('res: ', res);
});
}
复制代码
Generator 函数的错误处理相对复杂一些,极端情况下需要对执行和 Generator 函数进行双重错误捕获,具体如下所示:
const firstPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
// 需要注意这里的reject 没有被捕获
setTimeout(() => reject(result * 2), 1000);
});
};
const nextPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 3), 1000);
});
};
type Gen = Generator<Promise>;
function* gen(): Gen {
try {
yield firstPromise(1);
yield nextPromise(2);
} catch (err) {
console.error('Generator 函数错误捕获: ', err);
}
}
try {
const g = gen();
g.next();
// 返回 Promise 后还需要通过 Promise.prototype.catch 进行错误捕获
g.next();
// Generator 函数错误捕获
g.throw(‘err’);
// 执行器错误捕获
g.throw(‘err’);
} catch (err) {
console.error('执行错误捕获: ', err);
}
复制代码
在使用 g.throw
的时候还需要注意以下一些事项:
-
如果 Generator 函数本身没有捕获错误,那么 Generator 函数内部抛出的错误可以在执行处进行错误捕获
-
如果 Generator 函数内部和执行处都没有进行错误捕获,则终止进程并抛出错误信息
-
如果没有执行过
g.next
,则g.throw
不会在 Gererator 函数中被捕获(因为执行指针没有启动 Generator 函数的执行),此时可以在执行处进行执行错误捕获
Async
Async 是 Generator 函数的语法糖,相对于 Generator 而言 Async 的特性如下:
-
内置执行器:Generator 函数需要设计手动执行器或者通用执行器(例如 Co 执行器)进行执行,Async 语法则内置了自动执行器,设计代码时无须关心执行步骤
-
yield
命令无约束:在 Generator 中使用 Co 执行器时yield
后必须是 Promise 对象或者 Thunk 函数,而 Async 语法中的await
后可以是 Promise 对象或者原始数据类型对象、数字、字符串、布尔值等(此时会对其进行Promise.resolve()
包装处理) -
返回 Promise:
async
函数的返回值是 Promise 对象(返回原始数据类型会被 Promise 进行封装), 因此还可以作为await
的命令参数,相对于 Generator 返回 Iterator 遍历器更加简洁实用
举个简单的示例:
const firstPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
};
const nextPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 3), 1000);
});
};
async function co() {
const firstResult = await firstPromise(1);
// 1s 后打印 2
console.log('firstResult: ', firstResult);
// 等待 firstPromise 的状态发生变化后执行
const nextResult = await nextPromise(firstResult);
// 2s 后打印 6
console.log('nextResult: ', nextResult);
return nextResult;
}
co();
co().then((res) => {
console.log('res: ', res); // 6
});
复制代码
通过上述示例可以看出,async
函数的特性如下:
-
调用
async
函数后返回的是一个 Promise 对象,通过then
回调可以拿到 async 函数内部return
语句的返回值 -
调用
async
函数后返回的 Promise 对象必须等待内部所有await
对应的 Promise 执行完(这使得async
函数可能是阻塞式执行)后才会发生状态变化,除非中途遇到了return
语句 -
await
命令后如果是 Promise 对象,则返回 Promise 对象处理后的结果,如果是原始数据类型,则直接返回原始数据类型
上述代码是阻塞式执行,nextPromise
需要等待 firstPromise
执行完成后才能继续执行,如果希望两者能够并发执行,则可以进行下述设计:
const firstPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
};
const nextPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 3), 1000);
});
};
async function co() {
return await Promise.all([firstPromise(1), nextPromise(1)]);
}
co().then((res) => {
console.log('res: ', res); // [2,3]
});
复制代码
除了使用 Promise 自带的并发执行 API,也可以通过让所有的 Promise 提前并发执行来处理:
const firstPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
console.log(‘firstPromise’);
setTimeout(() => resolve(result * 2), 10000);
});
};
const nextPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
console.log(‘nextPromise’);
setTimeout(() => resolve(result * 3), 1000);
});
};
async function co() {
// 执行 firstPromise
const first = firstPromise(1);
// 和 firstPromise 同时执行 nextPromise
const next = nextPromise(1);
// 等待 firstPromise 结果回来
const firstResult = await first;
console.log('firstResult: ', firstResult);
// 等待 nextPromise 结果回来
const nextResult = await next;
console.log('nextResult: ', nextResult);
return nextResult;
}
co().then((res) => {
console.log('res: ', res); // 3
});
复制代码
Async 的错误处理相对于 Generator 会更加简单,具体示例如下所示:
const firstPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
// Promise 决断错误
setTimeout(() => reject(result * 2), 1000);
});
};
const nextPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 3), 1000);
});
};
async function co() {
const firstResult = await firstPromise(1);
console.log('firstResult: ', firstResult);
const nextResult = await nextPromise(1);
console.log('nextResult: ', nextResult);
return nextResult;
}
co()
.then((res) => {
console.log('res: ', res);
})
.catch((err) => {
console.error('err: ', err); // err: 2
});
复制代码
async
函数内部抛出的错误,会导致函数返回的 Promise 对象变为 rejected
状态,从而可以通过 catch
捕获, 上述代码只是一个粗粒度的容错处理,如果希望 firstPromise
错误后可以继续执行 nextPromise
,则可以通过 try...catch...
在 async
函数里进行局部错误捕获:
const firstPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
// Promise 决断错误
setTimeout(() => reject(result * 2), 1000);
});
};
const nextPromise = (result: number): Promise => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 3), 1000);
});
};
async function co() {
try {
await firstPromise(1);
} catch (err) {
console.error('err: ', err); // err: 2
}
// nextPromise 继续执行
const nextResult = await nextPromise(1);
return nextResult;
}
co()
.then((res) => {
console.log('res: ', res); // res: 3
})
.catch((err) => {
console.error('err: ', err);
});
复制代码
温馨提示:Callback 是 Node.js 中经常使用的编程方式,Node.js 中很多原生的 API 都是采用 Callback 的形式进行异步设计,早期的 Node.js 经常会有 Callback 和 Promise 混用的情况,并且在很长一段时间里都没有很好的支持 Async 语法。如果你对 Node.js 和它的替代品 Deno 感兴趣,可以观看 Ryan Dahl 在 TS Conf 2019 中的经典演讲 Deno is a New Way to JavaScript[26]。
31、 Object.defineProperty 有哪几个参数?各自都有什么作用?
32、 Object.defineProperty 和 ES6 的 Proxy 有什么区别?
33、 ES6 中 Symbol、Map、Decorator 的使用场景有哪些?或者你在哪些库的源码里见过这些 API 的使用?
34、 为什么要使用 TypeScript ? TypeScript 相对于 JavaScript 的优势是什么?
35、 TypeScript 中 const 和 readonly 的区别?枚举和常量枚举的区别?接口和类型别名的区别?
36、 TypeScript 中 any 类型的作用是什么?
37、 TypeScript 中 any、never、unknown 和 void 有什么区别?
38、 TypeScript 中 interface 可以给 Function / Array / Class(Indexable)做声明吗?
39、 TypeScript 中可以使用 String、Number、Boolean、Symbol、Object 等给类型做声明吗?
40、 TypeScript 中的 this 和 JavaScript 中的 this 有什么差异?
41、 TypeScript 中使用 Unions 时有哪些注意事项?
42、 TypeScript 如何设计 Class 的声明?
43、 TypeScript 中如何联合枚举类型的 Key?
44、 TypeScript 中 ?.、??、!.、_、** 等符号的含义?
45、 TypeScript 中预定义的有条件类型有哪些?
46、 简单介绍一下 TypeScript 模块的加载机制?
47、 简单聊聊你对 TypeScript 类型兼容性的理解?抗变、双变、协变和逆变的简单理解?
48、 TypeScript 中对象展开会有什么副作用吗?
49、 TypeScript 中 interface、type、enum 声明有作用域的功能吗?
50、 TypeScript 中同名的 interface 或者同名的 interface 和 class 可以合并吗?
51、 如何使 TypeScript 项目引入并识别编译为 JavaScript 的 npm 库包?
52、 TypeScript 的 tsconfig.json 中有哪些配置项信息?
53、 TypeScript 中如何设置模块导入的路径别名?
框架
54、 React Class 组件有哪些周期函数?分别有什么作用?
55、 React Class 组件中请求可以在 componentWillMount 中发起吗?为什么?
56、 React Class 组件和 React Hook 的区别有哪些?
57、 React 中高阶函数和自定义 Hook 的优缺点?
58、 简要说明 React Hook 中 useState 和 useEffect 的运行原理?
59、 React 如何发现重渲染、什么原因容易造成重渲染、如何避免重渲染?
60、 React Hook 中 useEffect 有哪些参数,如何检测数组依赖项的变化?
61、 React 的 useEffect 是如何监听数组依赖项的变化的?
62、 React Hook 和闭包有什么关联关系?
63、 React 中 useState 是如何做数据初始化的?
64、 列举你常用的 React 性能优化技巧?
65、 Vue 2.x 模板中的指令是如何解析实现的?
66、 简要说明 Vue 2.x 的全链路运作机制?
67、 简单介绍一下 Element UI 的框架设计?
68、 如何理解 Vue 是一个渐进式框架?
69、 Vue 里实现跨组件通信的方式有哪些?
70、 Vue 中响应式数据是如何做到对某个对象的深层次属性的监听的?
71、 MVVM、MVC 和 MVP 的区别是什么?各自有什么应用场景?、
72、 什么是 MVVM 框架?
工程
73、Vue CLI 3.x 有哪些功能?Vue CLI 3.x 的插件系统了解?
74、Vue CLI 3.x 中的 Webpack 是如何组装处理的?
75、Vue 2.x 如何支持 TypeScript 语法?
76、如何配置环境使得 JavaScript 项目可以支持 TypeScript 语法?
77、如何对 TypeScript 进行 Lint 校验?ESLint 和 TSLint 有什么区别?
78、Node.js 如何支持 TypeScript 语法?
79、TypeScript 如何自动生成库包的声明文件?
80、Babel 对于 TypeScript 的支持有哪些限制?
81、Webpack 中 Loader 和 Plugin 的区别是什么?
82、在 Webpack 中是如何做到支持类似于 JSX 语法的 Sourcemap 定位?
83、发布 Npm 包如何指定引入地址?
84、如何发布开发项目的特定文件夹为 Npm 包的根目录?
85、如何发布一个支持 Tree Shaking 机制的 Npm 包?
86、Npm 包中 peerDependencies 的作用是什么?
87、如何优雅的调试需要发布的 Npm 包?
88、在设计一些库包时如何生成版本日志?
89、了解 Git (Submodule)子模块吗?简单介绍一下 Git 子模块的作用?
90、Git 如何修改已经提交的 Commit 信息?
91、Git 如何撤销 Commit 并保存之前的修改?
92、Git 如何 ignore 被 commit 过的文件?
93、在使用 Git 的时候如何规范 Git 的提交说明(Commit 信息)?
94、简述符合 Angular 规范的提交说明的结构组成?
95、Commit 信息如何和 Github Issues 关联?
96、Git Hook 在项目中哪些作用?
97、Git Hook 中客户端和服务端钩子各自用于什么作用?
98、Git Hook 中常用的钩子有哪些?
99、pre-commit 和 commit-msg 钩子的区别是什么?各自可用于做什么?
100、husky 以及 ghook 等工具制作 Git Hook 的原理是什么?
101、如何设计一个通用的 Git Hook ?
102、Git Hook 可以采用 Node 脚本进行设计吗?如何做到?
103、如何确保别人上传的代码没有 Lint 错误?如何确保代码构建没有 Lint 错误?
104、如何在 Vs Code 中进行 Lint 校验提示?如何在 Vs Code 中进行 Lint 保存格式化?
105、ESLint 和 Prettier 的区别是什么?两者在一起工作时会产生问题吗?
106、如何有效的识别 ESLint 和 Prettier 可能产生冲突的格式规则?如何解决此类规则冲突问题?
107、在通常的脚手架项目中进行热更新(hot module replacement)时如何做到 ESLint 实时打印校验错误信息?
108、谈谈你对 SourceMap 的了解?
109、如何调试 Node.js 代码?如何调试 Node.js TypeScript 代码?在浏览器中如何调试 Node.js 代码?
110、列举你知道的所有构建工具并说说这些工具的优缺点?这些构建工具在不同的场景下应该如何选型?
111、VS Code 配置中的用户和工作区有什么区别?
112、VS Code 的插件可以只对当前项目生效吗?
113、你所知道的测试有哪些测试类型?
114、你所知道的测试框架有哪些?
115、什么是 e2e 测试?有哪些 e2e 的测试框架?
116、假设现在有一个插入排序算法,如何对该算法进行单元测试?
网络
117、CDN 服务如何实现网络加速?
118、WebSocket 使用的是 TCP 还是 UDP 协议?
119、什么是单工、半双工和全双工通信?
120、简单描述 HTTP 协议发送一个带域名的 URL 请求的协议传输过程?(DNS、TCP、IP、链路)
121、什么是正向代理?什么是反向代理?
122、Cookie 可以在服务端生成吗?Cookie 在服务端生成后的工作流程是什么样的?
123、Session、Cookie 的区别和关联?如何进行临时性和永久性的 Session 存储?
124、设置 Cookie 时候如何防止 XSS 攻击?
125、简单描述一下用户免登陆的实现过程?可能会出现哪些安全性问题?一般如何对用户登录的密码进行加密?
126、HTTP 中提升传输速率的方式有哪些?常用的内容编码方式有哪些?
127、传输图片的过程中如果突然中断,如何在恢复后从之前的中断中恢复传输?
128、什么是代理?什么是网关?代理和网关的作用是什么?
129、HTTPS 相比 HTTP 为什么更加安全可靠?
130、什么是对称密钥(共享密钥)加密?什么是非对称密钥(公开密钥)加密?哪个更加安全?
131、你觉得 HTTP 协议目前存在哪些缺点?
性能
133、在 React 中如何识别一个表单项里的表单做到了最小粒度 / 代价的渲染?
134、在 React 的开发的过程中你能想到哪些控制渲染成本的方法?
插件
135、Vue CLI 3.x 的插件系统是如何设计的?
136、Webpack 中的插件机制是如何设计的?
系统
137、\r\n(CRLF) 和 \n (LF)的区别是什么?(Vs Code 的右下角可以切换)
138、/dev/null 的作用是啥?
139、如何在 Mac 的终端中设置一个命令的别名?
140、如何在 Windows 中设置环境变量?
141、Mac 的文件操作系统默认区分文件路径的大小写吗?
142、编写 Shell 脚本时如何设置文件的绝对路径?
后端
143、Session、Cookie 的区别和关联?如何进行临时性和永久性的 Session 存储?
144、如何部署 Node.js 应用?如何处理负载均衡中 Session 的一致性问题?
145、如何提升 Node.js 代码的运行稳定性?
146、GraphQL 与 Restful 的区别,它有什么优点?
147、Vue SSR 的工作原理?Vuex 的数据如何同构渲染?
148、SSR 技术和 SPA 技术的各自的优缺点是什么?
149、如何处理 Node.js 渲染 HTML 压力过大问题?
业务思考
业务思考更多的是结合基础知识的广度和深度进行的具体业务实践,主要包含以下几个方面:
-
工程化:代码部署、CI / CD 流程设计、Jenkins、Gitlab、Docker 等
-
通用性:脚手架、SDK、组件库等框架设计
-
应用框架:Hybrid 混合、微前端、BFF、Monorepo
-
可视化:
-
低代码:通用表单设计、通用布局设计、通用页面设计、JSON Schema 协议设计等
-
测试:E2E 测试、单元测试、测试覆盖率、测试报告等
-
业务:数据、体验、复杂度、监控
工程化
150、你所知道的 CI / CD 工具有哪些?在项目中有接触过类似的流程吗?
151、如果让你实现一个 Web 前端的 CI / CD 工程研发平台,你会如何设计?
152、如果我们需要将已有项目中的线上产物资源(例如图片)转换成本地私有化资源,你有什么解决方案?
153、如何使用 Vue CLI 3.x 定制一个脚手架?比如内部自动集成了 i18n、 axios、Element UI、路由守卫等?
154、Jenkins 如何配合 Node.js 脚本进行 CI / CD 设计?
通用性
155、如果让你设计一个通用的项目脚手架,你会如何设计?一个通用的脚手架一般需要具备哪些能力?
156、如果让你设计一个通用的工具库,你会如何设计?一个通用的工具库一般需要具备哪些能力?
157、假设你自己实现的 React 或 Vue 的组件库要设计演示文档,你会如何设计?设计的文档需要实现哪些功能?
158、在设计工具库包的时候你是如何设计 API 文档的?
应用框架
159、谈谈 Electron、Nw.js、CEF、Flutter 和原生开发的理解?
160、谈谈桌面端应用中 HotFix 的理解?
161、你觉得什么样的场景需要使用微前端框架?
业务
162、什么是单点登录?如何做单点登录?
163、如何做一个项目的国际化方案?
164、如何做一个项目的监控和埋点方案?
165、如何建设项目的稳定性(监控、灰度、错误降级、回滚…)?
166、一般管理后台型的应用需要考虑哪些性能方面的优化?
167、简述一些提升项目体验的案例和技术方案(骨架屏、Loading 处理、缓存、错误降级、请求重试…)?
168、假设需要对页面设计一个水印方案,你会如何设计?
低代码
169、如何设计一个通用的 JSON Schema 协议使其可以动态渲染一个通用的联动表单?
170、一般的低代码平台需要具备哪些能力?
笔试实践
笔试更多的是考验应聘者的逻辑思维能力和代码书写风格,主要包含以下几个方面:
-
正则表达式
-
算法
-
数据结构
-
设计模式
-
框架的部分原理实现
-
TypeScript 语法
-
模板解析
数据结构
171、使用 TypeScript 语法将没有层级的扁平数据转换成树形结构的数据
// 扁平数据
[{
name: ‘文本1’,
parent: null,
id: 1,
}, {
name: ‘文本2’,
id: 2,
parent: 1
}, {
name: ‘文本3’,
parent: 2,
id: 3,
}]
// 树状数据
[{
name: ‘文本1’,
id: 1,
children: [{
name: ‘文本2’,
id: 2,
children: [{
name: ‘文本3’,
id: 3
}]
}]
}]
复制代码
模板解析
172、实现一个简易的模板引擎
const template = ‘嗨,{{ info.name.value }}您好,今天是星期 {{ day.value }}’;
const data = {
info: {
name: {
value: ‘张三’
}
},
day: {
value: ‘三’
}
};
render(template, data); // 嗨,张三您好,今天是星期三
复制代码
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
最后
由于篇幅限制,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!有需要的程序猿(媛)可以帮忙点赞+点击【学习资料】即可免费领取!
应用框架
159、谈谈 Electron、Nw.js、CEF、Flutter 和原生开发的理解?
160、谈谈桌面端应用中 HotFix 的理解?
161、你觉得什么样的场景需要使用微前端框架?
业务
162、什么是单点登录?如何做单点登录?
163、如何做一个项目的国际化方案?
164、如何做一个项目的监控和埋点方案?
165、如何建设项目的稳定性(监控、灰度、错误降级、回滚…)?
166、一般管理后台型的应用需要考虑哪些性能方面的优化?
167、简述一些提升项目体验的案例和技术方案(骨架屏、Loading 处理、缓存、错误降级、请求重试…)?
168、假设需要对页面设计一个水印方案,你会如何设计?
低代码
169、如何设计一个通用的 JSON Schema 协议使其可以动态渲染一个通用的联动表单?
170、一般的低代码平台需要具备哪些能力?
笔试实践
笔试更多的是考验应聘者的逻辑思维能力和代码书写风格,主要包含以下几个方面:
-
正则表达式
-
算法
-
数据结构
-
设计模式
-
框架的部分原理实现
-
TypeScript 语法
-
模板解析
数据结构
171、使用 TypeScript 语法将没有层级的扁平数据转换成树形结构的数据
// 扁平数据
[{
name: ‘文本1’,
parent: null,
id: 1,
}, {
name: ‘文本2’,
id: 2,
parent: 1
}, {
name: ‘文本3’,
parent: 2,
id: 3,
}]
// 树状数据
[{
name: ‘文本1’,
id: 1,
children: [{
name: ‘文本2’,
id: 2,
children: [{
name: ‘文本3’,
id: 3
}]
}]
}]
复制代码
模板解析
172、实现一个简易的模板引擎
const template = ‘嗨,{{ info.name.value }}您好,今天是星期 {{ day.value }}’;
const data = {
info: {
name: {
value: ‘张三’
}
},
day: {
value: ‘三’
}
};
render(template, data); // 嗨,张三您好,今天是星期三
复制代码
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-EwRSMf4X-1712074784509)]
[外链图片转存中…(img-l0dGKb3l-1712074784510)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
[外链图片转存中…(img-rC4azZDN-1712074784510)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
最后
由于篇幅限制,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!有需要的程序猿(媛)可以帮忙点赞+点击【学习资料】即可免费领取!
[外链图片转存中…(img-fyIcAjuC-1712074784511)]
[外链图片转存中…(img-kaLBIsTD-1712074784511)]