React: Prinsip Serat: Berkontribusi Pada Serat

Dibuat pada 11 Okt 2016  ·  9Komentar  ·  Sumber: facebook/react

Saya hanya ingin mendokumentasikan beberapa pola desain unik yang berlaku untuk Fiber, tetapi tidak harus yang lainnya. Saya akan mulai di sini.

  • Anda dapat mengubah serat yang sedang Anda kerjakan selama fase beginWork dan completeWork tetapi Anda mungkin tidak memiliki efek samping global lainnya. Jika Anda membutuhkan efek samping global, itu harus dipindahkan ke fase commitWork .
  • Fiber adalah struktur data tetap. Itu berbagi kelas tersembunyi yang sama. Jangan pernah menambahkan bidang di luar konstruksi di ReactFiber .
  • Tidak ada dalam rekonsiliasi yang menggunakan pengiriman dinamis. Yaitu kita tidak memanggil fungsi kelas satu, kecuali untuk kode pengguna seperti panggilan balik ref, komponen fungsional, metode render, dll. Sisanya adalah fungsi statis yang tersedia dalam penutupan. Yaitu menggunakan myHelper(obj) alih-alih obj.myHelper() . Setiap kali kita membutuhkan logika cabang, kita menggunakan pernyataan switch di atas tag yang merupakan angka yang menunjukkan jenis objek yang kita hadapi dan cabang mana yang akan diambil (lihat pencocokan pola).
  • Banyak modul yang dipakai dengan objek HostConfig . Ini adalah konstruktor tunggal yang dipanggil pada waktu inisialisasi. Ini harus inlinable oleh kompiler.
  • Tidak ada di Fiber yang menggunakan tumpukan JS normal. Artinya memang menggunakan tumpukan tetapi dapat dikompilasi menjadi fungsi datar jika diperlukan. Memanggil fungsi lain baik-baik saja - satu-satunya batasan adalah mereka tidak bisa rekursif.
  • Jika saya tidak dapat menggunakan rekursi, bagaimana cara melintasi pohon? Pelajari cara menggunakan algoritma traversal pohon daftar tertaut tunggal. Misal parent dulu, depth dulu:
let root = fiber;
let node = fiber;
while (true) {
  // Do something with node
  if (node.child) {
    node = node.child;
    continue;
  }
  if (node === root) {
    return;
  }
  while (!node.sibling) {
    if (!node.return || node.return === root) {
      return;
    }
    node = node.return;
  }
  node = node.sibling;
}

Kenapa harus serumit ini?

  • Kami dapat menggunakan tumpukan JS normal untuk ini tetapi setiap kali kami menghasilkan requestIdleCallback kami harus membangun kembali tumpukan ketika kami melanjutkan. Karena ini hanya berlangsung sekitar 50 md saat idle, kami akan meluangkan waktu untuk melepas dan membangun kembali tumpukan setiap kali. Hal ini tidak terlalu buruk. Namun, segala sesuatu di sepanjang tumpukan harus menyadari cara "melepas lelah" ketika kita membatalkan di tengah alur kerja.
  • Masuk akal kita bisa melakukan ini pada tingkat efek aljabar OCaml tapi saat ini kita tidak memiliki semua fitur yang kita butuhkan dan kita tidak mendapatkan pengorbanan kinerja yang kita inginkan di luar kotak atm. Ini adalah cara masa depan yang masuk akal ke depan.
  • Sebagian besar kode hidup di luar rekursi ini sehingga tidak terlalu menjadi masalah untuk sebagian besar kasus.
  • Sebagian besar dari apa yang React lakukan adalah dalam ruang dari apa yang dilakukan tumpukan normal. Misalnya memoisasi, penanganan kesalahan, dll. Menggunakan tumpukan normal juga, hanya membuat lebih sulit untuk membuat mereka berinteraksi.
  • Segala sesuatu yang kita taruh di tumpukan biasanya harus kita taruh di tumpukan juga karena kita memoizenya. Mempertahankan tumpukan dan tumpukan dengan data yang sama secara teoritis kurang efisien.
  • Yang mengatakan, semua pengoptimalan ini mungkin diperdebatkan karena tumpukan JS jauh lebih efisien daripada tumpukan JS.
  • Satu hal yang ingin saya coba adalah mengkompilasi komponen React untuk melakukan pekerjaan langsung pada struktur data ini, sama seperti kompilasi bahasa pemrograman normal untuk membuat mutasi dll. ke tumpukan. Saya pikir di situlah implementasi ideal dari React.

