前端大体系(手写代码+JS+React)

2,530 阅读29分钟

这是在准备面试过程中汇总的一些觉得平时也会有用的知识点,用于作为一个反思帖,希望日后能够定时更新,并且增加思考深度和个人理解。

🍉:暂未写出答案。

🍊:涉及算法和手写代码。

🔔:笔者在面试过程中真实遇到的题目

下篇传送门:➡️全栈大体系(工程化+浏览器+网络+安全+Node)

目录

  1. 手写代码
  2. Javascript基础
  3. ES6
  4. React
  5. HTML+CSS
  6. 前端工程化(在下篇)
  7. 浏览器+网络+通信+安全(在下篇)
  8. NodeJS(在下篇)
  9. 其他(在下篇)

手写代码

🍊实现一个promise(then resolve reject即可)

  1. 简版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)))
  }
}
  1. 完整版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
};

🔔🍊如何把数组打平

  1. flat():需要指定层数
  2. 常规递归
  3. 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

🔔关于类型检测

  1. typeof:只能检测除了null之外的基础类型和引用类型里的function,null和object和array 都会被检测为object
  2. instanceof:原理是基于原型链的查询,所以检测的比较全,但是也检测不出准确类型
  3. Object.prototype.toString.call()可以检测类型!!
  4. 如何是不是NaN?
    • Number.isNaN(value);
    • ES6的Object.is(NaN, NaN)

JS隐式类型转换

  1. 遇到 - * % / 运算符时,转化为number类型做运算
  2. +比较特殊,存在转为number和string两种情况
    1. 数字相加:数字和一个非字符串的类型相加,会转为number之后进行:1+true == 2
    2. 字符串拼接:字符串和其余类型相加,会转为string再进行,对于对象,会调用valueOf()/toString()转为字符串后,再进行拼接
  3. isNaN() 自带类型转化,会转为number再比较
  4. 为假的类型只有这些:""、0、false、null、undefined、NaN、[] [] == false "" === false ?

== 和 === 的区别

  • ===全等号比较值的同时还会比较类型
  • ==会进行类型转化后再做比较,具体转化方式如下⬇️
    1. Boolean == ?, 会转为Number比较。
    2. Number == String,会把 String 通过 Number() 方法转换为数字比较。
    3. Boolean == String,会转为数字
    4. 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__ 
    }
}

🔔关于闭包

  1. 定义:闭包是指有权访问另外一个函数作用域中的变量的函数
  2. 使用方式:
    1. 返回一个函数
    2. 作为函数参数传递
    3. 在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。
    4. IIFE(立即执行函数表达式)创建闭包,保存了全局作用域window和当前函数的作用域。

🔔关于this

  1. this是什么:this就是一个对象。不同情况下 this指向的不同
  2. 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. 同步队列代码一次执行
  2. 异步函数到了指定时间放到异步队列里
  3. 同步执行完毕,异步轮询执行(1宏任务->n微任务)

new

new操作符做了什么

  1. 创建一个空对象 var obj = new Object()
  2. 设置原型链:obj.proto = Object.prototype;
  3. 改变this指向,指向创建出得对象
  4. 运行构造函数,返回创建的对象

🍊手写一个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
}

🔔什么是伪数组?有哪些?如何将伪数组转化为标准数组?

  • 什么是伪数组
    1. 具有length属性
    2. 能够使用数组遍历方法遍历它们
    3. 不具有数组的push,pop等方法
  • 如何判断:Array.isArray()
  • 举个例子
    1. DOM选择器
    2. arguments对象
  • 怎么转换成正常的数组
    1. [].slice.call(伪数组)
    2. Array.form(伪数组)
    3. [...伪数组]

🔔🍉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,
  1. JavaScript是单线程语言,所以只有一个执行队列
  2. 主线程用于执行同步任务,一旦队列为空,就去执行Event Query中的任务
  3. 任务执行的顺序是:执行一个宏任务后,执行队列中所有的微任务,然后循环

🔔浏览器的EventLoop

  1. 一开始整段脚本在主线程中执行
  2. 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
  3. 当前宏任务执行完出队,检查微任务队列,如果有则依次执行,直到微任务队列为空
  4. 执行浏览器 UI 线程的渲染工作
  5. 检查是否有Web worker任务,有则执行
  6. 执行队首新的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空

注意:

  • 在浏览器页面中可以认为初始执行线程中没有代码,每一个script标签中的代码是一个独立的task,即会执行完前面的script中创建的microtask再执行后面的script中的同步代码。
  • 如果microtask一直被添加,则会继续执行microtask,“卡死”macrotask

