BTC数据结构

hash pointer(哈希指针)

在传统链表里,通过指针将各个节点串联起来。
指针指的是一个节点对象在内存中的首地址。

pic.1708405846740

顾名思义,区块链也是一种链表结构,节点为区块。不过区块链并未采用指针,而是使用了哈希指针。
如下图所示,该节点有两个指针指向这个节点(实际上为一个),其中 p 为该节点的地址,H()为该节点的哈希值,该值与节点中内容有关。当节点(区块)中内容发生变化,该哈希值也会发生改变,从而保证了区块内容不能被篡改。

pic.1708405882862

在比特币系统中,其最基本的数据结构便是一个个区块形成的区块链

如图所示,每个区块根据自己的内容生成自己的哈希值,此外,每个区块(除创世纪块)都保存有前一个区块的哈希值。这保证了区块内容不被篡改。

pic.1708405909365

如果我们篡改 A 的值,而 B 中保存有 A 的哈希值,所以 B 也得进行修改,同样 B 后的区块也得修改。
这就是区块链区别与普通链表的区别:普通链接修改节点不会影响其他节点,而区块链牵一发而动全身,后面的节点都得跟着改。

所以用户只需要保存最后一个区块的哈希值,就可以检测出区块链上内容是否被篡改过。

由于这个特性,那么对于个人节点而言,就不需要完整的保存链上的所有的节点内容,只需要保存常用的父级几千个节点即可。

merkle tree(默克尔树)

merkle tree 是比特币系统中一个重要的数据结构。区别于 binary tree,merkle tree 用哈希指针代替了普通指针。
一个简单的 merkle tree:

pic.1708405931818

tree 中,A B C D 表是一个个交易(tx),A 和 B 各有一个哈希值 H(1)和 H(2),将其合并放在一个节点中,C 和 D 同理,然后针对得到的两个节点又可以得到新的哈希值,即为图中根节点。实际中,会对图中的根节点再取一次哈希,也就是 root hash。
该数据机构的优点在于:只需要记住 root hash,便可以检测出对树中任何部位的修改。

区块:
每个区块分为两部分,包括块头(block header)和块身(block body)。
block header 中存储的是 root hash,但是 block header 没有交易的具体内容,block body 里存储的是交易列表。

merkle tree 的实际用途:可以用于提供 merkle proof。

区块链中的节点分为轻节点全节点。全节点保存整个区块的内容(block header 和 block body),而轻节点仅保存区块的块头信息(block header)。
那么就存在一个问题,轻节点想知道某个交易是否被写入到区块链上,该怎么办?打个比方,你向我转钱,我是个轻节点,我怎么知道这个交易已经被写到区块链里呢?

轻节点没有 block body,只有 root hash,那怎么证明某笔交易包含在这个 merkle tree 里呢?这就用到了 merkle proof。

merkle proof

从交易一直到根节点的路径,就属于 merkle proof。

有了 merkle proof,轻节点就可以算出 merkle tree 里是否包含某笔交易。

1
通过 merkle proof,可以验证计算得到的 root hash 是否与轻节点 block hader 里的 root hash 是否一致,如果一致说明该交易包含在 merkle tree 上。

ETH-以太坊概述

BTC 和 ETH 是最主要的两种加密货币,BTC 称为区块链 1.0,以太坊称为区块链 2.0。
以太坊对比特币设计中的某些不足进行了改进,比例:出块时间、共识协议、mining puzzle(对内存要求高,反 ASIC 芯片使用) ,并且用权益证明(POS)替代工作量证明(POW)。除此之外,以太坊还增加了对智能合约(smart contract)的支持。

为什么要开发智能合约

BTC 是一种去中心化的货币,在比特币取得成功之后,很多人开始思考:如果货币可以去中心化,还有什么可以去中心化。
以太坊的一个特性就是增加了对去中心化合约的支持。

BTC 货币中的最小单位为“聪”,Satoshi。ETH 的最小单位为“Wei”.

去中心化的合约

去中心化货币本身由政府发行,政府公信力为其背书,BTC 通过技术手段取代了政府的职能
现实生活中,我们经常提到的“契约”或“合约”,其有效性也是需要政府进行维护的,如果产生纠纷,政府合法性合同进行判决。ETH 的设计目的就是,通过技术手段取代政府对于合约的职能

去中心化的合同有什么好处:
若合同签署方并非一个国家,需要跨国转账,没有统一的司法管辖权(如:众筹)。如果可以编写无法修改的合约,一旦发布到区块链上,所有人(包括写智能合约的作者)只能按照制定的规则执行。

K线图

技术难点

value 对应的纵坐标

已知数据里的最大值maxValue、最小值minValue、y 轴的高度yAxisHeight,那么就可以得到 value 与yAxisHeight的比例ratio。想要得到刻度对应的数值,只需要将最小值+刻度间距iratio即可。
比如最大值 1000,最小值 500,y 轴高度为 100px,那么 1px 所代表(1000 - 500) / 100,即 5。
如果 y 轴刻度间距为 30px,那么对应的数值为500 + 30*i*ratio

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 计算Y轴刻度对应的数值:根据最大最小值动态变化
* @param {number} i 刻度对应下标
* @param {number} maxValue 最大值
* @param {number} minValue 最小值
* @param {number} yAxisHeight y轴高度
* @param {number} yAxisTickSpace y轴刻度间距
* @returns number 刻度对应的数值
*/
export const yAxisTickText = (
i,
maxValue,
minValue,
yAxisHeight,
yAxisTickSpace,
) => {
// ratio表示value 与 y轴高度的比例
const ratio = (maxValue - minValue) / yAxisHeight;
const value = (minValue + yAxisTickSpace * i * ratio).toFixed(2);
return value;
};

x 轴元素动态隔点展示

x 轴元素的数量是处于变化的,但 x 轴宽度是已知的,这样我们就能算出元素之间的间距xAxisItemSpace是多少。知道了xAxisItemSpace,那我们就能知道 x 轴刻度横坐标。

1
2
3
4
5
6
7
8
9
10
/**
* 求x轴刻度横坐标
* @param {number} i 下标
* @param {number} xAxisPointX x轴原点横坐标
* @param {number} xAxisItemSpace x轴刻度间距
* @returns number x轴刻度横坐标
*/
export const xAxisTickPointX = (i, xAxisPointX, xAxisItemSpace) => {
return xAxisPointX + i * xAxisItemSpace;
};

缩放时,考虑当元素太多时,如果展示所有的元素,会出现拥挤的情况。所以我们只能展示部分元素。
隔点展示:

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
// 绘制x轴刻度与文字
const xAxisItemMaxShowNumber = 4; // 展示个数
const remainder = Math.ceil(xAxisItemLength / (xAxisItemMaxShowNumber - 1));
for (let i = 0; i < xAxisItemLength; i++) {
const xAxisTickX = xAxisTickPointX(i, originalPointX, xAxisItemSpace);

// 隔点展示
if (i % remainder === 0 || i === xAxisItemLength - 1) {
renderText(
ctx,
xAxisTickX,
yAxisOriginPointY + tickWidth + 10,
myDataSource.current.map((x) => x.date)[i],
'center',
TEXT_COLOR.PRIMARY,
);
renderLine(
ctx,
xAxisTickX,
yAxisOriginPointY,
xAxisTickX,
yAxisOriginPointY + tickWidth,
COLOR.LINE,
);
}
}

三次贝塞尔曲线前后控制点