Mari kita coba dan lihat bagaimana kelanjutannya. :D

cc @spicyj @gaearon @acdlite

Reconciler React Core Team Discussion

Komentar yang paling membantu

Bukankah Anda hanya mengatasi kurangnya utas dalam bahasa?

Iya dan tidak.

Memang benar bahwa kami tidak memiliki opsi yang baik dalam JavaScript untuk menjalankan utas - dan itu adalah masalah besar. Kami mencoba menjelajahi berbagai opsi berjalan di Web Worker, JS paralel, mencoba mengusulkan struktur data persisten yang tidak dapat diubah bersama ke bahasa, bereksperimen dengan tweak VM khusus, dll. JavaScript bahasa tidak terlalu cocok untuk ini karena runtime bersama yang dapat berubah seperti prototipe . Ekosistem belum siap untuk itu karena Anda harus menduplikasi pemuatan kode dan inisialisasi modul di seluruh pekerja. Pengumpul sampah tidak seefisien saat ini jika mereka harus aman, dan pelaksana VM tampaknya tidak mau menanggung biaya implementasi struktur data persisten. Array yang diketik bersama yang dapat diubah tampaknya terus bergerak, tetapi mengharuskan semua data untuk melewati lapisan ini tampaknya tidak layak di ekosistem saat ini. Batas buatan antara bagian yang berbeda dari basis kode juga tidak bekerja dengan baik dan menimbulkan gesekan yang tidak perlu. Bahkan kemudian Anda memiliki banyak kode JS seperti perpustakaan utilitas yang harus diduplikasi di seluruh pekerja. Yang menyebabkan waktu mulai lebih lambat dan overhead memori. Jadi, ya, utas kemungkinan tidak mungkin sampai kita dapat menargetkan sesuatu seperti Majelis Web.

Namun , realisasi yang menarik adalah bahwa ada manfaat _lainnya_ dari arsitektur Fibre yang dapat diterapkan apakah Anda memiliki utas atau tidak.

ComponentKit , yang berjalan pada native, berfungsi menggunakan utas misalnya. Itu dapat mulai melakukan pekerjaan dengan prioritas lebih tinggi dalam satu utas sementara utas dengan prioritas lebih rendah masih terjadi. Mengarah ke implementasi yang jauh lebih sederhana. Namun, ada beberapa batasan.

Anda tidak dapat membatalkan utas latar belakang dengan aman. Membatalkan dan memulai kembali utas tidak terlalu murah. Dalam banyak bahasa, ini juga tidak aman karena Anda mungkin berada di tengah beberapa pekerjaan inisialisasi yang malas. Meskipun secara efektif terputus, Anda harus terus menghabiskan siklus CPU untuk itu. Salah satu solusinya adalah melakukan hal yang sama seperti yang dilakukan Fiber - buat API yang memiliki poin hasil sehingga Anda dapat melepas tumpukan dengan aman. Kemudian periksa bendera secara berkala jika Anda harus membatalkan. Itu juga yang dilakukan Fiber secara efektif.

Batasan lainnya adalah karena Anda tidak dapat segera membatalkan utas, Anda tidak dapat memastikan apakah dua utas memproses komponen yang sama pada waktu yang sama. Hal ini menyebabkan beberapa batasan seperti tidak dapat mendukung instance kelas stateful (seperti React.Component ). Meskipun itu mungkin hal yang baik untuk alasan lain.

