通过构建 UI 框架学习 JavaScript:第 5 部分 - 向 Dom 元素添加事件
本文是深入探讨 JavaScript 系列的第五篇。您可以访问与本项目相关的Github 仓库来查看之前的文章。
本系列并非全面涵盖所有 JavaScript 功能,而是根据各种问题的解决方案中出现的功能进行讲解。此外,每篇文章都基于其他开发者提供的教程和开源库,因此和您一样,我也在每篇文章中学习新知识。
在项目的这个阶段,我们已经构建了一个基本的 UI 框架 (Aprender)、测试库 (Examinar) 和模块打包器 (Maleta)。我们有一段时间没有动过这个框架了,所以在这篇文章中我们将回顾它。Aprender 最令人兴奋的功能是创建和渲染 DOM 元素,那么我们还能让它做些什么呢?
每个开发工具都是为了解决特定问题而构建的,我们的框架也不例外。它的主要目的是成为一种教育工具,但为了使教育有效,它需要在某些特定背景下进行。这个特定背景将是一个搜索应用程序,它允许用户从这些免费的公共 API中进行选择,搜索某些内容然后显示结果。我们将逐步构建处理此特定用例的功能,而不必担心我们的框架是否满足生产级工具的大量要求。例如,生产标准 UI 库必须处理每个 DOM 元素的各种怪癖和要求。Aprender 将仅处理创建应用程序所需的元素。
第一项工作是列出我们的搜索应用程序的用户故事:
- 作为用户,我可以查看搜索应用
- 作为用户,我可以选择 API
- 作为用户,选择 API 后,我可以查看解释该 API 的信息以及可以使用的搜索参数
- 作为用户,我可以在搜索字段中输入内容并点击搜索按钮
- 作为用户,点击搜索按钮后我可以查看搜索结果
- 作为用户,我可以清除搜索结果
我们还将重构我们的演示应用程序以反映新的目标:
const aprender = require('../src/aprender');
const Button = aprender.createElement('button', {
attrs: {
type: 'submit'
},
children: ['Search']
}
);
const Search = aprender.createElement('input', { attrs: { type: 'search' }});
const Form = aprender.createElement('form', {
attrs: {
id: 'form',
onsubmit: (e) => {
e.preventDefault();
console.log('I am being submitted..')
}
},
children: [
Search,
Button
]
},
);
const App = aprender.render(Form);
aprender.mount(App, document.getElementById('app'));
上述代码中唯一新添加的是分配给onsubmit
表单attrs
对象属性的函数,我们接下来将探讨这一功能。
事件和 DOM 元素
向 DOM 元素添加事件处理功能非常简单。您可以使用诸如 的方法获取元素的引用getElementById()
,然后使用该addEventListener
方法设置在触发事件时调用的函数。
对于 Aprender 的事件处理功能,我们将从Mithril中汲取灵感。在我们的框架中,该renderElement
函数负责将属性附加到 DOM 元素,因此我们将事件代码放在那里:
const EventDictionary = {
handleEvent (evt) {
const eventHandler = this[`on${evt.type}`];
const result = eventHandler.call(evt.currentTarget, evt);
if (result === false) {
evt.preventDefault();
evt.stopPropagation();
}
}
}
function renderElement({ type, attrs, children }) {
const $el = document.createElement(type);
for (const [attribute, value] of Object.entries(attrs)) {
if (attribute[0] === 'o' && attribute[1] === 'n') {
const events = Object.create(EventDictionary);
$el.addEventListener(attribute.slice(2), events)
events[attribute] = value;
}
$el.setAttribute(attribute, value);
}
for (const child of children) {
$el.appendChild(render(child));
}
return $el;
};
我们只关心注册on-event
处理程序。Mithril和Preact都会通过检查属性名称的前两个字母是否分别以 o 和n开头来筛选这些事件类型。我们也照做。将事件名称作为其第一个参数,将函数或对象作为第二个参数。通常,它的写法如下:addEventListener
aDomElement.addEventListener('click,' () => console.log('do something'));
与 Mithril 类似,我们将使用一个对象,但它的创建方式有所不同。Mithril 的源代码中有一些注释,解释了他们的方法,并深刻地解释了框架作者在构建工具时所考虑的因素。
new EventDict()
首先,与我们的方法不同,事件对象是使用构造函数模式创建的Object.create(EventDictionary)
。在 Mithril 中,每当调用时创建的对象都会被以下代码new EventDict()
阻止继承:Object.prototype
EventDict.prototype = Object.create(null);
Mithril 维护者Isiah Meadows表示,这样做的原因之一是为了防止第三方向 中添加诸如onsubmit
或onclick
之类的属性Object.prototype
。
我们不必担心这一点,因此我们创建一个名为 的对象EventDictionary
来实现该EventListener
接口。然后,我们使用Object.create
指定EventDictionary
为原型,并创建一个对象,该对象将保存相关 DOM 元素的处理程序列表on-event
。最后,将属性值赋给新创建的对象。
此后,每当在相关的 DOM 元素上触发事件时,都会调用handleEvent
on 函数并传入事件对象。如果事件存在于事件对象上,则使用 调用它,我们将 DOM 元素指定为上下文,并将事件对象作为唯一参数传递。如果我们的处理程序的返回值为,则该子句将停止浏览器的默认行为,并阻止事件传播。EventDictionary
call
this
false
result === false
有一篇非常深入的文章,详细解释了这两种创建对象Object.create
方法的区别。Stack Overflow 上的这个问题也对这两种模式提出了一些有趣的想法。new Func()
关于事件的一些信息
如果我们运行应用程序,应该会看到一个输入字段,旁边有一个按钮。输入一些文本并点击按钮,I am being submitted..
控制台就会登录。如果我们没记错的话,表单函数的第一行onsubmit
是:
const Form = aprender.createElement('form', {
// ...
onsubmit: (e) => {
e.preventDefault();
console.log('I am being submitted..')
}
// ...
},
);
它是什么e.preventDefault()
?它做什么?表单onsubmit
处理程序被调用时的默认行为是将其数据发送到服务器并刷新页面。显然,这并不总是理想的。例如,您可能希望在发送数据之前验证数据,或者您可能希望通过其他方法发送数据。该preventDefault
函数是 Event 对象上的一个方法,它告诉浏览器阻止默认操作。但是,如果您以编程方式创建如下表单:
const form = document.createElement('form');
form.action = 'https://google.com/search';
form.method = 'GET';
form.innerHTML = '<input name="q" value="JavaScript">';
document.body.append(form);
通过调用提交表单form.submit()
不会生成submit
事件,数据也会被发送。
我们要研究的下一个事件是关于输入字段的。我们需要捕获输入值,以便使用它向所选的 API 发出请求。为此,我们有几个事件可供选择:oninput
、onblur
和onchange
。
当获得焦点的元素失去焦点时,会触发该onblur
事件。在我们的例子中,只有当用户的焦点离开输入字段时,它才会触发。onchange
当用户更改表单控件(如输入字段)的值,然后将焦点从该控件上移开oninput
时,会触发该事件。最后, 会在每次值发生变化时触发。这意味着每次击键都会触发该事件。我们将使用该oninput
事件,因为它最适合我们的目的。onchange
同样,onblur
如果我们想在每次搜索元素失去焦点时验证输入,它也会很有用。注意:如果您像我一样,在刚开始使用 React 时对事件不太了解,那么您会惊讶地发现 React 的onchange
事件的行为与 完全相同oninput
。甚至还有一个关于它的问题。
我们的最后一步是select
为 API 选项列表创建一个元素,并onchange
为其附加一个事件处理程序。这样,我们的应用程序代码应该如下所示:
const aprender = require('../src/aprender');
const Button = aprender.createElement('button', {
attrs: {
type: 'submit'
},
children: ['Search']
}
);
const Search = aprender.createElement('input', {
attrs: {
type: 'search',
oninput: (e) => console.log(e.target.value)
}
});
const Form = aprender.createElement('form', {
attrs: {
id: 'form',
onsubmit: (e) => {
e.preventDefault();
console.log('I am being submitted..')
}
},
children: [
Search,
Button
]
},
);
const Dropdown = aprender.createElement('select', {
attrs: {
onchange: (e) => console.log(e.target.value)
},
children: [
aprender.createElement('option', {
children: ['--Please select an API--']
}),
aprender.createElement('option', {
children: ['API 1']
}),
aprender.createElement('option', {
children: ['API 2']
})
]
});
const SelectAPI = aprender.createElement('div', {
children: [
aprender.createElement('h2', { children: ['Select API: ']}),
Dropdown
]
})
const Container = aprender.createElement('div', {
children: [
SelectAPI,
Form
]
})
const App = aprender.render(Container);
aprender.mount(App, document.getElementById('app'));
概括
我们已经完成了我们的第一个用户故事:
- 作为用户,我可以查看搜索应用
在下一篇文章中我们将讨论:
- 作为用户,我可以选择一个 API。
此功能将向我们揭示UI 框架存在的核心原因——保持用户界面与应用程序状态同步。
链接链接:https://dev.to/carlmungazi/learn-javascript-by-building-a-ui-framework-part-5-adding-events-to-dom-elements-3kod