浏览器相关知识点

Web 应用趋势

  • 应用程序 Web 化,视频 , 音频 ,游戏的占比越来越高

  • 是 Web 应用移动化, Google 推出了 PWA 方案来整合 Web 和本地程序各自的优势

  • Web 操作系统化 ,可能以后浏览器会提供更多的模块给上层应用使用,如新支持的 WebAssembly;

OSI 七层网络模型

  • 应用层: HTTP,HTTPS,FTP,NFS,FMTP,DHCP,SNMP,TELNET,POP3,IRC,NNTP 这些应用层协议为应用提供服务,可以执行用户活动。
  • 表示层: 从应用层接受数据,并将 ASCII 码转化为 EBCDIC 码(二进制),执行数据压缩(有损/无损),使用 SSL (加密/解密)保证数据安全。
  • 会话层: 会话管理,身份验证,授权方面
  • 传输层:TCP,UDP, 流量控制,数据校验
  • 网络层:IP 逻辑寻址
  • 数据链层: 数据单元被称之为帧
  • 物理层:传输物理信号

进程与线程

进程是一个程序的运行实例,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。 线程是依附于进程的,而进程中使用多线程并行处理能提升运算效率。

进程和线程之间的关系有以下特点:

  • 进程中的任意一线程执行出错,都会导致整个进程的崩溃。

  • 线程之间可以对进程的公共数据进行读写操作。

  • 当一个进程关闭之后,操作系统会回收进程所占用的内存。

    虽然有些程序因为代码原因,或安装了额外的插件,可能导致内存泄漏.但只要关闭进程,内存就会被回收.

  • 进程之间的内容相互隔离。

    进程隔离是为保护操作系统中进程互不干扰的技术,每一个进程只能访问自己占有的数据.所以一个进程如果崩溃了,或者挂起了,是不会影响到其他进程的。如果进程之间需要进行数据的通信,这时候,就需要使用用于进程间通信(IPC)的机制了。

目前浏览器主要包含以下几个进程, 查看 Chrome 的进程类型

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

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

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

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

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

渲染进程的个数如何计算:

  • 如果从一个标签页中打开了另一个新标签页,当新标签页和当前标签页属于同一站点的话,那么新标签页会复用当前标签页的渲染进程。 Chrome 浏览器会将浏览上下文组中属于同一站点的标签分配到同一个渲染进程中,要同时满足 A 和 B 属于同一站点,且 A 和 B 之间有链接关系
    <a> 元素的 rel 属性为 noopener noreferrer 时会阻止使用同一个渲染进程。
  • chrome 浏览器实现了站点隔离,所以 iframe 也会遵循以下的渲染规则,如果和父页面属于同一站点,且有链接关系,也会运行在相同的渲染进程中。

请求的各个阶段

DNS 域名解析过程

域名使用 . 划分, 最后由根域名服务器统一管理,根域名服务器是一个服务器集群,有十几个根域名,但是有一千多台根域名服务器,这就需要通过任播技术找到最近的一台根域名服务器。

顶级域名服务器会管理 .net .com 这些域名, 权威域名服务器会管理 .qq baidu 这些域名。

  • 发起解析前会检查本地是否有缓存文件。

  • 调用 DNS 客户端发起查询请求,本地的 DNS 服务端一般有互联网服务提供商(ISP)管理,DNS 服务端会查询是否有缓存,如果有则返回带有 Non-authoritative 字段的数据。如果是根域名服务器则返回带有 Authoritative 的字段。缓存需要定期更新。
    DNS 迭代查询的时候使用的时 UDP 请求,数据量比较小,可以用一个 UDP 包返回,比较注重效率。更新缓存时使用的时 TCP 查询。

  • 获取最近的根域名服务器的地址,查询 .com 域名的服务器地址。

  • 获取最近的 .com 服务器的地址,查询 baidu.com 的服务器地址。

  • 获取最近的 baidu.com 权威域名服务器的地址,查询 mail.baidu.com 对应的 IP。

  • 可能会返回 mail.baidu.com 对应的 CDN(内容分发服务器) 地址,这就是最终的地址。

DNS 预解析

dns-prefetch(DNS 预获取)是前端网络性能优化的一种措施。它根据浏览器定义的规则,提前解析之后可能会用到的域名,使解析结果缓存到系统缓存中,缩短 DNS 解析时间,进而提高网站的访问速度。

每当在首次 DNS 解析后会对其 IP 进行缓存。至于缓存时长,每种浏览器都不一样,比如 Chrome 的过期时间是 1 分钟,在这个期限内不会重新请求 DNS。

每当 Chrome 浏览器启动的时候,就会自动的快速解析浏览器最近一次启动时记录的前 10 个域名。所以经常访问的网址就不存在 DNS 解析的延迟,进而打开速度更快。

  • dns-prefetch 仅对跨域域上的 DNS 查找有效,因此请避免使用它来指向相同域。这是因为,到浏览器看到提示时,您站点域背后的 IP 已经被解析。

  • http 页面下所有的 a 标签的 href 都会自动去启用 DNS Prefetch,也就是说网页的 a 标签 href 带的域名,是不需要在 head 里面加上 link 手动设置的。

    可以通过在服务器端发送 X-DNS-Prefetch-Control 报头,或是在文档中使用值为 http-equiv 的 <meta> 标签:

    1
    <meta http-equiv="x-dns-prefetch-control" content="on" />
  • 强制查询特定主机名,使用 rel 属性值为 link type 中的 dns-prefetch 的 标签来对特定域名进行预读取:

    1
    <link rel="dns-prefetch" href="http://www.baidu.com/" />
Keep-Alive

通常情况下,一旦服务器向客户端返回了请求数据,它就要关闭 TCP 连接。不过如果浏览器或者服务器在其头信息中加入了:

1
Connection:Keep-Alive

那么 TCP 连接在发送后将仍然保持打开状态,这样浏览器就可以继续通过同一个 TCP 连接发送请求。保持 TCP 连接可以省去下次请求时需要建立连接的时间,提升资源加载速度。比如,一个 Web 页面中内嵌的图片就都来自同一个 Web 站点,如果初始化了一个持久连接,你就可以复用该连接,以请求其他资源,而不需要重新再建立新的 TCP 连接。

如何保证页面文件能被完整地送达浏览器?

在网络中,一个文件通常会被拆分为很多数据包来进行传输,而数据包在传输过程中又有很大概率丢失或者出错。

联网,实际上是一套理念和协议组成的体系架构。其中,协议是一套众所周知的规则和标准,如果各方都同意使用,那么它们之间的通信将变得毫无障碍。

IP:把数据包送达目的主机

数据包要在互联网上进行传输,就要符合网际协议(Internet Protocol,简称 IP)标准, 访问网站就是向另一台有明确地址的计算机请求信息.

想通信一定要知道双方 IP 地址, 这些附加的信息会被装进一个叫 IP 头的数据结构里。IP 头是 IP 数据包开头的信息,包含 IP 版本、源 IP 地址、目标 IP 地址、生存时间等信息

  • 将要发送的数据交给网络进程
  • 创建 IP 头并附加到数据包上,并交给底层传输
  • 底层通过物理网络,将数据发送到 B
  • 数据被传输到 B 的网络层,会拆开 IP 头信息,并将数据交给上层处理
UDP:把数据包送达应用程序

到达主机后还需要把数据送给应用,,需要基于 IP 之上开发能和应用打交道的协议,最常见的是 用户数据包协议(User Datagram Protocol),简称 UDP。

UDP 中一个最重要的信息是端口号,端口号其实就是一个数字,每个想访问网络的程序都需要绑定一个端口号。通过端口号 UDP 就能把指定的数据包发送给指定的程序了,所以 IP 通过 IP 地址信息把数据包发送给指定的电脑,而 UDP 通过端口号把数据包分发给正确的程序。和 IP 头一样,端口号会被装进 UDP 头里面,UDP 头再和原始数据包合并组成新的 UDP 数据包。UDP 头中除了目的端口,还有源端口号等信息。

虽然 UDP 可以校验数据是否正确,但是对于错误的数据包,UDP 并不提供重发机制,只是丢弃当前的包,而且 UDP 在发送之后也无法知道是否能达到目的地。虽说 UDP 不能保证数据可靠性,但是传输速度却非常快,所以 UDP 会应用在一些关注速度、但不那么严格要求数据完整性的领域,如在线视频、互动游戏等。

  • 传输层会在数据包前面附加上 UDP 头,组成新的 UDP 数据包,再将新的 UDP 数据包交给网络层
  • 网络层再将 IP 头附加到数据包上,组成新的 IP 数据包,并交给底层;
  • 数据包被传输到主机 B 的网络层,在这里主机 B 拆开 IP 头信息,并将拆开来的数据部分交给传输层;
  • 在传输层,数据包中的 UDP 头会被拆开,并根据 UDP 中所提供的端口号,把数据部分交给上层的应用程序;
TCP:确保数据包的完整性

使用 UDP 来传输会存在两个问题:

  • 数据包在传输过程中容易丢失;

  • 大文件会被拆分成很多小的数据包来传输,这些小的数据包会经过不同的路由,并在不同的时间到达接收端,而 UDP 协议并不知道如何组装这些数据包,从而把这些数据包还原成完整的文件。

基于这两个问题,我们引入 TCP 了。TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。相对于 UDP,TCP 有下面两个特点:

  • 对于数据包丢失的情况,TCP 提供重传机制;

  • TCP 引入了数据包排序机制,用来保证把乱序的数据包组合成一个完整的文件。

既然要实现重传和数据包排序,必然需要一套链接机制,这也就是三次握手,和四次挥手

  • 首先,建立连接阶段。这个阶段是通过三次握手来建立客户端和服务器之间的连接。TCP 提供面向连接的通信传输。面向连接是指在数据通信开始之前先做好两端之间的准备工作,通过三次握手建立链接.

  • 其次,传输数据阶段。在该阶段,接收端需要对每个数据包进行确认操作,也就是接收端在接收到数据包之后,需要发送确认数据包给发送端。所以当发送端发送了一个数据包之后,在规定时间内没有接收到接收端反馈的确认消息,则判断为数据包丢失,并触发发送端的重发机制。同样,一个大的文件在传输过程中会被拆分成很多小的数据包,这些数据包到达接收端后,接收端会按照 TCP 头中的序号为其排序,从而保证组成完整的数据。

  • 最后,断开连接阶段。数据传输完毕之后,就要终止连接了,涉及到最后一个阶段“四次挥手”来保证双方都能断开连接。

