现代 js 框架存在的根本原因

2018/06/05 · JavaScript
· 1 评论 ·
框架

原文出处: [Alberto

React 性能优化大挑战:一次理解 Immutable data 跟 shouldComponentUpdate

2018/01/08 · JavaScript
· ReactJS

原文出处: TechBridge
Weekly/huli   

前阵子正在重构公司的专案,试了一些东西之后发现自己对于 React
的渲染机制其实不太了解,不太知道 render
什麽时候会被触发。而后来我发现不只我这样,其实还有满多人对这整个机制不太熟悉,因此决定写这篇来分享自己的心得。其实不知道怎麽优化倒还好,更惨的事情是你自以为在优化,其实却在拖慢效能,而根本的原因就是对
React 的整个机制还不够熟。被「优化」过的 component
反而还变慢了!这个就严重了。因此,这篇文章会涵盖到下面几个主题:

  1. Component 跟 PureComponent 的差异
  2. shouldComponentUpdate 的作用
  3. React 的渲染机制
  4. 为什麽要用 Immutable data structures

为了判别你到底对以上这些理解多少,我们马上进行几个小测验!有些有陷阱,请睁大眼睛看清楚啦!

React.Component 损害了复用性?

2016/09/07 · 基础技术 ·
binding.scala,
data-binding,
React,
scala.js

本文作者: 伯乐在线 –
ThoughtWorks
。未经作者许可,禁止转载!
欢迎加入伯乐在线 专栏作者。

本系列的上一篇文章《为什么ReactJS不适合复杂的前端项目》列举了前端开发中的种种痛点。本篇文章中将详细探讨其中“复用性”痛点。我们将用原生
DHTML API 、 ReactJS 和 Binding.scala
实现同一个需要复用的标签编辑器,然后比较三个标签编辑器哪个实现难度更低,哪个更好用。

Gimeno]()   译文出处:[众成翻译

sea_ljf]()   

澳门微尼斯人手机版 1

我曾见过很多很多人盲目地使用(前端)框架,如 React,Angular 或
Vue等等。这些框架提供了许多有意思的东西,然而通常人们(自以为)使用框架是因为:

  • 它们支持组件化;
  • 它们有强大的社区支持;
  • 它们有很多(基于框架的)第三方库来解决问题;
  • 它们有很多(很好的)第三方组件;
  • 它们有浏览器扩展工具来帮助调试;
  • 它们适合做单页应用。

澳门微尼斯人手机版 2

但这些都不是使用框架的根本原因。

最最本质的原因是:

澳门微尼斯人手机版 3

(UI 与状态同步非常困难)

React 小测验

标签编辑器的功能需求

在InfoQ的许多文章都有标签。比如本文的标签是“binding.scala”、“data-binding”、“scala.js”。

假如你要开发一个博客系统,你也希望博客作者可以添加标签。所以你可能会提供标签编辑器供博客作者使用。

如图所示,标签编辑器在视觉上分为两行。

澳门微尼斯人手机版 4

第一行展示已经添加的所有标签,每个标签旁边有个“x”按钮可以删除标签。第二行是一个文本框和一个“Add”按钮可以把文本框的内容添加为新标签。每次点击“Add”按钮时,标签编辑器应该检查标签是否已经添加过,以免重复添加标签。而在成功添加标签后,还应清空文本框,以便用户输入新的标签。

除了用户界面以外,标签编辑器还应该提供 API 。标签编辑器所在的页面可以用
API 填入初始标签,也可以调用 API
随时增删查改标签。如果用户增删了标签,应该有某种机制通知页面的其他部分。

是的,就是这原因,让我们来看看为什么

假设你正在设计这样一个 Web
应用:用户可以通过群发电子邮件来邀请其他人(参加某活动)。UX/UI
设计师设计如下:(在用户填写任何邮箱地址之前,)有一个空白状态,并为此添加一些帮助信息;(当用户填写邮箱之后,)展示邮箱的地址,每个地址的右侧均有一个按钮用于删除对应的地址。

澳门微尼斯人手机版 5

