1、 o n 与 on与 on与emit
(1)使用方式
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
<div id="app">
<button @click="clickHandler">按钮</button>
<p>{{message}}</p>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
},
methods: {
clickHandler: function () {
this.$emit('test','触发test事件并传值');
}
},
mounted() {
// 监听test事件
this.$on('test', function(value){
this.message = value
})
}
})
</script>
</body>
</html>
$on( event, callback )
-
参数:
{string | Array<string>} event
(数组只在 2.2.0+ 中支持),监听的事件{Function} callback
,监听的事件被触发后,执行的回调
-
用法:
监听当前实例上的自定义事件。事件可以由
vm.$emit
触发。回调函数会接收所有传入事件触发函数的额外参数。
$emit( event, […args] )
-
参数:
{string} event
,触发的事件[...args]
,传参
触发当前实例上的事件。附加参数都会传给监听器($on)回调。
(2)源码分析
// /src/core/instance/event.ts
export function eventsMixin(Vue: typeof Component) {
const hookRE = /^hook:/
// 发布订阅模式
Vue.prototype.$on = function (
event: string | Array<string>,
fn: Function
): Component {
const vm: Component = this
// 如果event是字符串数组,则遍历数组,每个元素执行一次on方法(递归,将字符串数组转化为字符串形式)
if (isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
// 如果事件中心有event事件,则直接push处理函数,如果没有event事件,则先注册事件
(vm._events[event] || (vm._events[event] = [])).push(fn)
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
// 触发event事件
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
if (__DEV__) {
const 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}".`
)
}
}
// cbs-callbacks,取出调度中心中该事件的处理函数
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
// 执行每一个回调函数
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
}
o n 、 on、 on、emit的设计用到了发布-订阅模式,_events为当前实例的任务调度中心,执行 o n 在调度中心注册事件,并将回调函数写入;执行 on在调度中心注册事件,并将回调函数写入;执行 on在调度中心注册事件,并将回调函数写入;执行emit触发调度中心的事件,取出被触发事件的回调函数依次执行。
(3)简易仿写
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
<div id="app">
<button @click="clickHandler">按钮</button>
<p>{{message}}</p>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!',
_events:[]
},
methods: {
clickHandler: function () {
this.myEmit('test','自定义触发test事件并传值');
},
//仿写$on
myOn: function(event, callback) {
const vm = this
if(event instanceof Array){
for(let i=0;i<event.length;i++){
vm.myOn(event[i], callback)
}
}else{
(vm._events[event]||(vm._events[event]=[])).push(callback)
}
return vm
},
//仿写$emit
myEmit: function(event, value) {
const vm = this
let cbs = vm._events[event]
if(cbs){
for(let i=0;i<cbs.length;i++){
cbs[i](value)
}
}
return vm
}
},
mounted() {
// 监听test事件
let that=this
this.myOn('test', function(value) {
that.message = value
})
}
})
</script>
</body>
</html>
2、$mount
// src/platforms/web/runtime-with-compiler.ts
//先用一个变量保存vue原型上的$mount
const mount = Vue.prototype.$mount //src/platforms/web/runtime/index.ts
//重新定义原型上的$mount
Vue.prototype.$mount = function (
el?: string | Element,
//此参数与服务端渲染有关
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
// 挂载点不能是body或者html,原因是vue会生成dom对象替换挂载点,如果是body,html,上面的信息会丢失(meta、link、script等)
if (el === document.body || el === document.documentElement) {
__DEV__ &&
warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
/**
* eg:
* new Vue({
render: h => h(App)
}).$mount('#app')
*/
/**
* eg:
* export default {
name: 'HelloWorld',
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
},
template: `<div class="hello">{{ msg }}</div>`
}
*/
// 判断options中是否有render,如果有,则直接return,否则走if逻辑
if (!options.render) {
let template = options.template
// 处理template或者el,都转换为模版字符串形式
if (template) {
if (typeof template === 'string') {
// string为id形式
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (__DEV__ && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (__DEV__) {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// @ts-expect-error
template = getOuterHTML(el)
}
// 用处理好的template去生成render
if (template) {
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
mark('compile')
}
//调用compileToFunctions函数,生成render
const { render, staticRenderFns } = compileToFunctions(
template,
{
outputSourceRange: __DEV__,
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
},
this
)
//生成的render挂载到options上
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
//调用最开始保存的mount
return mount.call(this, el, hydrating)
}
//src/platforms/web/runtime/index.ts
// public mount method
Vue.prototype.$mount = function (
// el,挂载点,可以是sting类型或者DOM对象
el?: string | Element,
// 服务端渲染会用到的参数,浏览器环境下不需要此参数
hydrating?: boolean
): Component {
// 如果el是字符串,并且在浏览器环境下,调用query函数,将el转换为DOM对象
el = el && inBrowser ? query(el) : undefined
// 调用mountComponent函数
return mountComponent(this, el, hydrating)
}
//core/instance/lifecycle
export function mountComponent(
vm: Component,
el: Element | null | undefined,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
// @ts-expect-error invalid type
//createEmptyVNode创建注释节点
vm.$options.render = createEmptyVNode
if (__DEV__) {
/* istanbul ignore if */
if (
(vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el ||
el
) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
//触发beforeMount钩子函数
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
//调用_render生成虚拟Node,调用_update更新虚拟Node
//if中的逻辑是打开浏览器性能分析时做前端埋点,主要是看else
if (__DEV__ && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
const watcherOptions: WatcherOptions = {
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}
if (__DEV__) {
watcherOptions.onTrack = e => callHook(vm, 'renderTracked', [e])
watcherOptions.onTrigger = e => callHook(vm, 'renderTriggered', [e])
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(
vm,
updateComponent,
noop,
watcherOptions,
true /* isRenderWatcher */
)
hydrating = false
// flush buffer for flush: "pre" watchers queued in setup()
const preWatchers = vm._preWatchers
if (preWatchers) {
for (let i = 0; i < preWatchers.length; i++) {
preWatchers[i].run()
}
}
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}