流量控制:通过滑动窗口,取值为 min(拥塞窗口,接受窗口);
拥塞控制:慢启动(从 1 开始指数增加),达到阈值之后每次+1(拥塞避免),如果超时阈值修改为当前窗口的一半,重新慢启动开始传输。(TCP Tahoe 版本已废弃)
快重传,快恢复,如果接收到三个确认重传的 ack,会直接将阈值修改为当前窗口的一半,并每次传输时窗口大小增加 1(拥塞避免)。(TCP Reno 版本)

HTTP1.1 HTTP2 HTTP3

  • HTTP1.1

    增加了持久连接的方法,它的特点是在一个 TCP 连接上可以传输多个 HTTP 请求,只要浏览器或者服务器没有明确断开连接,那么该 TCP 连接会一直保持,持久连接在 HTTP/1.1 中是默认开启的,所以你不需要专门为了持久连接去 HTTP 请求头设置信息,如果你不想要采用持久连接,可以在 HTTP 请求头中加上 Connection: close。目前浏览器中对于同一个域名,默认允许同时建立 6 个 TCP 持久连接。

    提供虚拟主机的支持,HTTP/1.0 中,每个域名绑定了一个唯一的 IP 地址,因此一个服务器只能支持一个域名.但是随着虚拟主机技术的发展,需要实现在一台物理主机上绑定多个虚拟主机,每个虚拟主机都有自己的单独的域名,这些单独的域名都公用同一个 IP 地址。HTTP/1.1 的请求头中增加了 Host 字段,用来表示当前的域名地址,这样服务器就可以根据不同的 Host 值做不同的处理。

    对动态生成的内容提供了支持,在设计 HTTP/1.0 时,需要在响应头中设置完整的数据大小,但是大部分页面内容都是动态的,,因此在传输数据之前并不知道最终的数据大小。HTTP/1.1 通过引入 Chunk transfer 机制来解决这个问题,服务器会将数据分割成若干个任意大小的数据块,每个数据块发送时会附上上个数据块的长度,最后使用一个零长度的块作为发送数据完成的标志。这样就提供了对动态内容的支持。

    安全性,cookies

    存在的问题

    TCP 慢启动,导致小的核心资源并不能占满带宽立即下载
    多个 TCP 链接会竞争贷款,可能让核心资源的下载速度变慢
    队头阻塞的问题,同一时间只能一个 TCP 链接只能处理一个请求,其他的任务需要排队
    头信息没有压缩,并且被频繁的重复传输

  • HTTP2
    解决 HTTP1.1 存在的问题 一个域名只使用一个 TCP 长连接和消除队头阻塞问题 。但是 HTTP2 仍然是基于 TCP 协议的,TCP 传输过程中的丢包,会导致 TCP 队头阻塞,所以随着丢包率的增加,HTTP/2 的传输效率也会越来越差。有测试数据表明,当系统达到了 2% 的丢包率时,HTTP/1.1 的传输效率反而比 HTTP/2 表现得更好。

    HTTP2 构建了一个 2 进制分帧层,其中是两个核心概念。帧是数据传输的最小单位。帧的最外层格式如下。

    HTTP2 把请求和响应报文分成头部帧和数据帧。由 type 字段标识。一个流用多个帧组成,同一个流的帧共用一个流 ID 标识,可以将乱序的帧关联起来。

    使用 HPACK 算法,减少头信息的小,减小发送包的体积,从而降低延迟。此算法包含静态表,动态表,Huffman 编码,静态表预定义了 61 个 header 的 key,value。动态表由双方维护自定义的 header 字段,Huffman 编码用于消除重复的字节。

    可以设置请求的权重,高优先级的请求会优先响应。
    可以服务端推送,如果请求的网页有依赖资源,服务器会主动将资源推送到客户端。

    但是 HTTP2 并没有解决 TCP 层面的队头阻塞,如果传输过程中的包丢失,依然会重传并等待。

  • HTTP3
    在 UDP 协议之上,实现了 QUIC 协议,实现了类似 TCP 的流量控制、传输可靠性的功能。虽然 UDP 不提供可靠性的传输,但 QUIC 在 UDP 的基础之上增加了一层来保证数据可靠性传输。它提供了数据包重传、拥塞控制以及其他一些 TCP 中存在的特性。

    集成了 TLS 加密功能。目前 QUIC 使用的是 TLS1.3,相较于早期版本 TLS1.3 有更多的优点,其中最重要的一点是减少了握手所花费的 RTT 个数。

    实现了 HTTP/2 中的多路复用功能。和 TCP 不同,QUIC 实现了在同一物理连接上可以有多个独立的逻辑数据流(如下图)。实现了数据流的单独传输,就解决了 TCP 中队头阻塞的问题。

    实现了快速握手功能。由于 QUIC 是基于 UDP 的,所以 QUIC 可以实现使用 0-RTT 或者 1-RTT 来建立连接,这意味着 QUIC 可以用最快的速度来发送和接收数据,这样可以大大提升首次打开页面的速度。

    同时也存在一下问题:

    浏览器的实现和官方定义差距过大,系统内核对 UPD 的优化不佳。中间设备僵化导致丢包率远大于 TCP 链接。

TLS 握手过程

  • 客户端发送给服务端,TLS 版本,加密套件,seq(随机数)1
  • 服务端响应 TLS 版本,加密套件,seq2
  • 服务端发送证书
  • 服务端发送公钥
  • 服务点通知客户端发送完成
  • 客户端生成 seq3(预主密钥) 用接收到的公钥加密后在发送给服务端
  • 服务端用私钥解密,获取预主密钥
  • 服务端和客户端分别用 seq1+seq2+seq3(预主密钥) 生成会话密钥
  • 后续的通信使用会话密钥

TLS 只有在握手的过程中使用非对称加密,非对称加密对资源的消耗很大,通信过程使用加密后的会话密钥。

报文格式

  • HTTP

页面性能指标

  • FP(First Paint):从开始加载到浏览器首次绘制像素到屏幕上的时间,也就是页面在屏幕上首次发生视觉变化的时间。

  • FCP(First Contentful Paint):浏览器首次绘制来自 DOM 的内容的时间。这是用户第一次开始看到页面内容,但仅仅有内容,并不意味着它是有用的内容(例如 Header、导航栏等),也不意味着有用户要消费的内容。

  • FMP(First Meaningful Paint):页面的主要内容绘制到屏幕上的时间。主要内容的定义因页面而异,例如对于博客文章,它的主要内容是标题和摘要,对于搜索页面,它的主要内容是搜索结果,对于电商的页面,图片则是主要内容。

  • FSP(First Screen Paint):页面从开始加载到首屏内容全部绘制完成的时间,用户可以看到首屏的全部内容。

  • TTI(Time to Interactive):表示网页第一次完全达到可交互状态的时间点,浏览器已经可以持续性的响应用户的输入。

HTTP 缓存

强制缓存

HTTP/1.1 定义的 Cache-Control 头用来区分对缓存机制的支持情况, 请求头和响应头都支持这个属性。通过它提供的不同的值来定义缓存策略。

  • 每次都提供最新的内容

    此方式下,每次有请求发出时,缓存会将此请求发到服务器(该请求应该会带有与本地缓存相关的验证字段),服务器端会验证请求中所描述的缓存是否过期,若未过期(注:实际就是返回 304),则缓存才使用本地缓存副本。

    1
    Cache-Control: no-cache
  • 过期
    过期机制中,最重要的指令是 “max-age=“,表示资源能够被缓存(保持新鲜)的最大时间。max-age 是距离请求发起的时间的秒数。针对应用中那些不会改变的文件,通常可以手动设置一定的时长以保证缓存有效,例如图片、css、js 等静态资源。

    Expires 响应头包含日期/时间, 即在此时候之后,响应过期。如果在 Cache-Control 响应头设置了 “max-age” 或者 “s-max-age” 指令,那么 Expires 头会被忽略。

协商缓存

强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存

  • Last-Modified / If-Modified-Since

    Last-Modified 是由服务器发送给客户端的 HTTP 响应头标签,时间值是当前资源文件的修改时间

    If-Modified-Since 是由客户端发送给服务器的 HTTP 请求头标签,客户端再次发起该请求时,会携带上次请求返回的 Last-Modified 的时间值,对比 If-Modified-Since 的时间和该资源在服务器端最后被修改的时间,决定是否更新资源

  • Etag / If-None-Match

    Etag 是由服务器发送给客户端的 HTTP 响应头标签,是服务器端生成的资源文件的一个唯一标识

    If-None-Match 是由客户端发送给服务器的 HTTP 请求头标签,客户端再次发起该请求时,会携带上次请求返回的 Etag 值,对比服务器端的 Etag 值和 If-None-Match 的值,决定是否更新资源

优先级:Etag / If-None-Match > Last-Modified / If-Modified-Since

重绘与重排

如果你通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的。

如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。

如果更改一个既不要布局也不要绘制的属性,把这个过程叫做合成

使用了 CSS 的 transform 来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。

请求时间线

  • Queuing(排队), 导致排队状态有很多
    遇到 CSS、HTML、JavaScript 等高优先级资源,图片、视频、音频 这些低优先级资源会让路。
    浏览器会为每个域名最多维护 6 个 TCP 连接,如果超过这个数量会处于排队状态。
    网络进程在为数据分配磁盘空间时,新的 HTTP 请求也需要短暂地等待磁盘分配结束。

    优化方案: 使用域名分片技术,提高整体的 TCP 链接数量, 把站点升级到 HTTP2。

  • Stalled(停滞), 一些其他的原因导致链接被推

  • Initial connection(初始化链接), 建立 TCP 所花费的时间如果是 HTTPS 请求,还会有 SSL 握手时间。

  • Request sent(请求发送),由于只需要把缓冲区中的数据发送出去,所以速度比较快。

  • TTFB(第一字节时间),收服务器第一个字节的数据,TTFB 时间越短,就说明服务器响应越快。

    优化方案: 提高运营商的网络带宽,使用 CDN 技术。服务器响应时间过长,问题可能出现在渲染,信息处理等方面,可以考虑提高缓存的使用效率。

  • Content Download(资源下载时间)

    优化方案: 压缩文件,流式传输。

