Тщательно изучите магию поддержки активности в Vue (часть 1)

Vue.js
Тщательно изучите магию поддержки активности в Vue (часть 1)

В последнем разделе упоминалось немногоVueСопутствующее содержимое встроенных компонентов, начиная с этого раздела, будет анализировать конкретный встроенный компонент. прежде всегоkeep-alive, это компонент, который часто используется в нашей повседневной разработке.Когда мы переключаемся между различными компонентами, нам часто требуется поддерживать состояние компонента, чтобы избежать потери производительности, вызванной повторным рендерингом компонента, иkeep-aliveЧасто используется вместе с динамическими компонентами, описанными в предыдущем разделе. Из-за слишком большого содержания,keep-aliveАнализ исходного кода будет разделен на верхнюю и нижнюю части, этот раздел в основном посвященkeep-aliveПервая визуализация разворота.

13.1 Основное использование

keep-aliveИспользование нужно только для добавления метки к самому внешнему слою динамического компонента.

<div id="app">
    <button @click="changeTabs('child1')">child1</button>
    <button @click="changeTabs('child2')">child2</button>
    <keep-alive>
        <component :is="chooseTabs">
        </component>
    </keep-alive>
</div>
var child1 = {
    template: '<div><button @click="add">add</button><p>{{num}}</p></div>',
    data() {
        return {
            num: 1
        }
    },
    methods: {
        add() {
            this.num++
        }
    },
}
var child2 = {
    template: '<div>child2</div>'
}
var vm = new Vue({
    el: '#app',
    components: {
        child1,
        child2,
    },
    data() {
        return {
            chooseTabs: 'child1',
        }
    },
    methods: {
        changeTabs(tab) {
            this.chooseTabs = tab;
        }
    }
})

Простой результат таков: динамическая составляющая находится вchild1,child2переключаться вперед и назад, когда второй переключатель наchild1час,child1сохраняет исходное состояние данных,num = 5.

13.2 От компиляции шаблона до генерации vnode

По опыту предыдущего анализа начнем с анализа шаблонов.Первый вопрос: Есть ли разница между встроенными компонентами и обычными компонентами в процессе компиляции? Ответ - нет, будь то встроенный или определяемый пользователем компонент, по сути компонент компилируется в шаблонrenderМетод обработки функции такой же. Детали здесь не анализируются. Если у вас есть какие-либо сомнения, вы можете обратиться к основному анализу в предыдущих разделах. В конечном счетеkeep-aliveизrenderРезультат функции следующий:

with(this){···_c('keep-alive',{attrs:{"include":"child2"}},[_c(chooseTabs,{tag:"component"})],1)}

имеютrenderфункция, то генерация будет выполняться от дочерней к родительскойVnodeобъектный процесс,_c('keep-alive'···)обработка, выполнитcreateElementСоздание компонентовVnode, что обусловленоkeep-aliveявляется компонентом, поэтому он вызоветcreateComponentфункция для создания дочерних компонентовVnode,createComponentЭто также было проанализировано ранее, эта связь и создание общих компонентовVnodeРазница в том,keep-aliveизVnodeИзбыточное содержимое атрибута будет удалено.так какkeep-aliveКромеslotВ дополнение к свойствам внутри компонента не имеют значения другие свойства, такие какclassстиль,<keep-alive clas="test"></keep-alive>подождите, так что вVnodeСлоям имеет смысл отбрасывать лишние атрибуты. и<keep-alive slot="test">Метод записи также устарел в версиях выше 2.6.abstractКак признак абстрактного компонента, и о его роли поговорим позже)

// 创建子组件Vnode过程
function createComponent(Ctordata,context,children,tag) {
    // abstract是内置组件(抽象组件)的标志
    if (isTrue(Ctor.options.abstract)) {
        // 只保留slot属性,其他标签属性都被移除,在vnode对象上不再存在
        var slot = data.slot;
        data = {};
        if (slot) {
            data.slot = slot;
        }
    }
}

13.3 Первоначальный рендеринг

keep-aliveОн особенный, потому что он не рендерит один и тот же компонент снова и снова, он просто использует кеш, оставшийся от первого рендеринга, для обновления узла. Итак, чтобы полностью понять принцип его реализации, нам нужно начать сkeep-aliveПервый рендер .

13.3.1 Блок-схема

Чтобы прояснить процесс, я примерно нарисовал блок-схему, которая примерно охватывает первоначальный рендеринг.keep-aliveВыполняемый процесс, анализ исходного кода будет выполняться в соответствии с этим процессом.

