Nodejs Exploration: Tiefgreifendes Verständnis des Prinzips der Single-Threaded High Concurrency

Nodejs Exploration: Tiefgreifendes Verständnis des Prinzips der Single-Threaded High Concurrency

Vorwort

Seit wir Node.js kennen, besteht unser Wissen darüber aus diesen Schlüsselwörtern: ereignisgesteuert, nicht blockierende E/A, effizient und leichtgewichtig. So beschreibt es sich selbst auf seiner offiziellen Website.
Node.js® ist eine JavaScript-Runtime, die auf der JavaScript-Engine V8 von Chrome basiert. Node.js verwendet ein ereignisgesteuertes, nicht blockierendes E/A-Modell, das es leicht und effizient macht.

Wenn wir also zum ersten Mal mit Nodejs in Kontakt kommen, werden wir einige Fragen haben:

1. Warum kann im Browser ausgeführtes Javascript auf einer so niedrigen Ebene mit dem Betriebssystem interagieren?
2. Ist Node.JS wirklich Single-Threaded?
3. Wenn es sich um ein Single-Thread-Verfahren handelt, wie verarbeitet es eine hohe Anzahl gleichzeitiger Anfragen?
4. Wie wird Node.js ereignisgesteuert implementiert?

Fühlen Sie sich nach dem Lesen dieser Fragen überfordert? Keine Sorge, lesen wir diesen Artikel langsam und behalten dabei diese Fragen im Hinterkopf.

Architektur auf einen Blick

Die obigen Fragen sind alle sehr grundlegend, also beginnen wir mit Node.js selbst und werfen einen Blick auf die Struktur von Node.js.

Die in Javascript geschriebene Standardbibliothek Node.js ist die API, die bei unserer Nutzung direkt aufgerufen werden kann. Sie können es im Lib-Verzeichnis im Quellcode sehen.

Knotenbindungen, diese Schicht ist der Schlüssel zur Kommunikation zwischen Javascript und dem zugrunde liegenden C/C++. Ersteres ruft Letzteres über Bindungen auf, um Daten miteinander auszutauschen. Implementiert in node.cc

Diese Schicht ist der Schlüssel zur Unterstützung des Betriebs von Node.js und ist in C/C++ implementiert.
V8: Die von Google gestartete Javascript-VM ist auch der Grund, warum Node.js Javascript verwendet. Sie bietet eine Umgebung, in der Javascript auf der Nicht-Browser-Seite ausgeführt werden kann. Ihre hohe Effizienz ist einer der Gründe, warum Node.js so effizient ist.
Libuv: Es bietet Node.js plattformübergreifende Funktionen, Thread-Pools, Ereignispools, asynchrone E/A und andere Funktionen und ist der Schlüssel zur Leistungsfähigkeit von Node.js.
C-ares: Bietet die Möglichkeit, DNS-bezogene Funktionen asynchron zu verarbeiten.
http_parser, OpenSSL, zlib usw.: bieten weitere Funktionen, darunter HTTP-Parsing, SSL, Datenkomprimierung usw.

Interaktion mit dem Betriebssystem

Wenn wir beispielsweise eine Datei öffnen und einige Vorgänge ausführen möchten, können wir den folgenden Code schreiben:

var fs = require('fs');fs.open('./test.txt', "w", function(err, fd) { //..etwas tun});

Der Aufrufvorgang dieses Codes kann grob wie folgt beschrieben werden: lib/fs.js → src/node_file.cc → uv_fs

lib/fs.js

asynchrone Funktion öffnen (Pfad, Flags, Modus) { Modus = Modusnummer (Modus, 0o666); Pfad = getPathFromURL (Pfad);
  validatePath(Pfad);
  validateUint32(Modus, 'Modus');
  returniere neuen FileHandle(
    warte auf binding.openFileHandle(pathModule.toNamespacedPath(path),
             stringToFlags(flags), Modus, kUsePromises));
}

src/node_file.cc

statisches void Öffnen (const FunctionCallbackInfo & args) { Umgebung * env = Umgebung::GetCurrent (args); const int argc = args.Length (); wenn (req_wrap_async ! = nullptr) { // öffnen (Pfad, Flags, Modus, req) AsyncCall (env, req_wrap_async, args, "öffnen", UTF8, AfterInteger,
              uv_fs_open, *Pfad, Flags, Modus);
  } sonst { // öffnen(Pfad, Flags, Modus, undefiniert, ctx) CHECK_EQ(argc, 5); FSReqWrapSync req_wrap_sync; FS_SYNC_TRACE_BEGIN(öffnen); int Ergebnis = SyncCall(Umgebung, Argumente[4], &req_wrap_sync, "öffnen",
                          uv_fs_open, *Pfad, Flags, Modus); FS_SYNC_TRACE_END(öffnen);
    args.GetReturnValue().Set(Ergebnis);
  }
}