如何验证数字证书

  • 申请数字证书
    网站需要准备一套私钥和公钥,私钥留着自己使用;
    向 CA 机构提交公钥、公司、站点等信息并等待认证,这个认证过程可能是收费的;
    CA 通过线上、线下等多种渠道来验证极客时间所提供信息的真实性,如公司是否存在、企业是否合法、域名是否归属该企业等;
    如信息审核通过,CA 会向极客时间签发认证的数字证书,包含了极客时间的公钥、组织信息、CA 的信息、有效时间、证书序列号等,这些信息都是明文的,同时包含一个 CA 生成的签名。

  • 浏览器验证证书合法性
    首先,浏览器利用证书的原始信息计算出信息摘要;然后,利用 CA 的公钥来解密数字证书中的数字签名,解密出来的数据也是信息摘要;最后,判断这两个信息摘要是否相等就可以了。
    浏览器是怎么获取到 CA 公钥的,当部署 HTTP 服务器的时候,除了部署当前的数字证书之外,还需要部署 CA 机构的数字证书,CA 机构的数字证书包括了 CA 的公钥,以及 CA 机构的一些基础信息。服务器会有两个数字证书,域名的数字证书,CA 机构的数字证书。然后在建立 HTTPS 链接时,服务器会将这两个证书一同发送给浏览器,于是浏览器就可以获取到 CA 的公钥了。
    证明 CA 机构的合法性,操作系统中内置这些 CA 机构的数字证书,,CA 机构众多,因此操作系统不可能将每家 CA 的数字证书都内置进操作系统。于是人们又想出来一个折中的方案,将颁发证书的机构划分为两种类型,根 CA(Root CAs)和中间 CA(Intermediates CAs),通常申请者都是向中间 CA 去申请证书的,而根 CA 作用就是给中间 CA 做认证,一个根 CA 会认证很多中间的 CA,而这些中间 CA 又可以去认证其他的中间 CA,也就形成了数字证书链

FAQ

  • 浏览器可以同时打开多个页签,他们端口一样吗?如果一样,数据怎么知道去哪个页签?

    端口一样的,网络进程知道每个 tcp 链接所对应的标签是那个,所以接收到数据后,会把数据分发给对应的渲染进程。

  • TCP 传送数据时 浏览器端就做渲染处理了么?如果前面数据包丢了 后面数据包先来是要等么?类似的那种实时渲染怎么处理?针对数据包的顺序性?

    接收到 http 响应头中的 content-type 类型时就开始准备渲染进程了,响应体数据一旦接受到便开始做 DOM 解析了!基于 http 不用担心数据包丢失的问题,因为丢包和重传都是在 tcp 层解决的。http 能保证数据按照顺序接收的(也就是说,从 tcp 到 http 的数据就已经是完整的了,即便是实时渲染,如果发生丢包也得在重传后才能开始渲染)

  • http 和 websocket 都是属于应用层的协议吗?

    都是应用层协议,而且 websocket 名字取的比较有迷惑性,其实和 socket 完全不一样,可以把 websocket 看出是 http 的改造版本,增加了服务器向客户端主动发送消息的能力。

浏览器渲染过程

用户输入

用户发出 URL 请求到页面开始解析的这个过程,就叫做导航。

搜索内容: 使用浏览器默认的搜索引擎合成新的带搜索关键字的 URL
合法的 URL: 加上协议,合成为完整的 URL

执行搜索交互后,会先执行 beforeunload 事件,可以通过此事件取消导航。此时浏览器的状态是在加载中,但是页面内容并没有被替换,因为需要等待提交文档阶段.

URL 请求过程

浏览器进程会通过进程间通信(IPC)把 URL 请求发送至网络进程。

是否命中 DNS 缓存,如果命中缓存直接返回。接下来就是利用 IP 地址和服务器建立 TCP 连接。连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的 Cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息。

如果时通过链接打开页面,如果两个页面是同一站点,新页面会复用父页面的渲染进程。

提交文档

  • 网络进程获取到响应之后,向渲染进程发起提交文档的消息
  • 渲染进程接收到提交文档的消息后,会和网络进程建立传输数据的管道
  • 等文档数据传输完成之后,渲染进程会返回确认提交的消息给浏览器进程
  • 浏览器进程在收到确认提交的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。

页面渲染

  • 构建 DOM 树
    HTML 解析器(HTMLParser)负责将 HTML 字节流转换为 DOM 结构。网络进程接收到响应头之后,会根据响应头中的 content-type 字段来判断文件的类型,如果这是一个 HTML 类型的文件,然后为该请求选择或者创建一个渲染进程。网络进程和渲染进程之间会建立一个共享数据的管道,网络进程加载了多少数据,HTML 解析器便解析多少数据。

    第一个阶段,通过分词器将字节流转换为 Token。

    第二个和第三个阶段是同步进行的,需要将 Token 解析为 DOM 节点,并将 DOM 节点添加到 DOM 树中。HTML 解析器维护了一个 Token 栈结构,使用栈解构实现了匹配算法。

  • 构建 StyleSheet
    属性标准化
    计算每个节点的样式,css 继承规则和层叠规则

  • 创建渲染树,遍历 DOM 树中的可见节点,计算出每个元素的样式(即 ComputedStyle)。这里的计算包括所有继承的样式和从 CSS 规则中继承来的样式。

  • 创建布局树
    计算布局信息,布局信息保存在布局树中。

  • 创建无障碍树

  • 分层,创建图层树
    定位属性的元素、定义透明属性的元素、使用 CSS 滤镜的元素,需要被剪裁等,都拥有层叠上下文属性
    可以通过 will-change 属性,让元素单独在合成线程中执行。

  • 图层绘制,渲染进程生成绘制指令,真实的绘制由渲染进程中的合成线程来完成。
    通常只绘制视口附近的图像,合成线程会将图层划分为图块。

    合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的。

    合成过程会使用 GPU 加速生成,这涉及到块进程操作。渲染进程把生成指令发送给 GPU 进程,生成的位图保存在 GPU 内存中。

    一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

资源加载/解析

  • JS 在 Css 前面,且在 head 中,会阻塞 DOM 的解析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <html>
    <head>
    <script>
    console.log(document.querySelector("p"));
    </script>
    <link rel="stylesheet" href="http://localhost:8000/big.css" />
    </head>

    <body>
    <p id="test">xxx</p>
    </body>
    </html>

    JS 在 Css 后面,且在head 中,会等待 Css 加载,因为可能会通过 JS 获取样式,而且阻塞 DOM 解析, 也就是说只要在 Js 文件后面 DOM 解析都会被阻止。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <html>
    <head>
    <link rel="stylesheet" href="http://localhost:8000/big.css" />
    <script>
    console.log(document.querySelector("p")); //null
    </script>
    </head>

    <body>
    <p id="test">xxx?</p>
    </body>
    </html>

    在 Firefox, Chrome, Edge 中,如果 <link rel="stylesheet" href="xxx"> 后面跟着 <script>, 则 CSS 加载完成后, 才能触发 DOMContentLoaded

  • Css 文件如果在 <head> 中,那么 Css 文件的加载不会影响 DOM 的解析,但是影响 DOM 的渲染
    因为 Css 文件的加载可以放在预解析线程中,所以不会影响 DOM 解析,但是 DOM 解析完成后,必须等待样式文件被解析,才能渲染页面。

    Css 文件如果在 <body> 中,那么 Css 文件的加载会阻塞 Css 资源后面的 DOM 的解析(阻塞 DOMContentLoaded 执行的时机)。这种情况称为 FOUC(Flash of Unstyled Content) 样式闪烁。也就是说 Css 资源前面的 DOM 会被渲染,Css 后面的 DOM 必须要等待 Css 资源加载完成后才会被渲染,而之前已经渲染的 DOM 会在 Css 资源加载后重绘。

react-draggable 源码分析

react-draggable 一个可以让组件实现拖拽的库

整体结构

只有两个核心文件,Draggable 和 DraggableCore