Node和浏览器的EventLoop区别

  • 在Node版本<11的时候,处理方式是:先执行完所有宏任务,再执行队列里的所有微任务
  • Node版本 >=11的时候,和浏览器的处理方式一样:执行完一个宏任务,执行微任务队列的所有任务,再去执行下一个宏任务

Node11之前的处理方式如下:

在node中事件每一轮循环按照顺序分为6个阶段:

  1. timers:执行满足条件的setTimeout、setInterval回调。
  2. I/O callbacks:是否有已完成的I/O操作的回调函数,来自上一轮的poll残留。
  3. idle,prepare:可忽略
  4. poll:等待还没完成的I/O事件,会因timers和超时时间等结束等待。
  5. check:执行setImmediate的回调。
  6. close callbacks:关闭所有的closing handles,一些onclose事件。

循环中的几个阶段的执行队列也分别称为Timers Queue、I/O Queue、Check Queue、Close Queue 在进入第一次循环之前,会先进行如下操作:

  1. 同步任务
  2. 发出异步请求
  3. 规划定时器生效的时间
  4. 执行process.nextTick()

按照我们的循环的6个阶段依次执行,每次拿出当前阶段中的全部任务执行,清空NextTick Queue,清空Microtask Queue。再执行下一阶段,全部6个阶段执行完毕后,进入下轮循环。即:

  1. 清空当前循环内的Timers Queue,清空NextTick Queue,清空Microtask Queue。
  2. 清空当前循环内的I/O Queue,清空NextTick Queue,清空Microtask Queue。
  3. 清空当前循环内的Check Queu,清空NextTick Queue,清空Microtask Queue。
  4. 清空当前循环内的Close Queu,清空NextTick Queue,清空Microtask Queue。
  5. 进入下轮循环。

注意:

  • 如果在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有哪些性能优化方式

  1. 避免不必要的重新渲染:React.memo()、PureComponent(对传递给组件的props的浅比较)
  2. 16之前 shouldComponentUpdate
  3. input框等受控组件,不用双向绑定的情况下没有必要存为state,存为私有属性即可。或者进行防抖
  4. 组件之间的组合方式应该减少硬编码,使用this.props.children来降低耦合性。因为现在存在很长的链式组件传输
  5. 使用immutable状态
  6. redux 合并dispatch => reudx-batched-actions
  7. reselect
  8. 容器组件避免复杂逻辑,尽量不写dom节点,将子组件作为pure来去处理复杂逻辑控制render防止重刷

React如何进行组件/逻辑复用?——详细讲解

  1. 高阶组件HOC
    1. 属性代理(Props Proxy): HOC 对传给 WrappedComponent W 的 porps 进行操作
    2. 反向继承(Inheritance Inversion): HOC 继承 WrappedComponent W。
  2. mixin已被弃用(HOC相当于升级版的mixin)
  3. 渲染属性(render props)
  4. react hooks
  5. 优缺点:
    • 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虽然摆脱了组件多层嵌套的问题,但是转化为了函数回调的嵌套

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

  1. Virtual DOM数据结构改变,改为链表
  2. 优化执行策略,快速响应用户,让用户觉得够快,不能阻塞用户的交互
    • 分为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优点:
    1. 体积小: redux-thunk的实现方式很简单,只有不到20行代码
    2. 使用简单: redux-thunk没有引入像redux-saga或者redux-observable额外的范式,上手简单
  • redux-thunk缺陷:
    1. 样板代码过多: 与redux本身一样,通常一个请求需要大量的代码,而且很多都是重复性质的
    2. 耦合严重: 异步操作与redux的action偶合在一起,不方便管理
    3. 功能孱弱: 有一些实际开发中常用的功能需要自己进行封装
  • redux-saga优点:
    1. 异步解耦: 异步操作被被转移到单独 saga.js 中,不再是掺杂在 action.js 或 component.js 中
    2. action摆脱thunk function: dispatch 的参数依然是一个纯粹的 action (FSA),而不是充满 “黑魔法” thunk function
    3. 异常处理: 受益于 generator function 的 saga 实现,代码异常/请求失败 都可以直接通过 try/catch 语法直接捕获处理
    4. 功能强大: redux-saga提供了大量的Saga 辅助函数和Effect 创建器供开发者使用,开发者无须封装或者简单封装即可使用
    5. 灵活: redux-saga可以将多个Saga可以串行/并行组合起来,形成一个非常实用的异步flow
    6. 易测试,提供了各种case的测试方案,包括mock task,分支覆盖等等
  • redux-saga缺陷:
    1. 额外的学习成本: redux-saga不仅在使用难以理解的 generator function,而且有数十个API,学习成本远超redux-thunk,最重要的是你的额外学习成本是只服务于这个库的,与redux-observable不同,redux-observable虽然也有额外学习成本但是背后是rxjs和一整套思想
    2. 体积庞大: 体积略大,代码近2000行,min版25KB左右
    3. 功能过剩: 实际上并发控制等功能很难用到,但是我们依然需要引入这些代码
    4. ts支持不友好: yield无法返回TS类型

