0%

reselect 是什么

reselect 是 redux 的一个中间件,它用于创建 selectors,根据官方仓库的说明

Selectors can compute derived data, allowing Redux to store the minimal possible state.
选择器可以计算派生数据,从而允许 Redux 存储尽可能少的状态。
Selectors are efficient. A selector is not recomputed unless one of its arguments changes.
选择器是高效的。除非选择器的一个参数发生更改,否则不会重新计算选择器。
Selectors are composable. They can be used as input to other selectors.
选择器是可组合。它们可以用作其他选择器的输入。

reselect 项目仓库地址

reselect 的源码只有 100 行。可以说是非常简单易懂的,但是以我目前的水平,阅读起来还是略有吃力,函数式编程就是绕啊。

尝试了一下将代码拷贝到本地 vscode 中进行调试运行,效果感觉还行,运行了几遍,整个流程就搞懂了。感觉这种小的工具库都可以用这种方式去分析它的源码。

1
2
3
export const createSelector = /* #__PURE__ */ createSelectorCreator(
defaultMemoize
);

这是这个库暴露的主要的 api,它是由 createSelectorCreator 这个工厂函数创建出来的,他接受一个 memoize 函数,同时可以接受选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
export function createSelectorCreator(memoize, ...memoizeOptions) {
return (...funcs) => {
let recomputations = 0;
const resultFunc = funcs.pop();
const dependencies = getDependencies(funcs);

const memoizedResultFunc = memoize(function () {
recomputations++;
// apply arguments instead of spreading for performance.
return resultFunc.apply(null, arguments);
}, ...memoizeOptions);

// If a selector is called with the exact same arguments we don't need to traverse our dependencies again.
const selector = memoize(function () {
const params = [];
const length = dependencies.length;

for (let i = 0; i < length; i++) {
// apply arguments instead of spreading and mutate a local list of params for performance.
params.push(dependencies[i].apply(null, arguments));
}

// apply arguments instead of spreading for performance.
return memoizedResultFunc.apply(null, params);
});

selector.resultFunc = resultFunc;
selector.dependencies = dependencies;
selector.recomputations = () => recomputations;
selector.resetRecomputations = () => (recomputations = 0);
return selector;
};
}

下面默认的 momize 的实现,momize 接受一个函数,通过闭包保存上一次调用该函数的参数和结果,如果参数相等,直接返回结果。
默认的实现的相等比较是浅相等,对于对象比较的是它们的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) {
let lastArgs = null;
let lastResult = null;
// we reference arguments instead of spreading them for performance reasons
return function () {
if (!areArgumentsShallowlyEqual(equalityCheck, lastArgs, arguments)) {
// apply arguments instead of spreading for performance.
lastResult = func.apply(null, arguments);
}

lastArgs = arguments;
return lastResult;
};
}

在返回的 selector 和 resultFunc 的计算中,使用到了 momize。这个库做了两次的记忆化处理,第一层是如果 selector 接受的参数相等,那么就不再计算 resultFunc 的依赖。第二层是 resultFunc 的依赖计算完成后,如果与之前计算的结果相同,不会再次执行 resultFunc。

数据类型

整型

byte:-128 ~ 127
short: -32768 ~ 32767
int: -2147483648 ~ 2147483647
long: -9223372036854775808 ~ 9223372036854775807

1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(String[] args) {
int i = 2147483647;
int i2 = -2147483648;
int i3 = 2_000_000_000; // 加下划线更容易识别
int i4 = 0xff0000; // 十六进制表示的16711680
int i5 = 0b1000000000; // 二进制表示的512
long l = 9000000000000000000L; // long型的结尾需要加L
}
}

浮点型

1
2
3
4
5
float f1 = 3.14f;
float f2 = 3.14e38f; // 科学计数法表示的3.14x10^38
double d = 1.79e308;
double d2 = -1.79e308;
double d3 = 4.9e-324; // 科学计数法表示的4.9x10^-324

对于 float 类型,需要加上 f 后缀

 强制转型

可以将浮点数强制转型为整数。在转型时,浮点数的小数部分会被丢掉。如果转型后超过了整型能表示的最大范围,将返回整型的最大值。
如果要进行四舍五入,可以对浮点数加上 0.5 再强制转型。

引用类型

除了整型,浮点型,布尔类型,字符类型,其他都为引用类型

判断相等

判断引用类型的变量内容是否相等,必须使用 equals() 方法

字符串

对于多行的字符串,使用 “””…”””表示多行字符串(java13 开始)

1
2
3
4
5
6
String s = """
SELECT * FROM
users
WHERE id > 100
ORDER BY name DESC
""";

由于多行字符串是作为预览特性(Preview Language Features)实现的,编译的时候,我们还需要给编译器加上参数:

1
javac --source 14 --enable-preview Main.java

常量

定义变量的时候,如果加上 final 修饰符,这个变量就变成了常量:

var 关键字

1
var sb = new StringBuilder();

编译器会自动推导类型

数组

自定义排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Basic {
public static void main(String[] args) {
// 不能使用基本类型,而要使用它们对应的类
Integer[] ns = {1, 2, 3, 4, 5};
for (int n : ns) {
System.out.println(n);
}
Arrays.sort(ns, new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
return b - a;
}
});
System.out.println(Arrays.toString(ns));
}
}

OOP

传参

引用类型传的是引用,会修改传递的应用类型数据本身

向下转型

1
2
3
4
Person p1 = new Student(); // upcasting, ok
Person p2 = new Person();
Student s1 = (Student) p1; // ok
Student s2 = (Student) p2; // runtime error! ClassCastException!

为了避免向下转型出错,Java 提供了 instanceof 操作符,可以先判断一个实例究竟是不是某种类型:

覆写

加上@Override 可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。@Override 不是必需的。

所有的 class 最终都继承自 Object,而 Object 定义了几个重要的方法:

  • toString():把 instance 输出为 String;
  • equals():判断两个 instance 是否逻辑相等;
  • hashCode():计算一个 instance 的哈希值。

如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为 final。用 final 修饰的方法不能被 Override:

多态 Polymorphic

Java 的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。

记录类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public final class Point {
private final int x;
private final int y;

public Point(int x, int y) {
this.x = x;
this.y = y;
}

public int x() {
return this.x;
}

public int y() {
return this.y;
}
}

使用 final 定义 class,除此以外,还需要正确覆写 equals() 和 hasCode()

反射

class 类

class 是由 JVM 在执行过程中动态加载的。JVM 在第一次读取到一种 class 类型时,将其加载进内存。

每加载一种 class,JVM 就为其创建一个 Class 类型的实例,并关联起来。注意:这里的 Class 类型是一个名叫 Class 的 class。它长这样:

1
2
3
public final class Class {
private Class() {}
}

以 String 类为例,当 JVM 加载 String 类时,它首先读取 String.class 文件到内存,然后,为 String 类创建一个 Class 实例并关联起来:

1
Class cls = new Class(String);

这个 Class 实例是 JVM 内部创建的,如果我们查看 JDK 源码,可以发现 Class 类的构造方法是 private,只有 JVM 能创建 Class 实例,我们自己的 Java 程序是无法创建 Class 实例的。
所以,JVM 持有的每个 Class 实例都指向一个数据类型(class 或 interface):

一个 Class 实例包含了该 class 的所有完整信息。由于 JVM 为每个加载的 class 创建了对应的 Class 实例,并在实例中保存了该 class 的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个 Class 实例,我们就可以通过这个 Class 实例获取到该实例对应的 class 的所有信息。

这种通过 Class 实例获取 class 信息的方法称为反射(Reflection)。

获取一个 class 的 Class 实例有三个方法
方法一:直接通过一个 class 的静态变量 class 获取:

1
Class cls = String.class;

方法二:如果我们有一个实例变量,可以通过该实例变量提供的 getClass()方法获取:

1
2
String s = "Hello";
Class cls = s.getClass();

方法三:如果知道一个 class 的完整类名,可以通过静态方法 Class.forName()获取:

1
Class cls = Class.forName("java.lang.String");

因为 Class 实例在 JVM 中是唯一的,所以,上述方法获取的 Class 实例是同一个实例。可以用==比较两个 Class 实例:

