Code Monkey home page Code Monkey logo

blog's People

Contributors

jessejyang avatar

Watchers

 avatar  avatar

blog's Issues

计算机网络学习总结 — TCP

概述

TCP,传输控制协议(Transmission Control Protocol),是一种面向连接的、可靠的、基于字节流的传输层通信协议。前面我们了解过同为传输层协议的 UDP,那么这两者的区别是什么呢?

与 UDP 区别

  1. 连接方式不同,UDP 面向无连接;TCP 面向有连接。
  2. 传输方式不同,UDP 面向报文,直接将报文发给网络层;TCP 面向字节流,会将报文按照 MTU 进行分包后再发给网络层。
  3. 可靠性不同,UDP 没有重发机制,传输不可靠;TCP 有重发机制,传输可靠。
  4. 传输速度不同,UDP 传输速度快;TCP 传输速度慢。

连接过程

三次握手

在学习 HTTP 时,我们提到过一个 HTTP 请求发送前需要建立 TCP 连接,而一个 TCP 连接的建立需要三次握手,如图所示:
TCP 三次握手

这里会有一个问题,为什么 TCP 需要三次握手,而不是两次或者四次?
网上的回答都很抽象,这里说下个人理解:
在我个人看来,TCP 的三次握手是由 TCP 的可靠性(发送端发送的数据,接收端必须要返回一个 ACK)和连接阶段的 MSS(最大消息长度,Maximum Segment Size)及序列号协商(客户端和服务器端要轮流作为发送端)来保证的。在连接时,要就本次连接的 MSS 及序列号,客户端和服务器端进行协商,协商就是要两端都知道对端想要多少值,如果这样,连接过程就是这样:

  1. 客户端作为发送端,告诉服务器端他的 MSS;
  2. 服务器端作为接收端,向发送端回复 ACK;
  3. 服务器端作为发送端,告诉客户端他接受的 MSS;
  4. 客户端作为接收端,向发送端回复 ACK;
    这样就需要四次握手,但是2,3同为服务器端到客户端是可以合并到一起的,也就是说只需要三次握手。

这样,TCP 的三次握手就可以这样理解:

  1. 客户端:我想跟你建立连接同时告诉你我的 MSS。(发送端)
  2. 服务器端:知道了,也告诉你我的 MSS。(接收端,发送端)
  3. 客户端:知道了。(接收端)

这样的话,如果只有两次是没办法进行协商的,如果四次就浪费了。

状态变化

TCP 状态机

TCP 首部格式

TCP 首部格式

由上图可以看出 TCP 首部格式,有 12 项:源端口号,目标端口号,序列号,确认应答号,数据偏移,保留,控制位,窗口大小,校验和,紧急指针,选项,填充。

源端口号、目标端口号

表明发送端及接收端端口号,各 16 位。

序列号

长度32位,建立连接时,发送端生成初始值,通过 SYN 包发送给接收端,没发送一次数据,该数据就会累加一次。

确认应答号

长度32位,指下次应该收到的数据的序列号。(抓包时有的 TCP 包应答号不变化)

数据偏移

长度4位,表示 TCP 所传输的数据部分应该从哪个位开始算起,也可以看做是首部长度。

保留

长度4位,为了以后扩展时使用,一般设置为0

控制位

长度8位,从左至右依次为 CWR、ECE、URG、ACK、PSH、RST、SYN、FIN。

窗口大小

长度16位,用于通知从相同 TCP 首部的确认应答号所指位置开始能够接收的数据大小(8位)。TCP 不允许发送超过窗口大小的数据。

校验和

长度16位,与 UDP 检验和类似,但无法关闭。

紧急指针

长度16位,只在 URG 控制位为 1 时有效,从数据首位到紧急指针所指示的位置为止为紧急数据。

选项

长度最大40字节,

TCP 的特点

可靠性

序列号与确认应答

简单来说就是发送端发送的报文,接收端收到之后需要向发送端发送确认应答(ACK),发送端收到确认应答,就说明数据已成功到达对端。
如果在一定时间内没有收到,这就可能存在两种情况:

  1. 发送端发送的报文没有到达接收端;
  2. 接收端的确认应答没有到达发送端;

重发超时的确定

重发超时是指在重发数据之前,等待确认应答到来的特定时间间隔。如果超过了这个时间仍未收到确认应答,发送端将进行数据重发;最理想的重发超时是能保证确认应答一定在这个时间内返回,随着网络情况不同,重发超时会进行调整。
TCP 在每次发包时都会计算往返时间(RTT,指报文段的往返时间)及其偏差,重发超时取往返时间及偏差之和稍大点的值。
最初的重发超时一般设置在6s左右,如果数据被重发后仍然没有应答,等待应答的时间将会以2倍,4倍的指数函数增长。达到一定重发次数之后,如果仍没有确认应答,就会判断为网络或对端主机异常,强制关闭连接。

以段为单位

MSS(Maximum Segment Size):最大消息长度,TCP 报文长度的最大值,最理想情况下为不会被 IP 分片的最大数据长度(不包含首部长度),MSS 在 TCP 三次握手时进行协商。接收端在接收到数据包之后,回复的确认应答包头部的序列号指示了下次应当接受的数据的序列号。

窗口大小

TCP 以段为单位进行发送数据,但是如果在 RTT 较长时会非常影响传输性能,为了解决这个问题,TCP 引入了窗口大小来解决。
窗口大小,即无需等待确认应答而可以继续发送数据的最大值。

窗口下的重发机制

在 TCP 的可靠性中,对于未完成的应答存在两种情况,TCP 对着两种情况的处理稍有不同:

  1. 报文没有到达接收端:接收端会发送三次相同序列号的确认应答包,发送端接收到三次相同序列号的确认应答之后,就会重发响应的数据包。
  2. 确认应答没有到达发送端:接收端在接收到数据包后回复带有下个数据包序列号的确认应答,如果该确认应答丢失,而下个数据包的确认应答没有丢失,成功被接收,则发送端认为丢失的确认应答对应的数据包已经被成功接收,如果确认应答全部丢失,理论上会触发超时。

流量控制

在实际情况中,接收端可能存在处理其他任务耗费时间等问题,导致无法对发送的包处理;或者在高负荷状态下无法接收任何数据;如果发送端接着发送数据,就会造成流量的浪费,流量控制就是为了解决这类问题。
接收端向发送端发送自己能够接收的数据大小,发送端不会发送超过这个值的数据,这个值就被称为窗口大小。接收端缓冲区的值一旦面临数据溢出时,窗口大小的值也会随之被设置为更小的值,从而控制数据发送量。

拥塞控制

流量控制是对接收端能够接受的最大数据量的控制,而拥塞控制是对网络能够承载的最大数据量进行的控制。
拥塞控制也是通过窗口大小来控制,拥塞控制的窗口叫做拥塞窗口。实际过程中发送的窗口大小由拥塞窗口及滑动窗口共同决定(取其中最小值)。
拥塞控制主要靠超时及重复确认应答来控制。

慢启动

在数据刚开始传输时由于不知道网络所能承载的数据量大小,所以存在一个慢启动的机制,在慢启动时,拥塞窗口大小为 1,即每次发送一个报文段,发送端每接收到一次确认应答,拥塞窗口大小增加一倍,即呈 1,2,4,8,... 指数形式增长,这样可以快速将网络填满。如果期间发生了丢包,会进行相应的处理:

  1. 超时:将拥塞窗口调整为 1,慢启动阈值调整为当前窗口大小的一半;
  2. 重复确认应答:将拥塞窗口调整为当前窗口大小的一半+3,慢启动阈值调整为当前窗口的一半;

慢启动阈值:当窗口大小超过此阈值后,将以线性增长的方式对拥塞窗口大小进行调整,初始值为滑动窗口大小。

四次挥手

虽说都是说是四次挥手但是不一定需要四次,至少我抓的大部分包都是三次挥手。
TCP 四次挥手

  1. 主动端向被动端发送 FIN,表明主动端数据发送完毕,进入FIN_WAIT_1
  2. 被动端回复 ACK,但仍然可能有未发送的数据,进入CLOSE_WAIT,主动端收到 ACK 进入FIN_WAIT_2
  3. 被动端在发送完毕后,向主动端发送 FIN,表示数据发送完毕,进入LAST_ACK
  4. 主动端接收到 FIN,进入TIME_WAIT,被动端收到 ACK 后进入 CLOSED

参考:
为什么 TCP 需要三次握手--知乎

webgl学习笔记-从画点开始

前言

本系列为作者初学 webgl 的过程记录文章,欢迎大牛多多指点,本系列掺杂作者本人个人理解绝对会存在错误,会出现不定期勘误。
作者文笔有限,主要以代码为主,见谅~

什么是 webgl

简单来说 webgl 为浏览器提供了渲染复杂三维图形的能力,不了解的可以自行搜索,这里不在赘述。

canvas 创建 webgl 上下文

html

<canvas id='canvas'></canvas>

js

const ctx = document.getElementById('canvas')
ctx.getContext('webgl') // 获取 webgl 上下文

对 canvas 比较了解的同学应该知道ctx.getContext存在两个选项2d/webgl,前者用来创建 2d 渲染上下文,后者用来创建 webgl 渲染上下文,这里主要是针对 webgl 绘制上下文的学习。

一个问题:假如我们描述一个点需要怎样描述?
我们需要描述这个点的位置、大小、颜色,那么这些属性在 webgl 中就可以通过顶点着色器与片元着色器来进行描述。

顶点着色器与片元着色器

顶点着色器: 顶点着色器是用来描述顶点特性(如位置)的程序。顶点是指二维或三维空间中的一个点,比如二维或三维图像的端点或交点。
片元着色器: 进行逐片元处理过程如光照的程序。片元是一个 webgl 的术语,可以将其理解为像素。

着色器可以通过下图理解:
webgl 着色器

上面的概念知道就好,下面我们来画个点,理解下着色器。

画个点

const ctx = document.getElementById('canvas').getContext('webgl')

// 顶点着色器
const VSHADER_SOURCE = [
    'void main() {',
    '    gl_Position = vec4(0.0, 0.0, 0.0, 1.0);', // 定义点的位置
    '    gl_PointSize = 10.0;', // 定义点的大小
    '}',
].join('\n')

// 片原着色器
const FSHADER_SOURCE = [
    'void main() {',
    '    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);',
    '}',
].join('\n')

if (ctx) {
    // initShader 定义在 utils/index.js 中,用于创建 program 并绑定着色器
    if (initShader(ctx, VSHADER_SOURCE, FSHADER_SOURCE)) {
        ctx.clearColor(0.0, 0.0, 0.0, 1.0)
        ctx.clear(ctx.COLOR_BUFFER_BIT)

        ctx.drawArrays(ctx.POINTS, 0, 1)
    }
}

通过调整着色器代码,可以改变点的位置、大小、颜色。

这里vec4GLSL中的数据类型,表示 4 个 float 的矢量。

点击画点

// 顶点着色器
const VSHADER_SOURCE = [
    'attribute vec4 a_Position;', // 定义一个变量
    'void main() {',
    '    gl_Position = a_Position;', // 定义点的位置
    '    gl_PointSize = 10.0;', // 定义点的大小
    '}',
].join('\n')

// 获取 a_Position 的存储位置
const a_Position = gl.getAttribLocation(gl.program, 'a_Position')

gl.vertexAttrib3f(a_Position, x, y, 0.0) // 设置属性值

上面的代码只包含差异部分,顶点着色器中定义了变量 a_Position,然后在 js 中获取变量存储位置,鼠标点击后获取点击位置并改变 a_Position 的值,然后 webgl 将点渲染出来。

这里作者在调整位置时候发现不论画布多大,canvas 坐标的范围始终是[-1, 1],应该跟其他因素有关,这里保留疑问。

画多个点

