跳至主要內容

前端八股合集

Chilfish大约 53 分钟

前端八股合集

[TOC]

计算机网络

GraphQL

设计准则

  • 查询为分层结构:查询与响应数据一对一匹配的分层和嵌套字段格式,查询和相应的形状类似于树。
  • 以产品为中心,更关心前端希望如何接收数据,并构建交付所需的运行时:前端就可以通过一次请求来获取需要的所有数据,服务器会按照 GraphQL 规范来从不同的端点获取数据
  • 使用特定于应用程序的类型系统:开发人员能在执行前确保查询使用了有效类型并且语法正确,否则查询前就会抛出错误
  • 客户端确切知道会以何种格式接收数据
  • 使用 GraphQL 的服务器结构是内省的

传统 RESTful 架构

REST 的设计范式侧重于分配 HTTP 请求方法和 URL 端点之间的关系

在 REST 中,方法和端点的不同组合有不同的封装功能,返回的数据格式依赖于端点,意味着就不保证能按照前端需要的方式来进行格式化,需要前端自己来做数据解析、数据操作

异同与优缺点

相似

都是设计用于处理 HTTP 请求并为这些请求提供响应的架构

不同

REST API 构建在方法和端点的连接上;

GraphQL API 被设计为只通过一个端点,始终用 POST 进行查询,其 URL 一般是 xxxx/graphql

请求到达 GraphQL 端点以后,客户端请求的载荷就会完全在请求体中处理,提供了比 REST API 更流畅的客户体验,因为后者可能要对多个数据块发多个请求,数据返回后还要进行解析处理

GraphQL 架构成本高,但维护成本低,使用效率高

什么是 HTTP

Hyper Text Transfer Protocol 超文本运输协议,是实现网络通信的一种规范

传输的数据并非计算机底层的二进制包,而是一些有意义的完整的数据,比如 HTML 文件,图片等,能够被上层应用识别

实际应用中,常被用来在浏览器和服务器之间传输数据,以明文形式传递信息

  • 支持 客户/服务器 模式
  • 简单快速,客户请求时只要传递方法和路径
  • 灵活,允许传递各种类型的数据,用 Content-Type 注明即可
  • 无连接,限制每次连接只处理一个请求,服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间
  • 无状态,无法根据之前的状态来决定本次操作

HTTP 版本区别

HTTP1.0 和 HTTP1.1

连接方面
  • HTTP1.0 默认使用非持久连接,而 HTTP1.1 默认使用持久连接
  • 持久连接可以使得多个 HTTP 请求复用同一个 TCP 连接,以此避免使用非持久连接时每次需要建立连接的时延
资源请求方面
  • HTTP1.0 中,存在着一些浪费带宽的对象
    • 无法只发送对象的一部分
    • 无法断点续传
  • HTTP1.1 中在请求头引入了 range 头域,允许只请求资源的某一部分,对应的返回码是 206(Partial Content)
缓存方面
  • HTTP1.0 中使用 header 里面的 If-Modified-SinceExpires 来作为缓存判断的标准
  • HTTP1.1 引入了更多缓存控制策略,例如 EtagIf-Match 等等
其他方面
  • HTTP1.1 新增了 host 字段,用来指定服务器的域名
    • HTTP1.0 默认每台服务器都只有一个唯一的 IP 地址,因此请求消息中的 URL 不包含主机名,也就是 hostname,但现在一台物理服务器可以存在多个虚拟主机,共享同一个 IP ,这是就需要 host 字段来区分
  • HTTP1.1 新增了很多请求方法,比如 PUTDELETEOPTIONS等等
  • HTTP1.0 只有GETPOSTHEAD

HTTP1.1 和 HTTP2.0

数据格式
  • HTTP2 是一个二进制协议,头信息和数据体都是二进制,统称为,这也是多路复用的基础
  • HTTP1.1 报文的头信息必须是文本,数据体则可以是文本也可以是二进制
多路复用
  • HTTP2 实现了多路复用,即在一个连接里,客户端和服务器都可以同时发送多个请求或者回应,不用按照顺序一一发送,解决了队头堵塞的问题
数据流
  • HTTP2 使用了数据流的概念:数据包不是按照顺序发送的,同一个连接里面的两个连续的数据包,可能属于不同的请求,因此要对数据包做标记指出它属于哪一个请求
头信息压缩
  • HTTP1.1 不带状态,所以每次请求都必须附加上所有信息,请求的很多字段都是重复的,比如 Cookie,会浪费带宽、影响速度
  • HTTP2 引入了头信息压缩机制
    • 头信息使用 gzip 或者 compress 压缩后再发送
    • 客户端和服务器端同时维护一张头信息表,所有字段都存入这个表,生成索引号,以后只需要发送索引号
服务器主动推送
  • HTTP2 允许服务器主动向客户端发送资源
  • 主动推送的是静态资源,和 WebSocket 发送即时数据不同

HTTPS 如何保证安全

什么是 HTTPS

HTTP + SSL / TLS = HTTPS

如何加密

  • 对称加密:采用协商好的密钥来加密数据
  • 非对称加密:实现身份认证和密钥协商
  • 摘要算法:验证信息的完整性
  • 数字签名:身份验证
对称加密

加密和解密使用同一个密钥,只要保证了密钥的安全性,就保证了整个通信过程的机密性

非对称加密

存在两个密钥:公钥与私钥

公钥公开给外界,私钥则需保密

公钥私钥均可以用以加密解密,但公钥加密后只能用私钥解密,私钥加密后只能用公钥解密

混合加密

在 HTTPS 通信中,实际采用的是对称加密 + 非对称加密

前面提到,在对称加密中,最关键的是需要保证密钥的安全,而 HTTPS 就采用非对称加密来解决密钥交换的问题

发送密文的一方,使用接收方的公钥,对这个“对称”的密钥进行加密处理;接收方再用自己的私钥解密,拿到这个密钥

数字签名

引入了第三方可信证书机构,确保公开密钥的安全性

HTTP 状态码

状态码分类

  • 1 表示消息
  • 2 表示成功
  • 3 表示重定向
  • 4 表示请求错误
  • 5 表示服务器错误

常用状态码

  • 100: 客户端在通过 POST 请求发送数据给服务器前,先征询服务器情况,看服务器是否处理 POST 请求
  • 206: 表示服务器成功处理了部分请求,一般用来做大文件的断点续传
  • 301: 永久重定向,新域名替换旧域名
  • 302: 临时重定向,常见于未登录用户跳转到登录注册页面
  • 304: 协商缓存,告诉客户端可以使用缓存数据
  • 400: 参数有误,服务器无法识别
  • 403: 客户被禁止访问,常见在要求内网权限
  • 404: 服务器找不到资源;服务器不想说明拒绝请求的理由
  • 503: 停机维护
  • 504: 网关超时

HTTP 请求方法

  • GET 向服务器请求数据
  • POST 将实体提交到指定的资源
  • PUT 上传文件,更新数据
  • DELETE 删除服务器上的对象
  • HEAD 获取报文首部,不返回报文主体
  • OPTIONS 询问支持的请求方法,用于跨域请求
  • CONNECT 要求通信的时候建立隧道
  • TRACE 回显服务器收到的请求

