Hooks-useState

与 UI 更新相关的状态才使用 state 。如果只想要一个数据持久化,请使用 useRef

使用

1
const [state, setState] = useState(initialState);

如果初始的 initialState 需要通过复杂计算获得,则可以传入一个函数。

1
2
3
4
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});

伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function SM() {
let state = null;
return function myUseState(value) {
state = state || value; // 第一次调用没有初始值,因此使用传入的初始值赋值

function dispatch(newValue) {
state = newValue;
// 假设此方法能触发页面渲染
// render()
}

return [state, dispatch];
};
}

异步还是同步

useState 返回的 setCount 和 class 组件的 this.setState 一样,在事件处理函数内部执行的是异步的,在执行之后并不能获取到最新的值。

怎么获取最新的值

如果新的 state 需要通过先前的 state 计算得出,可以将函数传给 setState。该函数接收先前的 state,并返回一个更新后的值。

1
2
3
4
5
6
7
8
const [count, setCount] = useState(0);

useEffect(() => {
setCount((prevCount) => prevCount + 1);
setCount((prevCount) => prevCount + 1);
setCount((prevCount) => prevCount + 1);
// 注意,这里不能获取到count最新值
}, []);

如果需要使用最新的 state,可以在 useEffect 里获取到
当触发更新的函数(即setCount)接收一个函数时,会创建对应的update。这里调用了 3 次就会有 3 个update。所以count为 3。

为什么返回值是个数组

数组解构时,变量可以命名为任意名称,取值由它的位置决定。

1
2
const [state, setState] = useState(null);
const [state2, setState2] = useState(null);

对象解构时,变量必须与属性同名才能取到正确的值。

1
2
const { state, setState } = useState(null);
const { state: state2, setState: setState2 } = useState(null);

如果使用返回的是对象,当代码块中多次使用 useState 时,对象属性得重命名才不会冲突。相比之下数组代码显示的简洁。

但数组也有不足之处:

  • 返回值必须按顺序取
  • 返回参数较多时,又不是所有返回值都需要的情况下,写法会比较奇怪。例如只使用 setState: const [, setState] = useState(null)

避免不必要的 useState

案例来自波神的文章: https://mp.weixin.qq.com/s/nR9zczAZ5WZb1oG7w5cJDA
截取文章的其中一段:

Hooks闭包陷阱

闭包陷阱

以下两段代码,来自 https://github.com/hacker0limbo/my-blog/issues/6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function createIncrement(i) {
let value = 0;
function increment() {
value += i;
console.log(value);
const message = `Current value is ${value}`;
return function logValue() {
console.log('value: ', value);
console.log(message);
};
}

return increment;
}

const inc = createIncrement(1);
const log = inc(); // 1
inc(); // 2
inc(); // 3

log(); // "Current value is 1"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function createIncrementFixed(i) {
let value = 0;
function increment() {
value += i;
console.log(value);
return function logValue() {
const message = `Current value is ${value}`;
console.log(message);
};
}

return increment;
}

const inc = createIncrementFixed(1);
const log = inc(); // 1
inc(); // 2
inc(); // 3

log(); // "Current value is 3"

截取文章中的一段话,如下:



对于这句话,我再做下解释:
在 value 变量修改值之后,message 变量保存的值仍旧是原始的值。这是因为基础数据类型的值是不可变的。

基础数据类型的值是不可变的

让我们举例说明,看下这段代码,思考 a 的值为多少?

1
2
3
4
5
6
let a = 1;
let b = a;
b++;

console.log(a);
console.log(b);

b++,b 的值被改变了,但是 a 没被改变,依然为 1。这意味着 a b 的等价,并不表示他们是同一个值。也就是说,a 赋予 b 的时候,重新给 b 分配了一块内存空间。因此我们说,基础数据类型,是按值访问的。 用图表示

基础数据类型的比较,是值在比较

1
2
3
const a = 1;
const b = 1;
a === b; // true

当 a 和 b 在比较的时候,本质上是值在比较。所以我们说基础数据类型的值是不可变的。

出现闭包陷阱的原因

为第一段代码第 6 行打上断点

  1. 开始执行createIncrement函数,先定义基础数据类型 value并赋值为 0,然后返回increment函数并赋值给inc
  2. inc 执行,进入increment函数,执行到第 8 行,value+=i,变量value的值变成了 1,同时产生了闭包对象Closure(createIncrement),该闭包对象存在increment作用域当中。

  1. 接着执行到第 10 行,声明了message变量,并将基础数据类型 value的值赋给message,此时value为 1,所以message = "Current value is 1",然后返回logValue函数,increment函数执行完毕,并赋值给log变量。因为闭包对象Closure(createIncrement)的产生,log变量将会引用着increment函数第一次执行时创建的作用域。如图所示,作用域里的 Local 对象「活动对象」中的message = "Current value is 1"

  1. 解释第 25 行,执行logValue函数,寻找message变量。 先查找 Local 活动对象,没找到,发现作用域链中的闭包对象Closure(increment)中存在message,且值为Current value is 1,打印messagelogValue函数执行完毕。
1
2
Local 活动对象:仅仅只有处于栈顶的执行上下文,才会生成 Local 对象。
除了 Local 活动对象,作用域链中的其他几种变量对象(Closure/Module/Global)都能够在函数解析时确定

虽然Closure(createIncrement)中的value已经变成了 3,但是并不影响基础数据类型 message 变量中的 value 值。
那是不是只要将引用类型赋值给 message 变量,就可以解决闭包陷阱问题?

