[Читать исходный код vue] Узнайте, как шаблоны и данные отображаются в DOM?

Vue.js

Рекомендуемые ресурсы для чтения

Адрес хостинга исходного кода vue.js

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

накопительная исходная сборка

Виртуальная библиотека DOM с открытым исходным кодом

[Чтение исходного кода vue] Что делает импорт Vue из 'vue'?

предисловие

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

В Vue.js мы можем использовать краткий синтаксис шаблона для декларативного рендеринга данных в DOM:

<div id="app">
  {{ msg }}
</div>
var app = new Vue({
  el: '#app',
  data: {
    msg: 'Hello world!'
  }
})

Страница результатов покажетHello world!. Это знания, которые вы знаете, когда начинаете работать с vue.js. Итак, теперь мы должны спросить, что сделал исходный код vue.js, чтобы шаблон и данные наконец можно было отобразить в DOM? ? ?

отnew Vue()Начинать

При написании проекта vue он будет находиться в файле входа проекта.main.jsСоздайте экземпляр vue в файле. следующим образом:

var app = new Vue({
  el: '#app',
  data: {
    msg: 'Hello world!'
  },
})

Из итогового вывода предыдущей статьи видно, чтоVue — это класс, реализованный с помощью функции.. Исходный код выглядит следующим образом:src/core/instance/index.jsсередина

// _init 方法所在的位置
import { initMixin } from './init' 
// Vue就是一个用 Function 实现的类,所以才通过 new 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)
}

когда мы в проектеnew Vue({})Когда объект передается, это фактически выполнение вышеуказанного метода, и переданные параметрыoptions, а потом позвонилthis._init(options)метод. Метод находится вsrc/core/instance/init.jsв файле. код показывает, как показано ниже:

import { initState } from './state'
Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // 定义了uid
    vm._uid = uid++

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

    vm._isVue = true
    // 合并options 
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      // 这里将传入的options全部合并在$options上。
      // 因此我们可以通过$el访问到 vue 项目中new Vue 中的el
      // 通过$options.data 访问到 vue 项目中new Vue 中的data
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // 初始化函数
    vm._self = vm
    initLifecycle(vm) // 生命周期函数
    initEvents(vm) // 初始化事件链
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }
    // 判断当前的$options.el是否有el 也就是说是否传入挂载的DOM对象
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }

Это видно из приведенного выше кодаthis._init(options)В основном комбинированная конфигурация, жизненный цикл инициализации, центр событий инициализации, рендеринг инициализации, данные инициализации, реквизиты, вычисления, наблюдатель и т. д. Важная часть ничего не делает в коде.

Затем давайте возьмем одну из функций в качестве примера для анализа: взятьinitState(vm)Например:

Почему данные, определенные в data, могут быть доступны в функции ловушки?

В проекте vue, когда данные определены, доступ к свойствам, определенным в данных, можно получить в функции ловушки компонента или в функции методов. почему это? ?

var app = new Vue({
  el: '#app',
  data:(){
      return{
          msg: 'Hello world!'
      }
  },
  mounted(){
    console.log(this.msg) // logs 'Hello world!'
  },

Анализ исходного кода: вы можете увидетьthis._init(options)метод, в разделе функции инициализации естьinitState(vm)функция. Этот метод действительно./state.jsСреда: Конкретный код выглядит следующим образом:

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  // 如果定义了 props 就初始化props;
  if (opts.props) initProps(vm, opts.props)
  // 如果定义了methods 就初始化methods;
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    // 如果定义了data,就初始化data;(要分析的内容从这里开始)
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

существуетinitStateРешение в методе: если данные определены, инициализируйте данные, продолжайте смотреть на функцию для инициализации данных:initData(vm). код показывает, как показано ниже:

function initData (vm: Component) {
 /* 
  这个data 就是 我们vue 项目中定义的data。也就是上面例子中的 
  data(){
    return {
      msg: 'Hello world!'
    }
  }
  */
  let data = vm.$options.data
  // 拿到data 后,做了判断,判断它是不是一个function
  data = vm._data = typeof data === 'function'
    ? getData(data, vm) // 如果是 执行了getData()方法 ,这个方法就是返回data
    : 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
    )
  }
  // 拿到data 定义的属性
  const keys = Object.keys(data) 
  // 拿到props
  const props = vm.$options.props
  // 拿到 methods
  const methods = vm.$options.methods
  let i = keys.length
  // 做了一个循环对比,如果在data 上定义的属性,就不能在props与methods在定义该属性。因为不管是data里定义的,在props里定义的,还是在medthods里定义的,最终都挂载在vm实例上了。见proxy(vm, `_data`, key)
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key) // 代理 定义了Getter 和 Setter
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}
// proxy 代理
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
  // 通过对象 sharedPropertyDefinition  定义了Getter 和 Setter
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
    // 当访问vm.key 的时候其实访问的是 vm[sourceKey][key]
    // 以上述开始的问题,当访问this.msg 实际是访问 this._data.msg
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  // 对vm 的 key 做了一次Getter 和 Setter
  Object.defineProperty(target, key, sharedPropertyDefinition)
  
}