Hal lain yang tidak dapat dibeli oleh utas secara otomatis adalah kemampuan untuk melanjutkan sebagian pekerjaan. Anda tidak bisa hanya membuat memo dari pekerjaan yang telah Anda lakukan di satu utas dan menggunakannya kembali di utas lain. Anda tentu dapat mengimplementasikannya dengan utas tetapi Anda akan berakhir dengan kompleksitas yang sama seperti Fiber - di atas utas.

Utas memiliki sedikit manfaat bahwa Anda dapat memulai pekerjaan berikutnya sedikit lebih awal karena Anda dapat menyisipkan instruksi saat utas latar belakang masih dimatikan. Namun, karena poin hasil kami cukup sering secara default, kami tidak berpikir itu akan menjadi masalah bagi Fiber.

Oleh karena itu, ya, kami menulis implementasi yang lebih kompleks karena kami kekurangan utas dalam JavaScript, tetapi karena kami terpaksa menghadapinya, kami akan mendapatkan fitur yang lebih baik daripada jika kami hanya mengandalkan utas untuk penjadwalan.

Bagaimana dengan paralelisme?

Memang benar bahwa utas dapat membuat Anda sejajar. Namun, ini tidak ada hubungannya dengan penjadwalan untuk responsif. Anda biasanya tidak ingin menghabiskan satu pekerjaan pemrosesan CPU yang telah dibatalkan karena pekerjaan dengan prioritas lebih tinggi telah tiba. Itu tidak membelikanmu apa-apa sama sekali.

Sebaliknya, yang Anda inginkan adalah menghitung pekerjaan independen di utas yang berbeda. Misalnya, dua saudara React dapat dihitung dalam utas paralel karena mereka terputus. Jika kami memiliki akses ke utas, itulah yang akan kami lakukan. Namun, segala sesuatu tentang Fiber masih berlaku untuk tujuan penjadwalan dalam arsitektur itu bahkan jika kita menggunakan utas untuk paralelisme subpohon.

Jadi, kesimpulannya, tidak, kami tidak _hanya_ mengatasi keterbatasan dalam bahasa.

_CATATAN: Ini bukan untuk mengatakan bahwa Anda harus memilih penjadwalan kooperatif daripada utas dalam proyek/kasus penggunaan Anda. Hanya saja untuk kasus penggunaan khusus kami itu masuk akal. Utas masih masuk akal dalam banyak kasus lain._

Semua 9 komentar

Bukankah Anda hanya mengatasi kurangnya utas dalam bahasa?

Iya dan tidak.

Memang benar bahwa kami tidak memiliki opsi yang baik dalam JavaScript untuk menjalankan utas - dan itu adalah masalah besar. Kami mencoba menjelajahi berbagai opsi berjalan di Web Worker, JS paralel, mencoba mengusulkan struktur data persisten yang tidak dapat diubah bersama ke bahasa, bereksperimen dengan tweak VM khusus, dll. JavaScript bahasa tidak terlalu cocok untuk ini karena runtime bersama yang dapat berubah seperti prototipe . Ekosistem belum siap untuk itu karena Anda harus menduplikasi pemuatan kode dan inisialisasi modul di seluruh pekerja. Pengumpul sampah tidak seefisien saat ini jika mereka harus aman, dan pelaksana VM tampaknya tidak mau menanggung biaya implementasi struktur data persisten. Array yang diketik bersama yang dapat diubah tampaknya terus bergerak, tetapi mengharuskan semua data untuk melewati lapisan ini tampaknya tidak layak di ekosistem saat ini. Batas buatan antara bagian yang berbeda dari basis kode juga tidak bekerja dengan baik dan menimbulkan gesekan yang tidak perlu. Bahkan kemudian Anda memiliki banyak kode JS seperti perpustakaan utilitas yang harus diduplikasi di seluruh pekerja. Yang menyebabkan waktu mulai lebih lambat dan overhead memori. Jadi, ya, utas kemungkinan tidak mungkin sampai kita dapat menargetkan sesuatu seperti Majelis Web.

