Skip to content
Mithril.js 2
Main Navigation PrzewodnikAPI

Polski

English
简体中文
繁體中文
Español
Français
Русский
Português – Brasil
Deutsch
日本語
한국어
Italiano
Türkçe
čeština
magyar

Polski

English
简体中文
繁體中文
Español
Français
Русский
Português – Brasil
Deutsch
日本語
한국어
Italiano
Türkçe
čeština
magyar

Wygląd

Sidebar Navigation

Pierwsze kroki

Instalacja

Prosta aplikacja

Zasoby

JSX

ES6+ na starszych przeglądarkach

Animacje

Testowanie

Przykłady

Integracja z zewnętrznymi bibliotekami

Obsługa ścieżek

Kluczowe koncepcje

Węzły Virtual DOM

Komponenty

Metody cyklu życia

Klucze

System automatycznego odświeżania

Różne

Porównanie frameworków

Migracja z v1.x

Migracja z wersji 0.2.x

API

Na tej stronie

Klucze ​

Czym są klucze? ​

Klucze reprezentują śledzone tożsamości. Można je dodać do elementów, komponentów i fragmentów vnode za pomocą magicznego atrybutu key. Użycie wygląda następująco:

javascript
m('.user', { key: user.id }, [
  /* ... */
]);

Są przydatne w następujących scenariuszach:

  • Podczas renderowania danych modelu lub innych danych stanu, klucze są potrzebne do utrzymania stanu lokalnego powiązanego z odpowiednim poddrzewem.
  • Podczas niezależnego animowania wielu sąsiadujących węzłów za pomocą CSS i możliwości usunięcia dowolnego z nich pojedynczo, klucze są potrzebne, aby upewnić się, że animacje pozostaną z elementami i nie zostaną nieoczekiwanie przeniesione do innych węzłów.
  • Gdy trzeba ponownie zainicjować poddrzewo na żądanie, należy dodać klucz, a następnie zmienić go i przerysować, kiedy tylko zajdzie potrzeba ponownej inicjalizacji.

Ograniczenia kluczy ​

Ważne: W przypadku fragmentów, ich elementy potomne muszą zawierać albo wyłącznie vnode z atrybutami key (fragment z kluczem), albo wyłącznie vnode bez atrybutów key (fragment bez klucza). Atrybuty key mogą istnieć tylko na vnode, które w ogóle obsługują atrybuty, a mianowicie elementy, komponenty i fragmenty vnode. Inne vnode, takie jak null, undefined i łańcuchy znaków, nie mogą mieć atrybutów żadnego rodzaju, więc nie mogą mieć atrybutów key i dlatego nie mogą być używane we fragmentach z kluczem.

Oznacza to, że konstrukcje takie jak [m(".foo", {key: 1}), null] i ["foo", m(".bar", {key: 2})] nie będą działać, ale [m(".foo", {key: 1}), m(".bar", {key: 2})] oraz [m(".foo"), null] będą działać. W przypadku pomyłki, zostanie wyświetlony pomocny komunikat o błędzie wyjaśniający to.

Łączenie danych modelu na listach widoków ​

Podczas renderowania list, zwłaszcza list edytowalnych, często mamy do czynienia z elementami, które mają stan i tożsamość, np. edytowalne TODO. Należy przekazać Mithril.js informacje potrzebne do ich śledzenia.

Załóżmy, że mamy prostą listę postów w mediach społecznościowych, gdzie można komentować posty i ukrywać je (np. zgłaszając je).

javascript
// `User` i `ComposeWindow` pominięte dla zwięzłości
function CommentCompose() {
  return {
    view: function (vnode) {
      var post = vnode.attrs.post;
      return m(ComposeWindow, {
        placeholder: 'Napisz swój komentarz...',
        submit: function (text) {
          return Model.addComment(post, text);
        },
      });
    },
  };
}

function Comment() {
  return {
    view: function (vnode) {
      var comment = vnode.attrs.comment;
      return m(
        '.comment',
        m(User, { user: comment.user }),
        m('.comment-body', comment.text),
        m(
          'a.comment-hide',
          {
            onclick: function () {
              Model.hideComment(comment).then(m.redraw);
            },
          },
          "Nie podoba mi się to"
        )
      );
    },
  };
}

function PostCompose() {
  return {
    view: function (vnode) {
      var comment = vnode.attrs.comment;
      return m(ComposeWindow, {
        placeholder: 'Napisz swój post...',
        submit: Model.createPost,
      });
    },
  };
}