这个表单的状态,可以被设计为一个数组,里面包含若干对象,对象由邮箱地址和唯一标识组成。开始的时候,数组为空。当(用户)输入邮箱地址并按下回车键之后,往数组中添加一项并更新
UI
。当用户点击删除按钮时,删除(数组中对应的)邮箱地址并更新
UI
。你感觉到了吗?每当你改变状态时,你都需要更新 UI

(你可能会说:)那又怎样?好吧,让我们看看如何在不用框架的情况下实现它:

第一题

以下程式码是个很简单的网页,就一个按钮跟一个叫做Content的元件而已,而按钮按下去之后会改变App这个
component 的 state。

JavaScript

class Content extends React.Component { render () { console.log(‘render
content!’); return <div>Content</div> } } class App extends
React.Component { handleClick = () => { this.setState({ a: 1 }) }
render() { console.log(‘render App!’); return ( <div> <button
onClick={this.handleClick}>setState</button> <Content />
</div> ); } } ReactDOM.render( <App />,
document.getElementById(‘container’) );

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Content extends React.Component {
  render () {
    console.log(‘render content!’);
    return <div>Content</div>
  }
}
  
class App extends React.Component {
  handleClick = () => {
    this.setState({
      a: 1
    })
  }
  render() {
    console.log(‘render App!’);
    return (
      <div>
        <button onClick={this.handleClick}>setState</button>
        <Content />
      </div>
    );
  }
}
  
ReactDOM.render(
  <App />,
  document.getElementById(‘container’)
);

请问:当你按下按钮之后,console 会输出什麽?

A. 什麽都没有(App 跟 Content 的 render function 都没被执行到)
B. 只有 render App!(只有 App 的 render function 被执行到)
C. render App! 以及 render content!(两者的 render function
都被执行到)

原生 DHTML 版

首先,我试着不用任何前端框架,直接调用原生的 DHTML API
来实现标签编辑器,代码如下:

JavaScript

<!DOCTYPE html> <html> <head> <script> var tags
= []; function hasTag(tag) { for (var i = 0; i < tags.length; i++)
{ if (tags[i].tag == tag) { return true; } } return false; } function
removeTag(tag) { for (var i = 0; i < tags.length; i++) { if
(tags[i].tag == tag) {
document.getElementById(“tags-parent”).removeChild(tags[i].element);
tags.splice(i, 1); return; } } } function addTag(tag) { var element =
document.createElement(“q”); element.textContent = tag; var removeButton
= document.createElement(“button”); removeButton.textContent = “x”;
removeButton.onclick = function (event) { removeTag(tag); }
element.appendChild(removeButton);
document.getElementById(“tags-parent”).appendChild(element); tags.push({
tag: tag, element: element }); } function addHandler() { var tagInput =
document.getElementById(“tag-input”); var tag = tagInput.value; if (tag
&& !hasTag(tag)) { addTag(tag); tagInput.value = “”; } }
</script> </head> <body> <div
id=”tags-parent”></div> <div> <input id=”tag-input”
type=”text”/> <button onclick=”addHandler()”>Add</button>
</div> <script> addTag(“initial-tag-1”);
addTag(“initial-tag-2”); </script> </body> </html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;script&gt;
    var tags = [];
 
    function hasTag(tag) {
      for (var i = 0; i &lt; tags.length; i++) {
        if (tags[i].tag == tag) {
          return true;
        }
      }
      return false;
    }
 
    function removeTag(tag) {
      for (var i = 0; i &lt; tags.length; i++) {
        if (tags[i].tag == tag) {
          document.getElementById("tags-parent").removeChild(tags[i].element);
          tags.splice(i, 1);
          return;
        }
      }
    }
 
    function addTag(tag) {
      var element = document.createElement("q");
      element.textContent = tag;
      var removeButton = document.createElement("button");
      removeButton.textContent = "x";
      removeButton.onclick = function (event) {
        removeTag(tag);
      }
      element.appendChild(removeButton);
      document.getElementById("tags-parent").appendChild(element);
      tags.push({
        tag: tag,
        element: element
      });
    }
 
    function addHandler() {
      var tagInput = document.getElementById("tag-input");
      var tag = tagInput.value;
      if (tag &amp;&amp; !hasTag(tag)) {
        addTag(tag);
        tagInput.value = "";
      }
    }
  &lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;div id="tags-parent"&gt;&lt;/div&gt;
  &lt;div&gt;
    &lt;input id="tag-input" type="text"/&gt;
    &lt;button onclick="addHandler()"&gt;Add&lt;/button&gt;
  &lt;/div&gt;
  &lt;script&gt;
    addTag("initial-tag-1");
    addTag("initial-tag-2");
  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
 

