react知识点整理

369 阅读25分钟

1、

2、React 事件机制

React并不是将事件绑定到元素的真实Dom上,而是在document中监听所有事件,当事件发生时,冒泡到document处,React将事件内容封装到真正的处理函数中运行 减少了内存消耗,组件挂载销毁时也能统一订阅和移除事件 event.nativeEvent是原生事件

2.1 React的事件和普通html事件有什么区别

react事件和dom事件

  • react中的event是syntheticEvent(合成事件),模拟出来DOM事件所有能力
  • event.nativeEvent是原生事件对象
  • 所有的事件,都被挂载到doucument上(react16),
  • react17开始事件绑定到root上
  • 和dom事件不一样和vue事件也不一样

1.命名规范不同

2.处理语法不同

3.必须调用preventDefault()阻止默认行为

2.2 React组件怎么做事件代理的,原理是什么

原理:React基于Virtual Dom实现了一个合成事件层,定义的事件处理器会接受到一个合成事件对象的实例,符合W3c标准,且与原生的浏览器事件拥有同样的接口,支持冒泡机制,所有事件都自动绑定在最外层上

事件委托:React会把所有的事件都绑定在结构的最外层,使用统一的事件监听器,这个事件监听器上维持一个映射来保存所有组件内部事件监听和处理函数

自动绑定:React组件中,每个方法上下文都会指向该组件的实例,即自动绑定this为当前组件

2.3、react事件绑定this的原因

是js本身特性决定的,this永远指向最后调用它的那个对象,在react事件中绑定this,来使事件的this指向当前组件,避免this的丢失 (也可以通过静态方法,this指向当前实例 fn = ()=>{ this.setState() } )

3、受控组件

区别是什么

1.共同点,都是指表单元素,或者表单组件
2.不同点,被react的state控制,就是受控组件。不会state控制,就是非受控。
3.受控组件的实现方式,就是设置state,使用事件调用setstate,更新数据和视图。
4.非受控组件,避开state,使用ref等等方式,更新数据和视图。
非受控组件使用场景:

  • 必须手动操作dom元素,setState实现不了
  • 文件上传<input type="file">
  • 某些富文本编辑器,需要传人dom元素

4、组件通讯

4.1 父子组件通讯

父组件通过属性=值的方式给子组件传递数据,子组件通过props参数获取父组件传递过来的数据。
在react中,子组件传递数据给父组件同样使用props,只是让父组件给子组件传递一个回调函数,在子组件中调用这个回调函数就可以。

参数验证: 对于传递给子组件的数据,有时候我们会进行验证,我们会通过prop-types库来进行参数验证。

import React, { Component } from 'react'
import PropTypes from 'prop-types'

function ChildCpn(props) {
  const { name, age, height, names } = props
  return (
    <div>
      <h2>{name + ' ' + age + ' ' + height}</h2>
      <ul>
        {
          names.map(item => {
            return <li>{item}</li>
          })
        }
      </ul>
    </div>
  )
}

ChildCpn.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number,
  height: PropTypes.number,
  names: PropTypes.array
}

export default class App extends Component {
  render() {
    return (
      <div>
        <ChildCpn name="haha" age='18' height={1.88} names={['小王', '小何', '小白']}></ChildCpn>
      </div>
    )
  }
}

4.2 Context(上下文)

Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。

export const LoginContext = React.createContext({name:'gu', age:'16'})

<LoginContext.Provider value={loginInfo} >
    {element}
</LoginContext.Provider>

1、函数组件
import { LoginContext } from '../App'
<LoginContext.Consumer>
    {(loginInfo) => {
        return <span>
            欢迎来到列表:{loginInfo.add}
        </span>
    }}
</LoginContext.Consumer

2、指定class.contentType = LoginContext
或者 static contentType = LoginContext

API

const MyContext = React.createContext(defaultValue);

创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。

只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。此默认值有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。

Context.Provider

<MyContext.Provider value={/* 某个值 */}>

每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。

Provider 接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。

当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。从 Provider 到其内部 consumer 组件(包括 .contextType 和 useContext)的传播不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件跳过更新的情况下也能更新。

通过新旧值检测来确定变化,使用了与 Object.is 相同的算法。

注意

当传递对象给 value 时,检测变化的方式会导致一些问题:详见注意事项

Class.contextType

class MyClass extends React.Component {
  componentDidMount() {
    let value = this.context;
    /* 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 */
  }
  componentDidUpdate() {
    let value = this.context;
    /* ... */
  }
  componentWillUnmount() {
    let value = this.context;
    /* ... */
  }
  render() {
    let value = this.context;
    /* 基于 MyContext 组件的值进行渲染 */
  }
}
MyClass.contextType = MyContext;

