通过构建 UI 框架学习 JavaScript:第 5 部分 - 向 Dom 元素添加事件

2025-06-08

通过构建 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'));
Enter fullscreen mode Exit fullscreen mode

上述代码中唯一新添加的是分配给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;
};
Enter fullscreen mode Exit fullscreen mode

我们只关心注册on-event处理程序。MithrilPreact都会通过检查属性名称的前两个字母是否分别以 o 和n开头来筛选这些事件类型。我们也照做。将事件名称作为其第一个参数,将函数或对象作为第二个参数。通常,它的写法如下:addEventListener

aDomElement.addEventListener('click,' () => console.log('do something'));
Enter fullscreen mode Exit fullscreen mode

与 Mithril 类似,我们将使用一个对象,但它的创建方式有所不同。Mithril 的源代码中有一些注释,解释了他们的方法,并深刻地解释了框架作者在构建工具时所考虑的因素。

new EventDict()首先,与我们的方法不同,事件对象是使用构造函数模式创建的Object.create(EventDictionary)。在 Mithril 中,每当调用时创建的对象都会被以下代码new EventDict()阻止继承:Object.prototype

EventDict.prototype = Object.create(null);
Enter fullscreen mode Exit fullscreen mode

Mithril 维护者Isiah Meadows表示,这样做的原因之一是为了防止第三方向 中添加诸如onsubmitonclick之类的属性Object.prototype

我们不必担心这一点,因此我们创建一个名为 的对象EventDictionary来实现该EventListener接口。然后,我们使用Object.create指定EventDictionary为原型,并创建一个对象,该对象将保存相关 DOM 元素的处理程序列表on-event。最后,将属性值赋给新创建的对象。

此后,每当在相关的 DOM 元素上触发事件时,都会调用handleEventon 函数并传入事件对象。如果事件存在于事件对象上,则使用 调用它,我们将 DOM 元素指定为上下文,并将事件对象作为唯一参数传递。如果我们的处理程序的返回值为,则该子句将停止浏览器的默认行为,并阻止事件传播EventDictionarycallthisfalseresult === 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..') 
      }
    // ...
  },
);
Enter fullscreen mode Exit fullscreen mode

它是什么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);
Enter fullscreen mode Exit fullscreen mode

通过调用提交表单form.submit()不会生成submit事件,数据也会被发送。

我们要研究的下一个事件是关于输入字段的。我们需要捕获输入值,以便使用它向所选的 API 发出请求。为此,我们有几个事件可供选择:oninputonbluronchange

当获得焦点的元素失去焦点时,会触发该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'));
Enter fullscreen mode Exit fullscreen mode

概括

我们已经完成了我们的第一个用户故事:

  • 作为用户,我可以查看搜索应用

在下一篇文章中我们将讨论:

  • 作为用户,我可以选择一个 API

此功能将向我们揭示UI 框架存在的核心原因——保持用户界面与应用程序状态同步。

链接链接:https://dev.to/carlmungazi/learn-javascript-by-building-a-ui-framework-part-5-adding-events-to-dom-elements-3kod
PREV
使用 Supabase 在 Flutter 中构建一个简单的杂货店应用程序介绍演示数据库设计关系 Supabase 设置创建表 Flutter 数据模型 Flutter 设置 Supabase 查询身份验证 Supabase 服务摘要
NEXT
理解设计模式:抽象工厂抽象工厂:基本思想抽象工厂模式:何时使用抽象工厂模式:优点和缺点抽象工厂模式示例示例 1:抽象工厂模式的基本结构示例 2 - 创建视频游戏的英雄装备结论