uv_fs

/* Öffnen Sie die Zieldatei. */
  dstfd = uv_fs_open(NULL, &fs_req,
                     req->neuer_Pfad,
                     Zielflaggen,
                     statsbuf.st_mode, NULL);
  uv_fs_req_cleanup(&fs_req);

Ein Bild von Node.js in einfachen Worten:

Insbesondere wenn wir fs.open aufrufen, ruft Node.js die Open-Funktion auf C/C++-Ebene über process.binding auf und ruft dann darüber die spezifische Methode uv_fs_open in Libuv auf. Schließlich wird das Ausführungsergebnis über einen Rückruf zurückgegeben, um den Vorgang abzuschließen.

Die Methoden, die wir in Javascript aufrufen, werden letztendlich über process.binding an die C/C++-Ebene übergeben und führen dort letztendlich die eigentlichen Operationen aus. So interagiert Node.js mit dem Betriebssystem.

Einzelner Thread

In herkömmlichen Webdienstmodellen wird Multithreading hauptsächlich zur Lösung von Parallelitätsproblemen verwendet. Da E/A blockiert ist, bedeutet ein einzelner Thread, dass Benutzer warten müssen, was offensichtlich unvernünftig ist. Daher werden mehrere Threads erstellt, um auf Benutzeranforderungen zu reagieren.
Node.js-Modell für HTTP-Dienste:

Der einzelne Thread von Node.js bedeutet, dass der Hauptthread ein „einzelner Thread“ ist und der Hauptthread den Programmcode Schritt für Schritt gemäß der Codierungsreihenfolge ausführt. Wenn der synchrone Code blockiert ist und der Hauptthread belegt ist, bleibt die nachfolgende Ausführung des Programmcodes hängen. Üben Sie einen Testcode:

var http = erfordern ('http'); Funktion sleep (Zeit) { var _exit = Date.now() + Zeit * 1000; während (Date.now() < _exit) {} return;
}var server = http.createServer(Funktion(req, res){
    Schlaf (10);
    res.end('Server schläft 10 Sek.');
});

server.listen(8080);

Hier ist ein Stapeldiagramm des Codeblocks:

Ändern Sie zuerst den Code von index.js entsprechend, öffnen Sie dann den Browser und Sie werden feststellen, dass der Browser nach 10 Sekunden antwortet und „Hello Node.js“ eingibt.

JavaScript ist eine interpretierte Sprache. Code wird Zeile für Zeile in den Stapel geschoben, und zwar in der Reihenfolge, in der er codiert und ausgeführt wird. Nach Abschluss der Ausführung wird der Code entfernt und die nächste Codezeile zur Ausführung eingefügt. Im Stapeldiagramm des obigen Codeblocks wird das Programm, wenn der Hauptthread die Anforderung akzeptiert, zur synchronen Ausführung in den Ruheausführungsblock verschoben (wir gehen davon aus, dass dies die Geschäftsverarbeitung des Programms ist). Wenn innerhalb von 10 Sekunden eine zweite Anforderung eingeht, wird sie in den Stapel verschoben und wartet 10 Sekunden, bis sie abgeschlossen ist, bevor die nächste Anforderung weiter verarbeitet wird. Nachfolgende Anforderungen werden angehalten und warten, bis die vorherige synchrone Ausführung abgeschlossen ist, bevor sie ausgeführt werden.

Dann fragen wir uns vielleicht: Warum kann ein einzelner Thread so effizient sein und Zehntausende von Prozessen gleichzeitig verarbeiten, ohne Blockaden zu verursachen? Dies ist das, was wir im Folgenden als ereignisgesteuert bezeichnen.

Ereignisgesteuert/Ereignisschleife

Eine Ereignisschleife ist eine Programmierkonstruktion, die auf Ereignisse oder Nachrichten in einem Programm wartet und diese versendet.

