归纳面试题

配置 nginx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
listen 80;
server_name localhost;
# 访问 /, 返回静态资源
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
# api转发,(请求后端api,转发到7001端口)
lacation /api/v1 {
proxy_pass http://127.0.0.1:7001;
}
# 报错页面
error_page 500 502 503 504 50x.html
location = /50x.html {
root /usr/share/nginx/html;
}
}

配置 webpack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const path = require("path");

module.exports = {
mode: "none",
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.join(__dirname, "dist"),
publicPath: "",
},
devServer: {
hotOnly: true,
proxy: {
"/api": {
target: "api.example.com",
changeOrigin: true,
pathRewrite: {
"^api": "",
},
},
},
},
modules: {
rules: [
{
test: /.js$/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
},
],
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin(),
new CopyWebpackPlugin(),
],
};

配置 rollup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import json form '@rollup/plugin-json';
import {nodeResolve} from '@rollup/plugin-node-resolve';

export default {
input: 'src/index.js',
output: {
// 如果是多入口或者分包就不能使用file,需要使用dir
file: 'dist/bundle.js',
// dir: 'dist',
// 多入口和分包也不能使用iife,可以使用amd.iife会将文件放在一个函数内
format: 'iife'
// format: 'amd'
},
// 由于roolup默认只支持esm,如果要加载cjs模块需要插件
plugins: [
// 加载json文件
json(),
// 加载cjs文件
nodeResolve()
]
}

React 问题

React.memo useMemo useCallback 都是什么,区别?应用场景?方法?

  • React.memo 是高阶组件,是对整个组件进行包裹.作用类似于 class 组件中的shouldComponentUpdate,对 props 是否变化做浅层比较.
    变化才会重新渲染.如果想深层比较,可以使用它的第二个参数.和shouldComponentUpdate不同的地方是第二个参数的返回值,props 如果相等是返回 true,而shouldComponentUpdate返回的是 false.
1
export default React.memo(MyComponent, areEqual); // 第二个参数areEqual
  • React.memo 是对整个组件进行包裹,useMemo 和 useCallback 是对更细粒度的值或函数进行优化.
  • useMemo 和 useCallback 差异在于返回值,一个是返回的 memorize 值,一个是返回 memorize 函数.
  • useMemo 和 useCallback 需要配合 React.memo,如果不使用 memo,那么每次 prop 整体都是一个新的对象/
  • Vue 会自动去做这件事,React 需要手动去做.

Hook 为啥不能变.不能在前面加判断语句?

要保证 hooks 是在最顶层调用.是为了保证多个 hooks 调用的顺序是一致.
Hooks 最终是构建了一个单项链表,每次执行都是按照同样的顺序被调用,如果放在循环,条件语句或者嵌套函数中肯定会破坏它的顺序型导致问题.

Fiber 算法

Fiber 是一个执行单元,一种数据结构.因为 React16 之前是将虚拟 DOM 树看出一个任务执行,递归处理,任务庞大且不能中断.
React16 之后,将整个任务分成一个个小的任务在空余时间里处理,主要是通过 requestIdleCallback 这个浏览器的 API 去获取浏览器的空余时间,如果有,就执行,没有就让出主线程.

useLayoutEffect,和 useEffect 区别?在哪个阶段执行? beginWork,commit Work

二者的函数签名是相同的,不同点在于触发的时机不同.
useLayoutEffect是同步处理副作用.useEffect 是异步处理.
useLayoutEffect在 DOM 更新后,浏览器渲染之前,useLayoutEffect内部的更新将会同步刷新.
useEffect是在浏览器渲染后.
useLayoutEffect是在commitWork阶段.

Redux 是双向数据绑定吗?

不是,是单向数据流.

redux 和 react-redux 区别

  1. redux 和组件进行对接的时候是直接在组件中进行创建。react-redux 是运用 Provider 将组件和 store 对接,使在 Provider 里的所有组件都能共享 store 里的数据,还要使用 connect 将组件和 react 连接.
  2. redux 获取 state 是直接通过 store.getState()。
    react-redux 获取 state 是通过 mapStateToProps 函数,只要 state 数据变化就能获取最新数据
  3. redux 是使用 dispatch 直接触发,来操作 store 的数据。
    react-redux 是使用 mapDispatchToProps 函数然后在调用 dispatch 进行触发

React vs Vue 区别

  1. 从写法上,react 推荐的是 jsx+内联样式,即all in js.Vue 是 template 的单文件格式,即 html,js,css 写在同一个文件中.
  2. 虚拟 DOM 的区别,主要是 diff 算法的区别
  3. 数据驱动视图.
  • Vue 是类似 MVVM 框架,数据响应式 => 转化 data 属性为 getter/setter =>watcher 实例对象进行依赖收集与监听 =>数据 setter 被调用时,watcher 对比前后两个值是否发生变化 => 通知视图是否进行渲染.
  • React 通过setState实现数据驱动视图. setState 将接收的第一个参数 state 存储在pending队列中,判断当前 React 是否处在批量更新状态,是的话就将需要更新的 state 组件添加到dirtyComponent中,不是的话,就遍历dirtyComponent中的所有组件,调用updateComponent方法更新每个dirty组件.

diff 算法区别

  1. dom 更新策略不同
    react 会自顶而下全 diff,vue 会跟踪每个组件的依赖关系,不需要重新渲染整个组件树.
    react 当状态发生改变会重新 render 页面,生成新的虚拟 dom 树,新旧 dom 树进行比较,进行打补丁的方式,局部更新 dom.
    所以需要手动的对不需要重新渲染的组件或者值进行 memo 优化.
    vue 是通过数据响应式的方式,通过Object.defineProperty把 data 的属性转化为getter/setter.同时进行依赖收集与监听,setter 被调用时进行组件更新.
  2. diff 算法源码实现不同
    vue 采用双端对比的算法,同时从新旧 children 的两端进行比较,借助 key 值找到可复用的节点,再进行相关操作.

react 为什么没有采用双指针?

因为目前 Fiber 上没有设置反向链表,单向链表无法使用双指针,所以无法对算法进行双指针优化.

react 异步组件

原理: 用 componentDidCatch 捕获异步请求,如果有异步请求渲染 fallback,等到异步请求执行完毕,渲染真实组件,借此整个异步流程完毕.

1
2
3
4
5
6
7
8
9
const LazyComponent = React.lazy(() => import("./test.js"));

export default function Index() {
return (
<Suspense fallback={<div>loading...</div>}>
<LazyComponent />
</Suspense>
);
}

setState 是异步还是同步

本质上是批量执行或非批量执行的问题.而且只在 React18 以前存在.
在 react17 中,setState 是批量执行的,因为执行前会设置 executionContext。但如果在 setTimeout、事件监听器等函数里,就不会设置 executionContext 了,这时候 setState 会同步执行。可以在外面包一层 batchUpdates 函数,手动设置下 executionContext 来切换成异步批量执行
在 react18 里面,如果用 createRoot 的 api,就不会有这种问题了,因为所有的 setState 都是异步批量执行了。

webpack

webpack plugin 原理

利用钩子机制,在生命周期的钩子上挂载函数实现扩展.

webpack 中 publicPath 作用?

静态资源的基础路径,一般在生成模式下使用.

tree shaking

作用是移除未引入的代码.
webpack 的 tree-shaking 在 production 下是默认开启的.
tree-shaking 需要依赖 ESModule 模块才能起作用.
在开发环境下可以通过设置optimization: { useExports: true; minimize: true}开启.
这个和babel-loaderpreset-env互斥,但是新版本的已经改为自动判断是否开启.所以不用担心.

Webpack  => sideEffects

可以在optimization中开启标识副作用,在package.json中标识是否有副作用.
也可以在package.json中标记哪些有副作用,不过在生产环境是默认开启的.

1
2
//package.json
sideEffects: ["./src/extends.js", "*.css"];

直接导出构造函数会被 tree-shaking 吗

看导出的构造函数有没有被使用了

webpack 打包体积如何减小?

chunk 分包,import动态导入,提取公共模块.

webpack 优化

在加快构建时间方面,作用最大的是配置 cache,可大大加快二次构建速度。

在减小打包体积方面,作用最大的是压缩代码、分离重复代码、Tree Shaking,可最大幅度减小打包体积。

在加快加载速度方面,按需加载、浏览器缓存、CDN 效果都很显著。

配置持久化 cache

1
2
3
cache: {
type: "filesystem"
}

压缩代码:

  • 压缩 js 代码:在 production 模式默认开启压缩.可以在 optimization 的 minimizer 中进行配置
  • 压缩 html 代码: 在 HtmlWebpackPlugin 中 minify 开启压缩.

