高效学习三部曲:React

210 阅读9分钟

生命周期

不同版本生命周期网站

  1. 挂载:

    • constructor
    • componentWillMount()
    • getDerivedStateFromProps(nextProps, prevState):挂载阶段有,更新阶段也有
    • componentDidMount()
  2. 更新:

    • componentWillReceiveProps(nextProps)=getDerivedStateFromProps(nextProps, prevState)
      componentWillReceiveProps (nextProps) {
           nextProps.openNotice !== this.props.openNotice&&this.setState({
               openNotice:nextProps.openNotice
           },() => {
             console.log(this.state.openNotice:nextProps)
             //将state更新为nextProps,在setState的第二个参数(回调)可以打         印出新的state
         })
       }
    
      static getDerivedStateFromProps(nextProps, prevState) {
         if (nextProps.isLogin !== prevState.isLogin) {
           return {
             isLogin: nextProps.isLogin,
           };
         }
         return null;
       }
    
       componentDidUpdate(prevProps, prevState) {
         if (!prevState.isLogin && this.props.isLogin) {
           this.handleClose();
         }
       }
    
    • shouldComponentUpdate(nextProps,nextState)
    • componentWillUpdate(nextProps,nextState)=getSnapshotBeforeUpdate(prevProps, prevState)
    • componentDidUpdate(prevProps,prevState)
  3. 卸载:

    • componentWillUnMount()

setState

微信截图_20210318111710.png
图片来源:React 渲染优化:diff 与 shouldComponentUpdate

  • 不可变值
    1. 不能直接修改this.state里的值,不会触发render更新
  • 可能是异步更新
// 第三,setState 可能是异步更新(有可能是同步更新) ----------------------------

this.setState({
    count: this.state.count + 1
}, () => {
    // 联想 Vue $nextTick - DOM
    console.log('count by callback', this.state.count) // 回调函数中可以拿到最新的 state
})
console.log('count', this.state.count) // 异步的,拿不到最新值
// // setTimeout 中 setState 是同步的
setTimeout(() => {
    this.setState({
        count: this.state.count + 1
    })
    console.log('count in setTimeout', this.state.count)
}, 0)
// 自己定义的 DOM 事件,setState 是同步的。在 componentDidMount 中
componentDidMount() {
    // 自己定义的 DOM 事件,setState 是同步的
    document.body.addEventListener('click', this.bodyClickHandler)
}

componentWillUnmount() {
    // 及时销毁自定义 DOM 事件
    document.body.removeEventListener('click', this.bodyClickHandler)
    // clearTimeout
}
  • 可能会被合并
// 第四,state 异步更新的话,更新前会被合并 ----------------------------
        
        // // 传入对象,会被合并(类似 Object.assign )。执行结果只一次 +1
        this.setState({
            count: this.state.count + 1
        })
        this.setState({
            count: this.state.count + 1
        })
        this.setState({
            count: this.state.count + 1
        })
// 传入函数,不会被合并。执行结果是 +3
this.setState((prevState, props) => {
    return {
        count: prevState.count + 1
    }
})
this.setState((prevState, props) => {
    return {
        count: prevState.count + 1
    }
})
this.setState((prevState, props) => {
    return {
        count: prevState.count + 1
    }
})
}
  • 函数组件默认没有state

  • React 组件如何通讯

React组件中的this

  1. React组件方法中为什么要绑定this
  2. [译] 为什么需要在 React 类组件中为事件处理程序绑定 this

React事件

  • bind this
  • 关于event参数
  • 传递自定义参数
// 获取 event
clickHandler3 = (event) => {
    event.preventDefault() // 阻止默认行为
    event.stopPropagation() // 阻止冒泡
    console.log('target', event.target) // 指向当前元素,即当前元素触发
    console.log('current target', event.currentTarget) // 指向当前元素,假象!!!

    // 注意,event 其实是 React 封装的。可以看 __proto__.constructor 是 SyntheticEvent 组合事件
    console.log('event', event) // 不是原生的 Event ,原生的 MouseEvent
    console.log('event.__proto__.constructor', event.__proto__.constructor)

    // 原生 event 如下。其 __proto__.constructor 是 MouseEvent
    console.log('nativeEvent', event.nativeEvent)
    console.log('nativeEvent target', event.nativeEvent.target)  // 指向当前元素,即当前元素触发
    console.log('nativeEvent current target', event.nativeEvent.currentTarget) // 指向 document !!!

    // 1. event 是 SyntheticEvent ,模拟出来 DOM 事件所有能力
    // 2. event.nativeEvent 是原生事件对象
    // 3. 所有的事件,都被挂载到 document 上
    // 4. 和 DOM 事件不一样,和 Vue 事件也不一样
}