function Post(vnode) {
  var showComments = false;
  var commentsFetched = false;

  return {
    view: function (vnode) {
      var post = vnode.attrs.post;
      var comments = showComments ? Model.getComments(post) : null;
      return m(
        '.post',
        m(User, { user: post.user }),
        m('.post-body', post.text),
        m(
          '.post-meta',
          m(
            'a.post-comment-count',
            {
              onclick: function () {
                if (!showComments && !commentsFetched) {
                  commentsFetched = true;
                  Model.fetchComments(post).then(m.redraw);
                }
                showComments = !showComments;
              },
            },
            post.commentCount,
            ' komentarz' + (post.commentCount === 1 ? '' : (post.commentCount >= 2 && post.commentCount <= 4 ? 'e' : 'y'))
          ),
          m(
            'a.post-hide',
            {
              onclick: function () {
                Model.hidePost(post).then(m.redraw);
              },
            },
            "Nie podoba mi się to"
          )
        ),
        showComments
          ? m(
              '.post-comments',
              comments == null
                ? m('.comment-list-loading', 'Ładowanie...')
                : [
                    m(
                      '.comment-list',
                      comments.map(function (comment) {
                        return m(Comment, { comment: comment });
                      })
                    ),
                    m(CommentCompose, { post: post }),
                  ]
            )
          : null
      );
    },
  };
}

function Feed() {
  Model.fetchPosts().then(m.redraw);
  return {
    view: function () {
      var posts = Model.getPosts();
      return m(
        '.feed',
        m('h1', 'Kanał'),
        posts == null
          ? m('.post-list-loading', 'Ładowanie...')
          : m(
              '.post-view',
              m(PostCompose),
              m(
                '.post-list',
                posts.map(function (post) {
                  return m(Post, { post: post });
                })
              )
            )
      );
    },
  };
}

Jak widać, kod hermetyzuje wiele funkcjonalności, ale skupimy się na dwóch fragmentach:

javascript
// W komponencie `Feed`
m(
  '.post-list',
  posts.map(function (post) {
    return m(Post, { post: post });
  })
);

// W komponencie `Post`
m(
  '.comment-list',
  comments.map(function (comment) {
    return m(Comment, { comment: comment });
  })
);

Każdy z nich odnosi się do poddrzewa ze skojarzonym stanem, o którym Mithril.js nie ma pojęcia (Mithril.js wie tylko o vnode, nic więcej). Kiedy pominie się klucz identyfikujący, mogą wystąpić dziwne i nieoczekiwane zachowania. W tym przypadku, spróbuj kliknąć "N komentarzy", aby pokazać komentarze, wpisać coś w polu tworzenia komentarza na dole, a następnie kliknąć "Nie podoba mi się to" na poście powyżej. Oto działająca wersja demonstracyjna, na której możesz to wypróbować, wraz z modelem mock. (Uwaga: jeśli używasz Edge lub IE, mogą wystąpić problemy z powodu długości hasha linku.)

Zamiast zachowywać się zgodnie z oczekiwaniami, komponent staje się całkowicie zdezorientowany i robi błędy: zamyka listę komentarzy, która była otwarta, a post po tym, na którym komentarze były otwarte, teraz nieustannie wyświetla "Ładowanie...", mimo że uważa, że już załadował komentarze. Dzieje się tak, ponieważ komentarze są ładowane z opóźnieniem i zakłada się, że za każdym razem przekazywany jest ten sam komentarz (co wydaje się rozsądne), ale w tym przypadku tak nie jest. Dzieje się tak z powodu sposobu, w jaki Mithril.js aktualizuje fragmenty bez klucza: aktualizuje je kolejno, jeden po drugim, w bardzo prosty sposób. Więc w tym przypadku diff może wyglądać tak:

  • Przed: A, B, C, D, E
  • Po zmianach: A, B, C -> D, D -> E, E -> (usunięto)

A ponieważ komponent pozostaje ten sam (zawsze jest to Comment), zmieniają się tylko atrybuty i nie jest on zastępowany.

Aby naprawić ten błąd, wystarczy dodać klucz, aby Mithril.js wiedział, że w razie potrzeby może przenieść stan, aby rozwiązać problem. Oto działający przykład z naprawionym kodem.

javascript
// W komponencie `Feed`
m(
  '.post-list',
  posts.map(function (post) {
    return m(Post, { key: post.id, post: post });
  })
);

// W komponencie `Post`
m(
  '.comment-list',
  comments.map(function (comment) {
    return m(Comment, { key: comment.id, comment: comment });
  })
);