解决办法

让我们稍微改下代码,将value改为引用类型,再赋值给 message

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
// 闭包陷阱
// 参考: https://github.com/hacker0limbo/my-blog/issues/6
const Trap = (props) => {
// debugger
function createIncrement(i) {
let value = {
a: 0,
};
function increment() {
value.a += i;
const message = value;

return function logValue() {
console.log('value: ', value);
console.log('message: ', message);
};
}

return increment;
}

const inc = createIncrement(1);
const log = inc(); // 1
inc(); // 2
inc(); // 3

log(); // "Current value is 3"

return (
<>
<div>闭包陷阱</div>
</>
);
};

export default Trap;

第一次执行increment函数时,value.a += 1,所以value = { a: 1 }message = { a: 1 }increment函数执行完毕,并赋值给log变量。

后面执行logValue时,虽然createIncrement 函数第一次执行时创建的作用域中的message = { a: 1 },但是message的引用指向着value,所以当value = { a: 3 }时,message也等于{ a: 3 }。所以将引用类型赋值给message确实能解决闭包陷阱问题。

小结:

  1. log变量引用着increment函数第一次执行时创建的作用域,此时message = "Current value is 1"
  2. 执行三次increment函数后,value变成了 3,然后执行logValue函数而message中的value依旧为 1 的原因是value变量是个基础数据类型。
  3. 想要拿到value变量的最新值,我们只需要把value变量从基础数据类型改为引用数据类,并让message指向value变量即可。

思考以下三种不同赋值方式message的值:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
function createIncrement(i) {
let value = {
a: 0,
};
function increment() {
value.a += i;
const message = `Current value is ${value.a}`;

return function logValue() {
console.log('value: ', value); // { a: 3 }
console.log('message: ', message); // Current value is 1
};
}

return increment;
}

function createIncrement(i) {
let value = {
a: 0,
};
function increment() {
value.a += i;
const message = {
a: value.a,
};

return function logValue() {
console.log('value: ', value); // { a: 3 }
console.log('message: ', message); // { a: 1 }
};
}

return increment;
}

function createIncrement(i) {
let value = {
a: 0,
};
function increment() {
value.a += i;
const message = {
a: value,
};

return function logValue() {
console.log('value: ', value); // { a: 3 }
console.log('message: ', message); // { a: { a: 3 } }
};
}

return increment;
}

hooks 出现的闭包陷阱,其实跟上面的问题是一样的。

Hooks 中的闭包陷阱

1
useEffect(fn, [deps]);

在 react hook 中有个经典的闭包陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { useEffect, useState } from 'react';

// hooks 中的闭包陷阱
const HookClosureTrap = () => {
const [count, setCount] = useState(0);

useEffect(() => {
// 获取到的count永远是0,这是因为闭包陷阱导致的问题
// 当依赖项为空数组时,fn只会执行一次,useEffect永远引用着HookClosureTrap第一次执行时创建的作用域。第一次创建是count为0
setInterval(() => {
setCount(count + 1);
}, 1000);
}, []);

return (
<>
<h1>hooks 中的闭包陷阱</h1>
<div>count: {count}</div>
</>
);
};

export default HookClosureTrap;

在这个例子中,我们给useEffect第二个参数deps传了个空数组,并使用setInterval希望隔秒count+1,然而实际效果是首次渲染时页面中count0,然后过了 1 秒页面中的count更新为1,之后一直保持为1不变,这就是useEffect的引起的闭包问题。
因为当依赖项为空数组时,fn(useEffect 的第一个参数)只会执行一次,而 useEffect 永远引用着 HookClosureTrap 第一次执行时创建的作用域。第一次创建时 count 为 0。

解决办法

添加依赖项

添加 count 依赖项,依赖项改变时 fn 重新执行,因为 setCount 会触发重新渲染,那么我们就能获取到 HookClosureTrap 最新的作用域,也就能获取到最新的 count

1
2
3
4
5
6
// 解决办法1:添加依赖项
useEffect(() => {
setInterval(() => {
setCount(count + 1);
}, 1000);
}, [count]);

虽然获取到了最新值,但是 count 显示结果却很混乱。
这是因为每次执行 useEffect 时就会创建一个定时器timer,下次执行时,也会执行上次创建的定时器。
所以我们需要在 useEffect 中返回一个回调函数,清除本次创建的定时器。

1
2
3
4
5
6
7
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1);
}, 1000);

return () => timer && clearInterval(timer);
}, [count]);

setState 函数式更新

setState 可以接收一个函数,函数的参数是上一次计算后该状态的值,可以理解为最新值

1
2
3
4
5
6
7
8
9
10
11
12
// 解决办法2:使用setState函数式更新

useEffect(() => {
const timer = setInterval(() => {
// 这里打印的count,依旧永远是0
console.log('count: ', count);

setCount((prev) => prev + 1);
}, 1000);

return () => timer && clearInterval(timer);
}, []);

useRef

1
const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。

也就是说,使用 useRef, 可以保持变量的引用不变。
这简直跟解决闭包陷阱的办法一模一样,就是把value变量改为了引用类型。

我们修改 ref 的值后,setState 触发更新,重新渲染时,拿到的虽然是 HookClosureTrap 第一次执行时创建的作用域,ref 引用地址不变,但是它的值已经被改变了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 解决办法3:使用useRef
const countRef = useRef(0);

