资讯专栏INFORMATION COLUMN

React系列 --- 从Mixin到HOC再到HOOKS(四)

Lionad-Morotar / 3681人阅读

摘要:返回元素的是将新的与原始元素的浅层合并后的结果。生命周期方法要如何对应到函数组件不需要构造函数。除此之外,可以认为的设计在某些方面更加高效避免了需要的额外开支,像是创建类实例和在构造函数中绑定事件处理器的成本。

React系列

React系列 --- 简单模拟语法(一)
React系列 --- Jsx, 合成事件与Refs(二)
React系列 --- virtualdom diff算法实现分析(三)
React系列 --- 从Mixin到HOC再到HOOKS(四)
React系列 --- createElement, ReactElement与Component部分源码解析(五)
React系列 --- 从使用React了解Css的各种使用方案(六)

Mixins(已废弃)

这是React初期提供的一种组合方案,通过引入一个公用组件,然后可以应用公用组件的一些生命周期操作或者定义方法,达到抽离公用代码提供不同模块使用的目的.

曾经的官方文档demo如下

var SetIntervalMixin = {
  componentWillMount: function() {
    this.intervals = [];
  },
  setInterval: function() {
    this.intervals.push(setInterval.apply(null, arguments));
  },
  componentWillUnmount: function() {
    this.intervals.map(clearInterval);
  },
};

var TickTock = React.createClass({
  mixins: [SetIntervalMixin], // Use the mixin
  getInitialState: function() {
    return { seconds: 0 };
  },
  componentDidMount: function() {
    this.setInterval(this.tick, 1000); // Call a method on the mixin
  },
  tick: function() {
    this.setState({ seconds: this.state.seconds + 1 });
  },
  render: function() {
    return 

React has been running for {this.state.seconds} seconds.

; }, }); React.render(, document.getElementById("example"));

但是Mixins只能应用在createClass的创建方式,在后来的class写法中已经被废弃了.原因在于:

mixin引入了隐式依赖关系

不同mixins之间可能会有先后顺序甚至代码冲突覆盖的问题

mixin代码会导致滚雪球式的复杂性

详细介绍mixin危害性文章可直接查阅Mixins Considered Harmful

高阶组件(Higher-order component)

HOC是一种React的进阶使用方法,大概原理就是接收一个组件然后返回一个新的继承组件,继承方式分两种

属性代理(Props Proxy)

最基本的实现方式

function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    render() {
      return 
    }
  }
}

从代码可以看出属性代理方式其实就是接受一个 WrappedComponent 组件作为参数传入,并返回一个继承了 React.Component 组件的类,且在该类的 render() 方法中返回被传入的 WrappedComponent 组件

抽离state && 操作props
function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        name: "PropsProxyHOC",
      };
    }

    logName() {
      console.log(this.name);
    }

    render() {
      const newProps = {
        name: this.state.name,
        logName: this.logName,
      };
      return ;
    }
  };
}

class Main extends Component {
  componentDidMount() {
    this.props.logName();
  }

  render() {
    return 
PropsProxyHOC
; } } export default PropsProxyHOC(Main);

demo代码可以参考这里
有种常见的情况是用来做双向绑定

function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    constructor(props) {
      super(props);
      this.state = { fields: {} };
    }

    getField(fieldName) {
      const _s = this.state;
      if (!_s.fields[fieldName]) {
        _s.fields[fieldName] = {
          value: "",
          onChange: event => {
            this.state.fields[fieldName].value = event.target.value;
            // 强行触发render
            this.forceUpdate();
            console.log(this.state);
          },
        };
      }

      return {
        value: _s.fields[fieldName].value,
        onChange: _s.fields[fieldName].onChange,
      };
    }

    render() {
      const newProps = {
        fields: this.getField.bind(this),
      };
      return ;
    }
  };
}

// 被获取ref实例组件
class Main extends Component {
  render() {
    return ;
  }
}

export default PropsProxyHOC(Main);

demo代码可以参考这里

获取被继承refs实例

因为这是一个被HOC包装过的新组件,所以想要在HOC里面获取新组件的ref需要用些特殊方式,但是不管哪种,都需要在组件挂载之后才能获取到.并且不能在无状态组件(函数类型组件)上使用 ref 属性,因为无状态组件没有实例。

通过父元素传递方法获取
function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    render() {
      const newProps = {};
      // 监听到有对应方法才生成props实例
      typeof this.props.getInstance === "function" && (newProps.ref = this.props.getInstance);
      return ;
    }
  };
}

