Анализ 34 вопросов интервью Vue, которые должны знать продвинутые фронтенд-разработчики (4)

Vue.js

Прочитав эту статью, вы узнаете

1. Каков жизненный цикл Vue?

2. Хуки-функции в Vue

3. В какую функцию-ловушку помещается Ajax-запрос?

4. Когда используется beforeDestroy?

Примечание. Версия этой статьи для vue: 2.6.11.

Каков жизненный цикл Vue?

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

В каждом узле жизненного цикла Vue предоставляет некоторые функции-ловушки, чтобы код разработчика мог быть выполнен. Функцию ловушки здесь можно просто понять, так как некоторые функции с определенными именами, такими как created и Mount, предварительно определены в экземпляре Vue, а содержимое тела функции разработано для заполнения разработчиком.При создании экземпляра он будет в определенном порядке для выполнения этих функций-ловушек, тем самым давая коду разработчика возможность выполниться.

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

// 比如这是Vue的源码
function Vue(options) {
  console.log('初始化');
  // 开始执行一些代码
  console.log('开始创建');
  options.created();
  // 开始执行一些代码
  console.log('创建完成');
  options.mounted();
  console.log('其他操作');
}

// 实例化Vue构造函数
new Vue({
  // 挂载两个方法
  created () {
    console.log('我是开发者的代码, 我需要在创建完成前执行')
  },
  mounted () {
    console.log('我是开发者的代码, 我需要在创建完成后执行')
  },
})
/**
初始化
开始创建
我是开发者的代码, 我需要在创建完成前执行
创建完成
我是开发者的代码, 我需要在创建完成后执行
其他操作
*/

Хуки-функции в Vue

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

Ниже перечислены все функции ловушек, перечисленные в исходном коде, которые могут содержать код разработчика.

var LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
];

перед созданием и созданием

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

В отличие от beforeCreate, значение val в модели данных было инициализировано при выполнении create, но DOM страницы по-прежнему не может быть получен. Это означает, что в созданном мы можем инициировать асинхронные запросы для выполнения операций присваивания моделей данных, но мы не можем выполнять операции DOM страницы.

beforeCreate и created выполнить анализ исходного кода

// Vue入口
function Vue (options) {
  if (!(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword');
  }
  // 调用_init方法
  this._init(options);
}

// _init实现
Vue.prototype._init = function (options) {
  var vm = this;
  ... 
  initLifecycle(vm);   //初始化生命周期
  initEvents(vm);  //初始化事件监听
  initRender(vm);  //初始定义渲染选项,并且对一些属性进行监听。
  //执行开发者的beforeCreate内的代码
  callHook(vm, 'beforeCreate');
  initInjections(vm); // resolve injections before data/props
  initState(vm);  // 初始化数据模型
  initProvide(vm); // resolve provide after data/props
   //执行开发者的created内的代码
  callHook(vm, 'created');

  if (vm.$options.el) {
    vm.$mount(vm.$options.el);
  }
};

// Vue中调用钩子函数的封装函数
function callHook (vm, hook) {
  ...
  // 开发者写好的某hook函数
  var handlers = vm.$options[hook];
  ...
  if (handlers) {
    for (var i = 0, j = handlers.length; i < j; i++) {
      ...
      // 封装好的调用开发者方法
      invokeWithErrorHandling(handlers[i], vm, null, vm, info);
      ...
    }
  }
  ...
}
  
// 执行hook函数  
function invokeWithErrorHandling (handler,context,args,vm,info) {
  var res;
  try {
    // 调用执行
    res = args ? handler.apply(context, args) : handler.call(context);
    ...
  } catch (e) {
    handleError(e, vm, info);
  }
}

до монтирования и монтирования

Как видно из приведенного ниже исходного кода, существует только одно суждение браузера между beforeMount и created, поэтому в настоящее время статус модели данных и страницы в функции ловушки такой же, как и created.

При выполнении монтирования модель данных и DOM страницы инициализируются Здесь мы можем присваивать значения модели данных и выполнять операции DOM.

Анализ исходного кода beforeMount и Mounted

// _init实现
Vue.prototype._init = function (options) {
  var vm = this;
  ... 
  if (vm.$options.el) {
    // 挂载执行
    vm.$mount(vm.$options.el);
  }
};

// 开始挂载组件信息
Vue.prototype.$mount = function (el, hydrating) {
  el = el && inBrowser ? query(el) : undefined;  // 浏览器判断
  return mountComponent(this, el, hydrating)
};
function mountComponent (vm, el, hydrating) {
  vm.$el = el;  //this.$el开始挂载到实例中
  ... 
  callHook(vm, 'beforeMount');  // 执行开发者的beforeMount内的代码
  ...
  updateComponent = function () {  // 定义全局更新函数updateComponent
    vm._update(vm._render(), hydrating);
  };
  ... 
  // 启动Watcher,绑定vm._watcher属性
  new Watcher(vm, updateComponent, noop, {
    before: function before () {
      if (vm._isMounted && !vm._isDestroyed) {
        // 执行开发者的beforeUpdate内的代码
        callHook(vm, 'beforeUpdate');
      }
    },
  }, true /* isRenderWatcher */);

  if (vm.$vnode == null) {
    vm._isMounted = true;
    // 执行开发者的mounted内的代码
    callHook(vm, 'mounted');
  }
  return vm
}

// Watch构造函数
var Watcher = function Watcher (vm, expOrFn, cb, options, isRenderWatcher) {
  this.vm = vm;
  ... 
  // 将上面的updateComponent进行复制给this.getter 属性
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    this.getter = parsePath(expOrFn);
    if (!this.getter) {
      this.getter = noop;
      ...
    }
  }
  ...
  // 调用get方法
  this.get()
};

