JavaScript Event Loop 机制详解与 Vue.js 中实践应用

2017/09/09 · CSS · Event
Loop,
Vue

原文出处: 王下邀月熊   

JavaScript Event Loop 机制详解与 Vue.js
中实践应用归纳于笔者的现代
JavaScript
开发:语法基础与实践技巧系列文章。本文依次介绍了函数调用栈、MacroTask
与 MicroTask 执行顺序、浅析 Vue.js 中 nextTick
实现等内容;本文中引用的参考资料统一声明在 JavaScript
学习与实践资料索引。

用 JavaScript 写一个区块链

2018/04/09 · CSS ·
区块链

原文出处: Xavier
Decuyper   译文出处:百度外卖前端/JeLewine   

图片 1

几乎每个人都听说过像比特币和以太币这样的加密货币,但是只有极少数人懂得隐藏在它们背后的技术。在这篇博客中,我将会用JavaScript来创建一个简单的区块链来演示它们的内部究竟是如何工作的。我将会称之为SavjeeCoin!全文分为三个部分:

  1. part1:实现一个基本的区块链
  2. part2:实现POW
  3. part3:交易与挖矿奖励

SVG 创建 Material Design 波纹效果按钮

2017/10/16 · HTML5 ·
SVG

原文出处: Dennis
Gaebel   译文出处:码农网

小峰   

随着Google Material
Design的出现,一种旨在跨平台和设备创建统一体验的视觉语言由此横空出世。Google通过“Material
Guidelines”动画部分描述的例子是如此地拟真,以致于许多人将这些互动视为Google品牌的一部分。

在本教程中,我们将向大家展示如何在Google Material Design规范的Radial
Action下构建波纹效果,并结合SVG和GreenSock功能。

图片 2

在线演示  源码下载

1. 事件循环机制详解与实践应用

JavaScript
是典型的单线程单并发语言,即表示在同一时间片内其只能执行单个任务或者部分代码片。换言之,我们可以认为某个同域浏览器上下中
JavaScript 主线程拥有一个函数调用栈以及一个任务队列(参考 whatwg
规范);主线程会依次执行代码,当遇到函数时,会先将函数入栈,函数运行完毕后再将该函数出栈,直到所有代码执行完毕。当函数调用栈为空时,运行时即会根据事件循环(Event
Loop)机制来从任务队列中提取出待执行的回调并执行,执行的过程同样会进行函数帧的入栈出栈操作。每个线程有自己的事件循环,所以每个
Web Worker有自己的,所以它才可以独立执行。然而,所有同属一个 origin
的窗体都共享一个事件循环,所以它们可以同步交流。

Event Loop(事件循环)并不是 JavaScript
中独有的,其广泛应用于各个领域的异步编程实现中;所谓的 Event Loop
即是一系列回调函数的集合,在执行某个异步函数时,会将其回调压入队列中,JavaScript
引擎会在异步代码执行完毕后开始处理其关联的回调。

图片 3

在 Web
开发中,我们常常会需要处理网络请求等相对较慢的操作,如果将这些操作全部以同步阻塞方式运行无疑会大大降低用户界面的体验。另一方面,我们点击某些按钮之后的响应事件可能会导致界面重渲染,如果因为响应事件的执行而阻塞了界面的渲染,同样会影响整体性能。实际开发中我们会采用异步回调来处理这些操作,这种调用者与响应之间的解耦保证了
JavaScript 能够在等待异步操作完成之前仍然能够执行其他的代码。Event Loop
正是负责执行队列中的回调并且将其压入到函数调用栈中,其基本的代码逻辑如下所示:

JavaScript

while (queue.waitForMessage()) { queue.processNextMessage(); }

1
2
3
while (queue.waitForMessage()) {
  queue.processNextMessage();
}

完整的浏览器中 JavaScript
事件循环机制图解如下:图片 4

在 Web
浏览器中,任何时刻都有可能会有事件被触发,而仅有那些设置了回调的事件会将其相关的任务压入到任务队列中。回调函数被调用时即会在函数调用栈中创建初始帧,而直到整个函数调用栈清空之前任何产生的任务都会被压入到任务队列中延后执行;顺序的同步函数调用则会创建新的栈帧。总结而言,浏览器中的事件循环机制阐述如下:

  • 浏览器内核会在其它线程中执行异步操作,当操作完成后,将操作结果以及事先定义的回调函数放入
    JavaScript 主线程的任务队列中。
  • JavaScript
    主线程会在执行栈清空后,读取任务队列,读取到任务队列中的函数后,将该函数入栈,一直运行直到执行栈清空,再次去读取任务队列,不断循环。
  • 当主线程阻塞时,任务队列仍然是能够被推入任务的。这也就是为什么当页面的
    JavaScript
    进程阻塞时,我们触发的点击等事件,会在进程恢复后依次执行。

Part1:实现一个基本的区块链

响应式动作

Google使用Radial Action定义Responsive Interaction如下:

Radial action is the visual ripple of ink spreading outward from the
point of input.
The connection between an input event and on-screen action should be
visually represented to tie them together. For touch or mouse, this
occurs at the point of contact. A touch ripple indicates where and
when a touch occurs and acknowledges that the touch input was
received.
Transitions, or actions triggered by input events, should visually
connect to input events. Ripple reactions near the epicenter occur
sooner than reactions further away.

Google非常清楚地表述了输入反馈应从原点出发,向外扩散。例如,如果用户直接在中心点击按钮,则纹波将从初始接触点向外扩展。这就是我们如何指出触摸发生的地点和时间的方式,以便向用户确认接收到的输入。

2. 函数调用栈与任务队列

在变量作用域与提升一节中我们介绍过所谓执行上下文(Execution
Context)的概念,在 JavaScript
代码执行过程中,我们可能会拥有一个全局上下文,多个函数上下文或者块上下文;每个函数调用都会创造新的上下文与局部作用域。而这些执行上下文堆叠就形成了所谓的执行上下文栈(Execution
Context Stack),便如上文介绍的 JavaScript
是单线程事件循环机制,同时刻仅会执行单个事件,而其他事件都在所谓的执行栈中排队等待:图片 5

