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

栏目: JavaScript · 发布时间: 5年前

内容简介:上一章的最后,总结了这篇文章,会涉及到组件更新的实现:

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

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

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

渲染 dom 的实现: $nextTick render

实例方法 / 事件

eventsMixin(Vue);
复制代码

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

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 不是数组为止;

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

检查当前的事件是否是以 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 还是建议大家挨个文章就看一下。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Beginning Google Maps API 3

Beginning Google Maps API 3

Gabriel Svennerberg / Apress / 2010-07-27 / $39.99

This book is about the next generation of the Google Maps API. It will provide the reader with the skills and knowledge necessary to incorporate Google Maps v3 on web pages in both desktop and mobile ......一起来看看 《Beginning Google Maps API 3》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

URL 编码/解码
URL 编码/解码

URL 编码/解码

SHA 加密
SHA 加密

SHA 加密工具