前言
在Vue.js内部,有下面这样一段代码,其中先定义了Vue构造函数,然后分别调用了initMixin、stateMixin、eventsMixin、lifecycleMixin、renderMixin这5个函数,并加Vue构造函数当做参数传给了这5个函数。
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
这5个函数的作用就是向Vue的原型中挂载方法。
而我们今天要详细介绍的,正是挂载在Vue原型上的方法,即Vue.prototype上的方法。
事件相关的实例方法
与事件相关的实例方法有4个,分别是vm.$on、vm.$once、vm.$off、vm.$emit。这4个方法是在eventsMixin中挂载到Vue构造函数的prototype属性中的。下面分别介绍一下它们的内部实现原理。
vm.$on
vm.$on用来监听当前实例上的自定义事件。事件的实现方式,其实就是在注册事件时将回调函数收集起来,在触发事件时将收集起来的回调函数依次调用即可。Vue.js的实现方式也是如此,其代码如下:
/**
* 参数
* {string | Array<string>} event
* { Function } callback
*/
Vue.prototype.$on = function (event, fn) {
var vm = this;
if (Array.isArray(event)) {
for (var i = 0, l = event.length; i < l; i++) {
this.$on(event[i], fn);
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn);
}
return vm
};
在上面代码中,当event参数为数组时,需要遍历数组,将其中的每一项递归调用vm.$on,使回调可以被注册到数组中每项事件名所指定的事件列表中。当event参数不为数组时,就像事件列表中添加回调。
vm. _events是一个对象,用来存储事件。我们在执行new Vue()时,Vue会执行this.init 方法进行一系列初始化操作,其中,就会在Vue.js的实例上创建一个_events属性,用来存储事件,其代码如下:
vm.__events = Object.create(null)
vm.$off
vm.$off用来移除自定义事件监听器。如果没有提供参数,则移除所有的事件监听器;如果只提供了事件,则移除该事件所有的监听器;如果同时提供了事件与回调,则只移除这个回调的监听器。其具体实现代码如下:
/**
* 参数
* {string | Array<string>} event
* { Function } callback
*/
Vue.prototype.$off = function (event, fn) {
var vm = this;
// 移除所有事件的监听器
if (!arguments.length) {
vm._events = Object.create(null);
return vm
}
// event支持数据
if (Array.isArray(event)) {
for (var i = 0, l = event.length; i < l; i++) {
this.$off(event[i], fn);
}
return vm
}
// 移除该事件的所有监听器
var cbs = vm._events[event];
if (!cbs) {
return vm
}
if (arguments.length === 1) {
vm._events[event] = null;
return vm
}
// specific handler
var cb;
var i = cbs.length;
while (i--) {
cb = cbs[i];
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1);
break
}
}
return vm
};
我们可以看到上面有一个判断条件,当arguments.length为0时,说明没有任何参数,这时就需要移除所有的事件监听器,因此我们重置了vm._envents属性。
由于vm.$off的第一个参数event支持数组,所以记下来需要处理event参数为数组的情况。也就是将数组遍历一遍,然后数组中的每一项调用vm.$off即可。
再接下来,我们需要处理只提供了事件名的情况。如果只提供了事件名,则移除该事件所有的监听器。即将this._event重置为空即可。
最后,我们来处理最后一种情况:如果同时提供了事件与回调,那么只移除这个回调的监听器。即将参数中提供的事件名从vm._events上取出列表,然后从列表中找到与参数中提供的回调函数相同的那个函数,并将它从列表中移除。
vm.$once
vm.$once监听一个自定义事件,但是只触发一次,在第一次触发之后移除监听器。可见,vm.$once和vm.$on的区别是前者只能被触发一次,所以实现这个功能的一个思路是:在vm.$once中调用vm.$on来实现监听自定义事件的功能,当自定义事件触发后会执行拦截器,将监听从事件列表中移除。具体代码如下:
/**
* 参数
* {string | Array<string>} event
* { Function } callback
*/
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
};
当自定义事件被触发时,会先执行函数on,然后手动执行函数fn,并将参数arguments传递给函数fn,这就实现了vm.$once的功能。
vm.$emit
vm.$emit用来触发当前实例上的事件。前面我们介绍过,所有的事件监听器回调函数都会存储在vm._events中,所以触发事件的实现思路是使用事件名从vm._events中取出对应的事件监听器回调函数列表,然后依次执行列表中的监听器回调并将参数传递给监听器回调。具体代码如下:
/**
* 参数
* { string } event
* { ...args }
*/
Vue.prototype.$emit = function (event) {
var vm = this;
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) {
}
}
}
return vm
};
这里我们使用event从vm._events中取出事件监听器回调函数列表,并将其赋值给变量cbs。如果cbs存在,则循环它,依次调用每一个监听器回调并将所有参数传给监听器回调。
toArray的作用是将类似数组的数据转换成真正的数组,它的第二个参数是起始位置。
生命周期相关的实例方法
与生命周期相关的实例方法有4个,分别是vm.$mount、vm.$forceUpdate、vm.$nextTick和vm.$destroy。其中有两个方法是从lifecycleMixin中挂载到Vue构造函数的原型上的,分别是vm.$forceUpdate和vm.$destroy。vm.$nextTick方法是从renderMixin中挂载到Vue构造函数的原型上的。而vm.$mount方法则是在跨平台的代码中挂载到Vue构造函数的原型上的。
vm.$forceUpdate
vm.$forceUpdate的作用是迫使Vue.js实例重新渲染。即执行实例watcher的update方法,就可以让实例重新渲染,其代码如下:
Vue.prototype.$forceUpdate = function () {
var vm = this;
if (vm._watcher) {
vm._watcher.update();
}
};
vm._watcher就是Vue.js实例的watcher,Vue.js的自动渲染通过变化侦测来侦测数据,即当数据发生变化时,Vue.js实例重新渲染。而vm.$forceUpdate是手动通知Vue.js实例重新渲染。
vm.$destroy
vm.$destroy的作用是完全销毁一个实例,它会清理该实例与其他实例的连接,并解绑其全部指令及监听器,同时会触发beforeDestroy和destroyed的钩子函数。
这个方法并不是很常用,大部分场景下并需要销毁组件,只需要使用v-if或者v-for等指令已数据驱动的方式控制子组件的生命周期即可。下面我们来看一看vm.$destory的实现原理
Vue.prototype.$destroy = function () {
var vm = this;
// 防止vm.$destroy被反复执行
// 对属性_isBeingDestroyed进行判断,如果它为true,说明Vue.js实例正在被销毁
if (vm._isBeingDestroyed) {
return
}
// 调用callHook函数触发beforeDestroy的钩子函数
callHook(vm, 'beforeDestroy');
vm._isBeingDestroyed = true;
// 删除自己与父级之间的连接
var parent = vm.$parent;
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
//如果当前实例有父级,同时父级没有被销毁且不是抽象组件,那么将自己从父级的子列表中删除
remove(parent.$children, vm);
}
// 销毁实例自身的watcher
if (vm._watcher) {
vm._watcher.teardown();
}
// 销毁用户使用vm.$watch所创建的watcher实例
var i = vm._watchers.length;
while (i--) {
vm._watchers[i].teardown();
}
// 添加_isDestroyed属性来表示Vue.js实例已被销毁。
vm._isDestroyed = true;
// 在vnode树上触发destroy钩子函数解绑指令
vm.__patch__(vm._vnode, null);
// 触发destroyed钩子函数
callHook(vm, 'destroyed');
// 最后移除实例上的所有事件监听器
vm.$off();
};
从上述代码可以看出,vm.$destroy的实现需要经过以下步骤:
- 需要在销毁组件之前,触发beforeDestroy钩子函数。
- 需要清理当前组件与父组件之间的连接。
- 销毁实例上的所有watcher,也就是将实例上所有的依赖追踪断掉。
- 触发destroyed钩子函数
- 移除实例上的所有事件监听器
vm.$nextTick
vm.$nextTick接收一个回调函数作为参数,它的作用是将回调延迟到下次DOM更新周期之后执行。它与全局方法Vue.nextTick一样,不同的是回调this自动绑定到调用它的实例上。
因为vm.$nextTick和Vue.nextTick是相同的,所以nextTick的具体实现并不是在Vue原型上的$nextTick方法中,而是抽象成了nextTick方法供两个方法共用。代码如下:
Vue.prototype.$nextTick = function (fn) {
return nextTick(fn, this)
};
可以看到,vm.$nextTick的具体实现在nextTick中,所以接下来我们详细介绍nextTick方法的实现方式。
var nextTick = (function () {
// callbacks用来存储所有需要执行的回调函数
var callbacks = [];
pending用来标志是否正在执行回调函数
var pending = false;
// 用来触发执行回调函数
var timerFunc;
// nextTickHandler函数用来执行callbacks里存储的所有回调函数
function nextTickHandler () {
pending = false;
var copies = callbacks.slice(0);
callbacks.length = 0;
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
//接下来是将触发方式赋值给timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// 先判断是否原生支持promise,如果支持,则利用promise来触发执行回调函数
var p = Promise.resolve();
var logError = function (err) { console.error(err); };
timerFunc = function () {
p.then(nextTickHandler).catch(logError);
if (isIOS) { setTimeout(noop); }
};
} else if (typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// 否则,如果支持MutationObserver,则实例化一个观察者对象,观察文本节点发生变化时,触发执行所有回调函数。
var counter = 1;
var observer = new MutationObserver(nextTickHandler);
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true
});
timerFunc = function () {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
} else {
//如果都不支持,则利用setTimeout设置延时为0。
timerFunc = function () {
setTimeout(nextTickHandler, 0);
};
}
// 最后是queueNextTick函数。因为nextTick是一个即时函数,所以queueNextTick函数是返回的函数,接受用户传入的参数,用来往callbacks里存入回调函数。
return function queueNextTick (cb, ctx) {
var _resolve;
callbacks.push(function () {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
timerFunc();
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve, reject) {
_resolve = resolve;
})
}
}
})();
从上述代码可知,nextTick的关键在于timeFunc(),该函数起到延迟执行的作用。
从上面的介绍,可以得知timeFunc()一共有三种实现方式。
PromiseMutationObserversetTimeout
其中Promise和setTimeout很好理解,是一个异步任务,会在同步任务以及更新DOM的异步任务之后回调具体函数。
而MutationObserver是HTML5中的新API,是个用来监视DOM变动的接口。他能监听一个DOM对象上发生的子节点删除、属性修改、文本内容修改等等。
vm.$mount
我们并不常用这个方法,其原因是如果在实例化Vue.js时设置了el选项,会自动把Vue.js实例挂载到DOM元素上,但理解这个方法却非常重要,因为无论我们在实例化Vue.js时是否设置el选项,想让Vue.js实例具有关联的DOM元素,只有使用vm.$mount方法这一种途径。
$mount方法在多个文件中被定义,如:
- src/platform/web/entry-runtime-with-compiler.js (构建时版本)
- src/platform/web/runtime/index.js(运行时版本)
- src/platform/weex/runtime/index.js(跨平台开发)
之所以有多个地方,是因为$mount实现是和平台、构建方式都相关的下面,我们选择compiler构建时版本分析
/*把原本不带编译的$mount方法保存下来,在最后会调用。*/
const mount = Vue.prototype.$mount
/*挂载组件,带模板编译*/
Vue.prototype.$mount = function () {
// query方法,实际上是对el参数做了一个转化,el可能是string 或者 element。如果是string,将返回document.querySelector(el)
el = el && query(el)
// 对 el 做了限制,Vue 不能挂载到 body、HTML 这样的跟节点上,因为它会替换掉这些元素
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
/*处理模板templete,编译成render函数,render不存在的时候才会编译template,否则优先使用render*/
if (!options.render) { // render函数不存在
let template = options.template
// 如果存在template配置项,根据特点进行不同判断处理
if (template) {
// 当template是字符串的时候
if (typeof template === 'string') {
if (template.charAt(0) === '#') {//template可能是"#xx",那么根据id获取element内容
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {//如果template存在nodeType(DOM节点),那么获取template.innerHTML 内容
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {//如果template配置项不存在template,但是存在el,那么根据el获取对应的element内容
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
// 经过上面的处理,将获取的template做为参数调用compileToFunctions方法
// compileToFunctions方法会返回render函数方法,render方法会保存到vm.$options下面
// 这里会有render以及staticRenderFns两个返回,这是vue的编译时优化,static静态不需要在VNode更新时进行patch,优化性能
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
// 调用const mount = Vue.prototype.$mount保存下来的不带编译的mount
return mount.call(this, el, hydrating)
}
从上述代码我们可以看出做了以下几件事
- 由于
el参数有两种类型,可能是string或者element,调用query方法,统一转化为Element类型 - 如果没有手写
render函数, 那么先获取template内容。再将template做为参数,调用compileToFunctions方法,返回render函数。 - 最后调用
mount.call,这个方法实际上会调用src/platform/web/runtime/index.js的mount方法,如下:
Vue.prototype.$mount = function (){
/*获取DOM实例对象*/
el = el && inBrowser ? query(el) : undefined
/*挂载组件*/
return mountComponent(this, el, hydrating)
}
数据相关的实例方法
与数据相关的实例方法有3个,分别是vm.$watch、vm.$set、vm.$delete。
其中,vm.$watch用于观察一个表达式或函数在Vue实例上的变化。回调函数调用时,会从参数得到新数据(new value) 和旧数据(old value)。vm.$watch其实是对Watcher的一种封装,通过Watcher完全可以实现vm.$watch的功能,但是vm.$watch中的参数deep和immediate是Watcher中所没有的。
vm.$set用来向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新属性,因为对于 Vue 来说,当我们向object数据里添加一对新的key/value,Vue是无法观测到的;而对于Array型数据,当我们通过数组下标修改数组中的数据时,Vue也是是无法观测到的。
vm.$delete用来删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到属性被删除的限制。
vm.$watch、vm.$set、vm.$delete这三个实例方法的内部实现原理已经在《【Vue2深度学习】变化侦测篇-变化侦测相关的API》一文中详细介绍了,这里就不再赘述了.