8s103pc5复用成定时器_JS核心理论之《React状态复用与Hooks用法》

状态逻辑复用

当页面和组件变得越来越复杂,如何更好的实现状态逻辑复用一直都是应用程序中重要的一部分,这直接关系着应用程序的质量以及维护的难易程度。

技术发展路线:Mixin -> HOC -> Hook

Mixin

Mixin(混入)是一种通过扩展收集功能的方式。
它本质上是将一个对象的属性拷贝到另一个对象上面去,不过你可以拷贝任意多个对象的任意个方法到一个新对象上去,这是继承所不能实现的。它的出现主要就是为了解决代码复用问题。

诸如Underscore的_.extend方法、jQuery的extend方法,都是一种Mixin。
例如使用_.extend实现日志的Mixin:

var LogMixin = {
  actionLog: function() {
    console.log('action...');
  },
};
function User() {  /*..*/  }

_.extend(User.prototype, LogMixin);

var user = new User();
user.actionLog();

React也提供了Mixin的实现,如果完全不同的组件有相似的功能,我们可以引入来实现代码复用,当然只有在使用createClass来创建React组件时才可以使用,因为在React组件的es6写法中它已经被废弃掉了。

var LogMixin = {
  log: function() {
    console.log('log');
  },
  componentDidMount: function() {
    console.log('in');
  },
  componentWillUnmount: function() {
    console.log('out');
  }
};

var User = React.createClass({
  mixins: [LogMixin],
  render: function() {
    return (<div>...</div>)
  }
});

缺陷:

  • Mixin 可能会相互依赖,相互耦合,不利于代码维护
  • 不同的Mixin中的方法可能会相互冲突
  • Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性

高阶组件

React对装饰模式的一种实现,高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。是没有副作用的纯函数

  1. 实现方式? 属性代理反向继承
//代理
function proxyHOC(WrappedComponent) {
  return class extends Component {
    render() {
      return <WrappedComponent {...this.props} />;
    }
  }
}
//反向继承
function inheritHOC(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      return super.render();
    }
  }
}

反向继承方式,除了像代理方式一样,能够增强原组件的props生命周期static方法获取refs, 还可以操作state渲染劫持

2. 实现什么功能? 组合渲染条件渲染操作Props获取Refs操作state状态管理渲染劫持

3. 如何使用?compose(借用reduce实现) 或 ES7的decorator

const compose = (...fns) => fns.reduce((f, g) => (...args) => g(f(...args)));
compose(logger,visible,style)(Input);

4. Hoc实际应用: 日志打点、表单验证、Redux的connector

5. 注意事项:

  • 静态属性需要手动拷贝,因为使用的组件已经不是原组件了,拿不到原组件的任何静态属性。
  • 通过forwardRef获取原组件的ref, 高阶组件并不能像透传props那样将refs透传,获取到的ref实际上是最外层的容器组件。
  • 不要在render函数中使用高阶, 每次调用高阶组件生成的都是是一个全新的组件,组件的唯一标识响应的也会改变,如果在render方法调用了高阶组件,这会导致组件每次都会被卸载后重新挂载。

怎么实现双向绑定?

基本思路是,监听文本框的onChange事件并手动调用setState, 把文本框的值同步到state中;而反方向,即state到UI是自动同步的。

原生实现方式:

import React from "react";

export default class Form extends React.Component {
    constructor() {
        super();
        this.state = {
            name: '',
        }
    }

    onChange = (evt) => {
        this.setState({
            name: evt.target.value
        })
    }

    render() {
        return (
            <form style={{textAlign: "left"}}>
                <div>
                    <label>Name: </label>
                    <input
                        value={this.state.name}
                        onChange={this.onChange}
                    />
                </div>

                <div>
                    <label>Name value: </label>
                    <label>{this.state.name}</label>
                </div>
            </form>
        )
    }
}

HOC实现方式,其代理了表单的onChange属性和value属性

import React from "react";

function proxyHoc(WrappedComponent) {
    return class extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                name: ''
            }
        }

        onChange = (evt) => {
            const {onChange} = this.props
            this.setState({name: evt.target.value}, () => {
                if (typeof onChange === 'function') {
                    onChange(evt);
                }
            })
        }

        render() {
            const newPros = {
                name: this.state.name,
                onChange: this.onChange
            }
            return <WrappedComponent {...this.props} {...newPros}/>
        }
    }
}

class Hoc extends React.Component {
    render() {
        return (
            <form style={{textAlign: "left"}}>
                <div>
                    <label>Name: </label>
                    <input {...this.props}/>
                </div>

                <div>
                    <label>Name value: </label>
                    <label>{this.props.name}</label>
                </div>
            </form>
        );
    }
}

export default proxyHoc(Hoc);

