Расширенное руководство по Vue-02 Углубленный анализ исходного кода Vue.js

Vue.js

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

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

Понять, что такое шаблон MVVM?

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

Хотя и не полностьюмодель МВВМ, но дизайн Vue также был вдохновлен им. В Vue пользователю больше не нужно вручную манипулировать элементами DOM, но данные привязаны кviewModelНа слое данные будут автоматически отображаться на странице, а изменения представления будут уведомленыviewModel层обновить данные.ViewModelЧто мыMVVMМост в узоре.


Каков принцип реактивных данных в Vue (версия 2.x)?

Принцип адаптивных данных в версии Vue2.x таков:Object.defineProperty(подробно в примечаниях Es6)

Когда Vue инициализируется, то есть создается новый Vue(), он вызывает базовый метод initData(), и в методе есть методObserv(), который инициализирует входящие данные для управления, реагирующего на данные, который будет выполнять управление, реагирующее на данные. , Серия операций, чтобы определить, было ли это замечено.判断观测的数据是对象还是数组。

Наблюдаемые данные являются объектом

Если наблюдение является объектом, будет вызван метод walk(), а Object.defineProperty будет вызываться внутри для наблюдения.Если свойство внутри объекта все еще является объектом, будет выполнено рекурсивное наблюдение.

В это время, когда значение текущего объекта будет получено, будет вызван метод get, и в методе get будет выполнен сбор зависимостей (watcher).Если операция присваивания выполняется для текущего объекта, метод set будет будет вызван, и метод set будет судить, отличаются ли старые и новые значения.То же самое, если не то же самое, метод уведомления будет вызываться для запуска обновления коллекции зависимостей, соответствующей данным.

Наблюдаемые данные представляют собой массив

Если массив наблюдается, массив не будет использовать описанный выше метод для сбора зависимостей.Vue переписывает метод прототипа массива внизу.Когда текущее наблюдение является массивом, Vue указывает прототип массива на его собственный определенный прототип метод. И перехватил только следующие 7 методов массива.

// 因为只有以下7中数组方法才会去改变原数组。
push, pop, shift, unshift, splice, sort, reverse

В методе-прототипе используется внутренний метод перехвата функций.Если пользователь использует 7 вышеуказанных методов массива, будет использоваться метод массива, переписанный Vue. В это время, когда массив изменяется, вы можете вручную вызвать метод уведомления, чтобы обновить попытку.

当然在对数据进行数据更新的时候,也会对新增的数据进行依赖收集观测。

如果数组中的数据也是对象,它会继续调用Object.defineProperty对其进行观测。

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

Основной код объекта наблюдения

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend() // ** 收集依赖 ** /
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      val = newVal
      childOb = !shallow && observe(newVal)
      dep.notify() /** 通知相关依赖进行更新 **/
    }
  })

Основной код массива наблюдений

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) { // 重写原型方法
  const original = arrayProto[method] // 调用原数组的方法
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify() // 当调用数组方法后,手动通知视图更新
    return result
  })
})

this.observeArray(value) // 进行深度监控

Приведенный выше контент лучше всего загрузить исходным кодом Vue на Github и посмотреть его вместе.


По какой причине Vue использует асинхронный рендеринг?

首先我们要知道Vue是组件级更新。

Когда мы используем метод в компоненте для обновления данных, например

data() {
    return {
        msg: 'hello word',
        name: '只会番茄炒蛋'
    }
}
methods:{
    add() {
        this.msg = '我更改了 => hello word'
        this.name = '我更改了 => 只会番茄炒蛋'
    }
}

Если представление визуализируется после изменения данных (данные изменяются дважды выше), это неизбежно повлияет на производительность, поэтому Vue использует метод асинхронного рендеринга, то есть несколько данных изменяются одновременно в одном событии, и один и тот же наблюдатель используется несколько раз. Сработав, он будет помещен в очередь только один раз. При изменении последних данных вызывается метод nexttick для асинхронного обновления представления.

内部还有一些其他的操作,例如添加 watcher 的时候给一个唯一的id, 更新的时候根据 id 进行一个排序,更新完毕还会调用对应的生命周期也就是 beforeUpdate 和 updated 方法等。

