Глубокое понимание механизма событий DOM

JavaScript

предисловие

В этой статье в основном представлены:

  1. Уровень события DOM
  2. Поток событий DOM
  3. Модель событий DOM
  4. агент событий
  5. Общие методы и свойства объектов Event

1. Уровень события DOM

Для разных уровней DOM наши методы обработки событий DOM также различаются.

Уровень DOM можно разделить на 4 уровня: уровень DOM0, «обычно называемый уровнем DOM0 до формирования спецификации DOM1», уровень DOM1, уровень DOM2 и уровень DOM3, иСобытия DOM разделены на 3 уровня: обработка событий на уровне DOM0, обработка событий на уровне DOM2 и обработка событий на уровне DOM3. Как показано ниже:

DOM级别与DOM事件.jpg

1. События уровня DOM 0

Прежде чем понять события уровня DOM0, нам необходимо понятьОбработчик HTML-событий, который также является самым ранним методом обработки событий такого рода.Код выглядит следующим образом:

<button type="button" onclick="fn" id="btn">点我试试</button>

<script>
    function fn() {
        alert('Hello World');
    }
</script>

Тогда возникает вопрос, то есть следует ли заключать в скобки fn?

В атрибуте onclick html при его использовании должны быть добавлены скобки, а в onclick js событию клика присваивается значение без скобок. Зачем? Мы говорим через факты:

// fn不加括号
<button type="button" onclick="fn" id="btn">点我试试</button>

<script>
    function fn() {
        alert('Hello World');
    }
    console.log(document.getElementById('btn').onclick);
    // 打印的结果如下:这个函数里面包括着fn,点击之后并没有弹出1
    /*
    ƒ onclick(event) {
    	fn
    }
    */
</script>

// fn 加括号,这里就不重复写上面代码,只需要修改一下上面即可
<button type="button" onclick="fn()"  id="btn">点我试试</button>
<script>
// 打印的结果如下:点击之后可以弹出1
/*
ƒ onclick(event) {
	fn()
}
*/
</script>

В приведенном выше коде мы запускаем метод fn, определяя атрибут onclick непосредственно в HTML-коде.Самый большой недостаток такого обработчика событий заключается в том, чтоСильная связь между HTML и JS, когда нам нужно изменить имя функции, мы должны изменить два места. Конечно преимущество в том, чтоНет необходимости манипулировать DOM для завершения привязки событий..

привязка события DOM0, метод привязки к поведению элемента,Эти методы выполняются в фазе всплытия (или целевой фазе) текущего поведения события элемента..

Итак, как мы достигаемНизкая связанность HTML и JS? Таким образом, появление событий обработки уровня DOM0 решает эту проблему. Событие уровня DOM0 предназначено для назначения функции свойству обработки события, например:

<button id="btn" type="button"></button>

<script>
    var btn = document.getElementById('btn');
    
    btn.onclick = function() {
        alert('Hello World');
    }
    
    // btn.onclick = null; 解绑事件 
</script>

В приведенном выше коде мы определяем идентификатор для кнопки, а затем получаем кнопку с этим идентификатором через JS и назначаем функцию атрибуту обработки события при нажатии, Этот метод является воплощением обработки событий на уровне DOM0. Мы можем сделать это, назначив null свойству обработчика событий.отменить событие. Шаги для обработки событий на уровне DOM 0:Сначала найдите узел DOM, а затем назначьте функцию обработчика свойству события объекта узла.

Недостатком погрузчиков событий DOM0-уровня в том, что«Событие» обработчика не может одновременно связывать несколько обработчиков., например, я также хочу добавить еще одну функцию к событию нажатия кнопки.

var btn = document.getElementById('btn');
    
btn.onclick = function() {
    alert('Hello World');
}
btn.onclick = function() {
    alert('没想到吧,我执行了,哈哈哈');
}

2. События уровня DOM2

