前言
上一章的最後,總結了 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
接收到的回撥函式 fn
的 this
指向 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
是否被渲染,如果沒渲染過,就初始化渲染,否則就做更新;
執行 restoreActiveInstance
把 activeInstance
換成原來的值;
其實就是更新完
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
完全銷燬一個例項。清理它與其它例項的連線,解綁它的全部指令及事件監聽器。
觸發 beforeDestroy
和 destroyed
的鉤子。
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
不自帶Promise
的polyfill
,所以如果你的目標瀏覽器不是原生支援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
,這允許渲染函式訪問佔位符節點上的資料;
把當前的 render
的 this
指向 vm._renderProxy
並把 vm.$createElement
當做引數傳給 render
;
當然,在渲染的過程當中,如果報錯,那麼就返回錯誤呈現結果或以前的Vnode,以防止呈現錯誤導致空白元件。
結束語
這一篇,基本上會講解的就是這部分內容,在這一篇文章寫完後,會總結一篇 vue 生命週期方法的實現
的文章和一篇 vue 例項化前的原始碼彙總
,由於涉及的知識點太多,分開了很多章去寫,理解和學習起 vue
的實現理念來,不是很方便,但是如果大家想了解作者到底是怎麼實現的 vue
還是建議大家挨個文章就看一下。