const points = []
canvas.addEventListener('click', e => {
    const x = (e.clientX - left) * 2 / canvas.width - 1
    const y = 1 - (e.clientY - top) * 2 / canvas.height
    points.push(x, y)
    clearCanvas()

    for (let i = 0, len = points.length; i < len; i += 2) {
        gl.vertexAttrib3f(a_Position, points[i], points[i + 1], 0.0) // 设置属性值
        gl.drawArrays(gl.POINTS, 0, 1) // 画点
    }
})

对比画一个点和画多个点会知道,webgl 在每次绘制时会将画布清空,所以画多个点时,需要将以前的点重新绘制。

修改点的颜色

// 片原着色器
const FSHADER_SOURCE = [
    'precision mediump float;',
    'uniform vec4 u_FragColor;', // 定义一个变量
    'void main() {',
    '    gl_FragColor = u_FragColor;',
    '}',
].join('\n')

gl.uniform4f(u_FragColor, points[i + 2], points[i + 3], points[i + 4], 1.0) // 设置颜色属性值

片原着色器中定义变量,使用uniform,对应设置为gl.uniform4f
在修改片原着色器过程中,漏掉了precision mediump float;,找到了stackoverflow的解释,在定义变量时需要告诉 GPU 变量的精度。

扩展一个 Demo

使用上面的例子,写一个模仿鼠标涟漪效果的 Demo,当然形状使用正方形代替。

Vue源码学习(web)—结构及初始化

主要结构

  1. scripts,主要包含了打包文件,build.jsconfig.js,仔细看下基本可以知道打出来的每个包的作用
  2. src,源码文件夹,这个后面说
  3. packagesvue-template-complier这些包的输出目录
  4. 其他都是一些测试,flowts

src结构

  1. platforms/scripts/config.js中的入口文件和一些平台差异处理,每个platforms文件夹结构基本与src保持一致
  2. compiler,将模板解析为render,包括一些指令的解析
  3. core,核心,包括了发布订阅vdom
  4. serverSSR,略
  5. sfc,解析vue文件
  6. shared,工具方法和常量

从简单模板开始

<div id="app">
  <button @click='change'>change</button>
  <div v-for='(n, i) in numbers' :key='i'>{{n.x}}</div>
</div>
<script>
const vm = new Vue({
  data: {
    numbers: [{ x: 1 }, { x: 2 }, { x: 3 }],
    update: 2
  },
  methods: {
    change: function () {
      this.numbers.push({x: ~~(Math.random() * 100)})
    }
  }
}).$mount('#app')
</script>

this._init(options)

主要方法

initLifecycle(vm) // 初始化生命周期,主要是一些标记`_isMounted`, `_isDestoryed`这些
initEvents(vm) // 父级与子级的监听绑定
initRender(vm) // 绑定渲染方法
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm) // 数据代理,初始化并监听数据
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

vm.$mount(vm.$options.el) // 挂载

initState(vm) -- 数据代理

主要方法

initProps(vm, opts.props) // 用于绑定组件`props`
initMethods(vm, opts.methods) // 方法,主要是绑定上下文
initData(vm) // 代理`data`到`_data`,通过`observe`方法监听数据
initComputed(vm, opts.computed) // 将`computed`属性定义到`this`
$watch // 创建一个`watch`
$mount // 挂载

Observer -- 观察者对象

  1. Array

this.observeArray => observe

  1. PlainObject

this.walk => defineReactive

compileToFunctions -- 模板编译(下篇内容)

入口:platforms/web/entry-runtime-with-compiler.js
将字符串形式的template编译为render方法

mountComponent -- 挂载

主要方法

updateComponent // 更新组件,实际是重新`render`
new Watch
this.value = this.get() // 此处触发更新,`vm._update`

this.get() -- 触发更新

pushTarget(this) // 用于`computed`等与属性的绑定
value = this.getter.call(vm, vm) // 触发`vm._update`
popTarget() // 出栈

vm._update -- 渲染/更新

通过patch方法将VNode渲染为真正的Dom(后续说明)

至此,整个Vue的初始化过程结束
下一节,template解析

计算机网络学习总结 — HTTP

超文本传输协议(HyperText Transfer Protocol), 是一种用于分布式、协作式和超媒体信息系统的应用层协议, 是万维网的数据通信的基础(维基百科)。这里我们主要关注一下什么是应用层,以及网络是如何分层的.


网络的分层

网络为什么要分层[1]?

  1. 各层之间相互独立, 某一层并不需要知道它下一层是如何实现的,而仅仅需要知道该层通过层间的接口所提供的服务。由于每一层只实现一种相对独立的功能,因而可以将一个难以处理的复杂问题分解为若干个较容易处理的更小问题,这样,整个问题的复杂度就下降了。
  2. 灵活性, 当任何一层发生变化时,只要层间接口关系保持不变,则在这层以上或以下各层均不受影响,此外,对某一层提供的服务还可以进行修改。当某层提供的服务不再需要时,甚至可以将这层取消
  3. 结构上可分割开。各层都可以采用最合适的技术来实现。
  4. 易于实现和维护。这种结构使得实现和调试一个庞大而又复杂的系统变得易于处理,因为整个系统已被分解为若干个相对独立的子系统。
  5. 能促进标准化工作。因为每一层的功能及其所提供的服务都已有了精确的说明。

OSI七层模型

开放式系统互联通信参考模型,由国际标准化组织提出。
OSI将网络分为七层: 应用层、表示层、会话层、传输层、网络层、链路层、物理层。

TCP/IP五层模型

TCP/IP是实际的标准, 分为五层: 应用层、传输层、网络层、链路层、物理层。

TCP/IPOSI的分层的对应关系, 如下图所示:
TCP/IP与OSI

TCP/IP也可以说是四层, 如果是四层的话就是把链路层物理层统称为网络接口层


HTTP的过程

让我们从一次请求过程来了解HTTP

请求的准备

  1. 在输入URL之后,首先需要DNS来将域名解析为IP地址。
  2. 建立TCP连接(三次握手),建立连接的时候是需要发送IP包的。
  3. 如果是HTTPS会进行TLS/SSL的握手。

此篇不会涉及过多TCP部分的内容,后续会有TCP部分的学习总结。

请求的构建

连接建立以后,就是要向服务器发送请求,那么请求格式是怎么样的呢?
HTTP1.1为明文传输,所以我们很容易能够在ChromeNetwork中看到,请求的格式如下图所示:
HTTP请求格式

从上图可以看出,请求分为了三部分:请求行,首部,实体。

首部与实体之间使用空行分隔。

请求行

请求行由三部分构成:方法,URLHTTP版本号,以空格隔开。

方法

主要的方法有:GET POST OPTIONS HEAD

GET

如果有参数,会将其放在URL中:
优点:

  1. 请求的URL可以被缓存
  2. 可以手动输入,并保存参数
  3. 相对较快(会在TCP第三次握手时将报文随握手包发送)。
    缺点:
  4. 参数有大小限制(受限于URL的长度)
  5. 参数可见,相对不安全

GET请求的过程[2]:

  1. 浏览器请求TCP连接(第一次握手)
  2. 服务器答应进行TCP连接(第二次握手)
  3. 浏览器确认,并发送GET请求头和数据
  4. 服务器返回200 OK响应
POST

参数存在于实体中:
优点:

  1. 能发送更多的数据。
  2. 参数不直接可见,相对安全(相对GET,抓包除外)。
    缺点:
  3. 相对较慢(在首部中相对GET多了几个用于协商的首部,且需要待第三次握手后再发送报文)。

POST请求的过程[2]:

  1. 浏览器请求TCP连接(第一次握手)
  2. 服务器答应进行TCP连接(第二次握手)
  3. 浏览器确认,并发送POST请求头(第三次握手)
  4. 服务器返回100 Continue响应
  5. 浏览器发送数据
  6. 服务器返回200 OK响应
OPTIONS

CORS 中预检请求

HEAD

请求资源的首部信息, 并且这些首部与GET方法请求时返回的一致。响应不应包含响应实体,即使包含了实体也必须忽略掉。

URL

URL(Uniform Resource Locator), 统一资源定位符, 是因特网上标准的资源的地址。

