DOM基础

事件绑定和解绑

1、attachEvent

IE8及以下:attachEvent

element.attachEvent(type, listener)

  • element:要绑定事件的对象,或HTML节点;
  • type:事件名称(带“on”),如“onclick”、“onmouseover”;
  • listener:要绑定的事件监听函数;
  • 默认在事件冒泡时执行。

2、addEventListener

标准的绑定事件监听函数的方法:addEventListener

element.addEventListener(type, listener, useCapture)

  • element: 要绑定事件的对象,或HTML节点;
  • type:事件名称(不带“on”),如“click”、“mouseover”;
  • listener:要绑定的事件监听函数;
  • userCapture:事件监听方式,只能是true或false。true,采用捕获(capture)模式;false,采用冒泡(bubbling)模式。若无特殊要求,一般是false。

3、区别

addEventListener方法的listener监听函数在元素的作用域内进行,this指向当前元素;attachEvent方法的listener监听函数在全局作用域下运行,this指向window

注意:target.addEventListener、target.attachEvent与target.onclick这类方法的区别:

  • 前面两种方法允许对一个target的某个事件同时绑定多个listener,后者在绑定多个listener的情况下只有最后一个会生效(后定义的会覆盖先定义的);
  • 后面两种只能在冒泡阶段触发listener。

4、兼容的事件绑定方法

javascript
function addEvent(target, type, listener) {
    try {
        // Chrome, FireFox, Opera, Safari, IE9&+
        target.addEventListener(type, listener, false)
    } catch (e) {
        try {
            // IE6 - IE10, not available in IE11
            target.attachEvent('on' + type, listener)
        } catch (err) {
            // all browsers
            target['on' + type] = listener
        }
    }
}

// or shorter one like this:
function addEvent(target, type, listener) {
    if (target.addEventListener) {
        // non-IE, IE9&+
        target.addEventListener(type, listener, false)
    } else if (target.attachEvent) {
        // IE6 - IE10, not available in IE11
        target.attachEvent('on' + type, listener)
    } else {
        // all browsers
        target['on' + type] = listener
    }
}
function addEvent(target, type, listener) {
    try {
        // Chrome, FireFox, Opera, Safari, IE9&+
        target.addEventListener(type, listener, false)
    } catch (e) {
        try {
            // IE6 - IE10, not available in IE11
            target.attachEvent('on' + type, listener)
        } catch (err) {
            // all browsers
            target['on' + type] = listener
        }
    }
}

// or shorter one like this:
function addEvent(target, type, listener) {
    if (target.addEventListener) {
        // non-IE, IE9&+
        target.addEventListener(type, listener, false)
    } else if (target.attachEvent) {
        // IE6 - IE10, not available in IE11
        target.attachEvent('on' + type, listener)
    } else {
        // all browsers
        target['on' + type] = listener
    }
}

5、兼容的事件解绑方法:

javascript
function removeEvent(target, type, listener) {
    if (target.removeEventListener) {
        target.removeEventListener(type, listener, false)
    } else if (target.detachEvent) {
        target.detachEvent('on' + type, listener)
    } else {
        target.detachEvent['on' + type] = null
    }
}
function removeEvent(target, type, listener) {
    if (target.removeEventListener) {
        target.removeEventListener(type, listener, false)
    } else if (target.detachEvent) {
        target.detachEvent('on' + type, listener)
    } else {
        target.detachEvent['on' + type] = null
    }
}

事件的冒泡和捕获

1、介绍

JS中事件流的三个阶段:捕获(低版本IE不支持)==>目标==>冒泡。

  • Capture:from general to specific;
  • Bubbling:from specific to general.

如果不同层的元素使用useCapture不同, 会先从最外层元素往目标元素寻找设定为capture模式的事件, 到达目标元素后执行目标元素的事件后,在循原路往外寻找设定为bubbling模式的事件。

DOM操作

1、获取元素

书写原生js脚本将body下的第二个div隐藏:

javascript
var oBody = document.getElementsByTagName('body')[0]
var oChildren = oBody.childNodes
var nDivCounter = 0

for (var i = 0, len = oChildren.length; i < len; i++) {
    if (oChildren[i].nodeName === 'DIV') {
        nDivCounter++
        if (nDivCounter === 2) {
            oChildren[i].style.display = 'none'
        }
    }
}
var oBody = document.getElementsByTagName('body')[0]
var oChildren = oBody.childNodes
var nDivCounter = 0

for (var i = 0, len = oChildren.length; i < len; i++) {
    if (oChildren[i].nodeName === 'DIV') {
        nDivCounter++
        if (nDivCounter === 2) {
            oChildren[i].style.display = 'none'
        }
    }
}

2、创建元素

html
<ul id="list" class="foo">
    <li>#0</li>
    <li><span>#1</span></li>
    <li>#2</li>
    <li>#3</li>
    <li>
        <ul>
            <li>#4</li>
        </ul>
    </li>
    <!-- ... -->
    <li><a href="//v2ex.com">#99998</a></li>
    <li>#99999</li>
    <li>#100000</li>
</ul>
<ul id="list" class="foo">
    <li>#0</li>
    <li><span>#1</span></li>
    <li>#2</li>
    <li>#3</li>
    <li>
        <ul>
            <li>#4</li>
        </ul>
    </li>
    <!-- ... -->
    <li><a href="//v2ex.com">#99998</a></li>
    <li>#99999</li>
    <li>#100000</li>
</ul>
  • 为ul元素添加一个类.bar
  • 删除第10个li
  • 在第500个li后面添加一个li,其文字内容为“<v2ex.com />”
  • 点击任意li弹框显示其为当前列表中的第几项

解答

javascript
// 还原题目真实DOM结构
var list = document.getElementById('list')
void function() {
    var html = ''
    for (var i = 0; i <= 10000; i++) {
        if (i === 1) {
            html += '<li><span>#1</span></li>'
        } else if (i === 4) {
            html += '<li><ul><li>#4</li></ul></li>'
        } else if (i === 9998) {
            html += '<li><a href="//v2ex.com">#9998</a></li>'
        } else {
            html += '<li>#' + i + '</li>'
        }
    }
    list.innerHTML = html
}()

// or, list.className += ' bar'
list.classList.add('bar')

var li10 = document.querySelector('#list > li:nth-of-type(10)')
li10.parentNode.removeChild(li10)

var newItem = document.createElement('LI')
var textNode = document.createTextNode('<v2ex.com />')
newItem.appendChild(textNode)

// index for css nth-of-type is 1-based
var li501 = document.querySelector('#list > li:nth-of-type(501)')
list.insertBefore(newItem, li501)

list.addEventListener('click', function(e) {
    var target = e.target || e.srcElement
    if (target.id === 'list') {
        alert('你点到最外层的ul上了,叫我怎么判断?')
        return
    }
    while (target.nodeName !== 'LI') {
        target = target.parentNode
    }

    var parentUl = target.parentNode
    var children = parentUl.childNodes
    var count = 0
    for (var i = 0, len = children.length; i < len; i++) {
        var node = children[i]
        if (node.nodeName === 'LI') {
            count++
        }
        if (node === target) {
            alert('是当前第' + count + '项')
            break
        }
    }
}, false)

// PS: if querySelector method is not available, the following can be changed.
var li10 = document.querySelector('#list > li:nth-of-type(10)')
var li501 = document.querySelector('#list > li:nth-of-type(501)')

// As below:
function getLiByIndex(index /* 0-based index */ ) {
    var count = -1
    for (var i = 0, len = list.childNodes.length; i < len; i++) {
        if (list.childNodes[i].nodeName === 'LI') {
            count++
            if (count === index) {
                return list.childNodes[i]
            }
        }
    }
}
var li10 = getLiByIndex(9)
var li501 = getLiByIndex(500)
// 还原题目真实DOM结构
var list = document.getElementById('list')
void function() {
    var html = ''
    for (var i = 0; i <= 10000; i++) {
        if (i === 1) {
            html += '<li><span>#1</span></li>'
        } else if (i === 4) {
            html += '<li><ul><li>#4</li></ul></li>'
        } else if (i === 9998) {
            html += '<li><a href="//v2ex.com">#9998</a></li>'
        } else {
            html += '<li>#' + i + '</li>'
        }
    }
    list.innerHTML = html
}()

// or, list.className += ' bar'
list.classList.add('bar')

var li10 = document.querySelector('#list > li:nth-of-type(10)')
li10.parentNode.removeChild(li10)

