Control Flow in JavaScript

这是一篇我在飞飞商城针对 JavaScript Control Flow 的演讲稿文字版。


Control Flow 是什么? #

Control Flow 就是控制流,简单地归纳,就是代码执行的顺序(和控制的手段)。

常见的控制流有:

问题儿: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 通过接受事件,执行不同的操作,至于是「谁」发起的,并不需要关心。

这样就降低了耦合度,formuserProfile (功能上)并没有依赖关系,都在完成属于自己的职责。

但是也是有缺陷的:

针对第二个问题,通过引入第三方库可解决,如 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 等接口的调用,可以在任何时候根据 resolvereject 的结果执行不同的操作,同时通过暴露 promise(本例中的 window.userNamePromise)到外部环境中,就可以在不同地方去使用这个 promise,根据其结果做不同的事。而由于 promise 会保证 then 等接口调用时 resolvereject 的结果是被正确取回(甚至缓存),因此不必担心执行时机不对的问题。

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。从代码中可以看到,其实状态机是通过它的接口,本例是 loginlogout 来接受外部信息并依此改变状态或直接改变状态,通过状态的改变,将执行特定状态下的行为。

一个无限状态机的特点是:

对前端来说,像是 a:hoverinput:focus 其实就是一种状态。

Control Flow Libs #

这一节只是介绍一下目前很流行的 Async.jsStep

在 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.parallelcheckEmailcheckUsername 将会以异步形式执行,而通过对 next 的调用,将会把结果汇总到 async.parallel 的第二个参数,在这里就可以进行最后的处理。

通过这种方法,不仅不需要去设置 setInterval,也不需要去检查 errorCount 之类的多余变量,一切交给 async 就可以了。

Async 除了 parallel 之外,还有 serieswaterfall 等接口,也有 eachArray.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

 
11
Kudos
 
11
Kudos

Now read this

Node.js 101: Promise and async

先回顾一下 Sagase 的项目结构: lib/ cli.js sagase.js Gruntfile.js package.json 上一篇讲了 package.json,这一篇讲 lib/sagase.js。 因为代码比较长,就分开一节节地讲,完整的点开 GitHub 看吧。 'use strict'; 通知编译器进入 strict mode,主要的作用是让编译器帮你检查一些坑,有经验的 JavaScript 开发者可以忽略。 var fs =... Continue →