挂载在 class 上的 contextType 属性可以赋值为由 React.createContext() 创建的 Context 对象。此属性可以让你使用 this.context 来获取最近 Context 上的值。你可以在任何生命周期中访问到它,包括 render 函数中。

注意:

你只通过该 API 订阅单一 context。如果你想订阅多个,阅读使用多个 Context 章节

如果你正在使用实验性的 public class fields 语法,你可以使用 static 这个类属性来初始化你的 contextType

class MyClass extends React.Component {
  static contextType = MyContext;
  render() {
    let value = this.context;
    /* 基于这个值进行渲染工作 */
  }
}

Context.Consumer

<MyContext.Consumer>
  {value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>

一个 React 组件可以订阅 context 的变更,此组件可以让你在函数式组件中可以订阅 context。

这种方法需要一个函数作为子元素(function as a child)。这个函数接收当前的 context 值,并返回一个 React 节点。传递给函数的 value 值等价于组件树上方离这个 context 最近的 Provider 提供的 value 值。如果没有对应的 Provider,value 参数等同于传递给 createContext() 的 defaultValue

注意

想要了解更多关于 “函数作为子元素(function as a child)” 模式,详见 render props

Context.displayName

context 对象接受一个名为 displayName 的 property,类型为字符串。React DevTools 使用该字符串来确定 context 要显示的内容。

示例,下述组件在 DevTools 中将显示为 MyDisplayName:

const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
<MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
<MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中

示例

动态 Context

一个更加复杂的方案是对上面的 theme 例子使用动态值(dynamic values):

theme-context.js

export const themes = {
  light: {
    foreground: '#000000',
    background: '#eeeeee',
  },
  dark: {
    foreground: '#ffffff',
    background: '#222222',
  },
};

export const ThemeContext = React.createContext(  themes.dark // 默认值);

themed-button.js

import {ThemeContext} from './theme-context';

class ThemedButton extends React.Component {
  render() {
    let props = this.props;
    let theme = this.context;    return (
      <button
        {...props}
        style={{backgroundColor: theme.background}}
      />
    );
  }
}
ThemedButton.contextType = ThemeContext;
export default ThemedButton;

app.js

import {ThemeContext, themes} from './theme-context';
import ThemedButton from './themed-button';

// 一个使用 ThemedButton 的中间组件
function Toolbar(props) {
  return (
    <ThemedButton onClick={props.changeTheme}>
      Change Theme
    </ThemedButton>
  );
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      theme: themes.light,
    };

    this.toggleTheme = () => {
      this.setState(state => ({
        theme:
          state.theme === themes.dark
            ? themes.light
            : themes.dark,
      }));
    };
  }

  render() {
    // 在 ThemeProvider 内部的 ThemedButton 按钮组件使用 state 中的 theme 值,    // 而外部的组件使用默认的 theme 值    return (
      <Page>
        <ThemeContext.Provider value={this.state.theme}>
          <Toolbar changeTheme={this.toggleTheme} />
        </ThemeContext.Provider>
        <Section>
          <ThemedButton />
        </Section>
      </Page>
    );
  }
}

ReactDOM.render(<App />, document.root);

在嵌套组件中更新 Context

从一个在组件树中嵌套很深的组件中更新 context 是很有必要的。在这种场景下,你可以通过 context 传递一个函数,使得 consumers 组件更新 context:

theme-context.js

// 确保传递给 createContext 的默认值数据结构是调用的组件(consumers)所能匹配的!
export const ThemeContext = React.createContext({
  theme: themes.dark,  toggleTheme: () => {},});

theme-toggler-button.js

import {ThemeContext} from './theme-context';

function ThemeTogglerButton() {
  // Theme Toggler 按钮不仅仅只获取 theme 值,  // 它也从 context 中获取到一个 toggleTheme 函数  return (
    <ThemeContext.Consumer>
      {({theme, toggleTheme}) => (        <button
          onClick={toggleTheme}
          style={{backgroundColor: theme.background}}>
          Toggle Theme
        </button>
      )}
    </ThemeContext.Consumer>
  );
}

export default ThemeTogglerButton;

app.js

import {ThemeContext, themes} from './theme-context';
import ThemeTogglerButton from './theme-toggler-button';