受前后元素纵坐标影响,由前后两个点和当前点的纵坐标构成一个平行四边形,即可得到当前元素的前后控制点。

还需要考虑首尾元素没有前后控制点的边界问题,所以要加入两个虚拟点。

辅助线

在 canvas 里,更新画布既是重选渲染整个画布,所以辅助线的绘制采用分层处理,创建一个新的画布覆盖上去,独立开来,不影响展示画布。

  1. 监听鼠标移动事件mousemove
  2. 清除画布
  3. 如果在 gird 区域,绘制辅助线和提示框

拖拽

  1. 监听鼠标按下事件mousedown,并创建拖动元素。可以做记忆化处理(优化手段)
  2. 监听开始拖动目标元素事件dragstart,并记录光标位置,即event.offsetX
  3. 监听拖动事件drag,拖动过程中,达到一定距离,清除画布(clearRect),然后更新要展示的数据,重新渲染即可
  4. 拖动结束时,需要隐藏拖动元素,并且如果左侧临时集合数据小于页数pageSize请求接口数据

缩放

  1. 监听滚轮事件wheel
  2. 放大event.deltaY > 0时,删除展示集合dataSource前后数据,并分别扔到存储被删除数据的临时集合里,直到最小展示条数
  3. 缩小时「尽可能多的展示数据」,分两个情况,
    1. 当已展示条数大于最大展示条数时 或 左侧临时集合条数小于最小展示条数时,请求接口并将请求数据合并到左侧临时集合中
    2. 删除临时集合的数据,并扔到展示集合中
  4. 处理完数据,清除画布,重新渲染

应用层的思考

数据如何来:
初始时,默认展示为 10 条pageSize=10,最大展示条数为 20 条maxShowSize = pageSize*2,所以需要预准备 30 条数据pageSize+maxShowSize,也就是首次加载 30 条数据。
更新时

  1. 拖拽什么时候请求新数据:拖拽结束后请求新数据

因为一次最多拖maxShowSize当左侧临时集合数据小于**maxShowSize**,请求接口数据,请求maxShowSize

  1. 缩放什么时候请求新数据:缩放结束后,当左侧临时集合数据小于**maxShowSize**

因为没有缩放结束事件,我们可以观察下缩放时的时间间隔,在wheel事件里打印Date.now(),发现滚动时间间隔在 200ms 以内,保险起见我们取个 500ms。
当滚动时间超过了 500ms,我们就判断为滚动结束:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let timer = null;

kWrapNode.addEventListener('wheel', function (e) {
// 1. 先判断是否停止 2.停止后做什么
if (timer) {
clearTimeout(timer);
}
// 模拟缩放结束事件
const wheelStop = () => {
// 滚动停止时执行的代码
console.warn('wheelStop');
if (leftDataSource.length < maxShowSize) {
// 请求数据
loadData(maxShowSize, dataSource[0].date).then((res) => {
leftDataSource = [...res, ...leftDataSource];
});
}
};
timer = setTimeout(wheelStop, 500);
});

Map和Set、Map和Object的区别

Map 和 Object 的区别

特性上的区别:

Map 更适合”键值对”这种数据结构。Object 的键只能是 string、整数、Symbol 类型,Map 的键可以是任意类型。

Map 实现了迭代协议,可用 for…of 遍历,而 Object 不行, Object 是不可迭代的。

Map 可用 size 属性获取长度,而 Object 没有获取长度的属性。

Map 遍历顺序可以保证,按插入时输出。
而 Object 无法保证,Object 的遍历顺序为:

  • 当 key 类型为Number,按 key 从小到大排序
  • 当 key 类型为String,会被转为Number类型,按 key 从小到大排序
  • 当 key 类型为Symbol,按创建的时间升序排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let obj = new Object();
obj['jack'] = 1;
obj[0] = 2;
obj['4'] = 3;
obj[5] = 3;
obj['2'] = 3;
obj[1] = 3;
obj['tom'] = 4;
obj['toni'] = 2;

//填入Object的元素key是自动按照字符串排序的,数字排在前面,且升序拍戏,字符串在后
for (let k in obj) {
console.log(k); // 0 1 2 5 jack tom toni
// console.log(typeof k); // 都是string,因为都会调用对象的toString()方法, key.toString()
}

当数据量大(长度为十几万以上)的时候,Map 的读入、写入、删除会比 Object 更快,数据量小的时候差别不多,object 稍微好一点点点

Map 的使用场景:

  • 聊天内容与聊天列表
    • 数据量大、频繁的写入、更新、对写入顺序有要求
  • 策略模式
    • 用于储存多个键值对应一个规则的情况

Map 和 Set 的应用

set:

  1. 去重
1
2
3
4
5
// 写法一
Array.from(new Set([1, 2, 3, 3, 4])) // [1,2,3,4]

// 写法二
[...new Set([1, 2, 3, 3, 4])] // [1,2,3,4]
  1. 取并集、交集、差集
1
2
3
4
5
6
7
8
9
10
11
const arr1 = new Set([1, 2, 3, 4, 5]);
const arr2 = new Set([1, 5, 6]);

//并集
new Set([...arr1, ...arr2]);

// 交集
new Set([...arr1].filter((item) => arr2.has(item)));

// 差集
new Set([...arr1].filter((item) => !arr2.has(item)));

map:
可以使用任意的数据类型作为键,比对象更合适

Array.from

可以将类数组和可遍历(iterator)对象转换为真正的数组。

1
Array.from(new Set([1, 2])); // [1, 2]

Map 和 Set 的区别

不同点

Set 结构类似于数组,成员唯一,没有排序的概念。Map 结构它保存的是键值对的集合。

添加成员的方法不同:
Set 使用 add 方法 ,如:set.add(6),Map 使用 set 方法,如 map.set(‘key’, ‘value’)

获取成员的方法不同:
Set 结构没有排序的概念,无法获取单个成员。Map 结构使用 map.get(‘key’) 获取成员的值。

相同点

Map 和 Set 都是 es 的新增的数据结构。

拥有同样的遍历方法:
keys()、values()、entries()、forEach()

Promise

同步与异步

同步是指发起一个请求时,如果未得到请求结果,代码逻辑将会等待,直到结果出来才会继续执行之后的代码。

异步是指当发起一个请求时,不会等待请求结果,直接继续执行后面的代码。请求结果的处理逻辑,会添加一个监听,等到反馈结果出来后,在回调函数中处理对应的逻辑。

使用 Promise 模拟一个发起请求的函数,该函数在 1s 之后返回数值 30。

1
2
3
4
5
6
7
8
9
function fn() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(30);
}, 1000);
});
}

fn().then((res) => console.log(res)); // 输出 30

在该函数的基础上,我们可以使用 async/await 来模拟同步的效果。

1
2
3
4
5
6
7
8
9
10
var foo = async function () {
var res = await fn();
console.log(res);
console.log('next code');
};
foo();

// 输出结果
// 30
// next code

而异步效果则会有不同的输出

1
2
3
4
5
6
7
8
9
10
var foo = function () {
fn().then((res) => {
console.log(res);
});
console.log('next code');
};

// 输出结果
// next code
// 30

ajax

ajax 是网页与服务端进行数据交互的一种技术。
我们可以通过服务端提供的接口,利用 ajax 向服务端请求需要的数据

整个过程的简单实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var url = 'http: www.demo.com/user/info';

var result;

var XHR = new XMLHttpRequest();
XHR.open('GET', url, true);
XHR.send();

