автор:steinsиз Thunder Front End
Оригинальный адрес:GitHub.com/Лин Руи1994/…
С ростом популярности таких фреймворков, как React Vue, виртуальный DOM становится все более и более популярным, snabbdom является одной из реализаций, и часть Virtual DOM версии Vue 2.x также модифицирована на основе snabbdom. Основной код библиотеки snabbdom состоит всего из более чем 200 строк, что очень подходит для читателей, которые хотят глубже понять реализацию Virtual DOM. Если вы не слышали о snabbdom, проверьте этоофициальная документация.
Почему выбирают снаббдом
- Основной код всего 200 строк, богатые тестовые примеры
- Мощная вставная система, крюковая система
- Vue использует snabbdom, и чтение snabbdom полезно для понимания реализации Vue.
Что такое виртуальный DOM
snabbdom — это реализация Virtual DOM, поэтому перед этим вам нужно знать, что такое Virtual DOM. С точки зрения непрофессионала, виртуальный DOM — это объект js, который является абстракцией реального DOM, сохраняет только некоторую полезную информацию и более легко описывает структуру дерева DOM. например, вsnabbdom
, заключается в определенииVNode
из:
export interface VNode {
sel: string | undefined;
data: VNodeData | undefined;
children: Array<VNode | string> | undefined;
elm: Node | undefined;
text: string | undefined;
key: Key | undefined;
}
export interface VNodeData {
props?: Props;
attrs?: Attrs;
class?: Classes;
style?: VNodeStyle;
dataset?: Dataset;
on?: On;
hero?: Hero;
attachData?: AttachData;
hook?: Hooks;
key?: Key;
ns?: string; // for SVGs
fn?: () => VNode; // for thunks
args?: Array<any>; // for thunks
[key: string]: any; // for any other 3rd party module
}
Как видно из приведенного выше определения, мы можем использовать объект js для описанияdom
структуру, можем ли мы сравнить объекты js в двух состояниях, записать их различия, а затем применить их к настоящему дереву dom? Ответ да, этоdiff
Основные шаги алгоритма следующие:
- Используйте объект js для описания структуры дерева dom, а затем используйте этот объект js, чтобы создать настоящее дерево dom и вставить его в документ.
- Когда состояние обновляется, сравните новый объект js со старым объектом js и получите разницу между двумя объектами.
- применить diff к реальному дому
Далее анализируем реализацию всего этого процесса.
Анализ исходного кода
Сначала начнем с простого примера и шаг за шагом проанализируем процесс выполнения всего кода.Ниже приведен официальный простой пример:
var snabbdom = require('snabbdom');
var patch = snabbdom.init([
// Init patch function with chosen modules
require('snabbdom/modules/class').default, // makes it easy to toggle classes
require('snabbdom/modules/props').default, // for setting properties on DOM elements
require('snabbdom/modules/style').default, // handles styling on elements with support for animations
require('snabbdom/modules/eventlisteners').default // attaches event listeners
]);
var h = require('snabbdom/h').default; // helper function for creating vnodes
var container = document.getElementById('container');
var vnode = h('div#container.two.classes', { on: { click: someFn } }, [
h('span', { style: { fontWeight: 'bold' } }, 'This is bold'),
' and this is just normal text',
h('a', { props: { href: '/foo' } }, "I'll take you places!")
]);
// Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode);
var newVnode = h('div#container.two.classes', { on: { click: anotherEventHandler } }, [
h('span', { style: { fontWeight: 'normal', fontStyle: 'italic' } }, 'This is now italic type'),
' and this is still just normal text',
h('a', { props: { href: '/bar' } }, "I'll take you places!")
]);
// Second `patch` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
во-первыхsnabbdom
модуль обеспечиваетinit
метод, который получает массив различныхmodule
, этот дизайн делает эту библиотеку более расширяемой, мы также можем реализовать свои собственныеmodule
, и вы можете ввести соответствующиеmodule
, например, если вам не нужно писатьclass
, то можно сразу поставитьclass
модуль снят.
перечислитьinit
метод вернетpatch
функция, эта функция принимает два аргумента, первый старыйvnode
узел илиdom
узел, второй параметр новыйvnode
узел, звонокpatch
Функция обновляет файл dom.vnode
можно сделать с помощьюh
функция для генерации. Он довольно прост в использовании, что и будет проанализировано в этой статье.
функция инициализации
export interface Module {
pre: PreHook;
create: CreateHook;
update: UpdateHook;
destroy: DestroyHook;
remove: RemoveHook;
post: PostHook;
}
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
// cbs 用于收集 module 中的 hook
let i: number,
j: number,
cbs = {} as ModuleHooks;
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
// 收集 module 中的 hook
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = [];
for (j = 0; j < modules.length; ++j) {
const hook = modules[j][hooks[i]];
if (hook !== undefined) {
(cbs[hooks[i]] as Array<any>).push(hook);
}
}
}
function emptyNodeAt(elm: Element) {
// ...
}
function createRmCb(childElm: Node, listeners: number) {
// ...
}
// 创建真正的 dom 节点
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
// ...
}
function addVnodes(
parentElm: Node,
before: Node | null,
vnodes: Array<VNode>,
startIdx: number,
endIdx: number,
insertedVnodeQueue: VNodeQueue
) {
// ...
}
// 调用 destory hook
// 如果存在 children 递归调用
function invokeDestroyHook(vnode: VNode) {
// ...
}
function removeVnodes(parentElm: Node, vnodes: Array<VNode>, startIdx: number, endIdx: number): void {
// ...
}
function updateChildren(parentElm: Node, oldCh: Array<VNode>, newCh: Array<VNode>, insertedVnodeQueue: VNodeQueue) {
// ...
}
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
// ...
}
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
// ...
};
}
вышеinit
Некоторые исходники метода, для удобства чтения, временно закомментируют конкретную реализацию некоторых методов, а затем подробно разберут, когда это будет полезно.
Вы можете узнать из параметров, что есть приемкаmodules
массив плюс необязательный параметрdomApi
, если не пройдет, будет использована нейтрализация браузераdom
Связанный API, вы можете увидеть подробностиздесь, этот дизайн также очень полезен, он позволяет пользователям настраивать API-интерфейсы, связанные с платформой, например, вы можете увидетьСвязанная реализация weex. Сначала здесь будетmodule
серединаhook
собирать, сохранять вcbs
середина. Затем определите различные функции, вы можете оставить их здесь, а затем вернутьpatch
функции, и ее конкретная логика здесь анализироваться не будет. такinit
Это конец.
h функция
В соответствии с процессом примера, давайте посмотримh
реализация метода
export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(sel: string, data: VNodeData, children: VNodeChildren): VNode;
export function h(sel: any, b?: any, c?: any): VNode {
var data: VNodeData = {},
children: any,
text: any,
i: number;
// 参数格式化
if (c !== undefined) {
data = b;
if (is.array(c)) {
children = c;
} else if (is.primitive(c)) {
text = c;
} else if (c && c.sel) {
children = [c];
}
} else if (b !== undefined) {
if (is.array(b)) {
children = b;
} else if (is.primitive(b)) {
text = b;
} else if (b && b.sel) {
children = [b];
} else {
data = b;
}
}
// 如果存在 children,将不是 vnode 的项转成 vnode
if (children !== undefined) {
for (i = 0; i < children.length; ++i) {
if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
}
}
// svg 元素添加 namespace
if (sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' && (sel.length === 3 || sel[3] === '.' || sel[3] === '#')) {
addNS(data, children, sel);
}
// 返回 vnode
return vnode(sel, data, children, text, undefined);
}
function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void {
data.ns = 'http://www.w3.org/2000/svg';
if (sel !== 'foreignObject' && children !== undefined) {
for (let i = 0; i < children.length; ++i) {
let childData = children[i].data;
if (childData !== undefined) {
addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel);
}
}
}
}
export function vnode(
sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | Text | undefined
): VNode {
let key = data === undefined ? undefined : data.key;
return {
sel: sel,
data: data,
children: children,
text: text,
elm: elm,
key: key
};
}
так какh
Последние два параметра функции являются необязательными, и существуют различные способы передачи, поэтому сначала будут форматироваться параметры, а затемchildren
свойства делать обработку, скорее всего не будетvnode
элементы вvnode
,еслиsvg
элемент, выполнит специальную обработку и, наконец, вернетvnode
объект.
патч функция
patch
функцияsnabbdom
ядро, вызывающееinit
вернет эту функцию, которая используется для выполненияdom
Что касается связанных обновлений, давайте взглянем на его конкретную реализацию.
function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node;
const insertedVnodeQueue: VNodeQueue = [];
// 调用 module 中的 pre hook
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
// 如果传入的是 Element 转成空的 vnode
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode);
}
// sameVnode 时 (sel 和 key相同) 调用 patchVnode
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
elm = oldVnode.elm as Node;
parent = api.parentNode(elm);
// 创建新的 dom 节点 vnode.elm
createElm(vnode, insertedVnodeQueue);
if (parent !== null) {
// 插入 dom
api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
// 移除旧 dom
removeVnodes(parent, [oldVnode], 0, 0);
}
}
// 调用元素上的 insert hook,注意 insert hook 在 module 上不支持
for (i = 0; i < insertedVnodeQueue.length; ++i) {
(((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);
}
// 调用 module post hook
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
return vnode;
}
function emptyNodeAt(elm: Element) {
const id = elm.id ? '#' + elm.id : '';
const c = elm.className ? '.' + elm.className.split(' ').join('.') : '';
return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
}
// key 和 selector 相同
function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}
позвонит первымmodule
изpre hook
, вам может быть интересно, почему вызов из каждого элемента неpre hook
, это потому, что элемент не поддерживаетpre hook
,иметь что-тоhook
не поддерживается вmodule
, вы можете проверить деталиДокументация здесь. Затем он определит, является ли первый переданный параметрvnode
типа, если нет, позвонюemptyNodeAt
затем преобразовать его вvnode
,emptyNodeAt
Конкретная реализация также очень проста, обратите внимание, что она зарезервирована только здесь.class
иstyle
, это иtoVnode
Реализация несколько иная, т.к. здесь не нужно сохранять много информации, типаprop
attribute
Ждать. Тогда позвониsameVnode
определить, одинаковые лиvnode
Node, конкретная реализация тоже очень проста, вот только суждениеkey
иsel
то же самое. Если так же, звонитеpatchVnode
, если не то же самое, позвонитcreateElm
создать новыйdom
node, то если есть родитель, вставьте его в дом, затем удалите старыйdom
узла для завершения обновления. Последний вызов элементаinsert hook
иmodule
Вверхpost hook
.
Дело в том, чтоpatchVnode
иcreateElm
функция, давайте сначала посмотримcreateElm
Функция, посмотрите, как она создаетсяdom
узла.
функция createElm
// 创建真正的 dom 节点
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
let i: any, data = vnode.data;
// 调用元素的 init hook
if (data !== undefined) {
if (isDef(i = data.hook) && isDef(i = i.init)) {
i(vnode);
data = vnode.data;
}
}
let children = vnode.children, sel = vnode.sel;
// 注释节点
if (sel === '!') {
if (isUndef(vnode.text)) {
vnode.text = '';
}
// 创建注释节点
vnode.elm = api.createComment(vnode.text as string);
} else if (sel !== undefined) {
// Parse selector
const hashIdx = sel.indexOf('#');
const dotIdx = sel.indexOf('.', hashIdx);
const hash = hashIdx > 0 ? hashIdx : sel.length;
const dot = dotIdx > 0 ? dotIdx : sel.length;
const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
const elm = vnode.elm = isDef(data) && isDef(i = (data as VNodeData).ns) ? api.createElementNS(i, tag)
: api.createElement(tag);
if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));
if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '));
// 调用 module 中的 create hook
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
// 挂载子节点
if (is.array(children)) {
for (i = 0; i < children.length; ++i) {
const ch = children[i];
if (ch != null) {
api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
}
}
} else if (is.primitive(vnode.text)) {
api.appendChild(elm, api.createTextNode(vnode.text));
}
i = (vnode.data as VNodeData).hook; // Reuse variable
// 调用 vnode 上的 hook
if (isDef(i)) {
// 调用 create hook
if (i.create) i.create(emptyNode, vnode);
// insert hook 存储起来 等 dom 插入后才会调用,这里用个数组来保存能避免调用时再次对 vnode 树做遍历
if (i.insert) insertedVnodeQueue.push(vnode);
}
} else {
// 文本节点
vnode.elm = api.createTextNode(vnode.text as string);
}
return vnode.elm;
}
Логика тут тоже очень понятная, первый элемент будет называтьсяinit hook
, то возможны три ситуации:
- Если текущий элемент является узлом комментария, он будет вызван
createComment
чтобы создать узел комментария и смонтировать его вvnode.elm
- Если селектора не существует, просто текст, вызовите
createTextNode
чтобы создать текст, затем смонтировать вvnode.elm
- Если есть селектор, селектор будет проанализирован, чтобы получить
tag
,id
иclass
, а затем позвонитеcreateElement
илиcreateElementNS
создать узел и смонтировать его вvnode.elm
. Тогда позвониmodule
Вверхcreate hook
, если он существуетchildren
, обойдите все дочерние узлы и рекурсивно вызовитеcreateElm
Создайтеdom
,пройти черезappendChild
монтировать на текущийelm
вверх, не существуетchildren
но существуетtext
, затем используйтеcreateTextNode
для создания текста. Наконец вызовите вызывающий элементcreate hook
и сохранить существуетinsert hook
изvnode
,так какinsert hook
нужно подождатьdom
на самом деле смонтироватьdocument
Он будет вызываться только выше, здесь для его сохранения используется массив, чтобы избежать необходимостиvnode
обход дерева.
Давайте взглянемsnabbdom
как это делаетсяvnode
изdiff
Да, эта частьVirtual DOM
Основной.
функция patchVnode
Что делает эта функция, так это передает дваvnode
Делатьdiff
, если есть обновление, сообщите об этомdom
начальство.
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
let i: any, hook: any;
// 调用 prepatch hook
if (isDef((i = vnode.data)) && isDef((hook = i.hook)) && isDef((i = hook.prepatch))) {
i(oldVnode, vnode);
}
const elm = (vnode.elm = oldVnode.elm as Node);
let oldCh = oldVnode.children;
let ch = vnode.children;
if (oldVnode === vnode) return;
if (vnode.data !== undefined) {
// 调用 module 上的 update hook
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
i = vnode.data.hook;
// 调用 vnode 上的 update hook
if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode);
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
// 新旧节点均存在 children,且不一样时,对 children 进行 diff
// thunk 中会做相关优化和这个相关
if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
} else if (isDef(ch)) {
// 旧节点不存在 children 新节点有 children
// 旧节点存在 text 置空
if (isDef(oldVnode.text)) api.setTextContent(elm, '');
// 加入新的 vnode
addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
// 新节点不存在 children 旧节点存在 children 移除旧节点的 children
removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
} else if (isDef(oldVnode.text)) {
// 旧节点存在 text 置空
api.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) {
// 更新 text
api.setTextContent(elm, vnode.text as string);
}
// 调用 postpatch hook
if (isDef(hook) && isDef((i = hook.postpatch))) {
i(oldVnode, vnode);
}
}
первый звонокvnode
Вверхprepatch hook
, если текущие дваvnode
Точно так же, возврат напрямую. Тогда позвониmodule
иvnode
Вверхupdate hook
. Затем он будет разделен на следующие ситуации, с которыми нужно иметь дело:
- оба существуют
children
и не то же самое, звонитеupdateChildren
- новый
vnode
существуетchildren
,Старыйvnode
не существуетchildren
, если старыйvnode
существуетtext
Сначала очистите, а потом звонитеaddVnodes
- новый
vnode
не существуетchildren
,Старыйvnode
существуетchildren
,перечислитьremoveVnodes
Удалитьchildren
- не существует
children
,новыйvnode
не существуетtext
, удалить старыйvnode
изtext
- оба существуют
text
,продлитьtext
последний звонокpostpatch hook
. Весь процесс очень ясен, нам нужно сосредоточиться на том,updateChildren
addVnodes
removeVnodes
.
updateChildren
function updateChildren(parentElm: Node, oldCh: Array<VNode>, newCh: Array<VNode>, insertedVnodeQueue: VNodeQueue) {
let oldStartIdx = 0,
newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newEndIdx = newCh.length - 1;
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
let oldKeyToIdx: any;
let idxInOld: number;
let elmToMove: VNode;
let before: any;
// 遍历 oldCh newCh,对节点进行比较和更新
// 每轮比较最多处理一个节点,算法复杂度 O(n)
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 如果进行比较的 4 个节点中存在空节点,为空的节点下标向中间推进,继续下个循环
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx];
// 新旧开始节点相同,直接调用 patchVnode 进行更新,下标向中间推进
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
// 新旧结束节点相同,逻辑同上
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
// 旧开始节点等于新的节点节点,说明节点向右移动了,调用 patchVnode 进行更新
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
// 旧开始节点等于新的结束节点,说明节点向右移动了
// 具体移动到哪,因为新节点处于末尾,所以添加到旧结束节点(会随着 updateChildren 左移)的后面
// 注意这里需要移动 dom,因为节点右移了,而为什么是插入 oldEndVnode 的后面呢?
// 可以分为两个情况来理解:
// 1. 当循环刚开始,下标都还没有移动,那移动到 oldEndVnode 的后面就相当于是最后面,是合理的
// 2. 循环已经执行过一部分了,因为每次比较结束后,下标都会向中间靠拢,而且每次都会处理一个节点,
// 这时下标左右两边已经处理完成,可以把下标开始到结束区域当成是并未开始循环的一个整体,
// 所以插入到 oldEndVnode 后面是合理的(在当前循环来说,也相当于是最后面,同 1)
api.insertBefore(parentElm, oldStartVnode.elm as Node, api.nextSibling(oldEndVnode.elm as Node));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
// 旧的结束节点等于新的开始节点,说明节点是向左移动了,逻辑同上
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
// 如果以上 4 种情况都不匹配,可能存在下面 2 种情况
// 1. 这个节点是新创建的
// 2. 这个节点在原来的位置是处于中间的(oldStartIdx 和 endStartIdx之间)
} else {
// 如果 oldKeyToIdx 不存在,创建 key 到 index 的映射
// 而且也存在各种细微的优化,只会创建一次,并且已经完成的部分不需要映射
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
// 拿到在 oldCh 下对应的下标
idxInOld = oldKeyToIdx[newStartVnode.key as string];
// 如果下标不存在,说明这个节点是新创建的
if (isUndef(idxInOld)) {
// New element
// 插入到 oldStartVnode 的前面(对于当前循环来说,相当于最前面)
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
newStartVnode = newCh[++newStartIdx];
} else {
// 如果是已经存在的节点 找到需要移动位置的节点
elmToMove = oldCh[idxInOld];
// 虽然 key 相同了,但是 seletor 不相同,需要调用 createElm 来创建新的 dom 节点
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
} else {
// 否则调用 patchVnode 对旧 vnode 做更新
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
// 在 oldCh 中将当前已经处理的 vnode 置空,等下次循环到这个下标的时候直接跳过
oldCh[idxInOld] = undefined as any;
// 插入到 oldStartVnode 的前面(对于当前循环来说,相当于最前面)
api.insertBefore(parentElm, elmToMove.elm as Node, oldStartVnode.elm as Node);
}
newStartVnode = newCh[++newStartIdx];
}
}
}
// 循环结束后,可能会存在两种情况
// 1. oldCh 已经全部处理完成,而 newCh 还有新的节点,需要对剩下的每个项都创建新的 dom
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
// 2. newCh 已经全部处理完成,而 oldCh 还有旧的节点,需要将多余的节点移除
} else {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
}
Весь процесс прост: сравните два массива, найдите одинаковые части для повторного использования и обновите их. Вся логика может показаться немного запутанной, но ее можно понять на следующем примере:
- Предположим, что старый порядок узлов — [A, B, C, D], а новый — [B, A, C, D, E].
- Первый раунд сравнения: начальный и конечный узлы не равны, поэтому проверьте, существует ли newStartVnode в старом узле, и, наконец, найдите вторую позицию, вызовите patchVnode для обновления, установите oldCh[1] пустым и вставьте dom в In. перед oldStartVnode, newStartIdx перемещается в середину, а статус обновляется следующим образом
- Второй раунд сравнения: oldStartVnode и newStartVnode равны, patchVnode напрямую, newStartIdx и oldStartIdx перемещаются в середину, а статус обновляется следующим образом
- Третий раунд сравнения: oldStartVnode пуст, oldStartIdx перемещается в середину, переходит к следующему раунду сравнения, и статус обновляется следующим образом
- Четвертый раунд сравнения: oldStartVnode и newStartVnode равны, patchVnode напрямую, newStartIdx и oldStartIdx перемещаются в середину, а статус обновляется следующим образом
- oldStartVnode и newStartVnode равны, patchVnode напрямую, newStartIdx и oldStartIdx перемещаются в середину, а статус обновляется следующим образом
- oldStartIdx больше, чем oldEndIdx, и цикл заканчивается. Поскольку старый узел заканчивает цикл первым и есть новые узлы, которые не были обработаны, вызовите addVnodes для обработки оставшихся новых узлов.
функции addVnodes и removeVnodes
function addVnodes(parentElm: Node, before: Node | null, vnodes: Array<VNode>, startIdx: number, endIdx: number, insertedVnodeQueue: VNodeQueue) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx];
if (ch != null) {
api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
}
}
}
function removeVnodes(parentElm: Node, vnodes: Array<VNode>, startIdx: number, endIdx: number): void {
for (; startIdx <= endIdx; ++startIdx) {
let i: any, listeners: number, rm: () => void, ch = vnodes[startIdx];
if (ch != null) {
if (isDef(ch.sel)) {
// 调用 destory hook
invokeDestroyHook(ch);
// 计算需要调用 removecallback 的次数 只有全部调用了才会移除 dom
listeners = cbs.remove.length + 1;
rm = createRmCb(ch.elm as Node, listeners);
// 调用 module 中是 remove hook
for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
// 调用 vnode 的 remove hook
if (isDef(i = ch.data) && isDef(i = i.hook) && isDef(i = i.remove)) {
i(ch, rm);
} else {
rm();
}
} else { // Text node
api.removeChild(parentElm, ch.elm as Node);
}
}
}
}
// 调用 destory hook
// 如果存在 children 递归调用
function invokeDestroyHook(vnode: VNode) {
let i: any, j: number, data = vnode.data;
if (data !== undefined) {
if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode);
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
if (vnode.children !== undefined) {
for (j = 0; j < vnode.children.length; ++j) {
i = vnode.children[j];
if (i != null && typeof i !== "string") {
invokeDestroyHook(i);
}
}
}
}
}
// 只有当所有的 remove hook 都调用了 remove callback 才会移除 dom
function createRmCb(childElm: Node, listeners: number) {
return function rmCb() {
if (--listeners === 0) {
const parent = api.parentNode(childElm);
api.removeChild(parent, childElm);
}
};
}
Эти две функции в основном используются для добавления vnode и удаления vnode, и логику кода можно в основном понять.
преобразователь функция
Как правило, наше приложение обновляется в соответствии с состоянием js, например, в следующем примере.
function renderNumber(num) {
return h('span', num);
}
Это означает, что еслиnum
Если изменений нет, тоvnode
провестиpatch
это просто не имеет смысла,
В таком случае,snabbdom
предлагает метод оптимизации, т.thunk
, функция также возвращаетvnode
узел, но вpatchVnode
В начале параметры будут сравниваться один раз, если они совпадают, то сравнение будет завершено, это немного похоже наReact
изpureComponent
,pureComponent
Неглубокое сравнение будет сделано по реализацииshadowEqual
, в сочетании сimmutable
Использование данных более эффективно. Приведенный выше пример может стать таким.
function renderNumber(num) {
return h('span', num);
}
function render(num) {
return thunk('div', renderNumber, [num]);
}
var vnode = patch(container, render(1))
// 由于num 相同,renderNumber 不会执行
patch(vnode, render(1))
Его конкретная реализация выглядит следующим образом:
export interface ThunkFn {
(sel: string, fn: Function, args: Array<any>): Thunk;
(sel: string, key: any, fn: Function, args: Array<any>): Thunk;
}
// 使用 h 函数返回 vnode,为其添加 init 和 prepatch 钩子
export const thunk = function thunk(sel: string, key?: any, fn?: any, args?: any): VNode {
if (args === undefined) {
args = fn;
fn = key;
key = undefined;
}
return h(sel, {
key: key,
hook: {init: init, prepatch: prepatch},
fn: fn,
args: args
});
} as ThunkFn;
// 将 vnode 上的数据拷贝到 thunk 上,在 patchVnode 中会进行判断,如果相同会结束 patchVnode
// 并将 thunk 的 fn 和 args 属性保存到 vnode 上,在 prepatch 时需要进行比较
function copyToThunk(vnode: VNode, thunk: VNode): void {
thunk.elm = vnode.elm;
(vnode.data as VNodeData).fn = (thunk.data as VNodeData).fn;
(vnode.data as VNodeData).args = (thunk.data as VNodeData).args;
thunk.data = vnode.data;
thunk.children = vnode.children;
thunk.text = vnode.text;
thunk.elm = vnode.elm;
}
function init(thunk: VNode): void {
const cur = thunk.data as VNodeData;
const vnode = (cur.fn as any).apply(undefined, cur.args);
copyToThunk(vnode, thunk);
}
function prepatch(oldVnode: VNode, thunk: VNode): void {
let i: number, old = oldVnode.data as VNodeData, cur = thunk.data as VNodeData;
const oldArgs = old.args, args = cur.args;
if (old.fn !== cur.fn || (oldArgs as any).length !== (args as any).length) {
// 如果 fn 不同或 args 长度不同,说明发生了变化,调用 fn 生成新的 vnode 并返回
copyToThunk((cur.fn as any).apply(undefined, args), thunk);
return;
}
for (i = 0; i < (args as any).length; ++i) {
if ((oldArgs as any)[i] !== (args as any)[i]) {
// 如果每个参数发生变化,逻辑同上
copyToThunk((cur.fn as any).apply(undefined, args), thunk);
return;
}
}
copyToThunk(oldVnode, thunk);
}
Вы можете просмотреть реализацию patchVnode.После предварительного исправления данные vnode будут сравниваться, например, когдаchildren
такой же,text
то же самое закончитсяpatchVnode
.
Эпилог
сюдаsnabbdom
Исходный код ядра был прочитан, и есть некоторые встроенныеmodule
, вы можете прочитать это сами, если вам интересно.