tween, haha

小说爽文看完了,索然无味。学习一下JS动画吧。

抬头一看,还挺麻烦。(因为怕麻烦,在家好几天没吃顿正经饭,吃的干粮。)

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
/*
* 提取几个函数来看看
* b为初始值,c为变化量。(比如可以变化高度,b初始为100,需要动画到50,c就是-50变化总量。)
* t,d是时间,但按帧数比较容易理解。假设d表示动画总帧数,t就是第t帧。每一帧所用的时间加起来就是动画总时间。哈哈,还是扯蛋到时间上了。
*/

function Linear(t, b, c, d) {
return c*t/d + b;
}

function QuadIn(t, b, c, d) {
return c*(t/=d)*t + b;
}

function QuadOut(t, b, c, d) {
return -c*(t/=d)*(t-2) + b;
}

/*
* 此处省略多行
*/

function BackIn(t, b, c, d, s) {
if(s == undefined) s = 1.70158;
return c*(t/=d)*t( (s+1)*t - s ) + b;
}

/*
* 此处省略几行
*/

废话不多说,直接上图。图呢?还没画好。稍等我去下载一个 gnuplot。啥也不会。

(图片暂时只能凑合。和我想画出来的还有些出入。要想写MathML好像还不行,没用过。麻烦,先不折腾了。)

我们需要构造一个函数 t ↦ st ∈ [0, d],s ∈ R,并且还得满足 0 ↦ bd ↦ b+c。也就是初始(t=0)值为 b,结束(t=d) 值为 b+c。

当然,我们用的时候还需要离散化。t取值 0, 1, 2, …, d。

验证上面 Linear(0, b, c, d) = b, Linear(d, b, c, d) = b+c; BackIn(0, b, c, d, s) = b, BackIn(d, b, c, d, s) = b+c

不过上面的写法看起来有点猥琐。

1
2
3
function QuadIn(t, b, c, d) {
return c*(t/=d)*t + b;
}

其实就是

1
2
3
function QuadIn(t, b, c, d) {
return c*(t/d)*(t/d) + b;
}

动画前,b,c,d都应该确定好。所以上面的函数其实只有一个“真参数” t。(不排除,途中,修改了其他参数,不过这就产生另一个新的函数了。)

设 k = t/d,则 t = kd。这个函数就是 k ↦ tk ∈ [0, 1],t ∈ [0, d],而且 t=kd。 这和上面的函数 t ↦ s 复合一下,就产生新函数 k ↦ sk ∈ [0, 1],s ∈ R,并且还得满足 0 ↦ b1 ↦ b+c。好像也没什么卵用。

我瞅了一眼tween.js中这一部分,它应该是这么弄得。(意思一下,源码不是这样)

1
2
3
4
5
6
7
8
function QuadIn_k(k) {
return k*k;
}

/*大概意思*/
function QuadIn(t,b, c, d) {
return c * QuadIn_k(t/d) + b;
}

修正量 b,c和 k 隔离开了。

说了这么多,我也不知道在讲什么。关键其实是如何构造一个函数?

那我就随便构造一个吧。弄个抛物线吧,取 d/3为对称轴,过 (0, b) (d, b+c)。经过艰苦计算得出(老了,一个抛物线算了一个小时)

t ↦ 3*c/d/d*(t-d/3)^2 + b - c/3

t ↦ -3*c/d/d*(t-d/3)^2 + b + c/3

我们随便娶一个

1
2
3
function QuadFun_1_3(t, b, c, d) {
return 3*c/d/d*(t-d/3)*(t-d/3) + b - c/3;
}

谈不上构造,你只要想办法把弄条曲线,通过 (0, b) (d, b+c) 就行了。

  • 比如 Linear 就是条直线。
  • 比如 QuadIn 就是对称轴为s轴的抛物线。
  • ……
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
65
66
67
68
69
<!DOCTYPE html>
<html>
<head>
<title>tween</title>
<style>
#test {
width: 500px;
height: 500px;
background-color: lightgreen;
position: absolute;
left: 500px;
top: 200px;
}

#test > div {
position: absolute;
background-color: red;
width: 3px;
height: 3px;
}
</style>
</head>
<body>
<div id="test"></div>
<script>

function ElasticEaseIn(t,b,c,d,a,p) {
if (t==0) return b;
if ((t/=d)==1) return b+c;
if (!p) p=d*.3;
if (!a || a < Math.abs(c)) { a=c; var s=p/4; }
else var s = p/(2*Math.PI) * Math.asin (c/a);
return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
}

function backIn(t, b, c, d, s) {
if(s===undefined) s=1.70158;
return c*(t/d)*(t/d)*((s+1)*(t/d)-s) + b;
}

// 随便搞一个函数
function QuadFun_1_3(t, b, c, d) {
return 3*c/d/d*(t-d/3)*(t-d/3) + b - c/3;
}

var t = 0;
var b = 0;
var c = 400;
var d = 60;
var result = [];
var raf;
var raf = requestAnimationFrame(function chuizi() {
var div;

if(t <= d) {
p = QuadFun_1_3(t, b, c, d); // 调用不同的动画函数
result.push({t: t, T: Date.now(), p: p});
div = document.createElement('div');
div.style.left = 6*t + "px";
div.style.top = p + "px";
test.appendChild(div);

t++;
requestAnimationFrame(chuizi);
}
});
</script>
</body>
</html>

用js库的时候,就可以自己选个合适的 动画函数 塞进去。

scrollLeft

闲来无事。发现自己没有……没有……没有亲手写过 无缝滚动 !!! ZZ 一样的混等。

scrollLeft
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>
scroll Left (¬_¬) Right
</title>
<style>
#demo {
width: 500px;
border: 1px dashed red;
overflow: hidden;
overflow-x: scroll;
}

table {
border-collapse: collapse;
}

td {
padding: 0;
}

.div {
width: 125px;
height: 100px;
background-image: linear-gradient(32deg, aqua, black, cyan, deeppink, fuchsia, gold, hotpink, indigo);
}

</style>
</head>
<body>
<!-- 容器 -->
<div id="demo" data-comment="this is scroll container">
<!-- 放个表格,样式很简洁,宽度自动增加。其中第一列为需要滚动的内容。 -->
<table>
<tr>
<!-- 展示内容要比容器长,否则,没有必要滚动,(尽管也可以滚动) -->
<!-- 细节,比如第一张图片还没有出去;新复制的第一张也不该显示出来 -->
<td id="listA" data-comment="origin content, column one">
<table>
<!-- 如果内容直接复制在 #tRow 中,就不需要外层表格了 -->
<tr id="tRow">
<td>
<div class="div">
1
</div>
</td>
<td>
<div class="div">
2
</div>
</td>
<td>
<div class="div">
3
</div>
</td>
<td>
<div class="div">
4
</div>
</td>
<td>
<div class="div">
5
</div>
</td>
</tr>
</table>
</td>
<td id="listB" data-comment="cloned content, column two">
</td>
</tr>
</table>
</div>
<script>
(async function 复制一份滚动内容() { //没什么卵用。随意写。
listB.innerHTML = listA.innerHTML;
}());

//假设 每秒移动 100px,
//再假设 移动频率 25, 即 40ms 移动一次。
//那么移动量 5
var deltaT = 40; //时间间隔
var deltaX = 5; //这就是每次移动量,正整数。
function goLeft() {
if(demo.scrollLeft >= listA.offsetWidth) {
demo.scrollLeft -= listA.offsetWidth;
}

demo.scrollLeft += deltaX;
}

