• 七天接手react项目 系列 —— 生命周期&受控和非受控组件&Dom 元素&Diffing 算法


    其他章节请看:

    七天接手react项目 系列

    生命周期&受控和非受控组件&Dom 元素&Diffing 算法

    生命周期

    首先回忆一下 vue 中的生命周期:

    vue 对外提供了生命周期的钩子函数,允许我们在 vue 的各个阶段插入一些我们的逻辑,比如:createdmountedbeforeDestroy等。

    vue-生命周期图

    react 中的生命周期是否也类似?请接着看:

    每个组件都包含 “生命周期方法”,你可以重写这些方法,以便于在运行过程中特定的阶段执行这些方法 —— react 官网-组件的生命周期

    请看一张 react 的生命周期图谱

    react-生命周期图谱

    从这张图我们知道:

    • 既然没有勾选”展示不常用的生命周期“,这里显示的 5 个方法就是常用的生命周期方法
    • 组件的生命周期可以分三个阶段:挂载、更新、卸载
    • 挂载时的顺序是:constructor()render()componentDidMount()

    Tip

    • componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。常做定时器、网络请求
    • componentDidUpdate() 会在更新后会被立即调用。首次渲染不会执行此方法
    • componentWillUnmount() 会在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等

    挂载和卸载

    以 Clock 组件为例:

    当 Clock 组件第一次被渲染到 DOM 中的时候,就为其设置一个计时器。这在 React 中被称为“挂载(mount)”。

    同时,当 DOM 中 Clock 组件被删除的时候,应该清除计时器。这在 React 中被称为“卸载(unmount)”。

    请看实现:

    class Clock extends React.Component {
        state = { date: new Date() }
        componentDidMount() {
            this.timerID = setInterval(
                () => this.tick(),
                1000
            )
        }
        // 组件卸载前会被调用
        componentWillUnmount() {
            clearInterval(this.timerID) // {1}
        }
        tick() {
            this.setState({
                date: new Date()
            });
        }
        handleUnmount = () => {
            // 从 DOM 中卸载组件
            ReactDOM.unmountComponentAtNode(document.getElementById('root'))
        }
        render() {
            return (
                <div>
                    <h1>Hello, world!</h1>
                    <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
                    <button onClick={this.handleUnmount}>卸载</button>
                </div>
            );
        }
    }
    

    页面显示:

    Hello, world!
    It is 11:34:16.
    
    卸载
    

    时间每秒都会更新,点击按钮”卸载“,页面将不再有任何信息,对应的 html 为 <div id="root"></div>

    TipunmountComponentAtNode() 从 DOM 中卸载组件,会将其事件处理器(event handlers)和 state 一并清除。

    :倘若将 clearInterval(this.timerID)(行{1})注释,点击”卸载“将报错如下:

    Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
    
    警告:无法对卸载的组件执行 React 状态更新。 这是一个空操作,但它表明您的应用程序中存在内存泄漏。 要修复,请取消 componentWillUnmount 方法中的所有订阅和异步任务。
    
    不要将定时器放入 render()

    倘若将上面例子中的定时器放在 render() 中。就像这样:

    render() {
        console.log(1)
        // 定时器
        this.timerID = setInterval(
            () => this.tick(),
            1000
        )
        return (
            // ...不变
        );
    }
    

    之前 render() 每秒执行一次,现在很快就会执行过万,因为每次执行都会生成一个定时器。

    过时的生命周期方法

    以下生命周期方法标记为“过时”。这些方法仍然有效,但不建议在新代码中使用它们 —— 官网-过时的生命周期方法

    • componentWillMount,现在改名为 UNSAFE_componentWillMount(),在挂载之前被调用

    • componentWillReceiveProps,现在改名为 UNSAFE_componentWillReceiveProps(),在已挂载的组件接收新的 props 之前被调用。第一次传的不算,以后传的才算,有人说应该叫 componentWillReceiveNewProps

    • componentWillUpdate,现在改名为 UNSAFE_componentWillUpdate(),当组件收到新的 props 或 state 时,会在渲染之前调用。

    倘若用了重命名之前的方法,控制台会有详细的警告信息。请看示例:

    class Clock extends React.Component {
        componentWillMount() {
    
        }
        UNSAFE_componentWillReceiveProps() {
    
        }
    }
    

    控制台输出:

    Warning: componentWillMount has been renamed, and is not recommended for use. See https://reactjs.org/link/unsafe-component-lifecycles for details.
    
    * Move code with side effects to componentDidMount, and set initial state in the constructor.
    * Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.
    
    Please update the following components: Clock
    

    Tip:既然这几个方法不建议使用,所以不打算深入研究

    UNSAFE_ 不是指安全性

    这里的 “unsafe” 不是指安全性,而是表示使用这些生命周期的代码在 React 的未来版本中更有可能出现 bug,尤其是在启用异步渲染之后 —— 官网-异步渲染之更新

    shouldComponentUpdate

    shouldComponentUpdate() 默认返回 true。用法如下:

    class Clock extends React.Component {
        state = { date: new Date() }
        componentDidMount() {
            this.timerID = setInterval(
                () => this.tick(),
                1000
            )
        }
        tick() {
            this.setState({
                date: new Date()
            });
        }
        render() {
            return (
                <div>
                    <h1>Hello, world!</h1>
                    <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
                </div>
            );
        }
        // 返回 false
        shouldComponentUpdate() {
            return false
        }
    }
    

    Clock 的时间不会再变化。render() 方法也不会再执行。请看下图:

    react-生命周期图2

    调用 setState(),如果 shouldComponentUpdate() 返回 false 则中断,不再执行 render()

    Tip:此方法仅作为性能优化的方式而存在。不要企图依靠此方法来“阻止”渲染,因为这可能会产生 bug —— 官网-shouldComponentUpdate()

    forceUpdate

    根据上图说明,调用 forceUpdate() 将致使组件调用 render() 方法,此操作会跳过该组件的 shouldComponentUpdate()

    通常应该避免使用 forceUpdate()

    新增生命周期方法

    相对旧的生命周期,新增如下两个方法,但都属于不常见的情形,所以不做详细研究。

    getDerivedStateFromProps

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

    此方法适用于罕见的用例,即 state 的值在任何时候都取决于 props。

    getDerivedStateFromProps 的存在只有一个目的:让组件在 props 变化时更新 state —— 官网-什么时候使用派生 state

    getSnapshotBeforeUpdate

    getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。

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

    在函数组件中使用生命周期

    我们可以在函数组件中使用 useEffect 来模拟常见的生命周期钩子:componentDidMount()componentDidUpdate()componentWillUnmount()

    体验 useEffect

    首先我们运行一个例子:

    function MyButton() {
        const [count, setCount] = React.useState(0)
    
        const add = () => {
            setCount(count + 1)
        }
    
        const unMount = () => {
            ReactDOM.unmountComponentAtNode(document.getElementById('root'))
        }
    
        // React.useEffect() 将写在此处 {1}
        
        return (
            <div>
                <button onClick={add}>{count}</button> <button onClick={unMount}>卸载</button>
            </div>
        );
    }
    ReactDOM.render(
        <MyButton />,
        document.getElementById('root')
    )
    

    页面显示两个按钮:

    0 卸载
    

    第一个按钮显示一个数字,每点击一次就会自增 1,点击第二个按钮,此组件就会被卸载。

    我们接下来在行{1}处添加 React.useEffect() 相关代码。请看示例:

    // 相当于 componentDidMount()、componentDidUpdate()
    React.useEffect(() => {
        console.log('a')
    })
    

    页面渲染后就会输出 a,之后每点击第一个按钮都会输出 a,点击卸载没有输出。

    可以给 useEffect 传递第二个参数,它是 effect 所依赖的值数组 —— 官网-effect 的条件执行

    倘若给 useEffect 第二个参数传递一个空数组,表明没有依赖值:

    // 相当于 componentDidMount()
    React.useEffect(() => {
        console.log('a')
    }, [])
    

    页面渲染后就会输出 a,但点击第一个按钮就不会再有输出。

    通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect 函数需返回一个清除函数 —— 官网-清除 effect

    倘若给 useEffect 函数返回一个函数。请看示例:

     React.useEffect(() => {
        console.log('a')
        return () => {
            console.log('b')
        }
    }, [])
    

    页面渲染后就会输出 a,但点击第一个按钮就不会再有输出,点击卸载输出 b

    优化函数组件 Clock 中的定时器

    在函数组件中使用 state中我们写过这么一个例子:

    function Clock() {
        const [name] = React.useState('pjl')
        const [date, setDate] = React.useState(new Date())
    
        setInterval(() => {
            console.log('setInterval')
            setDate(new Date())
        }, 1000)
    
        return (
            <div>
                <h1>Hello, world! {name}</h1>
                <h2>It is {date.toLocaleTimeString()}.</h2>
            </div>
        );
    }
    

    十秒就会输出一千多次 setInterval。定时器应该只执行一次,放在 componentDidMount 生命钩子中比较合适。以下是优化后的增强版:

    function Clock() {
        // console.log('Clock')
        const [name] = React.useState('pjl')
        const [date, setDate] = React.useState(new Date())
    
        React.useEffect(() => {
            console.log('useEffect')
            const timerId = setInterval(() => {
                // console.log('setInterval')
                setDate(new Date())
            }, 1000)
    
            return () => {
                clearInterval(timerId)
            }
        }, [name])
    
        const unMount = () => {
            ReactDOM.unmountComponentAtNode(document.getElementById('root'))
        }
    
        return (
            <div>
                <h1>Hello, world! {name}</h1>
                <h2>It is {date.toLocaleTimeString()}.</h2>
                <button onClick={unMount}>卸载</button>
            </div>
        );
    }
    

    受控组件和非受控组件

    在大多数情况下,我们推荐使用 受控组件 来处理表单数据。在一个受控组件中,表单数据是由 React 组件来管理的。另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理 —— 官网-非受控组件

    这里我们能接收两个信息:

    1. 推荐使用受控组件
    2. 受控组件和非受控组件的区别在于:表单数据由谁来处理 —— 是 react 组件管理,还是 dom 来处理。

    受控组件

    将表单写为受控组件:

    class NameForm extends React.Component {
        state = { value: '' }
        // 值若改变,则将其更新到 state 中
        handleChange = event => {
            this.setState({ value: event.target.value });
        }
    
        // 提交表单
        handleSubmit = event => {
            console.log('提交的名字: ' + this.state.value);
            event.preventDefault();
        }
    
        render() {
            return (
                <form onSubmit={this.handleSubmit}>
                    <label>
                        名字:
                        <input type="text" value={this.state.value} onChange={this.handleChange} />
                    </label>
                    <input type="submit" value="提交" />
                </form>
            );
        }
    }
    ReactDOM.render(
        <NameForm />,
        document.getElementById('root')
    );
    

    页面显示

    名字:[     输入框       ] 提交
    

    在输入框中输入”123“,点击”提交“按钮,控制台将输出 ”提交的名字: 123“。

    非受控组件

    重写 NameForm 组件,改为功能相同的非受控组件:

    class NameForm extends React.Component {
        input = React.createRef()
        handleSubmit = event => {
            console.log('提交的名字: ' + this.input.current.value);
            event.preventDefault();
        }
    
        render() {
            return (
                <form onSubmit={this.handleSubmit}>
                    <label>
                        名字:
                        <input type="text" ref={this.input} />
                    </label>
                    <input type="submit" value="提交" />
                </form>
            );
        }
    }
    

    勿过度使用 Refs —— 官网

    Tip:倘若发生事件的元素,是你要操作的元素时,可以通过 event.target 取得 dom。

    高阶函数和函数柯里化优化受控组件

    按照受控组件中的写法,如果我们定义多个 input,我们就得写多个 handleXxxx 处理方法。就像这样:

    class NameForm extends React.Component {
        state = { name: '', age: '' }
    
        // 2 个 input 对应 2 个处理方法
        handleName = event => {
            this.setState({ name: event.target.value });
        }
        handleAge = event => {
            this.setState({ age: event.target.value });
        }
    
        handleSubmit = event => {
            console.log({ name: this.state.name, age: this.state.age });
            event.preventDefault();
        }
    
        render() {
            return (
                <form onSubmit={this.handleSubmit}>
                    <label>
                        名字:
                        <input type="text" value={this.state.name} onChange={this.handleName} />
                    </label>
                    <label>
                        年龄:
                        <input type="text" value={this.state.age} onChange={this.handleAge} />
                    </label>
                    <input type="submit" value="提交" />
                </form>
            );
        }
    }
    

    如果我们有10个,岂不是要写10个处理方法!我们可以用高阶函数函数柯里化来对其优化。请看实现:

    class NameForm extends React.Component {
        state = { name: '', age: '' }
    
        // saveFormField 既是`高阶函数`,也使用了`函数柯里化`
        saveFormField = (stateName) => {
            return (event) => {
                this.setState({ [stateName]: event.target.value }) // {1}
            }
        }
    
        handleSubmit = event => {
            console.log({ name: this.state.name, age: this.state.age });
            event.preventDefault();
        }
    
        render() {
            return (
                <form onSubmit={this.handleSubmit}>
                    <label>
                        名字:
                        <input type="text" value={this.state.name} onChange={this.saveFormField('name')} />
                    </label>
                    <label>
                        年龄:
                        <input type="text" value={this.state.age} onChange={this.saveFormField('age')} />
                    </label>
                    <input type="submit" value="提交" />
                </form>
            );
        }
    }
    

    Tipthis.setState({ [stateName]: event.target.value }) 使用的语法是 可计算属性名

    高阶函数

    高阶函数是处理函数的函数,只要满足其中一个条件即可:

    • 参数是函数
    • 返回函数

    js 内置的高阶函数有:Array.forEach、setInterval、Promise等。

    函数柯里化

    通过函数调用继续返回函数,实现多次接收参数最后统一处理的函数编码形式。

    最二的一个示例是将:

    function sum(a,b,c){
        return a + b + c
    }
    

    改成 sum(1)(2)(3) 的形式。就像这样:

    const sum = (a) => {
        return (b) => {
            return (c) => {
                return a + b + c
            }
        }
    }
    
    // 6
    console.log(sum(1)(2)(3))
    

    DOM 元素

    React 实现了一套独立于浏览器的 DOM 系统,兼顾了性能和跨浏览器的兼容性。我们借此机会完善了浏览器 DOM 实现的一些特殊情况 ——官网-DOM 元素

    在 React 中,所有的 DOM 特性和属性(包括事件处理)都应该是小驼峰命名的方式。例如,与 HTML 中的 tabindex 属性对应的 React 的属性是 tabIndex。

    :例外的情况是 aria-* 以及 data-* 属性,一律使用小写字母命名。比如, 你依然可以用 aria-label 作为 aria-label。

    React 与 HTML 之间有很多属性存在差异,下面以 onChange 为例。

    Tip:比如 react 中用 htmlFor 代替 for,其他更多介绍请看 DOM 元素

    onChange

    onChange 事件与预期行为一致:每当表单字段变化时,该事件都会被触发。我们故意没有使用浏览器已有的默认行为,是因为 onChange 在浏览器中的行为和名称不对应,并且 React 依靠了该事件实时处理用户输入 —— 官网-onChange

    change 事件并不是每次元素的 value 改变时都会触发 —— mdn-change 事件

    原生 html 中 change 事件是这样的:

    <body>
        名字:<input name="name" />
    
        <script>
            document.querySelector('input').
                addEventListener('change', e => console.log(e.target.value))
        </script>
    </body>
    

    在输入框中输入 123,点击他处让 input 失去焦点,控制台输出 123

    在上面受控组件 NameForm 中增加一行:

    class NameForm extends React.Component {
        state = { value: '' }
        handleChange = event => {
          + console.log(event.target.value)
            this.setState({ value: event.target.value });
        }
    }
    

    在输入框中输入 123,控制台依次输出:

    1
    12
    123
    

    每当表单字段变化时,该事件都会被触发。事件名和行为相对应。

    Diffing 算法

    根节点

    当对比两棵树时,React 首先比较两棵树的根节点 —— 官网-Diffing 算法

    对比不同类型的元素

    当根节点为不同类型的元素时,React 会拆卸原有的树并且建立起新的树

    举个例子,当一个元素从 <a> 变成 <img>,从 <Article> 变成 <Comment>,或从 <Button> 变成 <div> 都会触发一个完整的重建流程

    当卸载一棵树时,对应的 DOM 节点也会被销毁。组件实例将执行 componentWillUnmount() 方法。

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

    <div>
      <Counter />
    </div>
    
    <span>
      <Counter />
    </span>
    

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

    对比同类型的元素

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

    比如:

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

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

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

    对比同类型的组件元素

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

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

    对子节点进行递归

    默认情况下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表

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

    <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 之后,使得树的转换效率得以提高:

    <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

    倘若用元素在数组中的下标作为 key,有时不仅会造成上面所说的性能问题,有时还会造成程序的错误。请看示例:

    function Demo() {
        const [todos, setTodos] = React.useState(['a', 'b'])
    
        const unshift = () => {
            setTodos([++seed, ...todos])
        }
        return (
            <div>
                <ul>
                    {
                        todos.map((item, index) => {
                            return <li key={index} data-index={index}> {item} <input type="text" /></li>
                        })
                    }
                </ul>
                <button onClick={unshift}>头部插入</button>
            </div>
        )
    }
    

    页面显示:

    a [   /* input 输入框 */   ]
    b [   /* input 输入框 */   ]
    头部插入
    

    在第一个输入框中输入 a,在第二个输入框中输入 b,然后点击按钮“头部插入”,界面错乱如下:

    1 [a                       ]
    a [b                       ]
    b [                        ]
    头部插入
    

    倘若将 key 改成唯一值,使用相同的操作,界面就正常:

    {
        todos.map((item, index) => {
            return <li key={item} data-index={index}> {item} <input type="text" /></li>
        })
    }
    
    1 [                        ]
    a [a                       ]
    b [b                       ]
    头部插入
    

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

    Tip:如果仅做简单展示,用元素在数组中的下标作为 key 也是可以的。

    其他章节请看:

    七天接手react项目 系列

  • 相关阅读:
    解决-linux 一次 高并发处理过程。
    Linux系统之时间同步方法
    拓展(华为优秀网站)
    Python3 JSON 数据解析
    从创建神经声音到成功完成PoC
    FPGA领域顶级学术会议
    1、Tomcat整体架构
    有营养的算法笔记五
    【Try to Hack】vulnhub DC2
    智安网络|探索语音合成技术的未来:揭秘人工智能配音技术的背后
  • 原文地址:https://www.cnblogs.com/pengjiali/p/16021635.html