// 被获取ref实例组件
class Main extends Component {
  render() {
    return 
Main
; } } const HOCComponent = PropsProxyHOC(Main); class ParentComponent extends Component { componentWillMount() { console.log("componentWillMount: ", this.wrappedInstance); } componentDidMount() { console.log("componentDidMount: ", this.wrappedInstance); } // 提供给高阶组件调用生成实例 getInstance(ref) { this.wrappedInstance = ref; } render() { return ; } } export default ParentComponent;

demo代码可以参考这里

通过高阶组件当中间层

相比较上一方式,需要在高阶组件提供设置赋值函数,并且需要一个props属性做标记

function PropsProxyHOC(WrappedComponent) {
  return class NewComponent extends React.Component {
    // 返回ref实例
    getWrappedInstance = () => {
      if (this.props.withRef) {
        return this.wrappedInstance;
      }
    };

    //设置ref实例
    setWrappedInstance = ref => {
      this.wrappedInstance = ref;
    };

    render() {
      const newProps = {};
      // 监听到有对应方法才赋值props实例
      this.props.withRef && (newProps.ref = this.setWrappedInstance);
      return ;
    }
  };
}

// 被获取ref实例组件
class Main extends Component {
  render() {
    return 
Main
; } } const HOCComponent = PropsProxyHOC(Main); class ParentComponent extends Component { componentWillMount() { console.log("componentWillMount: ", this.refs.child); } componentDidMount() { console.log("componentDidMount: ", this.refs.child.getWrappedInstance()); } render() { return ; } } export default ParentComponent;

demo代码可以参考这里

forwardRef

React.forwardRef 会创建一个React组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。这种技术并不常见,但在以下两种场景中特别有用:

转发 refs 到 DOM 组件

在高阶组件中转发 refs

const FancyButton = React.forwardRef((props, ref) => (
  
));

// You can now get a ref directly to the DOM button:
const ref = React.createRef();
Click me!;

以下是对上述示例发生情况的逐步解释:

我们通过调用 React.createRef 创建了一个 React ref 并将其赋值给 ref 变量。

我们通过指定 ref 为 JSX 属性,将其向下传递给

React 传递 ref 给 fowardRef 内函数 (props, ref) => ...,作为其第二个参数。

我们向下转发该 ref 参数到

); }

demo代码可以参考这里
本质上,useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”。
你应该熟悉 ref 这一种访问 DOM 的主要方式。如果你将 ref 对象以

形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。
然而,useRef() 比 ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。
这是因为它创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。
请记住,当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

HOOKS规范 在顶层调用HOOKS

不要在循环,条件,或者内嵌函数中调用.这都是为了保证你的代码在每次组件render的时候会按照相同的顺序执行HOOKS,而这也是能够让React在多个useState和useEffect执行中正确保存数据的原因

只在React函数调用HOOKS

React函数组件调用

从自定义HOOKS中调用

可以确保你源码中组件的所有有状态逻辑都是清晰可见的.

自定义HOOKS

我们可以将相关逻辑抽取出来

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

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

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

我必须以“use”开头为自定义钩子命名吗? 这项公约非常重要。如果没有它,我们就不能自动检查钩子是否违反了规则,因为我们无法判断某个函数是否包含对钩子的调用。

使用相同钩子的两个组件是否共享状态? 不。自定义钩子是一种重用有状态逻辑的机制(例如设置订阅并记住当前值),但是每次使用自定义钩子时,其中的所有状态和效果都是完全隔离的。

自定义钩子如何获得隔离状态? 对钩子的每个调用都处于隔离状态。从React的角度来看,我们的组件只调用useStateuseEffect

问题 Hook 会替代 render props 和高阶组件吗?

通常,render props 和高阶组件只渲染一个子节点。我们认为让 Hook 来服务这个使用场景更加简单。这两种模式仍有用武之地,(例如,一个虚拟滚动条组件或许会有一个 renderItem 属性,或是一个可见的容器组件或许会有它自己的 DOM 结构)。但在大部分场景下,Hook 足够了,并且能够帮助减少嵌套。

生命周期方法要如何对应到 Hook?

constructor:函数组件不需要构造函数。你可以通过调用 useState 来初始化 state。如果计算的代价比较昂贵,你可以传一个函数给 useState。

getDerivedStateFromProps:改为在渲染时安排一次更新。

shouldComponentUpdate:详见 React.memo.

render:这是函数组件体本身。

