Шаблон из DOM (внутренний механизм работы исходной точки Vue.js)

GitHub исходный код JavaScript Vue.js

написать впереди

Эта статья представляет собой серию статей об исходном коде Vue.js, написанных недавно (GitHub.com/ответы на…), я действительно получил много пользы в процессе чтения исходного кода. Я надеюсь, что эти мои собственные выводы также будут полезны небольшим партнерам, которые также хотят изучить исходный код Vue.js. Предыдущая статья также была опубликована в техническом блоге нашей компании (Dasouche).Приглашаем вас обратить внимание на технический блог нашей компании и предоставить порталblog.souche.com/.

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

Оригинальный адрес статьи:GitHub.com/ответы на….

В процессе обучения в Vue были добавлены китайские аннотации.GitHub.com/ответы на…, я надеюсь, что это может быть полезно другим друзьям, которые хотят изучить исходный код Vue.

Могут быть отклонения в понимании.Добро пожаловать, чтобы задать и указать, учиться вместе и добиваться прогресса вместе.

Начните с нового объекта Vue

let vm = new Vue({
    el: '#app',
    /*some options*/
});

Многим студентам интересно, что происходит внутри, когда создается новый объект Vue?

Как именно Vue.js отображает данные в данных в реальной среде хоста?

А как модифицировать данные через "реактив"?

Как шаблон, скомпилированный в HTML, можно использовать в реальной среде?

Директива Vue снова выполняется?

Имея в виду эти вопросы, давайте начнем с класса конструктора Vue.

Класс конструктора Vue

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  /*初始化*/
  this._init(options)
}

Конструкционная категория Vue только одна вещь, чтобы вызвать функцию _init

Давайте посмотрим на код инициализации

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-init:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    /*一个防止vm实例自身被观察的标志位*/
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    /*初始化生命周期*/
    initLifecycle(vm)
    /*初始化事件*/
    initEvents(vm)
    /*初始化render*/
    initRender(vm)
    /*调用beforeCreate钩子函数并且触发beforeCreate钩子事件*/
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    /*初始化props、methods、data、computed与watch*/
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    /*调用created钩子函数并且触发created钩子事件*/
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      /*格式化组件名*/
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      /*挂载组件*/
      vm.$mount(vm.$options.el)
    }
  }

_init в основном делает две вещи:

1. Инициализация (включая жизненный цикл, события, функции рендеринга, состояние и т. д.).

2. Компонент $mount.

Состояние будет инициализировано между жизненным хуком beforeCreate и created.Во время этого процесса реквизиты, методы, данные, вычисления и часы будут инициализированы по очереди, что означает, что Vue.js «отвечает» на данные в опциях (то есть два -путь).обязательный) процесс. Студенты, которые не понимают принцип отзывчивости Vue.js, могут сначала прочитать другую статью автора.«Принцип отклика Vue.js».

/*初始化props、methods、data、computed与watch*/
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  /*初始化props*/
  if (opts.props) initProps(vm, opts.props)
  /*初始化方法*/
  if (opts.methods) initMethods(vm, opts.methods)
  /*初始化data*/
  if (opts.data) {
    initData(vm)
  } else {
    /*该组件没有data的时候绑定一个空对象*/
    observe(vm._data = {}, true /* asRootData */)
  }
  /*初始化computed*/
  if (opts.computed) initComputed(vm, opts.computed)
  /*初始化watchers*/
  if (opts.watch) initWatch(vm, opts.watch)
}

двусторонняя привязка

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

function initData (vm: Component) {

  /*得到data数据*/
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}

  /*判断是否是对象*/
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }

  // proxy data on instance
  /*遍历data对象*/
  const keys = Object.keys(data)
  const props = vm.$options.props
  let i = keys.length

  //遍历data中的数据
  while (i--) {
    /*保证data中的key不与props中的key重复,props优先,如果有冲突会产生warning*/
    if (props && hasOwn(props, keys[i])) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${keys[i]}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(keys[i])) {
      /*判断是否是保留字段*/

      /*这里是我们前面讲过的代理,将data上面的属性代理到了vm实例上*/
      proxy(vm, `_data`, keys[i])
    }
  }
  /*Github:https://github.com/answershuto*/
  // observe data
  /*从这里开始我们要observe了,开始对数据进行绑定,这里有尤大大的注释asRootData,这步作为根数据,下面会进行递归observe进行对深层对象的绑定。*/
  observe(data, true /* asRootData */)
}

