Skip to content
Mithril.js 2
Main Navigation РуководствоAPI

Русский

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

Русский

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

Внешний вид

Sidebar Navigation

Начало работы

Установка Mithril.js

Простое приложение

Ресурсы

JSX

ES6+ в старых браузерах

Анимации

Тестирование

Примеры

Интеграция со сторонними библиотеками

Обработка путей

Ключевые концепции

Виртуальные DOM-узлы

Компоненты

Методы жизненного цикла

Ключи

Автоматическая перерисовка

Разное

Сравнение фреймворков

Переход с v1.x

Переход с v0.2.x

API

Содержание страницы

Ключи ​

Что такое ключи? ​

Ключи — это идентификаторы, используемые Mithril.js для отслеживания vnode. Вы можете добавить их к элементам, компонентам и фрагментам vnode через специальный атрибут key. Вот пример использования:

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

Ключи полезны в следующих случаях:

  • При отображении данных модели или других данных, имеющих состояние, ключи необходимы для того, чтобы локальное состояние оставалось привязанным к правильному поддереву.
  • При независимой анимации нескольких смежных узлов с помощью CSS и возможности удаления любого из них по отдельности, ключи гарантируют, что анимация останется с элементами и не будет неожиданно перескакивать на другие узлы.
  • Когда необходимо принудительно переинициализировать поддерево, добавьте ключ, затем измените его и перерисуйте при каждой необходимости переинициализации.

Ограничения ключей ​

Важно: Во всех фрагментах дочерние элементы должны содержать либо исключительно vnode с атрибутами key (фрагменты с ключами), либо исключительно vnode без атрибутов key (фрагменты без ключей). Атрибуты key могут быть только у тех vnode, которые поддерживают атрибуты: элементов, компонентов и фрагментов vnode. Другие vnode, такие как null, undefined и строки, не могут иметь атрибуты какого-либо рода, поэтому они не могут иметь атрибуты key и, следовательно, не могут использоваться во фрагментах с ключами.

Это означает, что конструкции вроде [m(".foo", {key: 1}), null] и ["foo", m(".bar", {key: 2})] не будут работать, но [m(".foo", {key: 1}), m(".bar", {key: 2})] и [m(".foo"), null] будут. Если вы забудете об этом, вы получите информативное сообщение об ошибке.

Связывание данных модели в списках представлений ​

При отображении списков, особенно редактируемых списков, часто приходится работать с редактируемыми TODO и подобными элементами. Они имеют состояние и идентификаторы, и вам необходимо предоставить Mithril.js информацию, необходимую для их отслеживания.

Предположим, у нас есть простой список сообщений в социальной сети. В нем можно комментировать и скрывать сообщения, например, по причине жалобы на них.

javascript
// `User` и `ComposeWindow` опущены для краткости
function CommentCompose() {
  return {
    view: function (vnode) {
      var post = vnode.attrs.post;
      return m(ComposeWindow, {
        placeholder: 'Напишите свой комментарий...',
        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);
            },
          },
          "Мне это не нравится"
        )
      );
    },
  };
}

