Node.js Por Baixo dos Panos #2 - Entendendo JavaScript

Lucas Santos - Sep 24 '19 - - Dev Community

No nosso artigo anterior discutimos algumas coisas sobre C ++, o que é o Node.js, o que é o JavaScript, suas histórias, como elas surgiram e como são agora. Também conversamos um pouco sobre como uma função do filesystem é realmente implementada no Node.js. e como o Node.js. é realmente dividido em componentes.

Agora, vamos ao nosso segundo artigo desta série. Neste artigo, exploraremos alguns aspectos do JavaScript.

JavaScript por baixo dos panos

Vamos colocar as coisas em ordem. Pudemos ver a aparência do código C ++ real que é executado sob toda a bobagem que escrevemos no Node.js, como o JavaScript é o componente de nível mais alto do Node.js, vamos começar perguntando como nosso código é executado, e como JavaScript funciona?

A maioria das pessoas conhece algumas frases prontas e as repetem:

  • JavaScript é de single-threaded
  • O Chrome usa o V8 como Engine de JavaScript
  • JavaScript usa filas de callback
  • Existe um event loop

Mas eles se aprofundaram nessas questões?

  • O que significa ser single-threaded?
  • O que raios é um engine JS? E o que, de fato, é o V8?
  • Como essas filas de callback funcionam? Existe apenas uma fila?
  • O que é um event loop? Como funciona? Quem controla? Faz parte do JS?

Se você é capaz de responder mais de duas delas, considere-se acima da média, porque a maioria dos desenvolvedores JavaScript em geral nem sabe que há algo por trás dessa linguagem... Mas, não tema, estamos aqui para ajudar, então vamos aprofundar o conceito de JavaScript e como ele realmente funciona e, o mais importante, por que outras pessoas falam tão mal dele.

Engines JavaScript

Atualmente, o engine JavaScript mais popular é o V8 (um dos melhores softwares já escritos pela humanidade, depois do Git). Isso se deve ao simples fato de que o navegador mais usado é o Chrome, ou é baseado no Chromium - que é o engine de navegação de código aberto do Chrome - como Opera, Brave e assim por diante... No entanto, não é o único. Temos o Chakra, escrito pela Microsoft para o navegador Edge, o SpiderMonkey, escrito pela Netscape - que agora é executado pelo Firefox - e muitos outros como Rhino, KJS, Nashorn e etc.

No entanto, como a V8 é usada no Chrome e no Node.js, vamos nos manter neles. Primeiro, vamos dar um panorama geral mostrando uma visão muito simplificada da aparência de um engine JavaScript:

Imagem da pilha de sessões, nas referências

Esse mecanismo consiste principalmente em dois componentes:

  • O heap de memória: onde toda a alocação de memória acontece
  • A pilha de chamadas (ou call stack): onde nosso código é colocado em frames e empilhado para ser executado

Nós teremos um artigo somente para o V8 mais tarde

O Runtime JavaScript

A maioria das APIs que os desenvolvedores usam são fornecidas pelo próprio engine, como pudemos ver nos capítulos anteriores quando escrevemos o código readFile. No entanto, algumas não são fornecidas pelo engine, como setTimeout, qualquer tipo de manipulação de DOM, como document ou mesmo AJAX (o objeto XMLHttpRequest). De onde essas APIs vem? Vamos pegar nossa imagem anterior e trazê-la para a dura realidade em que vivemos:

Image from Session Stack, link in the references

O engine é apenas uma pequena parte do que faz o JavaScript, bem... JavaScript... Existem APIs fornecidas pelo navegador que chamamos de Web APIs - ou também, APIs externas - essas APIs (como DOM,AJAX e setTimeout) são fornecidos pelos desenvolvedores do navegador - nesse caso, para Chrome, é o Google - ou pelo próprio runtime, como Node (com APIs diferentes). E eles são a principal razão pela qual a maioria das pessoas odiava (e ainda odeia) o JavaScript. Quando olhamos para o JavaScript de hoje, vemos um campo cheio pacotes do NPM e outras coisas, mas principalmente homogêneo por todos os lados. Bom... Nem sempre foi assim.

Naquela época, antes do ES6 e do Node.js existirem até mesmo como uma ideia, não havia consenso sobre como implementar essas APIs no lado do navegador, para que cada fornecedor tivesse sua própria implementação deles, ou não... O que significava que tínhamos que checar e escrever constantemente trechos de código que funcionavam apenas em navegadores específicos (lembra do IE?), um navegador específico poderia implementar o XMLHttpRequest um pouco diferente de outros navegadores ou a função setTimeout pode ser chamada de sleep em alguma implementação; na pior das hipóteses, a API nem ia existir. Isso está mudando gradualmente, então agora, felizmente, temos algum consenso e algum acordo sobre quais APIs devem existir e como elas devem ser implementadas, pelo menos as mais usadas e básicas.

Além disso, temos o event loop e a fila de callbacks. Sobre o qual falaremos mais tarde.

Call Stack

A maioria das pessoas já ouviu falar que o JS é uma linguagem de single-threaded, e ai todo mundo aceitou isso como a verdade final do universo sem saber o porquê. Ser de cingle-threaded significa que só temos uma call stack, ou seja, só podemos executar uma coisa de cada vez.

A call stack não faz parte do Javascript, mas sim do engine, no nosso caso, V8. Mas vou colocar aqui para que possamos ter uma noção de como as coisas devem funcionar em um fluxo

Sobre pilhas

Pilhas são um tipo de dado abstrato que serve como uma coleção de elementos. O nome "pilha" deriva da analogia de um conjunto de caixas empilhadas umas sobre as outras, embora seja fácil tirar uma caixa da parte superior da pilha, pegar uma caixa mais abaixo pode exigir que tiremos vários outros itens da pilha primeiro.

