Giter Site home page Giter Site logo

learning's Introduction

<Introduction/>

🌱 Actively studying compiler principles systematically and eager to engage in compiler-related work.

👋 Keep learning and challenging yourself.

🤔 How to make front-end development more enjoyable?

  • Automate repetitive tasks.
  • Define common problems and provide solutions.
  • Reduce the number of decisions to be made.
  • Tools should be stable, efficient, and flexible.

🤪 Intermittent development tools, continuous garbage production.

💪 Trying hard to lose weight.

<GitHub Stats/>

learning's People

Contributors

symind avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

zengshide123

learning's Issues

SVG

坐标系统

视口

文档打算使用的画布区域称作为视口。可以在 元素上使用 width 和 height 属性确定视口的大小。

使用默认用户坐标

阅读器设置了一个坐标系统,其中水平坐标(x 坐标)向右递增,垂直坐标(y 坐标)垂直向下递归。视口左上角为原点(0,0)。

viewBox

viewBox 属性允许指定一个给定的一组图形伸展以适应特定的容器元素。

viewBox 属性的值是一个包含4个参数的列表 min-x,min-y,width 和 height,以空格或者逗号分隔开,在用户空间中指定一个矩形区域映射到给定的元素。

保留宽高比

如果视口的宽高比和 viewBox 不一样,会发生什么情况?

<svg width="45px" height="135px" viewBox="0 0 90 90">

在这种情况下,SVG 可以做三件事。

  • 按较小的尺寸等比缩放图形,以使图形完全填充视口。
  • 按较大的尺寸等比缩放图形裁剪掉超出视口的部分。
  • 拉伸和挤压绘图使其恰好填充新的视口。

为 preserveAspectRatio 指定对齐方式

preserveAspectRatio 属性允许我们指定被缩放的图像相对视口的对齐方式,以及是希望它适配边缘还是要裁剪。

嵌套坐标系统

可以在任何时候将另一个 元素插入到文档中来建立新的视口和坐标系统。

moveto、lineto 和 closepath

每个路径都必须以 moveto 命令开始。

命令字母为大写的 M,紧跟着一个使用逗号或空格分隔的 x 和 y 坐标。这个命令用来设置绘制轮廓的『笔』的当前位置。

lineto 命令用大写 L 表示,它的后面也是由逗号或空格分割的 x 和 y 坐标。

大写 Z 表示的 closepath 命令绘制一条直线回到当前子路径的起点。

相对 moveto 和 lineto

如果使用小写命令字母,坐标会被解析为相对于当前画笔位置。

如果使用小写 m(moveto)启动路径,它的坐标会被解析为绝对位置,因为它没有参照位置来计算相对位置。

路径的快捷方式

水平和垂直 lineto 命令

路径可以使用 H 命令加绝对 x 坐标,或者 h 命令加相对 x 坐标,来指定一条水平线。类似地,垂直线可以使用 V 命令加绝对 y 坐标,或者 v 命令加相对 y 坐标来指定。

路径快捷方式表示法

  • 可以在 L 或者 l 后面放多组坐标。
  • 如果在 moveto 后面放置多对坐标,除了第一对坐标外,剩下的坐标都会被假设为它们跟在一个 lineto 后面。
  • 所有不必要的空白都可以消除。命令字母后面不需要空白,因为所有的命令都是一个字母。数字和命令之间不需要空白,因为命令字母并不能作为数字的一部分。正数和负数之间也不需要空白,因为负数的前置减号并不能作为正数的一部分。

椭圆弧

由于两点之间可以绘制无线条曲线,因此必须给出额外信息,已在它们之间绘制一条曲线路径。椭圆弧是绘制一个连接两个点的椭圆的一部分。

圆弧命令以字母 A (绝对坐标的缩写)或者 a (相对坐标的缩写)开始,紧跟以下 7 个参数。

  • 点所在椭圆的 x 半径和 y 半径。
  • 椭圆的 x 轴旋转角度 x-axis-rotation。
  • large-arc-flag 如果需要圆弧的角度小于 180 度,其为 0;如果圆弧的角度大于或等于 180 度,则为 1。
  • sweep-flag,如果需要的弧以负角度绘制则为 0,以正角度绘制则为 1。
  • 终点的 x 坐标和 y 坐标(起点由最后一个绘制的点或者最后一个 moveto 命令确定)。

贝塞尔曲线

二次贝塞尔曲线

使用 Q 或 q 命令指定一个二次曲线,后面紧跟着两组指定控制点和终点的坐标。

svg 还提供了流畅的二次曲线命令,用字母 T 表示(想要使用相对坐标,就用 t)。这个命令后面紧跟的是曲线的下一个端点;如规范所说,控制点会自动计算,方法是『使新的控制点与上一条命令中的控制点相对于当前点中心对称』。

三次贝塞尔曲线

使用 C 或者 c 命令指定一个三次曲线,这个命令后面紧跟三组坐标,用来指定起点的控制点,终点的控制点以及端点。

如果想要保证曲线之间的连接平滑,可以使用 S 命令(或者如果想要使用相对坐标,就用 s)。

Java Native Interface(JNI)规范内容

原文:https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html

介绍

JNI 允许在 Java 虚拟机(VM)中运行的 Java 代码与其他编程语言(如C、C++ 和汇编)编写的应用程序和库进行互操作。

JNI 最重要的好处是它对底层 Java VM 的实现没有任何限制。因此,Java VM 供应商可以添加对 JNI 的支持,而不影响 VM 的其他部分。开发者可以编写一个本地应用程序或库,并能够期望它能在所有支持 JNI 的Java VM 上工作。

Java Native Interface 概览

你可以使用 Java 来编写整个应用,但有些场景场景只使用 Java 不能满足你应用的需要。开发者可以使用 JNI 编写Java 本机方法来处理那些只使用 Java 不能实现的场景。

以下场景您需要使用 Java 本机方法:

  • 标准 Java 类库不支持应用程序所需的平台相关特性。
  • 您有一个使用其他语言实现的库,希望 Java 代码可以通过 JNI 访问它。
  • 您希望在汇编等较低级语言中实现对时间要求严格的一小部分代码。

通过 JNI 编程,您可以使用本机方法来:

  • 创建、检查和更新 Java 对象(包括数组和字符串)。
  • 调用 Java 方法。
  • 捕获或抛出异常。
  • 加载类或获取类信息。
  • 使用运行时类型检查。

设计概览

本节主要讨论 JNI 中的主要设计问题。本节中的大多数设计问题都与本机方法有关。

JNI 接口函数和指针

本机代码通过调用 JNI 函数来访问 Java VM 特性。JNI 函数通过接口指针实现。接口指针是指向指针的指针。这个指针指向一个指针数组,它的每个指针指向一个接口函数。

image

Go 语言切片

Go 语言数组是一个固定长度的、容纳同构类型元素的连续序列,因此 Go 数组类型具有两个属性:元素类型和数组长度。

Go 数组是值语义的。

可以称切片是数组的『描述符』。下面是切片在 Go 运行时中的内部表示:

type slice struct {
    array unsafe.Pointer
    len int
    cap int
}
  • array: 指向下层数组某元素的指针,该元素也是切片的起始元素。
  • len:切片的长度,即切片中当前元素的个数。
  • cap:切片的最大容量,cap >= len。

创建切片实例:

s := make([]byte, 5)

可以通过语法 u[low:high] 创建对已存在数组进行操作的切片,这被称为数组的切片化(slicing):

u := [10]byte{11, 12, 13, 14, 15, 16, 17, 18, 19, 20}
s := u[3:7]

还可以通过语法 s[low:high] 基于已有切片创建新的切片,这被称为切片的 reslicing。

esbuild 是如何进行 npm package 分发的

将所有 native package 作为 optionalDependencies,然后通过 package.json 中的 os 与 cpu 字段,让 npm/yarn/pnpm 在安装时自动选择符合的 native package。

esbuild package 中:

{
  "name": "esbuild",
  "bin": {
    "esbuild": "bin/esbuild"
  },
  "optionalDependencies": {
    "@esbuild/linux-loong64": "0.15.5",
    "esbuild-android-64": "0.15.5",
    "esbuild-android-arm64": "0.15.5",
    "esbuild-darwin-64": "0.15.5",
    "esbuild-darwin-arm64": "0.15.5",
    "esbuild-freebsd-64": "0.15.5",
    "esbuild-freebsd-arm64": "0.15.5",
    "esbuild-linux-32": "0.15.5",
    "esbuild-linux-64": "0.15.5",
    "esbuild-linux-arm": "0.15.5",
    "esbuild-linux-arm64": "0.15.5",
    "esbuild-linux-mips64le": "0.15.5",
    "esbuild-linux-ppc64le": "0.15.5",
    "esbuild-linux-riscv64": "0.15.5",
    "esbuild-linux-s390x": "0.15.5",
    "esbuild-netbsd-64": "0.15.5",
    "esbuild-openbsd-64": "0.15.5",
    "esbuild-sunos-64": "0.15.5",
    "esbuild-windows-32": "0.15.5",
    "esbuild-windows-64": "0.15.5",
    "esbuild-windows-arm64": "0.15.5"
  },
  "license": "MIT"
}

esbuild-darwin-arm64 package 中:

{
  "name": "esbuild-darwin-arm64",
  "os": [
    "darwin"
  ],
  "cpu": [
    "arm64"
  ]
}

optionalDependencies 介绍

如果依赖可被使用,但你希望当它无法被查找到或安装失败时由 npm 来处理,你需要将它放在 optionalDependencies 配置项中。这是一个 package 名字到版本或 url 的映射,与 dependencies 配置项一样。不同之处在于,构建失败不会导致安装失败。运行 npm install --no-optional 将阻止安装这些依赖项。

你的程序有责任来处理依赖的缺失。例如,类似这样的处理:

try {
  var foo = require('foo')
  var fooVersion = require('foo/package.json').version
} catch (er) {
  foo = null
}
if ( notGoodFooVersion(fooVersion) ) {
  foo = null
}

// .. then later in your program ..

if (foo) {
  foo.doFooThings()
}

optionalDependencies 中的条目将覆盖 dependencies 中同名的条目,因此通常最好只放在一个位置。

竞态条件(Race condition)与数据竞争(Data Race)的区别

原文:https://www.avanderlee.com/swift/race-condition-vs-data-race

竞态条件和数据竞争是相似的,但有一些区别。当开发多线程应用时,会经常使用这两个术语。它们也是许多异常的源头,包括著名的 EXC_BAD_ACCESS。通过理解两者的不同,你将学会如何在你的应用中解决并避免它们。

竞态条件与数据竞争

在深入研究这两种情况的代码示例之前,让我们简要地对比一下竞态条件和数据竞争:

  • 竞态条件

当事件的时序影响一段代码的正确性时,就会发生竞态条件。

  • 数据竞争

当一个线程访问一个可变对象的同时,另一个线程正在写入它,就会发生数据竞争。

使用代码示例解释竞态条件和数据竞争

我们可以使用银行转账的经典代码示例来进行说明。如果事件并非同步,那么由于事件顺序的不同,我们余额可能也会不同。

想象拥有两个账户:

let bankAccountOne = BankAccount(balance: 100)
let bankAccountTwo = BankAccount(balance: 100)

两个银行账号目前都拥有 10 欧元。银行提供一种转账方法,核实拥有足够的余额,并进行转账:

final class Bank {
    @discardableResult
    func transfer(amount: Int, from fromAccount: BankAccount, to toAccount: BankAccount) -> Bool {
        guard fromAccount.balance >= amount else {
            return false
        }
        toAccount.balance += amount
        fromAccount.balance -= amount

        return true
    }
}

在单线程应用中执行转账方法

对于单线程应用程序,我们可以精确地预测以下两次转账的结果:

bank.transfer(amount: 50, from: bankAccountOne, to: bankAccountTwo)
bank.transfer(amount: 70, from: bankAccountOne, to: bankAccountTwo)

在第一次转账 50 欧元后,第一个银行账号仅剩下 50 欧元,不足以进行第二次转账。所以,结果是:

print(bankAccountOne.balance) // 50 欧元
print(bankAccountTwo.balance) // 150 欧元

通过在多线程应用中进行转账来演示竞态条件

在多线程应用程序的情况下,我们将首先遇到一个竞态条件。当多个线程触发不同的转账时,我们无法预测哪个线程先执行。转账的顺序是不可预测的,并且执行顺序可能导致两个不同的结果。

bank.transfer(amount: 50, from: bankAcountOne, to: bankAcountTwo) // 在线程1上执行
bank.transfer(amount: 70, from: bankAcountOne, to: bankAcountTwo) // 在线程2上执行

当先执行的是 50 欧元转账时,我们会得到与第一个示例相同的结果:

print(bankAccountOne.balance) // 50 欧元
print(bankAccountTwo.balance) // 150 欧元

然而,当先执行的是 70 欧元转账时,得到的结果是:

print(bankAccountOne.balance) // 30 欧元
print(bankAccountTwo.balance) // 170 欧元

上述示例演示了竞态条件的影响。

这个实例很容易理解,并且有一定的意义。同样的情况也可能发生在现实生活中,支付顺序决定了哪些支付仍然可以完成。但是,如果我们深入转账方法,就会发现可能会发生的数据竞争问题。

在转账时数据竞争的影响

转账方法没有进行任何同步方案,来保证同一时间只有一个线程能够操作余额数据。

final class Bank {
    @discardableResult
    func transfer(amount: Int, from fromAccount: BankAccount, to toAccount: BankAccount) -> Bool {
        guard fromAccount.balance >= amount else {
            return false
        }
        toAccount.balance += amount
        fromAccount.balance -= amount

        return true
    }
}

在某些时序下,我们能够得到更加奇怪的结果。例如,当线程1正要更新余额时,线程 2 读取余额:

let bankAcountOne = BankAccount(balance: 100)
let bankAcountTwo = BankAccount(balance: 100)
bank.transfer(amount: 50, from: bankAcountOne, to: bankAcountTwo) // 在线程 1 上执行
bank.transfer(amount: 70, from: bankAcountOne, to: bankAcountTwo) // 在线程 2 上执行

----

// 线程 1 检查余额:
guard fromAccount.balance >= amount else {
    return false
}

// 线程 2,在同一时间也检查余额:
guard fromAccount.balance >= amount else {
    return false
}

// 线程 1 更新余额:
toAccount.balance += amount // 150
fromAccount.balance -= amount // 50

// 线程 2 更新余额:
toAccount.balance += amount // 170
fromAccount.balance -= amount // 30

// 结果:
print(bankAccountOne.balance) // 30 欧元
print(bankAccountTwo.balance) // 170 欧元

理论上,我们甚至可以得到以下结果:

print(bankAccountOne.balance) // 200 欧元
print(bankAccountTwo.balance) // -30 欧元

上面的示例详细演示了在没有进行任何同步方案时数据竞争的影响。在演示如何使用锁机制解决这个问题之前,我想解释一下为何发生数据竞争。

两个线程正在读取和写入相同的余额,这意味着我们符合数据竞争的预先定义:

当一个线程访问一个可变对象的同时,另一个线程正在写入它,就会发生数据竞争。

当对一块可被修改的内存进行读操作时,可能导致意想不到的行为和潜在的危机。

我们可以通过为转账方法增加一个锁定机制来解决竞争条件和数据竞争:

private let lockQueue = DispatchQueue(label: "bank.lock.queue")

@discardableResult
func transfer(amount: Int, from fromAccount: BankAccount, to toAccount: BankAccount) -> Bool {
    lockQueue.sync {
        guard fromAccount.balance >= amount else {
            return false
        }
        toAccount.balance += amount
        fromAccount.balance -= amount
        
        return true
    }
}

该示例使用一个默认为串行队列的派发队列,确保一次只有一个线程可以访问余额。锁定机制消除了数据竞争,因为不能再有多个线程访问相同的余额。尽管如此,竞态条件仍然可能发生,因为执行顺序仍然没有定义。但是,竞争条件是可以接受的,只要您确保数据竞争不会发生,它就不会破坏你的应用程序。

理解 key 属性

原文:https://stackoverflow.com/a/43892905

React 使用 key 属性来关联组件和 DOM 元素的关系,并在调度过程(reconciliation process)中使用。因此,保持 key 的唯一性是非常重要的,否则很有可能 React 会无法正确地区分元素并改变不正确的元素。同样重要的是,为了保持最佳性能,这些 key 在重新渲染过程中保持不变。

也就是说,如果已知数组是不会改变的,就不必使用上述方法。但是,只要可能,就应该应用最佳实践。

一位 React 开发者在 GitHub issue 中说到:

key 的主要目的不是性能,而是用作唯一标识(反过来通常又可以带来更好的性能)。 随机分配且变化的值不能作为唯一标识。
在不清楚数据是如何构建的情况下,我们无法提供 key。如果没有 id,建议使用哈希函数。
在使用数组时,已经有了内部 key,但它们是数组中的下标。当您插入一个新元素时,这些 key 是错误的。

简而言之,key 应该是:

  • 唯一的——同级组件中不能存在相同的 key。
  • 静态的——在渲染之间 key 值不变。

Paint Timing 1

原文:https://www.w3.org/TR/paint-timing/#performancepainttiming

概要

本文档定义了一个 API,用于在页面加载过程中捕获开发人员所关心的一系列关键时刻(First Paint、First Contentful Paint)。

介绍

加载不是一个单一的时间点——这是一种没有任何一个指标能够完全量化的用户体验。在加载过程中有多个时刻会影响用户是“快”还是“慢”。

First Paint(FP)是第一个关键时刻,然后是 First Contentful Paint(FCP)。这些指标标记了导航结束后,浏览器在屏幕上渲染像素时的那一时刻。这对用户来说很重要,因为它回答了这样一个问题:它正在发生吗?

这两个指标之间的主要区别是,FP 标记浏览器渲染出任何与导航前屏幕上不同的东西的时刻。相比之下,FCP 是浏览器渲染第一个 DOM 内容的时刻,它可能是文本、图像、SVG,甚至是 canvas 元素。

使用例子

var observer = new PerformanceObserver(function(list) {
    var perfEntries = list.getEntries();
    for (var i = 0; i < perfEntries.length; i++) {
        // Process entries
        // report back for analytics and monitoring
        // ...
    }
});

// register observer for long task notifications
observer.observe({entryTypes: ["paint"]});

术语

Paint:当浏览器将渲染树转换为屏幕上的像素时,它完成了“paint”(或“render”)。正式定义为在事件循环处理中发生“更新渲染”的时刻。

注意:渲染管线非常复杂,时间戳应是浏览器在此管线中能够记录的最新时间戳(尽最大努力)。通常建议此 API 使用将帧提交给操作系统进行显示的时间。

为什么要使用新的 JSX 转换