1. Jeder Node.js-Prozess hat nur einen Hauptthread, der Programmcode ausführt und einen Ausführungskontextstapel bildet.
2. Zusätzlich zum Hauptthread wird auch eine „Ereigniswarteschlange“ verwaltet. Wenn eine Netzwerkanforderung oder ein anderer asynchroner Vorgang eines Benutzers eintrifft, wird er vom Knoten in die Ereigniswarteschlange gestellt. Er wird nicht sofort ausgeführt und der Code wird nicht blockiert. Er wird weiter ausgeführt, bis der Code des Hauptthreads ausgeführt wird.
3. Nachdem der Hauptthread-Code ausgeführt wurde, beginnt die Ereignisschleife, dh der Ereignisschleifenmechanismus, das erste Ereignis vom Anfang der Ereigniswarteschlange herauszunehmen, weist einen Thread aus dem Thread-Pool zu, um dieses Ereignis auszuführen, nimmt dann weiterhin das zweite Ereignis heraus und weist einen Thread aus dem Thread-Pool zu, um es auszuführen, dann das dritte und das vierte. Der Hauptthread prüft kontinuierlich, ob sich in der Ereigniswarteschlange nicht ausgeführte Ereignisse befinden, bis alle Ereignisse in der Ereigniswarteschlange ausgeführt wurden. Danach wird der Hauptthread jedes Mal, wenn der Ereigniswarteschlange ein neues Ereignis hinzugefügt wird, benachrichtigt, es der Reihe nach herauszunehmen und zur Verarbeitung an EventLoop zu übergeben. Wenn ein Ereignis ausgeführt wird, wird der Hauptthread benachrichtigt, der Hauptthread führt den Rückruf aus und der Thread wird an den Thread-Pool zurückgegeben.
4. Der Haupt-Thread wiederholt ständig den dritten Schritt oben.

Der einzelne Thread von node.js, den wir sehen, ist nur ein js-Hauptthread. Der asynchrone Vorgang wird im Wesentlichen vom Threadpool ausgeführt. Node übergibt alle blockierenden Vorgänge zur Implementierung an den internen Threadpool. Er ist nur für die kontinuierliche Roundtrip-Planung verantwortlich und führt keine echten E/A-Vorgänge aus, wodurch asynchrone, nicht blockierende E/A realisiert wird. Dies ist die Essenz des einzelnen und ereignisgesteuerten Node-Threads.

Implementierung der Ereignisschleife in Node.js:

Node.js verwendet V8 als Parsing-Engine von js und verwendet seine eigene libuv für die E/A-Verarbeitung. Libuv ist eine ereignisgesteuerte plattformübergreifende Abstraktionsschicht, die einige zugrunde liegende Funktionen verschiedener Betriebssysteme kapselt und der Außenwelt eine einheitliche API bereitstellt. Der Ereignisschleifenmechanismus ist ebenfalls darin implementiert. In src/node.cc:

Umgebung*UmgebungErstellen(DatenIsolieren*DatenIsolieren,
                               Lokaler Kontext, int argc, const char* const* argv, int exec_argc, const char* const* exec_argv) {
  Isolieren* isolieren = Kontext->GetIsolate(); HandleScope handle_scope(isolieren);
  Kontext::Bereich context_scope(Kontext); auto env = neue Umgebung(isolate_data, Kontext,
                             v8_platform.GetTracingAgent());
  env->Start(argc, argv, exec_argc, exec_argv, v8_is_profiling); gibt Umgebung zurück;
}

Dieser Code erstellt eine Knotenausführungsumgebung. In der dritten Zeile sehen Sie uv_default_loop(), eine Funktion in der libuv-Bibliothek. Sie initialisiert die uv-Bibliothek selbst und die darin enthaltene default_loop_struct und gibt einen Zeiger darauf zurück, default_loop_ptr. Danach lädt Node die Ausführungsumgebung, führt einige Setup-Vorgänge durch und startet dann die Ereignisschleife.