A pilha possui dois métodos principais:

  • push: adiciona outro elemento à coleção
  • pop: remove o elemento adicionado mais recentemente que ainda não foi removido da pilha e retorna seu valor

Uma coisa importante sobre as pilhas é que a ordem de como os elementos são enviados realmente importa. Nas pilhas, a ordem na qual os elementos saem é chamada LIFO, um acrônimo para Last In First Out, que é bastante autoexplicativo.

Além disso, podemos ter outro método chamado peek, que lê o item adicionado mais recentemente (o topo da pilha) sem removê-lo.

Tudo o que precisamos saber sobre pilhas é o seguinte:

  • Eles são uma estrutura de dados na qual cada item da pilha possui um valor, no nosso caso, uma instrução ou chamada
  • Novos itens (chamadas) são adicionados ao topo da pilha
  • Os itens removidos também saem do topo da pilha

Pilhas e JavaScript

Basicamente, no JS, a pilha registra a posição que estamos executando atualmente em nosso programa. Se entrarmos em uma função, chamando-a, colocamos essa chamada no topo da pilha. Depois que retornamos de uma função, removemos o topo da pilha. Cada uma dessas chamadas é chamada de Stack Frame.

Vamos fazer, como primeiro exemplo, um programa simples, diferente do que tínhamos:

function multiply (x, y) {
    return x * y
}

function printSquare (x) {
    const s = multiply(x, x)
    console.log(s)
}

printSquare(5)
Enter fullscreen mode Exit fullscreen mode

Vamos rodar o nosso exemplo readFile mais tarde, quando tivermos tudo explicado

Quando o engine executa o código, primeiro, a call stack fica vazia. Após cada etapa, ela será preenchida com o seguinte:

Vamos aos poucos:

  • O passo 0 (não mostrado) é a pilha vazia, o que significa o início do nosso programa
  • Na primeira etapa, adicionamos a primeira chamada de função. A chamada para printSquare(5), já que todas as outras linhas são apenas declarações.
  • No segundo passo, entramos na definição da função printSquare
    • Veja como chamamos const s = multiply(x, x), então vamos adicionar o multiply(x, x) ao topo da pilha
    • Mais tarde, entramos em multiply, nenhuma chamada de função, nada é adicionado à pilha. Nós apenas damos um eval em x * y e devolvemos.
    • Retornar significa que a função terminou de executar, podemos removê-la da pilha
  • Na etapa 3, não temos mais o stack frame referenciando multiply(x, x). Então agora vamos para a linha logo após a última linha que executamos, é a linha console.log.

    • console.log é uma chamada de função, vamos adicionar ao topo da pilha
    • Depois que o console.log(s) é executado, podemos removê-lo da pilha
  • No passo 4, agora temos apenas um único stack frame: printSquare(5), que foi o primeiro que adicionamos

    • Como esta é a primeira chamada de função e não há outro código depois dela, isso significa que a função está concluída. Retiramos o stackframe da pilha
  • O passo 5 é igual ao passo 0, uma pilha vazia

As pilhas são exatamente como os stack traces são mostrados quando uma exceção é lançada. Um stack trace é basicamente o estado impresso da pilha de chamadas quando a exceção ocorreu:

function foo () {
    throw new Error('Exception');
}

function bar () {
    foo()
}

function start () {
    bar()
}

start()
Enter fullscreen mode Exit fullscreen mode

Deve printar algo como:

Uncaught Error: Exception foo.js:2
    at foo (foo.js:2)
    at bar (foo.js:6)
    at start (foo.js:10)
    at foo.js:13
Enter fullscreen mode Exit fullscreen mode

O at é só o nosso estado da pilha.

Stack Overflow

Não, esse erro não recebeu o nome do site, desculpa por te decepcionar. Na verdade, o site recebeu o nome de um dos erros mais comuns encontrados na programação desde o início dos tempos: o estouro da pilha ou Stack Overflow.

Um erro de stack overflow ocorre quando atingimos o tamanho máximo da call stack. Pilhas são estruturas de dados, o que significa que estão alocadas na memória e a memória não é infinita; portanto, isso pode acontecer com bastante facilidade, especialmente em funções recursivas não tratadas, como esta:

function f () {
  return f()
}

f()
Enter fullscreen mode Exit fullscreen mode

A cada chamada de f, empilharemos f na pilha, mas, como vimos, nunca podemos remover um item da pilha antes que ele chegue ao fim de sua execução, em outras palavras, quando o o código atinge um ponto em que nenhuma função é chamada. Portanto, nossa pilha teria o limite de espaço estourado porque não temos nenhuma condição de finalização:

Felizmente, o engine está nos observando e percebe que a função nunca irá parar de se chamar, causando um stack overflow, o que é um erro bastante sério, pois trava o aplicativo inteiro. Se não for interrompido, pode travar ou danificar a call stack como um todo.

Prós e contras de single-threading

A execução em um ambiente de single-threaded pode ser muito libertadora, pois é muito mais simples do que a execução em um mundo com várias threads, onde teríamos que nos preocupar com racing conditions e deadlocks. Neste mundo, essas coisas não existem, afinal, estamos fazendo apenas uma coisa de cada vez.

No entanto, single-threading também pode ser muito limitadora. Como temos uma única call stack, o que aconteceria se essa pilha fosse bloqueada por algum código que demorasse demais?

É isso que vamos descobrir no próximo artigo...

Não deixe de acompanhar mais do meu conteúdo no meu blog e se inscreva na newsletter para receber notícias semanais!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .