事件委托是在js中减少事件绑定的方式,是一种常用的js优化方法。曾经review其他人的代码,发现很多同学在循环的时候直接在循环元素上绑定事件。那么vue 循环中绑定事件是否需要事件委托?。
通过打印出下面简单实例编译出来的render函数
<div id="demo">
<a v-for="item in [1,2]" @click="clickFn">1</a>
</div>
我们得到下面的结果:
ƒanonymous() {
with(this) {
return _c('div', {
attrs: {
"id": "demo"
}
},
// 这部分就是循环
_l(([1, 2]),
function(item) {
return _c('a', {
on: {
"click": clickFn
}
},
[_v("1")])
}), 0)
}
}
查找源代码得_l 的函数定义如下:
export function renderList (
val: any,
render: (
val: any,
keyOrIndex: string | number,
index?: number
) => VNode
): ?Array<VNode> {
let ret: ?Array<VNode>, i, l, keys, key
if (Array.isArray(val) || typeof val === 'string') {
ret = new Array(val.length)
for (i = 0, l = val.length; i < l; i++) {
ret[i] = render(val[i], i)
}
}
... 省略了不相关分支代码
(ret: any)._isVList = true
return ret
}
也就是说这东西会return一个包含两个a标签,包含on属性的vnode的数组
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// 初始化父类绑定事件
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
let target: any
// 添加事件
function add (event, fn) {
target.$on(event, fn)
}
export function updateComponentListeners (
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm
// 更新事件监听
updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
target = undefined
}
export function updateListeners (
on: Object, // 绑定事件
oldOn: Object, // 旧绑定事件
add: Function, // 添加时间绑定函数
remove: Function, // 移除事件监听
createOnceHandler: Function, // 创建once监听帮助函数
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)
}
}
}
export function eventsMixin (Vue: Class<Component>) {
const hookRE = /^hook:/
// on方法定义
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
// 如果事件是数组,遍历并递归
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
// 将事件处理函数push到组件_events中
(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
}
以上是事件绑定相关的代码,一些主要步骤做了注释,可以看出最终事件绑定是通过$on进行的,$on 最终的绑定事件代码为: (vm._events[event] || (vm._events[event] = [])).push(fn) ,就是将事件处理函数push到_events数组中。整个事件绑定过程,以及最后事件实际push到_events中的过程都没有看到有将子元素事件绑定到父元素的相关代码。
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
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)) {
// 省略部分代码
// nodeOps 定义在 src\platforms\web\runtime\node-ops.js 中,主要是真正的dom操作。
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag) // 创建一个具有指定的命名空间URI和限定名称的元素。
: nodeOps.createElement(tag, vnode) // 其实就是document.createElement(tagName)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
// 省略部分代码
} else {
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// 插入父级元素
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
}
// 省略部分代码
}
虚拟DOM转化成真实DOM,该过程最主要的由createElm实现,其中 nodeOps.createElement(tag, vnode) 内部通过document.createElement(tagName) 创建真正的DOM元素。虚拟dom转化成真实DOM的过程也没有看到任何事件委托的实现。虚拟dom转化成真实dom的过程没有详细些,网上已经有人分析了这个过程,可参考:https://www.jianshu.com/p/49042bd8fef4
结论:以上从模板编译成render函数,最终返回的vnode数组;到事件绑定的实现;到虚拟都转化成真实dom。三个方面,都未曾看到有任何内部自动事件委托的实现。所以个人认为:循环列表中,如果元素较多,最好还是采用事件委托。
以上是个人分析过程,如有错误,欢迎批评指正。