react学习
一、简介和简单demo
1 react特点
采用组件化模式,声明式编码,提高开发效率和组件复用率
虚拟DOM和优秀的Diffing算法,尽量减少与真实DOM的交互。这是react高效的原因
2 简单小Demo
首先下载以下三个js,这三个js是react基本的js库
hello_react.html
<!DOCTYPE html> <html> <head><meta charset="UTF-8"><title>hello_react</title></head> <body> <!--准备好一个容器--> <div id="test"></div> <!--引入react核心库--> <script type="text/javascript" src="../js/react.development.js"></script> <!--引入react-dom,用于支持react操作DOM--> <script type="text/javascript" src="../js/react-dom.development.js"></script> <!--引入babel,用于将jsx转换为js--> <script type="text/javascript" src="../js/babel.js"></script> <script type="text/babel"> /*此处一定要写babel*/ //1.创建虚拟DOM const VDOM = <h1>Hello,React</h1> //2.渲染虚拟DOM到页面 ReactDOM.render(VDOM, document.getElementById('test')) </script> </body> </html>
右键选择open with live server(如果没有自行百度)在浏览器中打开
注意:有两个warning,第一个提示我们安装开发者工具,等我们装上开发者工具就没有了;第二个提示我们使用了内嵌的babel翻译,建议我们进行预编译以便提高性能
3 为什么用jsx?
我们再创建一个html,看一下不用jsx行不行?
创建02hello_react_no_jsx.html
<!DOCTYPE html> <html> <head><meta charset="UTF-8"><title>hello_react</title></head> <body> <!--准备好一个容器--> <div id="test"></div> <!--引入react核心库--> <script type="text/javascript" src="../js/react.development.js"></script> <!--引入react-dom,用于支持react操作DOM--> <script type="text/javascript" src="../js/react-dom.development.js"></script> <script type="text/javascript"> /*此处一定要写babel*/ //1.创建虚拟DOM const VDOM = React.createElement('h1', {id:'title'}, 'Hello React')//我们没有引入babel.js,使用React创建一个element //2.渲染虚拟DOM到页面 ReactDOM.render(VDOM, document.getElementById('test')) </script> </body> </html>
发现,不用jsx也是可以的。
但是,如果要求
const VDOM = React.createElement('h1', {id:'title'}, 'Hello React')
有好几层,我们创建是不是就需要多次调用React.createElement来满足我们的要求,这样是比较麻烦的。
比如标注1:React.createElement('h1', {id:'title'},React.createElement('span', {}, 'Hello React'))。
所以我们使用jsx语法就方便多了。
其实,经过babel翻译之后,上面的标注2语法会被翻译成标注1.
二、jsx规范以及虚拟DOM和真实DOM比较
1 虚拟DOM和真实DOM比较
1 虚拟DOM本质是Object对象(一般js对象)
2 虚拟DOM比较“轻”,真实DOM比较“重”。因为虚拟DOM是在React内部使用,无需真实DOM那么多属性。
3 虚拟DOM最终会被React转化为真实DOM,呈现在页面上
2 jsx规则
1 定义虚拟DOM时,不要写引号
const VDOM = <h1>Hello,React</h1>
2 标签中混入js表达式时,要用花括号{}
const myId='aaaa';
//1.创建虚拟DOM
const VDOM = <h1 id={myId}>Hello,React</h1>
3 样式的类名指定不要用class,要用className
4 内联样式要用style={{key:value}}的形式去写,外层花括号表示要写js表达式(要区分表达式和语句),内层花括号表示要写一个对象。
5 虚拟DOM必须只有一个根标签
6 标签必须闭合
7 标签首字母,
若小写字母开头,则将改标签转为html同名元素,若html中没有该标签对应的同名元素,则报错。
若大写字母开头,react就去渲染对应的组件,若没有该组件定义则报错。
三、react组件编程
1 模块化和组件化
模块化,指的是js代码,我们按照一个个业务模块来写js代码,一个模块对应一个js文件,只包含了对js的规整
组件化,指的是一个组件内包含了html,css,js,音视频等媒体。
2 函数式组件
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<!--准备好一个容器-->
<div id="test"></div>
<!--引入react核心库-->
<script type="text/javascript" src="../js/react.development.js"></script>
<!--引入react-dom,用于支持react操作DOM-->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../js/babel.js"></script>
<script type="text/babel"> /*此处一定要写babel*/
//1.创建函数式组件
function Demo()
{
return <h2>我是用函数定义的组件</h2>
}
//2.渲染组件到页面
ReactDOM.render(<Demo/>, document.getElementById('test'))
</script>
</body>
</html>
上面代码的执行过程
1 React解析组件标签,找到Demo组件
2 发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,随后呈现在页面上。
3 类式组件
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<!--准备好一个容器-->
<div id="test"></div>
<!--引入react核心库-->
<script type="text/javascript" src="../js/react.development.js"></script>
<!--引入react-dom,用于支持react操作DOM-->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../js/babel.js"></script>
<script type="text/babel"> /*此处一定要写babel*/
//1.创建类式组件
class Demo extends React.Component {
render() {
return <h2>我是用类定义的组件</h2>
}
}
//2.渲染组件到页面
ReactDOM.render(<Demo/>, document.getElementById('test'))
</script>
</body>
</html>
上面代码的执行过程
1 React解析组件标签,找到Demo组件
2 发现组件是使用类定义的,随后new出来该类的实例,并通过该实例调用到原型上的render方法
3 将返回的虚拟DOM转为真实DOM,随后呈现在页面上。
四、组件实例的三大核心属性-state
0 复习知识
0.1 实例中的this对象
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<script type="text/javascript">
function demo() {
'use strict'
console.log(this);
}
demo();
</script>
</body>
</html>
不用use strict输出
Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
使用use strict输出
undefined
再举个例子
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<script type="text/javascript">
class Weather {
changeWeather(){
console.log(this);
}
}
const weather = new Weather();
weather.changeWeather();
const weather2 = weather.changeWeather;
weather2();
</script>
</body>
</html>
输出:
Weather {}
undefined
这里 weather.changeWeather(); 是对象调用, weather2(); 是直接调用。
changeWeather方法放到哪里了?-----类的原型对象上,在堆上,供实例使用
类中所有方法都在局部开启了严格模式。
0.2 bind
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<script type="text/javascript">
function demo() {
console.log(this);
}
demo();
const x = demo.bind({a:1,b:2})
x()
</script>
</body>
</html>
输出:
Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
{a: 1, b: 2}
demo.bind为我们生成了一个新的函数,这个函数返回值为{a:1,b:2}
1 state
下面实例我们实现一个简单的功能,点击一串文字,来回切换文字内容
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<!--准备好一个容器-->
<div id="test"></div>
<!--引入react核心库-->
<script type="text/javascript" src="../js/react.development.js"></script>
<!--引入react-dom,用于支持react操作DOM-->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../js/babel.js"></script>
<script type="text/babel"> /*此处一定要写babel*/
class Weather extends React.Component {
constructor(props){
super(props)
this.state = {isHot:false}
}
render() {
console.log(this)
const {isHot} = this.state
return <h2 onClick={this.changeWather}>今天天气很{isHot ? '炎热':'凉爽'}</h2>
}
changeWather(){
console.log(this);
}
}
//渲染组件到页面
ReactDOM.render(<Demo/>, document.getElementById('test'))
</script>
</body>
</html>
注意点:
1 不能写成 <h2 onClick={changeWather()}>今天天气很{isHot ? '炎热':'凉爽'}</h2> ,因为{}内需要是表达式,表达式一定会返回一个变量, changeWeather() 返回的是undefined,要写成 changeWeather ,其返回的是一个函数,只有这样点击的时候才能够调用到函数
2 要加上this <h2 onClick={this.changeWather}> ,这是由js类的语法决定的,不能在类中直接调用类的另一个函数,类中的函数只能由类的实例来调用,所以要加上this来调用。
上述代码执行时,changeWeather中打印出来的是undefined,就是因为代码中注释中所描述的原因。解决这个问题,需要使用bind把原型中的changeWeather函数定义放到当前对象中。
代码修改如下
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<!--准备好一个容器-->
<div id="test"></div>
<!--引入react核心库-->
<script type="text/javascript" src="../js/react.development.js"></script>
<!--引入react-dom,用于支持react操作DOM-->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../js/babel.js"></script>
<script type="text/babel"> /*此处一定要写babel*/
class Weather extends React.Component {
constructor(props){
super(props)
this.state = {isHot:false}
this.changeWeather = this.changeWeather.bind(this);//右边部分顺着原型链找到原型中的changeWeather定义,然后通过bind生成一个新的函数,该函数返回当前对象this。左边把该新生成的函数赋值给this.changeWeather
}
render() {
console.log(this)
const {isHot} = this.state
return <h2 onClick={this.changeWeather}>今天天气很{isHot ? '炎热':'凉爽'}</h2>
}
//changeWeather放在哪里?-----Weather的原型对象上,供实例使用
//由于changerWeather是作为onClick的回调,所以不是通过实例调用的,是直接调用
//类中的方法默认开启了局部的严格模式,所以changeWeather中的this为undefined
changeWeather(){
console.log(this);
//获取原来的isHot值
const isHot = this.state.isHot;
//状态state不可直接更改,下面这行就是直接更改
//this.state.isHot=!isHot;
this.setState({isHot:!isHot});
}
}
//渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>
</body>
</html>
至此,我们可以页面查看现象,刚开始显示“炎热”,点击后显示“凉爽”,再点击显示“炎热”,依次循环。
当state变化时,react都不辱使命的重新执行一次render
对上面代码的精简和优化
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<!--准备好一个容器-->
<div id="test"></div>
<!--引入react核心库-->
<script type="text/javascript" src="../js/react.development.js"></script>
<!--引入react-dom,用于支持react操作DOM-->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../js/babel.js"></script>
<script type="text/babel"> /*此处一定要写babel*/
class Weather extends React.Component {
//初始化状态
state = {isHot:false}
render() {
console.log(this)
const {isHot} = this.state
return <h2 onClick={this.changeWeather}>今天天气很{isHot ? '炎热':'凉爽'}</h2>
}
//自定义方法----要用赋值语句+箭头函数的方法
changeWeather=()=>{
console.log(this);
const isHot = this.state.isHot;
this.setState({isHot:!isHot});
}
}
//渲染组件到页面
ReactDOM.render(<Weather/>, document.getElementById('test'))
</script>
</body>
</html>
1 去掉了构造函数,state直接在类里面定义
2 去掉了构造函数中changeWeather使用bind方法
3 changeWeather方法改为赋值语句,且使用箭头函数,箭头函数的作用是,当this方法为undefined时,使用其上层对象来作为this。
总结:
state是组件对象最重要的属性,值是对象
组件被称为“状态机”,通过更新组件的state来更新对应页面显示(重新渲染组件)
组件中render方法中的this为组件实例对象,组件自定义方法中this为undefined,如何解决? a.通过函数对象的bind强制绑定this,b.箭头函数
五、组件实例的三大核心属性-props
0 展开运算符复习
1 props的基本使用
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<div id="test1"></div>
<div id="test2"></div>
<div id="test3"></div>
<!--引入react核心库-->
<script type="text/javascript" src="../js/react.development.js"></script>
<!--引入react-dom,用于支持react操作DOM-->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../js/babel.js"></script>
<script type="text/babel"> /*此处一定要写babel*/
class Person extends React.Component {
render() {
console.log(this)
const {name, age, sex} = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
}
//渲染组件到页面
ReactDOM.render(<Person name="tom" age="19" sex="男"/>, document.getElementById('test1'))
ReactDOM.render(<Person name="jerry" age="20" sex="女"/>, document.getElementById('test2'))
// ReactDOM.render(<Person name="lucy" age="21" sex="女"/>, document.getElementById('test3'))
//另一种写法
const p = {name:"lucy", age:"21", sex:"女"}
ReactDOM.render(<Person {...p}/>, document.getElementById('test3'))//原生js不允许使用展开运算符展开一个对象,但是react+babel就可以了,且展开运算符只能用在属性传值的情形。
</script>
</body>
</html>
下面我们有个需求,姓名必须指定,且为字符串;性别为字符串,如果不指定默认为“男”;年龄为字符串,且数字类型,默认18;年龄显示为实际年龄+1;
我们需要引入PropTypes.js
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<div id="test1"></div>
<div id="test2"></div>
<div id="test3"></div>
<!--引入react核心库-->
<script type="text/javascript" src="../js/react.development.js"></script>
<!--引入react-dom,用于支持react操作DOM-->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../js/babel.js"></script>
<!--引入prop-types,用于对组件标签属性进行限制-->
<script type="text/javascript" src="../js/prop-types.js"></script>
<script type="text/babel"> /*此处一定要写babel*/
class Person extends React.Component {
render() {
console.log(this)
const {name, age, sex} = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age+1}</li>
</ul>
)
}
}
//对标签属性进行类型限制
Person.propTypes = {
name: PropTypes.string.isRequired,
sex: PropTypes.string,
age: PropTypes.number,
speak: PropTypes.func,
}
Person.defaultProps = {
sex: '不男不女',
age: 18,
}
//渲染组件到页面
ReactDOM.render(<Person name="tom" age={19} sex="男" speak={speak}/>, document.getElementById('test1'))
ReactDOM.render(<Person name="jerry" age={20} sex="女"/>, document.getElementById('test2'))
// ReactDOM.render(<Person name="lucy" age="21" sex="女"/>, document.getElementById('test3'))
//另一种写法
const p = {name:'lucy', age:21, sex:"女"}
ReactDOM.render(<Person {...p}/>, document.getElementById('test3'))//原生js不允许使用展开运算符展开一个对象,但是react+babel就可以了。
function speak() {
console.log('我说话了');
}
</script>
</body>
</html>
2 上述代码的优化
上述代码我们使用了Person.propTypes和Person.defaultProps对Person的类型进行限制,这种写法写在了类定义的外部,是React示例中的写法,我们希望Person类具有内聚性,也就是这两个属性希望写在类的内部。改法如下
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<div id="test1"></div>
<div id="test2"></div>
<div id="test3"></div>
<!--引入react核心库-->
<script type="text/javascript" src="../js/react.development.js"></script>
<!--引入react-dom,用于支持react操作DOM-->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../js/babel.js"></script>
<!--引入prop-types,用于对组件标签属性进行限制-->
<script type="text/javascript" src="../js/prop-types.js"></script>
<script type="text/babel"> /*此处一定要写babel*/
class Person extends React.Component {
//对标签属性进行类型限制
static propTypes = {
name: PropTypes.string.isRequired,
sex: PropTypes.string,
age: PropTypes.number,
speak: PropTypes.func,
}
static defaultProps = {
sex: '不男不女',
age: 18,
}
render() {
console.log(this)
const {name, age, sex} = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age+1}</li>
</ul>
)
}
}
//渲染组件到页面
ReactDOM.render(<Person name="tom" age={19} sex="男" speak={speak}/>, document.getElementById('test1'))
ReactDOM.render(<Person name="jerry" age={20} sex="女"/>, document.getElementById('test2'))
// ReactDOM.render(<Person name="lucy" age="21" sex="女"/>, document.getElementById('test3'))
//另一种写法
const p = {name:'lucy', age:21, sex:"女"}
ReactDOM.render(<Person {...p}/>, document.getElementById('test3'))//原生js不允许使用展开运算符展开一个对象,但是react+babel就可以了。
function speak() {
console.log('我说话了');
}
</script>
</body>
</html>
3 有关构造器是否传递props的讨论
在react中文官网中,搜索constructor,找到其指导文档,文档中有如下描述
如果我们希望在构造函数中使用this.props属性,我们必须传递props给构造器,且传递给super()方法
如下示例。
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<div id="test1"></div>
<!--引入react核心库-->
<script type="text/javascript" src="../js/react.development.js"></script>
<!--引入react-dom,用于支持react操作DOM-->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../js/babel.js"></script>
<!--引入prop-types,用于对组件标签属性进行限制-->
<script type="text/javascript" src="../js/prop-types.js"></script>
<script type="text/babel"> /*此处一定要写babel*/
class Person extends React.Component {
constructor(props) {
//构造器是否接收props,是否传递给super,取决于:是否希望在构造器中通过this访问props
super(props);
console.log('constructor中使用this.props:', this.props);
}
//对标签属性进行类型限制
static propTypes = {
name: PropTypes.string.isRequired,
}
static defaultProps = {
sex: '不男不女',
age: 18,
}
render() {
const {name, age, sex} = this.props
return (
<ul><li>姓名:{name}</li><li>性别:{sex}</li><li>年龄:{age+1}</li></ul>
)
}
}
//渲染组件到页面
ReactDOM.render(<Person name="tom"/>, document.getElementById('test1'))
</script>
</body>
</html>
如果我们不传props,打印出undefined;我们传递props,打印出this.props的值。
其实,这种情况基本不存在,一般我们不会写构造器,即使写构造器,也不会在构造器中使用this.props。而且我们已经传递了props,为啥还需要使用this.props呢?
4 函数式组件使用props
我们讲,组件实例的三大属性:state,props和refs,都是对组件实例来说的。对于函数式组件,本身是一个函数,根本不存在实例,所以函数式组件不能使用这三个东西。但是有一个例外,就是props。函数式组件可以使用props属性,原因是函数式组件可以接收参数。如下所示:
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<div id="test1"></div>
<!--引入react核心库-->
<script type="text/javascript" src="../js/react.development.js"></script>
<!--引入react-dom,用于支持react操作DOM-->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../js/babel.js"></script>
<!--引入prop-types,用于对组件标签属性进行限制-->
<script type="text/javascript" src="../js/prop-types.js"></script>
<script type="text/babel"> /*此处一定要写babel*/
function Person(props) {
const {name, age, sex} = props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age+1}</li>
</ul>
)
}
//渲染组件到页面
ReactDOM.render(<Person name="tom" sex="男" age={19}/>, document.getElementById('test1'))
</script>
</body>
</html>
函数式组件接收一个叫做props的参数,从父组件获取到props属性。
六、组件实例的三大核心属性-refs
1 字符串类型ref
组件的ref属性允许我们拿到该组件的一个真实dom节点,注意,拿到的是真实DOM节点,并非虚拟DOM节点。
该属性类似于原生html标签中的id属性。
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<div id="test1"></div>
<!--引入react核心库-->
<script type="text/javascript" src="../js/react.development.js"></script>
<!--引入react-dom,用于支持react操作DOM-->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../js/babel.js"></script>
<!--引入prop-types,用于对组件标签属性进行限制-->
<script type="text/javascript" src="../js/prop-types.js"></script>
<script type="text/babel"> /*此处一定要写babel*/
class Demo extends React.Component {
showData = ()=>{
console.log(this)
console.log(this.refs.input1);
alert(this.refs.input1.value);
}
showData2 = ()=>{
console.log(this.refs.input2);
alert(this.refs.input2.value);
}
render() {
return (
<div>
<input ref="input1" type="text" palceholder="点击按钮提示数据"/>
<button onClick={this.showData}>点我提示左侧的数据</button>
<input ref="input2" onBlur={this.showData2} type="text" palceholder="失去焦点提示数据"/>
</div>
)
}
}
//渲染组件到页面
ReactDOM.render(<Demo/>, document.getElementById('test1'))
</script>
</body>
</html>
我们点击按钮后输出,我们看到,打印出this中包含的refs属性,以及this.refs.input1(一个真实DOM节点)
2 回调形式的ref
根据react官网上的介绍,不推荐使用字符串形式的ref,会有性能问题。下面介绍回调形式的ref。回调形式的ref属性处更改为一个回调函数,比如input1,我们应该这样理解:把ref当前所处的节点挂在了实例自身上,并且取了个名字叫input1。
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<div id="test1"></div>
<!--引入react核心库-->
<script type="text/javascript" src="../js/react.development.js"></script>
<!--引入react-dom,用于支持react操作DOM-->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../js/babel.js"></script>
<!--引入prop-types,用于对组件标签属性进行限制-->
<script type="text/javascript" src="../js/prop-types.js"></script>
<script type="text/babel"> /*此处一定要写babel*/
class Demo extends React.Component {
showData = ()=>{
const {input1} = this
alert(input1.value);
}
showData2 = ()=>{
const {input2} = this
alert(input2.value);
}
render() {
return (
<div>
//把ref当前所处的节点挂在了实例自身上,并且取了个名字叫input1
<input ref={(currentNode)=>{this.input1 = currentNode}} type="text" palceholder="点击按钮提示数据"/>
<button onClick={this.showData}>点我提示左侧的数据</button>
<input onBlur={this.showData2} ref={c => this.input2 = c} type="text" palceholder="失去焦点提示数据"/>
</div>
)
}
}
//渲染组件到页面
ReactDOM.render(<Demo/>, document.getElementById('test1'))
</script>
</body>
</html>
React遇到ref属性时,发现是一个回调函数,React就会执行这个回调函数,入参是当前节点,并在函数体中把当前节点挂在实例this上,取名叫input1。然后我们就可以在其他能触及到该实例this的地方使用input1了。比如showData函数中。
3 createRef形式
这种方式是React最推荐的方式
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<div id="test1"></div>
<!--引入react核心库-->
<script type="text/javascript" src="../js/react.development.js"></script>
<!--引入react-dom,用于支持react操作DOM-->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../js/babel.js"></script>
<!--引入prop-types,用于对组件标签属性进行限制-->
<script type="text/javascript" src="../js/prop-types.js"></script>
<script type="text/babel"> /*此处一定要写babel*/
class Demo extends React.Component {
/*
React.createRef调用后可以返回一个容器,该容器可以存储被ref所标识的节点
*/
myRef = React.createRef()
myRef2 = React.createRef()
showData = ()=>{
console.log(this.myRef)
alert(this.myRef.current.value);
}
showData2 = ()=>{
console.log(this.myRef2)
alert(this.myRef2.current.value);
}
render() {
return (
<div>
<input ref={this.myRef} type="text" palceholder="点击按钮提示数据"/>
<button onClick={this.showData}>点我提示左侧的数据</button>
<input onBlur={this.showData2} ref={this.myRef2} type="text" palceholder="失去焦点提示数据"/>
</div>
)
}
}
//渲染组件到页面
ReactDOM.render(<Demo/>, document.getElementById('test1'))
</script>
</body>
</html>
我们首先给实例添加一个 myRef = React.createRef() 属性,调用后可以返回一个容器,该容器可以保存被ref所标识的节点。
我们在input中 <input ref={this.myRef} 这种方式使用
七、React事件
1 引言
2 onXxx
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<div id="test1"></div>
<!--引入react核心库-->
<script type="text/javascript" src="../js/react.development.js"></script>
<!--引入react-dom,用于支持react操作DOM-->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../js/babel.js"></script>
<!--引入prop-types,用于对组件标签属性进行限制-->
<script type="text/javascript" src="../js/prop-types.js"></script>
<script type="text/babel"> /*此处一定要写babel*/
class Demo extends React.Component {
/*
(1).通过OnXxx属性指定事件处理函数
a.React使用的是自定义(合成)事件,而不是使用原生DOM事件---为了更好的兼容性
b.React中的事件是通过事件委托方式处理的(委托给组件最外层的元素)----为了高效
(2).通过event.target得到发生事件的DOM元素对象----不要过度使用ref
*/
showData = (event)=>{
console.log(event.target)
alert(event.target.value);
}
render() {
return (
<div>
<input onBlur={this.showData} type="text" palceholder="失去焦点提示数据"/>
</div>
)
}
}
//渲染组件到页面
ReactDOM.render(<Demo/>, document.getElementById('test1'))
</script>
</body>
</html>
2 非受控组件
原生form表单
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<div id="test1"></div>
<!--引入react核心库-->
<script type="text/javascript" src="../js/react.development.js"></script>
<!--引入react-dom,用于支持react操作DOM-->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../js/babel.js"></script>
<!--引入prop-types,用于对组件标签属性进行限制-->
<script type="text/javascript" src="../js/prop-types.js"></script>
<script type="text/babel"> /*此处一定要写babel*/
class Login extends React.Component {
render() {
return (
<form action="http://meta.bgi.com">
用户名:<input type="text" name="username"/>
密码:<input type="password" name="password"/>
<button>登录</button>
</form>
)
}
}
//渲染组件到页面
ReactDOM.render(<Login/>, document.getElementById('test1'))
</script>
</body>
</html>
action表示表单提交给哪个url处理,不写请求方式默认GET, <input type="text" name="username"/> 中的name为提交表单时的参数名
点击登录后页面跳转到meta.bgi.com并携带两个参数。
下面我们提一个需求,输入用户名和密码,点击登录,弹框显示输入的用户名和密码。
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<div id="test1"></div>
<!--引入react核心库-->
<script type="text/javascript" src="../js/react.development.js"></script>
<!--引入react-dom,用于支持react操作DOM-->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../js/babel.js"></script>
<!--引入prop-types,用于对组件标签属性进行限制-->
<script type="text/javascript" src="../js/prop-types.js"></script>
<script type="text/babel"> /*此处一定要写babel*/
class Login extends React.Component {
handleSubmit = ()=>{
const {username, password} = this;
alert('用户名:${username.value},密码:${passwordvalue}')
}
render() {
return (
<form action="http://meta.bgi.com" onSubmit={this.handleSubmit}>
用户名:<input ref={c => this.username=c} type="text" name="username"/>
密码:<input ref={c => this.password=c} type="password" name="password"/>
<button>登录</button>
</form>
)
}
}
//渲染组件到页面
ReactDOM.render(<Login/>, document.getElementById('test1'))
</script>
</body>
</html>
我们可以看到,当我们提交后,确实能弹框显示用户名和密码。
且x掉弹框后页面发生了跳转
但是页面发生了跳转,我们希望页面不跳转不刷新,只弹框显示
我们去掉action <form action="http://meta.bgi.com" 。页面不跳转了,但是页面还是会刷新
我们使用event事件阻止表单提交,页面就不会刷新了。
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<div id="test1"></div>
<!--引入react核心库-->
<script type="text/javascript" src="../js/react.development.js"></script>
<!--引入react-dom,用于支持react操作DOM-->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../js/babel.js"></script>
<!--引入prop-types,用于对组件标签属性进行限制-->
<script type="text/javascript" src="../js/prop-types.js"></script>
<script type="text/babel"> /*此处一定要写babel*/
class Login extends React.Component {
handleSubmit = (event)=>{
event.preventDefault()
const {username, password} = this;
alert('用户名:'+username.value+',密码:'+password.value)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:<input ref={c => this.username=c} type="text" name="username"/>
密码:<input ref={c => this.password=c} type="password" name="password"/>
<button>登录</button>
</form>
)
}
}
//渲染组件到页面
ReactDOM.render(<Login/>, document.getElementById('test1'))
</script>
</body>
</html>
复习:原生html表单提交会自动触发页面刷新,如果指定了action页面还会跳转。以上示例是为了复习我们原生表单的特性。
上面我们就写了一个非受控组件。组件的状态不受React控制,现用现取。
3 受控组件
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<div id="test1"></div>
<!--引入react核心库-->
<script type="text/javascript" src="../js/react.development.js"></script>
<!--引入react-dom,用于支持react操作DOM-->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../js/babel.js"></script>
<!--引入prop-types,用于对组件标签属性进行限制-->
<script type="text/javascript" src="../js/prop-types.js"></script>
<script type="text/babel"> /*此处一定要写babel*/
class Login extends React.Component {
state = {
username:'',
password:'',
}
saveUsername = (event)=>{
this.setState({username:event.target.value})
}
savePassword = (event)=>{
this.setState({password:event.target.value})
}
handleSubmit = (event)=>{
event.preventDefault()
const {username, password} = this.state;
alert('用户名:'+username+',密码:'+password)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:<input onChange={this.saveUsername} type="text" name="username"/>
密码:<input onChange={this.savePassword} type="password" name="password"/>
<button>登录</button>
</form>
)
}
}
//渲染组件到页面
ReactDOM.render(<Login/>, document.getElementById('test1'))
</script>
</body>
</html>
我们修改<input>元素,去掉ref属性,添加onChange属性, <input onChange={this.saveUsername} 然后定义了两个函数,然后在函数中修改state this.setState({username:event.target.value}) 。然后在表单提交的时候,我们直接取state中的username。就是受控组件。也就是说组件的状态受React控制。
4 对上面的优化
对象基本知识复习
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<script type="text/javascript">
let a = 'name'
let obj = {}
// obj.name = 'tom'
obj[a] = 'tom'
console.log(obj)
</script>
</body>
</html>
我们想得到一个{'name':'tom'}的对象,我们可以使用[]的形式获取变量的值。 obj[a] = 'tom' 。
接下来我们对上例进行优化
我们发现saveUsername和savePassword函数高度重合,我们可以整合成一个
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<div id="test1"></div>
<!--引入react核心库-->
<script type="text/javascript" src="../js/react.development.js"></script>
<!--引入react-dom,用于支持react操作DOM-->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../js/babel.js"></script>
<!--引入prop-types,用于对组件标签属性进行限制-->
<script type="text/javascript" src="../js/prop-types.js"></script>
<script type="text/babel"> /*此处一定要写babel*/
class Login extends React.Component {
state = {
username:'',
password:'',
}
saveFormdata = (dataType)=>{
return (event)=>{
this.setState({[dataType]:event.target.value})
}
}
handleSubmit = (event)=>{
event.preventDefault()
const {username, password} = this.state;
alert('用户名:'+username+',密码:'+password)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:<input onChange={this.saveFormdata("username")} type="text" name="username"/>
密码:<input onChange={this.saveFormdata("password")} type="password" name="password"/>
<button>登录</button>
</form>
)
}
}
//渲染组件到页面
ReactDOM.render(<Login/>, document.getElementById('test1'))
</script>
</body>
</html>
上面有个变动点,就是 <input onChange={this.saveUsername} 改为了 <input onChange={this.saveFormdata("username")} 。
我们要注意,onChange是一个回调函数,所以修改前 <input onChange={this.saveUsername} 不能加(),加上() <input onChange={this.saveUsername()} 就变成onChange为函数的返回值了,而 this.saveUsername() 返回值是undefined,执行肯定会报错。而 <input onChange={this.saveFormdata("username")} 则不同,其函数的返回值还是一个函数,所以可以这样写。并且React帮我们把event传递到函数内的回调函数中。
5 高阶函数和函数的柯里化
5.1高阶函数
如果一个函数符合下面2个规范中的任何一个,那该函数就是高阶函数
1 若A函数,接收的参数是一个函数,那么该函数就是高阶函数
2 若A函数,调用的返回值依然是一个函数,那么A就是高阶函数
常见的高阶函数:Promise、setTimeout、arr.map()等等
5.2函数的柯里化
通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式。
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<script type="text/javascript">
//不使用柯里化
// function sum(a,b,c) {
// return a+b+c;
// }
// const result = sum(1,2,3)
//使用柯里化
function sum(a) {
return (b)=>{
return (c)=>{
return a+b+c
}
}
}
const result = sum(1)(2)(3)
console.log(result)
</script>
</body>
</html>
上例中如下部分其实就是用的函数的柯里化
saveFormdata = (dataType)=>{
return (event)=>{
this.setState({[dataType]:event.target.value})
}
}
在调用的地方
<input onChange={this.saveFormdata("username")}
我们首先传递了dataType到函数中,然后React生成一个事件event,然后再将event作为参数传递给内层函数,然后在内层函数中对dataType和event做了统一的处理。
差不多类似于这样调用 this.saveFormdata("username")(event) ,只不过event是React生成的事件,我们接触不到,柯里化调用 this.saveFormdata("username")(event) 也是在React内部实现的。
八、组件的生命周期
1 常用组件生命周期
组件从创建到死亡会经历一些特定的阶段
React组件中包含一系列钩子函数(生命周期回调函数),会在特定的时刻调用
我们在定义组件时,会在特定的生命周期回调函数中做特定的工作。
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<div id="test1"></div>
<!--引入react核心库-->
<script type="text/javascript" src="../js/react.development.js"></script>
<!--引入react-dom,用于支持react操作DOM-->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../js/babel.js"></script>
<!--引入prop-types,用于对组件标签属性进行限制-->
<script type="text/javascript" src="../js/prop-types.js"></script>
<script type="text/babel"> /*此处一定要写babel*/
class Login extends React.Component {
state = {opacity:1,}
//组件挂载完毕
componentDidMount(){
this.timer = setInterval(() => {
let {opacity} = this.state
opacity-=0.1
if(opacity <= 0) opacity = 1
this.setState({opacity})
}, 200);
}
//组件将要卸载时
componentWillUnmount(){
//清除定时器
clearInterval(this.timer)
}
death = ()=> {
//卸载组件
ReactDOM.unmountComponentAtNode(document.getElementById('test1'))
}
//初始化渲染、状态更新之后
render() {
return (
<div>
<h2 style={{opacity:this.state.opacity}}>React学不会怎么办?</h2>
<button onClick={this.death}>不活了</button>
</div>
)
}
}
//渲染组件到页面
ReactDOM.render(<Login/>, document.getElementById('test1'))
</script>
</body>
</html>
2 组件声明周期演示图
3 全部组件生命周期
初始化阶段:由组件render触发
constructor
componentWillMount
render
componentDidMount
更新阶段:由组件内部this.setState触发或父组件render触发
shoudComponentUpdate
componentWillUpdate
render
componentDidUpdate
卸载组件:
componentWillUnmount
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<div id="test1"></div>
<!--引入react核心库-->
<script type="text/javascript" src="../js/react.development.js"></script>
<!--引入react-dom,用于支持react操作DOM-->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../js/babel.js"></script>
<!--引入prop-types,用于对组件标签属性进行限制-->
<script type="text/javascript" src="../js/prop-types.js"></script>
<script type="text/babel"> /*此处一定要写babel*/
class Count extends React.Component {
//构造器
constructor(props){
console.log('Count---constructor')
super(props)
this.state = {count:0}
}
//组件将要挂载的钩子
componentWillMount(){
console.log('Count---componentWillMount')
}
//组件挂载完毕
componentDidMount(){
console.log('Count---componentDidMount')
}
//组件将要卸载时
componentWillUnmount(){
console.log('Count---componentWillUnmount')
}
//控制组件更新的阀门
shoudComponentUpdate(){
console.log('Count---shoudComponentUpdate')
return true
}
//组件将要更新的钩子
componentWillUpdate(){
console.log('Count---componentWillUpdate')
}
//组件更新完毕的钩子
componentDidUpdate(){
console.log('Count---componentDidUpdate')
}
//加1按钮的回调
add = ()=> {
const {count} = this.state
this.setState({count:count+1})
}
//卸载组件按钮的回调
death = ()=> {
//卸载组件
ReactDOM.unmountComponentAtNode(document.getElementById('test1'))
}
//强制更新按钮的回调
force = ()=> {
this.forceUpdate()
}
//初始化渲染、状态更新之后
render() {
console.log('Count---render')
const {count} = this.state
return (
<div>
<h2>当前求和为:{count}</h2>
<button onClick={this.add}>点我+1</button>
<button onClick={this.death}>卸载组件</button>
<button onClick={this.force}>不更改任何状态中的数据,强制更新一下</button>
</div>
)
}
}
class A extends React.Component {
state = {carName:'奔驰'}
changeCar = ()=>{
this.setState({carName:'奥拓'})
}
render() {
return (
<div>
<div>我是A组件</div>
<button onClick={this.changeCar}>换车</button>
<B carName={this.state.carName}/>
</div>
);
}
}
class B extends React.Component {
componentWillReceiveProps() {
console.log('B---componentWillReceiveProps')
}
render() {
return (
<div>
我是B组件,接收到的车是:{this.props.carName}
</div>
);
}
}
//渲染组件到页面
ReactDOM.render(<Count/>, document.getElementById('test1'))
// ReactDOM.render(<A/>, document.getElementById('test1'))
</script>
</body>
</html>
九、Diffing算法
1 Diffing算法
React维护了一个虚拟DOM,在页面渲染时,React会把虚拟DOM中的每个标签和真实DOM进行比较,有变化才更新,没变化不更新,这就是React中的Diffing算法。下面我们写一个简单的示例演示Diffing。
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<div id="test1"></div>
<!--引入react核心库-->
<script type="text/javascript" src="../js/react.development.js"></script>
<!--引入react-dom,用于支持react操作DOM-->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../js/babel.js"></script>
<!--引入prop-types,用于对组件标签属性进行限制-->
<script type="text/javascript" src="../js/prop-types.js"></script>
<script type="text/babel"> /*此处一定要写babel*/
class Time extends React.Component {
state = {date:new Date()}
componentDidMount() {
setInterval(() => {
this.setState({date:new Date()})
}, 1000)
}
render() {
return (
<div>
<h1>hello</h1>
<input type='text'/>
<span>现在是:{this.state.date.toTimeString()}</span>
</div>
)
}
}
//渲染组件到页面
ReactDOM.render(<Time/>, document.getElementById('test1'))
</script>
</body>
</html>
我们会看到页面上的时间一直在更新,而我们在输入框中输入内容,如果输入框这个标签有更新的话,其内容必将消失,但是我们看到的是输入框中的内容一直不变,这就说明了Diffing算法确实起作用了。
2 虚拟DOM中的key
虚拟DOM中key的作用:
1)简单的说,key是虚拟DOM对象的标识,在更新显示时key起着极其重要的作用
2)详细的说:当状态数据发生变化时,react会根据【新数据】生成【新的虚拟DOM】,随后React进行【新虚拟DOM】和【旧虚拟DOM】的diff比较,比较规则如下:
a. 旧虚拟DOM中找到了与新虚拟DOM相同的key:
(1)若虚拟DOM中内容没变,直接使用之前的真实DOM
(2)若虚拟DOM中的内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM
b. 旧虚拟DOM中未找到与新虚拟DOM相同的key
根据数据创建新的真实DOM,随后渲染到页面。
下面例子分别演示了使用index索引作为key和使用id作为key的比较
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>hello_react</title></head>
<body>
<div id="test1"></div>
<!--引入react核心库-->
<script type="text/javascript" src="../js/react.development.js"></script>
<!--引入react-dom,用于支持react操作DOM-->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!--引入babel,用于将jsx转换为js-->
<script type="text/javascript" src="../js/babel.js"></script>
<!--引入prop-types,用于对组件标签属性进行限制-->
<script type="text/javascript" src="../js/prop-types.js"></script>
<script type="text/babel"> /*此处一定要写babel*/
/**
慢动作回放----使用index索引值作为key
数据:
{id:1,name:'小张',age:18},
{id:2,name:'小李',age:19},
初始的虚拟DOM:
<li key=0>小张---18</li>
<li key=1>小李---19</li>
更新后的数据:
<li key=3>小王---20</li>
<li key=0>小张---18</li>
<li key=1>小李---19</li>
更新树后的虚拟DOM:
<li key=0>小王---20</li>
<li key=1>小张---18</li>
<li key=2>小李---19</li>
------------------------------------------------------------
慢动作回放----使用id唯一标识索引值作为key
数据:
{id:1,name:'小张',age:18},
{id:2,name:'小李',age:19},
初始的虚拟DOM:
<li key=1>小张---18</li>
<li key=2>小李---19</li>
更新后的数据:
<li key=3>小王---20</li>
<li key=1>小张---18</li>
<li key=2>小李---19</li>
更新树后的虚拟DOM:
<li key=3>小王---20</li>
<li key=1>小张---18</li>
<li key=2>小李---19</li>
*/
class Person extends React.Component {
state = {
persons: [
{id:1,name:'小张',age:18},
{id:2,name:'小李',age:19},
]
}
add = ()=>{
const {persons} = this.state
const p = {id:3,name:'小王',age:20}
this.setState({persons:[p,...persons]})
}
render() {
return (
<div>
<h2>展示人员信息</h2>
<h3>使用index索引值作为key</h3>
<button onClick={this.add}>添加一个小王</button>
<ul>
{
this.state.persons.map((personObj, index)=>{
return <li key={index}>{personObj.name}---{personObj.age}</li>
})
}
</ul>
<hr/>
<hr/>
<h3>使用id(数据的唯一标识)索引值作为key</h3>
<ul>
{
this.state.persons.map((personObj)=>{
return <li key={personObj.id}>{personObj.name}---{personObj.age}</li>
})
}
</ul>
</div>
)
}
}
//渲染组件到页面
ReactDOM.render(<Person/>, document.getElementById('test1'))
</script>
</body>
</html>
以上功能相同,但是其性能则相差很大。
有些同学说,我管他性能好坏,我实现功能就行了,这样确实没毛病,但是下面提一个新的需求,则我们不得不考虑两种方式的区别了。
我们在上面例子中<li>组件内添加一个<input>组件 <input type="text"/></li> 。然后我们做如下截图中的操作。
发现,数据错乱了。由此看出,由什么作为虚拟DOM的key还是很重要的。新旧虚拟DOM进行比较的时候,依据的就是虚拟DOM的key,如果使用了不得当的key,会带来很大的性能问题,而且有时候会发生如上图这种比较严重的数据错乱问题。
十、React脚手架
1 React脚手架
1 xxx脚手架:用来帮助程序员快速创建一个基于xxx库的模板项目。包含了所有需要的配置,下载好了相关依赖
React提供的脚手架:create-react-app
2 创建过程
第一步:全局安装create-react-app。npm install -g create-react-app。要使用脚手架,首先这个脚手架要存在,这一步就是安装这个脚手架。
第二步:切换到想创建项目的目录,使用命令npx create-react-app 项目名创建一个项目
第三步:进入项目文件夹,启动项目,npm start
2 使用create-react-app快速创建一个项目
npx create-react-app wechat --template typescript
如果提示A template was not provided,说明create-react-app太旧,需要首先卸载,重新安装
npm uninstall -g create-react-app
查看是否卸载
$ which create-react-app
/e/nodejs/node_global/create-react-app
如果没有卸载干净,则
rm -rf /c/Users/songzhenjing/AppData/Roaming/npm/create-react-app
重新执行
npx create-react-app wechat --template typescript
看到如下界面说明安装成功
我们启动项目看一下结果,启动命令都放在package.json文件里面了。我们进入到wechat目录,查看一下这个文件
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
执行npm run start启动项目,启动完毕,浏览器将会自动为我们打开如下这个页面
至此,脚手架创建React项目成功。
十一、React组件编程实例
1 效果
我们要做的实例效果如下:
上面一个输入框,当我们按回车时可以添加一个待完成任务,
中间列表展示待完成的任务,且鼠标进入背景色深色显示;每一行有个删除按钮
下面展示已完成/全部。且可以支持全选和取消全选。且有一个按钮"删除全部已完成"
2 组件拆分
我们可以把页面拆分为3个组件,头部Header,中间List,下部Footer。其中中间每一行是一个组件Item
3 代码实现
该用例使用的技术和js相关的基础知识对于初学者非常有帮助,如果可能的话,最好参照实例手敲一遍代码。
项目结构
3.1 index.html
<!doctype html> <html lang="en"> <head> </head> <body> <div id="root"></div> </body> </html>
3.2 index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';
ReactDOM.render(<App/>, document.getElementById('root'));
index.css
body { margin: 0; padding: 0; font-family: sans-serif; }
3.3 App组件
需要注意的问题:
1 中间列表组件List展示的数据,我们直接想的话,展示的数据要放到List组件中,但是,Footer组件需要获取List组件的状态用于展示数目,所以我们不能把展示的数据放到List中,应该放到父组件App中,我们通过props属性把数据传递给List和Footer组件。
2 同时,Header中添加一条数据的时候,这条数据要展示到List组件中,而List组件的数据在其父组件App中,所以我们要想办法把Header输入框中的数据传递给App组件没然后由App组件更新state,进而触发子组件List重新渲染,使新数据展示在List中。此处我们用的方式是回调函数(或者叫高阶函数)的方式把Header中的数据传递给父组件App。
import React, { Component } from 'react';
import PropTypes from 'prop-types'
import './App.css'
import Footer from './components/Footer/Footer';
import Header from './components/Header/Header';
import List from './components/List/List';
class App extends Component {
static propTypes = {
addTodo:PropTypes.func.isRequired
}
state = {
todos: [
{id:'001', name:'吃饭', done:true},
{id:'002', name:'睡觉', done:true},
{id:'003', name:'打代码', done:false},
]
}
//用于添加一个todo,接收的参数是todo对象
addTodo = (todoObj)=>{
const {todos} = this.state
const newTodos = [todoObj,...todos]
this.setState({todos:newTodos})
}
//
updateTodo = (id,done)=>{
const {todos} = this.state
const newTodos = todos.map((todoObj)=>{
if(todoObj.id === id) return {...todoObj,done:done}
else return todoObj
})
this.setState({todos:newTodos})
}
//用于删除一个todo,接收的参数是id
deleteTodo = (id)=>{
const {todos} = this.state
const newTodos = todos.filter((todoObj)=>{
return todoObj.id !== id
})
this.setState({todos:newTodos})
}
//用于
checkAllTodo = (done)=>{
const {todos} = this.state
const newTodos = todos.map((todoObj)=>{
return {...todoObj,done:done}
})
this.setState({todos:newTodos})
}
//清除所有已经完成的
clearAllDone = ()=>{
const {todos} = this.state
const newTodos = todos.filter((todoObj)=>{
return !todoObj.done
})
this.setState({todos:newTodos})
}
render() {
return (
<div className="todo-container">
<div className="todo-wrap">
<Header addTodo={this.addTodo}/>
<List todos={this.state.todos} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo}/>
<Footer todos={this.state.todos} checkAllTodo={this.checkAllTodo} clearAllDone={this.clearAllDone}/>
</div>
</div>
);
}
}
export default App;
App.css
body { background: #fff; } .btn { display: inline-block; padding: 4px 12px; margin-bottom: 0; font-size: 14px; line-height: 20px; text-align: center; vertical-align: middle; cursor: pointer; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); border-radius: 4px; } .btn-danger { color: #fff; background-color: #da4f49; border: 1px solid #bd362f; } .btn-danger:hover { color: #fff; background-color: #bd362f; } .btn:focus { outline: none; } .todo-container { width: 600px; margin: 0 auto; } .todo-container .todo-warp { padding: 10px; border: 1px solid #ddd; border-radius: 5px; }
3.4 List组件
import React, { Component } from 'react';
import PropTypes from 'prop-types'
import Item from '../Item/Item'
import './List.css'
export default class List extends Component {
static propTypes = {
todos:PropTypes.array.isRequired,
updateTodo:PropTypes.func.isRequired,
deleteTodo:PropTypes.func.isRequired
}
render() {
const {todos, updateTodo, deleteTodo} = this.props
return (
<ul className="todo-main">
{
todos.map((todo)=>{
return <Item key={todo.id} {...todo} updateTodo={updateTodo} deleteTodo={deleteTodo}/>
})
}
</ul>
);
}
}
List.css
.todo-main { margin-left: 0px; border: 1px solid #ddd; border-radius: 2px; padding: 0px; } .todo-empty { height: 40px; line-height: 40px; border: 1px solid #ddd; border-radius: 2px; padding-left: 5px; margin-top: 10px; }
3.5 Item组件
import React, { Component } from 'react';
import './Item.css'
export default class Item extends Component {
state = {mouse:false}
handleMouse = (flag)=>{
return ()=>{
this.setState({mouse:flag})
}
}
//勾选取消勾选
handleCheck = (id)=>{
return (event)=>{
this.props.updateTodo(id,event.target.checked)
}
}
//删除按钮
handleDelete = (id)=>{
if(window.confirm('确定删除吗?')) {
this.props.deleteTodo(id)
}
}
render() {
const {id,name,done} = this.props
const {mouse} = this.state
return (
<li style={{backgroundColor:mouse ? '#ddd' : 'white'}} onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)}>
<label>
<input type="checkbox" checked={done} onChange={this.handleCheck(id)}/>
<span>{name}</span>
</label>
<button onClick={()=>{this.handleDelete(id)}} className="btn btn-danger" style={{display:mouse ? 'block' : 'none'}}>删除</button>
</li>
);
}
}
Item.css
li { list-style: none; height:36px; line-height: 36px; padding:0 5px; border-bottom: 1px solid #ddd; } li label { float: left; cursor: pointer; } li label li input { vertical-align: middle; margin-right: 6px; position: relative; top: -1px; } li button { float: right; display: none; margin-top: 3px; } li:before { content: initial; } li:last-child { border-bottom: none; }
3.6 Header组件
import React, { Component } from 'react';
// import {nanoid} from 'nanoid'
import './Header.css'
export default class Header extends Component {
handleKeyUp =(e)=>{
//判断是否是回车
if(e.keyCode !== 13) return
if(e.target.value.trim() === '') {alert('输入不能为空');return}
console.log(e.target.value)
//准备好一个todo对象
const todoObj = {id:new Date(),name:e.target.value,done:false}
//传递给父组件
this.props.addTodo(todoObj)
// this.target.value = ''
}
render() {
return (
<div className="todo-header">
<input onKeyUp={this.handleKeyUp} type="text" placeholder="请输入你的任务名称,按回车键确认"></input>
</div>
);
}
}
Header.css
.todo-header input { width: 560px; height: 28px; font-size: 14px; border: 1px solid #ccc; border-radius: 4px; padding: 4px 7px; } .todo-header input:focus { outline: none; border-color: rgba(82, 168, 236, 0.8); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); }
3.7 Footer组件
import React, { Component } from 'react';
import './Footer.css'
export default class Footer extends Component {
handleCheckAll=(event)=>{
this.props.checkAllTodo(event.target.checked)
}
handleClearAllDone=()=>{
this.props.clearAllDone()
}
render() {
const {todos} = this.props
//计算已完成的
const doneCount = todos.reduce((pre,todo)=>{return pre+(todo.done ? 1 : 0)},0)
//总数
const total = todos.length
return (
<div className="todo-footer">
<label>
<input type="checkbox" onChange={this.handleCheckAll} checked={doneCount===total && total !== 0 ? true:false}/>
</label>
<span>
<span>已完成{doneCount}</span> / 全部{total}
</span>
<button onClick={this.handleClearAllDone} className="btn btn-danger">清除已完成任务</button>
</div>
);
}
}
Footer.css
.todo-footer { height: 40px; line-height: 40px; padding-left: 6px; margin-top: 5px; } .todo-footer label { display: inline-block; margin-right: 20px; cursor: pointer; } .todo-footer label input { position: relative; top: -1px; vertical-align: middle; margin-right: 5px; } .todo-footer button { float: right; margin-top: 5px; }
4 成果演示
5 总结
1 如何确定将数据放到哪个组件的state中?
某个组件使用:放在其自身的state中
某些组件使用:放到他们共同的父组件state中(官方称此操作为:状态提升)
2 关于父子之间的通信
【父组件】给【子组件】专递数据:通过props传递
【子组件】给【父组件】传递数据:通过props传递,要求父组件提前给子组件传递一个函数
3 注意defaultChecked和checked的区别
4 状态在哪里,操作状态的方法就在哪里
代码后面会上传到github,reactstudy/src_todos
十二、使用express搭建简单的nodejs webserver
1 express简介
Express是一个简洁而灵活的node.js Web应用框架。
2 搭建过程
2.1 安装express
全局安装express和express-generator,express-generator是express脚手架,能够为我们快速构建一个express项目。
$ npm install express --save -g
$ npm install express-generator --save -g
2.2 创建项目
使用express命令创建项目,项目名叫node_server
安装依赖
cd node_server,执行cnpm install
启动服务
npm run server
页面访问,看到如下页面说明服务搭建成功。
3 项目分析
项目目录结构
bin/www,用于项目启动,里面可以配置项目端口号之类的东西
app.js,main文件,请求首先到达这里,在这里可以配置路由转发
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
app.use('/', indexRouter);
app.use('/users', usersRouter);
上面的代码说明了users开头的请求交由usersRouter处理,其余请求交由indexRouter处理。
我们看一下routes/index.js
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
module.exports = router;
上面代码收到/请求后,交由views/index.jade渲染页面,传递的参数是title:Express
我们看一下index.jade
extends layout
block content
h1= title
p Welcome to #{title}
在这里面,定义了页面元素h1和p要显示的内容。
4 添加一个接口
我们在项目中添加一个/student接口,返回一个包含学生信息的json数据
修改routes/index.js,添加如下接口
router.get('/student',(request,response)=>{
const students = [
{id:'001',name:'tom',age:18},
{id:'002',name:'jerry',age:19},
{id:'003',name:'tony',age:20},
]
response.send(students)
})
,重启服务,查看页面:
十三、使用axios请求后台和配置代理
下面我们写一个例子,用户点击按钮请求后台获取学生信息,请求的url就是上一篇中的localhost:5000/student
1 在packages.json中配置代理
我们用create-react-app脚手架新建一个项目
下载axios依赖
cnpm install axios
修改App.js
import React, { Component } from 'react';
import './App.css'
import axios from 'axios'
class App extends Component {
getStudentData = ()=>{
axios.get('http://localhost:3000/student').then(
response => {console.log('成功了',response.data)},
error => {console.log('失败了',error)}
)
}
render() {
return (
<div>
<button onClick={this.getStudentData}>点我获取学生数据</button>
</div>
);
}
}
export default App;
然后,修改package.json,添加:
"proxy": "http://localhost:5000"
,然后启动上一节中的express后台。并启动本项目,
2 setupProxy.js配置多个代理
我们删除package.json中配置的代理。
在src目录下新建setupProxy.js文件
const proxy = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
proxy('/api1',{
target: 'http://localhost:5000',
changeOrigin:true,
pathRewrite:{'^/api1':''}
}),
proxy('/api2',{
target: 'http://localhost:5001',
changeOrigin:true,
pathRewrite:{'^/api2':''}
}),
)
}
修改App.js
import React, { Component } from 'react';
import './App.css'
import axios from 'axios'
class App extends Component {
getStudentData = ()=>{
axios.get('http://localhost:3000/api1/student').then(
response => {console.log('成功了',response.data)},
error => {console.log('失败了',error)}
)
}
getCarData = ()=>{
axios.get('http://localhost:3000/api2/car').then(
response => {console.log('成功了',response.data)},
error => {console.log('失败了',error)}
)
}
render() {
return (
<div>
<button onClick={this.getStudentData}>点我获取学生数据</button>
<button onClick={this.getCarData}>点我获取汽车数据</button>
</div>
);
}
}
export default App;
当请求以/api1开头则代理把请求转发给localhost:5000
当请求以/api2开头则代理把请求转发给localhost:5001
但是运行后,页面一直报 GET http://localhost:3000/api1/student 404 (Not Found) 。暂时未解决。
十四、fetch方式发送http请求获取数据
1 Promise语法
Promise语法符合“关注分离”原则,即把一个复杂的事情,分离出几个阶段,每一个阶段都会返回一个响应,前一个响应成功之后才进行下一阶段。
例子:比如我想让小明去打瓶酱油,第一步,小明出了小区首先看一下商店是否开门,然后小明给我一个Response说开门,然后第二步,小明到达商店,询问店员是否有酱油,店员说只有老抽,没有生抽,然后小明再给我一个Response,问我只有老抽买不买,我说买。第三步小明提着一瓶老抽回来了。
详细介绍待补充。
2 fetch介绍
传统我们使用xhr(XmlHttpResquest)发送请求。其实浏览器还自带另一种Promise风格的请求方式fetch。
例子:
我们修改 十三、使用axios请求后台和配置代理 中的实例,改为使用fetch方式发送请求
import React, { Component } from 'react';
class App extends Component {
getStudentData = ()=>{
// axios.get('http://localhost:3000/student').then(
// response => {console.log('成功了',response.data)},
// error => {console.log('失败了',error)}
// )
fetch('http://localhost:3000/student').then(
response => {
console.log('联系服务器成功了', response)
return response.json()
},
error => {
console.log('联系服务器失败了',error)
return new Promise(()=>{})
}
).then(
response => {
console.log('获取数据成功了',response)
},
error => {console.log('获取数据失败了',error)}
)
}
render() {
return (
<div>
<button onClick={this.getStudentData}>点我获取学生数据</button>
</div>
);
}
}
export default App;
第一个then返回一个Promise对象,第二个then返回真正的数据。
packages.json中的配置
"proxy": "http://localhost:5000"
效果展示:
十五、React路由
1 React路由原理
1.1 单页面应用
传统的网页,由一堆html组成,我们点击其中一个链接,页面在各个html之间来回切换,伴随着整个页面的刷新。
而React单页面应用(SPA)指的是,我们点击链接时,地址栏中的url发生了变化,但是页面并不会整个刷新,只刷新部分页面。React单页面是通过路由实现的,也就是说当点击某个链接时,地址栏中的url发生变化,React路由监控机制监控到这个变化,查询路由表来确定显示哪个组件。
1.2 前端路由的基石-history
下面一个例子,我们演示React是如何做到修改地址栏地址而不引起页面刷新的,进而我们可以猜测到React路由的实现原理。代码中我们使用了 history.js 来操作浏览器的history,这样可以屏蔽原生history API的复杂性
<html>
<body>
<a href="http://www.baidu.com" onclick="return push('/test1')">push test1</a><br><br>
<button onclick="push('/test2')">push test2</button><br><br>
<button onclick="replace('/test3')">replace test3</button><br><br>
<button onclick="back()">回退</button><br><br>
<button onclick="forward()">前进</button><br><br>
<script type="text/javascript" src="https://cdn.bootcss.com/history/4.7.2/history.js"></script>
<script>
let history = History.createBrowserHistory()
function push(path) {
history.push(path)
return false
}
function replace(path) {
history.replace(path)
}
function back() {
history.goBack()
}
function forward() {
history.goForward()
}
history.listen((location)=>{
console.log('请求路由路径变化了', location)
})
</script>
</body>
</html>
注意下图中地址栏的变化
浏览器中history是一个栈,浏览器当前页面显示的是栈顶的地址。当我们把一个地址push进栈顶时,浏览器显示就会发生变化。React就是采用浏览器的history特性,当地址栏path发生变化时,React监控到这一变化,显示不同的组件。
2 实例
我们写一个实例,左边导航栏有导航菜单,右面主面板。根据导航菜单的不同主面板显示不同的组件。
在这里我们用到了bootstrap.css
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter} from 'react-router-dom'
import App from './App';
ReactDOM.render(
<BrowserRouter>
<App/>
</BrowserRouter>,
document.getElementById('root')
);
路由器:BrowserRouter,负责监控地址栏path的变化,当path变化时,查找路由表显示相应的组件
App.js
import React, { Component } from 'react';
import {Route, Routes, Link,} from 'react-router-dom'
import About from "./components/About";
import Home from "./components/Home";
import "../public/bootstrap.css"
class App extends Component {
render() {
return (
<div>
<div className="row">
<div className="col-xs-offset-2 col-xs-8">
<div className="page-header"><h2>React Router Demo</h2></div>
</div>
</div>
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
{/*原生html中,靠<a>跳转不同的页面*/}
{/*<a className="list-group-item" href="./about.html">About</a>*/}
{/*<a className="list-group-item active" href="./home.html">Home</a>*/}
{/* 在React中靠路由链接实现切换组件---编写路由链接 */}
<Link className="list-group-item" to="/about">About</Link>
<Link className="list-group-item" to="/home">Home</Link>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
{/* 注册路由*/}
<Routes><Route path="/about" element={<About/>}/>
<Route path="/home" element={<Home/>}/>
</Routes>
</div>
</div>
</div>
</div>
</div>
);
}
}
export default App;
路由链接:Link,来自于react-route-dom。
<Link className="list-group-item" to="/about">About</Link>
注册路由:
<Routes> <Route path="/about" element={<About/>}/> <Route path="/home" element={<Home/>}/> </Routes>
About.jsx
import React, {Component} from 'react';
class About extends Component {
render() {
return (
<h3>我是About内容</h3>
);
}
}
export default About;
Home.jsx
import React, {Component} from 'react';
class Home extends Component {
render() {
return (
<h3>我是Home内容</h3>
);
}
}
export default Home;
演示(注意地址栏的变化,和页面是否有刷新)
3 HashRouter
上面我们使用的是BrowserRouter,下面我们使用HashRouter
我们把index.js中的BrowserRouter改为HashRouter
import React from 'react';
import ReactDOM from 'react-dom';
import {HashRouter} from 'react-router-dom'
import App from './App';
ReactDOM.render(
<HashRouter>
<App/>
</HashRouter>,
document.getElementById('root')
);
效果,
BrowersRouter和HashRouter的区别在地址栏中,HashRouter带有#,在提交数据时#后面的内容不会被传递
接下来P85