Draggable 的作用是初始化组件,并且预处理一些参数,最终的核心逻辑由 DraggableCore 完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
class Draggable extends React.Component<DraggableProps, DraggableState> {
//可选属性
static propTypes = {
allowAnyClick: PropTypes.bool, //任意鼠标键可以拖动
disabled: PropTypes.bool, // 禁止拖动
enableUserSelectHack: PropTypes.bool, //如果禁止页面元素可以被选择无效,可以使用这个属性
offsetParent: PropTypes.HTMLElement, // 计算初始偏移量的时候会相对于这个元素
grid: PropTypes.arrayOf(PropTypes.number), //以网格的形式移动元素
handle: PropTypes.string, // 指定元素可拖动的区域
cancel: PropTypes.string, //指定元素不能拖动的区域
nodeRef: PropTypes.object, //用于直接指定那个元素可以被拖动
onStart: PropTypes.func, //自定义拖动开始时的回调函数
onDrag: PropTypes.func, //自定义拖动中的回调函数
onStop: PropTypes.func, //自定义拖动结束时候的回调函数
onMouseDown: PropTypes.func,
scale: PropTypes.number, // 拖动时候的偏移比例
axis: PropTypes.oneOf(["both", "x", "y", "none"]), //指明可拖动的方向轴
// 拖动边界
bounds: PropTypes.oneOfType([
PropTypes.shape({
left: PropTypes.number,
right: PropTypes.number,
top: PropTypes.number,
bottom: PropTypes.number,
}),
PropTypes.string,
PropTypes.oneOf([false]),
]),
//自定义拖动时候的样式
defaultClassName: PropTypes.string,
defaultClassNameDragging: PropTypes.string,
defaultClassNameDragged: PropTypes.string,
// 默认位置
defaultPosition: PropTypes.shape({
x: PropTypes.number,
y: PropTypes.number,
}),
// 默认偏移量
positionOffset: PropTypes.shape({
x: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
y: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
}),
};
// 初始化状态
constructor(props: DraggableProps) {
super(props);
this.state = {
dragging: false,
dragged: false,
x: props.position ? props.position.x : props.defaultPosition.x,
y: props.position ? props.position.y : props.defaultPosition.y,
prevPropsPosition: { ...props.position },
// 用于计算在边界外的拖动
slackX: 0,
slackY: 0,
isElementSVG: false,
};
}

// 如果组件是受控的,检查新传入的值时候与上一次更新时候的值不同,避免组件频繁更新
static getDerivedStateFromProps(
{ position }: DraggableProps,
{ prevPropsPosition }: DraggableState
): ?$Shape<DraggableState> {
if (
position &&
(!prevPropsPosition ||
position.x !== prevPropsPosition.x ||
position.y !== prevPropsPosition.y)
) {
return {
x: position.x,
y: position.y,
prevPropsPosition: { ...position },
};
}
return null;
}
findDOMNode() {}
// 预处理用户提供的事件绑定方法
// 其他的事件属性与这个类似
onDragStart(e, coreData) {
// 创建一个事件对象,返回给使用者
const shouldStart = this.props.onStart(
e,
createDraggableData(this, coreData)
);
// 如果绑定方法返回 false,那么同样放回false,在实际调用时停止程序执行
if (shouldStart === false) return false;
//标记状态, 表示拖动开始
this.setState({ dragging: true, dragged: true });
}
onDrag() {
// Draggable 中的状态与 DraggableCore 中的状态是区分开的
if (!this.state.dragging) return false;

// 在这里处理边界问题和偏移量比例的问题
// 相当于把用户的自定义配置影响分离出来,DraggableCore 只处理核心拖拽的问题
function createDraggableData(draggable, coreData) {
const scale = draggable.props.scale;
return {
node: coreData.node,
x: draggable.state.x + coreData.deltaX / scale,
y: draggable.state.y + coreData.deltaY / scale,
deltaX: coreData.deltaX / scale,
deltaY: coreData.deltaY / scale,
lastX: draggable.state.x,
lastY: draggable.state.y,
};
}
const uiData = createDraggableData(this, coreData);

const newState: $Shape<DraggableState> = {
x: uiData.x,
y: uiData.y,
};

// Keep within bounds.
if (this.props.bounds) {
// Save original x and y.
const { x, y } = newState;

// Add slack to the values used to calculate bound position. This will ensure that if
// we start removing slack, the element won't react to it right away until it's been
// completely removed.
newState.x += this.state.slackX;
newState.y += this.state.slackY;

const [newStateX, newStateY] = getBoundPosition(
this,
newState.x,
newState.y
);
newState.x = newStateX;
newState.y = newStateY;

// Recalculate slack by noting how much was shaved by the boundPosition handler.
newState.slackX = this.state.slackX + (x - newState.x);
newState.slackY = this.state.slackY + (y - newState.y);

// Update the event we fire to reflect what really happened after bounds took effect.
uiData.x = newState.x;
uiData.y = newState.y;
uiData.deltaX = newState.x - this.state.x;
uiData.deltaY = newState.y - this.state.y;
}

// Short-circuit if user's callback killed it.
const shouldUpdate = this.props.onDrag(e, uiData);
if (shouldUpdate === false) return false;

this.setState(newState);
}
onDragStop() {}
render() {
return (
<DraggableCore
{...draggableCoreProps}
onStart={this.onDragStart}
onDrag={this.onDrag}
onStop={this.onDragStop}
>
{React.cloneElement(React.Children.only(children), {
className: className,
style: { ...children.props.style, ...style },
transform: svgTransform,
})}
</DraggableCore>
);
}
}

class DraggableCore extends React.Component<
DraggableCoreProps,
DraggableCoreState
> {
componentDidMount() {
this.mounted = true;
// 组件挂载时去查找需要挂载的元素,并绑定事件
const thisNode = this.findDOMNode();
// 因为移动短的touch 事件可能会让屏幕发生滚动,所以必须通过原生事件绑定传入 passive 参数
if (thisNode) {
addEvent(thisNode, eventsFor.touch.start, this.onTouchStart, {
passive: false,
});
}
}
findDOMNode(): ?HTMLElement {
return this.props?.nodeRef
? this.props?.nodeRef?.current
: ReactDOM.findDOMNode(this);
}

handleDragStart() {}
handleDrag() {}
handleDragStop() {}
// 在组件上绑定下面四个方法,当方法被触发时,可以区分是否是触摸设备
onMouseDown() {
const eventsFor = {
touch: {
start: "touchstart",
move: "touchmove",
stop: "touchend",
},
mouse: {
start: "mousedown",
move: "mousemove",
stop: "mouseup",
},
};
dragEventFor = eventsFor.mouse; // on touchscreen laptops we could switch back to mouse
return this.handleDragStart(e);
}
onMouseUp() {}
onTouchStart() {}
onTouchEnd() {}
}

getBoundPosition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
function getBoundPosition(
draggable: Draggable,
x: number,
y: number
): [number, number] {
// If no bounds, short-circuit and move on
if (!draggable.props.bounds) return [x, y];

// Clone new bounds
let { bounds } = draggable.props;
bounds = typeof bounds === "string" ? bounds : cloneBounds(bounds);
const node = findDOMNode(draggable);

if (typeof bounds === "string") {
const { ownerDocument } = node;
const ownerWindow = ownerDocument.defaultView;
let boundNode;
if (bounds === "parent") {
boundNode = node.parentNode;
} else {
boundNode = ownerDocument.querySelector(bounds);
}
if (!(boundNode instanceof ownerWindow.HTMLElement)) {
throw new Error(
'Bounds selector "' + bounds + '" could not find an element.'
);
}
const boundNodeEl: HTMLElement = boundNode; // for Flow, can't seem to refine correctly
const nodeStyle = ownerWindow.getComputedStyle(node);
const boundNodeStyle = ownerWindow.getComputedStyle(boundNodeEl);
// Compute bounds. This is a pain with padding and offsets but this gets it exactly right.
bounds = {
left:
-node.offsetLeft +
int(boundNodeStyle.paddingLeft) +
int(nodeStyle.marginLeft),
top:
-node.offsetTop +
int(boundNodeStyle.paddingTop) +
int(nodeStyle.marginTop),
right:
innerWidth(boundNodeEl) -
outerWidth(node) -
node.offsetLeft +
int(boundNodeStyle.paddingRight) -
int(nodeStyle.marginRight),
bottom:
innerHeight(boundNodeEl) -
outerHeight(node) -
node.offsetTop +
int(boundNodeStyle.paddingBottom) -
int(nodeStyle.marginBottom),
};
}

// Keep x and y below right and bottom limits...
if (isNum(bounds.right)) x = Math.min(x, bounds.right);
if (isNum(bounds.bottom)) y = Math.min(y, bounds.bottom);

// But above left and top limits.
if (isNum(bounds.left)) x = Math.max(x, bounds.left);
if (isNum(bounds.top)) y = Math.max(y, bounds.top);

return [x, y];
}

handleDragStart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
const handleDragStart: EventHandler<MouseTouchEvent> = (e) => {
// 用于自定义的 onMouseDown 事件
this.props.onMouseDown(e);

// 任意鼠标按键都一颗拖动,否则只有鼠标左键可以拖动
if (
!this.props.allowAnyClick &&
typeof e.button === "number" &&
e.button !== 0
)
return false;

// 查找到需要拖动的元素
const thisNode = this.findDOMNode();
if (!thisNode || !thisNode.ownerDocument || !thisNode.ownerDocument.body) {
throw new Error("<DraggableCore> not mounted on DragStart!");
}
const { ownerDocument } = thisNode;

// 控制可拖动区域和不可拖动区域
// handle cancel 属性传入的是控制区域的元素标签名称
// 用原生的 matches 方法检查这个标签是否在拖动元素的内部,如果有就停止执行 element.matches('p')
// 这是真正的 onStart 方法还没有执行,不需要重置状态
if (
this.props.disabled ||
!(e.target instanceof ownerDocument.defaultView.Node) ||
(this.props.handle &&
!matchesSelectorAndParentsTo(e.target, this.props.handle, thisNode)) ||
(this.props.cancel &&
matchesSelectorAndParentsTo(e.target, this.props.cancel, thisNode))
) {
return;
}

// Prevent scrolling on mobile devices, like ipad/iphone.
// Important that this is after handle/cancel.
if (e.type === "touchstart") e.preventDefault();

// Set touch identifier in component state if this is a touch event. This allows us to
// distinguish between individual touches on multitouch screens by identifying which
// touchpoint was set to this element.
// 记录触摸点的 id
const touchIdentifier = getTouchIdentifier(e);
this.setState({ touchIdentifier });

// Get the current drag point from the event. This is used as the offset.
// 首先如果存在触摸点,则回去touch 事件,否则获取原生的 event
// 通过 findDOMNode 查找需要拖动的元素, 获取用于计算相对位置的元素 offsetParent 如果没有这个属性就用上级 parentNode || document
// const x = (evt.clientX + offsetParent.scrollLeft - offsetParentRect.left) / scale;
// const y = (evt.clientY + offsetParent.scrollTop - offsetParentRect.top) / scale;
// 获取相对于父元素的鼠标位置
const position = getControlPosition(e, touchIdentifier, this);
if (position == null) return; // not possible but satisfies flow
const { x, y } = position;

// Create an event object with all the data parents need to make a decision here.
const coreEvent = createCoreData(this, x, y);

// 真正执行 onStart 方法
const shouldUpdate = this.props.onStart(e, coreEvent);
// 如果返回值为false 停止执行
if (shouldUpdate === false || this.mounted === false) return;

// Add a style to the body to disable user-select. This prevents text from
// being selected all over the page.
if (this.props.enableUserSelectHack) addUserSelectStyles(ownerDocument);

// Initiate dragging. Set the current x and y as offsets
// so we know how much we've moved during the drag. This allows us
// to drag elements around even if they have been moved, without issue.
this.setState({
dragging: true,

lastX: x,
lastY: y,
});

// Add events to the document directly so we catch when the user's mouse/touch moves outside of
// this element. We use different events depending on whether or not we have detected that this
// is a touch-capable device.
addEvent(ownerDocument, dragEventFor.move, this.handleDrag);
addEvent(ownerDocument, dragEventFor.stop, this.handleDragStop);
};