Namun , realisasi yang menarik adalah bahwa ada manfaat _lainnya_ dari arsitektur Fibre yang dapat diterapkan apakah Anda memiliki utas atau tidak.

ComponentKit , yang berjalan pada native, berfungsi menggunakan utas misalnya. Itu dapat mulai melakukan pekerjaan dengan prioritas lebih tinggi dalam satu utas sementara utas dengan prioritas lebih rendah masih terjadi. Mengarah ke implementasi yang jauh lebih sederhana. Namun, ada beberapa batasan.

Anda tidak dapat membatalkan utas latar belakang dengan aman. Membatalkan dan memulai kembali utas tidak terlalu murah. Dalam banyak bahasa, ini juga tidak aman karena Anda mungkin berada di tengah beberapa pekerjaan inisialisasi yang malas. Meskipun secara efektif terputus, Anda harus terus menghabiskan siklus CPU untuk itu. Salah satu solusinya adalah melakukan hal yang sama seperti yang dilakukan Fiber - buat API yang memiliki poin hasil sehingga Anda dapat melepas tumpukan dengan aman. Kemudian periksa bendera secara berkala jika Anda harus membatalkan. Itu juga yang dilakukan Fiber secara efektif.

Batasan lainnya adalah karena Anda tidak dapat segera membatalkan utas, Anda tidak dapat memastikan apakah dua utas memproses komponen yang sama pada waktu yang sama. Hal ini menyebabkan beberapa batasan seperti tidak dapat mendukung instance kelas stateful (seperti React.Component ). Meskipun itu mungkin hal yang baik untuk alasan lain.

Hal lain yang tidak dapat dibeli oleh utas secara otomatis adalah kemampuan untuk melanjutkan sebagian pekerjaan. Anda tidak bisa hanya membuat memo dari pekerjaan yang telah Anda lakukan di satu utas dan menggunakannya kembali di utas lain. Anda tentu dapat mengimplementasikannya dengan utas tetapi Anda akan berakhir dengan kompleksitas yang sama seperti Fiber - di atas utas.

Utas memiliki sedikit manfaat bahwa Anda dapat memulai pekerjaan berikutnya sedikit lebih awal karena Anda dapat menyisipkan instruksi saat utas latar belakang masih dimatikan. Namun, karena poin hasil kami cukup sering secara default, kami tidak berpikir itu akan menjadi masalah bagi Fiber.

Oleh karena itu, ya, kami menulis implementasi yang lebih kompleks karena kami kekurangan utas dalam JavaScript, tetapi karena kami terpaksa menghadapinya, kami akan mendapatkan fitur yang lebih baik daripada jika kami hanya mengandalkan utas untuk penjadwalan.

Bagaimana dengan paralelisme?

Memang benar bahwa utas dapat membuat Anda sejajar. Namun, ini tidak ada hubungannya dengan penjadwalan untuk responsif. Anda biasanya tidak ingin menghabiskan satu pekerjaan pemrosesan CPU yang telah dibatalkan karena pekerjaan dengan prioritas lebih tinggi telah tiba. Itu tidak membelikanmu apa-apa sama sekali.

Sebaliknya, yang Anda inginkan adalah menghitung pekerjaan independen di utas yang berbeda. Misalnya, dua saudara React dapat dihitung dalam utas paralel karena mereka terputus. Jika kami memiliki akses ke utas, itulah yang akan kami lakukan. Namun, segala sesuatu tentang Fiber masih berlaku untuk tujuan penjadwalan dalam arsitektur itu bahkan jika kita menggunakan utas untuk paralelisme subpohon.

