Ember.js: #each merender ulang bug

Dibuat pada 14 Sep 2018  ·  8Komentar  ·  Sumber: emberjs/ember.js

Saya memiliki array nilai true / false / undefined yang saya render sebagai daftar kotak centang.
Saat mengubah elemen array ke atau dari true, daftar kotak centang dirender ulang dengan kotak centang berikut (indeks + 1) yang mewarisi perubahan bersama dengan kotak centang yang diubah.
Kode:

{{#each range as |value idx|}}
  <label><input type="checkbox" checked={{value}} {{action makeChange idx on="change"}}>{{idx}}: {{value}}</label><br/>
{{/each}}

Ketika saya menggunakan {{#each range key="@index" as |value idx|}} itu bekerja dengan benar.

Twiddle: https://ember-twiddle.com/6d63548f35f99da19cee9f58fb64db59

embereach

Bug Has Reproduction Rendering

Komentar yang paling membantu

Saya rasa saya tahu apa yang terjadi di sini. Ini berantakan tapi saya akan mencoba menjelaskannya. Banyak kasus tepi (kesalahan pengguna garis batas) berkontribusi pada hal ini, dan saya tidak begitu yakin apa itu / bukan bug, apa dan bagaimana cara memperbaikinya.

Mayor 🔑

Pertama-tama, saya perlu menjelaskan apa yang dilakukan parameter key dalam {{#each}} . TL; DR mencoba menentukan kapan dan apakah masuk akal untuk menggunakan kembali DOM yang ada, vs hanya membuat DOM dari awal.

Untuk tujuan kita, mari kita terima karena "DOM menyentuh" ​​(misalnya memperbarui konten node teks, atribut, menambah atau menghapus konten, dll) mahal dan harus dihindari sebanyak mungkin.

Mari fokus pada bagian template yang agak sederhana:

<ul>
  {{#each this.names as |name|}}
    <li>{{name.first}} {{to-upper-case name.last}}</li>
  {{/each}}
</ul>

Jika this.names adalah ...

[
  { first: "Yehuda", last: "Katz" },
  { first: "Tom", last: "Dale" },
  { first: "Godfrey", last: "Chan" }
]

Maka Anda akan mendapatkan ...

<ul>
  <li>Yehuda KATZ</li>
  <li>Tom DALE</li>
  <li>Godfrey CHAN</li>
</ul>

Sejauh ini bagus.

Menambahkan item ke daftar

Sekarang bagaimana jika kita menambahkan { first: "Andrew", last: "Timberlake" } ke daftar? Kami mengharapkan template untuk menghasilkan DOM berikut:

<ul>
  <li>Yehuda KATZ</li>
  <li>Tom DALE</li>
  <li>Godfrey CHAN</li>
  <li>Andrew TIMBERLAKE</li>
</ul>

Tapi bagaimana caranya_?

Cara paling naif untuk mengimplementasikan {{#each}} helper adalah menghapus semua konten daftar setiap kali konten daftar berubah. Untuk melakukan ini, Anda perlu melakukan _ setidaknya_ 23 operasi:

  • Hapus 3 <li> node
  • Masukkan 4 <li> node
  • Masukkan 12 node teks (satu untuk nama depan, satu untuk spasi di antara dan satu untuk nama belakang, dikali 4 baris)
  • Panggil pembantu to-upper-case 4 kali

Ini sepertinya ... sangat tidak perlu dan mahal. Kita _ tahu_ tiga item pertama tidak berubah, jadi alangkah baiknya jika kita bisa melewatkan pekerjaan untuk baris tersebut.

🔑 @index

Implementasi yang lebih baik adalah mencoba menggunakan kembali baris yang ada dan tidak melakukan pembaruan yang tidak perlu. Satu idenya adalah dengan mencocokkan baris dengan posisinya di templat. Pada dasarnya inilah yang dilakukan key="@index" :

  1. Bandingkan objek pertama { first: "Yehuda", last: "Katz" } dengan baris pertama, <li>Yehuda KATZ</li> :
    1.1. "Yehuda" === "Yehuda", tidak ada yang bisa dilakukan
    1.2. (spasi tidak berisi data dinamis jadi tidak perlu perbandingan)
    1.3. "Katz" === "Katz", karena pembantu itu "murni", kita tahu kita tidak perlu memanggil kembali to-upper-case pembantu, dan oleh karena itu kita tahu keluaran dari pembantu itu ("KATZ" ) _also_ tidak berubah, jadi tidak ada yang bisa dilakukan di sini
  2. Demikian pula, tidak ada yang bisa dilakukan untuk baris 2 dan 3
  3. Tidak ada baris keempat di DOM, jadi masukkan yang baru
    3.1. Masukkan simpul <li>
    3.2. Masukkan simpul teks ("Andrew")
    3.3. Masukkan node teks (spasi)
    3.4. Panggil pembantu to-upper-case ("Timberlake" -> "TIMBERLAKE")
    3.5. Masukkan node teks ("TIMBERLAKE")

Jadi, dengan penerapan ini, kami mengurangi jumlah total operasi dari 23 menjadi 5 (👋 mengabaikan biaya perbandingan, tetapi untuk tujuan kami, kami mengasumsikan mereka relatif murah dibandingkan dengan yang lain). Tidak buruk.

Mempersiapkan item ke daftar

Tapi sekarang, apa yang akan terjadi jika, alih-alih _appending_ { first: "Andrew", last: "Timberlake" } ke daftar, kita _menambahkan_ sebagai gantinya? Kami mengharapkan template untuk menghasilkan DOM berikut:

<ul>
  <li>Andrew TIMBERLAKE</li>
  <li>Yehuda KATZ</li>
  <li>Tom DALE</li>
  <li>Godfrey CHAN</li>
</ul>

Tapi bagaimana caranya_?

  1. Bandingkan objek pertama { first: "Andrew", last: "Timberlake" } dengan baris pertama, <li>Yehuda KATZ</li> :
    1.1. "Andrew"! == "Yehuda", perbarui simpul teks
    1.2. (spasi tidak berisi data dinamis jadi tidak perlu perbandingan)
    1.3. "Timberlake"! == "Katz", cabut kembali pembantu to-upper-case
    1.4. Perbarui node teks dari "KATZ" menjadi "TIMBERLAKE"
  2. Bandingkan objek kedua { first: "Yehuda", last: "Katz" } dengan baris kedua, <li>Tom DALE</li> , 3 operasi lainnya
  3. Bandingkan objek kedua { first: "Tom", last: "Dale" } dengan baris kedua, <li>Godfrey CHAN</li> , 3 operasi lainnya
  4. Tidak ada baris keempat di DOM, jadi masukkan yang baru
    3.1. Masukkan simpul <li>
    3.2. Masukkan simpul teks ("Godfrey")
    3.3. Masukkan node teks (spasi)
    3.4. Panggil pembantu to-upper-case ("Chan" -> "CHAN")
    3.5. Masukkan node teks ("CHAN")

Itu 14 operasi. Aduh!

🔑 @ identitas

Tampaknya tidak perlu, karena secara konseptual, apakah kita melakukan prepending atau appending, kita masih hanya mengubah (menyisipkan) satu objek dalam array. Secara optimal, kami harus dapat menangani kasus ini seperti yang kami lakukan dalam skenario append.

Di sinilah key="@identity" masuk. Daripada mengandalkan _order_ elemen dalam larik, kami menggunakan identitas objek JavaScript mereka ( === ):

  1. Temukan baris yang sudah ada yang datanya cocok ( === ) objek pertama { first: "Andrew", last: "Timberlake" } . Karena tidak ada yang ditemukan, sisipkan (awali) baris baru:
    1.1. Masukkan node <li>
    1.2. Masukkan simpul teks ("Andrew")
    1.3. Masukkan node teks (spasi)
    1.4. Panggil pembantu to-upper-case ("Timberlake" -> "TIMBERLAKE")
    1.5. Masukkan node teks ("TIMBERLAKE")
  2. Temukan baris yang sudah ada yang datanya cocok ( === ) objek kedua { first: "Yehuda", last: "Katz" } . Menemukan <li>Yehuda KATZ</li> :
    2.1. "Yehuda" === "Yehuda", tidak ada yang bisa dilakukan
    2.2. (spasi tidak berisi data dinamis jadi tidak perlu perbandingan)
    2.3. "Katz" === "Katz", karena pembantu itu "murni", kita tahu kita tidak perlu memanggil kembali to-upper-case pembantu, dan oleh karena itu kita tahu keluaran dari pembantu itu ("KATZ" ) _also_ tidak berubah, jadi tidak ada yang bisa dilakukan di sini
  3. Demikian pula, tidak ada hubungannya dengan pertengkaran Tom dan Godfrey
  4. Hapus semua baris dengan objek yang tidak cocok (tidak ada, jadi tidak ada yang bisa dilakukan dalam kasus ini)

Dengan itu, kami kembali ke 5 operasi optimal.

Scaling Up

Sekali lagi ini adalah mengabaikan perbandingan dan biaya pembukuan. Memang, itu juga tidak gratis, dan dalam contoh yang sangat sederhana ini, mungkin tidak sepadan. Tapi bayangkan daftarnya besar dan setiap baris memanggil komponen yang rumit (dengan banyak pembantu, properti yang dihitung, sub-komponen, dll). Bayangkan feed berita LinkedIn, misalnya. Jika kami tidak mencocokkan baris yang tepat dengan data yang benar, argumen komponen Anda berpotensi banyak berubah dan menyebabkan lebih banyak pembaruan DOM daripada yang Anda perkirakan. Ada juga masalah dengan pencocokan elemen DOM yang salah dan kehilangan status DOM, seperti posisi kursor dan status pemilihan teks.

Secara keseluruhan, perbandingan ekstra dan biaya pembukuan dengan mudah bernilai sebagian besar waktu di aplikasi dunia nyata. Karena key="@identity" adalah default di Ember dan berfungsi dengan baik untuk hampir semua kasus, Anda biasanya tidak perlu khawatir tentang menyetel argumen key saat menggunakan {{#each}} .

Tabrakan 💥

Tapi tunggu, ada masalah. Bagaimana dengan kasus ini?

const YEHUDA = { first: "Yehuda", last: "Katz" };
const TOM = { first: "Tom", last: "Dale" };
const GODFREY = { first: "Godfrey", last: "Chan" };

this.list = [
  YEHUDA,
  TOM,
  GODFREY,
  TOM, // duplicate
  YEHUDA, // duplicate
  YEHUDA, // duplicate
  YEHUDA // duplicate
];

Masalahnya di sini adalah bahwa objek yang sama _bisa_ muncul beberapa kali dalam daftar yang sama. Ini merusak algoritme naif @identity , khususnya bagian di mana kami mengatakan "Temukan baris yang ada yang datanya cocok ( === ) ..." - ini hanya berfungsi jika data ke hubungan DOM adalah 1 : 1, yang tidak benar dalam kasus ini. Ini mungkin tampak tidak mungkin dalam praktiknya, tetapi sebagai kerangka kerja, kami harus menanganinya.

Untuk menghindari ini, kami menggunakan semacam pendekatan hybrid untuk menangani tabrakan ini. Secara internal, pemetaan kunci-ke-DOM terlihat seperti ini:

"YEHUDA" => <li>Yehuda...</li>
"TOM" => <li>Tom...</li>
"GODFREY" => <li>Godfrey...</li>
"TOM-1" => <li>Tom...</li>
"YEHUDA-1" => <li>Yehuda...</li>
"YEHUDA-2" => <li>Yehuda...</li>
"YEHUDA-3" => <li>Yehuda...</li>

Untuk sebagian besar, ini _is_ cukup langka, dan jika muncul, ini berfungsi dengan Baik ™ hampir sepanjang waktu. Jika, karena alasan tertentu, ini tidak berhasil, Anda selalu dapat menggunakan jalur kunci (atau mekanisme penguncian yang lebih canggih di RFC 321 ).

Kembali ke "🐛"

Setelah semua pembicaraan itu, kita sekarang siap untuk melihat skenario di Twiddle.

Pada dasarnya, kami mulai dengan daftar ini: [undefined, undefined, undefined, undefined, undefined] .

Catatan tidak terkait: Array(5) adalah _not_ sama dengan [undefined, undefined, undefined, undefined, undefined] . Ini menghasilkan "array berlubang" yang merupakan sesuatu yang harus Anda hindari secara umum. Namun, ini tidak terkait dengan bug ini, karena ketika mengakses "lubang" Anda memang mendapatkan kembali undefined . Jadi untuk tujuan _sangat sempit_ kita saja, keduanya sama.

Karena kami tidak menentukan kuncinya, Ember menggunakan @identity secara default. Selanjutnya, karena ini adalah tabrakan, kami berakhir dengan sesuatu seperti ini:

"undefined" => <input ...> 0: ...,
"undefined-1" => <input ...> 1: ...,
"undefined-2" => <input ...> 2: ...,
"undefined-3" => <input ...> 3: ...,
"undefined-4" => <input ...> 4: ...

Sekarang, katakanlah kita mengklik kotak centang pertama:

  1. Ini memicu perilaku default dari kotak pilih: mengubah status yang dicentang menjadi benar
  2. Ini memicu peristiwa klik, yang dicegat oleh pengubah {{action}} dan dikirim ulang ke metode makeChange
  3. Ini mengubah daftar menjadi [true, undefined, undefined, undefined, undefined] .
  4. Itu memperbarui DOM.

Bagaimana DOM diperbarui?

  1. Temukan baris yang ada yang datanya cocok dengan ( === ) objek pertama true . Karena tidak ada yang ditemukan, sisipkan (awali) baris baru <input checked=true ...>0: true...
  2. Temukan baris yang sudah ada yang datanya cocok dengan ( === ) objek kedua undefined . Menemukan <input ...>0: ... (sebelumnya baris PERTAMA):
    2.1. Perbarui simpul teks {{idx}} menjadi 1
    2.2. Jika tidak, sejauh yang bisa dikatakan Ember, tidak ada lagi yang berubah di baris ini, tidak ada yang bisa dilakukan
  3. Temukan baris yang sudah ada yang datanya cocok dengan ( === ) objek ketiga undefined . Karena ini adalah kedua kalinya kami melihat undefined , kunci internal adalah undefined-1 , jadi kami menemukan <input ...>1: ... (sebelumnya baris KEDUA):
    3.1. Perbarui simpul teks {{idx}} menjadi 2
    3.2. Jika tidak, sejauh yang bisa dikatakan Ember, tidak ada lagi yang berubah di baris ini, tidak ada yang bisa dilakukan
  4. Demikian pula, perbarui undefined-2 dan undefined-3
  5. Terakhir, hapus baris undefined-4 (karena ada satu baris undefined dalam larik setelah pembaruan)

Jadi ini menjelaskan bagaimana kami mendapatkan hasil yang Anda miliki di twiddle. Pada dasarnya semua baris DOM bergeser ke bawah satu, dan yang baru disisipkan di atas, sedangkan {{idx}} diperbarui untuk sisanya.

Bagian yang sangat tidak terduga adalah 2.2. Meskipun kotak centang pertama (yang diklik) digeser satu baris ke posisi kedua, Anda mungkin mengira Ember ke properti checked telah berubah menjadi true , dan karena nilai terikatnya tidak ditentukan, Anda mungkin berharap Ember mengubahnya kembali ke false , sehingga tidak mencentangnya.

Tapi ini bukan cara kerjanya. Seperti disebutkan di awal, mengakses DOM itu mahal. Ini termasuk _reading_ dari DOM. Jika, pada setiap pembaruan, kami harus membaca nilai terbaru dari DOM untuk perbandingan kami, itu akan sangat menggagalkan tujuan pengoptimalan kami. Oleh karena itu, untuk menghindarinya, kami mengingat nilai terakhir yang telah kami tulis ke DOM, dan membandingkan nilai saat ini dengan nilai yang di-cache tanpa harus membacanya kembali dari DOM. Hanya jika ada perbedaan kita menulis nilai baru ke DOM (dan menyimpannya dalam cache untuk waktu berikutnya). Ini adalah pengertian di mana kami berbagi pendekatan "DOM virtual" yang sama, tetapi kami hanya melakukannya di simpul daun, bukan memvirtualisasikan "struktur pohon" dari seluruh DOM.

Jadi, TL; DR, "mengikat" properti checked (atau properti value dari bidang teks, dll) tidak benar-benar berfungsi seperti yang Anda harapkan. Bayangkan jika Anda merender <div>{{this.name}}</div> dan secara manual memperbarui textContent dari elemen div menggunakan jQuery atau dengan chrome inspector. Anda tidak akan mengharapkan Ember memperhatikan itu dan memperbarui this.name untuk Anda. Ini pada dasarnya adalah hal yang sama: karena pembaruan ke properti checked terjadi di luar Ember (melalui perilaku default browser untuk kotak centang), Ember tidak akan tahu tentang itu.

Inilah mengapa pembantu {{input}} ada. Ia harus mendaftarkan event listener yang relevan pada elemen HTML yang mendasarinya dan mencerminkan operasi ke dalam perubahan properti yang sesuai, sehingga pihak yang berkepentingan (misalnya lapisan rendering) dapat diberitahukan.

Saya tidak yakin di mana itu meninggalkan kita. Saya mengerti mengapa ini mengejutkan, tetapi saya cenderung mengatakan ini adalah serangkaian kesalahan pengguna yang tidak menguntungkan. Mungkin kita harus melawan pengikatan properti ini pada elemen masukan?

Semua 8 komentar

@andrewtimberlake sepertinya menggunakan {{#each range key="@index" as |value idx|}} dapat mengatasi masalah ini.

Tapi sepertinya bug, key adalah untuk tujuan yang berbeda, https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/if?anchor= setiap

Saya rasa saya tahu apa yang terjadi di sini. Ini berantakan tapi saya akan mencoba menjelaskannya. Banyak kasus tepi (kesalahan pengguna garis batas) berkontribusi pada hal ini, dan saya tidak begitu yakin apa itu / bukan bug, apa dan bagaimana cara memperbaikinya.

Mayor 🔑

Pertama-tama, saya perlu menjelaskan apa yang dilakukan parameter key dalam {{#each}} . TL; DR mencoba menentukan kapan dan apakah masuk akal untuk menggunakan kembali DOM yang ada, vs hanya membuat DOM dari awal.

Untuk tujuan kita, mari kita terima karena "DOM menyentuh" ​​(misalnya memperbarui konten node teks, atribut, menambah atau menghapus konten, dll) mahal dan harus dihindari sebanyak mungkin.

Mari fokus pada bagian template yang agak sederhana:

<ul>
  {{#each this.names as |name|}}
    <li>{{name.first}} {{to-upper-case name.last}}</li>
  {{/each}}
</ul>

Jika this.names adalah ...

[
  { first: "Yehuda", last: "Katz" },
  { first: "Tom", last: "Dale" },
  { first: "Godfrey", last: "Chan" }
]

Maka Anda akan mendapatkan ...

<ul>
  <li>Yehuda KATZ</li>
  <li>Tom DALE</li>
  <li>Godfrey CHAN</li>
</ul>

Sejauh ini bagus.

Menambahkan item ke daftar

Sekarang bagaimana jika kita menambahkan { first: "Andrew", last: "Timberlake" } ke daftar? Kami mengharapkan template untuk menghasilkan DOM berikut:

<ul>
  <li>Yehuda KATZ</li>
  <li>Tom DALE</li>
  <li>Godfrey CHAN</li>
  <li>Andrew TIMBERLAKE</li>
</ul>

Tapi bagaimana caranya_?

Cara paling naif untuk mengimplementasikan {{#each}} helper adalah menghapus semua konten daftar setiap kali konten daftar berubah. Untuk melakukan ini, Anda perlu melakukan _ setidaknya_ 23 operasi:

  • Hapus 3 <li> node
  • Masukkan 4 <li> node
  • Masukkan 12 node teks (satu untuk nama depan, satu untuk spasi di antara dan satu untuk nama belakang, dikali 4 baris)
  • Panggil pembantu to-upper-case 4 kali

Ini sepertinya ... sangat tidak perlu dan mahal. Kita _ tahu_ tiga item pertama tidak berubah, jadi alangkah baiknya jika kita bisa melewatkan pekerjaan untuk baris tersebut.

🔑 @index

Implementasi yang lebih baik adalah mencoba menggunakan kembali baris yang ada dan tidak melakukan pembaruan yang tidak perlu. Satu idenya adalah dengan mencocokkan baris dengan posisinya di templat. Pada dasarnya inilah yang dilakukan key="@index" :

  1. Bandingkan objek pertama { first: "Yehuda", last: "Katz" } dengan baris pertama, <li>Yehuda KATZ</li> :
    1.1. "Yehuda" === "Yehuda", tidak ada yang bisa dilakukan
    1.2. (spasi tidak berisi data dinamis jadi tidak perlu perbandingan)
    1.3. "Katz" === "Katz", karena pembantu itu "murni", kita tahu kita tidak perlu memanggil kembali to-upper-case pembantu, dan oleh karena itu kita tahu keluaran dari pembantu itu ("KATZ" ) _also_ tidak berubah, jadi tidak ada yang bisa dilakukan di sini
  2. Demikian pula, tidak ada yang bisa dilakukan untuk baris 2 dan 3
  3. Tidak ada baris keempat di DOM, jadi masukkan yang baru
    3.1. Masukkan simpul <li>
    3.2. Masukkan simpul teks ("Andrew")
    3.3. Masukkan node teks (spasi)
    3.4. Panggil pembantu to-upper-case ("Timberlake" -> "TIMBERLAKE")
    3.5. Masukkan node teks ("TIMBERLAKE")

Jadi, dengan penerapan ini, kami mengurangi jumlah total operasi dari 23 menjadi 5 (👋 mengabaikan biaya perbandingan, tetapi untuk tujuan kami, kami mengasumsikan mereka relatif murah dibandingkan dengan yang lain). Tidak buruk.

Mempersiapkan item ke daftar

Tapi sekarang, apa yang akan terjadi jika, alih-alih _appending_ { first: "Andrew", last: "Timberlake" } ke daftar, kita _menambahkan_ sebagai gantinya? Kami mengharapkan template untuk menghasilkan DOM berikut:

<ul>
  <li>Andrew TIMBERLAKE</li>
  <li>Yehuda KATZ</li>
  <li>Tom DALE</li>
  <li>Godfrey CHAN</li>
</ul>

Tapi bagaimana caranya_?

  1. Bandingkan objek pertama { first: "Andrew", last: "Timberlake" } dengan baris pertama, <li>Yehuda KATZ</li> :
    1.1. "Andrew"! == "Yehuda", perbarui simpul teks
    1.2. (spasi tidak berisi data dinamis jadi tidak perlu perbandingan)
    1.3. "Timberlake"! == "Katz", cabut kembali pembantu to-upper-case
    1.4. Perbarui node teks dari "KATZ" menjadi "TIMBERLAKE"
  2. Bandingkan objek kedua { first: "Yehuda", last: "Katz" } dengan baris kedua, <li>Tom DALE</li> , 3 operasi lainnya
  3. Bandingkan objek kedua { first: "Tom", last: "Dale" } dengan baris kedua, <li>Godfrey CHAN</li> , 3 operasi lainnya
  4. Tidak ada baris keempat di DOM, jadi masukkan yang baru
    3.1. Masukkan simpul <li>
    3.2. Masukkan simpul teks ("Godfrey")
    3.3. Masukkan node teks (spasi)
    3.4. Panggil pembantu to-upper-case ("Chan" -> "CHAN")
    3.5. Masukkan node teks ("CHAN")

Itu 14 operasi. Aduh!

🔑 @ identitas

Tampaknya tidak perlu, karena secara konseptual, apakah kita melakukan prepending atau appending, kita masih hanya mengubah (menyisipkan) satu objek dalam array. Secara optimal, kami harus dapat menangani kasus ini seperti yang kami lakukan dalam skenario append.

Di sinilah key="@identity" masuk. Daripada mengandalkan _order_ elemen dalam larik, kami menggunakan identitas objek JavaScript mereka ( === ):

  1. Temukan baris yang sudah ada yang datanya cocok ( === ) objek pertama { first: "Andrew", last: "Timberlake" } . Karena tidak ada yang ditemukan, sisipkan (awali) baris baru:
    1.1. Masukkan node <li>
    1.2. Masukkan simpul teks ("Andrew")
    1.3. Masukkan node teks (spasi)
    1.4. Panggil pembantu to-upper-case ("Timberlake" -> "TIMBERLAKE")
    1.5. Masukkan node teks ("TIMBERLAKE")
  2. Temukan baris yang sudah ada yang datanya cocok ( === ) objek kedua { first: "Yehuda", last: "Katz" } . Menemukan <li>Yehuda KATZ</li> :
    2.1. "Yehuda" === "Yehuda", tidak ada yang bisa dilakukan
    2.2. (spasi tidak berisi data dinamis jadi tidak perlu perbandingan)
    2.3. "Katz" === "Katz", karena pembantu itu "murni", kita tahu kita tidak perlu memanggil kembali to-upper-case pembantu, dan oleh karena itu kita tahu keluaran dari pembantu itu ("KATZ" ) _also_ tidak berubah, jadi tidak ada yang bisa dilakukan di sini
  3. Demikian pula, tidak ada hubungannya dengan pertengkaran Tom dan Godfrey
  4. Hapus semua baris dengan objek yang tidak cocok (tidak ada, jadi tidak ada yang bisa dilakukan dalam kasus ini)

Dengan itu, kami kembali ke 5 operasi optimal.

Scaling Up

Sekali lagi ini adalah mengabaikan perbandingan dan biaya pembukuan. Memang, itu juga tidak gratis, dan dalam contoh yang sangat sederhana ini, mungkin tidak sepadan. Tapi bayangkan daftarnya besar dan setiap baris memanggil komponen yang rumit (dengan banyak pembantu, properti yang dihitung, sub-komponen, dll). Bayangkan feed berita LinkedIn, misalnya. Jika kami tidak mencocokkan baris yang tepat dengan data yang benar, argumen komponen Anda berpotensi banyak berubah dan menyebabkan lebih banyak pembaruan DOM daripada yang Anda perkirakan. Ada juga masalah dengan pencocokan elemen DOM yang salah dan kehilangan status DOM, seperti posisi kursor dan status pemilihan teks.

Secara keseluruhan, perbandingan ekstra dan biaya pembukuan dengan mudah bernilai sebagian besar waktu di aplikasi dunia nyata. Karena key="@identity" adalah default di Ember dan berfungsi dengan baik untuk hampir semua kasus, Anda biasanya tidak perlu khawatir tentang menyetel argumen key saat menggunakan {{#each}} .

Tabrakan 💥

Tapi tunggu, ada masalah. Bagaimana dengan kasus ini?

const YEHUDA = { first: "Yehuda", last: "Katz" };
const TOM = { first: "Tom", last: "Dale" };
const GODFREY = { first: "Godfrey", last: "Chan" };

this.list = [
  YEHUDA,
  TOM,
  GODFREY,
  TOM, // duplicate
  YEHUDA, // duplicate
  YEHUDA, // duplicate
  YEHUDA // duplicate
];

Masalahnya di sini adalah bahwa objek yang sama _bisa_ muncul beberapa kali dalam daftar yang sama. Ini merusak algoritme naif @identity , khususnya bagian di mana kami mengatakan "Temukan baris yang ada yang datanya cocok ( === ) ..." - ini hanya berfungsi jika data ke hubungan DOM adalah 1 : 1, yang tidak benar dalam kasus ini. Ini mungkin tampak tidak mungkin dalam praktiknya, tetapi sebagai kerangka kerja, kami harus menanganinya.

Untuk menghindari ini, kami menggunakan semacam pendekatan hybrid untuk menangani tabrakan ini. Secara internal, pemetaan kunci-ke-DOM terlihat seperti ini:

"YEHUDA" => <li>Yehuda...</li>
"TOM" => <li>Tom...</li>
"GODFREY" => <li>Godfrey...</li>
"TOM-1" => <li>Tom...</li>
"YEHUDA-1" => <li>Yehuda...</li>
"YEHUDA-2" => <li>Yehuda...</li>
"YEHUDA-3" => <li>Yehuda...</li>

Untuk sebagian besar, ini _is_ cukup langka, dan jika muncul, ini berfungsi dengan Baik ™ hampir sepanjang waktu. Jika, karena alasan tertentu, ini tidak berhasil, Anda selalu dapat menggunakan jalur kunci (atau mekanisme penguncian yang lebih canggih di RFC 321 ).

Kembali ke "🐛"

Setelah semua pembicaraan itu, kita sekarang siap untuk melihat skenario di Twiddle.

Pada dasarnya, kami mulai dengan daftar ini: [undefined, undefined, undefined, undefined, undefined] .

Catatan tidak terkait: Array(5) adalah _not_ sama dengan [undefined, undefined, undefined, undefined, undefined] . Ini menghasilkan "array berlubang" yang merupakan sesuatu yang harus Anda hindari secara umum. Namun, ini tidak terkait dengan bug ini, karena ketika mengakses "lubang" Anda memang mendapatkan kembali undefined . Jadi untuk tujuan _sangat sempit_ kita saja, keduanya sama.

Karena kami tidak menentukan kuncinya, Ember menggunakan @identity secara default. Selanjutnya, karena ini adalah tabrakan, kami berakhir dengan sesuatu seperti ini:

"undefined" => <input ...> 0: ...,
"undefined-1" => <input ...> 1: ...,
"undefined-2" => <input ...> 2: ...,
"undefined-3" => <input ...> 3: ...,
"undefined-4" => <input ...> 4: ...

Sekarang, katakanlah kita mengklik kotak centang pertama:

  1. Ini memicu perilaku default dari kotak pilih: mengubah status yang dicentang menjadi benar
  2. Ini memicu peristiwa klik, yang dicegat oleh pengubah {{action}} dan dikirim ulang ke metode makeChange
  3. Ini mengubah daftar menjadi [true, undefined, undefined, undefined, undefined] .
  4. Itu memperbarui DOM.

Bagaimana DOM diperbarui?

  1. Temukan baris yang ada yang datanya cocok dengan ( === ) objek pertama true . Karena tidak ada yang ditemukan, sisipkan (awali) baris baru <input checked=true ...>0: true...
  2. Temukan baris yang sudah ada yang datanya cocok dengan ( === ) objek kedua undefined . Menemukan <input ...>0: ... (sebelumnya baris PERTAMA):
    2.1. Perbarui simpul teks {{idx}} menjadi 1
    2.2. Jika tidak, sejauh yang bisa dikatakan Ember, tidak ada lagi yang berubah di baris ini, tidak ada yang bisa dilakukan
  3. Temukan baris yang sudah ada yang datanya cocok dengan ( === ) objek ketiga undefined . Karena ini adalah kedua kalinya kami melihat undefined , kunci internal adalah undefined-1 , jadi kami menemukan <input ...>1: ... (sebelumnya baris KEDUA):
    3.1. Perbarui simpul teks {{idx}} menjadi 2
    3.2. Jika tidak, sejauh yang bisa dikatakan Ember, tidak ada lagi yang berubah di baris ini, tidak ada yang bisa dilakukan
  4. Demikian pula, perbarui undefined-2 dan undefined-3
  5. Terakhir, hapus baris undefined-4 (karena ada satu baris undefined dalam larik setelah pembaruan)

Jadi ini menjelaskan bagaimana kami mendapatkan hasil yang Anda miliki di twiddle. Pada dasarnya semua baris DOM bergeser ke bawah satu, dan yang baru disisipkan di atas, sedangkan {{idx}} diperbarui untuk sisanya.

Bagian yang sangat tidak terduga adalah 2.2. Meskipun kotak centang pertama (yang diklik) digeser satu baris ke posisi kedua, Anda mungkin mengira Ember ke properti checked telah berubah menjadi true , dan karena nilai terikatnya tidak ditentukan, Anda mungkin berharap Ember mengubahnya kembali ke false , sehingga tidak mencentangnya.

Tapi ini bukan cara kerjanya. Seperti disebutkan di awal, mengakses DOM itu mahal. Ini termasuk _reading_ dari DOM. Jika, pada setiap pembaruan, kami harus membaca nilai terbaru dari DOM untuk perbandingan kami, itu akan sangat menggagalkan tujuan pengoptimalan kami. Oleh karena itu, untuk menghindarinya, kami mengingat nilai terakhir yang telah kami tulis ke DOM, dan membandingkan nilai saat ini dengan nilai yang di-cache tanpa harus membacanya kembali dari DOM. Hanya jika ada perbedaan kita menulis nilai baru ke DOM (dan menyimpannya dalam cache untuk waktu berikutnya). Ini adalah pengertian di mana kami berbagi pendekatan "DOM virtual" yang sama, tetapi kami hanya melakukannya di simpul daun, bukan memvirtualisasikan "struktur pohon" dari seluruh DOM.

Jadi, TL; DR, "mengikat" properti checked (atau properti value dari bidang teks, dll) tidak benar-benar berfungsi seperti yang Anda harapkan. Bayangkan jika Anda merender <div>{{this.name}}</div> dan secara manual memperbarui textContent dari elemen div menggunakan jQuery atau dengan chrome inspector. Anda tidak akan mengharapkan Ember memperhatikan itu dan memperbarui this.name untuk Anda. Ini pada dasarnya adalah hal yang sama: karena pembaruan ke properti checked terjadi di luar Ember (melalui perilaku default browser untuk kotak centang), Ember tidak akan tahu tentang itu.

Inilah mengapa pembantu {{input}} ada. Ia harus mendaftarkan event listener yang relevan pada elemen HTML yang mendasarinya dan mencerminkan operasi ke dalam perubahan properti yang sesuai, sehingga pihak yang berkepentingan (misalnya lapisan rendering) dapat diberitahukan.

Saya tidak yakin di mana itu meninggalkan kita. Saya mengerti mengapa ini mengejutkan, tetapi saya cenderung mengatakan ini adalah serangkaian kesalahan pengguna yang tidak menguntungkan. Mungkin kita harus melawan pengikatan properti ini pada elemen masukan?

@chancode - terima kasih atas penjelasannya yang luar biasa. Apakah itu berarti <input ... > tidak boleh digunakan tetapi hanya {{input ...}} untuk mencegah semua kesalahan seperti itu?

@ boris-petrov mungkin ada beberapa kasus terbatas yang dapat diterima .. seperti bidang teks yang hanya bisa dibaca untuk "salin url ini ke papan klip Anda", atau Anda _dapat_ menggunakan elemen masukan + {{action}} untuk mencegat DOM dan mencerminkan pembaruan properti secara manual (yang coba dilakukan twiddle, kecuali itu juga mengalami tabrakan @identity ), tetapi ya pada titik tertentu Anda baru saja mengimplementasikan kembali {{input}} dan menangani semua kasus tepi yang sudah ditangani untuk Anda. Jadi menurut saya _mungkin_ adil untuk mengatakan bahwa Anda sebaiknya menggunakan {{input}} sebagian besar, jika tidak semua, dari waktu ke waktu.

Namun, hal itu tetap tidak akan "memperbaiki" kasus ini di mana terdapat benturan dengan kunci. Lihat https://ember-twiddle.com/0f2369021128e2ae0c445155df5bb034?openFiles=templates.application.hbs%2C

Itulah sebabnya saya berkata, saya 100% tidak yakin apa yang harus saya lakukan. Di satu sisi saya setuju ini mengejutkan dan tidak terduga, di sisi lain, tabrakan semacam ini cukup jarang terjadi di aplikasi nyata dan itulah mengapa argumen "kunci" dapat disesuaikan (ini adalah kasus di mana kunci "@identity" default fungsinya tidak Cukup Baik ™, itulah sebabnya fitur itu ada).

@chancode - ini mengingatkan saya pada masalah lain yang saya buka beberapa waktu lalu . Apakah menurut Anda ada yang serupa di sana? Jawaban yang saya terima di sana (tentang perlunya menggunakan replace daripada set saat mengatur elemen array) masih terasa aneh bagi saya.

@ boris-petrov Saya rasa itu tidak ada hubungannya

hai, kami menggunakan sortablejs untuk daftar draggable dengan ember. tolong periksa demo ini untuk mereproduksi setiap masalah.

langkah:

  • seret salah satu item untuk bertahan
  • alihkan pilih ke 'v2'

Anda dapat melihat item yang diseret tetap berada di pohon dom.

tetapi, jika menyeret item ke posisi lain (bukan item terakhir), tampaknya berfungsi dengan baik.

Apakah halaman ini membantu?
0 / 5 - 0 peringkat