1
2
3
4
5
6
7
Integer n = new Integer(123);

boolean b1 = n instanceof Integer; // true,因为n是Integer类型
boolean b2 = n instanceof Number; // true,因为n是Number类型的子类

boolean b3 = n.getClass() == Integer.class; // true,因为n.getClass()返回Integer.class
boolean b4 = n.getClass() == Number.class; // false,因为Integer.class!=Number.class

用 instanceof 不但匹配指定类型,还匹配指定类型的子类。而用==判断 class 实例可以精确地判断数据类型,但不能作子类型比较。
通常情况下,我们应该用 instanceof 判断数据类型,因为面向抽象编程的时候,我们不关心具体的子类型。只有在需要精确判断一个类型是不是某个 class 的时候,我们才使用==判断 class 实例。

访问字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {

public static void main(String[] args) throws Exception {
Object p = new Person("Xiao Ming");
Class c = p.getClass();
Field f = c.getDeclaredField("name");
Object value = f.get(p);
System.out.println(value);
}
}

class Person {
public String name;

public Person(String name) {
this.name = name;
}
}

可以调用 f.setAccessible(true); 改变字段的可访问性

设置字段,可以使用 Field.set(Object, Object) 实现的,其中第一个 Object 参数是指定的实例,第二个 Object 参数是待修改的值。

调用方法

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) throws Exception {
// String对象:
String s = "Hello world";
// 获取String substring(int)方法,参数为int:
Method m = String.class.getMethod("substring", int.class);
// 在s对象上调用该方法并获取结果:
String r = (String) m.invoke(s, 6);
// 打印调用结果:
System.out.println(r);
}
}

如果方法是静态的,无需指定实例对象。invoke 方法传入的第一个参数永远为 null。如果调用非 public 方法,可以先通过 Method.setAccessible(true)设置可访问性
使用反射调用方法时,仍然遵循多台原则,即总是调用实际类型的覆写方法。

调用构造方法

调用 Class.newInstance()的局限是,它只能调用该类的 public 无参数构造方法。如果构造方法带有参数,或者不是 public,就无法直接通过 Class.newInstance()来调用。
为了调用任意的构造方法,Java 的反射 API 提供了 Constructor 对象,它包含一个构造方法的所有信息,可以创建一个实例。Constructor 对象和 Method 非常类似,不同之处仅在于它是一个构造方法,并且,调用结果总是返回实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String[] args) throws Exception {
// 获取构造方法Integer(int):
Constructor cons1 = Integer.class.getConstructor(int.class);
// 调用构造方法:
Integer n1 = (Integer) cons1.newInstance(123);
System.out.println(n1);

// 获取构造方法Integer(String)
Constructor cons2 = Integer.class.getConstructor(String.class);
Integer n2 = (Integer) cons2.newInstance("456");
System.out.println(n2);
}
}

多线程

创建

有三种方式

方法一:从 Thread 派生一个自定义类,然后覆写 run()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String[] args) {
Thread t = new MyThread();
t.start(); // 启动新线程
}
}

class MyThread extends Thread {
@Override
public void run() {
System.out.println("start new thread!");
}
}

方法二:创建 Thread 实例时,传入一个 Runnable 实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start(); // 启动新线程
}
}

class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("start new thread!");
}
}

或者用 Java8 引入的 lambda 语法进一步简写为:

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("start new thread!");
});
t.start(); // 启动新线程
}
}

可以对线程设定优先级,设定优先级的方法是:

1
Thread.setPriority(int n) // 1~10, 默认值5

线程的状态

Java 线程对象 Thread 的状态包括:New、Runnable、Blocked、Waiting、Timed Waiting 和 Terminated;
通过对另一个线程对象调用 join()方法可以等待其执行结束;
可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;
对已经运行结束的线程调用 join()方法会立刻返回。

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("hello");
});
System.out.println("start");
t.start();
t.join();
System.out.println("end");
}
}

中断线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1000);
t.interrupt(); // 中断t线程
t.join(); // 等待t线程结束
System.out.println("end");
}
}

class MyThread extends Thread {
public void run() {
Thread hello = new HelloThread();
hello.start(); // 启动hello线程
try {
hello.join(); // 等待hello线程结束
} catch (InterruptedException e) {
System.out.println("interrupted!");
}
hello.interrupt();
}
}

class HelloThread extends Thread {
public void run() {
int n = 0;
while (!isInterrupted()) {
n++;
System.out.println(n + " hello!");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
}
}

守护线程

守护线程是指为其他线程服务的线程。在 JVM 中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。

1
2
3
Thread t = new MyThread();
t.setDaemon(true);
t.start();

线程同步

synchronized 关键字对一个对象进行加锁,保证代码块在任意时刻最多只能有一个线程来执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Main {
public static void main(String[] args) throws Exception {
var add = new AddThread();
var dec = new DecThread();
add.start();
dec.start();
add.join();
dec.join();
System.out.println(Counter.count);
}
}

class Counter {
public static final Object lock = new Object();
public static int count = 0;
}

class AddThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock) {
Counter.count += 1;
}
}
}
}

class DecThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) {
synchronized(Counter.lock) {
Counter.count -= 1;
}
}
}
}

原子操作

JVM 规范定义了几种原子操作:

  • 基本类型(long 和 double 除外)赋值,例如:int n = m;
  • 引用类型赋值,例如:List list = anotherList。

同步方法

使用 synchronized 封装逻辑,这样的类就是线程安全的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Counter {
private int count = 0;

public void add(int n) {
synchronized(this) {
count += n;
}
}

public void dec(int n) {
synchronized(this) {
count -= n;
}
}

public int get() {
return count;
}
}

除了少数情况,大部分类,如 ArrayList 都是非线程安全的

1
2
3
4
5
6
7
8
public void add(int n) {
synchronized(this) { // 锁住this
count += n;
} // 解锁
}
public synchronized void add(int n) { // 锁住this
count += n;
} // 解锁

以上两种写法是等价的

对于 static 方法,是没有 this 实例的,因此 static 方法是针对类而不是实例。但是我们注意到任何一个类都有一个由 JVM 自动创建的 Class 实例,因此,对 static 方法添加 synchronized,锁住的是该类的 Class 实例。上述 synchronized static 方法实际上相当于:

1
2
3
4
5
6
7
public class Counter {
public static void test(int n) {
synchronized(Counter.class) {
...
}
}
}

死锁

可重入锁

java 的线程锁是可重入的锁。JVM 允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。
由于 Java 的线程锁是可重入锁,所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出 synchronized 块,记录-1,减到 0 的时候,才会真正释放锁。

多线程协调

多线程协调运行的原则就是:当条件不满足时,线程进入等待状态;当条件满足时,线程被唤醒,继续执行任务。

集合

hashmap

hashmap 通过以空间换时间的方式,提高查询速度。它用一个大数组存储所有的 value,并根据 key 直接计算出 value 应该存储在那个索引
在 map 的内部,对 key 做比较是通过 equals() 实现的,正确使用 Map 必须保证:

  • 作为 key 的对象必须正确覆写 equals()方法,相等的两个 key 实例调用 equals()必须返回 true;

  • 作为 key 的对象还必须正确覆写 hashCode()方法,且 hashCode()方法要严格遵循以下规范: - 如果两个对象相等,则两个对象的 hashCode()必须相等; - 如果两个对象不相等,则两个对象的 hashCode()尽量不要相等。 -
    上述第一条规范是正确性,必须保证实现,否则 HashMap 不能正常工作。
    而第二条如果尽量满足,则可以保证查询效率,因为不同的对象,如果返回相同的 hashCode(),会造成 Map 内部存储冲突,使存取的效率下降。

原理

实际上 HashMap 初始化时默认的数组大小只有 16,任何 key,无论它的 hashCode()有多大,都可以简单地通过:

1
int index = key.hashCode() & 0xf; // 0xf = 15

添加超过一定数量的 key-value 时,HashMap 会在内部自动扩容,每次扩容一倍,即长度为 16 的数组扩展为长度 32,由于扩容会导致重新分布已有的 key-value,所以,频繁扩容对 HashMap 的性能影响很大。如果我们确定要使用一个容量为 10000 个 key-value 的 HashMap,更好的方式是创建 HashMap 时就指定容量:

1
Map<String, Integer> map = new HashMap<>(10000);

servlet

servletContext

单例对象,在 web 部署启动的时候创建,在 web 工程停止的时候销毁

  • 获取 web.xml 配置的 context.param
  • 获取当前的工程路径 /
  • 获取工程部署后在服务器的绝对路径
  • 像 Map 一样存取数据

转发

可以 forward 共享 req 和 resp 对象
可以访问 WEB-INF 目录

缘起

写这篇文章的契机,是最近有看 react 源码的想法。先是看了这篇文章 如何阅读大型前端开源项目的源码 学习了一波姿势,然后开始下载 react 工程鼓捣起来。源码打断点,构建 umd 代码,然后看函数调用栈,执行到这一步,看到了这个页面,发现都是我不熟悉的东西。比如这个 Scope,虽然大体知道是作用域,但是具体是按照什么规则得到的,就不是很清楚了。所以决定先搞清一下 js 的作用域问题。

这都啥玩意啊

js 的作用域,是一个老生常谈的问题了,也是面试常考的内容。以前也通过看 js 高程,你不知道的 js 这些书对此有过据较为深入的学习,刷一些面试题的时候也背了一些“标准答案”,但是时间一长,也忘掉大半了。现在要我真说道说道,大概是说不出个所以然来的。今天搜索相关信息时看到这个博客的一一系列博文讲的真不错 https://blog.csdn.net/q1056843325/category_9267514.html ,自己对这一块其实挺欠缺的。纸上得来终觉浅,觉知此事要躬行。这次主要借鉴这位博主的文章内容,借助 Chrome 的开发者工具好好探究一下这个问题。

探究

预编译,变量提升与 let,const

脚本执行时,js 引擎会做下面三件事。

  1. 语法分析 引擎检查代码是否包含低级语法错误
  2. 预编译 在内存中开辟一些空间,存放变量与函数
  3. 解释执行

语法分析很简单,就是引擎检查你的代码有没有什么低级的语法错误
解释执行顾名思义便是执行代码了
预编译简单理解就是在内存中开辟一些空间,存放一些变量与函数

预编译的发生时间

我之前也以为预编译主要就是做了变量提升这件工作,也就是在脚本解释执行前执行一次。但实际上,在函数执行前都会进行预编译。

先看一下我之前理解的预编译,在 v8 引擎中时怎么做的。

预编译1

在脚本第一行执行前插入了 debugger 语句,这个时候预编译已经完成。我们看到 Scope 的 Global 作用域(浏览器中的 window 对象)中已经有了以下的属性,符合我们的认知,对于 let 声明的属性,预编译的时候是没有处理的。

1
2
3
4
5
{
a:undefined,
b:ƒ b(y),
c: undefined
}

然后继续执行,直到执行完了,我们看到 Scope 中多了一个 Script 的标签,其中保存了 aa 的值。这个按我的理解,也可以把它当作全局作用域,与 Global 不同的是,你没法在 window 对象上访问它。

预编译2

接下来就要执行函数了,在此之前,需要插入一点另外的知识

函数的 [[Scope]] 属性
函数是特殊的可执行对象。既然是对象,就可以拥有属性。函数中存在这一个内部属性[[Scope]](我们不能使用,供 js 引擎使用)函数被创建时,这个内部属性就会包含函数被创建的作用域中对象的集合。这个集合呈链式链接,被称为函数的作用域链。按照我的理解,这和词法作用域是一个概念。这个 [[Scope]]是会随着代码执行动态变化的。在这个例子中,b 函数的 [[Scope]] 一开始是没有 Script 的,在 let 声明执行后,Script 就被添加上了。

函数的执行环境
在函数执行时,会创建一个叫做执行环境/执行上下文(execution context)的内部对象,它定义了一个函数执行时的环境。函数每次执行时的执行环境独一无二,多次调用函数就多次创建执行环境,并且函数执行完毕后,执行环境就会被销毁,执行环境有自己的作用域链,用于解析标识符。

[[Scope]]和执行期上下文虽然保存的都是作用域链,但不是同一个东西。[[Scope]]属性是函数创建时产生的,会一直存在,而执行上下文在函数执行时产生,函数执行结束便会销毁。

预编译发生在每次函数执行前这个创建执行上下文的过程
预编译生成了下图中的 Local 作用域。而 Script 和 Global 作用域,则是函数属性 [[Scope]]的拷贝。
预编译3

这个博主的文章,里面多次提到了 AO 活动对象的概念,实际上这是 ES3 的说法了,还是以开发者工具的展现为准,这样更好理解一点。
至此,关于预编译的问题解决。

性能优化

js 引擎查找作用域链是为了解析标识符,变量在执行环境作用域链的位置越深,读写速度就越慢,因此应该多用局部变量(缓存)。

闭包

以前看过这样一个对于闭包的解释。当在一个函数中返回一个函数的时候,这个被返回的时候就可以保有当前函数作用域的变量的引用,这就形成了闭包。

但这样的理解是不充分的。实际上,只要是在函数的作用域内定义了函数,这个被定义的函数就具有闭包作用域。

闭包1

因此闭包的一大用途,就是实现私有变量。我们知道函数如果返回一个对象,在对它进行构造调用时,结果返回的就是该对象。不过使用这种方式,this 赋值的属性和原型绑定就失效了。

闭包2

可以看到,当执行以这种方式返回的函数时,可以通过闭包的方式访问执行构造调用时的 Local 作用域。
再回想一下之前说过的,函数每次执行时的执行环境独一无二,多次调用函数就多次创建执行环境,和 settimeout 打印的经典问题,顿时感觉豁然开朗了啊。

今天在看 ts 的装饰器文档的时候,对于类装饰器的一节的官方示例感到非常迷惑

类装饰器接受一个参数,为类的 constructor,你可以观察,修改或者替换它。如果类装饰器返回了一个值,他会用提供的类的构造函数替换类的声明

1
2
3
4
5
6
7
8
function classDecorator<T extends { new (...args: any[]): {} }>(
constructor: T
) {
return class extends constructor {
newProperty = "new property";
hello = "override";
};
}

最让我迷惑的是 return class extends constructor,为什么可以继承构造函数?要求的返回值是构造函数,但是这里返回的是一个类的声明?

说到底还是自己对于 es6 的 class 理解不够透彻。我又去看了一下阮一峰的 es6 入门的 class 章节,上面说明 ES6 的 class 只是 es5 对象原型写法的语法糖。

1
2
3
4
5
6
class Point {
// ...
}

typeof Point; // "function"
Point === Point.prototype.constructor; // true

看到这里,感觉到就豁然开朗了。类名变量实际上就是类的构造函数,extends 类名变量 和 extends 构造函数的写法实质上是等价。

es6 类声明语法的返回值也是类的构造函数,所以返回匿名类的语法也可以理解了。

同时因为继承的是构造函数,所以 extends 的可以是一个构造函数

1
2
3
4
5
function A() {}
A.prototype.test = 1;
class B extends A {}
console.log(new B().test);
// 输出1

需要注意的一点是,要获取类的构造函数,应该使用 类名.prototype.constructor,我一直以为是 类名.constructor。类名变量是构造函数,实际上构造函数的 constructor 是函数原型对象的 constructor。

目标,实现 OJ 前端代码自动打包构建镜像到腾讯云,远程 ssh 登录学校主机自动部署镜像

尝试了一下在 docker 中安装 jenkins,遇到了无法解决的坑,还是在 windows 下安装吧。

jenkins 插件下载加速

修改 jenkins_home 里 updates 中 default.json 文件,之后访问 /restart 路径 重启 jenkins 后生效

1
sed -i 's/http:\/\/updates.jenkins-ci.org\/download/https:\/\/mirrors.tuna.tsinghua.edu.cn\/jenkins/g' default.json && sed -i 's/http:\/\/www.google.com/https:\/\/www.baidu.com/g' default.json

无法执行 shell 脚本

Manage Jenkins>Configuration System
将 shell 设置为 git 安装目录 下 bin/sh.exe 的路径

node 环境

下载 jenkinsNode 插件后,直接设置为本机的 node 安装目录
nodejs配置

配置 publish over ssh

