关于异步:如何制作非阻塞的javascript代码?

How to make non-blocking javascript code?

如何进行简单的非块Javascript函数调用? 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
  //begin the program
  console.log('begin');
  nonBlockingIncrement(10000000);
  console.log('do more stuff');

  //define the slow function; this would normally be a server call
  function nonBlockingIncrement(n){
    var i=0;
    while(i<n){
      i++;
    }
    console.log('0 incremented to '+i);
  }

输出

1
2
3
"beginPage"
"0 incremented to 10000000"
"do more stuff"

如何形成这个简单的循环以异步执行并通过回调函数输出结果? 这个想法是不阻止"做更多的东西":

1
2
3
"beginPage"
"do more stuff"
"0 incremented to 10000000"

我已经尝试过关于回调和延续的教程,但它们似乎都依赖于外部库或函数。 他们都没有在真空中回答这个问题:如何编写Javascript代码是非阻塞的??

在询问之前,我非常努力地寻找这个答案; 请不要以为我没看。 我发现的一切都是Node.js特定的([1],[2],[3],[4],[5])或其他特定于其他函数或库([6],[7],[8], [9],[10],[11]),特别是JQuery和setTimeout()。 请帮我用Javascript编写非阻塞代码,而不是Javascript和Node等Javascript编写的工具。 请在将其标记为重复之前重新阅读该问题。


要使循环无阻塞,必须将其分成几个部分,并允许JS事件处理循环在继续执行下一部分之前使用用户事件。

实现这一目标的最简单方法是执行一定量的工作,然后使用setTimeout(..., 0)对下一部分工作进行排队。至关重要的是,排队允许JS事件循环处理在此期间排队的任何事件,然后再进行下一项工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function yieldingLoop(count, chunksize, callback, finished) {
    var i = 0;
    (function chunk() {
        var end = Math.min(i + chunksize, count);
        for ( ; i < end; ++i) {
            callback.call(null, i);
        }
        if (i < count) {
            setTimeout(chunk, 0);
        } else {
            finished.call(null);
        }
    })();
}

用法:

1
2
3
4
5
yieldingLoop(1000000, 1000, function(i) {
    // use i here
}, function() {
    // loop done here
});

请参阅http://jsfiddle.net/alnitak/x3bwjjo6/以获取演示,其中callback函数仅将变量设置为当前迭代计数,并且单独的基于setTimeout的循环轮询该变量的当前值并更新页面及其价值。


带回调的SetTimeout是要走的路。但是,要了解您的功能范围与C#或其他多线程环境中的功能范围不同。

Javascript不会等待你的函数的回调完成。

如果你说:

1
2
3
4
function doThisThing(theseArgs) {
    setTimeout(function (theseArgs) { doThatOtherThing(theseArgs); }, 1000);
    alert('hello world');
}

在您通过的功能之前,您的警报将会触发。

区别在于警报阻止了线程,但是你的回调没有。


据我所知,通常有两种方法可以做到这一点。一种是使用setTimeout(如果你在支持环境中这样做,则使用requestAnimationFrame)。 @Alnitak在另一个答案中展示了如何做到这一点。另一种方法是使用Web worker在单独的线程中完成阻塞逻辑,这样就不会阻止主UI线程。

使用requestAnimationFramesetTimeout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//begin the program
console.log('begin');
nonBlockingIncrement(100, function (currentI, done) {
  if (done) {
    console.log('0 incremented to ' + currentI);
  }
});
console.log('do more stuff');

//define the slow function; this would normally be a server call
function nonBlockingIncrement(n, callback){
  var i = 0;
 
  function loop () {
    if (i < n) {
      i++;
      callback(i, false);
      (window.requestAnimationFrame || window.setTimeout)(loop);
    }
    else {
      callback(i, true);
    }
  }
 
  loop();
}

使用web worker:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/***** Your worker.js *****/
this.addEventListener('message', function (e) {
  var i = 0;

  while (i < e.data.target) {
    i++;
  }

  this.postMessage({
    done: true,
    currentI: i,
    caller: e.data.caller
  });
});



/***** Your main program *****/
//begin the program
console.log('begin');
nonBlockingIncrement(100, function (currentI, done) {
  if (done) {
    console.log('0 incremented to ' + currentI);
  }
});
console.log('do more stuff');