handleDrag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const handleDrag: EventHandler<MouseTouchEvent> = (e) => {
// Get the current drag point from the event. This is used as the offset.
const position = getControlPosition(e, this.state.touchIdentifier, this);
if (position == null) return;
let { x, y } = position;

// 实现按网格拖动
if (Array.isArray(this.props.grid)) {
let deltaX = x - this.state.lastX,
deltaY = y - this.state.lastY;
const deltaX = Math.round(deltaX / this.props.grid[0]) * this.props.grid[0];
const deltaY = Math.round(deltaY / this.props.grid[0]) * this.props.grid[0];
if (!deltaX && !deltaY) return; // skip useless drag
(x = this.state.lastX + deltaX), (y = this.state.lastY + deltaY);
}

const coreEvent = createCoreData(this, x, y);

// Call event handler. If it returns explicit false, trigger end.
// 指定自定义方法,如果返回 false 手动停止执行
const shouldUpdate = this.props.onDrag(e, coreEvent);
if (shouldUpdate === false || this.mounted === false) {
try {
// $FlowIgnore
this.handleDragStop(new MouseEvent("mouseup"));
} catch (err) {}
return;
}

this.setState({
lastX: x,
lastY: y,
});
};

handleDragStop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const handleDragStop: EventHandler<MouseTouchEvent> = (e) => {
if (!this.state.dragging) return;

const position = getControlPosition(e, this.state.touchIdentifier, this);
if (position == null) return;
let { x, y } = position;

// Snap to grid if prop has been provided
if (Array.isArray(this.props.grid)) {
let deltaX = x - this.state.lastX || 0;
let deltaY = y - this.state.lastY || 0;
[deltaX, deltaY] = snapToGrid(this.props.grid, deltaX, deltaY);
(x = this.state.lastX + deltaX), (y = this.state.lastY + deltaY);
}

const coreEvent = createCoreData(this, x, y);

// Call event handler
const shouldContinue = this.props.onStop(e, coreEvent);
if (shouldContinue === false || this.mounted === false) return false;

const thisNode = this.findDOMNode();
if (thisNode) {
// Remove user-select hack
if (this.props.enableUserSelectHack)
removeUserSelectStyles(thisNode.ownerDocument);
}

// 重置状态并移除事件
// Reset the el.
this.setState({
dragging: false,
lastX: NaN,
lastY: NaN,
});

if (thisNode) {
// Remove event handlers
removeEvent(thisNode.ownerDocument, dragEventFor.move, this.handleDrag);
removeEvent(thisNode.ownerDocument, dragEventFor.stop, this.handleDragStop);
}
};

MySQl 分析函数

ROW_NUMBER

表示根据 col1 分组,在分组内部根据 col2 排序,而这个值就表示每组内部排序后的顺序编码

1
row_number() over( partition by col1 order by col2);

ROW_NUMBER 返回行信息没有排名

RANK DENSE_RANK

1
2
3
4
5
select *,
row_number () over(partition by s.SId order by s.score) as row_number1,
rank () over(partition by s.SId order by s.score) as rank1,
dense_rank () over(partition by s.SId order by s.score) as dense_rank1
from SC s ;

ROW_NUMBER 连续排名
RANK 值相同排名相同,排名跳跃
DENSE_RANK 值相同排名相同,排名连续

查询所有同学最高分对应的科目名称

MySQL 局部变量

Transact-SQL 局部变量是可以保存特定类型的单个数据值的对象。在脚本或批处理中通常会使用变量

  • 作为计数器来计算循环执行的次数或控制循环执行的次数。
  • 保存流程控制中的测试值
  • 保存要由存储过程返回码或函数返回值返回的数据值。

SELECT @local_variable

1
2
SELECT { @local_variable { = | += | -= | *= | /= | %= | &= | ^= | |= } expression }
[ ,...n ] [ ; ]

将右边的值赋给左边的变量。

操作员 行动
= 将后面的表达式分配给变量。
+= 添加和分配
-= 减法和赋值
*= 相乘并赋值
/= 划分和分配
%= 取模和赋值
&= 按位与并赋值
^= 按位异或并赋值
| = 按位或并赋值

SELECT @local_variable 通常用于将单个值返回到变量中。但是,当表达式是列名时,它可以返回多个值。如果 SELECT 语句返回多个值,则为变量分配最后一个返回的值。

如果 SELECT 语句没有返回任何行,则变量保留其当前值。如果表达式是不返回值的标量子查询,则变量设置为 NULL。

一个 SELECT 语句可以初始化多个局部变量。

  • 查询最近 12 个月的数据量, 没有的用 0 补全

因为把表中的所有月份 group by 之后,已有的月份不足 12 个月, 需要补齐,所以先生成一张 12 个月的空表

注意子查询中的日期不能使用 date_format 因为会失去时间格式,导致上面的变量在 addDate 中不可使用

1
2
3
select DATE_FORMAT( @u := ADDDATE(@u,INTERVAL 1 Month),'%Y-%m') as date from (
select @u := ADDDATE(now(),INTERVAL -6 Month) from Student s limit 12
) as t;

在拼接已存在的数据

1
2
3
4
5
6
7
8
select DATE_FORMAT( @u := ADDDATE(@u,INTERVAL 1 Month),'%Y-%m') as date from (
select @u := ADDDATE(now(),INTERVAL -6 Month) from Student s limit 12
) as t

left join (
select count(*) as sum,DATE_FORMAT(s.date,'%Y-%m') as date from Student s
group by DATE_FORMAT(s.date,'%Y-%m')
) as t2 on t2.date = t.date

最后把空值处理为 0

1
2
3
4
5
6
7
8
9
10
select date, if(ISNULL(d.sum),0,d.sum) as sum from (
select DATE_FORMAT( @u := ADDDATE(@u,INTERVAL 1 Month),'%Y-%m') as date from (
select @u := ADDDATE(now(),INTERVAL -6 Month) from Student s limit 12
) as t

left join (
select count(*) as sum,DATE_FORMAT(s.date,'%Y-%m') as date from Student s
group by DATE_FORMAT(s.date,'%Y-%m')
) as t2 on t2.date = t.date
) as d

MySQL 日期时间函数

ADDDATE()

1
2
3
4
5
-- date时间字段名  expr时间间隔
-- unit单位 指的时间单位 天day 小时hour表示等
DATE_ADD(date,INTERVAL expr unit)

-- 采用这种写法的时候,使用方法与DATE_ADD一致
  • 日期+天数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SELECT ADDDATE('2020-01-02 00:00:00', INTERVAL 1 day) from orders

--打印字段为:2020-01-03 00:00:00

-- 下面同理, 在写增加天为单位时 可以缩写成
SELECT ADDDATE('2020-01-02 00:00:00', 1) from orders

--默认为day
--打印字段为:2020-01-03 00:00:00

--如果减一天 则为 -1 这里也可写成字符串
SELECT ADDDATE('2020-01-01 00:00:00', '-1') from orders

--打印字段为:2020-01-01 00:00:00

  • 日期+小时
1
2
SELECT ADDDATE('2020-01-02 00:00:00', INTERVAL 1 HOUR) from orders
-- 打印字段为:2020-01-01 01:00:00
  • 日期+分钟
1
2
SELECT ADDDATE('2020-01-02 00:00:00', INTERVAL 1 MINUTE) from orders
-- 打印字段为:2020-01-01 00:01:00
  • 日期+秒
1
2
SELECT ADDDATE('2020-01-02 00:00:00', INTERVAL 1 SECOND) from orders
-- 打印字段为:2020-01-01 00:00:01

DATE_FORMAT()

date 参数是合法的日期。format 规定日期/时间的输出格式。

1
DATE_FORMAT(date,format)
格式 描述
%a 缩写星期名
%b 缩写月名
%c 月,数值
%D 带有英文前缀的月中的天
%d 月的天,数值(00-31)
%e 月的天,数值(0-31)
%f 微秒
%H 小时 (00-23)
%h 小时 (01-12)
%I 小时 (01-12)
%i 分钟,数值(00-59)
%j 年的天 (001-366)
%k 小时 (0-23)
%l 小时 (1-12)
%M 月名
%m 月,数值(00-12)
%p AM 或 PM
%r 时间,12-小时(hh:mm:ss AM 或 PM)
%S 秒(00-59)
%s 秒(00-59)
%T 时间, 24-小时 (hh:mm:ss)
%U 周 (00-53) 星期日是一周的第一天
%u 周 (00-53) 星期一是一周的第一天
%V 周 (01-53) 星期日是一周的第一天,与 %X 使用
%v 周 (01-53) 星期一是一周的第一天,与 %x 使用
%W 星期名
%w 周的天 (0=星期日, 6=星期六)
%X 年,其中的星期日是周的第一天,4 位,与 %V 使用
%x 年,其中的星期一是周的第一天,4 位,与 %v 使用
%Y 年,4 位
%y 年,2 位

NOW() CURDATE() CURTIME()

函数返回当前的日期和时间。

NOW() CURDATE() CURTIME()
2008-12-29 16:25:46 2008-12-29 16:25:46

身份认证

认证(Authentication)

在互联网中证明自己的身份:

  • 用户名密码登录
  • 邮箱发送登录链接
  • 手机号接收验证码
  • 通过第三方受信任的程序登陆,例如微信登陆

授权(Authorization)

用户授予第三方应用访问该用户某些资源的权限,在使用微信登陆第三方系统前,需要给第三方系统授权,允许使用微信登陆

实现授权的方式有:cookie、session、token、OAuth

凭证(Credentials)

实现认证和授权的前提是需要一种媒介(证书) 来标记访问者的身

  • HTTP 是无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息):每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。而这个状态需要通过 cookie 或者 session 去实现。

  • cookie 存储在客户端: cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。

  • cookie 是不可跨域的: 每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的(靠的是 domain)。

属性 说明
name=value 键值对,设置 Cookie 的名称及相对应的值,都必须是字符串类型- 如果值为 Unicode 字符,需要为字符编码。- 如果值为二进制数据,则需要使用 BASE64 编码。
domain 指定 cookie 所属域名,默认是当前域名
path 指定 cookie 在哪个路径(路由)下生效,默认是 ‘/‘。如果设置为 /abc,则只有 /abc 下的路由可以访问到该 cookie,如:/abc/read。
maxAge cookie 失效的时间,单位秒。如果为整数,则该 cookie 在 maxAge 秒后失效。如果为负数,该 cookie 为临时 cookie ,关闭浏览器即失效,浏览器也不会以任何形式保存该 cookie 。如果为 0,表示删除该 cookie 。默认为 -1。
expires 过期时间,在设置的某个时间点后该 cookie 就会失效。一般浏览器的 cookie 都是默认储存的,当关闭浏览器结束这个会话的时候,这个 cookie 也就会被删除
secure 该 cookie 是否仅被使用安全协议传输。安全协议有 HTTPS,SSL 等,在网络上传输数据之前先将数据加密。默认为 false。当 secure 值为 true 时,cookie 在 HTTP 中是无效,在 HTTPS 中才有效。
httpOnly 如果给某个 cookie 设置了 httpOnly 属性,则无法通过 JS 脚本 读取到该 cookie 的信息,但还是能通过 Application 中手动修改 cookie,所以只是在一定程度上可以防止 XSS 攻击,不是绝对的安全