Приведенный выше контент лучше всего загрузить исходным кодом Vue на Github и посмотреть его вместе.


Каков принцип реализации nextTick во Vue?

Прежде чем понять принцип реализации nextTick, какой Event Loop вам нужно освоить, а также понять микрозадачи и макрозадачи, здесь я кратко представлю.

Event Loop

Все также знают, что когда мы выполняем JS-код, мы фактически помещаем функции в стек выполнения, так что же нам делать, когда мы сталкиваемся с асинхронным кодом? На самом деле, когда встречается асинхронный код, он будет приостановлен и добавлен в очередь задач (существует много видов задач), когда его необходимо выполнить. Как только стек выполнения станет пустым, Event Loop достанет код, который нужно выполнить, из очереди задач и поместит его в стек выполнения для выполнения, то есть, по сути, асинхронное или синхронное поведение в JS.

Различные источники задач будут назначены разным очередям задач, а источники задач можно разделить на микрозадачи и макрозадачи. В спецификации ES6 микрозадачи называются заданиями, а макрозадачи — задачами.

微任务包括 process.nextTick ,promise.then ,MutationObserver,其中 process.nextTick 为 Node 独有。

宏任务包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering。

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

Vue внутренне пытается использовать собственные Promise.then, MutationObserver и setImmediate для асинхронных очередей, если среда выполнения не поддерживает это, вместо этого будет использоваться setTimeout(fn, 0).

官方原话

Когда вы устанавливаете vm.someData = 'new value', компонент не сразу перерисовывается. При обновлении очереди компонент будет циклом событий «тик» в следующем обновлении. В большинстве случаев нам не нужно заботиться об этом процессе, но если вы хотите, чтобы состояние, основанное на обновленном DOM, что-то делало, это может быть немного сложно. Хотя Vue.js обычно поощряет разработчиков использовать образ мышления, основанный на данных, чтобы избежать прямого контакта с DOM, но иногда нам приходится это делать. Для того, чтобы завершить обновление DOM Vue ожидания данных после изменения, можно использовать сразу после изменения данных Vue.nextTick (обратный вызов). Таким образом, функция обратного вызова будет вызываться после завершения обновления DOM.

总结:nextTick方法主要是使用了宏任务和微任务,定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。 所以这个nextTick方法就是异步方法

основной код принципа nextTick

let timerFunc  // 会定义一个异步方法
if (typeof Promise !== 'undefined' && isNative(Promise)) {  // promise
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && ( // MutationObserver
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' ) { // setImmediate
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () => {   // setTimeout
    setTimeout(flushCallbacks, 0)
  }
}
// nextTick实现
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
}

Приведенный выше контент лучше всего загрузить исходным кодом Vue на Github и посмотреть его вместе.


Принцип вычислений в Vue

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

функция вычисляемого кэша

Когда мы создаем вычисляемое свойство по умолчанию, оно создает наблюдателя, и этот наблюдатель имеет два свойства.lazy:true,dirty: true, то есть когда создается вычисляемое свойство, оно не выполняется по умолчанию, только когда пользователь принимает значение (то есть, когда оно используется в компоненте), оно будет судить, еслиdirty: trueЕсли это так, наблюдатель будет выполнен для получения значения, и после завершения оценки изменениеdirty: false, так что когда вы снова используете это вычисляемое свойство, условие оценки переходит вdirty: falseКогда операция оценки наблюдателя не выполняется, непосредственно возвращается результат последней оценки.

Так когда будет перерасчет поиска работы?

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

Основной код вычисляемого принципа