наблюдать будет двунаправленно связывать объекты в данных через defineReactive и, наконец, устанавливать методы установки и получения для объекта через Object.defineProperty. Метод getter в основном используется для сбора зависимостей.Для студентов, которые не понимают сбор зависимостей, вы можете обратиться к другой статье автора.«Коллекция зависимостей». Метод установки будет запущен при изменении объекта (нет случая добавления свойств, пожалуйста, используйте Vue.set для добавления свойств). В это время установщик уведомит Dep в закрытии. В Dep есть несколько наблюдателей, которые подписаться на изменение этого объекта Watcher object, Dep уведомит объект Watcher об обновлении представления.

Если необходимо изменить элемент массива, который является объектом, необходимо только рекурсивно связать элементы массива в обоих направлениях. Но на этот раз есть проблема, ? Если мы делаем pop, push и другие операции, а объект, который выталкивается, никогда не был связан в обоих направлениях, не говоря уже о pop, то как мы отслеживаем эти изменения в массиве?
Метод, предоставляемый Vue.js, состоит в том, чтобы переписать семь push, pop, shift, unshift, splice, sort, reverse.метод массива. Код для изменения метода прототипа массива может ссылаться наobserver/array.jsтак же какobserver/index.js.

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    //.......

    if (Array.isArray(value)) {
      /*
          如果是数组,将修改后可以截获响应的数组方法替换掉该数组的原型中的原生方法,达到监听数组数据变化响应的效果。
          这里如果当前浏览器支持__proto__属性,则直接覆盖当前数组对象原型上的原生数组方法,如果不支持该属性,则直接覆盖数组对象的原型。
      */
      const augment = hasProto
        ? protoAugment  /*直接覆盖原型的方法来修改目标对象*/
        : copyAugment   /*定义(覆盖)目标对象或数组的某一个方法*/
      augment(value, arrayMethods, arrayKeys)

      /*如果是数组则需要遍历数组的每一个成员进行observe*/
      this.observeArray(value)
    } else {
      /*如果是对象则直接walk进行绑定*/
      this.walk(value)
    }
  }
}

/**
 * Augment an target Object or Array by intercepting
 * the prototype chain using __proto__
 */
 /*直接覆盖原型的方法来修改目标对象或数组*/
function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}

/**
 * Augment an target Object or Array by defining
 * hidden properties.
 */
/* istanbul ignore next */
/*定义(覆盖)目标对象或数组的某一个方法*/
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}
/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */

import { def } from '../util/index'

/*取得原生数组的原型*/
const arrayProto = Array.prototype
/*创建一个新的数组对象,修改该对象上的数组的七个方法,防止污染原生数组方法*/
export const arrayMethods = Object.create(arrayProto)

/**
 * Intercept mutating methods and emit events
 */
 /*这里重写了数组的这些方法,在保证不污染原生数组原型的情况下重写数组的这些方法,截获数组的成员发生的变化,执行原生数组操作的同时dep通知关联的所有观察者进行响应式处理*/
;[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  // cache original method
  /*将数组的原生方法缓存起来,后面要调用*/
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator () {
    // avoid leaking arguments:
    // http://jsperf.com/closure-with-arguments
    let i = arguments.length
    const args = new Array(i)
    while (i--) {
      args[i] = arguments[i]
    }
    /*调用原生的数组方法*/
    const result = original.apply(this, args)

    /*数组新插入的元素需要重新进行observe才能响应式*/
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
        inserted = args
        break
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)

    // notify change
    /*dep通知所有注册的观察者进行响应式处理*/
    ob.dep.notify()
    return result
  })
})

Создайте новый объект Object.create(arrayProto) из прототипа массива. Изменив этот прототип, вы можете гарантировать, что собственный метод массива не будет загрязнен. Если текущий браузер поддерживаетprotoЭто свойство можно напрямую переопределить, чтобы объект массива имел переопределенный метод массива. Если нет браузера с этим атрибутом, необходимо пройтись по всем методам массива, которые нужно переписать def.Этот метод менее эффективен, поэтому предпочтительнее первый.

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