class App extends React.Component {
  constructor(props) {
    super(props);

    this.toggleTheme = () => {
      this.setState(state => ({
        theme:
          state.theme === themes.dark
            ? themes.light
            : themes.dark,
      }));
    };

    // State 也包含了更新函数,因此它会被传递进 context provider。    this.state = {      theme: themes.light,
      toggleTheme: this.toggleTheme,
    };  }

  render() {
    // 整个 state 都被传递进 provider    return (
      <ThemeContext.Provider value={this.state}>        <Content />
      </ThemeContext.Provider>
    );
  }
}

function Content() {
  return (
    <div>
      <ThemeTogglerButton />
    </div>
  );
}

ReactDOM.render(<App />, document.root);

消费多个 Context

为了确保 context 快速进行重渲染,React 需要使每一个 consumers 组件的 context 在组件树中成为一个单独的节点。

// Theme context,默认的 theme 是 “light” 值
const ThemeContext = React.createContext('light');

// 用户登录 context
const UserContext = React.createContext({
  name: 'Guest',
});

class App extends React.Component {
  render() {
    const {signedInUser, theme} = this.props;

    // 提供初始 context 值的 App 组件
    return (
      <ThemeContext.Provider value={theme}>        <UserContext.Provider value={signedInUser}>          <Layout />
        </UserContext.Provider>      </ThemeContext.Provider>    );
  }
}

function Layout() {
  return (
    <div>
      <Sidebar />
      <Content />
    </div>
  );
}

// 一个组件可能会消费多个 context
function Content() {
  return (
    <ThemeContext.Consumer>      {theme => (        <UserContext.Consumer>          {user => (            <ProfilePage user={user} theme={theme} />          )}        </UserContext.Consumer>      )}    </ThemeContext.Consumer>  );
}

如果两个或者更多的 context 值经常被一起使用,那你可能要考虑一下另外创建你自己的渲染组件,以提供这些值。

注意事项

因为 context 会根据引用标识来决定何时进行渲染(本质上是 value 属性值的浅比较),所以这里可能存在一些陷阱,当 provider 的父组件进行重渲染时,可能会在 consumers 组件中触发意外的渲染。举个例子,当每一次 Provider 重渲染时,由于 value 属性总是被赋值为新的对象,以下的代码会重新渲染下面所有的 consumers 组件:

class App extends React.Component {
  render() {
    return (
      <MyContext.Provider value={{something: 'something'}}>        <Toolbar />
      </MyContext.Provider>
    );
  }
}

为了防止这种情况,将 value 状态提升到父节点的 state 里:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: {something: 'something'},    };
  }

  render() {
    return (
      <MyContext.Provider value={this.state.value}>        <Toolbar />
      </MyContext.Provider>
    );
  }
}

5、setState为何使用不可变值

什么是不可变值?

其实,不可变值可以理解为函数式编程的纯函数,也就是:在修改数值时,生成的数值不影响原来的数值。
在shouldMountUpdate生命周期中会将要改变的值与之前的值做个比较来确定是否改变视图,以这种方式来优化性能。

问题分析🔍

setState在同步事件是异步的,因为每次state的改变都会引起重新渲染,为了提高性能,react会对组件中的setState操作进行合并,在事件循环机制中,宏任务执行完了之后才会去执行setState操作。官方解释

setState在异步事件中是同步更新状态。

这个是由react中批量更新的事务机制决定的,react会使用isBatchingUpdates变量去记录是否需要执行state队列中的内容,类似于锁的概念,当isBatchingUpdates的值变为false的时候,会开始执行state队列中的内容。在异步事件中,刚开始isBatchingUpdates是true,后面异步事件还未执行,isBatchingUpdates就变成了false,所以达到了同步的效果。

state = {
  count: 0
}

add = () => {
  this.setState({ count: this.state.count + 1 })
}

// 同步调用中,setState是异步的,所以最后this.state.count依然1
handleClick1 = () => {
  this.add()
  this.add()
  this.add()
}

// 异步调用中,setState是同步的,最后this.state.count是3
handleClick2 = () => {
  setTimeout(() => {
    this.add()
    this.add()
    this.add()
  }, 0)
}
复制代码

解决方案🙋

setState(updater, [callback])

// 第一个参数传对象时,异步更新
setState({ count: 0 })

// 第一个参数传函数时,可以获取到最新的state和props值 => 也是把setState的异步改成同步的方法
setState((state, props) => {
  return { count: state.count + props.step }
})

// 第二个参数是可选的,可以拿到合并更新完的最新结果
setState({ count: 0 },() => {
  xxxx
})