React 表单

  • 受控组件 组件中的值受this.state里控制:
  1. input textarea select用value
  2. checkbox radio 用 checked

props

  • props传递数据

  • props传递函数

  • props 类型检查

  • JSX本质是什么

  • context是什么,有何用途?

  • shouldComponentUpdate

  • 描述redux单项数据流

  • setState是同步还是异步

01.png

高级特新

函数组件

  • 纯函数,输入props,输出JSX
  • 没有实例,没有生命周期,没有state
  • 不能扩展其他方法

非受控组件

组件的值不是由this.state控制

使用场景:

  • 必须手动操作DOM元素,setState实现不了

  • 文件上床

  • 某些富文本编辑器,需要传入DOM元素

  • ref:

constructor(props) {
    super(props)
    this.state = {}
    this.nameInputRef = React.createRef() // 创建 ref
}
class App extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            name: '双越',
            flag: true,
        }
        this.nameInputRef = React.createRef() // 创建 ref
    }
    render() {
        // input defaultValue
        return <div>
            {/* 使用 defaultValue 而不是 value ,使用 ref */}
            <input defaultValue={this.state.name} ref={this.nameInputRef}/>
            {/* state 并不会随着改变 */}
            <span>state.name: {this.state.name}</span>
            <br/>
            <button onClick={this.alertName}>alert name</button>
        </div>

        // checkbox defaultChecked
        return <div>
            <input
                type="checkbox"
                defaultChecked={this.state.flag}
            />
        </div>

        // file
        return <div>
            <input type="file" ref={this.fileInputRef}/>
            <button onClick={this.alertFile}>alert file</button>
        </div>

    }
    alertName = () => {
        const elem = this.nameInputRef.current // 通过 ref 获取 DOM 节点
        alert(elem.value) // 不是 this.state.name
    }
    alertFile = () => {
        const elem = this.fileInputRef.current // 通过 ref 获取 DOM 节点
        alert(elem.files[0].name)
    }
}
  • defaultValue、defaulChecked
  • 手动操作DOM元素

Portals

让组件渲染到父组件以外

使用场景:

  • overflow: hidden
  • 父组件z-index值太小
  • fixed需要放在body第一层级
render() {
    // // 正常渲染
    // return <div className="modal">
    //     {this.props.children} {/* vue slot */}
    // </div>

    // 使用 Portals 渲染到 body 上。
    // fixed 元素要放在 body 上,有更好的浏览器兼容性。
    return ReactDOM.createPortal(
        <div className="modal">{this.props.children}</div>,
        document.body // DOM 节点
    )
}

context

应用场景:

  • 公共信息(语言、主题)如何传递给每个组件
  • 用props太繁琐
  • 用redux 小题大做

生产数据、消费数据(class组件和函数组件使用方式不一样)

import React from 'react'

// 创建 Context 填入默认值(任何一个 js 变量)
const ThemeContext = React.createContext('light')

// 底层组件 - 函数是组件
function ThemeLink (props) {
    // const theme = this.context // 会报错。函数式组件没有实例,即没有 this

    // 函数式组件可以使用 Consumer
    return <ThemeContext.Consumer>
        { value => <p>link's theme is {value}</p> }
    </ThemeContext.Consumer>
}

// 底层组件 - class 组件
class ThemedButton extends React.Component {
    // 指定 contextType 读取当前的 theme context。
    // static contextType = ThemeContext // 也可以用 ThemedButton.contextType = ThemeContext
    render() {
        const theme = this.context // React 会往上找到最近的 theme Provider,然后使用它的值。
        return <div>
            <p>button's theme is {theme}</p>
        </div>
    }
}
ThemedButton.contextType = ThemeContext // 指定 contextType 读取当前的 theme context。

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar(props) {
    return (
        <div>
            <ThemedButton />
            <ThemeLink />
        </div>
    )
}

class App extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            theme: 'light'
        }
    }
    render() {
        return <ThemeContext.Provider value={this.state.theme}>
            <Toolbar />
            <hr/>
            <button onClick={this.changeTheme}>change theme</button>
        </ThemeContext.Provider>
    }
    changeTheme = () => {
        this.setState({
            theme: this.state.theme === 'light' ? 'dark' : 'light'
        })
    }
}

export default App

异步组件

  • import()
  • React.lazy
  • React.Suspense