首先安装 Publish Over SSH 插件

报错 invalid private key
这是生成的 key 格式不同,插件不认新的 openssh 的格式,需要将私钥转回老格式

1
-----BEGIN RSA PRIVATE KEY-----

老格式文件的开头是这样的

构建配置

选择自由软件,配置 github 仓库地址,选择构建环境为设置的 nodejs 环境

构建脚本

1
2
3
4
5
cd FE && yarn && yarn build
cd FE && docker build -t ccr.ccs.tencentyun.com/jiezi19971225/ahpuoj-fe .
docker login --username=xxxxxx --password=xxxxxx ccr.ccs.tencentyun.com && docker tag ccr.ccs.tencentyun.com/jiezi19971225/ahpuoj-fe ccr.ccs.tencentyun.com/jiezi19971225/ahpuoj-fe:$BUILD_NUMBER && docker push ccr.ccs.tencentyun.com/jiezi19971225/ahpuoj-fe
docker rmi ccr.ccs.tencentyun.com/jiezi19971225/ahpuoj-fe:$BUILD_NUMBER
docker rmi ccr.ccs.tencentyun.com/jiezi19971225/ahpuoj-fe

构建后操作

1
2
3
4
5
cd /home/ahpuoj/ahpuojv2docker/compose
docker-compose pull fe
docker-compose up -d fe
// 这行命令用来删除老的镜像
docker image prune -f --filter "dangling=true"

it works

itWorks

面向对象

封装、抽象、继承、多态

面向对象的关键是其四大特性

封装 Encapsulation

也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据。
课程中提供的代码是 java,我用 ts 重新实现了一遍,当做是练习 ts 的熟练度了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
let id = 0;
class Wallet {
private id: number;
private createTime: Date;
private balance: number;
private balanceLastModifiedTime: Date;
constructor() {
this.id = id += 1;
this.createTime = new Date();
this.balance = 0;
this.balanceLastModifiedTime = new Date();
}

public getId(): number {
return this.id;
}
public getCreateTime(): Date {
return this.createTime;
}
public getBalance(): number {
return this.balance;
}
public getBalanceLastModifiedTime(): Date {
return this.balanceLastModifiedTime;
}
public increaseBalance(amount: number): void {
this.balance += amount;
this.balanceLastModifiedTime = new Date();
}
public decreaseBalance(amount: number): void {
this.balance -= amount;
this.balanceLastModifiedTime = new Date();
}
}

对于封装这个特性,需要编程语言本身提供访问权限控制的语法机制来支持。

之所以这样设计,是因为从业务的角度来说,id、createTime 在创建钱包的时候就确定好了,之后不应该再被改动,所有只提供 get 方法,这两个属性的初始化设置,对于调用者也是透明的,所以不提供用构造参数的方式进行外部赋值。

对于钱包余额 balance 这个属性,从业务的角度来说,只能增或者减,不会被重新设置。所在 Wallet 类中,只暴露了 increaseBalance() 和 decreaseBalance() 方法,并没有暴露 set 方法。alanceLastModifiedTime 这个属性,它完全是跟 balance 这个属性的修改操作绑定在一起的。只有在 balance 修改的时候,这个属性才会被修改,所以其相关的操作,都封装在 increaseBalance() 和 decreaseBalance() 方法中了

封装可以解决这些问题

如果对类中的属性访问不做控制,类的属性可能在代码的各个角落被随意修改,影响代码的可读性和可维护性
类通过有效的方法暴露必要的操作,也可以提高类的易用性

抽象 Abstraction

隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
interface Picture {
id: string;
blob: Blob;
metaInfo: Object;
}

interface IPictureStorage {
savePicture(pic: Picture): void;
getPicture(id: string): Picture;
deletePicture(id: string): void;
midifyMetaInfo(id: string, metaInfo: Object);
}

class PictureStorage implements IPictureStorage {
savePicture(pic: Picture): void {}
getPicture(id: string): Picture {
return {
id: "1",
blob: new Blob(),
metaInfo: {},
};
}
deletePicture(id: string): void {}
midifyMetaInfo(id: string, metaInfo: Object) {}
}

抽象这个特性非常用以实现,不需要编程语言提供特殊的语法机制来支持,只需要提供“函数”这一非常基础的语法机制,函数本身就是一种抽象。

相较于课程中代码,为了简化代码,有所改动。调用者在使用图片存储功能时,只需要了解 IPictureStorage 暴露了哪些方法就可以了,不需要查看 PictureStorage 里面的具体实现。

抽象可以解决这些问题

只关注功能点,不关注实现,很多设计原则体现了抽象的设计思想,如基于接口而非实现编程,开闭原则,代码解耦。我们在定义方法的时候,也要有抽象思维,不要再方法定义中,暴露太多的细节。如 getAliyunPictureUrl() 就不好,如果某一天讲图片存储地址改变了,那么函数命名也要改变,相反,应该叫做 getPictureUrl(),即便内部存储方式修改了,我们也不需要修改命名

继承 Inheritance

上升一个思维层面,去思考继承这一特性,可以这么理解:我们代码中有一个猫类,有一个哺乳动物类。猫属于哺乳动物,从人类认知的角度上来说,是一种 is-a 关系。我们通过继承来关联两个类,反应真实世界中的这种关系,非常符合人类的认知,而且,从设计的角度来说,也有一种结构美感。

度使用继承,继承层次过深过复杂,就会导致代码可读性、可维护性变差。我们应当 “多用组合少用继承”

多态 Polymorphism

子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现

这里就没有改写课程中的代码,换了一个更浅显的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Animal {
bark(): void {
console.log("animal bark");
}
}

class Dog extends Animal {
bark(): void {
console.log("dog wang wang");
}
}

class Cat extends Animal {
bark(): void {
console.log("cat miao miao");
}
}

function PlayWith(ani: Animal): void {
// do something
ani.bark();
}

let dog = new Dog();

let cat = new Cat();

PlayWith(dog);
PlayWith(cat);

多态需要编程语言拥有一下特殊语法机制

  • 编程语言要支持父类对象可以引用子类对象
  • 第二个语法机制是编程语言要支持继承
  • 第三个语法机制是编程语言要支持子类可以重写(override)父类中的方法

多态除了利用 继承加方法重写的方式来实现外,还有两种比较常见的实现方式,一个是利用接口类语法,另一个是 duck-typing 语法。

接口类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
interface CanBark {
bark(): void;
}

class Tiger implements CanBark {
bark(): void {
console.log("老虎吼");
}
}

class Lion implements CanBark {
bark(): void {
console.log("狮子吼");
}
}

function LookAt(ani: CanBark) {
ani.bark();
}

let tiger = new Tiger();

let lion = new Lion();

LookAt(tiger);

LookAt(lion);
duck-typing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface CanBark {
bark(): void;
}

const tiger = {
bark(): void {
console.log("老虎吼");
},
};

const lion = {
bark(): void {
console.log("狮子吼");
},
};

function LookAt(ani: CanBark) {
ani.bark();
}

LookAt(tiger);

LookAt(lion);

多态能提高代码的可扩展性和复用性,也是很多设计模式、设计原则、编程技巧的代码实现基础,比如策略模式、基于接口而非实现编程、依赖倒置原则、里式替换原则、利用多态去掉冗长的 if-else 语句等等。

抽象类与接口

抽象类是对成员变量和方法的抽象,是一种 is-a 关系,是为了解决代码复用问题。接口仅仅是对方法的抽象,是一种 has-a 关系,表示具有某一组行为特性,是为了解决解耦问题,隔离接口和具体的实现,提高代码的扩展性。

基于接口而非基于实现编程

越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。好的代码设计,不仅能应对当下的需求,而且在将来需求发生变化的时候,仍然能够在不破坏原有代码设计的情况下灵活应对。

假设我们的系统中有很多涉及图片处理和存储的业务逻辑。图片经过处理之后被上传到阿里云上。为了代码复用,我们封装了图片存储相关的代码逻辑,提供了一个统一的 AliyunImageStore 类,供整个系统来使用。