То же, что и при рендеринге обычных компонентов,Vueполучит ранее сгенерированныйVnodeОбъект выполняет процесс создания реального узла, также известный какpatchпроцесс,patchЭтап выполнения вызоветcreateElmсоздать настоящийdom, на пути к созданию узла,keep-aliveизvnodeОбъект будет считаться компонентомVnode, поэтому для компонентаVnodeбудет выполняться сноваcreateComponentфункционировать, это будетkeep-aliveКомпоненты инициализируются и создаются.

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
      var i = vnode.data;
      if (isDef(i)) {
        // isReactivated用来判断组件是否缓存。
        var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
        if (isDef(i = i.hook) && isDef(i = i.init)) {
            // 执行组件初始化的内部钩子 init
          i(vnode, false /* hydrating */);
        }
        if (isDef(vnode.componentInstance)) {
          // 其中一个作用是保留真实dom到vnode中
          initComponent(vnode, insertedVnodeQueue);
          insert(parentElm, vnode.elm, refElm);
          if (isTrue(isReactivated)) {
            reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
          }
          return true
        }
      }
    }

keep-aliveКомпонент сначала вызовет внутренний хукinitметод для инициализации, давайте сначала посмотримinitЧто дал процесс.

// 组件内部钩子
var componentVNodeHooks = {
    init: function init (vnode, hydrating) {
      if (
        vnode.componentInstance &&
        !vnode.componentInstance._isDestroyed &&
        vnode.data.keepAlive
      ) {
        // kept-alive components, treat as a patch
        var mountedNode = vnode; // work around flow
        componentVNodeHooks.prepatch(mountedNode, mountedNode);
      } else {
          // 将组件实例赋值给vnode的componentInstance属性
        var child = vnode.componentInstance = createComponentInstanceForVnode(
          vnode,
          activeInstance
        );
        child.$mount(hydrating ? vnode.elm : undefined, hydrating);
      }
    },
    // 后面分析
    prepatch: function() {}
}

При первом исполнении видно, что компонентvnodeнетcomponentInstanceАтрибуты,vnode.data.keepAliveтакже не имеет значения, поэтому будетперечислитьcreateComponentInstanceForVnodeметод для создания экземпляра компонента и назначения экземпляра компонентаvnodeизcomponentInstanceАтрибуты,Окончательный экземпляр исполнительного компонента$mountспособ монтирования экземпляра.

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

function createComponentInstanceForVnode (vnode, parent) {
    var options = {
      _isComponent: true,
      _parentVnode: vnode,
      parent: parent
    };
    // 内联模板的处理,忽略这部分代码
    ···
    // 执行vue子组件实例化
    return new vnode.componentOptions.Ctor(options)
  }
13.3.2 Параметры встроенных компонентов

Когда мы используем компоненты, мы часто определяем параметры компонента в виде объектов, в том числеdata,method,computedи т. д., и зарегистрируйтесь в родительском или корневом компоненте.keep-aliveТакже следуйте этому принципу, встроенные два слова также объясняютkeep-aliveвVueКонфигурация встроенных опций в исходном коде также зарегистрирована глобально, на исходный код этой части можно ссылатьсяУглубленный анализ исходного кода Vue — концепция динамических компонентов Vue, не запутаетесь ли вы?Введение во встроенные конструкторы компонентов и процесс регистрации в конце раздела. В этой части мы сосредоточимся наkeep-aliveконкретные варианты.