function goRight() {
if(demo.scrollLeft < listA.offsetWidth - demo.clientWidth) {
demo.scrollLeft += listA.offsetWidth;
}

demo.scrollLeft += -deltaX;
}

//setInterval(goLeft, deltaT);
</script>
</body>
</html>

分析一下

我们这里用 scrollLeft 来实现。分析清楚以后,offsetLeft细节虽然不同,但道理是一样。

  1. 假设容器宽度为  W ,内容宽度为  L 。(满足 L > W,我觉得 L <= W 没必要无缝滚动,你非要滚动请自行设计。)
    假设容器的scrollLeft值为 x,开始,x = 0;然后 x 不停增加,一直移动到右边 x = L - W(滚不动了,x大于 L-W,也会被设置为 L-W)。
    x ∈ [0, L-W] ⊂ N。

    • Element.scrollLeft,那个rtl我在 Chrome 71.0.3578.98 测试,和描述不符。不兼容吧。我们这里不要理会 rtl。
  2. 我们要的效果

为了能让红色块滚出去,我们把它复制一份,蓝色的。 此时 x ∈ [0, 2L-W]。

显然,开始x=0,不断增加 x,内容就向左滚动了,当 x>=L 时,红色块滚了出去,显示的是蓝色块部分内容。关键是,蓝色块和红色块的长相是一样的。如果我们滚出了红色块,进入了蓝色块,我们只要马上跳转到相应的红色块位置即可。

当 x ∈ [L, 2L-W],显示的是蓝色块。 显示范围对应于 [x, x+W-1],由于周期性,它与 [x-L, (x+W-1)-L] 显示完全一样。 这就是无缝而动的本质。

那么这一句跳回去,

1
2
3
if(demo.scrollLeft >= listA.offsetWidth) {
demo.scrollLeft -= listA.offsetWidth;
}

可以改为

1
2
3
4
5
6
7
8
9
10
11
12
const L = listA.offsetWidth;
const W = demo.clientWidth;

const MIN = L;
const MAX = 2*L - W;
const RANGE = MAX - MIN;

// 跳转条件
const BOUNDARY = MIN + Math.round(RANGE * Math.random());
if(demo.scrollLeft >= BOUNDARY) {
demo.scrollLeft -= listA.offsetWidth;
}

动起来?

  1. deltaX 多大? 每一次移动,走多少距离合适?

因为 x ∈ [0, 2L-W],显然,1 <= deltaX <= 2L-W。

  • deltaX=1 看起来就很好用。

  • 再看deltaX=2L-W。你一下滚到头了。步子迈得太大,容易扯蛋。初始 x=0

    • 第一次 goLeft 后,x=2L-W,
    • 第二次 goLeft 后,x=2L-W,(满足跳转条件,x减去L,即 x=L-W,加个deltaX,3L-2W,过头了,又变成了 2L-W,x=2L-W)
    • 第三次 goLeft 后,x=2L-W,同上
    • ……
      完全没有动画效果,而且我认为这个deltaX无效,为啥?后面每次都加过头了。瞎几把搞。
  • deltaX上限多少?就是每次加上去都不会过头。

也就是每次 goLeft 都满足表达式 x + deltaX <= 2L-W
如果用 demo.scrollLeft >= BOUNDARY,会很复杂。我们就简化一下,还是用原来的,demo.scrollLeft >= listA.offsetWidth,即 x 大于等于 L 时,就需要减去 L,然后再计算上面的表达式。

假设:
xm-1 < L,
xm >= L,
其中 x0=0,xn = xn-1 + deltaX,

由于 xm 满足跳转条件,此时 x=xm - L,

(xm - L) + deltaX <= 2L-W, 令 xm=2L-W,得 deltaX <= L;
(xm-1 + delataX - L) + deltaX <= 2L-W, 令 xm-1=L-1,得 deltaX <= L - 0.5W + 0.5

同时 deltaX = xm - xm-1 = (2L-W) - (L-1) = L - W + 1

所以 当 deltaX 小于 L-W+1 时,是有效的;而当 deltaX 小于 2L-W 时,是合理的。因为 scrollLeft 有自己的上限。

  1. 怎么动起来?我们取 deltaX 为个位数就行了,一般 L 都要比 W 大。频率取值大于24(人眼?),小于60(显示器刷新?)即可。 即16ms < deltaT < 42ms。若 deltaT=25,deltaX=4,大概每秒能移动160距离。当 deltaT不变时,deltaX变化就能调整速度,比如 deltaX=10 或者 deltaX=Math.round((L-W+1)*Math.random());

然后, setInterval(goLeft, deltaT) 就完事了。

向右,向上,向下,斜着滚动,都可以做同样的分析。

a little note about load event

废话不说,上码。

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
<!doctype html>
<script id="s1">
console.log('window: ', window, 'document: ', document);
</script>
<html>
<meta charset="UTF-8" id="cs">
<script id="s2">
console.log('charset: ', cs);
</script>
<script id="s3">
console.log('document.html: ', document.documentElement);
</script>
<head>
<script id="s4">
console.log('document.head: ', document.head, document.head.children); //特别注意这一句,此时title还没有
</script>
<title>
A title (¬_¬)
<script>
console.log('document.title???: ', document.title);
</script>
</title>
<script id="s5">
console.log('document.title: ', document.title);
</script>
<script id="s6">
console.log('document.body???: ', document.body); //no body
</script>
</head>
<body>
<script id="s7">
console.log('document.body: ', document.body); //yes body
</script>
<div id="div">
<script id="s8">
console.log('div: ', div);
</script>

<p id="p">
<script id="s9">
console.log('p: ', p);
</script>
</p>
</div>
</body>
</html>

这就是辣鸡代码。不过,我仅仅是为了展现一下 DOM 生成的一个侧面。可以看到有的对象很早就存在了,比如window;然而document.body生于<body>后,可以看到 #s7有,#s6没有。

看那个 HTMLCollection(5)当时 head 已有,并且只有5个子元素,后面的3个子元素还未生成 (可在head开始和结束分别输出子元素个数)。不过这是一个动态的聚合,后来在控制台就全部打印出来了。不扯蛋了。

load 事件部分简单分析,说说 onload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!doctype html>
<html onload="console.log(`html`);">
<head onload="console.log(`head`);">
<meta charset="UTF-8">
<title>
A title (¬_¬)
</title>
</head>
<body onload="console.log('body');">
<div id="div" onload="console.log('div');">
<p id="p" onload="console.log('p');">
</p>
</div>
</body>
</html>