6、类组件和函数组件有什么区别

  • 思想不同:类组件是面向对象的思想,函数组件是函数式编程
  • 类组件太重了,内部逻辑难以拆分和复用
  • 函数组件会捕获render内部的状态,函数组件会每次都重新创建一遍,可以实现状态的同步更新
  • 函数和react的理念更贴合,声明式编程 函数组件: 纯函数,输入props,输出JSX 没有实例、没有生命周期、没有state 不能扩展其他方法

7、react生命周期

image.png 挂载阶段: constructor---->getDerivedStatedFromProps---->render---->componentDidMount

更新阶段: getDerivedStateFromProps---->shouldComponentUpdate---->render---->getSnapshotBeforeUpdate---->componentDidUpdate

卸载阶段:componentWillUnmount

  • getDerivedStateFromProps

    • 触发时间:此生命周期并非props变化的时候才会调用(不单单更新时才会调用),他在挂载阶段也会执行
    • 替代的方法: componentWillReceiveProps
      class Example extends React.Component {
        static getDerivedStateFromProps(nextProps, prevState) {
          // 没错,这是一个static
        }
      }
    复制代码
    
  • getSnapshotBeforeUpdate

    • 触发时间:update发生的时候,在render之后,在组件dom渲染之前。
    • 替代的方法:componentWillUpdate
    class Example extends React.Component {
     getSnapshotBeforeUpdate(prevProps, prevState) {
     // ...
     }
    }
    复制代码
    

8、 react16+版本中为什么要更新生命周期?(为什么要废弃一些生命周期)

1.首先我们要理解Fiber架构

react16+版本以后,是将reconciler(diff+render+创建dom)+commit(patch),这两个操作是分开的,在15版本的时候,这两个操作是交替进行的,不能被打断

因为fiber架构下允许被打断,所以reconciler阶段会调用多次,等一切结束了才会调用commit,所以will也会被执行多次,被执行多次以后可能出现意料之外的bug

2.由于三个will钩子使用率不高,而且容易误解和滥用

9、portal(如何让组件渲染到父组件之外)

一个 portal 的典型用例是当父组件有 overflow: hidden 或 z-index 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框:

ReactDOM.createPortal(child, container)

class Modal extends React.Component {
    constructor(props) {
        super(props)
        this.el = document.createElement('div')
    }
    componentDidMount() {
        const modalRoot = document.getElementById('modal-root');
        modalRoot.appendChild(this.el);
    }
    componentWillUnmount() {
        const modalRoot = document.getElementById('modal-root');
        modalRoot.removeChild(this.el);
    }
    render() {
        return ReactDOM.createPortal(
            this.props.children,
            this.el
        )
    }
}

第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment。第二个参数(container)是一个 DOM 元素。

10、异步组件

import React, { lazy } from 'react'; const Index = lazy(() => import('./index/index'))

React.Suspense

React.lazy

React.lazy() 允许你定义一个动态加载的组件。这有助于缩减 bundle 的体积,并延迟加载在初次渲染时未用到的组件。 请注意,渲染 lazy 组件依赖该组件渲染树上层的 <React.Suspense> 组件。这是指定加载指示器(loading indicator)的方式。

React.Suspense 可以指定加载指示器(loading indicator),以防其组件树中的某些子组件尚未具备渲染条件。在未来,我们计划让 Suspense 处理更多的场景,如数据获取等。你可以在 我们的路线图 了解这一点 如今,懒加载组件是 <React.Suspense> 支持的唯一用例:

// 该组件是动态加载的
const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    // 显示 <Spinner> 组件直至 OtherComponent 加载完成
    <React.Suspense fallback={<Spinner />}>
      <div>
        <OtherComponent />
      </div>
    </React.Suspense>
  );
}

11、性能优化

11.1、shouldComponentUpdate

shouldComponentUpdate()

shouldComponentUpdate(nextProps, nextState)

根据 shouldComponentUpdate() 的返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响。默认行为是 state 每次发生变化组件都会重新渲染。大部分情况下,你应该遵循默认行为。

11.2、Purgement(class)和React.memo(函数组件)

React.PureComponent

React.PureComponent 与 React.Component 很相似。两者的区别在于 React.Component 并未实现 shouldComponentUpdate(),而 React.PureComponent 中以浅层对比 prop 和 state 的方式来实现了该函数。

如果赋予 React 组件相同的 props 和 state,render() 函数会渲染相同的内容,那么在某些情况下使用 React.PureComponent 可提高性能。

注意

React.PureComponent 中的 shouldComponentUpdate() 仅作对象的浅层比较。如果对象中包含复杂的数据结构,则有可能因为无法检查深层的差别,产生错误的比对结果。仅在你的 props 和 state 较为简单时,才使用 React.PureComponent,或者在深层数据结构发生变化时调用 forceUpdate() 来确保组件被正确地更新。你也可以考虑使用 immutable 对象加速嵌套数据的比较。

