React 进阶
- 一、认识 React
- 1、是什么?
- 2、React 特性
- 3、React 第一个实例(HTML 模板)
- 4、React 安装
- 二、React 核心
- 1、JSX
- 2、元素渲染
- 3、组件
- 4、Props
- 5、State
- 6、组件生命周期
- 7、事件处理
- 8、条件渲染
- 9、列表 & Key
- 10、表单
- 11、状态提升
- 12、组合 vs 继承
- 13、无障碍辅助功能
- 14、代码分割
- 13、Context
- PropTypes & defaultProps
- 获取真实的DOM节点(ref/refs)
- Fragment
- 14、Refs 转发
- 高阶组件(HOC)
- 面试官
- [题] 说说 Real DOM和 Virtual DOM 的区别?优缺点?
- [题] 说说 state 和 props有什么区别?
- [题] 说说 super()和super(props)有什么区别?
- [题] 说说 React中的 setState 执行机制?
- [题] 说说 React的事件机制?
- [题] 说说 React中组件之间如何通信?
- END
一、认识 React
1、是什么?
React 是一个用于构建用户界面的 JavaScript 库。
React 主要用于构建UI,很多人认为 React 是 MVC 中的 V(视图)。
React 起源于 Facebook 的内部项目,用来架设 Instagram 的网站,并于 2013 年 5 月开源。
React 拥有较高的性能,代码逻辑非常简单,越来越多的人已开始关注和使用它。
React,用于构建用户界面的 JavaScript 库,只提供了 UI 层面的解决方案
遵循组件设计模式、声明式编程范式和函数式编程概念,以使前端应用程序更高效
使用虚拟DOM来有效地操作DOM,遵循从高阶组件到低阶组件的单向数据流
帮助我们将界面成了各个独立的小块,每一个块就是组件,这些组件之间可以组合、嵌套,构成整体页面
2、React 特性
- 声明式编程
- Component组件化
- JSX语法
- 虚拟DOM
- 单向数据绑定
优势:
- 高效灵活
- 声明式的设计,简单使用
- 组件式开发,提高代码复用率
- 单向响应的数据流会比双向绑定的更安全,速度更快
3、React 第一个实例(HTML 模板)
1)引入React ,react-dom
的核心库,babel
可以将 ES6 代码转为 ES5 代码,以及对 JSX 的支持
2)ReactDOM.render()
:是 React 的最基本方法,用于将模板转为 HTML 语言,并插入指定的 DOM 节点。
3)React 独有的 JSX
语法,跟 JavaScript 不兼容。凡是使用 JSX
的地方,都要加上 type="text/babel"
<!DOCTYPE html>
<html><head><meta charset="UTF-8" /><title>Hello React!</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></head>
<body><div id="example"></div><script type="text/babel">ReactDOM.render(<h1>Hello, world!</h1>,document.getElementById('example'));</script>
</body>
</html>
4、React 安装
1)直接下载使用、直接使用eact CDN :
<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>
官方提供的 CDN 地址:
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<!-- 生产环境中不建议使用 -->
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
2)npm
【1】使用 create-react-app 快速构建 React 开发环境
create-react-app 是来自于 Facebook,通过该命令我们无需配置就能快速构建 React 开发环境。
create-react-app 自动创建的项目是基于 Webpack + ES6 。
Create React App 是一个用于学习 React 的舒适环境,也是用 React 创建新的单页应用的最佳方式。
npx create-react-app my-app
cd my-app
npm start# npx 不是拼写错误 —— 它是 npm 5.2+ 附带的 package 运行工具。
create-react-app 执行慢的解决方法:使用淘宝定制的 cnpm
$ npm install -g cnpm --registry=https://registry.npm.taobao.org
$ npm config set registry https://registry.npm.taobao.org$ cnpm install -g create-react-app
$ create-react-app my-app
$ cd my-app
$ npm start
运行结果:
项目的目录结构如下:
my-app
├── README.md 项目的说明文档
├── node_modules 项目的依赖包
├── package.json webpack配置和项目包管理文件,项目中依赖的第三方包(包的版本)和一些常用命令配置都在这个里边进行配置
├── package-lock.json 锁定安装时的版本号,并且需要上传到git,以保证其他人再npm install 时大家的依赖能保证一致
├── .gitignore git的选择性上传的配置文件,配置不需要上传的文件。
├── public 公共文件,里边有公用模板和图标等
│ ├── favicon.ico 网站或者说项目的图标,一般在浏览器标签页的左上角显示
│ ├── index.html 首页的模板文件
│ └── manifest.json 移动端配置文件
└── src 源代码├── App.css├── App.js 一个模块化编程├── App.test.js├── index.css├── index.js 项目的入口文件├── logo.svg└── serviceWorker.js 用于写移动端开发的└── setupTests.js
【2】Next.js
Next.js 是一个流行的、轻量级的框架,用于配合 React 打造静态化和服务端渲染应用。它包括开箱即用的样式和路由方案,并且假定你使用 Node.js 作为服务器环境。
【3】Gatsby
Gatsby 是用 React 创建静态网站的最佳方式。它让你能使用 React 组件,但输出预渲染的 HTML 和 CSS 以保证最快的加载速度。
【4】从头开始打造工具链
- package 管理器,比如 Yarn 或 npm。
- 打包器,比如 webpack 或 Parcel。
- 编译器,例如 Babel。
二、React 核心
1、JSX
JSX就是Javascript和XML结合的一种格式。React发明了JSX,利用HTML语法来创建虚拟DOM。当遇到
<
,JSX就当HTML解析,遇到{
就当JavaScript解析。
React的核心机制之一就是可以在内存中创建虚拟的DOM元素。React利用虚拟DOM来减少对实际DOM的操作从而提升性能。
jsx语法的优点:
1)JSX 执行更快,因为它在编译为 JavaScript 代码后进行了优化。
2)它是类型安全的,在编译过程中就能发现错误。
3)使用 JSX 编写模板更加简单快速。
1)手动安装使用JSX
安装插件
// babel 插件
npm i babel-core babel-loader babel-plugin-transform-runtime -D
npm i babel-preset-env babel-preset-stage-0 -D
// 安装能够识别转换jsx语法的包 babel-preset-react
npm i babel-preset-react -D
添加 .babelrc 配置文件
{"presets": ["env", "stage-0", "react"],"plugins": ["transform-runtime"]}
使用jsx语法
// 引用React、Component、ReactDOMimport React, { Component } from 'react'import ReactDOM from 'react-dom'; // 创建组件export default class Name extends Component {// render函数render() {var arr = [<h1>Hello world!</h1>,<h2>React is awesome</h2>,];// 这里面可以做任何js操作最后return 一个dom结构就可以了return (// 模板插入 JavaScript 变量。如果这个变量是一个数组,则会展开这个数组的所有成员<div>{arr}</div>)}}
2) jsx语法基本使用
(1)渲染html
单个html
ReactDOM.render(return (<h1>张培跃</h1>)
)
多个html
ReactDOM.render(return (<div><h1>张培跃</h1><h2>欢迎学习 React</h2><p>今天天气不错,挺风和日丽的!</p></div>))
变量的形式
var myInfo=<h1>我好帅!我好苦恼啊!</h1>;
ReactDOM.render(return myInfo;
)
【注意】不能在最外层添加多个标签,在最外面套一个div就行了,不然会报错,特别是渲染列表多重循环的时候
2)JSX语法(表达式)
在 JSX 语法中,你可以在大括号(
{}
)内放置任何有效的 JavaScript 表达式。
const name = 'Josh Perez';
const element = <h1>Hello, {name} {1+1}</h1>;
ReactDOM.render(element,document.getElementById('root')
);
3)JSX语法(三元运算)
JSX 中不能使用 if else
语句,可以使用 conditional (三元运算
) 表达式来替代。
const element = (<div><h1>{i == 1 ? 'True!' : 'False'}</h1>{judge ?div className="yes"></div>:<div className="no"></div>}</div>
)
4)jsx使用className
JSX 语法上 使用 camelCase(小驼峰命名)来定义属性的名称, class
属性需要写成 className
<div className="App"><header className="App-header"><img src={logo} className="App-logo" alt="logo" /></header>
</div>
5)jsx使用style
不包起来会报错
let ys = {color: yellow};<div><div style={{color: red}}> </div> // 内联 <div style={ys}> </div> // 变量的形式<div style={judge ? {color: red} : ys}> </div> // 三元表达式
</div>
6)jsx中使用数组渲染
let arr=[<h1>你是风儿</h1>,<h2>我是沙</h2>,<h3>缠缠绵绵到天涯</h3>
];
let arr= [你是风儿, 我是沙, 缠缠绵绵到天涯];
ReactDOM.render(return (<div>{arr} // 1、数组语法{arr.map((item, index)=> { // 2、利用map属性return <div key={index}>{item}</div>})}list.map((item, index) => { // 3、双重嵌套数据<div key={index}>item.map(itemChild, indexChild) { // 注意添加key 不添加直接报错return <div key={index}>{item.mes}</div>}</div> }) </div>)
)
7)JSX语法(属性)
JSX 语法上更接近 JavaScript 而不是 HTML,所以 React DOM 使用 camelCase(小驼峰命名)来定义属性的名称
for 属性需要写成 htmlFor
8)JSX语法(注释)
注释需要写在花括号( {}
)中
const element = (<div><h1>Hello!</h1>{/* 注释 */}<h2>Good to see you here.</h2></div>
);
2、元素渲染
元素是构成 React 应用的最小单位,它用于描述屏幕上输出的内容。
与浏览器的 DOM 元素不同,React 当中的元素事实上是普通的对象,React DOM 可以确保 浏览器 DOM 的数据内容与 React 元素保持一致。
1)将一个元素渲染为 DOM
首先我们在一个 HTML 页面中添加一个根 DOM 节点
<div id="root"></div>
仅使用 React 构建的应用通常只有单一的根 DOM 节点。如果你在将 React 集成进一个已有应用,那么你可以在应用中包含任意多的独立根 DOM 节点。
将一个 React 元素渲染到根 DOM 节点中,只需把它们一起传入 ReactDOM.render()
:
const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));
2)更新元素渲染
React 元素是不可变对象。当元素被创建之后,你是无法改变其内容或属性的。
目前更新界面的唯一办法是创建一个新的元素,然后将它传入 ReactDOM.render() 方法
注意:
在实践中,大多数 React 应用只会调用一次 ReactDOM.render()。在下一个章节,我们将学习如何将这些代码封装到有状态组件中。
3)React 只会更新必要的部分
值得注意的是 React DOM 首先会比较元素内容先后的不同,而在渲染过程中只会更新改变了的部分。
3、组件
组件,从概念上类似于 JavaScript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素。
React 允许将代码封装成组件(component),然后像插入普通 HTML 标签一样,在网页中插入这个组件。
1)React创建组件的三种方式及其区别
- 函数式定义的
无状态组件
- es5原生方式
React.createClass
定义的组件 - es6形式的
extends React.Component
定义的组件
函数组件:定义组件最简单的方式
这种组件只负责根据传入的props
来展示,不涉及到要state
状态的操作
无状态函数式组件形式上表现为一个只带有一个render
方法的组件类
function Welcome(props) {return <h1>Hello, {props.name}</h1>;
}ReactDOM.render(<Welcome name="Sebastian" />, mountNode
)
特点:
无状态组件不会被实例化,整体渲染性能得到提升
因为组件被精简成一个render方法的函数来实现的,由于是无状态组件,所以无状态组件就不会在有组件实例化的过程,无实例化过程也就不需要分配多余的内存,从而性能得到一定的提升。组件不能访问 this 对象
无状态组件由于没有实例化过程,所以无法访问组件this中的对象,例如:this.ref、this.state等均不能访问。若想访问就不能使用这种形式来创建组件组件无法访问生命周期的方法
无状态组件是不需要组件生命周期管理和状态管理,所以底层实现这种形式的组件时是不会实现组件的生命周期方法。所以无状态组件是不能参与组件的各个生命周期管理的。无状态组件只能访问输入的props,同样的props会得到同样的渲染结果,不会有副作用
- 无状态组件内部其实是可以使用ref功能的
不能通过this.refs访问到,但是可以通过将ref内容保存到无状态组件内部的一个本地变量中获取到。
function TestComp(props){let ref;return (<div><div ref={(node) => ref = node}>...</div></div>)
}
React.createClass:ES5定义组件的方式
var InputControlES5 = React.createClass({propTypes: { // 定义传入props中的属性各种类型initialValue: React.PropTypes.string},defaultProps: { // 组件默认的props对象initialValue: ''},// 设置 initial stategetInitialState: function() { // 组件相关的状态对象return {text: this.props.initialValue || 'placeholder'};},handleChange(event) {this.setState({ //this represents react component instancetext: event.target.value});},render() {return (<div>Type something:<input onChange={this.handleChange} value={this.state.text} /></div>);}
});
InputControlES6.propTypes = {initialValue: React.PropTypes.string
};
InputControlES6.defaultProps = {initialValue: ''
};
特点:
- 有状态的组件,会被实例化,可以访问组件的生命周期方法
- React.createClass会自绑定函数方法,导致不必要的性能开销,增加代码过时的可能性。
- 函数this自绑定
React.createClass创建的组件,其每一个成员函数的this都有React自动绑定,任何时候使用,直接使用this.method即可,函数中的this会被正确设置。 - React.createClass的mixins不够自然、直观;
React.Component:使用 ES6 的 class 来定义组件
class InputControlES6 extends React.Component {constructor(props) {super(props);// 设置 initial statethis.state = {text: props.initialValue || 'placeholder'};}handleClick() {console.log(this); // null}handleChange = (event) => {this.setState({text: event.target.value});}render() {return (<div>Type something:<input onChange={this.handleChange}value={this.state.text} /></div>);}
}
InputControlES6.propTypes = {initialValue: React.PropTypes.string
};
InputControlES6.defaultProps = {initialValue: ''
};
特点:
- 有状态的组件,会被实例化,可以访问组件的生命周期方法
- 函数不会自动绑定this,需要开发者手动绑定,否则this不能获取当前组件实例对象。
函数,手动绑定this有三种手动绑定方法:
- 在构造函数中完成绑定,
- 在调用时使用method.bind(this)来完成绑定,
- 使用arrow function来绑定
- 使用 class fields 语法(推荐)
class InputControlES6 extends React.Component {constructor(props) {super(props);this.state = {text: ''};this.handleChange= this.handleChange.bind(this);}handleChange(e) {console.log(this);this.setState({text: event.target.value});}render() {return (<div>Type something:<input onChange={this.handleChange}value={this.state.text} /></div>);}
}
<div onClick={this.handleChange.bind(this)}></div>
<div onClick={()=>this.handleClick()}></div> // 使用arrow function来绑定
handleChange = (e) => {...
}
React.createClass与React.Component区别
函数this自绑定
React.createClass创建的组件,其每一个成员函数的this都有React自动绑定;React.Component创建的组件,其成员函数不会自动绑定this,需要开发者手动绑定组件属性类型propTypes及其默认props属性defaultProps配置不同
React.createClass在创建组件时,有关组件props的属性类型及组件默认的属性会作为组件实例的属性来配置,其中defaultProps是使用getDefaultProps的方法来获取默认组件属性的
const TodoItem = React.createClass({propTypes: { // as an objectname: React.PropTypes.string},getDefaultProps(){ // return a objectreturn {name: '' }}render(){return <div></div>}
})
React.Component在创建组件时配置这两个对应信息时,他们是作为组件类的属性,不是组件实例的属性,也就是所谓的类的静态属性来配置的。
class TodoItem extends React.Component {static propTypes = {//类的静态属性name: React.PropTypes.string};static defaultProps = {//类的静态属性name: ''};...
}
组件初始状态state的配置不同
React.createClass创建的组件,其状态state是通过getInitialState方法来配置组件相关的状态
React.Component创建的组件,其状态state是在constructor中像初始化组件属性一样声明的。
const TodoItem = React.createClass({// return an objectgetInitialState(){ return {isEditing: false}}render(){ return <div></div> }
})
class TodoItem extends React.Component{constructor(props){super(props);this.state = { // define this.state in constructorisEditing: false} }render(){ return <div></div> }
}
Mixins的支持不同
Mixins(混入)是面向对象编程OOP的一种实现,其作用是为了复用共有的代码,将共有的代码通过抽取为一个对象,然后通过Mixins进该对象来达到代码复用。
React.createClass在创建组件时可以使用mixins属性,以数组的形式来混合类的集合。
React.Component这种形式并不支持Mixins,但是React提供一个全新的方式来取代Mixins,那就是Higher-Order Components
总结:
- 只要有可能,尽量使用无状态组件。
- 否则(如需要state、生命周期方法等),使用
React.Component
这种es6形式创建组件
2)组件:提取,组合/复合,以及渲染
- 组件名称必须以大写字母开头。小写字母开头的组件会被视为原生 DOM 标签
- 组件类只能包含一个顶层标签,否则会报错
function Avatar(props) {return (<img className="Avatar"src={props.user.avatarUrl}alt={props.user.name}/>);
}
function Welcome(props) {return <h1>Hello, {props.name}</h1>;
}
function Name(props) {return <h1>网站名称:{props.name}</h1>;
}ReactDOM.render((<div><Avatar user={props.user} /><Welcome name="See" /><Welcome name="hello" /><Welcome name="World" /><Name name="Cahal" /></div>),document.getElementById('root')
);
4、Props
【Props 的只读性】组件无论是使用函数声明还是通过 class 声明,都决不能修改自身的 props。
React的核心思想就是组件化思想,页面会被切分成一些独立的、可复用的组件
组件从概念上看就是一个函数,可以接受一个参数作为输入值,这个参数就是props,所以可以把props理解为从外部传入组件内部的数据
react具有单向数据流的特性,所以他的主要作用是从父组件向子组件中传递数据
props除了可以传字符串,数字,还可以传递对象,数组甚至是回调函数
class Welcome extends React.Component {render() {return <h1>Hello {this.props.name}</h1>;}
}const element = <Welcome name="Sara" onNameChanged={this.handleName} />;
5、State
一个组件的显示形态可以由数据状态和外部参数所决定,而数据状态就是state,一般在 constructor 中初始化
可以通过 this.state
属性读取。当用户点击组件,可以通过 this.setState
方法就修改状态值,每次修改以后,自动调用 this.render
方法,再次渲染组件。
class Button extends React.Component {constructor() {super();// 1) 向组件中添加局部的 statethis.state = {count: 0,};}updateCount() {// 3) 通过调用 setState 来改变state局部数据this.setState((prevState, props) => {return { count: prevState.count + 1 }});}render() {return (// 2) 获取局部数据<button onClick={() => this.updateCount()}>Clicked {this.state.count} times</button>);}
}
setState
还可以接受第二个参数,它是一个函数,会在setState调用完成并且组件开始重新渲染时被调用,可以用来监听渲染是否完成
this.setState({name:'JS每日一题'
},()=>console.log('setState finished'))
6、组件生命周期
主要讲述react16.4之后的生命周期,可以分成三个阶段:
- 创建阶段
- 更新阶段
- 卸载阶段
1)创建阶段
主要分成了以下几个生命周期方法:
- constructor
- getDerivedStateFromProps
- render
- componentDidMount
constructor
【执行时机】 实例过程中自动调用的方法
【应用】在该方法中,在方法内部通过super关键字获取来自父组件的props,通常的操作为初始化state状态或者在this上挂载方法
getDerivedStateFromProps
该方法是新增的生命周期方法,是一个静态的方法,因此不能访问到组件的实例
【执行时机】组件创建和更新阶段,不论是props变化还是state变化,也会调用
在每次render方法前调用,第一个参数为即将更新的props,第二个参数为上一个状态的state,可以比较props 和 state来加一些限制条件,防止无用的state更新
该方法需要返回一个新的对象作为新的state或者返回null表示state状态不需要更新
render
【执行时机】类组件必须实现的方法,
【应用】用于渲染DOM结构,可以访问组件state与prop属性
注意: 不要在 render 里面 setState, 否则会触发死循环导致内存崩溃
componentDidMount
【执行时机】组件挂载到真实DOM节点后执行,其在render方法之后执行
【应用】此方法多用于执行一些数据获取,事件监听等操作
2)更新阶段
主要分成了以下几个生命周期方法:
- getDerivedStateFromProps (同上)
- shouldComponentUpdate
- render(同上)
- getSnapshotBeforeUpdate
- componentDidUpdate
shouldComponentUpdate
【 执行时机】到新的props或者state时都会调用,通过返回true或者false告知组件更新与否
【应用】用于告知组件本身基于当前的props和state是否需要重新渲染组件,默认情况返回true
一般情况,不建议在该周期方法中进行深层比较,会影响效率
同时也不能调用setState,否则会导致无限循环调用更新
getSnapshotBeforeUpdate
【 执行时机】该周期函数在render后执行,执行之时DOM元素还没有被更新
该方法返回的一个Snapshot值,作为componentDidUpdate第三个参数传入
【应用】此方法的目的在于获取组件更新前的一些信息,比如组件的滚动位置之类的,在组件更新后可以根据这些信息恢复一些UI视觉上的状态
getSnapshotBeforeUpdate(prevProps, prevState) {console.log('#enter getSnapshotBeforeUpdate');return 'foo';
}componentDidUpdate(prevProps, prevState, snapshot) {console.log('#enter componentDidUpdate snapshot = ', snapshot);
}
componentDidUpdate
【 执行时机】组件更新结束后触发
【应用】在该方法中,可以根据前后的props和state的变化做相应的操作,如获取数据,修改DOM样式等
3)卸载阶段
- componentWillUnmount
componentWillUnmount
用于组件卸载前,清理一些注册是监听事件,或者取消订阅的网络请求等
一旦一个组件实例被卸载,其不会被再次挂载,而只可能是被重新创建
7、事件处理
React 元素的事件处理和 DOM 元素的很相似,但是有一点语法上的不同:
- 事件名称命名方式不同:React 事件的命名采用小驼峰式(onClick,),而不是纯小写。
- 事件处理函数书写不同:使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。
- 阻止默认行为方式:在 React 中你不能通过返回 false 的方式阻止默认行为。你必须显式的使用 preventDefault。
原生事件绑定方式:
<button onclick="handleClick()">按钮命名</button><script>
function handleClick(e) {...
}
</script>
React 合成事件绑定方式:
函数组件
function Form() {function handleClick(e) {console.log(e.nativeEvent); // 通过e.nativeEvent属性获取原生DOM事件e.preventDefault(); // 阻止默认事件console.log('You clicked submit.');}return <button onClick={handleClick}>按钮</button>;
}
class组件写法1(传统事件写法,不推荐)
class Form extends React.Component {constructor(props) {super(props);// 为了在回调中使用 `this`,这个绑定是必不可少的this.handleClick = this.handleClick.bind(this);}handleClick(e) {...}render() {return <button onClick={handleClick}>按钮</button>}
}
class组件写法2(class fields 语法,推荐)
class Form extends React.Component {handleClick = (e) => {...}render() {return <button onClick={handleClick}>按钮</button>}
}
更多:传递参数
以下两种方式都可以向事件处理函数传递参数:通过箭头函数和 Function.prototype.bind 来实现。
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
在这两种情况下,React 的事件对象 e 会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind 的方式,事件对象以及更多的参数将会被隐式的进行传递。
React 组件支持很多事件,除了 Click 事件以外,还有 KeyDown 、Copy、Scroll 等,完整的事件清单请查看官方文档。
8、条件渲染
React 中的条件渲染和 JavaScript 中的一样,使用 JavaScript 运算符 if 或者条件运算符去创建元素来表现当前的状态,然后让 React 根据它们来更新 UI。
1)元素变量
function LoginButton(props) {return (<button onClick={props.onClick}>Login</button>);
}function LogoutButton(props) {return (<button onClick={props.onClick}>Logout</button>);
}function Greeting(props) {const isLoggedIn = props.isLoggedIn;if (isLoggedIn) {return <UserGreeting />;}return <GuestGreeting />;
}class LoginControl extends React.Component {constructor(props) {super(props);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')
);
2)与运算符 &&
通过花括号包裹代码,在 JSX 中嵌入表达式
之所以能这样做,是因为在 JavaScript 中,true && expression 总是会返回 expression, 而 false && expression 总是会返回 false。
class LoginControl extends React.Component {render() {const unreadMessages = ['React', 'Re: React', 'Re:Re: React'];// const unreadMessages = props.unreadMessages;return (<div>{unreadMessages.length > 0 &&<h2>You have {unreadMessages.length} unread messages.</h2>}</div>);}
}
注意,返回 false 的表达式会使 && 后面的元素被跳过,但会返回 false 表达式。
render() {const count = 0;return (<div>{ count && <h1>Messages: {count}</h1>} // <div>0</div></div>);
}
3)三目运算符
condition ? true : false。
render() {const isLoggedIn = this.state.isLoggedIn;return (<div><div>The user is <b>{isLoggedIn ? 'currently' : 'not'}</b> logged in.</div>{isLoggedIn? <LogoutButton onClick={this.handleLogoutClick} />: <LoginButton onClick={this.handleLoginClick} />}</div>);
}
4)阻止组件渲染
在极少数情况下,你可能希望能隐藏组件,即使它已经被其他组件渲染。
若要完成此操作,你可以让 render 方法直接返回 null,而不进行任何渲染。
function WarningBanner(props) {if (!props.warn) {return null;}return (<div className="warning">Warning!</div>);
}class Page extends React.Component {render() {return (<div><WarningBanner warn={this.props.showWarning} /></div>);}
}
在组件的 render 方法中返回 null 并不会影响组件的生命周期。例如,上面这个示例中,componentDidUpdate 依然会被调用。
9、列表 & Key
1)基础列表组件
使用 Javascript 中的 map()
方法来遍历数组,每一个子元素都应该需要一个唯一的key
值,不设置的话会收到警告
const data = [{ id: 0, name: 'abc' },{ id: 1, name: 'def' },{ id: 2, name: 'ghi' },{ id: 3, name: 'jkl' }
];const ListItem = (props) => {return <li>{props.name}</li>;
};const List = () => {return (<ul>{data.map((item) => (<ListItem name={item.name} key={item.id}></ListItem>))}</ul>);
};
2)key
key 元素取值:最好是这个元素在列表中拥有的一个独一无二的字符串。
通常,我们使用数据中的 id 来作为元素的 key
当元素没有确定 id 的时候,万不得已你可以使用元素索引 index 作为 key
作用:React 存在 Diff算法,而元素元素key属性的作用是用于判断元素是新创建的还是被移动的元素,从而减少不必要的Diff
如果列表数据渲染中,在数据后面插入一条数据,key作用并不大
下面来看看在前面插入数据时,使用key与不使用key的区别:
this.state = {movies:[111,222,333]
}insertMovie() {const newMovies = [000 ,...this.state.numbers];this.setState({movies: newMovies})
}<ul>{this.state.movies.map((item, index) => {return <li>{item}</li>})}
</ul>
有key的时候,react根据key属性匹配原有树上的子元素以及最新树上的子元素,像上述情况只需要将000元素插入到最前面位置
没有key的时候,所有的li标签都需要进行修改
但是并不是拥有key值代表性能越高,如果说只是文本内容改变了,不写key反而性能和效率更高。因为不写key是将所有的文本内容替换一下,节点不会发生变化,而写key则涉及到了节点的增和删
- 元素的 key 只有放在就近的数组上下文中才有意义。
- key 值在兄弟节点之间必须唯一
- 把 key 传递给组件,不能用key关键字,要用其他name
总结
良好使用key属性是性能优化的非常关键的一步
- key 应该是唯一的
- key不要使用随机值(随机数在下一次 render 时,会重新生成一个数字)
- 避免使用 index 作为 key
react判断key的流程具体如下图:
3)在 JSX 大括号中嵌入 map()
function NumberList(props) {const numbers = props.numbers;return (<ul>{numbers.map((number) =><ListItem key={number.toString()} value={number} />)}</ul>);
}
10、表单
在 React 里,HTML 表单元素的工作方式和其他的 DOM 元素有些不同。
在 HTML 中,表单元素(如、 和 )通常自己维护 state,并根据用户输入进行更新。而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。
1)受控组件
我们可以把两者结合起来,使 React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。
class NameForm extends React.Component {constructor(props) {super(props);this.state = {name: '',desc: '',fruit: '',hover: []};}handleChangeName = (event) => {this.setState({name: event.target.value});}handleChangeDesc = (event) => {this.setState({desc: event.target.value});}handleChange = (event) => {this.setState({fruit: event.target.value});}handleChangeHover = (event) => {const target = event.target.value;const index = this.state.fruit.indexOf(target);if (index == -1) {this.state.fruit.push(target);} else {this.state.fruit.splice(index, 1);}}handleSubmit = (event) => {alert('提交');event.preventDefault();}render() {return (<form onSubmit={this.handleSubmit}>// input输入框<label htmlFor="name">名字:<input type="text" value={this.state.name} onChange={this.handleChangeName} /></label><label htmlFor="guests">Number of guests:<inputname="numberOfGuests"type="number"value={this.state.numberOfGuests}onChange={this.handleInputChange} /></label>// textarea <textarea value={this.state.desc} onChange={this.handleChangeDesc} />// select 单选<select value={this.state.fruit} onChange={this.handleChange}><option selected value="grapefruit">葡萄柚</option><option value="lime">酸橙</option><option value="coconut">椰子</option><option value="mango">芒果</option></select><select multiple={true} value={this.state.hover} onChange={this.handleChangeHover}><option value="dance">跳舞</option><option value="painting">绘画</option><option value="swim">游泳</option><option value="basketball">篮球</option></select>// radio<div onChange={this.onChangeValue}>sex:<input type="radio" value="Male" name="gender" /> Male<input type="radio" value="Female" name="gender" /> Female<input type="radio" value="Other" name="gender" /> Other</div><input type="submit" value="提交" /></form>);}
}
- 在受控组件上指定 value 的 prop 会阻止用户更改输入。如果你指定了 value,但输入仍可编辑,则可能是你意外地将value 设置为 undefined 或 null。
11、状态提升
【React官网】使用 react 经常会遇到几个组件需要共用状态数据的情况。这种情况下,我们最好将这部分共享的状态提升至他们最近的父组件当中进行管理。
React状态提升主要就是用来处理父组件和子组件的数据传递的。他可以让我们的数据流动的形式是自顶向下单向流动的,所有组件的数据都是来自于他们的父辈组件,也都是由父辈组件来统一存储和修改,再传入子组件当中。
通常,多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中去。
class Parent extends React.Component {constructor(props) {super(props)this.state = {count: 0}}setCount = () => {this.setState({count: this.state.count + 1})}render() {return (<div><ChildrenA count={this.state.count} /><ChildrenB onClick={this.setCount} /></div>);}
}
class ChildrenA extends React.Component {render() {return (<div>{this.props.count}</div>);}
}
class ChildrenB extends React.Component {handleSetNum = (e) => {this.props.setNum(e.target.value)}render() {return (<div>{this.props.count}<input onChange={(this.handleSetNum} value={this.props.count} /></div>);}
}
总结:
React的状态提升,其实是为了组件之间的数据更加单向性,在数据的传输上始终只会出现一对一的情况,在处理上,也方便我们只需要在向子组件传递数据的那个父辈组件上进行操作,并传回子组件,使得数据更新,这种方法也体现了React的单向数据流的设计思想,在复用组件的时候,组件的数据也不会相互干扰,使代码逻辑上会更加便于管理。
12、组合 vs 继承
React 有十分强大的组合模式。我们推荐使用组合而非继承来实现组件间的代码重用。
组合(包含关系)
1)this.props.children
this.props 对象的属性与组件的属性一一对应,但是有一个例外,就是 this.props.children 属性。它表示组件的所有子节点
有些组件无法提前知晓它们子组件的具体内容。这种情况我们使用一个特殊的 children prop 来将他们的子组件传递到渲染结果中
这使得别的组件可以通过 JSX 嵌套,将任意组件作为子组件传递给它们。
class NotesList extends React.Component {constructor(props) {super(props);}render() {return this.props.children}
}
class NotesList extends React.Component {constructor(props) {super(props);}render() {return (<FancyBorder color="blue"><h1 className="Dialog-title">Welcome</h1><p className="Dialog-message">Thank you for visiting our spacecraft!</p></FancyBorder>);}
}
this.props.children 的值有三种可能:
- 如果当前组件没有子节点,它就是 undefined
- 如果有一个子节点,数据类型是 object
- 如果有多个子节点,数据类型就是 array
2)自行约定
少数情况下,你可能需要在一个组件中预留出几个“洞”。这种情况下,我们可以不使用 children,而是自行约定:将所需内容传入 props,并使用相应的 prop。
class SplitPane extends React.Component {render() {return (<div className="SplitPane"><div className="SplitPane-left">{props.left}</div><div className="SplitPane-right">{props.right}</div></div>); }
}
class App extends React.Component {render() {return (<SplitPaneleft={<Contacts />}right={<Chat />} />); }
}
3)特例关系
有些时候,我们会把一些组件看作是其他组件的特殊实例,比如 WelcomeDialog 可以说是 Dialog 的特殊实例。
function Dialog(props) {return (<FancyBorder color="blue"><h1 className="Dialog-title">{props.title}</h1><p className="Dialog-message">{props.message}</p>{props.children}</FancyBorder>);
}class SignUpDialog extends React.Component {constructor(props) {super(props);this.handleChange = this.handleChange.bind(this);this.handleSignUp = this.handleSignUp.bind(this);this.state = {login: ''};}handleChange(e) {this.setState({login: e.target.value});}handleSignUp() {alert(`Welcome aboard, ${this.state.login}!`);}render() {return (<Dialog title="Mars Exploration Program"message="How should we refer to you?"><input value={this.state.login} onChange={this.handleChange} /><button onClick={this.handleSignUp}> Sign Me Up! </button></Dialog>);}
}
继承
React中,Props 和组合为你提供了清晰而安全地定制组件外观和行为的灵活方式。
组件可以直接引入(import)而无需通过 extend 继承它们。
所以react中,继承是不需要的
13、无障碍辅助功能
官方描述:网络无障碍辅助功能(Accessibility,也被称为 a11y,因为以 A 开头,以 Y 结尾,中间一共 11 个字母)是一种可以帮助所有人获得服务的设计和创造。无障碍辅助功能是使得辅助技术正确解读网页的必要条件。
我的理解:其实就是写react应用的一些语义规范,代码优化内容
1) HTML 属性使用带连字符(-)的命名法
JSX 支持所有 aria-* HTML 属性。虽然大多数 React 的 DOM 变量和属性命名都使用驼峰命名(camelCased)(如 hyphen-cased,kebab-case,lisp-case)。
<inputtype="text"aria-label={labelText}aria-required="true"onChange={onchangeHandler}value={inputValue}name="name"
/>
2)<Fragments>
标签
React 中的一个常见模式是一个组件返回多个元素。Fragments
允许你将子列表分组,而无需向 DOM 添加额外节点。
import React, { Fragment } from 'react';function ListItem({ item }) {return (<Fragment><dt>{item.term}</dt><dd>{item.description}</dd></Fragment>);
}function Glossary(props) {return (<dl>{props.items.map(item => (<ListItem item={item} key={item.id} />))}</dl>);
}
<Fragments>
标签缩写写法:
<><dt>{item.term}</dt><dd>{item.description}</dd>
</>
3)表单标记
所有的 HTML 表单控制,例如 <input> 和 <textarea>
,都需要被标注
<label htmlFor="userName">userName:</label>
<input id="userName" type="text" name="name"/><label htmlFor="password">Name:</label>
<input id="password" type="password" name="password"/>
3)表单错误提醒
W3C 展示用户推送
剩下其他的可以在react官网中阅读了解
无障碍辅助功能
14、代码分割
代码分割的优点:
- 可以实现在初始加载的时候减少所需加载的代码量
- 代码分割,引用时可实现异步加载、并行加载,能使加载速度更快
- 还可以实现按需加载,懒加载,路由懒加载,解决白屏等,最终提升性能
React 中的代码分割
- import()
- 使用 React Loadable
- 基于路由的代码分割
1)import() (最好的代码分割方式)
参数:模块的路径
返回值:返回一个 promise 对象
适用场合:
- 按需加载:在需要时在加载模块
- 条件加载:在if语句中做条件加载
- 动态模块路径:允许模块路径动态生成
目前,流行的脚手架或打包器都已经支持 import() 语法
// math.js
export function add(a, b) {return a + b;
}
// app.js
import { math} from './math.js';console.log(math(16, 26)); // 42
2)使用 React Loadable
React Loadable 是一个第三方库,提供友好的 API 进行使用(提供一个占位 View 的参数,在加载组件时呈现)
import Loadable from 'react-loadable';const LoadableOtherComponent = Loadable({loader: () => import('./OtherComponent'),loading: () => <div>Loading...</div>,
});const MyComponent = () => (<LoadableOtherComponent/>
react-loadable
3)基于路由的代码分割
// route.jsimport Home from '../containers/Home'
import My from '../containers/My'const routes = [{path: '/home',component: Home,},{path: '/my',component: My,},{path: '*',component: Home,},
]
export default routes;
// route.config.js
import React from 'react'
import { Redirect } from 'react-router-dom'
import { Switch, Route } from 'react-router'export const renderRoutes = (routes, extraProps = {}, switchProps = {}) =>routes ? (<Switch {...switchProps}>{routes.map((route, i) => {const key = route.key || iif (route.redirect) {const { redirect } = routeif (isString(redirect)) return <Redirect key={key} to={redirect} />if (isObject(redirect)) {return <Redirect key={key} {...redirect} />}}return (<Routekey={key}exact={route.exact}path={route.path}render={props => {if (route.render) {return route.render(props)}return (<route.component {...props} {...extraProps} route={route} />)}}strict={route.strict}/>)})}</Switch>) : null
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import Loadable from 'react-loadable';const Loading = () => <div>Loading...</div>;const Home = Loadable({loader: () => import('./routes/Home'),loading: Loading,
});const About = Loadable({loader: () => import('./routes/About'),loading: Loading,
});const App = () => (<Router><Switch><Route exact path="/" component={Home}/><Route path="/about" component={About}/></Switch></Router>
);
根据路由进行切割,即路由懒加载
import React, { Suspense, lazy } from 'react';const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));function MyComponent() {return (<div><Suspense fallback={<div>Loading...</div>}><section><OtherComponent /><AnotherComponent /></section></Suspense></div>);
}
13、Context
Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
常用作父组件向后代组件传递数据。请谨慎使用,因为这会使得组件的复用性变差。
context:
- 创建:
React.createContext
- 存在
Provider
组件用于创建数据源,通过value
属性用于给后代组件传递数据 - 存在
Consumer
组件用于接收数据,或者使用contextType
属性接收
eg:
通过 props 属性自上而下进行传递
class App extends React.Component {render() {return <Toolbar theme="dark" />;}
}function Toolbar(props) {// Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。// 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,// 因为必须将这个值层层传递所有组件。return (<div><ThemedButton theme={props.theme} /></div>);
}class ThemedButton extends React.Component {render() {return <Button theme={this.props.theme} />;}
}
使用 context
const ThemeContext = React.createContext('light');
class App extends React.Component {render() {// 使用一个 Provider 来将当前的 theme 传递给以下的组件树。// 无论多深,任何组件都能读取这个值。// 在这个例子中,我们将 “dark” 作为当前的值传递下去。return (<ThemeContext.Provider value="dark"><Toolbar /></ThemeContext.Provider>);}
}// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {return (<div><ThemedButton /></div>);
}class ThemedButton extends React.Component {// 指定 contextType 读取当前的 theme context。// React 会往上找到最近的 theme Provider,然后使用它的值。// 在这个例子中,当前的 theme 值为 “dark”。static contextType = ThemeContext;render() {return <Button theme={this.context} />;}
}
PropTypes & defaultProps
PropTypes 设置组件属性接收数据类型
defaultProps 设置组件属性的默认值
组件的props属性默认可以接受任意值,字符串、对象、函数等
组件类的PropTypes属性,可以用来类型检查验证组件实例的属性是否符合要求
eg:只要传的initialValue 不是数值就会报错
class InputControlES6 extends React.Component {constructor(props) {super(props);this.state = {text: props.initialValue};}render() {return (<input value={this.state.text} />);}
}
InputControlES6.propTypes = {initialValue: React.PropTypes.string
};
InputControlES6.defaultProps = {initialValue: 'hello'
};
<InputControlES6 initialValue={123}/>
获取真实的DOM节点(ref/refs)
组件并不是真实的 DOM 节点,而是存在于内存之中的一种数据结构,叫做虚拟 DOM (virtual DOM)。只有当它插入文档以后,才会变成真实的 DOM 。根据 React 的设计,所有的 DOM 变动,都先在虚拟 DOM 上发生,然后再将实际发生变动的部分,反映在真实 DOM上,这种算法叫做 DOM diff ,它可以极大提高网页的性能表现。
从组件获取真实 DOM 的节点,这时就要用到 ref
属性
class MyComponent extends React.Component({handleClick: function() {this.refs.myTextInput.focus();},render: function() {return (<div><input type="text" ref="myTextInput" /><input type="button" value="Focus the text input" onClick={this.handleClick} /></div>);}
});
Fragment
React 中的一个常见模式是一个组件返回多个元素。React.Fragments
允许你将子列表分组,而无需向 DOM 添加额外节点。
import React, { Fragment } from 'react';function ListItem({ item }) {return (<Fragment><dt>{item.term}</dt><dd>{item.description}</dd></Fragment>);
}function Glossary(props) {return (<dl>{props.items.map(item => (<ListItem item={item} key={item.id} />))}</dl>);
}
<Fragments>
标签 短语法(空标签):
可以像使用任何其他元素一样使用,但它不支持 key 或属性。
<><dt>{item.term}</dt><dd>{item.description}</dd>
</>
目前,key 是唯一可以传递给 Fragment
的属性
通常的一个使用场景是将一个集合映射到一个 Fragments 数组
<dl>{props.items.map(item => (// 没有`key`,React 会发出一个关键警告<React.Fragment key={item.id}><dt>{item.term}</dt><dd>{item.description}</dd></React.Fragment>))}
</dl>
14、Refs 转发
高阶组件(HOC)
1) 什么是高阶组件
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
高阶组件本身不是组件,它是一个参数为组件,返回值也是一个组件的函数
组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。
高阶作用用于强化组件,复用逻辑,提升渲染性能等作用。
2)它解决了什么问题?
复用逻辑
高阶组件更像是一个加工react组件的工厂,批量对原有组件进行加工,包装处理。我们可以根据业务需求定制化专属的HOC,这样可以解决复用逻辑。强化props
这个是HOC最常用的用法之一,高阶组件返回的组件,可以劫持上一层传过来的props
,然后混入新的props
,来增强组件的功能。代表作react-router
中的withRouter
赋能组件
HOC有一项独特的特性,就是可以给被HOC包裹的业务组件,提供一些拓展功能,比如说额外的生命周期,额外的事件,但是这种HOC,可能需要和业务组件紧密结合。典型案例react-keepalive-router
中的keepaliveLifeCycle
就是通过HOC方式,给业务组件增加了额外的生命周期。- 控制渲染
劫持渲染是hoc一个特性,在wrapComponent包装组件中,可以对原来的组件,进行条件渲染,节流渲染,懒加载
等功能。典型代表做react-redux
中connect
和dva
中dynamic
组件懒加载。
3)高阶组件衍生过程
(1)mixin模式
老版本的react-mixins。在react初期提供一种组合方法。通过React.createClass,加入mixins属性,具体用法和vue 中mixins相似
const customMixin = {componentDidMount(){console.log( '------componentDidMount------' )},say(){console.log(this.state.name)}
}const APP = React.createClass({mixins: [ customMixin ],getInitialState(){return {name:'alien'}},render(){const { name } = this.statereturn <div> hello ,world , my name is { name } </div>}
})
React.createClass连同mixins这种模式已经被废弃了。mixins 带来的一些负面的影响:
- mixin引入了隐式依赖关系
- 不同mixins之间可能会有先后顺序甚至代码冲突覆盖的问题
- mixin代码会导致滚雪球式的复杂性
(2)原型链继承
在有状态组件class,通过原型链继承来实现mixins
const customMixin = { /* 自定义 mixins */componentDidMount(){console.log( '------componentDidMount------' )},say(){console.log(this.state.name)}
}function componentClassMixins(Component,mixin){ /* 继承 */for(let key in mixin){Component.prototype[key] = mixin[key]}
}class Index extends React.Component{constructor(){super()this.state={ name:'alien' }}render(){return <div> hello,world<button onClick={ this.say.bind(this) } > to say </button></div>}
}
componentClassMixins(Index,customMixin)
(3)extends继承模式
这种模式的好处在于,可以封装基础功能组件,然后根据需要去extends我们的基础组件,按需强化组件,但是值得注意的是,必须要对基础组件有足够的掌握,否则会造成一些列意想不到的情况发生。
class Base extends React.Component{constructor(){super()this.state={name:'alien'}}say(){console.log('base components')}render(){return <div> hello,world <button onClick={ this.say.bind(this) } >点击</button> </div>}
}
class Index extends Base{componentDidMount(){console.log( this.state.name )}say(){ /* 会覆盖基类中的 say */console.log('extends components')}
}
export default Index
(4)HOC模式
简单尝试一个HOC
function HOC(Component) {return class wrapComponent extends React.Component{constructor(){super()this.state={name:'alien'}}render=()=><Component { ...this.props } { ...this.state } />}
}@HOC
class Index extends React.Component{say(){const { name } = this.propsconsole.log(name)}render(){return <div> hello,world <button onClick={ this.say.bind(this) } >点击</button> </div>}
}
(5)自定义hooks模式
hooks的诞生,一大部分原因是解决无状态组件没有state和逻辑难以复用问题。hooks可以将一段逻辑封装起来,做到开箱即用
hooks
hooks
4)HOC的编写和使用结构
(1)使用
装饰器模式
:对于class
声明的有状态组件,我们可以用装饰器模式
注意:包装顺序,越靠近Index
组件的,就是越内层的HOC,离组件Index
也就越近
@withStyles(styles)
@withRouter
@keepaliveLifeCycle
class Index extends React.Componen{/* ... */
}
函数包裹模式
:对于无状态组件(函数声明)
function Index(){/* .... */
}
export default withStyles(styles)(withRouter( keepaliveLifeCycle(Index) ))
(2)HOC模型
对于不需要传递参数的HOC
,我们编写模型我们只需要嵌套一层就可以
function withRouter(){return class wrapComponent extends React.Component{/* 编写逻辑 */}
}
对于需要参数的HOC
,我们需要一层代理
function connect (mapStateToProps){/* 接受第一个参数 */return function connectAdvance(wrapCompoent){/* 接受组件 */return class WrapComponent extends React.Component{ }}
}
5)两种不同的高阶组件
- 正向属性代理
- 反向的组件继承
正向属性代理
用组件包裹一层代理组件,在代理组件上,我们可以做一些,对源组件的代理操作
我们可以理解为父子组件关系,父组件对子组件进行一系列强化操作。
function HOC(WrapComponent){return class Advance extends React.Component{state={name:'alien'}render(){return <WrapComponent { ...this.props } { ...this.state } />}}
}
优点:
(1)正向属性代理可以和业务组件低耦合,零耦合,对于条件渲染和props属性增强
,只负责控制子组件渲染和传递额外的props
就可以,所以无须知道,业务组件做了些什么。所以正向属性代理,更适合做一些开源项目的hoc
,目前开源的HOC
基本都是通过这个模式实现的。
(2)同样适用于class
声明组件,和function
声明的组件。
(3)可以完全隔离业务组件的渲染,相比反向继承,属性代理这种模式。可以完全控制业务组件渲染与否,可以避免反向继承带来一些副作用,比如生命周期的执行。
(4)可以嵌套使用,多个hoc
是可以嵌套使用的,而且一般不会限制包装HOC
的先后顺序。
缺点:
(1) 一般无法直接获取业务组件的状态,如果想要获取,需要ref
获取组件实例。
(2)无法直接继承静态属性。如果需要继承需要手动处理,或者引入第三方库。
反向继承
反向继承,在于包装后的组件继承了业务组件本身,所以我们我无须在去实例化我们的业务组件。当前高阶组件就是继承后,加强型的业务组件。这种方式类似于组件的强化
class Index extends React.Component{render(){return <div> hello,world </div>}
}
function HOC(Component){return class wrapComponent extends Component{ /* 直接继承需要包装的组件 */}
}
export default HOC(Index)
优点:
(1)方便获取组件内部状态,比如state,props ,生命周期,绑定的事件函数
等
(2)es6
继承可以良好继承静态属性。我们无须对静态属性和方法进行额外的处理。
缺点:
(1)无状态组件无法使用。
(2)和被包装的组件强耦合,需要知道被包装的组件的内部状态,具体是做什么?
(3)如果多个反向继承hoc嵌套在一起,当前状态会覆盖上一个状态。这样带来的隐患是非常大的
6)编写高阶组件(详细讲解)
编写高阶组件
面试官
[题] 说说 Real DOM和 Virtual DOM 的区别?优缺点?
【Real DOM】,真实DOM, 意思为文档对象模型,是一个结构化文本的抽象,在页面渲染出的每一个结点都是一个真实DOM结构
【Virtual Dom】,本质上是以 JavaScript 对象形式存在的对 DOM 的描述
创建虚拟DOM目的就是为了更好将虚拟的节点渲染到页面视图中,虚拟DOM对象的节点与真实DOM的属性一一照应
在React中,JSX是其一大特性,可以让你在JS中通过使用XML的方式去直接声明界面的DOM结构
const vDom = <h1>Hello World</h1>
JSX实际是一种语法糖,在使用过程中会被babel进行编译转化成JS代码,上述VDOM转化为如下:
const vDom = React.createElement('h1', { className: 'hClass', id: 'hId' },'hello world'
)
两者的区别如下:
- 虚拟DOM不会进行排版与重绘操作,而真实DOM会频繁重排与重绘
- 虚拟DOM的总损耗是“虚拟DOM增删改+真实DOM差异增删改+排版与重绘”,真实DOM的总损耗是“真实DOM完全增删改+排版与重绘”
优缺点如下:
真实DOM
优势:易用
缺点:
1)效率低,解析速度慢,内存占用量过高
2)性能差:频繁操作真实DOM,易于导致重绘与回流
虚拟DOM
优势:
1)简单方便:如果使用手动操作真实DOM来完成页面,繁琐又容易出错,在大规模应用下维护起来也很困难
2)性能方面:使用Virtual DOM,能够有效避免真实DOM数频繁更新,减少多次引起重绘与回流,提高性能
3)跨平台:React借助虚拟DOM, 带来了跨平台的能力,一套代码多端运行
缺点:
1)在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化
2)首次渲染大量DOM时,由于多了一层虚拟DOM的计算,速度比正常稍慢
[题] 说说 state 和 props有什么区别?
相同点:
- 两者都是 JavaScript 对象
- 两者都是用于保存信息
- props 和 state 都能触发渲染更新
区别:
- props 是外部传递给组件的,而 state 是在组件内被组件自己管理的,一般在 constructor 中初始化
- props 在组件内部是不可修改的,但 state 在组件内部可以进行修改
- state 是多变的、可以修改
[题] 说说 super()和super(props)有什么区别?
1)ES6类
在ES6中,通过extends关键字实现类的继承,通过 super 关键字实现调用父类,super代替的是父类的构建函数,
在子类中不使用 super 关键字,则会引发报错。原因是 子类是没有自己的this对象的,它只能继承父类的this对象,然后对其进行加工
super(name)相当于调用sup.prototype.constructor.call(this,name)
super()就是将父类中的this对象继承给子类的,没有super() 子类就得不到this对象
所以在子类constructor中,必须先代用super才能引用this
class sup {constructor(name) {this.name = name}printName() {console.log(this.name)}
}class sub extends sup{constructor(name,age) {super(name) // super代表的事父类的构造函数this.age = age}printAge() {console.log(this.age)}
}let jack = new sub('jack',20)
jack.printName() //输出 : jack
jack.printAge() //输出 : 20
2)类组件
React中,类组件是基于es6的规范实现的,继承 React.Component
如果用到 constructor 就必须写 super 才初始化 this。在render中this.props都是可以使用的,这是React自动附带的,是可以不写 constructor ,super 的
调用 super() 的时候,我们一般都需要传入props作为参数,如果不传进去,React内部也会将其定义在组件实例中,但调用 this.props 为undefined
class HelloMessage extends React.Component {constructor(props) {super(props); // 必须传入 props}render (){return (<div>nice to meet you! {this.props.name}</div>);}
}
等同于
class HelloMessage extends React.Component{render (){return (<div>nice to meet you! {this.props.name}</div>);}
}
3)总结
- React中,类组件基于ES6,在constructor中必须使用super
- 调用super,不建议使用super()代替super(props),this.props在super()和构造函数结束之间仍是undefined
[题] 说说 React中的 setState 执行机制?
【setState】用来修改一个组件的state数据状态里面的值的状态,从而达到更新组件内部数据的作用
执行this.setState方法更新state状态,然后重新执行render函数,从而导致页面的视图更新
直接修改state的状态,state的状态是已经发生了改变,但是页面并不会有任何反应
这是因为React并不像vue2中调用Object.defineProperty数据响应式或者Vue3调用Proxy监听数据的变化
必须通过setState方法来告知react组件state已经发生了改变
关于state方法的定义是从React.Component中继承,定义的源码如下:
Component.prototype.setState = function(partialState, callback) {invariant(typeof partialState === 'object' ||typeof partialState === 'function' ||partialState == null,'setState(...): takes an object of state variables to update or a ' +'function which returns an object of state variables.',);this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
1)setState的更新类型
setState的更新类型分成:
- 异步更新
- 同步更新
异步更新:在组件生命周期或React合成事件中,setState是异步
changeText() {this.setState({message: "你好啊"})console.log(this.state.message); // Hello World
}// 想要立刻获取更新后的值,在第二个参数的回调中更新后会执行
changeText() {this.setState({message: "你好啊"}, () => {console.log(this.state.message); // 你好啊});
}
同步更新:在setTimeout或者原生dom事件中,setState是同步
changeText() {setTimeout(() => {this.setState({message: "你好啊});console.log(this.state.message); // 你好啊}, 0);
}componentDidMount() {const btnEl = document.getElementById("btn");btnEl.addEventListener('click', () => {this.setState({message: "你好啊,李银河"});console.log(this.state.message); // 你好啊,李银河})
}
2)setState 的批量更新策略
a:对同一个值进行多次 setState ,会对其进行覆盖,取最后一次的执行结果
handleClick = () => {this.setState({count: this.state.count + 1,})console.log(this.state.count) // 1this.setState({count: this.state.count + 1,})console.log(this.state.count) // 1this.setState({count: this.state.count + 1,})console.log(this.state.count) // 1
}
// 点击按钮触发事件,打印的都是 1,页面显示 count 的值为 2
b:如果是下一个state依赖前一个state的话,推荐给setState一个参数传入一个function,如下:
onClick = () => { this.setState((prevState, props) => { return {count: prevState.count + 1}; }); this.setState((prevState, props) => { return {count: prevState.count + 1}; });
}
c:在setTimeout或者原生dom事件中,由于是同步的操作,所以并不会进行覆盖现象
[题] 说说 React的事件机制?
React基于浏览器的事件机制自身实现了一套事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等。在React中这套事件机制被称之为合成事件
1)合成事件(SyntheticEvent)
合成事件是 React 模拟原生 DOM 事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器
兼容所有浏览器,拥有与浏览器原生事件相同的接口
React 元素的事件处理和 DOM 元素的很相似,但是有一点语法上的不同:
- 事件名称命名方式不同:React 事件的命名采用小驼峰式(onClick,),而不是纯小写。
- 事件处理函数书写不同:使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。
- 阻止默认行为方式:在 React 中你不能通过返回 false 的方式阻止默认行为。你必须显式的使用 preventDefault。
原生事件绑定方式:
<button onclick="handleClick()">按钮命名</button><script>
function handleClick(e) {...
}
</script>
React 合成事件绑定方式:
函数组件
function Form() {function handleClick(e) {console.log(e.nativeEvent); // 通过e.nativeEvent属性获取原生DOM事件e.preventDefault(); // 阻止默认事件console.log('You clicked submit.');}return <button onClick={handleClick}>按钮</button>;
}
class组件写法1
class Form extends React.Component {constructor(props) {super(props);// 为了在回调中使用 `this`,这个绑定是必不可少的this.handleClick = this.handleClick.bind(this);}handleClick(e) {...}render() {return <button onClick={handleClick}>按钮</button>}
}
class组件写法2
class Form extends React.Component {handleClick = (e) => {...}render() {return <button onClick={handleClick}>按钮</button>}
}
更多:
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
2)执行顺序
import React from 'react';
class App extends React.Component{constructor(props) {super(props);this.parentRef = React.createRef();this.childRef = React.createRef();}componentDidMount() {console.log("React componentDidMount!");this.parentRef.current?.addEventListener("click", () => {console.log("原生事件:父元素 DOM 事件监听!");});this.childRef.current?.addEventListener("click", () => {console.log("原生事件:子元素 DOM 事件监听!");});document.addEventListener("click", (e) => {console.log("原生事件:document DOM 事件监听!");});}parentClickFun = () => {console.log("React 事件:父元素事件监听!");};childClickFun = () => {console.log("React 事件:子元素事件监听!");};render() {return (<div ref={this.parentRef} onClick={this.parentClickFun}><div ref={this.childRef} onClick={this.childClickFun}>分析事件执行顺序</div></div>);}
}
export default App;输出顺序为:原生事件:子元素 DOM 事件监听!
原生事件:父元素 DOM 事件监听!
React 事件:子元素事件监听!
React 事件:父元素事件监听!
原生事件:document DOM 事件监听!
结论:
- React 所有事件都挂载在 document 对象上
- 当真实 DOM 元素触发事件,会冒泡到 document 对象后,再处理 React 事件
- 所以会先执行原生事件,然后处理 React 事件
- 最后真正执行 document 上挂载的事件
所以想要阻止不同时间段的冒泡行为,对应使用不同的方法,对应如下:
- 阻止合成事件间的冒泡,用e.stopPropagation()
- 阻止合成事件与最外层 document 上的事件间的冒泡,用e.nativeEvent.stopImmediatePropagation()
- 阻止合成事件与除最外层document上的原生事件上的冒泡,通过判断e.target来避免
document.body.addEventListener('click', e => { if (e.target && e.target.matches('div.code')) { return; } this.setState({ active: false, }); });
}
3)总结:
- React 上注册的事件最终会绑定在document这个 DOM 上,而不是 React 组件对应的 DOM(减少内存开销就是因为所有的事件都绑定在 document 上,其他节点没有绑定事件)
- React 自身实现了一套事件冒泡机制,所以这也就是为什么我们 event.stopPropagation()无效的原因。
- React 通过队列的形式,从触发的组件向父组件回溯,然后调用他们 JSX 中定义的 callback
- React 有一套自己的合成事件 SyntheticEvent
[题] 说说 React中组件之间如何通信?
React的组件灵活,多样,按照不同的方式可以分成很多类型的组件
组件间通信即指组件通过某种方式来传递信息以达到某个目的
1、通信
- 父组件向子组件传递
- 子组件向父组件传递
- 兄弟组件之间的通信
- 父组件向后代组件传递
- 非关系组件传递
1)父组件向子组件传递
React的数据流动为单向的,父组件向子组件传递是最常见的方式
props属性
父组件在子组件标签内传递参数,子组件通过props属性接收父组件传递过来的参数
function EmailInput(props) {return (<label>Email: <input value={props.email} /></label>);
}const element = <EmailInput email="123124132@163.com" />;
2)子组件向父组件传递
基本思路:父组件向子组件传一个函数,然后通过这个函数的回调,拿到子组件传过来的值
parent
class Parents extends Component {constructor() {super();this.state = {price: 0};}getItemPrice(e) {this.setState({price: e});}render() {return (<div><div>price: {this.state.price}</div>{/* 向子组件中传入一个函数 */}<Child getPrice={this.getItemPrice.bind(this)} /></div>);}
}
child
class Child extends Component {clickGoods(e) {// 在此函数中传入值this.props.getPrice(e);}render() {return (<div><button onClick={this.clickGoods.bind(this, 100)}>goods1</button><button onClick={this.clickGoods.bind(this, 1000)}>goods2</button></div>);}
}
3)兄弟组件之间的通信
基本思路:父组件作为中间层来实现数据的互通,通过使用父组件传递
class Parent extends React.Component {constructor(props) {super(props)this.state = {count: 0}}setCount = () => {this.setState({count: this.state.count + 1})}render() {return (<div><SiblingAcount={this.state.count}/><SiblingBonClick={this.setCount}/></div>);}
}
4)父组件向后代组件传递
父组件向后代组件传递数据是一件最普通的事情,就像全局数据一样
使用context
提供了组件之间通讯的一种方式,可以共享数据,其他数据都能读取对应的数据
context:
- 创建:
React.createContext
- 存在
Provider
组件用于创建数据源,通过value
属性用于给后代组件传递数据 - 存在
Consumer
组件用于接收数据,或者使用contextType
属性接收
const PriceContext = React.createContext('price')<PriceContext.Provider value={100}>
</PriceContext.Provider>
class MyClass extends React.Component {static contextType = PriceContext;render() {let price = this.context;/* 基于这个值进行渲染工作 */}
}
<PriceContext.Consumer>{ /*这里是一个函数*/ }{price => <div>price:{price}</div>}
</PriceContext.Consumer>
5)非关系组件传递
组件之间关系类型比较复杂的情况,建议将数据进行一个全局资源管理,从而实现通信,例如redux
总结
由于React是单向数据流,主要思想是组件不会改变接收的数据,只会监听数据的变化,当数据发生变化时它们会使用接收到的新值,而不是去修改已有的值
因此,可以看到通信过程中,数据的存储位置都是存放在上级位置中