Углубленный анализ исходного кода Vue — основа компонентов

Vue.js
Углубленный анализ исходного кода Vue — основа компонентов

компонентVueВажным ядром , когда мы выполняем разработку проекта, мы составим структуру страницы. Компонентизация означает независимость и совместное использование, и два вывода не противоречат друг другу. Независимая разработка компонентов позволяет разработчикам сосредоточиться на разработке и расширении функционального элемента, а концепция дизайна компонентов делает функциональные элементы более пригодными для повторного использования. Различные страницы могут совместно использовать функции компонентов. Для разработчиков пишитеVueКомпоненты освоеныVueОснова развития,VueОфициальный сайт также уделяет много места знакомству с системой компонентов и различными способами использования. В этом разделе мы углубимся вVueИсходный код внутри компонента, понятьИдея реализации регистрации компонентов в сочетании с основным процессом рендеринга и монтирования компонентов на основе монтирования экземпляра, представленного в предыдущем разделе, и, наконец, мы проанализируем, как компоненты связаны друг с другом.. Я считаю, что освоение этих низкоуровневых идей реализации поможет нам решать проблемы в будущем.vueБудет очевидная помощь по вопросам, связанным с компонентами.

5.1 Два метода регистрации компонентов

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

5.1.1 Глобальная регистрация
Vue.component('my-test', {
    template: '<div>{{test}}</div>',
    data () {
        return {
            test: 1212
        }
    }
})
var vm = new Vue({
    el: '#app',
    template: '<div id="app"><my-test><my-test/></div>'
})

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

5.1.2 Местная регистрация
var myTest = {
    template: '<div>{{test}}</div>',
    data () {
        return {
            test: 1212
        }
    }
}
var vm = new Vue({
    el: '#app',
    component: {
        myTest
    }
})

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

5.1.3 Процесс регистрации

После краткого обзора двух методов регистрации компонентов давайте посмотрим, что происходит в процессе регистрации.В качестве примера возьмем глобальную регистрацию компонентов. это проходитVue.component(name, {...})Регистрация компонентов,Vue.componentвVueСтатический метод, определенный на этапе введения исходного кода.

// 初始化全局api
initAssetRegisters(Vue);
var ASSET_TYPES = [
    'component',
    'directive',
    'filter'
];
function initAssetRegisters(Vue){
    // 定义ASSET_TYPES中每个属性的方法,其中包括component
    ASSET_TYPES.forEach(function (type) {
    // type: component,directive,filter
      Vue[type] = function (id,definition) {
          if (!definition) {
            // 直接返回注册组件的构造函数
            return this.options[type + 's'][id]
          }
          ...
          if (type === 'component') {
            // 验证component组件名字是否合法
            validateComponentName(id);
          }
          if (type === 'component' && isPlainObject(definition)) {
            // 组件名称设置
            definition.name = definition.name || id;
            // Vue.extend() 创建子组件,返回子类构造器
            definition = this.options._base.extend(definition);
          }
          // 为Vue.options 上的component属性添加将子类构造器
          this.options[type + 's'][id] = definition;
          return definition
        }
    });
}

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

function validateComponentName(name) {
    if (!new RegExp(("^[a-zA-Z][\\-\\.0-9_" + (unicodeRegExp.source) + "]*$")).test(name)) {
      // 正则判断检测是否为非法的标签
      warn(
        'Invalid component name: "' + name + '". Component names ' +
        'should conform to valid custom element name in html5 specification.'
      );
    }
    // 不能使用Vue自身自定义的组件名,如slot, component,不能使用html的保留标签,如 h1, svg等
    if (isBuiltInTag(name) || config.isReservedTag(name)) {
      warn(
        'Do not use built-in or reserved HTML elements as component ' +
        'id: ' + name
      );
    }
  }

После проверки правильности имени компонента он вызоветextendметод для создания конструктора подкласса для компонента, в это времяthis.options._baseпредставляет собойVueКонструктор.extendОпределение метода выделено во введении к главе о слиянии опций.Создайте подкласс на основе родительского класса, родительский класс в это времяVueИ подкласс процесса создания наследует метод родительского класса и объединится с вариантами родительского класса и, наконец, вернуть конструктор подкласса.