相关的 RFC:https://github.com/reactjs/rfcs/blob/createlement-rfc/text/0000-create-element-changes.md#detailed-design

动机

React.createElement(...) 有以下几个问题

  • 在每个元素构造期间,如果一个组件有 .defaultprops,我们需要对它进行动态检测。这不能被很好的优化,因为被调用的函数是高度 megamorphic 的(应该是 V8 中的术语,待查找)。
  • React.lazy 中,构造元素时 .defaultProps 不起作用。因此在这种情况下,我们必须在渲染阶段检查 defaultProps 是否被解析,这意味着无论如何都无法保持语义的一致。
  • Children 以可变参数进行传递,我们必须动态地将它们添加到 props 上,而不是在静态地在调用处获得 props 的结构。
  • 转换后使用 React.createElement,它是动态的属性查找,而不是一个模块作用域内的常量。这有些不足,有些运行成本。
  • 我们无法得知传递的 props 对象是否是用户所创建的可变对象,所以我们必须总是将它克隆一次。
  • keyref 从 JSX 提供的 props 中获取,所以即使我们不克隆,我们也必须将其移除,这将导致该对象变成 map-like。
  • keyref 可以通过扩展运算符动态传递,因此除非进行昂贵的分析,否则我们无法得知此类模式是否会包含它们 <div {...props} />
  • 转换需要 React 在 JSX 的作用域内。也就是说,你必须进行默认导入。这很不幸,因为像 hooks 这样的东西通常以命名参数的形式导入。理想情况下,您应该不需要导入任何东西就可以使用 JSX。

除了性能,这也是为了简化学习使用 React 时不得不掌握的大量概念。特别是,forwardRefdefaultProps 将不再是什么特别的东西。

设计细节

JSX 转换变更

Auto-import

理想情况下,您不需要编写任何导入来使用 JSX:

function Foo() {
  return <div />;
}

然后,它将被编译来包含这个依赖项。

import {jsx} from "react";
function Foo() {
  return jsx('div', ...);
}

将 key 从 props 中分开传递

jsx('div', props, key)

总是将 children 作为属性传递

createElement 中,children 以可变参数进行传递。在新的转换中,我们总是将它们添加到 props 对象中。

将它们以可变参数进行传递的原因是为了在 DEV 中区分静态子变量和动态子变量。我们可以传递一个布尔值或者使用两个不同的函数来区分它们。我的建议是将 <div>{a}{b}</div> 编译为 jsxs('div', {children: [a, b]}) ,将 <div>{a}</div> 编译为 jsx('div', {children:a})jsxs 函数指明顶部数组由 React 创建。此策略的优点是,即使你没有针对 PROD 和 DEV 进行单独构建,我们仍然可以发出关键警告,并且在 PROD 下也不会产生任何开销。

废除『模块模式(module pattern)』组件

const Foo = (props) => {
  return {
    onClick() {
      //...
    }
    render() {
      return <div onClick={this.onClick.bind(this)} />;
    }
  }
};

它的存在会导致一些实现的复杂性。

这是很直接的升级。这是一种非常不常用的模式,大多数人都不知道。关键是你的类构造函数需要具有 Component.prototype.isReactComponent 属性并可以被 new 调用(即不是箭头函数)。即使你碰巧使用了模块模式,在原型中添加 isReactComponent 属性,并使用函数表达式而不是箭头函数。

function Foo(props) {
  return {
    onClick() {
      //...
    }
    render() {
      return <div onClick={this.onClick.bind(this)} />;
    }
  }
};
Foo.prototype = {isReactComponent: true};

这里的重要目的是,如果我们要在类和函数组件之间引入不同的语义,我们需要在调用它们之前就要知道我们要使用哪个语义。

废除函数组件的 defaultProps

defaultProps 在类中非常有用,因为 props 对象被传递给许多不同的方法,如生命周期、回调等。每一个都有自己的作用域。这导致使用 JS 默认参数变得困难,因为您必须在每个函数中反复确定相同的默认值。

class Foo {
  static defaultProps = {foo: 1};
  componentDidMount() {
    let foo = this.props.foo;
    console.log(foo);
  }
  componentDidUpdate() {
    let foo = this.props.foo;
    console.log(foo);
  }
  componentWillUnmount() {
    let foo = this.props.foo;
    console.log(foo);
  }
  handleClick = () => {
    let foo = this.props.foo;
    console.log(foo);
  }
  render() {
    let foo = this.props.foo;
    console.log(foo);
    return <div onClick={this.handleClick} />;
  }
}

但是,在函数组件中,实际上不需要这种模式,因为您可以只使用 JS 默认参数,并且通常这些值的所有使用的位置都在同一作用域内。

function Foo({foo = 1}) {
  useEffect(() => {
    console.log(foo);
    return () => {
      console.log(foo);
    };
  });
  let handleClick = () => {
    console.log(foo);
  };
  console.log(foo);
  return <div onClick={handleClick} />;
}

当在没有 .prototype.isReactComponent 的组件上使用 defaultProps 时,createElement 将发出警告。这包括那些特殊的组件,如 forwardRefmemo

如果 props 整体进行传递,那么升级将变得困难,不过你总是可以在需要时对它进行重构。

function Foo({foo = 1, bar = "hello"}) {
  let props = {foo, bar};
  //...
}

React 编码规范

前言

规则来源于

  1. eslint-plugin-react 提供的规则,选择出有利于我们当前项目的部分;
  2. React 社区中的讨论。

愿望

  1. 每一条规则都应有详尽的解释、示例代码,能直观看到规则的作用、得知使用该规则的原因;
  2. 每一条规则都可以使用 eslint 进行自动化检查;
  3. 已有项目中完全没有违反的规则无需列出。

JSX

[强制] 没有子节点的组件使用自闭合语法(react/self-closing-comp)

解释:

JSX与HTML不同,所有元素均可以自闭合。

// Bad
<Foo></Foo>
<div></div>

// Good
<Foo />
<div />

[强制] 保持起始和结束标签在同一层缩进(react/jsx-wrap-multilines)

解释:

代码样式。

// Bad
class Message {
    render() {
        return <div>
            <span>Hello World</span>
        </div>;
    }
}

// Good
class Message {
    render() {
        return (
            <div>
                <span>Hello World</span>
            </div>;
        );
    }
}

[强制] 自闭合标签的/>前添加一个空格(react/jsx-space-before-closing)

解释:

代码样式。

// Bad
<Foo bar="bar"/>
<Foo bar="bar"  />

// Good
<Foo bar="bar" />

API

[强制] 禁止为继承自 PureComponent 的组件编写 shouldComponentUpdate 实现(react/no-redundant-should-component-update)

解释:

在 React 的实现中,PureComponent 并不直接实现 shouldComponentUpdate,而是添加一个 isReactPureComponent 的标记,由 CompositeComponent 通过识别这个标记实现相关的逻辑。因此在 PureComponent 上自定义 shouldComponentUpdate 并无法享受 super.shouldComponentUpdate 的逻辑复用,也会使得这个继承关系失去意义。

相关的 issue:facebook/react#9239

补充:

  1. 为了避免组件无效的 render 导致的性能开销使用 PureComponet 和 memo 是好的,但请记住它们也是存在开销的,请勿滥用。

相关的 issue:facebook/react#14463

  1. 使用 react-redux 中 connect 方法包裹的组件没有继承 PureComponent 或使用 memo 方法的必要。

相关的代码:https://github.com/reduxjs/react-redux/blob/754c1059ded0c1ea3a9b6dc1d870e31c22d8c3b7/src/components/connectAdvanced.js#L443

  1. 可使用 React Developer Tools 中开启 Highlight updates when components render 设置项观察组件渲染情况。

[强制] 禁止使用 String 类型的 Refs(react/no-string-refs)

解释:

它已过时并可能会在 React 未来的版本被移除。

补充:

相关 issue:facebook/react#8333 (comment)

String 类型的 Refs 存在的问题:

  1. 它要求 React 跟踪当前渲染的组件,这会导致 React 的执行稍微慢一些。
  2. 它不能像大多数人期望的那样使用“渲染回调(render callback)”模式。
class MyComponent extends Component {
  renderRow = (index) => {
    // 这将无法工作,ref 将被添加到 DataTable 上,而非 MyComponent 上:
    return <input ref={'input-' + index} />;

    // This would work though! Callback refs are awesome.
    return <input ref={input => this['input-' + index] = input} />;
  }
 
  render() {
    return <DataTable data={this.props.data} renderRow={this.renderRow} />
  }
}
  1. 它是不可组合的,也就是说,如果库在传递的子对象上放一个 ref,用户就不能再放一个 ref。回调引用是可组合的。

[强制] 避免使用不安全的生命周期函数 componentWillMount、componentWillReceiveProps、componentWillUpdate(react/no-unsafe)

解释:

以上生命周期函数被 React 官方视为是不安全的,公司 react 规范中推荐使用 constructor 代替 componentWillMount,具体生命周期迁移参考『迁移过时的生命周期』。

备注:

React 官方视为以上生命周期不安全的原因是:对于 Concurrent 模式(实验性)这个未来特性,避免在 willMount / willUpdate 等生命周期挂钩中产生副作用非常重要。因为 React 当前的渲染分为 reconcile 和 commit 两个阶段,reconcile 阶段可以被高权重用户事件中端导致重复执行,由于以上生命周期函数在此阶段中被调用,导致这些生命周期函数存在被重复调用的可能。

React RFC 0006-static-lifecycle-methods:https://github.com/reactjs/rfcs/blob/master/text/0006-static-lifecycle-methods.md

React v16.9.0 更新日志:https://react.docschina.org/blog/2019/08/08/react-v16.9.0.html

迁移过时的生命周期:https://react.docschina.org/blog/2018/03/27/update-on-async-rendering.html#migrating-from-legacy-lifecycles

Concurrent 模式介绍 (实验性):https://react.docschina.org/docs/concurrent-mode-intro.html

