vue 原始碼解析(例項化前) - 初始化全域性 API(最終章)

熱情的劉大爺發表於2019-02-12

前言

上一章的最後,總結了 Watcher 的實現,對於 vue 例項化前要做的事情,在這一章,就要終結了,所以這一篇,也就是 vue 例項化前的最終章。

這篇文章,會涉及到 vue 一些事件的實現:$on$once$off$emit

元件更新的實現:updated$forceUpdate$destroy

渲染 dom 的實現:$nextTickrender

例項方法 / 事件

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 還是建議大家挨個文章就看一下。

相關文章