DNS

什么是 DNS

Domain Names System ,进行域名和与之相对应的 IP 地址转换的服务

查询过程

  • 首先检索浏览器的 DNS 缓存,缓存中维护了一张域名与 IP 对照的表格
  • 若没有命中,搜索操作系统的 DNS 缓存
  • 若没有命中,操作系统就将域名发送到本地域名服务器,本地域名服务器递归查询自己的 DNS 缓存,成功则返回
  • 若没有命中,向上级服务器进行迭代查询
    • 本地域名服务器向根域名服务器发起请求,根域名服务器返回顶级域名服务器的地址给本地服务器
    • 本地服务器拿到顶级域名服务器的地址,就发起请求获取权限域名服务器的地址
    • 本地服务器向权限域名服务器发起请求,最终得到 IP 地址
  • 本地域名服务器返回 IP 地址给操作系统,同时自己缓存起 IP 地址
  • 操作系统将 IP 地址返回给浏览器,自己也缓存
  • 浏览器得到 IP 地址,同时更新缓存
img

CDN

什么是 CDN

CDN,Content Delivery Network,内容分发网络

简单来说, CDN 就是根据用户位置分配一个最近的资源,用户上网时就不需要直接访问源站,而是访问边缘节点,一个缓存了源站内容的代理服务器

原理

应用 CDN 前
  • 用户提交域名
  • 浏览器对域名进行解释
  • DNS 解析得到目的主机的 IP 地址
  • 根据 IP 地址访问发出请求
应用 CDN 后

应用 CDN 以后,DNS 返回的就不是一个 IP 地址,而是一个 CNAME 别名记录,指向 CDN 的全局负载均衡,CNAME 在域名解析的过程中承担了中间人的角色

负载均衡系统

  • 根据用户 IP 地址,查表得到地理位置,找近的边缘节点
  • 看用户所在的运营商网络,找相同网络的边缘节点
  • 检查边缘节点的负载情况,选择负载较轻的节点

综合以上,得到并返回一个最合适的边缘节点给用户

缓存代理

缓存系统会有选择地缓存那些最常用的那些资源

缓存代理有两个指标

  • 命中率:用户访问的资源恰好在缓存系统里,可以直接返回给用户,命中次数与所有访问次数之比
  • 回源率:缓存里没有,必须用代理的方式回源站取,回源次数与所有访问次数之比

现在的商业 CDN 命中率都在 90% 以上,相当于把源站的服务能力放大了 10 倍以上

UDP 和 TCP 的区别

UDP

面向数据报的通信协议,对于应用层传递下来的报文,加个首部就交给网络层

  • 利用 IP 提供面向无连接的通信服务
  • 传输途中丢包也不会重发
  • 乱序也无法纠正
  • 无法进行流量控制、拥塞控制

TCP

可靠的面向字节流的通信协议,把数据看作无结构的字节流来发送

  • 分地实现了数据传输时各种控制功能,可以进行丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制

区别

TCPUDP
可靠性可靠不可靠
连接性面向连接无连接
报文面向字节流面向报文
效率传输效率低传输效率高
双工性点对点全双工一对一、一对多、多对一、多对多
控制流量控制、拥塞控制

用途

TCP 用于

  • SMTP,电子邮件传输
  • TELNET,远程终端接入
  • HTTP,万维网
  • FTP,文件传输

UDP 用于

  • DNS,域名转换
  • NFS,远程文件服务器
  • 音视频传输

GET 和 POST 的区别

概念

GET:请求一个资源的指定形式,只被用于获取数据

POST:用于将实体提交到指定的资源,通常导致在服务器上的状态变化或者副作用

本质上都是 TCP 链接

区别

  • GET 在浏览器回退时无害,POST 会再次提交请求
  • GET 请求会被浏览器主动缓存,而 POST 需要手动设置了才会
  • GET 请求只能进行 URL 编码,POST 则支持多种
  • GET 请求参数会被完整保留在历史记录里面,POST 则不会
  • GET 请求在 URL 中传送的参数有长度限制,POST 没有
  • GET 不安全,因为参数直接暴露在 URL,POST 的则放在 Request body 中。但从数据传输的角度来说,都是不安全的,想要安全要用 HTTPS 加密

JavaScript

Map 和 Set

Set

Set 是什么

Set 是 ES6 新增的一种数据结构,一般称为集合

类似于数组,但是成员都是唯一的

Set 的方法
  • add(value):在尾部添加一个元素,返回该 Set
  • delete(value):移除一个指定元素,若移除成功返回 true ,若本来就没有这个元素,返回 false
  • has(value):返回一个布尔值,判断元素是否存在
  • clear():移除所有元素
  • entries():返回一个新的迭代器,键 === 值
  • keys():返回新的迭代器,包含按插入顺序排列的所有元素的值
  • values():和 keys() 一样
Set 的属性
  • size:Set 结构的元素个数不能用 length,而是 size

Map

Map 是什么

Map 类型是键值对的有序列表,键和值都可以是任意类型

Map 的方法
  • get(key):返回与 key 相关联的值,不存在则返回 undefined
  • delete(key):移除定键值对,若移除成功返回 true ,若本来就没有这个元素,返回 false
  • has(key):返回一个布尔值
  • clear():移除所有键值对
  • set(key, value):新增键值对,并返回该 Map 对象
Map 和 Object

Object 和 Map 都允许按键存取值、删除键值、检测键值等,所以以往一直把对象当 Map 使用

那么为什么我们还需要 Map 呢?在某些情况,Map 就会是更好的选择

意外的键:

  • 一个 Object 有它的原型对象,原型链上可能会有冲突的已存在的键名;
  • Map 只包含显式插入的键
  • 可以用 Object.create(null) 来创建一个没有原型的对象

键的类型:

  • Map 的键可以是任意类型
  • Object 的键只能是 String 或者 Symbol

键的顺序:

  • Map 的键是有序的

Size:

  • Map 的键值对个数可以通过 size 属性获取
  • Object 的只能手动计算

迭代:

  • Map 是可迭代的

性能:

  • 频繁增删键值对时,Map 的性能更好

事件模型

事件、事件流

JS 中的事件,可以简单理解为 HTML 文档或者浏览器中发生的交互操作,如加载事件,鼠标事件等等

事件流有三个阶段

  • 事件捕获
  • 事件执行(处于目标)
  • 事件冒泡

img

什么是事件模型

原始事件模型
  • HTML 代码直接绑定

    <input
      type="button"
      onclick="fun()"
    />
    
  • JS 代码绑定

    var btn = document.getElementById('.btn')
    btn.onclick = fun
    

特点

  • 绑定速度快,这属于 DOM0 级的事件,会以最快的速度绑定
  • 因此只支持冒泡不支持捕获
  • 同一个类型事件只可以绑定一次,后来的事件会覆盖之前的
  • 删除只需要把对应事件设置 null
标准事件模型

该事件就会经历事件流的三个阶段

  • 事件捕获:事件从 document 向下传到目标元素,期间依次检查途径节点是否绑定了事件监听函数,有则执行
  • 事件处理:到达目标元素,触发监听函数
  • 事件冒泡:从目标函数冒泡到 document 期间依次检查途径节点是否绑定了事件监听函数,有则执行