只有body有效果,其他没什么卵用。(不考虑其他资源标签,比如 img,video,audio,script……

但是从另一个角度,也可以理解为,浏览器没有为这类元素提供 load事件。(关键也没有必要提供)。

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
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>
A title (¬_¬)
</title>
<script>
window.onload = function (e) {
console.log('window: ', e);
}
</script>
</head>
<body onload="console.log('on body: ', event);">
<div id="div">
<p id="p">
</p>
</div>
<script>
document.body.onload = function (e) {
console.log('in body: ', e);
}
</script>
</body>
</html>

此时只有最后一句有效果,其他被覆盖。也可以说,三句是等价的,有一句就够了。这里别用 addEventListener,情况会更加复杂。

咋回事?

可以一句一句实验,打印 event。

可以发现,这三句都满足: event.target === document,event.currentTarget === window。

用 Chrome 打出来看看:

这个 load事件,它的 target 是 document;但是它的 propagation path 只有 window!!! 也就是说这个 load事件 传播到 window,没有然后了。可能由于历史原因,body上的 onload处理,实际是放到了 window 上面,但是 document.body.addEventListener('load', /\*...\*/),却监听在document.body上(可惜,传播只到window!你听不到啊!•﹏•);而在其他元素上挂 onload 是没啥用了。

按理来说,你好歹,这个 load事件,从 window->document->window,经历三阶段;实际只有 eventPhase===2,这个阶段。

load实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var e1 = new Event('load', {
bubbles: true,
});

window.addEventListener('load', e=>{
console.log('window: ', e);
});

document.addEventListener('load', e=>{
console.log('d: ', e);
});

document.documentElement.addEventListener('load', e=>{
console.log('d-html: ', e);
});

document.body.addEventListener('load', e=>{
console.log('d-body: ', e);
});

document.body.dispatchEvent(e1);

我这个 e1 是没有传播到 window。和自定义事件一点不熟,还得搞一搞。

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
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>
A title (¬_¬)
</title>
</head>
<body>
<div id="div">
<p id="p" onload="alert('p')">
</p>
</div>
<script>
document.currentScript.onload = function (e) {
console.log('me : ', e);
}
// 这个用法可能是错误的,load 应该用在带有 src 的 script 上,或者监测一个带有 async 的 script?
// 不过,这句即使正确。也只有加载完了,才会执行这里。 但是加载完(load事件也产生过了),执行这里也没啥用鸟。
</script>

<script>
document.body.onload = function (e) {};
document.body.addEventListener('load', function (e) {});
// 第一个加到 window 上,不仅仅是历史原因
// 第二个加到 document.body 上
// 仅仅是一个思路,未测试全部浏览器
</script>
</body>
</html>

很多地方都在胡说八道,看看就算了,别当真。

补充 (2019年1月22日08:31:55)

window.onload 和 body onload

1
2
3
4
5
6
7
document.body.onload = function(e){
console.log(e, this === window); // true
}

document.body.onclick = function (e) {
console.log(e, this=== document.body) // true
}

感觉自己很蠢。这样不就行了。

1
<body onload="console.log(this===window)"> <!-- true -->

看上面两段代码,和我昨天说的一样,这样更明显。昨天从 load事件出发、今天从 this 出发,都证明了 window.onload 和 body onload 等价。(未测试所有浏览器,记得吆!!!)

event fired?

关于事件的产生。就拿鼠标相关事件搞一搞。

消息

当移动鼠标,点击鼠标的时候,OS 是会分发消息的(就说 win32 GUI 吧,或许有其他特例)。至于怎么处理就是应用的事情。(当然 OS 自身作为一个大应用也处理自己该处理的消息。)

事件对象

假如我在浏览器中晃动、点击鼠标,那么 OS 就会把鼠标消息派发给浏览器。此时,关于浏览器中鼠标相关事件(一个事件就是一个JS中的对象,就像这样 {target: xxoo, bubbles: true, type: 'click', cancelable:true, path: ooxx, /* ... */},不同事件对象含有不同信息;一个数据结构而已)的产生我产生了以下的疑问。

  1. click

The click event fires when a pointing device button (e.g., a mouse’s primary button) is pressed and released on a single element. If the button is pressed on one element and released on a different one, the event is fired on the most specific ancestor element that contained both.

click fires after the mousedown and mouseup events, in that order.

我的理解就是:我点了鼠标,浏览器就生成了 click事件,管它有没有谁监听,即使没人理,也要传递一遍。

  1. mouseover

The mouseover event is fired when a pointing device is moved onto the element that has the listener attached or onto one of its children.

移动鼠标,浏览器不一定产生 mouseover事件;只有相关元素监听了 mouseover这类事件,你再去 over,浏览器才会产生 mousemove事件。(这和 click就不一样了;得监听了才有)

  1. mouseenter

The mouseenter event is fired when a pointing device (usually a mouse) is moved over the element that has the listener attached.

Though similar to mouseover, it differs in that it doesn’t bubble and that it isn’t sent to any descendants when the pointer is moved from one of its descendants’ physical space to its own physical space.

下面的图,和图下面的话,我不敢苟同。图中左边和右边情况都不明了,有啥可比性?左边四层div都挂了 mouseenter?还是右边最外层div挂了一个mouseover?

假设左边四层div都挂了 mouseenter,右边最外层div挂了 mouseover。 (默认排版,不加多余样式。)

  • 如果 Text 所处的位置在 他们内部,左边进去到Text上就依次产生了四个 mouseenter事件;右边进去也产生了四个 mouseover事件,每个还冒泡。
  • 如果 Text 所处的位置在 他们外部(比如 fixed 设定位置),那么移动到Text上,左边依次产生了4个事件。右边产生一个事件,冒泡上去。
  1. mousemove

The mousemove event is fired when a pointing device (usually a mouse) is moved while over an element.

这……我在一个元素上晃鼠标,就产生 mousemove事件吗? overenter至少说了需要listener,这个,居然啥也没说?

疑问

就抄这么多了。

  1. 事件的产生

我 click, over, enter, move 时,产生事件有啥条件?

从上面抄的来看,click,就会产生 click事件。这个事件对象的产生没啥要求?即使没有注册一个监听函数?那么这个 click事件 就是打酱油?啥也不干?那要它有何用?当页面单独放一个 checkbox 的时候,点击它,有反应,说明浏览器内部还是有监听的。

  1. 事件的传播

这个三阶段模型也怪怪的。

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
<!DOCTYPE html>
<html>
<head>
<title>
gg
</title>
</head>
<body>
<div id="test">
<input type="checkbox" id="cb">
</div>
<script>
// C capture, B bubble.
cb.addEventListener('click', e=>{
console.log('cb ', e.eventPhase, e.path);
});

test.addEventListener('click', e=>{
console.log('test C ', e.eventPhase, e.path)
}, true);

test.addEventListener('click', e=>{
console.log('test B ', e.eventPhase, e.path);
});

window.addEventListener('click', e=>{
console.log('w C ', e.eventPhase, e.path);
}, true);

window.addEventListener('click', e=>{
console.log('w B ', e.eventPhase, e.path);
});
</script>
</body>
</html>

任意一个都可以加 e.stopPropagation(),不影响 checkbox 能否选中,影响click事件传播;如果有一个加了 e.preventDefault(),就会影响 checkbox 选中对号,cb.checked === false,也就是不能选中,但不影响click事件传播。

事件非要从window一路下来,一个不拉到达目标?我如果一个事件监听函数都不注册,点击 checkbox,click事件还真的会千辛万苦
window->document->html->body->div#test->input#cb->div#test->body->html->document->window 走一圈?何必呢?一个事件的生命周期有多久?走到头或者stop就消散了吗?

未完待续。

checkbox notes

偷窥MouseEvent的时候,就把栗子抄了过来

1.

1
2
3
4
5
6
7
8
9
10
11
<p><label id="label"><input type="checkbox" id="checkbox"> Checked</label>

<script>
var evt = new MouseEvent("click", {
bubbles: true,
cancelable: true,
view: window
});
var cb = document.getElementById("checkbox");
cb.dispatchEvent(evt);
</script>

这里调用了一次 cb.dispatchEvent(evt); 就可以选中复选框。可是我又疯狂的调用 cb.dispatchEvent(evt); 没有什么效果。

然后我只能把 evt 再赋值一次,然后 dispatch 就好了。每次dispatch之前,evt要是全新的。这样才能选中、取消。

不知为何,每次需要刷新 evt? 和 evt.timeStamp 有关系吗?对这里的自定义事件不是很清楚。

2.

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
<p><label id="label"><input type="checkbox" id="checkbox"> Checked</label>

<script>
label.addEventListener('click', function (e) {
if(e.target===this) {
console.log('label target');
} else {
console.log('label capture')
}
}, true);

label.addEventListener('click', function (e) {
if(e.target===this) {
console.log('label target');
} else {
console.log('label bubble');
}
}, false);


window.addEventListener('click', (e) => {
console.log('window capture');
}, true);

window.addEventListener('click', (e) => {
console.log('window bubble');
}, false);
</script>

点击 checkbox,没什么大惊小怪的。
点击 label,可以看出:处理完了label上的事件,会再触发一次 checkbox 的 click事件。

  1. 做点笔记
    1
    <body style="height:1000cm; width:250cm;">
1
2
<!doctype html>
<body style="height:1000cm; width:250cm;">
1
2
<!doctype htmlxxoo>
<body style="height:1000cm; width:250cm;">

这几种情况下,document.documentElement.clientHeight 值是不一样的, document.documentElement.clientWidth 却是一样的。我只是想拿到 viewport 的值。我自己的感觉不知道说啥了。这都啥和啥啊?

备忘:
screen.width 在 FF 和 Chrome 指的就不一样;缩放一下页面,就看出来了。

不要提物理像素点了。 操作系统的 分辨率 也是一层抽象,应用程序就当是 OS 给你提供的 物理像素数据 吧。 还有css单位啥的。将这些东西都放在不同的坐标空间中,关系一目了然。莫再被 devicePixelRatio 等概念糊弄了。莫再提,莫再讲。

funargs

一路走来。很多东西都是随性而写。很多问题,并未能理解的深刻。问题就像递归一下,要解释这个问题,不得不去解释另一个问题;也怪自己不能很合适的抽象。

今天又是一个乱炖。

function arguments

1
2
3
4
5
function yes(){}
function no(){}
function yesno(no, yes){ return yes;}

yesno(yes, no);

函数就是一个值,可以做参数,也可以当返回值。也因此,在JavaScript中,函数参数大行其道,尤其是 callback,无处不在。

1
2
3
window.addEventListener("click", function() {
console.log("window was clicked!");
});

再举一个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function job1() {console.log("job1 !!!");}
function job2() {console.log("job2 !!!");}
function job3() {console.log("job3 !!!");}

function jobs(cb) {
console.log("jobs ...");
cb();
}

jobs(function () {
job1();
jobs(function () {
job2();
jobs(function() {
job3();
})
})
})

我也不知道为啥举了这个栗子,哎。人家都说回调多了,就进了地狱,我就是尝试一下地狱。

复制粘贴大法好

Callback function A callback function is a function passed into another function as an argument, which is then invoked inside the outer function to complete some kind of routine or action.

粘贴一些小的知识点

打开 谷歌浏览器的 console。我们来看点东西。我这里是一句一句执行的。

没毛病。haha的 Scopes 只有 Global,我这里没有定义 t。

擦!haha的 Scopes 变了,[Script, Global]。这就是 let 搞得鬼。let 偷偷修改了作用域。(const 同理)

这也就是为什么 t 有值,但是在 this 中找不到!!!

上面说了 let 偷偷修改了作用域,不仅仅是新建作用域这么简单

下图中,Scopes 多了 Block。第一个函数是被优化了,两个其实应该是一样的。作用域链上没用到的,谷歌浏览器直接踢了。

为毛一个多了 Script,一个多了 Block;简而言之,一个是修改,一个是新建(未来会不会有什么变化,到时再看)。这里没有谈及其他的特性,Scoping,TDZ,redeclaration。你以为新语法只有好处拿???不得不说,带来了不少新的问题。有空再搞一搞吧。请访问化学家兼计算机科学家的const 和 let 到底定义到哪儿去了?

1
2
3
4
5
6
7
8
9
10
11
<script>
let t = "go";
function ttt(){return t;}
console.dir(ttt);

{
let t = "go";
function ttt(){return t;}
console.dir(ttt);
}
</script>

JS代码所处的环境就像这样 Global.cpp_initialize(/** **/),这一句是外界(浏览器? JSVM?对不起,我不知道)提供,换句话说,我们的这些代码也处在一个外界提供的作用域;由外界提供 this,浏览器中就是 window。

我以前说过 this is not important ,是想说,很多时候你就不必关心 this 。arrow functions 还想探究一下;还是算了,扯着扯着就扯到蛋了,越来越远。感觉又挖坑了!我今天是来搞 function 的,callback 的。打算改写下 callback 的样子。

从网上搜了不少文章,谢谢各位前辈,写下自己的理解。

(写代码好吃力,各种错误;去网上粘点吧)

这里,我先发送一个请求,完了再发送第二个请求。(辣鸡代码,未处理异常,慎重!)

  1. callback
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    var url1 = "https://yesno.wtf/api";
    var url2 = "https://yesno.wtf/api";

    function request(url, f) {
    var xhr = new XMLHttpRequest();
    xhr.addEventListener('load', function (e) {
    f(xhr.response);
    });
    xhr.responseType = "json";
    xhr.open('get', url);
    xhr.send();
    }

    request(url1, function cb1(r) {
    console.log("1st: %o", r);
    request(url2, function cb2(r) {
    console.log("2nd: %o", r);
    // request(url3, function cb3() {});
    });
    });

如果想依次发多个请求,把自己就回调进去了,真是焦头烂额。

  1. 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
    const url1 = "https://yesno.wtf/api";
    const url2 = "https://yesno.wtf/api";

    function requestP(url) {
    return new Promise((s, f) => {
    let xhr = new XMLHttpRequest();
    xhr.addEventListener('load', function (e) {
    s(xhr.response);
    });
    xhr.responseType = "json";
    xhr.open('get', url);
    xhr.send();
    });
    }

    requestP(url1)
    .then(v=>{
    console.log("1st: %o", v);
    return requestP(url2);
    })
    .then(v=>{
    console.log("2nd: %o", v)
    // return requestP(url3);
    });

这样就把 回调 放在 then 里面了。 // 看了一眼别人的代码,哦,懂了。自己写起来,半天挤不出来。唉……

  1. generator
    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
    const url1 = "https://yesno.wtf/api";
    const url2 = "https://yesno.wtf/api";

    function requestN(url) {
    var xhr = new XMLHttpRequest();
    xhr.addEventListener('load', function (e) {
    //console.log(xhr.response);
    rg.next(xhr.response);/*我们再次启动 rg,并把结果塞回去*/
    });
    xhr.responseType = "json";
    xhr.open('get', url);
    xhr.send();
    }

    function* requestG() {
    let r1 = yield requestN(url1);
    console.log("1st: %o", r1);
    let r2 = yield requestN(url2);
    console.log("2nd: %o", r2);

    /*
    let r3 = yield requestN(url3);
    console.log("3rd: %o", r3);
    */
    }

    let rg = requestG();
    rg.next(); // 启动

rg 执行到了 yield 那里,就会计算后面的表达式,然后吐出一个这样的对象 {value: 值, done: false},rg 就休息了(我也感觉困了);当 xhr load 以后,执行语句 rg.next(xhr.response),就会再次唤醒 rg ,并且 yield 返回值就是此时传进去的参数,也就是说把 xhr.response 给了 r1;下次遇到 yield ,rg 又歇菜了。 (至于 rg.throw,rg.return 是干嘛的,我还没用过呢。)

简单说,yield 先要吐出来一个值;再次使用 next 启动时,yield 还要返回一个值,这货还脚踩两只船。吐出来的值(吐一个promise也行,吐一个函数也行),我们这里没用到;我们这里就只利用了 yield 返回值,也就是我们再次启动时,传进去的值,这样才能把结果给 r1、r2。

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
const url1 = "https://yesno.wtf/api";
const url2 = "https://yesno.wtf/api";

function requestP(url) {
return new Promise((s, f) => {
let xhr = new XMLHttpRequest();
xhr.addEventListener('load', function (e) {
s(xhr.response);
});
xhr.responseType = "json";
xhr.open('get', url);
xhr.send();
});
}

function* requestG() {
let r1 = yield requestP(url1);
console.log("1st: %o", r1);
let r2 = yield requestP(url2);
console.log("2nd: %o", r2);

/*
let r3 = yield requestP(url3);
console.log("3rd: %o", r3);
*/
}

let rg = requestG();
let p = rg.next(); // 启动

p.value // p 是 yield 吐出来的 {value: promise, done: false},我们取 promise。
.then(v=>{
console.log("give result to r1");
let p = rg.next(v);
return p.value;
})
.then(v=>{
console.log("give result to r2");
let p = rg.next(v);
return p.value;
})

代码写的虽然和狗屎一样,但是这次 yield 吐出了一个 promise 的值,(上此吐出来的值,我们并未在意)。我们在 then 中 调用了 next 方法,再次启动 generator,启动的时机不同。

  1. async/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
    25
    26
    27
    28
    const url1 = "https://yesno.wtf/api";
    const url2 = "https://yesno.wtf/api";

    function requestP(url) {
    return new Promise((s, f) => {
    let xhr = new XMLHttpRequest();
    xhr.addEventListener('load', () => {
    s(xhr.response);
    });
    xhr.responseType = "json";
    xhr.open('get', url);
    xhr.send();
    });
    }

    async function requestA() {
    let r1 = await requestP(url1);
    console.log("1st: %o", r1);
    let r2 = await requestP(url2);
    console.log("2nd: %o", r2);

    /*
    let r3 = await requestP(url3);
    console.log("3rd: %o", r3);
    */
    }

    requestA();

await 和 yield 很像,不过更过分。 await “等到”了完成了的 Promise,就会再次启动,并把值返回。

这四个功能基本相同,但写法却有很大不同,主要是越来越简洁清晰,爽快。

yield; next

看看它们的嘴脸。

1
2
3
4
5
delete Function; // Don't worry, be happy!
Function = Object.getPrototypeOf(function () {}).constructor; //这些内置对象,不在三界五行之中。你看它的“定义域”就知道了。
GeneratorFunction = Object.getPrototypeOf(function* (){}).constructor;
AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
AsyncGeneratorFunction = Object.getPrototypeOf(async function* () {}).constructor;

关于generator的自动执行。 yield 吐出来的那个值,在这个值完成了相关任务后,调用 next(result) 再返回结果。具体参考 co 库(听别人说的)。如今还是连字符串操作都不熟练,没心思吹了,打基础去了。

突然想到了这几个 request 函数,比如第一个。request执行了,内部 xhr 去哪里了?虽然上面挂着 load 事件。惊出了我一身冷汗,我再仔细想想。

1
2
3
4
5
6
7
8
9
10
function request(url, f) {
var xhr = new XMLHttpRequest();
xhr.addEventListener('load', function (e) { // 这个函数定义时候,引用了 xhr。 内存是否会泄露?
f(xhr.response);
xhr = null; // 这样是否会好点?
});
xhr.responseType = "json";
xhr.open('get', url);
xhr.send();
}

成天就知道玩弄概念。还是该多练。熟能生巧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
for(let i=0; i<10000; i++) {
setTimeout(v=>console.log(i),); // v没啥用,我就是不想打那俩个括号
}

var url1 = "https://yesno.wtf/api";
var url2 = "https://yesno.wtf/api";

function request(url, f) {
var xhr = new XMLHttpRequest();
xhr.addEventListener('load', function (e) {
f(xhr.response);
});
xhr.responseType = "json";
xhr.open('get', url);
xhr.send();
}

request(url1, function cb1(r) {
console.warn("1st: %o", r);
request(url2, function cb2(r) {
console.warn("2nd: %o", r);
// request(url3, function cb3() {});
});
});

先放一万个事件到队列。看结果,xhr 是在半路输出的,插队了。 xhr 很及时,也可能和 timeout 不在一个队列。

key? key!

偷偷摸摸看了阮一峰的数字签名是什么?,评论也都很精彩。小弟也来凑凑热闹,蹭蹭热度。说一下自己的理解,各位尽情可喷。

心中纵有千般疑问,也只能埋在心里。(搞不定数学啊!这里不谈数学,数学太他么难了。)

我觉得要一个一个问题来说。

私钥与公钥

这个我也不懂,我又不懂数学,我只会用程序生成。不过不要紧,只需记得它俩天生一对就行了。啥意思?假设有 【私钥A、公钥A】 和 【私钥B、公钥B】,还有一个字节串(文本文件,二进制文件都是字节串),如果我用 私钥A 加密一下,那么就只能用 公钥A 解密;用 公钥A 加密一下,也只能用 私钥A 解密。不关B什么鸟事。也不知道RSA钥匙空间是个啥情况?我就假设 钥匙对 的唯一性,只有配对才能互解;而这也是为啥可以加密,为啥可以签名的基石。

这里,还有几点说下。

  • 什么叫加密,解密?

加密就是换了一个形式,解密就是还原。举个栗子,伪代码,RSA_encrypt(bytes, 私钥A) ==> bytes', RSA_decrypt(bytes', 公钥A) ==> bytes 。注意,这里加密解密都是用 RSA 相关函数。特别注意 RSA_decrypt(bytes', 公钥B) 会出错(据说是满足不了一个数学条件)。还有,某些人会用 XXX_decrypt(bytes', 钥匙C) ==> bytes'' 这个嘛,大哥,我们用的是 RSA,你这个 bytes''还不如我手动打开 bytes' 在后面添加字节 xxoo,就好像是 decrypt(byptes', 'xxoo') ==> bytes''',我这是手动解密啊,哈哈。不知道说清楚没有?就是说我用RSA的算法加密了一个文件,你却用凯撒算法“解密”了一个新文件,说:“我解密了”。字节串变换字节串嘛。我上去就是一脚。

  • 公私有何区别?

私钥与公钥作为天造地设的一对,互相可解。看起来没啥区别啊!假如有一钥匙对,一个拿给你,一个拿给我。你加密,我就能解密;我加密,你也能解密。这不是公私不分嘛!其实这个钥匙对中,公私是分明的,你可以自己生成一对,打开看看它们的文本表示,私钥长得多,还可对私钥设置口令。有一个细节是,私钥与公钥加密、解密所用的函数都是不同的,大概 RSA_public_encrypt, RSA_private_decrypt; RSA_private_encrypt, RSA_public_decrypt (上一条的伪代码还是有些瑕疵。)所以,你要是把 私钥 当 公钥 分发出去,自己却留着 公钥 当 私钥,那么就会错误百出。只是,仅仅谈 加解密 这点,公私作用是一样的。

  • 丢了怎么办?

私钥丢失,比如说你唯一的电脑被儿子弄坏,修不好。恭喜你,你肯定有老婆了。只好重新生成秘钥对了。谁要是用你以前的公钥给你发密信,告诉他用新的公钥即可。

上面扯了这么久,下面我们来谈正事。

原本打算搞几张图片,活跃一下气氛,表达更形象。不会弄。算了。写到这,才发现,这个话题太大了,我根本驾驭不了。只能简化了。

  • 加密,我要给老王的老婆发消息,怕别人知道,就用老王的老婆的公钥加密一下,别人拿到也没用,老王的老婆拿到后,用她自己的私钥解密就行了。由此,可以看到,我想发密信给谁,用他的公钥加密后,再发,就比较放心了。

  • 签名,我先用老王的老婆的公钥把消息加密,然后我再用我的私钥加密一次。其他人拿到后,只能用我的公钥解开,大家一看,“哦,这是小李发出来的”,但是看不到我的原文;老王的老婆用我的公钥解密了,心里乐开了花,的确是小李发的,然后用她的私钥再解一次,就能看到内容了。或者,我先用我的私钥加密一次,再用老王的老婆的公钥加密一次,发出去。别人打不开,老王的老婆用私钥解密,再用我的公钥解密,“哦,是小李发来的”。在这里,我的私钥作用就是签名,确实是我(即使不是我,也是我的那个私钥)发出来的消息。实际不是这么用的,一般都是签在摘要上面。

  • 完整性,这里也提一提,毕竟还是容易混淆。想象这样一个场景,我发了 [str, hash(str)],你收到了,然后自己对str进行了hash,发现没错,那么str就是完整的;即使哪个2B截获了,改成[str’, hash(str’)],再发给你,你收到了,然后自己对str’进行了hash,还是没错,那么str’就是完整的。哈哈,这就是完整性。但是,情况不是这样的,坏就坏在,我像个傻逼一样,不该把 hash值发给你。我就发个 str给你,你算完hash,打电话(假定电话安全)和我核对一下。这里你可能要问,为啥不加密?因为我仅仅想说一下完整性这个概念,不想谈及安全。就比如,你昨天写了点字符串,你就可以算一个hash值,记在纸上;今天你想看看是不是完整的,有没有被改动,就再对它hash一下,和昨天的纸上记载的对比一下。当在网络上传输的时候,情况就开始复杂了。完整性本来是需要保存正本,然后副本来和正本一一对比;不过那样代价太大了,而hash,比如sha1提供了足够的可靠性,并且效率高啊,所以就用hash值来比较。

可以完全不提人,就画很多的钥匙对,概念可能更准确。但是实际钥匙都是个人或者机构持有的,所以就说成了谁的私钥,谁的公钥。比如 私钥小明 和 公钥小明 是一对的,如果某天 私钥小明 落入到了 大锤 手里,大家可能还不知道。 钥匙确实是配对的,但所属变了。对于签名来说,用 公钥小明,解开的一定是 私钥小明,我们就认为这个是消息是小明的,没想到却是大锤发的。严谨来说,我们可以说这个消息是 私钥小明 持有者发的。不客气的说,公钥小明 解开的,就是 私钥小明 签名的。具体谁人做的,无法保证,但一般就认为是小明,不排除小明给她老婆共享了 私钥小明

上面简介了三个概念,不过都很理想;当进入到现实的时候,很多问题就要考虑了。首要考虑安全,安全第一嘛!私钥的安全就要靠你自己了。别人要是把刀驾到你的脖子上,那也保不住了。我们只考虑公钥的安全。

理想与现实

我们也只考虑网络传输(你驱车万里送一个文件,或者通过安全电话一个字节一个字节口读1G的文件给对方,也当我啥也没说)。

我们假设老王是不敢威胁他老婆的,她老婆的私钥是安全的,只有她自己持有。也就是假设每个人的私钥是安全的(如果你被控制人身自由,电脑被控制,身不由己,就当我啥也没说,你也别往下看了;凡事都要满足一定的条件才能成立)。

(TMD,编不下去了)

你拿到了C的公钥,那就一定是C的公钥吗?

未完待续

nginx proxy

今天玩弄 aliyun ECS 的时候,在远程机上抄了一个 koa 的例子,哈哈,太不专业了。却不知道如何跑起来。就在网上乱搜一通。简单了解了下nginx 代理,下面谈点我的理解,仅仅是自己思考,没什么全面性和系统性。

正向代理

网上的例子。 就拿web访问来说,上谷歌上不去,弄个代理;有时候想隐藏自己的ip,弄个代理。

大概就是 client <-> proxy <-> server,(不管什么路由器,经过了多少网络节点……,只考虑这个三节点)。单看正向代理,第一是关于 client 的事情,不管它能不能直接访问 server,它都可以选择使用 proxy,比如上谷歌,能不能直连,不影响你是否能使用 proxy,或许你仅仅是想隐藏什么。第二关于 proxy ,它的确需要两头都能连通,它要提供功能,当 client 使用了它的时候,发出一个对 server 的请求,这时候你使用的 proxy 就接管了这个请求;至于是否隐藏 client 的IP等信息,就看 proxy 是如何实现的,它是可以把 client 的IP信息发给 server 的。整个流程看起来就是 client 访问了 server。然而,实际,client 只是访问了 proxy 而已;或者 clientproxy 说,请帮我访问下 server

再说一句,连不上谷歌,使用代理服务器,只是正向代理的一个场景。而能连上谷歌,你也可以使用代理服务器,因为你可能想做些私人的事。client 在某种程度上糊弄了 server

反向代理

比如我访问 https://www.aliyun.com/xxhttps://www.aliyun.com/oo (本故事纯属虚构,如有雷同,纯属巧合!)。我本以为是这样的 C <-> S。但是实际情况可能是 C <-> P <-> S1(xx)C <-> P <-> S2(oo) 。其实我访问的是 P,它把不同种类的请求分发给了后面不同的服务器。在 C <-> P 中, 这个 P 作为一个反向代理,糊弄了 C ,让整个流程变成 C <-> P <-->Ss。单看反向代理,就是 P 的事情,它会把请求分发到一堆 Ss;而 C 是不知道的,除非 P 愿意泄露点啥。

代理

然后就变成了 C <-> Pforward <-> Preverse <-> SsC 通过 Pforward 糊弄 PreversePreverse 用了一堆 Ss 反过来也在糊弄 PforwardC 通过 Pforward 以为自己访问了 Preverse,实际 Preverse 却把请求给了 Ss

悲伤

总想表达更清楚点,奈何几乎能一年无话,撞了南墙也不回头。写东西也是把自己绕进去。

实际应用

我在 aliyun ECS,开了 nginx 监听 80 端口,其中 location /hw { proxy_pass http://127.0.0.1/3456; } ,location /danteng { proxy_pass http://127.0.0.1/6543; } ;开了两个 node 应用,一个监听 3456,一个监听 6543。然后 访问 */hw 和 */danteng,就访问了不同的应用,也就是服务吧。

notes on async/await

大家好,又到了新一期“扯蛋”时间了。

我几乎没怎么用过 callbackspromisegeneratoryield 这些听起来牛逼哄哄的东西。我直接来看 async/await。有这么多前辈高人,我这小菜就捡了便宜,越过了这一系列发展过程中的酸甜苦辣,直接去偷取“果实”。还有我看CSS的时候,是无视了IE6这个可怕时代,直接看CSS3的知识。这也就是大家所说的速成,没什么内力,没经历过,就不明白现在为啥是这个样子,不扯了,去吃完扯面再说。

我先是看了边城理解 JavaScript 的 async/await,受益匪浅;接着又找到了allen_heasync/await 执行顺序详解,这篇文章就是我想要的,看下面的评论,不管测试结果,我要的就是这篇的解释和这个思路。我瞅了一眼阮一峰Node 定时器详解,这和浏览器中的Event loop还是有很多不同之处,尤其每一轮事件循环中要干的活是大大的不同。

此篇仅仅是一个开始,还需投入精力加深理解。

  1. 让我们测试几个代码,学习研究之用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
console.log("script start...");

setTimeout(()=>console.log("next loop, obviously!"));

function ss() {
console.log("I'm ss");
return "ss";
}

async function test() {
console.log("test start...");
const r = await ss();
console.log(`r: ${r}`);
console.log("test end...");
}

test();

Promise.resolve('how to put a promise to the microtask queue?').then(console.log, console.error);

console.log("script end...");

结果在此,请不要偷看,O(∩_∩)O

script start…
test start…
I’m ss
script end…
r: ss
test end…
how to put a promise to the microtask queue?
next loop, obviously!

  1. 略微改动。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
console.log("script start...");

setTimeout(()=>console.log("next loop, obviously!"));

async function ss() {
console.log("I'm ss");
return "ss";
}

async function test() {
console.log("test start...");
const r = await ss();
console.log(`r: ${r}`);
console.log("test end...");
}

test();

Promise.resolve('how to put a promise to the microtask queue?').then(console.log, console.error);

console.log("script end...");

script start…
test start…
I’m ss
script end…
how to put a promise to the microtask queue?
r: ss
test end…
next loop, obviously!

分析一下

这两段代码几乎一毛一样。仅仅是 await 那里,第一次等了个 Function call,第二次等了 AsyncFunction call。说白了,第一次等待返回的不是Promise的值 “ss”;第二次等待返回的是一个Promise的值 #value:”ss”, status:”resolved”#。(这里用词不是很准确,比如 Promise的值,你懂得就好。至于第二个值,只是意思意思表示一下,大概就那样子,没有字面量表示法。)

这个 await 还是很骚的。

  • 第一次,哎呀,是个字符串,不是Promise的值,舍不得交出去,等等吧。跳出了test的执行(注意是跳出去了,暂停了,执行的现场还是保存了)。执行完后面的几句代码,然后搂不住了,await 只好把东西交出来,也就是继续执行。完事,没码可执行,就把 microtask queue 清空了。最后到下一轮task了。

  • 第二次,这货更加过分,因为是个Promise,这货就是不肯交出来。直到 microtask queue 清空了,才知道大势已去,也只能返回这个Promise的值,赶紧执行剩余代码。新的一轮工作又开始了。

简单说就是在 AsyncFunction call 中 await expression, expression的值算出来以后(这里也很讲究,expression的计算,如果放在主线程,还是要卡住,像ajax比较适合,反正不在主线程计算,马上能返回,最后给个结果就行),await 拿到值,现在是不会交出来的;
而且还要对别人说:“滚,离我远点。”,没办法,只能暂时跳出这个 AsyncFunction 了。执行后面的代码。完事后,这里也是一个关键点。如果 await 拿的不是一个 Promise的值,那它就要交出来,AsynFunction call得以继续执行,接着清 microtask queue;但如果 await 手里拿的是一个 Promise的值,不管是pending状态还是完蛋状态,都会先清 mircotask queue,它会死皮赖脸的,要等 Promise 计算完后(非pending状态) ,才肯交出那个值,继续从暂停的地方执行。

当然,这个流程不是 await 控制的,只是把 await 拿到第一人称比较形象。这货就是个拖啊,先拖住再说。 await expression,expression计算出来以后,会有一跳,这就改变了代码的执行流程;待后来,它王者归来,返回胜利的果实。所以,它有自己的适用范围。你确实什么都可以给它 await 一下,但你从这个执行流程也可以看出来,你 await 的任务,如果要在主线程执行,也是没啥卵用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
**含有伪代码,慎重
*/

jobs();//简单任务

async function B2() {
let p = await new Promise(r=>{
sleep(一天);
setTimeout(()=>{
sleep(一天);
r("呵呵")
},一天);
});
console.log("p: " +p);
}

B2();

Steve_Jobs();//复杂计算
/**/

看吧,await 后面的先要睡一天,setTimout就几毫秒吧(我机子,console.time('sT');setTimeout(function(){},1000);console.timeEnd('sT');,cpu G3220,Chrome71 0.05 ~ 0.13 ms, node v10.4.0 0.06 ~ 0.95 ms,交互式不超过0.1ms,文件式就比较耗时,但也没有超过1ms;仅做参考,机器及环境相关),await拿到Promise(是pending状态)。出去算完 Steve_Jobs。 一天后,setTimeout任务来了,可是它又睡了一天,然后 Promise 就resolved了。此时 p 值才得以打印。流程就是这样。或许浏览器会直接让你滚蛋,他么的什么破代码,想卡死我?。nodejs上我也没有试过。

当然,小弟的栗子不好吃,不太恰当。

在下在上篇文章中,也说了,不爱提什么同步异步的。如下片段,

1
2
3
r1 = await job1();
r2 = await job2(r1);
r3 = await job3(r2, r1);

异不异步我不管,你口中的异步任务,还是按次序来。重要的是次序。

不过,

1
2
3
let r = await ajax(some-url); 
callback(r);
//...

确实比

1
2
ajax(some-url, callback);
//...

看起来舒服多了。尤其是你安排很多后续任务,才会有真正的优势。本篇的重点是刻画一下 await 这个货。具体怎么使用,我也不会。拜拜,我们下期再见。

刚才又想了想,有些地方不对

上面吹了那么多牛,后来,我发现,自己不能解释下面的代码,次序都是 step1->step2->step3;可是两段代码的互相穿插,穿插的顺序呢? await 到底和 then 之间有着怎样的爱恨情仇?

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
function takeLongtime(n) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(n + 200), n);
})
}

function step1(n) {
console.log(`step1: ${n}`);
return takeLongtime(n);
}

function step2(n) {
console.log(`step2: ${n}`);
return takeLongtime(n);
}

function step3(n) {
console.log(`step3: ${n}`);
return takeLongtime(n);
}

console.time('sT1');
step1(400)
.then(step2)
.then(step3)
.then(v=>{
console.log(v);
console.timeEnd('sT1');
});

async function af(){
console.time('sT2');
const n = 400;
const r1 = await step1(n);
const r2 = await step2(r1);
const r3 = await step3(r2);
console.log(`r3: ${r3}`);
console.timeEnd('sT2');
}

af();

这里我搞不清的是下面的问题:

  1. function cb(n) {}; setTimeout(cb, 5000, 1234);,当setTimeout执行了,那么可以认为Timer线程在五秒后会把 cb 及其参数 1234 打入冷宫,不,打入事件队列。主线程有空就执行一下。
  2. 那么
    new Promise(r=>{ setTimeout(()=>r("呵呵"), 5000) }).then(console.log, console.error);,它是如何执行的?
  • new Promise,首先setTimeout执行了,返回一个 pending 状态的 Promise 对象。(五秒后Timer线程会放 ()=>r('呵呵') 进入事件队列;当主线程执行了这个,Promise对象就会resolved,值就是 “呵呵”。)
  • 我想问的是,new Promise这个构造执行完,返回 pending 状态的 Promise 对象,然后呢?然后干什么?(离开这里,往下执行?将来再回来?)那后面的then也不能不管吧?既然是 pending,那也不能把后面 then 中的两个任务某一个放入 microtask queue 吧?应该是其他线程接管了。这个先放一放。

我又来装逼了

老阮的文章,其中还有朴灵的批注。啥也不说,看了看评论。只是觉得太浮躁了。细思极恐。披着各种外衣的。打着追求真理的幌子。所谓的概念不是自己的理解吗?不断提升吗?就一下子想得到正确的概念,正确的答案?真能一下子得到正确的概念,这个世界就简单了,还要学习吗?或者,哪有什么真理,你真的想过?写操作系统,写v8的那些人,代码还在不断更新,为啥不是一出来就是完美的?哪些可是真的大神啊。在哥德尔手里,数字能推翻公理化;在我手里也只能加加减减卡里剰几毛钱了。什么是“横看成岭侧成峰”,成天背几个概念,顶个毛用。线程咋咋,进程咋咋,你知道线程怎么表示?数据结构有多少字段?(sorry,我不知道)背那么清楚还不如踢足球?

1
2
3
window.isNaN(Object)
Number.isNaN(Object)
2**53 === 2**53 + 1

talk is cheap,show me the code。哈哈。

书是看不完的,得有自己的理解,思维。偷偷打基础,来日可与群雄一战。技术,知识将来才是真正的战场。

sync-and-async

同步?异步?

虽然是个菜,但是我还是有话要说。在学JS,等下就分析几个浏览器中JS代码片段。

同步,异步不知道谁翻译的。我书读的不多。可是觉得滥用了。这两个词,哎。我还是没有想的特别清晰。试着谈谈吧。

还得从其他地方说去,long long ago…

  1. 开始你有两只手,你左手码字,右手撸管。(手就是双核,码字,撸管就是进程啥的,脑子来调度。此操作可以是并行的,互不干扰,不行就让别人帮你。当然你也可以并发,去刷牙…)
  2. 后来你觉得没有节奏感,应该码一字,撸一下。此时还是并行,不过同步了一下。(什么同步?谁和谁?)
  3. 再后来你不小心被小郭妹子断了右手。
  4. 你在想该怎么办?
  • 你左手码完字,然后左手撸啊撸。(单任务)
  • 你左手码一字,左手又去撸一下。(并发了。不过来回切换上下文,也累啊。)

说了半天,什么是同步?异步?第二步提到的同步是啥?因为在第二步中,我们为了追求节奏感,可以认为这两个进程或者操作变成了一个大任务。而同步异步应该说的就是一个大任务中的各个小任务之间的执行顺序。我在人民广场吃炸鸡,你在网吧打游戏,这同不同步,异不异步有何用?除非弄成一个任务的两部分。

这里我们要提到层次结构。因为任务是可以细化的。

一个进程的生命周期中,每个线程一定是按照一定的次序执行的。不管是挨个执行指令,还是分支,循环,跳转……都是按次序的。虽然线程与线程之间看起来没了先后次序,那每个线程内部还是按次序执行。对这些操作系统理论,不清楚。但我坚信,局部是有次序的,它们一定会遵循一定的原则。一堆进程,看起来,乱乱的,但是每个进程内部都是有严格次序的,不会跑过去执行另一个进程空间的指令。就进程内部来说,一堆线程,乱乱的,可是每个线程也是有自己的次序。次序永存。

直接上码。无码最可耻。

1
2
3
4
5
6
7
function job1(){return "job1";}
function job2(){return "job2";}
function job3(){return "job3";}

job1();//1
job2();//2
job3();//3

这段代码的执行,有次序,1=>2=>3。

1
2
3
4
5
6
7
function job1(){return "job1";}
function job2(){return "job2";}
function job3(){return "job3";}

job1();//1
setTimeout(job2, 24*60*60*1000);//2
job3();//3

这段代码的执行,有次序,1=>2=>3。 job1执行;然后setTimeout执行,这里发生了一件事,它郑重的把job2和86400000交给了Timer线程,然后立马返回一个timeoutID(定时器编号,可使用clearTimeout清除),不过我们没保存这个值;然后job3执行。对吗?线程内还是按次序执行,一步一步来嘛(同步这个词实在让人困惑)。job1=>setTimeout=>job3,不费吹灰之力。大概在第二天的此刻却发生了一个可怕的事情,job2冒了出来(异步这个词也实在让人困惑)。在我看来他们完全是按照次序,即使你把job2放到事件队列,那还是按次序来,job2它自己内部的代码也是按次序在主线程执行。a=>b=>c是次序, b=>a=>c也是次序。job1,job2,job3每个里面都可以再加任务。

  1. 就算你把第二句换成 ajax(some-url, job2),还是job1=>ajax=>job3这个次序,(ajax把some-url, job2交给其他线程,立马返回。那个线程干完活,就把job2加到队列)不知啥时候job2会给你来一下。
  2. 如果ajax不是立马返回,(浏览器应该是废弃这种用法了),次序就是job1=>ajax-job2=>job3。(ajax-job2, ajax执行,调用job2,ajax返回)

如果不是立马返回,还有啥意思,难道要setTimeout(job2, 24*60*60*1000);一直等?不管在现实生活中还是在计算机系统中,都是不合理的!就是要把job2扔到队列中。他们把这种叫异步编程。如今,你可以想办法把它扔到task queue,或者microtask queue。但是话又说回来,当你明白了次序,你也会明白,啥都往队列中塞,也是不好的做法;举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
function job1() {return "job1";}
function job2() {
let i=0;
while(i<Number.MAX_SAFE_INTEGER){
i++;
}
return "job2";
}
function job3() {return "job3";}

job1();//1
setTimeout(job2, 1000);//2
job3();//3

你把job2扔到队列中也是没用的。到头来,它还是会回到主线程执行,说不定卡死。这种应该新开个Woker线程。

瞎扯事件

什么是事件。你对着浏览器大喊,“你个2B”,浏览器显示,“滚蛋”。这就是声音事件和浏览器对事件的处理。可惜的是,浏览器不是你实现的,要是你实现的,你想加什么事件都行,你还可以完全不处理鼠标事件。不过,估计也没有什么人会用你的浏览器了。

浏览器中有很多预定义的事件。你点一下鼠标,按一下键盘,都会出事。浏览器这个应用程序也真是复杂啊,操作系统GUI有一套消息机制,浏览器拿到这些消息,又在内部实现了自己的事件机制。一般的应用程序,处理下操作系统的消息就不错了,像win32中的 WM_QUIT……。Windows中的消息,有的消息是不进队列的,有的消息有时候不进有时候进。那看看浏览器。

1
2
3
4
5
6
7
8
9
function _2b(z){console.log('2B');}
window.addEventListener('click', _2b)
/*
some jobs
*/
document.body.click();
/*
some tasks
*/

一般用户都是用鼠标点击页面,_2b入队列。 代码中click函数触发,直接调用 _2b,不入队列。

我现在也没想好下面说啥。但我不怎么喜欢在这里用同步、异步。就是Script执行完,掏空microtask,然后UI善后;再拿一个task,完了掏空microtask,UI善后;……

谁和谁同步,谁和谁异步?晕。把job2放到队列叫异步?那拿到job2执行的时候,不就是同步了?同步的任务,里面没有异步代码?异步的任务,里面没有一句同步代码?两个语句什么关系?两个函数什么关系?两个任务什么关系?两个模块什么关系?你把馒头吃到嘴里,再到胃里。若说这两个是同步任务,那吃到嘴里细分,切牙咬和磨牙磨这小任务是什么关系?我觉得不需要这些伪概念。我觉得JS运行时这个层面,没啥同步、异步,只有Event Loop。所有的任务都遵守着看不见的秩序。

未完待续