На основе событий уровня DOM0 события уровня DOM2 компенсируют тот недостаток, что обработчик не может связать несколько обработчиков одновременно, что позволяет добавлять к обработчику несколько обработчиков. То есть, используя события DOM2, вы можете добавлять несколько обработчиков по желанию и удалять события DOM2 с помощью removeEventListener. код показывает, как показано ниже:

<button type="button" id="btn">点我试试</button>

<script>
    var btn = document.getElementById('btn');

    function fn() {
        alert('Hello World');
    }
    btn.addEventListener('click', fn, false);
    // 解绑事件,代码如下
    // btn.removeEventListener('click', fn, false);  
</script>

События уровня DOM2 определяют два метода, addEventListener и removeEventListener, которые используются для привязки и отвязки событий соответственно.

target.addEventListener(type, listener[, useCapture]);
target.removeEventListener(type, listener[, useCapture]);
/*
	方法中包含3个参数,分别是绑定的事件处理属性名称(不包含on)、事件处理函数、是否在捕获时执行事件处理函数(关于事件冒泡和事件捕获下面会介绍)
*/

Примечание:

IE8 и более ранние версии не поддерживают addEventListener и removeEventListener, и для реализации необходимо использовать attachEvent и detachEvent:

// IE8级以下版本只支持冒泡型事件,不支持事件捕获所以没有第三个参数
// 方法中包含2个参数,分别是绑定的事件处理属性名称(不包含on)、事件处理函数
btn.attachEvent('onclick', fn); // 绑定事件 
btn.detachEvent('onclick', fn); // 解绑事件 

3. События уровня DOM3

События уровня DOM3 добавляют дополнительные типы событий к событиям уровня DOM2, все из которых следующие:

  1. События пользовательского интерфейса, срабатывающие, когда пользователь взаимодействует с элементами на странице, например: загрузка, прокрутка
  2. Событие фокуса, срабатывающее, когда элемент получает или теряет фокус, например: размытие, фокус
  3. События мыши, срабатывающие, когда пользователь выполняет операции на странице с помощью мыши, например: dbclick, mouseup
  4. Событие колеса, срабатывающее при использовании колесика мыши или аналогичного устройства, например: колесико мыши
  5. Текстовое событие, срабатывающее при вводе текста в документ, например: TextInput
  6. События клавиатуры, срабатывающие, когда пользователь выполняет операцию на странице с помощью клавиатуры, например: нажатие клавиши, нажатие клавиши
  7. Событие композиции, инициируемое при вводе символов для IME (редактора метода ввода), например:
  8. События изменения, запускаемые при изменении базовой структуры DOM, например: DOMsubtreeModified

В то же время события уровня DOM3 такжеРазрешить пользователям настраивать некоторые события.

Развитие событийного уровня DOM делает обработку событий более полной и насыщенной, а следующей проблемой является упомянутая ранее модель событий DOM. «Всплывание событий и захват событий»

2. Поток событий DOM

Зачем существует поток событий?

Если событие щелчка зарегистрировано для кнопки, а событие щелчка зарегистрировано для другого родительского элемента div, то, когда мы нажимаем кнопку, первыйИнициировать событие в родительском элементе,ещесобытие на кнопкеНу, это требует соглашения для стандартизации порядка выполнения событий, то есть процесса выполнения событий.

В ходе разработки браузера появились две разные спецификации.

  • Браузеры IE ниже IE9 используютвсплывающее окно события, начиная с конкретных принимающих элементов, а затем постепенно распространяясь до неспецифических элементов.
  • Netscapte используетзахват событийВо-первых, событие получено развораченным элементом, и наиболее специфическим узлом, наконец, получает событие.
  • В веб-стандартах, сформулированных W3C,Обе схемы используются одновременно, работают как перехват событий, так и всплывающая всплывающая подсказка.

3. Модель событий DOM

Модель событий DOM делится на захват и всплытие.. Когда происходит событие, оно распространяется между дочерними и родительскими элементами. Это распространение делится на три этапа.

(1) Фаза захвата: фаза, на которой события распространяются от оконного объекта к целевому узлу сверху вниз;

(2) Целевой этап: этап, на котором реальный целевой узел обрабатывает событие;