为了实现标签编辑器的功能,我用了 45 行 JavaScript 代码来编写 UI
逻辑,外加若干的 HTML <div> 外加两行 JavaScript 代码填入初始化数据。

HTML 文件中硬编码了几个 <div>。这些<div>
本身并不是动态创建的,但可以作为容器,放置其他动态创建的元素。

代码中的函数来会把网页内容动态更新到这些 <div>
中。所以,如果要在同一个页面显示两个标签编辑器,id
就会冲突。因此,以上代码没有复用性。

就算用 jQuery 代替 DHTML
API,代码复用仍然很难。为了复用 UI ,jQuery
开发者通常必须额外增加代码,在 onload 时扫描整个网页,找出具有特定
class 属性的元素,然后对这些元素进行修改。对于复杂的网页,这些
onload 时运行的函数很容易就会冲突,比如一个函数修改了一个 HTML
元素,常常导致另一处代码受影响而内部状态错乱。

用原生(JS)实现相对复杂的 UI

以下代码很好地说明了使用原生 JavaScript 实现一个相对复杂的 UI
所需的工作量,使用像 jQuery 这样经典的库也需要差不多的工作量。

在这个例子中,HTML 负责创建静态页面,JavaScript
通过 document.createElement 动态改变(DOM
结构)。这引来了第一个问题:构建 UI 相关的 JavaScript
代码并不直观易读,我们将 UI 构建分为了两部分(译者注:应该是指 HTML与
JavaScript
两部分)。尽管我们使用了 innerHTML,可读性是增强了,但降低了(页面的)性能,同时可能存在
CSRF 漏洞。我们也可以使用模板引擎,但如果是大面积地修改
DOM,会面临两个问题:效率不高与需要重新绑定事件处理器。

但这也不是(不使用框架的)最大问题。最大的问题是每当状态发生改变时都要(手动)更新
UI
。每次状态更新时,都需要很多代码来改变
UI。当添加电子邮件地址时,只需要两行代码来更新状态,但要十三行代码更新
UI。(此例中)我们已经让 UI (界面与逻辑)尽可能简单了!!

澳门微尼斯人手机版 6

代码既难写又难理解,更麻烦的是它非常脆弱。假设我们需要(添加)同步服务器数据到邮件地址列表的功能,我们需要对比服务器返回结果与数组中数据的差异。这涉及对比所有数据的标识与内容,(当用户修改后,)可能需要在内存中保留一份标识相同但内容不同的数据。

为了高效地改变 DOM,我们需要编写大量点对点(译者注:指状态到
UI)的代码。但只要你犯下了很小的错误,UI
与状态将不再保持同步
:(可能会出现)丢失或呈现错误的信息、不再响应用户的操作,更糟糕的是触发了错误的动作(如点了删除按钮后删除了非对应的一项)。

因此,保持 UI 与状态同步,需要编写大量乏味且非常脆弱的代码。

第二题

以下程式码也很简单,分成三个元件:App、Table 跟 Row,由 App 传递 list 给
Table,Table 再用 map 把每一个 Row 都渲染出来。

JavaScript