而从 JavaScript 内存模型的角度,我们可以将内存划分为调用栈(Call
Stack)、堆(Heap)以及队列(Queue)等几个部分:图片 6

其中的调用栈会记录所有的函数调用信息,当我们调用某个函数时,会将其参数与局部变量等压入栈中;在执行完毕后,会弹出栈首的元素。而堆则存放了大量的非结构化数据,譬如程序分配的变量与对象。队列则包含了一系列待处理的信息与相关联的回调函数,每个
JavaScript
运行时都必须包含一个任务队列。当调用栈为空时,运行时会从队列中取出某个消息并且执行其关联的函数(也就是创建栈帧的过程);运行时会递归调用函数并创建调用栈,直到函数调用栈全部清空再从任务队列中取出消息。换言之,譬如按钮点击或者
HTTP
请求响应都会作为消息存放在任务队列中;需要注意的是,仅当这些事件的回调函数存在时才会被放入任务队列,否则会被直接忽略。

譬如对于如下的代码块:

JavaScript

function fire() { const result = sumSqrt(3, 4) console.log(result); }
function sumSqrt(x, y) { const s1 = square(x) const s2 = square(y) const
sum = s1 + s2; return Math.sqrt(sum) } function square(x) { return x *
x; } fire()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function fire() {
    const result = sumSqrt(3, 4)
    console.log(result);
}
function sumSqrt(x, y) {
    const s1 = square(x)
    const s2 = square(y)
    const sum = s1 + s2;
    return Math.sqrt(sum)
}
function square(x) {
    return x * x;
}
 
fire()

其对应的函数调用图(整理自这里)为:图片 7

这里还值得一提的是,Promise.then 是异步执行的,而创建 Promise 实例
(executor) 是同步执行的,譬如下述代码:

JavaScript

(function test() { setTimeout(function() {console.log(4)}, 0); new
Promise(function executor(resolve) { console.log(1); for( var i=0 ;
i<10000 ; i++ ) { i == 9999 && resolve(); } console.log(2);
}).then(function() { console.log(5); }); console.log(3); })() //
输出结果为: // 1 // 2 // 3 // 5 // 4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(function test() {
    setTimeout(function() {console.log(4)}, 0);
    new Promise(function executor(resolve) {
        console.log(1);
        for( var i=0 ; i<10000 ; i++ ) {
            i == 9999 && resolve();
        }
        console.log(2);
    }).then(function() {
        console.log(5);
    });
    console.log(3);
})()
// 输出结果为:
// 1
// 2
// 3
// 5
// 4

我们可以参考 Promise 规范中有关于 promise.then 的部分:

JavaScript

promise.then(onFulfilled, onRejected) 2.2.4 onFulfilled or onRejected
must not be called until the execution context stack contains only
platform code. [3.1]. Here “platform code” means engine, environment,
and promise implementation code. In practice, this requirement ensures
that onFulfilled and onRejected execute asynchronously, after the event
loop turn in which then is called, and with a fresh stack. This can be
implemented with either a “macro-task” mechanism such as setTimeout or
setImmediate, or with a “micro-task” mechanism such as MutationObserver
or process.nextTick. Since the promise implementation is considered
platform code, it may itself contain a task-scheduling queue or
“trampoline” in which the handlers are called.

1
2
3
4
5
promise.then(onFulfilled, onRejected)
 
2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].
 
Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.

规范要求,onFulfilled 必须在执行上下文栈(Execution Context Stack)
只包含 平台代码(platform code)
后才能执行。平台代码指引擎,环境,Promise
实现代码等。实践上来说,这个要求保证了 onFulfilled
的异步执行(以全新的栈),在 then 被调用的这个事件循环之后。

区块链

区块链是由一个个任何人都可以访问的区块构成的公共数据库。这好像没什么特别的,不过它们有一个有趣的属性:它们是不可变的。一旦一个区块被添加到区块链中,除非让剩余的其余区块失效,否则它是不会再被改变的。

这就是为什么加密货币是基于区块链的原因。你肯定不希望人们在交易完成后再变更交易!

SVG中的径向动作

有许多开发人员创作纹波技术,主要使用CSS技术,如@keyframes,transitions,transforms伪技巧,border-radius以及甚至额外的标记,如span或div。不使用CSS,让我们来看看如何通过GreenSock的TweenMax库用SVG来创建这个径向动作。

3. MacroTask(Task) 与 MicroTask(Job)

在面试中我们常常会碰到如下的代码题,其主要就是考校 JavaScript
不同任务的执行先后顺序:

JavaScript

// 测试代码 console.log(‘main1’); // 该函数仅在 Node.js 环境下可以使用
process.nextTick(function() { console.log(‘process.nextTick1’); });
setTimeout(function() { console.log(‘setTimeout’);
process.nextTick(function() { console.log(‘process.nextTick2’); }); },
0); new Promise(function(resolve, reject) { console.log(‘promise’);
resolve(); }).then(function() { console.log(‘promise then’); });
console.log(‘main2’); // 执行结果 main1 promise main2 process.nextTick1
promise then setTimeout process.nextTick2

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
// 测试代码
console.log(‘main1’);
 
// 该函数仅在 Node.js 环境下可以使用
process.nextTick(function() {
    console.log(‘process.nextTick1’);
});
 
setTimeout(function() {
    console.log(‘setTimeout’);
    process.nextTick(function() {
        console.log(‘process.nextTick2’);
    });
}, 0);
 
new Promise(function(resolve, reject) {
    console.log(‘promise’);
    resolve();
}).then(function() {
    console.log(‘promise then’);
});
 
console.log(‘main2’);
 
