事件相关实例方法
-
vm.$on用法:
vm.$on(event: {String | Array<String>}, callback: Function)Vue.prototype.$on = function (event, fn) { const vm = this if (Array.isArray(event)) { for (let i = 0; i < event.length; i++) { this.$on(event[i], fn) } } else { (vm._events[event] || (vm._events[event] = [])).push(fn) } return vm } -
vm.$off用法:
vm.$off(event: {String | Array<String>}, callback: Function)如果提供了没有提供参数,则移除所有事件监听器;如果只提供了事件,则移除该事件绑定的所有监听器;如果同时提供了两个参数,则只移除这个回调的监听器。
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; i < event.length; i++) { this.$off(event[i], fn) } return vm } const cbs = vm._events[event] if (!cbs) { return vm } if (arguments.length === 1) { vm._events[event] = null return vm } if (fn) { let i = cbs.length while (i--) { cb = cbs[i] if (cb === fn || cb === fn.fn) { cbs.splice(i, 1) break } } } return vm } -
vm.$once用法:
vm.$once(event: {String | Array<String>}, callback: Function)监听一个自定义事件,但是只触发一次,第一次触发后移除监听器。
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 }这里对函数
fn做了一层拦截,在函数执行前就先销毁了监听器,实现了需求。除此之外还需要注意on.fn = fn,由于我们监听的函数on与实际这一段代码正是我们在vm.$off中添加了if (cb === fn || cb === fn.fn)的原因。 -
vm.$emit用法:
vm.$emit(event: {String | Array<String>}, [...args])触发当前实例上的事件,附加参数都会传给监听器回调。实现起来相对简单,找出当前事件绑定的所有回调函数,传入参数并执行。
Vue.prototype.$emit = function (event) { const vm = this let cbs = vm._events[event] if (cbs) { const args = toArray(arguments, 1) for (let 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 }
生命周期相关实例方法
与生命周期有关实例方法有四个:vm.$mount, vm.$forceUpdate, vm.$nextTick, vm.$destroy。
-
vm.$forceUpdatevm.$forceUpdate就是迫使实例重新渲染,但只影响实例本身以及插入插槽内容的子组件。所以只需要执行实例watcher的update方法即可。前面提到过每个组件内部都有一个watcher,当它的状态改变时,就会通知组件内部使用虚拟 DOM 进行重新渲染操作。Vue.prototype.$forceUpdate = function () { const vm = this if (vm._watcher) { vm._watcher.update() } } -
vm.$destroyvm.$destroy是用来销毁一个实例,清理该实例与其他实例的链接,并解绑全部指令与监听器,同时触发beforeDestroy和destroyed钩子函数。但由于大部分场景下都能通过v-if或者v-for使用数据驱动的方式来控制子组件的生命周期,实用性并不是很高。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.abstact) { remove(parent.$children, vm) } // 从 watcher 监听的所有状态列表中移除 watcher // 不仅要移除组件自身的 watcher 实例,还要移除 vm.$watch 方法生成的实例 if (vm._watcher) { vm._watcher.teardown() } let i = vm._watchers.length while (i--) { vm._watchers[i].teardown() } vm._isDestroyed = true // 在 vnode 树上触发 destroy 钩子函数 vm.__patch__(vm.node, null) // 触发 destroyed 钩子函数 callHook(vm, 'destroyed') vm.$off() } function remove (arr, item) { if (arr.length) { const index = arr.indexOf(item) if (index > -1) { return arr.splice(index, 1) } } } -
vm.$nextTickvm.$nextTick是在实际开发中经常用到的一个 API,比如当更新组件数据后,需要对新 DOM 做一些操作,但是此时是获取不到更新后的 DOM ,因为还没有重新渲染,这种时候就要用到vm.$nextTick。vm.$nextTick接受一个回调函数作为参数,并将回调函数的调用延迟到下次 DOM 更新周期之后执行。思考一个问题,为什么 Vue 要引入异步更新队列?
我们知道每个组件内部有一个
watcher实例,组件内部任何一个状态的改变都会通知其更新,那么如果在一轮事件循环中,有两个状态改变了,必然会导致watcher收到两次通知,从而渲染两次,导致资源的浪费。而引入了异步更新队列后,将一个事件循环内收到通知的watcher实例缓存起来,并判断是否已经存在相同的实例,只有不存在才加入队列当中。在下一个时间循环中才触发watcher实例的渲染过程并清空队列。有一点需要注意的是,更新 DOM 的回调和
vm.$nextTick注册的回调都是推入到微任务队列中,所以可能会存在顺序问题。如果回调中要操作更新后的 DOM ,注意将vm.$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 let useMacroTask = false // 根据执行环境设置不同的宏任务执行函数 if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { macroTimerFunc = () => { setImmediate(flushCallbacks) } } else if (typeof MessageChannel !== 'undefined' && ( isNative(MessageChannel) || MessageChannel.toString() === '[object MessageChannelConstrcutor]' )) { const channel = new MessageChannel() const port = channel.port2 channel.port1.onmessage = flushCallbacks macroTimerFunc = () => { port.postMessage(1) } } else { macroTimerFunc = setTimeout(flushCallbacks, 0); } // 选择微任务执行函数 if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() microTimerFunc = () => { p.then(flushCallbacks) } } else { microTimerFunc = macroTimerFunc } export function widthMacroTask (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() } } // 如果在不存在回调且环境支持 Promise 的情况下,返回一个 Promise if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } } -
vm.$mount用法:
vm.$mount([elementOrSelector])如果在 Vue 实例初始化时没有传入
el选项,那么就必须手动挂载;如果已经传入了,Vue 则会调用这个方法自动挂载。所以清楚这个 API 的实现对我们理解 Vue 实例是如何与 DOM 关联上是很有帮助的。由于 Vue 存在不同构建版本,而不同版本下的
vm.$mount的表现都不一样,关键在于当前版本是否有编译器,编译器的实现原理在上一篇文章都已经提过了。对于有编译器的版本,会先检查template选项或是el选项提供的模板是否已经转换成渲染函数。如果没有则进入编译,将模板转换为渲染函数之后再进入挂载与渲染的过程。而只包含运行时版本的
vm.$mount没有编译步骤,默认已经存在渲染函数,如果不存在,会设置一个返回值的空节点 VNode 的默认函数。-
完整版的
vm.$mount只看完整版与运行时的差异部分:
- 先通过函数劫持,为运行时版本的
vm.$mount添加功能
const mount = Vue.prototype.$mount Vue.prototype.$mount = function (el) { // 兼容选择器和 DOM 元素两种写法 el = el && query(el) // dosomething return mount.call(this, el) }- 判断是否存在渲染函数,不存在再将模板编译,先将
template解析出来:
const mount = Vue.prototype.$mount Vue.prototype.$mount = function (el) { // 兼容选择器和 DOM 元素两种写法 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 { return this } } else if (el) { template = getOuterHTML(el) } } return mount.call(this, el) }- 模板获取之后,就要对其进行编译处理:
const mount = Vue.prototype.$mount Vue.prototype.$mount = function (el) { // 兼容选择器和 DOM 元素两种写法 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 { return this } } else if (el) { template = getOuterHTML(el) } } if (template) { const {render} = compileToFunctions( template, {...}, this ) options.render = render } return mount.call(this, el) } - 先通过函数劫持,为运行时版本的
-
运行时版本的
vm.$mount
运行时版本的
vm.$mount主要完成了下面几件事;触发生命周期钩子;创建组件内的vm._watcher;挂载组件。Vue.prototype.$mount = function (el) { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el) } export function mountComponent (vm, el) { if (!vm.$options.render) { // 开发环境发出警告 vm.$options.render = createEmptyNode() } callHook(vm, 'beforeMount') vm._watcher = new Watcher(vm, () => { vm._update(vm._render()) }, noop) callHook(vm, 'mounted') return vm }Watcher的实现在本系列最早的几篇文章中就分析过了,这里再来简单回顾一下。我们知道一旦 Vue 实例挂载之后,每当内部状态改变,都会触发渲染操作。而挂载的关键就在于new Watcher这段代码。当
Watcher第二个参数传入函数,在读取一个Watcher实例时,会执行这个函数,进而触发函数中访问到的所有响应式数据的getter,从而将当前的Watcher实例添加到各数据的依赖列表当中。当数据发生变化时,Watcher会得到通知,并再次执行传入的第二个参数,这就是持续渲染的原因。 -
本系列文章均是深入浅出 Vue.js的学习笔记,有兴趣的小伙伴可以去看书哈。