Однако после изменения собственного метода массива мы по-прежнему не можем напрямую изменить массив, подписав массив или установив длину, как у собственного массива.Методы $set() и $remove().

Более подробное объяснение двусторонней привязки данных и реализации Dep и Watcher см. в статье автора«Взгляд на привязку данных с точки зрения исходного кода».

компиляция шаблона

Во время процесса $mount, если сборка создается независимо, шаблон будет скомпилирован в функцию рендеринга во время этого процесса. Конечно, вы также можете использовать сборки во время выполнения. конкретная ссылкасреда выполнения только для компилятора.

Как шаблон компилируется в функцию рендеринга?

function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  /*parse解析得到ast树*/
  const ast = parse(template.trim(), options)
  /*
    将AST树进行优化
    优化的目标:生成模板AST树,检测不需要进行DOM改变的静态子树。
    一旦检测到这些静态树,我们就能做以下这些事情:
    1.把它们变成常数,这样我们就再也不需要每次重新渲染时创建新的节点了。
    2.在patch的过程中直接跳过。
 */
  optimize(ast, options)
  /*根据ast树生成所需的code(内部包含render与staticRenderFns)*/
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
}

baseCompile сначала проанализирует шаблон шаблона, чтобы получить синтаксическое дерево AST, затем выполнит некоторую оптимизацию с помощью оптимизации и, наконец, получит рендеринг и staticRenderFns с помощью генерирования.

parse

Исходный код синтаксического анализа можно найти вGitHub.com/ответы на….

parse будет анализировать инструкции, класс, стиль и другие данные в шаблоне шаблона, используя обычные методы для формирования синтаксического дерева AST.

optimize

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

generate

generate — это процесс преобразования синтаксического дерева AST в строку функции рендеринга, а результатом является строка рендеринга и строка staticRenderFns.

Для конкретной реализации компиляции шаблона, пожалуйста, обратитесь к«Поговорим о компиляции шаблона Vue.js».

Наблюдатель для просмотра

Объект Watcher обновит представление, вызвав метод updateComponent. Здесь упоминается, что на самом деле Watcher не обновляет представление в реальном времени, Vue.js по умолчанию хранит объект Watcher в очереди и обновляет представление асинхронного обновления на следующем тике, завершая оптимизацию производительности. Небольшие партнеры, заинтересованные в nextTick, могут обратиться к"Стратегия асинхронного обновления DOM Vue.js и nextTick".

updateComponent = () => {
    vm._update(vm._render(), hydrating)
}

updateComponent выполняет предложение, функция _render вернет новый узел Vnode, передаст _update для сравнения со старым объектом VNode, получит разницу между двумя узлами VNode с помощью процесса исправления и, наконец, отобразит эти различия в реальной среде. вид.

Что такое VNode?

VNode

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

Итак, можем ли мы абстрагировать реальное дерево DOM в абстрактное дерево, состоящее из объектов JavaScript, а затем преобразовать абстрактное дерево в настоящий DOM и перерисовать его на странице после изменения данных абстрактного дерева? Так появился виртуальный DOM, который представляет собой слой абстракции реального DOM и использует атрибуты для описания различных характеристик реального DOM. Когда он изменяется, вид изменяется.

Но такие манипуляции JavaScript с DOM для перерисовки всего слоя просмотра потребляют производительность, Можем ли мы каждый раз обновлять только его модификации? Поэтому Vue.js абстрагирует DOM в виртуальное дерево DOM с объектами JavaScript в качестве узлов, имитирует реальный DOM с узлами VNode и может создавать узлы, удалять узлы и изменять узлы в этом абстрактном дереве. нужно только управлять объектом JavaScript, что значительно повышает производительность. После модификации некоторые минимальные единицы, которые необходимо изменить, получаются с помощью алгоритма diff, а затем обновляются представления этих небольших единиц. Это уменьшает количество ненужных манипуляций с DOM и значительно повышает производительность.

