学习使用React
准备工作:首先先在html文件中引入如下的脚本:
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<!-- 用于编译jsx -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- 引入我们自己的脚本,需要打上type属性 -->
<script src="./reactApp.js" type="text/babel"></script>
创建组件,class声明和 function声明
// reactApp.js
const e = React.createElement; //将渲染函数中的jsx转化为react元素
//使用class创建组件,必须要有render函数
class Demo extends React.Component {
render() {
return <h1>Hello Demo!</h1>;
}
}
//使用函数创建组件
function Demo1() {
return <h1>Hello Demo1!</h1>;
}
const app = document.querySelector("#app");
//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(Demo), app);
组件的生命周期的使用
下图为react组件的生命周期:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tVLAi64s-1627715673108)(/Users/user/Desktop/react-life-cycle.png)]
以上的图清晰的解释了挂载时、更新时和卸载是会触发的生命周期函数。以下的程序可以触发各个生命周期函数:
const e = React.createElement; //将渲染函数中的jsx转化为react元素
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
console.log("construstor");
}
add = () => {
this.setState((state, props) => {
return {
count: state.count + 1,
};
});
};
static getDerivedStateFromProps(props, state) {
console.log("getDerivedStateFromProps");
//它应返回一个对象来更新 state
return state;
}
componentDidMount() {
console.log("componentDidMount");
}
shouldComponentUpdate() {
console.log("shouldComponentUpdate");
//返回false的话,使用setState则永远不会更新,也不会到达render阶段
return true;
}
getSnapshotBeforeUpdate(preveProps, preveState) {
console.log("getSnapshotBeforeUpdate");
//应返回一个快照值(或 null)。
return preveState;
}
componentDidUpdate() {
console.log("componentDidUpdate");
}
componentWillUnmount() {
console.log("componentWillUnmount");
}
render() {
console.log("render");
return (
<div>
<button onClick={this.add}>add</button>
{this.state.count}
</div>
);
}
}
function App() {
return (
<div>
<Demo></Demo>
</div>
);
}
const app = document.querySelector("#app");
//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(App), app);
let d = document.createElement("button");
d.innerText = "destroy";
d.onclick = function () {
//卸载组件方法
ReactDOM.unmountComponentAtNode(app);
};
document.body.appendChild(d);
个人理解的一些常用生命周期函数可以做的事情:
- render:简单说就是需要在这个函数里返回要渲染的东西
- constructor:应该要在其他语句前调用
super(props)
,可以在这里初始化state
,但是不应该在此处使用setState()
。也可以在此处发起一些异步请求。 - componentDidMount:此时可以获取到对应的DOM节点,可以在此处访问DOM节点。
- componentWillUnmount:组件卸载及销毁前调用。可以在此处做一些收尾工作,如清除定时器和取消网络请求等。
组件的页面状态和属性的设置
props的设置和使用
const e = React.createElement; //将渲染函数中的jsx转化为react元素
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
console.log("construstor");
}
add = () => {
this.setState((state, props) => {
return {
count: state.count + 1,
};
});
};
forceAdd = () => {
this.setState({ count: this.state.count + 1 });
//使用forceUpdate可强制重新渲染
this.forceUpdate(() => {
console.log("forceUpdate");
});
};
static getDerivedStateFromProps(props, state) {
console.log("getDerivedStateFromProps");
//它应返回一个对象来更新 state
return state;
}
componentDidMount() {
console.log("componentDidMount");
}
shouldComponentUpdate() {
console.log("shouldComponentUpdate");
//返回false的话,使用setState则永远不会更新,也不会到达render阶段,
//但是使用forceUpdate可以强制重新渲染
return true;
}
getSnapshotBeforeUpdate(preveProps, preveState) {
console.log("getSnapshotBeforeUpdate");
//应返回一个快照值(或 null)。
return preveState;
}
componentDidUpdate() {
console.log("componentDidUpdate");
}
componentWillUnmount() {
console.log("componentWillUnmount");
}
render() {
console.log("render");
return (
<div>
<button onClick={this.add}>add</button>
<button onClick={this.forceAdd}>forceAdd</button>
{this.state.count}
</div>
);
}
}
function App() {
return (
<div>
<Demo></Demo>
</div>
);
}
const app = document.querySelector("#app");
//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(App), app);
let d = document.createElement("button");
d.innerText = "destroy";
d.onclick = function () {
//卸载组件方法
ReactDOM.unmountComponentAtNode(app);
};
document.body.appendChild(d);
state的设置和使用
const e = React.createElement; //将渲染函数中的jsx转化为react元素
//在hook未出现之前,只能在class中使用state
class Clock extends React.Component {
constructor(props) {
super(props);
//只能在构造函数中初始化状态
this.state = {
date: new Date(),
count: 0,
};
}
getCount = () => {
this.setState({
date: new Date(),
});
this.setState((state, props) => {
console.log(state, props);
return {
count: state.count + 1,
};
});
};
render() {
//使用setState()函数来更新状态
return (
<div>
{/* 使用state时,需要使用this */}
<button onClick={this.getCount}>第{this.state.count}次获取时间</button>
<h1>It is {this.state.date.toLocaleTimeString("en-uk")}.</h1>
</div>
);
}
}
function App() {
return (
<div>
<Clock></Clock>
</div>
);
}
const app = document.querySelector("#app");
//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(App), app);
对于state
的使用,需要注意的是:
- 不要直接修改
state
,更新state
只能使用setState()
函数。 state
和props
的更新可能是异步的,所以不要依赖他们的值来更新下一个状态。
state的深入学习:
组件间的通信
父组件向子组件通信
- 使用props
- 使用ref
const e = React.createElement; //将渲染函数中的jsx转化为react元素
class Child extends React.Component {
render() {
return (
/**
* 可以使用Refs回调给DOM元素或者组件添加ref属性,
* 回调函数第一个参数即对应的元素或者组件
*/
<h1 ref={this.props.h1Ref}>从父组件接收到的数据:{this.props.data}</h1>
);
}
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.child = React.createRef(); //使用React.createRef()创建一个Refs
this.h1Ref = null;
}
getChildInfo = () => {
console.log(this.child);
};
getChildH1Info = () => {
console.log(this.h1Ref);
};
render() {
//使用props与子组件进行通信
//给子组件添加ref属性后,可以直接访问子组件的信息
return (
<div>
<button onClick={this.getChildInfo}>getChildInfo</button>
<button onClick={this.getChildH1Info}>getChildH1Info</button>
<Child
data={"传给子组件的数据"}
ref={this.child}
h1Ref={(el) => (this.h1Ref = el)}
/>
</div>
);
}
}
function App() {
return (
<div>
<Parent></Parent>
</div>
);
}
const app = document.querySelector("#app");
//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(App), app);
子组件向父组件通信
- 调用父组件传进来的回调函数
const e = React.createElement; //将渲染函数中的jsx转化为react元素
class Child extends React.Component {
render() {
return (
<button
onClick={() => {
// 使用回调函数向父组件通信
this.props.sendMsg("来自子组件的消息");
}}
>
发送消息给父组件
</button>
);
}
}
class Parent extends React.Component {
constructor(props) {
super(props);
}
sendMsg = (msg) => {
console.log(msg);
};
render() {
return (
<div>
{/* 向子组件传递回调函数 */}
<Child sendMsg={this.sendMsg} />
</div>
);
}
}
function App() {
return (
<div>
<Parent></Parent>
</div>
);
}
const app = document.querySelector("#app");
//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(App), app);
跨组件通信
- 使用Context
- 使用Redux(暂时搁置)
const e = React.createElement; //将渲染函数中的jsx转化为react元素
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext("light");
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
theme: "dark",
};
this.toggleTheme = () => {
console.log("toggle");
this.setState((state) => ({
theme: state.theme === "dark" ? "light" : "dark",
}));
};
}
render() {
// 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
// 无论多深,任何组件都能读取这个值。
// 在这个例子中,我们将 “dark” 作为当前的值传递下去。
return (
//此处对context使用了动态值
<ThemeContext.Provider value={this.state.theme}>
<Toolbar toggleTheme={this.toggleTheme} />
</ThemeContext.Provider>
);
}
}
// 中间的组件再也不必指明往下传递 theme 了。
class Toolbar extends React.Component {
render() {
return (
<div>
<ThemedButton onClick={this.props.toggleTheme} />
</div>
);
}
}
class ThemedButton extends React.Component {
// 指定 contextType 读取当前的 theme context。
// React 会往上找到最近的 theme Provider,然后使用它的值。
// 在这个例子中,当前的 theme 值为 “dark”。
// 此处也可以在类的外部写一行"ThemeButton.contextType=ThemeContext"来替换
static contextType = ThemeContext;
render() {
return <h1 {...this.props}>Theme:{this.context}</h1>;
}
}
const app = document.querySelector("#app");
//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(App), app);
此处稍微忽略了一些在函数式组件上的应用:Context.Consumer
、“在嵌套组件中更新Context”。
Context.Consumer的用法如下:
//我们可以在标签内使用一个函数来渲染一个依赖context的dom<MyContext.Consumer> {value => /* 基于 context 值进行渲染*/}</MyContext.Consumer>
若要在嵌套组件中更新Context,这个可以参考官网文档,讲的不错。他是吧context设置成了一个对象,对象中有一个更新方法,这样嵌套组件就可以使用这个方法更新了。使用hook进行嵌套更新的例子可以看之后关于Context Hook的使用。
有一个稍微不懂的问题就是文档中关于Context的注意事项(已解决):当value的值为state中的值时,更新后,value仍然是同一个对象,所以Provide并不会重新渲染。
高阶组件
高阶组件的概念和高阶函数类似,一个函数接受一个组件作为参数,然后经过处理返回一个组件,那么这个函数就可以被称为高阶组件。自己写了一个简单的例子如下:
const e = React.createElement; //将渲染函数中的jsx转化为react元素//创建高阶组件的函数function hoc(WrappedComponent, fn) { return class extends React.Component { constructor(props) { super(props); } render() { // 将传递进来的props传递给被包裹的组件 return <WrappedComponent {...this.props} onClick={fn}></WrappedComponent>; } };}function C(props) { console.log(props); //点击事件可由hoc中的参数决定 return <h1 onClick={props.onClick}>{props.name}</h1>;}//创建一个高阶组件const Demo = hoc(C, () => { console.log("wqq");});//创建一个高阶组件const Demo1 = hoc(C, () => { console.log("wqq1");});class App extends React.Component { render() { return ( <div> <Demo name={"WrappedComponent"} /> <Demo1 name={"WrappedComponent1"} /> </div> ); }}const app = document.querySelector("#app");//渲染react元素并挂载到对应的dom上ReactDOM.render(e(App), app);
需要注意,不要改变原始组件,若修改了原始组件,若使用另一个hoc继续修改,那么高阶组件的效果可能会在你的预期之外。详细可见https://zh-hans.reactjs.org/docs/higher-order-components.html。
只将相关的props传递给被包裹的组件。这是react官方提的一个约定。
以下是注意事项:
-
不要在render方法中使用hoc。引用官方文档的一句话“如果从
render
返回的组件与前一个渲染中的组件相同(===
),则 React 通过将子树与新子树进行区分来递归更新子树。 如果它们不相等,则完全卸载前一个子树。”,若我们在render函数中使用hoc,那每次render函数都会创建一个新的组件,注意是新的,这样以来react就会卸载前一个子树,然后重新渲染这个hoc。这样对性能有很大的影响。 -
在hoc中,refs并不会被传递。可以通过使用
React.forwardRef
来解决这个问题。 -
hoc不会有原始组件的静态方法。我们可以在函数中将需要的方法拷贝下来:
function enhance(WrappedComponent) { class Enhance extends React.Component {/*...*/} // 必须准确知道应该拷贝哪些方法 :( Enhance.staticMethod = WrappedComponent.staticMethod; return Enhance;}
"约定:最大化可组合性"看不出有什么。
const CommentWithRelay = Relay.createContainer(Comment, config);// React Redux 的 `connect` 函数const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
当时看不懂“最大化可组合性”的原因是:看不太懂以上两种写法的区别有什么。请教了一下导师后,清楚了一些。第一行这样写的原因可能是高阶组件初始化的时候,comment就需要使用到config。第二行的可能是,使用时,首先需要使用connect来初始化一些配置(这些配置可能与后面传进来的组件关系并不大),然后返回一个函数,这个函数再接受一个组件,执行后返回一个新组件。对组合性的理解就是:配置和组件的组合性,以上第二行的代码就是一种组合性。
函数式组件和 hook
hook的定义
首先看看官方定义:“Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。“。结合官方的定义,我的理解是之前我们写的几乎都是class组件,我们可以在class组件中使用生命周期函数、“refs”、"state"等,但是在函数组件上较难使用。于是官方就做了一些函数,这些函数能够让我们在函数组件中使用react特性,这些函数就叫做hook。
State Hook
使用"useState"这个hook挺简单的,例子如下:
const e = React.createElement; //将渲染函数中的jsx转化为react元素
const { useState } = React;
function App() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
const app = document.querySelector("#app");
//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(App), app);
在class组件中,我们使用"this.state={}"来初始化我们的state,然后使用"this.setState()"来更新我们state。在hook中,我们使用"useState"来初始化我们需要的状态变量,传入的参数即初始值。然后我们通过数组解构得到我们需要用到的变量和更新变量的方法。更新方法和"this.setState()"作用都是差不多的,都可以更新state。
在hook api索引中的补充:
- setState可以传入一个函数来更新,函数第一个参数为更新前的值,需要返回更新后的值。若更新前后值不变,则会跳过随后的渲染。
- useState也可以接受一个函数来初始化初始值,初始值可在在该函数中进行复杂计算。函数需要返回一个值作为初始值。
Effect Hook
使用"useEffect"hook也是比较简单的,如下:
const e = React.createElement; //将渲染函数中的jsx转化为react元素
const { useState, useEffect } = React;
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`You clicked1 ${count} times`);
return () => {
console.log("clean1");
};
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
const app = document.querySelector("#app");
//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(App), app);
“useEffect"相当于"componentDidMOunt”、“componentDidUpdate"和"componentWillUnmount"三个生命周期函数的组合。组建每经历一次这三个生命周期,都会执行一次"useEffect"中的函数(我们将它称为effect)。值得注意的是,我们在"useEffect"中返回了一个函数,这个函数将在下一次执行effect之前执行。我们可以在这个返回的函数中进行一下一些清除操作,官网文档的例子是个不错的例子。在函数组件中,我们也可以调用多次"useEffect”。那么问题来了,执行顺序是怎么样的呢?我用了一个例子测试了一下:
const e = React.createElement; //将渲染函数中的jsx转化为react元素
const { useState, useEffect } = React;
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`You clicked1 ${count} times`);
return () => {
console.log("clean1");
};
});
useEffect(() => {
console.log(`You clicked2 ${count} times`);
return () => {
console.log("clean2");
};
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
const app = document.querySelector("#app");
//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(App), app);
//点击按钮两次,打印结果如下:
You clicked1 0 times
You clicked2 0 times
clean1
clean2
You clicked1 1 times
You clicked2 1 times
clean1
clean2
You clicked1 2 times
You clicked2 2 times
执行effect的顺序是按照添加的先后顺序执行的,清除effect也是一样的顺序,但是执行effect之前是会先把所有的effect先清除完。
effect会在每次渲染的时候执行或者清除,这有时候会产生性能问题,"useEffect"提供了第二个参数,它是一个数组,里面存放state,若渲染前后state的值不变,则会跳过该effect,否则会执行和清除。使用例子如下:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
Context Hook
Context Hook使用注意的一点就是第一个参数必须为React.createContext
的返回值。以下的例子也展示了使用hook怎么嵌套更新context:
const e = React.createElement; //将渲染函数中的jsx转化为react元素
const { useState, useEffect, useContext } = React;
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee",
},
dark: {
foreground: "#ffffff",
background: "#222222",
},
};
const ThemeContext = React.createContext({
theme: themes.light,
change: () => {},
});
function App() {
const [theme, setTheme] = useState(themes.light);
return (
//将setTheme传递进上下文
<ThemeContext.Provider value={{ theme, changeTheme: setTheme }}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const themeObj = useContext(ThemeContext);
return (
<button
style={{
background: themeObj.theme.background,
color: themeObj.theme.foreground,
}}
//点击时则触发上下文的更新函数
onClick={() => {
themeObj.changeTheme(themes.dark);
}}
>
I am styled by theme context!
</button>
);
}
const app = document.querySelector("#app");
//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(App), app);
Ref Hook
官方文档解释ref hook解释的不错,这里引用他的解释:“useRef
就像是可以在其 .current
属性中保存一个可变值的’盒子‘。“。useRef返回的值并不会随着渲染的更新而更新,所以可以用来保存一个你想在下一次更新后还想使用的变量。官网使用了一个指向dom的ref,这个大家都可以理解,以下我使用的是只想timeout的ref:
//这个例子是给输入框加入一个防抖功能,inputVal是输入框的值,onInputChange是输入框的值更新了触发的函数
const [inputVal, setInputVal] = useState(initValue || '');
const timer = useRef(null);
useEffect(() => {
clearTimeout(timer.current);
timer.current = setTimeout(() => {
onInputChange(inputVal);
}, 200);
}, [inputVal]);
// const [timer, setTimer] = useState(null);
// useEffect(() => {
// clearTimeout(timer);
// setTimer(
// setTimeout(() => {
// onInputChange(inputVal);
// }, 200)
// );
// }, [inputVal]);
把注释去掉,然后使用注释中的代码也可以更新timer。使用state和ref都可以更新timer,那么为什么会优先使用ref呢?state是保存组件的状态的,它更偏向于保存和视图相关的一些状态。而ref则是保存和代码逻辑相关的一些变量,就比如例子中的timer。这是一个原因。另外一个原因是,当 ref 对象内容发生变化时,useRef
并不会通知你。变更 .current
属性不会引发组件重新渲染。而变更state会引发组件重新渲染,这里timer的更新并不需要我们对视图进行更新,所以使用ref更佳。
hook规则
**不要在循环,条件或嵌套函数中调用hook。**具体为什么,可以去学习一下其原理。
自定义hook
可以把自定义hook简单理解为在一个函数中使用hook,这个函数就可以被称为自定义hook了。自定义hook可以提高代码的复用性。这里要注意一个约定:自定义hook必须以"use"开头,这便于react自动去检查自定义hook是否违反了hook的规则。以下是自己写的简单例子,不过官方文档的例子更能体现自定义hook的特点。
const e = React.createElement; //将渲染函数中的jsx转化为react元素
const { useState, useEffect } = React;
//自定义hook,记录数字的奇偶性
function useNumType(number) {
const [type, setType] = useState(0);
useEffect(() => {
setType(number % 2);
});
return type ? "odd" : "even";
}
function App() {
const [count, setCount] = useState(0);
const type = useNumType(count);
return (
<div>
<p>You clicked {count} times</p>
<p>
{count} is {type}
</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
const app = document.querySelector("#app");
//渲染react元素并挂载到对应的dom上
ReactDOM.render(e(App), app);