function initComputed (vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null)
  const isSSR = isServerRendering()
  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) { // 如果依赖的值没发生变化,就不会重新求值
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

Приведенный выше контент лучше всего загрузить исходным кодом Vue на Github и посмотреть его вместе.


Как реализовано deep : true в Watch?

Официальное представление Vue для просмотра

  • Типы:{ [key: string]: string | Function | Object | Array }

  • подробно:一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每一个属性。

Обычно мы обычно используем watch в проекте для прослушивания соответствующего метода обработки при изменении атрибута в маршруте или данных.

Тогда сценарий использования deep: true заключается в том, что когда отслеживаемое нами свойство является объектом, мы обнаружим, что метод мониторинга в watch не выполняется, потому что Vue не может его обнаружить из-за ограничений современного JavaScript (и отказа от Object .observe) Добавление или удаление свойств объекта. Поскольку Vue выполняет процесс преобразования геттер/сеттер для свойств при инициализации экземпляра, свойство должно существовать в объекте данных, чтобы Vue мог преобразовать его так, чтобы он реагировал.

Глубокий означает глубокое наблюдение. Слушатель будет перемещаться вниз слой за слоем, добавляя этот слушатель ко всем свойствам объекта, но накладные расходы производительности будут очень большими. Любое изменение любого свойства в obj вызовет этот обработчик в слушателе.

В настоящее время мы можем оптимизировать эту проблему следующими способами.

// 使用字符串形式监听具体对象中的某个值。
watch: {
  'obj.a': {
    handler(newName, oldName) {
      console.log('obj.a changed');
    },
    immediate: true, // 立即执行一次handler方法
    deep: true // 深度监测
  }
} 

Следует отметить, что когда мы изменяем значение в массиве через индекс, это не приведет к изменению часов.Пожалуйста, см. выше принцип.Vue中响应式数据的原理是什么?

Конечно, в дополнение к методу изменения массива для отслеживания изменения массива Vue также предоставляет метод Vue.set().

deep : настоящий основной код в Watch

get () {
    pushTarget(this) // 先将当前依赖放到 Dep.target上
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      if (this.deep) { // 如果需要深度监控
        traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get方法
      }
      popTarget()
    }
    return value
}
function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}

Приведенный выше контент лучше всего загрузить исходным кодом Vue на Github и посмотреть его вместе.


Жизненный цикл во Vue

Прикрепите официальное введение Vue к жизненному циклу

Когда вызывается каждая функция жизненного цикла?

  • beforeCreate(){}
在实例初始化以后,数据观测(data observe)之前进行调用,此时获取不到data中的数据。
  • created(){}
在实例创建完成之后调用,这时候实例已经完成了数据观测(data observe),属性和方法的运算,watch/event 事件回调。
注意:这里没有$el
  • beforeMount(){}
在挂载之前调用,相关的render函数首次被调用。
  • mounted(){}
el绑定的元素被内部新创建的$el替换掉,并且挂载到实例上去之后调用。
  • beforeUpdate(){}
数据更新时调用,发生在虚拟DOM重新渲染和打补丁之前。
  • updated(){}
由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
  • beforeDestroy(){}
实例销毁之前调用。在这一步,实例仍然完全可用
  • destroyed(){}
Vue实例销毁后调用。调用后,Vue实例指示的所有东西都会解绑定,所有的事件监听器会被移除。
所有的子实例也会被销毁,该钩子在服务器端渲染期间不被调用。

Общие вещи, которые можно делать внутри жизненного цикла.

  • created(){}
通常我们在项目中会在created(){}生命周期中去调用ajax进行一些数据资源的请求,但是由于当前生命周期无法操作DOM
所以一般在项目中,所有的请求我都会统一放到mounted(){}生命周期中。
  • mounted(){}
在当前生命周期中,实例已经挂载完成,通常我会将ajax请求放到这个生命周期函数中。
如果有一些需要根据获取的数据并去初始化DOM操作,在这里是最佳方案。
  • beforeUpdate(){}
可以在这个生命周期函数中进一步地更改状态,这不会触发附加的重渲染过程
  • updated(){}
可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。 
该钩子在服务器端渲染期间不被调用。
  • destroyed(){}
可以执行一些优化操作,清空定时器,解除绑定事件

Из вышеприведенного описания можно сделать следующие выводы

  1. Запросы Ajax обычно помещаются в жизненный цикл created(){} или Mount(){}. И при создании в представленииdomОн не визуализируется, так что если вы перейдете непосредственно к операции в это времяdomУзел, не могу найти связанные элементы, смонтирован, потому что в это времяdomОн был визуализирован, поэтому им можно напрямую манипулироватьdomузел, обычно размещаемый вmounted, для обеспечения единства логики, т.к. жизненный цикл выполняется синхронно,ajaxвыполняется асинхронно.

    Примечание. Рендеринг на стороне сервера не поддерживает смонтированный метод, поэтому в случае рендеринга на стороне сервера он помещается в созданный

  2. Если в текущем компоненте есть таймер, используется метод $on, связывающийscroll mousemoveВ ожидании события его нужно очистить в хуке beforeDestroy.