Jasno zaznaczam, że w przypadku komentarzy, chociaż technicznie mogłoby to działać bez kluczy w tym przypadku, podobnie by się zepsuło, gdybyś dodał coś takiego jak zagnieżdżone komentarze lub możliwość ich edycji, i wtedy musiałbyś dodać do nich klucze.

Jak unikać problemów z kolekcjami animowanych obiektów ​

Czasami chcesz animować listy, bloki itp. Zacznijmy od prostego przykładu:

javascript
var colors = ['red', 'yellow', 'blue', 'gray'];
var counter = 0;

function getColor() {
  var color = colors[counter];
  counter = (counter + 1) % colors.length;
  return color;
}

function Boxes() {
  var boxes = [];

  function add() {
    boxes.push({ color: getColor() });
  }

  function remove(box) {
    var index = boxes.indexOf(box);
    boxes.splice(index, 1);
  }

  return {
    view: function () {
      return [
        m('button', { onclick: add }, 'Dodaj blok, kliknij blok, aby usunąć'),
        m(
          '.container',
          boxes.map(function (box, i) {
            return m(
              '.box',
              {
                'data-color': box.color,
                onclick: function () {
                  remove(box);
                },
              },
              m('.stretch')
            );
          })
        ),
      ];
    },
  };
}

Wygląda to całkiem niewinnie, ale wypróbuj ten przykład na żywo. W przykładzie kliknij, aby dodać kilka bloków, a następnie wybierz jeden z nich i obserwuj jego rozmiar. Chcemy, aby rozmiar i obrót były powiązane z blokiem (oznaczonym kolorem), a nie z pozycją w siatce. Zauważysz, że rozmiar zmienia się nagle, ale pozostaje stały w zależności od lokalizacji. Oznacza to, że musimy przypisać im klucze.

W tym przypadku przypisanie im unikalnych kluczy jest dość proste: po prostu utwórz licznik, który zwiększasz za każdym razem, gdy go odczytujesz.

javascript
var colors = ['red', 'yellow', 'blue', 'gray'];
var counter = 0;

function getColor() {
  var color = colors[counter];
  counter = (counter + 1) % colors.length;
  return color;
}

function Boxes() {
  var boxes = [];
  var nextKey = 0;

  function add() {
    boxes.push({ color: getColor() }); 
    var key = nextKey; 
    nextKey++; 
    boxes.push({ key: key, color: getColor() }); 
  }

  function remove(box) {
    var index = boxes.indexOf(box);
    boxes.splice(index, 1);
  }

  return {
    view: function () {
      return [
        m('button', { onclick: add }, 'Dodaj blok, kliknij blok, aby usunąć'),
        m(
          '.container',
          boxes.map(function (box, i) {
            return m(
              '.box',
              {
                key: box.key, 
                'data-color': box.color,
                onclick: function () {
                  remove(box);
                },
              },
              m('.stretch')
            );
          })
        ),
      ];
    },
  };
}

Oto poprawiona wersja demonstracyjna do przetestowania, aby zobaczyć, jak działa inaczej.

Ponowne inicjowanie widoków z fragmentami z pojedynczym elementem potomnym i kluczem ​

Kiedy masz do czynienia z komponentami ze stanem w modelach itp., często przydatne jest renderowanie widoków modeli z kluczami. Załóżmy, że masz następujący układ:

javascript
function Layout() {
  // ...
}

function Person() {
  // ...
}

m.route(rootElem, '/', {
  '/': Home,
  '/person/:id': {
    render: function () {
      return m(Layout, m(Person, { id: m.route.param('id') }));
    },
  },
  // ...
});

Prawdopodobnie twój komponent Person wygląda mniej więcej tak:

javascript
function Person(vnode) {
  var personId = vnode.attrs.id;
  var state = 'pending';
  var person, error;

  m.request('/api/person/:id', { params: { id: personId } }).then(
    function (p) {
      person = p;
      state = 'ready';
    },
    function (e) {
      error = e;
      state = 'error';
    }
  );

  return {
    view: function () {
      if (state === 'pending') return m(LoadingIcon);
      if (state === 'error') {
        return error.code === 404
          ? m('.person-missing', 'Osoba nie została znaleziona.')
          : m('.person-error', 'Wystąpił błąd. Spróbuj ponownie później.');
      }
      return m(
        '.person',
        m(
          m.route.Link,
          {
            class: 'person-edit',
            href: '/person/:id/edit',
            params: { id: personId },
          },
          'Edytuj'
        ),
        m('.person-name', 'Nazwa: ', person.name)
        // ...
      );
    },
  };
}

Powiedzmy, że dodałeś możliwość łączenia się z innymi osobami z tego komponentu, na przykład dodając pole "manager".