完整格式:协议类型:[//[访问资源需要的凭证信息@]服务器地址[:端口号]][/资源层级UNIX文件路径]文件名[?查询][#片段ID]

另外,与URL相关的定义还有URIURN
URI(Uniform Resource Identifier),统一资源标识符,用于标识某一互联网资源名称的字符串。
URN(Uniform Resource Name),统一资源名称,一种为资源提供持久的、位置无关的标识方式。

URLURNURI的子集,三者关系,如下图所示:
URL, URN, URI

举个栗子:urn:isbn:0-486-27557-4这是一个资源,它是URN,也是URI,但不是URL

HTTP 版本

主要版本:0.9、1.0、1.12

首部

首部是key: value形式,通过冒号空格分隔,为客户端和服务器分别处理请求和相应提供所需要的信息。
首部分为四类:请求首部,响应首部,通用首部,实体首部。

请求首部

顾名思义,只会在请求报文中存在。

  1. Accept-Charset:客户端支持的字符集,例:utf-8
  2. Accept-Encoding:客户端可以接受的内容编码形式,例:gzip
  3. Referer:对请求中URL的原始获取方。
  4. Host:请求资源所在服务器。
  5. ......

通用首部

  1. Accept-Language:提示用户期望获得的自然语言的优先顺序。
  2. User-Agent:用来识别发送请求的浏览器。
  3. Cache-Control:缓存控制。
  4. ......

实体首部

  1. Content-Encoding:实体的编码方式。
  2. Content-Language:实体的自然语言。
  3. Content-Type:实体的媒体类型。
  4. ......

响应首部

  1. Location:令客户端重定向至指定URL
  2. Retry-After:对再次发起请求的时机要求。
  3. ServerHTTP服务器的安装信息。
  4. ETag:资源匹配信息(缓存相关)。
  5. ......

一切准备完成后就是发送请求。

请求的发送与接收

请求发送接收的过程如下图所示:
请求的发送

请求的发送

  1. 浏览器将HTTP构建完成后,通过网络线程将请求报文交给TCP
  2. TCP将请求的报文进行分割,并将各个报文包入TCP的头部,交给IP
  3. IPTCP报文包入IP的头部,交个链路层。
  4. 链路层报上以太网首部通过物理层发送报文(待后续补充)。

请求的接收

  1. 网卡接收到请求后,会先查看目标MAC地址是否为自己的MAC地址,如果是会把以太网头部去除,交个上层协议。
  2. IP收到链路层发送的数据后,会检查目标IP是否为自己的IP,如果是把IP头部去除,交给上层协议。
  3. TCP收到数据后,去掉TCP头部,交给浏览器。

请求的响应

响应的构建

响应的格式

响应与请求除了状态行外结构基本相同,同样分为 3 部分:状态行,首部,实体。

状态行

状态行分为 3 部分:HTTP版本,状态码,短语,以空格分隔,其中短语为对状态码的解释。

状态码

状态码分类

主要状态码介绍
  1. 200 OK:表示从客户端发送来的请求在服务器端被正常处理了。对应请求资源的实体主体随报文首部作为响应返回(HEAD 方法不会返回实体,即使返回也会被忽略)。
  2. 204 Not Content:服务器接收的请求已成功处理,但是再返回的响应报文中不包含实体的主体部分。另外,也不允许返回任何实体的主体。(MDN:使用惯例是,在PUT 请求中进行资源更新,但是不需要改变当前展示给用户的页面,那么返回204 No Content。例如:提交表单后,不进行页面跳转)。
  3. 206 Partial Content:客户端进行了范围请求,而服务器成功执行了这部分的GET请求。响应报文中包含由Content-Range指定范围的的实体内容。
  4. 301 Moved Permanently:永久重定向,表示请求的资源已被分配了新的URL,以后请使用资源现在所指的URL。搜索引擎会根据该响应修正。
  5. 302 Found:临时重定向,表示请求的资源已被分配了新的URL,希望用户(本次)能使用新的URL访问。已改变,且将来还有可能发生改变。
  6. 303 See Other:请求对应的资源存在着另一个URL,应使用GET方法定向获取请求的资源。(301302303响应状态码返回时几乎所有的浏览器都会把POST改成 GET,并删除请求报文的主体,之后请求会自动再次发送。)。
  7. 304 Not Modified:服务器端资源未改变,可直接使用客户端未过期的缓存,返回结果不包含任何响应的主体部分。
  8. 307 Temporary Redirect:临时重定向。与302之间的唯一区别在于,当发送重定向请求的时候,307状态码可以确保请求方法和消息主体不会发生变化。
  9. 400 Bad Request:请求报文中存在语法错误。当错误发生时,需修改请求的内容后再次发送请求。另外,浏览器会像200 OK一样对待该状态码。
  10. 401 Unauthorized:表示由于缺乏目标资源要求的身份验证凭证,发送的请求未得到满足。这个状态码会与WWW-Authenticate首部一起发送,其中包含有如何进行验证的信息。
  11. 403 Forbidden:表示服务器端有能力处理该请求,但是拒绝授权访问(无权限)。
  12. 404 Not Found:表示服务器上无法找到请求的资源。
  13. 500 Internal Server Error:表示服务器端在执行请求时发生了错误,也有可能是 Web 应用存在的 Bug 或某些临时的故障。
  14. 503 Service Unavailable:表示服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。如果事先得知解除以上状况需要的时间,最好写入Retry-After首部字段再返回给客户端。

在完成了响应的构建之后,会按照与发送相同的方式将响应发送给客户端。最后TCP四次回收断开连接。

整个请求过程如下图所示:
HTTP请求过程


HTTP版本

HTTP/0.9 单行协议

HTTP/0.9非常简单,请求仅由单行构成,以唯一可用方法GET开头,其后跟目标资源的路径。

GET /index.html

响应也非常简单仅包含响应文档本身,如果出现错误会将错误信息以文档形式返回。

HTTP/1.0 构建可扩展性

主要引入了头部,及状态码,并支持多种文件格式。

HTTP/1.1 标准化的协议

HTTP/1.1 相对 1.0 的不同:

  1. 持久连接:默认都开启了Keep-Alive,所有连接都被保持,除非在请求头或响应头中指明要关闭:Connection: CloseKeep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件中设定这个时间(实际是在1.0版本引入,但并不会默认开启)。
  2. 管线化:无需等待上次请求返回也可直接发送下一个请求(实际使用受限,浏览器默认不开启)。
  3. 缓存:增加了新的缓存控制首部,如:Cache-Control等。
  4. Host:请求头指明了服务器的域名(对于虚拟主机来说),以及(可选的)服务器监听的TCP端口号。
  5. 内容协商机制: 包括语言,编码,类型等,并允许客户端和服务器之间约定以最合适的内容进行交换。

HTTP/2 为了更优异的表现

HTTP/1.1的区别:

  1. 二进制协议:在不改变方法,首部的基础上,转换为二进制协议,称为二进制分帧层。
  2. 多路复用:即在一个TCP连接中可以同时发送多个请求。
  3. 流控制:是一种阻止发送方向接收方发送大量数据的机制,以免超出后者的需求或处理能力:发送方可能非常繁忙、处于较高的负载之下,也可能仅仅希望为特定数据流分配固定量的资源。
  4. 服务端推送:服务端推送,服务器可以对一个客户端请求发送多个响应。
  5. 头部压缩。

缓存控制

浏览器在首次对资源进行请求时,会记录缓存相关的首部,在后续请求中根据记录的首部进行相应的资源读取操作,如读取缓存,资源验证等。
缓存的作用:减少请求次数,减少带宽;增加加载速度,减少白屏时间。

在说明缓存控制原理之前,先了解下缓存相关的首部。

缓存相关首部

Cache-Control

控制缓存的有效时间,及缓存行为。
主要值:

  1. max-age:缓存有效时间,即从相应时间后多长时间缓存过期。
  2. no-cache:不缓存,并非不对资源进行缓存,而是每次请求都需要向源资源服务器验证资源。
  3. no-store:禁止缓存,禁止浏览器对资源进行缓存,每次请求都要重新请求资源。
  4. public:允许所有用户缓存,包括浏览器,代理服务器等。
  5. private:仅允许单个用户缓存,不允许代理服务器缓存。
  6. ......

对于 Cache-Control: no-cache, max-age=900 这种情况,no-cache 与 max-age 的优先级与先后顺序有关。

Expires

HTTP/1.0提出,表示缓存过期时间,超过这个时间即缓存过期。如果响应中有max-ages-maxage会被覆盖。

expires: Sun, 02 Sep 2018 14:36:18 GMT

Last-modified

资源的最后修改时间,主要用于在服务器验证缓存是否被修改时使用。

last-modified: Fri, 18 May 2018 01:10:24 GMT

ETag

资源实体标识,由服务器分配,资源更新时ETge随之改变,分为ETag与弱ETag
**弱ETag**很容易生成,但不利于比较。**强ETag**是比较的理想选择,但很难有效地生成。

ETag: W/"5a323f72-152"(弱)
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"(强)

缓存请求过程

  1. 判断该资源是否有缓存。
  2. 如有缓存,判断缓存是否过期,如果缓存没过期,直接读取缓存,状态码200 (from memory/disk cache)
  3. 如果缓存过期,判断是否有ETagLast-Modified,如果存在,则在请求首部中发送对应的首部If-None-MatchIf-Modified-Since,如果没有则不发送这两个首部。
  4. 服务器端接收到请求后,判断根据If-None-MatchIf-Modified-Since来比较资源的ETag如果改变了比较Last-Modified,如果匹配则返回304,如果不匹配,将资源随实体返回,状态码200

from memory cache 与 from disk cache 为 Chrome 的缓存优化机制,但对于该采用什么方式,并没有找到明确答案。

强缓存与协商缓存

强缓存:不会向服务器发送请求,直接读取缓存的方式,在上述过程中直接返回状态码200 (from memory/disk cache)
协商缓存:请求资源时会向服务器发送If-None-MatchIf-Modified-Since(如果存在ETagLast-Modified),服务器验证后,返回304200

HTTPS

在学习HTTPS之前,我们了解下HTTP的缺点:

  1. 明文通信(不加密),不安全。
  2. 不验证通信方身份,可能遭遇伪装。
  3. 无法保证报文的完整性,可能在途中遭到了篡改。

HTTPS解决了上述缺点(HTTPS并非新协议,而是HTTP + TLS

对称加密

发送端和接收端使用相同的密钥。发送端使用密钥加密明文,接收端接收后使用密钥解析加密信息,得到明文。
对称加密最大的问题就是在一对多的时候的密钥传输问题,所以为了保证安全对称加密的密钥是绝对不能公开的。

非对称加密

非对称加密有两个密钥,一个是公钥,一个是私钥,公钥加密后的密文只能使用私钥解密,私钥加密后的密文只能使用公钥解密。那么只需要对外展示公钥就可以了。
但是,非对称加密的解密过程速度较慢(对称加密主要是位运算,而非对称加密包含了很多乘法或大数模)。

混合加密

充分利用对称及非对称加密的优势,使用非对称加密的方式传输对称加密的密钥,即有非对称加密的安全又有对称加密的速度。
HTTPS(或者说TLS)就是采用了这种方式。

证书与CA

在拿到公钥后,能不能就直接确定这个公钥是值得信任的?答案是肯定不能,如果公钥是某个黑客伪造的,他就可以修改从发送端接到的请求,在发给服务器了。所以,在拿到公钥后,首先要验证公钥是不是可以信任的,那么谁能保证公钥是可以信任的,那就必须要是一个你信任的机构,这个机构就是CA,而每个站点的公钥实际都是由CA签发的。所以CA可以验证公钥是否属于这个站点的。

签发及验证过程

  1. 服务器将公钥交给CA
  2. CA使用自己的私钥向服务器的公钥部署数字签名,并颁发公钥证书
  3. 客户端在接收到公钥证书后,客户端使用自己信任的CA的公钥(存在于客户端证书信任列表中)去验证签名是否与公钥匹配。
  4. 若匹配则认为公钥是可以信任的。

连接过程

  1. TCP三次握手后,客户端发送请求安全连接(Client Hello),报文中包含一个随机字符串,并列出客户端支持的加密套件,用于协商对称加密的加密方式。
  2. 服务器端回复(Server Hello),报文中包含服务端选择的加密方式;同时将证书和公钥发送给客户端,同时报文中包含一个随机字符串;最后发送完成握手协商结束(Server Hello Done)。
  3. 客户端接收到证书后,根据证书上的CA,使用该CA的公钥解密证书,来验证公钥是否安全,如果CA的证书由上级CA提供且不在信任列表内,则需要一直向上找到客户端信任的CA为止。
  4. 验证公钥安全后,客户端会使用公钥加密并发送一段叫做Pre-master secret的随机字符串,客户端及服务器端使用上面提到的三个随机字符串,并使用协商好的加密算法计算出对称加密的密钥。
  5. 客户端发送Change Cipher Spec报文,以提示服务器后续通信将采用计算好的对称加密密钥进行通信
  6. 客户端继续发送Finished报文,报文中包含连接至今全部报文的整体校验值,如果服务器端能够正确解密该报文,则握手成功
  7. 服务器端发送Change Cipher Spec报文
  8. 服务器端发送Finished报文,连接建立。

WebSocket

WebSocket是可以实现客户端与服务器端双向通信的新协议,除了借助HTTP完成一次握手外,与HTTP没有关系。

连接过程

  1. TCP三次握手。
  2. 客户端发送升级请求,请求首部如下:
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

上述首部中主要是用到了Upgrade首部,用于通知服务器切换协议到WebSocket
3. 服务器端接收到升级协议的请求后,如果支持WebSocket会响应该请求。

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

响应状态码:101,表示服务器端应客户端升级协议的请求正在升级协议。
4. WebSocket握手完成,后续通信将使用WebSocket协议。

参考:
[1]《计算机网络(第五版)》(谢希仁)
[2] http GET 和 POST 请求的优缺点、区别以及误区
[3] TLS 握手优化详解

计算机网络学习总结 — UDP

首先回顾下 TCP/IP 的分层模型。
从上到下分为五层:应用层、传输层、网络层、链路层、物理层。
TCP/IP与OSI

而我们今天学习的 TCP 与 UDP 就是传输层的协议。


前言

传输方式的分类

  1. 面向有连接
  2. 面向无连接

从两张图来理解:
面向有连接
面向无连接

面向有连接

在发送数据前,需要在收发主机之间连接一条通信线路。因此在面向有连接的方式下,必须在通信传输前后,专门进行建立和断开连接的处理。

面向无连接

不需要建立和断开连接。在面向无连接的通信中,不需要确认对端是否存在,即使对端不存在或无法接收数据,发送端也能将数据发送出去。

TCP 属于面向有连接类型,UDP 属于面向无连接类型。

UDP

UDP(User Datagram Protocol),即用户数据报文协议。UDP 不提供复杂的控制机制,进利用 IP 提供面向无连接的通信服务,如果需要流量控制,超时重发等复杂行为需要在应用层实现。包内容如下图所示:
UDP

从上图可以看出,UDP 包含了源端口、目标端口,了解这这两个端口号作用之前先了解什么是端口。

端口是传输层协议(UDP,TCP)往上层应用送数据时的区分,用于识别同一台计算机中进行通信的不同应用程序,也被称为程序地址。

特点

  1. 无连接的,即发送数据之前不需要建立连接,因此减少了开销和发送数据之前的时延。
  2. 不保证可靠交付,因此主机不需要为此复杂的连接状态表
  3. 面向报文的,意思是 UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界,在添加首部后向下交给 IP 层。
  4. 没有阻塞控制,因此网络出现的拥塞不会使发送方的发送速率降低。
  5. 支持一对一、一对多、多对一和多对多的交互通信,也即是提供广播和多播的功能。
  6. 首部开销小,首部只有 8 个字节,分为四部分。

使用场景

对效率要求较高,对准确率要求不高的场景:

  1. 包总量较少的通信。
  2. 视频,音频等多媒体通信(即使通信)
  3. 广播通信(广播,多播)

UDP 相关协议

DNS

DNS(Domain Name System),域名系统,用于实现网络 IP 地址和主机域名的映射。

域名

域名,是互联网基础架构的关键部分。它们为互联网上任何可用的网页服务器提供了人类可读的地址。
为了达到唯一性的目的,因特网在命名的时候采用了层次结构的命名方法。每一个域名(本文只讨论英文域名)都是一个标号序列(labels),用字母(A-Z,a-z,大小写等价)、数字(0-9)和连接符(-)组成,标号序列总长度不能超过255个字符,它由点号分割成一个个的标号(label),每个标号应该在63个字符之内,每个标号都可以看成一个层次的域名。级别最低的域名写在左边,级别最高的域名写在右边。域名服务主要是基于UDP实现的,服务器的端口号为53[2]。

组成

一个域名是由几部分(有可能只是一部分,也许是两部分,三部分...)组成的简单结构,它被点分隔,并需要从右到左阅读。
一个完整的域名(.com、.net、.cn、.top等)由二个或二个以上部分组成,各部分之间用英文的句号"."来分隔,最后一个"."的右边部分称为顶级域名(TLD)顶级域名"."的左边部分称为一级域名,一级域名"."的左边部分称为二级域名(SLD),二级域名的左边部分称为三级域名,以此类推,每一级的域名控制它下一级域名的分配。
以 ubanjiaoyu.com 为例

ubanjiaoyu com
二级域名(Second Level Domain) 顶级域名(Top-Level Domain)

uu: 子域名,比如,mail.example.com 和 calendar.example.com 是 example.com 的两个子域,而 example.com 则是顶级域 .com 的子域[3]。

域名服务器

域名服务器

DNS 过程

DNS 过程

  1. 先查询本机DNS缓存,没有找到则查询本地HOST文件,没有找到则查找本地域名服务器
  2. 本地域名服务器弱没有缓存则向根域名服务器进行查询
  3. 根域名服务器告诉本地域名服务器,下一步应该向顶级域名服务器的 IP 地址查询
  4. 本地域名服务器向顶级域名服务器进行查询
  5. 顶级域名服务器me告诉本地域名服务器,下一步向权威服务器的 IP 地址查询
  6. 本地域名服务器向权威服务器进行查询
  7. 权限服务器告诉本地域名服务器所查询的主机的 IP 地址
  8. 本地域名服务器最后把查询结果告诉主机

DNS 报文格式

DNS 报文格式

头部

会话标识(2字节):是 DNS 报文的 ID 标识,对于请求报文和其对应的应答报文,这个字段是相同的,通过它可以区分 DNS 应答报文是哪个请求的响应

标志(2字节):
1. QR(1bit)查询/响应标志,0 为查询,1 为响应
2. opcode(4bit)0 表示标准查询,1 表示反向查询,2 表示服务器状态请求
3. AA(1bit)表示授权回答
4. TC(1bit)表示可截断的
5. RD(1bit)表示期望递归
6. RA(1bit)表示可用递归
7. (zero) (3bit)填充
8. rcode(4bit)表示返回码,0 表示没有差错,3 表示名字差错,2 表示服务器错误

数量字段(总共8字节):Questions、Answer RRs、Authority RRs、Additional RRs 各自表示后面的四个区域的数目。Questions 表示查询问题区域节的数量,Answers 表示回答区域的数量,Authoritative namesversers 表示授权区域的数量,Additional recoreds 表示附加区域的数量。

正文

正文分为四部分:查询区域,结果区域,授权区域,附加区域。

Queries

查询名:长度不固定,且不使用填充字节,一般该字段表示的就是需要查询的域名(如果是反向查询,则为 IP,反向查询即由 IP 地址反查域名),一般的格式如下图所示。

查询类型[5]:

类型 助记符 说明
1 A 由域名获得IPv4地址
2 NS 查询域名服务器
5 CNAME 查询规范名称
6 SOA 开始授权
11 WKS 熟知服务
12 PTR 把IP地址转换成域名
13 HINFO 主机信息
15 MX 邮件交换
28 AAAA 由域名获得IPv6地址
252 AXFR 传送整个区的请求
255 ANY 对所有记录的请求

查询类:通常为 1,表明是 Internet 数据

资源记录(RR)区域(包括回答区域,授权区域和附加区域)

资源记录区域格式

域名(2字节或不定长):它的格式和Queries区域的查询名字字段是一样的。有一点不同就是,当报文中域名重复出现的时候,该字段使用2个字节的偏移指针来表示。
查询类型:表明资源纪录的类型。
查询类:对于 Internet 信息,总是IN。
生存时间(TTL):以秒为单位,表示的是资源记录的生命周期,即缓存有效时间。
资源数据:该字段是一个可变长字段,表示按照查询段的要求返回的相关资源记录的数据。可以是 Address(表明查询报文想要的回应是一个 IP 地址)或者 CNAME(表明查询报文想要的回应是一个规范主机名)等。

QUIC

QUIC(quick udp internet connection),快速 UDP 互联网连接,Google 提出的一种基于 UDP 改进的通信协议,其目的是降低网络通信的延迟,提供更好的用户互动体验。

参考:
[1]: follow-tcp-stream-where-does-field-stream-index-come-from
[2]: DNS协议详解及报文格式分析
[3]: 子域名
[4]: Nslookup tool
[5]: RFC 1035

我不知道的JavaScript之MutationObserver

MutationObserver 提供了在DOM变化时的一种响应方式,被用来在DOM3事件规范中替换Mutation Events

MDN

MutationObserver MDN

使用

let observer = new MutationObserver(
  function callback
)

observer.observe(Node target, MutationObserverInit options)

方法

observe

// 订阅`target`
void observe(Node target, MutationObserverInit options)

disconnect

// 停止订阅
void disconnect()

takeRecords

// 清空订阅队列并返回
Array takeRecords()

MutationObserverInit

Property Type Description
childList Boolean 设置为true将在target节点的子元素(包含文本节点)增加或删除时被通知
attributes Boolean 设置为true将在target的属性节点改变时被通知
characterData Boolean 设置为true将在target的数据(即节点的inner内容)改变时被通知
subtree Boolean 设置为true递归监听target的子树
attributeOldValue Boolean
characterDataOldValue Boolean
attributeFilter Boolean

评价

本质上应该是一个DOM观察者,具体使用场景未知

2018-02-14 更新

今天了解到一个使用场景,是可以监听DOM,防止脚本动态插入

2018-03-13 更新

可以使用此API在DOM中实现类似nextTick的功能,不知道Vue里面也是这样实现的

【译】开始在web中使用CPU计算

本文是关于我使用实验性的WebGPU API并与有兴趣使用GPU进行数据并行计算的Web开发人员分享我的旅程。

背景

众所周知,图形处理单元(GPU)是计算机中最初专用于处理图形的一个电子子系统。但是,在获取的10年里,GPU利用其独特的架构已经发展成为一种不仅能渲染3D图形,也允许开发人员实现多种类型算法的更加灵活的架构。这些功能称为GPU计算,将GPU用作通用科学计算的协处理器称为通用GPU(GPGPU)编程。

GPU计算为最近的机器学习热潮做出了重要贡献,因为卷积神经网络和其他模型可以利用该架构在GPU上更高效地运行。由于当前的Web平台缺乏GPU计算功能,W3C的“ Web上的GPU”社区小组正在设计一种API,为当前大多数设备上提供可用的现代GPU API。该API称为WebGPU

WebGPU是一个低级API,例如WebGL。如你所见,其非常强大且冗长。但是没关系我们更在乎的是性能。

在本文中,我将重点介绍WebGPU的GPU计算部分,老实说,我讲的会比较浅,让你可以自己开始玩就可以了。下一篇文章中我将更深入地探讨WebGPU渲染(画布,纹理等)。

PS: 目前,WebGPU已在Chrome 78实验性功能提供了。你可以在chrome://flags/#enable-unsafe-webgpu来开启它。它还处在实验阶段,API可能会不断变化,目前使用很不安全。由于尚未为WebGPU API实现GPU沙箱,因此可以读取其他进程的GPU数据!故不要在启用网络的情况下浏览网络。

访问GPU

在WebGPU中访问GPU很容易。调用navigator.gpu.requestAdapter(),该方法将会返回一个在解决时返回一个GPU adaptor的JavaScript promise。可以把返回的adaptor看做GPU。它既可以是集成GPU(与CPU在同一芯片上),也可以是独立GPU(通常是性能更高但功耗更高的PCIe卡)。

有了GPU适配器后,调用adapter.requestDevice()来获得一个promise,通过该promise可以得到一个能够用于执行一些GPU计算的GPU device。

const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();

如果你想的话,这两个方法都可以通过传入参数,来具体确定所需的适配器类型(电源偏好)和设备(扩展,限制)。为了简单起见,我们将在本文中使用默认选项。

写入缓冲存储器

让我们看看如何使用JavaScript将数据写入GPU的内存。由于现代网络浏览器中使用的沙箱模型,因此此过程并不简单。

下面的示例展示了如何将四个字节写入可从GPU访问的缓冲存储器。调用device.createBufferMappedAsync()来获取缓冲区的大小及其用法。即使此特定调用不需要指定标识GPUBufferUsage.MAP_WRITE,这里也要明确要数据将写入此缓冲区。最后通过promise来返回GPU缓冲区对象和它的原始二进制数据缓冲区。

如果您已经使用过ArrayBuffer,写入字节应该会很容易;使用TypedArray并将值复制过来。

// Get a GPU buffer and an arrayBuffer for writing.
// Upon success the GPU buffer is put in the mapped state.
const [gpuBuffer, arrayBuffer] = await device.createBufferMappedAsync({
  size: 4,
  usage: GPUBufferUsage.MAP_WRITE
});

// Write bytes to buffer.
new Uint8Array(arrayBuffer).set([0, 1, 2, 3]);

此时,GPU缓冲区映射到了CPU中,并且可以通过JavaScript进行读取/写入。为了使GPU能够访问它,必须调用gpuBuffer.unmap()将其取消映射。
使用映射/未映射的概念可以防止GPU和CPU同时访问内存的竞争情况。

读取缓冲存储器

现在,让我们看看如何将一个GPU缓冲区复制到另一个GPU缓冲区并读取出来。

由于我们正在写入第一个GPU缓冲区,并且希望将其复制到第二个GPU缓冲区,因此需要一个新的使用标志GPUBufferUsage.COPY_SRCdevice.createBuffer()可以同步地创建出处于未映射状态的第二个GPU缓冲区。这里使用标志是GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,因为它将用作第一个GPU缓冲区的目标,并在执行了GPU复制命令后就读入JavaScript。

// Get a GPU buffer and an arrayBuffer for writing.
// Upon success the GPU buffer is returned in the mapped state.
const [gpuWriteBuffer, arrayBuffer] = await device.createBufferMappedAsync({
  size: 4,
  usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC
});

// Write bytes to buffer.
new Uint8Array(arrayBuffer).set([0, 1, 2, 3]);

// Unmap buffer so that it can be used later for copy.
gpuWriteBuffer.unmap();

// Get a GPU buffer for reading in an unmapped state.
const gpuReadBuffer = device.createBuffer({
  size: 4,
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
});

由于GPU是独立的协处理器,所有GPU命令都是异步执行的。这就是为什么需要构建并批量发送GPU命令的列表的原因。在WebGPU中,由device.createCommandEncoder()方法返回的GPU命令编码是构建一批“缓冲”命令的JavaScript对象,这些命令将在某个时候发送到GPU。另一方面,GPUBuffer上的方法是“未缓冲的”,这意味着它们在被调用时会自动执行。

有了GPU命令编码器后,如下所示调用copyEncoder.copyBufferToBuffer()将此命令添加到命令队列中以供以后执行。最后,通过调用copyEncoder.finish()完成编码命令,将其提交到GPU设备命令队列。该队列负责处理通过device.getQueue().submit()完成提交的,改方法以GPU命令作为参数。队列中的命令将按照顺序执行。

// Encode commands for copying buffer to buffer.
const copyEncoder = device.createCommandEncoder();
copyEncoder.copyBufferToBuffer(
  gpuWriteBuffer /* source buffer */,
  0 /* source offset */,
  gpuReadBuffer /* destination buffer */,
  0 /* destination offset */,
  4 /* size */
);

// Submit copy commands.
const copyCommands = copyEncoder.finish();
device.getQueue().submit([copyCommands]);

需要注意,已发送的GPU队列命令,不一定会执行。通过调用gpuReadBuffer.mapReadAsync()可以读取第二个GPU缓冲区。它返回一个promise,一旦所有排队的GPU命令都已执行,它将使用包含与第一个GPU缓冲区相同的值的ArrayBuffer进行解析。

// Read buffer.
const copyArrayBuffer = await gpuReadBuffer.mapReadAsync();
console.log(new Uint8Array(copyArrayBuffer));

你可以试试这个简单的例子

简而言之,下面是关于缓冲存储器的操作你需要记住的:

  • 必须取消映射GPU缓冲区才能在设备队列提交中使用。
  • 映射后,可以使用JavaScript读写GPU缓冲区。
  • 调用mapReadAsync()mapWriteAsync()createBufferMappedAsync()createBufferMapped()可以映射GPU缓冲区。

着色器编程

在GPU上运行的仅执行计算(而不绘制三角形)的程序称为计算着色器。它们由数百个GPU内核(小于CPU内核)并行执行,这些GPU内核共同操作以处理数据。它们输入、输出到WebGPU中的缓冲区。

为了说明计算着色器在WebGPU中的使用,我们将尝试下矩阵乘法,这是机器学习中的一种常见算法,如下所示。
Figure 1. Matrix multiplication diagram

简而言之,我们要做的如下:

  1. 创建三个GPU缓冲区(两个用于矩阵相乘,一个用于结果矩阵)
  2. 描述计算着色器的输入和输出
  3. 编译计算着色器代码
  4. 设置计算管道
  5. 批量提交编码后的命令到GPU
  6. 读取结果矩阵GPU缓冲区

创建GPU缓冲区

为了简单起见,矩阵将表示为浮点数列表。第一个元素是行数,第二个元素是列数,其余是矩阵的元素。
Figure 2. Simple representation of a matrix in JavaScript and it's equivalent in mathematical notation

这三个GPU缓冲区是存储缓冲区,因为我们需要在计算着色器中存储和检索数据。这就是为什么我们使用GPUBufferUsage.STORAGE标志位来创建GPU缓冲区。结果矩阵使用标志GPUBufferUsage.COPY_SRC,因为一旦所有GPU队列命令全部执行完毕,它将被复制到另一个缓冲区以进行读取。

const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();


// First Matrix

const firstMatrix = new Float32Array([
  2 /* rows */, 4 /* columns */,
  1, 2, 3, 4,
  5, 6, 7, 8
]);

const [gpuBufferFirstMatrix, arrayBufferFirstMatrix] = await device.createBufferMappedAsync({
  size: firstMatrix.byteLength,
  usage: GPUBufferUsage.STORAGE,
});
new Float32Array(arrayBufferFirstMatrix).set(firstMatrix);
gpuBufferFirstMatrix.unmap();


// Second Matrix

const secondMatrix = new Float32Array([
  4 /* rows */, 2 /* columns */,
  1, 2,
  3, 4,
  5, 6,
  7, 8
]);

const [gpuBufferSecondMatrix, arrayBufferSecondMatrix] = await device.createBufferMappedAsync({
  size: secondMatrix.byteLength,
  usage: GPUBufferUsage.STORAGE,
});
new Float32Array(arrayBufferSecondMatrix).set(secondMatrix);
gpuBufferSecondMatrix.unmap();


// Result Matrix

const resultMatrixBufferSize = Float32Array.BYTES_PER_ELEMENT * (2 + firstMatrix[0] * secondMatrix[1]);
const resultMatrixBuffer = device.createBuffer({
  size: resultMatrixBufferSize,
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
});

绑定组布局和绑定组

绑定组布局和绑定组的概念特定于WebGPU。绑定组布局定义了着色器所需的输入/输出接口,而绑定组表示着色器的实际输入/输出数据。

在下面的示例中,绑定组布局期望计算着色器的编号绑定0、1和2处有一些存储缓冲区。另一方面,为此绑定组布局定义的绑定组将GPU缓冲区与绑定关联:gpuBufferFirstMatrix绑定到绑定0,gpuBufferSecondMatrix绑定到绑定1,resultMatrixBuffer绑定到绑定2。

const bindGroupLayout = device.createBindGroupLayout({
  bindings: [
    {
      binding: 0,
      visibility: GPUShaderStage.COMPUTE,
      type: "storage-buffer"
    },
    {
      binding: 1,
      visibility: GPUShaderStage.COMPUTE,
      type: "storage-buffer"
    },
    {
      binding: 2,
      visibility: GPUShaderStage.COMPUTE,
      type: "storage-buffer"
    }
  ]
});

const bindGroup = device.createBindGroup({
  layout: bindGroupLayout,
  bindings: [
    {
      binding: 0,
      resource: {
        buffer: gpuBufferFirstMatrix
      }
    },
    {
      binding: 1,
      resource: {
        buffer: gpuBufferSecondMatrix
      }
    },
    {
      binding: 2,
      resource: {
        buffer: resultMatrixBuffer
      }
    }
  ]
});

计算着色器代码

用于乘法矩阵的计算着色器代码用GLSL编写,GLSL是WebGL中使用的高级着色语言,其语法基于C编程语言。无需赘述,你应该能在下面找到三个用关键字buffer标记的存储缓冲区。该程序使用firstMatrixsecondMatrix作为输入,并使用resultMatrix作为其输出。

请注意,每个存储缓冲区都有一个binding限定符,该限定符与在上面声明的绑定组布局和绑定组中定义的相同索引相对应。

const computeShaderCode = `#version 450

  layout(std430, set = 0, binding = 0) readonly buffer FirstMatrix {
      vec2 size;
      float numbers[];
  } firstMatrix;

  layout(std430, set = 0, binding = 1) readonly buffer SecondMatrix {
      vec2 size;
      float numbers[];
  } secondMatrix;

  layout(std430, set = 0, binding = 2) buffer ResultMatrix {
      vec2 size;
      float numbers[];
  } resultMatrix;

  void main() {
    resultMatrix.size = vec2(firstMatrix.size.x, secondMatrix.size.y);

    ivec2 resultCell = ivec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y);
    float result = 0.0;
    for (int i = 0; i < firstMatrix.size.y; i++) {
      int a = i + resultCell.x * int(firstMatrix.size.y);
      int b = resultCell.y + i * int(secondMatrix.size.y);
      result += firstMatrix.numbers[a] * secondMatrix.numbers[b];
    }

    int index = resultCell.y + resultCell.x * int(secondMatrix.size.y);
    resultMatrix.numbers[index] = result;
  }
`;

设置管道

Chrome中的WebGPU当前使用字节码代替原始的GLSL代码。这意味着我们必须在运行计算着色器之前编译computeShaderCode。幸运的是,@webgpu/glslang包使我们能够以Chrome中的WebGPU接受的格式编译computeShaderCode。该字节码是格式基于SPIR-V的安全子集。

注意,“WebGPU” W3C社区组仍未决定编写WebGPU的着色语言。

import glslangModule from 'https://unpkg.com/@webgpu/[email protected]/dist/web-devel/glslang.js';

计算管道是实际描述我们将要执行的计算操作的对象。通过调用device.createComputePipeline()创建。该方法包含两个参数:我们之前创建的绑定组布局,以及一个计算阶段,该阶段定义了我们的计算着色器(主要GLSL函数)和使用glslang.compileGLSL()编译的实际计算着色器模块的入口点。

const glslang = await glslangModule();

const computePipeline = device.createComputePipeline({
  layout: device.createPipelineLayout({
    bindGroupLayouts: [bindGroupLayout]
  }),
  computeStage: {
    module: device.createShaderModule({
      code: glslang.compileGLSL(computeShaderCode, "compute")
    }),
    entryPoint: "main"
  }
});

提交命令

在使用我们的三个GPU缓冲区和具有绑定组布局的计算管道实例化绑定组之后,就该使用它们了。
让我们使用commandEncoder.beginComputePass()启动一个可编程计算过程编码器。我们将使用它来编码将执行矩阵乘法的GPU命令。通过passEncoder.setPipeline(computePipeline)设置其管道,并通过passEncoder.setBindGroup(0, bindGroup)在索引0处设置其绑定组。索引0对应于GLSL代码中的set = 0限定符。
现在,让我们讨论一下此计算着色器将如何在GPU上运行。我们的目标是逐步针对结果矩阵的每个单元并行执行此程序。例如,对于2乘4大小的结果矩阵,我们将调用passEncoder.dispatch(2,4)来编码执行命令。第一个参数“ x”是第一个维度,第二个参数“ y”是第二个维度,最后一个参数“ z”是第三个维度,默认情况下为1,因为我们在这里不需要它。在GPU中,对在一组数据上执行内核功能的命令进行编码称为调度。
Figure 3. Execution in parallel for each result matrix cell
在我们的代码中,“ x”和“ y”将分别是第一个矩阵的行数和第二个矩阵的列数。这样,我们现在可以使用passEncoder.dispatch(firstMatrix [0],secondMatrix [1])调度计算调用。
如上图所示,每个着色器都可以访问唯一的gl_GlobalInvocationID对象,该对象将用于得到要计算的结果矩阵像元。

const commandEncoder = device.createCommandEncoder();

const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(computePipeline);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.dispatch(firstMatrix[0] /* x */, secondMatrix[1] /* y */);
passEncoder.endPass();

可以通过调用passEncoder.endPass()来结束计算过程编码器。然后,创建一个GPU缓冲区用作目的地,以使用copyBufferToBuffer复制结果矩阵缓冲区。最后,使用copyEncoder.finish()完成编码命令,并通过使用GPU命令调用device.getQueue()submit()将它们提交到GPU设备队列。

// Get a GPU buffer for reading in an unmapped state.
const gpuReadBuffer = device.createBuffer({
  size: resultMatrixBufferSize,
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
});

// Encode commands for copying buffer to buffer.
commandEncoder.copyBufferToBuffer(
  resultMatrixBuffer /* source buffer */,
  0 /* source offset */,
  gpuReadBuffer /* destination buffer */,
  0 /* destination offset */,
  resultMatrixBufferSize /* size */
);

// Submit GPU commands.
const gpuCommands = commandEncoder.finish();
device.getQueue().submit([gpuCommands]);

读取结果矩阵

读取结果矩阵就像调用gpuReadBuffer.mapReadAsync()并记录由结果promise返回的ArrayBuffer一样容易。
Figure 4. Matrix multiplication result
我们的代码在控制台中记录的结果是“ 2、2、50、60、114、140”。

// Read buffer.
const arrayBuffer = await gpuReadBuffer.mapReadAsync();
console.log(new Float32Array(arrayBuffer));

恭喜你做到了。你可以看看这个示例

性能

那么在GPU上运行矩阵乘法与在CPU上运行矩阵乘法相比又如何呢?为了找出答案,我编写了刚刚针对CPU编写的程序。如下图所示,当矩阵的大小大于256 x 256时,使用GPU是一个显而易见的选择。
Figure 5. GPU vs CPU benchmark
本文只是我探索WebGPU的旅程的开始。很快就会有更多文章发表,它们将更深入地介绍GPU Compute,以及有关WebGPU中渲染(画布,纹理,采样器)的工作方式。

[译]Promises/A+

一个健全的,交互的 Javascript promises的开放标准—来源于社区,服务于社区。

一个promise代表一个异步操作的最终结果。promise交互的的主要方式是通过在其then方法中注册回调函数来接收一个promise的最终结果或者此promise无法完成的原因。
then方法行为的细节规范,为所有符合Promises/A+规范的实现提供了一个交互基础。因此本规范可以被认为非常稳定。尽管Promises/A+组织或许会偶尔修改本规范,并以轻微的向后兼容的修改来解决新发现的角落案例,但我们只有在仔细考虑,讨论和测试之后才会整合大型或后向不兼容的变更。
就发展而言,Promises/A+解释了早期的Promises/A 提案的行为条款,扩展它以涵盖实际行为并删除未指定或有问题的部分。
最终,Promises/A+规范的核心并不涉及到如何创建,完成,或拒绝promises,而是聚焦在提供一个具有交互性的then方法。但未来的配套规范可能涉及这些主题。

1. 术语

1.1. "promise"是一个具有符合本规范定义行为的then方法的对象或函数。
1.2. "thenable"是一个定义了then方法的对象或函数。
1.3. "value"是任意的合法的JavaScript值(包括undefined,thenable,或promise)。
1.4. "exception"是一个被throw方法抛出的值。
1.5. "reason"是一个说明了promise被拒绝执行的原因的值。

2. 要求

2.1. Promise 状态

一个Promise必须是下述三个状态之一:pending, fulfilled, 或 rejected。

2.1.1. pending状态下的promise:
  2.1.1.1. 可能会被转化到fulfilled或rejected状态。
2.1.2. fulfilled状态下的promise:
  2.1.2.1. 无法被转换到其他状态。
  2.1.2.2. 有一个无法被改变的"value"
2.1.3. rejected状态下的promise:
  2.1.3.1. 无法被转换到其他状态。
  2.1.3.2. 有一个无法被改变的"reason"。

这里的"无法被改变"意味着指向不变(例如:===),但并不意味着深度不可变。(1)

2.2. then方法

一个promise必须提供一个then方法以访问其当前的或最终的"value"或"reason"。
promise的then方法接受连个参数:

promise.then(onFulfilled, onRejected)

2.2.1. onFulfilledonRejected都是一个可选参数:
  2.2.1.1. 如果onFulfilled不是一个函数,则必须被忽略。
  2.2.1.2. 如果onRejected不是一个函数,则必须被忽略。
2.2.2. 如果onFulfilled是一个函数:
  2.2.2.1. 必须在promise完成后被调用,并将promise的"value"作为第一个参数。
  2.2.2.2. 不能在promise完成前被调用。
  2.2.2.3. 不能被多次调用。
2.2.3. 如果onRejected是一个函数:
  2.2.3.1. 必须在promise被拒绝后被调用,并将promise的"reason"作为第一个参数。
  2.2.3.2. 不能在promise拒绝前被调用。
  2.2.3.3. 不能被多次调用。
2.2.4. 在执行上下文栈只包含平台代码[3.1]之前,onFulfilledonRejected不能被调用。
2.2.5. onFulfilledonRejected必须作为函数被调用(即不能指定this值)。[3.2]
2.2.6. then或许会在一个promise中被多次调用。
  2.2.6.1. 如果/当promise被完成时,每一个thenonFulfilled回调必须按照各自的注册顺序依次执行。
  2.2.6.2. 如果/当promise被拒绝时,每一个thenonRejected回调必须按照各自的注册顺序依次执行。
2.2.7. then必须返回一个promise[3.3]。

promise2 = promise1.then(onFulfilled, onRejected);

  2.2.7.1. 如果onFulfilledonRejected返回了值x,执行此Promise的解决方法[[Resolve]](promise2, x)
  2.2.7.2. 如果onFulfilledonRejected抛出了 "exception" epromise2必须被以e为"reason"被拒绝。
  2.2.7.3. 如果onFulfilled不是一个函数且promise1已经被完成,promise2必须以与promise1相同的"value"完成。
  2.2.7.4. 如果onRejected不是一个函数且promise1已经被拒绝,promise2必须以与promise1相同的"reason"拒绝。

2.3. Promise解决方法

本规范的Promise解决方法是以一个promise和一个value为输入的抽象操作,以[[Resolve]](promise2, x)表示。如果x是一个"thenable",将尝试以promise来处理x(在这种假设下,x的行为接近一个promise),否则将以x为"value"来完成promise
对于暴露了与 Promises/A+ 规范兼容的then方法的"thenable",这样处理将能够使promise间实现交互。而且,它也能够使 符合Promises/A+ 的实现能够“吸收”不符合标准的但具有合理的then方法的实现。
运行[[Resolve]](promise2, x)时,会执行以下步骤:
2.3.1. 如果promisex指向相同,以TypeError作为reason来拒绝promise
2.3.2. 如果x是一个promise,采取其状态[3.4]:
  2.3.2.1. 如果x状态为pending,promise必须保持pending,直到x被resolved或rejected。
  2.3.2.2. 如果/当x状态为fulfilled,用同样的值完成promise
  2.3.2.3. 如果/当x状态为rejected,用同样的原因拒绝promise
2.3.3. 除此之外,如果x为一个对象或函数,
  2.3.3.1. 声明thenx.then[3.5]。
  2.3.3.2. 如果检索属性x.then时抛出了"exception" e ,则以e为"reason"拒绝promise。
  2.3.3.3. 如果then是一个函数,调用它,并将其then指向x,第一个参数为resolvePromise,第二个参数为rejectPromise,其中:
    2.3.3.3.1. 如果/当resolvePromisey为"value"被调用,运行[[Resolve]](promise2, y)。(2)
    2.3.3.3.2. 如果/当rejectPromiser为"reason"被调用,以r拒绝promise
    2.3.3.3.3. 如果resolvePromiserejectPromise都被调用,或者以相同参数多次调用,只执行第一次调用,并忽略其余任何形式的调用。
    2.3.3.3.4. 如果then执行过程中抛出"exception" e
      2.3.3.3.4.1. 如果resolvePromiserejectPromise已经被调用,则忽略。
      2.3.3.3.4.2. 除此之外,以e为"reason"拒绝promise。
  2.3.3.4. 如果then不是一个函数,以x完成promise。
2.3.4. 如果x不是一个对象或函数,以x完成promise。
如果一个promise做为在一个thenable循环链中的一环而被解决,这种[[Resolve]](promise2, y)的循环性质将导致[[Resolve]](promise2, y)被重复调用,遵循上述算法将会导致无限递归。本规范鼓励,但不强制要求实现对这种递归进行检测,若对其进行了检测,可以以一个TypeError为"reason"来拒绝执行promise。[3.6]

3. 说明

3.1. 此处的platform code指的是引擎,环境和promise履行代码。在实践中,要求确保onFulfilledonRejectedthen被调用后event loop转入并在新的堆栈中异步执行。这种异步执行可以用像setTimeoutsetImmediate这种“宏任务”机制,或者使用MutationObserverprocess.nextTick这种“微任务”机制实现。由于promise的执行被认为是平台代码,它本身可能包含任务调度队列或处理程序被调用的“蹦床”。
3.2. 也就是说,在严格模式下在onFulfilledonRejected 内部 this 将会是undefined;在宽松模式下,将指向全局对象。
3.3. 在针对本规范的实现符合所有要求的前提下,允许promise2 === promise1,但需要说明是否会产生promise2 === promise1以及在什么条件下产生。
3.4. 通常,只有x是来自当前实现时才认为其是一个真正的promise。此条款允许实现根据自身特定的属性去查询状态(3)。
3.5. 为避免多次访问x.then属性,首先要存储对x.then的引用,之后测试并调用此引用。这样的预防措施对于确保访问者被检索属性期间其属性的一致性非常重要。
3.6. 本规范的实现不应该设置临界值来界定thenable链的深度是否无限。只有真正的循环才应该导致一个TypeError;如果遇到了一个不同thenable组成的链,永远的递归是正确的行为。

译者注

(1): 若"value"或"reason"在thenable链传递过程中由a被修改为b,则必须满足a === b,即值类型需保持值不变,引用类型需保持指向不变;
(2): 这里的机制保证了promise实现与其他标准实现或thenable的交互;
(3): 此处真正的promise实际是2.3.3中x为对象或函数时的特殊情况,相当于把实现自身的实例化的promise对象特殊处理,在2.3.3中已经包含了对这种情况的处理;

针对本规范的实现

写给自己的Promise内部原理

Promise原理

前两天面试的时候被问到了Promise,虽说自己照着规范敲过一边,但要我描述竟然没很好的描述出来,特此整理。
PS:此篇并不涉及对Promise/a+规范的讲解,仅为个人对Promise的机制理解

Promise/a+介绍

规范地址, 自己的理解翻译

执行过程

正常用法

new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('I am from Promise')
  }, 1000)
}).then(data => {
  console.log(data)
})