class Row extends Component { render () { const {item, style} =
this.props; return ( <tr style={style}>
<td>{item.id}</td> </tr> ) } } class Table extends
Component { render() { const {list} = this.props; const itemStyle = {
color: ‘red’ } return ( <table> {list.map(item => <Row
key={item.id} item={item} style={itemStyle} />)} </table> ) } }
class App extends Component { state = { list:
Array(10000).fill(0).map((val, index) => ({id: index})) } handleClick
= () => { this.setState({ otherState: 1 }) } render() { const {list}
= this.state; return ( <div> <button
onClick={this.handleClick}>change state!</button> <Table
list={list} /> </div> ); } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Row extends Component {
  render () {
    const {item, style} = this.props;
    return (
      <tr style={style}>
        <td>{item.id}</td>
      </tr>
    )
  }
}
  
class Table extends Component {
  render() {
    const {list} = this.props;
    const itemStyle = {
      color: ‘red’
    }
    return (
      <table>
          {list.map(item => <Row key={item.id} item={item} style={itemStyle} />)}
      </table>
    )
  }
}
  
class App extends Component {
  state = {
    list: Array(10000).fill(0).map((val, index) => ({id: index}))
  }
  
  handleClick = () => {
    this.setState({
      otherState: 1
    })
  }
  
  render() {
    const {list} = this.state;
    return (
      <div>
        <button onClick={this.handleClick}>change state!</button>
        <Table list={list} />
      </div>
    );
  }
}

而这段程式码的问题就在于按下按钮之后,App的 render function
被触发,然后Table的 render function
也被触发,所以重新渲染了一次整个列表。

可是呢,我们点击按钮之后,list根本没变,其实是不需要重新渲染的,所以聪明的小明把
Table 从 Component 变成 PureComponent,只要 state 跟 props
没变就不会重新渲染,变成下面这样:

JavaScript

class Table extends PureComponent { render() { const {list} =
this.props; const itemStyle = { color: ‘red’ } return ( <table>
{list.map(item => <Row key={item.id} item={item} style={itemStyle}
/>)} </table> ) } } // 不知道什麽是 PureComponent
的朋友,可以想成他自己帮你加了下面的 function shouldComponentUpdate
(nextProps, nextState) { return !shallowEqual(this.props, nextProps) ||
!shallowEqual(this.state, nextState) }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Table extends PureComponent {
  render() {
    const {list} = this.props;
    const itemStyle = {
      color: ‘red’
    }
    return (
      <table>
          {list.map(item => <Row key={item.id} item={item} style={itemStyle} />)}
      </table>
    )
  }
}
  
// 不知道什麽是 PureComponent 的朋友,可以想成他自己帮你加了下面的 function
shouldComponentUpdate (nextProps, nextState) {
  return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState)
}

把 Table 从 Component 换成 PureComponent
之后,如果我们再做一次同样的操作,也就是按下change state按钮改变 App
的 state,这时候会提升效率吗?

A. 会,在这情况下 PureComponent 会比 Component 有效率
B. 不会,两者差不多
C. 不会,在这情况下 Component 会比 PureComponent 有效率

ReactJS 实现的标签编辑器组件

ReactJS 提供了可以复用的组件,即 React.Component 。如果用 ReactJS
实现标签编辑器,大概可以这样写:

JavaScript