В коде тоже есть логика,Vue.component()По умолчанию в качестве имени компонента будет использоваться первый параметр, но если параметры компонента имеютnameимущество,nameЗначение свойства переопределяет имя компонента.

Подводя итог, компонент глобальной регистрацииVueПеред созданием экземпляра создайтеVueконструктор подкласса и загружает информацию о компоненте в экземплярoptions.componentsв объекте.

** Далее естественно возникает вопрос, чем отличается реализация локальной регистрации от глобальной? **Мы не спешим анализировать процесс регистрации локальных компонентов, сначала на основе глобально зарегистрированных компонентов мы увидим, чем отличается процесс монтирования как компонента.

5.2 Создание компонента Vnode

В предыдущем разделе мы представилиVueКак поставить шаблон черезrenderПреобразование функции, которое в итоге порождаетVnode tree, без компонентов,_renderПоследним шагом функции является прямой вызовnew Vnodeсоздать полныйVnode tree. Однако есть большая часть ветки, которую мы не проанализировали, то есть сцена встречи с плейсхолдерами компонентов. Если вы сталкиваетесь с компонентами на этапе выполнения, процесс обработки намного сложнее, чем ожидалось, мы анализируем его с помощью блок-схемы.

5.2.1 Блок-схема создания Vnode

5.2.2 Анализ конкретного процесса

Давайте проанализируем этот процесс в сочетании с реальными примерами и блок-схемами:

  • Сцены
Vue.component('test', {
  template: '<span></span>'
})
var vm = new Vue({
  el: '#app',
  template: '<div><test></test></div>'
})
  • отецrenderфункция
function() {
  with(this){return _c('div',[_c('test')],1)}
}
  • VueИнициализация корневого экземпляра будет выполненаvm.$mount(vm.$options.el)Процесс монтирования инстанса, по предыдущей логике будет проходить весь процессrenderгенерация функцийVnode,так же какVnodeгенерировать реальныеDOMпроцесс.
  • renderгенерация функцийVnodeВо время процесса дочерний процесс будет отдавать приоритет генерации выполнения родительского процесса.Vnodeпроцесс, то есть_c('test')Функция будет выполнена первой.'test'сначала будет считать это нормальнымhtmlМетки также являются заполнителями для компонентов.
  • Если это общая метка, она будет выполнятьсяnew Vnodeprocess, который также является процессом, который мы анализировали в предыдущей главе; если он является заполнителем для компонента, он будет введен в предположении, что компонент был зарегистрирован.createComponentСоздание дочерних компонентовVnodeпроцесс.
  • createComponentзаключается в создании компонентаVnodeпроцесса, процесс создания снова объединит конфигурацию параметров и установит внутренние перехватчики, связанные с компонентами (роль внутренних перехватчиков будет снова упомянута в следующей статье), и, наконец, передастnew Vnode()сгенерировано сvue-componentначалоVirtual DOM
  • renderПроцесс выполнения функции также является циклическим рекурсивным вызовом для созданияVnodeпроцесс, после выполнения шагов 3 и 4, полныйVnode tree

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

// 内部执行将render函数转化为Vnode的函数
function _createElement(context,tag,data,children,normalizationType) {
  ···
  if (typeof tag === 'string') {
    // 子节点的标签为普通的html标签,直接创建Vnode
    if (config.isReservedTag(tag)) {
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      );
    // 子节点标签为注册过的组件标签名,则子组件Vnode的创建过程
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // 创建子组件Vnode
      vnode = createComponent(Ctor, data, context, children, tag);
    }
  }
}

config.isReservedTag(tag)Используется для определения того, является ли этикетка обычной.htmlМетка, если это обычный узел, он будет создан напрямуюVnodeузел, если нет, вам нужно определить, был ли зарегистрирован этот компонент-заполнитель, мы можем пройтиcontext.$options.components[组件名]Получите параметры зарегистрированного компонента. Как узнать, был ли компонент зарегистрирован глобально, см.resolveAssetреализация.

