Control Flow in JavaScript

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

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

Async 除了 parallel 之外,还有 serieswaterfall 等接口,也有each 等 Array.forEach 的特殊封装。前三个接口是我最常用的,而后面如 each 之类的,其实只是普通的数组操作的话,我建议直接 loop 或者 Lo-Dash,因为 async 对 each 的处理是只要 Array.forEach 存在就用,不存在就用 loop,在浏览器上使用会存在性能不足的隐患(在 Node.js 也可能会,毕竟是 V8)。

而 Step 则是实现了 async.parallel 等三个接口的超小巧库,搭配 Lo-Dash 使用还是非常不错的。

延伸阅读


▶ Walkthrough007

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值