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

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

Продолжая предыдущий раздел, мыVueКратко рассматривается сложный процесс монтажа с помощью графического процесса и метода анализа кода, а также обсуждается общий процесс компиляции шаблона. Однако в основе монтирования мы не анализировали, как функция рендеринга преобразуется в визуализацию после компиляции шаблона.DOMузла. Поэтому в этой главе мы вернемся кVueПоследнее звено монтирования инстанса: рендерингDOMузел. В рендеринг действительностиDOMв процессе,VueвиртуальныйDOMконцепция, котораяVueЕще одно важное понятие в архитектурном дизайне. виртуальныйDOMтак какJSобъект и реальностьDOMБуферный слой посередине справаJSЧастая операцияDOMПроблемы с производительностью, вызванные , имеют хороший эффект смягчения.

4.1 Virtual DOM

4.1.1 Процесс рендеринга в браузере

Когда браузер получаетHtmlфайл,JSДвижок и движок рендеринга браузера начинают работать. С точки зрения движка рендеринга, сначалаhtmlфайл анализируется вDOMдерево, тем временем браузер распознает и загрузитCSSстиль иDOMДеревья объединены в единое дерево рендеринга. С деревом рендеринга механизм рендеринга вычисляет информацию о положении всех элементов и, наконец, печатает окончательный контент на экране путем рисования.JSХотя движок и движок рендеринга являются двумя независимыми потоками, движок JS может запускать движок рендеринга.Когда мы изменяем положение или внешний вид элемента с помощью скрипта,JSдвигатель будет использоватьDOMСвязанныйAPIметод работыDOMОбъект, в это время начинает работать механизм рендеринга, и механизм рендеринга запускает перекомпоновку или перерисовку. Ниже приведены две концепции перерисовки оплавлением:

  • Reflow: когда мыDOMКогда модификация элемента вызывает изменение размера элемента, браузеру необходимо пересчитать размер и положение элемента и, наконец, отобразить результат пересчета.Этот процесс называется перекомпоновкой.
  • Перерисовать: когда мыDOMКогда модификация изменяет только цвет элемента, браузеру не нужно пересчитывать размер и положение элемента в это время, а нужно только перерисовать новый стиль. Этот процесс называется перекрашиванием.

Очевидно, что переформатирование дороже, чем перерисовка..

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

4.1.2 Буферный уровень — виртуальный DOM

виртуальныйDOMэто решить частую операциюDOMПродукты, вызывающие проблемы с производительностью. виртуальныйDOM(именуемый в дальнейшемVirtual DOM) заключается в абстрагировании состояния страницы какJSформа объекта, которая по существуJSи настоящийDOMсредний слой, когда мы хотим использоватьJSСкрипты запускаются массовоDOMПри работе приоритет будет отдаватьсяVirtual DOMэтоJSобъект и, наконец, уведомить и обновить реальную часть, сравнив часть, которая будет измененаDOM. Хотя операция реальная в итогеDOM,ноVirtual DOMНесколько изменений могут быть объединены в одну пакетную операцию, тем самым уменьшаяDOMКоличество перестановок, что, в свою очередь, сокращает время, необходимое для генерации дерева рендеринга и отрисовки.

давайте посмотрим настоящуюDOMЧто включено:

Браузер поставит реальныйDOMДизайн очень сложный, включает не только собственное описание атрибутов, определение размера и положения, но иDOMСобственные события браузера и т. д. Из-за такой сложной структуры мы часто работаемDOMБолее или менее приведет к проблемам с производительностью браузера. И как данные и правдаDOMслой буфера междуVirtual DOMпросто использовал для сопоставления с реальнымDOM, поэтому операции включения не требуютсяDOMметод, ему нужно сосредоточиться только на нескольких свойствах объекта.

// 真实DOM
<div id="real"><span>dom</span></div>

// 真实DOM对应的JS对象
{
    tag: 'div',
    data: {
        id: 'real'
    },
    children: [{
        tag: 'span',
        children: 'dom'
    }]
}

4.2 Vnode

VueВ оптимизации механизма рендеринга также введеноvirtual domпонятие, используетсяVnodeЭтот конструктор для описанияDOMузел.