// watcher的get方法运行getter方法
Watcher.prototype.get = function get () {
  ...
  var vm = this.vm;
  try {
    // 实际执行了Vue的构造函数里的_init方法定义的updateComponent函数
    // vm._update(vm._render(), hydrating);
    value = this.getter.call(vm, vm);
  } catch (e) {
  ...
  return value
};
  
Vue.prototype._update = function (vnode, hydrating) {
  var vm = this;
  ... 
  // 渲染页面,更新节点
  if (!prevVnode) {
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
  } else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode);
  }
  ...
};

перед обновлением и обновлением

Здесь следует отметить, что код в beforeUpdate не выполняется автоматически, как предыдущие четыре функции-хука, а запускается путем манипулирования значениями в модели данных.В примере на рисунке, поскольку смонтированный this.val=' 56789', что приводит к выполнению beforeUpdate, а когда выполняется beforeUpdate, значение в модели данных уже является последним значением после операции.

Выполнение Update происходит после beforeUpdate, и данные beforeUpdate согласуются со страницей.

beforeUpdate и анализ исходного кода обновлений

...  
// 启动Watcher,绑定vm._watcher属性
new Watcher(vm, updateComponent, noop, {
  before: function before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate');   // 执行开发者的beforeUpdate内的代码
    }
  },
}, true /* isRenderWatcher */);
...

//数据模型里面的值变化时触发该函数(可以看上一篇文章)
// 例如this.val=345改变data里的val属性的时候,该函数将得到执行。
function flushSchedulerQueue () {
  ...
  var watcher, id
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index];
    if (watcher.before) {
      //触发beforeUpdate的钩子函数
      watcher.before(); 
    }
  }
  ... 
   //调用activate的钩子函数
  callActivatedHooks(activatedQueue);
   //调用update的钩子函数
  callUpdatedHooks(updatedQueue);
  ...
}
  
// 调用updated钩子函数
function callUpdatedHooks (queue) {
  var i = queue.length;
  while (i--) { // 轮询队列里所有的变化
    var watcher = queue[i];
    var vm = watcher.vm;
    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'updated');  // 执行开发者的updated内的代码
    }
  }
}

активирован и деактивирован

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

import Vue from './node_modules/_vue@2.6.11@vue/dist/vue.common.dev'