javascript
function Person(vnode) {
  // ...

  return {
    view: function () {
      // ...
      return m(
        '.person',
        m(
          m.route.Link,
          {
            class: 'person-edit',
            href: '/person/:id/edit',
            params: { id: personId },
          },
          'Edytuj'
        ),
        m('.person-name', person.name),
        // ...
        m(
          '.manager',
          'Kierownik: ',
          m(
            m.route.Link,
            {
              href: '/person/:id',
              params: { id: person.manager.id },
            },
            person.manager.name
          )
        )
        // ...
      );
    },
  };
}

Zakładając, że ID osoby to 1, a ID managera to 2, przełączysz się z /person/1 na /person/2, pozostając na tej samej ścieżce. Użyłeś metody route resolver render. W efekcie drzewo zostało zachowane i nastąpiła zmiana z m(Layout, m(Person, {id: "1"})) na m(Layout, m(Person, {id: "2"})). W tym przypadku komponent Person pozostaje ten sam, więc nie jest reinicjalizowany. Ale w naszym przypadku jest to niepożądane, ponieważ oznacza to, że nowy użytkownik nie jest pobierany. W tym miejscu przydają się klucze. Możemy zmodyfikować route resolver w następujący sposób, aby to naprawić:

javascript
m.route(rootElem, '/', {
  '/': Home,
  '/person/:id': {
    render: function () {
      return m(
        Layout,
        // Umieść go w tablicy, na wypadek dodania kolejnych elementów w przyszłości.
        // Pamiętaj: fragmenty muszą zawierać wyłącznie elementy potomne z kluczami, albo nie zawierać ich wcale.
        [m(Person, { id: m.route.param('id'), key: m.route.param('id') })]
      );
    },
  },
  // ...
});

Częste pułapki ​

Istnieje kilka częstych pułapek, na które można się natknąć w związku z kluczami. Poniżej przedstawiono niektóre z nich, aby pomóc zrozumieć, dlaczego pewne rozwiązania nie działają.

Owijanie elementów posiadających klucze ​

Poniższe fragmenty kodu nie działają w ten sam sposób:

javascript
users.map(function (user) {
  return m('.wrapper', [m(User, { user: user, key: user.id })]);
});

users.map(function (user) {
  return m('.wrapper', { key: user.id }, [m(User, { user: user })]);
});

Pierwszy przykład przypisuje klucz do komponentu User, ale zewnętrzny fragment stworzony przez users.map(...) nie ma klucza (jest "unkeyed"). Owijanie elementu z kluczem w ten sposób jest nieprawidłowe i może prowadzić do różnych problemów – od dodatkowych żądań przy każdej zmianie listy, po utratę stanu przez wewnętrzne pola formularza. Zachowanie byłoby podobne do zepsutego przykładu listy postów, ale bez problemu z uszkodzeniem stanu.

Drugi przykład przypisuje klucz do elementu .wrapper, co zapewnia, że zewnętrzny fragment jest oznaczony kluczem. Dzięki temu usunięcie użytkownika nie wpłynie na stan innych instancji.

Umieszczanie kluczy wewnątrz komponentu ​

Załóżmy, że w przykładzie osoby zastosowano następujące rozwiązanie:

javascript
// UNIKAJ
function Person(vnode) {
  var personId = vnode.attrs.id;
  // ...

  return {
    view: function () {
      return m.fragment(
        { key: personId }
        // to, co wcześniej miałeś w widoku
      );
    },
  };
}

To nie zadziała, ponieważ klucz nie odnosi się do całego komponentu. Odnosi się tylko do widoku, więc dane nie są ponownie pobierane, wbrew oczekiwaniom.

Zalecanym rozwiązaniem jest umieszczenie klucza w vnode używającym komponentu, a nie wewnątrz samego komponentu, jak pokazano w przykładzie.

javascript
// PREFERUJ
return [m(Person, { id: m.route.param('id'), key: m.route.param('id') })];

Niepotrzebne używanie kluczy ​

Częstym błędnym założeniem jest, że klucze same w sobie są tożsamościami. Mithril.js wymaga, aby wszystkie elementy potomne we fragmencie miały klucze, albo żeby żadne z nich ich nie miały. W przeciwnym razie zgłosi błąd. Załóżmy, że masz następujący układ:

javascript
m('.page', m('.header', { key: 'header' }), m('.body'), m('.footer'));