什么是 Session

Session 一般配合 Cookie 使用,是一种记录服务器和客户端会话状态的机制

  • 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session
  • 请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器
  • 浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名
  • 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。
  • 在做负载均衡的时候, 需要把每一个请求分配到上一次处理他的服务器中, 这个问题可以通过 IP hash 来解决.

  • 如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session. 在同一服务中的所有不同域名的网站都可以自动登陆.一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。
    另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。

JWT

首先说明 Token 是一个宽泛的概念, 其本质都是让客户端保存一些信息, 从而降低服务器的压力. JWT 是其中比较好的实现.

JWT(JSON Web Token) 服务器认证以后,字符串发回给用户,就像下面这样。服务器完全只靠这个对象认定用户身份。服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

  • Header

Header 部分是一个 JSON 对象,描述 JWT 的元数据.alg 属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256), typ 属性表示这个令牌(token)的类型(type),JWT 令牌统一写为 JWT。

最后,将上面的 JSON 对象使用 Base64URL 算法转成字符串。

  • Payload

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了 7 个官方字段,供选用。也可以添加自己定义的字段, JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。这个 JSON 对象也要使用 Base64URL 算法转成字符串。

1
2
3
4
5
6
7
8
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号

  • Signature

Signature 部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照图片中的公式产生签名。

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用”点”(.)分隔,就可以返回给用户。

JWT 使用方法

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息 Authorization 字段里面。

1
Authorization: Bearer <token>

另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。

JWT 的特点

(1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。

(2)JWT 不加密的情况下,不能将秘密数据写入 JWT。

(3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。

(4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。

(5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。

(6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

React v16 源码分析 ⑩ 事件系统

合成事件

先看一个案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export default class App extends React.Component {
state = {
show: false,
};
ref = React.createRef();
componentDidMount() {
const that = this;
document.addEventListener("click", () => {
that.setState({ show: false });
console.log("document");
});
this.ref.current.addEventListener("click", () => {
console.log("ref");
});
}
render() {
return (
<div
ref={this.ref}
onClick={() => this.setState({ show: true }, console.log("dom"))}
>
点击 {this.state.show ? "show" : "null"}
</div>
);
}
}

当点击事件触发的时候会发现点击事件无效, 先打印出 ref,再打印 dom,后打印出 document,先分析一个打印顺序的问题

如果事件是绑定在原生上的,那么 render 的执行会早于 componentDidMount,所以按理来说打印 dom 的事件应该先执行,在执行打印 ref 的事件,但显然结果不是这样.

其实这就是 React 合成事件(SyntheticEvent),在项目初始化的时候, 在 react v16 版本会把所有的事件绑定在 document 元素上, v17 版本把元素修改为 root 元素,用官方的话讲就是:不需要使用 addEventListener 为已创建的 DOM 元素添加监听器。事实上,你只需要在该元素初始渲染的时候添加监听器即可。

因为这个原因,在冒泡阶段最先执行的是 div 的原生事件,又因为 div 上的合成事件虽然绑定在 document 上,但绑定时机在项目初始化的时候, 而 document 上原生的绑定事件是在 componentDidMount 生命周期中,所以按照原生事件的行为,先绑定的先执行,会先打印 dom 后打印 document,看一个复杂的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import React from "react";

export default class App extends React.Component {
parentRef = React.createRef();
childRef = React.createRef();
componentDidMount() {
this.parentRef.current.addEventListener(
"click",
() => {
console.log("父元素原生事件捕获");
},
true
);
this.parentRef.current.addEventListener("click", () => {
console.log("父元素原生事件冒泡");
});
this.childRef.current.addEventListener(
"click",
() => {
console.log("子元素原生事件捕获");
},
true
);
this.childRef.current.addEventListener("click", () => {
console.log("子元素原生事件冒泡");
});
document.addEventListener(
"click",
() => {
console.log("document 捕获");
},
true
);
document.addEventListener("click", () => {
console.log("document 冒泡");
});
}
parentBubble = () => {
console.log("父组件React事件冒泡");
};
parenteCapture = () => {
console.log("父组件React事件捕获");
};
childBubble = () => {
console.log("子组件React事件冒泡");
};
childeCapture = () => {
console.log("子组件React事件捕获");
};
render() {
return (
<div
ref={this.parentRef}
onClick={this.parentBubble}
onClickCapture={this.parenteCapture}
>
<p
ref={this.childRef}
onClick={this.childBubble}
onClickCapture={this.childeCapture}
>
事件
</p>
</div>
);
}
}

v16 版本

1
2
3
4
5
6
7
8
9
10
document 捕获
父元素原生事件捕获
子元素原生事件捕获
子元素原生事件冒泡
父元素原生事件冒泡
父组件React事件捕获
子组件React事件捕获
子组件React事件冒泡
父组件React事件冒泡
document 冒泡

虽然合成事件绑定在 document 元素上但只能在冒泡阶段触发, 因为合成事件绑定在冒包阶段而不是捕获阶段, 因此会先执行所有原生事件的捕获阶段, 捕获阶段结束之后由于合成事件在 document 仍然不会执行, 紧接着执行原生的冒泡事件.直到冒泡到 document 元素由于合成事件绑定的早,所有一次性的执行了合成事件的捕获和冒泡阶段,最后才是 document 冒泡事件

v17 版本

1
2
3
4
5
6
7
8
9
10
11

document 捕获
父组件React事件捕获
子组件React事件捕获
父元素原生事件捕获
子元素原生事件捕获
子元素原生事件冒泡
父元素原生事件冒泡
子组件React事件冒泡
父组件React事件冒泡
document 冒泡

由于在 v17 版本中事件是绑定在 root 上的,所以在捕获阶段可以执行合成事件的 冒泡阶段.

简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<body>
<div id="root">
<div id="parent">
<p id="child">事件</p>
</div>
</div>
</body>
<script>
let root = document.getElementById("root");
let parent = document.getElementById("parent");
let child = document.getElementById("child");

parent.addEventListener(
"click",
() => {
console.log("父元素原生事件捕获");
},
true
);
parent.addEventListener("click", () => {
console.log("父元素原生事件冒泡");
});
child.addEventListener(
"click",
() => {
console.log("子元素原生事件捕获");
},
true
);
child.addEventListener("click", () => {
console.log("子元素原生事件冒泡");
});

document.addEventListener(
"click",
function () {
console.log("document 捕获");
},
true
);
document.addEventListener("click", function () {
console.log("document 冒泡");
});

parent.onClick = function () {
console.log("父组件React事件冒泡");
};
parent.onClickCapture = () => {
console.log("父组件React事件捕获");
};
child.onClick = function () {
console.log("子组件React事件冒泡");
};
child.onClickCapture = () => {
console.log("子组件React事件捕获");
};
</script>

v16 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
document.addEventListener("click", dispatchEvent);
function dispatchEvent(event) {
let paths = [];
let current = event.target;
while (current) {
paths.push(current);
current = current.parentNode;
}
for (let i = paths.length - 1; i >= 0; i--) {
let handle = paths[i].onClickCapture;
handle && handle();
}
for (let i = 0; i < paths.length; i++) {
let handle = paths[i].onClick;
handle && handle();
}
}

v17 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function dispatchEvent(event, useCapture) {
let paths = [];
let current = event.target;
while (current) {
paths.push(current);
current = current.parentNode;
}
if (useCapture) {
for (let i = paths.length - 1; i >= 0; i--) {
let handle = paths[i].onClickCapture;
handle && handle();
}
} else {
for (let i = 0; i < paths.length; i++) {
let handle = paths[i].onClick;
handle && handle();
}
}
}
root.addEventListener("click", (e) => dispatchEvent(e, true), true);
root.addEventListener("click", (e) => dispatchEvent(e, false));

事件注册

这一过程是在全局执行的

1
2
3
4
5
SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();

先把所有的事件分为以下 5 种类型

  • simpleEvents

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    for (var i = 0; i < simpleEventPluginEvents.length; i++) {
    var eventName = simpleEventPluginEvents[i];
    var domEventName = eventName.toLowerCase();
    var capitalizedEvent = eventName[0].toUpperCase() + eventName.slice(1);

    // 把事件转换成 onEvent 的格式
    registerSimpleEvent(domEventName, "on" + capitalizedEvent);
    }

    registerSimpleEvent(ANIMATION_END, "onAnimationEnd");
    registerSimpleEvent(ANIMATION_ITERATION, "onAnimationIteration");
    registerSimpleEvent(ANIMATION_START, "onAnimationStart");
    registerSimpleEvent("dblclick", "onDoubleClick");
    registerSimpleEvent("focusin", "onFocus");
    registerSimpleEvent("focusout", "onBlur");
    registerSimpleEvent(TRANSITION_END, "onTransitionEnd");
  • enterLeaveEvents

    1
    2
    3
    4
    registerDirectEvent("onMouseEnter", ["mouseout", "mouseover"]);
    registerDirectEvent("onMouseLeave", ["mouseout", "mouseover"]);
    registerDirectEvent("onPointerEnter", ["pointerout", "pointerover"]);
    registerDirectEvent("onPointerLeave", ["pointerout", "pointerover"]);
  • changeEvents

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    registerTwoPhaseEvent("onChange", [
    "change",
    "click",
    "focusin",
    "focusout",
    "input",
    "keydown",
    "keyup",
    "selectionchange",
    ]);
  • selectEvent

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    registerTwoPhaseEvent("onSelect", [
    "focusout",
    "contextmenu",
    "dragend",
    "focusin",
    "keydown",
    "keyup",
    "mousedown",
    "mouseup",
    "selectionchange",
    ]);
  • beforeInputEvents

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    function registerEvents() {
    registerTwoPhaseEvent("onBeforeInput", [
    "compositionend",
    "keypress",
    "textInput",
    "paste",
    ]);
    registerTwoPhaseEvent("onCompositionEnd", [
    "compositionend",
    "focusout",
    "keydown",
    "keypress",
    "keyup",
    "mousedown",
    ]);
    registerTwoPhaseEvent("onCompositionStart", [
    "compositionstart",
    "focusout",
    "keydown",
    "keypress",
    "keyup",
    "mousedown",
    ]);
    registerTwoPhaseEvent("onCompositionUpdate", [
    "compositionupdate",
    "focusout",
    "keydown",
    "keypress",
    "keyup",
    "mousedown",
    ]);
    }

registerSimpleEvent 是为了保存原生事件和合成事件名称之间的对应关系,并且调用 registerTwoPhaseEvent

1
2
3
4
5
6
var topLevelEventsToReactNames = new Map();
// click onClick
function registerSimpleEvent(domEventName, reactName) {
topLevelEventsToReactNames.set(domEventName, reactName);
registerTwoPhaseEvent(reactName, [domEventName]);
}

registerTwoPhaseEvent 会调用 registerDirectEvent 用于绑定合成事件名称和其对应的真正事件,一个合成事件对应多个原生事件,也就是 JSX 中绑定的事件,可能会触发多个原生事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function registerTwoPhaseEvent(registrationName, dependencies) {
registerDirectEvent(registrationName, dependencies);
registerDirectEvent(registrationName + "Capture", dependencies);
}

function registerDirectEvent(registrationName, dependencies) {
// 合成事件和原生事件的对应关系 {onAbort:['abort']}
registrationNameDependencies[registrationName] = dependencies;

{
var lowerCasedName = registrationName.toLowerCase();

// 内部用于验证事件的对象,保存的是驼峰命名和非驼峰命名之间的关系 {onclick:onClick}
possibleRegistrationNames[lowerCasedName] = registrationName;

if (registrationName === "onDoubleClick") {
possibleRegistrationNames.ondblclick = registrationName;
}
}
//所有原生事件名称的 set 集合 set([click,cancel])
for (var i = 0; i < dependencies.length; i++) {
allNativeEvents.add(dependencies[i]);
}
}

注册过程就是通过几个全局变量完整的保存了,原生事件名称(click),原生事件绑定名称(onClick),合成事件名称 中的对应关系

事件绑定

绑定事件发生在 rendercreateRoot 方法中, 在创建了 root 节点之后会调用 listenToAllSupportedEvents, 会对所有的原生事件调用绑定函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
function listenToAllSupportedEvents(rootContainerElement) {
allNativeEvents.forEach(function (domEventName) {
// 特殊处理,这个方法只会在 document 上
if (domEventName !== "selectionchange") {
// 排除那些没有冒泡阶段的事件
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement);
}
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});

ownerDocument[listeningMarker] = true;
listenToNativeEvent("selectionchange", false, ownerDocument);
}

function listenToNativeEvent(domEventName, isCapturePhaseListener, target) {
var eventSystemFlags = 0;

// 添加捕获阶段的标识
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}

addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener
);
}

function addTrappedEventListener(
targetContainer,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
isDeferredListenerForLegacyFBSupport
) {
// 创建事件回调函数
var listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags
);

var isPassiveListener = undefined;
// 是否支持 passive 参数
// addEventListener 第三个参数可以写为一个对象 { capture: false, once: true,passive:true }
// capture 表示冒泡阶段执行还是捕获阶段执行
// once 标识绑定事件只会执行一次就被移除
// passive 是否执行默认事件,有些默认事件和浏览器行为绑定比如移动端 touchstart 会触发浏览器滚动
// 如果使用 event.preventDefault() 会调用函数后才会停止默认行为, 可以使用 passive:false 直接阻止默认行为
if (passiveBrowserEventsSupported) {
if (
domEventName === "touchstart" ||
domEventName === "touchmove" ||
domEventName === "wheel"
) {
isPassiveListener = true;
}
}

targetContainer = targetContainer;
var unsubscribeListener;

// 利用 addEventListener 绑定事件监听函数

if (isCapturePhaseListener) {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener
);
} else {
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener
);
}
} else {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventBubbleListenerWithPassiveFlag(
targetContainer,
domEventName,
listener,
isPassiveListener
);
} else {
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener
);
}
}
}