then方法将传入的callback存入Promise中,待调用resolve时依次执行then中注册的方法。
对,就是这样,结束了,再见👋!

花钱请的托A:等等,这样怎么输出?

new Promise((resolve, reject) => {
  resolve('I am from Promise') // 执行resolve的时候then还没有注册callback
}).then(data => {
  console.log(data) // 这个还会输出么?
})

// 此方法仅做演示说明
function resolve (data) {
  setTimeout(function execCallbackQueue () {
	// 这里依次执行callback
  }, 0)
}

resolve实际上是将执行回调队列的方法推进了js的任务队列,待主线程完成后(此时then已经将callback注册进Promise),会执行任务队列(不了解任务队列的同学可以自行google:任务队列,Event Loopmacrotaskmicrotask)。

由上面就可以知道,在主线程执行完成前是不会执行execCallbackQueue方法的,等到主线程执行完毕,才会执行任务队列中的execCallbackQueue方法,此时then方法已经被执行并将callback注册到Promise


思考下这样会是怎么样?

let promise = new Promise((resolve, reject) => {
  resolve('I am from Promise')
})
// 2s 后执行 `then`,`promise`一定已经被`resolve`了
setTimeout(() => {
  promise.then(data => {
    console.log(data)
  })
}, 2000)

原理总结

  1. new Promise(fun)实例化过程中将resolvereject为参数调用fun,实例化完成返回promise实例;
  2. resolve方法调用时将执行promise中已经注册的callback
  3. 对于已经resolvePromise,在调用其then方法时,会在下次事件循环执行至任务队列时调用其已经注册的callback