function PostCompose() {
  return {
    view: function (vnode) {
      var comment = vnode.attrs.comment;
      return m(ComposeWindow, {
        placeholder: 'Напишите свой пост...',
        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,
            ' comment',
            post.commentCount === 1 ? '' : 's'
          ),
          m(
            'a.post-hide',
            {
              onclick: function () {
                Model.hidePost(post).then(m.redraw);
              },
            },
            "Мне это не нравится"
          )
        ),
        showComments
          ? m(
              '.post-comments',
              comments == null
                ? m('.comment-list-loading', 'Загрузка...')
                : [
                    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', 'Лента'),
        posts == null
          ? m('.post-list-loading', 'Загрузка...')
          : m(
              '.post-view',
              m(PostCompose),
              m(
                '.post-list',
                posts.map(function (post) {
                  return m(Post, { post: post });
                })
              )
            )
      );
    },
  };
}

Здесь много функциональности, но я хотел бы подробнее рассмотреть два момента:

javascript
// В компоненте `Feed`
m(
  '.post-list',
  posts.map(function (post) {
    return m(Post, { post: post });
  })
);

// В компоненте `Post`
m(
  '.comment-list',
  comments.map(function (comment) {
    return m(Comment, { comment: comment });
  })
);

Каждый из этих фрагментов относится к поддереву со связанным состоянием, о котором Mithril.js ничего не знает (Mithril.js знает только о vnode). Если вы оставите их без ключей, поведение может стать странным и непредсказуемым. В этом случае попробуйте нажать на "N комментариев", чтобы раскрыть список комментариев, введите текст в поле ввода внизу, а затем нажмите "Мне это не нравится" на одном из сообщений выше. Вот рабочий пример, чтобы вы могли попробовать, с макетом модели. (Примечание: в Edge или IE могут возникнуть проблемы из-за большой длины URL.)

Вместо ожидаемого результата происходит сбой: закрывается открытый список комментариев, а у сообщения, следующего за тем, где были открыты комментарии, постоянно отображается "Загрузка...", даже если кажется, что комментарии уже загружены. Это происходит из-за ленивой загрузки комментариев. Компонент предполагает, что каждый раз передается один и тот же комментарий (что в целом логично), но в данном случае это не так. Это связано с тем, как Mithril.js обрабатывает фрагменты без ключей: он выполняет их патчинг один за другим итеративно, очень простым способом. В этом случае разница может выглядеть так:

  • До: A, B, C, D, E
  • Исправлено: A, B, C -> D, D -> E, E -> (удалено)

И поскольку компонент остается тем же (это всегда Comment), изменяются только атрибуты, и он не заменяется.

Чтобы исправить эту ошибку, добавьте ключ. Тогда Mithril.js сможет правильно перенести состояние, если это потребуется. Вот живой, рабочий пример с исправлением.

javascript
// В компоненте `Feed`
m(
  '.post-list',
  posts.map(function (post) {
    return m(Post, { key: post.id, post: post });
  })
);

// В компоненте `Post`
m(
  '.comment-list',
  comments.map(function (comment) {
    return m(Comment, { key: comment.id, comment: comment });
  })
);

Обратите внимание, что для комментариев, хотя технически это работало бы и без ключей в этом конкретном случае, это также сломалось бы, если бы вы добавили что-нибудь вроде вложенных комментариев или возможности их редактировать, и вам пришлось бы добавить к ним ключи.

Обеспечение бесперебойной работы коллекций анимированных объектов ​

В некоторых случаях может потребоваться анимировать списки, контейнеры и тому подобное. Начнем с простого примера:

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 }, 'Add box, click box to remove'),
        m(
          '.container',
          boxes.map(function (box, i) {
            return m(
              '.box',
              {
                'data-color': box.color,
                onclick: function () {
                  remove(box);
                },
              },
              m('.stretch')
            );
          })
        ),
      ];
    },
  };
}

На первый взгляд все выглядит безобидно, но попробуйте этот пример вживую. В этом примере создайте несколько коробок, щелкнув по кнопке, затем выберите коробку и следите за ее размером. Вы заметите, что размер изменяется скачкообразно, но остается постоянным для определенной позиции. Это происходит потому, что нам нужно присвоить элементам уникальные ключи.

В данном случае это довольно просто: достаточно создать счетчик, который увеличивается при каждом добавлении элемента.

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 }, 'Add box, click box to remove'),
        m(
          '.container',
          boxes.map(function (box, i) {
            return m(
              '.box',
              {
                key: box.key, 
                'data-color': box.color,
                onclick: function () {
                  remove(box);
                },
              },
              m('.stretch')
            );
          })
        ),
      ];
    },
  };
}

Вот исправленный пример, с которым вы можете поэкспериментировать, чтобы увидеть разницу.

Повторная инициализация представлений с помощью фрагментов с одним дочерним элементом и ключами ​

При работе с сущностями, отслеживающими состояние в моделях и т.п., часто бывает полезно отображать представления моделей с ключами. Предположим, у вас есть следующая структура:

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

function Person() {
  // ...
}

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

