弹性布局与最小宽度

By default, flex items won’t shrink below their minimum content size

这意味着项目的最小宽度被设置为其内容的宽度,并且不会缩小到该宽度,所以这里使用overflow: hidden无法让其收缩。

broken.png

flex 布局的元素会默认设置min-widthmin-width默认值为 0,但规范中对于flex项目则设置为auto。这可能会使块状元素占用比预期更多的空间,甚至使它们的容器超出小屏幕的屏幕边缘。

参考阅读:

这里是对规范的讨论 :https://github.com/w3c/csswg-drafts/issues/2248
最新规范:https://drafts.csswg.org/css-sizing-3/#valdef-width-auto

For width/height, specifies an automatic size. See the relevant layout module for how to calculate this.
For min-width/min-height, specifies an automatic minimum size. Unless otherwise defined by the relevant layout module, however, it resolves to a used value of 0. For backwards-compatibility, the resolved value of this keyword is zero for boxes of all [CSS2] display types: block and inline boxes, inline blocks, and all the table layout boxes. It also resolves to zero when no box is generated.

Promise 重试

Promise Retry (1).png

Promise cache 链

Promise 常见的用法是 .then() 链式调用,其实.cache() 也是可以链式调用的。
首先初始化一个Promise.reject() ,之后需要重试几次就后面添加几个cache,如果需要满足一定条件或需要延迟一定时间还可与再继续添加.then(test).catch(rejectDelay);

const max = 5;
let p = Promise.reject();
const attempt = () => {
    console.log(`attempt`)
    return Promise.reject('error')
};

for(var i=0; i<max; i++) {
    p = p.catch(attempt);
    // 可以继续追加 then(test).catch(rejectDelay);
}

p.then(res => console.log(res)).catch(e => console.error(e));

这种方式需要保证:

  • 只有在指定的最大重试次数下才有可能。(链的长度必须有限)
  • 建议使用较小的重试次数。(Promise 链消耗的内存与其长度成正比)

如果不满足这几种情况,使用递归才是最好的选择。

递归

递归方式比较简洁,本质与Promise cache 链的实现是一样的,区别只是在失败之后才添加下一个catch链。

function retry(fn, retries=3, err=null) {
  if (!retries) return Promise.reject(err);
  return fn().catch(err => {
      return retry(fn, (retries - 1), err,);
    });
}

参考文章:
promise-retry-design-patterns

JS 中的协程(Coroutine)

coroutines-xenonstack.png

为何要使用协程

  1. 单线程环境中的并发:一些编程语言/环境只有一个线程。这时如果需要并发,协程是唯一的选择。(注意JS规范没有事件循环)
  2. 简化代码:可以避免回调地狱
  3. 有效利用操作系统资源和硬件:协程相比线程,占用资源更少,上下文更快

何为协程

协程是具有以下功能的函数:

  • 可以暂停执行(暂停的表达式称为暂停点);
  • 可以从挂起点恢复 
(保留其原始参数和局部变量)。

在JS中协程是一种用function*语法标记的生成器函数,函数内部可以使用yield关键字暂停自身。然后可以通过发送一个值来恢复它。发送的值将显示为的返回值,yield执行将继续直到下一个yield。注意JS生成器并不是一个真正的协程,我们可以称之为浅协程

进程、线程和协程的比较

进程:变量隔离,自动切换运行上下文
线程:不变量隔离,自动切换运行上下文切换
协程:不变量隔离,不自动切换运行上下文切换

将协程视为轻量级线程是协程的最直观,最高级的隐喻。与实际线程相比,最大的概念差异是缺少调度程序(所有上下文切换必须由程序完成)

简易协程封装

每次你使用协程你总会做以下三件事。

  1. 调用函数然后返回作为结果的协程对象
  2. 调用协程对象上的next方法,执行到第一个yield
  3. 然后你能做的就是next(),运行协程并发送值给他

我们能把所有功能包装到一个函数中:

function coroutine(f) {
    var o = f(); // instantiate the coroutine
    o.next(); // execute until the first yield
    return function(x) {
        o.next(x);
    }
}

使用这个函数,我们之前的例子变成下面形式:

var test = coroutine(function*() {
    console.log('Hello!');
    var x = yield;
    console.log('First I got: ' + x);
    var y = yield;
    console.log('Then I got: ' + y);
});
// prints 'Hello!'

test('a dog'); ​// prints 'First I got: a dog'
test('a cat'); // prints 'Then I got: a cat

Stackful coroutines

