在eventsMixin中掛載到Vue構造函數的prototype中前端
將回調fn註冊到事件列表中便可,_events在實例初始化時建立。vue
Vue.prototype.$on = function(event, fn) {
const vm = this;
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
this.$on(event[i], fn);
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn);
}
return vm;
};
複製代碼
支持off
、off('eventName')
、off('eventName', fn)
、off(['eventName1', 'eventName2'])
、off(['eventName1', 'eventName2'], fn)
多種狀況node
Vue.prototype.$off = function(event, fn) {
const vm = this;
if (!arguments.length) {
vm._events = Object.create(null);
return vm;
}
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
this.$off(event[i], fn);
}
return vm;
}
const cbs = vm._events[event];
if (!cbs) {
return vm;
}
if (!fn) {
vm._events[event] = null;
return vm;
}
if (fn) {
const cbs = vm._events[event];
let cb;
let i = cbs.length;
while (i--) {
cb = cbs[i];
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1);
break;
}
}
}
return vm;
};
複製代碼
先移除事件監聽,再執行函數。git
Vue.prototype.$once = function(event, fn) {
const vm = this;
function on() {
vm.$off(event, on)
fn.apply(vm, arguments)
}
on.fn = fn;
vm.$on(event, on);
return vm;
};
複製代碼
取出對應event回調函數列表,再遍歷執行github
Vue.prototype.$emit = function(event) {
const vm = this;
let cbs = vm._events[event];
if (cbs) {
const args = Array.from(arguments).slice(1)
for (let i = 0, l = cbs.length; i < l; i ++) {
try {
cbs[i].apply(vm, args)
} catch (e) {
console.error(e, vm, `event handler for "${event}"`)
}
}
}
return vm;
};
複製代碼
執行_watcher.update(前面介紹過原理),手動通知實例從新渲染緩存
Vue.prototype.$forceUpdate = function() {
const vm = this;
if (vm._watcher) {
vm._watcher.update();
}
};
複製代碼
vm.$destroy能夠銷燬一個實例app
vm.__patch__(vm._vnode, null)
Vue.prototype.$destroy = function() {
const vm = this;
if (vm._isBeingDestroyed) {
return;
}
// callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true;
const parent = vm.$parent;
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm);
}
if (vm._watcher) {
vm._watcher.teardown();
}
let i = vm._watchers.length;
while (i--) {
vm._watchers[i].teardown();
}
vm._isDestroyed = true;
// vm.__patch__(vm._vnode, null);
// callHook(vm, 'destroyed')
vm.$off();
};
複製代碼
nextTick接收一個回調函數做爲參數,它的做用是將回調延遲到下次DOM更新週期以後執行。若是沒有提供回調且支持Promise的環境中,則返回一個Promise。異步
使用示例:函數
new Vue({
// ...
methods: {
example: function() {
this.msg = 1;
this.$nextTick(function () {
// DOM如今更新了
})
}
}
})
複製代碼
在同一輪事件循環中即便有數據發生了兩次相同的變化,也不會渲染兩次。由於Vue.js會將受到通知的watcher實例添加到隊列中緩存起來,添加到隊列以前會檢查是否已經存在相同的watcher,只有不存在,纔會將watcher實例添加到隊列中。下次事件循環會讓隊列裏的watcher觸發渲染流程並清空隊列。oop
JavaScript是單線程的腳本語言,任什麼時候候都只有一個主線程來處理任務。當處理異步任務時,主線程會掛起這任務,當任務處理完畢,JavaScript會將這個事件加入一個隊列,咱們叫事件隊列
,被放入事件隊列中的事件不會當即執行其回調,而是等待當前執行棧中的全部任務執行完畢後,主線程會去查找事件隊列中是否有任務。
異步任務有兩種類型:微任務和宏任務。當執行棧中的全部任務都執行完畢後,會去檢查微任務隊列中是否有事件存在,若是有則一次執行微任務隊列中事件對應的回調,直到爲空。而後去宏任務隊列中取出一個事件,把對應的回調加入當前執行棧,當執行棧中的全部任務都執行完畢後,檢查微任務隊列,如此往復,這個循環就是事件循環
。
屬於微任務的事件有:
屬於宏任務的事件有
下次DOM更新週期實際上是下次微任務執行時更新DOM。vm.$nextTick實際上是將回調添加到微任務中。只有特殊狀況下才會降級成宏任務。
nextTick通常狀況會使用Promise.then將flushCallbacks
添加到微任務隊列中 withMacroTask
包裹的函數所使用的nextTick方法會將回調添加到宏任務中。
const callbacks = [];
let pending = false;
function flushCallbacks() {
pending = false;
const copies = callbacks.slice(0);
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}
let microTimerFunc;
let macroTimerFunc;
function isNative() {
// 實現忽略
return true;
}
if (typeof setImmediate !== "undefined" && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks);
};
} else if (
typeof MessageChannel !== "undefined" &&
(isNative(MessageChannel) ||
MessageChannel.toString() === "[object MessageChannelConstructor]")
) {
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = flushCallbacks;
macroTimerFunc = () => {
port.postMessage(1);
};
} else {
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
let useMacroTask = false;
if (typeof Promise !== "undefined" && isNative(Promise)) {
const p = Promise.resolve();
microTimerFunc = () => {
p.then(flushCallbacks);
};
} else {
microTimerFunc = macroTimerFunc;
}
export function withMacroTask(fn) {
return (
fn._withTask ||
(fn._withTask = function() {
useMacroTask = true;
const res = fn.apply(null, arguments);
useMacroTask = false;
return res;
})
);
}
export function nextTick(cb, ctx) {
let _resolve;
callbacks.push(() => {
if (cb) {
cb.call(ctx);
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
if (useMacroTask) {
macroTimerFunc();
} else {
microTimerFunc();
}
}
if (!cb && typeof Promise !== "undefined") {
return new Promise(resolve => {
_resolve = resolve;
});
}
}
複製代碼
想讓Vue.js實例具備關聯的DOM元素,只有使用vm.$mount方法這一種途經。
Vue.prototype.$mount = function(el) {
el = el && query(el);
const options = this.$options;
if (!options.render) {
let template = options.template;
if (template) {
if (typeof template === "string") {
if (template.charAt(0) === "#") {
template = idToTemplate(template);
}
} else if (template.nodeType) {
template = template.innerHTML;
} else {
if (process.env.NODE_ENV !== "production") {
console.warn("invalid template option:" + template, this);
}
return this;
}
} else if (el) {
template = getOuterHTML(el);
}
if (template) {
const { render } = compileToFunctions(template, options, this);
options.render = render;
}
}
return mountComponent(this, el);
// return mount.call(this, el);
};
}
function mountComponent(vm, el) {
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode;
if (process.env.NODE_ENV !== "production") {
// 在開發環境發出警告
}
callHook(vm, "beforeMount");
// 掛載
// _update 調用patch方法執行節點的比對和渲染操做
// _render 執行渲染函數,獲得一份最新的VNode節點樹
// vm._watcher = new Watcher(
// vm,
// () => {
// vm._update(vm._render());
// },
// noop
// );
callHook(vm, "mounted");
return vm;
}
}
function createEmptyVNode() {}
function callHook() {}
function idToTemplate(id) {
const el = query(id);
return el && el.innerHTML;
}
function query(el) {
if (typeof el === "string") {
const selected = document.querySelector(el);
if (!selected) {
return document.createElement("div");
}
return selected;
} else {
return el;
}
}
function getOuterHTML(el) {
if (el.outerHTML) {
return el.outerHTML;
} else {
const container = document.createElement("div");
container.appendChild(el.cloneNode(true));
return container.innerHTML;
}
}
const cache = {};
function compile() {
// 03章節介紹過的生成代碼字符串
return {
render: ""
};
}
function compileToFunctions(template, options, vm) {
// options = extend({}, options);
// 檢查緩存
const key = options.delimiters
? String(options.delimiters) + template
: template;
if (cache[key]) {
return cache[key];
}
const compiled = compile(template, options);
const res = {};
res.render = createFunction(compiled.render);
return (cache[key] = res);
}
function createFunction(code) {
return new Function(code);
}
複製代碼
使用基礎Vue構造器建立一個"子類"
let cid = 1;
const ASSET_TYPES = ["component", "directive", "filter"];
exports.extend = function(Vue) {
Vue.extend = function(extendOptions) {
extendOptions = extendOptions || {};
const Super = this;
const SuperId = Super.cid;
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId];
}
// const name = extendOptions.name || Super.options.name;
const name = extendOptions.name;
if (process.env.NODE_ENV !== "production") {
if (!/^[a-zA-Z][\w-]*$/.test(name)) {
console.warn("");
}
}
const Sub = function VueComponent(options) {
this._init(options);
};
Sub.prototype = Object.create(Super.prototype);
Super.prototype.constructor = Sub;
Sub.cid = cid++;
Sub.options = { ...Super.options, ...extendOptions };
Sub["super"] = Super;
if (Sub.options.props) {
initProps(Sub);
}
if (Sub.options.computed) {
initComputed(Sub);
}
Sub.extend = Super.extend;
Sub.mixin = Super.mixin;
Sub.use = Super.use;
ASSET_TYPES.forEach(type => {
Sub[type] = Super[type];
});
if (name) {
Sub.options.components[name] = Sub;
}
Sub.superOptions = Super.options;
Sub.extendOptions = extendOptions;
Sub.sealedOptions = Object.assign({}, Sub.options);
cachedCtors[SuperId] = Sub;
return Sub;
};
};
function initProps(Comp) {
const props = Comp.options.props;
for (const key in props) {
proxy(Comp.prototype, `_props`, key);
}
}
function proxy(target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key];
};
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val;
};
Object.defineProperties(target, key, sharedPropertyDefinition);
}
function initComputed(Comp) {
const computed = Comp.options.computed;
for (const key in computed) {
definedComputed(Comp.prototype, key, computed[key]);
}
}
複製代碼
和前面介紹過的原理同樣
和前面介紹過的原理同樣
和前面介紹過的原理同樣
// Vue.filter、Vue.component、Vue.directive原理
const ASSET_TYPES = ["component", "directive", "filter"];
function isPlainObject() {}
exports.filterAndOther = function(Vue) {
Vue.options = Object.create(null);
ASSET_TYPES.forEach(type => {
Vue.options[type + "s"] = Object.create(null);
});
ASSET_TYPES.forEach(type => {
Vue.directive = function(id, definition) {
ASSET_TYPES.forEach(type => {
Vue.options[type + "s"] = Object.create(null);
});
ASSET_TYPES.forEach(type => {
Vue[type] = function(id, definition) {
if (!definition) {
return this.options[type + "s"][id];
} else {
if (type === "component" && isPlainObject(definition)) {
definition.name = definition.name || id;
definition = Vue.extend(definition);
}
if (type === "directive" && typeof definition === "function") {
definition = { bind: definition, update: definition };
}
this.options[type + "s"][id] = definition;
return definition;
}
};
});
};
});
};
複製代碼
會調用install方法,將Vue做爲參數傳入,install方法會被同一個插件屢次調用,插件只會安裝一次。
exports.use = function(Vue) {
Vue.use = function(plugin) {
const installedPlugins =
this._installedPlugins || (this._installedPlugins = []);
if (installedPlugins.indexOf(plugin) > -1) {
return this;
}
const args = Array.from(arguments).slice(1);
args.unshift(this);
if (typeof plugin.install === "function") {
plugin.install.apply(plugin, args);
} else if (typeof plugin === "function") {
plugin.apply(null, args);
}
installedPlugins.push(plugin);
return this;
};
};
複製代碼
全局註冊一個混入(mixin),影響註冊以後建立的每一個Vue.js實例。插件做者可使用混入向組件注入自定義行爲(例如:監聽生命週期鉤子)。不推薦在應用代碼中使用
function mergeOptions() {}
exports.mixin = function(Vue) {
Vue.mixin = function(mixin) {
this.options = mergeOptions(this.options, mixin);
return this;
};
};
複製代碼
前面介紹過的將模板編譯成渲染函數的原理
返回Vue.js安裝版本號,從Vue構建文件配置中取 完整代碼 (github.com/mfaying/sim…)
《深刻淺出Vue.js》