vue 源码解析(实例化前) - 初始化全局 API(最终章)

前言

上一章的最后,总结了 Watcher 的实现,对于 vue 实例化前要作的事情,在这一章,就要终结了,因此这一篇,也就是 vue 实例化前的最终章。javascript

这篇文章,会涉及到 vue 一些事件的实现:$on$once$off$emitvue

组件更新的实现:updated$forceUpdate$destroyjava

渲染 dom 的实现:$nextTickrendernode

实例方法 / 事件

eventsMixin(Vue);
复制代码

在该函数里面,,就是 $on$once$off$emit 的实现,只是在这几个方法实现的前面,有一个正则:api

var hookRE = /^hook:/;
复制代码

用来判断是不是以 hook: 开头的事件。数组

$on

对于 $on 的实现,其实就是一个发布订阅关系中,一个充当订阅的角色,和 $emit 是配合使用:浏览器

Vue.prototype.$on = function (event, fn) {
  var vm = this;
  if (Array.isArray(event)) {
    for (var i = 0, l = event.length; i < l; i++) {
      vm.$on(event[i], fn);
    }
  } else {
    (vm._events[event] || (vm._events[event] = [])).push(fn);
    if (hookRE.test(event)) {
      vm._hasHookEvent = true;
    }
  }
  return vm
};
复制代码

$on 当中,一开始的时候会保存当前的 this 指针,而后检查在调用 $on 方法时候,接收到的 event 参数是不是数组,若是是数组,就循环调用 $on 方法,一直到发现 event 不是数组为止;app

而后检查 vue 的构造函数下的 _events 对象是否存在当前的事件,不存在就建立一个数组,存在的话,把订阅的回调 fn 添加到 _events 的当前事件属性的数组当中;dom

检查当前的事件是不是以 hook: 开头的事件,若是是的话,就设置当前 vue_hasHookEvent 的状态为 true函数

$once

$once 就是用来监听一个自定义事件,可是只触发一次,在第一次触发以后移除监听器。

Vue.prototype.$once = function (event, fn) {
  var vm = this;
  function on() {
    vm.$off(event, on);
    fn.apply(vm, arguments);
  }
  on.fn = fn;
  vm.$on(event, on);
  return vm
};
复制代码

这里就比较简单了, $once 接收到的参数,和 $on 同样,其实在 $once 当中,直接绑定的也是 $on 方法;

在发布订阅的时候,直接执行的是在 $once 内部的 on 方法;

on 方法中,调用 $off 移除了事件监听器;

最后把 $once 接收到的回调函数 fnthis 指向 vue 构造函数,把在 $on 接收到的参数,传给 $once 的回调函数。

$off

$off 用来移除自定义事件监听器。

Vue.prototype.$off = function (event, fn) {
  var vm = this;
  // all
  if (!arguments.length) {
    vm._events = Object.create(null);
    return vm
  }
  // array of events
  if (Array.isArray(event)) {
    for (var i = 0, l = event.length; i < l; i++) {
      vm.$off(event[i], fn);
    }
    return vm
  }
  // specific event
  var cbs = vm._events[event];
  if (!cbs) {
    return vm
  }
  if (!fn) {
    vm._events[event] = null;
    return vm
  }
  if (fn) {
    var cb;
    var i = cbs.length;
    while (i--) {
      cb = cbs[i];
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1);
        break
      }
    }
  }
  return vm
};
复制代码

$off 不接收任何参数的时候,表明要把 vue 构造函数内的全部事件监听器所有卸载;

若是接收到的 event 是数组,那就循环调用 $off 去分别卸载每个数组内的事件监听器;

若是当前的事件不存在,就直接返回;

若是不存在回调函数的话,直接把当前事件给移除;

若是存在回调的话,检查当前的订阅数组,删除当前回调函数,并退出。

$emit