Скорее всего, ваш компонент Person выглядит примерно так:

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', 'Person not found.')
          : m('.person-error', 'An error occurred. Please try again later');
      }
      return m(
        '.person',
        m(
          m.route.Link,
          {
            class: 'person-edit',
            href: '/person/:id/edit',
            params: { id: personId },
          },
          'Edit'
        ),
        m('.person-name', 'Name: ', person.name)
        // ...
      );
    },
  };
}

Предположим, вы добавили ссылки на других пользователей из этого компонента, например, добавив поле "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 },
          },
          'Edit'
        ),
        m('.person-name', person.name),
        // ...
        m(
          '.manager',
          'Manager: ',
          m(
            m.route.Link,
            {
              href: '/person/:id',
              params: { id: person.manager.id },
            },
            person.manager.name
          )
        )
        // ...
      );
    },
  };
}

Предполагая, что ID человека был 1, а ID менеджера был 2, вы переключитесь с /person/1 на /person/2, оставаясь на том же маршруте. Но поскольку вы использовали метод разрешения маршрута render, дерево компонентов осталось прежним, и вы просто изменили m(Layout, m(Person, {id: "1"})) на m(Layout, m(Person, {id: "2"})). В этом случае компонент Person не изменился и, следовательно, не инициализируется повторно. Но в нашем случае это нежелательно, так как данные нового пользователя не загружаются. Здесь-то и пригодятся ключи. Чтобы это исправить, можно изменить конфигурацию маршрутизатора следующим образом:

javascript
m.route(rootElem, '/', {
  '/': Home,
  '/person/:id': {
    render: function () {
      return m(
        Layout,
        // Рекомендуется обернуть его в массив, чтобы упростить добавление других элементов в будущем.
        // Помните: фрагменты должны содержать либо только дочерние элементы с ключами,
        // либо вообще не содержать дочерних элементов с ключами.
        [m(Person, { id: m.route.param('id'), key: m.route.param('id') })]
      );
    },
  },
  // ...
});

Типичные ошибки ​

При работе с ключами часто возникают определенные ошибки. Вот некоторые из них, которые помогут вам понять, почему ваш код может работать не так, как ожидается.

Оборачивание элементов с ключами ​

Следующие два фрагмента кода ведут себя по-разному:

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 })]);
});

В первом случае ключ привязывается к компоненту User, но внешний фрагмент, созданный users.map(...), остается без ключа. Оборачивание элемента с ключом таким образом не работает и может привести к непредсказуемым результатам: от дополнительных запросов при каждом изменении списка до потери состояния внутренних элементов форм. Поведение будет похоже на неправильный пример списка сообщений, но без повреждения состояния.

Во втором случае ключ привязывается к элементу .wrapper, гарантируя, что внешний фрагмент имеет ключ. Это именно то, что вы, вероятно, хотели сделать, и удаление пользователя не вызовет проблем с состоянием других экземпляров пользователя.

Размещение ключей внутри компонента ​

Предположим, в примере с человеком вы сделали следующее:

javascript
// ИЗБЕГАЙТЕ
function Person(vnode) {
  var personId = vnode.attrs.id;
  // ...

  return {
    view: function () {
      return m.fragment(
        { key: personId }
        // что у вас было ранее в представлении
      );
    },
  };
}

Это не сработает, потому что ключ не применяется к компоненту в целом. Он применяется только к представлению, и поэтому данные не будут перезапрашиваться, как ожидалось.

Предпочтительное решение: поместите ключ в vnode, используя компонент, а не внутри самого компонента.

javascript
// ПРЕДПОЧТИТЕЛЬНО
return [m(Person, { id: m.route.param('id'), key: m.route.param('id') })];

Избыточное использование ключей ​

Распространено заблуждение, что ключи сами по себе являются идентификаторами. Mithril.js требует, чтобы все дочерние элементы фрагмента либо имели ключи, либо не имели их вовсе. В противном случае будет выдана ошибка. Предположим, у вас есть такая структура:

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