Jadi, kesimpulannya, tidak, kami tidak _hanya_ mengatasi keterbatasan dalam bahasa.

_CATATAN: Ini bukan untuk mengatakan bahwa Anda harus memilih penjadwalan kooperatif daripada utas dalam proyek/kasus penggunaan Anda. Hanya saja untuk kasus penggunaan khusus kami itu masuk akal. Utas masih masuk akal dalam banyak kasus lain._

Oke, jadi penjadwalan kooperatif mungkin memiliki beberapa manfaat dibandingkan utas preemptive tapi ...

Tidak bisakah Anda menggunakan fungsi generator seperti yang telah dilakukan oleh kerangka kerja penjadwalan lainnya?

Tidak.

Ada dua alasan untuk ini.

1) Generator tidak hanya membiarkan Anda menghasilkan di tengah tumpukan. Anda harus membungkus setiap fungsi dalam generator. Ini tidak hanya menambahkan banyak overhead sintaksis tetapi juga overhead runtime dalam implementasi yang ada. Wajar jika sintaksnya mungkin lebih membantu daripada tidak, tetapi masalah kinerja masih ada.

2) Alasan terbesar, bagaimanapun, adalah bahwa generator adalah stateful. Anda tidak dapat melanjutkan di tengah-tengahnya.

function* doWork(a, b, c) {
  var x = doExpensiveWorkA(a);
  yield;
  var y = x + doExpensiveWorkB(b);
  yield;
  var z = y + doExpensiveWorkC(c);
  return z;
}

Jika saya ingin menjalankan ini di beberapa irisan waktu, saya bisa melewati ini. Namun, jika saya mendapatkan pembaruan ke B ketika saya telah menyelesaikan doExpensiveWorkA(a) _and_ doExpensiveWorkB(b) tetapi tidak doExpensiveWorkC(c) tidak ada cara bagi saya untuk menggunakan kembali nilai x . Yaitu untuk melompat ke doExpensiveWorkB dengan nilai yang berbeda untuk b tetapi masih menggunakan kembali hasil doExpensiveWorkA(a) .

Ini penting untuk React karena kita melakukan banyak memoisasi.

Masuk akal bahwa Anda dapat menambahkan itu sebagai lapisan di sekitar, tetapi kemudian Anda benar-benar tidak mendapatkan banyak dari penggunaan generator.

Ada juga bahasa yang memiliki generator yang dirancang untuk kasus penggunaan yang lebih fungsional yang memiliki kemampuan ini. JS bukan salah satunya.

Ok, tapi bagaimana dengan efek aljabar OCaml? BuckleScript dapat mengompilasinya.

Ada beberapa overhead non-sepele dalam versi JS tetapi dengan asumsi itu akan baik-baik saja.

Untuk menggunakan efek aljabar di OCaml dengan cara ini, saya pikir kita perlu mengkloning setiap serat sehingga kita dapat melanjutkannya. Ada kemungkinan bahwa kompiler dapat menggunakan kembali bingkai / serat tumpukan yang tidak dapat diubah tanpa menyalin tetapi bukan itu cara kerjanya sekarang. Tanpa itu, kita hanya mendapatkan keuntungan terbatas.

Kami juga akan berakhir dengan banyak penangan bersarang yang akan menambah overhead dan juga menambahkan waktu pencarian linier untuk menemukan penangan teratas ketika kami menghasilkan.

Selain itu, untuk mendukung React API yang ada, kita memerlukan serangkaian fitur yang tumpang tindih dengan implementasi serat, seperti "return pointer" sehingga dengan menggabungkannya kita mendapatkan beberapa keuntungan.

Saya memiliki harapan besar bahwa suatu hari kita hanya dapat menggunakan efek untuk kasus penggunaan kita secara internal, tetapi kita juga dapat menggunakannya untuk membuat React API yang lebih sederhana itu sendiri. Namun, untuk saat ini, saya pikir mungkin lebih mudah untuk memiliki kontrol level yang lebih rendah meskipun kompleksitasnya lebih tinggi.

