④ React 列表与Keys、虚拟DOM相关说明、AJAX

查看专栏其它文章:

① React 介绍及JSX简单使用

② React 面向组件编程(state、props、refs)、事件处理

③ React 条件渲染、组件生命周期、表单与事件



本人是个新手,写下博客用于自我复习、自我总结。
如有错误之处,请各位大佬指出。
学习资料来源于:尚硅谷


列表与Keys

之前的文章里已经使用过 JavaScript 的 map() 方法来创建列表。在这里再详细说明其它内容。

简单使用演示:

const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((numbers) =>
  <li>{numbers}</li>
);
 
ReactDOM.render(
  <ul>{listItems}</ul>,
  document.getElementById('example')
);

然后我们再将以上实例重构成一个组件,组件接收数组参数,每个列表元素分配一个 key,不然会出现警告 a key should be provided for list items,意思就是需要包含 key。

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    <li key={number.toString()}>
      {number}
    </li>
  );
  return (
    <ul>{listItems}</ul>
  );
}
 
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('example')
);

通常情况下,请不要忘记加 key。那这个 Key 到底是用来做什么的,且选取什么值都可以吗?

Keys 可以在 DOM 中的某些元素被增加或删除的时候帮助 React 识别哪些元素发生了变化。因此你应当给数组中的每一个元素赋予一个确定的标识。且一个元素的 key 最好是这个元素在列表中拥有的一个独一无二的字符串。通常,我们使用来自数据的 id 作为元素的 key ( id :自己建立的,用于区分每个数据)。

const todoItems = todos.map((todo) =>
  <li key={todo.id}>
    {todo.text}
  </li>
);

当元素没有确定的 id 时,你可以使用他的序列号索引 index 作为 key

const todoItems = todos.map((todo, index) =>
  // 只有在没有确定的 id 时使用
  <li key={index}>
    {todo.text}
  </li>
);

复杂一些的情况:元素的 key 只有在它和它的兄弟节点对比时才有意义。

比方说,如果你提取出一个 ListItem 组件,你应该把 key 保存在数组中的这个 <ListItem /> 元素上,而不是放在 ListItem 组件中的 <li> 元素上。

function ListItem(props) {
  // 这里不需要指定key:
  return <li>{props.value}</li>;
}
 
function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    // key应该在数组的上下文中被指定
    <ListItem key={number.toString()} value={number} /> 
  );
  return (
    <ul>
      {listItems}
    </ul>
  );
}
 
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('example')
);

在这里需要注意:key 只会作为给 React 的提示,不会传递给你的组件。比如在这里子组件中,就不能读出 props.key。如果您的组件中需要使用和 key 相同的值,请将其作为属性传递。

说回正题,数组元素中使用的 key 在其兄弟之间应该是独一无二的。然而,它们不需要是全局唯一的。当我们生成两个不同的数组时,我们可以使用相同的键。

function Blog(props) {
  const sidebar = (
    <ul>
      {props.posts.map((post) =>
        <li key={post.id}>
          {post.title}
        </li>
      )}
    </ul>
  );
  const content = props.posts.map((post) =>
    <div key={post.id}>
      <h3>{post.title}</h3>
      <p>{post.content}</p>
    </div>
  );
  return (
    <div>
      {sidebar}
      <hr />
      {content}
    </div>
  );
}
 