Это вызовет ошибку, так как .header имеет ключ, а .body и .footer - нет. Но суть в том, что ключи здесь не нужны. Если вы используете ключи для подобных вещей, решение не в том, чтобы добавить ключи, а в том, чтобы их удалить. Добавляйте ключи только тогда, когда они действительно необходимы. Да, у базовых DOM-узлов есть идентификаторы, но Mithril.js не нужно отслеживать их для корректного обновления (patch). Практически никогда. Ключи нужны только для списков, где каждая запись имеет какое-то связанное состояние, которое Mithril.js сам не отслеживает, будь то в модели, в компоненте или в самом DOM.

И наконец: избегайте статических ключей. Они всегда излишни. Если вы не вычисляете атрибут key, вероятно, вы делаете что-то не так.

Обратите внимание, что если вам действительно нужен один элемент с ключом, используйте фрагмент с одним ключевым элементом. Это просто массив с одним дочерним элементом, который является элементом с ключом, например [m("div", {key: foo})].

Смешение типов ключей ​

Ключи интерпретируются как имена свойств объекта. Это значит, что 1 и "1" считаются одинаковыми. Во избежание проблем старайтесь не смешивать типы ключей. В противном случае могут возникнуть дублирующиеся ключи и неожиданное поведение.

javascript
// ИЗБЕГАЙТЕ
var things = [
  { id: '1', name: 'Book' },
  { id: 1, name: 'Cup' },
];

Если это абсолютно необходимо и вы не контролируете это, используйте префикс, обозначающий тип, чтобы ключи оставались разными.

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

Сокрытие элементов с ключами через пустые значения ​

"Дыры" (holes), такие как null, undefined и булевы значения, считаются vnode без ключа, поэтому код, подобный этому, не будет работать:

javascript
// ИЗБЕГАЙТЕ
things.map(function (thing) {
  return shouldShowThing(thing)
    ? m(Thing, { key: thing.id, thing: thing })
    : null;
});

Вместо этого отфильтруйте список перед возвращением, и Mithril.js корректно обработает его. В большинстве случаев Array.prototype.filter - это именно то, что вам нужно, и стоит попробовать.

javascript
// ПРЕДПОЧТИТЕЛЬНО
things
  .filter(function (thing) {
    return shouldShowThing(thing);
  })
  .map(function (thing) {
    return m(Thing, { key: thing.id, thing: thing });
  });

Дублирующиеся ключи ​

Ключи элементов фрагмента должны быть уникальными, иначе будет неясно, какому элементу соответствует тот или иной ключ. Это может привести к проблемам с перемещением элементов не так, как ожидается.

javascript
// ИЗБЕГАЙТЕ
var things = [
  { id: '1', name: 'Book' },
  { id: '1', name: 'Cup' },
];

Mithril.js использует пустой объект для сопоставления ключей с индексами, чтобы правильно обновлять (patch) фрагменты с ключами. Когда есть дублирующийся ключ, становится непонятно, куда переместился этот элемент, и Mithril.js будет работать некорректно и делать неожиданные вещи при обновлении, особенно если список изменился. Для корректного сопоставления старых и новых узлов Mithril.js требуются различные ключи, поэтому необходимо выбрать что-то уникальное в пределах контекста для использования в качестве ключа.

Использование объектов для ключей ​

Ключи элементов фрагмента рассматриваются как ключи свойств. Использование объектов в качестве ключей может привести к непредсказуемым результатам.

javascript
// ИЗБЕГАЙТЕ
things.map(function (thing) {
  return m(Thing, { key: thing, thing: thing });
});

Если у объекта есть метод toString, он будет вызван, и результат его работы будет использован в качестве ключа, возможно, без вашего ведома. Если метода toString нет, все объекты будут преобразованы в строку "[object Object]", что приведет к проблеме с дублирующимися ключами.

Pager
Предыдущая страницаМетоды жизненного цикла
Следующая страницаАвтоматическая перерисовка

Выпущено на условиях лицензии MIT.

Авторские права (c) 2024 Mithril Contributors

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

Выпущено на условиях лицензии MIT.

Авторские права (c) 2024 Mithril Contributors