此外,React.PureComponent 中的 shouldComponentUpdate() 将跳过所有子组件树的 prop 更新。因此,请确保所有子组件也都是“纯”的组件。

React.memo

const MyComponent = React.memo(function MyComponent(props) {
  /* 使用 props 渲染 */
});

React.memo 为高阶组件

如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。

React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useStateuseReducer 或 useContext 的 Hook,当 state 或 context 发生变化时,它仍会重新渲染。

默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

function MyComponent(props) {
  /* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
  /*
  如果把 nextProps 传入 render 方法的返回结果与
  将 prevProps 传入 render 方法的返回结果一致则返回 true,
  否则返回 false
  */
}
export default React.memo(MyComponent, areEqual);

此方法仅作为**性能优化**的方式而存在。但请不要依赖它来“阻止”渲染,因为这会产生 bug。

注意

与 class 组件中 shouldComponentUpdate() 方法不同的是,如果 props 相等,areEqual 会返回 true;如果 props 不相等,则返回 false。这与 shouldComponentUpdate 方法的返回值相反。

12、render props

具有 render prop 的组件接受一个返回 React 元素的函数,并在组件内部通过调用此函数来实现自己的渲染逻辑。

class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}

class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>

        {/*
          使用 `render`prop 动态决定要渲染的内容,
          而不是给出一个 <Mouse> 渲染结果的静态表示
        */}
        {this.props.render(this.state)}
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>移动鼠标!</h1>
        <Mouse render={mouse => (
          <Cat mouse={mouse} />
        )}/>
      </div>
    );
  }
}

二、redux(www.redux.org.cn/docs/introd…

blog.csdn.net/qingbaicai/…

  • state: 定义state数据,redux的state是集中管理、全局唯一,不可变性
  • actions: 描述如何修改状态,actions就是JSON对象、必须type属性,发送需要使用store.dispatch(为了了便于维护和拓展,最好拆分一下reducer,可以通过combineReducers完成)
  • reducer: 实质是一个函数,接受state、action进行计算,落实action传递的描述
  • store: redux的store就是一个粘合剂,将redux中的各个部分粘合在一起

通过createStore接受reducer创建store对象store对象提供了dispatch来发送action,通过getState获取全局state,通过subscribe订阅state变化

react-redux库提供的功能(react-redux集成(链接redux-state和redux-component):

1、向根组件注入Store=>Provider组件

2、链接React组件和Redux状态成=>connect

3、获取React组件所需的State和Actions=>map api

重点:容器型组件读写操作均是通过redux store中进行  以及对state数据的规划

具体实现步骤:

1、创建container存储容器型组件

编写容器型组件:通过redux的connect方法,将state、action映射到组建的props中

2、通过redux的createStore以reducers为参数创建store,通过react-redux的provider组件将store传入项目。

3、异步action关键时间点,发送阶段、成功/失败阶段

处理异步action需要redux-thunk的帮助

异步action

异步action关键时间点,发送阶段、成功/失败阶段。 处理异步action需要redux-thunk的帮助,redux中间件,对dispatch进行改造。 步骤引入redux-thunk的thunkMiddleware 在创建store时通过redux的applyMiddleware使用。

Action

Action 是把数据从应用传到 store 的有效载荷。 它是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store。 添加新 todo 任务的 action 是这样的:

const ADD_TODO = 'ADD_TODO'
{
  type: ADD_TODO,
  text: 'Build my first Redux app'
}

Reducer

Reducers 指定了应用状态的变化如何响应 actions 并发送到 store 的,记住 actions 只是描述了有事情发生了这一事实,并没有描述应用如何更新 state。 reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。

(previousState, action) => newState
import { VisibilityFilters } from './actions'

const initialState = {
  visibilityFilter: VisibilityFilters.SHOW_ALL,
  todos: []
};

function todoApp(state, action) {
  if (typeof state === 'undefined') {
    return initialState
  }

  // 这里暂不处理任何 action,
  // 仅返回传入的 state。
  return state
}

1.不要修改 state
2.在 default 情况下返回旧的 state

最后,Redux 提供了 combineReducers() 工具类来做上面 todoApp 做的事情,这样就能消灭一些样板代码了。有了它,可以这样重构 todoApp

import { combineReducers } from 'redux'

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

export default todoApp

combineReducers 接收一个对象,可以把所有顶级的 reducer 放到一个独立的文件中,通过 export 暴露出每个 reducer 函数,然后使用 import * as reducers 得到一个以它们名字作为 key 的 object:

import { combineReducers } from 'redux'

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

export default todoApp
import { combineReducers } from 'redux'
import * as reducers from './reducers'

const todoApp = combineReducers(reducers)

源码

reducers.js

import { combineReducers } from 'redux'
import {
  ADD_TODO,
  TOGGLE_TODO,
  SET_VISIBILITY_FILTER,
  VisibilityFilters
} from './actions'
const { SHOW_ALL } = VisibilityFilters

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    default:
      return state
  }
}

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case TOGGLE_TODO:
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: !todo.completed
          })
        }
        return todo
      })
    default:
      return state
  }
}

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