var newItem = document.createElement('LI')
var textNode = document.createTextNode('<v2ex.com />')
newItem.appendChild(textNode)

// index for css nth-of-type is 1-based
var li501 = document.querySelector('#list > li:nth-of-type(501)')
list.insertBefore(newItem, li501)

list.addEventListener('click', function(e) {
    var target = e.target || e.srcElement
    if (target.id === 'list') {
        alert('你点到最外层的ul上了,叫我怎么判断?')
        return
    }
    while (target.nodeName !== 'LI') {
        target = target.parentNode
    }

    var parentUl = target.parentNode
    var children = parentUl.childNodes
    var count = 0
    for (var i = 0, len = children.length; i < len; i++) {
        var node = children[i]
        if (node.nodeName === 'LI') {
            count++
        }
        if (node === target) {
            alert('是当前第' + count + '项')
            break
        }
    }
}, false)

// PS: if querySelector method is not available, the following can be changed.
var li10 = document.querySelector('#list > li:nth-of-type(10)')
var li501 = document.querySelector('#list > li:nth-of-type(501)')

// As below:
function getLiByIndex(index /* 0-based index */ ) {
    var count = -1
    for (var i = 0, len = list.childNodes.length; i < len; i++) {
        if (list.childNodes[i].nodeName === 'LI') {
            count++
            if (count === index) {
                return list.childNodes[i]
            }
        }
    }
}
var li10 = getLiByIndex(9)
var li501 = getLiByIndex(500)

事件代理/委托

事件代理/委托,是靠事件的冒泡机制实现的(所以,对于一些不具有冒泡特性的事件,比如focus、blur,就没有事件代理/委托这种说法了)。

1、优缺点

优点有:

  • 可以大量节省内存占用,减少事件注册,比如在table上代理所有td的click事件就非常棒;
  • 可以实现当新增子孙节点时无需再次对其绑定事件,对于动态内容部分尤为合适。

缺点有:

  • 如果把所有事件都代理到一个比较顶层的DOM节点上的话,比较容易出现误判,给不需要绑定事件的节点绑定了事件,比如把页面中所有事件都绑定到document上进行委托,就不是很合适;
  • 事件逐级冒泡到外部dom上再执行肯定没有直接执行快。

2、实现

javascript
// 只考虑IE 9&+
function delegate(element, targetSelector, type, handler) {
    element.addEventListener(type, function(e) {
        var targets = Array.prototype.slice.call(element.querySelectorAll(targetSelector))
        var target = e.target
        if (targets.indexOf(target) !== -1) {
            return handler.apply(target, arguments)
        }
    })
}

// 兼容写法
function delegate(element, targetClass, type, handler) {
    addEvent(element, type, function(e) {
        e = e || window.event
        var target = e.target || e.srcElement
        if (target.className.indexOf(targetClass) !== -1) {
            handler.apply(target, arguments)
        }
    })
}

function addEvent(target, type, listener) {
    if (target.addEventListener) {
        // non-IE, IE9&+
        target.addEventListener(type, listener, false)
    } else if (target.attachEvent) {
        // IE6 - IE10, not available in IE11
        target.attachEvent('on' + type, listener)
    } else {
        // all browsers
        target['on' + type] = listener
    }
}
// 只考虑IE 9&+
function delegate(element, targetSelector, type, handler) {
    element.addEventListener(type, function(e) {
        var targets = Array.prototype.slice.call(element.querySelectorAll(targetSelector))
        var target = e.target
        if (targets.indexOf(target) !== -1) {
            return handler.apply(target, arguments)
        }
    })
}

// 兼容写法
function delegate(element, targetClass, type, handler) {
    addEvent(element, type, function(e) {
        e = e || window.event
        var target = e.target || e.srcElement
        if (target.className.indexOf(targetClass) !== -1) {
            handler.apply(target, arguments)
        }
    })
}

function addEvent(target, type, listener) {
    if (target.addEventListener) {
        // non-IE, IE9&+
        target.addEventListener(type, listener, false)
    } else if (target.attachEvent) {
        // IE6 - IE10, not available in IE11
        target.attachEvent('on' + type, listener)
    } else {
        // all browsers
        target['on' + type] = listener
    }
}

