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 仍是建議你們挨個文章就看一下。

相關文章
相關標籤/搜索