Programming

协变、逆变(Covariance and contravariance)与子类型

协变与逆变(Covariance and contravariance)是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器构造出的多个复杂型别之间是否有父/子型别关系的用语。许多程式设计语言型别系统支持子型别。例如,如果CatAnimal的子型别,那么Cat型别的表达式可用于任何出现Animal型别表达式的地方。所谓的变型(variance)是指如何根据组成型别之间的子型别关系,来确定更复杂的型别之间(例如Cat列表之于Animal列表,回传Cat的函数之于回传Animal的函数…等等)的子型别关系。当我们用型别构造出更复杂的型别,原本型别的子型别性质可能被保持、反转、或忽略───取决于型别构造器的变型性质。

协变:subtyping 关系保留
逆变:subtyping 关系翻转

子类型关系(subtyping)

A => B:相比 B,A内涵扩张(自身属性更多),外延收缩(集合中元素减少)

数学中的子集: 如果集合 A 的任意一个元素都是集合 B 的元素,那么集合 A 称为集合 B 的子集。

T: U 的时候,任何需要形式参数 a: U 的函数,我们都能给一个实际参数 a: T ——子类型至少可以被当作它的超类型(supertype)。子类型一定是父类型(?)。

子类型与面向对象语言中(类或对象)的继承是两个概念。子类型反映了类型(即面向对象中的接口)之间的关系;而继承反映了一类对象可以从另一类对象创造出来,是语言特性的实现。因此,
子类型也称接口继承;继承称作实现继承。这也意味着子类型必须实现父类型所有接口,但它也可以拥有更多其他接口。

类型构造器(type constructor)

类型构造器就是一些带有泛型/模板参数的类型。当填满了参数,才会成为一个实际的类型。比如说 List<T>

协变允许子类型可以隐性地转换为父类型。如果一个地方需要一个父类型,你给他一个子类型,也成立,此时表示此处支持协变。**

Cat 是 Animal 的子类型。那么对于随意的(一元)类型构造器 M, M 和 M 可能会有什么关系呢?
协变(covariance):M: M 它们维持内部参数的关系不变。
逆变(contravariance):M: M 它们的关系被反转了。
不变(invariance):构造后,两类型没有子类型关系了。
对于采取哪种变化方式,由类型系统的具体实现来决定。
**
先从常理来分析,
对于协变,很好理解,比如DogAnimal,那Array<Dog>自然也是Array<Animal>。但是对于某种复合类型,比如函数。(p: Dog) => void却不是(p: Animal) => void,反过来却成立。
这里举例:
🙆 Array -> Array 协变 (外延扩张)
🙅~~ (p: Dog) => void -> (p: Animal) => void ~~
🙆 Func -> Func 逆变(外延收缩)
因为若函数体中需要调用 Dog 的方式,传入 Animal 可能会造成缺失。所以 需要Dog的函数不可用需要 Animal 函数替代,但需要 Animal 的函数可以被需要 Dog 的函数替换。
所以 Func extends Func,而 Dog extends Animal,常理来讲,对于函数参数逆变天生应生效。

但对于TS来说,函数参数支持逆变与协变,返回值支持协变的。数组支持协变。

Ts 中的函数参数类型既支持协变,也支持逆变,也就是双向协变(bivariace):
可以查看官方论证
https://github.com/Microsoft/TypeScript/wiki/FAQ#why-are-function-parameters-bivariant
但对于 extends 关键字来说,它则是遵循了“理应支持逆变,不支持协变”。

  1. interface Animal {
  2. name: string;
  3. eat(): void;
  4. }
  5. interface Dog extends Animal {
  6. wow(): void;
  7. }
  8. type PlayWith<T> = (p: T) => undefined | string;
  9. let playWithAnimal: PlayWith<Animal> = (animal) => {
  10. const hungry = !!(Math.random() > 0.5);
  11. if (hungry) {
  12. animal.eat();
  13. return `${animal.name} is eatting`;
  14. }
  15. return undefined;
  16. };
  17. let playWithDog: PlayWith<Dog> = (dog) => {
  18. const hungry = !!(Math.random() > 0.5);
  19. if (hungry) {
  20. dog.eat();
  21. return `${dog.name} is eatting`;
  22. } else {
  23. dog.wow();
  24. return `${dog.name} is barking`;
  25. }
  26. };
  27. playWithDog = playWithAnimal;
  28. playWithAnimal = playWithDog; // "strictFunctionTypes": true, 会报错
  29. type T = PlayWith<Animal> extends PlayWith<Dog> ? 1 : 0; // 1
  30. type T1 = PlayWith<Dog> extends PlayWith<Animal> ? 1 : 0; // 0

let playWithAnimal: PlayWith<Animal>
Type 'PlayWith<Dog>' is not assignable to type 'PlayWith<Animal>'.
Property 'wow' is missing in type 'Animal' but required in type 'Dog'.ts(2322)
MultipleRawBlock.tsx(96, 3): 'wow' is declared here.
但若不设置 strictFunctionTypes 则是不报错的

In summary, in the TypeScript type system, the question of whether a more-specific-type-accepting function should be assignable to a function accepting a less-specific type provides a prerequisite answer to whether an array of that more specific type should be assignable to an array of a less specific type. Having the latter not be the case would not be an acceptable type system in the vast majority of cases, so we have to take a correctness trade-off for the specific case of function argument types.

那为什么 ts 要让函数参数支持双变,也就是函数参数的类型集合为啥允许可大可小呢?
当然现在可以通过个 config 配置关闭不正确的 参数协变
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-6.html#strict-function-types

不管是协变,逆变,在某些特定场景下都是不安全的,任何类型系统对于某些构造器是否实现这些转化都有自己需求和必要性,灵活意味着可能引入不安全性,死板意味着安全,但是却给使用者带来了负担。