const posts = [
  {id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
  {id: 2, title: 'Installation', content: 'You can install React from npm.'}
];
ReactDOM.render(
  <Blog posts={posts} />,
  document.getElementById('example')
);

虚拟DOM相关说明

从React开篇就说到了虚拟DOM,也提到了对于React高效的原因:

  1. 虚拟(virtual)DOM, 不总是直接操作DOM

  2. DOM Diff算法, 最小化页面重绘

接下来将对虚拟DOM的相关内容进行一些简单的说明。


在说到何为 虚拟DOM 前,需要先说明何为 真实DOM 和其解析的流程。这部分详细内容可以看:【干货】浏览器是如何运作的?视频中的内容讲的很详细。

对于浏览器渲染引擎的大致工作流程如下(简化):

第一步,用HTML分析器,分析HTML元素,构建一颗DOM树。

第二步,用CSS分析器,分析CSS文件和元素上的样式,生成页面的样式表。

第三步,将DOM树和样式表,关联起来,构建一颗Render树。

第四步,有了Render树,浏览器开始布局,为每个Render树上的节点确定一个在显示屏上出现的精确坐标。

第五步,Render树和节点显示坐标都有了,就调用每个节点paint方法,把它们绘制出来。

然后再经过一个复杂的渲染过程,就将内容渲染了出来。

在这个过程中,当我们改变一个元素的尺寸位置属性时,会重新进行样式计算、布局、绘制以及后面的所有流程,这种行为被称为重排

当我们改变某个元素的颜色属性时,不会重新触发布局,但还是会触发样式计算和绘制,这种行为被称为重绘

操作真实DOM的代价?

而在这个过程中,JS操作真实DOM的代价还是很大的。当我们用传统的开发模式,原生JS操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程。在一次操作中,我需要更新10个DOM节点,浏览器收到第一个DOM请求后并不知道还有9次更新操作,因此会马上执行流程,最终执行10次。例如,第一次计算完,紧接着下一个DOM更新请求,这个节点的坐标值就变了,前一次计算为无用功。计算DOM节点坐标值就是白白的浪费性能。

那虚拟DOM有什么好处?

虚拟DOM就是为了解决浏览器性能问题而被设计出来的。若一次操作中有10次更新DOM的动作,虚拟DOM不会立即操作DOM,而是将这10次更新的内容保存到本地一个JS对象中,最终将这个JS对象一次性挂到DOM树上,再进行后续操作,避免大量无谓的计算量。(也就是现在我们看到的 React 和 Vue 都是这样操作)

diff算法是虚拟DOM技术的必然产物

这个diff算法是虚拟DOM技术的必然产物,因为我们需要对新旧虚拟DOM作对比,然后将变化的地方更新在真实DOM上。如果对diff算法感兴趣,可以查阅相关资料。

就比如上面列表中 key 的作用就是为了高效的更新虚拟DOM,其原理就是通过key精准判断两个节点是否是同一个,从而避免频繁更新不同元素,使得整个更新渲染过程更加高效,减少DOM操作量,提高性能。

vue和react的diff算法简单比较
在这里插入图片描述


验证DOM最小化页面重绘

在上面已经说到了,虚拟DOM 通过 Diff算法,实现了最小化页面重绘,即:可以更少的操作真实DOM,减少操作DOM的次数,更新页面的次数也就减少了。接下来我们将验证这点,虚拟DOM是否真的可以最小化页面重绘。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>vDOM</title>
</head>
<body>
<div id="example"></div>
<br>

<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>
<script type="text/babel">
  class HelloWorld extends React.Component {
    constructor(props) {
      super(props)
      this.state = {
        date: new Date()
      }
    }

    componentDidMount () {
      setInterval(() => {
        this.setState({
            date: new Date()
        })
      }, 1000)
    }

    render () {
      return (
        <p>
          Hello, <input type="text" placeholder="Your name here"/>!&nbsp;
          <span>It is {this.state.date.toTimeString()}</span>
        </p>
      )
    }
  }

  ReactDOM.render(<HelloWorld/>, document.getElementById('example'))
</script>
</body>
</html>

因为时间在不断变化(通过 setState 更新状态),所以会重新创建虚拟DOM树。而在这个过程中,虚拟DOM会对 新 / 旧 内容进行比较,然后对有差异的部分去进行局部重绘。假如是整体重绘,那么在输入框中输入一些内容应该也会导致差异。而无论我们怎么向输入框中输入内容,也不会引起虚拟DOM对其的更新。而向输入框中输入信息却无法导致更新,是因为之前在表单时提到过,表单元素本身虽然能够保留一些内部状态,并根据用户输入进行更新,但在React中,可变的状态通常保存在组件的状态属性中,并且只能用 setState() 方法进行更新。
在这里插入图片描述

原理图:
在这里插入图片描述


AJAX

React 组件的数据可以通过 componentDidMount 方法中的 Ajax 来获取,当从服务端获取数据时可以将数据存储在 state 中,再用 this.setState 方法重新渲染 UI。当使用异步加载数据时,在组件卸载前使用 componentWillUnmount 来取消未完成的请求。

相关用法就不在这里赘述了,如果对AJAX不熟悉,可以查阅相关文章:Ajax介绍以及工作原理和实现详解(JS实现Ajax 和 JQ实现Ajax)

常用的ajax请求库

  1. jQuery: 比较重, 如果需要另外引入不建议使用

  2. axios: 轻量级, 建议使用
    a. 封装XmlHttpRequest对象的ajax
    b. promise风格
    c. 可以用在浏览器端和node服务器端

  3. fetch: 原生函数, 但老版本浏览器不支持
    a. 不再使用XmlHttpRequest对象提交ajax请求
    b. 为了兼容低版本的浏览器, 可以引入兼容库fetch.js

使用简例(axios):

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>ajax</title>
</head>
<body>
<div id="example"></div>

<script type="text/javascript" src="../js/react.development.js"></script>
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<script type="text/javascript" src="../js/babel.min.js"></script>
<script type="text/javascript" src="https://cdn.bootcss.com/axios/0.17.1/axios.min.js"></script>
<script type="text/babel">
  class MostStarRepo extends React.Component {
    constructor (props) {
      super(props)
      this.state = {
        repoName: '',
        repoUrl: ''
      }
    }
    
    componentDidMount () {
      const url = `https://api.github.com/search/repositories?q=${this.props.searchWord}&sort=stars`
      //使用axios发送ajax请求
      axios.get(url).then(response => {
          //成功了
          const result = response.data
          //得到最受欢迎的repo
          const repo = result.items[0]
          this.setState({
            repoName: repo.name,
            repoUrl: repo.html_url
          })
      }).catch(error => {
          alert('请求失败 '+ error.message)
      })
    }

    render () {
      const {repoName, repoUrl} = this.state
      if(!repoName) {
        return <h2>loading...</h2>
      } else {
        return (
          <h2>
            most star repo is <a href={repoUrl}>{repoName}</a>
          </h2>
        )
      }
    }
  }

  ReactDOM.render(<MostStarRepo searchWord="r"/>, document.getElementById('example'))
</script>
</body>
</html>

使用简例(jQuery):

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>ajax</title>
<script src="https://cdn.staticfile.org/react/16.4.0/umd/react.development.js"></script>
<script src="https://cdn.staticfile.org/react-dom/16.4.0/umd/react-dom.development.js"></script>
<script src="https://cdn.staticfile.org/babel-standalone/6.26.0/babel.min.js"></script>
<script src="https://cdn.staticfile.org/jquery/2.1.4/jquery.min.js"></script>
</head>
<body>
<div id="example"></div>

<script type="text/babel">

class UserGist extends React.Component {
  constructor(props) {
      super(props);
      this.state = {username: '', lastGistUrl: ''};
  }

  componentDidMount() {
    this.serverRequest = $.get(this.props.source, function (result) {
      var lastGist = result[0];
      this.setState({
        username: lastGist.owner.login,
        lastGistUrl: lastGist.html_url
      });
    }.bind(this));
  }
 
  componentWillUnmount() {
    this.serverRequest.abort();
  }
 
  render() {
    return (
      <div>
        {this.state.username} 用户最新的 Gist 共享地址:
        <a href={this.state.lastGistUrl}>{this.state.lastGistUrl}</a>
      </div>
    );
  }
}
 
ReactDOM.render(
  <UserGist source="https://api.github.com/users/octocat/gists" />,
  document.getElementById('example')
);

</script>
</body>
</html>
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

只爭朝夕不負韶華

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值