XHR.onreadystatechange = function () {
if (XHR.readyState == 4 && XHR.state == 200) {
result = XHP.reponse;
console.log(result);
}
};

这看起来没什么麻烦的,但是这时候,如果我们还需要做另一个 ajax 请求,这个请求的参数是从上一个 ajax 请求中获取的,那么我们就不得不如下这么做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var url = "http: www.demo.com/user/info"

var result;

var XHR = new XMLHttpRequest()
XHR.open('GET', url, true)
XHR.send()

XHR.onreadystatechange = function () {
if (XHR.readyState == 4 && XHR.state == 200) {
result = XHP.reponse
console.log(result)

// 伪代码2
var url2 = 'http:xxx.yyy.com/zzz?ddd=' + result.someParams;
var XHR2 = new XMLHttpRequest();
XHR2.open('GET', url, true);
XHR2.send();
XHR2.onreadystatechange = function() {
...
}
}
}

当出现第三个(甚至更多)仍然依赖上一个请求的时候,代码就变成了一场灾难。

我们需要不停的嵌套回调函数。这样的灾难,我们称之为 回调地狱。

**Promise **可以帮助我们解决这个问题。

Promise

我们知道,如果要确保代码在谁之后执行,可以利用函数调用栈,将想要执行的代码放入回调函数中。

1
2
3
4
5
6
7
8
9
10
11
function want() {
console.log('这是你想要执行的代码');
}

function fn(want) {
console.log('这里表示执行了一大堆其他代码');

// 其他代码执行完后,最后执行回调函数
want && want();
}
fn(want);

或者可以利用任务队列

1
2
3
4
5
6
7
8
9
10
11
function want() {
console.log('这是你想要执行的代码');
}

function fn(want) {
// 根据事件循环的机制,我们就不用非得将代码放在最后面了,由你自由选择
want && setTimeout(want, 0);

console.log('这里表示执行了一大堆其他代码');
}
fn(want);

与 setTimeout 类似, Promise 也可以认为是一种任务分发器,它将任务分发到 PromiseJobs 执行队列中。通常的用法是,我们发起一个请求,然后等待并处理请求结果。

简单用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var tag = true;
var p = new Promise(function (resolve, reject) {
if (tag) {
resolve();
} else {
reject();
}
});

p.then(function (result) {
console.log(result);
}).catch(function (err) {
console.log(err);
});

定义

Promise 是一个对象,保存着未来某一时刻才会执行的事件。

我们常使用 Promise 来解决反馈结果需要等待的场景。

例如

  • 前端向服务端发送一个接口请求。请求结果不会马上返回,而是需要等待一段时间。
  • 加载图片,需要等待一段时间
  • 弹窗中,等待用户点击确认或者取消

基础知识

创建 Promise 实例

1
const p = new Promise();

Promise 函数中的第一个参数为一个回调函数,我们可以称之为 executor 。通常情况下,在这个函数中,我们将会执行发起请求操作,并修改结果的状态值。

1
2
3
4
5
6
7
8
const p = new Promise((resolve, reject) => {
if (true) {
resolve();
}
if (false) {
reject();
}
});

状态

状态有三种

  1. pending:等待结果状态
  2. fulfilled:已出结果,结果符合预期完成状态
  3. rejected:已出结果,结果未符合预期完成状态

promise 表达的就是从发起请求开始,从没有结果 padding 到有结果 fulfilled/rejected 的一个过程。

在 executor 函数中,我们可以分别使用 resolve 与 reject 将状态修改为对应的 fulfilled 与 rejected.

resolve/reject 是 executor 函数的两个参数。他们能够将请求结果的具体数据传递出去。

  1. Promise 实例拥有 then 方法,用来处理请求结果变为 fulfilled 状态时的逻辑。then的第一个参数也是一个回调函数,该函数的参数则是 resolve 传递出来的数据。第二个参数用来处理 rejected 状态时的逻辑。
  2. Promise 实例拥有 catch方法,用来处理请求结果变为 rejected 时的逻辑。catch的第一个参数也是一个回调函数,该函数的参数则是 reject 传递出来的数据。

基本使用

写个例子感受一下 Promise 的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function fn(num) {
return new Promise(function (resolve, reject) {
// 模拟一个请求,2s 后得到结果
setTimeout(function () {
if (typeof num == 'number') {
resolve(num);
} else {
var err = num + ' is not a number';
reject(err);
}
}, 2000);
});
}

fn('abc')
.then(function (resp) {
console.log(resp);
})
.catch(function (err) {
console.log(err);
});

// 注意观察该语句的执行顺序
console.log('next code');

then方法可以接收两个参数,第一个参数用来接收 fulfilled 状态的逻辑,第二个参数用来处理 rejected 状态的逻辑。

1
2
3
4
5
6
7
8
fn('abc').then(
function (resp) {
console.log(resp);
},
function (err) {
console.log(err);
},
);

因此 catch 方法其实与下面的写法等价。

1
2
3
fn('abc').then(null, function (err) {
console.log(er);
});

then方法返回的仍然是一个 Promise 实例对象,因此 then 方法可以链式调用,通过在内部 return 的方式,能够将数据持续往后传递。

封装 ajax

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
var url = 'http://www.demo.com/user/info';

// 封装一个get请求的方法
function getJSON(url) {
return new Promise(function (resolve, reject) {
// 利用ajax发送一个请求
var XHR = new XMLHttpRequest();
XHR.open('GET', url, true);
XHR.send();

// 等待结果
XHR.onreadystatechange = function () {
if (XHR.readyState == 4) {
if (XHR.status == 200) {
try {
var response = JSON.parse(XHR.responseText);
// 得到正确的结果修改状态并将数据传递出去
resolve(response);
} catch (e) {
reject(e);
}
} else {
// 得到错误结果并抛出异常
reject(new Error(XHR.statusText));
}
}
};
});
}

// 封装好之后,使用就很简单了
getJSON(url).then(function (resp) {
console.log(resp);
// 之后就是处理数据的具体逻辑
});

Promise.all

当有一个 ajax 请求,它的参数需要另外 2 个甚至更多请求都有了结果之后才能确定,那么这个时候,就需要 Promise.all 来帮助我们应该这个场景。

1
var p = Promise.all([p1, p2, p3]);

Promise.all 接收一个由 Promise 对象组成的数组作为参数,当 Promise 对象状态都变成 fulfilled 的时候,才会去调用 then 方法。

如果其中一个 Promise 对象状态变成 rejected,那么 p 的状态就会变成 rejected,第一个被 reject 的实例的返回值会传递给回调函数。

如果作为参数的 Promise 实例,自己定义了 catch 方法,那么它一旦被 rejected,并不会触发 Promise.all() 的 catch() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const p5 = new Promise(function (resolve) {
resolve('hello');
});

const p6 = new Promise(function (resolve, reject) {
throw new Error('报错了');
}).catch((e) => e);

Promise.all([p5, p6])
.then((res) => console.log(res))
.catch((e) => console.log(e));

// 输出结果
// ["hello", Error: 报错了]

Promise.race

与 Promise.all 相似的是,Promise.race 也是接收一个 Promise 对象组成的数组作为参数,不同的是,只要当数组中的一个 Promise 状态变为 fulfilled 或者 rejected 时,就可以调用 .then 方法了。

1
var p = Promise.race([p1, p2, p3]);