useEffect(() => {
const timer = setInterval(() => {
setCount(++countRef.current);

// 还有一种不常见的写法
// 对count使用前置自增运算符,也能得到正确的效果
// setCount(++count)
}, 1000);

return () => timer && clearInterval(timer);
}, []);

useReducer

useReducer 同 useRef 类似,使用引用数据类型解决这个问题

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
import { useEffect, useRef, useReducer } from 'react';

function countReducer(state, { type, payload }) {
switch (type) {
case 'add':
return {
count: state.count + payload,
};
default:
throw new Error();
}
}

// hooks 中的闭包陷阱
const HookClosureTrap = () => {
const [state, dispatch] = useReducer(countReducer, { count: 0 });

// 解决办法4: 使用useReducer
// useReducer 同 useRef 类似,使用引用数据类型解决这个问题
useEffect(() => {
setInterval(() => {
dispatch({ type: 'add', payload: 1 });
}, 1000);
}, []);

return (
<>
<h1>hooks 中的闭包陷阱</h1>

<div>state.count: {state.count}</div>
</>
);
};

export default HookClosureTrap;

除了useEffect,像useMemouseCallback都有 deps 参数,在这些 hook 里用到某个 state 时,如果 deps 为空数组,虽然 state 变了,但是却没有执行新传入的函数,依旧引用的之前的 state。

总结

createIncrement这段代码中,出现闭包陷阱的原因是,第一次执行increment后,该作用域被log变量保存(因为产生了闭包(Closure(createIncrement))所以能被保存),作用域中的Local对象中message = "Current value is 1"message是基础数据类型,所以后续打印message的值仍为第一次执行increment的值。如果希望message中的value为最新值,将value变量改为引用数据类型,并让message指向value变量即可。
react hooks 出现闭包陷阱的原因也是如此,采用添加依赖项、使用 setState 函数式更新、useRef、useReducer 都可以解决这个问题。

参考:
http://www.ferecord.com/react-hooks-closure-traps-problem.html
https://www.jianshu.com/p/6a512f78536a
https://cloud.tencent.com/developer/article/2016207
https://juejin.cn/post/6844904193044512782

Hooks与闭包

设计动机

  1. 解决组件之间状态难以复用的问题
  2. 让组件更容易理解

hooks 可以理解为更底层的实现,内部会自动帮我们管理状态,不用像ClassComponent组件一样在生命周期里写各种逻辑。

hooks能够让函数式组件拥有内部状态的基本原理,就是利用闭包的特性「闭包对象持久存在」。这让函数组件下次执行时能够获取到上次函数执行结束时state的值。

回顾下闭包的定义:闭包是一个特殊的对象。它由两部分组件,执行上下文 A 以及 A 中创建的函数 B。当 B 执行时,如果访问了 A 中的变量,那么闭包就产生了。
在 chrome 中,执行上下文 A 的函数名代指闭包。

闭包与模块

当我们定义一个 React 组件,并在其他模块在使用,这时候思考一下模块与闭包的关系:
在模 Counter.jsx 中定义一个 Counter 组件

1
2
// Counter.jsx
export default function Counter() {}

然后在 App 模块中使用 Counter 组件

1
2
3
4
5
// App.jsx
import Counter from './Counter';
export default function App() {
return <Counter />;
}

上面的代码转换成伪代码

1
2
3
4
5
6
7
8
9
10
11
const CounterModule = (function() {
return function Counter() {}
})()

const AppModule = (function A() {
const Counter = CounterModule

return funtion App() {
return Counter()
}
})()

当 App 函数执行时,访问了 AppModule 中定义的变量对象 Counter,那么闭包「Closure[A]」就产生了。

也就说,每一个 JS 模块都可以认为是一个独立的作用域,当代码执行时,该词法作用域创建执行上下文,如果模块内部,创建了可供外部引用访问的函数时,就为闭包的产生提供了条件,只要该函数在外部执行访问了模块内部的其他变量,闭包就会产生

函数组件本质上就是一个函数,当我们在一个函数组件 A 中导入另一个函数组件 B,并在 A 中执行时 B 时,闭包就会产生。

hooks 与闭包

这是一个很常规的组件,我们用伪代码来分析下

1
2
3
4
5
6
7
8
9
10
import { useState } from 'react';

function Demo() {
// 使用数组解构的方式,定义变量
const [counter, setCounter] = useState(0);

return (
<div onClick={() => setCounter(counter + 1)}>hello world, {counter}</div>
);
}

先看 useState 的伪代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function SM() {
let state = null;
return function myUseState(value) {
state = state || value; // 第一次调用没有初始值,因此使用传入的初始值赋值

function dispatch(newValue) {
state = newValue;
// 假设此方法能触发页面渲染
// render()
}

return [state, dispatch];
};
}

Demo 模块转为伪代码,代码如下:

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
// hooks与闭包
const HooksAndClosure = () => {
// 验证:myUseState执行时闭包产生了
const StateModule = (function SM() {
let state = null;
return function myUseState(value) {
state = state || value; // 第一次调用没有初始值,因此使用传入的初始值赋值

function dispatch(newValue) {
state = newValue;
// 假设此方法能触发页面渲染
// render()
}

return [state, dispatch];
};
})();

const myUseState = StateModule;
debugger;
const [counter, setCounter] = myUseState(0);

const addCounter = () => {
// 调用setCounter时也会产生闭包
debugger;
setCounter(counter + 1);
};

return (
<>
<h1>状态是如何被保存的</h1>

<button onClick={addCounter}>add Counter</button>
</>
);
};
export default HooksAndClosure;