createEventListenerWrapperWithPriority 会调用 getEventPriority 按事件名称为事件定义不同的优先级,不同的优先级对应不同的事件处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
function getEventPriority(domEventName) {
switch (domEventName) {
// Used by SimpleEventPlugin:
case "cancel":
case "click":
case "close":
case "contextmenu":
case "copy":
case "cut":
case "auxclick":
case "dblclick":
case "dragend":
case "dragstart":
case "drop":
case "focusin":
case "focusout":
case "input":
case "invalid":
case "keydown":
case "keypress":
case "keyup":
case "mousedown":
case "mouseup":
case "paste":
case "pause":
case "play":
case "pointercancel":
case "pointerdown":
case "pointerup":
case "ratechange":
case "reset":
case "resize":
case "seeked":
case "submit":
case "touchcancel":
case "touchend":
case "touchstart":
case "volumechange": // Used by polyfills:
// eslint-disable-next-line no-fallthrough

case "change":
case "selectionchange":
case "textInput":
case "compositionstart":
case "compositionend":
case "compositionupdate": // Only enableCreateEventHandleAPI:
// eslint-disable-next-line no-fallthrough

case "beforeblur":
case "afterblur": // Not used by React but could be by user code:
// eslint-disable-next-line no-fallthrough

case "beforeinput":
case "blur":
case "fullscreenchange":
case "focus":
case "hashchange":
case "popstate":
case "select":
case "selectstart":
return DiscreteEventPriority;

case "drag":
case "dragenter":
case "dragexit":
case "dragleave":
case "dragover":
case "mousemove":
case "mouseout":
case "mouseover":
case "pointermove":
case "pointerout":
case "pointerover":
case "scroll":
case "toggle":
case "touchmove":
case "wheel": // Not used by React but could be by user code:
// eslint-disable-next-line no-fallthrough

case "mouseenter":
case "mouseleave":
case "pointerenter":
case "pointerleave":
return ContinuousEventPriority;

case "message": {
// We might be in the Scheduler callback.
// Eventually this mechanism will be replaced by a check
// of the current priority on the native scheduler.
var schedulerPriority = getCurrentPriorityLevel();

switch (schedulerPriority) {
case ImmediatePriority:
return DiscreteEventPriority;

case UserBlockingPriority:
return ContinuousEventPriority;

case NormalPriority:
case LowPriority:
// TODO: Handle LowSchedulerPriority, somehow. Maybe the same lane as hydration.
return DefaultEventPriority;

case IdlePriority:
return IdleEventPriority;

default:
return DefaultEventPriority;
}
}

default:
return DefaultEventPriority;
}
}

function createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags
) {
var eventPriority = getEventPriority(domEventName);
var listenerWrapper;

switch (eventPriority) {
case DiscreteEventPriority:
listenerWrapper = dispatchDiscreteEvent;
break;

case ContinuousEventPriority:
listenerWrapper = dispatchContinuousEvent;
break;

case DefaultEventPriority:
default:
listenerWrapper = dispatchEvent;
break;
}

return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer
);
}

事件执行

当一个事件触发时, 会调用事件绑定时创建的回调函数,这个函数会以批处理的形式调用事件处理方法 dispatchEventsForPlugins

1
2
3
4
5
6
7
8
batchedUpdates(function () {
return dispatchEventsForPlugins(
domEventName, // 事件名称
eventSystemFlags, // 捕获阶段标识
nativeEvent, // 原生事件对象
ancestorInst
);
});

dispatchEventsForPlugins 首先获取触发事件的元素调用 extractEvents 方法,从原生 DOM 上的 stateNode 获取到 FiberNode,并且尝试获取当前元素的绑定事件,在根据不同的事件优先级,包装成不同的合成事件对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
function dispatchEventsForPlugins(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer
) {
var nativeEventTarget = getEventTarget(nativeEvent);
var dispatchQueue = [];

// 内部调用不同的处理函数,用于处理没有原生事件对应的 React 事件,例如 onBeforeInput
// if (shouldProcessPolyfillPlugins) {
// extractEvents$2(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget);
// extractEvents$1(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget);
// extractEvents$3(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget);
// extractEvents(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget);
// }
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags
);
processDispatchQueue(dispatchQueue, eventSystemFlags);
}

function extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
) {
// 不同的事件类型,对应不同的合成事件构造函数
// 内部实现 preventDefault stopPropagation
switch (domEventName) {
case "click":
SyntheticEventCtor = SyntheticMouseEvent;
case "drag":
SyntheticEventCtor = SyntheticDragEvent;
}

// 从当前节点循环遍历到根节点, 收集所有上级节点中绑定当前方法的节点和它的绑定函数
var _listeners = accumulateSinglePhaseListeners(
targetInst,
reactName,
nativeEvent.type,
inCapturePhase,
accumulateTargetOnly
);

if (_listeners.length > 0) {
// Intentionally create event lazily.
var _event = new SyntheticEventCtor(
reactName,
reactEventType,
null,
nativeEvent,
nativeEventTarget
);

dispatchQueue.push({
event: _event,
listeners: _listeners,
});
}
}