Vue использует такой абстрактный узел VNode, который является уровнем абстракции реального DOM и не зависит от конкретной платформы — это может быть платформа браузера или weex, и даже платформа узла может абстрагировать такое дерево. Дерево DOM выполняет такие операции, как создание, удаление и модификация, что также обеспечивает возможность изоморфизма внешнего и внутреннего интерфейса.

Давайте сначала посмотрим на определение класса VNode в исходном коде Vue.js.

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  functionalContext: Component | void; // only for functional component root nodes
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions
  ) {
    /*当前节点的标签名*/
    this.tag = tag
    /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
    this.data = data
    /*当前节点的子节点,是一个数组*/
    this.children = children
    /*当前节点的文本*/
    this.text = text
    /*当前虚拟节点对应的真实dom节点*/
    this.elm = elm
    /*当前节点的名字空间*/
    this.ns = undefined
    /*编译作用域*/
    this.context = context
    /*函数化组件作用域*/
    this.functionalContext = undefined
    /*节点的key属性,被当作节点的标志,用以优化*/
    this.key = data && data.key
    /*组件的option选项*/
    this.componentOptions = componentOptions
    /*当前节点对应的组件的实例*/
    this.componentInstance = undefined
    /*当前节点的父节点*/
    this.parent = undefined
    /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
    this.raw = false
    /*静态节点标志*/
    this.isStatic = false
    /*是否作为跟节点插入*/
    this.isRootInsert = true
    /*是否为注释节点*/
    this.isComment = false
    /*是否为克隆节点*/
    this.isCloned = false
    /*是否有v-once指令*/
    this.isOnce = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}

Это самый простой узел VNode, являющийся базовым классом других производных классов VNode, который определяет следующие данные.

tag: имя тега текущего узла

Данные: объект, соответствующий текущему узлу, который содержит некоторую конкретную информацию о данных, представляет собой тип VNodeData, вы можете обратиться к информации о данных в типе Vnodedata

Дети: детский узел ребенка - это массив

text: текст текущего узла

elm: реальный узел dom, соответствующий текущему виртуальному узлу.

ns: пространство имен текущего узла

контекст: область компиляции текущего узла

ФункциональныйКонтекст: область действия функционального компонента

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

componentOptions: опции опций компонента

componentInstance: экземпляр компонента, соответствующий текущему узлу.

parent: родительский узел текущего узла

raw: Короче говоря, будь то собственный HTML или обычный текст, true для innerHTML, false для textContent.

isStatic: является ли это статическим узлом

isRootInsert: вставлять ли как корневой узел

isComment: является ли это узлом комментария

isCloned: это клонированный узел

isOnce: есть ли команда v-once


Вот например у меня сейчас такое дерево VNode

{
    tag: 'div'
    data: {
        class: 'test'
    },
    children: [
        {
            tag: 'span',
            data: {
                class: 'demo'
            }
            text: 'hello,VNode'
        }
    ]
}

Результат после рендера такой

<div class="test">
    <span class="demo">hello,VNode</span>
</div>

Дополнительные методы работы с VNode см."VNode узел".

patch

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

Прежде всего, давайте поговорим об основном алгоритме сравнения патча.Алгоритм сравнения сравнивает узлы дерева одного и того же слоя вместо поиска и обхода дерева слой за слоем, поэтому временная сложность составляет всего O (n), что является достаточно эффективный алгоритм.

img
img

img
img

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

В процессе исправления, если два VNode считаются одним и тем же VNode (sameVnode), будет выполнено глубокое сравнение для получения минимальной разницы, в противном случае старый DOM-узел будет удален напрямую и будет создан новый DOM-узел.

Что такое тот же Vnode?

Давайте взглянем на реализацию sameVnode.

/*
  判断两个VNode节点是否是同一个节点,需要满足以下条件
  key相同
  tag(当前节点的标签名)相同
  isComment(是否为注释节点)相同
  是否data(当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息)都有定义
  当标签是<input>的时候,type必须相同
*/
function sameVnode (a, b) {
  return (
    a.key === b.key &&
    a.tag === b.tag &&
    a.isComment === b.isComment &&
    isDef(a.data) === isDef(b.data) &&
    sameInputType(a, b)
  )
}