整个上传流程包含三个步骤:创建 bucket(你可以简单理解为存储目录)、生成 access token 访问凭证、携带 access token 上传图片到指定的 bucket 中。代码实现非常简单,类中的几个方法定义得都很干净,用起来也很清晰,乍看起来没有太大问题,完全能满足我们将图片存储在阿里云的业务需求。

而有一天,我们要将图片改为上传到私有云,首先,AliyunImageStore 类中有些函数命名暴露了实现细节,比如,uploadToAliyun() 和 downloadFromAliyun()。
其次,将图片存储到阿里云的流程,跟存储到私有云的流程,可能并不是完全一致的。比如,阿里云的图片上传和下载的过程中,需要生产 access token,而私有云不需要 access token。

而要解决这些问题,编写代码的时候,要遵从 “基于接口而非实现编程”的原则

  • 函数的命名不能暴露任何实现细节。比如,前面提到的 uploadToAliyun() 就不符合要求,应该改为去掉 aliyun 这样的字眼,改为更加抽象的命名方式,比如:upload()。
  • 封装具体的实现细节。比如,跟阿里云相关的特殊上传(或下载)流程不应该暴露给调用者。我们对上传(或下载)流程进行封装,对外提供一个包裹所有上传(或下载)细节的方法,给调用者使用。
  • 为实现类定义抽象的接口。具体的实现类都依赖统一的接口定义,遵从一致的上传功能协议。使用者依赖接口,而不是具体的实现类来编程。

在定义接口的时候,不要暴露任何实现细节。接口的定义只表明做什么,而不是怎么做。而且,在设计接口的时候,我们要多思考一下,这样的接口设计是否足够通用,是否能够做到在替换具体的接口实现的时候,不需要任何接口定义的改动。

课后习题

在项目中很多地方,我们都是通过下面第 8 行的方式来使用接口的。这就会产生一个问题,那就是,如果我们要替换图片存储方式,还是需要修改很多类似第 8 行那样的代码。

1
2
3
4
5
6
public class ImageProcessingJob {
private static final String BUCKET_NAME = "ai_images_bucket";
public void process() { Image image = ...;
ImageStore imageStore = new PrivateImageStore(/*省略构造函数*/);
imagestore.upload(image, BUCKET_NAME);
}

看了评论区的答案,一种比较好的解决方式是使用工厂方法+配置文件

1
ImageStore imageStore = ImageStoreFactory.newInstance(SOTRE_TYPE_CONFIG);

多用组合 少用继承

继承最大的问题就在于:继承层次过深、继承关系过于复杂会影响到代码的可读性和可维护性。我们可以利用组合(composition)、接口、委托(delegation)三个技术手段,一块儿来解决继承存在的问题。

接口只声明方法,不定义实现。也就是说,每个会下蛋的鸟都要实现一遍 layEgg() 方法,并且实现逻辑是一样的,这就会导致代码重复的问题。那这个问题又该如何解决呢?

我们可以针对三个接口再定义三个实现类,它们分别是:实现了 fly() 方法的 FlyAbility 类、实现了 tweet() 方法的 TweetAbility 类、实现了 layEgg() 方法的 EggLayAbility 类。然后,通过组合和委托技术来消除代码重复。具体的代码实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
interface Flyable {
fly(): void;
}

class FlyAbility implements Flyable {
fly() {
console.log("fly");
}
}
interface Tweetable {
tweet(): void;
}

class TweetAbility implements Tweetable {
tweet() {
console.log("tweet");
}
}
interface Egglayable {
egglay(): void;
}

class EgglayAbility implements Egglayable {
egglay() {
console.log("egglay");
}
}

class Ostrich implements TweetAbility, EgglayAbility {
private tweetAbility = new TweetAbility();
private egglayAbility = new EgglayAbility();
public tweet(): void {
this.tweetAbility.tweet();
}

public egglay(): void {
this.egglayAbility.egglay();
}
}

大三数据库考试前记的考点

起步

  • 修改密码 alter user identified by 123456;
  • 解锁用户 alter user scott account unlock;
  • 用户登录 connect 用户名/密码
  • 修改 sqlplus 默认命令提示符 set sqlprompt jieziSQL>

sqlplus 基本命令

  • show user 显示当前用户
  • select * from tab 当前用户下的表和视图
  • set linesize 150 设置行宽
  • col ename for[mat] a10 设置列宽
  • set pagesize 30 设置页大小
  • set newpage 5 设置一页中空行的数量
  • define 定义变量
1
2
3
lj>define var = "test";
lj>define var;
DEFINE VAR = "test" (CHAR)

save,get,/,start,@

1
2
3
4
5
6
select * from myemp;
save o1; // 保存到o1.sql
get o1; // 显示o1.sql
/ // 执行上一条sql命令
start o1; // 执行方式1
@o1 // 执行方式2

numformat
有如下占位符
9 对应数字存在则显示,如果位数不足会显示乱码,如 56 在 999 掩码下会乱码 0 对应数字不存在补 0
$ 添加美元符号 S 强制显示符号,正数会显示+ * , 对应位置添加,

1
set numformat $999,999,999,999.00

desc

1
2
3
4
5
6
desc dba_tablespaces;
// 在查询过程中使用
select empno,ename,
#desc job
job from emp;

spool
spool filename [create | replace | append] 可使用简写 cre 新建 rep 替换 app 追加
spool out | off 关闭 spool 输出

1
2
3
lj>spool /home/jiezi19971225/result app;
lj>select * from emp;
lj>spool off;

ed[it] filename
上一条执行的 SQL 或者 PL/SQL 执行完毕后,可以用该命令编辑
col[umn]

1
2
3
4
5
6
lj> col ename heading 姓名;
lj> col comm null empty; // 该列null值显示为设定的字符串
lj>col sal off;
lj>col job justify [left|center|right];
lj>col job [print|noprint]; // 控制是否显示该列
lj>col job [wrapped|word_wrapped]; // 按照长度、单词折行

ttitle 和 btitle

1
2
3
4
5
6
7
8
lj>ttitle left 我是表头标题  // 参数可选left center right col n(在第n列打印)  skip n (跳到下一行开始的第几行) bold 以黑体打印 可以使用变量
lj>btitle left 我是表尾标题
lj>btitle [on|off] 切换显示

// 使用变量
lj>define varT='table header'
lj>ttitle left varT

设置空白行数为 4

1
set new page 4

用户管理与权限分配

用户相关

创建用户

1
2
3
4
create user user_name [identified by pass_word | identified exeternally]
default tablespace uses
temporary tablespaces temp
quota 10m on tbsp_1; // 置顶用户在tbsp_1表空间最多可使用的大小为10MB

删除用户

1
drop user user_name [cascade]; // 如果用户包含数据库对象必须加cascade

权限管理

如果不知道权限,可以 select * from system_privilege_map where privilege_name = ? 进行模糊查找

概要文件

概要文件:
是一个命名的资源限制的集合。系统资源的使用、口令的限制。
创建:

create profile 概要文件名 limit
sessions_per_user : 限制用户当前会话的数量
cpu_per_session: 限制一个会话使用的 cpu 时间
cpu_per_call :限制一个 sql 语句使用的 cpu 时间
logical_reads_per_session:限制每个会话读取的数据库数据块数,包括从内存和磁盘读取的总和
logical_reads_per_call:限制 sql 语句读取的数据库数据块数,包括从内存和磁盘读取的总和
private_sga :sga 中私有区域的大小
connect_time :指定一个会话连接到数据库的最大时间
idle_time default:指定一个会话可以连续空闲的最长时间,单位:分钟
composite_limit:设置用户对系统资源的综合消耗。由: cpu_per_session、logical_reads_per_session、private_sga、connect_time 综合决定
failed_login_attempts: 最大错误登录次数
password_lock_time:登录失败后账户被锁天数
password_life_time:密码有效天数
password_grace_time:用户密码被中止前多少天提醒用户修改密码
password_reuse_time:用户修改密码后多少天,用户才可以再次使用原来的密码
password_reuse_max:密码被重新使用后,可修改的次数
password_verify_function:密码复杂度审计函数

修改:

1
2
3
alter profile 概要文件名 limit 
sessions_per_user 20
failed_login_attemps 3;

删除:

1
drop profile 概要文件名;

说明:
一个用户所使用的概要文件被删除,则此用户使用默认的概要文件。
使某个用户使用某个用户配置文件
aler user 用户名 profile 概要文件名;
要使上面的限制生效,需要修改初始化参数 resource_limit
alter system set resource_limit=true;(11g)

权限相关

创建用户

1
create user jiezi19971225 identified by 123456;

记录一下学习 React 全家桶中的 react-router

examples

路由参数

通过 hooks 获取

1
let { id } = useParams();

Route

route props

所有渲染方式会传递以下三种 props