IE 事件模型

没有事件捕获阶段

this

this 是函数运行时自动生成的一个内部对象,只能在函数内部使用,指向调用它的对象

绑定规则

默认绑定

全局对象默认绑定到 window (非严格模式下,严格模式 this 会是 undefined

隐式绑定

函数可以作为某个对象的方法来调用,此时 this 自然就指向调用该方法的对象

new 绑定

通过 new 生成一个新的实例对象, this 就指向这个实例对象

显示修改

apply call bind

绑定优先级

new 显示 隐式 默认

箭头函数

ES6 推出了箭头函数,让我们在代码书写的时候就能确定 this 的指向

也因此,箭头函数不能作为构造函数使用

闭包

什么是闭包

当函数可以记住并访问所在的词法作用域时,就产生了闭包

每创建一个函数,闭包就会在函数创建的同时被创建出来

闭包的作用

能够在函数定义的作用域以外,使用函数定义作用域内的局部变量,并不会污染全局

闭包的原理

基于作用域链以及垃圾回收机制,通过维持函数作用域的引用,让函数作用域可以在当前作用域外被访问到

数组常用操作

创建指定大小的二维数组

const arr = new Array(n)
for (let i = 0; i < arr.length; i++) {
  arr[i] = new Array(m).fill(0)
}

数字转数组

Array.from()

先转字符串再转数组,转数组的过程中把字符转为数字

123456 → ‘123456’ → [1, 2, 3, 4, 5, 6]

const number = 123456

const arr = Array.from(String(number), (n) => Number(n))
map()

与上面思路一致

const number = 123456

const arr = String(number)
  .split('')
  .map((n) => {
    return Number(n)
  })

数组最后一个元素

arr1.slice(-1)[0]

arr2[arr2.length - 1]

arr3.at(-1) // 浏览器暂不支持

[...arr4].pop()

[...arr5].reverse()[0]

数组去重

// 解构 + set
const unique0 = (arr) => {
  return [...new Set(arr)]
}

// Array.from + set
const unique1 = (arr) => {
  return Array.from(new Set(arr))
}

// 排序去重, 但是如果要求顺序不变,还原的时候得多做一层
const unique2 = (arr) => {
  arr.sort()
  const res = []
  for (let i = 0; i < arr.length; i++) {
    // 和前一个一样就不加进 res
  }
  return res
}

// indexOf
const unique3 = (arr) => {
  let res = []
  for (let i = 0; i < arr.length; i++) {
    if (res.indexOf(arr[i]) === -1) {
      res.push(arr[i])
    }
  }
  return res
}

setTimeout 实现 setInterval

为什么要这样做

定时器指定的时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是何时执行代码。所以真正何时执行代码的时间是不能保证的,取决于何时被主线程的事件循环取到,并执行。

每个 setTimeout 产生的任务会直接 push 到任务队列中;而 setInterval 在每次把任务 push 到任务队列前,都要进行一下判断(看上次的任务是否仍在队列中,如果有则不添加,没有则添加)。

实现

function mySetInterval() {
  mySetInterval.timer = setTimeout(() => {
    arguments[0]()
    mySetInterval(...arguments)
  }, arguments[1])
}

mySetInterval.clear = function () {
  clearTimeout(mySetInterval.timer)
}

mySetInterval(() => {
  console.log('d')
}, 1000)

setTimeout(() => {
  // 5s 后清理
  mySetInterval.clear()
}, 5000)

数据类型

有哪些数据类型

基本数据类型:

  • String
  • Number
  • Boolean
  • undefined
  • null
  • Bigint
  • Symbol

引用数据类型

  • Object
  • Array
  • Function
  • Regexp

typeof

instanceof

通用类型判断

function typeOf(target) {
  return Object.prototype.toString.call(target).slice(8, -1).toLowerCase()
}

类型转换机制

显式转换

  • Number()

    • null --> 0
    • undefined --> NaN
    • object --> toPrimitive, toNumber
  • parseInt()

    逐个转换,遇到不能转的字符就停下来

  • String()

  • Boolean()

隐式转换

  • 自动转 boolean
  • 自动转 string
    • 复合类型转为原始类型,再转为字符串
    • 常见在 + 运算
  • 自动转 number
    • 除了+ ,其他运算符都会转数值

原型和原型链

原型

JavaScript 是一门基于原型的语言

当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾

每个函数都有一个特殊的属性,叫做原型,prototype

原型对象有一个自有属性 constructor ,指向函数

原型链

原型对象也可以有原型,并从中继承方法和属性,一层层就构成了原型链,在对象实例和构造器见有一个链接 (proto) ,通过此来上溯原型链

Promise

什么是 Promise

一个 Promise 对象,代表在这个对象创建出来时,不一定已知值的代理

它让我们能够把异步操作的最终返回值或者错误原因和相应的处理程序关联起来,就使得异步方法可以和同步方法一样返回值,即:异步方法并不会真的立即返回真正的值,而是返回一个待解决的 Promise 以便在未来某些时刻解决了以后交给相对应的处理程序

一个 Promise 对象,必然处于以下三个状态之一

  • pending 初始状态,没被兑现也没被拒绝
  • fulfilled 操作已经完成
  • rejected 操作失败

pending 态可以被兑现或者被拒绝,转化成 fulfilled 或者 rejected ,此时, then 方法排列起来的处理程序就会被链式调用,或者 catch 方法排列起来的错误处理程序被调用

链式调用

因为 catch 和 then 返回的也是一个 Promise ,所以就可以链式调用,很好地解决了回调地狱的问题,使代码结构更加清晰

继承

什么是继承

继承,是面向对象的软件技术中的一个概念

继承可以使得子类具有父类的各种属性和方法,不需要再次编写相同的代码

子类继承父类的同时,也一样可以重新定义一些属性或者重写一些方法,使其能够获得与父类不同的功能

实现方式

原型链继承

较常见的继承方式,原理就是原型链

function Parent() {
  this.name = 'parent1'
  this.play = [1, 2, 3]
}
function Child() {
  this.type = 'child2'
}
Child1.prototype = new Parent()
console.log(new Child())

但这种方法存在一个缺陷:两个实例共享同一个原型对象,内存空间是共享的

构造函数继承
组合继承
寄生组合式继承

apply, call & bind

apply

-Function.prototype.myApply = function (context, arr) {
|    context = context || window
|    context.fn = this
|    const result = context.fn(...arr)
|    delete context.fn
|    return result
|}

call

-Function.prototype.myCall = function  (context) {
|    context = context || window
|    context.fn = this
|    const args = [...arguments.slice(1)]
|    const result = context.fn(...args)
|    delete context.fn
|    return result
|}

bind


防抖节流

new

new 是什么

在 JavaScript 中,new 操作符用于创建一个给定构造函数的实例对象

创建出来的实例对象可以访问到构造函数中的属性,可以访问到构造函数原型链中的属性

new 的流程

首先分析 new 具体做了哪些工作

  • 创建一个新的对象
  • 将对象与构造函数通过原型链连接起来
  • 将构造函数中的 this 绑定到新建的对象上
  • 根据构造函数的返回类型来做判断
  • 不是对象则返回新建的那个对象
  • 是对象就返回 result

手写 new 操作符

function myNew(Func, ...args) {
  const obj = {}
  obj.__proto__ = Func.prototype
  const res = Func.apply(obj, args)
  return res instanceof Object ? res : obj
}

TypeScript

什么是 TypeScript

TypeScript 是 JavaScript 的超集

img

是一种静态类型检查的语言,提供了类型注解,在编译阶段就能捕获类型错误

扩展了 JavaScript 的语法,所有现有 JS 程序可以不做改变地在 TS 下工作

会被编译成 JavaScript

数据类型

  • boolean

  • number

  • string

  • null & undefined

    是所有类型的子类型

  • any

    编程阶段还不清楚类型,不希望类型检查器对其进行检查

  • void

    用于表示某方法没有返回值

  • never

    表示从不出现的值

    返回 never 的函数必须存在无法达到的终点

  • array

    const arr0: string[] = ['1', '2']
    const arr1: Array<string> = ['1', '2']
    
  • tuple

    元组,表示一个已知元素数量和类型的数组

    let tuple0: [number, string, boolead]
    tuple0 = [1, '1', true] // fine
    tuple0 = [1, '1'] // bad
    
  • enum

    为一组数值赋予友好的名字,主要用途是提高代码可读性

    enum Color {
      Red,
      Blue,
      Green,
    }
    const c: Color = Color.Green
    
  • object

type & interface

相同点

  • 都可以描述一个对象或者函数
  • 都可以拓展

不同点

type only
  • type 可以声明基本类型别名,联合类型,元组等类型
  • type 语句中还可以使用 typeof 获取实例的 类型进行赋值
interface only
  • interface 能够声明合并

总结

interface 是接口,type 是类型

希望定义一个变量类型,就用 type,如果希望是能够继承并约束的,就用 interface。

不知道该用哪个,说明只是想定义一个类型而非接口,所以应该用 type。

枚举

定义

枚举是一个被命名的整型常数的集合,用于声明一组命名的常数,ß 当一个变量有几种可能的取值时,可以将它定义为枚举类型

使用

数字枚举

当我们声明一个枚举类型是,虽然没有给它们赋值,但是它们的值其实是默认的数字类型,而且默认从 0 开始依次累加

如果我们将第一个值进行赋值后,后面的值也会根据前一个值进行累加 1

字符串枚举

如果设定了一个变量为字符串之后,后续的字段也需要赋值字符串,否则报错:

异构枚举

即将数字枚举和字符串枚举结合起来混合起来使用

场景

后端返回的字段有时候可能是一些无意义的数字,可以用枚举来一一对应

泛型

定义

泛型是程序设计语言的一种风格或者范式

允许我们在强类型程序设计语言编写代码时使用一些以后才指定的类型:在实例化时作为参数指明。在 TS 中,常见的就是定义函数、接口或者类的时候,不预先定义好具体的类型,而是使用的时候才指定类型的一种特性

function returnItem<T>(param: T): T {
  return param
}

泛型就给予开发者创造出灵活的方便重用的代码的能力

不预先定义好具体的类型,而在使用的时候在指定类型的一种特性的时候,这种情况下就可以使用泛型

使用方式

泛型通过<>的形式进行表述,可以声明:

  • 函数
  • 接口
函数声明
function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]]
}