此上皆基于个人理解,并非规范讲解,如有错误欢迎批评🤝
若想对Promise有更深的理解,推荐自己根据规范实现一边

【译】使用默认方式更新service worker

原文地址:https://developers.google.com/web/updates/2019/09/fresher-sw
作者:Jeff Posnick


从 Chrome 68 开始,service worker 脚本检查更新的HTTP请求将默认不受 HTTP cache 的影响。这可以解决开发人员的共同难题,即在 service worker 脚本上设置无意的 Cache-Control 标头可能导致的更新延迟。
如果你已经使用Cache-Control: max-age=0/service-worker.js脚本指定了HTTP缓存,那么由于新的默认行为,你应该不会看到/service-worker.js脚本任何更改。
此外,从Chrome 78开始,service worker中对于通过importScripts()加载的脚本将逐字节进行比较。对导入脚本的任何更改都会触发service worker更新流程,就像对顶级service worker发生改变一样。

背景

当每次访问一个service worker作用域下的新页面时,通过从JavaScript中显示得调用registration.update()或者通过pushsync事件来"唤醒"该service worker时,浏览器将并行请求最初由navigator.serviceWorker.register()请求的JavaScript资源,以更新service worker脚本。
出于本文的目的考虑,我们先假设其URL为/service-worker.js,并包含单个importScripts()引入脚本,这样调用将加载在service worker中运行的其他代码。

