1.双向绑定
<!DOCTYPE html>
<html>
<head>
<title>Vue事件处理</title>
</head>
<body>
<div id="demo">
<h1>双向绑定机制</h1>
<!--表单控件绑定-->
<input type="text" v-model="foo">
<!--自定义事件-->
<comp v-model="foo"></comp>
</div>
<script src="../../dist/vue.js"></script>
<script>
// 声明自定义组件
Vue.component('comp', {
template: `
<input type="text" :value="$attrs.value"
@input="$emit('input', $event.target.value)">
`
})
// 创建实例
const app = new Vue({
el: '#demo',
data: { foo: 'foo' }
});
console.log(app.$options.render);
</script>
</body>
</html>
生成的渲染函数
(function anonymous(
) {
with (this) {
return _c('div', { attrs: { "id": "demo" } },
[_c('h1', [_v("双向绑定机制")]),
_v(" "),
_c('input',
{
directives: [{ name: "model", rawName: "v-model", value: (foo), expression: "foo" }],
attrs: { "type": "text" },
domProps: { "value": (foo) },
on: { "input": function ($event) {
if ($event.target.composing) return;
foo = $event.target.value
}
}
}
)
, _v(" "),
_c('comp', {
model: {
value: (foo),
callback: function ($$v) { foo = $$v },
expression: "foo"
}
})
], 1)
}
})
1.1普通控件绑定
//通过 domProps 相关信息更新控件
domProps: { "value": (foo) },
//src/platforms/web/runtime/modules/dom-props.js
function updateDOMProps (oldVnode: VNodeWithData, vnode: VNodeWithData) {
if (isUndef(oldVnode.data.domProps) && isUndef(vnode.data.domProps)) {
return
}
let key, cur
const elm: any = vnode.elm
const oldProps = oldVnode.data.domProps || {}
let props = vnode.data.domProps || {}
// clone observed objects, as the user probably wants to mutate it
if (isDef(props.__ob__)) {
props = vnode.data.domProps = extend({}, props)
}
for (key in oldProps) {
if (!(key in props)) {
elm[key] = ''
}
}
for (key in props) {
cur = props[key]
// ignore children if the node has textContent or innerHTML,
// as these will throw away existing DOM nodes and cause removal errors
// on subsequent patches (#3360)
if (key === 'textContent' || key === 'innerHTML') {
if (vnode.children) vnode.children.length = 0
if (cur === oldProps[key]) continue
// #6601 work around Chrome version <= 55 bug where single textNode
// replaced by innerHTML/textContent retains its parentNode property
if (elm.childNodes.length === 1) {
elm.removeChild(elm.childNodes[0])
}
}
//这里通过参数key为value 设置input.value = ''
if (key === 'value' && elm.tagName !== 'PROGRESS') {
// store value as _value as well since
// non-string values will be stringified
elm._value = cur
// avoid resetting cursor position when value is the same
const strCur = isUndef(cur) ? '' : String(cur)
if (shouldUpdateValue(elm, strCur)) {
elm.value = strCur //这里进行赋值
}
} else if (key === 'innerHTML' && isSVG(elm.tagName) && isUndef(elm.innerHTML)) {
// IE doesn't support innerHTML for SVG elements
svgContainer = svgContainer || document.createElement('div')
svgContainer.innerHTML = `<svg>${cur}</svg>`
const svg = svgContainer.firstChild
while (elm.firstChild) {
elm.removeChild(elm.firstChild)
}
while (svg.firstChild) {
elm.appendChild(svg.firstChild)
}
} else if (
// skip the update if old and new VDOM state is the same.
// `value` is handled separately because the DOM value may be temporarily
// out of sync with VDOM state due to focus, composition and modifiers.
// This #4521 by skipping the unnecessary `checked` update.
cur !== oldProps[key]
) {
// some property updates can throw
// e.g. `value` on <progress> w/ non-finite value
try {
elm[key] = cur
} catch (e) {}
}
}
}
- directives 解析方式
- src/platforms/web/compiler/directives/model.js
- 平台特有的信息写在 web平台上
// directives: [{ name: "model", rawName: "v-model", value: (foo), expression: "foo" }],
export default function model (
el: ASTElement,
dir: ASTDirective,
_warn: Function
): ?boolean {
warn = _warn
const value = dir.value
const modifiers = dir.modifiers
const tag = el.tag
const type = el.attrsMap.type
if (process.env.NODE_ENV !== 'production') {
// inputs with type="file" are read only and setting the input's
// value will throw an error.
if (tag === 'input' && type === 'file') {
warn(
`<${el.tag} v-model="${value}" type="file">:\n` +
`File inputs are read only. Use a v-on:change listener instead.`,
el.rawAttrsMap['v-model']
)
}
}
//这里做平台上不同控件的判断
if (el.component) {
genComponentModel(el, value, modifiers)
// component v-model doesn't need extra runtime
return false
} else if (tag === 'select') {
genSelect(el, value, modifiers)
} else if (tag === 'input' && type === 'checkbox') {
genCheckboxModel(el, value, modifiers)
} else if (tag === 'input' && type === 'radio') {
genRadioModel(el, value, modifiers)
} else if (tag === 'input' || tag === 'textarea') {
genDefaultModel(el, value, modifiers)
} else if (!config.isReservedTag(tag)) {
genComponentModel(el, value, modifiers)
// component v-model doesn't need extra runtime
return false
} else if (process.env.NODE_ENV !== 'production') {
warn(
`<${el.tag} v-model="${value}">: ` +
`v-model is not supported on this element type. ` +
'If you are working with contenteditable, it\'s recommended to ' +
'wrap a library dedicated for that purpose inside a custom component.',
el.rawAttrsMap['v-model']
)
}
// ensure runtime directive metadata
return true
}
1.2自定义组件绑定
虽然是自定义,但最终还是要走4.1普通控件绑定 进行绑定,多了个transformModel处理方法
//src/core/vdom/create-element.js
_createElement{
createComponent()
}
//src/core/vdom/create-component.js
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
const baseCtor = context.$options._base
...
data = data || {}
// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor)
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)//这里做v-model操作,处理data.on 信息给下面使用
}
// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// functional component
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
//这里做data.on 的处理
const listeners = data.on //开始事件监听处理
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn
if (isTrue(Ctor.options.abstract)) {
// abstract components do not keep anything
// other than props & listeners & slot
// work around flow
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// install component management hooks onto the placeholder node
installComponentHooks(data)
// return a placeholder vnode
const name = Ctor.options.name || tag
//最终在这里传入 listeners 进行渲染成虚拟dom
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
// Weex specific: invoke recycle-list optimized @render function for
// extracting cell-slot template.
// https://github.com/Hanks10100/weex-native-directive/tree/master/component
/* istanbul ignore if */
if (__WEEX__ && isRecyclableComponent(vnode)) {
return renderRecyclableComponentTemplate(vnode)
}
return vnode
...
}
// transform component v-model info (value and callback) into
// prop and event handler respectively.
function transformModel (options, data: any) {
//系统模式使用 value和input事件,如果用户自定义了 优先取用户的属性和事件
const prop = (options.model && options.model.prop) || 'value'
const event = (options.model && options.model.event) || 'input'
;(data.attrs || (data.attrs = {}))[prop] = data.model.value
const on = data.on || (data.on = {})
const existing = on[event]
const callback = data.model.callback
if (isDef(existing)) {//如果用户已经定义了 @input ="xxx" 相同的事件,则把事件存储在数组里
if (
Array.isArray(existing)
? existing.indexOf(callback) === -1 //如是数组并且不是系统自带的input事件
: existing !== callback //如果不是数组,并且与新增方法的不一致
) {
on[event] = [callback].concat(existing)//保存的是数组
}
} else {
on[event] = callback //正常保存系统自带一个input方法
}
}
2.事件
2.1例子&生成render
<!DOCTYPE html>
<html>
<head>
<title>Vue事件处理</title>
</head>
<body>
<div id="demo">
<h1>事件处理机制</h1>
<!--普通事件-->
<p @click="onClick">this is p</p>
<!--自定义事件-->
<comp @myclick="onMyClick"></comp>
</div>
<script src="../../dist/vue.js"></script>
<script>
// 声明自定义组件
Vue.component('comp', {
template: `
<div @click="onClick">this is comp</div>
`,
methods: {
onClick() {
this.$emit('myclick')
}
}
})
// 创建实例
const app = new Vue({
el: '#demo',
methods: {
onClick() {
console.log('普通事件');
},
onMyClick() {
console.log('自定义事件');
}
},
});
console.log(app.$options.render);
</script>
</body>
</html>
//render生成的js内容
(function anonymous(
) {
with(this){
return _c('div',{attrs:{"id":"demo"}},
[
_c('h1',[_v("事件处理机制")]),_v(" "),
_c('p',{on:{"click":onClick}},[_v("this is p")]), _v(" "),
_c('comp',{on:{"myclick":onMyClick}})
]
,1)
}
})
2.2事件类型
所有事件都是基于已经构建生成的dom,所以需要在运行时动态添加
2.2.1 原生事件
注意事件多数是在path动态创建的,依赖于invokeCreateHooks触发绑定事件
/src/platforms/web/runtime/modules/events.js 的updateDOMListeners
2.2.1.1执行流程
Vue -> Vue._init -> Vue.$mount -> mountComponent -> watcher -> get updateComponent -> Vue._update -> patch -> createElm -> createChild -> invokeCreateHooks -> updateDOMListeners -> updateListeners ->
//src/core/vdom/patch.js
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
creatingElmInVPre++
}
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
//处理原生事件
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
// in Weex, the default insertion order is parent-first.
// List items can be optimized to use children-first insertion
// with append="tree".
const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
if (!appendAsTree) {
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
createChildren(vnode, children, insertedVnodeQueue)
if (appendAsTree) {
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
} else {
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) { //注意这里的data是包含事件on的内容,不单单是指vue里面的data属性
//如果有data定义就会有,对应的事件监听操作
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
function invokeCreateHooks (vnode, insertedVnodeQueue) {
//执行所有回调 这里包含下面的updateDOMListeners
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
//src/platforms/web/runtime/modules/events.js
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
return
}
const on = vnode.data.on || {}
const oldOn = oldVnode.data.on || {}
target = vnode.elm
normalizeEvents(on)
updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
target = undefined
}
function add (
name: string,
handler: Function,
capture: boolean,
passive: boolean
) {
if (useMicrotaskFix) {
const attachedTimestamp = currentFlushTimestamp
const original = handler
handler = original._wrapper = function (e) {
if (
// no bubbling, should always fire.
// this is just a safety net in case event.timeStamp is unreliable in
// certain weird environments...
e.target === e.currentTarget ||
// event is fired after handler attachment
e.timeStamp >= attachedTimestamp ||
// bail for environments that have buggy event.timeStamp implementations
// #9462 iOS 9 bug: event.timeStamp is 0 after history.pushState
// #9681 QtWebEngine event.timeStamp is negative value
e.timeStamp <= 0 ||
// #9448 bail if event is fired in another document in a multi-page
// electron/nw.js app, since event.timeStamp will be using a different
// starting reference
e.target.ownerDocument !== document
) {
return original.apply(this, arguments)
}
}
}
//添加事件监听
target.addEventListener(
name,
handler,
supportsPassive
? { capture, passive }
: capture
)
}
export default {
create: updateDOMListeners,
update: updateDOMListeners
}
//src/core/vdom/helpers/update-listeners.js
export function updateListeners (
on: Object,
oldOn: Object,
add: Function,
remove: Function,
createOnceHandler: Function,
vm: Component
) {
let name, def, cur, old, event
for (name in on) {
def = cur = on[name]
old = oldOn[name]
event = normalizeEvent(name)
/* istanbul ignore if */
if (__WEEX__ && isPlainObject(def)) {
cur = def.handler
event.params = def.params
}
if (isUndef(cur)) {
process.env.NODE_ENV !== 'production' && warn(
`Invalid handler for event "${event.name}": got ` + String(cur),
vm
)
} else if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur, vm)
}
if (isTrue(event.once)) {
cur = on[name] = createOnceHandler(event.name, cur, event.capture)
}
//关键添加事件地方
add(event.name, cur, event.capture, event.passive, event.params)
} else if (cur !== old) {
old.fns = cur
on[name] = old
}
}
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name)
remove(event.name, oldOn[name], event.capture)
}
}
}
2.2.2 自定义组件事件
事件的监听和派发均是实例
//src/core/instance/events.js
2.2.2.1执行流程
Vue -> Vue._init -> Vue.$mount -> mountComponent -> watcher -> get updateComponent -> Vue._update -> patch -> createElm -> createComponent -> hook.init ->createComponent... ->init() -> initEvents -> updateComponentListeners() -> updateDOMListeners() -> updateListeners ->
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
//初始化钩子
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
//初始化钩子
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
registerRef(vnode)
// make sure to invoke the insert hook
insertedVnodeQueue.push(vnode)
}
}
2.3全局事件 $on $emit
export function eventsMixin (Vue: Class<Component>) {
const hookRE = /^hook:/
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
//判断事件是否是 数组,即可以多个事件key绑定一个方法,递归继续调用$on绑定
// 传入$on([key1,key2],fn1)转化为 {key1:[fn1],key2:[fn1],}
// 传入$on([key1],fn2)转化为 {key1:[fn1,fn2],key2:[fn1],}
//上面一个key绑定多个方法 ,如fn1和fn2
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
//下面是真正的绑定如: _events = {key1:[fn1,fn2],key2:[fn1,fn2],}
(vm._events[event] || (vm._events[event] = [])).push(fn)
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
Vue.prototype.$once = function (event: string, fn: Function): Component {
const vm: Component = this
function on () {
vm.$off(event, on)//2.由于只执行一遍,所以立马取消,在全局的_event对象里已经清空所有监听
fn.apply(vm, arguments)//3.人为触发一次事件
}
on.fn = fn//该赋值是为了 在$off的时候找不到原来的fn,会通过 on.fn去判断移除的函数
vm.$on(event, on)//1.这里正常的调用了$on注册,对应当前定义的on方法
return vm
}
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
const vm: Component = this
// all
//如果不传参数 所有事件清空
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// array of events
//如果传入是数组事件名,遍历递归调用移除
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
}
// specific event
//下面是正真的单个事件移除,一个key对应一个数组数据
const cbs = vm._events[event]
if (!cbs) {
return vm
}
//如果没有传入指定的方法,也说明要直接删除单个
if (!fn) {
vm._events[event] = null
return vm
}
//找到方法匹配两个方法是否一致,
// specific handler
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
if (cb === fn || cb.fn === fn) {//这里的 .fn是上面$once留下来判断的依据
cbs.splice(i, 1)
break
}
}
return vm
}
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
if (process.env.NODE_ENV !== 'production') {
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}".`
)
}
}
//根据key找到对应的回调方法,然后逐一执行
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 方法主要是做 调用方法时候安全捕获
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
}
//src/core/instance/events.js
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
//这里把父亲的事件传入
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
let target: any
//这里定义了add 方法给下面updateListeners 传入使用,最终就是谁定义谁派发与接受
function add (event, fn) {
target.$on(event, fn)
}
function remove (event, fn) {
target.$off(event, fn)
}
export function updateComponentListeners (
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm
updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
target = undefined
}
//src/core/vdom/helpers/update-listeners.js
//这里传入参数 add 就是上面src/core/instance/events.js定义的
export function updateListeners (
on: Object,
oldOn: Object,
add: Function,
remove: Function,
createOnceHandler: Function,
vm: Component
) {
...
add(event.name, cur, event.capture, event.passive, event.params)
...
}