Promise.race 可以理解为 Promise 实例赛跑,哪个实例有了状态就返回哪个,通常用于处理规定时间内请求超时的情况。

如果 5 秒之内请求无法返回结果,变量 p 的状态就会变为 rejected,从而触发 catch 方法指定的回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Promise.race([
new Promise((resolve, reject) => {
// 模拟请求,10秒后返回数据
setTimeout(resolve, 10000, 'my name is a');
}),
new Promise(function (resolve, reject) {
// 处理请求超时
setTimeout(reject, 5000, new Error('超时了'));
}),
])
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log('err: ', err);
alert(err);
});

all 和 race 的区别

all 接受一个数组,数组成员是 promise 实例对象,如果 promise 的状态都为成功,则返回一个数组,如果其中一个失败,那么就会执行 catch

race 表示赛跑,参数和 all 一样,race 返回的是状态最先完成的 promise 的结果,不管成功还是失败
我们一般用 race 处理接口超时的情况。

以下代码,如果 5 秒之内请求无法返回结果,promise 状态就会变为 rejected,从而触发 catch 方法指定的回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Promise.race([
new Promise((resolve, reject) => {
// 模拟请求,10秒后返回数据
setTimeout(resolve, 10000, 'my name is a');
}),
new Promise(function (resolve, reject) {
// 处理请求超时
setTimeout(reject, 5000, new Error('超时了'));
}),
])
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log('err: ', err); // err: Error: 超时了
});

async/await

异步问题除了使用 Promise 来解决之外,还可以使用 ES7 中新增的语法 async/await 来搞定。

在函数声明前面,加上关键字 async,这就是 async 的具体使用了。

1
2
3
4
5
6
7
8
async function fn() {
return 30;
}

// 或者
const fn = async () => {
return 30;
};

然后我们查看一下 fn 的运行结果

1
2
3
4
5
6
7
8
console.log(fn());

// result
Promise = {
__proto__: Promise,
[[PromiseStatus]]: 'resolved',
[[PromiseValue]]: 30,
};

发现 fn 函数运行返回的是一个标准的 Promise 对象。也就是说 async 其实就是 Promise 的一个语法糖,目的是为了让写法更加简单。于是,我们可以使用 Promise 的相关语法来处理后续的逻辑

1
2
3
fn().then((res) => {
console.log(res); // 30
});

await 含义为等待,意思就是需要等待 await 后面函数运行完了,并且有了返回结果,才能继续执行下面的代码。这正是同步的效果。

需要注意的是,await 关键字只能在 async 函数中使用。并且 await 后面的函数运行后必须返回一个 Promise 对象才能实现同步的效果。

当我们使用一个变量去接收 await 的返回值时,该返回值为 Promise 中 resolve 传递出来的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function fn() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000, 30);
});
}

const foo = async () => {
const t = await fn();
console.log('t', t);
console.log('next code');
};

foo();

// 输出结果
// t: 30
// next code

从例子中我们可以看出,在 async 函数中,遇到 await 时,就会等待 await 后面的函数运行完毕,而不会直接执行 next code。

如果我们直接使用 then 方法,就不得不把后续的逻辑写在 then 方法中。

1
2
3
4
5
6
7
const foo = () => {
return fn().then((t) => {
console.log('t: ', t);
console.log('next doce');
});
};
foo();

很明显,如果使用 async/await 的话,代码会更简洁,逻辑也更清晰。

异常处理

在 Promise 中,我们使用 catch 方法来捕获异常,而不是使用 then 的第二个参数。因为:

  1. 更接近同步的语法(try/catch)
  2. then 中的错误也会被 catch 捕获

使用 then 的第二个参数,并不能捕获到第一个参数内部抛出的错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const p3 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve(123);
}, 1000);
});
p3.then(
(result) => {
console.log(result);
throw new Error('is err');
},
(error) => {
console.log('error', error);
},
);

// 输出结果
// 123
// Uncaught (in promise) Error: is err

使用 catch ,则可以捕获到上一个 then 内部抛出的错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const p3 = new Promise(function (resolve, reject) {
setTimeout(() => {
resolve(123);
}, 1000);
});
p3.then((result) => {
console.log(result);
throw new Error('is err');
}).catch((error) => {
console.log('error', error);
});

// 输出结果
// 123
// error Error: is err

而使用 async 时,我们使用 try/catch 来捕获异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function fn() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('some error');
}, 1000);
});
}

const foo = async () => {
try {
await fn();
} catch (e) {
console.log('e: ', e);
}
};
foo();

如果有多个 await ,只会捕获到第一个异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function fn1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('some error fn1');
}, 1000);
});
}
function fn2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('some error fn2');
}, 1000);
});
}

const foo = async () => {
try {
await fn1();
await fn2();
} catch (e) {
console.log('e: ', e); // some error fn1
}
};
foo();

封装

如何封装与使用息息相关。

加载图片

封装一个加载图片的函数,Promise 的使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function imageLoad(url) {
const img = new Image();
img.src = url;

return new Promise(function (resolve, reject) {
img.onload = function () {
resolve('图片加载成功');
};

img.onerror = function () {
reject('图片加载失败');
};
});
}

然后我们就可以使用 imageLoad 来执行图片加载完成之后的逻辑。

1
2
3
4
5
6
7
imageLoad('xxx.png')
.then((res) => {
alert(res);
})
.catch((err) => {
alert(err);
});

封装的核心关键是: Promise 的最终目的是,为了执行 then 中的回调函数,我们称它为 then_cb

所以在封装的时候,我们就应该思考如何在 Promise 内部,让 then_cb执行。

简易版 MyPromise

显而易见,Promise 包含原型方法 then,构造函数需要传递回调函数 executor,该回调函数包含两个参数,resolve 与 reject 。

根据这些特点,我们得出:

1
2
3
4
5
6
7
8
9
10
11
12
class MyPromise {
construcotr (executor) {
executor(this._resolve.bind(this), this._reject.bind(this)
}

_resolve (value) {}

_reject () {}

then (then_cb) {}
}

目的是为了调用 then_cb ,通过封装加载图片可以发现,调用 resolve 时,then_cb 才会执行,所以可以得出结论, then_cb 的执行需要被 resolve 触发

我们可以通过保存 then_cb 引用的方式来解决。所以代码就变成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyPromise {
constructor(executor) {
this.thenCallback = null;

executor(this._resolve.bind(this), this._reject.bind(this));
}

_resolve(value) {
this.thenCallback(value);
}

_reject(value) {}

then(then_cb) {
this.thenCallback = then_cb;
}
}

同理,再解决 catch 回调函数的执行问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MyPromise {
constructor(executor) {
this.thenCallback = null;
this.rejectCallback = null;
executor(this._resolve.bind(this), this._reject.bind(this));
}

_resolve(value) {
this.thenCallback(value);
}

_reject(value) {
this.rejectCallback(value);
}

then(then_cb, onRejected) {
this.thenCallback = then_cb;
this.rejectCallback = onRejected;
}

catch(onRejected) {
this.then(null, onRejected);
}
}

如果不追求别的特性,我们的 Promise 对象就已经封装好了,并且可以使用了。

1
2
3
4
5
6
7
8
9
10
11
12
const p = new MyPromise((resolve, reject) => {
setTimeout(() => {
// resolve('123')
reject('some err');
}, 1000);
});

// p.then(res => {
// console.log(res);
// })

p.catch((err) => [console.log('err', err)]);

再模拟将 then_cb 放入队列中执行,简单调整如下:

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
class MyPromise {
constructor(executor) {
this.thenCallback = null;
this.rejectCallback = null;
executor(this._resolve.bind(this), this._reject.bind(this));
}

_resolve(value) {
// this.thenCallback(value)
setTimeout(() => {
this.thenCallback(value);
}, 0);
}

_reject(value) {
// this.rejectCallback(value)
setTimeout(() => {
this.rejectCallback(value);
}, 0);
}

then(then_cb, onRejected) {
this.thenCallback = then_cb;
this.rejectCallback = onRejected;
}

catch(onRejected) {
this.then(null, onRejected);
}
}

加入队列机制后,就可以在 executor 中直接执行 resolve ,否则会报错 this.thenCallback is not a function

1
2
3
4
5
6
7
const p = new MyPromise((resolve, reject) => {
resolve('123');
});

p.then((res) => {
console.log(res);
});

Promise.all

Promise.all 返回的是一个数组,我们要将参数 array 内的每个 Promise 的执行结果放在一个数组 result 里,并且 result 数组成员的顺序要与传入时的 array 成员顺序保持一一对应。

实现 Promise.all 的重点在于对 all 参数内 Promise 实例全部执行完毕时机的判断。

因为 Promise 是异步的,我们不能保证 Promise 实例完成的时机与数组顺序一样。也就是说我们不能使用数组的 length 属性来表达 Promise 全都执行完毕。

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
const delay = (data) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(data);
}, Math.random() * 1000);
});
};