const ContextDemo = React.lazy(() => import('./ContextDemo'))

<React.Suspense fallback={<div>Loading...</div>}>
    <ContextDemo/>
</React.Suspense>

性能优化

性能优化对React更重要

  • shouldComponentUpdate React默认:父组件有更新,子组件则无条件也更新

  • PureComponent和React.memo

    1. PureComponent和React,SCU中实现了浅比较
    2. memo,函数组件中的PureComponent
    3. 浅比较已使用大部分情况(尽量不做深比较)
  • 不可变值immutable.js

    1. 彻底拥抱不可变值
    2. 基于共享数据(不是深拷贝)
const map1 = Immutable.Map({a: 1, b: 2, c: 3})
const map2 = map1.set('b', 50)
map1.get('b') //2
map2.get('b') //50
// 增加 shouldComponentUpdate
shouldComponentUpdate(nextProps, nextState) {
    // _.isEqual 做对象或者数组的深度比较(一次性递归到底)
    if (_.isEqual(nextProps.list, this.props.list)) {
        // 相等,则不重复渲染
        return false
    }
    return true // 不相等,则渲染
}

React高阶组件

  1. mixin,已被React弃用
  2. 高阶组件HOC 设计思想,传入一个组件,传出一个组件
// 高阶组件
const withMouse = (Component) => {
    class withMouseComponent extends React.Component {
        constructor(props) {
            super(props)
            this.state = { x: 0, y: 0 }
        }
  
        handleMouseMove = (event) => {
            this.setState({
                x: event.clientX,
                y: event.clientY
            })
        }
  
        render() {
            return (
                <div style={{ height: '500px' }} onMouseMove={this.handleMouseMove}>
                    {/* 1. 透传所有 props 2. 增加 mouse 属性 */}
                    <Component {...this.props} mouse={this.state}/>
                </div>
            )
        }
    }
    return withMouseComponent
}

const App = (props) => {
    const a = props.a
    const { x, y } = props.mouse // 接收 mouse 属性
    return (
        <div style={{ height: '500px' }}>
            <h1>The mouse position is ({x}, {y})</h1>
            <p>{a}</p>
        </div>
    )
}

export default withMouse(App) // 返回高阶函数
  1. Render Props 核心思想:通过一个函数将class组件的state作为props传递给纯函数组件
class Factory extends React.Component {
	constructor() {
    	this.state = {
        	/*state即多个组件的公共逻辑的数据*/
        }
    }
    /*修改 state*/
    render(){
    	return <div>{this.props.render(this.state)}</div>
    }
}

const App = () => (
	<Factory render={
    	/* render 是一个函数组件*/
        (props) => <p>{props.a} {props.b} ..</p>
    }/>
)

Redux

基本概念

  • store state
  • action
  • reducer

dispatch(action) reducer -> newState subscribe(订阅)

单项数据流

react-redux

异步action

//同步 action
export const addTodo = text => {
	//返回action对象
    return {
    	type: 'ADD_TODO',
        id: nextTodoId++,
        text
    }
}

//异步 action
export const addTodoAsync = text => {
	//返回函数,其中有dispatch参数
    return (dispatch) => {
    	//ajax 异步获取数据
        fetch(url).then(res => {
        	//执行异步action
            dispatch(addTodo(res.text))
        })
    }
}

中间件

02.png

  • redux-thunk
  • redux-promise
  • redux-saga

React-router

路由模式:hash、H5 history

03.png

04.png

05.png

路由配置:动态路由、懒加载

06.png

React 原理

函数式编程

  • 一种编程范式,概念比较多

  • 纯函数

  • 不可变值

vdom和diff

  • h函数
  • vnode数据结构
  • patch函数
  1. 只比较同一层级,不跨级比较
  2. tag不相同,则直接删除重建,不再深度比较
  3. tag和key,两者都相同,则认为是相同节点,不再深度比较
{
	tag: 'div',
    props: {
    	className: 'container',
        id: 'div1'
    }
    children: [
    	{tag: 'p', children: 'vdom'},
        {tag: 'ul', props: { style: 'font-size: 20px'}, children: [{tag: 'li', children: 'a'}]},
    ]
}

JSX本质

  • React.createElement即h函数,返回vnode
  • 第一个参数,可能是组件,也可能是html tag
  • 组件名,首字母必须大写(Rect规定)

合成事件

  • 所有事件都挂载到document上
  • event不是原生的,是SyntheticEvent合成事件对象
  • 和Vue事件不同,和DOM事件也不同

