我该如何在Javascript中创建一个异步函数?

154

看看这个代码

<a href="#" id="link">Link</a>
<span>Moving</span>

$('#link').click(function () {
    console.log("Enter");
    $('#link').animate({ width: 200 }, 2000, function() {
         console.log("finished");            
    });    
    console.log("Exit");    
});

正如您在控制台中看到的那样,“animate”函数是异步的,它会“fork”事件处理程序块代码的流程。实际上:

$('#link').click(function () {
    console.log("Enter");
    asyncFunct();
    console.log("Exit");    
});

function asyncFunct() {
    console.log("finished");
}

跟随代码块的流程!

如果我想要创建一个行为类似于 function asyncFunct() { } 的函数,该如何用javascript/jquery实现?我认为可以不使用setTimeout()来实现。


看一下jQuery源代码 :) - yatskevich
.animate() 方法使用回调函数。当动画完成时,Animate 将调用回调函数。如果您需要与 .animate() 相同的行为,则需要一个回调函数(由“主”函数在执行其他操作后调用)。如果您需要一个“完整”的异步函数(一个在不阻塞执行流的情况下调用的函数),则情况就不同了。在这种情况下,您可以使用带有接近 0 的延迟的 setTimeout()。 - Fabio Buda
@Fabio Buda:为什么callback()应该实现一种异步方式?事实上,它并没有。http://jsfiddle.net/5H9XT/9/ - markzzz
实际上,在“回调”之后,我引用了一个带有setTimeout的“完整”异步方法。我所说的回调是伪异步的,因为函数在其他代码之后被调用 :-) - Fabio Buda
12个回答

191
您无法创建真正的自定义异步函数。最终,您必须利用本地提供的技术,例如:
  • setInterval
  • setTimeout
  • requestAnimationFrame
  • XMLHttpRequest
  • WebSocket
  • Worker
  • 某些HTML5 API,例如File API、Web Database API
  • 支持onload的技术
  • ...等等
事实上,对于动画jQuery usessetInterval

3
昨天我和一个朋友讨论过这个问题,所以你的回答很完美!我理解并可以正确地识别异步函数并在JS中使用它们。但是为什么我们不能实现自定义异步函数这个问题对我来说不太清楚。就像一个黑匣子,我们知道如何让它工作(比如使用setInterval),但我们甚至无法打开它看看里面是如何做到的。你是否有更多相关信息? - Matheus Felipe
2
@MatheusFelipe 这些函数是 JavaScript 引擎实现的本地函数,你能够依赖的唯一事物就是规范,例如 HTML5 定时器,并且相信它们的黑匣子本质会按照规范进行操作。 - Spoike
10
迄今为止,@MatheusFelipe在这个话题上做的最好的演讲...。 - user57508
一些实现,特别是Node.js,支持setImmediate - Jon Surrell
3
关于 promises,它是否提供了一个可等待的对象? - Beingnin

74
您可以使用计时器:
setTimeout( yourFn, 0 );

(其中yourFn是指向您的函数的引用)

或者,使用Lodash

_.defer( yourFn );

在当前调用栈清空后再执行func函数。当func被调用时,可以提供任何额外的参数。


3
这个不行,我的JavaScript函数在画布上绘图时会导致用户界面无法响应。 - gab06
4
@gab06 - 我认为你的画布绘制函数是因为自身良好的原因而被阻塞。将其操作分成许多较小的操作,并使用计时器调用每个操作:你会发现以这种方式界面对您的鼠标点击等具有响应性。 - Marco Faustinelli
1
根据HTML5规范,setTimeout的最短时间为4毫秒。将其设置为0仍将花费该最短时间。但是,它作为函数延迟器运行得很好。 - hegez
对于 scope.setTimeout 函数,如果省略了 delay 参数,则默认使用值为 0 - AlexMelw

31

这里有一个简单的解决方案(其他人也已经写过了)http://www.benlesh.com/2012/05/calling-javascript-function.html

还有一个可用的解决方案:

function async(your_function, callback) {
    setTimeout(function() {
        your_function();
        if (callback) {callback();}
    }, 0);
}

测试1(可能输出“1 x 2 3”、“1 2 x 3”或“1 2 3 x”):

console.log(1);
async(function() {console.log('x')}, null);
console.log(2);
console.log(3);

测试 2(将始终输出“x 1”):

async(function() {console.log('x');}, function() {console.log(1);});

这个函数执行时设置了0的延迟时间 - 它模拟异步任务


7
TEST 1只能输出"1 2 3 x",而TEST 2每次都保证输出"1 x"。TEST 2出现意外结果的原因是因为调用了console.log(1),输出(undefined)被传递作为第二个参数给了async()函数。对于TEST 1,我认为你可能不完全理解JavaScript的执行队列。由于每个调用console.log()发生在同一个堆栈中,所以"x"最后被记录是有保障的。虽然我认为此回答存在误导性,但由于我的信誉度不够高,不能进行投票评价。 - Joshua Piccari
1
@Joshua:看起来 @fider 的意思是将测试2写成: async(function() {console.log('x')}, function(){console.log(1)}); - nzn
是的,@nzn 和 @Joshua 我所指的TEST 2是:async(function() {console.log('x')}, function(){console.log(1)}); - 我已经纠正了。 - fider
在异步函数中执行TEST 2,输出为1 x。代码如下:async(function() {setTimeout(()=>{console.log('x');},1000)}, function() {console.log(1);}); - Mohsen

10

这里有一个函数,它接受另一个函数并输出一个异步运行的版本。

var async = function (func) {
  return function () {
    var args = arguments;
    setTimeout(function () {
      func.apply(this, args);
    }, 0);
  };
};

它被用作一种简单的方法来创建异步函数:

var anyncFunction = async(function (callback) {
    doSomething();
    callback();
});

这与 @fider 的答案不同,因为该函数本身具有自己的结构(没有添加回调函数,它已经在函数中),并且还因为它创建了一个可以使用的新函数。


setTimeout不能在循环中使用(调用相同的函数多次,但参数不同)。 - user2284570
@user2284570 这就是闭包的作用。(function(a){ asyncFunction(a); })(a) - Swivel
1
据我所知,您也可以在没有闭包的情况下实现这一点:setTimeout(asyncFunction, 0, a); - Swivel
1
如果我们所说的异步是指在后台与主线程并行运行,那么这并不是真正的异步。所有这样做的只是将执行延迟到process.nextTick。无论您在函数中编写什么代码,都将在主线程上执行。如果函数被设置为计算PI,则应用程序将冻结,无论是否超时! - Mike M
1
我不明白为什么这个答案被点赞了。当我把它放进我的代码中时,程序会一直阻塞,直到函数完成,这正是它不应该做的。 - Martin Argerami

6

编辑: 我完全误解了这个问题。在浏览器中,我会使用setTimeout。如果很重要它运行在另一个线程中,我会使用Web Workers


1
这不是一个异步函数 :O - markzzz

6
迟来的是,但为了展示在ES6中引入promises后的简单解决方案,它能更轻松地处理异步调用:

您可以在一个新的promise中设置异步代码:

var asyncFunct = new Promise(function(resolve, reject) {
    $('#link').animate({ width: 200 }, 2000, function() {
        console.log("finished");                
        resolve();
    });             
});

注意在异步调用完成时设置resolve()
然后,在承诺的.then()内添加您想要在异步调用完成后运行的代码:
asyncFunct.then((result) => {
    console.log("Exit");    
});

Here is a snippet of it:

$('#link').click(function () {
    console.log("Enter");
    var asyncFunct = new Promise(function(resolve, reject) {
        $('#link').animate({ width: 200 }, 2000, function() {
            console.log("finished");             
            resolve();
        });    
    });
    asyncFunct.then((result) => {
        console.log("Exit");    
    });
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<a href="#" id="link">Link</a>
<span>Moving</span>

或者 JSFiddle

1
据我理解,在执行器中的代码(new Promise的参数)会立即运行,而不是在下一个时钟周期中运行。因此,我不确定这个答案是否正确。但是,似乎then处理程序总是在稍后的时钟周期中运行。 - Sam Hartman

5

这个页面将带领您了解创建异步JavaScript函数的基础知识。

自ES2017以来,编写异步JavaScript函数变得更加容易。您还应该阅读更多关于Promises的内容。


链接已失效。 - Martin Argerami

3
如果你想使用参数并且控制最大异步函数数量,可以使用我构建的简单异步工作者:
var BackgroundWorker = function(maxTasks) {
    this.maxTasks = maxTasks || 100;
    this.runningTasks = 0;
    this.taskQueue = [];
};

/* runs an async task */
BackgroundWorker.prototype.runTask = function(task, delay, params) {
    var self = this;
    if(self.runningTasks >= self.maxTasks) {
        self.taskQueue.push({ task: task, delay: delay, params: params});
    } else {
        self.runningTasks += 1;
        var runnable = function(params) {
            try {
                task(params);
            } catch(err) {
                console.log(err);
            }
            self.taskCompleted();
        }
        // this approach uses current standards:
        setTimeout(runnable, delay, params);
    }
}

BackgroundWorker.prototype.taskCompleted = function() {
    this.runningTasks -= 1;

    // are any tasks waiting in queue?
    if(this.taskQueue.length > 0) {
        // it seems so! let's run it x)
        var taskInfo = this.taskQueue.splice(0, 1)[0];
        this.runTask(taskInfo.task, taskInfo.delay, taskInfo.params);
    }
}

你可以像这样使用:

var myFunction = function() {
 ...
}
var myFunctionB = function() {
 ...
}
var myParams = { name: "John" };

var bgworker = new BackgroundWorker();
bgworker.runTask(myFunction, 0, myParams);
bgworker.runTask(myFunctionB, 0, null);

2
Function.prototype.applyAsync = function(params, cb){
      var function_context = this;
      setTimeout(function(){
          var val = function_context.apply(undefined, params); 
          if(cb) cb(val);
      }, 0);
}

// usage
var double = function(n){return 2*n;};
var display = function(){console.log(arguments); return undefined;};
double.applyAsync([3], display);

虽然和其他解决方案基本上没有本质区别,但我认为我的解决方案还有一些额外的优点:

  • 它允许函数带参数
  • 它将函数的输出传递给回调函数
  • 它被添加到Function.prototype中,使得调用更加简洁

此外,和内置函数Function.prototype.apply的相似性也很适合我。


1
很遗憾,JavaScript没有提供异步功能,它只能在单个线程中工作。但是大多数现代浏览器提供了Worker,这是第二个在后台执行且可以返回结果的脚本。
因此,我想到了一种解决方案,可以异步运行一个函数,该函数为每个异步调用创建一个worker。
下面的代码包含了要调用的async函数来在后台运行。
Function.prototype.async = function(callback) {
    let blob = new Blob([ "self.addEventListener('message', function(e) { self.postMessage({ result: (" + this + ").apply(null, e.data) }); }, false);" ], { type: "text/javascript" });
    let worker = new Worker(window.URL.createObjectURL(blob));
    worker.addEventListener("message", function(e) {
        this(e.data.result);
    }.bind(callback), false);
    return function() {
        this.postMessage(Array.from(arguments));
    }.bind(worker);
};

This is an example for usage:

(function(x) {
    for (let i = 0; i < 999999999; i++) {}
        return x * 2;
}).async(function(result) {
    alert(result);
})(10);

这会执行一个函数,该函数使用巨大的数字迭代一个 for 以演示异步性,并获取传递数字的两倍。 async 方法提供了一个 function,它在后台调用所需的函数,在 async 的参数中提供回调函数,其中 return 在其唯一参数中返回。 因此,在回调函数中,我使用 alert 弹出结果。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接