// Inside our /service-worker.js file:
importScripts('path/to/import.js');

// Other top-level code goes here.

有什么变化?

对于 Chrome 68 之前的版本,/service-worker.js的更新请求会受HTTP缓存的影响(包含大多数的fetch请求)。这就意味着,如果最初脚本的请求存在请求头Cacha-Control: max-age=600,接下来的600秒(10分钟)内脚本将不会通过网络进行更新,因此用户可能不会更新到service worker的最新版本。但是如果max-age大于86400(24小时),则将其视为86400,以避免用户永远被某个特定版本所困扰。

译者注:service worker 存在一个更新机制,至少一天会更新一次。

从Chrome 68开始,更新service worker脚本时,HTTP缓存将被忽略,因此,68版本后的浏览器中可以看到Web应用对其service worker脚本的请求频率增加,但importScripts的请求仍受HTTP缓存影响,并提供了一个新的注册选项updateViaCache来控制这种行为。

updateViaCache

在Chrome 68以后,开发者可以在调用navigator.serviceWorker.register()时传递新参数:updateViaCache,其有三个可选值:importsallnone

这些值决定了对于检查 service worker 更新而发出HTTP请求,浏览器的HTTP缓存是否起作用以及如何发挥作用。

  • 当值为imports时,HTTP缓存将不会影响/service-worker.js的更新,但会影响service worker中引入的脚本(在我们的例子中是指path/to/import.js)。这是Chrome 68之后版本的默认项。
  • 当值为all时,HTTP缓存将影响从顶级/service-worker.js脚本中发出的所有请求,包括引入的脚本,例如:path/to/import.js。此选项对应于Chrome 68之前版本的行为。
  • 当值为none时,HTTP缓存将不会影响从顶级/service-worker.js脚本中发出的所有请求,包括引入的脚本,例如假想的path/to/import.js

