当看到标题的时候,你心里一定会有疑问:行为一致性?什么行为?

行为在这里指的是函数的行为:同步执行还是异步执行。

行为一致性指的是确保我们写的函数始终同步执行或者异步执行,而不是有时候同步执行,有时候异步执行。

因为异步函数都是建立在其他异步的基础之上的,而异步函数的结果也是以异步的形式提供的,所以行为不一致的函数会引发混乱,引入bug,而且给使用的人带来困惑。

行为不一致的函数会引入bug

下面来看一个因为函数行为不一致而引发的bug的示例。

See the Pen sometime async function causes bug by xingzhi (@xingzhi) on CodePen.

示例中的ajaxGet函数对获取的数据进行了缓存。当每次调用时,如果数据已经缓存,直接使用缓存的数据,否则使用Ajax异步获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const ajaxGet = (function() {
  const cache = {};

  return function(url, cb) {
    if ( cache[url] ) {
      cb(cache[url]);
    } else {
      const xhr = new XMLHttpRequest();

      xhr.open('GET', url);
      xhr.send();

      xhr.onload = function() {
        cache[url] = this.responseText;
        cb(this.responseText);
      };
    }
  };
})();

在使用ajaxGet时是假定它始终是异步的,在调用它之后,立即更新了UI,提示用户正在获取数据;当数据成功获取后,再次更新UI,显示获取的数据。

当没有缓存时,先提示用户正在获取数据,然后显示获取的数据。而当有缓存时,会先显示获取的数据,然后提示用户正在获取数据,但是数据已经被这个提示替换掉了。

行为一致的函数

行为不一致是因为函数有多个分支,其中一些分支是同步的,比如上面的示例中,根据数据是否已经缓存分成了两个分支:当有缓存时直接使用数据(同步),否则异步获取(异步)。

而要维持函数的行为一致只需要以异步的形式提供同步分支的结果。

下面是改写后的函数,改动只有一处:当有缓存数据时,使用setTimeout把之前的同步执行的函数包裹起来,使它异步执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const ajaxGet = (function() {
  const cache = {};

  return function(url, cb) {
    if ( cache[url] ) {
      setTimeout(function() {
        cb(cache[url]);
      }, 0);
    } else {
      const xhr = new XMLHttpRequest();

      xhr.open('GET', url);
      xhr.send();

      xhr.onload = function() {
        cache[url] = this.responseText;
        cb(this.responseText);
      };
    }
  };
})();

下面是改写后的示例。

See the Pen always async function by xingzhi (@xingzhi) on CodePen.

在浏览器端使用setTimeout把同步行为转化为异步行为,而在Node中使用process.nextTick