class TagPicker extends React.Component { static defaultProps = {
changeHandler: tags => {} } static propTypes = { tags:
React.PropTypes.arrayOf(React.PropTypes.string).isRequired,
changeHandler: React.PropTypes.func } state = { tags: this.props.tags }
addHandler = event => { const tag = this.refs.input.value; if (tag
&& this.state.tags.indexOf(tag) == -1) { this.refs.input.value =
“”; const newTags = this.state.tags.concat(tag); this.setState({ tags:
newTags }); this.props.changeHandler(newTags); } } render() { return (
<section> <div>{ this.state.tags.map(tag => <q key={
tag }> { tag } <button onClick={ event => { const newTags =
this.state.tags.filter(t => t != tag); this.setState({ tags: newTags
}); this.props.changeHandler(newTags); }}>x</button> </q>
) }</div> <div> <input type=”text” ref=”input”/>
<button onClick={ this.addHandler }>Add</button>
</div> </section> ); } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class TagPicker extends React.Component {
 
  static defaultProps = {
    changeHandler: tags =&gt; {}
  }
 
  static propTypes = {
    tags: React.PropTypes.arrayOf(React.PropTypes.string).isRequired,
    changeHandler: React.PropTypes.func
  }
 
  state = {
    tags: this.props.tags
  }
 
  addHandler = event =&gt; {
    const tag = this.refs.input.value;
    if (tag &amp;&amp; this.state.tags.indexOf(tag) == -1) {
      this.refs.input.value = "";
      const newTags = this.state.tags.concat(tag);
      this.setState({
        tags: newTags
      });
      this.props.changeHandler(newTags);
    }
  }
 
  render() {
    return (
      &lt;section&gt;
        &lt;div&gt;{
          this.state.tags.map(tag =&gt;
            &lt;q key={ tag }&gt;
              { tag }
              &lt;button onClick={ event =&gt; {
                const newTags = this.state.tags.filter(t =&gt; t != tag);
                this.setState({ tags: newTags });
                this.props.changeHandler(newTags);
              }}&gt;x&lt;/button&gt;
            &lt;/q&gt;
          )
        }&lt;/div&gt;
        &lt;div&gt;
          &lt;input type="text" ref="input"/&gt;
          &lt;button onClick={ this.addHandler }&gt;Add&lt;/button&gt;
        &lt;/div&gt;
      &lt;/section&gt;
    );
  }
 
}
 

以上 51 行 ECMAScript 2015
代码实现了一个标签编辑器组件,即TagPicker。虽然代码量比 DHTML
版长了一点点,但复用性大大提升了。

如果你不用 ECMAScript 2015 的话,那么代码还会长一些,而且需要处理一些
JavaScript 的坑,比如在回调函数中用不了 this

ReactJS 开发者可以随时用 ReactDOM.render 函数把 TagPicker
渲染到任何空白元素内。此外,ReactJS 框架可以在 stateprops
改变时触发 render ,从而避免了手动修改现存的 DOM。

如果不考虑冗余的 key 属性,单个组件内的交互 ReactJS
还算差强人意。但是,复杂的网页结构往往需要多个组件层层嵌套,这种父子组件之间的交互,ReactJS
就很费劲了。

比如,假如需要在 TagPicker
之外显示所有的标签,每当用户增删标签,这些标签也要自动更新。要实现这个功能,需要给
TagPicker 传入 changeHandler 回调函数,代码如下:

JavaScript

class Page extends React.Component { state = { tags: [ “initial-tag-1”,
“initial-tag-2” ]澳门微尼斯人手机版, }; changeHandler = tags => { this.setState({ tags
}); }; render() { return ( <div> <TagPicker tags={
this.state.tags } changeHandler={ this.changeHandler }/>
<h3>全部标签:</h3> <ol>{ this.state.tags.map(tag
=> <li>{ tag }</li> ) }</ol> </div> ); } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Page extends React.Component {
 
  state = {
    tags: [ "initial-tag-1", "initial-tag-2" ]
  };
 
  changeHandler = tags =&gt; {
    this.setState({ tags });
  };
 
  render() {
    return (
      &lt;div&gt;
        &lt;TagPicker tags={ this.state.tags } changeHandler={ this.changeHandler }/&gt;
        &lt;h3&gt;全部标签:&lt;/h3&gt;
        &lt;ol&gt;{ this.state.tags.map(tag =&gt; &lt;li&gt;{ tag }&lt;/li&gt; ) }&lt;/ol&gt;
      &lt;/div&gt;
    );
  }
 
}
 

为了能触发页面其他部分更新,我被迫增加了一个 21 行代码的 Page 组件。

Page 组件必须实现 changeHandler 回调函数。每当回调函数触发,调用
Page 自己的 setState 来触发 Page 重绘。

从这个例子,我们可以看出, ReactJS
可以简单的解决简单的问题,但碰上层次复杂、交互频繁的网页,实现起来就很繁琐。使用
ReactJS 的前端项目充满了各种 xxxHandler
用来在组件中传递信息。我参与的某海外客户项目,平均每个组件大约需要传入五个回调函数。如果层次嵌套深,创建网页时,常常需要把回调函数从最顶层的组件一层层传入最底层的组件,而当事件触发时,又需要一层层把事件信息往外传。整个前端项目有超过一半代码都在这样绕圈子。

发表评论

电子邮件地址不会被公开。 必填项已用*标注