new Vue({
  el: '#app',
  template: `
    <div id="app">
      <keep-alive>
        <my-comp v-if="show" :val="val"></my-comp>
      </keep-alive>
    </div>`,
  data () { return { val: '12345', show: true } },
  components: {
    // 自定义子组件my-comp
    'my-comp': {
      template: '<div>{{val}}</div>',
      props: [ 'val' ],
      activated() {
        debugger; // 加载时触发执行
      },
      deactivated() {
        debugger; //两秒后触发执行
      }
    }
  },
  mounted() {
    setTimeout(() => {
      this.show = false
    }, 2000)
  }
})

активированный исходный код триггера

Он будет вызываться только тогда, когда компонент активируется кешем тегов.

// 当keep-alive的子组件被激活的时候insert方法将得到执行
// 也就是上面例子中this.show = true的时候
insert: function insert (vnode) {
  var context = vnode.context;
  var componentInstance = vnode.componentInstance;
  if (!componentInstance._isMounted) {
    componentInstance._isMounted = true;
    // 先调用keep-alive子组件的mounted钩子方法
    callHook(componentInstance, 'mounted');
  }
  if (vnode.data.keepAlive) {
    if (context._isMounted) {
      // 如果外部组件是已经加载完成的,即上面例子里的show初始为false,加载完后this.show=true
      // 将callActivatedHooks所调用的activatedQueue队列push进去值
      queueActivatedComponent(componentInstance);
    } else {
      // 如果外部组件未加载完成的。
      // 就像上面例子的写法,show初始为true,加载完后this.show=false
      // 然后在activateChildComponent直接触发activated钩子函数
      activateChildComponent(componentInstance, true /* direct */);
    }
  }
}
  
//数据模型里面的值变化时触发该函数(可以看上一篇文章)
//例如this.val=345改变data里的val属性的时候,该函数将得到执行。
//执行的时候触发callActivatedHooks函数,会在这时候调用activate钩子函数
function flushSchedulerQueue () {
  ...
  var watcher, id
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index];
    if (watcher.before) {
      //触发beforeUpdate的钩子函数
      watcher.before(); 
    }
  }
  ... 
   //调用activate的钩子函数
  callActivatedHooks(activatedQueue);
   //调用update的钩子函数
  callUpdatedHooks(updatedQueue);
  ...
}
  
// 数据模型data数据变化时触发执行
function callActivatedHooks (queue) {
  for (var i = 0; i < queue.length; i++) {
    ...
    // 调用activated的钩子函数执行
    activateChildComponent(queue[i], true /* true */);
  }
}
// 只有缓存的组件触发该钩子函数
function activateChildComponent (vm, direct) {
  ...
  if (vm._inactive || vm._inactive === null) {
    vm._inactive = false;
    for (var i = 0; i < vm.$children.length; i++) {
      // 递归调用子组件触发其钩子函数
      activateChildComponent(vm.$children[i]);
    }
    // 执行开发者的activated钩子函数内的代码
    callHook(vm, 'activated');
  }
}

Выполнение деактивированных

Триггер деактивированной функции ловушки срабатывает, когда компонент, закэшированный тегом поддержки активности, деактивируется, например компонент my-comp, обернутый тегом поддержки активности в следующем примере, когда для подкомпонента установлено значение false с помощью v -if будет реализована деактивированная функция ловушки.

деактивированный исходный код триггера

//对于deactivate的触发,只会是子组件destroy方法执行时被调用,
function destroy (vnode) { // 调用组件注销时触发
  if (!componentInstance._isDestroyed) {
    // 当触发的组件不是keep-alive标签的组件时触发$destroy
    if (!vnode.data.keepAlive) {
      // 触发实例组件的注销
      componentInstance.$destroy();
    } else {
      // 触发deactivated的钩子函数
      deactivateChildComponent(componentInstance, true /* direct */);
    }
  }
}
function deactivateChildComponent (vm, direct) {
  ...
  if (!vm._inactive) {
    vm._inactive = true;
    for (var i = 0; i < vm.$children.length; i++) {
      deactivateChildComponent(vm.$children[i]);  //递归执行触发deactivated钩子函数
    }
    // 执行开发者的deactivated内的代码
    callHook(vm, 'deactivated');
  }
}

перед уничтожением и уничтожением

Уничтожьте компонент вручную при монтировании, запустив выполнение функции ловушки beforeDestroy, и вы все равно увидите, что модель данных и DOM здесь не выходят из системы.