componentDidMount, componentDidUpdate, componentWillUnmount:useEffect Hook 可以表达所有这些的组合。

componentDidCatch and getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法,但很快会加上。

我可以只在更新时运行 effect 吗?

这是个比较罕见的使用场景。如果你需要的话,你可以 使用一个可变的 ref 手动存储一个布尔值来表示是首次渲染还是后续渲染,然后在你的 effect 中检查这个标识。

如何获取上一轮的 props 或 state?

目前,你可以通过ref来手动实现:

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  return (
    

Now: {count}, before: {prevCount}

); } function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; }
有类似 forceUpdate 的东西吗?

如果前后两次的值相同,useState 和 useReducer Hook 都会放弃更新。原地修改 state 并调用 setState 不会引起重新渲染。
通常,你不应该在 React 中修改本地 state。然而,作为一条出路,你可以用一个增长的计数器来在 state 没变的时候依然强制一次重新渲染:

const [ignored, forceUpdate] = useReducer(x => x + 1, 0);

function handleClick() {
  forceUpdate();
}
我该如何测量 DOM 节点?

要想测量一个 DOM 节点的位置或是尺寸,你可以使用 callback ref。每当 ref 被附加到另一个节点,React 就会调用 callback。

function MeasureExample() {
  const [rect, ref] = useClientRect();
  return (
    

Hello, world

{rect !== null &&

The above header is {Math.round(rect.height)}px tall

}
); } function useClientRect() { const [rect, setRect] = useState(null); const ref = useCallback(node => { if (node !== null) { setRect(node.getBoundingClientRect()); } }, []); return [rect, ref]; }

demo代码可以参考这里
使用 callback ref 可以确保 即便子组件延迟显示被测量的节点 (比如为了响应一次点击),我们依然能够在父组件接收到相关的信息,以便更新测量结果。

注意到我们传递了 [] 作为 useCallback 的依赖列表。这确保了 ref callback 不会在再次渲染时改变,因此 React 不会在非必要的时候调用它。

我该如何实现 shouldComponentUpdate?

你可以用 React.memo 包裹一个组件来对它的 props 进行浅比较:

const Button = React.memo((props) => {
  // 你的组件
});

React.memo 等效于 PureComponent,但它只比较 props。(你也可以通过第二个参数指定一个自定义的比较函数来比较新旧 props。如果函数返回 true,就会跳过更新。)

React.memo 不比较 state,因为没有单一的 state 对象可供比较。但你也可以让子节点变为纯组件,或者 用useMemo优化每一个具体的子节点。

如何惰性创建昂贵的对象?

第一个常见的使用场景是当创建初始 state 很昂贵时,为避免重新创建被忽略的初始 state,我们可以传一个函数给 useState,React 只会在首次渲染时调用这个函数

function Table(props) {
  // createRows() 只会被调用一次
  const [rows, setRows] = useState(() => createRows(props.count));
  // ...
}

你或许也会偶尔想要避免重新创建 useRef() 的初始值。useRef 不会像 useState 那样接受一个特殊的函数重载。相反,你可以编写你自己的函数来创建并将其设为惰性的:

function Image(props) {
  const ref = useRef(null);

  //  IntersectionObserver 只会被惰性创建一次
  function getObserver() {
    let observer = ref.current;
    if (observer !== null) {
      return observer;
    }
    let newObserver = new IntersectionObserver(onIntersect);
    ref.current = newObserver;
    return newObserver;
  }

  // 当你需要时,调用 getObserver()
  // ...
}
Hook 会因为在渲染时创建函数而变慢吗?

不会。在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的差别。
除此之外,可以认为 Hook 的设计在某些方面更加高效:

Hook 避免了 class 需要的额外开支,像是创建类实例和在构造函数中绑定事件处理器的成本。

符合语言习惯的代码在使用 Hook 时不需要很深的组件树嵌套。这个现象在使用高阶组件、render props、和 context 的代码库中非常普遍。组件树小了,React 的工作量也随之减少。

传统上认为,在 React 中使用内联函数对性能的影响,与每次渲染都传递新的回调会如何破坏子组件的 shouldComponentUpdate 优化有关。Hook 从三个方面解决了这个问题。

useCallback Hook 允许你在重新渲染之间保持对相同的回调引用以使得 shouldComponentUpdate 继续工作:

useMemo Hook 使控制具体子节点何时更新变得更容易,减少了对纯组件的需要。

最后,useReducer Hook 减少了对深层传递回调的需要,就如下面解释的那样。

如何避免向下传递回调?

