Статью написать не просто, поставь лайк, брат
Сосредоточьтесь на обмене исходным кодом Vue, статья разделена на общеупотребительную и исходную версии, общеупотребительная версия помогает понять принцип работы, исходная версия помогает понять внутренние детали, давайте учиться вместе Исследование на основе версии Vue【2.5.17】
Если вы находите типографику уродливой, пожалуйста, нажмитессылка нижеили потяните внизОбратите внимание на публичный аккаунттоже хорошо
[Vue Principle] Diff — процесс сравнения в исходной версии
Сегодня я, наконец, начал исследовать ключевой момент Vue, обновляющий DOM, а именно Diff.
Контента Diff не так уж и много, но если вы хотите рассказать о нем подробно, вам придется много говорить, и вам нужно сопоставить много картинок.
Это последняя статья Диффа, самая важная и самая подробная.
Поэтому в этой статье много контента, поэтому начнем с обзора контента.
1、分析 Diff 源码比较步骤
2、个人思考为什么如此比较
3、写个例子,一步步走个Diff 流程
Статья очень длинная и очень подробная.Если вас интересует этот контент, рекомендуется также читать исходный код во время чтения.Если вы пока не понимаете этот контент, вы можете прочитать народную версию, которая делает не включать исходный код в первую очередь.Diff - Народная версия
Начнем наш текст
в предыдущем постеDiff - Исходная версия diff от нового экземпляра для запуска, мы изучили, как работает Vue, от создания экземпляра до запуска сравнения
У вас все равно должно сложиться впечатление, что важной функцией, участвующей в Diff, является createPatchFunciton.
var patch = createPatchFunction();
Vue.prototype.__patch__ = patch
Итак, давайте посмотрим на эту функцию
createPatchFunction
function createPatchFunction() {
return function patch(
oldVnode, vnode, parentElm, refElm
) {
// 没有旧节点,直接生成新节点
if (!oldVnode) {
createElm(vnode, parentElm, refElm);
}
else {
// 且是一样 Vnode
if (sameVnode(oldVnode, vnode)) {
// 比较存在的根节点
patchVnode(oldVnode, vnode);
}
else {
// 替换存在的元素
var oldElm = oldVnode.elm;
var _parentElm = oldElm.parentNode
// 创建新节点
createElm(vnode, _parentElm, oldElm.nextSibling);
// 销毁旧节点
if (_parentElm) {
removeVnodes([oldVnode], 0, 0);
}
}
}
return vnode.elm
}
}
Что делает эта функция
Чем отличается новая нода от старой ноды, то полное обновление
Итак, вы видите получение oldVnode и vnode
Процесс обработки делится на
1、没有旧节点
2、旧节点 和 新节点 自身一样(不包括其子节点)
3、旧节点 和 新节点自身不一样
Давайте кратко рассмотрим эти три процесса.
1 нет старого узла
Старого узла нет, а значит страница только инициализирована, на данный момент сравнивать вообще не нужно.
Все новые напрямую, поэтому просто вызовите createElm
2 Старый узел такой же, как и сам новый узел
Судя по одинаковым ли нодам через sameVnode эта функция упоминалась в предыдущей статье
Когда старый узел и новый узел совпадают, вызовите patchVnode напрямую для обработки этих двух узлов.
patchVnode расскажет об этой функции ниже
Прежде чем говорить о patchVnode, давайте подумаем, что делает эта функция?
Что нам нужно делать, когда два Vnode сами по себе одинаковы?
Прежде всего, так же, как и он сам, мы можем просто понять, что два атрибута Vnode такие же, как тег и ключ
Затем мы не знаем, совпадают ли его дочерние узлы, поэтому нам определенно нужно сравнить дочерние узлы.
Поэтому одной из функций patchVnode является сравнение дочерних узлов
3 Старый узел и новый узел сами по себе не совпадают
Когда два узла не совпадают, это нетрудно понять, создайте новый узел напрямую, удалите старый узел
patchVnode
В предыдущей функции createPatchFunction есть функция patchVnode
Мы подумали, что одной из функций этой функции является сравнение потомков двух Vnodes.
Это то, что мы думаем, мы можем сначала перейти к исходному коду
function patchVnode(oldVnode, vnode) {
if (oldVnode === vnode) return
var elm = vnode.elm = oldVnode.elm;
var oldCh = oldVnode.children;
var ch = vnode.children;
// 更新children
if (!vnode.text) {
// 存在 oldCh 和 ch 时
if (oldCh && ch) {
if (oldCh !== ch)
updateChildren(elm, oldCh, ch);
}
// 存在 newCh 时,oldCh 只能是不存在,如果存在,就跳到上面的条件了
else if (ch) {
if (oldVnode.text) elm.textContent = '';
for (var i = 0; i <= ch.length - 1; ++i) {
createElm(
ch[i],elm, null
);
}
}
else if (oldCh) {
for (var i = 0; i<= oldCh.length - 1; ++i) {
oldCh[i].parentNode.removeChild(el);
}
}
else if (oldVnode.text) {
elm.textContent = '';
}
}
else if (oldVnode.text !== vnode.text) {
elm.textContent = vnode.text;
}
}
Давайте проанализируем эту функцию сейчас
Да, как мы и думали, эта функция сравнивает и обрабатывает дочерние узлы.
В общем, что делает эта функция
1. Если Vnode является текстовым узлом, обновите текст (текстовый узел не имеет дочерних узлов)
2. Если у Vnode есть дочерние узлы, сравните и обновите дочерние узлы.
Еще одно резюме состоит в том, что эта функция в основном выполняет два вида обработки суждений.
1. Является ли Vnode текстовым узлом
2. Есть ли у Vnode дочерние узлы?
Давайте взглянем на подробный анализ этих шагов
1 Vnode — это текстовый узел
Когда VNode имеет текстовый атрибут, это доказывает, что Vnode является текстовым узлом.
Сначала мы можем взглянуть на то, как выглядит Vnode текстового типа.
Поэтому, когда Vnode является текстовым узлом, все, что вам нужно сделать, это обновить текст.
Есть также две процедуры
1. Когда новый VNode.text существует и отличается от старого VNode.text
Напрямую обновить текстовое содержимое этого DOM
elm.textContent = vnode.text;
Примечание: textContent — это атрибут реального DOM, который сохраняет текст DOM, поэтому обновляйте этот атрибут напрямую.
2. Текст нового Vnode пуст, напрямую назначьте текст DOM пустому
elm.textContent = '';
2 Vnode имеет дочерние узлы
Когда у Vnode есть дочерние узлы, его необходимо сравнить, чтобы завершить обновление, потому что он не знает, совпадают ли дочерние узлы старого и нового узлов.
Есть три процедуры
1. И у старых, и у новых узлов есть дочерние узлы, и они не совпадают
2. Только новые узлы
3. Только старые узлы
Последние два узла, я думаю, разберутся все, но давайте поговорим об этом
1 только новые узлы
Есть только новые узлы, старых узлов нет, поэтому сравнения нет, все узлы совершенно новые
Так что просто создавайте все новые.Новый относится к созданию всех новых DOM и добавлению их к родительскому узлу.
2 только старые узлы
Есть только старые узлы и нет новых узлов, что указывает на то, что обновленная страница, все старые узлы исчезли
Тогда все, что вам нужно сделать, это удалить все старые узлы.
То есть удалить DOM напрямую
3 И старые, и новые узлы имеют дочерние узлы, и они не совпадают
Эй, есть новая функция, updateChildren
Предупреждение, эта функция очень важна.Это основной модуль Diff и содержит идею Diff.
Это может немного сбивать с толку, но не бойтесь, я верю, что благодаря своим исследованиям я смогу немного понять
Точно так же давайте подумаем о роли updateChildren
Помните условия, когда существуют и новый узел, и старый узел, как их сравнить, чтобы узнать, в чем разница?
О да, используя обход, сравните новый дочерний узел со старым дочерним узлом один за другим
Если то же самое, не обновляйте, если нет, обновляйте
Давайте проверим наши идеи и изучим исходный код updateChildren.
updateChildren
Эта функция очень длинная, но это не сложно. Он разделен на несколько процедур обработки, но это может быть немного запутанным.
Или вы можете сначала пропустить исходный код и посмотреть анализ, или просто посмотреть анализ и посмотреть исходный код.
function updateChildren(parentElm, oldCh, newCh) {
var oldStartIdx = 0;
var oldEndIdx = oldCh.length - 1;
var oldStartVnode = oldCh[0];
var oldEndVnode = oldCh[oldEndIdx];
var newStartIdx = 0;
var newEndIdx = newCh.length - 1;
var newStartVnode = newCh[0];
var newEndVnode = newCh[newEndIdx];
var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
// 不断地更新 OldIndex 和 OldVnode ,newIndex 和 newVnode
while (
oldStartIdx <= oldEndIdx &&
newStartIdx <= newEndIdx
) {
if (!oldStartVnode) {
oldStartVnode = oldCh[++oldStartIdx];
}
else if (!oldEndVnode) {
oldEndVnode = oldCh[--oldEndIdx];
}
// 旧头 和新头 比较
else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
}
// 旧尾 和新尾 比较
else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
}
// 旧头 和 新尾 比较
else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode);
// oldStartVnode 放到 oldEndVnode 后面,还要找到 oldEndValue 后面的节点
parentElm.insertBefore(
oldStartVnode.elm,
oldEndVnode.elm.nextSibling
);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
}
// 旧尾 和新头 比较
else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode);
// oldEndVnode 放到 oldStartVnode 前面
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
}
// 单个新子节点 在 旧子节点数组中 查找位置
else {
// oldKeyToIdx 是一个 把 Vnode 的 key 和 index 转换的 map
if (!oldKeyToIdx) {
oldKeyToIdx = createKeyToOldIdx(
oldCh, oldStartIdx, oldEndIdx
);
}
// 使用 newStartVnode 去 OldMap 中寻找 相同节点,默认key存在
idxInOld = oldKeyToIdx[newStartVnode.key]
// 新孩子中,存在一个新节点,老节点中没有,需要新建
if (!idxInOld) {
// 把 newStartVnode 插入 oldStartVnode 的前面
createElm(
newStartVnode,
parentElm,
oldStartVnode.elm
);
}
else {
// 找到 oldCh 中 和 newStartVnode 一样的节点
vnodeToMove = oldCh[idxInOld];
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode);
// 删除这个 index
oldCh[idxInOld] = undefined;
// 把 vnodeToMove 移动到 oldStartVnode 前面
parentElm.insertBefore(
vnodeToMove.elm,
oldStartVnode.elm
);
}
// 只能创建一个新节点插入到 parentElm 的子节点中
else {
// same key but different element. treat as new element
createElm(
newStartVnode,
parentElm,
oldStartVnode.elm
);
}
}
// 这个新子节点更新完毕,更新 newStartIdx,开始比较下一个
newStartVnode = newCh[++newStartIdx];
}
}
// 处理剩下的节点
if (oldStartIdx > oldEndIdx) {
var newEnd = newCh[newEndIdx + 1]
refElm = newEnd ? newEnd.elm :null;
for (; newStartIdx <= newEndIdx; ++newStartIdx) {
createElm(
newCh[newStartIdx], parentElm, refElm
);
}
}
// 说明新节点比对完了,老节点可能还有,需要删除剩余的老节点
else if (newStartIdx > newEndIdx) {
for (; oldStartIdx<=oldEndIdx; ++oldStartIdx) {
oldCh[oldStartIdx].parentNode.removeChild(el);
}
}
}
В первую очередь необходимо уточнить, чем занимается эта функция
Новый дочерний узел и старый дочерний узел обрабатываются, и цикл проходит и сравнивает их один за другим.
Как прокрутить?
1. Используйте пока
2. И новый, и старый массивы узлов настроены с двумя индексами в начале и в конце.
Два индекса нового узла: newStartIdx, newEndIdx
Два индекса старых узлов: oldStartIdx, oldEndIdx
Траверс в виде двух сторон, окруженных серединой
После завершения сравнения дочерних узлов головы startIdx увеличивается на 1.
После завершения сравнения дочерних узлов в хвосте endIdex уменьшается на 1.
Пока пройден один из массивов (startIdx Процесс обработки исходного кода разделен на две части. 1. Сравните старые и новые дочерние узлы 2. После сравнения обработайте оставшиеся узлы Давайте объясним эти два процесса один за другим Примечание. Здесь есть два массива, один из которых представляет собой новый массив sub-Vnode, а другой — старый массив sub-Vnode. Во время сравнения в два массива не будет внесено никаких изменений (например, без вставки или удаления их дочерних элементов). И весь процесс сравнения напрямую вставляет и удаляет реальную страницу DOM. Найдите один и тот же дочерний узел в старом и новом дочерних узлах, попробуйте использовать перемещение вместо нового для обновления DOM. Будут созданы только новые, если они действительно отличаются. Сначала подумайте, не перемещайте DOM Во-вторых, рассмотрите возможность перемещения DOM Окончательное рассмотрение, новый/удаленный DOM Не могу двигаться, постарайся не двигаться. Если он не работает, переместите его, если он не работает, то создайте новый. Начнем с логики сравнения в исходном коде. Пять логик сравнения следующие: Давайте проанализируем эти пять логик сравнения Когда две старые и новые головки одинаковые, ничего делать не нужно. В соответствии с нашим первым шагом обновление завершается без перемещения DOM. Но см. предложение, patchVnode Просто для продолжения обработки дочерних узлов этих двух одинаковых узлов или для обновления текста Поскольку мы не рассматриваем многоуровневую структуру DOM, если новый и старый заголовки совпадают, это конец. Можно сразу перейти к следующему циклу Та же обработка, что и при прямом контакте Хвост и хвост одинаковые, переходим сразу к следующей петле Этот шаг не соответствует отказу от перемещения DOM, поэтому вы можете перемещать только DOM Исходный код такой Перемещаясь по положению нового дочернего узла, старая головка находится в конце нового дочернего узла. Так что поместите дом oldStartVnode за oldEndVnode Но поскольку нет возможности поставить дом позади, можно использовать только insertBefore. То есть ставится перед узлом после oldEndVnode Схема вот такая затем обновите оба индекса То же самое не соответствует не перемещать DOM, вы можете только перемещать DOM Поместите DOM oldEndVnode непосредственно перед текущим oldStartVnode.elm. Схема вот такая затем обновите оба индекса Когда ни одна из предыдущих четырех логик сравнения не работает, это последний способ справиться с этим. Возьмите дочерний элемент нового дочернего узла, пройдите напрямую по массиву старых дочерних узлов и найдите тот же самый узел. Процесс, вероятно, 1. Создайте таблицу сопоставления со старым массивом дочерних узлов с vnode.key в качестве ключа. 2. Получите дочерний элемент в новом массиве дочерних узлов и оцените, находится ли его ключ на приведенной выше карте. 3. Если он не существует, создайте новый DOM 4. Существовать, продолжать ли судить о том же Vnode Поговорим об этом подробнее ниже Функция этой таблицы сопоставления в основном состоит в том, чтобы определить, какие старые дочерние узлы существуют. Например, ваш старый массив дочерних узлов Создайте таблицу сопоставления oldKeyToIdx с помощью createKeyToOldIdx {vnodeKey: индекс массива} Имя свойства — vnode.key, а значение свойства — позиция vnode в дочерних элементах. Вот и все (конкретный исходный код см. в предыдущей статье Diff - связанные вспомогательные функции версии исходного кода) Получите дочерний Vnode в новом дочернем узле, а затем получите его ключ Чтобы сопоставить таблицу сопоставления, чтобы определить, есть ли одинаковые узлы Создайте DOM напрямую и вставьте его перед oldStartVnode Найдите старый дочерний узел, а затем оцените, является ли он тем же Vnode, что и новый дочерний узел. Если то же самое, перейдите прямо к началу oldStartVnode. Если отличается, создайте и вставьте непосредственно перед oldStartVnode Выше мы говорили, что процесс сравнения дочерних узлов делится на два 1. Сравните старые и новые дочерние узлы 2. После сравнения обработайте оставшиеся узлы Сравнение старого и нового подузлов было сказано выше, и теперь идет другой процесс для сравнения оставшихся узлов, подробности см. Ниже. В updateChildren после сравнения старого и нового массивов в массиве могут остаться какие-то узлы, которые не были обработаны, поэтому здесь требуется унифицированная обработка. Новый дочерний узел пройден, а старый дочерний узел все еще может быть оставлен. Поэтому нам нужно пакетно удалить старые узлы, которые могут быть оставлены! Это обход оставшихся узлов и удаление DOM один за другим. Старый дочерний узел был пройден, и у нового дочернего узла могут быть остатки Итак, нам нужно разобраться с оставшимися новыми дочерними узлами. Очевидно, что оставшиеся новые дочерние узлы не существуют в старых дочерних узлах, поэтому создаются все новые. А вот у нового проблема, то есть куда его вставлять? Так что refElm в нем стал подозрительным, посмотрите исходники refElm получает узел после newEndIdx Узел, который в данный момент не обрабатывается, называется newEndIdx. То есть, если узел newEndIdx+1 существует, он должен быть обработан. Если newEndIdx не был перемещен и всегда является последним битом, то newCh[newEndIdx + 1] не существует. Тогда refElm пуст, тогда все оставшиеся новые узлы добавляются в конец дочернего элемента родительского узла, что эквивалентно Если newEndIdx был перемещен, он добавляется один за другим перед refElm, что эквивалентно Как показано Мы закончили весь контент Diff, и каждый должен понять идею Diff. Но я заставил себя задуматься над вопросом, который Ниже приведены сугубо личные непристойные мысли, без официальной сертификации, только для справки. Все наши сравнения заключаются в том, чтобы найти те же дочерние узлы, что и новый дочерний узел, и старый дочерний узел. И целью нашей сравнительной обработки является 1, не могу двигаться, постарайся не двигаться 2. Нет другого выбора, кроме как переехать 3. Не работает, создавай или удаляй Прежде всего, в начале сравнения это должно быть в соответствии с нашей первой целью не двигаться, чтобы найти узлы, которые нельзя перемещать. А голова, хвост и хвост больше соответствуют нашей первой цели, так она появилась в начале, хм, в этом можно разобраться Затем мы переходим к нашей второй цели, согласно подходу updateChildren, есть Сравнение старой головы и новой головы, сравнение старой головы и новой головы, сравнение с одним поиском Я начинаю задаваться вопросом, а? Я знаю, что сравнения «голова к хвосту» предназначены для движения, но почему это сравнение происходит? Очевидно, я могу использовать один метод поиска для выполнения всех операций перемещения? Я долго думал о связи между головой и хвостом, и думал, что это может быть, чтобы избежать потребления в экстремальных ситуациях? ? Например, когда мы удаляем сравнение головы и хвоста, все используют один метод поиска. Если головной и хвостовой узлы совпадают, необходимо пройти узел, чтобы найти конец, чтобы найти тот же узел. Это слишком дорого, поэтому здесь добавлено прямое сравнение, чтобы исключить операции потребления, вызванные экстремальными ситуациями. Конечно, это только моя личная идея, только для справки, хотя при этом я сделал пример теста. В дочерний узел добавляется подэлемент b div с двумя прямыми сравнениями. Используйте Vue для обновления, сравните скорость обновления, затем обновите десять раз, рассчитайте среднее 1. Все используют один поиск, время 0,91 мс 2. Добавьте сравнение головы и хвоста, время 0,853 мс. Это действительно быстрее Я считаю, что после такой длинной статьи все еще не собрали в уме все пункты знаний, и могут быть еще немного расплывчаты в отношении всего процесса Все в порядке, давайте сейчас возьмем пример, шаг за шагом пройдем процесс и завершим обновление. Следующие узлы, зеленый означает необработанные, серый означает обработанный, светло-зеленый означает обрабатываемый, красный означает недавно вставленный, следующим образом Теперь необходимо обновить Vue.Есть следующие две группы новых и старых дочерних узлов, которые необходимо сравнить, чтобы определить, какие узлы необходимо обновить. индекс обновления, newStartIdx++, oldStartIdx++ Начать следующий раунд обработки обновить индекс, newEndIdx--, oldStartIdx++ Начать следующий раунд обработки Индекс обновления, OldendidX-, NewstartIdx ++ Начать следующий раунд сравнения Не могу найти то же самое, напрямую создайте и вставьте его перед oldStartVnode индекс обновления, newStartIdx++ В этот момент newStartIdx> newEndIdx завершает цикл На данный момент в старом массиве Vnode и oldStartIdx, и oldEndIdx указывают на один и тот же узел, поэтому просто удалите узел oldVnode-4. ОК, относительно завершил все процессы Да, Diff-контент закончился, спасибо за просмотр Ввиду моих ограниченных возможностей неизбежно будут упущения и ошибки.Пожалуйста, потерпите меня.Если есть какие-либо неуместные описания, пожалуйста, свяжитесь со мной в фоновом режиме, большое спасибо.1 Сравните старые и новые дочерние узлы
Давайте проясним, какова цель сравнения?
Сравните шаги плана обновления
1、旧头 == 新头
2、旧尾 == 新尾
3、旧头 == 新尾
4、旧尾 == 新头
5、单个查找
1 старая голова == новая голова
sameVnode(oldStartVnode, newStartVnode)
newStartIdx ++ , oldStartIdx ++
2 старый хвост == новый хвост
sameVnode(oldEndVnode, newEndVnode)
newEndIdx ++ , oldEndIdx ++
3 старая голова == новый хвост
sameVnode(oldStartVnode, newEndVnode)
Как двигаться?
parentElm.insertBefore(
oldStartVnode.elm,
oldEndVnode.elm.nextSibling
);
oldStartIdx++,newEndIdx--
4 старый хвост == новая голова
sameVnode(oldEndVnode, newStartVnode)
Как двигаться?
parentElm.insertBefore(
oldEndVnode.elm,
oldStartVnode.elm
);
oldEndIdx--,newStartIdx++
5 Поиск одного обхода
1 Создайте таблицу карт
[{
tag:"div", key:1
},{
tag:"strong", key:2
},{
tag:"span", key:4
}]
oldKeyToIdx = {
1:0,
2:1,
4:2
}
2 Определите, существует ли новый дочерний узел в массиве старых дочерних узлов.
oldKeyToIdx[newStartVnode.key]
3 не существует в старом массиве дочерних узлов
createElm(newStartVnode, parentElm, oldStartVnode.elm);
4 существует в старом массиве дочерних узлов
Обработать возможные оставшиеся узлы
1 Новый дочерний узел пройден
newStartIdx > newEndIdx
for (; oldStartIdx <= oldEndIdx; ++oldStartIdx) {
oldCh[oldStartIdx]
.parentNode
.removeChild(el);
}
2 Старый дочерний узел пройден
oldStartIdx > oldEndIdx
for (; newStartIdx <= newEndIdx; ++newStartIdx) {
createElm(
newCh[newStartIdx],
parentElm,
refElm
);
}
var newEnd = newCh[newEndIdx + 1]
refElm = newEnd ? newEnd.elm :null;
for (; newStartIdx <= newEndIdx; ++newStartIdx) {
parentElm.appendChild(
newCh[newStartIdx]
);
}
for (; newStartIdx <= newEndIdx; ++newStartIdx) {
parentElm.insertBefore(
newCh[newStartIdx] ,
refElm
);
}
Подумайте, почему сравнение
Зачем так сравнивать?
Как сказать?
oldCh = ['header','span','div','b']
newCh = ['sub','b','div','strong']
идти процесс
1 прямое сравнение, узлы одинаковые, перемещать не нужно, просто обновите индекс
После ряда суждений [Old Head 2] и [New End 2] совпадают и перемещаются непосредственно в конец oldEndVnode.
3 После серии суждений [Old Head 2] и [New Tail 2] совпадают и перемещаются прямо к началу oldStartVnode.
4 Остался только один узел, иди на Страшный суд, одиночный поиск
5 Пакетное удаление старых узлов, которые могут остаться
наконец