// 需要明确组件是否已经被注册
  function resolveAsset (options,type,id,warnMissing) {
    // 标签为字符串
    if (typeof id !== 'string') {
      return
    }
    // 这里是 options.component
    var assets = options[type];
    // 这里的分支分别支持大小写,驼峰的命名规范
    if (hasOwn(assets, id)) { return assets[id] }
    var camelizedId = camelize(id);
    if (hasOwn(assets, camelizedId)) { return assets[camelizedId] }
    var PascalCaseId = capitalize(camelizedId);
    if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] }
    // fallback to prototype chain
    var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
    if (warnMissing && !res) {
      warn(
        'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
        options
      );
    }
    // 最终返回子类的构造器
    return res
  }

После получения зарегистрированного конструктора подкласса вызовитеcreateComponentспособ создания дочернего компонентаVnode

 // 创建子组件过程
  function createComponent (
    Ctor, // 子类构造器
    data,
    context, // vm实例
    children, // 子节点
    tag // 子组件占位符
  ) {
    ···
    // Vue.options里的_base属性存储Vue构造器
    var baseCtor = context.$options._base;

    // 针对局部组件注册场景
    if (isObject(Ctor)) {
      Ctor = baseCtor.extend(Ctor);
    }
    data = data || {};
    // 构造器配置合并
    resolveConstructorOptions(Ctor);
    // 挂载组件钩子
    installComponentHooks(data);

    // return a placeholder vnode
    var name = Ctor.options.name || tag;
    // 创建子组件vnode,名称以 vue-component- 开头
    var vnode = new VNode(("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),data, undefined, undefined, undefined, context,{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },asyncFactory);

    return vnode
  }

Большая часть кода удалена здесь, оставлено только созданиеVnodeСоответствующий код в конечном итоге пройдетnew Vueсоздать экземпляр имени сvue-component-началоVnodeузел. Двумя ключевыми шагами являются настройка слияния и установка функций подключения компонентов.Содержание слияния параметров можно просмотреть в первых двух разделах этой серии.Проверьте это здесь.installComponentHooksЧто делается при установке функции хука компонента.

  // 组件内部自带钩子
 var componentVNodeHooks = {
    init: function init (vnode, hydrating) {
    },
    prepatch: function prepatch (oldVnode, vnode) {
    },
    insert: function insert (vnode) {
    },
    destroy: function destroy (vnode) {
    }
  };
var hooksToMerge = Object.keys(componentVNodeHooks);
// 将componentVNodeHooks 钩子函数合并到组件data.hook中 
function installComponentHooks (data) {
    var hooks = data.hook || (data.hook = {});
    for (var i = 0; i < hooksToMerge.length; i++) {
      var key = hooksToMerge[i];
      var existing = hooks[key];
      var toMerge = componentVNodeHooks[key];
      // 如果钩子函数存在,则执行mergeHook$1方法合并
      if (existing !== toMerge && !(existing && existing._merged)) {
        hooks[key] = existing ? mergeHook$1(toMerge, existing) : toMerge;
      }
    }
  }
function mergeHook$1 (f1, f2) {
  // 返回一个依次执行f1,f2的函数
    var merged = function (a, b) {
      f1(a, b);
      f2(a, b);
    };
    merged._merged = true;
    return merged
  }

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

5.2.3 Разница между локальной регистрацией и глобальной регистрацией

Когда дело доходит до использования глобальной регистрации и локальной регистрации, остается вопрос, в чем разница между локальной регистрацией и глобальной регистрацией. На самом деле принцип частичной регистрации также прост, когда мы используем компоненты частичной регистрации, мы будем передавать параметры в конфигурации опций родительского компонента.componentsДобавьте конфигурацию объекта подкомпонента, аналогичную глобальной регистрации вVueизoptions.componentРезультат добавления конструктора подкомпонента аналогичен. Разница в том, что:

- 1. Конфигурация объекта, добавленная локальной регистрацией, находится под компонентом, а подкомпонент, добавленный глобальной регистрацией, находится под корневым экземпляром. - 2. Локальная регистрация добавляет объект конфигурации подкомпонента, а глобальная регистрация добавляет конструктор подкласса.

Поэтому в частичной регистрации отсутствует процесс построения конструктора подкласса, где этот процесс должен выполняться? назадcreateComponentИсходный код исходного кода различает локальные и глобальные зарегистрированные компоненты в зависимости от того, является ли параметр объектом или функцией.Если значением параметра является объект, компонент является локально зарегистрированным компонентом, и создается дочерний элемент.Vnodeвызовет родительский классextendметод для создания конструктора подкласса.

function createComponent (...) {
  ...
  var baseCtor = context.$options._base;

  // 针对局部组件注册场景
  if (isObject(Ctor)) {
      Ctor = baseCtor.extend(Ctor);
  }
}

5.3 Компонент Vnode отображает настоящий DOM

Согласно предыдущему анализу, независимо от того, является ли компонент зарегистрированным глобально или локально, экземпляр компонента не создается, так на каком этапе происходит процесс создания экземпляра компонента? Давайте посмотримVnode treeсделать реальнымDOMпроцесс.

5.3.1 Блок-схема рендеринга реального узла

5.3.2 Анализ конкретного процесса
    1. проходить черезvm._render()генерировать полныйVirtual DomПосле дерева выполнитьVnodeсделать реальнымDOMпроцесс, которыйvm.update()выполнение метода, и его ядром являетсяvm.__patch__.
    1. vm.__patch__внутренний пройдетcreateElmсоздать настоящийDOMэлемент, во время которого ребенок встречаетсяVnodeбудет вызываться рекурсивноcreateElmметод.
    1. В процессе рекурсивного вызова определяется, является ли тип узла типом компонента, путемcreateComponentМетод определения и рендеринга методаVnodeсценический методcreateComponentПо-другому он будет называть дочерний компонентinitИнициализируйте функцию ловушки и завершитеDOMвставлять.
    1. initЯдром функции хука инициализации являетсяnewСоздайте экземпляр этого подкомпонента и смонтируйте подкомпонент Процесс создания экземпляра подкомпонента восходит к процессу слияния конфигурации, инициализации жизненного цикла, инициализации центра событий и инициализации рендеринга. Монтирование экземпляра будет выполнено снова$mountОбработать.
    1. После завершения создания всех подкомпонентов и монтирования узла, наконец, возвращаемся к монтированию корневого узла.

__patch__Основной код проходит черезcreateElmСоздайте реальный узел, когда дочерний элемент встречается в процессе созданияvnode, он позвонитcreateChildren,createChildrenЦель состоит в том, чтобы соединитьvnodeрекурсивный вызовcreateElmСоздайте узлы подкомпонента.

// 创建真实dom
function createElm (vnode,insertedVnodeQueue,parentElm,refElm,nested,ownerArray,index) {
  ···
  // 递归创建子组件真实节点,直到完成所有子组件的渲染才进行根节点的真实节点插入
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
  ···
  var children = vnode.children;
  // 
  createChildren(vnode, children, insertedVnodeQueue);
  ···
  insert(parentElm, vnode.elm, refElm);
}
function createChildren(vnode, children, insertedVnodeQueue) {
  for (var i = 0; i < children.length; ++i) {
    // 遍历子节点,递归调用创建真实dom节点的方法 - createElm
    createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
  }
}

createComponentметод на дочерних компонентахVnodeобработки, помните, что вVnodeСоздать сцену как подпрограммуVnodeУстановлен ли ряд функций ловушек? На этом шаге мы можем судить о том, является ли это зарегистрированным подкомпонентом, по тому, есть ли у нас эти определенные ловушки. Если условия соблюдены, выполните компонентinitкрюк.

initКрючки делают только две вещи,Создайте конструктор компонента и выполните процесс монтирования подкомпонентов.(keep-aliveФилиал, чтобы увидеть анализ конкретной статьи)

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  var i = vnode.data;
  // 是否有钩子函数可以作为判断是否为组件的唯一条件
  if (isDef(i = i.hook) && isDef(i = i.init)) {
    // 执行init钩子函数
    i(vnode, false /* hydrating */);
  }
  ···
}
var componentVNodeHooks = {
  // 忽略keepAlive过程
  // 实例化
  var child = vnode.componentInstance = createComponentInstanceForVnode(vnode,activeInstance);
  // 挂载
  child.$mount(hydrating ? vnode.elm : undefined, hydrating);
}
function createComponentInstanceForVnode(vnode, parent) {
  ···
  // 实例化Vue子组件实例
  return new vnode.componentOptions.Ctor(options)
}

очевидноVnodeгенерировать реальныеDOMЭтот процесс также представляет собой процесс рекурсивного создания дочерних узлов,patchЕсли процесс встречает подVnode, подкомпонент будет создан первым, и процесс монтирования подкомпонента будет выполнен, и процесс монтирования вернется к_render,_updateпроцесс. Во всех детяхVnodeПосле рекурсивного монтирования и в конечном итоге действительно смонтируется корневой узел.

5.4 Установка соединения компонентов

В ежедневном развитии мы можем пройтиvm.$parentПолучите родительский экземпляр, вы также можете передать родительский экземплярvm.$childrenПолучите дочерние компоненты в экземпляре. Очевидно,VueУровень ассоциации устанавливается между компонентами и компонентами. В следующем содержании мы рассмотрим, как установить связь между компонентами.

Будь то родительский экземпляр или дочерний экземпляр, существуетinitLifecycleпроцесс. Этот процесс добавит текущий экземпляр к родительскому экземпляру.$childrenсвойства и установить свои собственные$parentСвойство указывает на родительский экземпляр. **Укажите конкретный сценарий применения:

<div id="app">
    <component-a></component-a>
</div>
Vue.component('component-a', {
    template: '<div>a</div>'
})
var vm = new Vue({ el: '#app'})
console.log(vm) // 将实例对象输出

из-заvueЭкземпляр up не имеет родителя, поэтомуvm.$parentдляundefined,vmиз$childrenсвойство указывает на дочерний компонентcomponentAпример.

ПодсборкаcomponentAиз$parentсвойство указывает на своего родителяvmпример, его$childrenСвойство указывает на NULL

Источник интерпретируется следующим образом:

function initLifecycle (vm) {
    var options = vm.$options;
    // 子组件注册时,会把父组件的实例挂载到自身选项的parent上
    var parent = options.parent;
    // 如果是子组件,并且该组件不是抽象组件时,将该组件的实例添加到父组件的$parent属性上,如果父组件是抽象组件,则一直往上层寻找,直到该父级组件不是抽象组件,并将,将该组件的实例添加到父组件的$parent属性
    if (parent && !options.abstract) {
        while (parent.$options.abstract && parent.$parent) {
        parent = parent.$parent;
        }
        parent.$children.push(vm);
    }
    // 将自身的$parent属性指向父实例。
    vm.$parent = parent;
    vm.$root = parent ? parent.$root : vm;

    vm.$children = [];
    vm.$refs = {};

    vm._watcher = null;
    vm._inactive = null;
    vm._directInactive = false;
    // 该实例是否挂载
    vm._isMounted = false;
    // 该实例是否被销毁
    vm._isDestroyed = false;
    // 该实例是否正在被销毁
    vm._isBeingDestroyed = false;
}

Наконец, кратко поговорим об абстрактных компонентах, вvueСуществует множество встроенных абстрактных компонентов, таких как<keep-alive></keep-alive>,<slot><slot>и т. д., эти абстрактные компоненты не появляются на пути дочернего родителя, и они не участвуютDOMрендеринг.

5.5 Резюме

Этот раздел объединяет фактический пример для анализа процесса регистрации компонента с процессом рендеринга монтирования компонента.VueМы можем определить глобальные компоненты и локальные компоненты.Глобальные компоненты должны быть зарегистрированы глобально.Основной методVue.component, ему нужно объявить и зарегистрироваться до создания экземпляра корневого компонента, причина в том, что нам нужно получить информацию о конфигурации компонента перед созданием экземпляра и объединить его вoptions.componentsв опциях. Суть регистрации в звонкеextendСоздайте конструктор подкласса, разница между глобальным и локальным заключается в том, что локальный конструктор подкласса создается при создании подкомпонента.Vnodeсцена. при создании субVnodeНаиболее важным шагом на этом этапе является определение множества хуков, которые используются внутри. с полнымVnode treeДальше будет настоящееDOMгенерация, на этом этапе, если встречается дочерний компонентVnodeБудет создан экземпляр субконструктора и смонтирован субкомпонент. После рекурсивного завершения монтирования подкомпонентов он, наконец, возвращается к монтированию корневого компонента. Обладая базовыми знаниями о компонентах, мы сосредоточимся на расширенном использовании компонентов в следующем разделе.