6. HOC有什么缺陷?

  • HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难。
  • HOC可以劫持props,在不遵守约定的情况下也可能造成冲突。

Hooks

为什么要使用Hooks

  • 状态逻辑复用

我们知道如果用类定义组件,在组件之间复用状态逻辑很难,可能要用到 render props (渲染属性)或者 Mixin 或者 HOC(高阶组件)
但无论是渲染属性,还是高阶组件,都会在原先的组件外包裹一层父容器(一般都是 div 元素),导致层级冗余
Mixin的问题是引入的逻辑和状态是可以相互覆盖的,而多个Hook之间互不影响。

  • 使用函数代替class

class组件变得越来越庞大,各个生命周期中会调用越来越多的逻辑,越来越难以维护,Hook可以让你更大限度的将公用逻辑抽离,将一个组件分割成更小的函数,而不是强制基于生命周期方法进行分割
class组件中需要注意的this指向绑定事件等问题,函数组件中不需要。

  • Hooks的优势

除了上述两点优势外,Hooks使副作用的关注点分离。副作用指那些没有发生在数据向视图转换过程中的逻辑,如 ajax 请求、访问原生dom 元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。以往这些副作用都是写在类组件生命周期函数中的。
useEffect 在全部渲染完毕后才会执行,useLayoutEffect 会在浏览器 layout 之后,painting 之前执行。

State Hook

我们先用类组件实现一个计数器:

export default class Count extends React.Component {
    constructor(props) {
        super(props);
        this.state = {count: 0}
    }

    render() {
        return (
            <div>
                <p>You clicked {this.state.count} times</p>
                <button onClick={() => {
                    this.setState({count: this.state.count + 1})
                }}>
                    Click me
                </button>
            </div>
        )
    }
}

接下来使用useState实现同样的功能:

import React, {useState} from "react";

export default function CounterHook() {
    const [count, setCount] = useState(0)
    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => {
                setCount(count + 1)
            }}>
                Click me
            </button>
        </div>
    )
}

useState是一个钩子,他可以为函数式组件增加一些状态,并且提供改变这些状态的函数,同时它接收一个参数,这个参数作为状态的默认值。

React 假设当你多次调用 useState 的时候,你能保证每次渲染时它们的调用顺序是不变的,每次渲染都是独立的闭包

Effect Hook

Effect Hook 可以让你在函数组件中执行一些具有 side effect(副作用)的操作。

useEffect(callback, deps) 接收两个参数:

  • 回调函数:在第组件一次render和之后的每次update后运行,React保证在DOM已经更新完成之后才会运行回调。
  • 状态依赖(数组):当配置了状态依赖项后,只有检测到配置的状态变化时,才会调用回调函数。

它跟 class 组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 具有相同的用途,只不过被合并成了一个 API。
componentDidMountcomponentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。

  useEffect(() => {
    // 只要组件render后就会执行
  });
  useEffect(() => {
    // 只有count改变时才会执行
  },[count]);

我们来看一个例子,修改标题,先用class组件实现:

import React from "react";

export default class UpdateTitle extends React.Component {
    state = {number: 0};

    counter = () => {
        this.setState({number: this.state.number + 1});
    };

    componentDidMount() {
        this.changeTitle();
    }

    componentDidUpdate() {
        this.changeTitle();
    }

    changeTitle = () => {
        document.title = `${this.state.number}`;
    };

    render() {
        return (
            <>
                <p>{this.state.number}</p>
                <button onClick={this.counter}>+</button>
            </>
        )
    }
}

我们再用useEffect实现同样的效果:

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

export default function UpdateTitleEffectHook() {
    const [number, setNumber] = useState(0)
    useEffect(() => {
        document.title = `${number}`
    })
    return (
        <>
            <p>{number}</p>
            <button onClick={() => setNumber(number + 1)}>+</button>
        </>
    )
}

注意:

副作用函数还可以通过返回一个函数来指定如何清除副作用,为防止内存泄漏,清除函数会在组件卸载前执行。
如果组件多次渲染,则在执行下一个 effect 之前,上一个 effect 就已被清除, 即执行return的函数。

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

export default function UpdateTitleEffectHook() {
    const [number, setNumber] = useState(0)
    useEffect(() => {
        document.title = `${number}`
        console.log(`执行更新:${number}`)
        return ()=>{
            console.log(`清除上一次副作用:${number}`)
        }
    })
    return (
        <>
            <p>{number}</p>
            <button onClick={() => setNumber(number + 1)}>+</button>
        </>
    )
}

点击几次后,你会发现输出如下结果:

执行更新:0

清除上一次副作用:0
执行更新:1

清除上一次副作用:1
执行更新:2

清除上一次副作用:2
执行更新:3