Vue.prototype.$emit = function (event) {
  var vm = this;
  {
    var lowerCaseEvent = event.toLowerCase();
    if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
      tip(
        "Event \"" + lowerCaseEvent + "\" is emitted in component " +
        (formatComponentName(vm)) + " but the handler is registered for \"" + event + "\". " +
        "Note that HTML attributes are case-insensitive and you cannot use " +
        "v-on to listen to camelCase events when using in-DOM templates. " +
        "You should probably use \"" + (hyphenate(event)) + "\" instead of \"" + event + "\"."
      );
    }
  }
  var cbs = vm._events[event];
  if (cbs) {
    cbs = cbs.length > 1 ? toArray(cbs) : cbs;
    var args = toArray(arguments, 1);
    for (var i = 0, l = cbs.length; i < l; i++) {
      try {
        cbs[i].apply(vm, args);
      } catch (e) {
        handleError(e, vm, ("event handler for \"" + event + "\""));
      }
    }
  }
  return vm
};
复制代码

在发布订阅的时候,要检查当前发布事件的命名问题;

若是当前的要发布的事件,存在回调,就依次发布事件到订阅的事件里面。

知识点:能够经过源码发现,上面的全部事件,都支持链式调用

组件更新的实现

updated

_update 用来更新组件信息

Vue.prototype._update = function (vnode, hydrating) {
  var vm = this;
  var prevEl = vm.$el;
  var prevVnode = vm._vnode;
  var restoreActiveInstance = setActiveInstance(vm);
  vm._vnode = vnode;
  
  if (!prevVnode) {
  
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);
  } else {
  
    vm.$el = vm.__patch__(prevVnode, vnode);
  }
  restoreActiveInstance();
  
  if (prevEl) {
    prevEl.__vue__ = null;
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm;
  }
  
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el;
  }
  
};
复制代码

把当前的 $el_vnode 保存起来;

调用 restoreActiveInstance 返回一个结果,保存到 restoreActiveInstance 变量当中;

var activeInstance = null;

function setActiveInstance(vm) {
  var prevActiveInstance = activeInstance;
  activeInstance = vm;
  return function () {
    activeInstance = prevActiveInstance;
  }
}
复制代码

实现的就是在更新的时候,使用接收到的 vue 实例,使用完毕后调用 return 回去的函数,替换回原来的实例对象;

检查当前的 vNode 是否被渲染,若是没渲染过,就初始化渲染,不然就作更新;

执行 restoreActiveInstanceactiveInstance 换成原来的值;

其实就是更新完 node 后,把 activeInstance 置空。

若是存在渲染节点,那么就给当前的 vm.$el 添加一个 __vue__ 属性,默认值为 null

__vue__ 指向更新时接收到的 vue 实例;

若是当前实例的父级 $parent 是 HOC,那么也更新其 $el

$forceUpdate

$forceUpdate 迫使 Vue 实例从新渲染。

注意它仅仅影响实例自己和插入插槽内容的子组件,而不是全部子组件。

Vue.prototype.$forceUpdate = function () {
  var vm = this;
  if (vm._watcher) {
    vm._watcher.update();
  }
};
复制代码

这里比较简单了,就是一个更新当前实例的监听, watcher 的实现,在上一章写过,入口:Vue 源码解析(实例化前) - 初始化全局API(三) ,这里介绍了 watcher 全部的实现。

$destroy

$destroy 彻底销毁一个实例。清理它与其它实例的链接,解绑它的所有指令及事件监听器。

触发 beforeDestroydestroyed 的钩子。

Vue.prototype.$destroy = function () {
  var vm = this;
  if (vm._isBeingDestroyed) {
    return
  }
  callHook(vm, 'beforeDestroy');
  vm._isBeingDestroyed = true;
  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--;
  }
  
  vm._isDestroyed = true;
  
  vm.__patch__(vm._vnode, null);
  
  callHook(vm, 'destroyed');
  
  vm.$off();
  
  if (vm.$el) {
    vm.$el.__vue__ = null;
  }
  
  if (vm.$vnode) {
    vm.$vnode.parent = null;
  }
};
复制代码

