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

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

в предыдущем разделеТщательно изучите магию поддержки активности в Vue (часть 1), у нас естьkeep-aliveНачальный процесс рендеринга компонента и информация о конфигурации компонента анализируются исходным кодом. Наиболее важным шагом в начальном процессе рендеринга является рендеринг компонентов.VnodeКэширование, которое также включает реальное хранилище узла компонента. С первым кешем, когда компонент снова рендерится,keep-aliveКакая у тебя магия? Далее мы полностью приоткроем завесу.

13.5 Подготовка

предыдущий разделkeep-aliveАнализ компонентов начинается с нарисованной мной блок-схемы. Если вы не хотите возвращаться и читать содержание предыдущего раздела, вы можете обратиться к следующему краткому обзору.

    1. keep-aliveЭто конфигурация параметра компонента, определенная внутри исходного кода. Сначала он будет зарегистрирован как глобальный компонент, чтобы разработчики могли использовать его глобально.renderФункция определяет свой процесс рендеринга
    1. В соответствии с обычными компонентами, когда родитель находится в процессе создания реального узла, он сталкивается сkeep-aliveКомпонент будет инициализировать и создавать экземпляр компонента.
    1. Создание экземпляра выполнит монтирование$mountпроцесс, этот шаг будет выполненkeep-aliveв опцияхrenderфункция.
    1. renderКогда функция первоначально визуализируется, она будет отображать визуализированный дочерний элемент.Vnodeкеш. в то же времяСоответствующие дочерние реальные узлы также будут кэшироваться..

Затем, когда вам нужно снова отрендерить уже отрендеренный компонент,keep-aliveКакая разница?

13.5.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++
        }
    },
    mounted() {
        console.log('child1 mounted')
    },
    activated() {
        console.log('child1 activated')
    },
    deactivated() {
        console.log('child1 deactivated')
    },
    destoryed() {
        console.log('child1 destoryed')
    }
}
var child2 = {
    template: '<div>child2</div>',
    mounted() {
        console.log('child2 mounted')
    },
    activated() {
        console.log('child2 activated')
    },
    deactivated() {
        console.log('child2 deactivated')
    },
    destoryed() {
        console.log('child2 destoryed')
    }
}

var vm = new Vue({
    el: '#app',
    components: {
        child1,
        child2,
    },
    data() {
        return {
            chooseTabs: 'child1',
        }
    },
    methods: {
        changeTabs(tab) {
            this.chooseTabs = tab;
        }
    }
})
13.5.2 блок-схема

В соответствии с анализом первого рендеринга я снова нарисовал простую блок-схему процесса рендеринга.

13.6 Анализ процесса

13.6.1 Рендеринг компонентов

Процесс повторного рендеринга начинается с изменения данных, в данном примере в динамическом компонентеchooseTabsИзменения в данных заставят процесс полагаться на обновления дистрибутива (в этой серии есть три статьи, которые подробно знакомят с базовой реализацией адаптивной системы Vue, и заинтересованные студенты могут извлечь из них уроки). Проще говоря,chooseTabsЭти данные будут собираться на этапе инициализации для использования соответствующих зависимостей данных. При изменении данных собранные зависимости будут отправлены и обновлены.

При этом будут выполняться родительские компоненты, отвечающие за экземпляр, смонтированный как зависимый процесс, т. е. выполнение родительского компонентаvm._update(vm._render(), hydrating);._renderа также_updateсоответственно представляют два процесса, где_renderФункция сгенерирует новый компонент для компонента на основе изменения данныхVnodeузел, и_updateв конечном итоге будет новымVnodeСоздание реальных узлов. В процессе генерации реальных узлов мы будем использоватьvitrual domизdiffПара алгоритмов до и послеvnodeУзлы сравниваются, чтобы как можно меньше менять реальные узлы, эту часть можно просмотретьУглубленный анализ исходного кода Vue — приходите и реализуйте алгоритм сравнения вместе со мной!, в котором подробно описано использованиеdiffИдея алгоритма сравнения разности узлов.

patchновый и старыйVnodeПроцесс сравнения узлов, в то время какpatchVnodeэто основной шаг, мы игнорируемpatchVnodeДругие процессы, связанные с выполнением подкомпонентовprepatchв процессе зацепления.

function patchVnode (oldVnode,vnode,insertedVnodeQueue,ownerArray,index,removeOnly) {
    ···
    // 新vnode  执行prepatch钩子
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
        i(oldVnode, vnode);
    }
    ···
}

воплощать в жизньprepatchПри подключении он получит экземпляр старого и нового компонентов и выполнит его.updateChildComponentфункция. а такжеupdateChildComponentБудет обновлено состояние старого экземпляра для нового экземпляра компонента, включаяprops,listenersподожди, в конце концовпередачаvueпри условии глобальногоvm.$forceUpdate()метод для повторного рендеринга экземпляра.

var componentVNodeHooks = {
    // 之前分析的init钩子 
    init: function() {},
    prepatch: function prepatch (oldVnode, vnode) {
        // 新组件实例
      var options = vnode.componentOptions;
      // 旧组件实例
      var child = vnode.componentInstance = oldVnode.componentInstance;
      updateChildComponent(
        child,
        options.propsData, // updated props
        options.listeners, // updated listeners
        vnode, // new parent vnode
        options.children // new children
      );
    },
}

function updateChildComponent() {
    // 更新旧的状态,不分析这个过程
    ···
    // 迫使实例重新渲染。
    vm.$forceUpdate();
}

Первый взгляд$forceUpdateчто ты сделал.$forceUpdateпредставляет собой API, открытый исходным кодом, они заставляютVueПовторный рендеринг экземпляра, по существу выполняющий зависимости, собранные экземпляром, в примереwatcherсоответствуетkeep-aliveизvm._update(vm._render(), hydrating);Обработать.

Vue.prototype.$forceUpdate = function () {
    var vm = this;
    if (vm._watcher) {
      vm._watcher.update();
    }
  };
13.6.2 Повторное использование компонентов кэша

из-заvm.$forceUpdate()заставитkeep-aliveкомпонент перерисовывается, поэтомуkeep-aliveкомпонент будет выполняться сноваrenderОбработать. На этот раз из-за первой парыvnodeкеш,keep-aliveв случаеcacheВ объекте обнаружен кэшированный компонент.

// keepalive组件选项
var keepAlive = {
    name: 'keep-alive',
    abstract: true,
    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;
          // keys命中的组件名移到数组末端
          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;
      }
      return vnode || (slot && slot[0])
    }
}

renderЛогика перед функцией может ссылаться на предыдущую статью, т.к.cacheОбъект хранит повторно используемыеvnodeобъект, так что прямо передатьcache[key]Получите экземпляр кэшированного компонента и назначьте егоvnodeизcomponentInstanceАтрибуты. Возможно, когда вы прочитаете это, вы будете сбиты с толку исходным кодом.keysчто делает этот массив, иpruneCacheEntryЕсть сомнения по поводу функции , здесь мы ответим на него, когда будем говорить о стратегии оптимизации кеша в конце статьи.

13.6.3 Замена реальных узлов

казненkeep-aliveкомпонент_renderпроцесс, за которым следует_updateгенерировать реальные узлы, опять же,keep-aliveподchild1дочерний компонент, поэтому_updateпроцедура вызоветcreateComponentСоздание подкомпонентов рекурсивноvnode, Этот процесс также был проанализирован при первоначальном рендеринге, мы можем сравнить, какие отличия в процессе при повторном рендеринге.

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    // vnode为缓存的vnode
      var i = vnode.data;
      if (isDef(i)) {
        // 此时isReactivated为true
        var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
        if (isDef(i = i.hook) && isDef(i = i.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
        }
      }
    }

В настоящее времяvnodeявляется подкомпонентом выборки кэшаvnode, а так как компонент помечен на первом рендереvnode.data.keepAlive = true;,такisReactivatedценностьtrue,i.initПроцесс инициализации дочернего компонента все равно будет выполняться. Однако из-за кеша процесс выполнения не совсем такой же.

var componentVNodeHooks = {
    init: function init (vnode, hydrating) {
      if (
        vnode.componentInstance &&
        !vnode.componentInstance._isDestroyed &&
        vnode.data.keepAlive
      ) {
        // 当有keepAlive标志时,执行prepatch钩子
        var mountedNode = vnode; // work around flow
        componentVNodeHooks.prepatch(mountedNode, mountedNode);
      } else {
        var child = vnode.componentInstance = createComponentInstanceForVnode(
          vnode,
          activeInstance
        );
        child.$mount(hydrating ? vnode.elm : undefined, hydrating);
      }
    },
}

очевидно, потому что естьkeepAlive, поэтому подкомпонент больше не проходит процесс монтирования, а просто выполняетсяprepatchХуки обновляют состояние компонента. И хорошо использовать кешvnodeРанее зарезервированные реальные узлы заменяются узлами.

13.7 Жизненный цикл

Посмотрим на примереkeep-aliveЖизненный цикл отличается от обычных компонентов.

в нашемchild1переключиться наchild2, затем переключитесь обратноchild1в процессе,chil1не будет выполняться сноваmountedхук, который будет выполняться толькоactivatedкрючок, покаchild2не будет выполнятьсяdestoryedхук, который будет выполняться толькоdeactivatedГук, зачем это?child2изdeactivatedКрюк лучше, чемchild1изactivatedВыполнить раньше времени, почему это?

13.7.1 deactivated

Начнем с разрушения комплектующих, когдаchild1переключиться наchild2час,child1будет выполнятьdeactivatedкрючок вместоdestoryedГук, зачем это? предыдущий анализpatchПроцесс будет сравнивать изменения старых и новых узлов, чтобы использовать как можно меньше реальных узлов.diffПосле того, как алгоритм и работа с узлом завершены, следующим важным шагом являетсяУничтожить и удалить старые компоненты. Код для этого шага выглядит следующим образом:

function patch(···) {
  // 分析过的patchVnode过程
  // 销毁旧节点
  if (isDef(parentElm)) {
    removeVnodes(parentElm, [oldVnode], 0, 0);
  } else if (isDef(oldVnode.tag)) {
    invokeDestroyHook(oldVnode);
  }
}

function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
  // startIdx,endIdx都为0
  for (; startIdx <= endIdx; ++startIdx) {
    // ch 会拿到需要销毁的组件
    var ch = vnodes[startIdx];
    if (isDef(ch)) {
      if (isDef(ch.tag)) {
        // 真实节点的移除操作
        removeAndInvokeRemoveHook(ch);
        invokeDestroyHook(ch);
      } else { // Text node
        removeNode(ch.elm);
      }
    }
  }
}

removeAndInvokeRemoveHookСтарый узел будет удален, а ключевой шаг — удалить настоящий узел из родительского элемента, если вам интересно, вы можете сами проверить эту часть логики.invokeDestroyHookЯвляется ядром выполнения хука компонента destroy. Если под компонентом есть подкомпоненты, он будет вызываться рекурсивноinvokeDestroyHookВыполните операцию уничтожения. Процесс уничтожения выполняет внутреннийdestoryкрюк.

function invokeDestroyHook (vnode) {
    var i, j;
    var data = vnode.data;
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.destroy)) { i(vnode); }
      // 执行组件内部destroy钩子
      for (i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](vnode); }
    }
    // 如果组件存在子组件,则遍历子组件去递归调用invokeDestoryHook执行钩子
    if (isDef(i = vnode.children)) {
      for (j = 0; j < vnode.children.length; ++j) {
        invokeDestroyHook(vnode.children[j]);
      }
    }
  }

Внутренний хук компонента был введен ранееinitа такжеprepatchкрючок, покаdestroyЛогика хука проще.

var componentVNodeHooks = {
  destroy: function destroy (vnode) {
    // 组件实例
    var componentInstance = vnode.componentInstance;
    // 如果实例还未被销毁
    if (!componentInstance._isDestroyed) {
      // 不是keep-alive组件则执行销毁操作
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy();
      } else {
        // 如果是已经缓存的组件
        deactivateChildComponent(componentInstance, true /* direct */);
      }
    }
  }
}

когда компонентkeep-aliveКэшированные компоненты, т.е. уже используемыеkeepAliveЕсли отмечено, уничтожение экземпляра не будет выполнено, т.е.componentInstance.$destroy()процесс.$destroyПроцесс будет выполнять ряд операций по уничтожению компонентов, из которыхbeforeDestroy,destoryedкрючок тоже есть$destoryвызов процедуры, покаdeactivateChildComponentПроцесс совершенно другой.

function deactivateChildComponent (vm, direct) {
  if (direct) {
    // 
    vm._directInactive = true;
    if (isInInactiveTree(vm)) {
      return
    }
  }
  if (!vm._inactive) {
    // 已经被停用
    vm._inactive = true;
    // 对子组件同样会执行停用处理
    for (var i = 0; i < vm.$children.length; i++) {
      deactivateChildComponent(vm.$children[i]);
    }
    // 最终调用deactivated钩子
    callHook(vm, 'deactivated');
  }
}

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

13.7.2 activated

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

function patch(···) {
  // patchVnode过程
  // 销毁旧节点
  {
    if (isDef(parentElm)) {
      removeVnodes(parentElm, [oldVnode], 0, 0);
    } else if (isDef(oldVnode.tag)) {
      invokeDestroyHook(oldVnode);
    }
  }
  // 执行组件内部的insert钩子
  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
}

function invokeInsertHook (vnode, queue, initial) {
  // delay insert hooks for component root nodes, invoke them after the
  // 当节点已经被插入时,会延迟执行insert钩子
  if (isTrue(initial) && isDef(vnode.parent)) {
    vnode.parent.data.pendingInsert = queue;
  } else {
    for (var i = 0; i < queue.length; ++i) {
      queue[i].data.hook.insert(queue[i]);
    }
  }
}

внутри того же компонентаinsertЛогика хука следующая:

// 组件内部自带钩子
  var componentVNodeHooks = {
    insert: function insert (vnode) {
      var context = vnode.context;
      var componentInstance = vnode.componentInstance;
      // 实例已经被挂载
      if (!componentInstance._isMounted) {
        componentInstance._isMounted = true;
        callHook(componentInstance, 'mounted');
      }
      if (vnode.data.keepAlive) {
        if (context._isMounted) {
          // vue-router#1212
          // During updates, a kept-alive component's child components may
          // change, so directly walking the tree here may call activated hooks
          // on incorrect children. Instead we push them into a queue which will
          // be processed after the whole patch process ended.
          queueActivatedComponent(componentInstance);
        } else {
          activateChildComponent(componentInstance, true /* direct */);
        }
      }
    },
  }

Когда компонент создается впервые, из-за экземпляра_isMountedне существует, поэтому он вызоветmountedкрючок, когда мы идем отchild2переключиться обратно сноваchild1когда из-заchild1просто деактивирован и не уничтожен, поэтому больше вызываться не будетmountedхук, который будет выполняться в это времяactivateChildComponentФункция обрабатывает состояние компонента. с анализомdeactivateChildComponentОсновы,activateChildComponentЛогика тоже хорошо понятна, та же_inactiveПомечены как включены и подразмысливые рекурсивные звонкиactivateChildComponentВыполнить обработку статуса.

function activateChildComponent (vm, direct) {
  if (direct) {
    vm._directInactive = false;
    if (isInInactiveTree(vm)) {
      return
    }
  } else if (vm._directInactive) {
    return
  }
  if (vm._inactive || vm._inactive === null) {
    vm._inactive = false;
    for (var i = 0; i < vm.$children.length; i++) {
      activateChildComponent(vm.$children[i]);
    }
    callHook(vm, 'activated');
  }
}

13.8 Оптимизация кэша — LRU

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

  • 1. FIFO: Стратегия «первым поступил — первым обслужен», мы записываем время, используемое данными, и когда размер кеша приближается к переполнению, данные, которые находятся дальше всего от текущего времени, сначала очищаются.

  • 2. LRU: наименее недавно использованный. Принцип, которому следует стратегия LRU, заключается в том, что если к данным обращались (использовались) недавно, вероятность доступа в будущем будет выше.Если данные записаны в массив, при доступе к данным данные будут быть перемещены в конец массива.В конце это указывает, что он использовался в последнее время.При переполнении буфера будут удалены данные головы массива, то есть будут удалены наименее часто используемые данные.

  • 3. LFU: политика наименьшего количества. Частота использования данных отмечена количеством раз, и тот, у которого меньшее количество раз, будет исключен при переполнении буфера.

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

var keepAlive = {
  render: function() {
    ···
    if (cache[key]) {
      vnode.componentInstance = cache[key].componentInstance;
      remove(keys, key);
      keys.push(key);
    } else {
      cache[key] = vnode;
      keys.push(key);
      if (this.max && keys.length > parseInt(this.max)) {
        pruneCacheEntry(cache, keys[0], keys, this._vnode);
      }
    }
  }
}

function remove (arr, item) {
  if (arr.length) {
    var index = arr.indexOf(item);
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}

В сочетании с практическим примером для анализа реализации логики кэша.

    1. Есть три компонентаchild1,child2,child3,keep-aliveМаксимальное количество кешей установлено на 2
    1. использоватьcacheобъект для хранения компонентаvnode,keyимя компонента,valueдля компонентаvnodeобъект, сkeysМассив для записи имени компонента, потому что это массив, поэтомуkeysзаказать.
    1. child1,child2Доступ к компонентам осуществляется последовательно, а кэшированный результат
keys = ['child1', 'child2']
cache = {
  child1: child1Vnode,
  child2: child2Vnode
}
    1. посетить сноваchild1Компонент, из-за попадания в кеш, вызоветremoveметод положитьkeysсерединаchild1удалить и просмотреть массивpushметод будетchild1толкнуть в тыл. Кэшированный результат изменяется на
keys = ['child2', 'child1']
cache = {
  child1: child1Vnode,
  child2: child2Vnode
}
    1. доступ кchild3, из-за ограничения количества кешей будет выполняться начальный кешpruneCacheEntryМетод удаляет наименее используемые данные.pruneCacheEntryопределяется следующим образом
function pruneCacheEntry (cache,key,keys,current) {
    var cached$$1 = cache[key];
    // 销毁实例
    if (cached$$1 && (!current || cached$$1.tag !== current.tag)) {
      cached$$1.componentInstance.$destroy();
    }
    cache[key] = null;
    remove(keys, key);
  }

При удалении кеша будетkeys[0]Репрезентативный компонент удаляется.Из-за предыдущей обработки последний доступный элемент будет расположен в конце массива, поэтому данные в заголовке часто наименее доступны, поэтому элемент в заголовке будет удален первым. и снова позвонюremoveметод, будетkeysПервый элемент удален.

Этоvueсредняя параkeep-aliveПроцесс оптимизации обработки кэша.