使用原生 JavaScript 构建响应式 UI – 第 2 部分:基于类的组件

Avatar of Brandon Smith
Brandon Smith

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

第 1 部分 中,我介绍了各种函数式风格的技术,用于根据一些 JavaScript 数据干净地渲染 HTML。我们将 UI 分解成组件函数,每个函数根据一些数据返回一段标记。然后我们将它们组合成视图,可以通过进行单个函数调用从新数据重建这些视图。

这是加分环节。在本篇文章中,目标是尽可能接近完整的基于类的 React 组件语法,使用原生 JavaScript(即不使用任何库/框架)。我想声明这里的一些技术并不是非常实用,但我认为它们仍然可以让我们有趣地探索 JavaScript 在近年来的发展,以及 React 究竟为我们做了什么。

文章系列

  1. 纯函数式风格
  2. 基于类的组件(您当前所在位置!)

从函数到类

让我们继续使用我们在第一篇文章中使用的相同示例:博客。我们的函数式 BlogPost 组件如下所示

var blogPostData = {
  author: 'Brandon Smith',
  title: 'A CSS Trick',
  body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
};

function BlogPost(postData) {
  return `<div class="post">
            <h1>${postData.title}</h1>
            <h3>By ${postData.author}</h3>
            <p>${postData.body}</p>
          </div>`;
}

document.querySelector('body').innerHTML = BlogPost(blogPostData);

在基于类的组件中,我们仍然需要相同的渲染函数,但我们将把它作为类的某个方法来使用。类的实例将保存自己的 BlogPost 数据,并知道如何渲染自身。

var blogPostData = {
  author: 'Brandon Smith',
  title: 'A CSS Trick',
  body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
};

class BlogPost {

  constructor(props) {
    this.state = {
      author: props.author,
      title: props.title,
      body: props.body
    }
  }

  render() {
    return `<div class="post">
              <h1>${this.state.title}</h1>
              <h3>By ${this.state.author}</h3>
              <p>${this.state.body}</p>
            </div>`;
  }

}

var blogPostComponent = new BlogPost(blogPostData);

document.querySelector('body').innerHTML = blogPostComponent.render();

修改状态

基于类(面向对象)编码风格的优势在于它允许封装状态。让我们假设我们的博客站点允许管理员用户直接在读者查看的页面上编辑他们的博客文章。BlogPost 组件的实例能够维护自己的状态,与外部页面和/或其他 BlogPost 实例分开。我们可以通过方法更改状态

class BlogPost {

  constructor(props) {
    this.state = {
      author: props.author,
      title: props.title,
      body: props.body
    }
  }

  render() {
    return `<div class="post">
              <h1>${this.state.title}</h1>
              <h3>By ${this.state.author}</h3>
              <p>${this.state.body}</p>
            </div>`;
  }

  setBody(newBody) {
    this.state.body = newBody;
  }

}

但是,在任何现实场景中,此状态更改都必须由网络请求或 DOM 事件触发。让我们来探索后者,因为它是最常见的情况。

处理事件

通常,监听 DOM 事件非常简单——只需使用 element.addEventListener()——但我们的组件仅计算为字符串,而不是实际的 DOM 元素,这使得它变得更加棘手。我们没有可以绑定的元素,并且仅仅将函数调用放在 onchange 中是不够的,因为它不会绑定到我们的组件实例。我们必须以某种方式从全局作用域(代码片段将在其中计算)引用我们的组件。这是我的解决方案

document.componentRegistry = { };
document.nextId = 0;

class Component {
  constructor() {
    this._id = ++document.nextId;
    document.componentRegistry[this._id] = this;
  }
}

class BlogPost extends Component {

  constructor(props) {
    super();

    this.state = {
      author: props.author,
      title: props.title,
      body: props.body
    }
  }

  render() {
    return `<div class="post">
              <h1>${this.state.title}</h1>
              <h3>By ${this.state.author}</h3>
              <textarea onchange="document.componentRegistry[${this._id}].setBody(this.value)">
                ${this.state.body}
              </textarea>
            </div>`;
  }

  setBody(newBody) {
    this.state.body = newBody;
  }

}

好的,这里有很多内容。

引用组件实例