首先,让我们看看什么是有栈协程,他如何工作,并且如何作为一个库实现。可能它比较容易解释,因为他构成和线程相似。
Fibers 或是有栈协程是一个处理函数调用的展开的栈。为了更准确的阐述这种协程的工作方式,我们将从底层的视角简要了解函数的栈帧和调用。那就让我们先来看看 Fibers 的特性。

  • 每个fiber拥有独立的栈
  • fiber的生命周期独立于调用它的代码
  • fiber能从进程中分离出来并加到另一个进程中
  • 不能同时运行在一个进程中

所提及属性的含义如下:

  • fiber的上下文切换必须由用户操作,而不是OS(OS依然可以通过剥夺其所在进程来剥夺fiber)
  • 在同一个进程中的两个fiber之间,不会出现真正的数据竞争,因为只有其一处于活动状态
  • fiber中的I/O操作应为异步,从而其他的fiber可以依次无阻塞的运行

现在,从对函数调用堆栈所做的解释开始,详细解释fiber的工作原理。

栈是内存的连续块,用于存储局部变量和函数参数。
但是,更重要的是,在每个函数调用之后(除少数例外情况),其他信息都会放入栈中,以使被调用函数知道如何返回被调用方并恢复处理器寄存器。

一些寄存器具有特殊用途,并在函数调用时保存在栈中。这些寄存器(在ARM体系结构的情况下)为:

  • SP – stack pointer
  • LR – link register
  • PC – program counter

栈指针SP是一个寄存器,用于保存属于当前函数调用的堆栈起始地址。由于有了这个值,可以很容易地引用保存在栈中的参数和局部变量。
在函数调用期间,链接寄存器LR非常重要。它存储了当前函数执行结束后将要执行的代码的返回地址(到被调用方的地址)。调用该函数时,PC被保存到LR。当函数返回时,使用LR恢复PC。
程序计数器PC是当前执行指令的地址。

每次调用函数时,都会保存链接寄存器,以便该函数知道完成后要返回的位置。

当栈式协程被执行时,被调用函数使用先前分配的栈来存储其参数和局部变量。因为所有信息都存储在栈式协程中调用的每个函数的堆栈中,所以fiber可能会在协程中调用的任何函数中暂停执行。

现在,让我们来看看上面的图片中发生了什么。首先,线程和fiber有自己独立的栈。绿色数字是操作发生的顺序号。

  1. 线程内部的常规函数​​调用,在栈上进行分配。
  2. 该函数创建fiber对象。最终分配了fiber的栈。创建fiber并不一定意味着它会立即执行。
    同样,激活帧被分配。激活帧中的数据是以这种方式设置的,即将其上下文保存到处理器的寄存器中将导致上下文切换到fiber的栈。
  3. 常规函数调用
  4. 调用协程,处理器的寄存器设置为激活帧的内容。
    5,6. 协程内部的常规函数​​调用
  5. 恢复协程–在协程调用期间发生类似的事情。激活帧会记住协程内部的处理器寄存器状态,该状态是在协程暂停期间设置的。

Stackless coroutines

在无堆栈协程的情况下,无需分配整个堆栈。它们消耗的内存少得多,但是因此存在一些限制。

首先,如果它们不为堆栈分配内存,那么它们如何工作?在有协程的情况下,要存储在栈上的所有数据都放在哪里?
答案是:在程序调用栈上。

无栈协程的秘密在于,它们只能从顶级函数中挂起自己。对于所有其他函数,它们的数据都分配在被调用栈上,因此从协程调用的所有函数必须在挂起协程之前完成。协程保留其状态所需的所有数据均在堆上动态分配。这通常需要几个局部变量和参数,其大小远小于预先分配的整个堆栈。

现在我们可以看到,只有一个栈。让我们一步一步地跟踪图片中发生的事情。 (协程激活帧有两种颜色–黑色是存储在栈中的颜色,蓝色是存储在堆中的颜色)。

  1. 常规函数调用,该帧存储在堆栈中
  2. 该函数创建协程。这意味着将激活帧分配到堆上的某个位置。
  3. 常规函数调用。
  4. 协程调用。协程的主体在通常的栈上分配。并且程序流程与常规函数的情况相同。
  5. 从协程调用常规函数。同样,一切仍在栈中。 (注意:协程目前无法暂停,因为它不是协程的顶层函数)
  6. 函数返回到协程的顶级函数(注意,协程现在可以暂停自身)
  7. 协程挂起–将需要在整个协程调用中保留的所有数据放入激活帧。
  8. 常规函数调用
  9. 协程恢复–这是通过常规函数调用发生的,但是会跳到先前的挂起点+从激活帧恢复变量状态。

栈式协程 vs 无栈协程