To oczywiście spowoduje błąd, ponieważ .header ma klucz, a .body i .footer nie mają. W takim przypadku klucze nie są potrzebne. Jeśli używasz kluczy w takich sytuacjach, rozwiązaniem nie jest dodawanie kluczy, lecz ich usuwanie. Dodawaj je tylko wtedy, gdy naprawdę, naprawdę ich potrzebujesz. Tak, bazowe węzły DOM mają tożsamości, ale Mithril.js nie musi śledzić tych tożsamości, aby poprawnie je załatać. Praktycznie nigdy tego nie robi. Klucze są potrzebne tylko w przypadku list, gdzie każdy wpis ma jakiś rodzaj powiązanego stanu, którego Mithril.js sam nie śledzi, czy to w modelu, w komponencie, czy w samym DOM.

Jeszcze jedno: unikaj statycznych kluczy. Są one zawsze niepotrzebne. Jeśli atrybut key nie jest obliczany dynamicznie, prawdopodobnie robisz coś źle.

Należy zauważyć, że jeśli rzeczywiście potrzebujesz pojedynczego elementu z kluczem w izolacji, użyj fragmentu z kluczem z jednym elementem potomnym. Jest to po prostu tablica zawierająca jeden element, który jest elementem z kluczem, np. [m("div", {key: foo})].

Mieszanie typów kluczy ​

Klucze są odczytywane jako nazwy właściwości obiektu. Oznacza to, że 1 i "1" są traktowane identycznie. Aby uniknąć problemów, unikaj mieszania typów kluczy, jeśli to możliwe. Jeśli to zrobisz, może to skutkować zduplikowanymi kluczami i nieoczekiwanym zachowaniem.

javascript
// UNIKAJ
var things = [
  { id: '1', name: 'Book' },
  { id: 1, name: 'Cup' },
];

Jeśli absolutnie musisz to zrobić i nie masz nad tym kontroli, użyj przedrostka oznaczającego typ, aby klucze pozostały różne.

javascript
things.map(function (thing) {
  return m(
    '.thing',
    { key: typeof thing.id + ':' + thing.id }
    // ...
  );
});

Ukrywanie elementów z kluczami z dziurami ​

Wartości takie jak null, undefined i wartości logiczne są traktowane jako vnodes bez klucza, więc kod taki jak ten nie zadziała:

javascript
// UNIKAJ
things.map(function (thing) {
  return shouldShowThing(thing)
    ? m(Thing, { key: thing.id, thing: thing })
    : null;
});

Zamiast tego, przefiltruj listę przed jej zwróceniem, a Mithril.js zrobi to, co trzeba. W większości przypadków Array.prototype.filter jest dokładnie tym, czego potrzebujesz i zdecydowanie powinieneś go wypróbować.

javascript
// PREFERUJ
things
  .filter(function (thing) {
    return shouldShowThing(thing);
  })
  .map(function (thing) {
    return m(Thing, { key: thing.id, thing: thing });
  });

Duplikaty kluczy ​

Klucze dla elementów fragmentu muszą być unikalne, w przeciwnym razie nie jest jasne, który klucz do którego elementu należy. Może to również powodować problemy z elementami, które nie przemieszczają się tak, jak powinny.

javascript
// UNIKAJ
var things = [
  { id: '1', name: 'Book' },
  { id: '1', name: 'Cup' },
];

Mithril.js używa pustego obiektu do mapowania kluczy na indeksy, aby wiedzieć, jak poprawnie załatać fragmenty z kluczami. Kiedy masz duplikat klucza, nie jest już jasne, gdzie dany element się przeniósł, więc Mithril.js może działać niepoprawnie w takiej sytuacji i robić nieoczekiwane rzeczy podczas aktualizacji, zwłaszcza jeśli lista się zmieniła. Odrębne klucze są wymagane, aby Mithril.js mógł poprawnie połączyć stare z nowymi węzłami, więc musisz wybrać coś lokalnie unikalnego, aby użyć jako klucza.

Używanie obiektów dla kluczy ​

Klucze dla elementów fragmentu są traktowane jako klucze właściwości. Użycie obiektów jako kluczy nie zadziała tak, jak myślisz.

javascript
// UNIKAJ
things.map(function (thing) {
  return m(Thing, { key: thing, thing: thing });
});

Jeśli obiekt posiada metodę toString, zostanie ona wywołana, a jej wartość zwrócona będzie użyta jako klucz. Może to prowadzić do nieoczekiwanych rezultatów.

Pager
Poprzednia stronaMetody cyklu życia
Następna stronaSystem automatycznego odświeżania

Opublikowano na licencji MIT.

Copyright (c) 2024 Mithril Contributors

https://mithril.js.org/keys.html

Opublikowano na licencji MIT.

Copyright (c) 2024 Mithril Contributors