4.2.1 Конструктор Vnode

var VNode = function VNode (tag,data,children,text,elm,context,componentOptions,asyncFactory) {
    this.tag = tag; // 标签
    this.data = data;  // 数据
    this.children = children; // 子节点
    this.text = text;
    ···
    ···
  };

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

Исходный код также определяет созданиеVnodeсопутствующие методы.

4.2.2 Создание узла комментариев Vnode

// 创建注释vnode节点
var createEmptyVNode = function (text) {
    if ( text === void 0 ) text = '';

    var node = new VNode();
    node.text = text;
    node.isComment = true; // 标记注释节点
    return node
};

4.2.3 Создать текстовый узел Vnode

// 创建文本vnode节点
function createTextVNode (val) {
    return new VNode(undefined, undefined, undefined, String(val))
}

4.2.4 Клонировать vnode

function cloneVNode (vnode) {
    var cloned = new VNode(
      vnode.tag,
      vnode.data,
      vnode.children && vnode.children.slice(),
      vnode.text,
      vnode.elm,
      vnode.context,
      vnode.componentOptions,
      vnode.asyncFactory
    );
    cloned.ns = vnode.ns;
    cloned.isStatic = vnode.isStatic;
    cloned.key = vnode.key;
    cloned.isComment = vnode.isComment;
    cloned.fnContext = vnode.fnContext;
    cloned.fnOptions = vnode.fnOptions;
    cloned.fnScopeId = vnode.fnScopeId;
    cloned.asyncMeta = vnode.asyncMeta;
    cloned.isCloned = true;
    return cloned
  }

Уведомление:cloneVnodeправильноVnodeКлон — это всего лишь поверхностная копия, он не создает глубокого клона дочерних узлов.

4.3 Создание виртуального DOM

Кратко рассмотрим процесс монтирования.Vueпример$mountметод, в то время как$mountядроmountComponentфункция. Если мы пройдемtemplateШаблон, шаблон будет сначала проанализирован компилятором и, наконец, сгенерирован соответствующий код в соответствии с различными платформами.В настоящее время соответствующий код должен бытьwithИнкапсулированное предложениеrenderФункция; если переданоrenderфункции, пропустите процесс компиляции шаблона и сразу переходите к следующему этапу. Следующий этап – получитьrenderфункция, вызовvm._render()метод будетrenderфункция преобразуется вVirtual DOM, и, наконец, пройтиvm._update()метод будетVirtual DOMсделать как реальныйDOMузел.

Vue.prototype.$mount = function(el, hydrating) {
    ···
    return mountComponent(this, el)
}
function mountComponent() {
    ···
    updateComponent = function () {
        vm._update(vm._render(), hydrating);
    };
}

давайте сначала посмотримvm._render()как методПреобразование функции рендеринга в виртуальный DOMиз.

Рассматривая содержание первой главы, статья знакомитVueМногие свойства и методы определяются при вводе кода, одним из которых являетсяrenderMixinпроцесс, мы только упоминали ранее, что он будет определять функции, связанные с рендерингом.На самом деле, он определяет только два важных метода,_renderФункция — одна из них.

// 引入Vue时,执行renderMixin方法,该方法定义了Vue原型上的几个方法,其中一个便是 _render函数
renderMixin();//
function renderMixin() {
    Vue.prototype._render = function() {
        var ref = vm.$options;
        var render = ref.render;
        ···
        try {
            vnode = render.call(vm._renderProxy, vm.$createElement);
        } catch (e) {
            ···
        }
        ···
        return vnode
    }
}

Помимо прочего кода, ядром функции _render являетсяrender.call(vm._renderProxy, vm.$createElement)часть,vm._renderProxyОн был проанализирован агентом данных, который в основном предназначен для фильтрации и обнаружения данных, а также связанrenderкогда функция выполняетсяthisнаправление.vm.$createElementметод будетrenderПередаются параметры функции.Помните, почеркомrenderфункция, мы будем использоватьrenderпервый параметр функцииcreateElementНапишите функцию рендеринга, здесьcreateElementпараметры определены$createElementметод.

