上一章的最後,總結了 Watcher
的實現,對於 vue
實例化前要作的事情,在這一章,就要終結了,因此這一篇,也就是 vue
實例化前的最終章。javascript
這篇文章,會涉及到 vue
一些事件的實現:$on
、 $once
、 $off
、 $emit
;vue
組件更新的實現:updated
、 $forceUpdate
、 $destroy
;java
渲染 dom
的實現:$nextTick
、 render
。node
eventsMixin(Vue);
複製代碼
在該函數裏面,,就是 $on
、 $once
、 $off
、 $emit
的實現,只是在這幾個方法實現的前面,有一個正則:api
var hookRE = /^hook:/;
複製代碼
用來判斷是不是以 hook:
開頭的事件。數組
對於 $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
就是用來監聽一個自定義事件,可是隻觸發一次,在第一次觸發以後移除監聽器。
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
用來移除自定義事件監聽器。
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
去分別卸載每個數組內的事件監聽器;
若是當前的事件不存在,就直接返回;
若是不存在回調函數的話,直接把當前事件給移除;
若是存在回調的話,檢查當前的訂閱數組,刪除當前回調函數,並退出。
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
};
複製代碼
在發佈訂閱的時候,要檢查當前發佈事件的命名問題;
若是當前的要發佈的事件,存在回調,就依次發佈事件到訂閱的事件裏面。
知識點:能夠經過源碼發現,上面的全部事件,都支持鏈式調用
_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
迫使 Vue 實例從新渲染。
注意它僅僅影響實例自己和插入插槽內容的子組件,而不是全部子組件。
Vue.prototype.$forceUpdate = function () {
var vm = this;
if (vm._watcher) {
vm._watcher.update();
}
};
複製代碼
這裏比較簡單了,就是一個更新當前實例的監聽, watcher
的實現,在上一章寫過,入口:Vue 源碼解析(實例化前) - 初始化全局API(三) ,這裏介紹了 watcher
全部的實現。
$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
鉤子。
將回調延遲到下次 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(二) 章節中,作過講解,不瞭解的你們能夠過去看一下。
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
仍是建議你們挨個文章就看一下。