StateModule 返回 内部函数 myUseState 赋值给 myUseState,myUseState执行时(第 22 行)访问了 StateModule 的 state 变量(第 8 行),此时闭包对象「Closure(SM)」就产生了

现在我们知道了调用 useState 会产生闭包,那么调用 setState 会不会产生闭包呢?答案是会



可以看到,调用 dispatch 时,闭包对象Closure(SM)产生了,这个闭包对象保存着上次的 state 结果。

umijs/max 设计师指南

Ant design

ant design 是蚂蚁集团开源的组件库,我们公司项目使用 ant design V5 。设计师在设计时尽量贴合该组件库设计风格,节省开发成本。
首页包括定制主题,还有与研发相结合的一些介绍,可以了解下。
pic.1708325384489
pic.1708325394155

定制主题

进入主题编辑器,页面如下,解释下该页面的功能:

  • 左侧为可视化配置面板,右侧为预览面板
  • 全局表示该配置对所有组件生效,组件则只对当前组件生效
  • 颜色可修改主题色相关变量,尺寸修改文字大小、间距,风格修改圆角、阴影
  • 设计师修改配置后,导出配置文件发给前端开发人员即可
    pic.1708325448524
    pic.1708325454691

用于设计

确定好主题变量之后,设计师可以根据主题变量的值,设置一套变量用于自己喜欢的设计平台,保持风格统一。
在设计平台上定义的变量名与 ant design 主题变量名保持一致,具体有哪些变量,参考主题编辑器左侧配置面板。

推荐设计工具: kitchen使用文档
大大提升设计师与工程师的协作效率:https://www.yuque.com/kitchen/doc/custom-token

视频合成流程图

pic.1708334059416

CrossPage.js 是一个简单的页面间通信模块,包括 CrossPageExchangeItem 类和 CrossPageExchange 类。
CrossPageExchangeItem 类用于创建一个新的通信项 exchangeItem,CrossPageExchange 类拥有 createItem/getItem/deleteItem 静态方法。

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* 简单的页面间通信的模块
*/

class CrossPageExchangeItem {
constructor(id) {
this.id = id;
this.setDataHandler = null;
this.cancelHandler = null;
}
getId() {
return this.id;
}

onSetData(cb) {
this.setDataHandler = cb;
}

setData(data) {
if (typeof this.setDataHandler === 'function') {
this.setDataHandler(data);
}
}

onCancel(cb) {
this.cancelHandler = cb;
}

cancel() {
if (typeof this.cancelHandler === 'function') {
this.cancelHandler();
}
}

remove() {
this.setDataHandler = null;
CrossPageExchange.deleteItem(this.id);
}
}

class CrossPageExchange {
static latestId = 0;
static items = new Object(null);

static createItem() {
let newItem = new CrossPageExchangeItem(this.latestId);
this.items[this.latestId] = newItem;
this.latestId++;

return newItem;
}

static getItem(id) {
return this.items[id] || null;
}

static deleteItem(id) {
delete this.items[id];
}
}

module.exports = {
CrossPageExchange,
};

WaitObject 类,它通过 Promise 提供了一种等待的机制。
wait 方法是一个异步函数,它等待 this.p 的状态被解决(resolved)。通过 await this.p ,函数回暂停执行,直到 this.p 被解决,然后返回被解决的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class WaitObject {
constructor() {
this.p = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}

async wait() {
return await this.p;
}
}

module.exports = {
WaitObject,
};

小程序异步模型

pic.1708334106626
pic.1708334112205

code review

一直觉得我主管浩哥是个很强的技术大佬,正好他让我接手了一个小程序项目,那么就来学习一下他写的代码吧!

小程序

data 应只包括渲染相关的数据,所以这里如果只是为了保存定时器 ID,没必要使用 setData

pic.1708405990424
pic.1708405995750

建议将定时器 ID 保存在 this 上即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Page({
timerId: null,

onLoad: function () {
// 初始化定时器
this.timerId = setInterval(() => {
console.log('定时器在运行');
}, 1000);
},

onUnload: function () {
const { timerId } = this;
if (timerId) {
// 清除定时器
clearInterval(timerId);

// 将定时器ID设置为null
this.timerId = null;
}
},
});

wx.request 封装

为什么执行reject()之后,还要执行 resolve()
pic.1708406004196

看到这里时,我有点疑惑,为什么执行reject()之后,还要执行 resolve()
执行reject()之后,Promise 状态已经被固定了,再执行resolve()也没有意义。打了个断点(**f11 **step into next function call )看下,发现resolve(res)并没有执行。

为了不造成混淆,建议在res.data.code !== 0代码块末尾加个return,或者在reject()前面加个return
pic.1708406013027

错误处理:通过自定义的错误类 ErrorRequestErrorBizErrorAuth 对不同类型的错误进行了分类
pic.1708406020699

看下控制台:
pic.1708406026545

并没有提示错误信息输出。父类构造函数Error起到了什么作用?

点击 Error,如图所示:
pic.1708406032344

可以看到构造函数ErrorConstructor接收一个 message 参数,那我们把接口的错误消息传递进去看下,修改下代码
pic.1708406042041

效果如下:
pic.1708406047672

修改过后在控制台展示了更详细的错误信息。