swap([7, 'seven']) // ['seven', 7]

定义泛型时,可以一次定义多个类型参数

接口声明
interface IReturnItem<T> {
  (param: T): T
}
类声明

浏览器

垃圾回收机制

内存泄漏

内存泄漏,指的是没有正常回收该被回收的对象,导致他们常驻于内存,使得内存占用越来越高,导致应用程序卡顿甚至崩溃

常见原因
  • 创建了全局变量,且没有手动回收
  • 事件监听器 / 定时器 / 闭包未正常清理
  • 用 JS 对象做缓存,且没有过期策略和大小控制

V8 垃圾回收

分代垃圾收集

V8 中所有 JS 对象都是通过堆来分配内存,管理的堆被分成了两代:新生代,老生代,新生代又可以细分为两个子代:Nursery 和 Intermediate

Mark-Compact

进程和线程

概念

进程是资源分配的最小单位,线程是 CPU 调度的最小单位

进程中任一线程出错,都会导致整个进程崩溃

线程间共享进程中的数据

进程关闭后,操作系统会回收内存及申请的资源

进程之间的内容是相互隔离的

Chrome 中的进程和线程

  • 浏览器进程:负责界面显示,用户交互,存储,子进程管理
  • 渲染进程:将 HTML CSS JS 文件转换成网页,每个 Tab 都有一个渲染进程
  • GPU 进程,网页的 UI 都采用 GPU 来绘制
  • 网络进程:负责网络资源的加载
  • 插件进程:负责插件的运行,因为插件容易崩溃,所以插件需要独立的进程来与页面隔离

进程与线程的区别

  • 进程可以看成是独立的应用
  • 进程是资源分配的最小单位,线程是 CPU 调度的最小单位
  • 线程间共享进程中的数据,进程之间的隔离的
  • 进程切换开销大,线程切换开销小

字段

  • Name:Cookie 的名称
  • Value:Cookie 的值
  • Size:Cookie 的大小
  • Path:能够访问该 Cookie 的路径
  • Secure:是否通过 HTTPS 来传输 Cookie
  • Domain:能够访问该 Cookie 的域名。
    • Cookie 并不严格遵守同源策略,一个子域可以设置或获取父域的 Cookie
  • Expires / Max-Size:Cookie 的超时时间,若不设置,则默认和 session 一起失效,也就是关闭浏览器后失效

特性

  • 创建成功后就无法修改名称
  • 每个域名下的 Cookie 不能超过 20 个
  • 每个 Cookie 的大小不能超过 4 kb

使用场景

和 session 结合使用,把 sessionId 存储到 Cookie 中,浏览器每次发请求携带 sessionId 向服务端表明身份

统计页面点击次数

缓存

本地缓存

对于某些数据和请求,可以直接在业务代码侧进行缓存处理,比如利用 localStorage sessionStorage 等等

内存缓存

有时候会出现一个资源被使用多次的情况,比如图标。对于已经存储在内存中的资源,再去请求就是多此一举了

Cache API

当本地存储和内存都没有命中,也并非就直接开始发请求获取数据了。

Service Worker 是一个后台运行的独立线程,能够在代码里手动启用

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('./sw.js').then(() => {})
}

可以利用 Service Worker 对所有网络请求进行拦截,查看是否有缓存的请求内容,有则直接返回缓存,Cache API 提供的缓存是永久性的,不会随浏览器关闭而消失

// sw.js
self.addEventListenr('fetch', (e) => {
  e.respondWith(
    caches
      .match(e.request)
      .then((cache) => {
        return cache || fetch(e.request)
      })
      .catch((err) => {
        return fetch(e.request)
      }),
  )
})

HTTP 缓存