Принцип компиляции шаблона в Vue

После просмотра исходного кода обнаруживается, что Vue вызывает метод parseHTML на нижнем уровне для преобразования шаблона в синтаксическое дерево AST (некоторые методы используются внутри) и, наконец, преобразует синтаксическое дерево AST в функцию рендеринга (отрисовка функция), которая объединяет данные для создания виртуального DOM.Создайте новый пользовательский интерфейс после дерева, Diff и Patch.

О виртуальном DOM

После того, как компилятор Vue скомпилирует шаблоны, он компилирует эти шаблоны в функцию рендеринга. Когда функция вызывается, она отрисовывает и возвращает дерево виртуального DOM. Когда у нас есть это виртуальное дерево, мы передаем его функции Patch, которая отвечает за фактическое применение этих виртуальных DOM к реальному DOM.

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

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

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


О разнице между v-if и v-show и о том, как реализован базовый слой.

Здесь я кратко описываю разницу между двумя

  • v-if
如果当前条件判断不成立,那么当前指令所在节点的DOM元素不会渲染
  • v-show
当前指令所在节点的DOM元素始终会被渲染,只是根据当前条件去动态改变 display: none || block
从而达到DOM元素的显示和隐藏。

низкоуровневая реализация

  • v-if

Нижний слой Vue инкапсулирует некоторые специальные методы, и код находится здесь. vue/пакеты/weex-vue-framework/factory.js

VueTemplateCompiler.compile(`<div v-if="true"><span v-for="i in 3">hello</span></div>`);

with(this) {
    return (true) ? _c('div', _l((3), function (i) {
        return _c('span', [_v("hello")])
    }), 0) : _e() // _e()方法创建一个空的虚拟dom等等。
}

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

  • v-show

В v-show ничего не скомпилировано, есть только одна директива, в которой есть директива v-show