// Some browsers do not support dynamically changing type for <input>
// so they need to be treated as different nodes
/*
  判断当标签是<input>的时候,type是否相同
  某些浏览器不支持动态修改<input>类型,所以他们被视为不同类型
*/
function sameInputType (a, b) {
  if (a.tag !== 'input') return true
  let i
  const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
  const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
  return typeA === typeB
}

Когда тег, ключ и isComment двух VNodes одинаковы, а данные определены или не определены одновременно, и если тег является входным, тип должен быть одинаковым. В это время два VNode рассматриваются как один и тот же Vnode, и операция patchVnode может выполняться напрямую.

Правила для patchVnode следующие:

1. Если старый и новый виртуальные узлы являются статическими, а их ключи одинаковыми (представляющими один и тот же узел), а новый виртуальный узел клонируется или помечается один раз (отмеченный атрибут v-once, визуализируется только один раз), то необходимо только заменить elm и componentInstance могут быть.

2. Если и у старого, и у нового узла есть дочерние дочерние узлы, операция diff выполняется над дочерними узлами и вызывается updateChildren, который также является ядром diff.

3. Если у старого узла нет дочерних узлов, а у нового узла есть дочерние узлы, сначала очистите текстовое содержимое DOM старого узла, а затем добавьте дочерние узлы к текущему узлу DOM.

4. Когда у нового узла нет дочерних узлов, а у старого узла есть дочерние узлы, удалите все дочерние узлы узла DOM.

5. Когда старый и новый узлы не имеют дочерних узлов, это просто замена текста.

updateChildren

  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0
    let 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, idxInOld, elmToMove, refElm

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        /*前四种情况其实是指定key的时候,判定为同一个VNode,则直接patchVnode即可,分别比较oldCh以及newCh的两头节点2*2=4种情况*/
        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]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        /*
          生成一个key与旧VNode的key对应的哈希表(只有第一次进来undefined的时候会生成,也为后面检测重复的key值做铺垫)
          比如childre是这样的 [{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}]  beginIdx = 0   endIdx = 2  
          结果生成{key0: 0, key1: 1, key2: 2}
        */
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        /*如果newStartVnode新的VNode节点存在key并且这个key在oldVnode中能找到则返回这个节点的idxInOld(即第几个节点,下标)*/
        idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
        if (isUndef(idxInOld)) { // New element
          /*newStartVnode没有key或者是该key没有在老节点中找到则创建一个新的节点*/
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
          newStartVnode = newCh[++newStartIdx]
        } else {
          /*获取同key的老节点*/
          elmToMove = oldCh[idxInOld]
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !elmToMove) {
            /*如果elmToMove不存在说明之前已经有新节点放入过这个key的DOM中,提示可能存在重复的key,确保v-for的时候item有唯一的key值*/
            warn(
              'It seems there are duplicate keys that is causing an update error. ' +
              'Make sure each v-for item has a unique key.'
            )
          }
          if (sameVnode(elmToMove, newStartVnode)) {
            /*Github:https://github.com/answershuto*/
            /*如果新VNode与得到的有相同key的节点是同一个VNode则进行patchVnode*/
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
            /*因为已经patchVnode进去了,所以将这个老节点赋值undefined,之后如果还有新节点与该节点key相同可以检测出来提示已有重复的key*/
            oldCh[idxInOld] = undefined
            /*当有标识位canMove实可以直接插入oldStartVnode对应的真实DOM节点前面*/
            canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
            newStartVnode = newCh[++newStartIdx]
          } else {
            // same key but different element. treat as new element
            /*当新的VNode与找到的同样key的VNode不是sameVNode的时候(比如说tag不一样或者是有不一样type的input标签),创建一个新的节点*/
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
            newStartVnode = newCh[++newStartIdx]
          }
        }
      }
    }
    if (oldStartIdx > oldEndIdx) {
      /*全部比较完成以后,发现oldStartIdx > oldEndIdx的话,说明老节点已经遍历完了,新节点比老节点多,所以这时候多出来的新节点需要一个一个创建出来加入到真实DOM中*/
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      /*如果全部比较完成以后发现newStartIdx > newEndIdx,则说明新节点已经遍历完了,老节点多余新节点,这个时候需要将多余的老节点从真实DOM中移除*/
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }

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