如果 Service Worker 里也没有命中缓存,就要考虑 HTTP 缓存

  1. 强缓存

    强缓存中,浏览器不向服务器发请求,而是直接从本地读取内容,此处本地一般指硬盘

    相关响应头

    • Expires: 设置一个过期时间,浏览器与本地时间对比,判断是否过期
    • Cache-Control: 设置一个 max-age 来控制过期时间
  2. 协商缓存

    简单来说,就是请求文件前先问一下服务器: 我本地的文件过期了吗?

    相关请求 / 相应头

    • Last-Modified: 服务器返回最后修改时间
    • If-Modified-Since: 客户端询问是否自此修改,返回 200 即为更新了, 304 即没更新
    • ETag: 更精确的标识,一般用文件的 MD5 作为 ETag
    • If-None-Match: 与 ETag 相结合使用

Push Cache

HTTP 还是没有检查到缓存,就会到最后一关, Push Cache

HTTP2 出现以前,服务器没有主动推送能力,而现在,在请求一个资源的同时,服务器考虑到你将来可能会用到某些资源,就提前为你推送

多标签页通信

本质上都是通过中介者模式来实现的

  • websocket 协议,利用其服务器推送的功能,服务器就可以充当中介者,标签页向服务器推送数据,服务器再转发数据给其他标签页
  • SharedWorker ,会在页面存在的生命周期内创建一个唯一的线程,并且多页面也是共用这个线程,利用这个共享的线程自然就可以通信
  • localStorage ,通过监听本地存储来实现通信

跨域

跨域问题的来源,是浏览器为了安全而引入的基于同源策略的安全特性

同源:协议、主机名和端口都相同才是同源

跨域是浏览器的限制,实际上请求是正常发出和相应的

解决方案

CORS 跨域资源代理

服务端在相应头中添加 Access-Control-Allow-* 头,告知浏览器通过,前端不需要做相应改动

CORS 将请求分成了简单请求和需预检请求

  • 简单请求

    满足以下条件即为简单请求

    • 请求方法是 GET POST HEAD 之一
    • 请求头只包含 Accept Accept-Language Content-Language Content-Type

    对于简单请求,浏览器就会直接发送 CORS 请求,会在头信息里面加一个 Origin 字段,说明本次请求来自哪一个源

    如果源在许可范围内,服务器返回的响应就会多出一些信息头

    Access-Control-Allow-Origin: http://api.bob.com  // 和Orign一致
    Access-Control-Allow-Credentials: true   // 表示是否允许发送Cookie
    Access-Control-Expose-Headers: FooBar   // 指定返回其他字段的值
    Content-Type: text/html; charset=utf-8   // 表示文档类型
    

    否则,就只是单纯正常返回 HTTP 响应,则浏览器没有找到上述信息,就会报跨域的错误

  • 需预检请求

    不满足上述条件,即为需预检请求。浏览器就会先向服务端发送一个 OPTIONS 请求,来判定请求是否被允许

    OPTIONS 请求的头信息中除了 Origin 以外,还包含另两个字段

    • Access-Control-Request-Method: 列出浏览器的 CORS 请求可能用到哪些 HTTP 方法
    • Access-Control-Request-Headers: 指定浏览器 CORS 请求会额外发送的头信息
反向代理

反向代理解决跨域问题的方案依赖同源的服务端对请求做一个转发处理,将请求从跨域请求转换成同源请求

JSONP

原理:<script> 标签没有跨域限制,通过它的 src 属性,发送带 callback 参数的 GET 请求,服务端返回数据拼接到 callback 中返回给浏览器

具有很大局限性,只支持 GET 方法

同时不安全,很容易受到 XSS 攻击

渲染原理

渲染步骤

  • 解析接收到的文档,构建一棵 DOM 树
  • 对 CSS 进行解析,生成 CSSOM 规则树
  • 根据这两个树来构建渲染树,渲染树的节点被称作渲染对象,是一个个包含样式的矩形,和 DOM 元素相对应,但对应关系不一定是一对一的
  • 生成渲染树以后,他们还没有位置和大小,浏览器就会自动根据渲染树来布局,也称自动回流
  • 布局结束后就是绘制阶段

渲染优化

针对 JS 文件

由于 JS 脚本可以操作 DOM 节点,所以他会阻碍解析。

  • 把它放在 body 最后
  • body 中间尽量不写 script 标签
  • 利用 async 和 defer 异步加载
    • async 在下载完成后,立即异步加载,加载后立即执行,无法保证多个 async 标签的顺序
    • defer 在下载完成后,立即异步加载,加载后,等待 DOM 树解析完毕再执行,多个 defer 标签也会严格按照顺序执行
针对 CSS 文件
  • link: 浏览器会派发一个新等线程去加载资源文件,与此同时渲染线程仍会继续向下渲染
  • @import: 渲染进程会暂时停止渲染,去服务器加载资源文件
  • style: 直接渲染

所以为了保证渲染速度,css 文件一般放在 head 中,导入外部文件时尽量使用 link ,如果 css 少,尽可能直接写在 style 标签中

针对 DOM 树
  • HTML 文件的代码层级不要太深
  • 使用语义化的标签
  • 减少选择器嵌套层级