将后端的数据复制给 this.data this.data = data这点挺好的,这样,在捕获到错误之后,可以通过error.data来获取后端给的信息。

但是我没看到他是怎么使用的,所以我在错误类上加了个获取 this.data 的方法,方便在业务层面获取到:如下

1
2
3
4
5
6
class ErrorBiz extends ErrorRequest {
// 添加一个方法,用于获取业务错误信息
getBizErrorData() {
return this.data;
}
}

业务中使用:

1
2
3
4
5
try {
const age = await api.getPhotoCheckAge(phoneUrl);
} catch (err) {
const bizErrorData = err.getBizErrorData();
}

css

指向特定元素,通过附加类的预选元素的选择器,中间没有空格。如.title.active
pic.1708406055781

适配刘海屏设备安全区域,保证内容不被遮挡。

  1. padding-bottom: env(safe-area-inset-bottom);:
    • env() 函数是 CSS 中的一个环境变量函数,用于获取设备环境变量的值。
    • safe-area-inset-bottom 表示底部安全区域的大小。
    • 这行样式代码的作用是设置底部内边距为底部安全区域的大小。
  2. padding-bottom: constant(safe-area-inset-bottom);:
    • constant() 函数是 CSS 中的一个函数,用于获取一个常量的值。
    • safe-area-inset-bottom 同样表示底部安全区域的大小。
    • 这行样式代码的作用也是设置底部内边距为底部安全区域的大小,但使用了 constant 函数。

pic.1708406115148

任务进度条
后端提供任务总时长,过了多少时间除以总时间就可以算出当前进度,Math.round((Date.now() - startTime) / totalTime * 100)
pic.1708406070351

Js

列表滚动加载,判断是否已全部加载结束
const isLoadEnd = respList.length < this.data.pageSize,也就说,如果本次请求响应的条数小于本次请求的条数,就可以表示已全部加载结束
pic.1708406077622
不需要后端给你总页数pages/total,再根据const isLoadEnd = current < pages 或者 const isLoadEnd = showList.length < total判断是否已全部加载结束

音标

主要音标体系

国际音标

pic.1708334606737

英音音标

DJ 音标: Daniel Jones

pic.1708334613426

新版 DJ 音标,也被叫做 Gimson 音标

pic.1708334620624

IPA63:1963 年版本的 DJ 音标

IPA88(Gimson):1988 年版本的 DJ 音标

pic.1708334639629

美式音标

KK 音标

在台湾地区被广泛使用,基本也只在台湾使用
pic.1708334722865

美式英语学哪种音标?

DJ 音标 。 虽然是用来标记英式英语的,但是用来标记美式英语也毫无压力
pic.1708334742662
元音:

iː ɪ e æ ɑː ɔː ɒ ʌ ɜː ə
uː ʊ əʊ ɔɪ ɪə ʊə
无声子音 p t k f s θ ʃ
有声子音 b d g v z ð ʒ
其他子音 m n ŋ lɫ r h w j

发音过程

浊音:气流向上,振动声带发出的音
轻音:气流向上,不振动声带发出的音
元音:气流通过口腔时不受阻碍发出的音,也就是舌头不与任何口腔部位接触,嘴唇也不能闭拢
辅音:与元音相反
pic.1708334772339

元音三要素

圆唇度、舌位高低、舌位前后
pic.1708334782287

元音图:以视觉的方式表现元音三要素。
能抽象地描述咱们“感知到”的元音
pic.1708334790903
四边形之所以上大下小,是因为区分高元音和低元音舌位横向(前后)差别的大小

辅音

辅音发音分为三阶段

成阻(catch):气流被阻塞
pic.1708334803619

持阻(hold):气流持续向外冲,但仍被阻塞
双唇闭拢,但仍向外吐气(从而形成气压)
pic.1708334810242

除阻(release):阻塞被去除,气流冲出
pic.1708334815988

影响辅音发音的三要素

清浊、调音部位、调音方式

清浊

清浊,也就是声带是否振动
中文没有清浊之分

清音:发音时声带不振动的音
浊音:发音时声带振动的音

调音部位/发音部位

调音部位/发音部位,也就是发辅音的时候,阻碍气流所用到的主要发音器官
pic.1708334825883
pic.1708334831822

调音方式

也可以叫做发音方式/发音方法,可以理解为发音器官怎么阻挡气流(成阻),以及怎么释放气流(除阻)而发出辅音的

塞音/爆破音

如/b/ 在成阻时完全阻塞气流再释放而发出的音,一般被称作“塞音”(stop;读 se,即阻塞的“塞”),或爆破音(plosive;学术界也称“爆发音”)

擦音/摩擦音(fricative)

如 “斯”的拼音[si],用舌尖和牙齿形成一个缝隙,让气流通过,从而发出响声

鼻音(nasal)

如“呢”的拼音[ne],需要我们把气流送入鼻腔才能发出,这种称为“鼻音”(nasal)

还有其他音,如:颤音、闪音、近音、边近音(边音)等

音标

/p/ 清辅音

调音方式属于“塞音”
pic.1708334840624
p 的三种常见情况(音位变体):

  • 送气:单词如 peak
  • 不送气:如 speak ,p 读 ‘b’,“斯比克”
  • 无声除阻:如 trapdoor

要想明确属于哪种情况,可以查看国际音标的严式注音,**[]**表示国际音标
pic.1708334900568

/b/ 浊辅音

