个性化阅读
专注于IT技术分析

在Vanilla JS中模拟React和JSX

本文概述

很少有人不喜欢框架, 但是即使你是其中之一, 也应注意并采用使生活更轻松的功能。

过去, 我反对使用框架。但是, 最近, 我在一些项目中有使用React和Angular的经验。头几次, 我打开代码编辑器并开始用Angular编写代码, 这感觉很奇怪而且很不自然。特别是经过十多年的编码而不使用任何框架。一段时间后, 我决定致力于学习这些技术。很快就发现了一个很大的区别:操作DOM非常容易, 在需要时可以很容易地调整节点的顺序, 并且不需要花很多页面代码来构建UI。

尽管我仍然更喜欢不附加到框架或体系结构上的自由, 但是我不能忽略这样的事实, 即在一个框架中创建DOM元素会更加方便。因此, 我开始研究模仿香草JS体验的方法。我的目标是从React中提取一些想法, 并演示如何在纯JavaScript(通常称为Vanilla JS)中实现相同的原理, 从而使开发人员的生活更加轻松。为此, 我们构建一个简单的应用程序来浏览GitHub项目。

简单的GitHub搜索应用

我们正在构建的应用程序。

无论我们使用JavaScript构建前端的哪种方式, 我们都将访问和操作DOM。对于我们的应用程序, 我们将需要构造每个存储库的表示形式(缩略图, 名称和描述), 并将其作为列表元素添加到DOM中。我们将使用GitHub Search API来获取结果。而且, 由于我们在谈论JavaScript, 因此我们来搜索JavaScript存储库。查询API时, 将获得以下JSON响应:

{
    "total_count": 398819, "incomplete_results": false, "items": [
        {
            "id": 28457823, "name": "freeCodeCamp", "full_name": "freeCodeCamp/freeCodeCamp", "owner": {
                "login": "freeCodeCamp", "id": 9892522, "avatar_url": "https://avatars0.githubusercontent.com/u/9892522?v=4", "gravatar_id": "", "url": "https://api.github.com/users/freeCodeCamp", "site_admin": false
            }, "private": false, "html_url": "https://github.com/freeCodeCamp/freeCodeCamp", "description": "The https://freeCodeCamp.org open source codebase "+
                          "and curriculum. Learn to code and help nonprofits.", // more omitted information
        }, //...
    ]
}

React的方法

React使将HTML元素写入页面变得非常简单, 这是我用纯JavaScript编写组件时一直希望拥有的功能之一。 React使用JSX, 它与常规HTML非常相似。

但是, 那不是浏览器读取的内容。

在幕后, React将JSX转换为对React.createElement函数的一堆调用。让我们来看一个使用GitHub API中的一项的JSX示例, 并查看其含义。

<div className="repository">
  <div>{item.name}</div>
  <p>{item.description}</p>
  <img src={item.owner.avatar_url} />
</div>;
;
React.createElement(
    "div", { className: "repository" }, React.createElement(
        "div", null, item.name
    ), React.createElement(
        "p", null, item.description
    ), React.createElement(
        "img", { src: item.owner.avatar_url }
    )
);

JSX非常简单。你编写常规的HTML代码, 并通过添加大括号从对象中插入数据。将执行括号内的JavaScript代码, 并将其值插入到结果DOM中。 JSX的优点之一是React可以创建一个虚拟DOM(页面的虚拟表示)来跟踪更改和更新。每当信息更新时, React都不会修改整个HTML, 而是修改页面的DOM。这是React所要解决的主要问题之一。

jQuery方法

开发人员过去经常使用jQuery。我想在此提及它, 因为它仍然很流行, 并且因为它非常接近纯JavaScript中的解决方案。 jQuery通过查询DOM来获取对DOM节点(或DOM节点集合)的引用。它还使用各种功能来包装该引用, 以修改其内容。

尽管jQuery有自己的DOM构造工具, 但我经常在野外看到的只是HTML串联。例如, 我们可以通过调用html()函数将HTML代码插入选定的节点。根据jQuery文档, 如果我们想使用类demo-container更改div节点的内容, 我们可以这样做:

$( "div.demo-container" ).html( "<p>All new content.<em>You bet!</em></p>" );

这种方法使创建DOM元素变得容易。但是, 当我们需要更新节点时, 我们需要查询所需的节点, 或者(通常)在需要更新时退回到重新创建整个代码段。

DOM API方法

浏览器中的JavaScript具有内置的DOM API, 使我们可以直接访问创建, 修改和删除页面中的节点。这在React的方法中得到了体现, 并且通过使用DOM API, 我们比该方法的优势更近了一步。我们仅修改实际需要更改的页面元素。但是, React也会跟踪单独的虚拟DOM。通过比较虚拟和实际DOM之间的差异, React能够确定哪些部分需要修改。

这些额外的步骤有时很有用, 但并非总是如此, 直接操作DOM可能更有效。我们可以使用_document.createElement_函数创建新的DOM节点, 该函数将返回对创建的节点的引用。跟踪这些引用为我们提供了一种简单的方法, 使其仅修改包含需要更新的零件的节点。

使用与JSX示例相同的结构和数据源, 我们可以通过以下方式构造DOM:

var item = document.createElement('div');
item.className = 'repository';

var nameNode = document.createElement('div');
nameNode.innerHTML = item.name
item.appendChild(nameNode);

var description = document.createElement('p');
description.innerHTML = item.description;
item.appendChild(description );

var image = new Image();
Image.src = item.owner.avatar_url;
item.appendChild(image);

document.body.appendChild(item);