例如,以下代码将注册service worker,并确保在检查更新/service-worker.js脚本或通过importScripts()引用的任何脚本时,其不受HTTP缓存影响。

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js', {
    updateViaCache: 'none',
    // Optionally, set 'scope' here, if needed.
  });
}

对引入脚本的检查更新

在Chrome 78之前的版本,通过importScripts()引入的service worker脚本将仅被检索一次(检查是使用HTTP缓存还是通过网络请求,者取决于updateViaCache的值)。最初的获取之后,它将被浏览器存储在内部,并且永远不会重新获取。
仅仅有一种方式可以强制更新已经安装的service worker引入的脚本就是更新其URL,通常是通过添加一个semver值(例如 importScripts('https://example.com/v1.1.0/index.js'))或通过包含内容的哈希值(例如 importScripts('https://example. com/index.abcd1234.js'))。更改导入的URL的副作用是service worker脚本的内容发生了更改,这又触发了service worker的更新流程
从Chrome 78开始,每次对service worker脚本执行更新检查时,都将同时检查导入脚本的内容是否已更改。如果updateViaCache被设置为all或者imports(默认值),对于引入的脚本将根据其使用的Cache-Control头部来进行检查更新。如果updateViaCache被设置为none,对于引入的脚本将直接通过网络请求获取。

如果是逐字节地与由service worker引入并暂存的脚本比对而引起的更新,即使顶级service worker文件保持不变,也将触发完整的service worker更新流程。

Chrome 78的行为与几年前Firefox 56在Firefox中实现的行为相同。Safari也已经实现了此行为。

开发者需要做什么

如果你通过使用Cache-Control: max-age=0(或类似值)为/service-worker.js脚本有效地选择了HTTP缓存,那么不需要任何更改,因为这是默认行为。
如果想在/service-worker.js脚本脚本中开启HTTP缓存,你想这样做或者这是你的默认环境的默认行为,你或许会看到/service-worker.js中向服务器发送的命中HTTP缓存的请求数量增加。如果想让Cache-Control头部影响/service-worker.js的更新,则当你注册service worker的时候需要显示地设置updateViaCache: 'all'
考虑到浏览器版本升级需要些时间,因此即使在较新的浏览器上可以忽略它们,但依然推荐在service worker脚本上设置Cache-Control: max-age=0HTTP头。
开发者可以利用这个时机来决定是否要从HTTP缓存中显式选择导入的脚本,如果合适的话可以在其service worker注册时添加updateViaCache:'none'

提供导入的脚本

从Chrome 78开始,由于需要检查更新importScripts()加载的资源,开发者或许会看到更多通过importScripts()加载的资源的HTTP请求。
如果想避免这种额外的HTTP流量,可以在脚本的URL中包含semverhash,并设置长效的Cache-Control头,并使用默认的updateViaCache: "imports"行为。
另外,如果希望检查导入的脚本经常更新,请确保为它们提供Cache-Control: max-age=0,或者使用updateViaCache: 'none'

扩展阅读

对于Web开发者,建议阅读Jake Archibald的The Service Worker LifecycleCaching best practices & max-age gotchas

Vue源码学习(web)—模板编译

new Vue(opts)中,opts不含render,且包含templateel

初始化compileToFunctions

// entry-runtime-with-compiler.js
const { render, staticRenderFns } = compileToFunctions(template, {
    shouldDecodeNewlines,
    shouldDecodeNewlinesForHref,
    delimiters: options.delimiters,
    comments: options.comments
}, this)

其中compileToFunctionscompiler/indexcreateCompilercreateCompileToFunctionFn创建,createCompiler又由createCompilerCreator创建
作用:

  1. createCompilerCreatorbaseOptionsprototype创建finalOptions,创建warn并将自定义modulesdirectivesbaseOptions合并

疑惑:为什么要const finalOptions = Object.create(baseOptions),感觉没必要,除非不想深拷贝

  1. createCompileToFunctionFn通过闭包缓存编译结果
  2. createCompiler包装,返回coderenderstaticRenderFns

解析过程

template => AST 方法 parseHTML

// template
<div id="app">
  <button @click='change'>change</button>
  <div v-for='(n, i) in numbers' :key='i'>{{n.x}}</div>
</div>

let textEnd = html.indexOf('<'),判断标签起始下标,如果textEnd === 0,依次使用正则判断是否属于: 注释,条件注释,DoctypeEnd tagStart tag

Start tag为例

  1. 使用正则匹配结果找到tagName,并将tagNamehtml中去除
  2. 解析改标签的attr,若匹配属性的结果返回保存到attrs数组中
// 实际是个数组
{
    0:' id="app"',
    1:"id",
    2:"=",
    3:"app",
    4:undefined,
    5:undefined,
    groups:undefined,
    index:0,
    input:' id="app">↵    <button @click="change">change</button>↵    <div v-for="(n, i) in numbers" :key="i">{{n.x}}</div>↵  </div>',
    length:6
}

若匹配到改tag的结束,将所有结果返回。
3. 得到返回值后,解析返回值,并将结果传递给option.start中,生成AST,保存到栈中,若存在currentParent,同时将其置入currentParentchildren中;对于本身不是闭合标签,将该标签保存为objectpushstack(待关闭标签栈)中,等待读取闭合标签,并执行上述步骤。
4. 解析到闭合标签后,从stack中将匹配到的闭合标签移除,将结果传递给option.endoption.end中将栈中最后一个移除,并修改currentParent

通过上述步骤,就得到了编译后的AST,图示如下:
image

生成AST

上述过程已经生成,createASTElement只不过是个简单的转换

对指令属性的格式化

在解析为AST后,依次对v-for,v-if,v-once,element进行格式化,主要方式就是若存在该指令将其重el.attrsMap中删除,并做对应解析,并将解析结果扩展到el中。

v-for

(i, j, k) in a为例

{
    for: 'a',
    alias: 'i',
    iterator1: 'j',
    iterator2: 'k',
}
v-if
{
    if: string
    else?: string,
    elseif?: string
}
v-once
{
    once?: boolean
}
key
{
    key?: string
}
ref
{
    ref: string,
    refInFor: boolean
}
solt
{
    slotName?: string,
    slotScope?: string,
    slotTarget?: string
}
component

解析isinline-template指令

{
    component?: string, // `is`
    inlineTemplate?: string // `inline-template`
}
属性格式化
  1. 匹配修饰符,保存到数组中
  2. value解析过滤器
    将表达式字符串按单个字符进行解析,若匹配到|,将上一个匹配中间的值作为过滤器保存到数组中
  3. 特殊修饰符的转换
    .prop(未找到文档,应该是prop转换来的),.camel.sync
  4. 将解析的指令,propevent保存至对应数组

按照上述步骤,将template编译为了AST

AST => render

先看下最终结果

// 手动格式化了下
with(this){
    return _c(
        'div',
        {attrs:{"id":"app"}},
        [
            _c(
                'button',
                {on:{"click":change}},
                [
                    _v("change")
                ]
            ),
            _v(" "),
            _l(
                (numbers),
                function(n,i){
                    return _c(
                        'div',
                        {key:i},
                        [
                            _v(_s(n.x))
                        ]
                    )
                }
            )
        ],
        2
    )
}

render各方法及codegen对应生成条件

// core/instance/render.js
// initRender
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

// core/instance/render-helpers/index.js
// installRenderHelper
target._o = markOnce // el.once && !el.onceProcessed
target._n = toNumber // 修饰符 .number
target._s = toString // 指令或属性只能接受字符串时
target._l = renderList // el.for && !el.forProcessed
target._t = renderSlot // el.tag === 'slot'
target._q = looseEqual // `radio`及`checkbox`组件`v-model`
target._i = looseIndexOf // 同`_q`,数组形式
target._m = renderStatic // el.staticRoot && !el.staticProcessed
target._f = resolveFilter // 过滤器
target._k = checkKeyCodes // 键盘事件修饰符
target._b = bindObjectProps // v-bind 一个对象
target._v = createTextVNode // 文本VNode
target._e = createEmptyVNode // 注释VNode,占位
target._u = resolveScopedSlots // scoped-slots
target._g = bindObjectListeners // v-on

我不知道的JavaScript之Error

简介

Error构造函数可以创建一个继承自它的并可以在运行时抛出错误的error对象。你也可以继承它并创建自己的Error对象。

使用(测试环境:node6)

new Error([message[, fileName[, lineNumber]]])

PS:node6只有一个message参数,其他两个无效,Browser下未测试

参数

message: // 可选,错误描述
fileName: // 可选,定义错误文件名称
lineNumber: // 可选,定义错误行数

Properties

有效

Error.prototype.stack

错误堆栈跟踪
Error.prototype.message
错误信息,参数中的message

无效(很多只在FF有效)

Error.prototype.description

Error.prototype.number

Error.prototype.fileName

Error.prototype.lineNumber

Error.prototype.columnNumber

Methods

有效

Error.prototype.toString()

return Error.stack

无效

Error.prototype.toSources()

Returns a string containing the source of the specified Error object; you can use this value to create a new object. Overrides the Object.prototype.toSource() method.
感觉没什么用,懒得翻译了

原生Error类型

  • EvalError

一个关于 eval 函数的错误。此异常不再会被JavaScript抛出,但是EvalError对象仍然保持兼容性。

  • RangeError

一个值不在其所允许的范围或者集合中。

  • ReferenceError

引用错误,存在一个不存在的变量被引用。

  • SyntaxError

表尝试解析语法上不合法的代码的错误。

  • TypeError

类型错误,用来表示值的类型非预期类型时发生的错误。

  • URIError

表示以一种错误的方式使用全局URI处理函数而产生的错误。

  • InternalError(非标准)

出现在JavaScript引擎内部的错误。 例如: "InternalError: too much recursion"(内部错误:递归过深)。

创建自己的Error

这个没什么好说的,继承Error就可以了。

webgl学习笔记-绘制和变换三角形

缓冲区

缓冲区对象是 webgl 系统中提供的一块存储区,可以在其中保存想要绘制的所有顶点的数据,然后一次性向顶点着色器中传入数据。

缓冲区绘制