img
img

Во-первых, слева и справа от нового и старого узлов VNode есть переменный маркер, и эти переменные будут перемещаться ближе к середине в процессе обхода. Завершите цикл, когда oldStartIdx

Соответствующая связь между индексом и узлом VNode:
oldStartIdx => oldStartVnode
oldEndIdx => oldEndVnode
newStartIdx => newStartVnode
newEndIdx => newEndVnode

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

Во-первых, всего существует 2*2=4 метода сравнения для парного сравнения oldStartVnode, oldEndVnode и newStartVnode, newEndVnode.

Когда начало или конец старого и нового узлов VNode удовлетворяют тому же самому Vnode, то есть тому же самому Vnode(oldStartVnode, newStartVnode) или sameVnode(oldEndVnode, newEndVnode), вы можете напрямую исправить узел VNode.

img
img

Если oldStartVnode и newEndVnode удовлетворяют sameVnode, то есть sameVnode(oldStartVnode, newEndVnode).

В настоящее время это означает, что oldStartVnode работает после oldEndVnode.Когда patchVnode выполняется, реальный узел DOM должен быть перемещен за oldEndVnode.

img
img

Если oldEndVnode и newStartVnode удовлетворяют sameVnode, то есть sameVnode(oldEndVnode, newStartVnode).

Это показывает, что oldEndVnode располагался впереди oldStartVnode, а реальный узел DOM перемещался впереди oldStartVnode во время выполнения patchVnode.

img
img

Если ни одно из вышеперечисленных условий не выполняется, через createKeyToOldIdx будет получен oldKeyToIdx, в котором ключ — это старый VNode, а значение — хэш-таблица, соответствующая индексной последовательности. Из этой хэш-таблицы вы можете узнать, существует ли старый узел VNode с тем же ключом, что и newStartVnode.Если тот же самый Vnode одновременно удовлетворяется, patchVnode переместит реальный DOM (elmToMove) на передний план реального DOM, соответствующего на старыйStartVnode.

img
img

Конечно, также возможно, что newStartVnode не может найти непротиворечивый ключ в старом узле VNode, или даже если ключ тот же самый, но не тот же самый Vnode, тогда createElm будет вызван для создания нового узла DOM.

img
img

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

1. Когда oldStartIdx > oldEndIdx в конце, старый узел VNode был пройден, а новый узел еще не пройден. Это показывает, что новые узлы VNode на самом деле больше, чем старые узлы VNode, то есть больше, чем реальный DOM.Необходимо вставить оставшиеся (то есть вновь добавленные) узлы VNode в реальные узлы DOM.В это время , вызовите addVnodes (пакетный вызов интерфейса createElm для добавления этих узлов в настоящий DOM).

img
img

2. Точно так же, когда newStartIdx > newEndIdx, новый узел VNode был пройден, но старый узел все еще остался, что указывает на то, что реальный узел DOM является избыточным и должен быть удален из документа.В это время вызовите removeVnodes, чтобы удалить эти избыточные узлы. настоящие узлы DOM.

img
img

Для более подробной реализации diff обратитесь к статье автораВиртуальный DOM и diff (реализовано Vue.js).MarkDown).

Сопоставление с реальным DOM

Поскольку Vue использует виртуальную DOM, виртуальная DOM может работать на любой платформе, поддерживающей язык JavaScript.Например, платформа браузера или weex, поддерживаемая в настоящее время Vue, соответствует реализации виртуальной DOM. Так как же в конечном итоге виртуальный DOM сопоставляется с реальным узлом DOM?

Vue сделал адаптационный слой для платформы, см. платформу браузера/platforms/web/runtime/node-ops.jsи платформу weex см./platforms/weex/runtime/node-ops.js. Различные платформы предоставляют один и тот же интерфейс наружу через уровень адаптации.Когда виртуальный DOM работает с реальным узлом DOM, ему нужно только вызвать интерфейс этих слоев адаптации, и внутреннюю реализацию не нужно беспокоить, она изменится. в зависимости от платформы.и изменить.