// 执行结果
main1
promise
main2
process.nextTick1
promise then
setTimeout
process.nextTick2

我们在前文中已经介绍过 JavaScript
的主线程在遇到异步调用时,这些异步调用会立刻返回某个值,从而让主线程不会在此处阻塞。而真正的异步操作会由浏览器执行,主线程则会在清空当前调用栈后,按照先入先出的顺序读取任务队列里面的任务。而
JavaScript 中的任务又分为 MacroTask 与 MicroTask 两种,在 ES2015 中
MacroTask 即指 Task,而 MicroTask 则是指代 Job。典型的 MacroTask 包含了
setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI
rendering 等,MicroTask 包含了 process.nextTick, Promises,
Object.observe, MutationObserver 等。
二者的关系可以图示如下:图片 8

参考 whatwg
规范
中的描述:一个事件循环(Event Loop)会有一个或多个任务队列(Task
Queue,又称 Task Source),这里的 Task Queue 就是 MacroTask Queue,而
Event Loop 仅有一个 MicroTask Queue。每个 Task Queue
都保证自己按照回调入队的顺序依次执行,所以浏览器可以从内部到JS/DOM,保证动作按序发生。而在
Task 的执行之间则会清空已有的 MicroTask 队列,在 MacroTask 或者
MicroTask 中产生的 MicroTask 同样会被压入到 MicroTask
队列中并执行。参考如下代码:

JavaScript

function foo() { console.log(“Start of queue”); bar();
setTimeout(function() { console.log(“Middle of queue”); }, 0);
Promise.resolve().then(function() { console.log(“Promise resolved”);
Promise.resolve().then(function() { console.log(“Promise resolved
again”); }); }); console.log(“End of queue”); } function bar() {
setTimeout(function() { console.log(“Start of next queue”); }, 0);
setTimeout(function() { console.log(“End of next queue”); }, 0); }
foo(); // 输出 Start of queue End of queue Promise resolved Promise
resolved again Start of next queue End of next queue Middle of queue

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
function foo() {
  console.log("Start of queue");
  bar();
  setTimeout(function() {
    console.log("Middle of queue");
  }, 0);
  Promise.resolve().then(function() {
    console.log("Promise resolved");
    Promise.resolve().then(function() {
      console.log("Promise resolved again");
    });
  });
  console.log("End of queue");
}
 
function bar() {
  setTimeout(function() {
    console.log("Start of next queue");
  }, 0);
  setTimeout(function() {
    console.log("End of next queue");
  }, 0);
}
 
foo();
 
// 输出
Start of queue
End of queue
Promise resolved
Promise resolved again
Start of next queue
End of next queue
Middle of queue

上述代码中首个 TaskQueue 即为 foo(),foo() 又调用了 bar() 构建了新的
TaskQueue,bar() 调用之后 foo() 又产生了 MicroTask 并被压入了唯一的
MicroTask 队列。我们最后再总计下 JavaScript MacroTask 与 MicroTask
的执行顺序,当执行栈(call stack)为空的时候,开始依次执行:

《这一段在我笔记里也放了好久,无法确定是否拷贝的。。。如果有哪位发现请及时告知。。。(*ฅ́˘ฅ̀*)♡》

  1. 把最早的任务(task A)放入任务队列
  2. 如果 task A 为null (那任务队列就是空),直接跳到第6步
  3. 将 currently running task 设置为 task A
  4. 执行 task A (也就是执行回调函数)
  5. 将 currently running task 设置为 null 并移出 task A
  6. 执行 microtask 队列
  • a: 在 microtask 中选出最早的任务 task X
  • b: 如果 task X 为null (那 microtask 队列就是空),直接跳到 g
  • c: 将 currently running task 设置为 task X
  • d: 执行 task X
  • e: 将 currently running task 设置为 null 并移出 task X
  • f: 在 microtask 中选出最早的任务 , 跳到 b
  • g: 结束 microtask 队列
  1. 跳到第一步

  2. 浅析 Vue.js 中 nextTick 的实现


在 Vue.js 中,其会异步执行 DOM 更新;当观察到数据变化时,Vue
将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个
watcher
被多次触发,只会一次推入到队列中。这种在缓冲时去除重复数据对于避免不必要的计算和
DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue
刷新队列并执行实际(已去重的)工作。Vue 在内部尝试对异步队列使用原生的
Promise.then 和 MutationObserver,如果执行环境不支持,会采用
setTimeout(fn, 0) 代替。

《因为本人失误,原来此处内容拷贝了 https://www.zhihu.com/question/55364497
这个回答,造成了侵权,深表歉意,已经删除,后续我会在 github
链接上重写本段》

而当我们希望在数据更新之后执行某些 DOM 操作,就需要使用 nextTick
函数来添加回调:

JavaScript