07.png 作用:

  1. 更好的兼容性和跨平台
  2. 挂载到document上,减少内存消耗,避免频繁解绑
  3. 方便事件的统一管理(如事物机制)

setState batchUpdate

  • 有时异步(普通使用),有时同步(setTimeout、自定义DOM事件:addEventListener)
  • 有时合并(对象形式),有时不合并(函数形式)【后者比较好理解(像Object.assign),主要讲解前者】

setState主流程

08.png

batchUpdate机制

  1. setState无所谓异步还是同步
  2. 看是否能命中batchUpdate机制
  3. 判断isBatchingUpdates

命中batchUpdate机制:

  1. 生命周期(和它调用的函数)
  2. React中注册的事件(和它调用的函数)
  3. React可以管理的入口

不能命中batchUpdate机制:

  1. setTimeout setInterval等(和它调用的函数)
  2. 自定义的DOM事件(和它调用的函数)

transaction(事务)机制

组件渲染过程

  • props state
  • render()生成vnode
  • patch(elem,vnode)

JSX如何渲染为页面

  • JSX即createElement函数
  • 执行生成vnode
  • patch(elem, vnode)和patch(vnode,newVnode)

setState之后如何更新页面

  • setState(newState) --> dirtyComponents(可能有子组件)
  • render()生成newVnode
  • patch(vnode,newVnode)

fiber

patch被拆分为两个阶段:

  • reconciliation阶段-执行diff算法,纯JS计算
  • commit阶段-将diff结果渲染成DOM

存在的性能问题

  • JS是单线程,且和DOM渲染共用一个线程
  • 当组件足够复杂,组件更新时计算和渲染都压力大
  • 同时再有DOM操作需求(动画,鼠标拖拽等),将卡顿

解决方案fiber

React内部运行机制

  • 将reconciliation阶段进行任务拆分(commit无法拆分)
  • DOM需要渲染时暂停,空闲时恢复
  • window.requestIdleCallback

面试题

组件之间如何通讯

  • 父子组件props
  • 自定义事件
  • Redux和Context

JSX本质

  • createElement
  • 执行返回vnode

Context是什么,如何应用?

  • 父组件,向其下所有子孙组件传递信息
  • 如一些简单的公共信息:主题色、语言等
  • 复杂的公共信息,使用redux

shouldComponentUpdate用途

  • 性能优化
  • 配合“不可变值”一起使用,否则会出错

redux 单项数据流

09.png

setState 场景题 10.png

什么是纯函数

  • 返回一个新值,没有副作用,不会修改原值
  • 重点:不可变值
  • arr1 = arr.slice()

React组件生命周期

  • 单组件生命周期
  • 父子组件生命周期
  • 注意SCU

React发起ajax应该在哪个生命周期

  • componentDidMount

渲染列表,为何使用key

  • 必须使用key,且不能是index和random
  • diff算法中通过tag和key来判断,是否是sanmeNode
  • 减少渲染次数,提升渲染性能

函数组件和class组件区别

  • 纯函数,输入props,输出JSX
  • 没有实例,没有生命周期,没有state
  • 不能扩展其他方法

什么是受控组件

  • 表单的值,受state控制
  • 需要自行监听onChange,更新state
  • 对比非受控组件

何时使用异步组件

  • 加载大组件
  • 路由懒加载

多个组件有公共逻辑,如何抽离

  • 高阶组件
  • Render Props
  • mixin 已被React废弃

redux如何进行异步请求

  • 使用异步action
  • 如 redux-thunk

react-router如何配置懒加载 11.png

PureComponent有何区别

  • 实现了浅比较的shouldComponentUpdate
  • 优化性能
  • 但要结合不可变值使用

React事件和DOM事件的区别

  • 所有事件挂载到document上
  • event不是原生的,是SyntheticEvent合成事件对象
  • dispatchEvent

React性能优化

  • 渲染列表时加key
  • 自定义事件、DOM事件及时销毁
  • 合理使用异步组件
  • 减少函数bind this的次数
  • 合理使用SCU PureComponent和memo
  • 合理使用Immutable.js
  • wepack层面的优化
  • 前端通用的性能优化,如图片懒加载
  • 使用SSR

React和Vue的区别

  • 都支持组件化
  • 都是数据驱动视图
  • 都使用vdom操作dom
  • React使用JSX拥抱JS,Vue使用模板拥抱html
  • React函数式编程,Vue声明式编程
  • React更多需要自力更生,Vue把想要的都给你

好文章

  1. 深度剖析:如何实现一个 Virtual DOM 算法