Promise._all = (array) => {
return new Promise((resolve, reject) => {
let count = 0;
const result = [];
for (let i = 0, len = array.length; i < len; i++) {
array[i].then((data) => {
result[i] = data;
count++;
// 因为 array[i] 的执行是异步的,所以这种判断是错误的
// 如果 i===2 的 promise 先执行完毕,result[2] 导致 result.length === 3
if (result.length === array.length) {
resolve(result);
}
}, reject);
}
});
};

const p1 = delay(1);
const p2 = delay(2);
const p3 = delay(3);
Promise._all([delay(2), delay(1), delay(3)]).then(
(res) => {
console.log('res: ', res); // [2, 1, 3]
},
(err) => {
console.log('err: ', err);
},
);

上面的代码,如果最后一个 Promise 先执行完毕,赋值时 result[2] = data,那么 result.length等于 3, 满足 result.length === array.length的判断条件,就会提前执行 resolve 。

所以,我们可以参考垃圾回收机制的引用计数法,在内部添加一个计数变量 count 来判断是否所有 Promise 都已执行完毕。当 count 等于传入的 Promise 数组长度时,表示所有 Promise 都有了结果,然后我们再执行 resolve 将结果传递出去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Promise._all = (array) => {
return new Promise((resolve, reject) => {
let count = 0;
const result = [];
for (let i = 0, len = array.length; i < len; i++) {
array[i].then((data) => {
result[i] = data;
count++;
// 因为array[i]的执行是异步的,所以这种判断是错误的
// 如果i===2的promise先执行完毕,result[2]导致result.length === 3
// if (result.length === array.length) {
// resolve(result)
// }
if (count === array.length) {
resolve(result);
}
}, reject);
}
});
};

V8引擎如何工作的

v8 是谷歌开源的 JS 引擎,用于执行 JS 代码。清楚 JS 代码的执行顺序,有助于我们了解函数调用栈、事件循环等概念。

V8 的工作流程图

pic.1708406473535
主要了解其中 4 个重要的概念

1.Scanner

scanner 表示扫描器,用于对纯文本 JS 代码进行词法分析。它会将代码分析为 tokens。tokens 表示不能再分割的最小单位,可能是单是字符,可能是一串字符串。
例如

1
const a = 20;

会被转为 token 集合,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[
{
type: 'Keyword',
value: 'const',
},
{
type: 'Identifier',
value: 'a',
},
{
type: 'Punctuator',
value: '=',
},
{
type: 'Numeric',
value: '20',
},
];

2.Parser

parser 表示解析器。解析过程是一个语法分析的过程,它会将 tokens 转换为抽象语法树「Abstract Syntax Tree」,同时验证语法,有问题就抛出错误。

继续上个例子,tokens 被解析为 AST 后的样子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "Literal",
"value": 20,
"raw": "20"
}
}
],
"kind": "const"
}
],
"sourceType": "script"
}

解析分为两种情况,预解析与全量解析。

预解析

在实际应用中,有大量代码,声明了函数,但未被执行。因此,如果全部都做全量解析的话,就会产生很多无用功。

预解析有以下特点

  • 预解析会跳过未被使用的代码
  • 不会生成 AST,会产生 scopes 信息
  • 解析速度快
  • 根据规范抛出特点的错误
1
2
3
4
5
6
7
8
function foo1() {
console.log('foo1');
}
function foo2() {
console.log('foo2');
}

foo2();

对于 foo1 来说,函数并没有声明,那么生成 ATS 并没有意义。所以 foo1 采用的就是预解析,可以观察到 foo1 函数作用域的信息已经生成了。也就是说,作用域的范围信息,在预解析阶段就已经确定了。
pic.1708406487925

全量解析

全量解析会解析当前作用域的所有代码。会生成 AST,并且进一步明确更多的信息。

  • 解析给使用的代码
  • 生成 AST
  • 构建具体的 scopes 信息,变量引用,声明等。
  • 抛出所有的语法错误

需要区分的是,作用域和作用域链的信息在预解析阶段就确定了

1
2
3
4
5
6
7
8
9
10
11
12
13
// 声明时未调用,因此会被认为是不被执行的代码,进行预解析
function foo() {
console.log('foo');
}

// 声明时未调用,因此会被认为是不被执行的代码,进行预解析
function fn() {}

// 函数立即执行,只进行一次全量解析
(function bar() {})();

// 执行 foo,那么需要重新对 foo 函数进行全量解析,此时 foo 函数被解析了两次
foo();

3.Lgnition

Lgnition 是 V8 提供的一个解释器。它会将 AST 转为字节码「bytecode」。我们可以把这个过程理解为预编译。

4.TurboFan

TurboFan 是 V8 引擎的编译器模块。他会将 lgnition 收到的信息转为汇编代码。汇编语言可以理解为,表达了对寄存器的一个交互过程。

汇编代码就是对机器代码的封装,让人勉强能读懂。比如计算机一条加法指令为 10001010,汇编语言可用 add 表示。

汇编入门:

寄存器 概述
eax 累加器,可用于加减乘除等操作,使用频率高
1
2
mov eax, 5; // 将数字5,传送到寄存器 eax 中
add eax, 6; // eax 寄存器加6,此时 eax 得到新的结果

汇编就是使用约定好的指令,对寄存器进行各种操作。

lgnition + turboFan ,也就是「边解释边执行」。

5.Orinoco

Orinoco 是 V8 的垃圾回收器。

垃圾回收器会定期执行以下任务

  1. 标记活动对象和非活动对象「标记阶段」
  2. 回收/重用非活动对象所占用的空间「清理阶段」
  3. 整理内存「整理阶段」

es6常用特性

默认参数

1
2
3
4
var fn = function (width, height) {
var width = width || 50;
var height = height || 100;
};

没有默认参数之前,一切没有问题,知道遇到参数 0 时,就有问题了。0 为假值,就无法变为参数本身的值。在 es6 中,修复了这一缺陷。

1
2
3
var fn = function (width = 50, height = 100) {
...
}