Saya berharap dapat melihat implementasi lain yang memprioritaskan kesederhanaan implementasi atau menggunakan kompiler kompleks untuk menyelesaikan masalah yang sama.

Hai! Terima kasih telah menulis pemikiran ini! Mereka hebat!

Saya menyadari sudah beberapa minggu sejak Anda menulis ini, tetapi ada satu hal yang tidak saya mengerti. Saya mungkin salah membaca atau melewatkan sesuatu, tetapi di bawah bagian "fungsi generator", Anda mengatakan:

2) Alasan terbesar, bagaimanapun, adalah bahwa generator adalah stateful. Anda tidak dapat melanjutkan di tengah-tengahnya.

function* doWork(a, b, c) {
  var x = doExpensiveWorkA(a);
  yield;
  var y = x + doExpensiveWorkB(b);
  yield;
  var z = y + doExpensiveWorkC(c);
  return z;
}

Jika saya ingin menjalankan ini di beberapa irisan waktu, saya bisa melewati ini. Namun, jika saya mendapatkan pembaruan ke B ketika saya telah menyelesaikan doExpensiveWorkA(a) dan doExpensiveWorkB(b) tetapi tidak doExpensiveWorkC(c) tidak ada cara bagi saya untuk menggunakan kembali nilai x. Yaitu untuk melompat ke doExpensiveWorkB dengan nilai yang berbeda untuk b tetapi masih menggunakan kembali hasil doExpensiveWorkA(a) .
Ini penting untuk React karena kita melakukan banyak memoisasi.

Bukankah memoisasi sebenarnya membuat ini tidak menjadi masalah? Dengan asumsi doExpensiveWorkA adalah fungsi memo, maka memanggilnya lagi dengan nilai yang sama akan cepat, secara efektif memungkinkan Anda untuk "melewati" pekerjaan berat dan melanjutkan ke doExpensiveWorkB .

Saya tidak tahu apa yang diwakili oleh fungsi doExpensiveWork * ini, jadi mungkin tidak mungkin untuk membuat memo dengan cara ini.

Ini benar-benar lebih merupakan pengganti untuk pekerjaan lain dalam generator ini yang mungkin mahal.

Masalah dengan memoisasi doExpensiveWorkA adalah bahwa React tidak memiliki kesempatan untuk menyuntikkan memoisasi di sana. Termasuk strategi memoisasi lokalnya (menggunakan konteks dan kunci induk). Anda bisa menggunakan memoisasi global untuk metode itu tetapi kemudian Anda berakhir dengan masalah pembatalan cache.

Itu masuk akal. Terima kasih telah mengklarifikasi!

Masalah dengan memoisasi doExpensiveWorkA adalah bahwa React tidak memiliki kesempatan untuk menyuntikkan memoisasi di sana. Termasuk strategi memoisasi lokalnya (menggunakan konteks dan kunci induk).

Saya sedikit bingung. Mengapa tidak ada kesempatan? Kita bisa melakukan sesuatu seperti:

const doExpensiveWorkA = lodash.memoize(function doExpensiveWorkA(a) {
    .....
    and can just do local memoization strategy **in the local function**
    ....
})

function* doWork(a, b, c) {
  var x = doExpensiveWorkA(a);
  yield;
  var y = x + doExpensiveWorkB(b);
  yield;
  var z = y + doExpensiveWorkC(c);
  return z;
}

@NE-SmallTown Jika demikian, saya pikir itu akan berhasil juga. Mungkin masalahnya akan terlalu bergantung pada cache global, atau tidak elegan dari struktur serat?

Saya harus mengatakan ini adalah penjelasan yang sangat baik.

Apakah halaman ini membantu?
0 / 5 - 0 peringkat