Подводя итог: инициализация данных на самом деле./state.jsв файле. воплощать в жизньinitState()метод, который определяет, что если данные определены, инициализирует данные.

Если данные являются функцией, выполнитеgetData()методreturn data.call(vm, vm). Затем выполните циклическое сравнение атрибутов, определенных в данных на виртуальной машине, реквизитах на виртуальной машине и атрибутах в методах на виртуальной машине.Если атрибуты определены для данных, атрибуты не могут быть определены в реквизитах и методы. Потому что независимо от того, определен ли он в данных, в свойствах или в методах, он в конечном итоге монтируется на экземпляре виртуальной машины. См. прокси(vm,_data, ключ).

Затем привяжите методы Getter и Setter к свойствам виртуальной машины с помощью прокси-метода. Вернемся к предыдущему вопросу: при доступе к this.msg фактически осуществляется доступ к vm._data.msg. Следовательно, данные, определенные в data, действительно могут быть доступны в функции ловушки.

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

Вы можете самостоятельно добавить другое содержимое для инициализации, далее см. раздел «Монтирование ВМ». В конце инициализации обнаруживается, что при наличии атрибута el он вызываетсяvm.$mountМетоды Mount VM, монтируемая цель - сделать шаблон в конечный DOM, затем следующий запрос процесса Vue Bar Mount

Реализация монтирования экземпляра Vue

Во Vue мы переходим$mountМетод экземпляра для монтирования vm. Далее мы рассмотрим реализацию$mount('#app')Что делал исходный код? ? ?

new Vue({
  render: h => h(App),
}).$mount('#app')

$mountМетоды определены в нескольких файлах, таких какsrc/platform/web/entry-runtime-with-compiler.js,src/platform/web/runtime/index.js,src/platform/weex/runtime/index.js. потому что$mountРеализация этого метода связана с платформой и методом построения.

Просто выберите версию компилятора$mountПроанализируйте его, адрес файла находится по адресуsrc/platform/web/entry-runtime-with-compiler.js, код показан ниже:

// 获取vue 原型上的 $mount 方法, 存在变量 mount 上。
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // query 定义在 './util/index'文件中
  // 调用原生的DOM api querySelector() 方法。最后将el转化为一个DOM 对象。
  el = el && query(el)
  ...
  return mount.call(this, el, hydrating)
}

Читая код, мы видим, что код сначала получает прототип vue на$mountметод, сохраните его в переменной mount, а затем переопределите метод. Этот метод обрабатывает входящий el, который может быть строкой или объектом DOM. а потом позвонилquery()метод, который находится в./util/indexв файле. В основном вызывайте собственный метод DOM API querySelector(). Наконец, преобразуйте el в объект DOM и верните его. Выше размещена только основная часть кода.

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

Исходный код также получает $options, чтобы определить, следует ли определять метод рендеринга. Если метод рендеринга не определен, строка el или шаблона в конечном итоге будет скомпилирована вrender()функция.

наконецreturn mount.call(this, el, hydrating). Крепление здесь на прототипе vue$mountметод. в файле./runtime/index. код показывает, как показано ниже:

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

