En comparación con C/C ++, el procesamiento de JavaScript en la memoria en el JavaScript que utilizamos nos ha hecho prestar más atención a la redacción de la lógica de negocios en el desarrollo. Sin embargo, con la complejidad continua del negocio, el desarrollo de aplicaciones de una sola página, las aplicaciones HTML5 móviles, los programas Node.js, etc., el desbordamiento de retraso y memoria causado por problemas de memoria en JavaScript ya no se ha vuelto desconocido.
Este artículo discutirá el uso y la optimización de la memoria desde el nivel de lenguaje de JavaScript. Desde los aspectos con los que todos están familiarizados o un poco escuchados, hasta las cosas que no todos notarán la mayor parte del tiempo, los analizaremos uno por uno.
1. Gestión de memoria a nivel de idioma
1.1 alcance
El alcance es un mecanismo operativo muy importante en la programación de JavaScript. No atrae la atención de los principiantes en la programación síncrona de JavaScript, pero en la programación asíncrona, las habilidades de control de buen alcance se han convertido en una habilidad necesaria para los desarrolladores de JavaScript. Además, el alcance juega un papel crucial en la gestión de la memoria de JavaScript.
En JavaScript, se pueden llamar funciones, con declaraciones y ámbitos globales que se pueden alcanzar.
Como se muestra en el siguiente código como ejemplo:
La copia del código es la siguiente:
var foo = function () {
var local = {};
};
foo ();
console.log (local); // => indefinido
var bar = function () {
local = {};
};
bar();
console.log (local); // => {}
Aquí definimos la función foo () y la función bar (). Su intención es definir una variable llamada local. Pero el resultado final es completamente diferente.
En la función foo (), utilizamos la instrucción VAR para declarar que se define una variable local. Debido a que se forma un alcance dentro del cuerpo de la función, esta variable se define en el alcance. Además, no hay un procesamiento de extensión de alcance en la función foo (), por lo que después de ejecutar la función, la variable local también se destruye. Sin embargo, no se puede acceder a esta variable en el alcance externo.
En la función Bar (), la variable local no se declara utilizando una declaración VAR, sino que define directamente local como una variable global. Por lo tanto, se puede acceder a esta variable mediante el alcance externo.
La copia del código es la siguiente:
local = {};
// La definición aquí es equivalente a
global.local = {};
1.2 cadena de alcance
En la programación de JavaScript, definitivamente encontrará escenarios de anidación de funciones de múltiples capas, que es una representación típica de las cadenas de alcance.
Como se muestra en el siguiente código:
La copia del código es la siguiente:
función foo () {
var val = 'Hola';
Function Bar () {
función baz () {
global.val = 'World;'
}
Baz ();
console.log (val); // => hola
}
bar();
}
foo ();
Según la explicación anterior sobre el alcance, puede pensar que el resultado que se muestra en el código aquí es mundo, pero el resultado real es hola. Muchos principiantes comenzarán a sentirse confundidos aquí, así que echemos un vistazo a cómo funciona este código.
Desde JavaScript, la búsqueda de identificadores variables comienza desde el alcance actual y mira hacia afuera hasta el alcance global. Por lo tanto, el acceso a las variables en el código JavaScript solo se puede realizar hacia afuera, pero no en reversa.
La ejecución de la función Baz () define una variable global Val en el alcance global. En la función Bar (), al acceder al identificador Val, el principio de buscar desde el interior hacia el exterior es: si no se encuentra en el alcance de la función de la barra, va a la capa anterior, es decir, el alcance de la función foo ().
Sin embargo, la clave para confundir a todos es: este acceso de identificador encuentra una variable de coincidencia en el alcance de la función foo (), y no continuará mirando hacia afuera, por lo que la variable global definida en la función baz () no tiene ningún efecto en este acceso variable.
1.3 Cierre
Sabemos que la búsqueda de identificadores en JavaScript sigue el principio de adentro hacia afuera. Sin embargo, con la complejidad de la lógica de negocios, una sola orden de entrega está lejos de satisfacer las nuevas necesidades.
Echemos un vistazo al siguiente código:
La copia del código es la siguiente:
función foo () {
var local = 'hola';
Función de retorno () {
regresar local;
};
}
bar bar = foo ();
console.log (bar ()); // => hola
La tecnología que permite que el alcance exterior acceda al alcance interno como se muestra aquí es el cierre. Gracias a la aplicación de funciones de orden superior, el alcance de la función foo () está "extendido".
La función foo () devuelve una función anónima, que existe dentro del alcance de la función foo (), por lo que puede acceder a la variable local dentro del alcance de la función foo () y guardar su referencia. Debido a que esta función devuelve directamente la variable local, la función Bar () se puede ejecutar directamente en el alcance externo para obtener la variable local.
Los cierres son una característica de alto nivel de JavaScript, y podemos usarlos para lograr efectos cada vez más complejos para satisfacer las diferentes necesidades. Sin embargo, debe tenerse en cuenta que debido a que la función con referencia de variable interna se saca de la función, las variables en este alcance no se destruirán después de que la función se ejecute hasta que se cancelen todas las referencias a las variables internas. Por lo tanto, la aplicación de cierres puede hacer que la memoria sea poco libre.
2. Mecanismo de reciclaje de memoria de JavaScript
Aquí tomaré el motor V8 utilizado por Chrome y Node.js y lanzado por Google como ejemplo para introducir brevemente el mecanismo de reciclaje de memoria de JavaScript. Para obtener contenido más detallado, puede comprar el libro de mi buen amigo Park Ling "en profundidad y fácil de entender Node.js" para el aprendizaje, que es una introducción muy detallada en el capítulo "Control de la memoria".
En V8, todos los objetos JavaScript se asignan a través del "montón".
Cuando declaramos y asignamos valores en el Código, V8 asignará una parte de esta variable en la memoria de Heap. Si la memoria solicitada es insuficiente para almacenar esta variable, V8 continuará solicitando la memoria hasta que el tamaño del montón alcance el límite de memoria de V8. Por defecto, el límite superior de la memoria del montón de V8 es de 1464 MB en sistemas de 64 bits y 732 MB en sistemas de 32 bits, que es de aproximadamente 1,4 GB y 0,7 GB.
Además, V8 gestiona los objetos JavaScript en la memoria del montón en generaciones: la nueva generación y la antigua generación. La nueva generación son objetos JavaScript con breves ciclos de supervivencia, como variables temporales, cadenas, etc.; Si bien la antigua generación es objetos con largos ciclos de supervivencia después de múltiples colecciones de basura, como controladores principales, objetos de servidor, etc.
Los algoritmos de reciclaje de basura siempre han sido una parte importante del desarrollo de lenguajes de programación, y los algoritmos de reciclaje de basura utilizados en V8 son principalmente los siguientes:
1. Algoritmo de escavia: la gestión del espacio de memoria se realiza mediante la copia, se usa principalmente en el espacio de memoria de la nueva generación;
2. Algoritmo de barrido de marca y algoritmo de marca compacto: la memoria de almacenamiento se clasifica y se recicla mediante el marcado, se utiliza principalmente para la inspección y el reciclaje de objetos de la antigua generación.
PD: Las implementaciones de colección de basura V8 más detalladas se pueden aprender leyendo libros, documentos y código fuente relacionados.
Echemos un vistazo a qué circunstancias el motor JavaScript reciclará qué objetos.
2.1 Alcance y referencia
Los principiantes a menudo creen erróneamente que cuando se ejecuta la función, el objeto declarado dentro de la función será destruido. Pero, de hecho, esta comprensión no es rigurosa y completa, y es fácil confundirse por ella.
La referencia es un mecanismo muy importante en la programación de JavaScript, pero extrañamente, la mayoría de los desarrolladores no le prestarán atención ni lo entenderán. La referencia se refiere a la relación abstracta "Acceso al código a los objetos". Es algo similar a los punteros C/C ++, pero no lo mismo. La referencia también es el mecanismo más crítico del motor JavaScript en la recolección de basura.
El siguiente código es un ejemplo:
La copia del código es la siguiente:
// ......
var val = 'Hola mundo';
función foo () {
Función de retorno () {
devolver val;
};
}
global.bar = foo ();
// ......
Después de leer este código, ¿puede saber qué objetos aún sobreviven después de que esta parte del código se ejecute?
De acuerdo con los principios relevantes, los objetos que no se recicla y se lanzan en este código incluyen Val y Bar (). ¿Qué los hace que no puedan reciclarlos exactamente?
¿Cómo realiza el motor JavaScript la recolección de basura? El algoritmo de recolección de basura mencionado anteriormente solo se usa durante el reciclaje. Entonces, ¿cómo sabe qué objetos se pueden reciclar y qué objetos deben continuar sobreviviendo? La respuesta es una referencia a un objeto JavaScript.
En el código JavaScript, incluso si simplemente escribe un nombre de variable como una sola línea sin hacer nada, el motor JavaScript pensará que este es un comportamiento de acceso al objeto y hay una referencia al objeto. Para garantizar que el comportamiento de recolección de basura no afecte la operación de la lógica del programa, el motor JavaScript no debe reciclar los objetos en uso, de lo contrario será desordenado. Por lo tanto, el estándar para juzgar si un objeto está en uso es si todavía hay una referencia al objeto. Pero, de hecho, este es un compromiso, porque las referencias de JavaScript pueden transferirse, por lo que puede haber algunas referencias que se llevan al alcance global, pero de hecho, ya no es necesario acceder a ellas en la lógica comercial y debe reciclarse, pero el motor JavaScript aún creerá rigidamente que el programa aún lo necesita.
Cómo usar variables y referencias en la postura correcta es la clave para optimizar JavaScript desde el nivel de idioma.
3. Optimice su JavaScript
Finalmente llegué al grano. Muchas gracias por ver esto con paciencia. Después de tantas presentaciones anteriores, creo que tiene una buena comprensión del mecanismo de gestión de memoria de JavaScript. Entonces las siguientes habilidades te harán sentir mejor.
3.1 Hacer un buen uso de las funciones
Si tiene el hábito de leer excelentes proyectos de JavaScript, encontrará que al desarrollar el código JavaScript front-end, muchos grandes a menudo usan una función anónima para envolverlo en la capa más externa del código.
La copia del código es la siguiente:
(función() {
// Código comercial principal
}) ();
Algunos son aún más avanzados:
La copia del código es la siguiente:
; (function (win, doc, $, indefinido) {
// Código comercial principal
}) (ventana, documento, jQuery);
Incluso las soluciones de carga modular front-end tales como requisitos, mares, ozjs, etc., todos adoptan una forma similar:
La copia del código es la siguiente:
// requierejs
Define (['jQuery'], function ($) {
// Código comercial principal
});
// Seajs
Define ('módulo', ['dep', 'subrayar'], function ($, _) {
// Código comercial principal
});
Si dice que muchos códigos de proyectos de código abierto de Node.js no se procesan de esta manera, entonces está equivocado. Antes de ejecutar el código, Node.js envolverá cada archivo .js en el siguiente formulario:
La copia del código es la siguiente:
(función (exporta, requerir, módulo, __Dirname, __filename) {
// Código comercial principal
});
¿Cuáles son los beneficios de hacer esto? Todos sabemos que al comienzo del artículo, dijimos que JavaScript puede haber alcanzado funciones, con declaraciones y alcance global. También sabemos que los objetos definidos en el alcance global pueden sobrevivir hasta que salga el proceso. Si es un objeto grande, será problemático. Por ejemplo, a algunas personas les gusta representar plantillas en JavaScript:
La copia del código es la siguiente:
<? Php
$ db = mysqli_connect (servidor, usuario, contraseña, 'myapp');
$ temas = mysqli_query ($ db, "seleccionar * de temas;");
?>
<! Doctype html>
<html lang = "en">
<Evista>
<meta charset = "utf-8">
<title> ¿Eres un chico divertido invitado por monos? </title>
</ablo>
<Body>
<ul id = "temas"> </ul>
<script type = "text/tmpl" id = "topic-tmpl">
<li>
<h1> <%= título%> </h1>
<p> <%= contenido%> </p>
</li>
</script>
<script type = "text/javaScript">
var data = <? Php echo JSON_ENCODE ($ temas); ?>;
var topictmpl = document.queryselector ('#topic-tmpl'). innerhtml;
var render = function (tmlp, ver) {
var compilado = tmlp
replace (// n/g, '// n')
.replace (/<%= ([/s/s/s]+?)%>/g, función (coincidencia, código) {
return '" + Escape (' + Code + ') +"';
});
compilado = [
'var res = "";',
'con (ver || {}) {',
'res = "' + compilado + '";',
'}',
'RETURN RES;'
] .Join ('/n');
var fn = nueva función ('ver', compilado);
devolver fn (ver);
};
var temas = document.QuerySelector ('#temas');
función init ()
data.ForEach (función (tema) {
topics.innerhtml += render (topictmpl, topic);
});
}
init ();
</script>
</body>
</html>
Este tipo de código a menudo se puede ver en los trabajos de los novatos. ¿Cuáles son los problemas aquí? Si la cantidad de datos obtenidos de la base de datos es muy grande, la variable de datos estará inactiva después de que el front-end complete la representación de la plantilla. Sin embargo, debido a que esta variable se define en el alcance global, el motor JavaScript no lo reciclará y lo destruirá. Esto continuará existiendo en la memoria del montón de la generación anterior hasta que la página esté cerrada.
Pero si hacemos algunas modificaciones muy simples y envolvemos una capa de funciones fuera del código lógico, el efecto será muy diferente. Después de completar la representación de la interfaz de usuario, la referencia del código a los datos también se cancela. Cuando se ejecuta la función más externa, el motor JavaScript comienza a verificar los objetos en ella, y los datos se pueden reciclar.
3.2 Nunca definir variables globales
Acabamos de hablar sobre eso cuando una variable se define en el alcance global, el motor JavaScript no lo reciclará y destruirá por defecto. Esto continuará existiendo en la memoria del montón de la generación anterior hasta que la página esté cerrada.
Entonces siempre hemos seguido un principio: nunca use variables globales. Aunque las variables globales son realmente muy fáciles de desarrollar, los problemas causados por las variables globales son mucho más graves que la conveniencia que aporta.
Hacer que las variables sean menos propensas a ser recicladas;
1. La confusión se causa fácilmente cuando varias personas colaboran;
2. Es fácil interferir en la cadena de alcance.
3. Junto con la función de envoltura anterior, también podemos manejar "variables globales" a través de funciones de envoltura.
3.3 variables de referencia manualmente
Si no se necesita una variable en el código de negocio, entonces la variable se puede desactivar manualmente para que se recicle.
La copia del código es la siguiente:
var data = { / * algunos big data * /};
// bla bla bla
datos = nulo;
3.4 Haga un buen uso de las devoluciones de llamada
Además de usar cierres para el acceso a la variable interna, también podemos usar la función de devolución de llamada ahora muy popular para el procesamiento de negocios.
La copia del código es la siguiente:
función getData (devolución de llamada) {
var data = 'algunos big data';
devolución de llamada (nulo, datos);
}
getData (function (err, data) {
console.log (datos);
Las funciones de devolución de llamada son una tecnología de estilo de aprobación de continuación (CPS). Este estilo de programación transfiere el enfoque comercial de la función desde el valor de retorno a la función de devolución de llamada. Y tiene muchos beneficios sobre los cierres:
1. Si los parámetros pasados son el tipo básico (como cadenas, valores numéricos), los parámetros formales pasados en la función de devolución de llamada se copiarán, y será más fácil reciclar después de utilizar el código comercial;
2. A través de devoluciones de llamada, además de completar solicitudes síncronas, también podemos usarlas en programación asíncrona, que ahora es un estilo de escritura muy popular;
3. La función de devolución de llamada en sí suele ser una función anónima temporal. Una vez que se ejecute la función de solicitud, la referencia a la función de devolución de llamada se cancelará y se reciclará.
3.5 Buena gestión de cierre
Cuando nuestras necesidades comerciales (como vinculación de eventos circulares, atributos privados, devoluciones de llamada con argumentos, etc.) deben usar cierres, tenga cuidado con los detalles.
Se puede decir que los eventos vinculantes de bucle son un curso obligatorio para comenzar con los cierres de JavaScript. Supongamos un escenario: hay seis botones, correspondientes a seis eventos. Cuando el usuario hace clic en el botón, los eventos correspondientes se emiten en el lugar especificado.
La copia del código es la siguiente:
var btns = document.QuerySelectorAll ('. Btn'); // 6 elementos
var sotado = document.querySelector ('#output');
eventos var = [1, 2, 3, 4, 5, 6];
// Caso 1
para (var i = 0; i <btns.length; i ++) {
btns [i] .onclick = function (evt) {
output.inntext + = 'Clicked' + Events [i];
};
}
// Caso 2
para (var i = 0; i <btns.length; i ++) {
BTNS [i] .Onclick = (function (index) {
Función de retorno (EVT) {
output.inntext + = 'Clicked' + Events [index];
};
})(i);
}
// Caso 3
para (var i = 0; i <btns.length; i ++) {
btns [i] .onclick = (function (evento) {
Función de retorno (EVT) {
output.inntext + = 'Clicked' + Event;
};
}) (eventos [i]);
}
La primera solución aquí es obviamente un error de evento de unión de bucle típico. No lo explicaré en detalle aquí. Puede consultar mi respuesta a un internautor en detalle; La diferencia entre la segunda y la tercera soluciones se encuentra en los parámetros pasados en el cierre.
Los parámetros pasados en el segundo esquema son el subíndice de bucle actual, mientras que este último se pasa directamente al objeto de evento correspondiente. De hecho, este último es más adecuado para grandes cantidades de aplicaciones de datos, porque en la programación funcional de JavaScript, los parámetros pasados en las llamadas de funciones son objetos de tipos básicos, por lo que los parámetros formales obtenidos en el cuerpo de la función serán un valor de copia, de modo que este valor se define como una variable local dentro del alcance del cuerpo de función. Una vez que se completa la unión del evento, la variable de eventos se puede desferenciar manualmente para reducir el uso de la memoria en el alcance externo. Además, cuando se elimina un elemento, la función de escucha de eventos correspondiente, el objeto de evento y la función de cierre también se destruyen y recicla.
3.6 La memoria no es un caché
El papel del almacenamiento en caché en el desarrollo empresarial juega un papel importante y puede reducir la carga de los recursos del espacio-tiempo. Pero debe tenerse en cuenta que no debe usar la memoria como caché fácilmente. La memoria es cosa de cada centímetro de tierra para cualquier desarrollo del programa. Si no es un recurso muy importante, no lo ponga directamente en la memoria, ni formule un mecanismo de vencimiento para destruir automáticamente el caché de vencimiento.
4. Verifique el uso de memoria de JavaScript
En el desarrollo diario, también podemos usar algunas herramientas para analizar y solucionar el uso de la memoria en JavaScript.
4.1 navegador Blink/WebKit
En el navegador Blink/WebKit (Chrome, Safari, Opera, etc.), podemos usar la herramienta de perfiles de las herramientas de desarrollador para realizar verificaciones de memoria en nuestros programas.
4.2 Verificación de memoria en Node.js
En Node.js, podemos usar módulos Node-HeapDump y Node-MemWatch para la verificación de memoria.
La copia del código es la siguiente:
var HeapDump = require ('Heapdump');
var fs = require ('fs');
ruta var = requerir ('ruta');
fs.WriteFilesync (Path.Join (__ Dirname, 'App.pid'), Process.pid);
// ...
La copia del código es la siguiente: <span style = "font-family: georgia, 'Times New Roman', 'bitStream Charter', Times, serif; font-size: 14px; line-height: 1.5em;"> Después de introducir el nodo-heapDump en el código de negocios, debemos enviar una señal SIGUSR2 al proceso de nodo.js en un tiempo de nodo y soltar node-heapdump. la memoria del montón. </span>
Copie el código de la siguiente manera: $ Kill -USR2 (CAT App.pid)
De esta manera, habrá un archivo de instantánea nombrado en el formato Heapdump- <ec>. <SEC> .HeapSnapShot en el directorio de archivos. Podemos abrirla utilizando la herramienta Perfiles en las herramientas de desarrollador del navegador y verificarla.
5. Resumen
El artículo volverá pronto. Este intercambio te muestra principalmente el siguiente contenido:
1. JavaScript está estrechamente relacionado con el uso de la memoria a nivel de idioma;
2. Gestión de memoria y mecanismos de reciclaje en JavaScript;
3. Cómo usar la memoria de manera más eficiente para que el JavaScript producido pueda ser más expandido y enérgico;
4. Cómo realizar comprobaciones de memoria al encontrar problemas de memoria.
Espero que al aprender este artículo, pueda producir un mejor código JavaScript para que su madre se sienta a gusto y que su jefe se sienta a gusto.