这是在准备面试过程中汇总的一些觉得平时也会有用的知识点,用于作为一个反思帖,希望日后能够定时更新,并且增加思考深度和个人理解。
🍉:暂未写出答案。
🍊:涉及算法和手写代码。
🔔:笔者在面试过程中真实遇到的题目
下篇传送门:➡️全栈大体系(工程化+浏览器+网络+安全+Node)
目录
- 手写代码
- Javascript基础
- ES6
- React
- HTML+CSS
- 前端工程化(在下篇)
- 浏览器+网络+通信+安全(在下篇)
- NodeJS(在下篇)
- 其他(在下篇)
手写代码
🍊实现一个promise(then resolve reject即可)
- 简版promise(只支持resolve reject和then,不带链式调用)
class PromiseSimple {
callbackList = []
rejectList = []
constructor(fn) {
fn(this._resolve.bind(this), this._reject.bind(this))
}
then(onFulfilled) {
this.callbackList.push(onFulfilled)
}
catch(onFulfilled) {
this.rejectList.push(onFulfilled)
}
_resolve(value){
setTimeout(() => this.callbackList.forEach(func => func(value)))
}
_reject(value){
setTimeout(() => this.rejectList.forEach(func => func(value)))
}
}
- 完整版promise(支持链式调用)
function Promise(excutor) {
var self = this
self.onResolvedCallback = []
function resolve(value) {
setTimeout(() => {
self.data = value
self.onResolvedCallback.forEach(callback => callback(value))
})
}
excutor(resolve.bind(self))
}
Promise.prototype.then = function(onResolved) {
var self = this
return new Promise(resolve => {
self.onResolvedCallback.push(function() {
var result = onResolved(self.data)
if (result instanceof Promise) {
result.then(resolve)
} else {
resolve(result)
}
})
})
}
🍊手写发布订阅模式 Node EventEmitter(emit,on,off,once)
class EventEmit {
constructor() {
this.eventMap = {}
}
on(name, cb) {
if (!this.eventMap[name]) {
this.eventMap[name] = [cb]
} else {
this.eventMap[name].push(cb)
}
}
off(name, cb) {
if (this.eventMap[name]) {
const index = this.eventMap[name].indexOf(cb)
this.eventMap[name].splice(index, 1)
}
}
trigger(name, ...args) {
if (this.eventMap[name] && this.eventMap[name].length) {
this.eventMap[name].forEach(cb => {
cb(args)
})
}
}
once(name, fn) {
if (!this.eventMap[name]) {
this.on(name, (...args) => {
fn(args)
this.off(name, fn)
})
}
}
}
🍊手写debounce和throttle
// 防抖动:在一定时间间隔m后没有继续触发的话再执行,如果一直触发,则会一直清空计时器重新计时。一般用于resize或者input的onChange事件
function debounce(fn) {
let timeout = null;
return function() {
clearTimeout(timeout);
timeout = setTimeout(() => {
fn.call(this, arguments);
}, 1000);
};
}
// 节流:触发的话就执行,但是在规定时间间隔m内最多只执行一次,如果距离上次执行间隔不到m,则不调用
function throttle(fn) {
let canRun = true;
return function() {
if(!canRun) {
return;
}
canRun = false;
setTimeout( () => {
fn.call(this, arguments);
canRun = true;
}, 1000);
};
}
🍊使用promise实现限制最大并发数
/*
限制最大并发数
在 // ... 处填写代码,可以输出结果为 2,1,4,3
*/
const ConcurrentCount = 2 // 最大并发数
class Task {
constructor () {
this.count = 0
this.taskList = []
}
addTask(promiseCreate) {
const taskMeta = (promise) => {
this.count ++;
if(this.count <= ConcurrentCount) {
promise().then((rs) => {
console.log(rs)
}).finally(() => {
this.count--
if (this.taskList.length > 0) {
this.count--;
taskMeta(this.taskList.shift())
}
})
} else {
this.taskList.push(promise)
}
}
taskMeta(promiseCreate)
}
}
let timeOut = ((log, n) => {
return () => {
return new Promise((rs) => {
setTimeout(() => {
rs(log)
}, n)
})
}
})
const task = new Task()
task.addTask(timeOut('1', 1000))
task.addTask(timeOut('2', 300))
task.addTask(timeOut('3', 1200))
task.addTask(timeOut('4', 200))
🍊手写promise.all
Promise.all = function(arr){
return new Promise((resolve,reject) => {
if(!Array.isArray(arr)){
throw new TypeError(`argument must be a array`)
}
if (arr.length === 0 ) return Promise.resolve([]);
var length = arr.length;
var resolveNum = 0;
var resolveResult = [];
for(let i = 0; i < length; i++){
arr[i].then(data => {
resolveNum++;
resolveResult.push(data)
if(resolveNum == length){
return resolve(resolveResult)
}
}).catch(data => {
return reject(data)
})
}
})
}
🍊手写promise.retry
Promise.retry = function(fn, times, delay) {
return new Promise(function(resolve, reject){
var error;
var attempt = function() {
if (times == 0) {
reject(error);
} else {
fn().then(resolve)
.catch(function(e){
times--;
error = e;
setTimeout(function(){attempt()}, delay);
});
}
};
attempt();
});
};
🍊sleep函数
function sleep(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(true);
}, time);
});
}
🍊将一个同步callback包装成promise形式
nodeGet(param, function (err, data) { })
// 转化成promise形式
function nodeGetAysnc(param) {
return new Promise((resolve, reject) => {
nodeGet(param, function (err, data) {
if (err !== null) return reject(err)
resolve(data)
})
})}
🍊手写一个promiseify包装器
function promisify(fn,context){
return (...args) => {
return new Promise((resolve,reject) => {
fn.apply(context,[...args,(err,res) => {
return err ? reject(err) : resolve(res)
}])
})
}
}
🍊深拷贝实现方式及原理分析
function deepClone(obj) {
var result = Array.isArray(obj) ? [] : {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'object' && obj[key]!==null) {
result[key] = deepClone(obj[key]);
} else {
result[key] = obj[key];
}
}
}
return result;
}
🔔🍊函数currying
- 柯里化其实是函数式编程的一个过程,为实现多参函数提供了一个递归降解的实现思路——把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数
- 使用场景:
- 编写小模块的代码,可以更轻松的重用和配置
- 避免频繁调用具有相同参数的函数
- 延迟执行
- 一个简单的实现
function curry (fn) {
const c = (...args) => (args.length === fn.length) ?
fn(...args) : (..._args) => c(...args, ..._args)
return c
}
快排
var sortArray = function(nums) {
const quicksort = (arr, begin, end) => {
if (begin >= end) { return }
let left = begin, right = end
while(left < right) {
while (arr[right] >= arr[begin] && left < right) { right-- }
while (arr[left] <= arr[begin] && left < right) { left++ }
[arr[left], arr[right]] = [arr[right], arr[left]]
}
[arr[begin], arr[right]] = [arr[right], arr[begin]]
quicksort(arr, begin, right - 1)
quicksort(arr, right + 1, end)
}
quicksort(nums, 0, nums.length - 1)
return nums
};
归并
function mergeSort(arr) { //采用自上而下的递归方法
var len = arr.length;
if(len < 2) {
return arr;
}
var middle = Math.floor(len / 2),
left = arr.slice(0, middle),
right = arr.slice(middle);
return merge(mergeSort(left), mergeSort(right));
}
function merge(left, right)
{
var result = [];
console.time('归并排序耗时');
while (left.length && right.length) {
if (left[0] <= right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}
while (left.length)
result.push(left.shift());
while (right.length)
result.push(right.shift());
console.timeEnd('归并排序耗时');
return result;
}
反转链表
var reverseList = function(head) {
if (!head || !head.next) return head;
var pre = null
while (head) {
const next = head.next;
head.next = pre
pre = head
head = next
}
return pre
};
🔔🍊如何把数组打平
- flat():需要指定层数
- 常规递归
- reduce + concat
function fn(arr) {
return arr.reduce((sum, n) => sum.concat(Array.isArray(n) ? fn(n) : n), [])
}
数组乱序
// 著名的Fisher–Yates shuffle 洗牌算法
function shuffle(arr){
let m = arr.length;
while(m > 1){
let index = parseInt(Math.random() * m--);
[arr[index],arr[m]] = [arr[m],arr[index]];
}
return arr;
}
数组去重
// ES5
function removeDup(arr){
var result = [];
var hashMap = {};
for(var i = 0; i < arr.length; i++){
var temp = arr[i]
if(!hashMap[temp]){
hashMap[temp] = true
result.push(temp)
}
}
return result;
}
// ES6
[...new Set(arr)]
🍉扩展数组 range(3) => [1,2,3]
字符串模板的实现
function templateString(template, target) {
return template.replace(/\$\{([^\}]*?)\}/g, ($0, $1) => {
return target[$1.replace(/\s/g, '')]
})
}
🔔🍊实现获取cookie
function getCookie(name) { //获取指定名称的cookie值
// (^| )name=([^;]*)(;|$),match[0]为与整个正则表达式匹配的字符串,match[i]为正则表达式捕获数组相匹配的数组;
var arr = document.cookie.match(new RegExp("(^| )"+name+"=([^;]*)(;|$)"));
if(arr != null) {
console.log(arr);
return unescape(arr[2]);
}
return null;
}
var cookieData=getCookie('token'); //cookie赋值给变量。复制代
🍉链表
🍉二叉树
🍉队列
Javascript
🔔关于类型检测
- typeof:只能检测除了null之外的基础类型和引用类型里的function,null和object和array 都会被检测为object
- instanceof:原理是基于原型链的查询,所以检测的比较全,但是也检测不出准确类型
Object.prototype.toString.call()可以检测类型!!- 如何是不是NaN?
- Number.isNaN(value);
- ES6的Object.is(NaN, NaN)
JS隐式类型转换
- 遇到 - * % / 运算符时,转化为number类型做运算
- +比较特殊,存在转为number和string两种情况
- 数字相加:数字和一个非字符串的类型相加,会转为number之后进行:1+true == 2
- 字符串拼接:字符串和其余类型相加,会转为string再进行,对于对象,会调用valueOf()/toString()转为字符串后,再进行拼接
- isNaN() 自带类型转化,会转为number再比较
- 为假的类型只有这些:""、0、false、null、undefined、NaN、[] [] == false "" === false ?
== 和 === 的区别
- ===全等号比较值的同时还会比较类型
- ==会进行类型转化后再做比较,具体转化方式如下⬇️
- Boolean == ?, 会转为Number比较。
- Number == String,会把 String 通过 Number() 方法转换为数字比较。
- Boolean == String,会转为数字
- Number == Object,会调用 valueOf 方法将Object转换为数字
🔔🍊实现instanceof
Instanceof运算符的第一个变量是一个对象,暂时称为A;第二个变量一般是一个函数,暂时称为B。沿着A的__proto__这条线来找,同时沿着B的prototype这条线来找,如果两条线能找到同一个引用,即同一个对象,那么就返回true。如果找到终点还未重合,则返回false。
function new_instance_of(leftVaule, rightVaule) {
let rightProto = rightVaule.prototype; // 取右表达式的 prototype 值
leftVaule = leftVaule.__proto__; // 取左表达式的__proto__值
while (true) {
if (leftVaule === null) {
return false;
}
if (leftVaule === rightProto) {
return true;
}
leftVaule = leftVaule.__proto__
}
}
🔔关于闭包
- 定义:闭包是指有权访问另外一个函数作用域中的变量的函数
- 使用方式:
- 返回一个函数
- 作为函数参数传递
- 在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。
- IIFE(立即执行函数表达式)创建闭包,保存了全局作用域window和当前函数的作用域。
🔔关于this
- this是什么:this就是一个对象。不同情况下 this指向的不同
- this指向的不同情况:
- 对象调用,this 指向该对象(前边谁调用 this 就指向谁 obj.say()
- 直接调用的函数,this指向的是全局 window对象 say()
- 通过new的方式,this永远指向新创建的对象。
- 箭头函数中的this。由于箭头函数没有单独的this值。箭头函数的 this与声明所在的上下文相同。也就是说调用箭头函数的时候,不会隐式地调用this参数,而是从定义时的函数继承上下文。
🔔为什么箭头函数可以更改this指向
- 箭头函数会默认帮我们绑定外层this的值,所以在箭头函数中this的值和外层的this是一样的。
🔔关于原型链
- 每个构造函数(constructor)都有一个原型对象(prototype),原型对象都包含一个指向构造函数的指针,而实例(instance)都包含一个指向原型对象的内部指针
- 如果试图引用对象(实例instance)的某个属性,会首先在对象内部寻找该属性,直至找不到,然后才在该对象的原型(instance.prototype)里去找这个属性
- 实例对象的__proto__是它的构造函数的prototype
三个关键概念:
- prototype:无论什么时候,只要创建了一个新的函数,就会根据一个特定的规则为该函数创建一个prototype属性,只有函数才有prototype属性
- proto:所有对象都包含一个__proto__属性指向它的构造函数的prototype
- constructor:所有对象都包含constructor属性,这个属性包含一个指向prototype属性所在函数的指针
🔔🍊实现继承的几种方式(ES5/ES6)
ES5:借用构造函数
function Animal(type) {
this.type = type;
}
function Duck() {
//继承Animal
Animal.call(this,'duck');
}
const duck = new Duck();
console.log(duck.type);//duck
- 优点
- 保证了原型链中引用类型值的独立,不再被所有实例共享
- 子类型创建时也能够向父类型传递参数
- 缺点
- 不能继承父类型原型链上的属性
ES5:原型链继承
function Animal(type) {
this.type = type;
this.color = ['yellow'];
}
Animal.prototype.walk = function () {
console.log('walk...');
}
function Duck() {
this.type = 'duck';
}
Duck.prototype = new Animal();
- 优点:可以继承父类原型上的属性和方法
- 缺点:当某一个实例修改原型链上某一个属性时,如果实例类型是引用类型,那么其它实例的属性也会被修改。
ES5:组合继承
function Animal(type) {
this.type = type;
this.color = ['yellow'];
}
Animal.prototype.walk = function () {
console.log('walk...');
}
function Duck() {
Animal.call(this, 'duck');
}
Duck.prototype = Object.create(Animal.prototype);
Duck.prototype.constructor = Duck;
- 使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承.
🔔🍊ES6 继承
// ES6
class Parent {
constructor(name,age){
this.name = name;
this.age = age;
}
}
class Child extends Parents{
constructor(name,age,sex){
super(name,age);
this.sex = sex; // 必须先调用super,才能使用this
}
}
🔔ES5的继承和ES6 的继承有什么区别,除了语法糖之外还有别的区别吗?
- ES5的继承会先创建子构造函数的实例,然后通过call继承父类的属性
- ES6的继承是先新建父构造函数的this,再通过子构造函数修饰this,使父类的行为可继承
Object.create()、new Object()和{}的区别
- new Object()和{}无区别,都是新建一个空对象
- Object.create(A),是新建一个—__proto__指向原型A的对象
🔔异步有哪些场景?Js实现异步的具体方式:
- 同步队列代码一次执行
- 异步函数到了指定时间放到异步队列里
- 同步执行完毕,异步轮询执行(1宏任务->n微任务)
new
new操作符做了什么
- 创建一个空对象 var obj = new Object()
- 设置原型链:obj.proto = Object.prototype;
- 改变this指向,指向创建出得对象
- 运行构造函数,返回创建的对象
🍊手写一个new
//Parent 构造函数
function newFun(Parent){
var obj = {}; // 首先创建一个对象
obj.__proto__ = Parent.prototype; // 然后将该对象的__proto__属性指向构造函数的protoType
var result = Parent.call(obj) // 执行构造函数的方法,将obj作为this传入
return typeof(result) == 'object' ? result : obj
}
🔔什么是伪数组?有哪些?如何将伪数组转化为标准数组?
- 什么是伪数组
- 具有length属性
- 能够使用数组遍历方法遍历它们
- 不具有数组的push,pop等方法
- 如何判断:Array.isArray()
- 举个例子
- DOM选择器
- arguments对象
- 怎么转换成正常的数组
[].slice.call(伪数组)Array.form(伪数组)[...伪数组]
🔔🍉requestAnimationFrame的作用及使用,替代setTimeout的写法
for of 和 for in区别
- for...in 循环实际是为循环可枚举(enumerable)对象而设计的【不推荐用for...in来循环一个数组,因为,不像对象,数组的 index跟普通的对象属性不一样,是重要的数值序列指标。】
- for of 用来遍历迭代器对象(数组、set、map、string、伪数组、generator)【一个数据结构只要具有 Symbol.iterator 属性,就可以认为是"可遍历的"(iterable)】
🍉defineProperty 可以设置哪些属性
🔔数组最快取最大值
Math.max.apply(null, arr)或者````Math.max(...arr)```
🔔宏任务和微任务
- 宏任务:整体代码,setInterval,setTimeout
- 微任务:promise,process.newTick,
- JavaScript是单线程语言,所以只有一个执行队列
- 主线程用于执行同步任务,一旦队列为空,就去执行Event Query中的任务
- 任务执行的顺序是:执行一个宏任务后,执行队列中所有的微任务,然后循环
🔔浏览器的EventLoop
- 一开始整段脚本在主线程中执行
- 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
- 当前宏任务执行完出队,检查微任务队列,如果有则依次执行,直到微任务队列为空
- 执行浏览器 UI 线程的渲染工作
- 检查是否有Web worker任务,有则执行
- 执行队首新的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空
注意:
- 在浏览器页面中可以认为初始执行线程中没有代码,每一个script标签中的代码是一个独立的task,即会执行完前面的script中创建的microtask再执行后面的script中的同步代码。
- 如果microtask一直被添加,则会继续执行microtask,“卡死”macrotask
Node和浏览器的EventLoop区别
- 在Node版本<11的时候,处理方式是:先执行完所有宏任务,再执行队列里的所有微任务
- Node版本 >=11的时候,和浏览器的处理方式一样:执行完一个宏任务,执行微任务队列的所有任务,再去执行下一个宏任务
Node11之前的处理方式如下:
在node中事件每一轮循环按照顺序分为6个阶段:
- timers:执行满足条件的setTimeout、setInterval回调。
- I/O callbacks:是否有已完成的I/O操作的回调函数,来自上一轮的poll残留。
- idle,prepare:可忽略
- poll:等待还没完成的I/O事件,会因timers和超时时间等结束等待。
- check:执行setImmediate的回调。
- close callbacks:关闭所有的closing handles,一些onclose事件。
循环中的几个阶段的执行队列也分别称为Timers Queue、I/O Queue、Check Queue、Close Queue 在进入第一次循环之前,会先进行如下操作:
- 同步任务
- 发出异步请求
- 规划定时器生效的时间
- 执行process.nextTick()
按照我们的循环的6个阶段依次执行,每次拿出当前阶段中的全部任务执行,清空NextTick Queue,清空Microtask Queue。再执行下一阶段,全部6个阶段执行完毕后,进入下轮循环。即:
- 清空当前循环内的Timers Queue,清空NextTick Queue,清空Microtask Queue。
- 清空当前循环内的I/O Queue,清空NextTick Queue,清空Microtask Queue。
- 清空当前循环内的Check Queu,清空NextTick Queue,清空Microtask Queue。
- 清空当前循环内的Close Queu,清空NextTick Queue,清空Microtask Queue。
- 进入下轮循环。
注意:
- 如果在timers阶段执行时创建了setImmediate则会在此轮循环的check阶段执行,如果在timers阶段创建了setTimeout,由于timers已取出完毕,则会进入下轮循环,check阶段创建timers任务同理。
- setTimeout优先级比setImmediate高,但是由于setTimeout(fn,0)的真正延迟不可能完全为0秒,可能出现先创建的setTimeout(fn,0)而比setImmediate的回调后执行的情况。
V8的垃圾回收机制
- 在V8中,主要将内存分为新生代和老生代两代。新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。
- 主要有两种回收机制,计数回收算法,标记清除算法,复制算法(新生代中采用的,from和to两个内存空间交替运行)
ES6
🔔数组方法
- find():为了解决indexOf()识别不出NaN的问题
- copyWithin()
- includes()
- fill()
- flat()
- ES5旧的方法:filter()、reduce()...
🔔🍉Set用过吗
🔔🍉Map的使用场景
🔔promise、async await、Generator的区别
- async是generator的语法糖
- async用来解决promise的then调用链长的问题
🔔🍉async里面有多个await请求,可以怎么优化?
React
🍉Why React? Why not Vue?
🍉Vue、react 双向数据绑定原理及区别
为什么使用虚拟DOM
- 优点:
- 基本的性能优化:减少DOM操作,因为频繁的DOM操作会造成浏览器回流和重绘。通过虚拟DOM,尽可能将差异的部分一次性更新
- 无需直接操作DOM,提高开发效率
- 支持跨平台,如Node没有DOM,如果想实现SSR服务端渲染,只能借助虚拟DOM,因为虚拟DOM本身就是js对象
- 缺点:
- 无法进行极致优化:在一些性能要求极高的应用中虚拟DOM无法进行针对性的极致优化,比如VScode采用直接手动操作DOM的方式进行极端的性能优化
虚拟DOM实现原理,看过源码吗?
- 虚拟DOM本质上是JavaScript对象,是对真实DOM的抽象
- 状态变更时,记录新树和旧树的差异
- 最后把差异更新到真正的dom中
Diff算法
React在传统diff算法的基础上做了优化,结合虚拟DOM是它的性能提升的一大亮点,分别对下面几个diff进行了优化
🔔diff 策略(diff复杂度如何从O(n^3)下降到O(n))
- tree diff:一棵树只会对同一层次的节点进行比较,发生变化直接删除下面所有子节点
- component diff:
- 如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。
- 如果是不同类型的组件,直接替换
- 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。
- element diff:设置唯一key
🔔🍉在列表末尾插入节点、和在列表开头插入节点 diff会怎么处理?
🔔🍉不能用index做key的原因
setState到底是异步还是同步?
setState可以同步可以异步——详细讲解
- 同步:原生事件、setTimeout
- 异步:合成事件(onClick)、钩子函数
- 批量更新:在异步时,如果对同一个值进行多次setState,取最后一次执行
- 在setTimeout等地方中进入eventLoop后、不管在哪里使用的,都会统一变成同步调用,且不会进行批量更新
- setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数setState(partialState, callback)中的callback拿到更新后的结果。
- setState的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout中不会批量更新,在“异步”中如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState多个不同的值,在更新时会对其进行合并批量更新。
🔔React有哪些性能优化方式
- 避免不必要的重新渲染:React.memo()、PureComponent(对传递给组件的props的浅比较)
- 16之前 shouldComponentUpdate
- input框等受控组件,不用双向绑定的情况下没有必要存为state,存为私有属性即可。或者进行防抖
- 组件之间的组合方式应该减少硬编码,使用this.props.children来降低耦合性。因为现在存在很长的链式组件传输
- 使用immutable状态
- redux 合并dispatch => reudx-batched-actions
- reselect
- 容器组件避免复杂逻辑,尽量不写dom节点,将子组件作为pure来去处理复杂逻辑控制render防止重刷
React如何进行组件/逻辑复用?——详细讲解
- 高阶组件HOC
- 属性代理(Props Proxy): HOC 对传给 WrappedComponent W 的 porps 进行操作
- 反向继承(Inheritance Inversion): HOC 继承 WrappedComponent W。
- mixin已被弃用(HOC相当于升级版的mixin)
- 渲染属性(render props)
- react hooks
- 优缺点:
- HOC相比Mixin的优势:
- HOC通过外层组件通过Props影响内层组件的状态,而不是直接改变其State不存在冲突和互相干扰,这就降低了耦合度
- 不同于 Mixin 的打平+合并,HOC具有天然的层级结构(组件树结构),这又降低了复杂度
- HOC的缺陷:
- 扩展性限制: HOC无法从外部访问子组件的State,因此无法通过shouldComponentUpdate滤掉不必要的更新,React 在支持 ES6 Class 之后提供了React.PureComponent来解决这个问题
- Ref 传递问题: Ref 被隔断,后来的React.forwardRef 来解决这个问题
- Wrapper Hell: HOC可能出现多层包裹组件的情况,多层抽象同样增加了复杂度和理解成本
- 命名冲突: 如果高阶组件多次嵌套,没有使用命名空间的话会产生冲突,然后覆盖老属性
- 不可见性: HOC相当于在原有组件外层再包装一个组件,你压根不知道外层的包装是啥,对于你是黑盒
- Render Props
- 优点:上述HOC的缺点Render Props都可以解决
- Render Props缺陷:
- 使用繁琐: HOC使用只需要借助装饰器语法通常一行代码就可以进行复用,Render Props无法做到如此简单
- 嵌套过深: Render Props虽然摆脱了组件多层嵌套的问题,但是转化为了函数回调的嵌套
- HOC相比Mixin的优势:
React 16.8做了哪些更新
关于React Hooks
🔔React Hooks优缺点:
- 优点
- 解决了HOC和Render Props的嵌套问题(组件和组件之间共享状态难),更加简洁
- 解决了在生命周期的多个地方处理数据获取和副作用的问题
- 支持函数式组件,class的机制不好理解,解决了类组件的几大问题:
- this 指向容易错误
- 分割在不同声明周期中的逻辑使得代码难以理解和维护
- 代码复用成本高(高阶组件容易使代码量剧增)
- React Hooks缺陷:
- 额外的学习成本(Functional Component 与 Class Component 之间的困惑)
- 写法上有限制(不能出现在条件、循环中),并且写法限制增加了重构成本
- 破坏了PureComponent、React.memo浅比较的性能优化效果(为了取最新的props和state,每次render()都要重新创建事件处函数)
- 在闭包场景可能会引用到旧的state、props值
- 内部实现上不直观(依赖一份可变的全局状态,不再那么“纯”)
- React.memo并不能完全替代shouldComponentUpdate(因为拿不到 state change,只针对 props change)
🍉React Hook的原理?
🔔为什么useState useEffect不能带条件判断?
React 依赖于 Hook 的调用顺序,如果带了条件判断,会导致hooks调用顺序改变,更新失败。具体原因是因为hooks本身的实现机制,是链式调用
🔔🍉useState他的执行原理?
🔔🍉常用的Hooks
useMemo、useCallback、useContext的用法
- useMemo():去避免不必要的复杂逻辑重复计算
🔔关于Fiber
- Virtual DOM数据结构改变,改为链表
- 优化执行策略,快速响应用户,让用户觉得够快,不能阻塞用户的交互
- 分为Reconciliation(协调阶段) 和 Commit(提交阶段,不能被打断)
- 通过某些调度策略合理地分配CPU资源
- 使自己的Reconcilation可被截断
- 在协调截断的时间片用完之后,让出控制权(可能会被多次中断、造成WillMount执行两次的情况)
🔔Redux和Mobx对比
- redux将数据保存在单一的store中,mobx将数据保存在分散的多个store中
- redux使用plain object保存数据,需要手动处理变化后的操作;mobx适用observable保存数据,数据变化后自动处理响应的操作
- redux使用不可变状态,这意味着状态是只读的,不能直接去修改它,而是应该返回一个新的状态,同时使用纯函数;mobx中的状态是可变的,可以直接对其进行修改
- mobx相对来说比较简单,在其中有很多的抽象,mobx更多的使用面向对象的编程思维;redux会比较复杂,因为其中的函数式编程思想掌握起来不是那么容易,同时需要借助一系列的中间件来处理异步和副作用
- mobx中有更多的抽象和封装,调试会比较困难,同时结果也难以预测;而redux提供能够进行时间回溯的开发工具,同时其纯函数以及更少的抽象,让调试变得更加的容易
关于Mobx
🔔🍉mobx是如何实现监听的
关于Redux
🔔Redux的工作流程
- 首先,用户(通过View)发出Action,发出方式就用到了dispatch方法。
- 然后,Store自动调用Reducer,并且传入两个参数:当前State和收到的Action,Reducer会返回新的State
- State一旦有变化,Store就会调用监听函数,来更新View。
到这儿为止,一次用户交互流程结束。可以看到,在整个流程中数据都是单向流动的,这种方式保证了流程的清晰。
🔔React-Redux的工作流程
- Provider: Provider的作用是从最外部封装了整个应用,并向connect模块传递store
- connect: 负责连接React和Redux
- 获取state: connect通过context获取Provider中的store,通过store.getState()获取整个store tree 上所有state
- 包装原组件: 将state和action通过props的方式传入到原组件内部wrapWithConnect返回一个ReactComponent对象Connect,Connect重新render外部传入的原组件WrappedComponent,并把connect中传入的mapStateToProps, mapDispatchToProps与组件上原有的props合并后,通过属性的方式传给WrappedComponent
- 监听store tree变化: connect缓存了store tree中state的状态,通过当前state状态和变更前state状态进行比较,从而确定是否调用this.setState()方法触发Connect及其子组件的重新渲染

redux-saga和redux-thunk对比
- redux-thunk优点:
- 体积小: redux-thunk的实现方式很简单,只有不到20行代码
- 使用简单: redux-thunk没有引入像redux-saga或者redux-observable额外的范式,上手简单
- redux-thunk缺陷:
- 样板代码过多: 与redux本身一样,通常一个请求需要大量的代码,而且很多都是重复性质的
- 耦合严重: 异步操作与redux的action偶合在一起,不方便管理
- 功能孱弱: 有一些实际开发中常用的功能需要自己进行封装
- redux-saga优点:
- 异步解耦: 异步操作被被转移到单独 saga.js 中,不再是掺杂在 action.js 或 component.js 中
- action摆脱thunk function: dispatch 的参数依然是一个纯粹的 action (FSA),而不是充满 “黑魔法” thunk function
- 异常处理: 受益于 generator function 的 saga 实现,代码异常/请求失败 都可以直接通过 try/catch 语法直接捕获处理
- 功能强大: redux-saga提供了大量的Saga 辅助函数和Effect 创建器供开发者使用,开发者无须封装或者简单封装即可使用
- 灵活: redux-saga可以将多个Saga可以串行/并行组合起来,形成一个非常实用的异步flow
- 易测试,提供了各种case的测试方案,包括mock task,分支覆盖等等
- redux-saga缺陷:
- 额外的学习成本: redux-saga不仅在使用难以理解的 generator function,而且有数十个API,学习成本远超redux-thunk,最重要的是你的额外学习成本是只服务于这个库的,与redux-observable不同,redux-observable虽然也有额外学习成本但是背后是rxjs和一整套思想
- 体积庞大: 体积略大,代码近2000行,min版25KB左右
- 功能过剩: 实际上并发控制等功能很难用到,但是我们依然需要引入这些代码
- ts支持不友好: yield无法返回TS类型
什么是传送门(Portals)?
- Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。
- 常用于展示提示框
触发rerender的场景
- state变化
- this.forceUpdate()
- props改变
- 父组件render
- context变化
- 函数式组件每次都会render
🔔🍉React的兼容性
IE8?
HTML+CSS
🔔🍊彻底弄懂css居中
[详解](https://juejin.cn/post/6844904116246806535)
🍉清除浮动的方法
🔔🍊多行文字处理成省略号
没有规范的实现的支持,需要通过一些奇技淫巧实现。。。这种实现方式只支持webkit内核浏览器,所以在IE火狐都不支持
div {
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
🔔BFC 块级格式化上下文
- 创建 BFC:
- 根元素;
- float 不是 none;
- position 为 absolute 或 fixed;
- display 为 inline-block, flex,inline-flex,table-cell, table-caption;
- overflow 为 hidden,auto,scroll;
- 匿名表格单元格元素。
- 特性
- 内部的盒会在垂直方向一个接一个排列(可以看作BFC中有一个的常规流)。
- 处于同一个 BFC 中的元素相互影响,可能会发生 margin collapse;
- 每个元素的 margin box 的左边,与容器块 border box 的左边相接触(对于从左往右的格式化,否则相反),即使存在浮动也是如此。
- BFC 就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素,反之亦然。
- 计算 BFC 的高度时,考虑 BFC 所包含的所有元素,连浮动元素也参与计算。
- 浮动盒区域不会叠加到 BFC 上。
- 作用
- 清除元素内部浮动
- 解决外边距合并问题
- 实现自适应布局。
IFC
- 创建 IFC: IFC 只有在一个块元素中仅包含内联级别元素时才会生成。
- 特性
- 内部的 Boxes 会在水平方向,一个接一个地放置。
- 这些 Boxes 垂直方向的起点从包含块盒子的顶部开始。
- 摆放这些 Boxes 的时候,它们在水平方向上的外边距、边框、内边距所占用的空间都会被考虑在内。
- 在垂直方向上,这些框可能会以不同形式来对齐(vertical-align):它们可能会使用底部或顶部对齐,也可能通过其内部的文本基线(baseline)对齐。
- 能把在一行上的框都完全包含进去的一个矩形区域,被称为该行的行框(line box)。行框的宽度是由包含块(containing box)和存在的浮动来决定。
- IFC中的line box一般左右边都贴紧其包含块,但是会因为 float 元素的存在发生变化。float 元素会位于 IFC 与与 line box 之间,使得 line box 宽度缩短。
- IFC 中的 line box 高度由 CSS 行高计算规则来确定,同个 IFC 下的多个 line box 高度可能会不同(比如一行包含了较高的图片,而另一行只有文本)。
- 当 inline-level boxes 的总宽度少于包含它们的 line box 时,其水平渲染规则由 text-align 属性来确定,如果取值为 justify,那么浏览器会对 inline-boxes(注意不是 inline-table 和 inline-block boxes)中的文字和空格做出拉伸。
- 当一个 inline box 超过 line box 的宽度时,它会被分割成多个 boxes,这些 boxes 被分布在多个line box 里。如果一个 inline box 不能被分割(比如只包含单个字符,或 word-breaking 机制被禁用,或该行内框受 white-space 属性值为 nowrap 或 pre 的影响),那么这个 inline box 将溢出这个 line box。
HTML语义化
- 语义化的 HTML 在没有CSS的情况下也能呈现较好的内容结构与代码结构。
- 有利于SEO,有助于爬虫抓取更多的有效信息,爬虫是依赖于标签来确定上下文和各个关键字的权重。
- 方便其他设备的解析(如屏幕阅读器、盲人阅读器等),利于无障碍阅读,提高可访问性。
- 便于团队开发和维护,语义化更具可读性,遵循W3C标准的团队都遵循这个标准,可以减少差异化。
具体有
- 根元素: html
- 文档元数据: head, title, base, meta, link, style
- 脚本: script, noscript, template
- 章节: body, section, nav, article, aside, h1 - h6, header, footer, address, main
- 组织内容: div, p, pre, ol, ul, li, dl, dt, dd
- 文字形式: a, em, strong, small, s, span, br
- 嵌入内容: img, iframe, embed, video, audio, canvas, svg, math,
- 表格
- 表单
- 交互元素
*参考文章
- 2019年17道高频React面试题及详解
- (2.4w字,建议收藏)😇原生JS灵魂之问(下), 冲刺🚀进阶最后一公里(附个人成长经验分享)
- Javascript高级程序设计(第三版)