Control Flow in JavaScript
这是一篇我在飞飞商城针对 JavaScript Control Flow 的演讲稿文字版。
Control Flow 是什么? #
Control Flow 就是控制流,简单地归纳,就是代码执行的顺序(和控制的手段)。
常见的控制流有:
- if-else
- switch-case
- for / while loop
- goto
问题儿:JavaScript #
由于 JavaScript 的运行环境,决定了她拥有异步执行的能力。
if (checkEmail() === false ||
checkUsername() === false) {
// validation failed
}
如果是使用 ajax 来做验证的话,就不能如此实现。
function checkEmail () {
$.get({
url: 'api/check_email.json'
})
// where is the return?!
}
function checkUsername () {
$.get({
url: 'api/check_username.json'
})
// where is the return?!
}
如果使用 async: false
,那就会造成浏览器的阻塞。
怎么办呢?幸好,解决方案是有的。
Callback #
亦即回调函数。
function checkEmail (data, callback) {
$.get({
url: 'api/check_email.json',
data: data,
success: function (data) {
callback.success.call(null, data)
},
error: function (err) {
callback.error.call(null, err)
},
complete: function () {
callback.complete.call(null)
}
})
}
var validating = 2
, errorCount = 0
, done
done = function () {
if (validating) {
return;
}
if (errorCount) {
// validation failed
}
}
checkEmail({
success: function () {
validating--
},
error: function () {
errorCount++
},
complete: done
})
这不是一个好的实现方式,只是为大家演示一下,callback 的使用方法。
Callback 的优点有:
- 简单易用
- 无需额外的库支持
缺点有:
- 越复杂的逻辑越容易产生很深的嵌套
Event #
事件大家都知道:
$('button').click(function(){})
上例就可以改造成这样:
<!-- 假设 HTML 架构如下 -->
<form>
<input id="email">
<input id="username">
</form>
var $form = $('form')
, $email = $('#email')
function checkEmail () {
$email.attr('data-required', true)
$.get({
success: function(){
$email.attr('data-valid', true)
},
error: function(){
$email.attr('data-valid', false)
},
complete: function(){
$email.removeAttr('data-required')
$form.trigger('validationDone')
}
})
}
$form.on('validationDone', function(){
if ($form.find('[data-required]').length) {
return;
}
if ($form.find('[data-valid=false]').length) {
// validation failed
}
})
不是一个好例子,是吧?不过这是为了和之后的方案进行对比,实际事件的使用场景可以这样:
<div id="user-profile">
<!-- ... -->
<form>
<input type="submit" value="Save">
</form>
</div>
var $userProfile = $('#user-profile')
, $form = $userProfile.find('form')
$userProfile
.on('userProfileDidUpdate',
function (e, profile) {}
)
.on('userProfileUpdateFailed',
function (e) {}
)
.on('userProfileDidFinishUpdate',
function (e) {}
)
$form.on('submit', function(){
$.ajax({
success: function (data) {
$form.trigger('userProfileDidUpdate', data)
},
error: function () {
$form.trigger('userProfileUpdateFailed')
},
complete: function () {
$form.trigger('userProfileDidFinishUpdate')
}
})
})
form
通过事件,告诉大家「我完成该做的事了,至于进一步该干什么,我不管」。
userProfile
通过接受事件,执行不同的操作,至于是「谁」发起的,并不需要关心。
这样就降低了耦合度,form
和 userProfile
(功能上)并没有依赖关系,都在完成属于自己的职责。
但是也是有缺陷的:
- 易(人为)出错,难调试。
userProfileDidFinishUpdated
和userProfileDidFinishUpdate
就是两个不同事件,如果用到全局变量做事件名,则增加了依赖 - DOM 上使用容易被
stopPropagtion
影响
针对第二个问题,通过引入第三方库可解决,如 EventEmitter2:
var em = new EventEmitter2({
wildcard: true
})
em.on('form.*', function () {
console.log(arguments)
})
em.on('form.submit-disable', function (disabled) {
$form.find('[type=submit]').prop('disabled', disabled)
})
em.emit('form.submit-disable', true)
$(form).on('submit', function () {
if (valid) {
em.emit('form.valid', true)
} else {
em.emit('form.invalid', true)
}
})
EventEmitter2 其实是为 Node.js 做准备的,但用在浏览器端也毫无问题,而 Node.js 也有自己的 events.EventEmitter,用哪一个,萝卜青菜咯。
Messaging #
消息其实是和事件很相似的东西,只是概念上有差异,以 [Postal.js] 为例:
var channel1 = postal.channel()
, channel2 = postal.channel()
channel1.subscribe(
'demo',
function (data) {
console.log('channel 1 says:', data)
}
)
channel2.subscribe(
'demo',
function (data) {
console.log('channel 2 says:', data)
}
)
channel1.publish('demo', 'hello world')
// console logs:
// channel 1 says: hello world
// channel 2 says: hello world
事件和消息的差异在于:
- 事件只能作用于事件的直接对象(或父节点)
- 消息可以作用于任何对象——只要该对象订阅了该消息
Node.js 为例:
var EventEmitter = require('events').EventEmitter
, ee1 = new EventEmitter()
, ee2 = new EventEmitter()
ee1.on('demo', function (data) {
console.log('Hello', data)
})
ee2.on('demo', function (data) {
console.log('Hi', data)
})
ee2.emit('demo', 'Chris')
// console logs:
// hi Chris
因此 Objective-C 也使用了 [Notification],以使 app 可以响应系统事件。
Promise #
Promise 是什么就不必多说了,在 2013 年 12 月也确定了被加入到 JavaScript 内置对象中(link),也就是尘埃落定,在 Node.js 和新浏览器里可以肆无忌惮地使用。
因为用 Promise 来改造文章开头的示例,实际和 callback 也差不多,所以就用别的例子来说说 Promise:
// a.js
function getUsername () {
var promise = new Promise(function (resolve, reject) {
if (done) {
resolve(data)
} else {
reject(err)
}
})
return promise
}
window.userNamePromise = getUsername()
// b.js
userNamePromise.then(
function (data) {
// do sth. with data
return something
},
function (err) {
// handle error
throw new Error()
}
)
// c.js
userNamePromise
.then(
function (something) {}
).catch(function (err) {})
如果看不懂上面的例子,建议好好看一遍上面的 link,学习学习。
简要地说,Promise 解决了事件只能作用于特定对象和同一时间内只能触发一次的限制。
通过对 then
等接口的调用,可以在任何时候根据 resolve
和 reject
的结果执行不同的操作,同时通过暴露 promise(本例中的 window.userNamePromise
)到外部环境中,就可以在不同地方去使用这个 promise,根据其结果做不同的事。而由于 promise 会保证 then
等接口调用时 resolve
或 reject
的结果是被正确取回(甚至缓存),因此不必担心执行时机不对的问题。
BTW,jQuery 也是带有 promise 机制的,比如 $.ajax
返回的就是一个 promise 对象。
Finite State Machines #
无限状态机是比较好玩的东西,但遗憾的是用在本文最初的例子的话,代码量很多,也会复杂,因此使用别的例子:
var user = new machina.Fsm({
initialState: 'notLogin',
login: function (username, password) {
if (username && password) {
this.transition('login')
} else {
this.transition('notLogin')
}
},
isLoggedIn: function () {
return this.state === 'login'
},
logout: function () {
this.transition('notLogin')
},
states: {
'login': {
_onEnter: function () {
this.handle('user.login')
},
_onExit: function () {
this.handle('user.logout')
},
'user.login': function (payload) {},
'user.exit': function (payload) {}
}
}
})
user.isLoggedIn() // false
user.login('feifei', '123456')
user.isLoggedIn() // true
user.logout()
本例使用了 machina.js。从代码中可以看到,其实状态机是通过它的接口,本例是 login
和 logout
来接受外部信息并依此改变状态或直接改变状态,通过状态的改变,将执行特定状态下的行为。
一个无限状态机的特点是:
- 可存在的状态有限
- 某一时刻,只能处于一个状态中
- 可以从一个状态转变成另一个状态
- 可以接受输入或者产生输出
对前端来说,像是 a:hover
、input:focus
其实就是一种状态。
Control Flow Libs #
这一节只是介绍一下目前很流行的 Async.js 和 Step。
在 Node.js 开始大肆抢占众人眼球的时候,人们也发现了一个问题:异步是爽啊,可「怎么让不同的异步操作完成后最后调用一个接口去处理所有结果呢?」,又或者,「好多嵌套,能不能少一些啊」。
Async.js 和 Step 就是为了解决这类问题而生的。以 Async.js 为例,该怎么去解决本文最初的例子:
function checkEmail (next) {
$.get().then(function () {
next(null, data)
}, function () {
next(err)
})
}
async.parallel([
checkEmail,
checkUsername
], function (err, results) {
if (err) {
// validation failed
return false;
}
// post data
})
通过 async.parallel
,checkEmail
和 checkUsername
将会以异步形式执行,而通过对 next
的调用,将会把结果汇总到 async.parallel
的第二个参数,在这里就可以进行最后的处理。
通过这种方法,不仅不需要去设置 setInterval
,也不需要去检查 errorCount
之类的多余变量,一切交给 async 就可以了。
Async 除了 parallel
之外,还有 series
、waterfall
等接口,也有 each
等 Array.forEach
的特殊封装。前三个接口是我最常用的,而后面如 each
之类的,其实只是普通的数组操作的话,我建议直接 loop 或者 Lo-Dash,因为 async 对 each
的处理是只要 Array.forEach
存在就用,不存在就用 loop,在浏览器上使用会存在性能不足的隐患(在 Node.js 也可能会,毕竟是 V8)。
而 Step 则是实现了 async.parallel
等三个接口的超小巧库,搭配 Lo-Dash 使用还是非常不错的。
Generator #
Generator 是 ECMAScript 6 新增加的特性,具体内容在 Node.js 101: generator 里讲了,不冗述。
[Postal.js]: https://github.com/postaljs/postal.js
[Notification]: https://developer.apple.com/library/mac/documentation/general/conceptual/devpedia-cocoacore/Notification.html