анализ чтения исходного кода snabbdom

внешний интерфейс исходный код JavaScript Vue.js
анализ чтения исходного кода snabbdom

автор: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определить, одинаковые лиvnodeNode, конкретная реализация тоже очень проста, вот только суждениеkeyиselто же самое. Если так же, звонитеpatchVnode, если не то же самое, позвонитcreateElmсоздать новыйdomnode, то если есть родитель, вставьте его в дом, затем удалите старый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);
    }
  }
}

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

  1. Предположим, что старый порядок узлов — [A, B, C, D], а новый — [B, A, C, D, E].

snabbdom-1

  1. Первый раунд сравнения: начальный и конечный узлы не равны, поэтому проверьте, существует ли newStartVnode в старом узле, и, наконец, найдите вторую позицию, вызовите patchVnode для обновления, установите oldCh[1] пустым и вставьте dom в In. перед oldStartVnode, newStartIdx перемещается в середину, а статус обновляется следующим образом

snabbdom-2

  1. Второй раунд сравнения: oldStartVnode и newStartVnode равны, patchVnode напрямую, newStartIdx и oldStartIdx перемещаются в середину, а статус обновляется следующим образом

snabbdom-3

  1. Третий раунд сравнения: oldStartVnode пуст, oldStartIdx перемещается в середину, переходит к следующему раунду сравнения, и статус обновляется следующим образом

snabbdom-4

  1. Четвертый раунд сравнения: oldStartVnode и newStartVnode равны, patchVnode напрямую, newStartIdx и oldStartIdx перемещаются в середину, а статус обновляется следующим образом

snabbdom-5

  1. oldStartVnode и newStartVnode равны, patchVnode напрямую, newStartIdx и oldStartIdx перемещаются в середину, а статус обновляется следующим образом

snabbdom-6

  1. 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, вы можете прочитать это сами, если вам интересно.

Пролистайте и обратите внимание на внешний публичный аккаунт Xunlei.