使用缓冲区对象向顶点着色器传入数据的步骤为:

  1. 创建缓冲区对象(gl.createBuffer
  2. 绑定缓冲区对象(gl.bindBuffer
  3. 将数据写入缓冲区对象(gl.bufferData
  4. 将缓冲区对象分配给一个 attribute 变量(gl.vertexAttribPointer
  5. 开启 attribute 变量(gl.enableVertexAttribArray

使用webgl缓冲区绘制点的代码示例:

const canvas = document.getElementById('canvas')
const gl = canvas.getContext('webgl')

// 顶点着色器
const VSHADER_SOURCE = [
  'attribute vec4 a_Position;',
  'void main() {',
  '    gl_Position = a_Position;', // 定义点的位置
  '    gl_PointSize = 10.0;', // 定义点的大小
  '}',
].join('\n')

// 片原着色器
const FSHADER_SOURCE = [
  'void main() {',
  '    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);',
  '}',
].join('\n')

// initShader 定义在 utils/index.js 中,用于创建 program 并绑定着色器
if (gl && GlHelper.initShader(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
  gl.clearColor(0.0, 0.0, 0.0, 1.0)
  gl.clear(gl.COLOR_BUFFER_BIT)

  const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
  const vertices = new Float32Array([
    0, 0, 0.5, 0.5, -0.5, -0.5
  ])

  // 创建缓冲区
  const vertexBuffer = gl.createBuffer()
  if (!vertexBuffer) {
    console.error('创建Buffer失败!')
  } else {
    // 绑定缓冲区
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
    // 将数据写入缓冲区
    gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)
    // 指定变量
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
    // 开启变量
    gl.enableVertexAttribArray(a_Position)
    // 绘制缓冲区内的数据
    gl.drawArrays(gl.POINTS, 0, 3)
  }
}

使用 Buffer 绘制多个点

drawArrays

gl.drawArrays(mode, start, end)提供了绘制点、线、回路等绘制模式,通过第一个参数指定不同的值,就能以 7 种不同的方式来绘制图形。

基本图形 参数 mode 描述
gl.POINTS 绘制 v0,...,vn 的点
三角形 gl.LINES 按照 (v0, v1),(v2,v3),... 规则绘制线,点数不足将忽略
线条 gl.LINE_STRIP 按照 (vs, vs+1),(vs+1,vs+2),... 规则绘制线
回路 gl.LINE_LOOP 按照 (vs, vs+1),(vs+1,vs+2),... 规则绘制线
三角形 gl.TRIANGLES 按照(v0, v1, v2),(v3, v4, v5),... 的规则绘制三角形,点数不足将忽略
三角带 gl.TRIANGLE_STRIP 按照(v0, v1, v2),(v1, v2, v3),... 的规则绘制三角形
三角扇 gl.TRIANGLE_FAN 按照(v0, v1, v2),(v0, v2, v3),... 的规则绘制三角形

下面我们用 Demo 来看下各种图形是怎么样的:
使用 drawArrays 绘制更多图形

Demo 中我们采用不同的方式绘制了同样的缓冲区数据,得到了7中不同结果,可以结合 Demo 去理解每种类型分别的绘制规则。

// Demo中绘制的缓冲区数据
const vertices = new Float32Array([
  0, -0.5, 0.5, 0, 0, 0.5, -0.5, 0
])

变换

如果用 JS 写过动画的同学应该知道,可以通过相隔指定时间修改元素的宽、高等属性,来实现元素的动画,同样的方法我们可以通过修改指定图形的坐标来调整绘制图形的位置,进而实现动画。
由于 webgl 是建立在三维坐标系上的,对三维坐标的变换就不得不提到矩阵,这里不做详细介绍,只提供平移、旋转、缩放的简单公式,仅供参考。

平移

x' = x + Tx
y' = y + Ty
z' = z + Tz

平移就是图形上的点全部加上相同的距离,可以通过调整 Tx、Ty、Tz 的值来实现曲线的平移效果。

图形的平移变换Demo

旋转

x' = x * cosα - y * sinα
y' = x * sinα + y * cosα
z' = z

上述公式表明了,将图形以 z 轴旋转 α 角度时,x、y 的坐标变换公式。

缩放

x' = S * x
y' = S * y
z' = S * z

啊,这个很好理解,乘以缩放系数就对了。

图形的缩放变换Demo

【译】不是 TypeScript 的 TypeScript -- JSDoc 的超能力

原文链接:https://fettblog.eu/typescript-jsdoc-superpowers/
作者:@ddprrt
时间:2019.07.16

我们可以把 TypeScript 看做为 JavaScript 添加了类型注释的薄层,而类型注释可以确保不会犯任何错误。TypeScript 团队也在努力确保类型检查适用于常规 JavaScript 文件。TypeScript的编译器(tsc)以及 VSCode 等编辑器中的语言支持提供了出色的开发人员体验,而无需任何编译步骤。我们来看看如何使用。

目录

带有 JSDoc 注释的 TypeScript

在最优的情况下,TypeScript 能够通过从使用 JavaScript 的方式正确推断来找出正确的类型。

function addVAT(price, vat) {
  return price * (1 + vat) // 喔!你使用并加上了数字,所以他是 number。
}

在上面的例子中,我们增加了值。这个操作只对 number 是合法的,有了这些信息,TypeScript 知道addVAT的返回值将是 number。
为确保输入值正确,我们可以添加默认值:

function addVAT(price, vat = 0.2) { // 棒, `vat` 也是 number!
  return price * (1 + vat)
}

但类型推断只能到目前为止。我们可以通过添加 JSDoc 注释为 TypeScript 提供更多信息:

/**
 * Adds VAT to a price
 * 
 * @param {number} price The price without VAT
 * @param {number} vat The VAT [0-1]
 * 
 * @returns {number}
 */
function addVAT(price, vat = 0.2) {
  return price * (1 + vat)
}

关于这点 Paul Lewis 有一个很棒的视频。类型有很多很多,比评论中的几种基本类型更多。结果就是使用 JSDoc 类型可以让你走得很远。

激活检查

为了确保您不仅能够获得类型信息,而且在编辑器中(或通过tsc)获得实际的错误反馈,请激活源文件中的@ts-check标志:

// @ts-check

如果有一个特定的行出错,但你知道这样更好,请添加 @ts-ignore 标志:

// @ts-ignore
addVAT('1200', 0.1); // would error otherwise

内联类型

定义参数的时候,希望确保尚未分配的变量具有正确的类型,这是可以使用 TypeScript 的内联类型注释。

/** @type {number} */
let amount;
amount = '12'; // 💥 does not work

不要忘记正确的注释语法。使用//的内联注释不起作用。

定义对象

除了基本类型,在 JavaScript 中还经常使用到复杂类型和对象,这种情况对基于注释的类型注释也没有问题:

/**
 * @param {[{ price: number, vat: number, title: string, sold?: boolean }]} articles
 */
function totalAmount(articles) {
  return articles.reduce((total, article) => {
    return total + addVAT(article)
  }, 0)
}

我们定义了一个复杂的对象类型(就像我们在 TypeScript 中所做的那样)内联作为参数。
内联注释一切都会很快变得拥挤。通过 @typedef 定义对象类型是一种更优雅的方式:

/**
 * @typedef {Object} Article
 * @property {number} price
 * @property {number} vat
 * @property {string} string
 * @property {boolean=} sold
 */

/**
 * Now we can use Article as a proper type
 * @param {[Article]} articles
 */
function totalAmount(articles) {
  return articles.reduce((total, article) => {
    return total + addVAT(article)
  }, 0)
}

写的更多,但更具可读性。此外,TypeScript 可以识别名称为 Article 的 Article,从而在 IDE 中提供更好的信息。
请注意名为sold的可选参数。它的定义是@property {boolean =} sold。另一种语法是@property {boolean} [sold]。功能@params也是如此。

定义函数

函数也能够在内联中被定义, 就像对象一样:

/**
 * @param {string} url
 * @param {(status: number, response?: string) => void} cb
 */
function loadData(url, cb) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url)
  xhr.onload = () => {
    cb(xhr.status, xhr.responseText)
  }
}

同样,这很快就会变得非常混乱。 可以使用@callback修改:

/**
 * @callback LoadingCallback
 * @param {number} status
 * @param {string=} response
 * @returns {void}
 */

/**
 * @param {string} url
 * @param {LoadingCallback} cb
 */
function loadData(url, cb) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url)
  xhr.onload = () => {
    cb(xhr.status, xhr.responseText)
  }
}

@callback采用与函数注释相同的参数,与@typedef类似

导入类型

@typedef允许您从任何其他 .js 或 .ts 文件导入类型。这样,您就可以在 TypeScript 中编写 TypeScript 类型定义,并将它们导入源文件中。
article.ts:

export type Article = {
  title: string,
  price: number,
  vat: number,
  sold?: boolean,
}

main.js

// The following line imports the Article type from article.ts and makes it
// available under Article
/** @typedef { import('./article').Article } Article */

/** @type {Article} */
const article = {
  title: 'The best book in the world',
  price: 10,
  vat: 0.2
}

还可以直接在类型注释中导入类型:

/** @type {import('./article').Article} */
const article = {
  title: 'The best book in the world',
  price: 10,
  vat: 0.2
}

这在没有环境类型定义的情况下处理混合的 TypeScript 时非常棒。

使用泛型

只要存在可以通用的类型,TypeScript 的泛型语法就可用:

/** @type PromiseLike<string> */
let promise;

// checks. `then` is available, and x is a string
promise.then(x => x.toUpperCase())

您可以使用@template注释定义更精细的泛型(尤其是带有泛型的函数)。

/**
 * @template T
 * @param {T} obj
 * @param {(keyof T)[]} params
 */
function pluck(obj, ...params) {
  return params.map(el => obj[el])
}

这样很方便,但对复杂的泛型有点难。内联泛型仍然使用 TypeScript 方式:

/** @type { <T, K extends keyof T>(obj: T, params: K[]) => Array<T[K]>} */
function values(obj, ...params) {
  return params.map(el => obj[el])
}

const numbers = values(article, 'price', 'vat')
const strings = values(article, 'title')
const mixed = values(article, 'title', 'vat')

还有更复杂的泛型?考虑将它们放在 TypeScript 文件中并通过导入功能导入它。

枚举

将特殊结构化的 JavaScript 对象转换为枚举,并确保值一致:

/** @enum {number} */
const HTTPStatusCodes = {
  ok: 200,
  forbidden: 403,
  notFound: 404,
}

枚举与常规 TypeScript 枚举有很大不同, 枚举确保此对象中的每个键都具有指定的类型。

/** @enum {number} */
const HTTPStatusCodes = {
  ok: 200,
  forbidden: 403,
  notFound: 404,
  errorsWhenChecked: 'me' // 💣
}

这就是他们所做的一切。

typeof

这是我最喜欢的工具之一,typeof 也可用。为您节省大量编辑时间:

/**
 * @param {number} status The status code as a number
 * @param {string} data The data to work with
 */
function defaultCallback(status, data) {
  if(status === 200) {
    document.body.innerHTML = data
  }
}

/**
 * @param {string} url the URL to load data from
 * @param {typeof defaultCallback} cb what to do afterwards
 */
function loadData(url, cb) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url)
  xhr.onload = () => {
    cb(xhr.status, xhr.responseText)
  }
}

从类扩展

extends允许您在从基本 JavaScript 类扩展时指定通用参数。请参阅以下示例:

/**
 * @template T
 * @extends {Set<T>}
 */
class SortableSet extends Set {
  // ...
}

另一方面,@augments可以使泛型参数更具体:

/**
 * @augments {Set<string>}
 */
class StringSet extends Set {
  // ...
}

真方便!

写在最后

纯 JavaScript 中增加 TypeScript 注释可以更好地维护项目。特别是在输入泛型时,TypeScript 还有一些功能,但是对于很多基本任务,你可以在不安装任何编译器情况下获得很多编辑器的能力。
知道的更多?给我发一条推文。我很高兴在这里添加它们。

更多关于 TypeScript 的文章

TypeScript: Match the exact object shape
TypeScript: The constructor interface pattern
TypeScript and React Guide: Added a new styles chapter
TypeScript and React Guide: Added a new render props chapter
TypeScript and React Guide: Added a new prop types chapter

想评论?给我发推特吧!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.