调音方式属于“塞音”
/b/ 声带不振动。
感觉声带有振动,是/b/后面的元音振动

/t/ 清辅音

调音方式属于“塞音”
音位变体:

送气 t + 元音 top /tɑːp/
不送气 s + t stop /stɑːp/ 和拼音”du”的声母”d”一样
无声除阻 t + 某些辅音 setback forget me t 不发音
闪音(flap) 元音之间的字母 t 在非重读音节 water better a lot of
鼻音除阻 /t/ + /n/ eaten button certain
省略 /n/ + /t/ t 可省略不读。winter 听上去像 winner。不省略也行

/d/ 浊辅音

调音方式属于“塞音”
音位变体

无声除阻 landmine
闪音 medal
鼻音除阻 sudden
边音除阻 badly

/k/ 清辅音

调音方式属于“塞音”
调音部位为“软腭”
pic.1708334984742
音位变体

送气 k + 元音 kite /kaɪt/ 和”开”的声母一样
不送气 s + k sky /skaɪ/ 和”该”的声母一样
无声除阻 k + 某些辅音 doctor /‘dɑːktər/

/g/ 浊辅音

/k/ /g/ 中国人最常见的错误就是在末尾加元音
pick /pɪk/ 匹克
pig /pɪg/ 逼格

/m/ 浊辅音

常见错误发音:
很多人在/m/后加一个不存在的元音/uː/或者/ʊ/,如:
room /ruːm/ 乳母
home /hoʊm/ 后母
room /tuːm/ 兔母

/n/ 浊辅音

pic.1708335132190
pic.1708335140937
pic.1708335150867
pic.1708335157571
pic.1708335163474

/ŋ/ 浊辅音

注意后鼻音
错误发音:
pic.1708335179080

/f/ 清辅音 /v/ 浊辅音

/s/ 清辅音 /z/ 浊辅音

/θ/ 清辅音 /ð/ 浊辅音

θ时,舌尖在上下齿之间(被轻轻咬住)
/ʃ/ 清辅音 /ʒ/ 浊辅音
/ʃ/ 上下齿不闭合。拼音“史”的调音部位更后一点,更接近硬腭,可以理解为更卷舌
/ʒ/ 可以理解为声带振动的 /ʃ/
pic.1708335220344
pic.1708335225855

// 清辅音 // 浊辅音

// 属于“塞擦音”(阻塞+摩擦)
“吃”的调音部位更后一点,可以理解为更卷舌

// 可以理解为声带振动版的 //

pic.1708335235178
pic.1708335239059

/h/ 清辅音
调音部位为声门

/w/ 浊辅音

和普通话“我”的声母一样

/r/ 浊辅音

音位变体:

齿龈近音 r + 元音 浊辅音 right /raɪt/
r 化 元音 + r 这也是美音最大特征之一
car /kɑːr/ her /hɜːr/

/j/ 浊辅音

pic.1708335252017
pic.1708335258358
pic.1708335261737
pic.1708335265971

/l/
3 种音位变体:
清晰 L:舌边音,是个浊辅音
pic.1708335279609
pic.1708335283717
pic.1708335287884
pic.1708335292155
不发音 L:没规律

/ts/ 清辅音 /dz/
/dz/是浊辅音,可以理解为声带振动板的 /ts/

pic.1708335304594
pic.1708335308829
pic.1708335314157
pic.1708335318842

/tr/ 清辅音 /dr/ 浊辅音
pic.1708335326300
pic.1708335331928
pic.1708335339170
pic.1708335347923
pic.1708335353758

BEM规范

BEM 规范是什么

BEM 是 块(block) 、元素(element)、修饰符(modifier)的简写,由 Yandex 团队提出的一种 css 类命名约定。

  • 连字符:表示某个块或块子元素的多单词之间的连接符号

__ 双下划线:连接块和块子元素

– 双连字符:作为块或块子元素的修饰符号

以这段 html 为例:

1
2
3
4
5
6
7
8
9
10
11
12
<div class="article">
<div class="article__body">
<div class="article__body-left">
<div class="article__body-logo">
<img class="icon" />
</div>
</div>
<button class="button button--primary"></button>
<button class="button button--default"></button>
</div>
<div class="article__footer article__footer--primary"></div>
</div>

block

block 表示组件的顶级抽象,它仅仅作为一个边界,不应该在 block 上添加样式和修饰

1
2
3
4
5
6
7
8
9
10
11
// 正确
.article {
&__body {
background: red;
}
}

// 错误
.article {
background: red;
}

element

element 表示 block 下的子元素,该元素依赖于块。

1
2
3
4
5
6
7
.article {
&__body {
}

&__footer {
}
}

如果元素下还嵌套子元素,均可用 **-** 连接,这样可以清楚的知道该元素的父级。

**.icon****.button** 这种可以独立存在的子元素,避免创建不必要的父级。
假如把 .button 命名为 .article__body-button ,后续的开发人员要在** **.article__footer 里用 .article__body-button 的样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div class="article">
<div class="article__body">
<div class="article__body-left">
<div class="article__body-logo">
<img class="icon" />
</div>
</div>
<button class="article__body-button button--primary"></button>
<button class="article__body-button button--default"></button>
</div>
<div class="article__footer article__footer--primary">
<!-- 假设这里要用到article__body-button的样式 -->
<button class="article__body-button"></button>
</div>
</div>

可以看到,.article__body-button.article__footer 下,这会使代码变得混乱和不一致,应该避免这种情况的出现。