减少重排重绘
  • 尽量在低层级的 DOM 进行操作
  • 不要使用 table 布局
  • 不要频繁操作元素的样式,对于一些静态页面,建议直接修改类名而非样式
  • 使用 absolute 或者 fixed 使元素脱离文档流,他们的变化就不会影响其他元素
  • 避免频繁操作 DOM ,可以创建一个文档片段 documentFragment ,在其上应用所有 DOM 操作,最后再添加到文档中(虚拟 DOM 的思想
  • 将多个读操作或者写操作放在一起,不要读写穿插写
优化关键渲染路径

首次渲染的速度很影响体验,要最大限度减小这三种可变因素

  • 关键资源的数量
  • 关键路径长度
  • 关键字节的数量

常规方法如下

  • 对关键路径进行分析和特性描述
  • 最大限度减少关键资源数量,例如可以把他们标记为异步加载
  • 优化字节数以缩短下载时间
  • 优化其余关键资源的加载顺序

什么是预解析

当执行 JavaScript 脚本时,另一个线程解析剩下的文档,并加载后面需要通过网络加载的资源。这种方式可以使资源并行加载从而使整体速度更快。

预解析并不改变 DOM 树,它将这个工作留给主解析过程,自己只解析外部资源的引用,比如外部脚本、样式表及图片。

CSS

  • link 是 HTML 标签
  • @import 是 CSS 提供的
  • link 引入的样式是在页面加载的时候加载,@import 引入的样式在页面加载完成后加载
  • link 没有兼容性问题,而 @import 不兼容 ie5 及以下
  • link 可以通过 js 操作 DOM 动态引入样式表改变样式,@import 不可以

1px 问题

一些概念

像素:由一个数字序列表示的图像中的一个最小单元,单位是 px ,不可再次分割

设备像素比 DPR = 设备像素 / 设备独立像素

CSS 像素,也叫虚拟像素,指的是 CSS 代码中使用的逻辑像素。在 CSS 规范中,有绝对长度和相对长度单位, px 是一个相对单位,相对于设备像素

设备像素,也叫物理像素,指的是设备能控制显示的最小物理单位

设备独立像素,也叫逻辑像素,可以认为是计算机坐标系统中的一个点,越小则越清晰

移动端 1px 问题解决

解决方案一

利用伪元素 ::after + transform 进行缩放

解决方案二

增加媒体查询,分别对不同 DPR 的设备进行不同的缩放

元素居中

水平居中

行内元素

text-align: center

块级元素
// 自动边距
 {
  margin: 0 auto;
}
// flex布局
 {
  display: flex;
  justify-content: center;
}
// 绝对定位
 {
  position: absolute;
  left: 50%;
  transform: translate(-50%, 0);
}

垂直居中

行内元素
.parent {
  height: h;
}
.child {
  line-height: h;
}
块级元素
 {
  position: absolute;
  top: 50%;
  transform: translate(0, -50%);
}
 {
  position: absolute;
  top: 0;
  bottom: 0;
  margin: auto 0;
}
 {
  position: absolute;
  top: 50%;
  height: h;
  margin-top: -0.5h;
}
 {
  display: flex;
  align-items: center;
}

BFC

Blocking Formatting Context 块级格式化上下文,是页面中的一块渲染区域,有自己的独立的渲染规则,目的在于形成一个相对于外界的独立的空间,让内部的子元素不影响到外界元素

触发条件

  • 根元素
  • 浮动元素
  • overflow 不为 visible
  • position 是 absolute 或者 fixed
  • display 不是 block 或 inline

应用场景

防止 margin 塌陷

同一个 BFC 中的两个相邻的盒子的 margin 会发生塌陷

对其中一个多包裹一层容器,将其变为新的一个 BFC

清除内部浮动
多栏布局

移动端适配

rem

rem: font size of the root element

是指相对于根元素字体大小的单位,是一个相对单位

1 rem 就相当于 html 中指定的字体大小,默认一般是 16px

Tailwind

什么是 Tailwind

Tailwind 是一个工具型的 CSS 框架,即 Utility CSS,它提供的 CSS Class 让我们可以设计、自定义任何组件

最大的特点就是上手难度低,开发体验好

Tailwind 的优势

简洁

对比行内样式,tailwind 最大的优势,自然就是简洁

样式规范

在使用 Tailwind 的时候,除了某些特殊情况需要自定义样式,大多数时候都是在使用官方制定好的样式规范,便自然而然地会写出有规范的样式

功能优先

没错,再也不用为了起一个好的类名想破头了。

除此之外,如果是多人维护的项目,如果没事先规范好的话,一堆类名实在是看不懂,维护和交接都有很大困难。

而使用 Tailwind 以后,所有的 class 都是功能性的。

方便的自定义

虽然说 Tailwind 给了一套完整且规范的 Utility Class 库,但有时我们不免需要自己制定一些特殊的样式,比如颜色,阴影等等。

此时就只需要在 tailwind.config.js 中自己增加几条规则即可。

轻量化

Tailwind 本身提内置了一个 purge 功能:只要设定完成,在项目 build 阶段就会自动将没有用到的 style 去除,最终 build 后的 css 文件一般都会很小,在 10kb 以内

好用的插件扩展

Tailwind 简化了样式的书写,但对于初学者往往会不清楚自己该怎么编写,就得来回在文档搜寻,降低效率。

Tailwind 就提供了 VScode 的相对应的插件,有自动提示功能,颜色还能在提示中预览。

深色模式

使用 Tailwind 能很方便地为自己的网站 / APP 添加深色模式

<div class="bg-white dark:bg-gray-800">
  <h1 class="text-gray-900 dark:text-white">Dark mode is here!</h1>
  <p class="text-gray-600 dark:text-gray-300">Lorem ipsum...</p>
</div>
其他

除此之外,Tailwind 还有许多其他的优势,就不一一详细说明

  • 响应式设计
  • 对伪类的良好支持
  • 提取组件
  • 函数和指令
  • 可以添加新的功能类

Tailwind 的缺点

不允许字符串拼接

这就给开发平添了一层复杂性,带有一定的风险,但可以靠引入其他库,比如 classNames 来解决

样式覆盖问题
<div class="red blue"></div>

上面的这个 div 是蓝色还是红色?并不能通过代码顺序确定,而是通过在内置的属性中的顺序确定

雪碧图

什么是雪碧图

雪碧图,CSS Sprites

将多张较小的图片合并到一张大的图片上,大的图片背景是透明的,使用的时候直接将大图作为背景图片,根据不同的 background-position 来定位具体要展示的图片

试想一个网站有很多小图,那么用户打开网站,大量的小图引起了大量的请求,不仅对服务器造成了比较大的压力,对用户而言,图片的显示效果也不佳

优点

  • 降低了服务器压力
  • 减少了网络请求,加快页面渲染

缺点

  • 后期维护困难
  • 对图片位置的计算很严格
  • 只能用作背景图片,局限性大

改进

如上文说到,雪碧图的缺点主要是应用太麻烦了

但如果有帮我们自动化打包、定位的工具,那雪碧图就很好用:像使用普通图片一样使用雪碧图

  • 自动读取雪碧图目录下的文件合成大图
  • 记录下每个小图的尺寸位置
  • 提供一个工具函数来使用雪碧图

React

React Fiber

Fiber 是什么

Fiber 是 React 16 中采用的新的协调(Reconciler)引擎,支援虚拟 DOM 的渐进式渲染

Fiber 将原有的 Stack Reconciler 替换为 Fiber Reconciler,提高了复杂应用的可响应性和性能:

  • 对大型复杂任务分片
  • 对任务划分优先级,优先调度高优先级的任务
  • 调度过程中,可以对任务进行挂起、恢复、终止等操作

从 React 原理说起

一个 React 组件的渲染主要经历两个阶段:

  • 调度阶段(Reconciler):用新的数据生成一棵新的树,然后通过 Diff 算法,遍历旧的树,快速找出需要更新的元素,放到更新队列中去,得到新的更新队列
  • 渲染阶段(Renderer):遍历更新队列,通过调用宿主环境的 API,实际更新渲染对应的元素。宿主环境如 DOM,Native 等

Fiber 主要更新的就是调度阶段的处理方式

Fiber 对比之前的 Stack

React 16 之前使用的是 Stack Reconciler(栈协调器),使用递归的方式创建虚拟 DOM,递归的过程是不能中断的

这个时候,如果组件的层级过深,递归更新组件的时间超过一帧,就会出现掉帧卡顿的现象

React 16 之后就改用了 Fiber Reconciler 纤维协调器,将递归中无法中断的更新重构为迭代中的异步可中断更新过程,这样就能够更好的控制组件的渲染

Fiber 的工作原理

在 Fiber 中,一个大的任务会被分片,每一个任务片的运行时间都很短,虽然总任务时间不变,但这种方式就使得唯一的线程不会被独占,每个任务都能在这段时间内得到执行的机会

同时,为了完成渐进式渲染,Fiber 架构引入了新的数据结构 Fiber Node 和 Fiber Node Tree

{
    tag: TypeOfWork, // fiber 类型
    type: 'div', // 和 fiber 相关的组件类型
    return: Fiber | null, // 父节点
    child: Fiber | null, // 子节点
    sibling: Fiber | null, // 同级节点
    alternate: Fiber | null, // diff 的变化记录
    ...
}

工作流程:

  • ReactDOM.render() 引导 React 启动或调用 setState() 的时候,就开始创建或更新 Fiber Node Tree
  • 从根节点开始遍历 Fiber Node Tree, 并且构建 workInProgress Tree,这就叫做调和阶段
    • 在这个阶段可以暂停、终止或重启
    • React 会生成两棵树,一棵是代表当前状态的 current tree,一棵是待更新的 workInProgress tree
    • 遍历 current tree,重用或更新 Fiber Node 到 workInProgress tree,workInProgress tree 完成后会替换 current tree
    • 每更新一个节点,同时生成该节点对应的 Effect List
    • 为每个节点创建更新任务
  • 将创建的更新任务加入任务队列,等待调度
    • 调度由 scheduler 模块完成,其核心职责是执行回调
    • scheduler 模块实现了跨平台兼容的 requestIdleCallback
    • 每处理完一个 Fiber Node 的更新,可以中断、挂起,或恢复
  • 根据 Effect List 更新 DOM
    • React 会遍历 Effect List 将所有变更一次性更新到 DOM 上
    • 这一阶段的工作会导致用户可见的变化。因此该过程不可中断,必须一直执行直到更新完成

React 生命周期

生命周期流程

React 的生命周期可以分为三个阶段。

创建阶段

创建阶段主要有以下几个生命周期方法

  • constructor
  • getDerivedStateFromProps
  • render
  • componentDidMount

constructor

创建实例过程会自动调用的方法。在方法内部,通过super关键字获取来自父组件的props

在该方法中,通常进行的操作是

  • 初始化state
  • this上挂载方法

getDerivedStateFromProps

是一个静态的方法,不能访问到组件的实例

执行时机:组件的创建和更新阶段,不论是props还是state变化都会调用

在每次render之前调用,第一个参数是即将更新的props, 第二个参数为上一个状态的state

返回一个新对象作为新的state,若不需要更新,返回null

render

用于渲染DOM结构,是类组件必须实现的方法

可以访问stateprops属性

不能在方法内部setState,否则会触发死循环导致内存崩溃

componentDidMount

组件挂载到真实的DOM节点后执行,顺序在render

用于执行一些数据获取、事件监听的操作

更新阶段

该阶段的函数主要为如下方法:

  • getDerivedStateFromProps
  • shouldComponentUpdate
  • render
  • getSnapshotBeforeUpdate
  • componentDidUpdate

getDerivedStateFromProps

同创建阶段

render

也与创建阶段介绍的一致

shouldComponentUpdate

告知组件本身,基于当前的propsstate是否需要重新渲染组件

执行时机:propsstate更新时都会调用,通过返回一个布尔值告知是否需要重新渲染

render一样,不能调用setState

getSnapshotBeforeUpdate

该周期函数在render后执行,执行时DOM元素还没有被更新

该方法返回的一个Snapshot值,作为componentDidUpdate第三个参数传入

此方法的目的在于获取组件更新前的一些信息,比如组件的滚动位置之类的,在组件更新后可以根据这些信息恢复一些 UI 视觉上的状态

componentDidUpdate

执行时机:组件更新结束

该方法可以根据前后的propsstate变化来做出相应的操作,如获取数据,修改DOM样式等等

卸载阶段

componentWillUnmount

此方法用于组件卸载前,清理一些注册的监听事件,或者取消订阅的网络请求等

一旦一个组件实例被卸载,其不会被再次挂载,而只可能是被重新创建

新旧生命周期对比

新版的生命周期减少了以下三种方法:

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

新增了两个方法:

  • getDerivedStateFromProps
  • getSnapshotBeforeUpdate

Hooks

底层原理

调用时序

Hooks 通过链表的结构存储,严格按照时序执行,以保证 Hooks 之间数据不会错乱。正因如此, Hooks 不能“时有时无”,也就是说,不能放在循环或者条件语句中

初始化的时候,都会执行 mountWorkInProgressHook

每执行一个 Hook 函数,就会产生一个 Hook 对象,里面保存了 Hook 的信息,然后将一个个 Hook 以链表的形式串接起来,并赋值给 workInProgressmemoizedState

所以在函数组件执行后, Hooks 和 workInProgress 关系如下

图片

useState

setState 后发生了什么

React 会将传入的参数对象与组件当前的状态合并,然后触发调和过程(Reconciliation)。经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个 UI 界面

在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染。在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染

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

setState 是同步的还是异步的

setState 默认是异步的,一些情况是同步的

  • 在 React 可以控制的地方,比如生命周期事件以及合成事件,就是异步的,会合并操作、延迟更新
  • 在 React 无法控制的地方,比如原生事件,addEventListenr, setTimeout 等等,就是同步更新

设计为默认异步,可以很好地提高性能:每一次改变 state 都会带来一次 render ,所以批量更新 state 可以减少 render 次数

React18 改进了原生事件中的 setState ,现在原生事件也可以批处理了,可以认为 setState 现在就是真正的异步操作

在 React18 之前也有方法可以在原生事件中进行批处理的 setState ,unstable_batchedUpdates

第二个参数

setState 第二个参数是可选的,一个回调函数,会在组件重新渲染后执行,帮助我们拿到更新后的 state 的值

useEffect

useContext

有些时候,想从某个祖先节点传递一个数据给一个后代节点,如果通过 props 传递的话,会一层层经过很多不必要的组件,带来很多麻烦以及浪费,context api 就是解决这个问题的

createContext

首先,就需要创建一个上下文,订阅了该上下文的组件,就可以拿到上下文中提供的数据

const MyContext = createContext(defaultValue)

如果需要使用这个创建出来的上下文,就需要通过 Context.Provider 最外层包装组件,然后显示地通过 <MyContext.Provider value={{xx: xx}}> 来传 value ,指定 context 要对外暴露的信息

useContext

创建出上下文了,接下来就是要获取上下文

子组件中可以通过 useContext 这个 Hook 来获取 Provider 提供的内容

useMemo

useCallback

useCallback 的作用是避免子组件不必要的 reRender

在 React 中,默认情况下,父组件重新渲染的时候,它的所有子组件也会重新渲染,但其实这些 state 的变化其实跟子组件是没有任何关系的,所以子组件的重新渲染毫无意义

要优化这种情况,就需要用到 useCallback ,用 useCallback 把函数包裹起来后,在父组件中只有当 deps 变化以后,子组件才会跟着 reRender

useRef

useRef 主要的作用,就是帮助我们获取到 DOM 元素或者组件实例,它还可以用来保存在组件生命周期内不会变化的值

  • 每次渲染时, useRef 的返回值都是同一个
  • ref.current 变化时,不会造成重新渲染

事件合成

React 并不是将 click 事件绑定到了 div 的真实 DOM 上,而是在 document 处监听了所有的事件,当事件发生并且冒泡到 document 处的时候,React 将事件内容封装并交由真正的处理函数运行。这样的方式不仅仅减少了内存的消耗,还能在组件挂载销毁时统一订阅和移除事件

除此之外,冒泡到 document 上的事件也不是原生的浏览器事件,而是由 React 自己实现的合成事件(SyntheticEvent)。因此如果不想要是事件冒泡的话应该调用 event.preventDefault() 方法,而不是调用 event.stopPropgation() 方法。

合成事件抹平了浏览器之间的兼容问题,另外这是一个跨浏览器原生事件包装器,赋予了跨浏览器开发的能力;

对于原生浏览器事件来说,浏览器会给监听器创建一个事件对象。如果你有很多的事件监听,那么就需要分配很多的事件对象,造成高额的内存分配问题。但是对于合成事件来说,有一个事件池专门来管理它们的创建和销毁,当事件需要被使用时,就会从池子中复用对象,事件回调结束后,就会销毁事件对象上的属性,从而便于下次复用事件对象。

虚拟 DOM

什么是虚拟 DOM

虚拟 DOM 可以看做一棵模拟了 DOM 树的 JavaScript 对象树

为什么要用虚拟 DOM

旧的传统的 Web 应用中,每次微小的数据变动都会引起 DOM 树的重新渲染

虚拟 DOM 就可以把所有操作累加起来,统计出计算出来的所有变化以后,统一一次性地更新 DOM 树

01.png

虚拟 DOM 的原理

节点更新后,虚拟 DOM 会比较新旧两棵 DOM 树的区别,保证最小化的 DOM 操作,保证了执行效率

React Diff

算法时间复杂度:O(n)

tree diff

因为 DOM 节点很少出现跨层级的移动操作,所以 React 就通过 updateDepth 来对虚拟 DOM 树进行层级控制:只会对相同层级的 DOM 节点比较;当发现节点不存在,该节点以及子节点会被完全删除

component diff
  • 如果组件是同一类型的,就按照原策略比较虚拟 DOM 树
  • 如果不是,就把组件判断为 dirty component ,从而替换组件下所有子节点
  • 对同类型的组件,很有可能其虚拟 DOM 并没有变化,所以 React 提供 shouldComponentUpdate 方法来判断组件是否要进行 diff 操作
element diff

当节点属于同一层级,就开始进行 element diff

总共有三种操作:插入,移动,删除

05.png
diff 总结
  • 将复杂度优化到了 O(n)

  • 分层求异

  • 相同类生成相似树形结构,不同类生成不同树形结构

  • 设置唯一 key

    key 是 React 用来追踪列表中哪些元素被修改、被添加或者被移除的辅助标识,开发过程中需保证 key 在同级元素中的唯一性

对比真实 DOM

  • 简单方便,不需要再去手动操作真实 DOM
  • 性能优化,有效地避免了 DOM 树频繁更新,减少多次重绘与回流
  • 跨平台,React 借助虚拟 DOM 得到了跨平台的能力
  • 性能要求极高的话还是需要手动操作真实 DOM 进行极致优化
  • 首次渲染 DOM 时,由于多了一层虚拟 DOM 的计算,速度会稍慢

React 组件

函数式组件和类组件

受控组件和非受控组件

受控组件

受我们控制的组件,组件的状态全程响应外部数据

非受控组件

不受我们控制的组件,初始化的时候接受外部数据,然后在自己内部存储状态

组件通信方法

父子组件
父向子

直接在子组件标签内传递参数,子组件通过 props 就可以接收到

子向父

父组件传递一个函数给子组件,通过这个函数的回调,拿到子组件传过来的值

兄弟组件

利用共同的父组件作为中间层

祖孙组件

利用 context 来共享数据

无关系组件

使用一些全局的状态管理库,比如 redux valtio 等等

React Native

RN 中有三个主要的模块

  • 平台层:负责原生组件的渲染、提供各式各样的原生能力
  • 桥接层:负责解析 JS 代码,解决 JS 和 Java / OC 互调,由 C++ 实现
  • JS 层:负责页面具体的逻辑

桥接层

web 应用中,浏览器既是 JS 执行引擎,也是渲染引擎,但在 RN 应用中,渲染层是基于平台原生的能力,所以只需要再实现一个执行引擎,RN 选择的是 JSCore 作为执行引擎,平台层想要拿到 JS 的模块和回调函数,就得通过桥接层暴露的 C++ 的接口获取

JSCore

JSCore 是桥接层中的主要模块,它是 RN 架构中的 JS 引擎,负责 JS 代码的加载和解析

通信

Native 和 JS 向对方提供能力以及通信,都是以模块为功能单位的

  • Native 模块由平台层代码实现,JS 通过模块的 ID 和方法的 ID 调用
  • JS 模块向平台层提供操作 JS 环境的 API
  • JS 环境中维护了一份 Native 模块的 ID 映射表,平台层也一样,二者实际是只是在和 C++ 模块通信

RN 中的线程

  • MAIN / UI 线程:运行的主应用程序线程,应用程序的 UI 可以由主线程更改
  • Shadow Thread:计算创建的布局,是一个后台线程
  • Javascript 线程:执行 JS 代码

渲染过程

  • 启动应用,主线程开始执行,并加载 JS 代码
  • JS 代码加载成功后,主线程将其发送到 JS 线程
  • React 开始渲染的时候,就开始进行 diff 操作,生成一个新的虚拟 DOM 布局,就发给 Shadow 线程计算
  • 计算出布局,就把布局参数发送给主线程
  • 主线程在屏幕上渲染内容

桥 Bridge

定义

React Native 中有一个 Bridge 用来将 Native 和 JS 环境黏合在一起

前端工程化

Webpack

什么是 Webpack

Webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具

静态模块,指开发阶段可以被 Webpack 直接引用,直接打包进 bundle.js 的资源

构建流程

Webpack 的运行时一个串行的过程,工作流程就是把各个插件串联起来,运行过程中会广播事件,插件只需要监听它关心的事件

  • 初始化参数,从配置文件和 Shell 语句中得到最终参数
  • 开始编译,使用上一步的参数初始化 Compiler ,加载所有的配置插件,执行 run 方法开始编译
  • 确定入口,根据配置的 entry 找到入口文件
  • 编译模块,从入口文件出发,调用所有配置的 Loader 对模块进行翻译,递归执行本步骤直到所有入口依赖的文件都经过处理
  • 完成编译,此时已经知道了每个模块翻译后的内容以及依赖关系
  • 输出资源,根据依赖关系,组装成一个个包含多模块的 Chunk 加入到输出列表
  • 输出完成,根据配置的路径和文件名把文件内容写入到文件系统

Webpack 的热更新

热更新⼜称热替换(Hot Module Replacement),缩写为 HMR, 可以做到不⽤刷新浏览器⽽将新变更的模块替换掉旧的模块。

Babel

原理

  • 解析,将代码解析生成抽象语法树 AST
  • 转换,对 AST 再进行一系列操作,通过 bable-traverse 对其遍历,进行添加、更新以及移除等操作
  • 生成,讲变换后的 AST 通过 bab le-generator 再转换回 JS 代码

Next.js

SSR

什么是 SSR

优势

  • 性能提高,用户加载的代码更少
  • 对 SEO 友好,爬虫更容易看到网站

其他

base64

优缺点

优点:

  • 二进制数据,比如图片,转换成字符串,方便数据传输
  • 对数据进行了简单的加密
  • 可以有效减少 http 请求

缺点:

  • 编码后体积变大
  • 编码和解码耗费额外工作量