// keepalive组件选项
  var KeepAlive = {
    name: 'keep-alive',
    // 抽象组件的标志
    abstract: true,
    // keep-alive允许使用的props
    props: {
      include: patternTypes,
      exclude: patternTypes,
      max: [String, Number]
    },

    created: function created () {
      // 缓存组件vnode
      this.cache = Object.create(null);
      // 缓存组件名
      this.keys = [];
    },

    destroyed: function destroyed () {
      for (var key in this.cache) {
        pruneCacheEntry(this.cache, key, this.keys);
      }
    },

    mounted: function mounted () {
      var this$1 = this;
      // 动态include和exclude
      // 对include exclue的监听
      this.$watch('include', function (val) {
        pruneCache(this$1, function (name) { return matches(val, name); });
      });
      this.$watch('exclude', function (val) {
        pruneCache(this$1, function (name) { return !matches(val, name); });
      });
    },
    // keep-alive的渲染函数
    render: function render () {
      // 拿到keep-alive下插槽的值
      var slot = this.$slots.default;
      // 第一个vnode节点
      var vnode = getFirstComponentChild(slot);
      // 拿到第一个组件实例
      var componentOptions = vnode && vnode.componentOptions;
      // keep-alive的第一个子组件实例存在
      if (componentOptions) {
        // check pattern
        //拿到第一个vnode节点的name
        var name = getComponentName(componentOptions);
        var ref = this;
        var include = ref.include;
        var exclude = ref.exclude;
        // 通过判断子组件是否满足缓存匹配
        if (
          // not included
          (include && (!name || !matches(include, name))) ||
          // excluded
          (exclude && name && matches(exclude, name))
        ) {
          return vnode
        }

        var ref$1 = this;
        var cache = ref$1.cache;
        var keys = ref$1.keys;
        var key = vnode.key == null
          ? componentOptions.Ctor.cid + (componentOptions.tag ? ("::" + (componentOptions.tag)) : '')
          : vnode.key;
          // 再次命中缓存
        if (cache[key]) {
          vnode.componentInstance = cache[key].componentInstance;
          // make current key freshest
          remove(keys, key);
          keys.push(key);
        } else {
        // 初次渲染时,将vnode缓存
          cache[key] = vnode;
          keys.push(key);
          // prune oldest entry
          if (this.max && keys.length > parseInt(this.max)) {
            pruneCacheEntry(cache, keys[0], keys, this._vnode);
          }
        }
        // 为缓存组件打上标志
        vnode.data.keepAlive = true;
      }
      // 将渲染的vnode返回
      return vnode || (slot && slot[0])
    }
  };

keep-aliveПараметры в основном аналогичны параметрам компонентов, которые мы обычно пишем, с той лишь разницей, чтоkeep-ailveкомпоненты бесполезныtemplateвместо этого используйтеrenderфункция.keep-aliveПо сути, это просто процесс хранения и извлечения кеша, и фактического рендеринга узлов нет, поэтому используйтеrenderОбработка - лучший вариант.

13.3.3 Кэширование vnodes

Вернемся сначала к анализу блок-схемы. сказано вышеkeep-aliveКомпонент монтируется после создания экземпляра компонента. во время монтажа$mountназад сноваvm._render(),vm._update()процесс. так какkeep-aliveимеютrenderфункции, поэтому мы можем сосредоточиться непосредственно наrenderреализация функции.

    1. Во-первых, чтобы получитьkeep-aliveсодержимое нижнего слота, т.е.keep-aliveДочерний компонент, который необходимо отрисовать, в примере этоchil1 Vnodeобъект, соответствующий исходному кодуgetFirstComponentChildфункция
  function getFirstComponentChild (children) {
    if (Array.isArray(children)) {
      for (var i = 0; i < children.length; i++) {
        var c = children[i];
        // 组件实例存在,则返回,理论上返回第一个组件vnode
        if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
          return c
        }
      }
    }
  }

    1. Судя по тому, что компонент удовлетворяет условиям соответствия кэша, вkeep-aliveВо время использования компонентаVueНам разрешено использовать исходный кодinclude, excludeопределить условия соответствия,includeУказывает, что кэшироваться будут только компоненты с совпадающими именами.excludeУказывает, что любые компоненты, чьи имена совпадают, не будут кэшироваться. В качестве альтернативы мы можем использоватьmaxЧтобы ограничить количество совпадающих экземпляров, которые можно кэшировать, зачем ограничивать число? Мы упомянем об этом позже.

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

var include = ref.include;
var exclude = ref.exclude;
// 通过判断子组件是否满足缓存匹配
if (
    // not included
    (include && (!name || !matches(include, name))) ||
    // excluded
    (exclude && name && matches(exclude, name))
) {
    return vnode
}

// matches
function matches (pattern, name) {
    // 允许使用数组['child1', 'child2']
    if (Array.isArray(pattern)) {
        return pattern.indexOf(name) > -1
    } else if (typeof pattern === 'string') {
        // 允许使用字符串 child1,child2
        return pattern.split(',').indexOf(name) > -1
    } else if (isRegExp(pattern)) {
        // 允许使用正则 /^child{1,2}$/g
        return pattern.test(name)
    }
    /* istanbul ignore next */
    return false
}

Если компонент не соответствует требованиям кеша, он напрямую вернет компонентvnode, не делайте никакой обработки, в это время компонент войдет в обычную ссылку для монтажа.

    1. renderВажным шагом в выполнении функции является кэширование.vnode, так как это первое выполнениеrenderфункция, в опцияхcacheиkeysданные не имеют значения, гдеcacheэто пустой объект, который мы будем использовать для кэширования{ name: vnode }перечисление, при этомkeysМы используем для кэширования имен компонентов.Итак, мы рендерим в первый разkeep-aliveКогда дочерний компонент, который необходимо отобразить, будетvnodeкеш.
    cache[key] = vnode;
    keys.push(key);
    1. будет кэшироватьсяvnodeотметьте и поместите субкомпонентVnodeвозвращение.vnode.data.keepAlive = true