协变是不安全的

虽然从道理上来将,协变是行得通的,但它也存在不安全的可能,如下。
如这里的 Cat[] -> Animal[] 是可行的,但同时 如果数组的内容可变(Animal[] 变为 Cat[] 后还能再进行其他 Animal 子类型进入)

  1. class Animal { }
  2. class Cat extends Animal {
  3. meow() {
  4. console.log('cat meow');
  5. }
  6. }
  7. class Dog extends Animal {
  8. wow() {
  9. console.log('dog wow');
  10. }
  11. }
  12. const catList: Cat[] = [new Cat()];
  13. let animalList: Animal[] = [new Animal()];
  14. const dog = new Dog();
  15. // covariance is not type safe
  16. animalList = catList; // 支持协变 用 Cat 代替 Animal 不报错
  17. animalList.push(dog); // Dog extends Animal 可以注入数组(但此时的 Animal[] 是被代替的 Cat[])
  18. catList.forEach(cat => cat.meow()); // 再使用 Cat[] 时报错,cat.meow is not a function

于是从数组的例子可以看出,不可变的协变,是安全的。可变协变必然是不安全的。
无论是协变,还是逆变,在某些实际使用场景下从道理上来讲,都是可以进行支持的,但若从当前范畴进入另一个逻辑范畴,就会导致错误的出现。

variance

一种支持限制 协变,逆变及不可变 处理的 类型验证方式(类似判断 instanceof,这里则是判断是否是 协变后类型)
https://flow.org/en/docs/lang/variance/

Tools

react-hook-form

采用注册 原生表单域,不自行实现表单域
采用 非受控 实现的 form
https://react-hook-form.com/faqs

React Hook Form Formik Redux Form
Component uncontrolled & controlled controlled controlled
Rendering minimum re-render re-render according to local state changes which means as you type in the input. re-render according to state management lib (Redux) changes which means as you type in the input.
API Hooks Component (RenderProps, Form, Field) + Hooks Component (RenderProps, Form, Field)
Package size Small
react-hook-form@7.0.0<br />**8KB**
Medium
formik@2.1.4<br />**15KB**
Large
redux-form@8.3.6<br />**26.4KB**
Validation Built-in, Yup, Zod, Joi, Superstruct and build your own. Build yourself or Yup Build yourself or Plugins
Learning curve Low to Medium Medium Medium

很适合用于从头开始的表单定制,但是表单联动,验证,可能会变得复杂起来。

React

key

Each time React renders your components, it’s calling your functions to retrieve the new React elements that it uses to update the DOM. If you return the same element types, it keeps those components/DOM nodes around, even if all* the props changed. The exception to this is the key prop. This allows you to return the exact same element type, but force React to unmount the previous instance, and mount a new one.

不传 key 和 传相同 key,React 的处理方式是相同的。及通过 render 去做增量变化(fiber diff),DOM结构保留(变化最小化)。
而rerender 中 key 产生变化,则是会直接进入 unmount 再 mount 的情况。

Props & JSX

https://kentcdodds.com/blog/optimize-react-re-renders
由于 React 的 rerender 执行是 JSX 收到父级的 rerender 通知,通过 render 函数重新生成 UI descriptor objects,然后比对上一次,进行 diff。若有 子JSX 使用 props 则再通知子JSX进行比对的过程。
所以如果 组件 的子组件存在需要传递 props 的情况,每次 props 这个 obj 必然是一个新的 obj,对应的 JSX 必然会收到 rerender 通知。
在 rerender 的执行中,会导致 props 这个对象本身产生变化,除了使用 memo 来浅对比 props,还有一种方式,就是将 子组件(JSX) 提升。
如这里的 Logger 不依赖 Counter 的任何内容,可提升到顶层,通过 props 传递 JSX 的方式,这样 Counter 的 rerender 就不会导致 这段 JSX 收到 rerender 通知:

  1. import * as React from 'react'
  2. import ReactDOM from 'react-dom'
  3. function Logger(props) {
  4. console.log(`${props.label} rendered`)
  5. return null // what is returned here is irrelevant...
  6. }
  7. function Counter(props) {
  8. const [count, setCount] = React.useState(0)
  9. const increment = () => setCount(c => c + 1)
  10. return (
  11. <div>
  12. <button onClick={increment}>The count is {count}</button>
  13. {props.logger}
  14. </div>
  15. )
  16. }
  17. ReactDOM.render(
  18. <Counter logger={<Logger label="counter" />} />,
  19. document.getElementById('root'),
  20. )

In summary, if you’re experiencing performance issues, try this:

  1. “Lift” the expensive component to a parent where it will be rendered less often.
  2. Then pass the expensive component down as a prop.

You may find doing so solves your performance problem without needing to spread React.memo all over you codebase like a giant intrusive band-aid 🤕😉

所以对于复杂的 JSX 块,如果确实不需要直接父级的 props,可以做提升处理,至少可以省去 memo 的比对过程。

CSS

box-shadow

box-shadow: offset-x offset-y blur spread color positioon

box-shadow有六个参数

  1. offset-x 阴影在x轴的距离。正数向右偏移,负数向左偏移。
  2. offset-y 阴影在y轴的距离。正数向下偏移,负数向上偏移。
  3. blur 模糊的尺度,// 越大模糊(渐变)效果越大范围越大(同时渐变范围变大)
  4. spread 阴影的扩展。 // 越大阴影范围越大

(前四个都是以px 等距离单位为单位的数值)

  1. color 阴影的颜色。可以是任何合法的颜色表示法表示的数据。
  2. position 阴影的位置,两种取值
    1. outset 不用设置 默认就是外部阴影
    2. inset 阴影位于内部。