前端面试题之React

文章目录


1.React生命周期

  1. 生命周期

V16.3 之前

我们可以将生命周期分为三个阶段:

挂载阶段
组件更新阶段
卸载阶段
分开来讲:

挂载阶段

constructor:避免将 props 的值复制给 state
componentWillMount
render:react 最重要的步骤,创建虚拟 dom,进行 diff 算法,更新 dom 树都在此进行
componentDidMount

组件更新阶段

componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
render
componentDidUpdate

卸载阶段

componentWillUnMount
React的生命周期从广义上分为三个阶段:挂载、更新渲染、卸载。

新增后

即更新后的生命周期为:

挂载阶段

constructor
static getDerivedStateFromProps
render
componentDidMount

更新阶段
static getDerivedStateFromProps

该函数在挂载阶段和组件更新阶段都会执行,即每次获取新的props 或 state 之后都会被执行,在挂载阶段用来代替componentWillMount;在组件更新阶段配合 componentDidUpdate,可以覆盖 componentWillReceiveProps 的所有用法。

同时它是一个静态函数,所以函数体内不能访问 this,会根据 nextProps 和 prevState 计算出预期的状态改变,返回结果会被送给 setState,返回 null 则说明不需要更新 state,并且这个返回是必须的。

shouldComponentUpdate
render
getSnapshotBeforeUpdate

该函数会在 render 之后, DOM 更新前被调用,用于读取最新的 DOM 数据。

返回一个值,作为 componentDidUpdate 的第三个参数;配合 componentDidUpdate, 可以覆盖componentWillUpdate 的所有用法。

componentDidUpdate
卸载阶段

componentWillUnmount
在这里插入图片描述

初始化:

	** constructor()
	会在其装载之前被调用
	如果在函数内部使用的话,应该在开头使用super(props)
	否则props访问不到
	作用:
	初始化状态,通过赋值一个对象到this.state
	绑定事件处理函数到一个实例

挂载:

当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:
在这里插入图片描述

constructor(props)

在React组件挂载之前,会调用他的构造函数。在为 React.Component 子类实现构造函数时,应在其他语句之前前调用super(props)实现继承。否则,this.props 在构造函数中可能会出现未定义的 bug。

constructor(props) {
  super(props);//继承
  // 不要在这里调用 this.setState()
  this.state = { counter: 0 };
  this.handleClick = this.handleClick.bind(this);
}

static getDerivedStateFromProps(props, state)

代替componentWillReceiveProps()。

getDerivedStateFromProps 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容。

componentDidMount()

componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方。

这个方法是比较适合添加订阅的地方。如果添加了订阅,请不要忘记在 componentWillUnmount() 里取消订阅

你可以在 componentDidMount()直接调用 setState()。它将触发额外渲染,但此渲染会发生在浏览器更新屏幕之前。如此保证了即使在 render() 两次调用的情况下,用户也不会看到中间状态。请谨慎使用该模式,因为它会导致性能问题。通常,你应该在 constructor() 中初始化 state。如果你的渲染依赖于 DOM 节点的大小或位置,比如实现 modals 和 tooltips 等情况下,你可以使用此方式处理


更新:

当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:

在这里插入图片描述

shouldComponentUpdate()

默认是true,如果是false就直接卸载了。

getSnapshotBeforeUpdate()

代替componentWillUpdate

getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期的任何返回值将作为参数传递给 componentDidUpdate()

此用法并不常见,但它可能出现在 UI 处理中,如需要以特殊方式处理滚动位置的聊天线程等。

应返回 snapshot 的值(或 null)。

例如:

class ScrollingList extends React.Component {
  constructor(props) {
    super(props);
    this.listRef = React.createRef();
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // 我们是否在 list 中添加新的 items ?
    // 捕获滚动位置以便我们稍后调整滚动位置。
    if (prevProps.list.length < this.props.list.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // 如果我们 snapshot 有值,说明我们刚刚添加了新的 items,
    // 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。
    //(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.listRef}>{/* ...contents... */}</div>
    );
  }
}

在上述示例中,重点是从 getSnapshotBeforeUpdate 读取 scrollHeight 属性,因为 “render” 阶段生命周期(如 render)和 “commit” 阶段生命周期(如 getSnapshotBeforeUpdatecomponentDidUpdate)之间可能存在延迟。


componentDidUpdate()

componentDidUpdate() 会在更新后会被立即调用。首次渲染不会执行此方法。

当组件更新后,可以在此处对 DOM 进行操作。如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求。(例如,当 props 未发生变化时,则不会执行网络请求)。

componentDidUpdate(prevProps) {
  // 典型用法(不要忘记比较 props):
  if (this.props.userID !== prevProps.userID) {
    this.fetchData(this.props.userID);
  }
}

卸载:

当组件从 DOM 中移除时会调用如下方法:

componentWillUnmount() 会在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在componentDidMount() 中创建的订阅等。

componentWillUnmount()不应调用 setState(),因为该组件将永远不会重新渲染。组件实例卸载后,将永远不会再挂载它。

错误

componentDidCatch()

2.state和props的区别

主要区别在于 props 是不可变的
state 可以根据与用户交互来改变。

stateprops
可以从父组件中接收初始值可以从父组件中接收初始值
可以在组件中设置默认值可以在组件中设置默认值
可以设置子组件的初始值可以设置子组件的初始值
父组件不可以改变state值父组件可以改变props值
在组件的内部变化不在组件的内部变化
在组件内被组件自己管理的是传递给组件的(类似于函数的形参)

3.父子组件之间的通信

通过props传递

父组件