export default todoApp

Store

在前面的章节中,我们学会了使用 action 来描述“发生了什么”,和使用 reducers 来根据 action 更新 state 的用法。

Store 就是把它们联系到一起的对象。Store 有以下职责:

import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)

createStore() 的第二个参数是可选的, 用于设置 state 初始状态。这对开发同构应用时非常有用,服务器端 redux 应用的 state 结构可以与客户端保持一致, 那么客户端可以将从网络接收到的服务端 state 直接用于本地数据初始化。

let store = createStore(todoApp, window.STATE_FROM_SERVER)

发起 Actions

现在我们已经创建好了 store ,让我们来验证一下!虽然还没有界面,我们已经可以测试数据处理逻辑了。

import {
  addTodo,
  toggleTodo,
  setVisibilityFilter,
  VisibilityFilters
} from './actions'

// 打印初始状态
console.log(store.getState())

// 每次 state 更新时,打印日志
// 注意 subscribe() 返回一个函数用来注销监听器
const unsubscribe = store.subscribe(() =>
  console.log(store.getState())
)

// 发起一系列 action
store.dispatch(addTodo('Learn about actions'))
store.dispatch(addTodo('Learn about reducers'))
store.dispatch(addTodo('Learn about store'))
store.dispatch(toggleTodo(0))
store.dispatch(toggleTodo(1))
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))

// 停止监听 state 更新
unsubscribe();

数据流

Redux 应用中数据的生命周期遵循下面 4 个步骤: www.redux.org.cn/docs/basics…

1. 调用 store.dispatch(action)
2. Redux store 调用传入的 reducer 函数。
3. 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
4. Redux store 保存了根 reducer 返回的完整 state 树。\

2、 搭配react

2.1 安装 React Redux

npm install --save react-redux

容器组件(Smart/Container Components)和展示组件(Dumb/Presentational Components)

Redux 的 React 绑定库是基于 容器组件和展示组件相分离 的开发思想。所以建议先读完这篇文章再回来继续学习。这个思想非常重要。

已经读完了?那让我们再总结一下不同点:

展示组件容器组件
作用描述如何展现(骨架、样式)描述如何运行(数据获取、状态更新)
直接使用 Redux
数据来源props监听 Redux state
数据修改从 props 调用回调函数向 Redux 派发 actions
调用方式手动通常由 React Redux 生成
容器 connect
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'

const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case 'SHOW_ALL':
      return todos
    case 'SHOW_COMPLETED':
      return todos.filter(t => t.completed)
    case 'SHOW_ACTIVE':
      return todos.filter(t => !t.completed)
  }
}

const mapStateToProps = state => {
  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
  }
}

const mapDispatchToProps = dispatch => {
  return {
    onTodoClick: id => {
      dispatch(toggleTodo(id))
    }
  }
}

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

export default VisibleTodoList
将容器放到一个组件
import React from 'react'
import Footer from './Footer'
import AddTodo from '../containers/AddTodo'
import VisibleTodoList from '../containers/VisibleTodoList'

const App = () => (
  <div>
    <AddTodo />
    <VisibleTodoList />
    <Footer />
  </div>
)

export default App
传入 Store

建议的方式是使用指定的 React Redux 组件 <Provider> 来 魔法般的 让所有容器组件都可以访问 store,而不必显示地传递它。只需要在渲染根组件时使用即可。

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'

let store = createStore(todoApp)

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

redux中间件,对dispatch进行改造

import { createStore, applyMiddleware } from 'redux' // 引入createStore函数
import  thunkMiddle from 'redux-thunk'
const store = createStore(rootReducer,applyMiddleware(thunkMiddle)) //创建store

三、react-router

