查看专栏其它文章:
② React 面向组件编程(state、props、refs)、事件处理
本人是个新手,写下博客用于自我复习、自我总结。
如有错误之处,请各位大佬指出。
学习资料来源于:尚硅谷
列表与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高效的原因:
-
虚拟(virtual)DOM, 不总是直接操作DOM
-
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操作量,提高性能。
验证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"/>!
<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请求库
-
jQuery: 比较重, 如果需要另外引入不建议使用
-
axios: 轻量级, 建议使用
a. 封装XmlHttpRequest对象的ajax
b. promise风格
c. 可以用在浏览器端和node服务器端 -
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>