class App extends React.Component{
  render(){
    return(
      <div>
        <Children name="父向子"/>
      </div>
    )
  }
}

子组件

class Children extends Component{
  constructor(props){
    super(props);
  }
  render(){
    return(
      <div>这是:{this.props.name}</div> // 这是父向子
    )
  }
}

通过回调函数

class Father extends Component{
  constructor(props){
    super(props)
    this.state = {
      bgcolor:'pink'
    }
  }
  bgChange(color){
    this.setState({
      bgcolor:color
    })
  }
  render(props){
    <div style={{background:this.state.bgcolor}}>
            // 给子组件传递的值 color 
      <Children
      		 bgcolor={this.state.bgcolor} 
      		changeColor={(color)=>{this.bgChange(color)}}
       /> 
         // changeColor 子组件的参数=color 当做形参
    </div>
  }
}
class Children extends Component{
  constructor(props){
    super(props);
  }
  handerClick(){
    this.props.changeColor('skyblue') // 执行父组件的changeColor 并传参 必须和父组件中的函数一模一样
  }
  render(){
    return(
      <div>
        <div>父组件的背景色{this.props.bgcolor}</div> // 子组件接收父组件传过来的值 bgcolor
        <button onClick={(e)=>{this.handerClick(e)}}>改变父组件背景</button> // 子组件执行函数
      </div>
    )
  }
}

使用usecontext

const value = useContext(MyContext);

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>value prop 决定。

<MyContext.Provider> 这个对象是共享的

当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memoshouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。

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

const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);  return (    <button style={{ background: theme.background, color: theme.foreground }}>      I am styled by theme context!    </button>  );
}

4.类组件和函数组件的区别

函数组件类组件
没有生命周期要用hooks有生命周期
this不确定this确定指向自己
没有自己的state有自己的state
有更高的性能。当不需要使用生命周期钩子时,应该首先使用无状态函数组件内部使用 state,维护自身状态的变化,有状态组件根据外部组件传入的 props 和自身的 state进行渲染。
  • 函数组件的this不确定,没有生命周期要用hooks,没有state值
  • 类组件是一个类,是数据的方法的封装,有生命周期,有state
  • 组件内部状态且与外部无关的组件,可以考虑用类组件,这样状态树就不会过于复杂,易于理解和管理。
  • 当一个组件不需要管理自身状态时,也就是无状态组件,应该优先设计为函数组件。比如自定义的 、 等组件。

5.常用HOOK

useState

import React, { useState } from 'react';