// HTML <div id=”example”>{{message}}</div> // JS var vm =
new Vue({ el: ‘#example’, data: { message: ‘123’ } }) vm.message = ‘new
message’ // 更改数据 vm.$el.textContent === ‘new message’ // false
Vue.nextTick(function () { vm.$el.textContent === ‘new message’ // true
})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// HTML
<div id="example">{{message}}</div>
 
// JS
var vm = new Vue({
  el: ‘#example’,
  data: {
    message: ‘123’
  }
})
vm.message = ‘new message’ // 更改数据
vm.$el.textContent === ‘new message’ // false
Vue.nextTick(function () {
  vm.$el.textContent === ‘new message’ // true
})

在组件内使用 vm.$nextTick() 实例方法特别方便,因为它不需要全局 Vue
,并且回调函数中的 this 将自动绑定到当前的 Vue 实例上:

JavaScript

Vue.component(‘example’, { template: ‘<span>{{ message
}}</span>’, data: function () { return { message: ‘没有更新’ } },
methods: { updateMessage: function () { this.message = ‘更新完成’
console.log(this.$el.textContent) // => ‘没有更新’
this.$nextTick(function () { console.log(this.$el.textContent) // =>
‘更新完成’ }) } } })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Vue.component(‘example’, {
  template: ‘<span>{{ message }}</span>’,
  data: function () {
    return {
      message: ‘没有更新’
    }
  },
  methods: {
    updateMessage: function () {
      this.message = ‘更新完成’
      console.log(this.$el.textContent) // => ‘没有更新’
      this.$nextTick(function () {
        console.log(this.$el.textContent) // => ‘更新完成’
      })
    }
  }
})

src/core/util/env

JavaScript

/** * 使用 MicroTask 来异步执行批次任务 */ export const nextTick =
(function() { // 需要执行的回调列表 const callbacks = []; //
是否处于挂起状态 let pending = false; // 时间函数句柄 let timerFunc; //
执行并且清空所有的回调列表 function nextTickHandler() { pending = false;
const copies = callbacks.slice(0); callbacks.length = 0; for (let i = 0;
i < copies.length; i++) { copies[i](); } } // nextTick
的回调会被加入到 MicroTask 队列中,这里我们主要通过原生的 Promise 与
MutationObserver 实现 /* istanbul ignore if */ if (typeof Promise !==
‘undefined’ && isNative(Promise)) { let p = Promise.resolve(); let
logError = err => { console.error(err); }; timerFunc = () => {
p.then(nextTickHandler).catch(logError); // 在部分 iOS 系统下的
UIWebViews 中,Promise.then
可能并不会被清空,因此我们需要添加额外操作以触发 if (isIOS)
setTimeout(noop); }; } else if ( typeof MutationObserver !== ‘undefined’
&& (isNative(MutationObserver) || // PhantomJS and iOS 7.x
MutationObserver.toString() === ‘[object
MutationObserverConstructor]’) ) { // 当 Promise 不可用时候使用
MutationObserver // e.g. PhantomJS IE11, iOS7, Android 4.4 let counter =
1; let observer = new MutationObserver(nextTickHandler); let textNode =
document.createTextNode(String(counter)); observer.observe(textNode, {
characterData: true }); timerFunc = () => { counter = (counter + 1) %
2; textNode.data = String(counter); }; } else { //
如果都不存在,则回退使用 setTimeout /* istanbul ignore next */
timerFunc = () => { setTimeout(nextTickHandler, 0); }; } return
function queueNextTick(cb?: Function, ctx?: Object) { let _resolve;
callbacks.push(() => { if (cb) { try { cb.call(ctx); } catch (e) {
handleError(e, ctx, ‘nextTick’); } } else if (_resolve) {
_resolve(ctx); } }); if (!pending) { pending = true; timerFunc(); } //
如果没有传入回调,则表示以异步方式调用 if (!cb && typeof Promise !==
‘undefined’) { return new Promise((resolve, reject) => { _resolve =
resolve; }); } }; })();

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
/**
* 使用 MicroTask 来异步执行批次任务
*/
export const nextTick = (function() {
  // 需要执行的回调列表
  const callbacks = [];
 
  // 是否处于挂起状态
  let pending = false;
 
  // 时间函数句柄
  let timerFunc;
 
  // 执行并且清空所有的回调列表
  function nextTickHandler() {
    pending = false;
    const copies = callbacks.slice(0);
    callbacks.length = 0;
    for (let i = 0; i < copies.length; i++) {
      copies[i]();
    }
  }
 
  // nextTick 的回调会被加入到 MicroTask 队列中,这里我们主要通过原生的 Promise 与 MutationObserver 实现
  /* istanbul ignore if */
  if (typeof Promise !== ‘undefined’ && isNative(Promise)) {
    let p = Promise.resolve();
    let logError = err => {
      console.error(err);
    };
    timerFunc = () => {
      p.then(nextTickHandler).catch(logError);
 
      // 在部分 iOS 系统下的 UIWebViews 中,Promise.then 可能并不会被清空,因此我们需要添加额外操作以触发
      if (isIOS) setTimeout(noop);
    };
  } else if (
    typeof MutationObserver !== ‘undefined’ &&
    (isNative(MutationObserver) ||
      // PhantomJS and iOS 7.x
      MutationObserver.toString() === ‘[object MutationObserverConstructor]’)
  ) {
    // 当 Promise 不可用时候使用 MutationObserver
    // e.g. PhantomJS IE11, iOS7, Android 4.4
    let counter = 1;
    let observer = new MutationObserver(nextTickHandler);
    let textNode = document.createTextNode(String(counter));
    observer.observe(textNode, {
      characterData: true
    });
    timerFunc = () => {
      counter = (counter + 1) % 2;
      textNode.data = String(counter);
    };
  } else {
    // 如果都不存在,则回退使用 setTimeout
    /* istanbul ignore next */
    timerFunc = () => {
      setTimeout(nextTickHandler, 0);
    };
  }
 
  return function queueNextTick(cb?: Function, ctx?: Object) {
    let _resolve;
    callbacks.push(() => {
      if (cb) {
        try {
          cb.call(ctx);
        } catch (e) {
          handleError(e, ctx, ‘nextTick’);
        }
      } else if (_resolve) {
        _resolve(ctx);
      }
    });
    if (!pending) {
      pending = true;
      timerFunc();
    }
 
    // 如果没有传入回调,则表示以异步方式调用
    if (!cb && typeof Promise !== ‘undefined’) {
      return new Promise((resolve, reject) => {
        _resolve = resolve;
      });
    }
  };
})();

创造一个区块

区块链是由许许多多的区块链接在一起的(这听上去好像没毛病..)。链上的区块通过某种方式允许我们检测到是否有人操纵了之前的任何区块。

那么我们如何确保数据的完整性呢?每个区块都包含一个基于其内容计算出来的hash。同时也包含了前一个区块的hash。

下面是一个区块类用JavaScript写出来大致的样子:

const SHA256 = require(“crypto-js/sha256″); class Block {
constructor(index, timestamp, data, previousHash = ”) { this.index =
index; this.previousHash = previousHash; this.timestamp = timestamp;
this.data = data; this.hash = this.calculateHash(); } calculateHash() {
return SHA256(this.index + this.previousHash + this.timestamp +
JSON.stringify(this.data)).toString(); } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const SHA256 = require("crypto-js/sha256");
class Block {
  constructor(index, timestamp, data, previousHash = ”) {
    this.index = index;
    this.previousHash = previousHash;
    this.timestamp = timestamp;
    this.data = data;
    this.hash = this.calculateHash();
  }
 
  calculateHash() {
    return SHA256(this.index + this.previousHash + this.timestamp + JSON.stringify(this.data)).toString();
  }
}

因为JavaScript中并不支持sha256所以我引入了crypto-js库。然后我定义了一个构造函数来初始化我区块的属性。每一个区块上都被赋予了index属性来告知我们这个区块在整个链上的位置。我们同时也生成了一个时间戳,以及需要在区块里存储的一些数据。最后是前一个区块的hash。

创建SVG

不管你信不信,其实我们并不需要如Adobe
Illustrator或甚至Sketch这样花哨的应用程序来创作这个效果。SVG的标记可以使用我们可能已经熟悉并用到工作中的几个XML标签来编写。

<svg version=”1.1″ xmlns=””
xmlns:xlink=”; <symbol viewbox=”0 0
100 100″/> </svg>

1
2
3
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <symbol viewbox="0 0 100 100"/>
</svg>

对于使用SVG精灵图标的用户,你会注意到的使用。symbol元素允许在单个symbol实例中匹配相关的XML,并随后实例化它们,或者换句话说——就像盖章一样在整个应用程序中使用它们。每个盖章的实例与其唯一的创建者相同:它所在的symbol。

symbol元素接受诸如viewBox和preserveAspectRatio之类的属性,这些属性可以在引用use元素定义的矩形视口中提供符合缩放比例的能力。Sara
Soueidan写了一篇精彩的文章,并建立了一个交互式工具,以帮助你了解viewBox坐标系统。简单地说就是,定义初始的x和y坐标值(0,0),然后定义SVG画布的宽度和高度(100,100)。

这个XML拼图的下一个部分是添加我们打算动画化为波纹的形状。这是放入circle元素的地方。

<svg version=”1.1″ xmlns=””
xmlns:xlink=”; <symbol viewbox=”0 0
100 100″> <circle/> </symbol> </svg>

1
2
3
4
5
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <symbol viewbox="0 0 100 100">
    <circle/>
  </symbol>
</svg>

circle需要一些更多的信息,然后它才能在SVG的viewBox内正确地显示。

<circle cx=”1″ cy=”1″ r=”1″/>

1
<circle cx="1" cy="1" r="1"/>

属性cx和cy是相对于SVG
viewBox的坐标位置;我们的例子中就是symbol。为了使点击的时候感觉更自然,我们需要确保在接收到输入时触发点直接放在用户手指下方。

图片 9

上图中间那个例子,其属性创建了一个半径为1px大小为2px ×
2px的圆。这将确保我们的圆不会像最后那个示例中所看到的那样裁剪。

<div style=”height: 0; width: 0; position: absolute; visibility:
hidden;” aria-hidden=”true”> <svg version=”1.1″
xmlns=””
xmlns:xlink=””
focusable=”false”><symbol id=”ripply-scott” viewbox=”0 0 100
100″><circle id=”ripple-shape” cx=”1″ cy=”1″
r=”1″/></symbol></svg></div>

1
2
<div style="height: 0; width: 0; position: absolute; visibility: hidden;" aria-hidden="true">
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" focusable="false"><symbol id="ripply-scott" viewbox="0 0 100 100"><circle id="ripple-shape" cx="1" cy="1" r="1"/></symbol></svg></div>

对于最后的触摸,我们将用包含内联CSS的div来包装它,以简洁地隐藏sprite。这样可以防止在渲染时占用页面中的空间。

在撰写本文时,SVG精灵包含symbol块引用它自己的渐变定义——正如你在演示中将看到的——通过ID找不到渐变和正确地渲染;使用visibility
属性代替display的原因:none在Firefox和其他大多数浏览器上作为整个渐变都会失败。

所有IE直到IE11都需要使用focusable=”false”
;除了Edge,因为它还没有测试过。这是来自SVG
1.2规范的一个提案,描述了键盘焦点控制应该如何工作。IE实现了这一点,其他的浏览器则不行。为了与HTML一致,并且为了更好的控制,SVG
2将转而采用tabindex。

5. 延伸阅读

  • 深入浅出 Node.js 全栈架构 – Node.js
    事件循环机制详解与实践

    1 赞 3 收藏
    评论

图片 10

创造一个链

现在我们可以在Blockchain类中将区块链接起来了!下面是用JavaScript实现的代码:

class Blockchain{ constructor() { this.chain =
[this.createGenesisBlock()]; } createGenesisBlock() { return new
Block(0, “01/01/2017”, “Genesis block”, “0”); } getLatestBlock() {
return this.chain[this.chain.length – 1]; } addBlock(newBlock) {
newBlock.previousHash = this.getLatestBlock().hash; newBlock.hash =
newBlock.calculateHash(); this.chain.push(newBlock); } isChainValid() {
for (let i = 1; i < this.chain.length; i++){ const currentBlock =
this.chain[i]; const previousBlock = this.chain[i – 1]; if
(currentBlock.hash !== currentBlock.calculateHash()) { return false; }
if (currentBlock.previousHash !== previousBlock.hash) { return false; }
} return true; } }

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
class Blockchain{
  constructor() {
    this.chain = [this.createGenesisBlock()];
  }
 
  createGenesisBlock() {
    return new Block(0, "01/01/2017", "Genesis block", "0");
  }
 
  getLatestBlock() {
    return this.chain[this.chain.length – 1];
  }
 
  addBlock(newBlock) {
    newBlock.previousHash = this.getLatestBlock().hash;
    newBlock.hash = newBlock.calculateHash();
    this.chain.push(newBlock);
  }
 
  isChainValid() {
    for (let i = 1; i < this.chain.length; i++){
      const currentBlock = this.chain[i];
      const previousBlock = this.chain[i – 1];
 
      if (currentBlock.hash !== currentBlock.calculateHash()) {
        return false;
      }
 
      if (currentBlock.previousHash !== previousBlock.hash) {
        return false;
      }
    }
    return true;
  }
}

在构造函数里,我通过创建一个包含创世块的数组来初始化整个链。第一个区块是特殊的,因为它不能指向前一个区块。我还添加了下面两个方法:

  • getLatestBlock()返回我们区块链上最新的区块。
  • addBlock()负责将新的区块添加到我们的链上。为此,我们将前一个区块的hash添加到我们新的区块中。这样我们就可以保持整个链的完整性。因为只要我们变更了最新区块的内容,我们就需要重新计算它的hash。当计算完成后,我将把这个区块推进链里(一个数组)。

最后,我创建一个isChainValid()来确保没有人篡改过区块链。它会遍历所有的区块来检查每个区块的hash是否正确。它会通过比较previousHash来检查每个区块是否指向正确的上一个区块。如果一切都没有问题它会返回true否则会返回false

编写标记

让我们写一个语义的button元素作为我们的对象,以显示此波纹。

JavaScript

<button>Click for Ripple</button>

1
<button>Click for Ripple</button>

大多数我们熟悉的button的标记结构是直截了当的,包括一些填充文本。

JavaScript

<button> Click for Ripple <svg> <use
xlink:href=”#ripply-scott”></use> </svg> </button>

1
2
3
4
5
6
<button>
  Click for Ripple
  <svg>
    <use xlink:href="#ripply-scott"></use>
  </svg>
</button>

为了利用先前创建的symbol元素,我们需要方法来引用它,通过使用按钮的SVG中的use元素来引用符号的ID属性值。

JavaScript

<button id=”js-ripple-btn” class=”button styl-material”> Click for
Ripple <svg class=”ripple-obj” id=”js-ripple”> <use width=”100″
height=”100″ xlink:href=”#ripply-scott”
class=”js-ripple”></use> </svg> </button>

1
2
3
4
5
6
<button id="js-ripple-btn" class="button styl-material">
  Click for Ripple
  <svg class="ripple-obj" id="js-ripple">
    <use width="100" height="100" xlink:href="#ripply-scott" class="js-ripple"></use>
  </svg>
</button>

最终标记具备了CSS和JavaScript
hooks的附加属性。以“js-”开头的属性值表示仅存在于JavaScript中的值,因此删除它们将阻碍交互,但不会影响样式。这有助于区分CSS选择器和JavaScript
hooks,以避免在将来需要删除或更新时相互混淆。

use元素必须有定义的宽度和高度,否则将不会对查看者可见。你也可以在CSS中定义,如果你直接在元素本身上决定不要的话。

使用区块链

我们的区块链类已经写完啦,可以真正的开始使用它了!

let savjeeCoin = new Blockchain(); savjeeCoin.addBlock(new Block(1,
“20/07/2017”, { amount: 4 })); savjeeCoin.addBlock(new Block(2,
“20/07/2017”, { amount: 8 }));

1
2
3
let savjeeCoin = new Blockchain();
savjeeCoin.addBlock(new Block(1, "20/07/2017", { amount: 4 }));
savjeeCoin.addBlock(new Block(2, "20/07/2017", { amount: 8 }));

在这里我仅仅是创建了一个区块链的实例,并且命名它为SavjeeCoin!之后我在链上添加了一些区块。区块里可以包含任何你想要放的数据,不过在上面的代码里,我选择添加了一个带有amount属性的对象。

联结点样式

当编写CSS的时候,要达到预期的效果你所要做的并不多。

.ripple-obj { height: 100%; pointer-events: none; position: absolute;
top: 0; left: 0; width: 100%; z-index: 0; fill: #0c7cd5; } .ripple-obj
use { opacity: 0; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.ripple-obj {
  height: 100%;
  pointer-events: none;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 0;
  fill: #0c7cd5;
}
 
.ripple-obj use {
  opacity: 0;
}

这就是在删除用于一般样式的声明时,还留下的内容。pointer-events的使用消除了SVG纹波成为鼠标事件的目标,因为我们只需要父对象反应:button元素。

纹波最初必须是不可见的,因此要将不透明度值设置为零。我们还将波纹对象定位在button的左上方。我们可以使波纹形状居中,但是由于此事件是基于用户交互而发生的,所以担心位置没有意义。

试着操作吧!

在介绍里我曾说过区块链是不可变的。一旦添加,区块就不可能再变更了。让我们试一下!

// 检查是否有效(将会返回true) console.log(‘Blockchain valid? ‘ +
savjeeCoin.isChainValid()); // 现在尝试操作变更数据
savjeeCoin.chain[1].data = { amount: 100 }; // 再次检查是否有效
(将会返回false) console.log(“Blockchain valid? ” +
savjeeCoin.isChainValid());

1
2
3
4
5
6
7
8
// 检查是否有效(将会返回true)
console.log(‘Blockchain valid? ‘ + savjeeCoin.isChainValid());
 
// 现在尝试操作变更数据
savjeeCoin.chain[1].data = { amount: 100 };
 
// 再次检查是否有效 (将会返回false)
console.log("Blockchain valid? " + savjeeCoin.isChainValid());

我会在一开始通过运行isChainValid()来验证整个链的完整性。我们操作过任何区块,所以它会返回true。

之后我将链上的第一个(索引为1)区块的数据进行了变更。之后我再次检查整个链的完整性,发现它返回了false。我们的整个链不再有效了。

赋予它生机

赋予生机正是这个互动所有的意义。

<script
src=”;
<script src=”js/ripple.js”/>

1
2
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.17.0/TweenMax.min.js"/>
<script src="js/ripple.js"/>

为了动画化波纹,我们将使用GreenSock的TweenMax库,因为它是使用JavaScript对对象进行动画处理的最佳库之一;特别是涉及与动画SVG跨浏览器有关的问题。

var ripplyScott = (function() {} return { init: function() {} }; })();

1
2
3
4
5
var ripplyScott = (function() {}
  return {
    init: function() {}
  };
})();

我们将要使用的模式是所谓的模块模式,因为它有助于隐藏和保护全局命名空间。

var ripplyScott = (function() {} var circle =
document.getElementById(‘js-ripple’), ripple =
document.querySelectorAll(‘.js-ripple’); function rippleAnimation(event,
timing) {…} })();

1
2
3
4
5
6
var ripplyScott = (function() {}
  var circle = document.getElementById(‘js-ripple’),
      ripple = document.querySelectorAll(‘.js-ripple’);
 
  function rippleAnimation(event, timing) {…}
})();