new Vue({
    el: '#app',
    render: function(createElement) {
        return createElement('div', {}, this.message)
    },
    data() {
        return {
            message: 'dom'
        }
    }
})

инициализация_init, EстьinitRenderфункция, которая используется для определения метода функции рендеринга, включаяvm.$createElementопределение метода, кроме$createElement,_cАналогично определяются методы. вvm._cдаtemplateкомпилируется внутренне вrenderМетод, вызываемый при вызове функции,vm.$createElementнаписано от рукиrenderМетод, вызываемый при вызове функции.Единственная разница между ними заключается в разнице в последнем параметре. сгенерировано по шаблонуrenderметод, гарантирующий, что дочерние узлыVnode, пока написано от рукиrenderТребуются некоторые проверки и преобразования.

function initRender(vm) {
    vm._c = function(a, b, c, d) { return createElement(vm, a, b, c, d, false); }
    vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };
}

createElementметод на самом деле правильный_createElementИнкапсуляция метода при вызове_createElementРаньше он сначала будет обрабатывать входящие параметры, ведь рукописныеrenderСпецификации параметров функции неоднородны. Возьмем простой пример.

// 没有data
new Vue({
    el: '#app',
    render: function(createElement) {
        return createElement('div', this.message)
    },
    data() {
        return {
            message: 'dom'
        }
    }
})
// 有data
new Vue({
    el: '#app',
    render: function(createElement) {
        return createElement('div', {}, this.message)
    },
    data() {
        return {
            message: 'dom'
        }
    }
})

Здесь, если второй параметр является переменной или массивом, по умолчанию не передаетсяdata,потому чтоdataОбычно существует в виде объекта.

function createElement (
    context, // vm 实例
    tag, // 标签
    data, // 节点相关数据,属性
    children, // 子节点
    normalizationType,
    alwaysNormalize // 区分内部编译生成的render还是手写render
  ) {
    // 对传入参数做处理,如果没有data,则将第三个参数作为第四个参数使用,往上类推。
    if (Array.isArray(data) || isPrimitive(data)) {
      normalizationType = children;
      children = data;
      data = undefined;
    }
    // 根据是alwaysNormalize 区分是内部编译使用的,还是用户手写render使用的
    if (isTrue(alwaysNormalize)) {
      normalizationType = ALWAYS_NORMALIZE;
    }
    return _createElement(context, tag, data, children, normalizationType) // 真正生成Vnode的方法
  }

4.3.1 Обнаружение спецификации данных

VueПоскольку он открыт для пользователяrenderКогда функция используется для рендеринга шаблона вручную, необходимо учитывать неопределенность, вызванную операцией пользователя, поэтому_createElementпри созданииVnodeСтандартизация данных будет проверена заранее, а ошибки недопустимого типа данных будут заранее представлены пользователю. Далее мы перечислим несколько ошибок, которые легко допустить в реальных сценариях, а также нам удобно понимать обработку таких ошибок в исходном коде.

  1. сделать это с реактивными объектамиdataАтрибуты
new Vue({
    el: '#app',
    render: function (createElement, context) {
       return createElement('div', this.observeData, this.show)
    },
    data() {
        return {
            show: 'dom',
            observeData: {
                attr: {
                    id: 'test'
                }
            }
        }
    }
})
  1. Когда значение ключа специального атрибута не является строкой, когда нечисловой тип
new Vue({
    el: '#app',
    render: function(createElement) {
        return createElement('div', { key: this.lists }, this.lists.map(l => {
           return createElement('span', l.name)
        }))
    },
    data() {
        return {
            lists: [{
              name: '111'
            },
            {
              name: '222'
            }
          ],
        }
    }
})

Эти спецификации будут созданыVnodeУзел нашел и сообщил об ошибке ранее, исходный код выглядит следующим образом:

function _createElement (context,tag,data,children,normalizationType) {
    // 1. 数据对象不能是定义在Vue data属性中的响应式数据。
    if (isDef(data) && isDef((data).__ob__)) {
      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() // 返回注释节点
    }
    if (isDef(data) && isDef(data.is)) {
      tag = data.is;
    }
    if (!tag) {
      // 防止动态组件 :is 属性设置为false时,需要做特殊处理
      return createEmptyVNode()
    }
    // 2. key值只能为string,number这些原始数据类型
    if (isDef(data) && isDef(data.key) && !isPrimitive(data.key)
    ) {
      {
        warn(
          'Avoid using non-primitive value as key, ' +
          'use string/number value instead.',
          context
        );
      }
    }
    ···
  }

Эти нормативные тесты гарантируют, что последующее наблюдениеVirtual DOM treeполное поколение .

4.3.2 Нормализация дочерних узлов

Virtual DOM treeкаждымVnodeВиртуал в виде дереваDOMдерева, все, что нам нужно при преобразовании реальных узлов, это такой полныйVirtual DOM tree, поэтому нам нужно убедиться, что каждый дочерний узелVnodeТипа, есть два сценария для анализа.

  • Компиляция шаблонаrenderфункция, теоретическиtemplateШаблон создается путем компиляцииrenderфункцииVnodeТипа, но есть исключение, функциональный компонент возвращает массив (для этого специального примера можно посмотреть статью анализ функциональных компонентов), на этот разVueобработка заключается в преобразовании всегоchildrenСвести в одномерный массив.
  • Определяемые пользователемrenderфункция, на этот раз делится на два случая, один из которых, когдаchidrenКогда это текстовый узел, на этот раз через введенный ранееcreateTextVNodeсоздать текстовый узелVNode; другой относительно сложный, когдаchildrenимеютv-forПри наличии вложенного массива логика обработки в это время заключается в обходеchildren, оцените каждый узел, если это все еще массив, продолжайте рекурсивно вызывать, пока тип не станет базовым типом, вызовитеcreateTextVnodeметод вVnode. После такой рекурсииchildrenтакже становится своего родаVnodeмассив .
function _createElement() {
    ···
    if (normalizationType === ALWAYS_NORMALIZE) {
      // 用户定义render函数
      children = normalizeChildren(children);
    } else if (normalizationType === SIMPLE_NORMALIZE) {
      // 模板编译生成的的render函数
      children = simpleNormalizeChildren(children);
    }
}

// 处理编译生成的render 函数
function simpleNormalizeChildren (children) {
    for (var i = 0; i < children.length; i++) {
        // 子节点为数组时,进行开平操作,压成一维数组。
        if (Array.isArray(children[i])) {
        return Array.prototype.concat.apply([], children)
        }
    }
    return children
}

// 处理用户定义的render函数
function normalizeChildren (children) {
    // 递归调用,直到子节点是基础类型,则调用创建文本节点Vnode
    return isPrimitive(children)
      ? [createTextVNode(children)]
      : Array.isArray(children)
        ? normalizeArrayChildren(children)
        : undefined
  }

// 判断是否基础类型
function isPrimitive (value) {
    return (
      typeof value === 'string' ||
      typeof value === 'number' ||
      typeof value === 'symbol' ||
      typeof value === 'boolean'
    )
  }

4.3.4 Фактический сценарий

После обнаружения данных и нормализации компонентов следующим шагом является прохождениеnew VNode()для создания полногоVNodeдерево, обратите внимание_renderВ процессе будут встречаться подкомпоненты, в это время инициализация подкомпонентов будет иметь приоритет, и эта часть будет специально проанализирована в компонентной ссылке. Закончим практическим примеромrenderфункционировать, чтобыVirtual DOMанализ.

  • templateФорма шаблона
var vm = new Vue({
  el: '#app',
  template: '<div><span>virtual dom</span></div>'
})
  • Компиляция и генерация шаблонаrenderфункция
(function() {
  with(this){
    return _c('div',[_c('span',[_v("virual dom")])])
  }
})
  • Virtual DOM treeРезультат (сокращенная версия)
{
  tag: 'div',
  children: [{
    tag: 'span',
    children: [{
      tag: undefined,
      text: 'virtual dom'
    }]
  }]
}

4.4 Виртуальный Vnode сопоставляется с реальным DOM