{
    SealHandleScope-Siegel (isolier);
    bool mehr;
    env.performance_state()->Markieren(
        Knoten::Leistung::NODE_PERFORMANCE_MILESTONE_LOOP_START);
    Tun {
      uv_run(env.event_loop(), UV_RUN_DEFAULT);

      v8_platform.DrainVMTasks(isolieren);

      mehr = uv_loop_alive(env.event_loop()); wenn (mehr)
        weitermachen;

      RunBeforeExit(&env); // Gib `beforeExit` aus, wenn die Schleife entweder nach der Ausgabe aktiv wurde
      // Ereignis oder nach dem Ausführen einiger Rückrufe.
      mehr = uv_loop_alive(env.event_loop());
    } während (mehr == wahr);
    env.performance_state()->Markieren(
        Knoten::Leistung::NODE_PERFORMANCE_MILESTONE_LOOP_EXIT);
  }

  env.set_trace_sync_io(falsch);

  const int exit_code = EmitExit(&env);
  Führt einen Fehler aus, indem er auf "Ausführen" klickt.

„more“ wird verwendet, um anzugeben, ob mit dem nächsten Zyklus fortgefahren werden soll. env->event_loop() gibt den zuvor in env gespeicherten default_loop_ptr zurück und die Funktion uv_run startet die Ereignisschleife von libuv im angegebenen UV_RUN_DEFAULT-Modus. Wenn keine E/A-Ereignisse und keine Timer-Ereignisse vorhanden sind, gibt uv_loop_alive „false“ zurück.

Ausführungsreihenfolge der Ereignisschleife

Gemäß der offiziellen Einführung von Node.js enthält jede Ereignisschleife 6 Phasen, die der Implementierung im libuv-Quellcode entsprechen, wie in der folgenden Abbildung dargestellt:

  • Timerphase: Diese Phase führt den Rückruf des Timers aus (setTimeout, setInterval).
  • I/O-Callback-Phase: Führen Sie einige Systemaufruffehler aus, z. B. Rückrufe bei Netzwerkkommunikationsfehlern
  • Leerlauf, Vorbereitungsphase: wird nur intern vom Knoten verwendet
  • Polling-Phase: Neue I/O-Ereignisse abrufen. Unter entsprechenden Voraussetzungen wird der Knoten hier blockiert.
  • Prüfphase: Führen Sie den Rückruf von setImmediate() aus.
  • Phase zum Schließen von Rückrufen: Führen Sie den Rückruf zum Schließen des Sockets aus.

Kernfunktion uv_run: Quellcode Kernquellcode

int uv_run(uv_loop_t* loop, uv_run_mode mode) { int timeout; int r; int ran_pending; //Zuerst prüfen, ob unsere Schleife noch aktiv ist //Alive bedeutet, ob sich ein asynchroner Task in der Schleife befindet //Wenn nicht, einfach beenden r = uv__loop_alive(loop); if (!r)
    uv__update_time(loop); //Die legendäre Ereignisschleife, richtig gelesen! Es ist eine große Weile
  while (r != 0 && loop->stop_flag == 0) { //Ereignisphase aktualisieren uv__update_time(loop); //Timer-Rückruf verarbeiten uv__run_timers(loop); //Asynchroner Task-Rückruf verarbeiten ran_pending = uv__run_pending(loop); //Nutzlose Phase uv__run_idle(loop);
    uv__run_prepare(loop); //Das ist hier zu beachten //Von hier bis zum folgenden uv__io_poll ist es sehr schwer zu verstehen //Denken Sie zuerst daran, dass Timeout eine Zeit ist //Nachdem uv_backend_timeout berechnet wurde, wird es an uv__io_poll übergeben
    //Wenn Timeout = 0, überspringt uv__io_poll Timeout = 0 direkt; wenn ((Modus == UV_RUN_ONCE && !ran_pending) || Modus == UV_RUN_DEFAULT)
      Zeitüberschreitung = uv_backend_timeout(Schleife);

    uv__io_poll(loop, timeout); // einfach setImmediate ausführen
    uv__run_check(loop); //Dateideskriptoren und andere Operationen schließen uv__run_closing_handles(loop); if (mode == UV_RUN_ONCE) { /* UV_RUN_ONCE bedeutet Vorwärtsfortschritt: mindestens ein Callback muss
       * aufgerufen wurde, wenn es zurückkehrt. uv__io_poll() kann zurückkehren, ohne
       * I/O (also: keine Callbacks) wenn das Timeout abgelaufen ist - das heißt wir
       * über ausstehende Timer verfügen, die die Vorwärtsfortschrittsbeschränkung erfüllen.
       *
       * UV_RUN_NOWAIT gibt keine Garantien für den Fortschritt und wird daher weggelassen.
       * der Scheck.
       */
      uv__update_time(Schleife);
      uv__run_timers(Schleife);
    }

    r = uv__loop_alive(Schleife); wenn (Modus == UV_RUN_ONCE || Modus == UV_RUN_NOWAIT) abbrechen;
  } /* Die if-Anweisung lässt gcc es in einen bedingten Speicher kompilieren. Vermeidet
   * Verschmutzen einer Cache-Zeile.
   */
  wenn (Schleife->Stopp_Flag != 0) Schleife->Stopp_Flag = 0; returniere r;
}