检查当前的 vue 实例是否正在卸载;

注册一个 beforeDestroy 钩子:

function callHook(vm, hook) {

  pushTarget();
  var handlers = vm.$options[hook];
  if (handlers) {
    for (var i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm);
      } catch (e) {
        handleError(e, vm, (hook + " hook"));
      }
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook);
  }
  popTarget();
}
复制代码

检查当前的实例化对象中,有没有当前的 hook 钩子,若是在实例化 Vue 构造函数的时候,配置属性里面没有当前钩子,就跳过;若是有的话,执行。

执行完 beforeDestroy 后,开始从当前实例化对象的父级去移除当前对象;

卸载当前实例上的 watcher 对象;

从数据对象中移除引用冻结对象可能没有观察者;

在当前渲染树上调用销毁钩子;

执行 destroyed 钩子;

卸载全部的事件监听器;

把和当前有关系的一些属性,全设为 null

组件渲染

这里最主要作的就是有关组件渲染的方法,$nextTick 和 组件的 render 钩子。

$nextTick

将回调延迟到下次 DOM 更新循环以后执行。在修改数据以后当即使用它,而后等待 DOM 更新。它跟全局方法 Vue.nextTick 同样,不一样的是回调的 this 自动绑定到调用它的实例上。

2.1.0 起新增:若是没有提供回调且在支持 Promise 的环境中,则返回一个 Promise。请注意 Vue 不自带 Promisepolyfill,因此若是你的目标浏览器不是原生支持 Promise (IE:大家都看我干吗),你得自行 polyfill

Vue.prototype.$nextTick = function (fn) {
  return nextTick(fn, this)
};
复制代码

nextTick 在以前 Vue 源码解析(实例化前) - 初始化全局API(二) 章节中,作过讲解,不了解的你们能够过去看一下。

render

Vue.prototype._render = function () {
  var vm = this;
  var ref = vm.$options;
  var render = ref.render;
  var _parentVnode = ref._parentVnode;

  if (_parentVnode) {
    vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject;
  }

  vm.$vnode = _parentVnode;
  
  var vnode;
  try {
    vnode = render.call(vm._renderProxy, vm.$createElement);
  } catch (e) {
    handleError(e, vm, "render");
    if (vm.$options.renderError) {
      try {
        vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e);
      } catch (e) {
        handleError(e, vm, "renderError");
        vnode = vm._vnode;
      }
    } else {
      vnode = vm._vnode;
    }
  }
  if (!(vnode instanceof VNode)) {
    if (Array.isArray(vnode)) {
      warn(
        'Multiple root nodes returned from render function. Render function ' +
        'should return a single root node.',
        vm
      );
    }
    vnode = createEmptyVNode();
  }
  // set parent
  vnode.parent = _parentVnode;
  return vnode
};
复制代码

检查配置的属性当中,是否存在 _parentVnode 属性,若是存在就把他的 data.scopedSlots 指向实例化对象的 $scopedSlots

点击查看 $scopedSlots 的使用

设置父 parent Vnode ,这容许渲染函数访问占位符节点上的数据;

把当前的 renderthis 指向 vm._renderProxy 并把 vm.$createElement 当作参数传给 render

固然,在渲染的过程中,若是报错,那么就返回错误呈现结果或之前的Vnode,以防止呈现错误致使空白组件。

结束语

这一篇,基本上会讲解的就是这部份内容,在这一篇文章写完后,会总结一篇 vue 生命周期方法的实现的文章和一篇 vue 实例化前的源码汇总,因为涉及的知识点太多,分开了不少章去写,理解和学习起 vue 的实现理念来,不是很方便,可是若是你们想了解做者究竟是怎么实现的 vue 仍是建议你们挨个文章就看一下。

相关文章
相关标签/搜索