React基础
React 是一个声明式,高效且灵活的用于构建用户界面的 JavaScript 库。使用 React 可以将一些简短、独立的代码片段组合成复杂的 UI 界面,这些代码片段被称作 “组件” 。
在使用react之前我们需要确保我们计算机中安装有Node.js运行环境。随后打开命令行,我们可以用npx create-react-app <应用名>
来快速创建一个React应用。
在新建项目中可以看到src/index.js文件中最重要的是ReactDOM.render()
函数,整个页面的渲染都是从这里开始的。
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>
document.getElementById('root')
);
第一个参数是我们要渲染的所有元素,可以看到这里用的是HTML语法。第二个参数代表将渲染的结果插入到页面中的哪一个容器中,这里对应public/index.html中id为root的元素:<div id="root"></div>
第一个参数其实是React对JS的一种语法扩展,被称为JSX。它允许将HTML标签和JS代码混合使用,所以我们不再需要分开定义HTML和JS文件,编写和阅读代码会方便很多。可以尝试去修改这个标签:
ReactDOM.render(
<h1>hello,React!!!</h1>
document.getElementById('root')
);
浏览器中的内容也会自动刷新
当然了,这里面的<App />
不是一个标准的标签,按住ctrl点它会发现,它会跳到另一个函数的定义。因为React中的组件都可以被简化成一个函数的定义 函数的输出是一段HTML标签,也就是组件最终渲染的结果。我们可以通过组合其他组件或者HTML标签来创建更为复杂的组件
举个例子:根据数据来动态地生成用户界面,数据通常是通过API从后端获取,这里为了简单,在文件开头定义了一个返回假数据的函数fetchTodos()
//src/index.js
function APP(){
const todos=fetchTodos();//调用函数获取数据
//通过map遍历todos中的每一条数据
return (
<>
<ul>
{todos.map((todo) => (
<li>
<input type="defaultChecked={todo.completed}" />
<label>{todo.title}</label>
</li>
))}
</ul>
</>;
)
}
ReactDOM.render(<App/>, document.getElementById("root"));
注意:如果我们想在HTML标签中嵌入JS的表达式,需要将它们嵌套在花括号{}
中.
这里生成的每一个列表元素由复选框input和一个标签label组成,复选框是否选中取决于数据中的compelted属性。
因为很多常见的框架在React中都有很好的集成。所以可以直接用react-bootstrap来对页面做美化。 直接按照文档中的步骤来安装这个包
npm install react-bootstrap bootstrap@5.1.3
然后在文件中导入要用的组件和样式表(VSCode会在输入组件的时候自动导入缺少的包),替换之前的HTML标签:
function APP(){
const todos=fetchTodos();
return (
<>
<Container>
{todos.map((todo) => (
<InputGroup key={todo.id}>
<InputGroup.Checkbox checked={todo.completed} />
<FormControl
value={todo.title}
style={
textDecoration:todo.completed ? "line-through 4px" : "none",
}
/>
</InputGroup>
))}
</Container>
</>;
)
}
//加入了删除线效果
当组件变得复杂以后,我们可以考虑将可重用的代码拆分成一个个独立的组件,也就是模块化,比如列表元素可以被抽离成一个单独的组件TodoItem
。而列表中的文字和复选框状态可以用类似于HTML标签属性的语法并作为函数参数props
传递进来
function TodoItem(props){ // props 属性 是只读的,我们不可以在函数内部修改它
return (
<InputGroup key={props.id}>
<InputGroup.Checkbox checked={props.completed} />
<FormControl
value={props.title}
style={
textDecoration:props.completed ? "line-through 4px" : "none",
}
/>
</InputGroup>
)
}
function App(){
return (
<>
<Container>
{todos.map((todo) => (
<TodoItem
key={todo.id}
title={tofo.title}
completed={todo.completed}
/>
))}
</Container>
</>;
)
}
为了实现数据的动态更新,需要用到states
状态,在React中定义一个状态,需要用到useState()方法。它会返回两个值,第一个是存储当前状态变量的,第二个是存储用来修改状态变量的方法,最后的参数代表状态的初始值:
const [todos,setTodos]=useState(fetchTodos());
要修改一个状态是不能直接对这里的todos变量赋值的,必须调用setTodos方法
function TodoItem(props){ // props 属性 是只读的,我们不可以在函数内部修改它
return (
<InputGroup key={props.id}>
<InputGroup.Checkbox
checked={props.completed}
onChange={props.onToggle}
/>
<FormControl
value={props.title}
style={
textDecoration:props.completed ? "line-through 4px" : "none",
}
/>
<Button variant="outline-danger" onClick={props.onDelete}>
<Trash />
</Button>
</InputGroup>
)
}
//按钮事件 -> 调用setTodos()更新状态 -> 界面刷新
function App(){
return (
<>
<Container>
{todos.map((todo) => (
<TodoItem
key={todo.id}
title={tofo.title}
completed={todo.completed}
onDelete={() => {
setTodos(todos.filter((x) => x.id !== todo.id));
}}
onToggle={() => {
setTodos(
todos.map((x) =>
x.id === todo.id ? ...x, compelted : !x.completed} : x
);
}}
/>
))}
</Container>
</>;
)
}
props及state
组件接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素。
组件
- 函数式组件
- Class类组件
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
渲染组件
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Sara" />;
ReactDOM.render(
element,
document.getElementById('root')
);
组件的组合与拆分
// 页面内多次引用
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<img className="Avatar"
src={props.author.avatarUrl}
alt={props.author.name}
/>
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
// 拆分后为
function UserInfo(){
return (
<div className="UserInfo">
<img className="Avatar"
src={props.author.avatarUrl}
alt={props.author.name}
/>
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
)
}
function Comment(props) {
return (
<div className="Comment">
<UserInfo />
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
state
- setState 构造函数是唯一可以给state赋值的地方
this.setState({comment: 'Hello'});
- state更新可能是异步的
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
// Correct
this.setState(function(state, props) {
return {
counter: state.counter + props.increment
};
});
- state更新会合并
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
- 单向数据流 state 只在当前的组件里生效,属于组件内的属性,重复实例化相同的组件,内部的内存地址也是不一样的; 例如Clock中计时器都是独立的
合成事件
class App extends Component {
state = { val: 0 }
increment = () => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出的是更新前的val --> 0
}
render() {
return (
<div onClick={this.increment}>
{Counter is: ${this.state.val}}
</div>
)
}
}
生命周期
class App extends Component {
state = { val: 0 }
componentDidMount() {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出的还是更新前的值 --> 0
}
render() {
return (
<div>
{Counter is: ${this.state.val}}
</div>
)
}
}
原生事件
class App extends Component {
state = { val: 0 }
changeValue = () => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出的是更新后的值 --> 1
}
componentDidMount() {
document.body.addEventListener('click', this.changeValue, false)
}
render() {
return (
<div>
{Counter is: ${this.state.val}}
</div>
)
}
}
setTimeout
class App extends Component {
state = { val: 0 }
componentDidMount() {
setTimeout(_ => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出更新后的值 --> 1
}, 0)
}
render() {
return (
<div>
{Counter is: ${this.state.val}}
</div>
)
}
}
批处理
class App extends Component {
state = { val: 0 }
batchUpdates = () => {
this.setState({ val: this.state.val + 1 })
this.setState({ val: this.state.val + 1 })
this.setState({ val: this.state.val + 1 })
}
render() {
return (
<div onClick={this.batchUpdates}>
{Counter is ${this.state.val}} // 1
</div>
)
}
}
- setState 只在合成事件和生命周期中是“异步”的,在原生事件和 setTimeout 中都是同步的;
- setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的, 只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”, 当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
- setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setState , setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。
生命周期
render
是class组件必需的方法,获取最新的 props 和 state, 在不修改组件 state 的情况下,每次调用时都返回相同的结果
constructor
如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。
- 通过给 this.state 赋值对象来初始化内部 state。
- 为事件处理函数绑定实例
constructor(props) {
super(props);
// 不要在这里调用 this.setState()
this.state = { counter: 0 };
this.handleClick = this.handleClick.bind(this);
}
不要调用 setState() 并且避免将 props 的值复制给 state
this.state = { color: props.color }; // wrong
componentWillUnmount
componentWillUnmount() 会在组件卸载及销毁之前直接调用。例如,清除 timer,取消网络请求;
componentWillUnmount() 中不应调用 setState(),因为该组件将永远不会重新渲染;
事件处理
语法格式
- 无需调用addEventListener进行事件监听,也无需考虑兼容性,React已经封装好了一些的事件类型属性;
- 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串;
- 不能通过返回 false 的方式阻止默认行为。你必须显式的使用 preventDefault;
// DOM
<button onclick="activateLasers()">
Activate Lasers
</button>
// React
<button onClick={activateLasers}>
Activate Lasers
</button>
// JS
<form onsubmit="console.log('You clicked submit.'); return false">
<button type="submit">Submit</button>
</form>
function Form() {
function handleSubmit(e) {
e.preventDefault();
console.log('You clicked submit.');
}
return (
<form onSubmit={handleSubmit}>
<button type="submit">Submit</button>
</form>
);
}
接收参数
- 事件对象 e 会被作为第二个参数传递;
- 通过箭头函数的方式,事件对象必须显式的进行传递;
- 通过 Function.prototype.bind 的方式,事件对象以及更多的参数将会被隐式的进行传递;
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
条件渲染
if else 渲染
class LoginControl extends React.Component {
constructor(props) {
super(props);
this.handleLoginClick = this.handleLoginClick.bind(this); //登录
this.handleLogoutClick = this.handleLogoutClick.bind(this); //登出
this.state = {isLoggedIn: false}; //当前的登录状态
}
handleLoginClick() {
this.setState({isLoggedIn: true});
}
handleLogoutClick() {
this.setState({isLoggedIn: false});
}
//渲染
render() {
const isLoggedIn = this.state.isLoggedIn;
let button;
if (isLoggedIn) {
button = <LogoutButton onClick={this.handleLogoutClick} />;
} else {
button = <LoginButton onClick={this.handleLoginClick} />;
}
return (
<div>
<Greeting isLoggedIn={isLoggedIn} />
{button}
</div>
);
}
}
ReactDOM.render(
<LoginControl />,
document.getElementById('root')
);
与运算符 &&
function Mailbox(props) {
const unreadMessages = props.unreadMessages;
return (
<div>
<h1>Hello!</h1>
{unreadMessages.length > 0 &&
<h2>
You have {unreadMessages.length} unread messages.
</h2>
}
</div>
);
}
const messages = ['React', 'Re: React', 'Re:Re: React'];
ReactDOM.render(
<Mailbox unreadMessages={messages} />,
document.getElementById('root')
);
// 返回false的表达式,会跳过元素,但会返回该表达式
render() {
const count = 0;
return (
<div>
{ count && <h1>Messages: {count}</h1>}
</div>
);
}
三元运算符
render() {
const isLoggedIn = this.state.isLoggedIn;
return (
<div>
{isLoggedIn
? <LogoutButton onClick={this.handleLogoutClick} />
: <LoginButton onClick={this.handleLoginClick} />
}
</div>
);
}
如何阻止组件渲染
function WarningBanner(props) {
if (!props.warn) {
return null;
}
return (
<div className="warning">
Warning!
</div>
);
}
class Page extends React.Component {
constructor(props) {
super(props);
this.state = {showWarning: true};
this.handleToggleClick = this.handleToggleClick.bind(this);
}
handleToggleClick() {
this.setState(state => ({
showWarning: !state.showWarning
}));
}
render() {
return (
<div>
<WarningBanner warn={this.state.showWarning} />
<button onClick={this.handleToggleClick}>
{this.state.showWarning ? 'Hide' : 'Show'}
</button>
</div>
);
}
}
ReactDOM.render(
<Page />,
document.getElementById('root')
);
列表
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('root')
);
// 若没有key,会warning a key should be provided for list items
// key可以帮助react diff,最好不用index作为key,会导致性能变差;
// 如果不指定显式的 key 值,默认使用索引用作为列表项目的 key 值;
key注意点
key要保留在map的遍历元素上
// demo1
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('root')
);
// demo2
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('root')
);
// demo3
function NumberList(props) {
const numbers = props.numbers;
return (
<ul>
{numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
)}
</ul>
);
}