Ich habe den Code sehr ausführlich geschrieben und glaube, dass ihn diejenigen, die mit C-Code nicht vertraut sind, leicht verstehen können. Ja, die Ereignisschleife ist nur eine große Weile! Damit ist der Schleier des Geheimnisses gelüftet.

uv__io_poll-Phase

Diese Phase ist sehr clever gestaltet. Der zweite Parameter dieser Funktion ist ein Timeout-Parameter, und dieses Timeout stammt von der Funktion uv_backend_timeout. Schauen wir uns das mal an!

Quellcode

int uv_backend_timeout(const uv_loop_t* loop) { wenn (loop->stop_flag != 0) gibt 0 zurück; wenn (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop)) gibt 0 zurück; wenn (!QUEUE_EMPTY(&loop->idle_handles)) gibt 0 zurück; wenn (!QUEUE_EMPTY(&loop->pending_queue)) gibt 0 zurück; wenn (loop->closing_handles) gibt 0 zurück; gibt uv__next_timeout(loop) zurück;
}

Es stellt sich heraus, dass es sich um eine mehrstufige if-Funktion handelt. Lassen Sie uns sie Schritt für Schritt analysieren.

1. stop_flag: Wenn dieses Flag 0 ist, bedeutet dies, dass die Ereignisschleife nach dem Ausführen dieser Runde beendet wird und die Rückgabezeit 0 ist

2. !uv__has_active_handles und !uv__has_active_reqs: Wie die Namen andeuten, muss die Timeout-Zeit 0 sein, wenn keine asynchronen Aufgaben (einschließlich Timer und asynchroner E/A) vorhanden sind.

3. QUEUE_EMPTY(idle_handles) und QUEUE_EMPTY(pending_queue): Asynchrone Aufgaben werden in pending_queue registriert. Unabhängig davon, ob sie erfolgreich waren oder nicht, wurden sie registriert. Wenn nichts vorhanden ist, sind diese beiden Warteschlangen leer, sodass kein Warten erforderlich ist.

4. closing_handles: Unsere Schleife ist in die Abschlussphase eingetreten, kein Grund zu warten

Alle oben genannten Bedingungen werden beurteilt und beurteilt, nur um auf diesen Satz zu warten, der uv__next_timeout (Schleife) zurückgibt. Dieser Satz sagt uv__io_poll: Wie lange wirst du anhalten? Als nächstes sehen wir weiter, wie dieses magische uv__next_timeout die Zeit erhält.

int uv__next_timeout(const uv_loop_t* Schleife) { const Struktur heap_node* heap_node; const uv_timer_t* Handle;
  uint64_t Unterschied;

  heap_node = heap_min((const struct heap*) &loop->timer_heap); if (heap_node == NULL) return -1; /* auf unbestimmte Zeit blockieren */

  handle = container_of(heap_node, uv_timer_t, heap_node); if (handle->timeout time) return 0; //Dieser Code liefert die Schlüsselanleitung diff = handle->timeout - loop->time; //Darf nicht größer sein als das Maximum INT_MAX
  wenn (diff > INT_MAX)
    diff = INT_MAX; gibt diff zurück;
}

Nach Ablauf der Wartezeit wird die Prüfphase eingeleitet. Anschließend wird die Phase „closing_handles“ eingeleitet und eine Ereignisschleife wird beendet. Da es sich um eine Quellcodeanalyse handelt, werde ich nicht ins Detail gehen. Sie können nur die offizielle Dokumentation lesen.

Zusammenfassen

1. Nodejs interagiert mit dem Betriebssystem. Die Methoden, die wir in Javascript aufrufen, werden schließlich über process.binding an die C/C++-Ebene übergeben und führen schließlich die eigentlichen Operationen aus. So interagiert Node.js mit dem Betriebssystem.