Теперь есть другая проблема, мы просто сопоставляем виртуальный DOM с реальным DOM. Как добавить в эти DOM атрибуты attr, class, style и другие атрибуты DOM?

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

const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

/*构建cbs回调函数,web平台上见/platforms/web/runtime/modules*/
  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }

Точно так же будут разные реализации для разных платформ.В качестве примера мы возьмем веб-платформу. Информацию о функциях ловушек веб-платформы см./platforms/web/runtime/modules. Существуют операции над атрибутами DOM attr, class, props, events, style и transition (переходное состояние).

Взяв в качестве примера attr, код очень прост.

/* @flow */

import { isIE9 } from 'core/util/env'

import {
  extend,
  isDef,
  isUndef
} from 'shared/util'

import {
  isXlink,
  xlinkNS,
  getXlinkProp,
  isBooleanAttr,
  isEnumeratedAttr,
  isFalsyAttrValue
} from 'web/util/index'

/*更新attr*/
function updateAttrs (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  /*如果旧的以及新的VNode节点均没有attr属性,则直接返回*/
  if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) {
    return
  }
  let key, cur, old
  /*VNode节点对应的Dom实例*/
  const elm = vnode.elm
  /*旧VNode节点的attr*/
  const oldAttrs = oldVnode.data.attrs || {}
  /*新VNode节点的attr*/
  let attrs: any = vnode.data.attrs || {}
  // clone observed objects, as the user probably wants to mutate it
  /*如果新的VNode的attr已经有__ob__(代表已经被Observe处理过了), 进行深拷贝*/
  if (isDef(attrs.__ob__)) {
    attrs = vnode.data.attrs = extend({}, attrs)
  }

  /*遍历attr,不一致则替换*/
  for (key in attrs) {
    cur = attrs[key]
    old = oldAttrs[key]
    if (old !== cur) {
      setAttr(elm, key, cur)
    }
  }
  // #4391: in IE9, setting type can reset value for input[type=radio]
  /* istanbul ignore if */
  if (isIE9 && attrs.value !== oldAttrs.value) {
    setAttr(elm, 'value', attrs.value)
  }
  for (key in oldAttrs) {
    if (isUndef(attrs[key])) {
      if (isXlink(key)) {
        elm.removeAttributeNS(xlinkNS, getXlinkProp(key))
      } else if (!isEnumeratedAttr(key)) {
        elm.removeAttribute(key)
      }
    }
  }
}

/*设置attr*/
function setAttr (el: Element, key: string, value: any) {
  if (isBooleanAttr(key)) {
    // set attribute for blank value
    // e.g. <option disabled>Select one</option>
    if (isFalsyAttrValue(value)) {
      el.removeAttribute(key)
    } else {
      el.setAttribute(key, key)
    }
  } else if (isEnumeratedAttr(key)) {
    el.setAttribute(key, isFalsyAttrValue(value) || value === 'false' ? 'false' : 'true')
  } else if (isXlink(key)) {
    if (isFalsyAttrValue(value)) {
      el.removeAttributeNS(xlinkNS, getXlinkProp(key))
    } else {
      el.setAttributeNS(xlinkNS, key, value)
    }
  } else {
    if (isFalsyAttrValue(value)) {
      el.removeAttribute(key)
    } else {
      el.setAttribute(key, value)
    }
  }
}

export default {
  create: updateAttrs,
  update: updateAttrs
}

attr нужно только обновить атрибут attr DOM, когда вызываются хуки создания и обновления.

наконец

На данный момент мы разобрались со всем процессом от шаблона до реального DOM. А теперь посмотрите на эту картинку еще раз, она яснее?

о

Автор: Ран Мо

Электронная почта: answerhuto@gmail.com или answerhuto@126.com

Github: github.com/answershuto

Блог:answershuto.github.io/

Знать домашнюю страницу:ууууууу. chi.com/people/grass-…

Знать столбец:zhuanlan.zhihu.com/ranmo

Самородки:Талант /user/289926…

Ос Китай:no.OSCHINA.net/U/3161824/no…

Пожалуйста, указывайте источник при перепечатке, спасибо.

Добро пожаловать, чтобы обратить внимание на мой общедоступный номер