为了解决问题,我们将抓取一些元素并将它们存储在变量中;特别是use元素,它包含button内的svg。整个动画逻辑将驻留在rippleAnimation函数中。该函数将接受动画序列和事件信息的时序参数。

var ripplyScott = (function() {} var circle =
document.getElementById(‘js-ripple’), ripple =
document.querySelectorAll(‘.js-ripple’); function rippleAnimation(event,
timing) { var tl = new TimelineMax(); x = event.offsetX, y =
event.offsetY, w = event.target.offsetWidth, h =
event.target.offsetHeight, offsetX = Math.abs( (w / 2) – x ), offsetY =
Math.abs( (h / 2) – y ), deltaX = (w / 2) + offsetX, deltaY = (h / 2) +
offsetY, scale_ratio = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY,
2)); } })();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var ripplyScott = (function() {}
  var circle = document.getElementById(‘js-ripple’),
      ripple = document.querySelectorAll(‘.js-ripple’);
 
  function rippleAnimation(event, timing) {
    var tl           = new TimelineMax();
        x            = event.offsetX,
        y            = event.offsetY,
        w            = event.target.offsetWidth,
        h            = event.target.offsetHeight,
        offsetX      = Math.abs( (w / 2) – x ),
        offsetY      = Math.abs( (h / 2) – y ),
        deltaX       = (w / 2) + offsetX,
        deltaY       = (h / 2) + offsetY,
        scale_ratio  = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
  }
})();