(3) Стадия всплытия: стадия, на которой события распространяются от целевого узла к оконному объекту снизу вверх.

Как упоминалось выше, третий параметр addEventListener указывает, выполняется ли событие в фазе захвата или всплытия, если установлено значение true, это означает, что событие выполняется в фазе захвата, а если установлено значение false , это означает, что событие выполняется в фазе всплытия. Так что же такое пузырьковое всплытие и захват событий? Это можно пояснить с помощью следующего рисунка:

1. Запись событий

захватсверху донизу, событие сначала идет от объекта окна, затем в документ (объект), затем в html-тег (получение html-тега через document.documentElement), затем в тег body (получение тега body через document.body), и затем в соответствии с обычной структурой html он передается слой за слоем и, наконец, достигает целевого элемента. нам просто нужноТретий параметр addEventListener изменен на trueзахват события может быть достигнут. код показывает, как показано ниже:

<!-- CSS 代码 -->
<style>
    body{margin: 0;}
    div{border: 1px solid #000;}
    #grandfather1{width: 200px;height: 200px;}
    #parent1{width: 100px;height: 100px;margin: 0 auto;}
    #child1{width: 50px;height: 50px;margin: 0 auto;}
</style>

<!-- HTML 代码 -->
<div id="grandfather1">
    爷爷
    <div id="parent1">
        父亲
        <div id="child1">儿子</div>
    </div>
</div>

<!-- JS 代码 -->
<script>
    var grandfather1 = document.getElementById('grandfather1'),
        parent1 = document.getElementById('parent1'),
        child1 = document.getElementById('child1');
    
    grandfather1.addEventListener('click',function fn1(){
        console.log('爷爷');
    },true)
    parent1.addEventListener('click',function fn1(){
        console.log('爸爸');
    },true)
    child1.addEventListener('click',function fn1(){
        console.log('儿子');
    },true)

    /*
        当我点击儿子的时候,我是否点击了父亲和爷爷
        当我点击儿子的时候,三个函数是否调用
    */
    // 请问fn1 fn2 fn3 的执行顺序?
    // fn1 fn2 fn3 or fn3 fn2 fn1  
</script>

Давайте сначала посмотрим на результаты:

Когда мы нажимаем на тег div с идентификатором child1, выводится результат: дедушка => папа => сын, что прямо противоположно пузырьковому воспроизведению событий.

2. Всплывание событий

Так называемое всплытие событий — это событие, при котором событие всплывает слой за слоем, подобно пузырьку, из того места, где оно было впервые сгенерировано. нам просто нужноТретий параметр addEventListener изменен на falseМожет быть достигнуто всплытие событий. код показывает, как показано ниже:

//html、css代码同上,js代码只是修改一下而已
var grandfather1 = document.getElementById('grandfather1'),
    parent1 = document.getElementById('parent1'),
    child1 = document.getElementById('child1');

grandfather1.addEventListener('click',function fn1(){
    console.log('爷爷');
},false)
parent1.addEventListener('click',function fn1(){
    console.log('爸爸');
},false)
child1.addEventListener('click',function fn1(){
    console.log('儿子');
},false)

/*
   当我点击儿子的时候,我是否点击了父亲和爷爷
   当我点击儿子的时候,三个函数是否调用
*/
// 请问fn1 fn2 fn3 的执行顺序?
// fn1 fn2 fn3 or fn3 fn2 fn1  

Давайте сначала посмотрим на результаты:

Например, тег div с идентификатором child1 на приведенном выше рисунке является целью события.После щелчка он также инициирует событие щелчка на родительском элементе, слой за слоем вплоть до самого внешнего HTML-кода или документа.

Примечание: когда третий параметрfalseИли когда он пустой, это означает связывание в фазе барботирования.

В-четвертых, делегирование событий (делегирование событий)

1. Что такое брокер событий и почему его следует оптимизировать?

Поскольку событие будет распространяться до родительского узла на этапе всплытия, функция прослушивателя дочернего узла может быть определена на родительском узле, а функция прослушивателя родительского узла может единообразно обрабатывать события нескольких дочерних элементов. Такой подход называется делегированием событий.

Например, если одноклассник в общежитие приходит одновременно, то один путь - все по одному тупо идут его забирать, а другой способ - доверить это дело заведующему общежитием, и пусть выходит один человек получить всех курьеров, Затем раздать каждому студенту общежития по одному в зависимости от получателя;

Здесь забрать курьера — это событие, каждый одноклассник ссылается на элемент ДОМ, который должен отреагировать на событие, а начальник общежития, выходящий забрать курьера, — это элемент агента, поэтому именно этот элемент действительно привязывает событие, которое распределяется по получателю.Процесс экспресс-доставки заключается в том, что при выполнении события необходимо определить, какой из проксируемых элементов должен соответствовать в данный момент отвечающему событию.

Затем, используя механизм всплытия или захвата событий, мы можем оптимизировать привязку событий. В JS, если мы регистрируем все больше и больше событий, производительность страницы ухудшается, потому что:

  • Функции являются объектами и занимают память. Чем больше объектов в памяти, тем хуже производительность браузера.
  • Зарегистрированные события обычно указывают элементы DOM. Чем больше событий, тем больше элементов DOM используется, и время готовности к взаимодействию со страницей будет отложено.
  • При удалении дочернего элемента вам не нужно рассматривать возможность удаления события привязки

2. Преимущества

  • Сокращение потребления памяти и повышение производительности

Допустим есть список с большим количеством элементов списка, нам нужно отреагировать на событие при клике по каждому элементу списка

// 例4
<ul id="list">
  <li>item 1</li>
  <li>item 2</li>
  <li>item 3</li>
  ......
  <li>item n</li>
</ul>

Если вы привяжете функцию к каждому элементу списка, она будет потреблять много памяти и потреблять много производительности с точки зрения эффективности. С помощью прокси-сервера событий нам нужно только привязать методы к родительскому контейнеру ul, чтобы независимо от того, какой элемент-потомок был нажат, поведение щелчка контейнера будет запускаться в соответствии с механизмом передачи пузырькового распространения, а затем будет выполнен соответствующий метод.В зависимости от источника события мы можем знать, кто нажал, и делать разные вещи.

  • Динамически связывать события

Во многих случаях нам нужно динамически добавлять или удалять элементы списка с помощью пользовательских операций.Если мы привязываем события к каждому дочернему элементу в начале, то при изменении списка нам нужно повторно привязывать события к вновь добавленным элементам, поэтому что элементы, которые собираются удалить, будут связаны с событиями.Элемент отвязывает событие, если вы используете прокси события, вы избавите себя от таких проблем.

2. Как реализовать

Далее давайте реализуем делегирование события элемента li под родительским элементом #list в приведенном выше примере его родительскому элементу:


<ul id="list">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
</ul>

<script>
// 给父层元素绑定事件
document.getElementById('list').addEventListener('click', function (e) {
    // 兼容性处理
    var event = e || window.event;
    var target = event.target || event.srcElement;
    // 判断是否匹配目标元素
    if (target.nodeName.toLocaleLowerCase() === 'li') {
        console.log('the content is: ', target.innerHTML);
    }
});
</script>

Это обычный метод реализации делегирования событий, но в этом методе есть ошибки.Когда в отслеживаемом элементе есть дочерний элемент, то мы щелкаем по дочернему элементу, событие будет недействительным, поэтому мы можем связаться с всплытием, упомянутым в предыдущем разделе. статьи Механизм распространения событий для устранения этой ошибки. Улучшенный код делегирования событий:

<ul id="list">
    <li>1 <span>aaaaa</span></li>
    <li>2 <span>aaaaa</span></li>
    <li>3 <span>aaaaa</span></li>
    <li>4</li>
</ul>

<script>


// 给父层元素绑定事件
document.getElementById('list').addEventListener('click', function (e) {
    // 兼容性处理
    var event = e || window.event;
    var target = event.target || event.srcElement;
    // 判断是否匹配目标元素
    /* 从target(点击)元素向上找currentTarget(监听)元素,
    找到了想委托的元素就触发事件,没找到就返回null */
    while(target.tagName !== 'LI'){
    
        if(target.tagName === 'UL'){
            target = null
            break;
        }
        target = target.parentNode
    }
    if (target) {
    console.log('你点击了ul里的li')
    }
});

В-пятых, общие методы и свойства объекта Event.

1.event. preventDefault()

Если этот метод вызывается, поведение события по умолчанию больше не будет срабатывать.. Что такое событие по умолчанию? Например, как только форма нажимает кнопку отправки (отправить), страница обновляется, страница тега по умолчанию перескакивает или точка привязки позиционируется.

Сценарий использования 1: тег a используется только как обычная кнопка, нажмите, чтобы выполнить функцию, не хотите переходить на страницу и не хотите позиционировать привязку.

метод первый

<a href="javascript:;">链接</a>

Способ второй

Используйте метод JS, чтобы предотвратить это, и привяжите метод к его событию click.Когда мы нажимаем на тег A, событие click будет запущено первым, а затем будет выполнено поведение по умолчанию.

<a id="test" href="http://www.google.com">链接</a>
<script>
    test.onclick = function(e){
        e = e || window.event;
        return false;
    }
</script>

Способ третий

<a id="test" href="http://www.google.com">链接</a>
<script>
    test.onclick = function(e){
        e = e || window.event;
        e.preventDefault();
    }
</script>

Используйте сценарий 2: в поле ввода можно ввести не более шести символов, как этого добиться?

Код реализации выглядит следующим образом:

<input type="text" id='tempInp'>
<script>
    tempInp.onkeydown = function(ev) {
        ev = ev || window.event;
        let val = this.value.trim() //trim去除字符串首位空格(不兼容)
        // this.value=this.value.replace(/^ +| +$/g,'') 兼容写法
        let len = val.length
        if (len >= 6) {
            this.value = val.substr(0, 6);
            //阻止默认行为去除特殊按键(DELETE\BACK-SPACE\方向键...)
            let code = ev.which || ev.keyCode;
            if (!/^(46|8|37|38|39|40)$/.test(code)) {
                ev.preventDefault()
            }
        }
    }
</script>

2.event.stopPropagation() & event.stopImmediatePropagation()

Метод event.stopPropagation() останавливает передачу события к родительскому элементу, предотвращая выполнение любых обработчиков родительских событий.. Демонстрационный код выглядит следующим образом:

// 在事件冒泡demo代码的基础上修改一下
child1.addEventListener('click',function fn1(e){
    console.log('儿子');
    e.stopPropagation()
},false)

stopImmediatePropagation как для предотвращения передачи события в родительский элемент, так и для предотвращения срабатывания других элементов того же типа прослушивателя событий. Эффекта первого можно добиться только stopPropagation. Давайте посмотрим на пример:

<button id="btn">点我试试</button>
<script>
const btn = document.querySelector('#btn');
btn.addEventListener('click', event => {
  console.log('btn click 1');
  event.stopImmediatePropagation();
});
btn.addEventListener('click', event => {
  console.log('btn click 2');
});
document.body.addEventListener('click', () => {
  console.log('body click');
});
</script>

По распечатанным результатам мы обнаружили, что после использования stopImmediatePropagation при нажатии кнопки не сработает не только событие привязки тела, но и не сработает другое событие клика кнопки.

3.event.target & event.currentTarget

На картинке выше мы видим, чтоevent.targetнаправлениеЭлемент, вызвавший инициирующее событие,а такжеevent.currentTargetявляетсяэлемент, привязанный к событию.

Суммировать

Так что не помните, когдаe.currentTargetа такжеe.targetРавные, когда не равные, чтобы понять, на кого они указывают.

  • e.targetУказывает на объект, который запускает прослушиватель событий, «настоящего отправителя события».
  • e.currentTargetУказывает на объект «слушатель», чтобы добавить слушателя к событию.

6. Справочные статьи