Параметр el представляет смонтированный элемент, который может быть строкой или объектом DOM. Если это строка, она будет вызываться в среде браузера.query()метод в объект DOM. Второй параметр связан с рендерингом на стороне сервера, в среде браузера нам не нужно передавать второй параметр. Вызывается при последнем возвратеmountComponent()метод. Метод определен вsrc/core/instance/lifecycle.js, код показан ниже:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
    vm.$el = el
    ...
    let updateComponent
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      updateComponent = () => {
        const name = vm._name
        const id = vm._uid
        const startTag = `vue-perf-start:${id}`
        const endTag = `vue-perf-end:${id}`
    
        mark(startTag)
        const vnode = vm._render()
        mark(endTag)
        measure(`vue ${name} render`, startTag, endTag)
    
        mark(startTag)
        vm._update(vnode, hydrating)
        mark(endTag)
        measure(`vue ${name} patch`, startTag, endTag)
      }
    } else {
      updateComponent = () => {
        vm._update(vm._render(), hydrating)
      }
    }
  
    new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

Читая код, мы видим, что этот метод сначала создает экземпляр рендеринга.Watcher, в его callback-функции будет вызыватьсяupdateComponentметод, который вызывается в этом методеvm._render()Метод сначала создает виртуальный узел DOM и, наконец, вызываетvm._updateОбновите ДОМ.

Установите, когда он будет окончательно признан корневым узломvm._isMountedдляtrue, Указывает, что этот экземпляр смонтирован и выполняется одновременноmountedфункция крючка.vm.$vnodeПредставляет родительский виртуальный узел экземпляра Vue, поэтому, если он имеет значение Null, это означает, что в настоящее время он является корневым экземпляром Vue.

Такvm._render()Как создать виртуальные узлы DOM?

_render()Визуализировать виртуальные узлы DOM

В Vue 2.0 рендеринг всех компонентов Vue в конечном итоге потребуетrender(). Вью_render()Это закрытый метод экземпляра, который используется для рендеринга экземпляра в виртуальный узел DOM. Его определениеsrc/core/instance/render.jsВ файле код такой:

  Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options
    
    ...
    
    let vnode
    try {
      currentRenderingInstance = vm
      vnode = render.call(vm._renderProxy, vm.$createElement)
    }
  }

Приведенный выше код получает функцию рендеринга из $options экземпляра vue. пройти черезcall()называется_renderProxyа такжеcreateElement()метод, давайте изучимcreateElement()метод.

createElement()

createElement()вinitRender()середина. следующим образом:

// 该函数是在 _init() 过程中执行 initRender()
// 见 './init.js' 文件中的 initRender(vm) 传入vm。就执行到下面的方法。
export function initRender (vm: Component) {
    // 被编译后生成的render函数
    vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) 
    // 手写render函数 创建 vnode 的方法。
    vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) 
}

initRender()выполняется во время _initinitRender()Видеть./init.jsв файлеinitRender(vm)Пройти в вм.

В фактической разработке проекта vue пример рукописной функции рендеринга выглядит следующим образом:

new Vue({
  render(createElement){
    return createElement('div',{
      style:{color:'red'}
    },this.msg)
  },
  data(){
    return{
      msg:"hello world"
    }
  }
}).$mount('#app')

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

см. далее_renderProxyметод:

_renderProxy

_renderProxyметод, который также выполняется в процессе инициализации. см. документацию./init.js, код выглядит следующим образом:

import { initProxy } from './proxy'

if (process.env.NODE_ENV !== 'production') {
    initProxy(vm)
} else {
    vm._renderProxy = vm
}

Если текущая среда является рабочей средой, назначьте виртуальную машину непосредственноvm._renderProxy;

Если текущая среда является средой разработки, выполнитеinitProxy().

Функция находится в./proxy.jsВ файле код такой:

initProxy = function initProxy (vm) {
    // 判断浏览器是否支持 proxy 。
    if (hasProxy) {
      // determine which proxy handler to use
      const options = vm.$options
      const handlers = options.render && options.render._withStripped
        ? getHandler
        : hasHandler
      vm._renderProxy = new Proxy(vm, handlers)
    } else {
      vm._renderProxy = vm
    }
  }