  • match
  • location
  • history
    会在 props 接受这三个 props
component

当你使用 component 时,router 使用 React.CreateElement 根据传递的 component 创建一个新的 React element 。这意味着如果你提供了一个内联的函数给 component 属性,你会在每次 render 的时候都创建这个组件,这会导致旧组件的 unmount 和 新组建的 mount

其背后的原理在于,react 在比较组件状态以便决定如何更新 dom 节点时,首先要比较组件的 type 和 key。在使用<Route component={() => (<Bar idx={this.state.idx}/>)}/>时,由于调用了 React.createElement,组件的 type 不是 Bar 这个类,而是一个匿名函数。App 组件每次 render 时都生成一个新的匿名函数,导致生成的组件的 type 总是不相同,所以会产生重复的 unmounting 和 mounting。

render

允许方便的内联渲染,避免不必要的重新挂载。当路由匹配的时候,这个函数会被调用。函数可以访问所有的 props。

<Route component> 优先级高于 <Route render>

children

有时你需要渲染渲染子组件,不管路由是否匹配。这种情况下,可以使用 children 。

exact

当为 true,进行精确匹配。

strict

当为 true,路径中的 slash 也必须精确匹配。

sensitive

当为 true,大小写敏感。

history 是可变对象

1
2
3
4
5
6
7
8
9
10
11
12
class Comp extends React.Component {
componentDidUpdate(prevProps) {
// will be true
const locationChanged = this.props.location !== prevProps.location;

// INCORRECT, will *always* be false because history is mutable.
const locationChanged =
this.props.history.location !== prevProps.history.location;
}
}

<Route component={Comp} />;

to

可以是一个路径字符串,也可以是一个对象

1
2
3
4
5
6
{
pathname:'xxx',
search:'xxx',
hash:'xxx',
state:'xxx'
}

也可以是一个函数,传递当前的 location,返回一个字符串或者对象

replace

当为 true,替换历史栈中的当前记录,而不是添加一条记录

activeClassName

默认的类名是 active,这将会被连接到 className 属性后

当为 true,activeClassName 和 activeStyle 只会在精确匹配时生效

结合 Route 的 strict,会跳转到严格匹配的路由

isActive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<NavLink
to="/events/123"
isActive={(match, location) => {
if (!match) {
return false;
}

// only consider an event active if its event id is an odd number
const eventID = parseInt(match.params.eventID);
return !isNaN(eventID) && eventID % 2 === 1;
}}
>
Event 123
</NavLink>

vue 3.0 在 2020.4.17 发布了 beta 版本,可以说离正式发布已经很近了,是时候学习一波新姿势了。

composition api

目前 vue 3.0 的文档还未更新,可以先看这个了解 vue3.0 的新语法 vue composition api rfc
以下是我的学习笔记。

核心概念 响应性

响应性 vue 3.0 最重要的概念,vue3.0 使用 proxy 赋予对象响应性,一个对象具有响应性,那么当这个对象发生改变的时候,就可以自动触发视图的更新或者 watchEffect 中的副作用。

proxy

概念

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。Proxy 实际上重载(overload)了点运算符,即用自己的定义覆盖了语言的原始定义。所以可以实现 Object.defineProperty 做不到的对于对象新增属性,数组下标的响应式操作。

构造

1
var proxy = new Proxy(target, handler);

要使得 proxy 起作用,必须针对 Proxy 实例(上例中是 proxy)进行操作,而不是对目标对象 target 进行操作,如果 handler 没有设置任何拦截,那么等同于直接通向原对象

proxy 实例也可以作为其他对象的原型对象

1
2
3
4
5
6
7
8
9
10
11
var proxy = new Proxy(
{},
{
get: function (target, propKey) {
return 35;
},
}
);

let obj = Object.create(proxy);
obj.time; // 35

proxy 对象是 obj 对象的原型,obj 对象本身没有 time 属性,所以根据原型链,会在 proxy 对象上读取该属性,导致被拦截

支持的操作

几乎可以支持全部对于对象的访问操作,甚至包括 函数的调用,constructor 的调用,具体看文档就好。

setup

回到 Vue3 这里

setup 相当于原来的 options,可以接受两个参数

第一个参数 props,props 是响应性的,可以触发 watchEffect,注意 props 不能被解构,否则会失去响应性。

第二个参数 context 是一个不可变对象。它有以下属性,其实也都对应了 2.0 中的 api。