能够从子函数产生的协程实现称为栈式,即它们可以记住整个调用栈。协程的其他实现(只能从协程函数的顶层执行)只能是无堆栈的。JavaScript 实现就是无栈式的(Python,C#和Kotlin也是如此)。

协程是无限制的协作式多任务任务:在协程内部,任何函数都可以挂起整个协程(函数激活本身,函数调用方的激活,调用方的调用方的激活等)。但是JS只能直接从生成器内部挂起生成器,而只有当前函数激活被挂起。由于这些限制,生成器有时被称为浅协程。

在下面的示例中,我们创建一个生成器,生成一个🐶,然后调用forEach将其传递给匿名函数的数组it => { ... }。由于匿名函数调用仍然算作子函数,因此我们无法yield从内部进行调用。此示例在运行时失败,但是在编译语言中,这将导致编译错误。

function* createGenerator() {
    console.log(yield "🐶");
    [1, 2, 3].forEach(it => {
        console.log(yield it); // runtime error
    });
} 
const c = createGenerator();
console.log(c.next("A"));
console.log(c.next("B"));
console.log(c.next("C"));

用如下方式避免使用回调函数:

function* genFunc() {
    for (const x of ['a', 'b']) {
        yield x; // OK
    }
}

参考文章:
https://blog.panicsoftware.com/coroutines-introduction/
https://dkandalov.github.io/yielding-generators
https://x.st/javascript-coroutines
https://exploringjs.com/es6/ch_generators.html#ch_generators_ref_3

Typescript 中泛型的高级用法

typescript-generics.png

继承类型

extends关键字不仅可以用在类和接口上,还可以用在类型上。

function myGenericFunction<T extends string>(arg: T): T {
    return arg;
}

条件类型

type MyType<T> = T extends string ? boolean : number;

映射类型

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}

他允许我们在T类型集合上进行迭代。

通用映射类型

// 全键可选
type Partial<T> = {
    [P in keyof T]?: T[P];
};

// 全键必需
type Required<T> = {
    [P in keyof T]-?: T[P];
};

// 全键只读
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

我们还可以写出更多的通用映射类型,如:

// 可为空类型
type Nullable<T> {
    [P in keyof T]: T[P] | null
}

// 包装一个类型的属性
type Proxy<T> = {
    get(): T
    set(value: T): void
}
type Proxify<T> = {
    [P in keyof T]: Proxy<T[P]>
}
function proxify(o: T): Proxify<T> {
    // ...
}
let proxyProps = proxify(props)

元组类型

var user: [number, string] = [1, "Steve"];
var employee: [number, string][];
employee = [[1, "Steve"], [2, "Bill"], [3, "Jeff"]];

交叉类型 ( & )和联合类型 ( | )

  • 交叉类型是将多个类型合并为一个类型。
  • 联合类型,如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。
type landAnimal = {
  name: string
  canLiveOnLand: true
}
type waterAnimal = {
  name: string
  canLiveInWater: true
}
// 交叉类型:两栖动物
type amphibian = landAnimal & waterAnimal
let toad: amphibian = {
  name: 'toad',
  canLiveOnLand: true,
  canLiveInWater: true,
}

全集和空集

  • any 类型,泛指一切可能的类型,对应全集。
  • never 类型对应空集。任何值,即使是 undefined 或 null 也不能赋值给 never 类型。

类型保护与区分类型

类型断言

类型断言有两种语法 <类型>值值 as 类型

使用类型保护

使用类型断言,需要多次判断十分麻烦。所以使用类型保护
这种param is SomeType的形式,就是类型保护,它用来明确一个联合类型变量的具体类型
类型谓词 谓词为 parameterName is Type这种形式,parameterName必须是来自于当前函数签名里的一个参数名。

  • typeof类型保护只能用于 number, string, boolean, symbol(只有这几种类型会被认为是类型保护)
  • instanceof类型保护用于类

参考文章:
https://www.freecodecamp.org/news/typescript-curry-ramda-types-f747e99744ab/

Webview 远程调试

Android 使用 ADB 调试

通过 Homebrew 安装 android-platform-tools
brew cask install android-platform-tools
使用connect命令连接设备:
adb connect <ip>:19129

然后开启手机上的USB调试功能。
访问Chrome://inspect,点击Inspect,弹出开发者工具进行调试。

adb disconnect 断开所有连接。

常见问题

  • 如果会出现白屏或者 404 错误,这是因为Chrome需要访问 https://chrome-devtools-frontend.appspot.com,来选择要使用兼容的 devtools。所以将该域名加入代理即可。
  • 如果调试工具界面错乱,则说明调试工具的版本过低(Chrome 63 后移除了 /deep/ 选择器),需要下载一个低版本Chrome,这里推荐下载Chromium。

IOS

在移动端,启用 设置 -> Safari 浏览器 -> 高级 -> Web 检查器,然后使用数据线链接,或下载[Safari Technology Preview] 无线远程调试(因为稳定版 Safari 不支持)。

参考文章:
https://saubcy.com/2019/01/13/chrome-remote-devices-window-breaks/