那么为什么在浏览器渲染完后,再执行清理的方法还能找到上次的state呢?原因很简单,我们在useEffect中返回的是一个函数,这形成了一个闭包,这能保证我们上一次执行函数存储的变量不被销毁和污染。

useLayoutEffect

  • useEffect 在全部渲染完毕后才会执行
  • useLayoutEffect 会在 浏览器 layout 之后,painting 之前执行

dfec930226900015141e9630855c6f9b.png

其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect, 在浏览器执行绘制之前 useLayoutEffect 内部的更新计划将被同步刷新。
可以使用它来读取 DOM 布局并同步触发重渲染

Ref Hook

使用useRef,你可以轻松的获取到dom的ref

useRef 返回的 ref 对象在组件的整个生命周期内保持不变,也就是说每次重新渲染函数组件时,返回的 ref 对象都是同一个。

而如果使用 React.createRef ,每次重新渲染组件都会重新创建 ref

useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数(initialValue),这意味着你可以用它来保存一个任意值。

const ref = useRef(initialValue);
ref.current

我们来看一个获取文本框架焦点的方法:

import React, {useRef} from "react";

export default function FocusRefHook() {
    const inputRef = useRef()

    function focus() {
        inputRef.current.focus()
    }

    return (
        <>
            <input type="text" ref={inputRef}/>
            <button onClick={focus}>Get Focus</button>
        </>
    );
}

forwardRef

forwardRef 可以将父组件中的 ref 对象转发到子组件中的 dom 元素上,使得可以在父组件中操作子组件的 ref 对象,子组件接受 props 和 ref 作为参数。

function Child(props,ref){
  return (
    <input type="text" ref={ref}/>
  )
}

//使forwardRef将父组件ref转发到子组件dom上
Child = React.forwardRef(Child);

function Parent(){
  const inputRef = useRef();  // { current:'' }
  function getFocus(){
    inputRef.current.value = 'focus';
    inputRef.current.focus();
  }
  return (
      <>
        <Child ref={inputRef}/>
        <button onClick={getFocus}>获得焦点</button>
      </>
  )
}

自定义Hook

组件之间重用一些状态逻辑,之前要么用 render props ,要么用高阶组件,要么使用 redux;自定义 Hook 可以让你在不增加组件的情况下达到同样的目的。

Hook 是一种复用状态逻辑的方式,它不复用 state 本身,每次调用都有一个完全独立的 state。

我们来用自定义Hook实现双向绑定:

import React, {useState, useCallback} from "react";

function useBind(init) {
    let [value, setValue] = useState(init);
    let onChange = useCallback(function (event) {
        setValue(event.currentTarget.value);
    }, []);

    return {
        value,
        onChange
    };
}

export default function BindCustomHook() {
    let bindObj = useBind('')
    return (
        <>
            <label>实时值: {bindObj.value}</label><br/>
            <input {...bindObj}/>
        </>
    )
}

注意事项

  • 只能在React class式组件或自定义Hook中使用Hook,不要在其他 JavaScript 函数中调用。
  • 不要在循环,条件或 嵌套函数 中调用Hook。
  • 在两个组件中使用相同的 Hook 不会共享 state,每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。
  • 在一个组件中多次调用 useState 或者 useEffect,每次调用 Hook,它都会获取独立的 state,是完全独立的。
  • useEffect 不能接收 async 作为回调函数, 因为它要么返回一个能清除副作用的函数,要么就不返回任何内容。而 async 返回的是 promise。
  • 为什么每次更新的时候都要运行 Effect? 防止因忘记正确地处理 componentDidUpdate 而引入的bug产生。

比如fetchData时:

const [data, setData] = useState({ hits: [] });


// 注意 async 的位置,这种写法,虽然可以运行,但是会发出警告
// 每个带有 async 修饰的函数都返回一个隐含的 promise
useEffect(async () => {
const result = await axios(
  'https://xxx',
);
setData(result.data);
}, []);

// 更优雅的方式
useEffect(() => {
const fetchData = async () => {
  const result = await axios(
    'https://xxx',
  );
  setData(result.data);
};
fetchData();
}, []);
  • 一些性能优化点
  1. 浅比较: Hook 内部使用 Object.is 来比较新/旧 state 是否相等
  2. 减少渲染次数:默认情况,只要父组件状态变了(不管子组件依不依赖该状态),子组件也会重新渲染;
  • 类组件:可以使用 pureComponent
  • 函数组件:使用 useMemo

useMemo 本身也有开销
useMemo 会记住一些值,同时在后续 render 时,将依赖数组中的值取出来和上一次记录的值进行比较,如果不相等才会重新执行回调函数,否则直接返回记住的值。
这个过程本身就会消耗一定的内存和计算资源。因此,过度使用 useMemo 可能会影响程序的性能。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值