React 高级篇学习
一、无障碍
- 无障碍体现在:HTML语义化、无鼠标场景的兼容、无障碍表单等等。WCAG(网页内容无障碍指南)有相关标准
- react提供 支持碎片化标签的包裹,同时不影响语义(应该同vue slot标签一样并不会被渲染出来)
- 无障碍表单react的jsx的语法 使用 “htmlfor” 代替 原生html的 “for”
- react运行过程中可能会失焦,需要通过编程式方式控制焦点,react获取元素的方式是通过以下方式
class hello extend React.Compenent {
constrctor(props){
super(props)
this.inputElement = React.createRef()
}
render(){
return (
<input ref={this.inputElement}></input>
)
}
}
二、代码分割
- 使用 lazy函数,通过动态import进行组件引入
- import仅支持默认导出,如果是统一暴露,需要中间模块做一个默认导出
- 利用 suspense 可以对异步组件做降级处理
- 利用react-router 中的switch route 可以对动态组件选择性地渲染
const hello = () => {
<Router>
<Suspend fallback={<div>loading...</div>}>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
</Switch>
</Suspend>
</Router>
}
三、错误边界
- 错误边界是一种捕获子树中出现错误的降级组件
- 如果在class组件中定义生命周期钩子 getDerivedStateFromError 或 componentDidCatch,此组件即为错误边界
- 写一个错误边界
class ErrorBoundary extends React.Compenent {
constructor(props){
super(props)
this.errorState = false
}
static getDerivedStateFromError(error){
return { errorState: true }
}
componentDidCatch(error, errorInfo) {
// 你同样可以将错误日志上报给服务器
logErrorToMyService(error, errorInfo);
}
render(){
if(this.state.errorState){
return <h2>something went wrong</h2>
}
return this.props.children
}
}
// 然后作为一个常规组件使用它
<ErrorBoundary>
<Hello />
</ErrorBoundary>
- 错误边界注意事项:
- 只能捕获错误边界子树中的错误,本身错误并不能捕获
- react16以后若是错误边界没捕获不到异常,则会卸载整个组件树
- 错误边界无法捕获事件处理器异常,只能在内部通过try catch自行进行javascript捕获
四、context
- context是为了解决 嵌套组件值传递 需要逐层传递props的问题
const ThemeContext = React.createContext('light')
class App extends react.compenent {
render () {
return {
<ThemeContext.Provider theme="dark">
<Toolbar>
</ThemeContext.Provider>
}
}
}
const Toolbar = () => {
return (
<ThemesButtons />
)
}
class ThemesButtons extends React.Compenent {
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
- 深层组件提升(对组件的控制反转),避免中间冗余组件的props传递
function Page(props) {
const user = props.user;
const userLink = (
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
);
return <PageLayout userLink={userLink} />;
}
// 现在,我们有这样的组件:
<Page user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<PageLayout userLink={...} />
// ... 渲染出 ...
<NavigationBar userLink={...} />
// ... 渲染出 ...
{props.userLink}
五、Ref转发(未完待续)
const ref = React.createRef()
<FancyButton ref={ref}></ FancyButton>
const FancyButton = React.forwardRef((props, ref) => {
return (
<button ref={ref}>
{props.children}
</button>
)
})
console.log(ref.current) // 为button这个dom元素
六、Fragments
class Columns extends React.Component {
render() {
return (
<React.Fragment>
<td>Hello</td>
<td>World</td>
</React.Fragment>
);
}
}
// 或者
class Columns extends React.Component {
render() {
return (
<>
<td>Hello</td>
<td>World</td>
</>
);
}
}
key 是唯一可以传递给 Fragment 的属性。
function Glossary(props) {
return (
<dl>
{props.items.map(item => (
// 没有`key`,React 会发出一个关键警告
<React.Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</React.Fragment>
))}
</dl>
);
}
七、高阶组件(HOC)
- 高阶组件是一个函数,入参是一个react组件和其他数据,返回值是一个包装之后的组件(目的是对相同逻辑组件封装)
- 优势:将业务逻辑抽象集中起来,与数据解耦。
// CommentListWithSubscription 和 BlogPostWithSubscription具有相同业务逻辑,只是数据不同,那么通过withSubscription传入不同的数据即可。
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
)
// 此函数接收一个组件...
function withSubscription(WrappedComponent, selectData) {
// ...并返回另一个组件...
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount() {
// ...负责订阅相关的操作...
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
// ... 并使用新数据渲染被包装的组件!
// 请注意,我们可能还会传递其他属性
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
- 避免在高阶组件内直接修改原始组件,而是使用组合的方式
// no no no
function logProps(InputComponent) {
InputComponent.prototype.componentDidUpdate = function(prevProps) {
console.log('Current props: ', this.props);
console.log('Previous props: ', prevProps);
};
// 返回原始的 input 组件,暗示它已经被修改。
return InputComponent;
}
// 每次调用 logProps 时,增强组件都会有 log 输出。
const EnhancedComponent = logProps(InputComponent);
// yes yes
function logProps(WrappedComponent) {
return class extends React.Component {
componentDidUpdate(prevProps) {
console.log('Current props: ', this.props);
console.log('Previous props: ', prevProps);
}
render() {
// 将 input 组件包装在容器中,而不对其进行修改。Good!
return <WrappedComponent {...this.props} />;
}
}
}
八、与第三方库协同
九、深入JSX
- JSX是React.createElement语法糖
<MyButton color="blue" shadowSize={2}>
Click Me
</MyButton>
React.createElement(
MyButton,
{color: 'blue', shadowSize: 2},
'Click Me'
)
- 在 JSX 类型中可以使用点语法
import React from 'react';
const MyComponents = {
DatePicker: function DatePicker(props) {
return <div>Imagine a {props.color} datepicker here.</div>;
}
}
function BlueDatePicker() {
return <MyComponents.DatePicker color="blue" />;
}
- 用户定义的组件必须以大写字母开头
- 想通过通用表达式来(动态)决定元素类型,你需要首先将它赋值给大写字母开头的变量
import React from 'react';
import { PhotoStory, VideoStory } from './stories';
const components = {
photo: PhotoStory,
video: VideoStory
};
function Story(props) {
// 错误!JSX 类型不能是一个表达式。
return <components[props.storyType] story={props.story} />;
}
function Story(props) {
// 正确!JSX 类型可以是大写字母开头的变量。
const SpecificStory = components[props.storyType];
return <SpecificStory story={props.story} />;
}
- JavaScript表达式可以作为标签属性
- 字符串字面量也可以作为标签属性
- 标签属性默认值是true
- 标签属性可以使用展开运算符
function App2() {
const props = {firstName: 'Ben', lastName: 'Hector'};
return <Greeting {...props} />;
}
- JavaScript表达式可以作为子元素
<MyComponent>{msg}</MyComponent>
function TodoList() {
const todos = ['finish doc', 'submit pr', 'nag dan to review'];
return (
<ul>
{todos.map((message) => <Item key={message} message={message} />)}
</ul>
);
}
- 函数也可以作为子元素
function ListOfTenThings() {
return (
<Repeat numTimes={10}>
{(index) => <div key={index}>This is item {index} in the list</div>}
</Repeat>
);
}
- false, null, undefined, and true 是合法的子元素。但它们并不会被渲染
十、通过Portal进行事件冒泡
- portal是通过 ReactDOM.createPortal(child, container) 将元素挂载到非直接父元素dom结点的一种方式
- portal子元素仍能够冒泡到父元素,尽管其并未挂在父元素上
- 对下方代码做一个解释:Modal组件的子元素挂载到了新创建的div节点上,又由于child组件本身并无定义点击事件,因此会冒泡到parent组件的onclik事件,这就是如何利用portal进行事件冒泡。
// 在 DOM 中有两个容器是兄弟级 (siblings)
const appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');
class Modal extends React.Component {
constructor(props) {
super(props);
this.el = document.createElement('div');
}
componentDidMount() {
modalRoot.appendChild(this.el);
}
componentWillUnmount() {
modalRoot.removeChild(this.el);
}
render() {
return ReactDOM.createPortal(
this.props.children,
this.el
);
}
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {clicks: 0};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(state => ({
clicks: state.clicks + 1
}));
}
render() {
return (
<div onClick={this.handleClick}>
<p>Number of clicks: {this.state.clicks}</p>
<p>
Open up the browser DevTools
to observe that the button
is not a child of the div
with the onClick handler.
</p>
<Modal>
<Child />
</Modal>
</div>
);
}
}
function Child() {
// 这个按钮的点击事件会冒泡到父元素
// 因为这里没有定义 'onClick' 属性
return (
<div className="modal">
<button>Click</button>
</div>
);
}
ReactDOM.render(<Parent />, appRoot);
- 还有一些知识,例如refs回调,hook如何使用等未做记录。