我们定义了大量的变量,所以让我们一个一个地讨论这些变量所负责的内容。

var tl = new TimelineMax();

1
var tl = new TimelineMax();

此变量创建动画序列的时间轴实例以及所有时间轴在TweenMax中实例化的方式。

var x = event.offsetX; var y = event.offsetY;

1
2
var x = event.offsetX;
var y = event.offsetY;

事件偏移量是一个只读属性,它将鼠标指针的偏移值报告给目标节点的填充边。在这个例子中,就是我们的button。x的事件偏移量从左到右计算,y的事件偏移量从上到下计算;都从零开始。

var w = event.target.offsetWidth; var h = event.target.offsetHeight;

1
2
var w = event.target.offsetWidth;
var h = event.target.offsetHeight;

这些变量将返回按钮的宽度和高度。最终计算结果将包括元素边框和填充的大小。我们需要这个值才能知道我们的元素有多大,这样我们才可以将波纹传播到最远的边缘。

var offsetX = Math.abs( (w / 2) – x ); var offsetY = Math.abs( (h / 2) –
y );

1
2
var offsetX = Math.abs( (w / 2) – x );
var offsetY = Math.abs( (h / 2) – y );

偏移值是点击距离元素中心的偏移距离。为了填满目标的整个区域,波纹必须足够大,可以从接触点覆盖到最远的角落。使用初始x和y坐标将不会再次将其从零开始,对于x,是从左到右的值,对于y,是从上到下的值。这种方法让我们使用这些值的时候无论目标的中心点点击在哪一边,都会检测距离。