13.3.4 Сохранение реальных узлов

мы возвращаемсяcreateComponentлогика, упомянутая ранееcreateComponentбудет выполняться первымkeep-aliveПроцесс инициализации компонентов также включает в себя монтирование подкомпонентов. и мы проходимcomponentInstanceпонятноkeep-aliveэкземпляр компонента, а следующийВажным шагом является установка реальногоdomсохранить сноваvnodeсередина.

function createComponent(vnode, insertedVnodeQueue) {
    ···
    if (isDef(vnode.componentInstance)) {
        // 其中一个作用是保留真实dom到vnode中
        initComponent(vnode, insertedVnodeQueue);
        // 将真实节点添加到父节点中
        insert(parentElm, vnode.elm, refElm);
        if (isTrue(isReactivated)) {
            reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
        }
        return true
    }
}

insertИсходный код не указан, это просто операция вызоваdomизapi, вставьте дочерний узел в родительский узел, мы можем сосредоточиться наinitComponentЛогика ключевых шагов.

function initComponent() {
    ···
    // vnode保留真实节点
    vnode.elm = vnode.componentInstance.$el;
    ···
}

Поэтому мы явно возвращаемся к оставленному ранее вопросу, почемуkeep-aliveнужен одинmaxограничить количество кэшируемых компонентов. Причина в том,keep-aliveКэшированные данные компонентов в дополнение к включениюvnodeКроме этого объекта описания существует еще реальныйdomузлов, а мы знаем, что настоящие объекты узлов огромны, поэтому хранение большого количества компонентов кэша требует высокой производительности. Поэтому нам необходимо строго контролировать количество кэшируемых компонентов, а также нам необходимо оптимизировать стратегию кэширования, о которой мы продолжим упоминать в следующей статье.

так какisReactivatedзаfalse,reactivateComponentФункция также не будет выполняться. ужеkeep-aliveАнализ начального процесса рендеринга завершен.

Если анализ шагов игнорируется, делается только сводка по начальному процессу рендеринга: встроенныйkeep-aliveкомпонент, так что дочерний компонент будетvnodeи настоящийelmкэшировано.

13.4 Абстрактные компоненты

В конце этого раздела вскользь упоминается упомянутая выше концепция абстрактных компонентов.VueЕсли встроенные компоненты имеют параметр, описывающий тип компонента, этот параметр{ astract: true }, что указывает на то, что компонент является абстрактным компонентом. Что такое абстрактный компонент и почему этот тип различия? Я думаю, что это сводится к двум причинам.

    1. Абстрактные компоненты не имеют реальных узлов, они не будут разобраны и преобразованы в реальные на этапе рендеринга компонента.domузел, но только как промежуточный уровень передачи данных, вkeep-aliveЭто обработка кэша компонентов.
    1. Когда мы представили инициализацию компонентов, мы упомянули, что компоненты родитель-потомок явно устанавливают уровень отношений, который закладывает основу для связи между компонентами родитель-потомок. мы можем оглянуться назад сноваinitLifecycleкод.
Vue.prototype._init = function() {
    ···
    var vm = this;
    initLifecycle(vm)
}

function initLifecycle (vm) {
    var options = vm.$options;
    
    var parent = options.parent;
    if (parent && !options.abstract) {
        // 如果有abstract属性,一直往上层寻找,直到不是抽象组件
      while (parent.$options.abstract && parent.$parent) {
        parent = parent.$parent;
      }
      parent.$children.push(vm);
    }
    ···
  }

Дочерний компонент смонтирует родительский экземпляр в свою собственную опцию на этапе регистрации.parentсвойства, вinitLifecycleВ процессе вы получите обратноеparentродительский компонент включенvnode, и для$childrenсвойство для добавления дочернего компонентаvnode, Если в процессе нахождения родительского компонента в обратном порядке родительский компонентabstractатрибут, вы можете определить, что компонент является абстрактным компонентом, в это время используйтеparentЦепочка ищет до тех пор, пока компонент не станет абстрактным компонентом.initLifecycleобработки, чтобы каждый компонент мог найти родительский компонент верхнего уровня и дочерний компонент нижнего уровня, так что между компонентами формируется тесное дерево взаимосвязей.

При первой обработке кеша, когда компонент рендерится во второй раз,keep-aliveЧто за магия будет, и какие оптимизации кеша остались до этого? Они будут раскрыты один за другим в следующем подразделе.