VueTemplateCompiler.compile(`<div v-show="true"></div>`);
/**
with(this) {
    return _c('div', {
        directives: [{
            name: "show",
            rawName: "v-show",
            value: (true),
            expression: "true"
        }]
    })
}

Он будет обрабатывать эту инструкцию только во время работы, код выглядит следующим образом:

// v-show 操作的是样式  定义在platforms/web/runtime/directives/show.js
bind (el: any, { value }: VNodeDirective, vnode: VNodeWithData) {
    vnode = locateNode(vnode)
    const transition = vnode.data && vnode.data.transition
    const originalDisplay = el.__vOriginalDisplay =
      el.style.display === 'none' ? '' : el.style.display
    if (value && transition) {
      vnode.data.show = true
      enter(vnode, () => {
        el.style.display = originalDisplay
      })
    } else {
      el.style.display = value ? originalDisplay : 'none'
    }
}

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


Почему v-for нельзя использовать с v-if в проекте

Вы также можете узнать причину, посмотрев исходный код

VueTemplateCompiler.compile(`<div v-if="false" v-for="i in 3">hello</div>`);

with(this) {
    return _l((3), function (i) {
        return (false) ? _c('div', [_v("hello")]) : _e()
    })
}

Мы знаем, что v-for имеет более высокий приоритет, чем v-if, поэтому на этапе компиляции будет обнаружено, что он добавил v-if к каждому элементу внутри, чтобы он был проверен на этапе выполнения, который потребляет много производительности. Поэтому нам следует избегать подобных операций в проекте.

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

Мы можем добиться этого, вычислив свойства

<div v-for="i in computedNumber">hello</div>

export default {
    data() {
        return {
            arr: [1, 2, 3]
        }
    },
    computed: {
        computedNumber() {
            return arr.filter(item => item > 1)
        }
    }
}

Что касается исходного кода инструкции синтаксического анализа, рекомендуется также взглянуть на процесс реализации исходного кода.


О процессе внедрения виртуального DOM

В моем понимании это использование объекта для описания нашей виртуальной DOM-структуры, например:

<div id="container">
    <p></p>
</div>

// 简单用对象来描述的虚拟DOM结构
let obj = {
    tag: 'div',
    data: {
        id: "container"
    },
    children: [
        {
            tag: 'p',
            data: {},
            children: {}
        }
    ]
}

Конечно, реализация на Vue сложнее, я добавлю здесь несколько взглядов для облегчения понимания.

const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2

function createElement (context, tag, data, children, normalizationType, alwaysNormalize) {

    // 兼容不传data的情况
    if (Array.isArray(data) || isPrimitive(data)) {
        normalizationType = children
        children = data
        data = undefined
    }

    // 如果alwaysNormalize是true
    // 那么normalizationType应该设置为常量ALWAYS_NORMALIZE的值
    if (alwaysNormalize) normalizationType = ALWAYS_NORMALIZE
        // 调用_createElement创建虚拟节点
        return _createElement(context, tag, data, children, normalizationType)
    }

    function _createElement (context, tag, data, children, normalizationType) {
        /**
        * 如果存在data.__ob__,说明data是被Observer观察的数据
        * 不能用作虚拟节点的data
        * 需要抛出警告,并返回一个空节点
        * 
        * 被监控的data不能被用作vnode渲染的数据的原因是:
        * data在vnode渲染过程中可能会被改变,这样会触发监控,导致不符合预期的操作
        */
        if (data && data.__ob__) {
            process.env.NODE_ENV !== 'production' && warn(
            `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
            'Always create fresh vnode data objects in each render!',
            context
            )
            return createEmptyVNode()
        }

        // 当组件的is属性被设置为一个falsy的值
        // Vue将不会知道要把这个组件渲染成什么
        // 所以渲染一个空节点
        if (!tag) {
            return createEmptyVNode()
        }

        // 作用域插槽
        if (Array.isArray(children) && typeof children[0] === 'function') {
            data = data || {}
            data.scopedSlots = { default: children[0] }
            children.length = 0
        }

        // 根据normalizationType的值,选择不同的处理方法
        if (normalizationType === ALWAYS_NORMALIZE) {
            children = normalizeChildren(children)
        } else if (normalizationType === SIMPLE_NORMALIZE) {
            children = simpleNormalizeChildren(children)
        }
        let vnode, ns

        // 如果标签名是字符串类型
        if (typeof tag === 'string') {
            let Ctor
            // 获取标签名的命名空间
            ns = config.getTagNamespace(tag)

            // 判断是否为保留标签
            if (config.isReservedTag(tag)) {
                // 如果是保留标签,就创建一个这样的vnode
                vnode = new VNode(
                    config.parsePlatformTagName(tag), data, children,
                    undefined, undefined, context
                )

                // 如果不是保留标签,那么我们将尝试从vm的components上查找是否有这个标签的定义
            } else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
                // 如果找到了这个标签的定义,就以此创建虚拟组件节点
                vnode = createComponent(Ctor, data, context, children, tag)
            } else {
                // 兜底方案,正常创建一个vnode
                vnode = new VNode(
                    tag, data, children,
                    undefined, undefined, context
                )
            }

        // 当tag不是字符串的时候,我们认为tag是组件的构造类
        // 所以直接创建
        } else {
            vnode = createComponent(tag, data, context, children)
        }

        // 如果有vnode
        if (vnode) {
            // 如果有namespace,就应用下namespace,然后返回vnode
            if (ns) applyNS(vnode, ns)
            return vnode
        // 否则,返回一个空节点
        } else {
            return createEmptyVNode()
        }
    }
}

Приведенный выше контент лучше всего загрузить исходным кодом Vue на Github и посмотреть его вместе.


Временная сложность алгоритма сравнения и принцип алгоритма сравнения в Vue

Я мало что знаю об алгоритме, поэтому я просто смотрю видео и введение в статью, чтобы описать его.

комплект из двух деревьевdiffАлгоритм представляет собой временную сложностьO(n3),VueоптимизированныйO(n3) сложностьЗадача преобразуется в O(n)сложностьПроблема (только сравнение братьев и сестер без учета межуровневых проблем) Во внешнем интерфейсе вы редко перемещаете элементы Dom между уровнями. Таким образом, Virtual Dom будет сравнивать только элементы одного уровня.

Принцип алгоритма diff в Vue (видео публичного занятия на станции B и исходный код Vue)

  • 1. Сначала сравните с тем же уровнем, затем сравните дочерние узлы
  • 2. Сначала определите, есть ли у одной стороны сын, а у другой нет сына.
  • 3. Сравните ситуацию с обоими сыновьями
  • 4. Рекурсивно сравнить дочерние узлы

самопонимание

第一种情况:同级比较
当新节点和旧节点不相同情况,新节点直接替换旧节点。

第二种情况:同级比较,节点一致,但一方有子节点,一方没有
当新旧节点相同情况下,如果新节点有子节点,但旧节点没有,那么旧会直接将新节点的子节点插入到旧节点中。
当新旧节点相同情况下,如果新节点没有子节点,但旧节点有子节点,那么旧节点会直接删除子节点。

第三种情况:新旧节点相同,且都有子节点。(这时候旧用到了上图双指针比较方法。)
情况一:
    旧:1234
    新:12345
    当前双指针指向新旧1,1和4,5,判断首部节点一致,指针向后移继续判断,直到最后一项不相同,将新5插入到旧4后面。
    
情况二:
    旧:1234
    新:01234
    当前双指针指向新旧1,0和4,4 发现不想等时会从最后的指针查看,这时候发现相同后,会从后面往前移动指针进行判断。直到到达首部,将新0插入到旧1之前。

情况三:
    旧:1234
    新:4123
    当前发现头部和头部不想等,并且尾部和尾部不想等的时候,就混进行头尾/尾头的互相比较。这时候发现旧的4在新的第一位,就会将自己的4调整到1的前面。
    
情况四:
    旧:1234
    新:2341
    当前发现头部和头部不想等,并且尾部和尾部不想等的时候,就混进行头尾/尾头的互相比较。这时候发现旧的1在新的第四位,就会将自己的1调整到4的后面。
    
特殊情况五:(也就是我们循环数组时候需要加key值的原因)
    旧:1234
    新:2456
    这时候递归遍历会拿新的元素的Key去旧的比较然后移动位置,如果旧的没有就直接将新的放进去,反之将旧的中有,新的没有的元素删除掉。

Благодаря приведенному выше содержанию мы примерно понимаем часть алгоритма сравнения.

основной исходный код

core/vdom/patch.js

const oldCh = oldVnode.children // 老的儿子 
const ch = vnode.children  // 新的儿子
if (isUndef(vnode.text)) {
    if (isDef(oldCh) && isDef(ch)) {
        // 比较孩子
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else if (isDef(ch)) { // 新的儿子有 老的没有
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) { // 如果老的有新的没有 就删除
        removeVnodes(oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {  // 老的有文本 新的没文本
        nodeOps.setTextContent(elm, '') // 将老的清空
    }
} else if (oldVnode.text !== vnode.text) { // 文本不相同替换
    nodeOps.setTextContent(elm, vnode.text)
}
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, vnodeToMove, 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

    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(newCh)
    }

    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)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        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, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }

Зачем использовать ключ в v-for

Причину можно увидеть из приведенного выше рисунка и пятого усложнения алгоритма сравнения.

Например, когда мы используем цикл for для создания трех чекбоксов, когда мы удаляем первый элемент массива текущего цикла с помощью кнопки, мы обнаружим, что первый элемент все еще выбран, а последний элемент удален. диф в процессе. При сравнении старого и нового виртуального DOM обнаруживается, что DOM непротиворечив. В это время первый удаляемый DOM повторно используется внутри (содержимое будет фактическим, а не удаленным содержимым).После сравнения старый DOM будет использоваться повторно. Последний элемент dom удаляется.

1 (1是选中状态)                1 (1是选中状态)
2                              2
3                              3 (被删除了)

Описание может быть немного запутанным, вы можете сами попрактиковаться в проекте. (ps: цикл v-for должен добавить ключ, а ключ не может быть индексным индексом)


Продолжаем подводить итоги. . .