react-router: 实现了路由的核心功能 react-router-dom: 基于react-router,加入了在浏览器运行环境下的一些功能,例如:Link组件,会渲染一个a标签,Link组件源码a标签行; BrowserRouter和HashRouter组件,前者使用pushState和popState事件构建路由,后者使用window.location.hash和hashchange事件构建路由。

1.路由的基本使用,一共需要用到4个组件

  1. <BrowserRouter>或<HashRouter>:包裹在需要使用组件路由的最外层
  2. <Routes>与<Route>:<Routes><Route>需要配合使用,且必须用<Routes>包裹<Route>
  3. <Link>:跳转到某个组件

2.<NavLink>:导航高亮组件

  1. 如果高亮的类名是active,直接写<NavLink to='xx'>即可
  2. 如果高亮的类名不是active,则需要使用<NavLink to='xx' className={函数}>这种写法
  3. <NavLink end>: 当添加上了end属性后,如果子级路由匹配到了高亮,那么父级路由就不再高亮

3.<Navigate/>:重定向组件

  1. 语法:<Navigate to='/路径'/>
  2. <Navigate replace>replace表示跳转模式,默认是false,表示push,如果为true,表示replace

5.useRoutes():路由表

  1. 语法:const element=useRoutes([{path:'路径',element:'组件',children:'子路由'}])
  2. 在路由区直接使用{element},不需要再反复写<Routes><Route></Route></Routes>
  3. 在路由组件呈现的位置使用<Outlet>组件,进行渲染,相当于vue的<Router-view>

4、outlet作为元素渲染

< NavLink >是< Link >的一个特定版本,会在匹配上当前的url的时候给已经渲染的元素添加参数,组件的属性有

属性属性说明
activeClassName(string)设置选中样式,默认值为active
activeStyle(object)当元素被选中时,为此元素添加样式
exact(bool)为true时,只有当导致和完全匹配class和style才会应用
strict(bool)为true时,在确定为位置是否与当前URL匹配时,将考虑位置pathname后的斜线
isActive(func)判断链接是否激活的额外逻辑的功能

6.useNavigate():编程式导航
语法:\

  1. const navigate = useNavigate();\

  2. navigate('/跳转路径',{replace:跳转模式(push或者replace,默认是push),state:{参数}})

  3. navigate(-1):后退一步,navigate(1):前进一步

import ReactDOM from 'react-dom';
import { BrowserRouter,  useRoutes, Navigate,useNavigate } from 'react-router-dom'
function App() {
  const element = useRoutes([
    { path: '/home', element: <Home /> },
    { path: '/news', element: <News /> },
    { path: '/', element: <Navigate to='/home' /> },
  ])
  return <div>{element}</div>
}
function Home() {
  const navigate=useNavigate()
  function handleClick(){
    navigate('/news')
  }
  return <button onClick={handleClick}>跳转到new组件</button>
}
function News() {
  return <div>news组件</div>
}
ReactDOM.render(<BrowserRouter><App /></BrowserRouter>, document.getElementById('root'))

7.路由组件传递参数
1. params参数传参 useParams

import ReactDOM from 'react-dom';
import { BrowserRouter, Link, useRoutes, useParams, useMatch } from 'react-router-dom'
function App() {
  const element = useRoutes([
    // 注册路由(声明接受):<Route path='/xxx/xx/:key/:key'/>
    { path: '/home/:id', element: <Home /> },
  ])
  return (
    <div>
      {/* 路由链接(携带参数):<Link to='xx/value/value'></Link> */}
      <Link to='/home/10' >传递id为10</Link>
      {element}
    </div>
  );
}
function Home() {
  // 获取路由参数有两种方法useParams()或者useMatch()
  const { id } = useParams()
  // const {params:{id}}=useMatch({path:'/home/:id'})
  return <div>id是{id}
  </div>
}
ReactDOM.render(<BrowserRouter><App /></BrowserRouter>, document.getElementById('root'))
复制代码

2.search参数传参useSearchParams

import { BrowserRouter, Link, useRoutes, useSearchParams, useLocation } from 'react-router-dom'
function App() {
  const element = useRoutes([
    // 2.注册路由(无需声明,正确注册即可)
    { path: '/home', element: <Home /> },
  ])
  return (
    <div>
      {/* 1.路由链接(携带参数):<Link to='/xxx/xx?key=value&key=value'></Link> */}
      <Link to='/home?age=100&name=jack' >传递search参数</Link>
      {element}
    </div>
  );
}
function Home() {
  /* 3.接受参数也有两种
        1.通过const [search,setSearch] = useSearchParams() 
          1.调用search.get(key)
          2.setSearch:更新收到的search参数
        2.通过 const {search} = useLocation()
          1.获取到的search是urlencoded编码字符串,需要借助querystring解析
   */
  const [search, setSearch] = useSearchParams()
  const name = search.get('name')
  const age = search.get('age')
  // const {search} = useLocation()
  return <div>名字是{name}年龄是{age}
  </div>
}
ReactDOM.render(<BrowserRouter><App /></BrowserRouter>, document.getElementById('root'))