  • context.attrs 就是组件上的 attrs,等同于 2.0 的 $attrs
  • context.slots 可访问通过组件插槽分发的的内容,等同于 2.0 的 $slots
  • context.emit 就是 2.0 的 $emit 方法

setup 中的 this

setup 中不支持使用 this

reactive

赋予一个对象响应性,相当于 2.0 中的 Vue.observable()

ref

接受一个非引用类型值,返回一个 reactive 的可变 ref 对象,对象有一个唯一的 value 属性

当其作为一个 reactive 对象的属性的时候,会自动 unwrap 内部的值,使它的行为与普通属性一致

toRefs

接受一个 reactive 对象,返回一个对象,对象中每个属性都是 ref 对象
这在函数返回一个对象的时候很有用,返回值可以被解构而不失响应特性

computed

接受一个 getter 函数,返回一个不可变的响应 ref 对象

readonly

接受一个对象(reactive 或者 朴素的)或者一个 ref,返回一个 readonly 的 proxy

watchEffect

立即运行一个函数,同时动态地跟踪它的依赖项,并在依赖项发生变化时重新运行它。

当 watchEffect 在 setup 中或者生命周期钩子中被调用,watcher 会被链接到组件的生命周期中,会在组件卸载的时候自动停止

watchEffect 也会返回一个 stop 函数,可以手动调用

side effect

有时在 watchEffect 中需要执行一些异步的方法,当再次触发 watchEffect 但是异步操作还未完成,就需要清理一些操作。

watchEffect 可以接受一个 onInvalidate 参数,大致形式如下

1
2
3
4
5
6
7
8
watchEffect((onInvalidate) => {
const token = performAsyncOperation(id.value);
onInvalidate(() => {
// id has changed or watcher is stopped.
// invalidate previously pending async operation
token.cancel();
});
});

不像是 react 中 useEffect 返回一个函数的原因是,异步函数的返回值是很重要的。
vue 会缓冲无效的副作用,当在一个 tick 中多次发生副作用的时候,避免不必要的重复调用。一个组件的 update 钩子也是一个副作用。watchEffect 会在 update 钩子触发前触发。触发的顺序是 beforeUpdate=》watchEffect=》onUpdate

生命周期

Vue 3.0 提供了在 setup 中使用的生命周期的钩子函数,与 2.0 的对应关系如下

beforeCreate -> use setup()
created -> use setup()
beforeMount -> onBeforeMount
mounted -> onMounted
beforeUpdate -> onBeforeUpdate
updated -> onUpdated
beforeDestroy -> onBeforeUnmount
destroyed -> onUnmounted
errorCaptured -> onErrorCaptured

新提供了一下供调试使用的钩子
onRenderTracked
onRenderTriggered

1
2
3
4
5
6
export default {
onRenderTriggered(e) {
debugger;
// inspect which dependency is causing the component to re-render
},
};

inject provide

在 vue 3.0 中的使用方式如下,provide 可以注入普通数据,也可以注入响应性数据。官方的例子,provide 的键值用的是 Symbol,当然也可以用字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Parent.vue
<template>
<div>
<div>parent</div>
<slot></slot>
</div>
</template>

<script>
import { provide, ref } from "vue";
export default {
setup() {
const themeRef = ref("dark");
provide("ThemeSymbol", themeRef);
},
};
</script>

// Son.vue
<template>
<div>
<div>son</div>
<div>{{ theme }}</div>
</div>
</template>

<script>
import { watchEffect, inject, ref } from "vue";
export default {
setup() {
const theme = inject("ThemeSymbol", ref("light"));
watchEffect(() => {
console.log(`theme set to: ${theme.value}`);
});
return {
theme,
};
},
};
</script>

template Refs

在 vue2.0 中,通过 ref 可以获取到 虚拟 dom 的引用。3.0 中也是如此,但是有一些变化。
下面的代码中,我们在 setep 创建了一个名叫 root 的 ref,同时在 template 的 div 的 ref 属性值为 root,那么在初次渲染后,root 的值将会指向虚拟 dom 引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div ref="root"></div>
</template>

<script>
import { ref, onMounted } from 'vue'

export default {
setup() {
const root = ref(null)

onMounted(() => {
// the DOM element will be assigned to the ref after initial render
console.log(root.value) // <div/>
})

return {
root
}
}
}
</script>

Reactivity Utilities

unref

如果参数是一个 ref,返回它的内部值,否则返回参数自身,实际上是 val = isRef(val) ? val.value : val 的语法糖

toRef

被用来从 reactive 对象中的属性中 创建一个 ref

1
2
3
4
5
6
const state = reactive({
foo: 1,
bar: 2,
});

const fooRef = toRef(state, "foo");

toRefs 这个之前已经写了

将一个 reactive 对象转换为 普通对象,对象的每一个属性都是一个 ref 对象

isRef && isProxy && isReactive && isReadonly

都是一些顾名思义的方法了,不赘述了

Advanced Reactivity APIs

customRef

平时应该很少用到,知道有这个方法就好了。允许你定义自己的 ref 方法,可以加入额外的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function useDebouncedRef(value, delay = 200) {
let timeout;
return customRef((track, trigger) => {
return {
get() {
track();
return value;
},
set(newValue) {
clearTimeout(timeout);
timeout = setTimeout(() => {
value = newValue;
trigger();
}, delay);
},
};
});
}

export default {
setup() {
return {
text: useDebouncedRef("hello"),
};
},
};

markRaw

标记一个对象,这个对象不会被添加响应性

1
2
3
4
5
6
const foo = markRaw({});
console.log(isReactive(reactive(foo))); // false

// also works when nested inside other reactive objects
const bar = reactive({ foo });
console.log(isReactive(bar.foo)); // false

shallowReactive

创建一个只追踪它自己的属性的响应性的对象(不会深度追踪嵌套的对象)

1
2
3
4
5
6
7
8
9
10
11
12
const state = shallowReactive({
foo: 1,
nested: {
bar: 2,
},
});

// mutating state's own properties is reactive
state.foo++;
// ...but does not convert nested objects
isReactive(state.nested); // false
state.nested.bar++; // non-reactive

shallowReadonly

创建一个只使得它自己的属性是只读的对象(不会深度使嵌套的对象只读)

1
2
3
4
5
6
7
8
9
10
11
12
const state = shallowReadonly({
foo: 1,
nested: {
bar: 2,
},
});

// mutating state's own properties will fail
state.foo++;
// ...but works on nested objects
isReadonly(state.nested); // false
state.nested.bar++; // works

shallowRef

创建一个 ref,只使得它自身的 value 属性是响应性的

1
2
3
4
5
const foo = shallowRef({});
// mutating the ref's value is reactive
foo.value = {};
// but the value will not be converted.
isReactive(foo.value); // false

toRaw

可以获得 reactive 或者 readonly 的原始对象,允许你对原始对象进行操作。这是一个你没有其他方法时的逃生舱,可以被用来临时读取或者写入而不会触发响应追踪。这个方式是不推荐使用的,当你使用这个方法的时候,会给与警告。

vuex

vue3.0 搭配的是 vuex 的 4.0 版本,大部分 api 的用法都没有改变。

创建 store 的方法现在使用 creatStore。
vue3.0 的写法套路如下,首先需要用 useStore() 方法(和 react hooks 一模一样啊)获取 store 对象,如果返回 store 中的属性,用 computed 处理,如果返回 action,需要用 store 手动 dispatch。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<div>
Clicked: times, count is {{ counter.count }}.
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="incrementSync">-</button>
</div>
</template>

<script>
import { computed, watchEffect, onUpdated, onBeforeUpdate } from "vue";
import { useStore } from "vuex";

export default {
setup() {
const store = useStore();
return {
counter:computed(() => store.state.counter)
increment: () => store.dispatch("counter/increment"),
decrement: () => store.dispatch("counter/decrement"),
incrementSync: () => store.dispatch("counter/incrementSync"),
};
},
};
</script>

mapState,mapGetters,mapActions 这些辅助方法在 Vue3.0 写法下是无法使用的。因为 setup 中无法使用 this ,而这些辅助方法在 2.0 中是需要访问 this.$store 的。当然我们完全可以用 2.0 的写法用这些辅助方法。

vue-router

主要的破坏性更新是

  • 使用历史模式 从 mode:’history’ 改为 history: createWebHistory()
  • 捕获所有路由的写法现在应该是 /:catchAll(.*)
  • router.match 和 router.resolve 方法被合并到 router.resolve
  • router.getMathedComponents 这个方法被移除了
    可以通过以下代码实现这个方法
1
2
3
router.currentRoute.value.matched
.map((record) => Object.values(record.components))
.flat();

目前合并了四个 rfc

动态路由 实现了以下 API

  • router.addRoute(route: RouteRecord) Add a new route
  • router.removeRoute(name: string | symbol) Remove an existing route
  • router.hasRoute(name: string | symbol): boolean Check if a route exists
  • router.getRoutes(): RouteRecord[] Get the current list of routes

移除了 router-link 的 tag 和 event prop,不再自动给内部 a 元素添加点击事件,增加了 scoped-slot api

数据的机器级表示与处理

数制和编码

信息的二进制编码

计算机内部处理的所有数据都必须是数字化编码了的数据。

采用二进制表示方式的原因如下:

  • 二进制只有两种基本状态,使用有两个稳定状态的物理器件就可以表示二进制数的每一位,而制造有两个稳定状态的物理器件要比制造有多个稳定状态的物理器件容易得多。如用高低两个电位,脉冲的有无,脉冲的正负极性。
  • 二进制的编码,计数和运算规则都很简单
  • 两个符号 1 和 0 正好与逻辑命题的真假相对应,为计算机中实现逻辑运算和程序中的逻辑判断提供了便利的条件

机器指令的操作数只能是以下四种简单的基本数据类型:无符号定点整数,带符号定点整数,浮点数和非数值型数据

指令所处理的数据类型分为数值数据和非数值数据两种。数值数据可用来表示数量的多少,可比较其大小,分为整数和实数,整数又分为无符号整数和带符号整数,实数用浮点数表示。非数值数据是一个没有大小之分的位串,不表示数量的多少,主要用来表示字符数据和逻辑数据。

在计算机内部,数值的表示方法又两大类,第一种是直接用二进制数表示,另一种是采用二进制编码的十进制数(Binary Coded Decimal Number 简称 BCD)表示。

表示一个数值数据要确定三个要素:进位计数制,定/浮点表示和编码规则。

进位计数制

十进制数字可以表示成以上形式,10 称为基数,$10^i$ 称为第 i 位上的权,运算时采用逢十进一。

类似地,二进制数的基数是 2,运算时逢二进一。

以上规律可以扩展到 R 进制的数字系统。

进制转换相关不再赘述。

定点与浮点表示

定点表示

定点表示法用来对定点小数和定点整数进行表示,对于定点小数,其小数点总是固定在数的左边,一般用来表示浮点数的尾数部分。对于定点整数,其小数点总是固定在数的最右边。

浮点表示

对于任意一个实数 X,可以表示成如下形式:

其中 S 取值为 0 或 1,用来决定数 X 的符号;M 是一个二进制定点小数,称为数 X 的尾数(mantissa),E 是一个二进制定点整数,称为数 X 的指数;R 是基数(radix、base)。在基数 R 一定的情况下,尾数 M 的位数反映数 X 的有效位数,它决定了数的表示精度,有效位数越多,表示精度就越高,阶 E 的位数决定数 X 的表示范围,阶 E 的位数决定数 X 的表示范围;阶 E 的值确定了小数点的位置。

从浮点数的形式来看,绝对值最小的非零数是如下形式的数:$0.0···01R^{-11···1}$,而绝对值最大的数的形式应为 $0.11···1R^{11···1}$,所以假设 m 和 n 分别表示阶和尾数的位数,基数为 2,则浮点数 X 的绝对值的范围为:

浮点数的最小数是定点小数的最小数 $2^{-n}$ 去除一个很大的数$2^{2^m-1}$,而浮点数的最大数则是定点小数的最大数 $1-2^{-n}$乘以这个大数$2^{2^m-1}$

定点数的编码表示

定/浮点表示解决了小数点的表示问题,但是对于一个数值数据来说,还有一个正负号的表示问题。一般用 0 和 1 来表示数字的符号,称为符号数字化,一般规定 0 表示正号,1 表示负号。

通常将数值数据在计算机内部编码表示的数称为机器数,而机器数真正的值称为机器数的真值

约定一个机器数$X$的真值$X_T$用 n 位的二进制数编码,有以下方式:

原码表示法

一个数的原码表示由符号位直接跟数值位构成,称为“符号-数值”表示法。

原码编码规则如下