(:з[__] Dan 宝在 stackoverflow 上的回答 - 在 React 中,我应该在 componentWillMount 还是 componentDidMount 中进行初始网络请求?https://stackoverflow.com/a/41612993

[强制] 禁止使用数组索引作为 key(react/no-array-index-key)

解释:

React 使用 key 来判断哪些元素已经改变、添加或删除。

补充:

在此多说一些,我想一定有人和我之前的一样对 key 存在这样的误解,key 的目的是为了更好的性能。不,这不是事实。

key 的主要目的是作为唯一标识,在 fiber diff 阶段能够正确地判断出元素改变、添加或删除状态,尤其是对自身拥有状态的组件,这非常重要。

而在此前提下,对于属性没有变更的组件,只有实现了 shouldComponentUpdate / memo 方法,才可以避免重复渲染。

相关 issue:facebook/react#1342 (comment)

Understanding the key prop: https://stackoverflow.com/questions/28329382/understanding-unique-keys-for-array-children-in-react-js/43892905#43892905

// Bad
function Posts() {
	const [list, setList] = useState([]);
	useEffect(() => {
		fetchPosts().then(setList);
	}, []);
	return list.map((post, index) => <input key={index} defaultValue={post.title} />);
}

// Good
function Posts() {
	const [list, setList] = useState([]);
	useEffect(() => {
		fetchPosts().then(setList);
	}, []);
	return list.map(post => <input key={post.id} defaultValue={post.title} />);
}


// Good - 如果没有 id 等唯一标识,可以由前端主动生成唯一标识
function Posts() {
	const [list, setList] = useState([]);
	useEffect(() => {
		fetchPosts().then(list => {
			for (const post of list) {
				post.id = SomeLibrary.generateUniqueID();
			}
			setList(list);
		});
	}, []);
	return list.map(post => <input key={post.id} defaultValue={post.title} />);
}

[建议] 避免在JSX的属性值中直接使用对象和函数表达式(react/jsx-no-bind)

解释:

PureComponent 使用 shallowEqual 对 props 和 state 进行比较来决定是否需要渲染,而在 JSX 的属性值中使用对象、函数表达式会造成每一次的对象引用不同,从而 shallowEqual 会返回 false,导致不必要的渲染。

补充:

函数组件中为避免子组件刷新,可使用 useCallback 和 useMemo 等 hook 来保持函数、对象类型的属性引用不变。

// Bad
class WarnButton {
    alertMessage(message) {
        alert(message);
    }
    render() {
        return <button type="button" onClick={() => this.alertMessage(this.props.message)}>提示</button>
    }
}

// Good
class WarnButton {
    @bind()
    alertMessage() {
        alert(this.props.message);
    }
    render() {
        return <button type="button" onClick={this.alertMessage}>提示</button>
    }
}

// Bad
function Foo() {
	async function handleOk() {
		try {
        	await fetch();
		} catch (error) {
			message.error('error');
			return;
		}
		message.success('success');
    }
	return <Modal onOk={handleOk} />;
}

// Good 
function Foo() {
	const handleOk = useCallback(async () => {
		try {
        	await fetch();
		} catch (error) {
			message.error('error');
			return;
		}
		message.success('success');
    }, []);

	return <Modal onOk={handleOk} />;
}

[建议] 禁止在函数组件上使用 defaultProps(react/require-default-props,ignoreFunctionalComponents: true)

解释:

defaultProps 在类中非常有用,因为 props 对象被传递给许多不同的方法,如生命周期、回调等。每一个都有自己的作用域。这导致使用 JS 默认参数变得困难,因为您必须在每个函数中反复确定相同的默认值。

class Foo {
  static defaultProps = {foo: 1};
  componentDidMount() {
    let foo = this.props.foo;
    console.log(foo);
  }
  componentDidUpdate() {
    let foo = this.props.foo;
    console.log(foo);
  }
  componentWillUnmount() {
    let foo = this.props.foo;
    console.log(foo);
  }
  handleClick = () => {
    let foo = this.props.foo;
    console.log(foo);
  }
  render() {
    let foo = this.props.foo;
    console.log(foo);
    return <div onClick={this.handleClick} />;
  }
}

但是,在函数组件中,实际上不需要这种模式,因为您可以只使用 JS 默认参数,并且通常这些值的所有使用的位置都在同一作用域内。

function Foo({foo = 1}) {
  useEffect(() => {
    console.log(foo);
    return () => {
      console.log(foo);
    };
  });
  let handleClick = () => {
    console.log(foo);
  };
  console.log(foo);
  return <div onClick={handleClick} />;
}

React 团队之后的计划为,当在没有 .prototype.isReactComponent 的组件上使用 defaultProps 时,createElement 将发出警告。这包括那些特殊的组件,如 forwardRefmemo

如果 props 整体进行传递,那么升级将变得困难,不过你总是可以在需要时对它进行重构。

function Foo({foo = 1, bar = "hello"}) {
let props = {foo, bar};
//...
}
补充:

React RFC 0000-create-element-changes:http://0000-create-element-changes.md/ https://github.com/reactjs/rfcs/blob/createlement-rfc/text/0000-create-element-changes.md#detailed-design

[注意] 单页应用中应避免使用 location.href = 'url to jump' 进行应用内部跳转

解释:

location.href = 'url to jump' 会导致页面刷新导致重新请求静态资源。

// Bad
function Foo() {
	const handleClick = useCallback(() => {
		location.href = '/path/to/jump';
	});
	return <div onClick={handleClick}>点击</div>;
}


// Good
function Foo() {
    const history = useHistory();
	const handleClick = useCallback(() => {
        // 在 react-router 中使用其 history 对象进行跳转
		history.push('/path/to/jump');
	});
	return <div onClick={handleClick}>点击</div>;
}

Shell 脚本 if 语句

if 语句的语法

在 shell 脚本中,常见的 if 语句结构如下:

if
    command-list1
then
    command-list2
else
    command-list3
fi

如果 command-list1 中的语句的 exit code 为 0,then 从句执行。如果 exit code 不是 0,else 从句执行。command-list1 可以简单也可以复杂。它可以是一个或多个被 ;&&&|| 或换行分割的命令。

  1. if [ condition ]

[ 是传统 test 命令的别名,[test 是标准 POSIX 工具。所有 POSIX shell 已经将其内置。典型的 test 用法有判断文件是否存在或一个数是否与另一个相等。

  1. if [[ condition ]]

这是 ksh 中对 test 命令升级版,bash、zsh、yash、busybox sh 也支持。它更多的特性,例如它可以使用通配符来判断字符串是否匹配。

  1. if ((condition))

另一个 ksh 的扩展,bash 和 zsh 也支持。用于执行数学表达式。和 [[ ... ]] 一样,这个形式不在 POSIX 标准中。

  1. if (command)

在子 shell 中运行命令。当命令完成,它返回 exit code。

使用这样的子 shell 的一个常见原因是,有的命令会对 shell 环境分配变量或其他更改,以此限制命令的副作用。在子 shell 完成后,这些副作用不会被保留。

  1. if command

执行普通的命令。

常见的 test 命令

  • [ -a FILE ] 文件存在时返回 true
  • [ -z STRING ] "STRING" 的长度为 0 时返回 true
  • [ -n STRING ] or [ STRING ] "STRING" 的长度不为 0 时返回 true
  • [ STRING1 == STRING2 ] 如果字符串相等返回 true
  • [ STRING1 != STRING2 ] 如果字符串不相等返回 true
  • [ ARG1 OP ARG2 ] "OP" 可选为 -eq、-ne、-lt、-le、-gt 或 -ge。如果 "ARG1" 分别等于、不等于、小于、小于或等于、大于或大于或等于 "ARG2",则这些算术运算符返回 true。"ARG1" 和 "ARG2" 是整数。

参考

使用 WebGL 绘制线条

原文:https://blog.scottlogic.com/2019/11/18/drawing-lines-with-webgl.html

我最近有机会帮助 D3FC 在 WebGL 中实现一些功能。 D3FC 是一个 D3 库的扩展,提供常用组件,使构建交互式图表更容易。

在上一篇文章中,我讨论了在 WebGL 中绘制点所采用的方法。我们在顶点着色器中创建了一些正方形,然后在片元着色器中丢弃不需要的像素。这些形状具有相似的高度和宽度,因此效果很好 —— 片元着色器需要丢弃的像素相对较少。

然而,这种技术不适合绘制线条。线条通常又细又长,这意味着片段着色器必须在大量不需要的像素上运行。

此外,我们需要将点进行连通,这意味着 GL_POINT 不是最好的模式。

这篇文章探索一种不同的渲染方法,充分利用着色器,着眼于如何在 WebGL 中以最少的跨缓冲区传输数据来渲染线条。

利用正方形进行绘制

WebGL 中的一切都是使用三角形构建的。我们可以将两个三角形组合在一起以创建一个矩形。

我们的第一个想法可能是通过在它们之间绘制一个矩形来将每个点连接到下一个点。然而,当我们尝试这样做时,我们发现这种技术存在问题。

image

这两个矩形在顶部留下一个倾角。所以我们需要想一个更好的方法来连接各点。

利用梯形进行绘制

我们可以画梯形而不是矩形。这些将使我们能够将两个组合在一起而没有任何重叠或间隙。这种类型的接头称为斜接。

image

我们如何在定点着色器中实现。

我们可以利用 GL_TRIANGLE_STRIP。三角形带是一系列共享顶点的三角形。我们只需要定义每个顶点一次,WebGL 会将三角形连接成一条带。

对于每个点,我们需要两个顶点——一个用于线的外部连接,一个用于内部连接。

为简单起见,我们假设我们正在绘制一条包含三个点的线:A、B 和 C。B 是顶点着色器正在考虑的当前点,A 是前一个点,C 是下一个点。我们可以通过缓冲区传递这些值。

image

gl_Position.xy = gl_Position.xy + (directionToMove * distanceToMove);

方向

为了帮助我们计算斜接的方向,我们可以使用 Tom McLaughlan 开发的斜接工具。如您所见,斜接线是线 AB 和 BC 的切线的法向量。所以如果我们找到切线,我们可以用它来计算方向。

image

我们可以通过添加法线向量 AB 和 BC 来计算切线。

vec2 AB = normalize(gl_Position.xy - prev.xy);
vec2 BC = normalize(next.xy - gl_Position.xy);

gl_Position 为我们提供了当前顶点在剪辑空间中的位置。剪辑空间在两个方向上的范围从 -1 到 +1,正方形也是如此。然而,我们正在绘制的画布更可能是一个矩形。这通常不是问题,因为画布大小映射到剪辑空间。但是,这种映射的一个副作用是剪辑空间中向量的角度与我们画布上的角度不同。

ECMA404 标准 - JSON 语法

原文:https://www.ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf

范围

JSON 是一种轻量的、基于文本的、具有语言无关性的语法,用于定义数据交换格式。它起源于 ECMAScript 编程语言,但其具有编程语言无关性。JSON 定义了一个小的结构化规则集对结构化数据进行可移植表示。

定义合法 JSON 语法是本规范的唯一目,不定义具体的语义,也不定义存在于编程语言内部时的数据结构。JSON 语法可被用于表示多种语义,可被编程语言以多种方式进行处理。要使 JSON 进行有意义的信息交换,需要相关各方在应用的语义上达成一致。定义 JSON 具体的语义说明可以是其他规范的主题。 同样,编程语言对 JSON 的映射也可以单独定义。 例如 ECMA-262 定义了合规的 JSON 文本和 ECMAScript 运行时数据结构之间的映射。

标准

合规的 JSON 文本是完全符合本规范定义的 JSON 语法的 Unicode 码点序列。JSON 处理器不应接受任何不符合 JSON 语法的输入。 符合条件的处理器可能会施加语义限制,以限制它将处理的 JSON 文本。

引用标准

以下文档对于本文档的应用是必不可少的。凡是注日期的引用文件,仅所引用的版本适用。凡是不注日期的引用文件,其最新版本(包括所有的修改)适用于本规范。

ISO/IEC 10646, Information Technology – Universal Coded Character Set (UCS)

The Unicode Consortium. The Unicode Standard http://www.unicode.org/versions/latest

Bray, T., Ed. "The JavaScript Object Notation (JSON) Data Interchange Format", RFC 8259

该规范和 [RFC 8259] 都提供了 JSON 的语法规范,但是使用了不同的形式。两个规范的目的是定义相同的语法。

JSON 文本

JSON 文本是符合 JSON 语法并为 Unicode 编码的单词(Token)序列,单词包括六种结构性单词、字符串、数字和三种字面名单词。

六种结构性单词:

单词 Unicode 编码 说明
[ U+005B 左方括号
{ U+007B 左花括号
] U+005D 右方括号
} U+007D 右花括号
: U+003A 冒号
, U+002C 逗号

三种字面名单词:

单词 Unicode 编码
true U+0074 U+0072 U+0075 U+0065
false U+0066 U+0061 U+006C U+0073 U+0065
null U+006E U+0075 U+006C U+006C

任何单词前后均可添加额外的空白。空白可以由以下一个或多个码元构成的序列:制表符(U+0009)、换行符(U+000A)、回车 (U+000D)和空格(U+0020)。空白不允许存在任何单词中,除了字符串中允许有空格。

JSON 值

JSON 值可以是对象、数组、数字、字符串、true、false 或 null。

image

对象

一个对象由一对包裹着零个或多个键值对的花括号构成。键是字符串。每个键后都有一个冒号,将其与值分开。单个逗号将值与后面的键分开。JSON 语法不对用作键的字符串施加任何限制,不要求键的字符串是唯一的,并且不对键值对的排序赋予任何意义。

image

数组

一个数组是由一对包裹着零个或多个值的方括号构成。这些值用逗号分隔。JSON 语法没有为值的顺序定义任何特定的含义。但是,JSON 数组通常用于一些排序具有语义的情况。

image

数字

一个数字是一个没有前导零的十进制数字序列。它首位可能减号(U+002D)。它可能有小数部分,以小数点(U+002E)为前缀。 它可能有一个指数,以 e(U+0065)或 E(U+0045)为前缀,以及可选的 +(U+002B)或 –(U+002D)。这些数字的编码范围为 U+0030 至 U+0039。

image

不允许使用不能表示为数字序列的值(例如 Infinity 和 NaN)。

字符串

字符串是由一对引号(U+0022)包裹的 Unicode 码点序列。除必须转义的码点外,所有码点都可以放在引号内:引号(U+0022),反斜线(U+005C)和控制字符 U+0000 至 U+001F。有些字符使用由两个字符构成的转义序列表示。

\" 表示引号(U+0022)。
\\ 表示反斜线(U+005C)。
\/ 表示正斜线(U+002F)。
\b 表示退格(U+0008)。
\f 表示换页(U+000C)。
\n 表示换行(U+000A)。
\r 表示回车(U+000D)。
\t 表示制表符(U+0009)。

例如,仅包含单个反斜线的字符串可以表示为"\\"

任何字符都可以表示为十六进制转义序列。十六进制数字的含义由 ISO/IEC 10646 确定。如果字符编码在基本多语言平面( Basic Multilingual Plane)中(U+0000 至 U+FFFF),则可以表示为6个字符的序列:反斜线后跟小写字母 u,再跟4个十六进制数字。

以下四种情况产生相同的结果:

"\u002F"
"\u002f"
"/"
"/"

为了转义不在基本多语言平面中的码点,可以将该字符表示为12个字符的序列,对应该码点的 UTF-16 编码。例如,仅包含 G 谱号字符(U+1D11E)的字符串可以表示为 "\uD834\uDD1E"。注意 JSON 语法允许 Unicode 当前没有分配字符的码点。然而,JSON 文本处理器是将这样的转义字符序列转换为单个码点还是原样显示,由处理器根据语义自行决定。

image

对 npm 依赖的依赖进行更改

如果你想对 npm 依赖的依赖进行更改,例如,根据已知的安全问题来替换依赖的版本,用 fork 替换现有的依赖,或者确保在任何地方都使用相同版本的包,此时可以使用 npm 的 overrides 配置。

overrides 可以将依赖树中的包替换为其他的版本,或全完替换为其他的包。可以根据需要决定变更的范围。

确保软件包 foo 始终安装为 1.0.0 版本,无论依赖的 package.json 中选择的是哪个版本:

{
  "overrides": {
    "foo": "1.0.0"
  }
}

上面是配置的简写形式,可以使用完整的配置对象形式来覆盖包本身以及包的子依赖。下面的配置让 foo 始终为 1.0.0 版本,同时在 foo 下的任何深度的 bar1.0.0 版本:

{
  "overrides": {
    "foo": {
      ".": "1.0.0",
      "bar": "1.0.0"
    }
  }
}

仅覆盖 bar 下的 foo 包为 1.0.0 版本:

{
  "overrides": {
    "baz": {
      "bar": {
        "foo": "1.0.0"
      }
    }
  }
}

overrides 配置的 key 可以包含版本。仅覆盖 [email protected] 下的 foo 包,让其版本为 1.0.0

{
  "overrides": {
    "[email protected]": {
      "foo": "1.0.0"
    }
  }
}

你不能在 overrides 中配置一个与直接关联的 dependencies 中版本定义不同的包。为了更加容易的处理该限制,你可以 $ 前缀加上包名,来直接引用此包在 dependencies 中的版本定义:

{
  "dependencies": {
    "foo": "^1.0.0"
  },
  "overrides": {
    // 错误,将抛出 EOVERRIDE 错误
    // "foo": "^2.0.0"
    // 正确,版本定义匹配
    // "foo": "^1.0.0"
    // 最佳实践,使用 `$` 前缀引用 `dependencies` 中的版本定义
    "foo": "$foo"
  }
}

注意

overrides 依赖 npm 8.3 以上版本,若 npm 低于该版本可以使用 npm-force-resolutions

参考资料

https://docs.npmjs.com/cli/v8/configuring-npm/package-json#overrides

Source Map

原文:https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#

背景

原始 source map 格式(v1)由 Joseph Schorr 创建,用于 Closure 支持对优化后的 JavaScript 代码进行源代码级调试(尽管该格式本身与语言无关)。然而,随着使用 source map 的项目规模不断扩大,格式的冗长开始成为问题。v2 提供一些简单性和灵活性,以减小 source map 的总体大小。即使对 v2 版本的格式进行了更改,source map 文件的大小也限制了其用途。v3 格式基于@podivilov 的建议。

相关文档:
Revision 2 proposal

术语

术语 定义
转换后的代码 由编译器转换后的代码。
源代码 尚未通过编译器的源代码。
Base 64 VLQ VLQ 是一个 Base64 值,其中最高有效位(第6位)被用于表示是否『连续』,最低有效位被用作符号位。注:VLQ Base64 编码可以表示的值被限制为32位。
Source Mapping URL 在转换后的代码中引用 source map 的 URL。

v3 版本格式

目的

  • 减少总体大小以提高解析时间、内存消耗和下载时间。
  • 支持源代码级调试,允许双向映射。
  • 支持服务器端堆栈跟踪。

格式

{
    "version" : 3,
    "file": "out.js",
    "sourceRoot": "",
    "sources": ["foo.js", "bar.js"],
    "sourcesContent": [null, null],
    "names": ["src", "maps", "are", "fun"],
    "mappings": "A,AAAB;;ABCDE;"
}

第1行:文件整体是一个 JSON 对象。
第2行:文件版本始终是对象中的第一项,并且必须是正整数。
第3行:可选项,与该 source map 关联的转换后的代码文件名。
第4行:可选项,源文件根目录,用于在服务器上重新定位源文件或删除『sources』字段中的重复值。此值将被添加到『sources』字段中的各项前。
第5行:『mappings』使用的源文件的列表。
第6行:可选项,源代码列表,在宿主环境中无法检索源代码时非常有用。内容按与第5行中『sources』项相同的顺序列出。如果应当按名称检索某些原始资源,则可使用『null』。
第7行:『mappings』项所使用的符号名列表。
第8行:编码后的映射数据的字符串。

『mappings』数据结构如下:

  • 组(group)表示转换后的代码文件中的行,由『;』分隔。
  • 段(segment)由『,』分隔。
  • 段由1个、4个或5个可变长度字段构成。

每个段中的字段为:

  1. 表示在转换后的代码的第几列,列的索引从零开始。
  2. 如果存在,表示对应『sources』列表的第几项,项的索引从零开始。
  3. 如果存在,表示对应源代码中第几行,行的索引从零开始。
  4. 如果存在,表示对应源代码中第几列,列的索引从零开始。
  5. 如果存在,表示对应『names』列表的第几项,项的索引从零开始。

注意:使用谷歌日历进行测试,此编码相对于 V2 格式 source map 的大小减少了 50%。

Go 语言中的 Import

1. Direct import

// Golang program to demonstrate the 
// application of direct import
package main
    
import "fmt"
    
// Main function
func main() {
    fmt.Println("Hello Geeks")
}

2. Grouped import

// A program to demonstrate the
// application of grouped import
package main
    
import (
    "fmt"
    "math"
)
    
// Main function
func main() {
  
    // math.Exp2(5) returns 
    // the value of 2^5, wiz 32
    c := math.Exp2(5)
      
    // Println is a function in fmt package 
    // which prints value of c in a new
    // line on console
    fmt.Println(c)
}

3. Nested import

// Golang Program to demonstrate
// application of nested import
package main
   
 import (
    "fmt"
    "math/rand"
)
  
func main() {
    // this generates & displays a
    // random integer value < 100
    fmt.Println(rand.Int(100))
}

4. Aliased import

// Golang Program to demonstrate
// the application of aliased import
package main
	
import (
	f "fmt"
	m "math"
)
	
// Main function
func main() {

	// this assigns value
	// of 2^5 = 32 to var c
	c := m.Exp2(5)	
	
	// this prints the
	// value stored in var c
	f.Println(c)				
}

5. Dot import

// Golang Program to demonstrate
// the application of dot import
package main

import (
	"fmt"
	. "math"
)

func main() {

	// this prints the value of
	// 2^5 = 32 on the console
	fmt.Println(Exp2(5))	
}

6. Blank import

// PROGRAM1
package main
// Program to demonstrate importance of blank import

import (
	"fmt"
	"math/rand"
)

func main() {
	fmt.Println("Hello Geeks")
}

// -------------------------------------------------------------------------------------
// Program1 looks accurate and everything
// seems right but the compiler will throw an
// error upon building this code. Why? Because
// we imported the math/rand package but
// We didn't use it anywhere in the program.
// That's why. The following code is a solution.
//-------------------------------------------------------------------------------------

// PROGRAM2
package main
// Program to demonstrate application of blank import

import (
	"fmt"
	_ "math/rand"
)

func main() {
	fmt.Println("Hello Geeks")
	// This program compiles successfully and
	// simply prints Hello Geeks on the console.
}

7. Relative import

package main
	
import "github.com/gopherguides/greet"
	
// Main function
func main() {
	// The hello function is in
	// the mentioned directory
	greet.Hello()
	// This function simply prints
	// hello world on the console screen
}

在使用 TypeScript 的 React 项目中使用 Web Component

原文:https://goulet.dev/posts/consuming-web-component-react-typescript/

在我构建了第一个 Web Component 后,想在使用 TypeScript 的 React 项目中使用它。当我在 React 中添加了它后,得到了一个错误:[ts] Property 'wc-menu-button' does not exist on type 'JSX.IntrinsicElements'. [2339]

image

导致这个问题的原因是,React 仅知道标准的 HTML 元素,所以当我添加自定义的元素后,React 无法识别它,并向我展示一个错误。

有两种方案解决这个问题。一种在 Web Component 项目中处理,另一种在 React 项目中处理。

方案一:在 Web Component 中定义类型

如果你是 Web Component 的作者,你可以在 JSX 的 IntrinsicElements 中定义你的组件,如下:

declare global {
  namespace JSX {
    interface IntrinsicElements {
      "my-element": any;
    }
  }
}

你可能会觉的将 my-element 的类型设为 any 并不理想,你是对的。是不是将组件的属性定义为一个接口,让后将组件设置为该类型会更好?像下面这样:

declare global {
  namespace JSX {
    interface IntrinsicElements {
      "my-element": MyElementAttributes;
    }

    interface MyElementAttributes {
      name: string;
    }
  }
}

这看起来很好。但当你向这个自定义组件设置 keyref 属性时,将看到这样的错误 [ts] Property 'ref' does not exist on type 'MyElementAttributes'. [2339]

image

MyElementAttributes 需要继承 React 的 HTMLAttributes 类,如下:

interface MyElementAttributes extends HTMLAttributes {
  name: string;
}

现在当你尝试使用 tsc(TypeScript 编译器)编译你的 Web Component 时将报错,因为编译器无法获取 HTMLAttributes 类型。你需要安装 @types/react 作为开发时的依赖。但是现在你的 Web Component 项目依赖于 React 类型,这是一种倒退,你基于标准自定义的组件不应该依赖于某个前端框架。你可以依赖 React,也可以将你的组件声明为 any 类型,然后编写完备的文档去描述所有属性。

或使用一些工具来帮忙

如果你不想自己处理生成 IntrinsicElement 类型的问题,你可以使用 StencilJS 类似的工具来构建你的 Web Component。它将为你处理类型,生成一个全局声明将你的 Web Component 定义在 IntrinsicElements 接口中。

方案二:在 React 项目中定义类型

与方案一类似。该方案将类型定义在 React 项目中,而不是 Web Component 项目中。我习惯创建一个 declarations.d.ts 文件,然后将类型定义在这里:

declare namespace JSX {
  interface IntrinsicElements {
    "wc-menu-button": any;
  }
}

现在你可以在 React 项目中使用你的 Web Component 了。

Go 语言测量函数执行时间

测量一段代码

start := time.Now()
// Code to measure
duration := time.Since(start)

// Formatted string, such as "2h3m0.5s" or "4.503μs"
fmt.Println(duration)

// Nanoseconds as int64
fmt.Println(duration.Nanoseconds())

测量函数调用

func foo() {
	defer duration(track("foo"))
	// Code to measure
}
func track(msg string) (string, time.Time) {
	return msg, time.Now()
}

func duration(msg string, start time.Time) {
	log.Printf("%v: %v\n", msg, time.Since(start))
}

tree shaking 与死代码删除

原文:https://medium.com/@Rich_Harris/tree-shaking-versus-dead-code-elimination-d3765df85c80

我一直在研究一种名为 Rollup 的工具,它将 JavaScript 模块捆绑在一起。它的一个特性是 tree shaking 功能,我的意思是它只包含产出实际运行所需要的代码。

Axel Rauschmayer 问这个术语是从哪来的?

Amjad Masad 说这个只是死代码删除的不同命名。

但它们实际上是不同的东西,即使它们有相同的目标(更少的代码)。

死代码删除(dead code elimination)是愚蠢的

不准确的类比:想象你做蛋糕时,把整个鸡蛋扔进搅拌碗里,然后把它们搅碎,而不是把它们打开,然后把里面的东西倒出来。一旦蛋糕从烤箱中出来,你可以清理蛋壳碎片,不过这很棘手,因为大部分的蛋壳都留在了里面。

这就是死代码消除所包含的内容:获取产出,然后不完美地删除你不想要的部分。另一方面,tree shaking 问的是相反的问题:如果我想做蛋糕,我需要在搅拌碗里加入哪些配料?

我们不是排除死代码,而是保留使用代码。理想情况下,最终结果应该是相同的,但由于 JavaScript 静态分析的局限性,情况并非如此。使用代码保留可以获得更好的结果,它是防止用户下载未使用代码的一种更合乎逻辑的方法。

这个想法只有在你考虑模块和打包的时候才有意义。

(Rollup 并不完美 —— 随后会有具体解释 —— 所以最好的产出是通过这两个步骤来获得,即 Rollup 后 UglifyJS 或 Webpack 2 与Uglify 插件。)

是的,这可能不是一个好的名字

一方面,这影射着您在摇掉枯枝。另一方面,除了我以外的人已经用它来描述这种技术,主要是在 Dart 社区。我不记得我是在哪儿学到的这个术语。

我想过在 Rollup 中使用『live code inclusion』阶段,但因为 tree shaking 是一个已有的概念,我似乎不必再引入更多的困惑。也许那是个错误的决定。

Rollup tree shaking 的限制

它目前并没有从已使用的对象中删除未使用的方法之类的东西,有时它被迫做出最坏的假设,以确保得到的程序是正确的。这是因为在像 JavaScript 这样的动态语言中进行静态分析是很困难的。2016年,随着我们添加类型跟踪和其他技术,将会有更多的改进。

Rollup 不仅 tree shaking

Rollup 的目标是生成最高效的包,看起来就像是人编写的。tree shaking 是一部分,Rollup 还做了其它工作——它不会将模块封装在函数中,它不会在把一个模块加载器放在你的产出顶部,它不会从中间 AST 生成结果代码,而是尽可能地保留您的原始代码。正因如此,它特别适合用来编写库(它可以用于应用程序,尽管 Webpack 有很多 Rollup 没有的特性,所以见仁见智),特别是那些很少有非 ES6 依赖的(尽管它可以使用 CommonJS 模块)。

React 是创建声明式 UI 框架的 LLVM

原文:https://agent-hunt.medium.com/react-is-also-the-llvm-for-creating-declarative-ui-frameworks-767e75ce1d6a

『LLVM 项目是一个模块化的、可重用的编译器和工具链技术的集合。』LLVM 使创建编程语言变得容易。

当我们初次遇到 React 时,它是作为一个用于构建用户界面的 JavaScript 库被引入的。当你深入研究其内部原理后,React 也可以被称为创建声明式 UI 框架的 LLVM。它使得创建自己的声明式 UI 框架变得容易。

如果您问为什么要构建声明式 UI,那是因为声明式代码更容易编写、阅读、管理和调试或几乎不调试。手动管理 UI 的更新很麻烦而且容易出错,而像 React 这样的工具在这方面做得更好。

React 最初是面向 Web 应用的。很快我们便发现这些理念不仅适用于 Web 应用,还适用于 iOS 和 Android。我们可以使用相同的组件声明方式为所有这些不同的平台构建 UI。

image

React 中有3个基本块。

  1. React Component API:提供组件 API 和生命周期
  2. React-Reconciler:它是核心 diff 算法,管理声明式 UI 背后的命令式更新。Reconciler 找出应用程序 UI 在不同状态之间的变化并在幕后应用这些变化
  3. React Renderer:渲染器不过是实现了一些 react-reconciler 所需要的函数。react-reconciler 将根据需要调用这些函数,以对最终目标更新变化。如果使用 DOM API 实现这些函数,则目标是 Web 应用。如果你使用 iOS UI Kit API 实现这些函数,目标是 iOS。如果使用 Android UI API 实现这些函数,则目标是 Android。事实上,任何支持绘图的系统都可以作为 React 的渲染目标。

image

react-reconciler 在 React 16 版本中被完全重写,使用了一个更好的实现 React Fiber。在 React Fiber 之前,Reconciler 是紧密耦合的。现在它已经解耦,并且更容易编写自定义渲染器。

JavaScript 位操作符

ECMAScript 中的所有数值都以 IEEE 754 64 位格式存储,但位操作并不直接应用到 64 位表示,而是先将值转换为 32 位整数,再进行位操作,之后再把结果转换为 64 位。对开发者而言,就好像只有 32 位整数一样,因为 64 位整数存储格式是不可见的。

有符号整数使用 32 位的前 31 位表示整数值。第 32 位表示数值的符号,如 0 表示正,1 表示负。这一位表示符号为(sign bit),它的值决定了数值其余部分的格式。

按位非

按位非操作符用波浪符号(~)表示,它的作用是返回数值的补数。

按位与

按位与操作符用和号(&)表示,有两个操作数。

按位或

按位或操作符用管道符(|)表示,同样有两个操作数。

按位异或

按位异或用脱字符(^)表示,同样有两个操作数。

左移

左移操作符用两个小于号(<<)表示,会按照指定的位数将数值的所有位向左移动。

有符号右移

有符号右移由两个大于号(>>)表示,会将数值的所有 32 位都向右移,同时保留符号(正或负)。

无符号右移

无符号右移用 3 个大于号表示(>>>),会将数值的所有 32 位都向右移。

与有符号右移不同,无符号右移会给空位补 0,而不管符号位是什么。对正数来说,这跟符号右移效果相同。但对负数来说,结果就差太多了。

TypeScript: 接口 vs. 类型别名

原文:TypeScript: Interfaces vs Types

1. 对象(Objects) / 方法(Functions)

二者均可被用来声明对象方法的签名,但语法不同。

接口

interface Point {
  x: number;
  y: number;
}

interface SetPoint {
  (x: number, y: number): void;
}

类型别名

type Point = {
  x: number;
  y: number;
};

type SetPoint = (x: number, y: number) => void;

2. 其他类型

接口不同,类型别名可以被用于其他类型,如基本类型、联合类型和元组。

// 基本类型
type Name = string;

// object
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };

// 联合类型
type PartialPoint = PartialPointX | PartialPointY;

// 元组
type Data = [number, string];

3. 扩展(Extend)

二者均可扩展,但语法不同。另外,请注意接口类型别名并不是互斥的。接口可以扩展类型别名,反之亦然。

接口扩展接口

interface PartialPointX { x: number; }
interface Point extends PartialPointX { y: number; }

类型别名扩展类型别名

type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };

接口扩展类型别名

type PartialPointX = { x: number; };
interface Point extends PartialPointX { y: number; }

类型别名扩展接口

interface PartialPointX { x: number; }
type Point = PartialPointX & { y: number; };

4. 实现(Implements)

可以以完全相同的方式实现接口类型别名。 但是请注意,接口被视为静态蓝图。 因此,他们不能实现或扩展被定义为联合类型类型别名

interface Point {
  x: number;
  y: number;
}

class SomePoint implements Point {
  x = 1;
  y = 2;
}

type Point2 = {
  x: number;
  y: number;
};

class SomePoint2 implements Point2 {
  x = 1;
  y = 2;
}

type PartialPoint = { x: number; } | { y: number; };

// FIXME: 不可以实现一个联合类型
class SomePartialPoint implements PartialPoint {
  x = 1;
  y = 2;
}

5. 声明合并

类型别名不同,接口可以被定义多次,并将被视为一个接口(合并了所有声明的成员)。

// 这里的两个定义将合并为:
// interface Point { x: number; y: number; }
interface Point { x: number; }
interface Point { y: number; }

const point: Point = { x: 1, y: 2 };

TypeScript 类型系统是图灵完备的

出处:microsoft/TypeScript#14833

这不是一个 bug 反馈,我当然不希望 TypeScript 类型系统因为这条 issue 而受到限制。但是,我注意到当前版本(2.2 版)的类型系统是图灵完备的。

通过映射类型、递归类型定义、索引访问成员类型以及可以创建任意数量的类型,来实现图灵完备。

TrueExprFalseExprTest 定义为适合的类型,如下的实现将具备图灵完备性:

type MyFunc<TArg> = {
  "true": TrueExpr<MyFunction, TArg>,
  "false": FalseExpr<MyFunc, TArg>
}[Test<MyFunc, TArg>];

即使我没有正式证明(编辑:与此同时,我确实 - 见下文)提到的设备使 TypeScript 图灵完整,但通过查看以下测试给定类型是否表示素数的代码示例应该很明显数字:
Even though I didn't formally prove (edit: in the meantime, I did - see below) that the mentioned device makes TypeScript turing complete, it should be obvious by looking at the following code example that tests whether a given type represents a prime number:

type StringBool = "true"|"false";

interface AnyNumber { prev?: any, isZero: StringBool };
interface PositiveNumber { prev: any, isZero: "false" };

type IsZero = TNumber["isZero"];
type Next = { prev: TNumber, isZero: "false" };
type Prev = TNumber["prev"];

type Add<T1 extends AnyNumber, T2> = { "true": T2, "false": Next<Add<Prev, T2>> }[IsZero];

// Computes T1 * T2
type Mult<T1 extends AnyNumber, T2 extends AnyNumber> = MultAcc<T1, T2, _0>;
type MultAcc<T1 extends AnyNumber, T2, TAcc extends AnyNumber> =
{ "true": TAcc, "false": MultAcc<Prev, T2, Add<TAcc, T2>> }[IsZero];

// Computes max(T1 - T2, 0).
type Subt<T1 extends AnyNumber, T2 extends AnyNumber> =
{ "true": T1, "false": Subt<Prev, Prev> }[IsZero];

interface SubtResult<TIsOverflow extends StringBool, TResult extends AnyNumber> {
isOverflowing: TIsOverflow;
result: TResult;
}

// Returns a SubtResult that has the result of max(T1 - T2, 0) and indicates whether there was an overflow (T2 > T1).
type SafeSubt<T1 extends AnyNumber, T2 extends AnyNumber> =
{
"true": SubtResult<"false", T1>,
"false": {
"true": SubtResult<"true", T1>,
"false": SafeSubt<Prev, Prev>
}[IsZero]
}[IsZero];

type _0 = { isZero: "true" };
type _1 = Next<_0>;
type _2 = Next<_1>;
type _3 = Next<_2>;
type _4 = Next<_3>;
type _5 = Next<_4>;
type _6 = Next<_5>;
type _7 = Next<_6>;
type _8 = Next<_7>;
type _9 = Next<_8>;

type Digits = { 0: _0, 1: _1, 2: _2, 3: _3, 4: _4, 5: _5, 6: _6, 7: _7, 8: _8, 9: _9 };
type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type NumberToType = Digits[TNumber]; // I don't know why typescript complains here.

type _10 = Next<_9>;
type _100 = Mult<_10, _10>;

type Dec2<T2 extends Digit, T1 extends Digit>
= Add<Mult<_10, NumberToType>, NumberToType>;

function forceEquality<T1, T2 extends T1>() {}
function forceTrue<T extends "true">() { }

//forceTrue<Equals< Dec2<0,3>, Subt<Mult<Dec2<2,0>, _3>, Dec2<5,7>> >>();
//forceTrue<Equals< Dec2<0,2>, Subt<Mult<Dec2<2,0>, _3>, Dec2<5,7>> >>();

type Mod<TNumber extends AnyNumber, TModNumber extends AnyNumber> =
{
"true": _0,
"false": Mod2<TNumber, TModNumber, SafeSubt<TNumber, TModNumber>>
}[IsZero];
type Mod2<TNumber extends AnyNumber, TModNumber extends AnyNumber, TSubtResult extends SubtResult<any, any>> =
{
"true": TNumber,
"false": Mod<TSubtResult["result"], TModNumber>
}[TSubtResult["isOverflowing"]];

type Equals<TNumber1 extends AnyNumber, TNumber2 extends AnyNumber>
= Equals2<TNumber1, TNumber2, SafeSubt<TNumber1, TNumber2>>;
type Equals2<TNumber1 extends AnyNumber, TNumber2 extends AnyNumber, TSubtResult extends SubtResult<any, any>> =
{
"true": "false",
"false": IsZero<TSubtResult["result"]>
}[TSubtResult["isOverflowing"]];

type IsPrime = IsPrimeAcc<TNumber, _2, Prev<Prev>>;

type IsPrimeAcc<TNumber, TCurrentDivisor, TCounter extends AnyNumber> =
{
"false": {
"true": "false",
"false": IsPrimeAcc<TNumber, Next, Prev>
}[IsZero<Mod<TNumber, TCurrentDivisor>>],
"true": "true"
}[IsZero];

forceTrue< IsPrime<Dec2<1,0>> >();
forceTrue< IsPrime<Dec2<1,1>> >();
forceTrue< IsPrime<Dec2<1,2>> >();
forceTrue< IsPrime<Dec2<1,3>> >();
forceTrue< IsPrime<Dec2<1,4>>>();
forceTrue< IsPrime<Dec2<1,5>> >();
forceTrue< IsPrime<Dec2<1,6>> >();
forceTrue< IsPrime<Dec2<1,7>> >();
Besides (and a necessary consequence of being turing complete), it is possible to create an endless recursion:

type Foo<T extends "true", B> = { "true": Foo<T, Foo<T, B>> }[T];
let f: Foo<"true", {}> = null!;
Turing completeness could be disabled, if it is checked that a type cannot use itself in its definition (or in a definition of an referenced type) in any way, not just directly as it is tested currently. This would make recursion impossible.

//edit:
A proof of its turing completeness can be found here

数据流分析

流分析使用的中间表示

各种数据流分析

到达定值

可用表达式

到达表达式

活跃分析

使用数据流分析结果的几种转换

公共子表达式删除

常数传播

复写传播

死代码删除

加速数据流分析

位向量

基本块

节点排序

使用 - 定制链和定值 - 使用链

工作表算法

增量数据流分析

别名分析

基于类型的别名分析

基于流的别名分析

使用可能别名信息

严格的纯函数语言中的别名分析

Go 语言中如何并行执行多个函数

原文:https://www.digitalocean.com/community/tutorials/how-to-run-multiple-functions-concurrently-in-go

介绍

Go 中的两个特性,goroutines 和 channels,在一起使用时使并发更容易。 Goroutine 解决了在程序中设置和运行并发代码的困难,通道解决了并发运行的代码之间安全通信的困难。

使用 Goroutine 并行执行函数

Go 执行此操作的一种方法是使用称为 goroutine 的功能。 goroutine 是一种特殊类型的函数,可以在其他 goroutine 也在运行时运行。当一个程序被设计为一次运行多个代码流时,该程序被设计为同时运行。通常,当一个函数被调用时,它会在它继续运行之后完全在代码之前完成运行。这被称为在“前台”运行,因为它会阻止您的程序在完成之前执行任何其他操作。使用 goroutine,当 goroutine 在“后台”运行时,函数调用将立即继续运行下一个代码。当代码在完成之前不阻止其他代码运行时,它被视为在后台运行。

在 main.go 文件中粘贴或键入以下代码以开始使用。

package main

import (
	"fmt"
)

func generateNumbers(total int) {
	for idx := 1; idx <= total; idx++ {
		fmt.Printf("Generating number %d\n", idx)
	}
}

func printNumbers() {
	for idx := 1; idx <= 3; idx++ {
		fmt.Printf("Printing number %d\n", idx)
	}
}

func main() {
	printNumbers()
	generateNumbers(3)
}

这个初始程序定义了两个函数,generateNumbers 和 printNumbers,然后在 main 函数中运行这些函数。 generateNumbers 函数将要“生成”的数字数量作为参数,在本例中为 1 到 3,然后将这些数字中的每一个打印到屏幕上。 printNumbers 函数还没有接受任何参数,但它也会打印出数字 1 到 3。

package main

import (
	"fmt"
	"sync"
)

func generateNumbers(total int, wg *sync.WaitGroup) {
	defer wg.Done()

	for idx := 1; idx <= total; idx++ {
		fmt.Printf("Generating number %d\n", idx)
	}
}

func printNumbers(wg *sync.WaitGroup) {
	defer wg.Done()

	for idx := 1; idx <= 3; idx++ {
		fmt.Printf("Printing number %d\n", idx)
	}
}

func main() {
	var wg sync.WaitGroup

	wg.Add(2)
	go printNumbers(&wg)
	go generateNumbers(3, &wg)

	fmt.Println("Waiting for goroutines to finish...")
	wg.Wait()
	fmt.Println("Done!")
}

使用 Channel 在 Goroutine 间安全地进行通信

并发编程中比较困难的部分之一是在同时运行的程序的不同部分之间进行安全通信。如果你不小心,你可能会遇到只有并发程序才有可能出现的问题。例如,当程序的两个部分同时运行时,可能会发生数据竞争,其中一部分尝试更新变量,而另一部分同时尝试读取它。发生这种情况时,读取或写入可能会发生乱序,导致程序的一个或两个部分使用错误的值。 “数据竞赛”这个名称来自程序的两个部分相互“竞赛”以访问数据。

内存一致性模型

内存一致性模型描述的是程序在执行过程中内存操作正确性的问题。内存操作包括读操作和写操作,每一操作又可以用两个时间点界定:发出(Invoke)和响应(Response)。在假定没有流水线的情况下(即单个处理器内指令的执行是按顺序执行的),设系统内共有N个处理器,每个处理器可发出image个内存读写操作(读或写),那么总共有:image种可能执行的顺序。内存一致性模型描述的就是这些操作可能的执行顺序中哪些是正确的。

最终一致性(英文:Eventual consistency)是分布式计算里的一种内存一致模型,它指对于已改变写的数据的读取,最终都能取得已更新的数据,但不完全保证能立即取得已更新的数据。这种模型通常可以实现较高的可用性。最终一致性,通过乐观复制(英文:Optimistic replication),或称延迟复制(lazy replication)实现。这种概念最初始于移动应用,后来在各类分布式系统中也有广泛的应用。达到最终一致性的分布式系统被称为副本达到了“收敛(converged)”状态。最终一致性是一种较弱的保证。如果某个系统满足更强的一致性约束(例如线性一致性),它就同时具有最终一致性,但是反过来则未必成立,仅保证最终一致性的系统无法保证更强的约束。

Paxos、Raft 协议是什么?

渐进式 Web 动画

原视频:https://www.youtube.com/watch?v=laPsceJ4tTY&t=1s

什么是移动端优先的动画

  1. 受原生应用的设计范式启发,尤其是对手势的使用。
  2. 在移动设备上开发,并且有意地设计为在移动浏览器环境中工作,而非桌面端。
  3. 即使在配置较低的设备上也有很好的性能。

一些设计范式

水平滑动

我们只使用 CSS 就可以应用它。

.tab-panels-container {
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  overflow-x: scroll;
  width: 100vw;
  display: flex;
}

.tab-panel {
  scroll-snap-align: start;
  /* only supported in Chrome */
  scroll-snap-stop: always;
}

滑动消失

一个使用 React-Spring 和 React-Use-Gesture 实现的例子。

有关触摸动画的深刻见解

让我们来学习一下移动端基础的触摸动画,来看看有哪些深刻的见解可以被我们采用。

立即响应

如果你引入了一些延迟,将会极大地破坏用户体验。会产生额外的心智负担,让用户感觉他们的行为与动画有很强的割裂感。

滚动衰减

  • 衰减函数不是像弹簧一样提前确定端点,而是简单地逐渐降低速度,直到达到 0。
  • iOS 有两个默认的衰减率:『快』(0.99)和『默认』(0.998)。
// 从苹果『Designing Fluid Interfaces』演讲中获得
const projection = startVelocity => (startVelocity * 0.998) / (1 - 0.998)

// 使用示例
const velocityY = 1.2 // in px/ms
const projectedEndPoint = projection(velocityY) // 679px

回弹动画(Rubberbanding)

const clamp = (min, max, val) => Math.max(Math.min(val, max), min)
const rubberBand = (distance, dimension, constant = 0.55) =>
    (distance * dimension * constant) /
    (dimension * constant * distance)

const rubberBandClamp = (min, max, delta, constant) => delta < min
     -rubberBand(min - delta, max - min, constant) + min
    : delta > max
        ? rubberBand(delta - max, max - min, constant) + max
        : delta

触摸取消

event.preventDefault()?

touch-action

touch-action: all;

浏览器处理所有的手势(默认)。

touch-action: none;

禁止元素在浏览器中的所有手势处理。

touch-action: pan-x;

禁止浏览器中的垂直、y 轴手势。

touch-action: pan-y;

禁止浏览器中的水平、x 轴手势。

移动端浏览器概要

  1. 利用迟滞来确保用户不会无意中触发动画。
  2. 使用 touch-action CSS 属性禁止浏览器的默认表现。
  3. 注意移动浏览器有很多不同之处,既有默认的手势表现,也有对 touch-action 等属性的支持。

性能

只使用 transform 和 opacity

限制自己仅使用这两个能够使用 GPU 进行加速的 CSS 属性,可以在移动设备上获得每秒 60 帧的最佳效果。

简化:更简单的动画意味着更好的性能

让动画开始时间保持在 100ms 以下

“在此时间内响应用户操作,用户会觉得效果是及时的。再过一段时间,用户行为和交互反应之间的联系就会断开。”

如何诊断

  • 使用 Chrome 开发者工具,执行性能分析。
  • 使用 React 开发这工具,发现没有必要的 React 组件调度。

总结

  1. 请直接在移动设备上开发——如果你关注性能,最好使用一个性能差一点的手机。
  2. 从简单的设计模式开始,从移动端原生应用的设计模式中汲取灵感。
  3. 当添加一个触摸动画,考虑使用迟滞和触摸取消技术。

同构 JavaScript:Web 应用的未来

原文:https://medium.com/airbnb-engineering/isomorphic-javascript-the-future-of-web-apps-10882b7a2ebc#.4nyzv6jea

在 Airbnb 过去的几年里,我们在构建富 Web 应用体验时学到了很多东西。2011年,我们携我们的移动端网站进军单页面应用领域,并推出了“愿望列表”和新设计的搜索页面等。它们都是大型的 JavaScript 应用程序,这意味着大部分在浏览器中运行的代码都是为了支持更现代的交互体验。

这种方式现在很普遍,像 Backbone.js、Ember.js 和 Angular.js 这样的库让开发人员更容易构建这些富 JavaScript 应用。然而我们发现,此类应用有一些显著的缺点。为了解释原因,让我们先快速回顾一下 Web 应用的历史。

JavaScript 的进化

Web 刚出现时,浏览体验是这样的:Web 浏览器会请求指定的页面(例如,“http://www.geocities.com”,使互联网上的某个服务器生成一个 HTML 页面并通过网络将其发回。因为浏览器功能不是很强大,大多是静态的 HTML 页面。JavaScript 是为了让网页更具动态性而创造的,但起初没有支持太多功能。

经过多年个人电脑的发展,富有创造力的技术人员将 Web 推向了极致,浏览器也在不断发展。现在,Web 已经发展为一个功能完备的应用程序平台,而高性能的 JavaScript 运行时和 HTML5 标准使开发人员能够创建以前只有在原生平台才能实现的富应用程序。

单页应用

不久,开发人员就利用这些新功能使用 JavaScript 在浏览器中构建整个应用程序。像 Gmail 这样的应用程序是单页应用程序的典型例子,它可以立即响应用户的交互,不必在每呈现一个新页面就往返一次服务器。

像 Backbone.js、Ember.js 和 Angular.js 这样的库通常被称为客户端 MVC(Model-View-Controller)或 MVVM(Model-View-ViewModel)库。典型的客户端 MVC 结构如下:

image

大部分应用程序逻辑(视图、模板、控制器、模型、国际化等)都存在于客户端中,通过 API 获取数据。服务器可以用任何语言编写,比如 Ruby、Python 或 Java,它主要为了提供初始的 HTML 页面。一旦浏览器下载了JavaScript 文件,就会执行他们来初始化客户端应用程序,从 API 获取数据并渲染其余的 HTML 页面。

这对用户来说非常好,因为一旦应用程序完成初始化加载,就可以支持页面间的快速导航,而无需刷新页面,甚至可以离线工作。

这对开发人员来说也是非常好的,因为理想的单页应用程序清晰地分离客户端和服务器之间的关注点,促进了一个良好的开发工作流程,并避免需要在两者之间共享太多的逻辑,而这两者通常是用不同的语言编写的。

局限

然而,在实践中,这种方式有一些致命的缺点。

SEO

一个只能在客户端运行的应用程序不能为爬虫程序提供 HTML,因此默认情况下它的 SEO 很差。网络爬虫的功能是向服务器发送请求并解析返回结果;但是如果服务器返回一个空白页,就没有什么价值了。有解决办法,但仍然无法通过一些考验。

性能

同样,如果服务器不渲染完整的 HTML 页面,而是等待客户端 JavaScript 来渲染,那么在看到页面上的内容之前,用户将经历几秒钟的白屏或加载动画。有大量的研究表明,一个速度慢的网站会对用户产生巨大的影响,从而影响收入。亚马逊声称页面的加载时间每减少 100 毫秒,收入就会增加 1%。Twitter 花了一年时间并投入 40 名工程师重建了他们的网站,将其在服务器上渲染而不是在客户端上,声称加载速度提高了 5 倍。

包括我在内的一些开发人员都对这种方式感到痛心——通常只有在投入时间和精力构建一个单页应用程序之后,才能清楚地知道缺点是什么。

互补

归根结底,我们想要结合新老方式以取长补短:我们想要服务器端提供完整的 HTML 以提高性能和 SEO,我们也想要客户端应用程序的用户体验和灵活性。

为此,我们在 Airbnb 进行了“同构 JavaScript”应用程序的实验,这是一种可以同时在客户端和服务器端运行的 JavaScript 应用程序。

同构应用程序可能看起来像这样:

image

这意味着拥有更好的性能、更好的可维护性、友好的 SEO。

有了 Node.js,一个高性能的、稳定的服务器端 JavaScript 宿主环境,让我们现在可以实现这个梦想了。通过建立适当的抽象,我们编写应用程序的逻辑可以同时在服务器和客户端运行——这就是同构 JavaScript 的定义。

抽象,抽象,抽象

这些项目往往是大型的、全栈的 Web 框架,表明了问题的难度。客户端和服务器是非常不同的环境,因此我们必须创建一组抽象,将我们应用程序的逻辑与底层实现分离。

路由

我们需要将 URI 映射到对应的路由处理程序。我们的路由处理程序需要能够访问 HTTP 头、Cookie 和 URI 信息,并能够在不直接访问 window.location(browser)或 req 和 res(Node.js)的情况下完成重定向。

获取和持久化数据

我们想独立于请求机制来描述用于渲染特定页面或组件所需要的资源。资源描述符可以是指向 JSON 的普通 URI,对于大型应用程序,将资源封装到模型和集合中,然后指定一个模型类和主键,在某些情况下将转换为一个 URI(译者注:此处所描述的方法可能类似于 GraphQL 这种 API 查询语言)。

渲染视图

无论我们选择直接操作DOM,使用基于字符串的 HTML 模板,还是具有 DOM 抽象的 UI 组件库,我们都需要能够同构地渲染标记的能力。根据应用程序的需要,我们应该能够在服务器或客户端上渲染任何视图。

构建和打包

事实证明,编写同构应用程序的代码只是成功的一半。像 Grunt 和 Browserify 这样的工具是工作流的重要组成部分,可以让应用程序真正启动并运行。构建步骤有很多:编译模板,包含客户端依赖、进行转换、代码压缩等。简单的情况是将所有应用程序代码、视图和模板合并到一个包中,但对于较大的应用程序,这可能会导致数百 KB 的下载量。更好的方法是进行代码拆分和按需加载。

组合小模块

首个面世的同构框架意味着必须一次性解决以上所有问题。这会导致框架庞大且笨重,难以采用并整合到现有的应用。随着越来越多的开发人员的参与,我们将看到大量可重用的小模块,可以将他们集成在一起来构建同构的应用程序。

事实证明,大多数 JavaScript 模块已经可以同构使用,几乎不需要修改。例如,在服务器上可以使用流行的库,如Underscore、Backbone.js、Handlebars.js、Moment,甚至 jQuery。

为了演示这一点,我创建了一个名为 isomorphic tutorial 的示例应用程序,您可以在 GitHub 上查看它。通过将几个可以同构使用的模块组合在一起,只需几百行代码就可以轻松创建一个简单的同构应用程序。它使用 Director 处理服务端和浏览器的路由,Superagent 处理 HTTP 请求,Handlebars.js 处理模板,所有这些都构建在 Express.js 应用程序基础之上。当然,随着应用程序复杂性的增加,必须引入更多的抽象,但我希望随着更多开发人员的尝试,会出现新的库和标准。

Android Webview

https://developer.android.com/guide/webapps/webview.html
https://developer.android.com/reference/android/webkit/WebView
JNI 调用在其中:https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/native/webview/
https://chromium.googlesource.com/chromium/src/android_webview/support_library/boundary_interfaces/

Chromium 视角

Webkit 边界接口

注意:边界接口的基准副本存在于 chromium 仓库中;这个 chromium 子目录以镜像的形式存在于 Android 项目中,供 webkit AndroidX 模块使用。

如果您在 Android 仓库中阅读此文件,请不要在此文件夹中进行更改。 请在 chromium 中进行更改,或联系 frameworks/support/webkit/ 的 OWNERS。

如果您正在读取 chromium 仓库中的这个文件,您应该可以随意进行更改。请注意,这个文件夹有严格的导入要求(由DEPS强制),因为它必须继续构建镜像到 Android。

Parcel 的 Tree shaking 实现

原文:https://medium.com/@devongovett/parcel-v1-9-0-tree-shaking-2x-faster-watcher-and-more-87f2e1a70f79#4ed3

Parcel 编译后完全移除掉模块系统,提升所有模块到同一顶级作用域中,清除掉生产包中的无用代码。Parcel 同时支持 ES6 和 CommonJS 模块的 Tree shaking。

Parcel Tree shaking 的实现包含以下四个阶段:

  1. 提升阶段 —— 独立地发生在每个文件中。在这个阶段中,Parcel 对所有顶级变量进行唯一重命名,从而避免与其它模块中的变量产生冲突,对模块中的导入导出标识生成元数据。
  2. 组合阶段 —— 当所有的文件被处理完毕后,Parcel 以正确的顺序将所有模块组合在一起。在这里需要考虑许多边界问题,尤其是对于 CommonJS 模块,例如循环依赖、副作用、内嵌的 require。最终生成一个包含所有代码的单一文件。
  3. 链接阶段 —— 当所有文件通过正确的顺序被组合完毕后,Parcel 使用在提升阶段中产生的元数据,将各模块的导入和导出链接起来。它会处理 CommonJS 和 ES6 模块之间的互操作性,因此您可以 import 一个 CommonJS 文件,或者 require 一个 ES6 模块。
  4. 清理阶段 —— 在这个阶段中,将删除无用的声明和顶级变量。大多数的压缩工作在文件层面,发生在提升阶段后。剩下的唯一工作就是清理掉顶级作用域中没有在模块之间使用的声明,并压缩剩下的顶级变量名。

下面示例展示了 Parcel 的 Tree shaking 如何移除未使用的导出,并重命名变量,以便它们不与其他模块冲突。输出包中没有模块包装函数,只有一个 IIFE 包装了整个包,模块的成本为零。

// index.js
import {add} from './math';
console.log(add(2, 3));

// math.js
export function add(a, b) {
    return a + b;
}
export function sub(a, b) {
    return a - b;
}

// 打包的输出
(function () {
// ASSET: math.js
function $3$export$add(a, b) {
    return a + b;
}

// ASSET: index.js
console.log($3$export$add(2, 3));
})();

// 压缩后
(function () {function a($, r) {return $+r}console.log(a(2,3));})();

Parcel 也支持 package.json 中的 sideEffects: false 标记,这表明库作者告知 Parcel 和其它打包工具,该库在初始化阶段中不包含任何副作用,允许打包工具更加有效地判断需要包含哪些代码。

jest-worker

动机:在部署 next.js 后发现一个 next.js 实例创建了很多 jest-worker 线程,其作用什么?

jest-worker 有什么用?

该模块提供 Promise 的接口、最小化开销和 worker 绑定机制,在 fork 的进程下并行执行繁重的任务。

例子

parent.js 文件

import JestWorker from 'jest-worker';
 
async function main() {
  const worker = new JestWorker(require.resolve('./Worker'));
  const result = await worker.hello('Alice'); // "Hello, Alice"
}
 
main();

worker.js 文件

export function hello(param) {
  return 'Hello, ' + param;
}

next.js 使用它做什么?

调用 next.js 的方式

import next from 'next';

const app = next({dev});
const handle = app.getRequestHandler();
await app.prepare();

jest-worker 在 next.js 的一下文件中被使用。

next-js/packages/next/build/index.ts
next-js/packages/next/build/webpack/plugins/terser-webpack-plugin/src/index.js
next-js/packages/next/export/index.ts
next-js/packages/next/next-server/server/config-utils.ts
next-js/packages/next/next-server/server/lib/squoosh/main.ts
next-js/packages/next/server/next-dev-server.ts

next.js 启动流程

画线条很难

原文:https://mattdesl.svbtle.com/drawing-lines-is-hard

在 OpenGL,尤其是在 WebGL,要画好线条是非常困难的。在这里,我探索了一些不同的 2D 和3D 线条渲染技术,每个都带有一个小例子。

例子的源码在这里:https://github.com/mattdesl/webgl-lines

Line 基原

WebGL 包含这些线段 gl.LINESgl.LINE_STRIPgl.LINE_LOOP。听起来不错是么?但并不是。这里有一些问题:

  • 驱动实现渲染、过滤的方式可能会略微不同,在设备或浏览器之间可能无法获得一致的渲染。
  • 最大线条宽度依赖驱动。例如最大值可能被限制为 1.0。
  • 无法控制线条连接处或端点处的样式。
  • 并非所有设备都支持 MSAA,大多数浏览器不支持屏幕外缓冲区。最终线条可能会出现锯齿。
  • 对于虚线,glLineStipple 已被弃用,在 WebGL 中不存在。

三角剖分线条

一种常见方法是将一条线拆分为由三角形组成的条带,这使得可以最大程度地控制直线。

它的一种典型实现方法是获取路径上每个点的法线,并在两侧向外扩展一半厚度。

有以下选择进行反走样:

  • 期望支持 MSAA,这样将永远不需要将线条渲染到屏幕外缓冲区。
  • 添加更多三角形为线条进行羽化。
  • 使用纹理查找对 alpha 值进行渐变;非常简单,但扩展性不好。
  • 在片元着色器中,根据屏幕空间中线条的投影比例计算抗锯齿。
  • 使用 gl.LINES 围绕线条边缘渲染第二遍。

扩展定点着色器

屏幕空间投影线

上一个方案对于 2D 线条效果很好,但可能不适合在 3D 空间中的设计需求。

为什么不用正则表达式来解析 HTML、XML

原文:https://stackoverflow.com/questions/6751105/why-its-not-possible-to-use-regex-to-parse-html-xml-a-formal-explanation-in-la

有限自动机(正则表达式底层的数据结构)除了状态之外不存储其他信息,如果有任意深度的嵌套,则需要任意多状态的自动机,这与有限自动机的概念相冲突。

正则表达式的定义等同于:可以通过有限自动机来测试字符串是否与模式匹配(每个模式都有一个不同的自动机)。一个有限的自动机不存储额外信息——没有栈,没有堆。它只有限数量的内部状态,每个内部状态都可以从被测试的字符串中读取一个输入单元,以此来决定下一个状态哪个。它有两种终止状态:“是的,匹配”和“不,不匹配”。

另一方面,HTML 具有可以任意深度嵌套的结构。要确定文件是否为有效的HTML,需要检查所有结束标记是否与先前的开始标记匹配。要解析它,你需要知道那些元素还没有被闭合。

正则语言的泵引理

定义

假设 image 是正则语言,则存在正整数 image,对任意字符串 imageimage(n 为泵长度,可以理解为正则语言等效的极小化 DFA 的状态个数),可以将 image 写成 image 的形式,使得以下说法成立:

  1. image
  2. image
  3. image

扩展

将正则表达式转换为确定的有限自动机

原文:https://lambda.uta.edu/cse5317/notes/node9.html
参考代码:https://gist.github.com/DmitrySoshnikov/a6c1535ffca1b4056a923a04299e60dc

弹簧动画背后的物理原理

原文:https://blog.maximeheckel.com/posts/the-physics-behind-spring-animations/

胡克定律

首先,弹簧动画之所以叫这个名字,是因为动画本身遵循弹簧的物理特性,也称之为谐波振荡器(Harmonic Oscillator)。这个术语和与之相关的数学可能看起来非常可怕和复杂,但我对它十分了解,我会尽可能地简化这些内容。当我在大学时,我们这样定义一个谐波振荡器:

一个系统,当偏离其平衡时,会受到与偏移量 x 成正比的力 F。

这种力的公式被称为胡克定律,定义如下:

F = -k*x

其中 k 是一个正数,被称为弹性系数(stiffness),我们可以写成:

力 = 负的 弹性系数 * 偏移量

这意味着:

  • 如果我们将弹簧拉到离平衡点一定距离(即 x > 0),它将开始移动
  • 如果我们不去按压它(即 x = 0),它会保持静止

也许你可能在学校或 Youtube 科技类频道听说过力等于物体的质量乘以它的加速度,它转化为以下公式:

F = m*a

在这里 m 是质量,a 是加速度。

因此,根据这个公式和上面的公式,我们可以推导出:

m*a = -k*x

这相当于

a = -k*x / m

加速度 = 负的 弹性系数 * 偏移量 / 质量

我们现在得到一个方程,可以根据弹簧的偏移量和附着在弹簧上的物体的质量来计算出加速度。而通过加速度我们可以计算出以下状态:

  • 物体在任何时间的速度
  • 物体在任何时间的位置

要获得物体的速度,你需要将加速度与先前记录的速度相加,可以转化为以下等式:

v2 = v1 + a*t

速度 = 原速度 + 加速度 * 时间

最后,我们可以计算出位置,因为它遵循类似的规律:物体的位置等于原位置加上速度:

p2 =  p1 + v*t

位置 = 原位置 + 速度 * 时间

相比时间间隔,作为前端开发人员,我们更加熟知的是 帧速率“每秒帧数”。考虑到 Framer Motion 动画的流畅度,我们假设它的弹簧动画以每秒 60 帧的速度运行,因此时间间隔是恒定的,等于 1/600.01666

将数学表达式转换为 JavaScript 代码

现在我们已经完成了数学运算,你可以看到,通过了解物体的质量、弹簧的刚度和位移,我们可以知道在任何给定时间,附着在弹簧上的物体的位置,即任何给定的框架。我们可以在 Javascript 中翻译上面的所有方程,并且对于给定的位移计算对象的所有位置 600 帧,即 10 秒:

const loop = (stiffness, mass) => {
  /* Spring Length, set to 1 for simplicity */
  let springLength = 1;

  /* Object position and velocity. */
  let x = 2;
  let v = 0;

  /* Spring stiffness, in kg / s^2 */
  let k = -stiffness;

  /* Framerate: we want 60 fps hence the framerate here is at 1/60 */
  let frameRate = 1 / 60;

  /* Initiate the array of position and the current framerate i to 0 */
  let positions = [];
  let i = 0;

  /* We loop 600 times, i.e. for 600 frames which is equivalent to 10s*/
  while (i < 600) {
    let Fspring = k * (x - springLength);

    let a = Fspring / mass;
    v += a * frameRate;
    x += v * frameRate;

    i++;

    positions.push({
      position: x,
      frame: i,
    });
  }

  /**
   * positions is an array of number where each number
   * represents the position of the object in a spring
   * motion at a specific frame
   *
   * We use this array to plot all the position of the
   * object for 10 seconds.
   */
  return positions;
};

考虑阻尼(damping)

阻尼是指通过耗散能量使振荡减慢并最终停止的力

它的公式是:

Fd = -d * v

其中 d 是阻尼比,v 是速度

阻尼 = 负的 阻尼比 * 速度

考虑阻尼后会让我们在第一部分建立的加速度公式带来一些变化。我们知道

F = m*a

但这里的 F 是弹力加上阻尼力,而不仅仅是弹力,因此:

Fs + Fd = m*a -> a = (Fs + Fd)/m

我们现在可以将这个新公式添加到我们在上一部分中展示的 Javascript 代码中(我强调了与之前的实现相比我对代码所做的添加):

const loop = (stiffness, mass, damping) => {
  /* Spring Length, set to 1 for simplicity */
  let springLength = 1;

  /* Object position and velocity. */
  let x = 2;
  let v = 0;

  /* Spring stiffness, in kg / s^2 */
  let k = -stiffness;

  /* Damping constant, in kg / s */
  let d = -damping;

  /* Framerate: we want 60 fps hence the framerate here is at 1/60 */
  let frameRate = 1 / 60;

  let positions = [];
  let i = 0;

  /* We loop 600 times, i.e. for 600 frames which is equivalent to 10s*/
  while (i < 600) {
    let Fspring = k * (x - springLength);
    let Fdamping = d * v;

    let a = (Fspring + Fdamping) / mass;
    v += a * frameRate;
    x += v * frameRate;

    i++;

    positions.push({
      position: x,
      frame: i,
    });
  }

  return positions;
};

一个实际例子

Go 语言反射

Go 反射的三大法则

  • 反射世界的入口:经由接口(interface{})类型变量值进入反射的世界并获得对应的反射对象(reflect.Value 或 reflect.Type)。
  • 反射世界的出口:反射对象(reflect.Value)通过化身为一个接口(interface{})类型变量值的形式走出反射世界。
  • 修改反射对象的前提:反射对象对应的 reflect.Value 必须是可设置的(Settable)。

Newline

阅读各类词法解析器的代码时,经常看到对 \r\n\u2028\u2029\u0085 这些符号的处理。\r\n 还算熟悉,表示换行,在 mac 中用 \n,在 windows 中用 \r\n。说来惭愧,不仅对于其余3个控制符毫不了解,而且细思 mac 和 windows 中换行符为何不同,也是完全不清楚。通过搜索,在维基百科中发现有对换行符有详尽的描述,故以此文记录。

原文:https://en.wikipedia.org/wiki/Newline

Newline(通常称为 line endingend of line(EOL)line feedline break)是字符编码规范(例如 ASCII 或 EBCDIC)中的控制符或控制符序列,用于表示一行文本的结尾和一个新的开头。 一些文本编辑器在按 ↵Enter键 时会设置此特殊字符。

显示(或打印)文本文件时,此控制符使文本编辑器在新行中显示随后的文本。

历史

在19世纪中期,远在电传打印机和电传打字机出现之前,摩尔斯电码操作员或电报员就发明了摩尔斯电码,并使用摩尔斯电码符号对书面文本信息中的空白文本进行编码。发送两个连续的摩尔斯电码“A”,被用来在文本信息表示换行

后来,在现代电传打字机时代,开发了标准化的字符集控制码来进行空白文本格式化。ASCII 由国际标准化组织(ISO)和美国标准协会(ASA)共同开发,后者是美国国家标准协会(ANSI)的前身。在1963年至1968年期间,ISO 草案标准仅支持将 CR+LF 或 LF 作为换行符,而 ASA 草案仅支持 CR+LF。

CR+LF 通常在许多采用电传打字机(通常是 Teletype 33 ASR)作为终端的早期计算机系统上使用,因为需要该序列才能将打印头移动到下一行行首。 将换行分为两个功能字符来解决打印头不能及时从最右边返回到下一行行首来打印下一个字符的问题。 在 CR 之后打印的字符,通常会在打印头移动至行首的过程中,被打印成页面中的污点。“解决方案是将换行符分为两个字符:CR 是把行移到行首,LF 是把纸上移。”实际上,通常需要发送额外的字符(额外的 CR 或 NUL),这些字符会被忽略,但为打印头移动到左边空白处留出时间。

在此类系统上,能在应用层隐藏此类硬件详细信息的设备驱动程序尚未被开发出来,应用程序必须直接与电传打字机通信并遵循其约定。CR+LF 的组合是为了满足电传打字机的需要。DEC 的大多数微型计算机系统都使用此约定。CP/M 还使用它来在微型计算机使用的相同终端上进行打印。从那里开始,MS-DOS(1981)为了兼容而采用了 CP/M 的 CR+LF,并且该约定被 Microsoft 的更高版本的 Windows 操作系统继承。

Multics 操作系统于1964年开始开发,仅使用 LF 作为其换行符。Multics 使用设备驱动程序将此字符转换为打印机所需的任何顺序(包括额外的填充字符),并且单个字符更易于编程。没有使用似乎更明显的字符 CR,因为 CR 提供了有用的功能,即在一行上叠印一行以产生黑体和删除线效果。 也许更重要的是,单独使用 LF 作为行终止符已被纳入最终的 ISO/IEC 646 标准草案中。 Unix 遵循 Multics 惯例,后来类 Unix 的系统遵循 Unix。 这在 Windows 和类似 Unix 的操作系统之间造成了冲突,因此,一个操作系统上组成的文件不能由另一操作系统正确格式化或解释(例如,用 Windows 文本编辑器(如记事本)编写的 UNIX Shell 脚本)。

表示法

回车(CR)和换行(LF)的概念是紧密相关的,可以单独考虑,也可以一起考虑。在打字机和打印机这样的物理介质中,需要“下移”和“横移”两个动作来在页面上创建一个新行。虽然机器(打字机或打印机)的设计必须分别考虑它们,但软件的抽象可以将它们组合在一起作为一个事件。这就是为什么可以将字符编码中的换行符定义可以将 CR 和 LF 合并成一个(通常称为 CR+LF 或 CRLF)。

Unicode

Unicode 标准定义了一些符合标准的应用程序应该识别为行终止符的字符:

- - - -
LF Line Feed U+000A 换行符
VT Vertical Tab U+000B 纵向制表符
FF Form Feed U+000C 换页符
CR Carriage Return U+000D 回车符
CR+LF Carriage Return U+000D 回车符
NEL Next Line U+0085 下一行
LS Line Separator Line Separator 行分隔符
PS Paragraph Separator U+2029 段落分隔符

与将所有行终止符转换为单个字符(如 LF)的方式相比,这似乎过于复杂。 但是,Unicode 旨在在将文本文件从任何现有编码转换为 Unicode 并转换回 Unicode 时保留所有信息。 因此,Unicode 应该包含现有编码中包含的字符。 NL 以 0x15 的代码包含在 EBCDIC 中,并且通常映射到 NEL,这是 C1 控制集中的控制字符。因此,它由 ECMA 48 定义,并由符合 ISO/IEC 2022(等效于ECMA 35)的编码识别。C1 控制集也与 ISO-8859-1 兼容。Unicode 标准中采用的方法允许双向转换保留信息,同时仍使应用程序能够识别所有可能的行终止符。

通常不需要识别和使用大于 0x7F 的换行代码(NEL、LS 和 PS)。它们是 UTF-8 格式的多个字节,NEL 字符在 Windows-1252 中被用作省略号(…)字符。例如:

  • ECMAScript 将 LS 和 PS 作为换行,但将 U+0085 (NEL) 视为空白符而非换行。
  • Windows 10 在其默认的文本编辑器 Notepad 中不将任何 NEL、LS 或 PS 作为换行。
  • gedit 是 GNOME 桌面环境的默认文本编辑器,它将 LS 和 PS 视为换行符,但 NEL 不是。
  • JSON 允许字符串中包含 LS 和 PS 字符,但 ES2019 之前的 ECMAScript 将它们作为换行符处理,因此是非法语法。
  • 为了与 JSON 兼容,YAML 不再像1.2版本那样识别它们。

转义符

转义符是组合的字符,它不被作为文本显示,而是被程序拦截,并被假定执行一个特殊的函数。转义符通常用于处理(集合、搜索、替换等)特殊字符。

Special character Escape sequence
line feed \n
carriage return \r
tabulator \t

编程语言

为了便于创建可移植程序,编程语言提供了一些抽象来处理不同环境中使用不同换行序列。C语言提供了转义序列 '\n'(换行符)'\r'(回车符)。但是,这些字符不需要等同于 ASCII LF 和 CR 控制字符。C 标准只保证了两件事:

  1. 每个转义序列都映射到一个唯一的数字,该数字可以存储在单个 char 值中。
  2. 在文本模式下写入文件、设备节点或套接字 /fifo 时,'\n' 透明地转换为系统使用的本机换行序列,该序列可能长于一个字符。 在文本模式下阅读时,本机换行符序列会转换回 '\n'。 在二进制模式下,不执行任何转换,并且直接输出由 '\n' 产生的内部表示。

Java、PHP 和 Python 提供 '\r\n' 序列(用于 ASCII CR+LF),与 C 相反,它们保证它们的值分别为 U+000D 和 U+000A。

Java I/O 库不会在输入或输出时透明地将这些转换为与平台特定的换行序列。相反,它们提供了用于写入自动添加本机换行符序列的的函数,以及用于读取接受 CR、LF 或 CR+LF 中任何一个作为行终止符的行的函数(请参见 BufferedReader.readLineFile())。 可以用 System.lineSeparator() 方法得到基础行分隔符。

Python 允许在读取文件、导入模块和执行文件时提供“通用换行支持”。

例子:

String eol = System.lineSeparator();
String lineColor = "Color: Red" + eol;

换行序列不同带来的问题

不同的换行约定会导致不同类型的系统之间传输的文本文件显示不正确。

开启 CSS 硬件加速时,fixed 定位元素不再相对于视口进行定位的原因

问题

当给一个元素使用 transform、will-change 等属性开启 CSS 硬件加速时,会导致 fixed 定位元素不再相对于视口(viewport)进行定位。

原因

fixed 定位的元素相对于 fixed 定位包含块(fixed positioning containing block,通常是视口)进行定位和调整大小。

元素的包含块(containing block)由距离最近的创建了 fixed 定位包含块的祖先元素建立:

  • 如果祖先不是行级元素
    包含块由祖先的 padding 边界形成。
  • 如果祖先是行级元素
    包含块由祖先的第一个 box fragment 的 block-start 和 inline-start 内容边界,和最后一个 box fragment 的 block-end 和 inline-end 内容边界决定。

如果没有祖先元素创建,那么 fixed 定位包含块为视口,因此,在滚动文档时,fixed 定位的元素不会移动。

可以使元素创建 fixed 定位包含块的属性有 transform、will-change、contain 等。

参考

如何 revert 多个 git commit?

原文:https://stackoverflow.com/questions/1463340/how-to-revert-multiple-git-commits

问题

拥有一个 git 仓库如下:

A <- B <- C <- D <- HEAD

想要分支的 HEAD 指向 A,也就是说想要移除 B、C、D 和 HEAD 。

看起来可以使用 revert,但如何 revert 多个 commit? 还是一次只能 revert 一个? 顺序是否重要?

解决方案

一般的规则是,不应该重写已经发布的历史,因为可能已经有人基于它来进行了他们的工作。如果重写历史,将在合并他们的更改时产生问题。

所以解决方案是创建一个新的 commit 来移除。

使用 git revert 命令

git revert --no-commit D
git revert --no-commit C
git revert --no-commit B
git commit -m "the commit message for all of them"

git revert --no-commit B..HEAD  
git commit -m 'message'

git revert --no-commit HEAD~3..

.. 用于创建范围,HEAD~3..HEAD~3..HEAD 是一样的。

使用 git checkout 命令

该方法添加的文件不会被删除。

git checkout -f A -- . # checkout that revision over the top of local files
git commit -a

使用 git reset 命令

git reset --hard A
git reset --soft D # (or ORIG_HEAD or @{1} [previous location of HEAD]), all of which are D
git commit

编译 Python 为 WebAssembly

WebAssembly 与asm.js 的区别

Why create a new standard when there is already asm.js?
What is the difference between asm.js and web assembly?
Why WebAssembly is Faster Than asm.js

概括一下 WebAssembly:

  • 是具有静态类型的 AST 二进制格式,可以被现有 JavaScript 引擎执行,
  • 它比 JavaScript 更紧凑10-20%(gzip 后比较),解析速度快一个数量级,
  • 它可以用于 JavaScript 语法不擅长的低级操作,例如64位整数、特殊 CPU 指令、SIMD 等,
  • 在某种程度上可以与 asm.js 相互转换。

因此,目前 WebAssembly 是对 asm.js 的迭代,目标仅为 C/C++ 等类似语言。

在 Web 中执行 Python

Python 代码编译为 WebAssembly/asm.js 的唯一阻碍不只是 GC。WebAssembly/asm.js 都是低级的静态类型代码,无法完全表达 Python 代码。WebAssembly/asm.js 当前的工具链基于 LLVM,可以编译为 LLVM IR 的语言就可以转换为 WebAssembly/asm.js。但遗憾的是,Python 太过动态,无法融入其中,Unladen SwallowPyPy 的几次尝试都证明了这一点。

asm.js 的演示文稿中有关于动态语言状态的幻灯片。这意味着目前只能将整个 VM(C/C++ 的实现)编译到 WebAssembly/asm。js和解释(如果可能,使用JIT)原始源。对于Python,有几个现有项目:

解决方案

目前,如果您正在构建一个传统的网站或web应用程序,其中下载数兆字节的JS文件几乎不是一个选项,那么看看Python-to-JavaScript转换器(例如Transcrypt)或JavaScript Python实现(例如Brython)。或者与编译为JavaScript的语言列表中的其他语言一起试试运气。

否则,如果下载大小不是问题,并且您已经准备好处理许多粗糙的边缘,请在以上三个选项中选择一个。

参考

https://stackoverflow.com/questions/44761748/compiling-python-to-webassembly

TypeScript 继承内置函数(如Error、Array和Map)可能无法工作

TypeScript 编译

class FooError extends Error {
  constructor(m: string) {
    super(m);
  }
  sayHello() {
    return 'hello ' + this.message;
  }
}
var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
        return extendStatics(d, b);
    }
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var FooError = /** @class */ (function (_super) {
    __extends(FooError, _super);
    function FooError(m) {
        return _super.call(this, m) || this;
    }
    FooError.prototype.sayHello = function () {
        return 'hello ' + this.message;
    };
    return FooError;
}(Error));

ECMAScript Function 对象

ECMAScript 函数对象将参数化的 ECMAScript 代码封装在词法环境中,并支持对代码进行动态执行。ECMAScript 函数对象是普通对象,具有与其他普通对象相同的内部插槽和内部方法。ECMAScript 函数对象的代码可以是严格模式代码或非严格模式代码。代码为严格模式代码的 ECMAScript 函数对象称为严格函数。非严格模式代码的函数称为非严格函数。

所有 ECMAScript 函数对象都有这里定义的 [[Call]] 内部方法。同时也是构造函数的 ECMAScript 函数还有 [[Construct]] 内部方法。

[[Construct]]

ECMAScript 函数对象 F 的内部方法 [[Construct]] 接收参数 argumentsList(一个 ECMAScript 语言类型的 List)和 newTarget(一个构造函数)。它在被调用时执行以下步骤:

  1. Assert: F 是 ECMAScript 函数对象。
  2. Assert: Type(newTarget) 是对象。
  3. 让 callerContext 为正在运行的执行上下文。
  4. 让 kind 为 F.[[ConstructorKind]]。
  5. 如果 kind 为 base,则
    1. 让 thisArgument 为 ? OrdinaryCreateFromConstructor(newTarget, "%Object.prototype%")。
  6. 让 calleeContext 为 PrepareForOrdinaryCall(F, newTarget)。
  7. Assert: calleeContext 当前为正在运行的执行上下文。
  8. 如果 kind 为 base,执行 OrdinaryCallBindThis(F, calleeContext, thisArgument)。
  9. 让 constructorEnv 为 calleeContext 的词法环境。
  10. 让 result 为 OrdinaryCallEvaluateBody(F, argumentsList)。
  11. 从执行上下文栈中移除 calleeContext,并将 callerContext 恢复为正在运行的执行上下文。
  12. 如果 result.[[Type]] 为 return,则
    1. 如果 Type(result.[[Value]]) 为 Object,返回 NormalCompletion(result.[[Value]])。
    2. 如果 kind 为 base,返回 NormalCompletion(thisArgument)。
  13. 如果 result.[[Value]] 不是 undefined,抛出 TypeError 异常。
  14. 否则,ReturnIfAbrupt(result)。
  15. 然会 ? constructorEnv.GetThisBinding()。

运行时语义:ClassDefinitionEvaluation

  1. 让 env 为正在运行的执行上下文的词法环境。
  2. 让 classScope 为 NewDeclarativeEnvironment(env)。
  3. 如果 classBinding 不是 undefined
  4. 如果 ClassHeritage 不存在
  5. 否则
    1. 设置正在运行的执行上下文的词法环境为 classScope。
    2. 让 superclassRef 为 ClassHeritage 的解析结果。
    3. 设置正在运行的执行上下文的词法环境为 env。
    4. 如果 superclass 为 null,则
      1. 让 protoParent 为 null。
      2. 让 constructorParent 为 %Function.prototype%。
    5. 否则如果 IsConstructor(superclass) 为 false,抛出 TypeError 异常。
    6. 否则
      1. 让 protoParent 为 ? Get(superclass, "prototype")。
      2. 如果 Type(protoParent) 既不是 Object 也不是 Null,抛出 TypeError 异常。
      3. 让 constructorParent 为 superclass。
  6. 让 proto 为 ! OrdinaryObjectCreate(protoParent)。
  7. 如果 ClassBody 不存在,让 constructor 为空。
  8. 否则,让 constructor 为 ClassBody 的 ContructorMethod。
  9. 如果 contructor 为空,则

SuperCall : super Arguments

  1. 让 newTarget 为 GetNewTarget()。
  2. Assert: Type(newTarget) 为 Object。
  3. 让 func 为 ! GetSuperConstructor()。
  4. 让 argList 为 Arguments 的 ? ArgumentListEvaluation。
  5. 如果 IsConstructor(func) 为 false,抛出 TypeError 异常。
  6. 让 result 为 ? Construct(func, argList, newTarget)。
  7. 让 thisER 为 GetThisEnvironment()。
  8. 返回 ? thisER.BindThisValue(result)。

Object 对象

Object 构造函数

  • 是 %Object%
  • 是全局对象 Object 属性的初始值。
  • 当作为一个构造函数调用时,创建一个普通对象。
  • 当作为函数而非构造函数调用时执行类型转换。

Function.prototype.apply ( thisArg, argArray )

当使用 thisArg 和 argArray 参数调用 apply 方法时,将执行以下步骤:

  1. 让 func 为 this。
  2. 如果 IsCallable(func) 为 false,抛出 TypeError 异常。
  3. 如果 argArray 为 undefined 或 null,则
    a. 执行 PrepareForTailCall()。
    b. 返回 ? Call(func, thisArg)。
  4. 让 argArray 为 ? CreateListFromArrayLike(argArray)。
  5. 执行 PrepareForTailCall()。
  6. 返回 ? Call(func, thisArg, argList)。

Error 构造函数

  • 是 %Error%。
  • 是全局对象 Error 属性的初始值。
  • 当作为一个函数而非构造函数,创建并初始化一个新的 Error 对象。因此,具有相同参数的Error() 等价于 new Error()。
  • 被设计为可衍生的。它可以用作类定义的 extends 子句的值。想要继承指定错误行为的子类构造函数必须包含对Error构造函数的超调用,以创建并初始化带有[[ErrorData]]内部槽的子类实例。

Error ( message )

当使用 message 参数调用 Error 函数时,将采取以下步骤:

  1. 如果 NewTarget 为 undefined,让 newTarget 为 active function object;否则让 newTarget 为 NewTarget。
  2. 让 O 为 ? OrdinaryCreateFromConstructor(newTarget, "%Error.prototype%", « [[ErrorData]] »)。
  3. 如果 message 不为 undefined,则
    a. 让 msg 为 ? ToString(message)。
    b. 让 msgDesc 为 PropertyDescriptor { [[Value]]: msg, [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }。
    c. 执行 ! DefinePropertyOrThrow(O, "message", msgDesc)。
  4. 返回 O。

Call ( F, V [ , argumentsList ] )

抽象操作 Call 接受参数 F(一个 ECMAScript 语言值)和 V(一个ECMAScript语言值),以及可选参数 argumentsList(一个ECMAScript语言值列表)。它用于调用函数对象的 [[call]] 内部方法。F 是函数对象,V 是 ECMAScript 语言值,就是 [[Call]] 的 this 值,argumentsList 是传递给内部方法对应参数的值。如果 argumentsList 不存在,则使用一个新的空列表作为它的值。它在被调用时执行以下步骤:

  1. 如果 argumentsList 不存在, 将 argumentsList 设置为一个新的空 List。
  2. 如果 IsCallable(F) 是 false,抛出 TypeError 异常。
  3. 返回 ? F.[[Call]](V, argumentsList)。

Go 语言类型嵌入与方法集合

Go 的设计哲学之一是偏好组合,Go 支持用组合的**来实现一些面向对象领域经典的机制,比如继承。而具体的方式就是利用类型嵌入(type embedding)。

  1. 在接口类型中嵌入接口类型

  2. 在结构体类型中嵌入接口类型

  3. 在结构体类型中嵌入结构体类型

react-reconciler

原文:https://github.com/facebook/react/tree/master/packages/react-reconciler

这是一个用于创建自定义 React 渲染器的实验库。

它的 API 不像 React、React Native 或 React DOM 那样稳定,也不遵循一般的版本控制方案。

使用时请自行承担风险。

使用

const Reconciler = require('react-reconciler');

const HostConfig = {
  // 您在这里需要实现一些方法。
  // 以下有更多信息和示例。
};

const MyRenderer = Reconciler(HostConfig);

const RendererPublicAPI = {
  render(element, container, callback) {
    // 调用 MyRenderer.updateContainer() 来调度根节点上的更改。
    // 请参阅 ReactDOM、React Native 或 React ART 实际案例。
  }
};

实际案例

『host config』是您需要提供的对象,它描述了如何在宿主环境(例如 DOM、canvas、控制台或任何您的渲染目标)中处理一些事情。看起来是这样的:

const HostConfig = {
  createInstance(type, props) {
    // 例如,DOM 渲染器返回一个 DOM 节点
  },
  // ...
  supportsMutation: true, // it works by mutating nodes
  appendChild(parent, child) {
    // 例如,DOM 渲染器将调用  .appendChild() 方法
  },
  // ...
};

要了解如何编写一个非常简单的自定义渲染器,请阅读以下文章:

完整的支持方法列表可以在这里找到。对于它们的功能,建议查看下面的具体示例。

React 库包含几个渲染器。它们都有自己的宿主配置。

React 库中的示例与第三方渲染器的声明稍有不同。特别是,上面提到的 HostConfig 对象从来没有显式声明过,而是代码中的一个模块。但是,它的导出对应于需要在代码中声明的 HostConfig 对象上的属性。

参考(不完整! )

目前,我们不能保证记录每一个 API 细节,因为宿主配置在不同版本之间仍然经常变化。下面的文档是本着尽最大努力的精神提供的,而不是一个 API 保证。它关注的是那些不经常改变的部分。这是在快速开发 React 本身的需求和这个库对自定义渲染器社区的有用性之间取得平衡的妥协。如果您注意到某个部分是过时的或与最新稳定版本的行为不匹配,请提交一个 issue 或 pull request,尽管响应可能需要一些时间。

模式

调解器有两种模式:可变模式(mutation mode)和不可变模式(persistent mode)。您必须指定其中一个。

如果您的目标平台类似于 DOM,并且具有类似于 appendChild、removeChild 等方法,那么应该使用可变模式。这与 React DOM、React ART 和传统 React Native 渲染器使用的模式相同。

const HostConfig = {
  // ...
  supportsMutation: true,
  // ...
}

如果目标平台具有不可变树,则需要使用不可变模式。在这种模式下,现有的节点为不可变的,而是每次更改都克隆父树,然后在根替换整个父树。这是新的 React Native 渲染器使用的节点,代号为『Fabric』。

const HostConfig = {
  // ...
  supportsPersistence: true,
  // ...
}

根据模式的不同,协调器将调用 host config 上的不同方法。如果您不确定您需要哪一个,您可能需要可变模式。

核心方法

createInstance(type, props, rootContainer, hostContext, internalHandle)

这个方法应该返回一个新创建的节点。例如,DOM 渲染器会在这里调用 document.createElement(type),然后从 props 设置属性。

createTextInstance(text, rootContainer, hostContext, internalHandle)

createInstance 相同,但是用于文本节点。如果您的渲染器不支持文本节点,您可以在这里抛出异常。

appendInitialChild(parentInstance, child)

这个方法应该改变父实例,并将子对象添加到它的子对象列表中。例如,在 DOM 中,这将调用 parentInstance.appendChild(child) 方法。

这个方法发生在渲染阶段。它可以修改父实例和子对象,但不能修改任何其他节点。它是在树还在构建时调用的,并没有渲染到屏幕上的实际树。

finalizeInitialChildren(instance, type, props, rootContainer, hostContext)

prepareUpdate(instance, type, oldProps, newProps, rootContainer, hostContext)

shouldSetTextContent(type, props)

getRootHostContext(rootContainer)

此方法允许您从树的根返回初始宿主上下文。有关宿主上下文的解释,请参见 getChildHostContext

如果不打算使用宿主上下文,可以返回 null。

这个方法发生在渲染阶段。不要在此修改树。

getChildHostContext(parentHostContext, type, rootContainer)

宿主上下文允许您追踪一些您在树中的位置信息,所以 hostContextcreateInstance 中作为可选参数。例如,DOM 渲染器使用它来跟踪它是在 HTML 树还是 SVG 树中,因为 createInstance 的实现在它们之间有所不同。

如果希望某个类型的节点不影响传递的上下文,则可以返回 parentHostContext。或者,您可以返回任何自定义对象,用于您希望传递的信息。

如果您不想在此做任何事情,返回 parentHostContext

这个方法发生在渲染阶段。不要在此修改树。

getPublicInstance(instance)

确定哪个对象被公开为 ref。您可能希望返回实例本身。但在某些情况下,只暴露一部分可能是有意义的。如果你不想在这里做任何事情,返回 instance。

prepareForCommit(containerInfo)

resetAfterCommit(containerInfo)

preparePortalMount(containerInfo)

now()

您可以将其代理给 performance.now 或您环境中等价的对象。

scheduleTimeout(fn, delay)

您可以将其代理给 setTimeout 或您环境中等价的对象。

cancelTimeout(id)

您可以将其代理给 clearTimeout 或您环境中等价的对象。

hideInstance(instance)

这个方法应该使 实例 隐藏,而不需要将其从树中移除。例如,它可以应用视觉样式来隐藏它。当 fallback 被展现时,它被 Suspense 用来隐藏树。

unhideInstance(instance, props)

这个方法应该使 实例 可见,撤销 hideInstance 所做的事情。

不可变方法

水合方法

Unicode 字符编码模型

原文:http://www.unicode.org/reports/tr17/

概要

本文档阐明了一些用于描述字符编码的术语,以及 Unicode 的不同形式适用于哪些地方。它将互联网体系架构委员会(Internet Architecture Board,IAB)的三层文本流细化为四层结构。

状态

此文档已被 Unicode 成员和其他相关方审阅过,并已被 Unicode 联盟批准出版。这是一份稳定的文档,可以用作参考资料或被其他规范引用为规范性参考资料。

内容

  1. 字符编码模型
  2. 抽象字符表(Abstract Character Repertoire)
    1. 版本控制
    2. 字符与字形
    3. 兼容字符
    4. 子集
  3. 编码字符集(Coded Character Set,CCS)
  4. 字符编码方式(Character Encoding Form,CEF)
  5. 字符编码方案(Character Encoding Scheme,CES)
    1. 字节顺序
  6. Character Maps
  7. Transfer Encoding Syntax
  8. Data Types and API Binding
  9. Definitions and Acronyms
    1. References
    2. Acknowledgements
    3. Modifications

1 字符编码模型

Unicode 字符编码模型的四个层级可以概括为:

  • ACR:抽象字符表(Abstract Character Repertoire)
    要编码的字符的集合,例如某些字母或符号
  • CCS:编号字符集(Coded Character Set)
    从抽象字符集到非负整数集的映射
  • CEF:字符编码方式(Character Encoding Form)
    从 CCS 的非负整数集到定宽的特定码元序列集(如 32 位整数)的映射
  • CES:字符编码方案(Character Encoding Scheme)
    码元序列集之间的可逆转换(从一个或多个 CEF 到一个序列化的字节序列)

除了这四个独立的层级之外,还有两个实用的概念:

  • CM:字符映射表(Character Map)
  • TES:传输编码语法(Transfer Encoding Syntax)

IAB 模型,被定义在 [RFC 2130] 中,分为三个层级:编码字符集(Coded Character Set, CCS),字符编码方案(Character Encoding Scheme,CES)和传输编码语法(Transfer Encoding Syntax,TES)。但是,为了充分应对 Unicode 字符编码模型的需要,定义了四个层级。其中,抽象字符表被隐式地定义在 IAB 模型中。Unicode 模型将 TES 独立于模型之外,同时在 CCS 和 CES 之间添加了一个额外的层级。

下面的部分给出了四个层级的定义、解释和示例,以及字符映射和传输编码语法。

2 抽象字符表

抽象字符表被定义为要编码的无序的抽象的字符的集合。抽象 的意思是这些对象是按照约定定义的。

字符表由两种类型: 封闭的(fixed)开放的(opened) 。在大多数字符编码中,封闭的字符表通常很小。字符表一旦确立,就永不变更。向指定的字符表添加一个新的抽象字符将创建一个新的字符表,然后给字符一个编号,构成一个新的对象。对于 Unicode 标准来说,字符表天生就是开放的。因为 Unicode 是通用编码,任何可被编码的抽象字符都是字符表的潜在成员,无论该字符当前是否已知。

字符表的例子:

  • 日文字音和日文表意文字的 JIS X 0208 (CS 01058) [封闭的]
  • 西欧字符和 Latin-1 (CS 00697) [封闭的]
  • POSIX 可移植字符表 [封闭的]
  • IBM 日语字符表 [封闭的]
  • Windows 西欧字符表 [开放的]
  • Unicode/10646 字符表 [开放的]

2.1 版本控制

Unicode 标准通过发布主要版本和次要版本对字符表进行版本控制:1.0、1.1、2.0、2.1、3.0 等。每个版本的字符表由包含在该版本中的抽象字符枚举所定义。

Unicode 标准的字符表现在是严格按增量扩充的,尽管由于[Unicode]和[10646]的合并,在最早的版本(1.0 和 1.1)中没有完全保证增量扩充,影响了向后兼容性。从 2.0 版开始,Unicode 字符编码稳定性策略[Stability]保证了不会从字符表中移除任何字符。

注意:Unicode 字符编码稳定性策略也以其他形式限制对标准的变更。例如,许多字符属性受到一致性的约束,一些属性一旦被分配就不能变更。对规范化稳定性的保证防止了变更会破坏现有编码字符的映射,也限制了未来的版本中可以被添加到字符表中的字符类型。

2.2 字符与字形

字符表的元素是抽象字符。字符不同于字形,字形是代表一个字符或字符一部分的特殊图像。相同字符的字形可能有非常不同的形状,如下图所示的字母 a。

字符 字形
image image image image image image image

字形并不总是对应一个字符。例如,序列“f”后跟“ i”可以显示为单个字形,称为“连字”。请注意,字形已合并在一起,点从“ i”中消失,如下图所示。

字符序列 字形
image image !image

另一方面,可以通过两个字形序列来实现 fi,如下图中所示的假想示例。是使用单个字形还是使用两个字形的序列取决于字体和渲染软件。

字符序列 字形
image image image image

2.4 子集

与大多数字符表不同,Unicode 覆盖范围是所有字符。考虑到编写系统时的复杂性,在实际中这意味着几乎所有的实现都只是支持字符表的某个子集,而非所有字符。

Unicode 标准没有预设子集,每个实现都需要定义并支持它希望解析的字符表的子集。

3 编号字符集

编码字符集被定义为一个从抽象字符集到非负整数集的映射。整数范围不必是连续的。在 Unicode 标准中,Unicode 标量值显式定义了这样一个不连续的整数范围。

如果编码字符集将一个抽象字符映射一个整数,则视为它被定义在了一个编码字符集中。该整数是指定抽象字符的 码点(code point ) 。然后,这个抽象字符就是一个编码字符。

编码字符集是 ISO 和字符编码委员会产出的基本对象。它们将已定义的字符表与非负整数关联起来,然后可以明确地使用非负整数来引用字符表中的特定抽象字符。

编码字符集也可以称为字符编码(character encoding)、编码字符表(coded character repertoire)、字符集定义(character set definition)或代码页(code page)。

3.1 字符命名

负责字符编码的 JTC1 小组委员会 SC2 要求为其编码字符集中的每个抽象字符分配唯一的字符名。供应商编码的字符集或 SC2 以外的标准委员会产出的编码通常不遵循这种做法,在这些编码中,为字符提供的名称通常都是变量和注释,而不是字符编码的标准部分。

3.2 代码空间

被用于映射抽象字符的非负整数范围定义代码空间的相关概念。代码空间类型的传统边界与编码方式密切相关(见下文),因为抽象字符到非负整数的映射是在特定的编码方式下完成的。有效代码空间的例子有 0..7F、0..FF、0..FFFF、0..10FFFF、0..7FFFFFFF、0..FFFFFFFF。

4 字符编码方式

字符编码方式是从 CCS 中的整数集到 码元(code unit ) 序列集的映射。码元是一个整数,表示在计算机体系结构中占用特定二进制的宽度,例如 8 位字节。编码方式将字符表示为计算机中的实际数据。码元序列不一定具有相同的长度。

  • 序列长度都相同的字符编码方式称为固定宽度。
  • 序列长度不同的字符编码方式称为可变宽度。

编码字符集的字符编码方式被定义为映射该编码字符集中所有编码字符的字符编码方式。

注意:在许多情况下,对于给定的编码字符集只有一种字符编码方式。在某些情况下,只指定了字符编码方式。依赖码元序列和整数之间的隐式关系,隐式定义了编码字符集。

解析一个字符序列时,由三种可能性:

  1. 序列格式错误(ill-formed)。
    序列不完整,或者无法匹配编码方式规范。例如
    • 在 CP950 中 0xA3 不完整。若其后面没有跟随另一个格式正确的字节,那么格式错误。
    • 在UTF-16 中 0xD800 不完整。若其后面没有跟随另一个格式正确的 16 位值,那么格式错误。
    • 在 UTF-8 中 0xC0 格式错误。它不是一个格式正确的 UTF-8 序列的初始字节。
  2. 序列表示一个有效码点,但是没有被分配。这个序列可能在将来的某个字符编码的版本中被分配。
    • 在1999年之前,在 CP950 中 0xA3 0xBF 未被分配。
    • 在 Unicode 5.0 中 0x0EDE 未被分配。
  3. 源序列被分配:它表示一个有效的编码字符。它有三种类型:
    第一种类型,是一个普通的被分配的字符,例如
    • 在 Unicode 5.0 中 0x0EDD 被分配
      第二种类型,是一个需用户自行定义的(user-defined)字符,例如
    • 0xE000 是一个需要用户进行定义的字符,它的语义解释由标准以外的各方达成一致。
      第三种类型,是 Unicode 标准特有的:非字符(noncharacter)。这是一种内部使用的需用户自行定义的字符,不公开使用。例如
    • 在 Unicode 5.0 种 0xFFFF 是一个被分配的非字符

CCS 的编码方式可以产生与抽象字符相关的固定宽度或可变宽度的码元序列。编码方式可能涉及 CCS 的整数到一组码元序列的任意可逆映射。

编码方式有多种类型。下面是一些有关编码方式的更重要的例子。

定宽编码方式的例子:

名字 字符被编码为 备注
7-bit a single 7-bit quantity example: ISO 646
8-bit G0/G1 a single 8-bit quantity with constraints on use of C0 and C1 spaces
8-bit a single 8-bit quantity with no constraints on use of C1 space
8-bit EBCDIC a single 8-bit quantity with the EBCDIC conventions rather than ASCII conventions
16-bit (UCS-2) a single 16-bit quantity within a code space of 0..FFFF
32-bit (UCS-4) a single 32-bit quantity within a code space 0..7FFFFFFF
32-bit (UTF-32) a single 32-bit quantity within a code space of 0..10FFFF
16-bit DBCS process code a single 16-bit quantity example: UNIX widechar implementations of Asian CCS's
32-bit DBCS process code a single 32-bit quantity example: UNIX widechar implementations of Asian CCS's
DBCS Host two 8-bit quantities following IBM host conventions

变宽编码方式的例子:

名字 字符被编码为 备注
UTF-8 Unicode 中1到4个8位码元 只被用于 Unicode
UTF-16 1到2个16位码元 只被用于 Unicode

编码方式定义了编码的一个基本问题:每个字符有多少码元。每个字符的码元数量对国际化软件很重要。在以前,这等同于每个字符需要多少字节表达。随着 Unicode 和 10646 引入 UCS-2、UTF-16、UCS-4 和 UTF-32,它泛化为两个概念:码元的规范宽度,和用于表示每个字符所需的码元数量。与 ISO/IEC 10646 相关联的 UCS-2 编码方式,仅能表示 BMP 中的字符,是一种定宽编码方式。相比之下,UTF-16 使用一个或两个码元,能够覆盖整个 Unicode 的代码空间。

UTF-8 是一个很好的例子。在 UTF-8 中,用于表示字符数据的基本码元是 8 位宽(即一个字节或八位)。UTF-8 的宽度映射为:

0x00..0x7F 1 byte
0x80..0x7FF 2 bytes
0x800..0xD7FF, 0xE000..0xFFFF 3 bytes
0x10000 .. 0x10FFFF 4 bytes

用于特定编码字符集的编码方式示例:

名字 编码方式
JIS X 0208 generally transformed from the kuten notation to a 16-bit “JIS code” encoding form, for example "nichi", 38 92 (kuten) → 0x467C JIS code
ISO 8859-1 has the 8-bit G0/G1 encoding form
CP 037 8-bit EBCDIC encoding form
CP 500 8-bit EBCDIC encoding form
US ASCII 7-bit encoding form
ISO 646 7-bit encoding form
Windows CP 1252 8-bit encoding form
Unicode 4.0, 5.0 UTF-16, UTF-8, or UTF-32 encoding form
Unicode 3.0 either UTF-16 (default) or UTF-8 encoding form
Unicode 1.1 either UCS-2 (default) or UTF-8 encoding form
ISO/IEC 10646:2003 depending on the declared implementation levels, may have UCS-2, UCS-4, UTF-16, or UTF-8.
ISO/IEC 10646:2009 UTF-8, UTF-16, or UTF-32

用于特定编码字符集的编码方式示例:

名字 编码方式
JIS X 0208 generally transformed from the kuten notation to a 16-bit “JIS code” encoding form, for example "nichi", 38 92 (kuten) → 0x467C JIS code
ISO 8859-1 has the 8-bit G0/G1 encoding form
CP 037 8-bit EBCDIC encoding form
CP 500 8-bit EBCDIC encoding form
US ASCII 7-bit encoding form
ISO 646 7-bit encoding form
Windows CP 1252 8-bit encoding form
Unicode 4.0, 5.0 UTF-16, UTF-8, or UTF-32 encoding form
Unicode 3.0 either UTF-16 (default) or UTF-8 encoding form
Unicode 1.1 either UCS-2 (default) or UTF-8 encoding form
ISO/IEC 10646:2003 depending on the declared implementation levels, may have UCS-2, UCS-4, UTF-16, or UTF-8
ISO/IEC 10646:2009 UTF-8, UTF-16, or UTF-32

5 字符编码方案

字符编码方案(CES)是从码元序列到字节序列的可逆转换,用以下三种方法中的一种:

  1. 简单 CES 按顺序将 CEF 的每个码元映射到唯一的序列化的字节序列。
  2. 复合 CES 使用两种或更多简单的 CES,加上它们之间的转换方法。
    复合 CES 的性质意味着可能有不同的字节序列对应于相同的码元序列。虽然这些序列不是唯一的,但是码元的原始序列可以从这些序列中毫不含糊地恢复。
  3. 压缩 CES 将码元序列映射到字节序列,同时最小化字节序列的长度。一些压缩 CES 被设计成为每个码元序列产生一个唯一的字节序列,以便可以比较压缩的字节序列是否相等或通过二进制比较进行排序。其他压缩 CES 仅仅是可逆的。

字符编码方案与跨平台持久化数据的问题相关,例如码元比字节宽,在这种情况下,可能需要进行字节交换以将数据匹配指定平台的字节顺序。特别是

  • 大多数定宽的面向字节的编码方式都有一个到 CES 的映射:每 7 位或每 8 位都映射到一个具有相同值的字节。
  • 大多数变宽的面向字节的编码方式也只是简单地将码元序列序列化为字节序列。
    • UTF-8 遵循这种方案,因为它是一种面向字节的编码方式。
    • UTF-16 必须为字节序列化指定字节顺序,因为它涉及 16 位的数量。字节顺序是 UTF-16BE 和 UTF-16LE 之间的唯一区别,在UTF-16BE 中,16 位值的两个字节按大端顺序序列化,而在 UTF-16LE 中,它们按小端顺序序列化。

重要的是不要混淆字符编码方式(CEF)和 CES。

  1. CEF 将码点映射到码元,而 CES 将码元序列转换为字节序列。
  2. CES 必须考虑 CEF 中所有比字节宽的码元的序列化字节顺序。

一些 Unicode 编码方案与三种 Unicode 编码方式的命名相同。在不加限定地使用时,术语 UTF-8、UTF-16 和 UTF-32 是作为Unicode 编码方式还是作为 Unicode 编码方案并不明确。这种不明确对 UTF-8 通常是无危险的,因为 UTF-8 编码方案很容易从 UTF-8 编码方式定义的字节序列派生出来。然而,对于 UTF-16 和 UTF-32,这种不明确就有问题了。作为编码方式,UTF-16 和 UTF-32 的码元通过 16 位或 32 位的数据类型访问内存;它没有相关的字节顺序,也没有使用 BOM。

作为编码方案,UTF-16 和 UTF-32 表示序列化字节,例如数据流或文件中的序列化字节;它们的字节顺序是两种中的一种,并且数据的首部可能有一个 BOM。当 UTF-16 或 UTF-32 的使用可能会被误解,并且它们作为 Unicode 编码方式或 Unicode 编码方案的使用之间的区别很重要时,应该使用完整的术语。例如,使用 UTF-16 编码方式或 UTF-16 编码方案。它们也可以分别缩写为 UTF-16 CEF 或 UTF-16 CES。

Unicode 字符编码方案的例子:

  • Unicode 标准有 7 种字符编码方案:UTF-8、UTF-16、UTF-16BE、UTF-16LE、UTF-32、UTF-32BE 和 UTF-32LE。

    • UTF-8、UTF-16BE、UTF-16LE、UTF-32BE 和 UTF32-LE 是简单的 CES。
    • UTF-16 和 UTF-32 是复合 CES,由数据起始处单个的、可选的 字节顺序标记(byte order mark) 和一个简单的 CES 组成。
    Name CEF CES
    UTF-8 + simple
    UTF-16 + compound
    UTF-16BE   simple
    UTF-16LE   simple
    UTF-32 + compound
    UTF-32BE   simple
    UTF-32LE   simple
    • Unicode 1.1 有三种字符编码方案:UTF-8、UCS-2BE 和 UCS-2LE,尽管后者当时并不是这样命名的。

压缩字符编码方案的例子:

  • BOCU-1
  • Punycode
  • SCSU (and RCSU)

字节顺序

处理器架构对多字节机器整数映射到存储位置的处理方式不同。小端架构将最低有效字节放在较低的地址,而大端架构从最高有效字节开始。

操作内存中的码元时,这种差异无关紧要,但是当使用指定的 CES 将码元序列化为字节序列时,字节顺序就变得重要了。在读取数据流时,有两种字节顺序:与处理器读取的字节顺序相同或相反。在前一种情况下,不需要采取特别的操作;在后一种情况下,数据在处理之前需要进行字节反转。

根据数据流的外部标记,区分三种字节顺序:Big Endian(BE)、Little Endian(LE)和 默认或内部标记(default or internally marked)

在 Unicode 中,码点为 U+FEFF 的字符被定义为字节顺序标记,而它的字节反转后对应的码点 U+FFFE 是 UTF-16 中的非字符(U+FFFE)。因此,在数据流首部,字节顺序标记用于明确地表示码元的字节顺序。

ECMAScript 『Executable Code 和 Execution Contexts』 章节解释

原文:The ECMAScript “Executable Code and Execution Contexts” chapter explained - https://medium.com/@g.smellyshovel/the-ecmascript-executable-code-and-execution-contexts-chapter-explained-fa6e098e230f

1. 代理组(Agent Cluster)

让我们从规范第八章中最深层的抽象开始 —— 代理组。

代理组是相互可以通过共享内存方式进行通信的代理(Agent)的最大集合。

我们讲通过本文的下一个章节来了解代理。

我们可以从上面的定义中得出两个重要的结论:

  1. 代理组是代理的集合;
  2. 可以通过共享内存方式进行通信的代理总在同一个代理组中。

代理组是代理的集合

理解代理组仅仅是一个在规范中使用的机制是很重要的:

代理组是一种仅用于规范中的机制,ECMAScript 引擎中不必存在对应的组成。

ECMAScript 引擎不必实现一个对应于代理组的构件,但必须提供代理组的相关功能。 因此,可以隐式地实现代理组,例如,由代理的某种本身的内在属性来实现,该属性存储一些标识符,标识代理所依赖的代理组。

// 例子 1
class Agent(agentCluster) {
    this.agentCluster = agentCluster; // 像这样
}
let a1 = new Agent("C1");
let a2 = new Agent("C1");
let a3 = new Agent("C2");

但是为什么规范强制引擎将代理划分为代理组呢?这个问题的答案已经在上一节的第2个结论中给出了:

可以通过共享内存的方式进行通信的代理总是在同一个代理组中。

两个代理必须属于同一个代理组,才能够使用共享内存方式进行通信。

// 例子 2。
class Agent(agentCluster) {
    this.agentCluster = agentCluster;
    canCommunicate(with, by) {
        if (by === "sharedMemory") {
            if (with.agentCluster !== this.agentCluster) {
                return false;
            }
        }
        return true;
    }
}

let a1 = new Agent("C1");
let a2 = new Agent("C1");
let a3 = new Agent("C2");

a1.canCommunicate(a2, "sharedMemory"); // true
a1.canCommunicate(a3, "sharedMemory"); // false

为了更好地理解,我来做个类比。

想象一群人(即代理)。其中一部分人是聋哑人,只能用手语与其他聋哑人交流。剩下的人没有这种疾病,但他们仍然只能通过话语与非聋哑人交流。

为了通过可对他人使用的交流手段来区分这些人,我们必须引入一些抽象概念。对人来说这种的概念可能是『群体』。同样,规范中将代理组作为代理的『群体』。

人是一个具体事物(我们可以触摸或看到的东西),然而群体是一个抽象概念。也适用于代理组。代理是具体事物(至少目前如此),但代理组不是。它只是虚构出的代理集合。

代理之间的通信

可以通过共享内存方式进行通信的代理总是在同一个代理组中。

还记得第2个结论吗?对我来说,这个结论又产生了3个问题:

  1. 为什么代理间需要通信?
  2. 通过共享内存方式进行通信实际上意味着什么?
  3. 还有其他的通信方式吗?

让我们从第3个问题开始:

有些可以通过消息传递进行通信的代理不能使用共享内存进行通信;它们永远不在同一个代理组中。

如你所见,答案是肯定的。甚至对于在不同代理组中的代理也存在通信机会。

然而,正如我前面提到的,这些代理不能共享内存。因此,为了相互交流,它们必须使用其他的交流方式。『消息传递』便用于此目的。

现在让我们回答第2个问题。

不同代理中的程序可以通过未指定的方式共享内存。至少,SharedArrayBuffer 对象的内存可以在同一个代理组中的代理之间共享。

共享内存意味着在同一个代理组中的代理可以对同一个内存区域进行操作(即读和写)。

当然,为了在内存上进行操作,提供了一些特殊的 API。其中一个 API 是 SharedArrayBuffer 对象。此外,规范中这些对象描述的共享内存功能作为 ECMAScript 引擎必须实现的最小功能集。

不同代理中的程序可以通过未指定的方式共享内存。

从这一部分可以看出,引擎还可以通过『未指定』的方式(API)组织共享内存。 此处的『未指定』一词表示未在规范中定义的方式,即,引擎本身以及其他标准可以定义它们。

总结

  1. 代理组是规范中使用的一种抽象,它将代理划分为若干集合,在这些集合内它们可以与集合内的其他代理共享内存;
  2. 不在同一个代理组中的代理也可以相互通信。消息传递方式用于这类情况。

2. 代理

你还记得我把代理组比作群体吗?我说过代理是具体的事物:

代理是具体事物(至少目前如此),但代理组不是。它只是虚构出的代理集合。

好吧,实际上不是。 但是让我解释一下,然后再向我丢石头。

在我们谈论代理组时,代理确实是具体的事物。 但这仅仅是因为代理组是过于高度的抽象,我们只需要掌握一些具体的事物即可。

但是现在你必须明白代理也只是一个抽象概念。

代理包括一个 ECMAScript 执行上下文(execution context)集合、一个执行上下文栈(execution context stack)、一个正在运行的执行上下文(running execution context)、一个命名作业队列(named job queue)集合、一个代理记录(Agent Record)和一个执行线程。

简单地说,代理是个容器,它封装了引擎用于正确执行代码的所有前置概念。

本文将对这些概念进行分类。

执行线程

代理的执行线程在代理的执行上下文中执行代理作业队列中的作业,这一过程在各个代理中独立执行,除非某个执行线程可以被多个代理用作执行线程,而且共享该线程的代理中没有一个具有 [[CanBlock]] 属性为 true 的代理记录。

规范指出执行线程执行代理作业队列中的作业,这并不能 100% 准确地描述正在发生的事情。

执行线程甚至不是规范中的概念。此外,除专门讨论代理的章节外,任何地方都没有提到执行线程。执行线程没有出现在任何规范所描述的算法中。

更正确的说法是,代理作业队列中的作业在某个执行线程上执行。

您应该理解代码执行包括解析和执行独立的代码节点,是解释器的任务。

然而,代码是在某个执行线程中执行的。但是允许解释器在不同的执行线程之间拆分执行。

因此,当规范说这是执行线程直接执行代码时,这就意味着代码是(由解释器)在某个执行线程中执行的。尽管如此,在规范方面并不存在解释器,所以执行线程是根据规范实际执行代码的线程。

创建代理

当一个代理的执行线程执行代理的作业队列中的作业时,当前代理是这些作业内部代码周边的代理。

本质上,代理是一个沙箱,在其中执行一些代码。这个沙箱让代码能够访问它的所有组成部分,以便正确执行代码(并完全执行)。此外,执行代码本身与周边代理的组成部分相互作用。

但是,正如我们已经知道的,代码不是自己执行的,而是由执行线程执行的(至少从规范的角度来说)。因此,规范说代理是代码周边的代理:换句话说,代码周边的代理是执行代码的沙箱。

3. 任务队列(Job Queue)

任务队列是待决任务(PendingJob)记录的先进先出队列。

让我们分部分析这个定义。

一个任务队列是先进先出队列...

…关于待决任务记录的队列。

『待决』表示任务队列中的任务被推迟,尚未完成。

4. 任务(Job)

任务是抽象操作,它在没有其他 ECMAScript 计算正在进行时启动一个 ECMAScript 计算。

该规范规定,只有当没有正在运行的执行上下文且执行上下文栈为空时,才能启动任务的执行。

让我们创建一个新任务

例如,在 JavaScript 中,我们可以创建一个新的 Promise,根据规范,它将创建一个新的待决任务并进入队列。所以,让我们开始吧。

// 例子 5。
new Promise((resolve) => {
    setTimeout(() => resolve("Two"));
}).then((message) => {
    console.log(message);
});

console.log("One");

这里我们隐式地创建了一个新的 Promise 任务。此任务将进入所在代理的 Promise 任务队列。但是什么时候执行呢?

我们已经知道,当没有正在运行的执行上下文时。我们将在稍后讨论执行上下文时考虑这个条件,但现在您可以认为这与主脚本的结尾是相同的。

主脚本(即插入新的 Promise 任务的脚本)将首先完成,因此该脚本的第一个输出是 One。然后,当没有其他东西可执行时,Promise 任务队列中的第一个待决任务将从该队列中移除并执行。因此第二个输出是 Two。

一旦任务开始执行,该任务总是执行到完成。

当任务执行与它相关联的代码时,与它所在代理相关联的其他任务不可执行它们的代码。

创建脚本任务

5. 执行上下文栈

到目前为止我们知道了,每个代理由一堆东西组成,其中一些是在所在代理初始化之后创建的。

执行上下文栈用于追踪执行上下文。

规范使用它来表示新的执行上下文创建顺序和其代码执行顺序。

在任何时间点上,实际中每个代理至多有一个正在执行代码的上下文。这称为代理的运行中执行上下文。

运行中执行上下文永远在执行上下文栈的顶部。

6. 执行上下文

执行上下文是一种规范设备,ECMAScript 引擎用于追踪运行时代码的执行。

执行上下文用于确保 ECMAScript 引擎能够监控运行时代码的执行。事实上,这就是执行上下文的主要目的。

执行上下文的状态组成

代码执行状态

函数

脚本还是模块

生成器

创建执行上下文

无论如何,我们不能显式地创建一个新的执行上下文。执行上下文只是规范使用的一种机制,它甚至可能不会被特定的 EСMAScript 引擎显式地实现。

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.