Здесь мы видим, что DOM был очищен.

beforeDestroy и анализ уничтоженного исходного кода

// Vue的原型链方法 $destroy 
Vue.prototype.$destroy = function () {
  var vm = this;
  ...
  // 执行开发者的beforeDestroy内的代码
  callHook(vm, 'beforeDestroy');
  ...
  var parent = vm.$parent;
  if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
    remove(parent.$children, vm);
  }
  // 将数据监听移除
  if (vm._watcher) {
    vm._watcher.teardown();
  }
  var i = vm._watchers.length;
  while (i--) {
    vm._watchers[i].teardown();
  }
  if (vm._data.__ob__) {
    vm._data.__ob__.vmCount--;
  }
  // 调用一次渲染,将页面dom树置为null
  vm.__patch__(vm._vnode, null);
  //调用开发者的destroyed钩子函数代码
  callHook(vm, 'destroyed');
  // 关闭时间监听
  vm.$off();
  // 移除Vue的所有依赖
  if (vm.$el) {
    vm.$el.__vue__ = null;
  }
  // 节点置为null
  if (vm.$vnode) {
    vm.$vnode.parent = null;
  }
};

errorCaptured

Функция-ловушка, представленная после версии 2.5.0+, предназначена для стабильности. Когда в компоненте-потомке возникает исключение, эта функция-ловушка будет запущена. Она имеет три параметра: объект ошибки, экземпляр компонента, в котором произошла ошибка, и источник ошибки. Вы можете Активно возвращать false, чтобы предотвратить дальнейшее распространение ошибки на вышеприведенный родительский компонент.

Вы можете увидеть следующий пример: я напрямую выбрасываю новую ошибку в смонтированный подкомпонент my-comp, а функция ловушки erroeCaptured во внешнем компоненте запускается и выполняется.

Анализ исходного кода errorCaptured

Видно, что его суть на самом деле является try catch, который оборачивает подкомпоненты, один раз перехватывает все пойманные исключения и во время catch решает, продолжать ли выбрасывать ошибки на внешний слой.

// errorCaptured的执行则不通过callHook来执行,而是直接取了$options.errorCaptured来执行
function handleError (err, vm, info) {
  ... 
  var hooks = cur.$options.errorCaptured;
  if (hooks) {
    for (var i = 0; i < hooks.length; i++) {
      try {
        // 执行开发者定义的errorCaptured函数
        var capture = hooks[i].call(cur, err, vm, info) === false;
        // 如果钩子函数返回为false时,直接return,不在往上传播错误
        if (capture) { return }
      } catch (e) {
        globalHandleError(e, cur, 'errorCaptured hook');
      }
    }
  }
}

serverPrefetch

Этот метод представляет собой недавно добавленную функцию-ловушку в версии 2.6+ и может запускаться только во время рендеринга на стороне сервера. Он вернет промис, потому что здесь невозможно выполнить отладку с помощью браузера. Этот API пока не будет представлен, и будет подробно написано позже.

В какую функцию ловушки помещается запрос Ajax?

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

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

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

Когда используется beforeDestroy?

На самом деле, destroy достаточно для большинства разрушенных сцен, а когда используется beforeDestroy?

Посмотрите на разницу между ними: когда выполняется beforeDestroy, DOM страницы все еще не уничтожается, а когда выполняется Destroy, страница перерисовывается, поэтому мы можем выполнить некоторые специальные операции на странице до того, как компонент будет уничтожен. перед уничтожением.


References

[1] https://github.com/vuejs/vue/blob/v2.6.11/dist/vue.common.dev.js

[2] https://cn.vuejs.org/

постскриптум

Если вы хотите обсудить технологию или у вас есть какие-либо комментарии или предложения по этой статье, вы можете отсканировать приведенный ниже QR-код, подписаться на общедоступную учетную запись WeChat «Full Stacker» и добро пожаловать в WeChat автора для взаимодействия с автором в любое время. . Добро пожаловать! Искренне надеемся встретиться с вами.

Если у вас есть какие-либо вопросы, добро пожаловать в группу для общения~