назадupdateComponentПоследний процесс виртуальногоDOMдерево растетvirtual dom, он позвонитVueна прототипе_updateметод, виртуальныйDOMКартирование становится реальнымDOM. Это можно узнать из исходного кода,_updateЕсть два тайминга вызова, один происходит на этапе начального рендеринга, а другой — на этапе обновления данных.

updateComponent = function () {
    // render生成虚拟DOM,update渲染真实DOM
    vm._update(vm._render(), hydrating);
};

vm._updateМетод определен вlifecycleMixinсередина.

lifecycleMixin()
function lifecycleMixin() {
    Vue.prototype._update = function (vnode, hydrating) {
        var vm = this;
        var prevEl = vm.$el;
        var prevVnode = vm._vnode; // prevVnode为旧vnode节点
        // 通过是否有旧节点判断是初次渲染还是数据更新
        if (!prevVnode) {
            // 初次渲染
            vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)
        } else {
            // 数据更新
            vm.$el = vm.__patch__(prevVnode, vnode);
        }
}

_updateядро__patch__метод, если это рендеринг на стороне сервера, так как нетDOM,_patchметод - пустая функция,DOMОбъекты окружающей среды браузера,__patch__даpatchСсылка на функцию.

// 浏览器端才有DOM,服务端没有dom,所以patch为一个空函数
  Vue.prototype.__patch__ = inBrowser ? patch : noop;

а такжеpatchметод сноваcreatePatchFunctionвозвращаемое значение метода,createPatchFunctionМетод передает объект в качестве параметра, и объект имеет два свойства:nodeOpsа такжеmodules,nodeOpsИнкапсулирует серию собственных операцийDOMметоды объекта. а такжеmodulesОпределяет функцию ловушки модуля.

 var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });

// 将操作dom对象的方法合集做冻结操作
 var nodeOps = /*#__PURE__*/Object.freeze({
    createElement: createElement$1,
    createElementNS: createElementNS,
    createTextNode: createTextNode,
    createComment: createComment,
    insertBefore: insertBefore,
    removeChild: removeChild,
    appendChild: appendChild,
    parentNode: parentNode,
    nextSibling: nextSibling,
    tagName: tagName,
    setTextContent: setTextContent,
    setStyleScope: setStyleScope
  });

// 定义了模块的钩子函数
  var platformModules = [
    attrs,
    klass,
    events,
    domProps,
    style,
    transition
  ];

var modules = platformModules.concat(baseModules);

действительноcreatePatchFunctionФункция имеет более тысячи строк кода, которые неудобно здесь перечислять, внутри она определяет ряд вспомогательных методов, а ядром является вызовcreateElmметодdomоперации, создание узлов, вставка дочерних узлов, рекурсивное создание полногоDOMдерево и вставить вBodyсередина. И на этапе создания реальной сцены будетdiffалгоритм оценки до и послеVnodeразличия, чтобы свести к минимуму изменения реальной фазы. Позже будет глава, чтобы объяснитьdiffалгоритм.createPatchFunctionПроцессу нужно только сначала запомнить некоторые выводы, и инкапсулированная функция будет вызываться внутри функции.DOM api,согласно сVirtual DOMрезультаты для создания реальных узлов. Среди них, если встречается компонентVnode, он будет рекурсивно вызывать процесс монтирования подкомпонентов, и этот процесс мы также проанализируем в следующих главах.

4.5 Резюме

В этом разделе анализируютсяmountComponentДва основных методаrenderа такжеupdate, перед анализом он фокусируется на существованииJSоперация иDOMРендеринг моста:Virtual DOM.JSправильноDOMПакетная операция узла будет напрямую отражена в первомVirtual DOMНа этом объекте описания конечный результат будет непосредственно воздействовать на реальный узел. Можно сказать,Virtual DOMЗначительно улучшена производительность рендеринга. В статье подчеркиваетсяrenderфункция преобразуется вVirtual DOMпроцесс и примерно описывает_updateИдеи реализации функций. На самом деле в этих двух процессах задействованы компоненты, поэтому в этом разделе нельзя подробно проанализировать многие ссылки, и в следующем разделе начнется вхождение в тему компонентов. Я верю, что после анализа компонентов у читателей будет более глубокое понимание и осмысление всего процесса рендеринга.