模板字符串

模板字符串是增强版的字符串,用反引号````标识。
如果要在模板字符串中嵌入变量,将变量名写在${}中。
模板字符串会保留所有的空格和换行。

解构赋值

es5 写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 对象
var obj = {
age: 18,
name: 'yy',
};

var age = obj.age;
var name = obj.name;

// 数组
var arr = [1, 2];
var item1 = arr[0];
var item2 = arr[1];

解构赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 对象
var obj = {
age: 18,
name: 'yy',
};

var { age, name } = obj;

// 如果你想自定义变量名,可用冒号重命名,如myage
var { age: myage, name } = obj;

// 数组
var arr = [1, 2];
var [item1, item2] = arr;

箭头函数

箭头函数内部的 this 是词法作用域,由上下文确定

1
2
3
4
var _this = this;
$('.btn').click(function (event) {
_this.sendData();
});

有了箭头函数,就不必使用 that=this、_this=this、.bind(this)…,它总是可以按我们预期使用。

1
2
3
$('.btn').click((event) => {
this.sendData();
});

更简洁的语法:

1
2
3
4
5
const fn = () => {
dosomething;

return true;
};

如果返回结果是单个语句,可以省了{}return

1
2
3
4
const fn = () => true;

// 如果返回的是对象,需要用()包裹
const fn = () => ({ a: 1 });

let 和 const

使用 letconst 生命的变量,只在代码块里有效。代码块可以理解为大括号{}

如果你在 if,for 里面使用 letconst,那么只能再块里使用,在外部访问会报错。

1
2
3
4
5
if (true) {
const a = 1;
}

console.log(a); // Uncaught ReferenceError: a is not defined

暂时性死区

在使用let声明变量之前,对变量进行访问会报错

1
2
3
4
5
6
if (true) {
tmp = 'abc'
console.log(tem) Uncaught ReferenceError: Cannot access 'tmp' before initialization

let tmp
}

不存在变量提升

1
2
3
4
5
console.log(foo); // undefined
var foo = 2;

console.log(bar); // ReferenceError: bar is net defined
let bar = 2;

不能重复声明

不能在布局相同作用域重复声明

1
2
3
4
5
6
7
8
9
10
11
12
13
function fn() {
let x = 1;
let x = 2; // SyntaxError: Identifier 'x' has already been declared
}

if (true) {
let x = 1;
let x = 2; // SyntaxError: Identifier 'x' has already been declared
}

//全局作用域可以,后者覆盖前者
let x = 1;
let x = 2;

const声明一个只读的常量,一旦声明,常量的值就不能改变。

1
2
3
const x = 1;

x = 2; // TypeError: Assignment to constant variable.

如果const声明的是一个对象,可对这个对象下的属性进行增删改操作

1
2
3
4
5
6
7
8
9
10
11
12
13
const o = {
x: 1,
y: 2,
};

// 添加
o.z = 3;

// 删除
delete o['z'];

// 修改
o.x = 11; // o = { x: 11, y: 1 }

如果不希望 const 生命的对象被修改,可用 defineProperty定义属性的权限。

1
2
3
4
5
6
7
8
9
const o = { x: 1 };

Object.defineProperty(o, 'x', {
writable: false,
});

o.x = 2;

console.log(o); // { x: 1 }

Class 类

为了语法更接近传统的面向对象语言(java 和 c++),引入了 Class 类的概念,作为对象的模板。

通过 class 关键字定义类:

1
2
3
4
5
6
7
8
9
10
11
12
class Student {
// 构造函数
constructor(name, age) {
this.name = age; // this指向实例对象
this.age = age;
}

// 定义方法,方法名前面不需要function,挂载在原型上
printName() {
return this.name;
}
}

extends

extends 关键字用于继承父类

super

super 可当做函数使用,也可当做对象使用。

作为函数时,只能在子类构造函数里调用。

1
2
3
4
5
class B extends A {
constructor() {
super();
}
}

super代表的是父类的构造函数,但是返回的是子类的实例。 相当于 A.prototype.construcor.call(this)。也就是说,super()内部的 this 指向的是子类 B。

作为对象时,在普通方法中,指向的是父类的原型对象;在静态方法中,指向父类。

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
class Parent {
static myMethod(msg) {
console.log('static', msg);
}

myMethod(msg) {
console.log('instance', msg);
}
}

class Child extends Parent {
static myMethod(msg) {
super.myMethod(msg);
}

myMethod(msg) {
super.myMethod(msg);
}
}

// super在静态方法中,指向父类
Child.myMethod(1); // static 1

// super在普通方法中,指向父类的原型
var child = new Child();
child.myMethod(2); // instance 2

链判断运算符 ?.

如果一个对象为undefinednull,我们试着去访问该对象下的属性时就会抛出异常

1
2
const obj = undefined;
console.log(obj.name); // Uncaught TypeError: Cannot read properties of undefined (reading 'name')

使用链判断运算符,就不会抛出异常

1
2
3
console.log(obj?.name); // undefined
等价于;
console.log(obj == undefined ? undefined : obj.name);

还有一个常用的错误使用,链运算不能应用于赋值运算符左侧

1
obj?.name = 'tom'; // The left-hand side of an assignment expression may not be an optional property access.ts(2779

ts 非空断言
在有些情况下,你如果明确 obj 对象不为nullundefined,你可以使用非空断言符

1
obj!.name = 'tom';

这是 TypeScript 中的非空断言操作符,用来告诉编译器您已经检查过属性是否为非空,并且可以放心地访问它。

html和css的布局规则

html/css 的布局规则:

想了解 html/css 的布局规则,重要的不是规则是什么,而是什么因素会影响你使用对应的规则。当你理解了这些因素,那么相同场景下,你自然而然会想到使用什么规则。

场景(场景和关联)

举个例子:

  1. 土地:大小,价格,建什么东西

一句话概括就是:往容器放东西,怎么放。
土地:容器
大小和价格: 因素
东西:元素
怎么放:规则

那么,应用到浏览器中:
容器:也就是我们的浏览器视口(viewport)
因素:矩形、坐标系、边界
元素:盒模型
规则:文档流、定位、浮动、BFC、flex 等等

前面提到,要了解布局规则,重要的不是规则是什么,而是什么因素会影响你使用对应的规则,那么我们分别介绍下容器、因素、元素、规则,并从中建立关联。

容器(viewport):

web 浏览器视口指的是我们可见的区域,不包括浏览器菜单。移动设备的视口默认值为 980px,一般情况下这比移动设备要大,那么内容就会显示不完全,如果直接缩放那会导致字体变小。为了让视口的值等于移动设备的宽度,我们会在 html 的头部添加以下标签:

1
<meta name="viewport" content="width=device-width" />

因素:

  1. 矩形:可缩放 -> 响应式布局
  2. 坐标系:拖拽、视差 -> 定位、滚动条、层叠上下文元素
  3. 边界:视口有边界、坐标系无边界

可见,容器的不同因素直接影响着我们在实现某种效果时使用哪些规则。接下来看看元素。

元素:

  • 标准盒模型:块模型、行内模型
  • 怪异盒模型
  • 弹性盒模型

先了解下盒模型的结构,从里到外:content → padding → border → margin,各个部分组合在一起就是我们页面上看到的内容
pic.1708406175712

标准盒模型:

我们常说的盒模型指的是 W3C 标准盒模型

  • 元素的widthheight只包含 content 区域
  • 盒子实际大小等于width+padding+border