首先,我们必须从 HTML 字符串中获取对组件的当前实例的引用。React 能够更轻松地做到这一点,因为 JSX 实际上会转换为一系列函数调用,而不是 HTML 字符串。这允许代码直接传入 this,并且 JavaScript 对象的引用会被保留。另一方面,我们必须序列化 JavaScript 字符串以插入到 HTML 字符串中。因此,对组件实例的引用必须以某种方式表示为字符串。为了实现这一点,我们在构造时间为每个组件实例分配一个唯一的 ID。您不必将此行为放在父类中,但它是继承的良好用途。本质上发生的事情是,每当构造 BlogPost 实例时,它都会创建一个新的 ID,将其存储为自身的一个属性,并在该 ID 下在 document.componentRegistry 中注册自身。现在,任何地方的任何 JavaScript 代码都可以检索我们的对象,前提是它拥有该 ID。我们可能编写的其他组件也可以扩展 Component 类并自动获取自己的唯一 ID。

调用方法

因此,我们可以从任何任意 JavaScript 字符串中检索组件实例。接下来,我们需要在事件(onchange)触发时在其上调用方法。让我们隔离以下代码片段并逐步了解正在发生的事情

<textarea onchange="document.componentRegistry[${this._id}].setBody(this.value)">
  ${this.state.body}
</textarea>

您可能熟悉通过将代码放在 on_______ HTML 属性中来连接事件监听器。当事件触发时,其中的代码将被计算并运行。

document.componentRegistry[${this._id}] 在组件注册表中查找并通过其 ID 获取组件实例。请记住,所有这些都在模板字符串中,因此 ${this._id} 计算为当前组件的 ID。生成的 HTML 将如下所示

<textarea onchange="document.componentRegistry[0].setBody(this.value)">
  Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
</textarea>

我们在该对象上调用方法,将 this.value(其中 this 是事件发生的元素;在我们的例子中,是 <textarea>)作为 newBody 传递。

响应状态更改进行更新

我们的 JavaScript 变量的值发生了变化,但我们需要实际执行重新渲染才能看到其值反映在页面上。在我们之前的文章中,我们这样重新渲染

function update() {
  document.querySelector('body').innerHTML = BlogPost(blogPostData);
}

这是我们必须对类风格组件进行一些调整的另一个地方。我们不希望在每次重新渲染时都丢弃并重建组件实例;我们只需要重建 HTML 字符串。需要保留内部状态。因此,我们的对象将单独存在,我们只需再次调用 render()

var blogPost = new BlogPost(blogPostData);

function update() {
  document.querySelector('body').innerHTML = blogPost.render();
}

然后,每当我们修改状态时,我们都必须调用 update()。这是 React 为我们透明地做的另一件事;它的 setState() 函数修改状态,并触发该组件的重新渲染。我们必须手动执行此操作

// ...
setBody(newBody) {
  this.state.body = newBody;
  update();
}
// ...

请注意,即使当我们有一个复杂的嵌套组件结构时,也永远只有一个 update() 函数,并且它将始终应用于根组件。

子组件

React(以及几乎所有其他 JavaScript 框架)区分构成组件的元素和组件以及其子组件。子组件可以从外部传入,允许我们编写自定义组件,这些组件是其他任意内容的容器。我们也可以做到这一点。

class BlogPost extends Component {

  constructor(props, children) {
    super();

    this.children = children;
    this.state = {
      author: props.author,
      title: props.title,
      body: props.body
    }
  }

  render() {
    return `<div class="post">
              <h1>${this.state.title}</h1>
              <h3>By ${this.state.author}</h3>
              <textarea onchange="document.componentRegistry[${this._id}].setBody(this.value)">
                ${this.state.body}
              </textarea>
              <div>
                ${this.children.map((child) => child.render()).join('')}
              </div>
            </div>`;
  }

  setBody(newBody) {
    this.state.body = newBody;
    update();
  }

}

这允许我们编写如下用法代码

var adComponents = ...;
var blogPost = new BlogPost(blogPostData, adComponents);

这会将组件插入标记中的指定位置。

总结

React 看起来很简单,但它做了很多微妙的事情来让我们的生活更轻松。最明显的是性能;只渲染状态更新的组件,并最大限度地减少执行的 DOM 操作。但一些不那么明显的事情也很重要。

其中之一是,通过进行细粒度的 DOM 更改而不是完全重建 DOM,React 保留了一些使用我们的技术时会丢失的自然 DOM 状态。当我们放弃 DOM 并重建它时,CSS 过渡、用户调整大小的文本区域、焦点以及输入中的光标位置都会丢失。对于我们的用例来说,这是可行的。但在很多情况下,可能不行。当然,我们可以自己进行 DOM 修改,但那样我们就回到了原点,并且失去了我们的声明式函数语法。

React 使我们能够在更易于维护的声明式风格中编写代码,同时获得 DOM 修改的优势。我们已经证明原生 JavaScript 可以做到这两点,但它无法兼得。

文章系列

  1. 纯函数式风格
  2. 基于类的组件(您当前所在位置!)