如果你唯一想到的是代码执行的效率, 那么这种方法非常好。但是, 效率不仅仅以执行速度来衡量, 还以维护, 易扩展性和可塑性来衡量。这种方法的问题在于它非常冗长, 有时令人费解。即使我们只是构造一个基本结构, 我们也需要编写一堆函数调用。第二个大缺点是创建和跟踪的变量数量庞大。假设你正在使用的组件包含30个DOM元素, 则需要创建和使用30个不同的DOM元素和变量。你可以重用其中的一些, 并且以可维护性和可塑性为代价进行一些处理, 但是它可能会变得非常杂乱, 非常迅速。

另一个明显的缺点是由于需要编写的代码行数众多。随着时间的流逝, 将元素从一个父对象移动到另一个父元素的难度越来越大。我从React真的很欣赏这件事。我可以查看JSX语法, 并在几秒钟内获得包含哪个节点, 位于何处, 并根据需要进行更改。而且, 虽然起初似乎没什么大不了, 但大多数项目都有不断的变化, 这会让你寻找更好的方法。

可用的解决方案

直接使用DOM可以工作并完成工作, 但是这也使得构建页面非常冗长, 尤其是当我们需要添加HTML属性和嵌套节点时。因此, 该想法将是捕获使用JSX之类的技术的一些好处, 并使我们的生活更简单。我们尝试复制的优势如下:

  1. 用HTML语法编写代码, 以便DOM元素的创建变得易于阅读和修改。
  2. 由于我们没有使用类似React的虚拟DOM, 因此我们需要一种简单的方法来指示和跟踪我们感兴趣的节点。

这是一个简单的函数, 可以使用HTML代码段完成此操作。

Browser.DOM = function (html, scope) {
   // Creates empty node and injects html string using .innerHTML 
   // in case the variable isn't a string we assume is already a node
   var node;
   if (html.constructor === String) {
       var node = document.createElement('div');
       node.innerHTML = html;
   } else {
       node = html;
   }

   // Creates of uses and object to which we will create variables
   // that will point to the created nodes

   var _scope = scope || {};

   // Recursive function that will read every node and when a node
   // contains the var attribute add a reference in the scope object

   function toScope(node, scope) {
       var children = node.children;
       for (var iChild = 0; iChild < children.length; iChild++) {
           if (children[iChild].getAttribute('var')) {
               var names = children[iChild].getAttribute('var').split('.');
               var obj = scope;
               while (names.length > 0)
               {
                   var _property = names.shift();
                   if (names.length == 0)
                   {
                       obj[_property] = children[iChild];
                   }
                   else
                   {
                       if (!obj.hasOwnProperty(_property)){
                           obj[_property] = {};
                       }
                       obj = obj[_property];
                   }
               }
           }
           toScope(children[iChild], scope);
       }
   }

   toScope(node, _scope);

   if (html.constructor != String) {
       return html;
   }
   // If the node in the highest hierarchy is one return it

   if (node.childNodes.length == 1) {
    // if a scope to add node variables is not set
    // attach the object we created into the highest hierarchy node
    
       // by adding the nodes property.
       if (!scope) {
           node.childNodes[0].nodes = _scope;
       }
       return node.childNodes[0];
   }

   // if the node in highest hierarchy is more than one return a fragment
   var fragment = document.createDocumentFragment();
   var children = node.childNodes;
   
   // add notes into DocumentFragment
   while (children.length > 0) {
       if (fragment.append){
           fragment.append(children[0]);
       }else{
          fragment.appendChild(children[0]);
       }
   }

   fragment.nodes = _scope;
   return fragment;
}

这个想法很简单但是很强大。我们将要创建的HTML作为字符串发送给函数, 在HTML字符串中, 向要为其创建引用的节点添加var属性。第二个参数是一个对象, 这些引用将存储在该对象中。如果未指定, 我们将在返回的节点或文档片段上创建” nodes”属性(如果最高层级节点不止一个)。一切都用不到60行代码完成。

该功能分为三个步骤:

  1. 创建一个新的空节点, 并在该新节点中使用innerHTML创建整个DOM结构。
  2. 遍历节点, 如果var属性存在, 则在范围对象中添加一个属性, 该属性指向具有该属性的节点。
  3. 返回层次结构中的最高节点, 如果有多个节点, 则返回一个文档片段。

那么, 用于呈现示例的代码现在看起来如何?

var UI = {};
var template = '';
template += '<div class="repository">'
template += ' <div var="name"></div>';
template += ' <p var="text"></p>'
template += ' <img var="image"/>'
template += '</div>';

var item = Browser.DOM(template, UI);

UI.name.innerHTML = data.name;
UI.text.innerHTML = data.description;
UI.image.src = data.owner.avatar_url;

首先, 我们定义对象(UI), 我们将在其中存储对创建的节点的引用。然后, 我们将构成要使用的HTML模板作为字符串, 用” var”属性标记目标节点。之后, 我们使用模板和将存储引用的空对象调用函数Browser.DOM。最后, 我们使用存储的引用将数据放置在节点内。

这种方法还将构建DOM结构和将数据插入单独的步骤分开, 这有助于保持代码的组织性和结构良好。这使我们能够分别创建DOM结构并在数据可用时填充(或更新)数据。

总结

尽管我们中的某些人不喜欢切换到框架并移交控制权的想法, 但重要的是, 我们必须认识到框架带来的好处。它们如此受欢迎是有原因的。

尽管框架可能并不总是适合你的风格或需求, 但是可以采用, 模拟甚至有时与框架分离一些功能和技术。有些事情在翻译中总是会丢失, 但是可以获取和使用很多东西, 而这只花费了框架成本的一小部分。

赞(0)
未经允许不得转载:srcmini » 在Vanilla JS中模拟React和JSX

评论 抢沙发

评论前必须登录!