盒子的实际宽度我们可以看做是总宽度,和元素本身设置的 width 不是一个概念

pic.1708406185130

我们布局中广泛应用的模型,分为块模型和行内模型,我们叫做块元素和行内元素。
块元素
display: block可以将元素设置为块元素,它拥有以下行为:

  • 它占据父元素的所有宽度,绝大数情况下(不包含行内块元素)
  • 独占一行
  • 拥有widthheight属性
  • 它的marginborderpadding 属性会将盒子周边的元素给“推开”

行内元素
display: inline可以将元素设置为行内元素,也叫行内元素,它拥有以下行为:

  • 盒子不会独占一行
  • widthheight 属性不起作用
  • 水平方向的外边距、内边距、边框会被应用,且会把其它 inline 状态的盒子推开
  • 垂直方向的外边距、内边距、边框会被应用,但不会把其它 inline 状态的盒子推开

默认为行内元素有:a、span、em、strong

除了标准盒模型,还有怪异盒模型,也可以称 IE 盒模型。因为 IE8 之前,IE 默认使用怪异盒模型,并且没有可用的机制来切换。

怪异盒模型

  • 元素的widthheight不仅包括content,还包括paddingborder
  • 盒子实际的大小取决于 width

pic.1708406192021

图中我们给盒子设置了width: 200pxpadding: 10px,那么 content 就会被挤压为 180px(200 - padding * 2)。

应用:使用怪异盒模型能解决 1px 边框线问题。

box-sizing

box-sizing 属性可以切换盒模型模式,默认值是content-box,可选择有border-boxinherit
content-box:w3c 标准盒模型
border-box:IE 盒模型
inherit:从父元素集成 box-sizing 属性的值

弹性盒模型

对于一些特殊布局,如水平垂直居中,采用传统布局不容易实现,采用弹性盒模型就很方便实现。

推荐阮一峰老师的文章:Flex 布局教程,再补充容易忽视的几点:

作用在 flex 容器上
display: flex:
默认: align-items: stretch、flex-direction: row
align-content:
指定如何在纵轴上 项目之间和周围 分配空间。单轴下(flex-wrap: nowrap)此属性不生效
align-items:
初始值:stretch
stretch:表示 flex 项目会被拉伸至最高的项目高度的高度
flex-flow: row wrap === flex-direction: row + flex-wrap: wrap

作用在 flex 项目上
flex-grow:
初始值:0
flex-shrink:
初始值:1
flex-basic:
初始值:auto,即项目本身的大小
指定了 flex 项目在主轴上的初始大小(如果主轴是水平轴,那就是 flex 项目的宽,否则是高)
flex: 1 200px === flex: 1 + flex-basic: 200px
指定了 flex 项目在主轴上初始化(width: 200px)后,按比例分配剩余空间

flex 缩写
flex === flex-grow flex-shrink flex-basic

flex: 1
flex: 1,此属性可以按同等比例分配项目的大小,那么它的完整写法是什么呢?

不等于 1 1 auto
重点理解下缩写下 flex-basic 的作用:
在按比例分配项目之前,计算项目是否有多余的空间,默认为 auto,即项目自身的大小。
如果设置为 auto,那么按照项目自身大小初始化后等比例分配剩余空间,也就会出现项目大小不一样的情况,所以不是 1 1 auto。

等于 1 1 带单位的长度值
假设我们设置为 1 1 0px,按照 0px 初始化后等比例分配剩余空间,那每个项目的大小就会相同。

flex 常用布局

需求:同一行内,如果有一个标题占两行,整行标题都占两行,否则默认一行。标题超出两行时用省略号表示,效果如下:
pic.1708406202621
这个需求的难点在于卡片的高度不是固定的。有什么办法可以让同行内的标题高度保持统一?