复制代码

3.state参数传参useLocation

import ReactDOM from 'react-dom';
import { BrowserRouter, Link, useRoutes, useLocation, Outlet } from 'react-router-dom'
function App() {
  // 2.注册路由(无需声明,正确注册即可)
  const element = useRoutes([
    { path: '/home', element: <Home /> },
  ])
  return (
    <div>
      {/* 1.路由链接(携带参数):<Link to='/xx' state={{key:value}}></Link> */}
      <Link to='/home' state={{ age: 10, name: 'jack' }} >state参数传参</Link>
      {element}
    </div>
  );
}
function Home() {
  // 3.接受参数:const {state} = useLocation()
  const { state } = useLocation()
  return <div>名字{state.name}年龄{state.age}
    <Outlet />
  </div>
}
ReactDOM.render(<BrowserRouter><App /></BrowserRouter>, document.getElementById('root'))

三、react hooks

3.1

Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是自定义的 Hook 中,我们稍后会学习到。)

一个假设: 假设任何以 「use」 开头并紧跟着一个大写字母的函数就是一个 Hook

第一个只在: 只在 React 函数组件中调用 Hook,而不在普通函数中调用 Hook。(Eslint 通过判断一个方法是不是大坨峰命名来判断它是否是 React 函数)

第二个只在: 只在最顶层使用 Hook,而不要在循环,条件或嵌套函数中调用 Hook。

3.2为什么需要状态复用

Hook 的状态复用写法: Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React。

// 单个name的写法
const { name, setName } = useName();

// 梅开二度的写法
const { name : firstName, setName : setFirstName } = useName();

const { name : secondName, setName : setSecondName } = useName();
  1. 方法和属性好追溯吗?这可太好了,谁产生的,哪儿来的一目了然。
  2. 会有重名、覆盖问题吗?完全没有!内部的变量在闭包内,返回的变量支持定义别名。
  3. 多次使用,没开N度?你看上面的代码块内不就“梅开三度” 了吗?

当你调用useEffect 时,就是在告诉 React 在完成对 DOM 的更改后运行你的“副作用”函数。由于副作用函数是在组件内声明的,所以它们可以访问到组件的 props 和 state。

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);    return () => {      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);    };  });
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

除此之外,还有一些使用频率较低的但是很有用的 Hook。比如,useContext 让你不使用组件嵌套就可以订阅 React 的 Context。

function Example() {
  const locale = useContext(LocaleContext);  const theme = useContext(ThemeContext);  // ...
}

另外 useReducer 可以让你通过 reducer 来管理组件本地的复杂 state。

function Todos() {
  const [todos, dispatch] = useReducer(todosReducer);  // ...

React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。

四、面试题

JSX本质是什么

JSX 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖。 执行返回vnode

如下 JSX 代码:

<MyButton color="blue" shadowSize={2}>
  Click Me
</MyButton>

会编译为:

React.createElement(
  MyButton,
  {color: 'blue', shadowSize: 2},
  'Click Me'
)

context是什么,如何应用

父组件,向其下所有子孙组件传递信息,如公共信息
复杂的公共信息使用redux

shouldComponentUpdate

性能优化,配合不可变数据使用

redux单向数据流

image.png

image.png

函数组件和class的区别

函数组件纯函数,输入props,输出jsx
函数组件中没有this,没有state(函数组件用hook的useState),也没有生命周期,这就决定了函数组件都是展示性组件,接收props,渲染DOM,而不关注其他逻辑。
因为函数组件不需要考虑组件状态和组件生命周期方法中的各种比较校验,所以有很大的性能提升空间。

抽离组件公共逻辑

高阶组件 状态提升

redux如何执行异步请求

异步action 如:redux-thunk

react性能优化

  • 渲染列表时加key
  • 自定义事件、dom及时销毁
  • 合理使用异步组件
  • 减少函数bind this次数
  • 合理使用scu purecomponent 和memo
  • webpack层面优化
  • 前端通用性能优化

react和vue的区别

相同点:

  • 都支持组件化
  • 数据驱动视图
  • 使用vdom操作Dom 区别:
  • react使用jsx,vue使用模板
  • react是函数式编程,vue生命式编程
  • react更灵活