modifier

修饰符,改变块或元素的样式。
.button--primary.article__footer--primary

BEM 规范的优点

  1. 当想要创建新组件时,我们可以容易的知道哪些修饰符和子组件已存在
  2. 从 html 结构上,能快速知道元素的依赖关系,如,我们用连接符 - 表示元素下的嵌套
  3. 统一的命名方式,方便团队成员的阅读

总结

推荐用法:

1
2
3
4
5
6
7
8
9
10
11
12
<div class="article">
<div class="article__body">
<div class="article__body-left">
<div class="article__body-logo">
<img class="icon" />
</div>
</div>
<button class="button button--primary"></button>
<button class="button button--default"></button>
</div>
<div class="article__footer article__footer--primary"></div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.article {
&__body {
&-left {
}
&-logo {
}
.icon {
}
.button {
&--primary {
}
&--default {
}
}
}
&__footer {
&--primary {
}
}
}
  • block 不要添加修饰符和样式
  • element 下,子元素均用连接符 - 连接
  • element 下,没有后代的元素可用单个单词表示,方便复用

BTC-共识协议

数字货币存在的问题

数字货币本身为带有签名的数据文件,可以进行复制。即:对用户来说,可以将同一货币花费两次。这就是“双花攻击”。
如何解决:对货币添加唯一编号(不可篡改),每次支付向货币发行单位查询真伪。该方法每次交易都依赖于第三方机构来判断货币真伪且防止双花攻击。这是一个典型的第三方中心化方案。
现实中,我们的交易往往依赖于一个可信的第三方机构(支付宝、微信),这个机构具有较高的可信度,有政府背书,所以可以采用这种方案。
但是,很多场景下,并不存在这样一个可信赖的第三方机构。基于这个背景,以去中心化思想为核心的比特币系统便吸引了人们的注意力。

去中心化需要解决的问题

  • 货币由谁发行,如何发行,什么时候发行?

在比特币系统中由挖矿来决定货币发行权的发行量

  • 如何验证交易是否有效?如何防止双花攻击?

该问题的解决,依赖于系统中维护的一个数据结构,记录货币的使用情况(是否被花过,被谁花过?)。该数据结构由系统中全体用户共同维护,保证了交易的有效性。该数据结构,便是区块链。
该数据结构中,有两种哈希指针,一种指向前面的区块,使得形成区块链,第二种(红色部分)则是为了说明比特币的来源。说明比特币的来源非凭空捏造,可以防止双花攻击。

如下,假如 A 获得铸币权,并发布了 10 个比特币(该交易称为铸币交易)。
pic.1708405674927

在进行交易时,A 给 B 转账,需要付款人的签名和收款人的地址(公钥的哈希)。收款方需要知道付款方的公钥(为了验证签名是否有效。实际上其他节点都需要知道付款方的公钥,验证交易合法性)。

这里存在一个问题:
:::info
假设 B 的同伙 B’,B’ 伪造一笔 A 到 B 的转账交易,用自己的公钥说是 A 的公钥,然后用假造的私钥签个名,别的节点收到这笔交易后,要假造的公钥去验证这个签名。验证结果肯定是对的,这让别的节点以为这个交易是合法的,这就出问题了。相当于把 A 账上的钱偷走了。
:::

解决这个问题的关键,就是第二种哈希指针。虽然 B’伪造了 A 到 B 的交易,但是实际中 A 转账的时候提供的公钥需要和铸币交易中的公钥对的上,如果对不上,说明该交易不合法。

比特币区块信息

区块信息可以分为两部分:block header 和 block body
block header 由以下信息:

  • version(版本协议)
  • hash of previous block header(指向前一个区块指针)
  • merkle root hash(默克尔树根哈希值)
  • target(挖矿难度目标阈值)
  • nonce(随机数)

hash of previous block header 只计算区块头部部分的哈希(merkle root hash 保证了 block body 内容不被篡改,所以只需要计算 block header 即可保证整个区块内容不会被篡改)。
区块链中,轻节点(只存储区块 block header 信息)只利用区块链,并不参与区块链系统的维护和构造。

比特币共识协议

投票方案:可行的投票方案前提是系统中的大多数节点时“好”的节点,在这种情况下进行共识协议设置。
比特币系统中,若直接投票,将某节点打包交易到区块,将其发给其他节点,其他节点检查该候选区块,检查若正确投赞成票,若票数过半,则加入区块链。存在以下问题:

  1. 恶意节点不断打包不合法区块,导致一直无法达成共识
  2. 无强迫投票收到,某些节点可以不投票(行政不作为)
  3. 网络延迟未知,投票需要等待多久?
  4. 最重要的问题,比特币系统中任何人都可以创建账户,且建账户及其简单,若黑客使用计算机专门生成大量公私钥对,当其产生超过系统中一半数目,就可以获得支配地位(女巫攻击)

所以比特币系统并没采用这种简单的账户数目方案,而是依据算力进行投票。每个节点都可以自行组装一个候选区块,然后尝试各种 nonce 值,这就是挖矿。当某个节点找到符合要求的 nonceh(block header)<=target,便获得了记账权,从而可以将区块发布到系统中。其他节点收到区块后,验证区块合法性,如果绝大多数节点验证通过,则接口该区块为最新区块并加入到区块链。

比特币激励机制