什么是传送门(Portals)?

  • Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。
  • 常用于展示提示框

触发rerender的场景

  1. state变化
  2. this.forceUpdate()
  3. props改变
  4. 父组件render
  5. context变化
  6. 函数式组件每次都会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:
    1. 根元素;
    2. float 不是 none;
    3. position 为 absolute 或 fixed;
    4. display 为 inline-block, flex,inline-flex,table-cell, table-caption;
    5. overflow 为 hidden,auto,scroll;
    6. 匿名表格单元格元素。
  • 特性
    1. 内部的盒会在垂直方向一个接一个排列(可以看作BFC中有一个的常规流)。
    2. 处于同一个 BFC 中的元素相互影响,可能会发生 margin collapse;
    3. 每个元素的 margin box 的左边,与容器块 border box 的左边相接触(对于从左往右的格式化,否则相反),即使存在浮动也是如此。
    4. BFC 就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素,反之亦然。
    5. 计算 BFC 的高度时,考虑 BFC 所包含的所有元素,连浮动元素也参与计算。
    6. 浮动盒区域不会叠加到 BFC 上。
  • 作用
    1. 清除元素内部浮动
    2. 解决外边距合并问题
    3. 实现自适应布局。

IFC

  • 创建 IFC: IFC 只有在一个块元素中仅包含内联级别元素时才会生成。
  • 特性
    1. 内部的 Boxes 会在水平方向,一个接一个地放置。
    2. 这些 Boxes 垂直方向的起点从包含块盒子的顶部开始。
    3. 摆放这些 Boxes 的时候,它们在水平方向上的外边距、边框、内边距所占用的空间都会被考虑在内。
    4. 在垂直方向上,这些框可能会以不同形式来对齐(vertical-align):它们可能会使用底部或顶部对齐,也可能通过其内部的文本基线(baseline)对齐。
    5. 能把在一行上的框都完全包含进去的一个矩形区域,被称为该行的行框(line box)。行框的宽度是由包含块(containing box)和存在的浮动来决定。
    6. IFC中的line box一般左右边都贴紧其包含块,但是会因为 float 元素的存在发生变化。float 元素会位于 IFC 与与 line box 之间,使得 line box 宽度缩短。
    7. IFC 中的 line box 高度由 CSS 行高计算规则来确定,同个 IFC 下的多个 line box 高度可能会不同(比如一行包含了较高的图片,而另一行只有文本)。
    8. 当 inline-level boxes 的总宽度少于包含它们的 line box 时,其水平渲染规则由 text-align 属性来确定,如果取值为 justify,那么浏览器会对 inline-boxes(注意不是 inline-table 和 inline-block boxes)中的文字和空格做出拉伸。
    9. 当一个 inline box 超过 line box 的宽度时,它会被分割成多个 boxes,这些 boxes 被分布在多个line box 里。如果一个 inline box 不能被分割(比如只包含单个字符,或 word-breaking 机制被禁用,或该行内框受 white-space 属性值为 nowrap 或 pre 的影响),那么这个 inline box 将溢出这个 line box。

HTML语义化

  1. 语义化的 HTML 在没有CSS的情况下也能呈现较好的内容结构与代码结构。
  2. 有利于SEO,有助于爬虫抓取更多的有效信息,爬虫是依赖于标签来确定上下文和各个关键字的权重。
  3. 方便其他设备的解析(如屏幕阅读器、盲人阅读器等),利于无障碍阅读,提高可访问性。
  4. 便于团队开发和维护,语义化更具可读性,遵循W3C标准的团队都遵循这个标准,可以减少差异化。

具体有

  1. 根元素: html
  2. 文档元数据: head, title, base, meta, link, style
  3. 脚本: script, noscript, template
  4. 章节: body, section, nav, article, aside, h1 - h6, header, footer, address, main
  5. 组织内容: div, p, pre, ol, ul, li, dl, dt, dd
  6. 文字形式: a, em, strong, small, s, span, br
  7. 嵌入内容: img, iframe, embed, video, audio, canvas, svg, math,
  8. 表格
  9. 表单
  10. 交互元素

*参考文章

  1. 2019年17道高频React面试题及详解
  2. (2.4w字,建议收藏)😇原生JS灵魂之问(下), 冲刺🚀进阶最后一公里(附个人成长经验分享)
  3. Javascript高级程序设计(第三版)