在大型的组件树中,我们推荐的替代方案是通过 contextuseReducer 往下传一个 dispatch 函数:

const TodosDispatch = React.createContext(null);

function TodosApp() {
  // 提示:`dispatch` 不会在重新渲染之间变化
  const [todos, dispatch] = useReducer(todosReducer);

  return (
    
      
    
  );
}

TodosApp 内部组件树里的任何子节点都可以使用 dispatch 函数来向上传递 actions

function DeepChild(props) {
  // 如果我们想要执行一个 action,我们可以从 context 中获取 dispatch。
  const dispatch = useContext(TodosDispatch);

  function handleClick() {
    dispatch({ type: "add", text: "hello" });
  }

  return ;
}

总而言之,从维护的角度来这样看更加方便(不用不断转发回调),同时也避免了回调的问题。像这样向下传递 dispatch 是处理深度更新的推荐模式。

React 是如何把对 Hook 的调用和组件联系起来的?

React 保持对当先渲染中的组件的追踪。多亏了 Hook 规范,我们得知 Hook 只会在 React 组件中被调用(或自定义 Hook —— 同样只会在 React 组件中被调用)。
每个组件内部都有一个「记忆单元格」列表。它们只不过是我们用来存储一些数据的 JavaScript 对象。当你用 useState() 调用一个 Hook 的时候,它会读取当前的单元格(或在首次渲染时将其初始化),然后把指针移动到下一个。这就是多个 useState() 调用会得到各自独立的本地 state 的原因。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/106505.html

相关文章

  • Mixinhooks,谈谈对React16.7.0-alpha中即将引入的hooks的理解

    摘要:已经被废除,具体缺陷可以参考二为了解决的缺陷,第二种解决方案是高阶组件简称。我们定义了父组件,存在自身的,并且将自身的通过的方式传递给了子组件。返回一个标识该的变量,以及更新该的方法。   为了实现分离业务逻辑代码,实现组件内部相关业务逻辑的复用,在React的迭代中针对类组件中的代码复用依次发布了Mixin、HOC、Render props等几个方案。此外,针对函数组件,在Reac...

    ZweiZhao 评论0 收藏0
  • Mixinhooks,谈谈对React16.7.0-alpha中即将引入的hooks的理解

    摘要:已经被废除,具体缺陷可以参考二为了解决的缺陷,第二种解决方案是高阶组件简称。我们定义了父组件,存在自身的,并且将自身的通过的方式传递给了子组件。返回一个标识该的变量,以及更新该的方法。   为了实现分离业务逻辑代码,实现组件内部相关业务逻辑的复用,在React的迭代中针对类组件中的代码复用依次发布了Mixin、HOC、Render props等几个方案。此外,针对函数组件,在Reac...

    funnyZhang 评论0 收藏0
  • Mixinhooks,谈谈对React16.7.0-alpha中即将引入的hooks的理解

    摘要:已经被废除,具体缺陷可以参考二为了解决的缺陷,第二种解决方案是高阶组件简称。我们定义了父组件,存在自身的,并且将自身的通过的方式传递给了子组件。返回一个标识该的变量,以及更新该的方法。   为了实现分离业务逻辑代码,实现组件内部相关业务逻辑的复用,在React的迭代中针对类组件中的代码复用依次发布了Mixin、HOC、Render props等几个方案。此外,针对函数组件,在Reac...

    wizChen 评论0 收藏0
  • React深入】MixinHOCHook

    摘要:与继承相比,装饰者是一种更轻便灵活的做法。它只是一种模式,这种模式是由自身的组合性质必然产生的。对比原生组件增强的项可操作所有传入的可操作组件的生命周期可操作组件的方法获取反向继承返回一个组件,继承原组件,在中调用原组件的。 导读 前端发展速度非常之快,页面和组件变得越来越复杂,如何更好的实现状态逻辑复用一直都是应用程序中重要的一部分,这直接关系着应用程序的质量以及维护的难易程度。 本...

    fox_soyoung 评论0 收藏0
  • React系列 --- 简单模拟语法(一)

    摘要:系列系列简单模拟语法一系列合成事件与二系列算法实现分析三系列从到再到四系列与部分源码解析五系列从使用了解的各种使用方案六前言我们先不讲什么语法原理先根据效果强行模拟语法使用实现一个简易版的第一步我们先用类 React系列 React系列 --- 简单模拟语法(一)React系列 --- Jsx, 合成事件与Refs(二)React系列 --- virtualdom diff算法实现分析...

    piglei 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<