说明:上面的实现方案中addEvent方法的最后一种实现方式, 即target['on' + type]的方式会将之前绑定的事件覆盖掉,是有点问题的。 但是考虑到兼容性,一般来说代码是走不到这个地方的,所以也没有问题。

阻止事件传播和默认行为

1、阻止事件传播

The stopPropagation() method of the Event interface prevents further propagation of the current event in the capturing and bubbling phases. It does not, however, prevent any default behaviors from occurring; for instance, clicks on links are still processed. If you want to stop those behaviors, see the preventDefault() method.

javascript
e = e || window.event
if (e.stopPropagation) {
    e.stopPropagation()
} else {
    // IE 8&-
    e.cancelBubble = true
}
e = e || window.event
if (e.stopPropagation) {
    e.stopPropagation()
} else {
    // IE 8&-
    e.cancelBubble = true
}

2、阻止事件的默认行为

javascript
e = e || window.event
if (e.preventDefault) {
    // none-IE, IE 9&+
    e.preventDefault()
} else {
    // IE 5-8
    e.returnValue = false
}
e = e || window.event
if (e.preventDefault) {
    // none-IE, IE 9&+
    e.preventDefault()
} else {
    // IE 5-8
    e.returnValue = false
}

3、stopImmediatePropagation

The stopImmediatePropagation() method of the Event interface prevents other listeners of the same event from being called.

If several listeners are attached to the same element for the same event type, they are called in the order in which they were added. If stopImmediatePropagation() is invoked during one such call, no remaining listeners will be called.

4、stopPropagation和stopImmediatePropagation

  • stopPropagation will prevent any parent handlers from being executed;
  • stopImmediatePropagation will prevent any parent handlers and also any other handlers from executing.

5、jQuery中的return false

原生JS中return false只会阻止默认行为, 而用jQuery的话会同时阻止事件传播和阻止事件的默认行为:

javascript
$('a').click(function() {
    // 同时阻止默认行为和事件传播
    return false
})

document.getElementById('link').onclick = function(e) {
    // 阻止默认行为
    return false
}
$('a').click(function() {
    // 同时阻止默认行为和事件传播
    return false
})

document.getElementById('link').onclick = function(e) {
    // 阻止默认行为
    return false
}

下面这段是在jQuery 3.6.0中找到的代码,供参考:

javascript
ret = ((jQuery.event.special[handleObj.origType] || {}).handle ||
    handleObj.handler).apply(matched.elem, args);

if (ret !== undefined) {
    if ((event.result = ret) === false) {
        event.preventDefault();
        event.stopPropagation();
    }
}
ret = ((jQuery.event.special[handleObj.origType] || {}).handle ||
    handleObj.handler).apply(matched.elem, args);

if (ret !== undefined) {
    if ((event.result = ret) === false) {
        event.preventDefault();
        event.stopPropagation();
    }
}

参考资料

事件的几种target

1、target

The target property of the Event interface is a reference to the object onto which the event was dispatched. It is different from Event.currentTarget when the event handler is called during the bubbling or capturing phase of the event.

2、currentTarget

The currentTarget read-only property of the Event interface identifies the current target for the event, as the event traverses the DOM. It always refers to the element to which the event handler has been attached, as opposed to Event.target, which identifies the element on which the event occurred and which may be its descendant.

3、currentTarget和target的比较

  • target指向事件直接作用的对象,而currentTarget指向绑定该事件的对象;
  • 当处于捕获或冒泡阶段时,两者指向不一致;当处于目标阶段时,两者指向一致。

获取事件对象和目标对象:

javascript
function (e) {
  e = e ? e : window.event
  var target = e.target || e.srcElement
  // do some things here
}
function (e) {
  e = e ? e : window.event
  var target = e.target || e.srcElement
  // do some things here
}

4、srcElement

Initially implemented in Internet Explorer, Event.srcElement is a now-standard alias (defined in the DOM Standard but flagged as "historical") for the Event.target property. It's supported in all major browser engines, but only for compatibility reasons. Use Event.target instead.

This feature is no longer recommended. Though some browsers might still support it, it may have already been removed from the relevant web standards, may be in the process of being dropped, or may only be kept for compatibility purposes.

Avoid using it, and update existing code if possible. Be aware that this feature may cease to work at any time.

参考资料

虚拟列表