function Example() {
  // 声明一个叫 "count" 的 state 变量  const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useEffect

import React, { useState, useEffect } from 'react';
function Example() {
  const [count, setCount] = useState(0);

  // Similar to componentDidMount and componentDidUpdate:  useEffect(() => {    // Update the document title using the browser API    document.title = `You clicked ${count} times`;  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

第二个参数

没有:每次渲染后都执行

空数组:只执行一次,相当于componentDidMount

一个值:这个值变化了就执行

多个值:有一个不相等就执行

useContext

如果想要组件之间共享状态

使用 Context ,首先顶层先声明 Provier 组件,并声明 value属性,接着在后代组件中声明 Consumer 组件,这个 Consumer子组件,只能是唯一的一个函数,函数参数即是 Context 的负载。如果有多个 Context ,Provider 和 Consumer任意的顺序嵌套即可。

import React,{useContext}  from 'react';
import './App.css';
//创建context
const numberContext = React.createContext();
//它返回一个具有两个值的对象
//{Provider , Consumer}
function App(){
//使用Provider为所有子孙提供value值
return (
<numberContext.Provider value={520}>
<div>
<ShowAn />
</div>
</numberContext.Provider>
)
}

function ShowAn(){
//使用Consumer从上下文获取value
//调用useContext,传入从React.createContext获取的上下文对象。
const value = useContext(numberContext);
return(
// <numberContext.Consumer>
// {value=><div>the answer is {value}</div>}
// </numberContext.Consumer>
<div>
the answer is {value}
</div>
)
}
export default App;

useReducer

第一个参数:reducer函数,没错就是我们上一篇文章介绍的。第二个参数:初始化的state。返回值为最新的state和dispatch函数(用来触发reducer函数,计算对应的state)。按照官方的说法:对于复杂的state操作逻辑,嵌套的state的对象,推荐使用useReducer。

实现登录

const initState = {
name: '',
pwd: '',
isLoading: false,
error: '',
isLoggedIn: false,
}
function loginReducer(state, action) {
switch(action.type) {
case 'login':
return {
...state,
isLoading: true,
error: '',
}
case 'success':
return {
 ...state,
isLoggedIn: true,
isLoading: false,
}
case 'error':
return {
...state,
error: action.payload.error,
name: '',
pwd: '',
isLoading: false,
}
default: 
return state;
}
}
function LoginPage() {
const [state, dispatch] = 
useReducer(loginReducer, initState);
const { name, pwd, 
isLoading, error, isLoggedIn } 
= state;
const login = (event) => {
event.preventDefault();
dispatch({ type: 'login' });
login({ name, pwd })
.then(() => {
dispatch({ type: 'success' });
})
.catch((error) => {
dispatch({
type: 'error'
payload: { error: error.message }
});
});
}
return ( 
//  返回页面JSX Element
  )
}

useCallback

和useMomo配对使用
需要传入两个参数

callback(仅仅是个函数),并把要做事情的函数放在callback函数体内执行
deps 要做事情的函数需要引入的外部参数或者是依赖参数

返回一个 memoized 回调函数。在依赖参数不变的情况下,返回的回调函数是同一个引用地址

useMemo

需要传入两个参数
callback(仅仅是个函数),并把要做事情的函数放在callback函数体内执行,(需要有返回值)
deps 要做事情的函数需要引入的外部参数或者是依赖参数

useMemo 的返回值
返回一个 memoized 值。在依赖参数不变的的情况返回的是上次第一次计算的值

usecallback和usemome一起使用写计算

import React, { FC, useCallback, useMemo, useState } from 'react';

const Index: FC = (props) => {
const [count, setCount] = useState(0);

const isEvenNumber = useMemo(() => {
    return count % 2 === 0;
  }, [count]);

const onClick = useCallback(() => {
setCount(count + 1);
}, [count]);

return (
<div>
  <div>{count} is {isEvenNumber ?
   'even':'odd'} number</div>
<button onClick={onClick}></button>
</div>
);
};

useCallback 和 useMemo 的参数都是一个函数加一个依赖数组,依赖没有改变时直接返回内存中缓存的结果,无需重复计算。简单理解就是 useCallback 缓存事件处理函数,useMemo 缓存二次计算的结果,如上面的点击事件,以及通过 count 值判断奇数偶数的二次计算结果。

6.react中更新了一个state或props时,生命周期函数调用的顺序

static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()

7.state什么时候是同步更新,什么时候是异步更新?

合成事件和钩子函数中是异步的

原生事件和setTimeout 中是同步的

先放个深层次的链接https://github.com/sisterAn/blog/issues/26
setState 并不是单纯同步/异步的,它的表现会因调用场景的不同而不同。在源码中,通过 isBatchingUpdates 来判断setState 是先存进 state 队列还是直接更新,如果值为 true 则执行异步操作,为 false 则直接更新。

  • 异步: 在 React 可以控制的地方,就为 true,比如在 React 生命周期事件和合成事件中,都会走合并操作,延迟更新的策略。
  • 同步: 在 React 无法控制的地方,比如原生事件,具体就是在 addEventListener 、setTimeout、setInterval 等事件中,就只能同步更新。

一般认为,做异步设计是为了性能优化、减少渲染次数:

  • setState设计为异步,可以显著的提升性能。如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后进行批量更新;
  • 如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步。state和props不能保持一致性,会在开发中产生很多的问题;

7-1 原生事件与合成事件

原生事件

  • 原生事件就是js的原生事件,如通过document.addEventListener来设置的监听事件。

  • 在react中即使有自己的一套事件机制(见下面合成事件),但有时候的业务场景我们仍然需要使用原生事件。比如我们封装一个Modal弹窗组件,需要在点击非弹窗区域时关掉弹窗,此时我们只能针对document进行原生点击事件监听。

  • 由于原生事件需要绑定在真实DOM上,所以一般是在componentDidMount阶段或者组件/元素的ref的函数执行阶段进行绑定操作,并且注意要在componentWillUnmount阶段进行解绑操作以避免内存泄漏。

合成事件

  • React有自己的一套事件机制,它重新封装了绝大部分的原生事件。合成事件采用了事件池,这样做可以大大节省内存,而不会频繁的创建和销毁事件对象。
  • 在React中,如果需要绑定事件,我们常常在jsx中这么写:
handleClick(){
}
<div onClick={this.handleClick.bind(this)}>
   react事件
</div>
  • 大致原理就是React并不是将click事件绑在该div的真实DOM上,而是在document处监听所有支持的事件,当事件发生并冒泡至document处时,React将事件内容封装并交由真正的处理函数运行。

在这里插入图片描述

合成事件的特点

1.React 上注册的事件最终会绑定在document这个 DOM 上,而不是 	  React 组件对应的 DOM(减少内存开销就是因为所有的事件都绑定在 document 上,其他节点没有绑定事件)
2.React 通过队列的形式,从触发的组件向父组件回溯,然后调用他们 JSX 中定义的 callback
3.React 通过对象池的形式管理合成事件对象的创建和销毁,减少了垃圾的生成和新对象内存的分配,提高了性能

合成事件和原生事件的顺序

  componentDidMount() {
    this.parent.addEventListener('click', (e) => {
      console.log('dom parent');
    })
    this.child.addEventListener('click', (e) => {
      console.log('dom child');
    })
    document.addEventListener('click', (e) => {
      console.log('document');
    })
  }

  childClick = (e) => {
    console.log('react child');
  }

  parentClick = (e) => {
    console.log('react parent');
  }

  render() {
    return (
      <div onClick={this.parentClick} ref={ref => this.parent = ref}>
        <div onClick={this.childClick} ref={ref => this.child = ref}>
          test
        </div>
      </div>)
  }


打印结果是
dom child
dom child
react child
react parent

顺序结论

无论是否是对于同一元素监听的同种类型事件,原生事件总是比合成事件先触发。
这是由于上面我们说到的合成事件最终都会绑定到documnet DOM上导致的,当合成事件监听到后,总是冒泡到document才会真正触发。而documnet DOM上监听的原生事件则总是最后触发

8.什么时候render

props变了或者state变了

react render渲染条件

点击Parent组件的div,触发更新,Son组件会打印child render!么?

function Son() {
  console.log('child render!');
  return <div>Son</div>;
}


function Parent(props) {
  const [count, setCount] = React.useState(0);

  return (
    <div onClick={() => {setCount(count + 1)}}>
      count:{count}
      {props.children}
    </div>
  );
}


function App() {
  return (
    <Parent>
      <Son/>
    </Parent>
  );
}

const rootEl = document.querySelector("#root");
ReactDOM.render(<App/>, rootEl);

render需要满足的条件

React创建Fiber树时,每个组件对应的fiber都是通过如下两个逻辑之一创建的:
render。即调用render函数,根据返回的JSX创建新的fiber。
bailout。即满足一定条件时,React判断该组件在更新前后没有发生变化,则复用该组件在上一次更新的fiber作为本次更新的fiber。
可以看到,当命中bailout逻辑时,是不会调用render函数的。
所以,Son组件不会打印child render!是因为命中了bailout逻辑。

bailout需要满足的条件

什么情况下会进入bailout逻辑?当同时满足如下4个条件时:

  1. oldProps === newProps ?

即本次更新的props(newProps)不等于上次更新的props(oldProps)。
注意这里是全等比较。
我们知道组件render会返回JSX,JSX是React.createElement的语法糖。
所以render的返回结果实际上是React.createElement的执行结果,即一个包含props属性的对象。
即使本次更新与上次更新props中每一项参数都没有变化,但是本次更新是React.createElement的执行结果,是一个全新的props引用,所以oldProps !== newProps。
如果我们使用了PureComponent或Memo,那么在判断是进入render还是bailout时,不会判断oldProps与newProps是否全等,而是会对props内每个属性进行浅比较。

  1. context没有变化

即context的value没有变化。

  1. workInProgress.type === current.type ?

更新前后fiber.type是否变化,比如div是否变为p。

  1. !includesSomeLane(renderLanes, updateLanes) ?

当前fiber上是否存在更新,如果存在那么更新的优先级是否和本次整棵fiber树调度的优先级一致?
如果一致则进入render逻辑。
就我们的Demo来说,Parent是整棵树中唯一能触发更新的组件(通过调用setCount)。
所以Parent对应的fiber是唯一满足条件4的fiber。

Demo的详细执行逻辑

所以,Demo中Son进入bailout逻辑,一定是同时满足以上4个条件。我们一个个来看。
条件2,Demo中没有用到context,满足。
条件3,更新前后type都为Son对应的函数组件,满足。
条件4,Son本身无法触发更新,满足。
所以,重点是条件1。让我们详细来看下。
本次更新开始时,Fiber树存在如下2个fiber

FiberRootNode
      |
  RootFiber

其中FiberRootNode是整个应用的根节点,RootFiber是调用ReactDOM.render创建的fiber。
首先,RootFiber会进入bailout的逻辑,所以返回的App fiber和更新前是一致的。

FiberRootNode
      |
  RootFiber      
      |
  App fiber

由于App fiber是RootFiber走bailout逻辑返回的,所以对于App fiber,oldProps === newProps。并且bailout剩下3个条件也满足。
所以App fiber也会走bailout逻辑,返回Parent fiber。

FiberRootNode
      |
  RootFiber      
      |
   App fiber
      |
 Parent fiber

由于更新是Parent fiber触发的,所以他不满足条件4,会走render的逻辑。
接下来是关键
如果render返回的Son是如下形式:

<Son/>

会编译为

React.createElement(Son, null)

执行后返回JSX。
由于props的引用改变,oldProps !== newProps。会走render逻辑。
但是在Demo中Son是如下形式:

{props.children}

其中,props.children是Son对应的JSX,而这里的props是App fiber走bailout逻辑后返回的。
所以Son对应的JSX与上次更新时一致,JSX中保存的props也就一致,满足条件1。
可以看到,Son满足bailout的所有条件,所以不会render。

9.生命周期

1.组件生命周期的执行次数

1.只执行一次: constructor、componentDidMount
2.执行多次:render、getDerivedStateFromProps、shouldComponentUpdate、getSnapshotBeforeUpdate、componentDidUpdate
3.有条件执行:componentWillUnmount

2.组件的生命周期执行顺序

假设组件嵌套关系parent组件中有child组件

不更新dom执行顺序如下

img

1602223811(1).jpg

更新dom执行顺序如下

img

1602213835(1).jpg

修改父组件的state

img

1602214634(1).jpg

修改子组件的state

img

1602214666(1).jpg

3. 结论

1.完成前的顺序是从根部到子部,完成时是从子部到根部。(类似于事件机制)
2.子组件setState是不能触发其父组件的生命周期更新函数,只能触发更低一级别的生命周期更新函数。

4.多个组件的执行顺序

1. 父子组件

挂载阶段

分两个阶段:

第 一 阶段,由父组件开始执行到自身的 render,解析其下有哪些子组件需要渲染,并对其中 同步的子组件 进行创建,按 递归顺序 挨个执行各个子组件至 render,生成到父子组件对应的 Virtual DOM 树,并 commit 到 DOM。
第 二 阶段,此时 DOM 节点已经生成完毕,组件挂载完成,开始后续流程。先依次触发同步子组件各自的 componentDidMount,最后触发父组件的。
注意:如果父组件中包含异步子组件,则会在父组件挂载完成后被创建。

所以执行顺序是:

父组件 getDerivedStateFromProps —>
同步子组件 getDerivedStateFromProps —> 同步子组件 componentDidMount —>
父组件 componentDidMount —>
异步子组件 getDerivedStateFromProps —> 异步子组件 componentDidMount

更新阶段

React 的设计遵循单向数据流模型 ,也就是说,数据均是由父组件流向子组件。

第 一 阶段,由父组件开始,执行

static getDerivedStateFromProps
shouldComponentUpdate
更新到自身的 render,解析其下有哪些子组件需要渲染,并对 子组件 进行创建,按 递归顺序 挨个执行各个子组件至 render,生成到父子组件对应的 Virtual DOM 树,并与已有的 Virtual DOM 树 比较,计算出 Virtual DOM 真正变化的部分 ,并只针对该部分进行的原生DOM操作。

第 二 阶段,此时 DOM 节点已经生成完毕,组件挂载完成,开始后续流程。先依次触发同步子组件以下函数,最后触发父组件的。

getSnapshotBeforeUpdate()
componentDidUpdate()
React 会按照上面的顺序依次执行这些函数,每个函数都是各个子组件的先执行,然后才是父组件的执行。

所以执行顺序是:

父组件 getDerivedStateFromProps —>
父组件 shouldComponentUpdate —>
子组件 getDerivedStateFromProps —>
子组件 shouldComponentUpdate —>
子组件 getSnapshotBeforeUpdate —>
父组件 getSnapshotBeforeUpdate —>
子组件 componentDidUpdate —>
父组件 componentDidUpdate

卸载阶段

componentWillUnmount(),顺序为 父组件的先执行,子组件按照在 JSX 中定义的顺序依次执行各自的方法。

注意 :如果卸载旧组件的同时伴随有新组件的创建,新组件会先被创建并执行完 render,然后卸载不需要的旧组件,最后新组件执行挂载完成的回调。

2. 兄弟组件

挂载阶段

若是同步路由,它们的创建顺序和其在共同父组件中定义的先后顺序是 一致 的。

若是异步路由,它们的创建顺序和 js 加载完成的顺序一致。

更新阶段、卸载阶段

兄弟节点之间的通信主要是经过父组件(Redux 和 Context 也是通过改变父组件传递下来的 props 实现的),满足React 的设计遵循单向数据流模型, 因此任何两个组件之间的通信,本质上都可以归结为父子组件更新的情况 。

所以,兄弟组件更新、卸载阶段,请参考 父子组件。

10.setState在生命周期中的使用注意事项

setstate 回调函数

this.setState({
          commentDlg: false,
          defVal: ''
}, () => {
          Toast.fail('c_set_fail');
 });

1、getDerivedStateFromProps

仅当子组件的props发生变化时getDerivedStateFromProps生命钩子才会被触发。该生命周期会有一个参数nextProps,表示子组件被更新后的props。因此可以在该周期获取最新的props在通过setState更新组件状态。

2、shouldComponentUpdate

子组件props或state更新都会触发shouldComponentUpdate生命钩子。该生命周期有两个参数nextProps,nextState 表示更新后的props和更新后的state, 该生命周期是整提高组件性能的一个重要函数,它通过判断当前状态与之前状态来返回一个布尔值并决定是否更新视图,如果返回false视图始终不会更新。返回true就会更新视图

3、getSnapshotBeforeUpdate

getSnapshotBeforeUpdate生命周期在shouldComponentUpdate返回true后被触发。在这两个生命周期只要视图更新就会触发,因此不能再这两个生命周期中使用setState。否则会导致死循环。

4、componentDidUpdate

componentDidUpdate生命周期在shouldComponentUpdate返回true后触发。在此生命周期中setState会导致视图再次更新,类似于componentDidMount,因此除非特殊业务需求,否则不建议在此生命周期中使用setState。

5、componentWillUnmount

componentWillUnmount生命周期在组件被卸载后触发,在此生命周期使用setState不会触发。

11.虚拟DOM

什么是虚拟DOM

就是虚拟的DOM节点,通过JS的Object对象模拟DOM中的节点,然后再通过特定的render()方法将其渲染成真实的DOM节点

为什么要用虚拟的DOM?

因为真实的DOM元素非常庞大,性能开销会很大。

真实DOM转换成虚拟DOM

虚拟DOM就是普通的js对象,包括tag,props,children三个属性。

与diff差分算法结合的优点

页面渲染的时候,计算出Virtual DOM真正变化的部分,并只针对该部分进行的原生DOM操作,而不是渲染整个页面,从而保证了每次操作后,页面的高效渲染

12.diff差分算法 规则就是同层比较

传统的diff算法

计算一个树形结构转换成另一个树形结构的最少操作,是一个复杂且值得研究的问题,传统 diff 算法通过循环递归的方法对节点进行操作,算法复杂度 为O(n^3),其中n为树中节点的总数,这效率太低了,如果 React 只是单纯的引入 diff 算法,而没有任何的优化的话,其效率远远无法满足前端渲染所需要的性能。那么React 是如何实现一个高效、稳定的 diff 算法。

React中的diff算法

React 将 Virtual DOM 树转换为 actual DOM 树的最小操作的过程称为调和, diff 算法便是调和的结果,React 通过制定大胆的策略,将 O(n^3)的时间复杂度转换成 O(n)

diff策略

下面是 React diff 算法的 3 个策略:

策略一:Web UI 中 DOM 节点跨层级的移动操作特别少。可以忽略不计。
策略二:拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
策略三:对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。
基于以上三个策略,React 分别对 tree diff、component diff 以及 element diff 进行算法优化。

tree diff

对于策略一,React 对树的算法进行了简单明了的优化,即对树进行分层比较,两颗树只会对同一层级的节点进行比较。

既然 DOM 节点跨层级的移动,可以少到忽略不计,针对这种现象,React 通过 updateDepth 对 Virtual DOM 树进行层级控制,只对相同层级的DOM节点进行比较,即同一父节点下的所有子节点,当发现该节点已经不存在时,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个DOM树的比较。
DOM 节点跨层级的移动操作,diff 会有怎样的表现喃?
在这里插入图片描述

由于 React 只会简单的考虑同层级节点的位置变换,对于不同层级的节点,只有创建和删除操作。当根节点R发现子节点中A消失了,就会直接销毁A;当D节点发现多了一个子节点A,就会创建新的A子节点(包括其子节点)。执行的操作为:

create A —> create B —> create C —> delete A

所以。当出现节点跨级移动时,并不会像想象中的那样执行移动操作,而是以 A 为根节点的整个树被整个重新创建,这是影响 React 性能的操作,所以 官方建议不要进行 DOM 节点跨层级的操作。 在开发组件中,保持稳定的 DOM 结构有助于性能的提升。例如,可以通过CSS隐藏或显示节点,而不是真正的移除或添加 DOM 节点。

component diff

  • 如果是同一类型的组件,即同一父节点下的所有子节点,当发现该节点已经不存在时,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个DOM树的比较。
  • 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点
  • 对于同一类型下的组件,有可能其 Virtual DOM 没有任何变化,如果能确切知道这一点,那么就可以节省大量的 diff 算法时间。因此, React 允许用户通过 shouldComponentUpdate()来判断该组件是否需要大量 diff 算法分析。
  • 在这里插入图片描述
    如上图3-1,当 D 组件变成 G 时,即使这两个组件结构相似,但一旦 React 判断D和G是两个不同类型的组件时,就不会再比较这两个组件的结构,直接进行删除组件D, 重新创建组件G及其子组件。虽然这两个组件是不同类型单结构类似,diff 算法会影响性能,正如 React 官方博客所言:

不同类型的组件很少存在相似 DOM 树的情况,因此,这种极端因素很难在实际开发过程中造成重大影响。

element diff

当节点处于同一层级时,diff 提供三种节点操作:

  • INSERT_MARKUP(插入):如果新的组件类型不在旧集合里,即全新的节点,需要对新节点执行插入操作。
  • MOVE_EXISTING (移动):旧集合中有新组件类型,且 element 是可更新的类型,generatorComponentChildren 已调用 receiveComponent,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点。
  • REMOVE_NODE (删除):旧组件类型,在新集合里也有,但对应的 elememt 不同则不能直接复用和更新,需要执行删除操作,或者旧组件不在新集合里的,也需要执行删除操作。
    允许开发者对同一层级的同组子节点,添加唯一key进行区分,虽然只是小小的改动,但性能上却发生了翻天覆地的变化。
    在这里插入图片描述

多种情况

1.	当节点类型相同时,去看一下属性是否相同 产生一个属性的补丁包 {type:'ATTRS', attrs: {class: 'list-group'}}
2.新的dom节点不存在 {type: 'REMOVE', index: xxx}
3.节点类型不相同 直接采用替换模式 {type: 'REPLACE', newNode: newNode}
4.文本的变化:{type: 'TEXT', text: 1}

在某一时间节点调用 React 的 render() 方法,会创建一棵由 React 元素组成的树。在下一次 state 或 props 更新时,相同的 render() 方法会返回一棵不同的树。React 需要基于这两棵树之间的差别来判断如何高效的更新 UI,以保证当前 UI 与最新的树保持同步。

此算法有一些通用的解决方案,即生成将一棵树转换成另一棵树的最小操作次数。然而,即使使用最优的算法,该算法的复杂程度仍为 O(n 3 ),其中 n 是树中元素的数量。

如果在 React 中使用该算法,那么展示 1000 个元素则需要 10 亿次的比较。这个开销实在是太过高昂。于是 React 在以下两个假设的基础之上提出了一套 O(n) 的启发式算法:

  1. 两个不同类型的元素会产生出不同的树;
  2. 开发者可以通过设置 key 属性,来告知渲染哪些子元素在不同的渲染下可以保存不变;

在实践中,我们发现以上假设在几乎所有实用的场景下都成立。

Diffing 算法

当对比两棵树时,React 首先比较两棵树的根节点。不同类型的根节点元素会有不同的形态。

对比不同类型的元素

当根节点为不同类型的元素时,React 会拆卸原有的树并且建立起新的树。举个例子,当一个元素从 <a> 变成 <img>,从 <Article> 变成 <Comment>,或从 <Button> 变成 <div> 都会触发一个完整的重建流程。

当卸载一棵树时,对应的 DOM 节点也会被销毁。组件实例将执行 componentWillUnmount() 方法。当建立一棵新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中。组件实例将执行 UNSAFE_componentWillMount() 方法,紧接着 componentDidMount() 方法。所有与之前的树相关联的 state 也会被销毁。

在根节点以下的组件也会被卸载,它们的状态会被销毁。比如,当比对以下更变时:

<div>
  <Counter />
</div>

<span>
  <Counter />
</span>

React 会销毁 Counter 组件并且重新装载一个新的组件。

注意:

这些方法被认为是过时的,在新的代码中应该避免使用它们

  • UNSAFE_componentWillMount()

对比同一类型的元素

当对比两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性。比如:

<div className="before" title="stuff" />

<div className="after" title="stuff" />

通过对比这两个元素,React 知道只需要修改 DOM 元素上的 className 属性。

当更新 style 属性时,React 仅更新有所更变的属性。比如:

<div style={{color: 'red', fontWeight: 'bold'}} />

<div style={{color: 'green', fontWeight: 'bold'}} />

通过对比这两个元素,React 知道只需要修改 DOM 元素上的 color 样式,无需修改 fontWeight

在处理完当前节点之后,React 继续对子节点进行递归。

对比同类型的组件元素

当一个组件更新时,组件实例会保持不变,因此可以在不同的渲染时保持 state 一致。React 将更新该组件实例的 props 以保证与最新的元素保持一致,并且调用该实例的UNSAFE_componentWillReceiveProps()UNSAFE_componentWillUpdate() 以及 componentDidUpdate()方法。

下一步,调用 render() 方法,diff 算法将在之前的结果以及新的结果中进行递归。

注意:

这些方法已过时,在新代码中应避免使用它们

  • UNSAFE_componentWillUpdate()
  • UNSAFE_componentWillReceiveProps()

对子节点进行递归

默认情况下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation。

在子元素列表末尾新增元素时,更新开销比较小。比如:

<ul>
  <li>first</li>
  <li>second</li>
</ul>

<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

React 会先匹配两个 <li>first</li> 对应的树,然后匹配第二个元素 <li>second</li> 对应的树,最后插入第三个元素的 <li>third</li> 树。

如果只是简单的将新增元素插入到表头,那么更新开销会比较大。比如:

<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

React 并不会意识到应该保留 <li>Duke</li><li>Villanova</li>,而是会重建每一个子元素。这种情况会带来性能问题。

Keys

为了解决上述问题,React 引入了 key 属性。当子元素拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素。以下示例在新增 key 之后,使得树的转换效率得以提高:

<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

现在 React 知道只有带着 '2014' key 的元素是新元素,带着 '2015' 以及 '2016' key 的元素仅仅移动了。

实际开发中,编写一个 key 并不困难。你要展现的元素可能已经有了一个唯一 ID,于是 key 可以直接从你的数据中提取:

<li key={item.id}>{item.name}</li>

当以上情况不成立时,你可以新增一个 ID 字段到你的模型中,或者利用一部分内容作为哈希值来生成一个 key。这个 key 不需要全局唯一,但在列表中需要保持唯一。

最后,你也可以使用元素在数组中的下标作为 key。这个策略在元素不进行重新排序时比较合适,如果有顺序修改,diff 就会变慢。

当基于下标的组件进行重新排序时,组件 state 可能会遇到一些问题。由于组件实例是基于它们的 key 来决定是否更新以及复用,如果 key 是一个下标,那么修改顺序时会修改当前的 key,导致非受控组件的 state(比如输入框)可能相互篡改,会出现无法预期的变动。

在 Codepen 有两个例子,分别为 展示使用下标作为 key 时导致的问题,以及不使用下标作为 key 的例子的版本,修复了重新排列,排序,以及在列表头插入的问题

权衡

请谨记协调算法是一个实现细节。React 可以在每个 action 之后对整个应用进行重新渲染,得到的最终结果也会是一样的。在此情境下,重新渲染表示在所有组件内调用 render 方法,这不代表 React 会卸载或装载它们。React 只会基于以上提到的规则来决定如何进行差异的合并。

我们定期优化启发式算法,让常见用例更高效地执行。在当前的实现中,可以理解为一棵子树能在其兄弟之间移动,但不能移动到其他位置。在这种情况下,算法会重新渲染整棵子树。

由于 React 依赖启发式算法,因此当以下假设没有得到满足,性能会有所损耗。

  1. 该算法不会尝试匹配不同组件类型的子树。如果你发现你在两种不同类型的组件中切换,但输出非常相似的内容,建议把它们改成同一类型。在实践中,我们没有遇到这类问题。
  2. Key 应该具有稳定,可预测,以及列表内唯一的特质。不稳定的 key(比如通过 Math.random() 生成的)会导致许多组件实例和 DOM 节点被不必要地重新创建,这可能导致性能下降和子组件中的状态丢失。

key 是如何工作的?

**key相同,若组件属性有所变化,则react只更新组件对应的属性;没有变化则不更新。
 key值不同,则react先销毁该组件(有状态组件的componentWillUnmount会执行),
然后重新创建该组件(有状态组件的constructor和componentWillUnmount都会执行)**

13.react如何将组件内部的方法暴露给外部

我有一个Toast组件,在外部需要调用它的show方法来控制他的显隐状态。
之前我的写法是写一个静态类方法,然后在constructor中去修改它的作用域,代码如下

// @flow
import React from 'react';
import './style.less';

type Props={
    time:number,
    text:string,
}

export default class Toast extends React.Component {
    constructor(props:Props) {
        super(props);
        this.state = {
            isShow: false,
            text: '',
        };
        Toast.show = this.show.bind(this); // 最重要的一步!!
    }
    static  show({ text }) {
        const _self = this;
        this.setState({
            isShow: true,
            text,
        }, () => {
            _self.timer = setTimeout(() => {
                this.setState({
                    isShow: false,
                });
            }, this.props.time);
        });
    }



    componentWillUnmount() {
        this.setState({
            isShow: false,
        });
        clearInterval(this.timer);
    }

    render() {
        const { isShow, text } = this.state;
        return (
            <div className="toast-wrapper">
                {isShow && <div className="toast">{text}</div>}
            </div>
        );
    }
}

13.ReactNavigation

常用方法

navigation.navigate()

navigation.push()

` this.props.navigation.dispatch(
CommonActions.navigate({
name: 'NASNetworkLocation',
  params: {deviceMessage: params, NASLocation: value, pageType:'select'}
})
 )
this.props.navigation.reset({
index: 1,
routes: [
 {
name: 'NASNetworkLocation',
params: {  deviceMessage: params, NASLocation: value },
},
],
})
`

从a页面,到b页面,然后b页面给页面数据 这里是在跳转的时候带一个callback回调函数 执行顺序

 //这是跳过去的时候,有一个callback
 this.props.navigation.navigate('FaceCamera'
                , {
                    callback: (data) => {
                        this.setState({
                            commentDlg: true,
                            unMarkfaceUrl: data.faceUrl.uri,
                            unMarkfaceId: data.faceId
                        })
                    }
                }
            )
  //然后传回来的时候 前面的页面就可以接收到
   state.params.callback(
              {
                faceId: data.data.faceInfoMetas[0].faceId,
                faceUrl: this.state.uploadUri
              }
            );

14.简述Redux

1.什么时候用

某个组件的状态需要共享

某个状态需要在任何地方都可以拿到

一个组件改变需要改变全局状态

一个组件需要改变另一个组件的状态

2.store

Store就是保存数据的地方,整个应用只有一个Store

用createStore这个函数来生成store

import { createStore } from 'redux';
const store = createStore(fn);

3.state

当前时刻的 State,可以通过store.getState()拿到。

import { createStore } from 'redux';
const store = createStore(fn);

const state = store.getState();

Redux 规定, 一个 State 对应一个 View。只要 State 相同,View 就相同。你知道 State,就知道 View 是什么样,反之亦然。

4.Action

State 的变化,会导致 View 的变化。但是,用户接触不到 State,只能接触到 View。所以,State 的变化必须是 View 导致的。Action 就是 View 发出的通知,表示 State 应该要发生变化了。

Action 是一个对象。其中的type属性是必须的,表示 Action 的名称。其他属性可以自由设置,

const action = {
  type: 'ADD_TODO',
  payload: 'Learn Redux'
};

5.Action Creator

View 要发送多少种消息,就会有多少种 Action。

const ADD_TODO = '添加 TODO';

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

const action = addTodo('Learn Redux');

6.store.dispatch()

store.dispatch()是 View 发出 Action 的唯一方法。

import { createStore } from 'redux';
const store = createStore(fn);

store.dispatch({
  type: 'ADD_TODO',
  payload: 'Learn Redux'
});

上面代码中,store.dispatch接受一个 Action 对象作为参数,将它发送出去。

结合 Action Creator,这段代码可以改写如下。

store.dispatch(addTodo('Learn Redux'));

7.Reducer

Store 收到 Action 以后,必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。

Reducer 是一个函数,它接受 Action 和当前 State 作为参数,返回一个新的 State。

const reducer = function (state, action) {
  // ...
  return new_state;
};

整个应用的初始状态,可以作为 State 的默认值。下面是一个实际的例子。

const defaultState = 0;
const reducer = (state = defaultState, action) => {
  switch (action.type) {
    case 'ADD':
      return state + action.payload;
    default: 
      return state;
  }
};

const state = reducer(1, {
  type: 'ADD',
  payload: 2
});

上面代码中,reducer函数收到名为ADD的 Action 以后,就返回一个新的 State,作为加法的计算结果。其他运算的逻辑(比如减法),也可以根据 Action 的不同来实现。

实际应用中,Reducer 函数不用像上面这样手动调用,store.dispatch方法会触发 Reducer 的自动执行。为此,Store 需要知道 Reducer 函数,做法就是在生成 Store 的时候,将 Reducer 传入createStore方法。

import { createStore } from 'redux';
const store = createStore(reducer);

上面代码中,createStore接受 Reducer 作为参数,生成一个新的 Store。以后每当store.dispatch发送过来一个新的 Action,就会自动调用 Reducer,得到新的 State。

为什么这个函数叫做 Reducer 呢?因为它可以作为数组的reduce方法的参数。请看下面的例子,一系列 Action 对象按照顺序作为一个数组。

const actions = [
  { type: 'ADD', payload: 0 },
  { type: 'ADD', payload: 1 },
  { type: 'ADD', payload: 2 }
];

const total = actions.reduce(reducer, 0); // 3

上面代码中,数组actions表示依次有三个 Action,分别是加0、加1和加2。数组的reduce方法接受 Reducer 函数作为参数,就可以直接得到最终的状态3

Reducer 函数最重要的特征是,它是一个纯函数。也就是说,只要是同样的输入,必定得到同样的输出。

纯函数是函数式编程的概念,必须遵守以下一些约束。

  • 不得改写参数
  • 不能调用系统 I/O 的API
  • 不能调用Date.now()或者Math.random()等不纯的方法,因为每次会得到不一样的结果

由于 Reducer 是纯函数,就可以保证同样的State,必定得到同样的 View。但也正因为这一点,Reducer 函数里面不能改变 State,必须返回一个全新的对象,请参考下面的写法。

// State 是一个对象
function reducer(state, action) {
  return Object.assign({}, state, { thingToChange });
  // 或者
  return { ...state, ...newState };
}

// State 是一个数组
function reducer(state, action) {
  return [...state, newItem];
}

最好把 State 对象设成只读。你没法改变它,要得到新的 State,唯一办法就是生成一个新对象。这样的好处是,任何时候,与某个 View 对应的 State 总是一个不变的对象。

15.react中从a页面跳到b页面,取消a中的一些监听事件

16.ref的使用

可以用ref来获取某个子节点的实例,然后通过当前class组件实例的一些特定属性来直接获取子节点实例。ref有三种实现方法:

  • 字符串格式:字符串格式,这是React16版本之前用得最多的,例如:

    span

  • this.refs.info
  • 函数格式:ref对应一个方法,该方法有一个参数,也就是对应的节点实例,例如:<p ref={ele => this.info = ele}>
  • createRef方法:React 16提供的一个API,使用React.createRef()来实现
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }
  render() {
    return <div ref={this.myRef} />;
  }
}
  • 注意
  • 不可以在render阶段访问refs,因为DOM还没有生成。

React遗留问题

  • redux实现计算器
  • vue和react的区别

关于key的解释上个链接算了

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值