1. Por que o JavaScript é um único thread?
Uma característica importante do idioma JavaScript é um único encadeamento, o que significa que você só pode fazer uma coisa ao mesmo tempo. Então, por que o JavaScript não pode ter vários threads? Isso melhorará a eficiência.
O encadeamento único do JavaScript está relacionado ao seu objetivo. Como linguagem de script de navegador, o principal objetivo do JavaScript é interagir com os usuários e operar o DOM. Isso determina que só pode ser um único rosqueado, caso contrário, causará problemas de sincronização muito complexos. Por exemplo, suponha que o JavaScript tenha dois threads ao mesmo tempo, um thread adiciona conteúdo em um determinado nó DOM e o outro thread exclui esse nó, qual encadeamento o navegador deve tomar neste momento?
Portanto, para evitar a complexidade, o JavaScript é um único thread desde o nascimento, que se tornou a característica central desse idioma e não mudará no futuro.
Para utilizar o poder de computação das CPUs com vários núcleos, o HTML5 propôs o padrão do trabalhador da web, permitindo que os scripts JavaScript criassem vários threads, mas os threads infantis são completamente controlados pelo encadeamento principal e não podem operar o DOM. Portanto, esse novo padrão não altera a natureza do threading único JavaScript.
2. Fila de tarefas
Trebo único significa que todas as tarefas precisam ser filadas e a tarefa anterior será executada antes que a próxima tarefa seja executada. Se a tarefa anterior levar muito tempo, a próxima tarefa terá que esperar.
Se a fila for por causa da grande quantidade de computação e da CPU estiver muito ocupada, tudo bem, mas muitas vezes a CPU está ociosa porque o dispositivo de IO (dispositivo de entrada e saída) é muito lento (como a operação AJAX lê dados da rede) e você deve esperar o resultado para ser lançado antes de executá -lo.
O designer do idioma JavaScript percebeu que, naquele momento, a CPU poderia ignorar completamente o dispositivo IO, suspender as tarefas de espera e executar as próximas tarefas primeiro. Aguarde até que o dispositivo IO retorne o resultado, depois vire e continue a tarefa suspensa.
Portanto, o JavaScript possui dois métodos de execução: um é que a CPU é executada em sequência, a tarefa anterior termina e, em seguida, a próxima tarefa é executada, que é chamada de execução síncrona; O outro é que a CPU ignora as tarefas com um longo tempo de espera e processa as tarefas subsequentes primeiro, que é chamada de execução assíncrona. Os programadores escolhem independentemente que tipo de método de execução adotar.
Especificamente, o mecanismo operacional de execução assíncrona é o seguinte. (O mesmo vale para a execução síncrona, pois pode ser considerada como execução assíncrona sem tarefas assíncronas.)
(1) Todas as tarefas são executadas no thread principal para formar uma pilha de contexto de execução.
(2) Além do encadeamento principal, há também uma "fila de tarefas". O sistema coloca as tarefas assíncronas na "fila de tarefas" e continua a executar tarefas subsequentes.
(3) Depois que todas as tarefas na "pilha de execução" forem executadas, o sistema lerá a "fila de tarefas". Se neste momento, a tarefa assíncrona encerrou o estado de espera, ele entrará na pilha de execução da "fila de tarefas" e retomará a execução.
(4) O encadeamento principal continua repetindo o terceiro passo acima.
A figura a seguir é um diagrama esquemático da linha principal e da fila de tarefas.
Enquanto o encadeamento principal estiver vazio, ele lerá a "fila de tarefas". Este é o mecanismo de execução do JavaScript. Esse processo será repetido continuamente.
3. Eventos e funções de retorno de chamada
"Fila de tarefas" é essencialmente uma fila de eventos (também entendida como uma fila de mensagens). Quando um dispositivo de OI conclui uma tarefa, ele adiciona um evento à "fila de tarefas", indicando que as tarefas assíncronas relevantes podem entrar na "pilha de execução". O tópico principal diz a "fila de tarefas", o que significa ler quais eventos estão dentro.
Os eventos na "fila de tarefas" incluem eventos, além de eventos de dispositivos de IO, mas também eventos gerados por usuários (como cliques de mouse, rolagem de página etc.). Enquanto a função de retorno de chamada for especificada, esses eventos entrarão na "fila de tarefas" quando ocorrerem e aguardam a leitura do thread principal.
O chamado "retorno de chamada" é o código que será pendurado pelo thread principal. Tarefas assíncronas devem especificar uma função de retorno de chamada. Quando a tarefa assíncrona retornar da "fila de tarefas" para a pilha de execução, a função de retorno de chamada será executada.
"Fila de tarefas" é a primeira estrutura de dados da primeira saída, com eventos classificados primeiro e preferidos para retornar ao thread principal. O processo de leitura do tópico principal é basicamente automático. Enquanto a pilha de execução for limpa, o primeiro evento na "fila de tarefas" retornará automaticamente ao thread principal. No entanto, devido à função "timer" mencionada posteriormente, o encadeamento principal precisa verificar o tempo de execução e alguns eventos devem retornar ao thread principal no horário especificado.
4. Loop de eventos
O tópico principal lê eventos da "fila de tarefas". Esse processo está em loop continuamente, portanto, todo o mecanismo de corrida também é chamado de loop de eventos.
Para entender melhor o loop de eventos, consulte a foto abaixo (citado pelo discurso de Philip Roberts "Ajuda, estou preso em um loop de eventos").
Na figura acima, quando o encadeamento principal está em execução, ele gera pilha e pilha. O código na pilha chama várias APIs externas, que adicionam vários eventos (clique, carregam, concluídos) à "fila de tarefas". Enquanto o código na pilha for executado, o encadeamento principal lerá a "fila de tarefas" e executará as funções de retorno de chamada correspondentes a esses eventos, por sua vez.
Execute o código na pilha, sempre executado antes de ler a "fila de tarefas". Por favor, veja o exemplo a seguir.
A cópia do código é a seguinte:
var req = novo xmlHttPrequest ();
req.open ('get', url);
req.onload = function () {};
req.onerror = function () {};
req.send ();
O método req.send no código acima é uma operação AJAX para enviar dados para o servidor. É uma tarefa assíncrona, o que significa que o sistema lerá a "fila de tarefas" somente depois que todo o código no script atual é executado. Portanto, é equivalente ao método de escrita a seguir.
A cópia do código é a seguinte:
var req = novo xmlHttPrequest ();
req.open ('get', url);
req.send ();
req.onload = function () {};
req.onerror = function () {};
Ou seja, as partes da função de retorno de chamada especificadas (Onload e Onerror) não são importantes antes ou depois do método send (), porque fazem parte da pilha de execução, e o sistema sempre as executará antes de ler a "fila de tarefas".
5. Timer
Além de colocar tarefas assíncronas, a "fila de tarefas" também tem outra função, que é colocar eventos cronometrados, ou seja, especificar quanto tempo certos código serão executados após. Isso é chamado de função "timer", que é o código executado regularmente.
A função do timer é concluída principalmente por duas funções: setTimeout () e setInterval (). Seus mecanismos internos de corrida são exatamente os mesmos. A diferença é que o código especificado pelo primeiro é executado ao mesmo tempo, enquanto este é executado repetidamente. O seguinte discute principalmente setTimeout ().
setTimeout () aceita dois parâmetros, o primeiro é a função de retorno de chamada e o segundo é o número de milissegundos para adiar a execução.
A cópia do código é a seguinte:
console.log (1);
setTimeout (function () {console.log (2);}, 1000);
console.log (3);
O resultado da execução do código acima é 1, 3, 2, porque o setTimeout () atrasa a segunda linha até depois de 1000 milissegundos.
Se o segundo parâmetro do setTimeout () estiver definido como 0, isso significa que a função de retorno de chamada especificada (intervalo de 0 milissegundos) será executado imediatamente após a execução do código atual (a pilha de execução for limpa).
A cópia do código é a seguinte:
setTimeout (function () {console.log (1);}, 0);
console.log (2);
Os resultados da execução do código acima são sempre 2 e 1, porque o sistema executará a função de retorno de chamada na "fila de tarefas" somente após a execução da segunda linha.
O padrão HTML5 especifica que o valor mínimo (intervalo mais curto) do segundo parâmetro de setTimeout () não deve ser inferior a 4 milissegundos. Se for menor que esse valor, aumentará automaticamente. Antes disso, os navegadores mais antigos definem o intervalo mínimo para 10 milissegundos.
Além disso, para essas mudanças de DOM (especialmente as peças que envolvem renderização de páginas), elas geralmente não são executadas imediatamente, mas a cada 16 milissegundos. No momento, o efeito do uso de requestanimationframe () é melhor que o setTimeout ().
Deve -se notar que o setTimeout () apenas insere o evento na "fila de tarefas". Você deve esperar até que o código atual (pilha de execução) seja executado antes que o encadeamento principal execute a função de retorno de chamada que ele especifica. Se o código atual levar muito tempo, pode levar muito tempo para esperar, para que não haja garantia de que a função de retorno de chamada seja executada no momento especificado pelo setTimeout ().
6. loop de eventos node.js
O Node.js também é um loop de eventos de thread único, mas seu mecanismo de corrida é diferente do do ambiente do navegador.
Consulte o diagrama abaixo (autor @busyrich).
De acordo com a figura acima, o mecanismo de corrida do Node.js é o seguinte.
(1) V8 analisa os scripts JavaScript.
(2) O código analisado chama a API do nó.
(3) A biblioteca Libuv é responsável pela execução da API do nó. Ele atribui tarefas diferentes a diferentes encadeamentos, forma um loop de eventos e retorna os resultados da execução da tarefa ao mecanismo V8 de maneira assíncrona.
(4) O mecanismo V8 retorna o resultado ao usuário.
Além dos dois métodos Settimeout e SetInterval, o Node.js também fornece outros dois métodos relacionados à "fila de tarefas": process.nextTick e SetImediate. Eles podem nos ajudar a aprofundar nossa compreensão das "filas de tarefas".
O método Process.NextTick pode acionar a função de retorno de chamada no final da atual "pilha de execução" antes que o thread principal lê a "fila de tarefas" na próxima vez. Ou seja, as tarefas que especifica sempre ocorrem antes de todas as tarefas assíncronas. O método SetImediate aciona a função de retorno de chamada na cauda da "fila de tarefas" atual, ou seja, a tarefa que ela especifica é sempre executada na próxima vez que o thread principal lê a "fila de tarefas", que é muito semelhante ao setTimeout (FN, 0). Consulte o exemplo a seguir (via Stackoverflow).
A cópia do código é a seguinte:
process.nextTick (função a () {
console.log (1);
process.nextTick (função b () {console.log (2);});
});
setTimeout (função timeout () {
console.log ('Timeout disparado');
}, 0)
// 1
// 2
// Tempo limite disparou
No código acima, como a função de retorno de chamada especificada pelo processo. Isso significa que, se houver múltiplos processos.
Agora, vamos olhar para o SetImediate.
A cópia do código é a seguinte:
setImediate (função a () {
console.log (1);
setImediate (função b () {console.log (2);});
});
setTimeout (função timeout () {
console.log ('Timeout disparado');
}, 0)
// 1
// Tempo limite disparou
// 2
No código acima, existem dois setimedados. O primeiro setImediate especifica que a função de retorno de chamada A é acionada na cauda da "fila de tarefas" atual (o próximo "loop de evento"); Em seguida, o Settimeout também especifica que o tempo limite da função de retorno de chamada é acionado na cauda da "fila de tarefas" atual; portanto, no resultado da saída, o tempo limite disparado é classificado atrás de 1. Quanto ao ranking 2 atrás do tempo limite, é porque outra característica importante do SetImediate: um "evento de eventos" pode acionar uma função de chamada especificada por setimmed.
Obtivemos uma diferença importante a partir disso: o processo múltiplo. De fato, é exatamente por isso que o Node.js versão 10.0 adiciona o método SetImediate. Caso contrário, a chamada recursiva para o processo.
A cópia do código é a seguinte:
process.nextTick (function foo () {
process.NextTick (foo);
});
De fato, agora se você escrever um processo recursivo.
Além disso, como a função de retorno de chamada especificada pelo processo.