function processDispatchQueue(dispatchQueue, eventSystemFlags) {
var inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;

// 循环事件队列,依次触发合成事件
for (let i = 0; i < dispatchQueue.length; i++) {
const {event, listeners} = dispatchQueue[i];
var previousInstance;

(function processDispatchQueueItemsInOrder(
event: ReactSyntheticEvent,
dispatchListeners: Array<DispatchListener>,
inCapturePhase: boolean
): void {
let previousInstance;
if (inCapturePhase) {
// 反响遍历模拟捕获阶段
for (let i = dispatchListeners.length - 1; i >= 0; i--) {
const { instance, currentTarget, listener } = dispatchListeners[i];
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
} else {
// 正向循环模拟冒泡阶段
for (let i = 0; i < dispatchListeners.length; i++) {
const { instance, currentTarget, listener } = dispatchListeners[i];
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
}
}
})()

rethrowCaughtError();
}

React v16 源码分析 ⑪ 更新流程

useState 与 setState 更新流程大致相同,只是入口函数不同。

react 更新是有优先级的,高优先级的任务可能会打断低优先级任务的,低优先级任务会在高优先级更新后的状态上进行更新。

不同组件用不同的处理函数:

  • ReactDOM.createRoot 对应 HostRoot
  • this.setState 对应 ClassComponent
  • this.forceUpdate 对应 ClassComponent
  • useState dispatcher 对应 FunctionComponent
  • useReducer dispatcher 对应 FunctionComponent

HostRoot,ClassComponent 对应如下的结构,tag 字段区分不同的更新触发场景:

  • ReplaceState: 代表在 ClassComponent 生命周期函数中直接改变 this.state
  • UpdateState: 默认情况,通过 ReactDOM.createRoot 或者 this.setState 触发更新
  • CaptureUpdate: 代表发生错误的情况下在 ClassComponent 或 HostRoot 中触发更新(比如通过 getDerivedStateFromError 方法)
  • ForceUpdate: 代表通过 this.forceUpdate 触发更新
1
2
3
4
5
6
7
8
9
10
11
12
13
function createUpdate(eventTime, lane) {
const update = {
eventTime,
lane,
// 区分触发更新的场景
tag: UpdateState,
payload: null,
// UI 渲染后触发的回调函数
callback: null,
next: null,
};
return update;
}

函数组件触发更新时的 update 对象结构:

1
2
3
4
5
6
7
8
const update = {
lane,
action,
// 优化策略相关
hasEagerState: false,
eagerState: null,
next: null,
};
  • 承载更新内容的字段不同,类组件是 payload 字段
  • 更新的紧急程度是 lane 字段表示的
  • 更新之间的顺序,通过 next 字段指向下一个 update,从而形成一个链表

updateQueue 是一个 update 对象组成的链表

  • baseState: 参与计算的初始 state, update 基于该 state 计算新的 state, 可以类比为心智模型中的 master 分支。
  • firstBaseUpdate 与 lastBaseUpdate: 表示更新前该 FiberNode 中已保存的 update, 以链表的形式串联起来。链表头部为 firstBaseUpdate,链表尾部为 lastBaseUpdate。
  • shared.pending: 触发更新后,产生的 update 会保存在 shared.pending 中形成单向环状链表。计算 state 时,该环状链表会被拆分并接在 lastBaseUpdate 后面。
1
2
3
4
5
6
7
8
const updateQueue = {
baseState: null,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
},
};

一轮跟新后如果有两个低优先级更新没有处理,那么这两个更新会放在 baseState 上,如果有另外两个更新进来会放在 shared.pending 上并且形成一个环状链表,下一次 commit 时 shared.pending 会被拼接到 baseState 后面,循环处理哪些符合优先级的更新,基于符合条件的更新,来计算最终的state.

在类组件上会挂载 setState 方法, 在调用的时候会把状态添加到队列中

1
2
3
4
5
6
function Component(props, context, updater) {
this.updater = updater || ReactNoopUpdateQueue;
}
Component.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, "setState");
};

在 render 阶段的 beginWork 中会调用类组件对应的方法创建 Fiber 节点, 类组件会在这时实例化,实例化完成后立即执行 adoptClassInstance 方法, 为实例提供用于更新的 this.updater 对象

1
2
3
4
5
6
7
8
9
10
function constructClassInstance() {
var instance = new ctor(props, context);
adoptClassInstance(workInProgress, instance);
}

function adoptClassInstance(workInProgress, instance) {
instance.updater = classComponentUpdater;
workInProgress.stateNode = instance;
set(instance, workInProgress);
}

当事件被触发之后会调用 updater.enqueueSetState, 与首次 render 阶段时为 FiberRoot 创建更新队列相似

在首次渲染时已经通过 initializeUpdateQueue 为节点初始化了更新队列,现在需要把 update 对象添加到队列中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
var classComponentUpdater = {
isMounted: isMounted,
enqueueSetState: function (inst, payload, callback) {
var fiber = get(inst);
var eventTime = requestEventTime();
var lane = requestUpdateLane(fiber);
var update = createUpdate(eventTime, lane);
update.payload = payload;

if (callback !== undefined && callback !== null) {
update.callback = callback;
}

enqueueUpdate(fiber, update);
var root = scheduleUpdateOnFiber(fiber, lane, eventTime);

if (root !== null) {
entangleTransitions(root, fiber, lane);
}
},
enqueueReplaceState: function (inst, payload, callback) {
var fiber = get(inst);
var eventTime = requestEventTime();
var lane = requestUpdateLane(fiber);
var update = createUpdate(eventTime, lane);
update.tag = ReplaceState;
update.payload = payload;

if (callback !== undefined && callback !== null) {
update.callback = callback;
}

enqueueUpdate(fiber, update);
var root = scheduleUpdateOnFiber(fiber, lane, eventTime);

if (root !== null) {
entangleTransitions(root, fiber, lane);
}
},
enqueueForceUpdate: function (inst, callback) {
var fiber = get(inst);
var eventTime = requestEventTime();
var lane = requestUpdateLane(fiber);
var update = createUpdate(eventTime, lane);
update.tag = ForceUpdate;

if (callback !== undefined && callback !== null) {
update.callback = callback;
}

enqueueUpdate(fiber, update);
var root = scheduleUpdateOnFiber(fiber, lane, eventTime);

if (root !== null) {
entangleTransitions(root, fiber, lane);
}
},
};

update 对象会被添加到 updateQueue.shared.pending 中,并且形成循环链表,如果有新的更新会成为新的头节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function enqueueUpdate(fiber, update, lane) {
var updateQueue = fiber.updateQueue;

if (updateQueue === null) {
// Only occurs if the fiber has been unmounted.
return;
}

var sharedQueue = updateQueue.shared;

if (isInterleavedUpdate(fiber)) {
var interleaved = sharedQueue.interleaved;

if (interleaved === null) {
// This is the first update. Create a circular list.
update.next = update;
// At the end of the current render, this queue's interleaved updates will
// be transferred to the pending queue.

pushInterleavedQueue(sharedQueue);
} else {
update.next = interleaved.next;
interleaved.next = update;
}

sharedQueue.interleaved = update;
} else {
var pending = sharedQueue.pending;

if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}

sharedQueue.pending = update;
}
}

接着触发事件的 Fiber 节点会被传入 scheduleUpdateOnFiber 进行调度, 其中会执行 markUpdateLaneFromFiberToRoot 将每一个 Fiber 节点的 lanes 都合并到父级节点上,这样在 rootFiber 节点上就包含了所有子节点的更新优先级信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function markUpdateLaneFromFiberToRoot(sourceFiber, lane) {
// Update the source fiber's lanes
sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
var alternate = sourceFiber.alternate;

if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, lane);
}

{
if (
alternate === null &&
(sourceFiber.flags & (Placement | Hydrating)) !== NoFlags
) {
warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
}
}

var node = sourceFiber;
var parent = sourceFiber.return;

while (parent !== null) {
parent.childLanes = mergeLanes(parent.childLanes, lane);
alternate = parent.alternate;

if (alternate !== null) {
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
} else {
{
if ((parent.flags & (Placement | Hydrating)) !== NoFlags) {
warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
}
}
}

node = parent;
parent = parent.return;
}

if (node.tag === HostRoot) {
var root = node.stateNode;
return root;
} else {
return null;
}
}

React v16 源码分析 ③ concurrent 模式简介

React 官方提供了三种模式可以选择:

  • legacy 模式: ReactDOM.render(, rootNode)。这是当前 React app 使用的方式。当前没有计划删除本模式,但是这个模式可能不支持这些新功能。
  • blocking 模式: ReactDOM.createBlockingRoot(rootNode).render()。目前正在实验中。作为迁移到 concurrent 模式的第一个步骤。
  • concurrent 模式: ReactDOM.createRoot(rootNode).render()。目前在实验中,未来稳定之后,打算作为 React 的默认开发模式。这个模式开启了所有的新功能。

但即使对于最新版本的 React 也没有主动提供 createRoot 这个 api,需要安装 Alpha 版本, 效果仍然相当于 legacy 模式.

版本演化

React v15 的版本中用到了 Reconciler 和 Renderer 两个部分

这样存在的问题就是对节点的更新是递归同步更新的,如果节点非常多,即使只有一次 state 变更,React 也需要进行复杂的递归更新,更新一旦开始,中途就无法中断,直到遍历完整颗树,才能释放主线程。

而 React v16 添加了前两章提到的,Scheduler, Fiber 这些都为最终实现 concurrent 模式提供了支持

但是在 React v16 版本中只是对能实现 concurrent 模式的这些模块进行了尝试. 最重要的还是在 v16.8 版本中发布了全新了 Hooks Api.

在 v17.0 版本的时候又提出了全新的 lane 模型用于处理更新的优先级. 虽然在 v17.0 版本中没有重大的更新,但是 concurrent 模式已经可以与老的模式稳定共存.

启动过程

传入不同的 RootTag 用于标记不同的类型.这个变量会参与到初始化流程,优先级判断的逻辑中.

更新入口

legacy 模式

1
2
3
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});

concurrent 模式

1
2
3
4
5
6
ReactDOMRoot.prototype.render = ReactDOMBlockingRoot.prototype.render =
function (children: ReactNodeList): void {
const root = this._internalRoot;
// 执行更新
updateContainer(children, root, null, null);
};

异同点

不同的模式,传入不同的 rootTag 类型,最终都调用了 updateContainer 函数串联了 react-dom 与 react-reconciler.

legacy 下的更新会先调用 unbatchedUpdates, 更改执行上下文为 LegacyUnbatchedContext, 之后调用 updateContainer 进行更新.

concurrent 和 blocking 不会更改执行上下文, 直接调用 updateContainer 进行更新.

另外在 React 官方文档中可以找到:

legacy 模式在合成事件中有自动批处理的功能,但仅限于一个浏览器任务。非 React 事件想使用这个功能必须使用 unstable_batchedUpdates。在 blocking 模式和 concurrent 模式下,所有的 setState 在默认情况下都是批处理的,这也意味着 concurrent 模式下所有的更新都是异步的.

  • Copyrights © 2015-2026 SunZhiqi

此时无声胜有声!

支付宝
微信