2. Das sogenannte Single-Threaded-NodeJS ist nur der Haupt-Thread. Alle Netzwerkanforderungen oder asynchronen Aufgaben werden zur Implementierung an den internen Thread-Pool übergeben. Es ist nur für die kontinuierliche Roundtrip-Planung verantwortlich, und die Ereignisschleife steuert kontinuierlich die Ereignisausführung.

3. Der Grund, warum Nodejs eine hohe Parallelität mit einem einzelnen Thread verarbeiten kann, liegt am Ereignisschleifenmechanismus der Libuv-Schicht und der zugrunde liegenden Thread-Pool-Implementierung.

4. Die Ereignisschleife ist der Hauptthread, der kontinuierlich Ereignisse aus der Ereigniswarteschlange des Hauptthreads liest und die Ausführung aller asynchronen Rückruffunktionen steuert. Die Ereignisschleife hat insgesamt 7 Stufen, jede Stufe hat eine Aufgabenwarteschlange. Wenn alle Stufen einmal nacheinander ausgeführt werden, schließt die Ereignisschleife einen Tick ab.

Das Obige ist der detaillierte Inhalt der Erkundung von Nodejs, um ein tiefgreifendes Verständnis des Prinzips der Single-Threaded-Hochparallelität zu erlangen. Weitere Informationen zu Nodejs finden Sie in den anderen verwandten Artikeln auf 123WORDPRESS.COM!

Das könnte Sie auch interessieren:
  • Fehlerbehebung bei hohem Speicherverbrauch von NodeJs, tatsächlicher Kampfrekord
  • Detaillierte Erläuterung der Verwendung des in Nodejs integrierten Verschlüsselungsmoduls zur Peer-to-Peer-Verschlüsselung und -Entschlüsselung
  • Detaillierte Erklärung asynchroner Iteratoren in nodejs
  • Detaillierte Erklärung der in Node.js integrierten Module
  • Quellcodeanalyse des Nodejs-Modulsystems
  • Eine kurze Diskussion über ereignisgesteuerte Entwicklung in JS und Nodejs
  • So verwenden Sie das Modul-FS-Dateisystem in Nodejs
  • Zusammenfassung einiger Tipps zum Umgehen der Node.js-Codeausführung
  • Nodejs-Fehlerbehandlungsprozessaufzeichnung
  • So schreiben Sie mit nodejs ein Tool zur Generierung von Entitätsklassen für Datentabellen für C#

<<:  Mysql-Trigger werden in PHP-Projekten zum Sichern, Wiederherstellen und Löschen von Informationen verwendet

>>:  CentOS 7: Erläuterung zum Wechseln des Boot-Kernels und des Boot-Modus

Artikel empfehlen

MySQL-Komplettabsturz: Detaillierte Erklärung der Abfragefilterbedingungen

Überblick In tatsächlichen Geschäftsszenarioanwen...

Umfassende Erklärung zu dynamischem SQL von MyBatis

Inhaltsverzeichnis Vorwort Dynamisches SQL 1. Sch...

Die Hauptidee zum dynamischen Festlegen von Routing-Berechtigungen in Vue

Ich habe zuvor einige dynamische Routing-Einstell...

Eine kurze Analyse der Verwendung von Rahmen- und Anzeigeattributen in CSS

Einführung in Rahmeneigenschaften border -Eigensc...

Zusammenfassung gängiger Befehle für Ubuntu-Server

Die meisten der folgenden Befehle müssen in der K...

Fallbeispiel zur TypeScript-Schnittstellendefinition

Die Rolle der Schnittstelle: Schnittstelle, auf E...

Detaillierte Erklärung zur Verwendung von Teleport in Vue3

Inhaltsverzeichnis Zweck des Teleports So funktio...

Super ausführliches Tutorial zur Installation und Konfiguration von MySQL8.0.22

Hallo zusammen, heute lernen wir die Installation...

Implementierung der Webpack-Codefragmentierung

Inhaltsverzeichnis Hintergrund CommonsChunkPlugin...

MySQL-Batch löschen großer Datenmengen

MySQL-Batch löschen großer Datenmengen Angenommen...

Konfigurieren von MySQL und Squel Pro auf dem Mac

Als Reaktion auf die Popularität von nodejs haben...

Detaillierte Erklärung zur Verwendung von JavaScript-Funktionen

Inhaltsverzeichnis 1. Deklarieren Sie eine Funkti...

JS realisiert die automatische Wiedergabe der Timeline

Vor kurzem habe ich einen solchen Effekt implemen...