打包的代码加上 hash,实现缓存文件
开启 tree Shaking,移除未引用代码(production 默认开启)
开启方法: 在 package.json 配置 sideEffects: false(表示都没有副作用,可以清除所有未引用代码),如果有,写个数组[‘*.css’],一般都是 css 文件
按照路由拆分代码,实现按需加载

多页应用实现多入口打包,设置多个 entry 入口,htmlWebpackPlugin 也是多个
提取公共模块,也就是分离代码,在 optimization 中设置 splitChunks,

图片压缩

  • 使用雪碧图,字体图标, 5k 一下使用 base64 编码
    使用 cdn

使用预加载
script 标签中 rel=”preload”

  • DNS 预解析 dns-prefetch
  • 资源预加载 preload
  • 预渲染 prerender

减少重排重绘

  • 不使用 table 布局
  • css 属性读写分离
  • 样式批量修改
  • 减少 dom 深度
  • 将没用的元素设置为不可见 visibility: hidden

polyfill 按需加载 babel 典型配置

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
presets: [
[
"@babel/preset-env",
{
target: {}, // 适配哪些浏览器,可以写在.browserlistrc
corejs: 3, // corejs版本
useBuiltIns: "usage", // 按需加载usage,完全加载entry,
modules: false, // 是否将es6转为其他
},
],
],
};

postcss 什么作用?

处理 css 兼容性的工具,一般有多种工具,比如autoprefixer处理不同浏览器的前缀.

sass-loader 用了什么方法

1
2
3
4
5
6
7
8
9
10
const nodeSass = require("node-sass");
const path = require("path");

let result = nodeSass.renderSync({
file: path.resolve(__dirname, "../src/scss/index.scss"),
outputStyle: "expanded",
});
module.exports = function () {
return result.css.toString();
};

可以看到,sass-loader 使用了 node-sass 解析 sass 文件导出为 css 文件.输出为字符串交给下一个 loader.

Esm 是什么?

es6 module 是一种模块化开发的规范.设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系和导入导出的变量.
主要功能由两个命令构成: export, import.

  • 只能作为模块顶层的语句出现
  • import 的模块名只能是字符串常量
  • import 之后是不可修改的 例如,在使用 CommonJS 时,必须导入完整的工具 (tool) 或库 (library) 对象,且可带有条件判断来决定是否导入。

Umi 了解吗

Element-ant 之类的是怎么实现响应式布局?

参照了 Bootstrap 的 响应式设计,预设了五个响应尺寸:xs、sm、md、lg 和 xl。
也就是通过类名.
在 less 或者 scss 文件中设置变量.媒体查询通过断点设置 css.

Vue

Vue 对数组是怎么处理的

  1. 先获取原生 Array 的原型方法,因为拦截后还是需要原生的方法帮我们实现数组的变化。
  2. 对 Array 的原型方法使用 Object.defineProperty 做一些拦截操作。
  3. 把需要被拦截的 Array 类型的数据原型指向改造后原型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { def } from "../util/index";

const arrayProto = Array.prototype; // 这是个空数组继承了Array的所有方法
export const arrayMethods = Object.create(arrayProto); // 这是个对象继承了所有Array的方法

const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];

methodsToPatch.forEach(function (method) {
// 缓存原来的方法
const original = arrayProto[method]; // 原来Array 的方法
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args); // 调用还是用原来Array的方法调用
const ob = this.__ob__;
let inserted;
// 判断传入的方法,进行修改args,push,unshift只接收一种参数,就把args赋值给inserted
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
// splice是接收3个参数,实际要处理的参数就是第3个往后的
case "splice":
inserted = args.slice(2);
break;
}
if (inserted) ob.observeArray(inserted);
// notify change
ob.dep.notify();
return result;
});
});

$set 为什么可以检测数组变化?

如果 target 是一个数组且索引有效,就设置 length 的属性。
通过 splice 方法把 value 设置到 target 数组指定位置。
设置的时候,vue 会拦截到 target 发生变化,然后把新增的 value 也变成响应式
最后返回 value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function set(target, key, val) {
//...
// target是传入数组或对象,key是索引,val是修改的值
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val;
}
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
//...
defineReactive$$1(ob.value, key, val);
ob.dep.notify();
return val;
}

$nextTick 的作用和原理

Vue 中 DOM 更新是异步的,批量的.在修改 DOM 后立即获取,是获取不到的,需要等待 dom 更新后才能获取.
tis.$nextTick 接收一个函数,函数会在下一次循环 DOM 渲染完毕后执行,在这里也就得到了最新的值.

mutationObserver 了解吗

属于微任务,观察 dom 元素,并在检测到更改时触发回调.

1
2
3
4
5
6
// 通过new创建实例,接收一个回调
// 回调的两个参数,一个是包含了MutationRecord 的数组,一个是观察者对象本身
const observer = new MutationObserver((mutationRecords, observer) => {});
// target是观察目标,options 监听哪方面的内容
observer.observe(target, options);
// 主要是在回调中处理,当观察元素或者子元素变化,就做出相应处理

父子组件的生命周期执行顺序?

执行顺序:父组件先创建,然后子组件创建;子组件先挂载,然后父组件挂载,
否则父组件先挂载就是结束了,子组件没地方挂载了.
大体流程: 先父后字,再先子后父.界限在挂载那里改为先子后父,前面的都是先父后子.

渲染阶段
父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted
卸载阶段
父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed

Http

axios 和 fetch 区别

  1. 传递数据方式.axios 放在 data 中,fetch 放在 body 中并且是 string 形式.
  2. 设置超时.axios 设置 timeout 即可,fetch 需要使用AbortController属性设置.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const url = "https://jsonplaceholder.typicode.com/todos";

const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => controller.abort(), 4000);

fetch(url, {
signal: signal,
})
.then((response) => response.json())
.then(console.log)
.catch((err) => {
console.error(err.message);
});
  1. 数据转化. 对于返回类型,axios 可以自动转化,默认是 json 类型.fetch 需要清楚后端传过来是什么类型,并手动转化.
  2. 拦截器.axios 自带,fetch 需要手动封装.
1
2
3
4
5
6
// axios
axios.interceptors.request.use((response) => {
console.log(response.data);
});
// fetch手动封装
fetch = () => {};
  1. 并发请求. 都支持.
1
2
3
4
5
6
7
8
9
10
axios.all([axios.get("..."), axios.get("...")]).then(
axios.spread((obj1, obj2) => {
//...
})
);
// fetch
Promise.all([fetch("..."), fetch("...")]).then(async ([res1, res2]) => {
const a = await res1.json();
const b = await res2.json();
});
  1. 使用 fetch 封装,注意和 axios 的区别,无法捕获非网络异常导致的 error.必须手动 Promise.reject()抛出.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const url = "https://jsonplaceholder.typicode.com/todos";

fetch(url)
.then((response) => {
if (!response.ok) {
throw new Error(
`This is an HTTP error: The status is ${response.status}`
);
}
return response.json();
})
.then(console.log)
.catch((err) => {
console.log(err.message);
});

interceptor 原理