图片 11

注意圆将如何覆盖整个元素的过程,无论输入的起始点何处发生。根据起始点的交互来覆盖整个表面,我们需要做一些数学。

以下是我们如何使用464 x
82作为宽和高,391和45作为x和y坐标来计算偏移量的过程:

var offsetX = (464 / 2) – 391 = -159 var offsetY = (82 / 2) – 45 = -4

1
2
var offsetX = (464 / 2) – 391 = -159
var offsetY = (82 / 2) – 45 = -4

通过将宽度和高度除以2来找到中心,然后减去由x和y坐标检测到的报告值。

Math.abs()方法返回数字的绝对值。使用上面的算术得到值159和4。

var deltaX = 232 + 159 = 391; var deltaY = 41 + 4 = 45;

1
2
var deltaX  = 232 + 159 = 391;
var deltaY  = 41 + 4 = 45;

三角计算点击的整个距离,而不是距离中心的距离。选择三角的原因是x和y总是从零开始从左到右,所以当相反方向(从右到左)点击的时候,我们需要方法来检测点击。

图片 12

学过基础数学课程的小伙伴应该都知道勾股定理。公式为:高(a)的平方加底(b)的平方,得到斜边(c)的平方。

a2 + b2 = c2

var scale_ratio = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));

1
var scale_ratio = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));

使用这个公式让我们来看一下计算:

var scale_ratio = Math.sqrt(Math.pow(391, 2) + Math.pow(45, 2));

1
var scale_ratio = Math.sqrt(Math.pow(391, 2) + Math.pow(45, 2));

Math.pow()方法返回第一个参数的幂;在这个例子中增加了一倍。391的2次方为152881。后面45的2次方等于2025。将这两个值相加并取结果的平方根将留下393.58099547615353,这就是我们需要的波纹比例。

