写点什么

前端工程师面试题自检

作者:loveX001
  • 2022-11-07
    浙江
  • 本文字数:29121 字

    阅读完需:约 96 分钟

将虚拟 Dom 转化为真实 Dom

题目描述:JSON 格式的虚拟 Dom 怎么转换成真实 Dom


{  tag: 'DIV',  attrs:{  id:'app'  },  children: [    {      tag: 'SPAN',      children: [        { tag: 'A', children: [] }      ]    },    {      tag: 'SPAN',      children: [        { tag: 'A', children: [] },        { tag: 'A', children: [] }      ]    }  ]}把上诉虚拟Dom转化成下方真实Dom<div id="app">  <span>    <a></a>  </span>  <span>    <a></a>    <a></a>  </span></div>

复制代码


实现代码如下:


// 真正的渲染函数function _render(vnode) {  // 如果是数字类型转化为字符串  if (typeof vnode === "number") {    vnode = String(vnode);  }  // 字符串类型直接就是文本节点  if (typeof vnode === "string") {    return document.createTextNode(vnode);  }  // 普通DOM  const dom = document.createElement(vnode.tag);  if (vnode.attrs) {    // 遍历属性    Object.keys(vnode.attrs).forEach((key) => {      const value = vnode.attrs[key];      dom.setAttribute(key, value);    });  }  // 子数组进行递归操作  vnode.children.forEach((child) => dom.appendChild(_render(child)));  return dom;}
复制代码

说说 Vue2.0 和 Vue3.0 有什么区别

  • 重构响应式系统,使用Proxy替换Object.defineProperty,使用Proxy优势:

  • 可直接监听数组类型的数据变化

  • 监听的目标为对象本身,不需要像Object.defineProperty一样遍历每个属性,有一定的性能提升

  • 可拦截apply、ownKeys、has等 13 种方法,而Object.defineProperty不行

  • 直接实现对象属性的新增/删除

  • 新增Composition API,更好的逻辑复用和代码组织

  • 重构 Virtual DOM

  • 模板编译时的优化,将一些静态节点编译成常量

  • slot优化,将slot编译为lazy函数,将slot的渲染的决定权交给子组件

  • 模板中内联事件的提取并重用(原本每次渲染都重新生成内联函数)

  • 代码结构调整,更便于 Tree shaking,使得体积更小

  • 使用 Typescript 替换 Flow

9 种前端常见的设计模式

1. 外观模式

外观模式是最常见的设计模式之一,它为子系统中的一组接口提供一个统一的高层接口,使子系统更容易使用。简而言之外观设计模式就是把多个子系统中复杂逻辑进行抽象,从而提供一个更统一、更简洁、更易用的 API。很多我们常用的框架和库基本都遵循了外观设计模式,比如 JQuery 就把复杂的原生 DOM 操作进行了抽象和封装,并消除了浏览器之间的兼容问题,从而提供了一个更高级更易用的版本。其实在平时工作中我们也会经常用到外观模式进行开发,只是我们不自知而已


兼容浏览器事件绑定


let addMyEvent = function (el, ev, fn) {    if (el.addEventListener) {        el.addEventListener(ev, fn, false)    } else if (el.attachEvent) {        el.attachEvent('on' + ev, fn)    } else {        el['on' + ev] = fn    }};
复制代码


封装接口