  • 当 $XT$为正数时,$X{n-1} = 0,X_i=X_i^” (0\leq i \leq n-2)$
  • 当 $XT$为负数时,$X{n-1} = 1,X_i=X_i^” (0\leq i \leq n-2)$

原码表示的优点是:与真值的对应关系直观,方便,用原码实现乘除运算也比较简便。

缺点是:0 的表示不唯一,加减法运算规则复杂。在加减运算过程中,要判定是否是两个异号数相加或两个同号数详见,若是,则必须判定两个数的绝对值大小,根据判断结果决定结果符号,然后用绝对值大的数减去绝对值小的数。现代计算机不用原码表示整数,只用定点原码小数来表示浮点数的尾数部分。

补码表示法

补码表示可以实现加减运算的统一,用加法来实现减法运算。计算机中,补码用来表示带符号整数,补码表示法也称为“2-补码”(two’s complement)表示法,由符号位后跟上真值的模$2^n$补码构成。

可以用以下方法求一个数的补码:对于正数,符号位取 0,其余各位同真值中对应的各位。对于负数,符号位取 1,其余各位由真值各位取反,末尾加 1 得到。

补码运算的推导

两个正数相加,两个负数相加,可能会出现溢出的情况。

变形补码

为了便于判断运算结果是否溢出,某些计算机中采用了一种双符号位的补码表示方式,也成为变形补码,或者模 4 补码。在双符号位中,左符是真正的符号位,右符用来判别溢出。

反码表示法

负数的补码可用各位求反,末尾加 1 得到,如果仅仅求反而不加 1,得到的是负数的反码表示。

反码表示存在以下不足:0 的表示不为 1,表数范围比补码少 1,运算时需考虑循环进位。

移码表示法

浮点数实际上使用两个定点数来表示的,用一个定点小数来表示浮点数的尾数,一个定点整数表示浮点数的阶。一般来说,浮点数的阶都用一种称为移码的编码方式,通常将阶的编码表示称为阶码

阶可以是正数,也可以是负数。当进行浮点数的加减运算时,必须先对阶,为了简化比较操作,在操作过程中不涉及阶的符号,可以对每个阶都加上一个正的常数,称为偏置常数(bias)

整数的表示

计算机中的整数分为无符号整数带符号整数两种。

省略有关 C 语言相关处理内容。

浮点数的表示

浮点数的表示范围

浮点数的表示范围

根据浮点数的格式格式,只要尾数为 0,阶码取任何值都为 0,这样的数被称为机器零。因此机器零不唯一。

浮点数的规格化

规格化数的标志是真值的尾数部分中最高位具有非零数字。

  • 右规 当有效数位进到小数点前面时,需要进行右规,右规时,尾数每右移一位,阶码+1
  • 左规 当尾数出现 $ 0.0···bbbb $的结果时,需要进行左规,右规时,尾数每左移一位,阶码-1
IEEE 754 标准

32 位包括 1 位符号位 8 位阶码 23 位有效位

64 位包括 1 位符号位 52 位有效位

规格化尾数最高位总是 1,所以隐含表示,省略一位

阶码使用移码表示 大小为 $2^{n-1}-1$

全 0 +0

符号位 1 之后全 0 -0

浮点数除 0 的结果是正负无穷大,而不是一出一场

0 11111111 00000000000000000000000 正无穷大

1 11111111 00000000000000000000000 负无穷大

Not a Number 非数 sqrt(-0.4) 0/0 使用全 1 的阶码和非 0 的尾数

非规格化数 阶码全 0 尾数非 0

十进制数的表示

ASCII

BCD 码

有权 BCD 码

每个十进制数位的 4 个二进制数位都有一个确定的权,它选取 4 位二进制按照计数顺序的前 10 个代码与十进制数字相对应。每位的权分别为 8,4,2,1,因此称为 8421 码,也称自然 BCD 码。

无权 BCD 码

无权 BCD 码是指每个十进制数位的 4 个基 2 码没有确定的权。如余 3 码和格雷码。

余 3 码是在 8421 BCD 数码的基础上每个码加上 3 (0011)形成的

格雷码在一组数的编码中,任意两个相邻的代码只有一位二进制数不同。最大数和最小数也仅有一位不同,即收尾相连。

数据的宽度和存储

数据的宽度和单位

机器字长是 CPU 内部用于整数运算时的数据通路的宽度,字长等于 CPU 内部用于整数运算的运算器位数和通用寄存器宽度。

描述距离,频率等数值通常用 10 的幂次表示,因而在由时钟频率计算得到的总线带宽或外设数据传输率中,度量单位表示的也是 10 的幂次,通常用 K 表示 1024,k 表示 1000,其他前缀字母均大写,表示大小由上下文决定。

数据的存储和排列顺序

一般用最低有效位最高有效位分别表示数的最低位和最高位。对于带符号的数,最高位就是符号位。

如果以字节为一个排列基本单位 LSB 表示 最低有效字节,MSB 表示 最高有效字节

排列方式有两种,大端和小端

小端方式将最高有效字节放在高地址单元,最低有效字节存放在地地质单元。Intel 80x86 采用小端方式。

数据的基本运算

!(x^y) 等价于表达式 x == y

左移运算和右移运算

C 的移位操作有逻辑移位算数移位

  • 逻辑移位不考虑符号位的问题,左移时,高位移出,低位补 0。右移时,低位移出,高位补 0。
  • 算数移位左移时,高位移出,低位补 0.右移时,低位移出,高位补符号。
位扩展运算和位截断运算

截断一个数可能因为溢出而改变它的数值。

整数加减运算

整数加法运算器通常包含以下输出标志:

  • 零标志 ZF,当结果所有位都是 0 时,ZF=1,否则,ZF=0
  • 溢出标志 OF,OF=1 表示带符号整数的加减运算发生溢出
    全加器 溢出标志的逻辑表达式
    $OF=Cn \oplus C(n-1)$
    因为若为两个正数相加,最高位必然没有进位,而若次高位有进位,则说明溢出 若为两个负数相加,最高位必然有进位,若次高位没有进位,则说明溢出
  • 符号标志 SF 一般取有符号数的最高位
  • 进/借位标志 CF 表示无符号数加减法时的进/借位。加法时,CF=1 表示加法有进位,减法时,CF=1 表示不够减。因此,加法时 CF 等于进位输出 C,减法时,CF 等于进位输出 C 的取反。
整数乘除运算

好难啊,看不懂,先坑着

常量的乘除运算

编译器在处理变量与常数相乘,往往用移位和加减法组合代替乘法运算。

浮点数运算

加减运算

计算机实现上述过程需要经过对阶,尾数相减,规格化和舍入四个步骤

阶码溢出判断
在进行尾数规格化和尾数舍入时,可能会对结果的阶码执行加减运算,必须考虑阶码的溢出问题。如果阶码全为 1,发生阶码上溢,此时可将结果置为无穷,若阶码全为 0,发生阶码下溢,此时可将结果置为 0。

乘除运算

无需对阶的步骤,但是对于结果的处理步骤是一样的。