var ripplyScott = (function() { var circle =
document.getElementById(‘js-ripple’), ripple =
document.querySelectorAll(‘.js-ripple’); function rippleAnimation(event,
timing) { var tl = new TimelineMax(); x = event.offsetX, y =
event.offsetY, w = event.target.offsetWidth, h =
event.target.offsetHeight, offsetX = Math.abs( (w / 2) – x ), offsetY =
Math.abs( (h / 2) – y ), deltaX = (w / 2) + offsetX, deltaY = (h / 2) +
offsetY, scale_ratio = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY,
2)); tl.fromTo(ripple, timing, { x: x, y: y, transformOrigin: ‘50% 50%’,
scale: 0, opacity: 1, ease: Linear.easeIn },{ scale: scale_ratio,
opacity: 0 }); return tl; } })();

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
var ripplyScott = (function() {
  var circle = document.getElementById(‘js-ripple’),
      ripple = document.querySelectorAll(‘.js-ripple’);
 
  function rippleAnimation(event, timing) {
    var tl           = new TimelineMax();
        x            = event.offsetX,
        y            = event.offsetY,
        w            = event.target.offsetWidth,
        h            = event.target.offsetHeight,
        offsetX      = Math.abs( (w / 2) – x ),
        offsetY      = Math.abs( (h / 2) – y ),
        deltaX       = (w / 2) + offsetX,
        deltaY       = (h / 2) + offsetY,
        scale_ratio  = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
 
    tl.fromTo(ripple, timing, {
      x: x,
      y: y,
      transformOrigin: ‘50% 50%’,
      scale: 0,
      opacity: 1,
      ease: Linear.easeIn
    },{
      scale: scale_ratio,
      opacity: 0
    });
 
    return tl;
  }
})();

使用TweenMax中的fromTo方法,我们可以传递目标——波纹形状——并设置包含整个运动序列方向的对象文字。鉴于我们想要从中心向外形成动画,SVG需要将转换原点设置为中间位置。考虑到我们想要之后要进行动画处理,需要设置opacity
为1,因此缩放也需要调整到最小的位置。不知道你回想起了没有,之前我们在CSS中设置了opacity为0的use元素以及我们从值1开始并返回到零的原因。最后部分是返回时间轴实例。

var ripplyScott = (function() { var circle =
document.getElementById(‘js-ripple’), ripple =
document.querySelectorAll(‘.js-ripple’); function rippleAnimation(event,
timing) { var tl = new TimelineMax(); x = event.offsetX, y =
event.offsetY, w = event.target.offsetWidth, h =
event.target.offsetHeight, offsetX = Math.abs( (w / 2) – x ), offsetY =
Math.abs( (h / 2) – y ), deltaX = (w / 2) + offsetX, deltaY = (h / 2) +
offsetY, scale_ratio = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY,
2)); tl.fromTo(ripple, timing, { x: x, y: y, transformOrigin: ‘50% 50%’,
scale: 0, opacity: 1, ease: Linear.easeIn },{ scale: scale_ratio,
opacity: 0 }); return tl; } return { init: function(target, timing) {
var button = document.getElementById(target);
button.addEventListener(‘click’, function(event) {
rippleAnimation.call(this, event, timing); }); } }; })();

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
var ripplyScott = (function() {
  var circle = document.getElementById(‘js-ripple’),
      ripple = document.querySelectorAll(‘.js-ripple’);
 
  function rippleAnimation(event, timing) {
    var tl           = new TimelineMax();
        x            = event.offsetX,
        y            = event.offsetY,
        w            = event.target.offsetWidth,
        h            = event.target.offsetHeight,
        offsetX      = Math.abs( (w / 2) – x ),
        offsetY      = Math.abs( (h / 2) – y ),
        deltaX       = (w / 2) + offsetX,
        deltaY       = (h / 2) + offsetY,
        scale_ratio  = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
 
    tl.fromTo(ripple, timing, {
      x: x,
      y: y,
      transformOrigin: ‘50% 50%’,
      scale: 0,
      opacity: 1,
      ease: Linear.easeIn
    },{
      scale: scale_ratio,
      opacity: 0
    });
 
    return tl;
  }
 
  return {
    init: function(target, timing) {
      var button = document.getElementById(target);
 
      button.addEventListener(‘click’, function(event) {
        rippleAnimation.call(this, event, timing);
      });
    }
  };
})();

返回的对象字面值将控制我们的波纹,方法是通过将事件侦听器附加到所需的目标,调用rippleAnimation,以及最后传递我们将在下一步讨论的参数。

ripplyScott.init(‘js-ripple-btn’, 0.75);

1
ripplyScott.init(‘js-ripple-btn’, 0.75);

最后通过使用模块并传递init函数来对按钮进行调用,init函数传递按钮和序列的时序。看,就是这样!

希望你喜欢这篇文章,并从中受到启迪!欢迎使用不同的形状来检查演示,并查看源代码。不妨尝试新的形状、新的图层形状,最重要的是发挥你的想象力,放飞你的创意。

注意:其中一些技术是试验性的,只能在现代浏览器中运行。

浏览器支持:Chrome Firefox Internet Explorer Safari Opera

在Github上查看这个项目

译文链接:
英文原文:Creating Material Design Ripple Effects with
SVG
翻译作者:码农网 – 小峰
[ 转载必须在正文中标注并保留原文链接、译文链接和译者等信息。]

1 赞 1 收藏
评论

图片 10

结论

这个小栗子还远未达到完成的程度。它还没有实现POW(工作量证明机制)或P2P网络来与其它矿工来进行交流。

但他确实证明了区块链的工作原理。许多人认为原理会非常复杂,但这篇文章证明了区块链的基本概念是非常容易理解和实现的。

发表评论

电子邮件地址不会被公开。 必填项已用*标注