节点为什么要提供算力和电力成本去竞争记账权?
比特币引入奖励机制,通过设置出块奖励来解决该问题。
一个获得合法区块的节点,可以在去区块中加入一个特殊交易(铸币交易)。这也是唯一一种产生新比特币的途经。

比特币系统规定,起初每个区块可以获得 50 个比特币,但之后每隔 21 万个区块,奖励减半

区块中保存交易记录,如果仅仅设置出块奖励,那么,会不会存在节点只想发布区块获得出块奖励而不想打包交易?
比特币系统设计了 Tranction fee(交易费),对于获得记账权节点来说,除了出块奖励之外,还可以得到打包交易的交易费。但目前来说,交易费远远小于出块奖励。等到未来出块奖励变少,可能区块链的维护便主要依赖于交易费了。

BTC密码学原理

比特币(bitcion):基于去中心化,以区块链作为底层技术的加密货币。

hash(哈希)

hash,可以简单理解为对某一事物的投影操作,即A-->Hash(A)
哈希函数(cryptographic hash function)主要有三个特性:

  • collision resistance(碰撞反抗)
  • hiding(隐秘性)
  • puzzle friendly(谜题友好)

哈希碰撞:
给定一个哈希函数,不同的输入,得到相同的输出,则称为 hash 碰撞。
如:给定 x 和 y,且 x!=y,可以得到 Hash(x)=Hash(y)。

collision resistance

collision resistance(碰撞反抗):表示对哈希碰撞具有抗碰撞性
在实际应用中,哈希碰撞基本上难以避免,我们只要保证给定 x,很难找到一个 y,能够在 x!=y 的前提下,使得 Hash(x)=Hash(y),就认为其是 collision resistance 的。

用处:如果我们自己有一条信息 x,我们希望别人知道我有 x 但不想让别人知道 x 具体是什么,就可以通过告诉其 Hash(x),由于该性质,保证了 x!=y 时,Hash(x)和 Hash(y)是不相等的。我们只需要告诉别人 Hash(x)即可,对方可以通过 Hash(x)知道你确实知道 x 这个信息,但他无法(很难)通过 Hash(x)反推出 x。

hiding

我们认为,给定 x 和 Hash(),可以很容易得到 Hash(x),但没有办法在已知 Hash(x)和 Hash()的情况下,反推出 x 的具体取值,当然这也是一个理想的情况。

digital commitment(数据保证):collision resistance + hiding
在视频中,肖老师提到关于股市预 测的案例,某个人对某个股票进行涨停预测,我们如何保证能够知晓其预测是否准确?最简单的是提前公布, 等待实际结果出现后验证。但实际中,当提前发布预测后,可能会由于预测者本身对股市实际结果造成影响。 所以,应该将提前将其写于纸上并密封,交给第三方机构保管,等到实际结果出现后开启密封与实际对比,这 就是 digital commitment。而第三方机构需要能够使人信服,在实际生活中,有很多场景并不存在一个这样的 第三方机构,而区块链技术正为此提供了一个很好的解决方法。 我们把预测结果看作 x,提前公布 Hash(x),等 到预测结果发生时间来临后,公布 x,如果根据 x 可以得到公布的 Hash(x),则说明公布的 x 确实为所预先预测的 内容。从而,我们可以实际进行判断预测是否准确。实际使用中,为了 x 足够大,会对 x 进行“加盐”,对 x 拼接一 个 nonce,对其整体取 Hash## puzzle friendly
在比特币系统中,还需要第三个性质 Puzzle friendly。该性质要求哈希值计算事先不可预测, 仅仅根据输入很难预测出输出。例如:我们需要一个哈希值,存在于某一个范围内,只能通过不停运算查找出来。
该性质保证了比特币系统中,只能通过“挖矿”获得比特币。
:::info
挖矿:试图通过暴力手段不断尝试,直到得到一个符合约定规则的 hash 值。这也是为什么挖矿是一个非常消耗算力的过程。
:::
也就是说,该性质保证了工作量证明(POW)机制 可以运行下去【“挖矿难,但验证易”】。

SHA-256:
在比特币系统中采用 SHA-256 哈希函数。该函数具备以上三个特性。
sha256 具有极强的抗碰撞性。目前为止,世界上最强的超级计算机也不具备人为制作碰撞的能力。

签名

比特币中账户管理 在第三方中心化系统中,账户开通依赖于第三方。但去中心化的比特币系统中,很明显不能 进行“申请账户”。
在比特币系统中,申请账户是用户自己来处理的,即自己创建一个公钥-私钥对(来自于非对称加密体系 asymmetric encryption algorithm)。公钥和私钥的应用保证了“签名”的应用。

1
2
对称加密体系:提前商量好一个秘钥,加密和解密采用同一个秘钥。
存在问题:秘钥显然不能以明文的方式在网络上传输,容易被盗取。

非对称加密体系:
用接收方的公钥加密,接收方的私钥解密。这就解决了对称加密体系中秘钥分发的问题
pic.1708405707415

当在比特币网络中进行转账时,通过 “签名”可以明确是由哪个账户转出的,从而防止不良分子对其他账户比特币的盗取

1
加密,解决的是秘钥分发的问题;签名,解决的是明确由哪个账户转出的问题

在发布交易时,通过发起方私钥签名,其他人可以根据发起方公钥进行验证,从而保证该交易由发起方发起

也就是说,只有拥有私钥,才能将该账户中的比特币转走。 【注意:比特币系统中,很难通过生成大量公私钥对来获取他人私钥】

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×