Como começar rapidamente com VUE3.0: Aprenda
sobre o contexto de execução, pilha de execução e mecanismo de execução (tarefas síncronas, tarefas assíncronas, microtarefas, macrotarefas e loops de eventos) em js
É um ponto de teste frequente em entrevistas. alguns amigos estão Você pode ficar confuso quando questionado, então vou resumir hoje, esperando que possa ser útil para você na frente da tela.
Antes de falar sobre o contexto de execução e o mecanismo de execução js
js
vamos falar sobre threads e processos.
Em termos oficiais,线程
é a menor unidade de escalonamento CPU
.
? Em termos oficiais,进程
é a menor unidade de alocação de recursos CPU
.
线程
线程
é uma unidade de execução de programa baseada em进程
. Em termos进程
,线程
é um fluxo de execução em um programa.
Existe apenas um fluxo de execução em um进程
denominado单线程
. Ou seja, quando o programa é executado, os caminhos do programa percorridos são organizados em ordem consecutiva. Os anteriores devem ser processados antes que os posteriores sejam executados.
Vários fluxos de execução em um进程
são chamados de多线程
, ou seja, vários线程
diferentes podem ser executados simultaneamente em um programa para executar tarefas diferentes, o que significa que um único programa pode criar vários线程
de execução paralelos para completar suas respectivas tarefas. .
O autor dará um exemplo simples abaixo. Por exemplo, se abrirmos qq音乐
e ouvirmos músicas, qq音乐
pode ser entendido como um processo. No qq音乐
podemos baixar enquanto ouvimos músicas. para músicas é um tópico e o download é um processo. Se abrirmos vscode
novamente para escrever código, será outro processo.
Os processos são independentes uns dos outros, mas alguns recursos são compartilhados entre threads no mesmo processo.
O ciclo de vida de um thread passa por cinco estágios.
Novo estado: depois de usar a palavra-chave new
e Thread
ou sua subclasse para criar um objeto thread, o objeto thread está no novo estado. Ele permanece neste estado até que o programa start()
o thread.
Estado pronto: quando o objeto thread chama start()
, o thread entra no estado pronto. O thread no estado pronto está na fila de prontos e pode ser executado imediatamente, desde que obtenha o direito de uso CPU
.
Estado de execução: se o thread no estado pronto obtiver recursos CPU
, ele poderá executar run()
e o thread estará no estado de execução. O thread no estado de execução é o mais complexo, pode ficar bloqueado, pronto e morto.
Estado de bloqueio: se um thread executar sleep(睡眠)
, suspend(挂起)
, wait(等待)
e outros métodos, após perder os recursos ocupados, o thread entrará no estado de bloqueio a partir do estado de execução. O estado pronto pode ser inserido novamente após o tempo de suspensão expirar ou os recursos do dispositivo terem sido obtidos. Pode ser dividido em três tipos:
bloqueio de espera: o thread no estado de execução executa wait()
, fazendo com que o thread entre no estado de bloqueio de espera.
Bloqueio síncrono: o thread não consegue adquirir o bloqueio de sincronização synchronized
(porque o bloqueio de sincronização está ocupado por outros threads).
Outro bloqueio: quando I/O
é emitida chamando sleep()
ou join()
do thread, o thread entrará no estado de bloqueio. Quando sleep()
expira, join()
espera o thread terminar ou expirar, ou o processamento I/O
é concluído e o thread retorna ao estado pronto.
Estado de morte: quando um thread em execução conclui sua tarefa ou ocorrem outras condições de encerramento, o thread muda para o estado finalizado.
JS
é de thread único. Como linguagem de script de navegador, JS
é usado principalmente para interagir com usuários e operar DOM
. Isso determina que só pode ser single-threaded, caso contrário causará problemas de sincronização muito complexos. Por exemplo, suponha que JavaScript
tenha dois threads ao mesmo tempo. Um thread adiciona conteúdo a um determinado nó DOM
e o outro thread exclui o nó. Nesse caso, qual thread o navegador deve usar?
Quando JS
analisa um fragmento de código executável (geralmente a fase de chamada de função), ele primeiro realiza algum trabalho preparatório antes da execução. (Contexto de execução (referido como EC
)" ou também pode ser chamado de ambiente de execução .
Existem três tipos de contexto de execução em javascript
, que são:
contexto de execução global Este é o contexto de execução padrão ou mais básico. Haverá apenas um contexto global em um programa, e ele existirá durante todo o ciclo de vida do programa. Script javascript
. A parte inferior da pilha de execução não será destruída pelo estouro da pilha. O contexto global irá gerar um objeto global (tomando o ambiente do navegador como exemplo, este objeto global é window
) e vincular o valor this
a este objeto global.
Contexto de execução de função Sempre que uma função é chamada, um novo contexto de execução de função é criado (independentemente de a função ser chamada repetidamente).
Contexto de execução da função eval O código executado dentro eval
também terá seu próprio contexto de execução, mas como eval
não é usado com frequência, ele não será analisado aqui.
Mencionamos anteriormente que js
criará um contexto de execução quando estiver em execução, mas o contexto de execução precisa ser armazenado, então o que é usado para armazená-lo? Você precisa usar a estrutura de dados da pilha.
A pilha é uma estrutura de dados do tipo primeiro a entrar, último a sair.
Então, em resumo, o contexto de execução usado para armazenar o contexto de execução criado quando o código está em execução é a pilha de execução .
Ao executar um trecho de código JS
Em seguida, JS
criará um contexto de execução global e push
para a pilha de execução. Nesse processo, JS
alocará memória para todas as variáveis neste código e atribuirá um valor inicial (indefinido). O mecanismo JS
entrará na fase de execução, neste processo JS
executará o código linha por linha, ou seja, atribuirá valores (valores reais) às variáveis que foram alocadas na memória uma por uma.
Se houver function
neste código, JS
criará um contexto de execução de função e push
para a pilha de execução. O processo de criação e execução é igual ao contexto de execução global.
Quando uma pilha de execução for concluída, o contexto de execução será retirado da pilha e, em seguida, o próximo contexto de execução será inserido.
Deixe-me dar um exemplo abaixo. Se tivermos o seguinte código em nosso programa:
console.log("Global Execution Context start"); função primeiro() { console.log("primeira função"); segundo(); console.log("Novamente primeira função"); } função segundo() { console.log("segunda função"); } primeiro(); console.log("Fim do contexto de execução global");
Vamos analisar brevemente o exemplo acima.
Primeiro, uma pilha de execução será criada
, depois um contexto global será criado e o contexto de execução será push
para a pilha de execução
para iniciar a execução. , e Global Execution Context start
encontra first
método, executa o método, cria um contexto de execução de função e push
para a pilha de execução
para executar first
contexto de execução, gera first function
que encontra second
método, executa o método. , cria um contexto de execução de função e push
para a pilha de execução para
executar second
contexto de execução second function
first
second
função de saída foi executada, retirada da pilha e inserida no próximo contexto de execução
first
execução, saída Again first function
first
contexto de execução foi executado, retirado da pilha e inserido na próxima Contexto de execução Contexto de execução global
Contexto de execução global Continuar execução e saída Global Execution Context end
Usamos uma imagem para resumir
tudo bem. Depois de falar sobre o contexto de execução e a pilha de execução, vamos falar sobre o mecanismo de execução de js. Falando sobre o mecanismo de execução de js
js
entender as tarefas síncronas, tarefas assíncronas, tarefas macro e micro tarefas em js
.
Em js
, as tarefas são divididas em tarefas síncronas e tarefas assíncronas. Então, o que são tarefas síncronas e o que são tarefas assíncronas?
Tarefas síncronas referem-se a tarefas enfileiradas para execução no thread principal. A próxima tarefa só pode ser executada após a execução da tarefa anterior.
Tarefas assíncronas referem-se a tarefas que não entram no thread principal, mas entram na "fila de tarefas" (as tarefas na fila de tarefas são executadas em paralelo com o thread principal somente quando o thread principal está ocioso e a "fila de tarefas" notifica o). thread principal, uma tarefa assíncrona. Uma vez executada, a tarefa entrará no thread principal para execução. Por ser um armazenamento em fila, ele atende à regra primeiro a entrar, primeiro a sair . Tarefas assíncronas comuns incluem setInterval
, setTimeout
, promise.then
, etc.
introduziu anteriormente tarefas síncronas e tarefas assíncronas. Agora vamos falar sobre o loop de eventos.
As tarefas síncronas e assíncronas entram em diferentes "locais" de execução, respectivamente, e entram no thread principal de forma síncrona. Somente quando a tarefa anterior for concluída a próxima tarefa poderá ser executada. As tarefas assíncronas não entram no thread principal, mas entram Event Table
e registram funções.
Quando o item especificado for concluído, Event Table
moverá esta função para Event Queue
. Event Queue
é uma estrutura de dados de fila, portanto, atende à regra primeiro a entrar, primeiro a sair.
Quando as tarefas no thread principal estiverem vazias após a execução, a função correspondente será lida Event Queue
e executada no thread principal.
O processo acima será repetido continuamente, o que geralmente é chamado de Event Loop .
Vamos resumir com uma imagem
Deixe-me apresentar brevemente um exemplo
function test1() { console.log("log1"); setTimeout(() => { console.log("setTimeout 1000"); }, 1000); setTimeout(() => { console.log("setTimeout 100"); }, 100); console.log("log2"); } test1(); // log1, log2, setTimeout 100, setTimeout 1000
Sabemos que em js, as tarefas síncronas serão executadas primeiro antes das tarefas assíncronas, então o exemplo acima produzirá log1、log2
primeiro
e, em seguida, executará tarefas assíncronas após as síncronas
.
100
setTimeout 100
de retorno de chamada com um atraso de 1000
milissegundos executará a saída setTimeout 1000
mais tarde.
contanto que você entenda as tarefas síncronas e assíncronas mencionadas pelo autor acima, não haverá problema. Então deixe-me dar outro exemplo, amigos, vamos ver qual será o resultado.
função teste2() { console.log("log1"); setTimeout(() => { console.log("setTimeout 1000"); }, 1000); setTimeout(() => { console.log("setTimeout 100"); }, 100); nova promessa((resolver, rejeitar) => { console.log("nova promessa"); resolver(); }).então(() => { console.log("promessa.então"); }); console.log("log2"); } test2();
Para resolver o problema acima, não basta conhecer tarefas síncronas e assíncronas. Também precisamos conhecer tarefas macro e micro.
Em js
, as tarefas são divididas em dois tipos, um é chamado de tarefa macro MacroTask
e o outro é chamado de tarefa micro MicroTask
.
A tarefa macro comum MacroTask
tem
o bloco de código principal
setTimeout()
setInterval()
setImmediate() - Node
requestAnimationFrame() - o navegador.
A micro tarefa comum MicroTask
tem
Promise.then()
process.nextTick() - Node
. exemplo acima Envolve tarefas macro e micro tarefas. Qual é a ordem de execução de tarefas macro e micro tarefas?
Em primeiro lugar, quando o script
geral (como a primeira tarefa macro) começar a ser executado, todo o código será dividido em duas partes: tarefas síncronas e tarefas assíncronas. As tarefas síncronas entrarão diretamente no thread principal para execução em sequência, e as tarefas assíncronas entrarão na fila assíncrona e serão divididas em tarefas macro e micro tarefas.
A tarefa macro entra Event Table
e registra uma função de retorno de chamada nela. Sempre que o evento especificado for concluído, Event Table
moverá esta Event Queue
para a Fila de Eventos. A tarefa micro também entrará em outra Event Table
e se registrará nela. . Sempre que o evento especificado for concluído, Event Table
moverá esta função para Event Queue
Quando as tarefas no thread principal forem concluídas e o thread principal estiver vazio, Event Queue
da microtarefa será verificada. , todos Execute, caso contrário, execute a próxima tarefa macro.
Usamos uma imagem para resumi-la.
Depois de compreender os exemplos acima de tarefas macro e micro tarefas assíncronas, podemos facilmente obter a resposta.
Sabemos que em js, as tarefas síncronas serão executadas primeiro, antes das tarefas assíncronas, portanto, o exemplo acima produzirá log1、new promise、log2
primeiro. Deve-se notar aqui que o bloco de código principal da nova promessa é sincronizado.
Após
a execução da tarefa macro, todas as micro tarefas geradas por esta tarefa macro serão executadas, então promise.then
.
, outra tarefa de macro será executada, atrasando A função de retorno de chamada de 100
milissegundos priorizará a execução e gerará setTimeout 100
Esta macrotarefa não gera microtarefas, portanto não há microtarefas que precisam ser executadas
para continuar executando a próxima macrotarefa. A função com atraso de 1000
priorizará a execução e gerará setTimeout 1000
portanto, após a execução do método test2, ela gerará log1、new promise、log2、promise.then、setTimeout 100、setTimeout 1000
em sequência
. opiniões diferentes sobre se deve executar
js
primeiro com tarefas macro e depois com micro tarefas ou com micro tarefas antes de tarefas macro. O entendimento do autor é que, se todo o bloco de códigojs
for considerado uma tarefa macro, nossa ordem de execuçãojs
será primeiro a macro tarefa e depois a micro tarefa.
Como diz o ditado, praticar uma vez é melhor do que assistir cem vezes. Darei dois exemplos abaixo. Se você conseguir fazer isso corretamente, então você domina o conhecimento do mecanismo de execução js
.
Exemplo 1
função test3() { console.log(1); setTimeout(função(){ console.log(2); nova promessa(função (resolver) { console.log(3); resolver(); }).então(função(){ console.log(4); }); console.log(5); }, 1000); nova promessa(função (resolver) { console.log(6); resolver(); }).então(função(){ console.log(7); setTimeout(função(){ console.log(8); }); }); setTimeout(função(){ console.log(9); nova promessa(função (resolver) { console.log(10); resolver(); }).então(função(){ console.log(11); }); }, 100); console.log(12); } test3();
Vamos analisá-lo em detalhes.
Primeiro, o bloco de código js
geral é executado como uma tarefa de macro e 1, 1、6、12
são gerados em sequência.
Depois que a tarefa macro geral do bloco de código é executada, uma microtarefa e duas macrotarefas são geradas, de modo que a fila de tarefas macro tem duas tarefas macro e a fila de microtarefas tem uma microtarefa.
Após a execução da tarefa macro, todas as microtarefas geradas por esta tarefa macro serão executadas. Como há apenas uma microtarefa, 7
serão geradas. Esta microtarefa gerou outra macrotarefa, portanto, atualmente existem três macrotarefas na fila de macrotarefas.
Entre as três macrotarefas, aquela sem atraso definido é executada primeiro, então 8
é a saída. Esta macrotarefa não gera microtarefas, portanto não há microtarefas para executar e a próxima macrotarefa continua a ser executada.
Atrase a execução da macrotarefa por 100
milissegundos, produza 9、10
e gere uma microtarefa, para que a fila de microtarefas tenha atualmente uma microtarefa.
Depois que a macrotarefa for executada, todas as microtarefas geradas pela macrotarefa serão executadas, então a microtarefa será
executada
. Todas as microtarefas na fila de tarefas geram 11
A execução da macrotarefa gera 2、3、5
com um atraso de 1000
milissegundos e uma microtarefa é gerada.
a macrotarefa será executada. Todas as microtarefas serão geradas, portanto, todas as microtarefas na fila de microtarefas serão executadas e 4
serão gerados
. Portanto, o exemplo de código acima produzirá 1、6、12、7、8、9、10、11、2、3、5、4
, 12, 7, 8, 9, 10, 11. , 2, 3, 5, 4 em sequência.
Exemplo 2:
Modificamos ligeiramente o exemplo 1 acima e introduzimos async
e await
async function test4() { console.log(1); setTimeout(função(){ console.log(2); nova promessa(função (resolver) { console.log(3); resolver(); }).então(função(){ console.log(4); }); console.log(5); }, 1000); nova promessa(função (resolver) { console.log(6); resolver(); }).então(função(){ console.log(7); setTimeout(função(){ console.log(8); }); }); const resultado = aguarda async1(); console.log(resultado); setTimeout(função(){ console.log(9); nova promessa(função (resolver) { console.log(10); resolver(); }).então(função(){ console.log(11); }); }, 100); console.log(12); } função assíncrona async1() { console.log(13) return Promise.resolve("Promise.resolve"); } test4();
Qual será a saída do exemplo acima? Aqui podemos resolver facilmente o problema de async
e await
.
Sabemos async
e await
são na verdade sintaxe para Promise
. Aqui só precisamos saber await
é equivalente a Promise.then
. Portanto, podemos entender o exemplo acima como o seguinte código
function test4() { console.log(1); setTimeout(função(){ console.log(2); nova promessa(função (resolver) { console.log(3); resolver(); }).então(função(){ console.log(4); }); console.log(5); }, 1000); nova promessa(função (resolver) { console.log(6); resolver(); }).então(função(){ console.log(7); setTimeout(função(){ console.log(8); }); }); nova promessa(função (resolver) { console.log(13); return resolve("Promise.resolve"); }).então((resultado) => { console.log(resultado); setTimeout(função(){ console.log(9); nova promessa(função (resolver) { console.log(10); resolver(); }).então(função(){ console.log(11); }); }, 100); console.log(12); }); } test4();
Você consegue obter facilmente o resultado depois de ver o código acima?
Primeiro, todo o bloco de código js
é inicialmente executado como uma tarefa macro e gera 1、6、13
em sequência.
Depois que a tarefa macro geral do bloco de código é executada, duas microtarefas e uma tarefa macro são geradas, de modo que a fila de tarefas macro tem uma tarefa macro e a fila de microtarefas tem duas microtarefas.
Após a execução da tarefa macro, todas as microtarefas geradas por esta tarefa macro serão executadas. Portanto 7、Promise.resolve、12
será gerado. Esta microtarefa gerou mais duas macrotarefas, portanto a fila de macrotarefas atualmente tem três macrotarefas.
Entre as três macrotarefas, aquela sem atraso definido é executada primeiro, então 8
é a saída. Esta macrotarefa não gera microtarefas, portanto não há microtarefas para executar e a próxima macrotarefa continua a ser executada.
Atrase a execução da macrotarefa por 100
milissegundos, produza 9、10
e gere uma microtarefa, para que a fila de microtarefas tenha atualmente uma microtarefa.
Depois que a macrotarefa for executada, todas as microtarefas geradas pela macrotarefa serão executadas, então a microtarefa será
executada
. Todas as microtarefas na fila de tarefas geram 11
A execução da macrotarefa gera 2、3、5
com um atraso de 1000
milissegundos e uma microtarefa é gerada.
a macrotarefa será executada. Todas as microtarefas geradas executarão todas as microtarefas na fila de microtarefas e gerarão 4
Portanto, o exemplo de código acima produzirá 1, 6, 13, 7 1、6、13、7、Promise.resolve、12、8、9、10、11、2、3、5、4
4, vocês fizeram certo?
Muitos amigos ainda podem não entender setTimeout(fn)
. Não é óbvio que o tempo de atraso não deve ser executado imediatamente?
Podemos entender setTimeout(fn)
como setTimeout(fn,0)
, o que na verdade significa a mesma coisa.
Sabemos que js é dividido em tarefas síncronas e tarefas assíncronas setTimeout(fn)
é uma tarefa assíncrona, portanto, mesmo que você não defina um tempo de atraso aqui, ela entrará na fila assíncrona e não será executada até que o thread principal seja executado. parado.
O autor vai mencionar isso novamente, você acha que o tempo de atraso que definimos após setTimeout
, js
será definitivamente executado de acordo com o nosso tempo de atraso, acho que não. O tempo que definimos é apenas para que a função de retorno de chamada possa ser executada, mas se o thread principal está livre é outra questão. Podemos dar um exemplo simples.
função teste5() { setTimeout(função(){ console.log("setTimeout"); }, 100); seja i = 0; enquanto (verdadeiro) { eu++; } } test5();
O exemplo acima definitivamente gerará setTimeout
após 100
milissegundos? Não, porque nosso thread principal entrou em um loop infinito e não tem tempo para executar tarefas de fila assíncronas.
GUI渲染
é mencionada aqui. Alguns amigos podem não entendê-la. Vou apresentá-la em detalhes em um artigo sobre navegadores.
Como JS引擎线程
e GUI渲染线程
são mutuamente exclusivos, para permitir que宏任务
e DOM任务
prossigam de maneira ordenada, o navegador iniciará GUI渲染线程
após o resultado da execução de uma宏任务
e antes do execução da próxima宏任务
, renderize a página.
Portanto, a relação entre tarefas macro, micro tarefas e renderização de GUI é a seguinte:
tarefa macro -> micro tarefa -> renderização GUI -> tarefa macro ->...
[Recomendação de tutorial em vídeo relacionado: front end da web]
O acima é uma análise aprofundada do JavaScript Para obter detalhes sobre o contexto de execução e o mecanismo de execução, preste atenção a outros artigos relacionados no site PHP chinês para obter mais informações!