先看一个两行超出隐藏的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
.title {
width: 200px;
background-color: yellow;

word-break: break-all;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.title2 {
background-color: pink;
}
1
2
3
4
<div class="title">
翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁翁
</div>
<div class="title title2">请请请请请请请请请</div>

pic.1708406216076
先来重点理解display: flex和 flex 下的height: 100%

  • 当我们给容器设置 display: flex 时,那么 align-items 属性的值为 stretch,即同一行内的 flex 项目会被拉伸至最高的项目高度的高度
  • 给 flex 项目设置 height: 100%,会填充容器剩余高度。

实现:

  1. 把容器设置为弹性盒子display: flex
  2. 给标题设置height: 100%
  3. 给标题兄弟元素设置flex-shrink: 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div class="section">
<div class="card">
<div class="card-content">1</div>
<div class="card-title">
请问请问群翁群翁群翁群问请问群翁群翁群翁群问请问群翁群翁群翁群
</div>
</div>
<div class="card">
<div class="card-content">2</div>
<div class="card-title">2222222222</div>
</div>
<div class="card">
<div class="card-content">3</div>
<div class="card-title">3333</div>
</div>
<div class="card">
<div class="card-content">4</div>
<div class="card-title">444</div>
</div>
</div>
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
.section {
display: flex; // 同一行内的flex项目会被拉伸至最高的项目高度的高度
flex-wrap: wrap;
width: 300px;
}
.section .card {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid red;
margin: 0 10px;
}
.section .card .card-content {
flex-shrink: 0; // 防止被标题height: 100%属性挤压

width: 100px;
height: 100px;
background-color: pink;
}
.section .card .card-title {
height: 100%; // 填充容器剩余高度

line-height: 20px; // 加上行高样式更稳定
word-break: break-all;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
background-color: yellow;
}

规则

下面介绍文档流、定位、浮动、BFC 等布局规则。

文档流:

在不对页面元素进行任何控制的情况下,浏览器默认的布局方式。从上到下,从左到右。

定位(position):

解决某个元素随页面发生变化而固定位置,它不作为一种主要布局方式,而是用于管理和微调页面中一些特殊项的位置

  • 静态定位:默认值,不做任何
  • 相对定位:相对自己定位,不会脱离文档流
  • 绝对定位:相对于 html,会脱离文档流,或相对于最近被定位的祖先元素定位。多配个相对定位使用
  • 固定定位:相对于浏览器视口固定定位
  • 粘性定位:初始像静态定位,当它的位置相对视口达到预设值时,就会想固定定位一样被固定住

浮动(float)

在传统布局里,信息是纵向排列(从上到下)的,浮动可使信息横向排列。浮动会脱离文档流,正常文档流的元素会围绕着浮动元素。

  • left:左浮
  • right:右浮
  • none:不浮动
  • inherit:继承父元素浮动属性

在使用浮动时,我们得清楚的知道我们要把元素摆放在哪个位置。因为使用浮动时,可能会存在可视化布局与源顺序不同。

假设现源顺序布局为如下

1
div1 div2 div3

我们给 div2 和 div3 同时设置 float: right,那么视觉效果就为:

1
div1 div3 div2

这是因为 div2 在源顺序上比 div3 等级更高(在 DOM 上,div2 先出现,并声明了 float: right),所以在视觉上更靠右。

BFC:块级格式化上下文(block format context )

定义:可以看作是一块独立的区域空间,拥有普通盒子没有的一些特性。它解决个体之间的位置交互问题。

触发 BFC 的方法:

  • html 根元素
  • position: absolute、fixed
  • display: flex、inline-block、table-cell
  • overflow: 除 visibility
  • float: 除 none

来看下元素间的交互问题以及解决方法:

  • 同一块 BFC 下,相邻垂直方向盒子外边距会发生重叠:
1
2
3
4
<div class="box">
<div class="child"></div>
<div class="child"></div>
</div>
1
2
3
4
5
6
7
8
9
10
.box {
width: 150px;
border: 1px solid red;
}
.child {
height: 50px;
width: 50px;
background-color: blueviolet;
margin: 50px;
}

pic.1708406228923
我们给每个 child 设置了margin: 50px,但它们的间距不是 100px,而是 50px,如果我们想让间距变成 100px,那么只要给其中一个盒子设置为 BFC,这样就不再同一块 BFC 下

1
2
3
4
5
6
<div class="box">
<div class="wrap">
<div class="child"></div>
</div>
<div class="child"></div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
.box {
width: 150px;
border: 1px solid red;
}
.child {
height: 50px;
width: 50px;
background-color: blueviolet;
margin: 50px;
}
.wrap {
overflow: hidden;
}

pic.1708406235633

  • 浮动问题:
1
2
3
<div style="border: 1px solid #000;">
<div style="width: 100px;height: 100px;background: #eee;float: left;"></div>
</div>

pic.1708406243403
为外层 div 添加overflow: hidden

1
2
3
<div style="border: 1px solid #000;overflow: hidden;">
<div style="width: 100px;height: 100px;background: #eee;float: left;"></div>
</div>

pic.1708406250328

  • 元素被浮动元素覆盖的问题
1
2
3
4
5
6
7
8
9
<div style="height: 400px;">
<div style="height: 100px;width: 100px;float: left;background: lightblue">
我是一个左浮动的元素
</div>
<div style="width: 200px; height: 200px;background: #eee;">
我是一个没有设置浮动, 也没有触发 BFC 元素, width: 200px; height:200px;
background: #eee;
</div>
</div>

pic.1708406257409
给被浮动元素覆盖的元素添加overflow: hidden

1
2
3
4
5
6
7
8
9
<div style="height: 400px;">
<div style="height: 100px;width: 100px;float: left;background: lightblue">
我是一个左浮动的元素
</div>
<div style="width: 200px; height: 200px;background: #eee;overflow: hidden;">
我是一个没有设置浮动, 也没有触发 BFC 元素, width: 200px; height:200px;
background: #eee;
</div>
</div>

pic.1708406277606
小结:
解决塌陷:给其中一个盒子设置为 BFC,这样就不再同一块 BFC 下
清除浮动:给浮动元素父元素设置为 BFC
防止元素被浮动元素覆盖:给元素设置 BFC,就不会被浮动元素覆盖

IFC:行内格式化上下文(inline formatting context)

pic.1708406355114
让块水平居中

1
2
3
4
5
6
7
8
9
10
11
12
.p {
width: 100px;
background-color: pink;

text-align: center;
}
.child {
width: 50px;
background-color: yellow;

display: inline-block;
}
1
2
3
<div class="p">
<div class="child">12</div>
</div>

pic.1708406370175

总结(对 html/css 规则)

抽象点来说,容器就是一个可以容纳个体的空间,当我们把一个个个体放到容器时,容器的因素决定了我们使用什么规则来存放从而更加合理。
具体点来说,视口就是我们的容器,元素就是个体,视口的因素(矩形、坐标系和边界等)决定了我们使用什么规则来布局。元素分为标准盒模型、IE 盒模型和弹性盒模型,规则分为文档流、定位、浮动、BFC 等等。

参考:
https://zhuanlan.zhihu.com/p/183050328
https://blog.csdn.net/weixin_41682025/article/details/110343448

js中的重要概念

在当前函数中,要寻找到变量的值是从哪里来的,就首先会从当前执行上下文中查找,如果没有找到,则会去作用域链中查找。这里需要注意的是,作用域链本身就是存在于函数对象中的一个属性 [[Scopes]],因此不是一层一层的往上查找「这里经常理解有误」,该属性是在代码预解析阶段就已经确认好的。

作用域:作用域是规定变量与函数可访问范围的一套规则。

作用域的范围信息,是在预解析阶段就已经确定的

执行上下文:执行上下文包含了跟踪对应可执行代码执行进度所需要的所有状态,每个执行上下文中都有特定的实体对象用于记录这些特定状态。

函数调用栈:管理所有的执行上下文

运行时上下文:只能有一个,且处于栈顶。当代码执行过程中,有新的函数调用,新的执行上下文会被创建,入栈,并且成为新的运行时上下文

完整的作用域链:

都具备一个属性 [[Scopes]],该属性中存储了当前函数可访问的所有变量对象

  • Global 全局对象:不会做任何优化,会包含全局对象中的所有属性与方法
  • Script 对象:在全局环境下,由 let 和 const 声明的变量对象
  • Closure 对象:我们讨论比较多的闭包对象,嵌套函数生成,仅会保存当前作用域能够访问的变量属性
  • Local 对象:以上的几种变量对象,都会存在于函数的 [[Scopes]]属性之中,因为他们都能够在函数解析时确认,而 Local 对象则不行,需要在函数的执行过程中才能确定,并且在执行过程中,该对象中的属性是随时会发生变化的,该对象除了会存储当前函数上下文中所有的变量与函数声明,还会额外记录 this 的指向。

Local: 活动对象

函数参数,var 声明的变量,let/const 声明的变量,function 声明的变量,class 声明的变量,this 指向等共同组成。
仅仅只有处于栈顶的执行上下文,才会生成 Local 对象。并且 Local 对象的具体内容会在执行上下文的生命周期中不断变化。也就意味着,在执行上下文的创建阶段,只有函数参数、function 声明的变量、this 指向 能够明确具体的值,其他变量的初始值都为 undefined,然后在代码执行过程中逐步明确赋值。

web3综述

区块链:去中心化记账系统

比特币

基于区块链这一技术的一种加密货币(还有以太币、莱特币等等)

  • 密码学基础
  • 比特币的数据结构
  • 共识协议和系统实现
  • 挖矿算法和难度调整
  • 比特币脚本
  • 软分叉和硬分叉
  • 匿名和隐私保护

以太坊

  • 基于账户的分布式账本
  • 数据结构:状态树、交易树、收据树
  • GHOST 协议
  • 挖矿:memory-hard mining puzzle
  • 挖矿难度调整
  • 权益证明
  • 智能合约

以太坊生态:

  1. 去中心化应用(DApps Decentralized Applications):
    1. 数字金融服务(去中心化交易所、借贷平台、稳定币)
    2. 数字身份验证
    3. 去中心化游戏
    4. 去中心化社交媒体
  2. 智能合约:以太坊是一个智能合约平台。智能合约应用场景包括:代币发行、投票、众筹等
  3. 钱包:以太坊生态系统有各种钱包应用程序,用于存储、管理和交易以太坊资产和代币
  4. 基础设备和开发工具:开发者可以通过这些工具构建和部署智能合约和 DApps。如 Solidity、以太坊虚拟机(EVM)
  5. NFT(Non-Fungible Token 非同质化代币)市场:以太坊是最著名的 NFT 平台之一。支持各种数字艺术、虚拟土地、链上凭证等 NFT 的发行和交易
  6. 去中心化金融(DeFi Decentralized Finance):以太坊生态系统中最受欢迎和发展最快的领域之一。DeFi 应用提供了一系列金融服务,如去中心化交易所 DEX、链上借贷等,旨在提供更开放、透明和无需信任的金融体系。
Your browser is out-of-date!

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

×