// Create web worker and callback register
var worker = new Worker('./worker.js'),
    callbacks = {};

worker.addEventListener('message', function (e) {
  callbacks[e.data.caller](e.data.currentI, e.data.done);
});

//define the slow function; this would normally be a server call
function nonBlockingIncrement(n, callback){
  const caller = 'nonBlockingIncrement';
 
  callbacks[caller] = callback;
 
  worker.postMessage({
    target: n,
    caller: caller
  });
}

您无法运行Web工作器解决方案,因为它需要单独的worker.js文件来承载工作逻辑。


你不能同时执行两个循环,记住JS是单线程。

所以,这样做永远不会奏效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function loopTest() {
    var test = 0
    for (var i; i<=100000000000, i++) {
        test +=1
    }
    return test
}

setTimeout(()=>{
    //This will block everything, so the second won't start until this loop ends
    console.log(loopTest())
}, 1)

setTimeout(()=>{
    console.log(loopTest())
}, 1)

如果要实现多线程,则必须使用Web Workers,但它们必须具有单独的js文件,并且只能将对象传递给它们。

但是,我通过生成Blob文件设法使用没有分离文件的Web Workers,我也可以通过它们回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
//A fileless Web Worker
class ChildProcess {
     //@param {any} ags, Any kind of arguments that will be used in the callback, functions too
    constructor(...ags) {
        this.args = ags.map(a => (typeof a == 'function') ? {type:'fn', fn:a.toString()} : a)
    }

    //@param {function} cb, To be executed, the params must be the same number of passed in the constructor
    async exec(cb) {
        var wk_string = this.worker.toString();
        wk_string = wk_string.substring(wk_string.indexOf('{') + 1, wk_string.lastIndexOf('}'));            
        var wk_link = window.URL.createObjectURL( new Blob([ wk_string ]) );
        var wk = new Worker(wk_link);

        wk.postMessage({ callback: cb.toString(), args: this.args });
 
        var resultado = await new Promise((next, error) => {
            wk.onmessage = e => (e.data && e.data.error) ? error(e.data.error) : next(e.data);
            wk.onerror = e => error(e.message);
        })

        wk.terminate(); window.URL.revokeObjectURL(wk_link);
        return resultado
    }

    worker() {
        onmessage = async function (e) {
            try {                
                var cb = new Function(`return ${e.data.callback}`)();
                var args = e.data.args.map(p => (p.type == 'fn') ? new Function(`return ${p.fn}`)() : p);

                try {
                    var result = await cb.apply(this, args); //If it is a promise or async function
                    return postMessage(result)

                } catch (e) { throw new Error(`CallbackError: ${e}`) }
            } catch (e) { postMessage({error: e.message}) }
        }
    }
}

setInterval(()=>{console.log('Not blocked code ' + Math.random())}, 1000)

console.log("starting blocking synchronous code in Worker")
console.time("
blocked"
);

var proc = new ChildProcess(blockCpu, 43434234);

proc.exec(function(block, num) {
    //This will block for 10 sec, but
    block(10000) //This blockCpu function is defined below
    return `

bla bla ${num}
` //Captured in the resolved promise
}).then(function (result){
    console.timeEnd("
blocked"
)
    console.log("End of blocking code", result)
})
.catch(function(error) { console.log(error) })

//random blocking function
function blockCpu(ms) {
    var now = new Date().getTime();
    var result = 0
    while(true) {
        result += Math.random() * Math.random();
        if (new Date().getTime() > now +ms)
            return;
    }  
}


如果您正在使用jQuery,我创建了Alnitak答案的延迟实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function deferredEach (arr, batchSize) {

    var deferred = $.Deferred();

    var index = 0;
    function chunk () {
        var lastIndex = Math.min(index + batchSize, arr.length);

        for(;index<lastIndex;index++){
            deferred.notify(index, arr[index]);
        }

        if (index >= arr.length) {
            deferred.resolve();
        } else {
            setTimeout(chunk, 0);
        }
    };

    setTimeout(chunk, 0);

    return deferred.promise();

}

然后,您将能够使用返回的promise来管理进度并完成回调:

1
2
3
4
5
6
var testArray =["Banana","Orange","Apple","Mango"];
deferredEach(testArray, 2).progress(function(index, item){
    alert(item);
}).done(function(){
    alert("Done!");
})