1、当使用 axios 时,Axios 被实例化,并内部分别实例化 request、response 拦截器
2、当使用 use 添加拦截器时,触发 InterceptorManager.prototype.use 方法,将传过来的 resolve、reject 回调,添加在 this.handlers[]中
3、使用 axios.get(‘https://xxxx‘) 时,触发 Axios.prototype.request ,内部触发 InterceptorManager.prototype.forEach
3.1 request 拦截器 和 response 拦截器 触发 Axios.prototype.request 内方法不一样 分别是:this.interceptors.request.forEach、this.interceptors.response.forEach 二者主要区别在于想队列 chain[] 中添加的位置不同,request 拦截器从头开始添加 和 response 拦截器从尾开始添加,而 chain[]本身就含有 xhr 的 promise 调用返回
3.2 从而实现:先触发 request 拦截器=>发请求,请求返回=>触发 response 拦截器

  1. Axios.prototype.request 最后 return promise,就是我们最后使用的 axios.get(‘’)这个方法的原样,所有 axios.get(‘’).then(res)会再被调用,res 就是请求返回的数据被 response 拦截器处理后的最终结果了

fetch 的 interceptor 原理

拦截器的原理很简单,原生 fetch,接受两个参数,一个是请求路径 resource,resource 可以是 url 也可以是 request 对象,另外一个参数是 config,可以配置 method,header 等请求参数。而拦截器则是通过替换 window.fetch,在其外面再套一层处理起 request 以及 response。

1
2
3
4
5
6
7
8
9
10
function initFetchInterceptor() {
const { fetch: originalFetch } = window;

window.fetch = async (...args) => {
const [resource, config] = args;
const response = await originalFetch(resource, config);

return response;
};
}

request 拦截比较简单,就是直接获取 resource 以及 config 进行处理,替换为自己想要的参数再发起请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// request拦截
function initFetchInterceptor() {
const { fetch: originalFetch } = window;

window.fetch = async (...args) => {
// resource => https://api.github.com?name=meadery
const [resource, config] = args;
// 把resouce替换
const _reource = "https://api.github.com?name=meadery";
const response = await originalFetch(_reource, config);

return response;
};
}

response 拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const btn = document.getElementById("btn");

function initFetchInterceptor() {
const { fetch: originalFetch } = window;

window.fetch = async (...args) => {
const [resource, config] = args;
console.log("resource: ", resource);
console.log("config: ", config);

const response = await originalFetch(resource, config);
console.log("response: ", response);
const json = () =>
response
.clone()
.json()
.then(() => "hello interceptor");
response.json = json;
return response;
};
}

initFetchInterceptor();

btn.addEventListener("click", async function () {
const response = await fetch("https://api.github.com", {
method: "GET",
});
response.json().then((data) => {
console.log("data: ", data);
});
console.log("response: ", response);
});

CORS 的请求头

CORS 是跨域资源共享.
当浏览器发现域是不同的,会向服务端发送一个 options 请求,检查请求是否允许.
前端可以在该方法中添加Access-Control-Request-Methods,表头中提供一些信息,真实请求何时到来,数据类型是什么.
服务端响应发回Access-Control-Allow-Origin,返回允许的地址.

ES6

Map,set, weakMap,weakSet 区别?

Set 是类数组的数据结构,内部存储的值并不重复.
weakSet 的成员只能是对象类型,而且是弱引用,就是如果没有对象引用内部的对象,那就可能被 GC,所以不能遍历.
Map 是键值对的组合,和 Object 的区别是 key 可以是任意类型,而对象只能是 string 和 symbol.
weakMap 类似于 Map,不过只接受对象做 key.
弱引用是所引用的对象的其他引用被清除,垃圾回收就会自动释放该对象占用的内存.(就是没被人用了,就清除了)

Proxy 和 Reflect?

Proxy 和 Reflect 是 ES6 推出的新特性.
Prxoy 可以代理对象的属性,内部有陷阱函数可以对代理的对象进行操作.通过 Rflect 可以将代理对象的方法返回.

1
2
3
4
5
6
7
8
9
10
11
12
const obj = {
name: 'Tom'
}

const handler = () => {
get(target, key, receiver) {
console.log(key,'key')
return Reflect.get(target, key, receiver)
}
}

const proxy = new Proxy(obj, handler)

receiver 表示代理对象或者继承对象

Promise 并发控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//自定义请求函数
var request = (url) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`任务${url}完成`);
}, 1000);
}).then((res) => {
console.log("外部逻辑", res);
});
};
// 执行任务
async function fn() {
let urls = [
"bytedance.com",
"tencent.com",
"alibaba.com",
"microsoft.com",
"apple.com",
"hulu.com",
"amazon.com",
]; // 请求地址
let pool = []; //并发池
let max = 3; //最大并发量
for (let i = 0; i < urls.length; i++) {
let url = urls[i];
let task = request(url);
task.then((data) => {
//每当并发池跑完一个任务,从并发池删除个任务
pool.splice(pool.indexOf(task), 1);
console.log(`${url} 结束,当前并发数:${pool.length}`);
});
// 异步队列等待这个同步任务把pool塞满
pool.push(task);
// 当pool满后,停止循环,执行异步队列中的task,继续循环
if (pool.length === max) {
//利用Promise.race方法来获得并发池中某任务完成的信号
//跟await结合当有任务完成才让程序继续执行,让循环把并发池塞满
console.log("停止循环");
await Promise.race(pool);
}
}
}
fn();

Node

node 写过 cli 吗

简历项目

在项目中发现首屏加载时间 xx 秒,有很大优化空间,经过研究采用 xx 方案优化到了 xx 秒.提升了 60%性能.

借鉴 xx 项目中优秀的方案,讲解

竟可能多说解决方案