let myEvent = {    // ...    stop: e => {        e.stopPropagation();        e.preventDefault();    }};
复制代码


场景


  • 设计初期,应该要有意识地将不同的两个层分离,比如经典的三层结构,在数据访问层和业务逻辑层、业务逻辑层和表示层之间建立外观 Facade

  • 在开发阶段,子系统往往因为不断的重构演化而变得越来越复杂,增加外观 Facade 可以提供一个简单的接口,减少他们之间的依赖。

  • 在维护一个遗留的大型系统时,可能这个系统已经很难维护了,这时候使用外观 Facade 也是非常合适的,为系系统开发一个外观 Facade 类,为设计粗糙和高度复杂的遗留代码提供比较清晰的接口,让新系统和 Facade 对象交互,Facade 与遗留代码交互所有的复杂工作。


优点


  • 减少系统相互依赖。

  • 提高灵活性。

  • 提高了安全性


缺点


不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。

2. 代理模式

是为一个对象提供一个代用品或占位符,以便控制对它的访问


假设当 A 在心情好的时候收到花,小明表白成功的几率有 60%,而当 A 在心情差的时候收到花,小明表白的成功率无限趋近于 0。小明跟 A 刚刚认识两天,还无法辨别 A 什么时候心情好。如果不合时宜地把花送给 A,花被直接扔掉的可能性很大,这束花可是小明吃了 7 天泡面换来的。但是 A 的朋友 B 却很了解 A,所以小明只管把花交给 B,B 会监听 A 的心情变化,然后选择 A 心情好的时候把花转交给 A,代码如下:


let Flower = function() {}let xiaoming = {  sendFlower: function(target) {    let flower = new Flower()    target.receiveFlower(flower)  }}let B = {  receiveFlower: function(flower) {    A.listenGoodMood(function() {      A.receiveFlower(flower)    })  }}let A = {  receiveFlower: function(flower) {    console.log('收到花'+ flower)  },  listenGoodMood: function(fn) {    setTimeout(function() {      fn()    }, 1000)  }}xiaoming.sendFlower(B)
复制代码


场景


HTML 元 素事件代理


<ul id="ul">  <li>1</li>  <li>2</li>  <li>3</li></ul><script>  let ul = document.querySelector('#ul');  ul.addEventListener('click', event => {    console.log(event.target);  });</script>
复制代码


优点


  • 代理模式能将代理对象与被调用对象分离,降低了系统的耦合度。代理模式在客户端和目标对象之间起到一个中介作用,这样可以起到保护目标对象的作用

  • 代理对象可以扩展目标对象的功能;通过修改代理对象就可以了,符合开闭原则;


缺点


处理请求速度可能有差别,非直接访问存在开销

3. 工厂模式

工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。该模式使一个类的实例化延迟到了子类。而子类可以重写接口方法以便创建的时候指定自己的对象类型。


class Product {    constructor(name) {        this.name = name    }    init() {        console.log('init')    }    fun() {        console.log('fun')    }}
class Factory { create(name) { return new Product(name) }}
// uselet factory = new Factory()let p = factory.create('p1')p.init()p.fun()
复制代码


场景


  • 如果你不想让某个子系统与较大的那个对象之间形成强耦合,而是想运行时从许多子系统中进行挑选的话,那么工厂模式是一个理想的选择

  • 将 new 操作简单封装,遇到 new 的时候就应该考虑是否用工厂模式;

  • 需要依赖具体环境创建不同实例,这些实例都有相同的行为,这时候我们可以使用工厂模式,简化实现的过程,同时也可以减少每种对象所需的代码量,有利于消除对象间的耦合,提供更大的灵活性


优点


  • 创建对象的过程可能很复杂,但我们只需要关心创建结果。

  • 构造函数和创建者分离, 符合“开闭原则”

  • 一个调用者想创建一个对象,只要知道其名称就可以了。

  • 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。


缺点


  • 添加新产品时,需要编写新的具体产品类,一定程度上增加了系统的复杂度

  • 考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度


什么时候不用


当被应用到错误的问题类型上时,这一模式会给应用程序引入大量不必要的复杂性.除非为创建对象提供一个接口是我们编写的库或者框架的一个设计上目标,否则我会建议使用明确的构造器,以避免不必要的开销。


由于对象的创建过程被高效的抽象在一个接口后面的事实,这也会给依赖于这个过程可能会有多复杂的单元测试带来问题。

4. 单例模式

顾名思义,单例模式中 Class 的实例个数最多为 1。当需要一个对象去贯穿整个系统执行某些任务时,单例模式就派上了用场。而除此之外的场景尽量避免单例模式的使用,因为单例模式会引入全局状态,而一个健康的系统应该避免引入过多的全局状态。


实现单例模式需要解决以下几个问题:


  • 如何确定 Class 只有一个实例?

  • 如何简便的访问 Class 的唯一实例?

  • Class 如何控制实例化的过程?

  • 如何将 Class 的实例个数限制为 1?


我们一般通过实现以下两点来解决上述问题:


  • 隐藏 Class 的构造函数,避免多次实例化

  • 通过暴露一个 getInstance() 方法来创建/获取唯一实例


Javascript 中单例模式可以通过以下方式实现:


// 单例构造器const FooServiceSingleton = (function () {  // 隐藏的Class的构造函数  function FooService() {}
// 未初始化的单例对象 let fooService;
return { // 创建/获取单例对象的函数 getInstance: function () { if (!fooService) { fooService = new FooService(); } return fooService; } }})();
复制代码


实现的关键点有:


  • 使用 IIFE 创建局部作用域并即时执行;

  • getInstance() 为一个 闭包 ,使用闭包保存局部作用域中的单例对象并返回。


我们可以验证下单例对象是否创建成功:


const fooService1 = FooServiceSingleton.getInstance();const fooService2 = FooServiceSingleton.getInstance();
console.log(fooService1 === fooService2); // true
复制代码


场景例子


  • 定义命名空间和实现分支型方法

  • 登录框

  • vuex 和 redux 中的 store


优点


  • 划分命名空间,减少全局变量

  • 增强模块性,把自己的代码组织在一个全局变量名下,放在单一位置,便于维护

  • 且只会实例化一次。简化了代码的调试和维护


缺点


  • 由于单例模式提供的是一种单点访问,所以它有可能导致模块间的强耦合

  • 从而不利于单元测试。无法单独测试一个调用了来自单例的方法的类,而只能把它与那个单例作为一 个单元一起测试。

5. 策略模式

策略模式简单描述就是:对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。把它们一个个封装起来,并且使它们可以互相替换


<html><head>    <title>策略模式-校验表单</title>    <meta content="text/html; charset=utf-8" http-equiv="Content-Type"></head><body>    <form id = "registerForm" method="post" action="http://xxxx.com/api/register">        用户名:<input type="text" name="userName">        密码:<input type="text" name="password">        手机号码:<input type="text" name="phoneNumber">        <button type="submit">提交</button>    </form>    <script type="text/javascript">        // 策略对象        const strategies = {          isNoEmpty: function (value, errorMsg) {            if (value === '') {              return errorMsg;            }          },          isNoSpace: function (value, errorMsg) {            if (value.trim() === '') {              return errorMsg;            }          },          minLength: function (value, length, errorMsg) {            if (value.trim().length < length) {              return errorMsg;            }          },          maxLength: function (value, length, errorMsg) {            if (value.length > length) {              return errorMsg;            }          },          isMobile: function (value, errorMsg) {            if (!/^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|17[7]|18[0|1|2|3|5|6|7|8|9])\d{8}$/.test(value)) {              return errorMsg;            }                          }        }
// 验证类 class Validator { constructor() { this.cache = [] } add(dom, rules) { for(let i = 0, rule; rule = rules[i++];) { let strategyAry = rule.strategy.split(':') let errorMsg = rule.errorMsg this.cache.push(() => { let strategy = strategyAry.shift() strategyAry.unshift(dom.value) strategyAry.push(errorMsg) return strategies[strategy].apply(dom, strategyAry) }) } } start() { for(let i = 0, validatorFunc; validatorFunc = this.cache[i++];) { let errorMsg = validatorFunc() if (errorMsg) { return errorMsg } } } }
// 调用代码 let registerForm = document.getElementById('registerForm')
let validataFunc = function() { let validator = new Validator() validator.add(registerForm.userName, [{ strategy: 'isNoEmpty', errorMsg: '用户名不可为空' }, { strategy: 'isNoSpace', errorMsg: '不允许以空白字符命名' }, { strategy: 'minLength:2', errorMsg: '用户名长度不能小于2位' }]) validator.add(registerForm.password, [ { strategy: 'minLength:6', errorMsg: '密码长度不能小于6位' }]) validator.add(registerForm.phoneNumber, [{ strategy: 'isMobile', errorMsg: '请输入正确的手机号码格式' }]) return validator.start() }
registerForm.onsubmit = function() { let errorMsg = validataFunc() if (errorMsg) { alert(errorMsg) return false } } </script></body></html>
复制代码


场景例子


  • 如果在一个系统里面有许多类,它们之间的区别仅在于它们的'行为',那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。

  • 一个系统需要动态地在几种算法中选择一种。

  • 表单验证


优点


  • 利用组合、委托、多态等技术和思想,可以有效的避免多重条件选择语句

  • 提供了对开放-封闭原则的完美支持,将算法封装在独立的 strategy 中,使得它们易于切换,理解,易于扩展

  • 利用组合和委托来让 Context 拥有执行算法的能力,这也是继承的一种更轻便的代替方案


缺点


  • 会在程序中增加许多策略类或者策略对象

  • 要使用策略模式,必须了解所有的 strategy,必须了解各个 strategy 之间的不同点,这样才能选择一个合适的 strategy

6. 迭代器模式

如果你看到这,ES6 中的迭代器 Iterator 相信你还是有点印象的,上面第 60 条已经做过简单的介绍。迭代器模式简单的说就是提供一种方法顺序一个聚合对象中各个元素,而又不暴露该对象的内部表示。


迭代器模式解决了以下问题:


  • 提供一致的遍历各种数据结构的方式,而不用了解数据的内部结构

  • 提供遍历容器(集合)的能力而无需改变容器的接口


一个迭代器通常需要实现以下接口:


  • hasNext():判断迭代是否结束,返回 Boolean

  • next():查找并返回下一个元素


为 Javascript 的数组实现一个迭代器可以这么写:


const item = [1, 'red', false, 3.14];
function Iterator(items) { this.items = items; this.index = 0;}
Iterator.prototype = { hasNext: function () { return this.index < this.items.length; }, next: function () { return this.items[this.index++]; }}
复制代码


验证一下迭代器是否工作:


const iterator = new Iterator(item);
while(iterator.hasNext()){ console.log(iterator.next());}//输出:1, red, false, 3.14
复制代码


ES6 提供了更简单的迭代循环语法 for...of,使用该语法的前提是操作对象需要实现 可迭代协议(The iterable protocol),简单说就是该对象有个 Key 为 Symbol.iterator 的方法,该方法返回一个 iterator 对象。


比如我们实现一个 Range 类用于在某个数字区间进行迭代:


function Range(start, end) {  return {    [Symbol.iterator]: function () {      return {        next() {          if (start < end) {            return { value: start++, done: false };          }          return { done: true, value: end };        }      }    }  }}
复制代码


验证一下:


for (num of Range(1, 5)) {  console.log(num);}// 输出:1, 2, 3, 4
复制代码

7. 观察者模式

观察者模式又称发布-订阅模式(Publish/Subscribe Pattern),是我们经常接触到的设计模式,日常生活中的应用也比比皆是,比如你订阅了某个博主的频道,当有内容更新时会收到推送;又比如 JavaScript 中的事件订阅响应机制。观察者模式的思想用一句话描述就是:被观察对象(subject)维护一组观察者(observer),当被观察对象状态改变时,通过调用观察者的某个方法将这些变化通知到观察者。


观察者模式中 Subject 对象一般需要实现以下 API:


  • subscribe(): 接收一个观察者 observer 对象,使其订阅自己

  • unsubscribe(): 接收一个观察者 observer 对象,使其取消订阅自己

  • fire(): 触发事件,通知到所有观察者


用 JavaScript 手动实现观察者模式:


// 被观察者function Subject() {  this.observers = [];}
Subject.prototype = { // 订阅 subscribe: function (observer) { this.observers.push(observer); }, // 取消订阅 unsubscribe: function (observerToRemove) { this.observers = this.observers.filter(observer => { return observer !== observerToRemove; }) }, // 事件触发 fire: function () { this.observers.forEach(observer => { observer.call(); }); }}
复制代码


验证一下订阅是否成功:


const subject = new Subject();
function observer1() { console.log('Observer 1 Firing!');}

function observer2() { console.log('Observer 2 Firing!');}
subject.subscribe(observer1);subject.subscribe(observer2);subject.fire();
//输出:Observer 1 Firing! Observer 2 Firing!
复制代码


验证一下取消订阅是否成功:


subject.unsubscribe(observer2);subject.fire();
//输出:Observer 1 Firing!
复制代码


场景


  • DOM 事件


document.body.addEventListener('click', function() {    console.log('hello world!');});document.body.click()
复制代码


  • vue 响应式


优点


  • 支持简单的广播通信,自动通知所有已经订阅过的对象

  • 目标对象与观察者之间的抽象耦合关系能单独扩展以及重用

  • 增加了灵活性

  • 观察者模式所做的工作就是在解耦,让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响到另一边的变化。


缺点


过度使用会导致对象与对象之间的联系弱化,会导致程序难以跟踪维护和理解

8. 中介者模式

  • 在中介者模式中,中介者(Mediator)包装了一系列对象相互作用的方式,使得这些对象不必直接相互作用,而是由中介者协调它们之间的交互,从而使它们可以松散偶合。当某些对象之间的作用发生改变时,不会立即影响其他的一些对象之间的作用,保证这些作用可以彼此独立的变化。

  • 中介者模式和观察者模式有一定的相似性,都是一对多的关系,也都是集中式通信,不同的是中介者模式是处理同级对象之间的交互,而观察者模式是处理 Observer 和 Subject 之间的交互。中介者模式有些像婚恋中介,相亲对象刚开始并不能直接交流,而是要通过中介去筛选匹配再决定谁和谁见面。


场景


例如购物车需求,存在商品选择表单、颜色选择表单、购买数量表单等等,都会触发 change 事件,那么可以通过中介者来转发处理这些事件,实现各个事件间的解耦,仅仅维护中介者对象即可。


var goods = {   //手机库存    'red|32G': 3,    'red|64G': 1,    'blue|32G': 7,    'blue|32G': 6,};//中介者var mediator = (function() {    var colorSelect = document.getElementById('colorSelect');    var memorySelect = document.getElementById('memorySelect');    var numSelect = document.getElementById('numSelect');    return {        changed: function(obj) {            switch(obj){                case colorSelect:                    //TODO                    break;                case memorySelect:                    //TODO                    break;                case numSelect:                    //TODO                    break;            }        }    }})();colorSelect.onchange = function() {    mediator.changed(this);};memorySelect.onchange = function() {    mediator.changed(this);};numSelect.onchange = function() {    mediator.changed(this);};
复制代码


  • 聊天室里


聊天室成员类:


function Member(name) {  this.name = name;  this.chatroom = null;}
Member.prototype = { // 发送消息 send: function (message, toMember) { this.chatroom.send(message, this, toMember); }, // 接收消息 receive: function (message, fromMember) { console.log(`${fromMember.name} to ${this.name}: ${message}`); }}
复制代码


聊天室类:


function Chatroom() {  this.members = {};}
Chatroom.prototype = { // 增加成员 addMember: function (member) { this.members[member.name] = member; member.chatroom = this; }, // 发送消息 send: function (message, fromMember, toMember) { toMember.receive(message, fromMember); }}
复制代码


测试一下:


const chatroom = new Chatroom();const bruce = new Member('bruce');const frank = new Member('frank');
chatroom.addMember(bruce);chatroom.addMember(frank);
bruce.send('Hey frank', frank);
//输出:bruce to frank: hello frank
复制代码


优点


  • 使各对象之间耦合松散,而且可以独立地改变它们之间的交互

  • 中介者和对象一对多的关系取代了对象之间的网状多对多的关系

  • 如果对象之间的复杂耦合度导致维护很困难,而且耦合度随项目变化增速很快,就需要中介者重构代码


缺点


系统中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介 者对象自身往往就是一个难以维护的对象。

9. 访问者模式

访问者模式 是一种将算法与对象结构分离的设计模式,通俗点讲就是:访问者模式让我们能够在不改变一个对象结构的前提下能够给该对象增加新的逻辑,新增的逻辑保存在一个独立的访问者对象中。访问者模式常用于拓展一些第三方的库和工具。


// 访问者  class Visitor {    constructor() {}    visitConcreteElement(ConcreteElement) {        ConcreteElement.operation()    }}// 元素类  class ConcreteElement{    constructor() {    }    operation() {       console.log("ConcreteElement.operation invoked");      }    accept(visitor) {        visitor.visitConcreteElement(this)    }}// clientlet visitor = new Visitor()let element = new ConcreteElement()elementA.accept(visitor)
复制代码


访问者模式的实现有以下几个要素:


  • Visitor Object:访问者对象,拥有一个 visit()方法

  • Receiving Object:接收对象,拥有一个 accept() 方法

  • visit(receivingObj):用于 Visitor 接收一个 Receiving Object

  • accept(visitor):用于 Receving Object 接收一个 Visitor,并通过调用 Visitor 的 visit() 为其提供获取 Receiving Object 数据的能力


简单的代码实现如下:


Receiving Object:
function Employee(name, salary) { this.name = name; this.salary = salary;}
Employee.prototype = { getSalary: function () { return this.salary; }, setSalary: function (salary) { this.salary = salary; }, accept: function (visitor) { visitor.visit(this); }}Visitor Object:
function Visitor() { }
Visitor.prototype = { visit: function (employee) { employee.setSalary(employee.getSalary() * 2); }}
复制代码


验证一下:


const employee = new Employee('bruce', 1000);const visitor = new Visitor();employee.accept(visitor);
console.log(employee.getSalary());//输出:2000
复制代码


场景


对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作


需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,也不希望在增加新操作时修改这些类。


优点


  • 符合单一职责原则

  • 优秀的扩展性

  • 灵活性


缺点


  • 具体元素对访问者公布细节,违反了迪米特原则

  • 违反了依赖倒置原则,依赖了具体类,没有依赖抽象。

  • 具体元素变更比较困难

介绍一下 Rollup

Rollup 是一款 ES Modules 打包器。它也可以将项目中散落的细小模块打包为整块代码,从而使得这些划分的模块可以更好地运行在浏览器环境或者 Node.js 环境。


Rollup 优势:


  • 输出结果更加扁平,执行效率更高;

  • 自动移除未引用代码;

  • 打包结果依然完全可读。


缺点


  • 加载非 ESM 的第三方模块比较复杂;

  • 因为模块最终都被打包到全局中,所以无法实现 HMR

  • 浏览器环境中,代码拆分功能必须使用 Require.js 这样的 AMD


  • 我们发现如果我们开发的是一个应用程序,需要大量引用第三方模块,同时还需要 HMR 提升开发体验,而且应用过大就必须要分包。那这些需求 Rollup 都无法满足。

  • 如果我们是开发一个 JavaScript 框架或者库,那这些优点就特别有必要,而缺点呢几乎也都可以忽略,所以在很多像 React 或者 Vue 之类的框架中都是使用的 Rollup 作为模块打包器,而并非 Webpack


总结一下Webpack 大而全,Rollup 小而美


在对它们的选择上,我的基本原则是:应用开发使用 Webpack,类库或者框架开发使用 Rollup


不过这并不是绝对的标准,只是经验法则。因为 Rollup 也可用于构建绝大多数应用程序,而 Webpack 同样也可以构建类库或者框架。



参考:前端进阶面试题详细解答

Webpack Proxy 工作原理?为什么能解决跨域

1. 是什么


webpack proxy,即webpack提供的代理服务


基本行为就是接收客户端发送的请求后转发给其他服务器


其目的是为了便于开发者在开发模式下解决跨域问题(浏览器安全策略限制)


想要实现代理首先需要一个中间服务器,webpack中提供服务器的工具为webpack-dev-server


2. webpack-dev-server


webpack-dev-serverwebpack 官方推出的一款开发工具,将自动编译和自动刷新浏览器等一系列对开发友好的功能全部集成在了一起


目的是为了提高开发者日常的开发效率,「只适用在开发阶段」


关于配置方面,在webpack配置对象属性中通过devServer属性提供,如下:


// ./webpack.config.jsconst path = require('path')
module.exports = { // ... devServer: { contentBase: path.join(__dirname, 'dist'), compress: true, port: 9000, proxy: { '/api': { target: 'https://api.github.com' } } // ... }}
复制代码


devServetr里面proxy则是关于代理的配置,该属性为对象的形式,对象中每一个属性就是一个代理的规则匹配


属性的名称是需要被代理的请求路径前缀,一般为了辨别都会设置前缀为/api,值为对应的代理匹配规则,对应如下:


  • target:表示的是代理到的目标地址

  • pathRewrite:默认情况下,我们的 /api-hy 也会被写入到 URL 中,如果希望删除,可以使用pathRewrite

  • secure:默认情况下不接收转发到https的服务器上,如果希望支持,可以设置为false

  • changeOrigin:它表示是否更新代理后请求的 headershost地址


2. 工作原理


proxy工作原理实质上是利用http-proxy-middleware 这个http代理中间件,实现请求转发给其他服务器


举个例子:


在开发阶段,本地地址为http://localhost:3000,该浏览器发送一个前缀带有/api标识的请求到服务端获取数据,但响应这个请求的服务器只是将请求转发到另一台服务器中


const express = require('express');const proxy = require('http-proxy-middleware');
const app = express();
app.use('/api', proxy({target: 'http://www.example.org', changeOrigin: true}));app.listen(3000);
// http://localhost:3000/api/foo/bar -> http://www.example.org/api/foo/bar
复制代码


3. 跨域


在开发阶段, webpack-dev-server 会启动一个本地开发服务器,所以我们的应用在开发阶段是独立运行在 localhost的一个端口上,而后端服务又是运行在另外一个地址上


所以在开发阶段中,由于浏览器同源策略的原因,当本地访问后端就会出现跨域请求的问题


通过设置webpack proxy实现代理请求后,相当于浏览器与服务端中添加一个代理者


当本地发送请求的时候,代理服务器响应该请求,并将请求转发到目标服务器,目标服务器响应数据后再将数据返回给代理服务器,最终再由代理服务器将数据响应给本地



在代理服务器传递数据给本地浏览器的过程中,两者同源,并不存在跨域行为,这时候浏览器就能正常接收数据


注意:「服务器与服务器之间请求数据并不会存在跨域行为,跨域行为是浏览器安全策略限制」

HTTP 前生今世


  • HTTP 协议始于三十年前蒂姆·伯纳斯 - 李的一篇论文

  • HTTP/0.9 是个简单的文本协议,只能获取文本资源;

  • HTTP/1.0 确立了大部分现在使用的技术,但它不是正式标准;

  • HTTP/1.1 是目前互联网上使用最广泛的协议,功能也非常完善;

  • HTTP/2 基于 Google 的 SPDY 协议,注重性能改善,但还未普及;

  • HTTP/3 基于 Google 的 QUIC 协议,是将来的发展方向

谈一谈 HTTP 协议优缺点

超文本传输协议, HTTP 是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范


  • HTTP 特点

  • 灵活可扩展 。一个是语法上只规定了基本格式,空格分隔单词,换行分隔字段等。另外一个就是传输形式上不仅可以传输文本,还可以传输图片,视频等任意数据。

  • 请求-应答模式 ,通常而言,就是一方发送消息,另外一方要接受消息,或者是做出相应等。

  • 可靠传输 ,HTTP 是基于 TCP/IP,因此把这一特性继承了下来。

  • 无状态 ,这个分场景回答即可。

  • HTTP 缺点

  • 无状态 ,有时候,需要保存信息,比如像购物系统,需要保留下顾客信息等等,另外一方面,有时候,无状态也会减少网络开销,比如类似直播行业这样子等,这个还是分场景来说。

  • 明文传输 ,即协议里的报文(主要指的是头部)不使用二进制数据,而是文本形式。这让 HTTP 的报文信息暴露给了外界,给攻击者带来了便利。

  • 队头阻塞 ,当 http 开启长连接时,共用一个TCP连接,当某个请求时间过长时,其他的请求只能处于阻塞状态,这就是队头阻塞问题。


http 无状态无连接


  • http 协议对于事务处理没有记忆能力

  • 对同一个url请求没有上下文关系

  • 每次的请求都是独立的,它的执行情况和结果与前面的请求和之后的请求是无直接关系的,它不会受前面的请求应答情况直接影响,也不会直接影响后面的请求应答情况

  • 服务器中没有保存客户端的状态,客户端必须每次带上自己的状态去请求服务器

  • 人生若只如初见,请求过的资源下一次会继续进行请求


http 协议无状态中的 状态 到底指的是什么?!


  • 【状态】的含义就是:客户端和服务器在某次会话中产生的数据

  • 那么对应的【无状态】就意味着:这些数据不会被保留

  • 通过增加cookiesession机制,现在的网络请求其实是有状态的

  • 在没有状态的http协议下,服务器也一定会保留你每次网络请求对数据的修改,但这跟保留每次访问的数据是不一样的,保留的只是会话产生的结果,而没有保留会话

说一说正向代理和反向代理

正向代理


我们常说的代理也就是指正向代理,正向代理的过程,它隐藏了真实的请求客户端,服务端不知道真实的客户端是谁,客户端请求的服务都被代理服务器代替来请求。


反向代理


这种代理模式下,它隐藏了真实的服务端,当我们向一个网站发起请求的时候,背后可能有成千上万台服务器为我们服务,具体是哪一台,我们不清楚,我们只需要知道反向代理服务器是谁就行,而且反向代理服务器会帮我们把请求转发到真实的服务器那里去,一般而言反向代理服务器一般用来实现负载平衡。


负载平衡的两种实现方式?


  • 一种是使用反向代理的方式,用户的请求都发送到反向代理服务上,然后由反向代理服务器来转发请求到真实的服务器上,以此来实现集群的负载平衡。

  • 另一种是 DNS 的方式,DNS 可以用于在冗余的服务器上实现负载平衡。因为现在一般的大型网站使用多台服务器提供服务,因此一个域名可能会对应多个服务器地址。当用户向网站域名请求的时候,DNS 服务器返回这个域名所对应的服务器 IP 地址的集合,但在每个回答中,会循环这些 IP 地址的顺序,用户一般会选择排在前面的地址发送请求。以此将用户的请求均衡的分配到各个不同的服务器上,这样来实现负载均衡。这种方式有一个缺点就是,由于 DNS 服务器中存在缓存,所以有可能一个服务器出现故障后,域名解析仍然返回的是那个 IP 地址,就会造成访问的问题。

UDP 和 TCP 有什么区别

  • TCP 协议在传送数据段的时候要给段标号;UDP 协议不

  • TCP 协议可靠;UDP 协议不可靠

  • TCP 协议是面向连接;UDP 协议采用无连接

  • TCP 协议负载较高,采用虚电路;UDP 采用无连接

  • TCP 协议的发送方要确认接收方是否收到数据段(3 次握手协议)

  • TCP 协议采用窗口技术和流控制


TCP 为什么要三次握手

客户端和服务端都需要直到各自可收发,因此需要三次握手


  • 第一次握手成功让服务端知道了客户端具有发送能力

  • 第二次握手成功让客户端知道了服务端具有接收和发送能力,但此时服务端并不知道客户端是否接收到了自己发送的消息

  • 所以第三次握手就起到了这个作用。`经过三次通信后,服务端



你可以能会问,2 次握手就足够了?。但其实不是,因为服务端还没有确定客户端是否准备好了。比如步骤 3 之后,服务端马上给客户端发送数据,这个时候客户端可能还没有准备好接收数据。因此还需要增加一个过程




TCP 有 6 种标示:SYN(建立联机) ACK(确认) PSH(传送) FIN(结束) RST(重置) URG(紧急)


举例:已失效的连接请求报文段


  • client发送了第一个连接的请求报文,但是由于网络不好,这个请求没有立即到达服务端,而是在某个网络节点中滞留了,直到某个时间才到达server

  • 本来这已经是一个失效的报文,但是server端接收到这个请求报文后,还是会想client发出确认的报文,表示同意连接。

  • 假如不采用三次握手,那么只要server发出确认,新的建立就连接了,但其实这个请求是失效的请求,client是不会理睬server的确认信息,也不会向服务端发送确认的请求

  • 但是server认为新的连接已经建立起来了,并一直等待client发来数据,这样,server 的很多资源就没白白浪费掉了

  • 采用三次握手就是为了防止这种情况的发生,server 会因为收不到确认的报文,就知道client并没有建立连接。这就是三次握手的作用


三次握手过程中可以携带数据吗


  • 第一次、第二次握手不可以携带数据,因为一握二握时还没有建立连接,会让服务器容易受到攻击

  • 而第三次握手,此时客户端已经处于 ESTABLISHED (已建立连接状态) ,对于客户端来说,已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据也是没问题的。


为什么建立连接只通信了三次,而断开连接却用了四次?


  • 客户端要求断开连接,发送一个断开的请求,这个叫作(FIN)。

  • 服务端收到请求,然后给客户端一个 ACK,作为 FIN 的响应。

  • 这里你需要思考一个问题,可不可以像握手那样马上传 FIN 回去?

  • 其实这个时候服务端不能马上传 FIN,因为断开连接要处理的问题比较多,比如说服务端可能还有发送出去的消息没有得到 ACK;也有可能服务端自己有资源要释放。因此断开连接不能像握手那样操作——将两条消息合并。所以,服务端经过一个等待,确定可以关闭连接了,再发一条 FIN 给客户端

  • 客户端收到服务端的 FIN,同时客户端也可能有自己的事情需要处理完,比如客户端有发送给服务端没有收到 ACK 的请求,客户端自己处理完成后,再给服务端发送一个 ACK。



为了确保数据能够完成传输。因为当服务端收到客户端的 FIN 报文后,发送的 ACK 报文只是用来应答的,并不表示服务端也希望立即关闭连接。


当只有服务端把所有的报文都发送完了,才会发送 FIN 报文,告诉客户端可以断开连接了,因此在断开连接时需要四次挥手。


  • 关闭连接时,当收到对方的 FIN 报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了

  • 所以你未必会马上关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送 FIN 报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和 FIN 报文多数情况下都是分开发送的。

diff 算法是怎么运作

每一种节点类型有自己的属性,也就是 prop,每次进行 diff 的时候,react 会先比较该节点类型,假如节点类型不一样,那么 react 会直接删除该节点,然后直接创建新的节点插入到其中,假如节点类型一样,那么会比较 prop 是否有更新,假如有 prop 不一样,那么 react 会判定该节点有更新,那么重渲染该节点,然后在对其子节点进行比较,一层一层往下,直到没有子节点


  • 把树形结构按照层级分解,只比较同级元素。

  • 给列表结构的每个单元添加唯一的key属性,方便比较。

  • React 只会匹配相同 classcomponent(这里面的class指的是组件的名字)

  • 合并操作,调用 componentsetState 方法的时候, React 将其标记为 - dirty.到每一个事件循环结束, React 检查所有标记 dirtycomponent重新绘制.

  • 选择性子树渲染。开发人员可以重写shouldComponentUpdate提高diff的性能


优化⬇️


为了降低算法复杂度,Reactdiff会预设三个限制:


  1. 只对同级元素进行Diff。如果一个DOM节点在前后两次更新中跨越了层级,那么React不会尝试复用他。

  2. 两个不同类型的元素会产生出不同的树。如果元素由div变为p,React 会销毁div及其子孙节点,并新建p及其子孙节点。

  3. 开发者可以通过 key prop来暗示哪些子元素在不同的渲染下能保持稳定。考虑如下例子:


Diff 的思路


该如何设计算法呢?如果让我设计一个Diff算法,我首先想到的方案是:


  1. 判断当前节点的更新属于哪种情况

  2. 如果是新增,执行新增逻辑

  3. 如果是删除,执行删除逻辑

  4. 如果是更新,执行更新逻辑


  • 按这个方案,其实有个隐含的前提——不同操作的优先级是相同的

  • 但是React团队发现,在日常开发中,相较于新增删除更新组件发生的频率更高。所以Diff会优先判断当前节点是否属于更新


基于以上原因,Diff算法的整体逻辑会经历两轮遍历:


  • 第一轮遍历:处理更新的节点。

  • 第二轮遍历:处理剩下的不属于更新的节点。



diff 算法的作用


计算出 Virtual DOM 中真正变化的部分,并只针对该部分进行原生 DOM 操作,而非重新渲染整个页面。


传统 diff 算法


通过循环递归对节点进行依次对比,算法复杂度达到 O(n^3) ,n 是树的节点数,这个有多可怕呢?——如果要展示 1000 个节点,得执行上亿次比较。。即便是 CPU 快能执行 30 亿条命令,也很难在一秒内计算出差异。


React 的 diff 算法


  1. 什么是调和?


将 Virtual DOM 树转换成 actual DOM 树的最少操作的过程 称为 调和 。


  1. 什么是 React diff 算法?


diff算法是调和的具体实现。


diff 策略


React 用 三大策略 将 O(n^3)复杂度 转化为 O(n)复杂度


策略一(tree diff):


  • Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。


策略二(component diff):


  • 拥有相同类的两个组件 生成相似的树形结构,

  • 拥有不同类的两个组件 生成不同的树形结构。


策略三(element diff):


对于同一层级的一组子节点,通过唯一 id 区分。


tree diff


  • React 通过 updateDepth 对 Virtual DOM 树进行层级控制。

  • 对树分层比较,两棵树 只对同一层次节点 进行比较。如果该节点不存在时,则该节点及其子节点会被完全删除,不会再进一步比较。

  • 只需遍历一次,就能完成整棵 DOM 树的比较。



那么问题来了,如果 DOM 节点出现了跨层级操作,diff 会咋办呢?


答:diff 只简单考虑同层级的节点位置变换,如果是跨层级的话,只有创建节点和删除节点的操作。



如上图所示,以 A 为根节点的整棵树会被重新创建,而不是移动,因此 官方建议不要进行 DOM 节点跨层级操作,可以通过 CSS 隐藏、显示节点,而不是真正地移除、添加 DOM 节点


component diff


React 对不同的组件间的比较,有三种策略


  1. 同一类型的两个组件,按原策略(层级比较)继续比较 Virtual DOM 树即可。

  2. 同一类型的两个组件,组件 A 变化为组件 B 时,可能 Virtual DOM 没有任何变化,如果知道这点(变换的过程中,Virtual DOM 没有改变),可节省大量计算时间,所以 用户 可以通过 shouldComponentUpdate() 来判断是否需要 判断计算。

  3. 不同类型的组件,将一个(将被改变的)组件判断为dirty component(脏组件),从而替换 整个组件的所有节点。


注意:如果组件 D 和组件 G 的结构相似,但是 React 判断是 不同类型的组件,则不会比较其结构,而是删除 组件 D 及其子节点,创建组件 G 及其子节点。


element diff


当节点处于同一层级时,diff 提供三种节点操作:删除、插入、移动。


  • 插入:组件 C 不在集合(A,B)中,需要插入

  • 删除:

  • 组件 D 在集合(A,B,D)中,但 D 的节点已经更改,不能复用和更新,所以需要删除 旧的 D ,再创建新的。

  • 组件 D 之前在 集合(A,B,D)中,但集合变成新的集合(A,B)了,D 就需要被删除。

  • 移动:组件 D 已经在集合(A,B,C,D)里了,且集合更新时,D 没有发生更新,只是位置改变,如新集合(A,D,B,C),D 在第二个,无须像传统 diff,让旧集合的第二个 B 和新集合的第二个 D 比较,并且删除第二个位置的 B,再在第二个位置插入 D,而是 (对同一层级的同组子节点) 添加唯一 key 进行区分,移动即��。


总结


  1. tree diff:只对比同一层的 dom 节点,忽略 dom 节点的跨层级移动


如下图,react 只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点不存在时,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。


这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。



这就意味着,如果 dom 节点发生了跨层级移动,react 会删除旧的节点,生成新的节点,而不会复用。


  1. component diff:如果不是同一类型的组件,会删除旧的组件,创建新的组件



  1. element diff:对于同一层级的一组子节点,需要通过唯一 id 进行来区分


  • 如果没有 id 来进行区分,一旦有插入动作,会导致插入位置之后的列表全部重新渲染

  • 这也是为什么渲染列表时为什么要使用唯一的 key。


diff 的不足与待优化的地方


尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,会影响 React 的渲染性能


与其他框架相比,React 的 diff 算法有何不同?



diff 算法探讨的就是虚拟 DOM 树发生变化后,生成 DOM 树更新补丁的方式。它通过对比新旧两株虚拟 DOM 树的变更差异,将更新补丁作用于真实 DOM,以最小成本完成视图更新



具体的流程是这样的:


  • 真实 DOM 与虚拟 DOM 之间存在一个映射关系。这个映射关系依靠初始化时的 JSX 建立完成;

  • 当虚拟 DOM 发生变化后,就会根据差距计算生成 patch,这个 patch 是一个结构化的数据,内容包含了增加、更新、移除等;

  • 最后再根据 patch 去更新真实的 DOM,反馈到用户的界面上。



在回答有何不同之前,首先需要说明下什么是 diff 算法。


  • diff 算法是指生成更新补丁的方式,主要应用于虚拟 DOM 树变化后,更新真实 DOM。所以 diff 算法一定存在这样一个过程:触发更新 → 生成补丁 → 应用补丁

  • React 的 diff 算法,触发更新的时机主要在 state 变化与 hooks 调用之后。此时触发虚拟 DOM 树变更遍历,采用了深度优先遍历算法。但传统的遍历方式,效率较低。为了优化效率,使用了分治的方式。将单一节点比对转化为了 3 种类型节点的比对,分别是树、组件及元素,以此提升效率。

  • 树比对:由于网页视图中较少有跨层级节点移动,两株虚拟 DOM 树只对同一层次的节点进行比较。

  • 组件比对:如果组件是同一类型,则进行树比对,如果不是,则直接放入到补丁中。

  • 元素比对:主要发生在同层级中,通过标记节点操作生成补丁,节点操作对应真实的 DOM 剪裁操作。同一层级的子节点,可以通过标记 key 的方式进行列表对比。

  • 以上是经典的 React diff 算法内容。自 React 16 起,引入了 Fiber 架构。为了使整个更新过程可随时暂停恢复,节点与树分别采用了 FiberNode 与 FiberTree 进行重构fiberNode 使用了双链表的结构,可以直接找到兄弟节点与子节点

  • 然后拿 Vue 和 Preact 与 React 的 diff 算法进行对比

  • PreactDiff 算法相较于 React,整体设计思路相似,但最底层的元素采用了真实 DOM 对比操作,也没有采用 Fiber 设计。Vue 的 Diff 算法整体也与 React 相似,同样未实现 Fiber 设计

  • 然后进行横向比较,React 拥有完整的 Diff 算法策略,且拥有随时中断更新的时间切片能力,在大批量节点更新的极端情况下,拥有更友好的交互体验。

  • Preact 可以在一些对性能要求不高,仅需要渲染框架的简单场景下应用。

  • Vue 的整体 diff 策略与 React 对齐,虽然缺乏时间切片能力,但这并不意味着 Vue 的性能更差,因为在 Vue 3 初期引入过,后期因为收益不高移除掉了。除了高帧率动画,在 Vue 中其他的场景几乎都可以使用防抖和节流去提高响应性能。


学习原理的目的就是应用。那如何根据 React diff 算法原理优化代码呢?这个问题其实按优化方式逆向回答即可。


  • 根据 diff 算法的设计原则,应尽量避免跨层级节点移动。

  • 通过设置唯一 key 进行优化,尽量减少组件层级深度。因为过深的层级会加深遍历深度,带来性能问题。

  • 设置 shouldComponentUpdate 或者 React.pureComponet 减少 diff 次数。

Compositon api

Composition API也叫组合式 API,是 Vue3.x 的新特性。


通过创建 Vue 组件,我们可以将接口的可重复部分及其功能提取到可重用的代码段中。仅此一项就可以使我们的应用程序在可维护性和灵活性方面走得更远。然而,我们的经验已经证明,光靠这一点可能是不够的,尤其是当你的应用程序变得非常大的时候——想想几百个组件。在处理如此大的应用程序时,共享和重用代码变得尤为重要


  • Vue2.0 中,随着功能的增加,组件变得越来越复杂,越来越难维护,而难以维护的根本原因是 Vue 的 API 设计迫使开发者使用watch,computed,methods选项组织代码,而不是实际的业务逻辑。

  • 另外 Vue2.0 缺少一种较为简洁的低成本的机制来完成逻辑复用,虽然可以minxis完成逻辑复用,但是当mixin变多的时候,会使得难以找到对应的data、computed或者method来源于哪个mixin,使得类型推断难以进行。

  • 所以Composition API的出现,主要是也是为了解决 Option API 带来的问题,第一个是代码组织问题,Compostion API可以让开发者根据业务逻辑组织自己的代码,让代码具备更好的可读性和可扩展性,也就是说当下一个开发者接触这一段不是他自己写的代码时,他可以更好的利用代码的组织反推出实际的业务逻辑,或者根据业务逻辑更好的理解代码。

  • 第二个是实现代码的逻辑提取与复用,当然mixin也可以实现逻辑提取与复用,但是像前面所说的,多个mixin作用在同一个组件时,很难看出property是来源于哪个mixin,来源不清楚,另外,多个mixinproperty存在变量命名冲突的风险。而Composition API刚好解决了这两个问题。


通俗的讲:


没有Composition API之前 vue 相关业务的代码需要配置到 option 的特定的区域,中小型项目是没有问题的,但是在大型项目中会导致后期的维护性比较复杂,同时代码可复用性不高。Vue3.x 中的 composition-api 就是为了解决这个问题而生的


compositon api 提供了以下几个函数:


  • setup

  • ref

  • reactive

  • watchEffect

  • watch

  • computed

  • toRefs

  • 生命周期的hooks


都说 Composition API 与 React Hook 很像,说说区别


从 React Hook 的实现角度看,React Hook 是根据 useState 调用的顺序来确定下一次重渲染时的 state 是来源于哪个 useState,所以出现了以下限制


  • 不能在循环、条件、嵌套函数中调用 Hook

  • 必须确保总是在你的 React 函数的顶层调用 Hook

  • useEffect、useMemo等函数必须手动确定依赖关系


而 Composition API 是基于 Vue 的响应式系统实现的,与 React Hook 的相比


  • 声明在setup函数内,一次组件实例化只调用一次setup,而 React Hook 每次重渲染都需要调用 Hook,使得 React 的 GC 比 Vue 更有压力,性能也相对于 Vue 来说也较慢

  • Compositon API的调用不需要顾虑调用顺序,也可以在循环、条件、嵌套函数中使用

  • 响应式系统自动实现了依赖收集,进而组件的部分的性能优化由 Vue 内部自己完成,而React Hook需要手动传入依赖,而且必须必须保证依赖的顺序,让useEffectuseMemo等函数正确的捕获依赖变量,否则会由于依赖不正确使得组件性能下降。


虽然Compositon API看起来比React Hook好用,但是其设计思想也是借鉴React Hook的。

setState 原理分析

1. setState 异步更新


  • 我们都知道,React通过this.state来访问state,通过this.setState()方法来更新state。当this.setState()方法被调用的时候,React会重新调用render方法来重新渲染UI

  • 首先如果直接在setState后面获取state的值是获取不到的。在React内部机制能检测到的地方, setState就是异步的;在React检测不到的地方,例如setInterval,setTimeoutsetState就是同步更新的



因为setState是可以接受两个参数的,一个state,一个回调函数。因此我们可以在回调函数里面获取值



  • setState方法通过一个队列机制实现state更新,当执行setState的时候,会将需要更新的state合并之后放入状态队列,而不会立即更新this.state

  • 如果我们不使用setState而是使用this.state.key来修改,将不会触发组件的re-render

  • 如果将this.state赋值给一个新的对象引用,那么其他不在对象上的state将不会被放入状态队列中,当下次调用setState并对状态队列进行合并时,直接造成了state丢失


1.1 setState 批量更新的过程


react生命周期和合成事件执行前后都有相应的钩子,分别是pre钩子和post钩子,pre钩子会调用batchedUpdate方法将isBatchingUpdates变量置为true,开启批量更新,而post钩子会将isBatchingUpdates置为false


  • isBatchingUpdates变量置为true,则会走批量更新分支,setState的更新会被存入队列中,待同步代码执行完后,再执行队列中的state更新。 isBatchingUpdatestrue,则把当前组件(即调用了 setState的组件)放入 dirtyComponents 数组中;否则 batchUpdate 所有队列中的更新

  • 而在原生事件和异步操作中,不会执行pre钩子,或者生命周期的中的异步操作之前执行了pre钩子,但是pos钩子也在异步操作之前执行完了,isBatchingUpdates必定为false,也就不会进行批量更新



enqueueUpdate包含了React避免重复render的逻辑。mountComponentupdateComponent方法在执行的最开始,会调用到batchedUpdates进行批处理更新,此时会将isBatchingUpdates设置为true,也就是将状态标记为现在正处于更新阶段了。 isBatchingUpdatestrue,则把当前组件(即调用了 setState 的组件)放入dirtyComponents 数组中;否则 batchUpdate 所有队列中的更新


1.2 为什么直接修改 this.state 无效


  • 要知道setState本质是通过一个队列机制实现state更新的。 执行setState时,会将需要更新的 state 合并后放入状态队列,而不会立刻更新state,队列机制可以批量更新state

  • 如果不通过setState而直接修改this.state,那么这个state不会放入状态队列中,下次调用setState时对状态队列进行合并时,会忽略之前直接被修改的state,这样我们就无法合并了,而且实际也没有把你想要的state更新上去


1.3 什么是批量更新 Batch Update


在一些mv*框架中,,就是将一段时间内对model的修改批量更新到view的机制。比如那前端比较火的ReactvuenextTick机制,视图的更新以及实现)


1.4 setState 之后发生的事情


  • setState操作并不保证是同步的,也可以认为是异步的

  • ReactsetState之后,会经对state进行diff,判断是否有改变,然后去diff dom决定是否要更新UI。如果这一系列过程立刻发生在每一个setState之后,就可能会有性能问题

  • 在短时间内频繁setStateReact会将state的改变压入栈中,在合适的时机,批量更新state和视图,达到提高性能的效果


1.5 如何知道 state 已经被更新


传入回调函数


setState({    index: 1}}, function(){    console.log(this.state.index);})
复制代码


在钩子函数中体现


componentDidUpdate(){    console.log(this.state.index);}
复制代码


2. setState 循环调用风险


  • 当调用setState时,实际上会执行enqueueSetState方法,并对partialState以及_pending-StateQueue更新队列进行合并操作,最终通过enqueueUpdate执行state更新

  • performUpdateIfNecessary方法会获取_pendingElement,_pendingStateQueue_pending-ForceUpdate,并调用receiveComponentupdateComponent方法进行组件更新

  • 如果在shouldComponentUpdate或者componentWillUpdate方法中调用setState,此时this._pending-StateQueue != null,就会造成循环调用,使得浏览器内存占满后崩溃


3 事务


  • 事务就是将需要执行的方法使用wrapper封装起来,再通过事务提供的perform方法执行,先执行wrapper中的initialize方法,执行完perform之后,在执行所有的close方法,一组initializeclose方法称为一个wrapper

  • 那么事务和setState方法的不同表现有什么关系,首先我们把4setState 简单归类,前两次属于一类,因为它们在同一调用栈中执行,setTimeout中的两次setState属于另一类

  • setState调用之前,已经处在batchedUpdates执行的事务中了。那么这次batchedUpdates方法是谁调用的呢,原来是ReactMount.js中的_renderNewRootComponent方法。也就是说,整个将React组件渲染到DOM中的过程就是处于一个大的事务中。而在componentDidMount中调用setState时,batchingStrategyisBatchingUpdates已经被设为了true,所以两次setState的结果没有立即生效

  • 再反观setTimeout中的两次setState,因为没有前置的batchedUpdates调用,所以导致了新的state马上生效


4. 总结


  • 通过setState去更新this.state,不要直接操作this.state,请把它当成不可变的

  • 调用setState更新this.state不是马上生效的,它是异步的,所以不要天真以为执行完setStatethis.state就是最新的值了

  • 多个顺序执行的setState不是同步地一个一个执行滴,会一个一个加入队列,然后最后一起执行,即批处理

watch 的理解

watch没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开 deep:true 选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听


注意:Watcher : 观察者对象 , 实例分为渲染 watcher (render watcher),计算属性 watcher (computed watcher),侦听器 watcher(user watcher)三种

Promise

这里你谈 promise的时候,除了将他解决的痛点以及常用的 API 之外,最好进行拓展把 eventloop 带进来好好讲一下,microtask(微任务)、macrotask(任务) 的执行顺序,如果看过 promise 源码,最好可以谈一谈 原生 Promise 是如何实现的。Promise 的关键点在于callback 的两个参数,一个是 resovle,一个是 reject。还有就是 Promise 的链式调用(Promise.then(),每一个 then 都是一个责任人)


  • PromiseES6 新增的语法,解决了回调地狱的问题。

  • 可以把 Promise看成一个状态机。初始是 pending 状态,可以通过函数 resolvereject,将状态转变为 resolved 或者 rejected 状态,状态一旦改变就不能再次变化。

  • then 函数会返回一个 Promise 实例,并且该返回值是一个新的实例而不是之前的实例。因为 Promise 规范规定除了 pending 状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个 then 调用就失去意义了。 对于 then 来说,本质上可以把它看成是 flatMap


1. Promise 的基本情况


简单来说它就是一个容器,里面保存着某个未来才会结束的事件(通常是异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息


一般 Promise 在执行过程中,必然会处于以下几种状态之一。


  • 待定(pending):初始状态,既没有被完成,也没有被拒绝。

  • 已完成(fulfilled):操作成功完成。

  • 已拒绝(rejected):操作失败。


待定状态的 Promise 对象执行的话,最后要么会通过一个值完成,要么会通过一个原因被拒绝。当其中一种情况发生时,我们用 Promisethen 方法排列起来的相关处理程序就会被调用。因为最后 Promise.prototype.thenPromise.prototype.catch 方法返回的是一个 Promise, 所以它们可以继续被链式调用


关于 Promise 的状态流转情况,有一点值得注意的是,内部状态改变之后不可逆,你需要在编程过程中加以注意。文字描述比较晦涩,我们直接通过一张图就能很清晰地看出 Promise 内部状态流转的情况



从上图可以看出,我们最开始创建一个新的 Promise 返回给 p1 ,然后开始执行,状态是 pending,当执行 resolve之后状态就切换为 fulfilled,执行 reject 之后就变为 rejected 的状态


2. Promise 的静态方法


  • all 方法

  • 语法: Promise.all(iterable)

  • 参数: 一个可迭代对象,如 Array

  • 描述: 此方法对于汇总多个 promise 的结果很有用,在 ES6 中可以将多个 Promise.all 异步请求并行操作,返回结果一般有下面两种情况。

  • 当所有结果成功返回时按照请求顺序返回成功结果。

  • 当其中有一个失败方法时,则进入失败方法

  • 我们来看下业务的场景,对于下面这个业务场景页面的加载,将多个请求合并到一起,用 all 来实现可能效果会更好,请看代码片段


// 在一个页面中需要加载获取轮播列表、获取店铺列表、获取分类列表这三个操作,页面需要同时发出请求进行页面渲染,这样用 `Promise.all` 来实现,看起来更清晰、一目了然。

//1.获取轮播数据列表function getBannerList(){ return new Promise((resolve,reject)=>{ setTimeout(function(){ resolve('轮播数据') },300) })}//2.获取店铺列表function getStoreList(){ return new Promise((resolve,reject)=>{ setTimeout(function(){ resolve('店铺数据') },500) })}//3.获取分类列表function getCategoryList(){ return new Promise((resolve,reject)=>{ setTimeout(function(){ resolve('分类数据') },700) })}function initLoad(){ Promise.all([getBannerList(),getStoreList(),getCategoryList()]) .then(res=>{ console.log(res) }).catch(err=>{ console.log(err) })} initLoad()
复制代码


  • allSettled 方法

  • Promise.allSettled 的语法及参数跟 Promise.all 类似,其参数接受一个 Promise 的数组,返回一个新的 Promise唯一的不同在于,执行完之后不会失败,也就是说当 Promise.allSettled 全部处理完成后,我们可以拿到每个 Promise 的状态,而不管其是否处理成功

  • 我们来看一下用 allSettled 实现的一段代码


const resolved = Promise.resolve(2);const rejected = Promise.reject(-1);const allSettledPromise = Promise.allSettled([resolved, rejected]);allSettledPromise.then(function (results) {  console.log(results);});// 返回结果:// [//    { status: 'fulfilled', value: 2 },//    { status: 'rejected', reason: -1 }// ]
复制代码


从上面代码中可以看到,Promise.allSettled 最后返回的是一个数组,记录传进来的参数中每个 Promise 的返回值,这就是和 all 方法不太一样的地方。


  • any 方法

  • 语法: Promise.any(iterable)

  • 参数: iterable 可迭代的对象,例如 Array

  • 描述: any 方法返回一个 Promise,只要参数 Promise 实例有一个变成 fulfilled状态,最后 any返回的实例就会变成 fulfilled 状态;如果所有参数 Promise 实例都变成 rejected 状态,包装实例就会变成 rejected 状态。


const resolved = Promise.resolve(2);const rejected = Promise.reject(-1);const anyPromise = Promise.any([resolved, rejected]);anyPromise.then(function (results) {  console.log(results);});// 返回结果:// 2
复制代码


从改造后的代码中可以看出,只要其中一个 Promise 变成 fulfilled状态,那么 any 最后就返回这个p romise。由于上面 resolved 这个 Promise 已经是 resolve 的了,故最后返回结果为 2


  • race 方法

  • 语法: Promise.race(iterable)

  • 参数: iterable 可迭代的对象,例如 Array

  • 描述: race方法返回一个 Promise,只要参数的 Promise 之中有一个实例率先改变状态,则 race 方法的返回状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 race 方法的回调函数

  • 我们来看一下这个业务场景,对于图片的加载,特别适合用 race 方法来解决,将图片请求和超时判断放到一起,用 race 来实现图片的超时判断。请看代码片段。


//请求某个图片资源function requestImg(){  var p = new Promise(function(resolve, reject){    var img = new Image();    img.onload = function(){ resolve(img); }    img.src = 'http://www.baidu.com/img/flexible/logo/pc/result.png';  });  return p;}//延时函数,用于给请求计时function timeout(){  var p = new Promise(function(resolve, reject){    setTimeout(function(){ reject('图片请求超时'); }, 5000);  });  return p;}Promise.race([requestImg(), timeout()]).then(function(results){  console.log(results);}).catch(function(reason){  console.log(reason);});

// 从上面的代码中可以看出,采用 Promise 的方式来判断图片是否加载成功,也是针对 Promise.race 方法的一个比较好的业务场景
复制代码



promise 手写实现,面试够用版:


function myPromise(constructor){    let self=this;    self.status="pending" //定义状态改变前的初始状态    self.value=undefined;//定义状态为resolved的时候的状态    self.reason=undefined;//定义状态为rejected的时候的状态    function resolve(value){        //两个==="pending",保证了状态的改变是不可逆的       if(self.status==="pending"){          self.value=value;          self.status="resolved";       }    }    function reject(reason){        //两个==="pending",保证了状态的改变是不可逆的       if(self.status==="pending"){          self.reason=reason;          self.status="rejected";       }    }    //捕获构造异常    try{       constructor(resolve,reject);    }catch(e){       reject(e);    }}// 定义链式调用的then方法myPromise.prototype.then=function(onFullfilled,onRejected){   let self=this;   switch(self.status){      case "resolved":        onFullfilled(self.value);        break;      case "rejected":        onRejected(self.reason);        break;      default:          }}
复制代码

TCP 和 UDP 的概念及特点

TCP 和 UDP 都是传输层协议,他们都属于 TCP/IP 协议族:


(1)UDP


UDP 的全称是用户数据报协议,在网络中它与 TCP 协议一样用于处理数据包,是一种无连接的协议。在 OSI 模型中,在传输层,处于 IP 协议的上一层。UDP 有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。


它的特点如下:


1)面向无连接


首先 UDP 是不需要和 TCP 一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。


具体来说就是:


  • 在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了

  • 在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作


2)有单播,多播,广播的功能


UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。


3)面向报文


发送方的 UDP 对应用程序交下来的报文,在添加首部后就向下交付 IP 层。UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文


4)不可靠性


首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。


并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关心对方是否已经正确接收到数据了。


再者网络环境时好时坏,但是 UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。


5)头部开销小,传输数据报文时是很高效的。


UDP 头部包含了以下几个数据:


  • 两个十六位的端口号,分别为源端口(可选字段)和目标端口

  • 整个数据报文的长度

  • 整个数据报文的检验和(IPv4 可选字段),该字段用于发现头部信息和数据中的错误


因此 UDP 的头部开销小,只有 8 字节,相比 TCP 的至少 20 字节要少得多,在传输数据报文时是很高效的。


(2)TCP TCP 的全称是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP 是面向连接的、可靠的流协议(流就是指不间断的数据结构)。


它有以下几个特点:


1)面向连接


面向连接,是指发送数据之前必须在两端建立连接。建立连接的方法是“三次握手”,这样能建立可靠的连接。建立连接,是为数据的可靠传输打下了基础。


2)仅支持单播传输


每条 TCP 传输连接只能有两个端点,只能进行点对点的数据传输,不支持多播和广播传输方式。


3)面向字节流


TCP 不像 UDP 一样那样一个个报文独立地传输,而是在不保留报文边界的情况下以字节流方式进行传输。


4)可靠传输


对于可靠传输,判断丢包、误码靠的是 TCP 的段编号以及确认号。TCP 为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。


5)提供拥塞控制


当网络出现拥塞的时候,TCP 能够减小向网络注入数据的速率和数量,缓解拥塞。


6)提供全双工通信


TCP 允许通信双方的应用程序在任何时候都能发送数据,因为 TCP 连接的两端都设有缓存,用来临时存放双向通信的数据。当然,TCP 可以立即发送一个数据段,也可以缓存一段时间以便一次发送更多的数据段(最大的数据段大小取决于 MSS)

Chrome 打开一个页面需要启动多少进程?分别有哪些进程?

打开 1 个页面至少需要 1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程,共 4 个;最新的 Chrome 浏览器包括:1 个浏览器(Browser)主进程、1 个 GPU 进程、1 个网络(NetWork)进程、多个渲染进程和多个插件进程。


  • 浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。

  • 渲染进程:核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。

  • GPU 进程:其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。

  • 网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。

  • 插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。

TCP 和 UDP 的使用场景

  • TCP 应用场景: 效率要求相对低,但对准确性要求相对高的场景。因为传输中需要对数据确认、重发、排序等操作,相比之下效率没有 UDP 高。例如:文件传输(准确高要求高、但是速度可以相对慢)、接受邮件、远程登录。

  • UDP 应用场景: 效率要求相对高,对准确性要求相对低的场景。例如:QQ 聊天、在线视频、网络语音电话(即时通讯,速度要求高,但是出现偶尔断续不是太大问题,并且此处完全不可以使用重发机制)、广播通信(广播、多播)。

浏览器的垃圾回收机制

(1)垃圾回收的概念

垃圾回收:JavaScript 代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。


回收机制


  • Javascript 具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存。

  • JavaScript 中存在两种变量:局部变量和全局变量。全局变量的生命周期会持续要页面卸载;而局部变量声明在函数中,它的生命周期从函数执行开始,直到函数执行结束,在这个过程中,局部变量会在堆或栈中存储它们的值,当函数执行结束后,这些局部变量不再被使用,它们所占有的空间就会被释放。

  • 不过,当局部变量被外部函数使用时,其中一种情况就是闭包,在函数执行结束后,函数外部的变量依然指向函数内部的局部变量,此时局部变量依然在被使用,所以不会回收。

(2)垃圾回收的方式

浏览器通常使用的垃圾回收方法有两种:标记清除,引用计数。 1)标记清除


  • 标记清除是浏览器常见的垃圾回收方式,当变量进入执行环境时,就标记这个变量“进入环境”,被标记为“进入环境”的变量是不能被回收的,因为他们正在被使用。当变量离开环境时,就会被标记为“离开环境”,被标记为“离开环境”的变量会被内存释放。

  • 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。


2)引用计数


  • 另外一种垃圾回收机制就是引用计数,这个用的相对较少。引用计数就是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是 1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减 1。当这个引用次数变为 0 时,说明这个变量已经没有价值,因此,在在机回收期下次再运行时,这个变量所占有的内存空间就会被释放出来。

  • 这种方法会引起循环引用的问题:例如: obj1obj2通过属性进行相互引用,两个对象的引用次数都是 2。当使用循环计数时,由于函数执行完后,两个对象都离开作用域,函数执行结束,obj1obj2还将会继续存在,因此它们的引用次数永远不会是 0,就会引起循环引用。


function fun() {    let obj1 = {};    let obj2 = {};    obj1.a = obj2; // obj1 引用 obj2    obj2.a = obj1; // obj2 引用 obj1}
复制代码


这种情况下,就要手动释放变量占用的内存:


obj1.a =  null obj2.a =  null
复制代码

(3)减少垃圾回收

虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。


  • 对数组进行优化: 在清空一个数组时,最简单的方法就是给其赋值为[ ],但是与此同时会创建一个新的空对象,可以将数组的长度设置为 0,以此来达到清空数组的目的。

  • object进行优化: 对象尽量复用,对于不再使用的对象,就将其设置为 null,尽快被回收。

  • 对函数进行优化: 在循环中的函数表达式,如果可以复用,尽量放在函数的外面。

URL 有哪些组成部分

一个完整的 URL 包括以下几部分:


  • 协议部分:该 URL 的协议部分为“http:”,这代表网页使用的是 HTTP 协议。在 Internet 中可以使用多种协议,如 HTTP,FTP 等等本例中使用的是 HTTP 协议。在"HTTP"后面的“//”为分隔符;

  • 域名部分

  • 端口部分:跟在域名后面的是端口,域名和端口之间使用“:”作为分隔符。端口不是一个 URL 必须的部分,如果省略端口部分,将采用默认端口(HTTP 协议默认端口是 80,HTTPS 协议默认端口是 443);

  • 虚拟目录部分:从域名后的第一个“/”开始到最后一个“/”为止,是虚拟目录部分。虚拟目录也不是一个 URL 必须的部分。本例中的虚拟目录是“/news/”;

  • 文件名部分:从域名后的最后一个“/”开始到“?”为止,是文件名部分,如果没有“?”,则是从域名后的最后一个“/”开始到“#”为止,是文件部分,如果没有“?”和“#”,那么从域名后的最后一个“/”开始到结束,都是文件名部分。本例中的文件名是“index.asp”。文件名部分也不是一个 URL 必须的部分,如果省略该部分,则使用默认的文件名;

  • 锚部分:从“#”开始到最后,都是锚部分。本例中的锚部分是“name”。锚部分也不是一个 URL 必须的部分;

  • 参数部分:从“?”开始到“#”为止之间的部分为参数部分,又称搜索部分、查询部分。本例中的参数部分为“boardID=5&ID=24618&page=1”。参数可以允许有多个参数,参数与参数之间用“&”作为分隔符。


用户头像

loveX001

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
前端工程师面试题自检_JavaScript_loveX001_InfoQ写作社区