Сначала определите, поддерживает ли браузерproxy. Это новое дополнение к ES6. Оно используется для настройки уровня «перехвата» перед целевым объектом. Доступ к объекту из внешнего мира должен сначала пройти этот уровень перехвата. Поэтому он предоставляет механизм фильтрации и перехватить доступ к внешнему миру.

Если браузер не поддерживаетproxy, затем назначьте vm непосредственноvm._renderProxy;

если браузер это поддерживаетproxy, затем выполнитеnew Proxy().

В итоге:vm._renderвыполняетсяcreateElementметод и возвращает виртуальный узел DOM. Так что же такое виртуальный DOM? ? ?

виртуальный DOM

Прежде чем исследовать виртуальный DOM vue, я рекомендуюВиртуальная библиотека DOM с открытым исходным кодом. Если у вас есть время, заинтересованные друзья могут пойти и узнать больше об этом. Это функция для представления уровня представления приложения. view.js реализует виртуальный DOM, используя его. Это значительно повышает производительность программы. Далее давайте посмотрим, как это делает vue.js.

vnode определяется вsrc/core/vdom/vnode.jsфайл следующим образом:

export default class VNode {
 tag: string | void;
 data: VNodeData | void;
 children: ?Array<VNode>;
 text: string | void;
 elm: Node | void;
 ...
}

Виртуальный DOM — это объект js, который представляет собой абстрактное описание реального DOM, такое как имя тега, данные, имя дочернего узла и т. д. Поскольку виртуальный DOM используется только для отображения реального DOM, он не содержит методов для управления DOM. Поэтому он легче и проще. Поскольку виртуальный DOM создаетсяcreateElementметод, как эта часть достигается? ? ?

createElement

Эксплойт Vue.jscreateElementМетод создает узел DOM, который определен вsrc/core/vdom/create-elemenet.jsВ файле код такой:

export function createElement (
 context: Component, // vm 实例
 tag: any, // 标签
 data: any, // 数据
 children: any,// 子节点 可以构造DOM 树
 normalizationType: any,
 alwaysNormalize: boolean
): VNode | Array<VNode> {
 // 对参数不一致的处理
 if (Array.isArray(data) || isPrimitive(data)) {
   normalizationType = children
   children = data
   data = undefined
 }
 if (isTrue(alwaysNormalize)) {
   normalizationType = ALWAYS_NORMALIZE
 }
 // 处理好参数,则调用 _createElement() 去真正的创建节点。
 return _createElement(context, tag, data, children, normalizationType)
}

createElementметод правильный_createElementИнкапсуляция метода, позволяющая сделать входящие параметры более гибкими.После обработки этих параметров вызывается функция, которая собственно и создает DOM-узел_createElement, код показан ниже:

export function _createElement (
 context: Component,
 tag?: string | Class<Component> | Function | Object,
 data?: VNodeData,
 children?: any,
 normalizationType?: number
): VNode | Array<VNode> {
   ...
   if (normalizationType === ALWAYS_NORMALIZE) {
       children = normalizeChildren(children)
   } else if (normalizationType === SIMPLE_NORMALIZE) {
       children = simpleNormalizeChildren(children)
   }
   ...
}

_createElementМетод предоставляет 5 следующих параметров:

  • contextПредставляет контекст узла DOM, который имеет тип Component;
  • tagПредставляет метку, которая может быть строкой или компонентом;
  • dataПредставляет данные на узле DOM, это тип VNodeData, который можно использовать вflow/vnode.jsнайти его определение в ;
  • childrenПредставляет дочерний узел текущего узла DOM любого типа, который затем необходимо нормализовать как стандартный массив VNode;
  • normalizationTypeУказывает тип спецификации дочернего узла, а методы для разных типов различаются. В основном это относится к тому, является ли функция рендеринга скомпилированной или написанной от руки.

Процесс функции createElement немного подробнее, в этой статье речь пойдет о нормализации дочерних элементов и создании VNode.

нормализация детей

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

