предисловие
В этой статье в основном написан исходный код Vue2.0 от руки.принцип алгоритма сравнения
В предыдущей статье мы в основном представили VueПринцип асинхронного обновленияЭто оптимизация производительности обновления представления. Эта статья также посвящена оптимизации обновления рендеринга. Когда шаблон изменяется, мы можем использовать алгоритм сравнения для сравнения старого и нового виртуального DOM, чтобы увидеть, можно ли выполнить повторное использование узлов. Алгоритм сравнения также является популярным тестовым сайтом для Vue-интервью.
Для людей:
1. Хотите иметь глубокое понимание исходного кода vue для лучшего ежедневного развития бизнеса
2. Хотите владеть исходным кодом vue framework в резюме (больше не боитесь вопросов интервьюера о серийных убийцах, ха-ха)
3. Учащиеся, у которых нет времени смотреть официальный исходный код или им трудно понять с первого взгляда исходный код
текст
<script>
// Vue实例化
let vm = new Vue({
el: "#app",
data() {
return {
a: 123,
};
},
template: `<div id="a">hello {{a}}</div>`,
});
setTimeout(() => {
vm.a = 1;
}, 1000);
</script>
Давайте подумаем, если мы изменим значение a в шаблоне через 1 секунду после завершения первоначального рендеринга, что сделает vue, чтобы отобразить последнее значение?
1. Удалите последний визуализированный реальный дом и повторно визуализируйте новый узел дома, чтобы применить последнее значение.
2. Повторно используйте старый дом и измените значение textContent внутреннего текстового узла.
Из этих двух схем накладные расходы на производительность последней, очевидно, меньше.Давайте посмотрим, как vue использует алгоритм diff для выполнения обновлений рендеринга.
1. Патч основного метода рендеринга переписан
// src/vdom/patch.js
export function patch(oldVnode, vnode) {
const isRealElement = oldVnode.nodeType;
if (isRealElement) {
// oldVnode是真实dom元素 就代表初次渲染
} else {
// oldVnode是虚拟dom 就是更新过程 使用diff算法
if (oldVnode.tag !== vnode.tag) {
// 如果新旧标签不一致 用新的替换旧的 oldVnode.el代表的是真实dom节点--同级比较
oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el);
}
// 如果旧节点是一个文本节点
if (!oldVnode.tag) {
if (oldVnode.text !== vnode.text) {
oldVnode.el.textContent = vnode.text;
}
}
// 不符合上面两种 代表标签一致 并且不是文本节点
// 为了节点复用 所以直接把旧的虚拟dom对应的真实dom赋值给新的虚拟dom的el属性
const el = (vnode.el = oldVnode.el);
updateProperties(vnode, oldVnode.data); // 更新属性
const oldCh = oldVnode.children || []; // 老的儿子
const newCh = vnode.children || []; // 新的儿子
if (oldCh.length > 0 && newCh.length > 0) {
// 新老都存在子节点
updateChildren(el, oldCh, newCh);
} else if (oldCh.length) {
// 老的有儿子新的没有
el.innerHTML = "";
} else if (newCh.length) {
// 新的有儿子
for (let i = 0; i < newCh.length; i++) {
const child = newCh[i];
el.appendChild(createElm(child));
}
}
}
}
Мы непосредственно видим, что ветвь ELSE представляет собой процесс обновления рендеринга, который можно разделить на следующие шаги.
1.diff сравнивает только на одном уровне
2. В соответствии с различными ситуациями новых и старых дочерних узлов vnode они обрабатываются отдельно.
2.updateProperties обновить свойства
// src/vdom/patch.js
// 解析vnode的data属性 映射到真实dom上
function updateProperties(vnode, oldProps = {}) {
const newProps = vnode.data || {}; //新的vnode的属性
const el = vnode.el; // 真实节点
// 如果新的节点没有 需要把老的节点属性移除
for (const k in oldProps) {
if (!newProps[k]) {
el.removeAttribute(k);
}
}
// 对style样式做特殊处理 如果新的没有 需要把老的style值置为空
const newStyle = newProps.style || {};
const oldStyle = oldProps.style || {};
for (const key in oldStyle) {
if (!newStyle[key]) {
el.style[key] = "";
}
}
// 遍历新的属性 进行增加操作
for (const key in newProps) {
if (key === "style") {
for (const styleName in newProps.style) {
el.style[styleName] = newProps.style[styleName];
}
} else if (key === "class") {
el.className = newProps.class;
} else {
// 给这个元素添加属性 值就是对应的值
el.setAttribute(key, newProps[key]);
}
}
}
Сравните новые и старые vnodes для обновления атрибутов
3.updateChildren обновить базовый метод дочернего узла-diff
// src/vdom/patch.js
// 判断两个vnode的标签和key是否相同 如果相同 就可以认为是同一节点就地复用
function isSameVnode(oldVnode, newVnode) {
return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key;
}
// diff算法核心 采用双指针的方式 对比新老vnode的儿子节点
function updateChildren(parent, oldCh, newCh) {
let oldStartIndex = 0; //老儿子的起始下标
let oldStartVnode = oldCh[0]; //老儿子的第一个节点
let oldEndIndex = oldCh.length - 1; //老儿子的结束下标
let oldEndVnode = oldCh[oldEndIndex]; //老儿子的起结束节点
let newStartIndex = 0; //同上 新儿子的
let newStartVnode = newCh[0];
let newEndIndex = newCh.length - 1;
let newEndVnode = newCh[newEndIndex];
// 根据key来创建老的儿子的index映射表 类似 {'a':0,'b':1} 代表key为'a'的节点在第一个位置 key为'b'的节点在第二个位置
function makeIndexByKey(children) {
let map = {};
children.forEach((item, index) => {
map[item.key] = index;
});
return map;
}
// 生成的映射表
let map = makeIndexByKey(oldCh);
// 只有当新老儿子的双指标的起始位置不大于结束位置的时候 才能循环 一方停止了就需要结束循环
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
// 因为暴力对比过程把移动的vnode置为 undefined 如果不存在vnode节点 直接跳过
if (!oldStartVnode) {
oldStartVnode = oldCh[++oldStartIndex];
} else if (!oldEndVnode) {
oldEndVnode = oldCh[--oldEndIndex];
} else if (isSameVnode(oldStartVnode, newStartVnode)) {
// 头和头对比 依次向后追加
patch(oldStartVnode, newStartVnode); //递归比较儿子以及他们的子节点
oldStartVnode = oldCh[++oldStartIndex];
newStartVnode = newCh[++newStartIndex];
} else if (isSameVnode(oldEndVnode, newEndVnode)) {
//尾和尾对比 依次向前追加
patch(oldEndVnode, newEndVnode);
oldEndVnode = oldCh[--oldEndIndex];
newEndVnode = newCh[--newEndIndex];
} else if (isSameVnode(oldStartVnode, newEndVnode)) {
// 老的头和新的尾相同 把老的头部移动到尾部
patch(oldStartVnode, newEndVnode);
parent.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling); //insertBefore可以移动或者插入真实dom
oldStartVnode = oldCh[++oldStartIndex];
newEndVnode = newCh[--newEndIndex];
} else if (isSameVnode(oldEndVnode, newStartVnode)) {
// 老的尾和新的头相同 把老的尾部移动到头部
patch(oldEndVnode, newStartVnode);
parent.insertBefore(oldEndVnode.el, oldStartVnode.el);
oldEndVnode = oldCh[--oldEndIndex];
newStartVnode = newCh[++newStartIndex];
} else {
// 上述四种情况都不满足 那么需要暴力对比
// 根据老的子节点的key和index的映射表 从新的开始子节点进行查找 如果可以找到就进行移动操作 如果找不到则直接进行插入
let moveIndex = map[newStartVnode.key];
if (!moveIndex) {
// 老的节点找不到 直接插入
parent.insertBefore(createElm(newStartVnode), oldStartVnode.el);
} else {
let moveVnode = oldCh[moveIndex]; //找得到就拿到老的节点
oldCh[moveIndex] = undefined; //这个是占位操作 避免数组塌陷 防止老节点移动走了之后破坏了初始的映射表位置
parent.insertBefore(moveVnode.el, oldStartVnode.el); //把找到的节点移动到最前面
patch(moveVnode, newStartVnode);
}
}
}
// 如果老节点循环完毕了 但是新节点还有 证明 新节点需要被添加到头部或者尾部
if (newStartIndex <= newEndIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
// 这是一个优化写法 insertBefore的第一个参数是null等同于appendChild作用
const ele =
newCh[newEndIndex + 1] == null ? null : newCh[newEndIndex + 1].el;
parent.insertBefore(createElm(newCh[i]), ele);
}
}
// 如果新节点循环完毕 老节点还有 证明老的节点需要直接被删除
if (oldStartIndex <= oldEndIndex) {
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
let child = oldCh[i];
if (child != undefined) {
parent.removeChild(child.el);
}
}
}
}
Этот код очень длинный, но его можно легко разделить на следующие пункты для понимания
1. Используйте двойное движение указателя для сравнения новых и старых узлов.
2. Используйте isSameVnode, чтобы определить, являются ли голова, хвост, голова и хвост нового и старого дочерних узлов одним и тем же узлом. хвостовая головка) операция
3. Если все не равны, выполните сравнение грубой силы. Если вы найдете таблицу сопоставления с использованием ключа и индекса, переместите старый дочерний узел на передний план. Если вы не можете его найти, вставьте его напрямую.
4. Рекурсивная обработка патчей старых дочерних узлов
5. Наконец, удалите как можно больше старых дочерних узлов и добавьте как можно больше новых дочерних узлов в соответствующую позицию.
4. Модернизация метода обновления рендеринга прототипа _update
// src/lifecycle.js
export function lifecycleMixin(Vue) {
// 把_update挂载在Vue的原型
Vue.prototype._update = function (vnode) {
const vm = this;
const prevVnode = vm._vnode; // 保留上一次的vnode
vm._vnode = vnode;
if (!prevVnode) {
// patch是渲染vnode为真实dom核心
vm.$el = patch(vm.$el, vnode); // 初次渲染 vm._vnode肯定不存在 要通过虚拟节点 渲染出真实的dom 赋值给$el属性
} else {
vm.$el = patch(prevVnode, vnode); // 更新时把上次的vnode和这次更新的vnode穿进去 进行diff算法
}
};
}
Измените метод _update, чтобы сохранить последний узел vnode в _vnode экземпляра Vue для исправления, чтобы сравнить старый и новый виртуальный дом.
5. Интеллект-карта алгоритма сравнения
резюме
На данный момент исходный код алгоритма сравнения Vue завершен. Все в основном понимают весь процесс сравнения. Объем кода и сложность этой статьи немного слишком велики. Рекомендуется прочитать ее несколько раз, потому что алгоритм сравнения очень важная точка знаний в Vue и общий тестовый сайт для интервью.Вы можете посмотреть на интеллект-карту.Напишите основной код самостоятельно
Наконец, если вы найдете эту статью полезной, помнитеКак СанлианБольшое спасибо!
Ссылка на сериал (будет обновлена позже)
- Рукописный Vue2.0 Исходный код (1) - Принцип отзывчивых данных
- Рукописный исходный код Vue2.0 (2) - принцип компиляции шаблона
- Рукописный исходный код Vue2.0 (3) — принцип начального рендеринга
- Рукописный исходный код Vue2.0 (четыре) — принцип обновления рендеринга
- Рукописный исходный код Vue2.0 (5) - принцип асинхронного обновления
- Рукописный исходный код Vue2.0 (6) - принцип алгоритма diff
- Рукописный исходный код Vue2.0 (7) - Принцип микширования
- Рукописный исходный код Vue2.0 (8)-компонентный принцип
- Рукописный исходный код Vue2.0 (9) - принцип свойства прослушивания
- Рукописный исходный код Vue2.0 (10) — Принцип вычисляемых свойств
- Рукописный исходный код Vue2.0 (11) — принцип глобального API
- Самые полные вопросы интервью Vue + подробные ответы
- Рукописный исходный код vue-router
- Рукописный исходный код vuex
- Рукописный исходный код vue3.0
Группа передовых рыболовных технологий Brother Shark
Приветствую всех на технических биржахСвязь