_createElement4-й параметр полученchildrenявляются произвольными типами, поэтому нам нужно нормализовать их как типы VNode.

это основано наnormalizationTypeотличается, звонитnormalizeChildren(children)а такжеsimpleNormalizeChildren(children)методы, они определены вsrc/core/vdom/helpers/normalzie-children.jsВ файле код такой:

// render 函数是编译生成的时候调用
// 拍平数组为一维数组
export function simpleNormalizeChildren (children: any) {
  for (let i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}
// 返回一维数组
export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}

simpleNormalizeChildrenМетод Вызов функции рендеринга сцены генерируется компилятором. Однако, когда дочерний узел является компонентом, когда функциональный компонент возвращает массив вместо корня, он пройдетArray.prototype.concatМетодchildrenМассив выравнивается так, что его глубина составляет всего один уровень.

normalizeChildrenСуществует два типа сценариев вызова метода, один сценарий представляет собой написанную от руки функцию рендеринга, когдаchildrenКогда есть только один узел, Vue.js позволяет пользователям размещатьchildrenНаписан как примитивный тип для создания одного простого текстового узла, который будет вызыватьcreateTextVNodeСоздает узел DOM для текстового узла; другой сценарий — при компиляцииslot,v-forКогда ситуация будет иметь вложенные массивы, вызовыnormalizeArrayChildrenметод, код выглядит следующим образом:

function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
  const res = []
  let i, c, lastIndex, last
  for (i = 0; i < children.length; i++) {
    c = children[i]
    if (isUndef(c) || typeof c === 'boolean') continue
    lastIndex = res.length - 1
    last = res[lastIndex]
    //  nested
    if (Array.isArray(c)) {
      if (c.length > 0) {
        c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
        // merge adjacent text nodes
        if (isTextNode(c[0]) && isTextNode(last)) {
          res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
          c.shift()
        }
        res.push.apply(res, c)
      }
    } else if (isPrimitive(c)) {
      if (isTextNode(last)) {
        res[lastIndex] = createTextVNode(last.text + c)
      } else if (c !== '') {
        res.push(createTextVNode(c))
      }
    } else {
      // 如果两个节点都为文本节点,则合并他们。
      if (isTextNode(c) && isTextNode(last)) {
        res[lastIndex] = createTextVNode(last.text + c.text)
      } else {
        if (isTrue(children._isVList) &&
          isDef(c.tag) &&
          isUndef(c.key) &&
          isDef(nestedIndex)) {
          c.key = `__vlist${nestedIndex}_${i}__`
        }
        res.push(c)
      }
    }
  }
  return res
}

normalizeArrayChildrenПринимает 2 аргумента.

  • childrenУказывает дочерний узел, который необходимо нормализовать;
  • nestedIndexпредставляет вложенный индекс; потому что синглchildМожет быть типом массива.normalizeArrayChildrenв основном траверсchildren, получить один узелc, тогда правильноcТип суждения, если это тип массива, то рекурсивно вызыватьnormalizeArrayChildren; если это базовый тип, передатьcreateTextVNodeметод преобразуется в тип VNode, иначе это уже тип VNode, еслиchildrenявляется списком, и список вложен, то в соответствии сnestedIndexобновить егоkey.

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

В этот момент дети стали массивом типа VNode. Это нормализация детей.

Создание виртуальных узлов DOM

назадcreateElementфункция, нормализованнаяchildrenПосле этого следующим шагом является создание экземпляра DOM, код выглядит следующим образом:

let vnode, ns
if (typeof tag === 'string') {
  let Ctor
  ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
  if (config.isReservedTag(tag)) {
    // platform built-in elements
    vnode = new VNode(
      config.parsePlatformTagName(tag), data, children,
      undefined, undefined, context
    )
  } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
    // component
    vnode = createComponent(Ctor, data, context, children, tag)
  } else {
    // 不认识的节点的处理
    vnode = new VNode(
      tag, data, children,
      undefined, undefined, context
    )
  }
} else {
  // direct component options / constructor
  vnode = createComponent(tag, data, context, children)
}

Прямо здесьtagсудить, еслиstringтип, затем рассудите, что если это какой-то встроенный узел, создайте общий VNode напрямую, если это зарегистрированное имя компонента, то передайтеcreateComponentСоздайте VNode типа component, в противном случае создайте VNode с неизвестной меткой. еслиtagЯвляетсяComponentтипа, звоните напрямуюcreateComponentСоздайте узел VNode типа Component.

В этот момент,createElementМетод создает экземпляр виртуального дерева DOM, который используется для описания реального дерева DOM, так как же отобразить его как реальное дерево DOM? ? ? На самом деле этоvm._updateЗавершенный.

update отображает виртуальный DOM в реальный DOM

_updateМетод заключается в том, как преобразовать виртуальный DOM в реальный DOM. Эта часть кода находится вsrc/core/instance/lifecycle.jsВ файле код такой:

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    if (!prevVnode) {
      // 数据的首次渲染时候执行
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    }
   ...
  }

Читая код, мы видим, что при первом отображении данных вызовvm.__patch__()Метод, он получил четыре параметра, в сочетании с процессом разработки нашего фактического проекта vue.vm.$elЭто объект DOM с идентификатором app, то есть:<div id="app"></div>;vnodeСоответствует возвращаемому значению вызова функции рендеринга;hydratingВ случае несерверного рендеринга этоfalse,removeOnlyявляется ложным.

vm.__patch__Определение метода отличается на разных платформах.Определение веб-платформы находится вsrc/platforms/web/runtime/index.js, код выглядит следующим образом:

// 是否在浏览器环境
Vue.prototype.__patch__ = inBrowser ? patch : noop

На веб-платформе рендеринг на стороне сервера также влияет на этот метод. Поскольку при рендеринге на стороне сервера нет реальной среды DOM браузера, поэтому нет необходимости преобразовывать VNode в DOM, поэтому это пустая функция, а при рендеринге на стороне браузера она указывает на метод patch, который определен вsrc/platforms/web/runtime/patch.jsВ файле код такой:

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'

const modules = platformModules.concat(baseModules)

export const patch: Function = createPatchFunction({ nodeOps, modules })

Прочтите код, чтобы узнатьcreatePatchFunctionВозвращаемое значение метода передается в объект, где

  • nodeOpsИнкапсулирует ряд методов манипулирования DOM;
  • modulesОпределяет реализацию функции ловушки модуля;createPatchFunctionМетод определен вsrc/core/vdom/patch.jsВ файле код такой:
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

export function createPatchFunction (backend) {
  let i, j
  const cbs = {}

  const { modules, nodeOps } = backend

  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]])
      }
    }
  }
    
  // ...
  // 定义了一些辅助函数
  
  
  // 当调用 vm.__dispatch__时,其实就是调用下面的 patch 方法
  // 这块应用了函数柯理化的技巧
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    // ...
    return vnode.elm
  }
}

createPatchFunctionРяд вспомогательных методов определен внутри и, наконец, возвращаетpatchметод, этот метод назначаетсяvm._updateВызов в функцииvm.__patch__. Другими словамиvm.__dispatch__, на самом деле звонитpatch (oldVnode, vnode, hydrating, removeOnly)метод, этот блок на самом деле является применением метода курирования функций.

patchМетод получает 4 параметра, а именно:

  • oldVnodeПредставляет старый узел VNode, который также может не существовать или быть объектом DOM;
  • vnodeПредставляет узел VNode, возвращенный после выполнения _render;
  • hydratingУказывает, является ли это рендеринг сервера;
  • removeOnlyЭто для переходной группы.

анализироватьpatchметод, потому что входящийoldVnodeна самом деле является DOM-контейнером, поэтомуisRealElementверно, то звонитеemptyNodeAtметод положитьoldVnodeПреобразуется в виртуальный узел DOM (объект js), а затем вызываетсяcreateElmметод. код показывает, как показано ниже:

if (isRealElement) {
    oldVnode = emptyNodeAt(oldVnode)
}
function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    vnode = ownerArray[index] = cloneVNode(vnode)
  }

  vnode.isRootInsert = !nested // for transition enter check

  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  // 接下来判断 vnode 是否包含 tag,
  // 如果包含,先对tag的合法性在非生产环境下做校验,看是否是一个合法标签;
  // 然后再去调用平台 DOM 的操作去创建一个占位符元素。
  if (isDef(tag)) {
    if (process.env.NODE_ENV !== 'production') {
      if (data && data.pre) {
        creatingElmInVPre++
      }
      if (isUnknownElement(vnode, creatingElmInVPre)) {
        warn(
          'Unknown custom element: <' + tag + '> - did you ' +
          'register the component correctly? For recursive components, ' +
          'make sure to provide the "name" option.',
          vnode.context
        )
      }
    }
     // 调用 createChildren 方法去创建子元素:
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    setScope(vnode)

    /* istanbul ignore if */
    if (__WEEX__) {
      // ...
    } else {
      // 调用 createChildren 方法去创建子元素
      // 用 createChildren 方法遍历子虚拟节点,递归调用 createElm
      // 在遍历过程中会把 vnode.elm 作为父容器的 DOM 节点占位符传入。
      createChildren(vnode, children, insertedVnodeQueue)
      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }
      insert(parentElm, vnode.elm, refElm)
    }

    if (process.env.NODE_ENV !== 'production' && data && data.pre) {
      creatingElmInVPre--
    }
  } else if (isTrue(vnode.isComment)) {
    vnode.elm = nodeOps.createComment(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  } else {
    vnode.elm = nodeOps.createTextNode(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  }
}

createElmЦель метода — создать настоящий DOM из виртуального узла и вставить его в родительский узел. Определите, содержит ли vnode тег. Если да, сначала проверьте действительность тега в непроизводственной среде, чтобы убедиться, что это допустимый тег, а затем вызовите операцию DOM платформы, чтобы создать элемент-заполнитель. тогда позвониcreateChildrenспособ создания дочерних элементов,createChildrenКод метода следующий:

createChildren(vnode, children, insertedVnodeQueue)

function createChildren (vnode, children, insertedVnodeQueue) {
  if (Array.isArray(children)) {
    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(children)
    }
    for (let i = 0; i < children.length; ++i) {
      createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
    }
  } else if (isPrimitive(vnode.text)) {
    nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
  }
}

createChildrenМетод обходит дочерние виртуальные узлы и рекурсивно вызываетcreateElm, в процессе обхода vnode.elm будет передан в качестве заполнителя узла DOM родительского контейнера. тогда позвониinvokeCreateHooksметод выполняет все хуки создания и помещает vnode вinsertedVnodeQueueсередина. последний звонокinsertМетод вставляет DOM в родительский узел, так как он вызывается рекурсивно, дочерний элемент будет вызываться первымinsert, поэтому порядок вставки всего узла дерева vnode сначала дочерний, а затем родительский.insertметод определен вsrc/core/vdom/patch.jsВ файле код такой:

insert(parentElm, vnode.elm, refElm)

function insert (parent, elm, ref) {
  if (isDef(parent)) {
    if (isDef(ref)) {
      if (ref.parentNode === parent) {
        nodeOps.insertBefore(parent, elm, ref)
      }
    } else {
      nodeOps.appendChild(parent, elm)
    }
  }
}

Прочтите код, чтобы увидеть,insertМетод вызывает некоторые вспомогательные методы для вставки дочернего узла в родительский узел (фактически это вызов API нативного DOM для работы с DOM), эти вспомогательные методы определены вsrc/platforms/web/runtime/node-ops.jsв файле. На этом этапе узел DOM, динамически созданный Vue, завершен. эмм~~ Оглядываясь назад на эту картинку.

конец

Недавно я серьезно посмотрю на исходный код vue.js. [Читать исходный код vue] будет обновляться в соответствии с серией. Делясь собственными знаниями, я также надеюсь общаться с большим количеством сверстников, вот и все.

первый раз:[Чтение исходного кода vue] Что делает импорт Vue из 'vue'?

Вторая статья:[Читать исходный код vue] Узнайте, как шаблоны и данные отображаются в DOM?【В настоящее время читаю】