oceania

oceania 查看完整档案

深圳编辑深圳信息职业技术学院  |  软件技术 编辑!!undefined  |  打杂 编辑 x.com 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

oceania 赞了文章 · 2019-08-14

中文输入法与React文本输入框的问题与解决方案

问题来源是来自这个React官方存储库的issue #3926,与这个议题关联的有很多其他的issue,来自许多项目,有些是与React相关,有些则是vue或其它JS套件。也已经有其他的项目是专注于解决这个问题,例如react-composition,不过它是一个使用ES5语法的React组件。在其他的讨论区上也有类似的问题与解答。本文的目的是希望能针对这个问题提供一些说明、现在暂时性的解决方案。

下图为目前解决React中"Controlled"(受控制的)input元件的演示,可以到这里去测试:

input元件的演示

注意事项: 目前的解决方案我认为是暂时性的,结果都放在这个github库上。这要分为"Controlled"(受控制的)与"Uncontrolled"(不受控制的)两个种类的组件,影响的主要是input与textarea两个组件,输入法(IME, input method editor)的问题,不只会发生在中文,同样的在日文、韩文或其它使用输入法的语言应该都有同样问题。

问题何来

React组件主要使用onChange人造事件,作为文本输入框(input)或文字输入区(textarea)触发文字输入时的事件,这个事件用起来很直觉,理应当是如此。但onChange在浏览器上,只要在这个文本输入框上,有任何的键盘动作它都会触发,也就是如果你是使用了中文、日文、韩文输入法(IME),不论是哪一种,拼音的、笔划的还是其他的,只要有按下一个键盘的动作,就会触发一次浏览器上这个元素的change事件,对于原本就使用键盘上的英文字符作为输入的语言来说,这没什么太大的问题,但对于要使用输入法的语言用户来说,不停的触发change事件,可能会造成程序功能上的运行逻辑问题。

举出一个实际的应用情况,一个使用React撰写的搜索计算机书籍的功能,用户可以在文本输入框里输入要搜索的书名,程序中是利用onChange事件触发,进行比对数据库中的书籍标题,当你想搜索一本名为"林哥的Java教程",第一个字为"林",拼音输入法需要输入"lin"三个键盘上的字符,在"林"这个字从输入法编辑器中加到真正的input元素前,onChange已经捕捉到"lin"三个字符,在列表中已搜索出一大堆有关"linux"的书籍。细节就不说了,还有可能对字符数量的的检查之类的问题。不过,这是正确的程序运作逻辑吗?很明显的这是一个大问题。

当然,你也可以用对中文字词检查的修正方式,或是干脆不要用change事件,改用其他按钮触发之类的事件来作这事情,或是不要用React中的"Controlled"(受控制的)input或textare组件,但这会局限住在程序开发应用上的自由,要如何选择就看你自己了,是不要使用它还是想办法正视问题来解决它。

网页上的DOM元素与"Uncontrolled"(不受控制的)的组件

这个问题在浏览器中,早就已经有了可应对的解决方法,DOM事件中有一组额外的CompositionEvent(组成事件)可以辅助开发者,它可以在可编辑的DOM元素上触发,主要是input与textarea上,所以可以用来辅助解决change事件的输入法问题。CompositionEvent(组成事件)共有三个事件,分别为compositionstartcompositionupdatecompositionend,它们代表的是开始进行字的组成、刷新与结束,也就是代表开始以输入法编辑器来组合键盘上的英文字符,选字或刷新字的组合,到最后输出字到真实DOM中的文本输入框中,实务上每个中文字在输入时,compositionstartcompositionend都只会会被触发一次,而compositionupdate则是有可能多次触发。

藉由CompositionEvent的辅助来解决的方式,也就是说在网页上的input元素,可以利用CompositionEvent作为一个信号,如果正在使用IME输入中文时,change事件中的代码就先不要运行,等compositionend触发时,接着的change事件才可以运行其中的代码,运作的原理就是这样简单而已。

在React应用中,如果是一个"Uncontrolled"(不受控制的)的input组件,它与网页上真实DOM中的input元素的事件行为无差异,也就是说,直接使用CompositionEvent的解决方式,就可以解决这个输入法的问题,以下面的代码为例子:

// @flow
import React from 'react'

const Cinput = (props: Object) => {
  // record if is on Composition
  let isOnComposition: boolean = false

  const handleComposition = (e: KeyboardEvent) => {
    if (e.type === 'compositionend') {
      // composition is end
      isOnComposition = false
    } else {
      // in composition
      isOnComposition = true
    }
  }

  const handleChange = (e: KeyboardEvent) => {
    // only when onComposition===false to fire onChange
    if (e.target instanceof HTMLInputElement && !isOnComposition) {
      props.onChange(e)
    }
  }

  return (
    <input
      {...props}
      onCompositionStart={handleComposition}
      onCompositionUpdate={handleComposition}
      onCompositionEnd={handleComposition}
      onChange={handleChange}
    />
  )
}

export default Cinput

上面这是一个典型的"Uncontrolled"(不受控制的)input组件,主要是它不用value这个属性。但如果它有来自上层组件的value属性与值,也就是上层组件用props传递给它value属性的值,就成了"Controlled"(受控制的)组件,它的事件整个模式就会与网页上的真实DOM中的input元素不一样,这后面再说明。

这个解决方案在几乎所有能支持CompositionEvent的浏览器(IE9以上)都可以运行得很好,不过在Google Chrome浏览器在2016年的版本53之后,更动了changecompositionend触发顺序,所以需要针对Chrome浏览器调整一下,如果是在Chrome浏览器中触发compositionend时,也要运行一次在原本在change要运行的代码,就改成这样而已。下面在上个代码中的handleComposition函数中,多加了侦测是否为Chrome浏览器,与触发原本的onChange方法代码,修改过的代码如下:

// detect it is Chrome browser?
const isChrome = !!window.chrome && !!window.chrome.webstore

const handleComposition = (e: KeyboardEvent) => {
  if (e.type === 'compositionend') {
    // composition is end
    isOnComposition = false

    // fixed for Chrome v53+ and detect all Chrome
    // https://chromium.googlesource.com/chromium/src/
    // +/afce9d93e76f2ff81baaa088a4ea25f67d1a76b3%5E%21/
    if (e.target instanceof HTMLInputElement && !isOnComposition && isChrome) {
      // fire onChange
      props.onChange(e)
    }
  } else {
    // in composition
    isOnComposition = true
  }
}

"Uncontrolled"(不受控制的)input或textarea组件,解决方式就是这么简单而已,利用CompositionEvent过滤掉不必要的change事件。

注: 其它的解决方式还有,像InputEvent中有一个isComposing属性,它也可以作为侦测目前是否正在进行输入法的组字工作,但InputEvent事件目前只有Firefox中可以用,看起来没什么前景。另外,W3C新提出的IME API或许是一个未来较佳的解决方案,但目前只有IE11 有实作,其他浏览器品牌都没有。

"Controlled"(受控制的)的组件

在React应用中,使用"Controlled"(受控制的)的input或textarea组件是另一回事,它会开始复杂起来。

"Controlled"(受控制的)的组件并不是只有加上value这个属性这么简单,input或textarea组件所呈现的值,主要会来自state,state有可能是上层组件的,利用props一层层传递过来的,或是这个组件中本身就有的state,直接赋给在这个组件中的render中的input或textarea组件。也就是说,input最后呈现的文字如果要进行改变,就需要改变到组件(不论在何处)的state,要改变state只有透过setState方法,而setState方法有可能是个异步(延时)运行的情况。

把这整个流程串接在一起后,我相信事件触发的不连续情况会变得很严重,需要对不同情况下作测试与评估。目前我所作的测试还只是最基本的组件运用而已,复杂的组件情况还没有开始进行。因为state有很多种用途,有时候内部使用,有时候要对外部用户输入介面的事件,或是有时候要对服务器端的数据接收或传送,不论是不是要使用Redux、MobX或Flux之类的state容器函数库或框架,最终要进行重新渲染的工作,还是得调用React中的setState方法才行。

在基本的测试时,我发现"Controlled"(受控制的)的input组件,它不仅事件触发不连续的情况严重,而且有可能在不同浏览器上会有不同的结果。完全不会有问题的只有一个浏览器,就是上面注释中所说的已经实作出IME API的IE11,IE11上可能根本不需要任何解决方案,它的输入法编辑器是独立于浏览器上的文本输入框之外的。

目前已测试的结果是有三种情况,"Chrome, Opera, IE, Edge"为一种,"Firefox"为一种,"Safari"为一种。我为这三种情况分别写了不同的解决方式的代码,但这个事件触发的不连续情况,现在无法有一致性的解决方案,我只能推测这大概可能是React内部设计的问题。

不论是三种的那一种解决方案,有一个重点是你不能像上面的一般性解决方案,阻挡change事件时要运行的代码,也就是阻挡setState变动state值,因为只要一经阻挡,input组件的value值就赋不到值,而且也不会触发重新渲染。所以你只能让change事件不断触发,就像往常一样。

那么要如何解决程序逻辑运作的问题?

我使用了另一个内部的state对象中的值,称为innerValue,它是对比在input组件上不断因触发change事件而输入的值,称为inputValueinnerValue是个会经过CompositionEvent修正过的值,所以它永远不会带有在输入法组字过程的字符串值。

这个解决方案,是一个"挂羊头卖狗肉"的用法,不论用户在input组件如何输入,输入的过程都会改变inputValue而已,inputValue是一个暂存与呈现用的值,最终用来进行程序逻辑运算的是innerValue。以最一开始的例子来说,用户输入"林哥的Java教程",在一开始的"林"字输入时,inputValue是从"lin"到输入完成变为"林",而innerValue是在输入期间是空字符串值,输入完成才会变为"林"。所以,搜索功能可以用innerValue来作为运算的依据,用这个值来搜索对应的数据,这才是正确的运算逻辑,因为innerValue才是真正的不带输入法组字过程的值。

大致上说明一下解决方式的代码,首先它有两个在这个模块作用域中的全局变量,一个用来记录是否在输入法的组字过程中,另一个是给专给Safari浏览器用的:

// if now is in composition session
let isOnComposition = false

// for safari use only, innervalue can't setState when compositionend occurred
let isInnerChangeFromOnChange = false

在专门处理change事件的handleChange方法中,判断isInnerChangeFromOnChange这一段是专门为了解决Safari浏览器的问题所写,Safari浏览器的行为是CompositionEvent在触发时,其中的event.target.value居然是组字过程中的英文字符,而不是触发这个事件的input元素的所有字符串,这也是特别怪异的地方,所以才会利用在compositionend后会再触发一次change的特性,在这里刷新innerValue

后面的代码,是代表在输入法的组字过程中,setState方法使用的差异,在组字过程中(isOnComposition === true)的话,只会更动inputValue值,而不会更动到innerValue的值,这对应了上述所说的一个运作过程,一般的输入键盘上的字符时不会有输入法的问题,则是两个值一并更动。代码如下:

handleChange = (e: Event) => {
   // console.log('change type ', e.type, ', target ', e.target, ', target.value ', e.target.value)

  // Flow check
  if (!(e.target instanceof HTMLInputElement)) return

  if (isInnerChangeFromOnChange) {
    this.setState({ inputValue: e.target.value, innerValue: e.target.value })
    isInnerChangeFromOnChange = false
    return
  }

  // when is on composition, change inputValue only
  // when not in composition change inputValue and innerValue both
  if (!isOnComposition) {
    this.setState({
      inputValue: e.target.value,
      innerValue: e.target.value,
    })
  } else {
    this.setState({ inputValue: e.target.value })
  }
}

在专门处理composition事件的handleComposition方法中,主要是为了在compositionend触发时,进行刷新innerValue所撰写的一些代码。在第一种情况时,也就是在Chrome, IE, Edge, Opera浏览器时,只需要直接用e.target.value刷新innerValue即可。在第二种情况是Firefox,它不知道为什么会掉值,所以还需要帮它再一并刷新innerValue一次。第三种情况,上面有说过了,特别的怪异情况,所以对innerValue的刷新改到compositionend之后的那个change事件去作了。代码如下:

handleComposition = (e: Event) => {
   // console.log('type ', e.type, ', target ', e.target, ',target.value ', e.target.value, ', data', e.data)

   // Flow check
  if (!(e.target instanceof HTMLInputElement)) return

  if (e.type === 'compositionend') {
    // Chrome is ok for only setState innerValue
    // Opera, IE and Edge is like Chrome
    if (isChrome || isIE || isEdge || isOpera) {
      this.setState({ innerValue: e.target.value })
    }

    // Firefox need to setState inputValue again...
    if (isFirefox) {
      this.setState({ innerValue: e.target.value, inputValue: e.target.value })
    }

    // Safari think e.target.value in composition event is keyboard char,
    //  but it will fire another change after compositionend
    if (isSafari) {
       // do change in the next change event
      isInnerChangeFromOnChange = true
    }

    isOnComposition = false
  } else {
    isOnComposition = true
  }
}

注: 目前这个暂时的解决方式,其方式并不是参考自react-composition项目,解决方式虽然有些类似,但react-composition用的是ES5的React工厂样式组件语法,我对这种语法并不熟悉。在写这篇文档时,才仔细看了一下react-composition的代码,只能说它的作者实际上也有测试过这个问题,也知道只有用另一个state中的值才能解决这问题。

总结

如果你是使用"Uncontrolled"(不受控制的)的组件,那么解决方法很简单,就如同上面所说的,像一般的网页上的DOM元素的解决方式即可。

但对于"Controlled"(受控制的)的组件来说,目前的解决方案是一种try-and-error(试误法)的暂时性解决方案,我目前只能按照已测试的平台与浏览器去修正,没测过的浏览器与平台,就不得而知了。

关于这个"Controlled"(受控制的)的组件的事件触发,目前看到有在不同浏览器上的事件触发不连续情况,我也有发一个议题(Issue)给React官方。或许比较好的治本方案,是需要从state更动方式的内部代码,或是人造事件触发的顺序,进行一些调整,这超出我的能力范围,就有待开发团队的回应了。

最后,如果你正好有需要到这个功能,或是你认为这个功能有需要,你可以帮忙测试看看或是提供一些建议。我已经把所有的代码、演示、线上测试、解决方案都集中到这个Github库的react-compositionevent中。或许你现在需要一个解决方案,你可以用里面目前的暂时性解决方式试试也可以。

查看原文

赞 39 收藏 40 评论 4

oceania 赞了回答 · 2019-06-12

解决正确设置了charles,使用charles抓包某些app,无法抓到请求,这是为什么呢?

楼主,你这个问题我亲自下载了APP来研究,终究抓出来了。
来吧。看看我的解析过程。
我首先也抱着将信将疑的态度 打开了Charles.改变我的手机WiFi代理到我的电脑,端口到8888。
然后打开APP以后、下拉刷新,果然没数据。
然后凭感觉,我觉得他是走的TCP/UDP协议。
我先试试先。然后我换了个抓包工具CocoaPacketAnalyzer,这个软件抓包工具能抓TCP/UDP协议的包。
然后关掉了手机的代理,Mac开了个共享WIFI,方便走Mac的网卡进行通讯,这样我就能抓到包了、
点击Start开始抓,然后APP这边下拉刷新,为了准确的抓到数据,我下拉刷新了3次。
然后Stop停止抓包,开始分析。
吼吼吼。慢慢找呀找,一条一条的找。
终于找到了图例
然后我要开始验证了。
图片描述

这不就抓出来了。。。。

附上URL:点击这里打开URL

(可能你打开的时候请求不出来数据,因为token失效或者有IP限制,你还是按照我上面写的自己抓出来吧~)

关注 7 回答 3

oceania 赞了文章 · 2019-05-22

30分钟精通React今年最劲爆的新特性——React Hooks

你还在为该使用无状态组件(Function)还是有状态组件(Class)而烦恼吗?
——拥有了hooks,你再也不需要写Class了,你的所有组件都将是Function。

你还在为搞不清使用哪个生命周期钩子函数而日夜难眠吗?
——拥有了Hooks,生命周期钩子函数可以先丢一边了。

你在还在为组件中的this指向而晕头转向吗?
——既然Class都丢掉了,哪里还有this?你的人生第一次不再需要面对this。

这样看来,说React Hooks是今年最劲爆的新特性真的毫不夸张。如果你也对react感兴趣,或者正在使用react进行项目开发,答应我,请一定抽出至少30分钟的时间来阅读本文好吗?所有你需要了解的React Hooks的知识点,本文都涉及到了,相信完整读完后你一定会有所收获。

一个最简单的Hooks

首先让我们看一下一个简单的有状态组件:

class Example 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>
    );
  }
}

我们再来看一下使用hooks后的版本:

import { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

是不是简单多了!可以看到,Example变成了一个函数,但这个函数却有自己的状态(count),同时它还可以更新自己的状态(setCount)。这个函数之所以这么了不得,就是因为它注入了一个hook--useState,就是这个hook让我们的函数变成了一个有状态的函数。

除了useState这个hook外,还有很多别的hook,比如useEffect提供了类似于componentDidMount等生命周期钩子的功能,useContext提供了上下文(context)的功能等等。

Hooks本质上就是一类特殊的函数,它们可以为你的函数型组件(function component)注入一些特殊的功能。咦?这听起来有点像被诟病的Mixins啊?难道是Mixins要在react中死灰复燃了吗?当然不会了,等会我们再来谈两者的区别。总而言之,这些hooks的目标就是让你不再写class,让function一统江湖。

React为什么要搞一个Hooks?

想要复用一个有状态的组件太麻烦了!

我们都知道react都核心思想就是,将一个页面拆成一堆独立的,可复用的组件,并且用自上而下的单向数据流的形式将这些组件串联起来。但假如你在大型的工作项目中用react,你会发现你的项目中实际上很多react组件冗长且难以复用。尤其是那些写成class的组件,它们本身包含了状态(state),所以复用这类组件就变得很麻烦。

那之前,官方推荐怎么解决这个问题呢?答案是:渲染属性(Render Props)高阶组件(Higher-Order Components)。我们可以稍微跑下题简单看一下这两种模式。

渲染属性指的是使用一个值为函数的prop来传递需要动态渲染的nodes或组件。如下面的代码可以看到我们的DataProvider组件包含了所有跟状态相关的代码,而Cat组件则可以是一个单纯的展示型组件,这样一来DataProvider就可以单独复用了。

import Cat from 'components/cat'
class DataProvider extends React.Component {
  constructor(props) {
    super(props);
    this.state = { target: 'Zac' };
  }

  render() {
    return (
      <div>
        {this.props.render(this.state)}
      </div>
    )
  }
}

<DataProvider render={data => (
  <Cat target={data.target} />
)}/>

虽然这个模式叫Render Props,但不是说非用一个叫render的props不可,习惯上大家更常写成下面这种:

...
<DataProvider>
  {data => (
    <Cat target={data.target} />
  )}
</DataProvider>

高阶组件这个概念就更好理解了,说白了就是一个函数接受一个组件作为参数,经过一系列加工后,最后返回一个新的组件。看下面的代码示例,withUser函数就是一个高阶组件,它返回了一个新的组件,这个组件具有了它提供的获取用户信息的功能。

const withUser = WrappedComponent => {
  const user = sessionStorage.getItem("user");
  return props => <WrappedComponent user={user} {...props} />;
};

const UserPage = props => (
  <div class="user-container">
    <p>My name is {props.user}!</p>
  </div>
);

export default withUser(UserPage);

以上这两种模式看上去都挺不错的,很多库也运用了这种模式,比如我们常用的React Router。但我们仔细看这两种模式,会发现它们会增加我们代码的层级关系。最直观的体现,打开devtool看看你的组件层级嵌套是不是很夸张吧。这时候再回过头看我们上一节给出的hooks例子,是不是简洁多了,没有多余的层级嵌套。把各种想要的功能写成一个一个可复用的自定义hook,当你的组件想用什么功能时,直接在组件里调用这个hook即可。

图片描述

生命周期钩子函数里的逻辑太乱了吧!

我们通常希望一个函数只做一件事情,但我们的生命周期钩子函数里通常同时做了很多事情。比如我们需要在componentDidMount中发起ajax请求获取数据,绑定一些事件监听等等。同时,有时候我们还需要在componentDidUpdate做一遍同样的事情。当项目变复杂后,这一块的代码也变得不那么直观。

classes真的太让人困惑了!

我们用class来创建react组件时,还有一件很麻烦的事情,就是this的指向问题。为了保证this的指向正确,我们要经常写这样的代码:this.handleClick = this.handleClick.bind(this),或者是这样的代码:<button onClick={() => this.handleClick(e)}>。一旦我们不小心忘了绑定this,各种bug就随之而来,很麻烦。

还有一件让我很苦恼的事情。我在之前的react系列文章当中曾经说过,尽可能把你的组件写成无状态组件的形式,因为它们更方便复用,可独立测试。然而很多时候,我们用function写了一个简洁完美的无状态组件,后来因为需求变动这个组件必须得有自己的state,我们又得很麻烦的把function改成class。

在这样的背景下,Hooks便横空出世了!

什么是State Hooks?

回到一开始我们用的例子,我们分解来看到底state hooks做了什么:

import { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

声明一个状态变量

import { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);

useState是react自带的一个hook函数,它的作用就是用来声明状态变量。useState这个函数接收的参数是我们的状态初始值(initial state),它返回了一个数组,这个数组的第[0]项是当前当前的状态值,第[1]项是可以改变状态值的方法函数。

所以我们做的事情其实就是,声明了一个状态变量count,把它的初始值设为0,同时提供了一个可以更改count的函数setCount。

上面这种表达形式,是借用了es6的数组解构(array destructuring),它可以让我们的代码看起来更简洁。不清楚这种用法的可以先去看下我的这篇文章30分钟掌握ES6/ES2015核心内容(上)

如果不用数组解构的话,可以写成下面这样。实际上数组解构是一件开销很大的事情,用下面这种写法,或者改用对象解构,性能会有很大的提升。具体可以去这篇文章的分析Array destructuring for multi-value returns (in light of React hooks),这里不详细展开,我们就按照官方推荐使用数组解构就好。

let _useState = useState(0);
let count = _useState[0];
let setCount = _useState[1];

读取状态值

<p>You clicked {count} times</p>

是不是超简单?因为我们的状态count就是一个单纯的变量而已,我们再也不需要写成{this.state.count}这样了。

更新状态

  <button onClick={() => setCount(count + 1)}>
    Click me
  </button>

当用户点击按钮时,我们调用setCount函数,这个函数接收的参数是修改过的新状态值。接下来的事情就交给react了,react将会重新渲染我们的Example组件,并且使用的是更新后的新的状态,即count=1。这里我们要停下来思考一下,Example本质上也是一个普通的函数,为什么它可以记住之前的状态?

一个至关重要的问题

这里我们就发现了问题,通常来说我们在一个函数中声明的变量,当函数运行完成后,这个变量也就销毁了(这里我们先不考虑闭包等情况),比如考虑下面的例子:

function add(n) {
    const result = 0;
    return result + 1;
}

add(1); //1
add(1); //1

不管我们反复调用add函数多少次,结果都是1。因为每一次我们调用add时,result变量都是从初始值0开始的。那为什么上面的Example函数每次执行的时候,都是拿的上一次执行完的状态值作为初始值?答案是:是react帮我们记住的。至于react是用什么机制记住的,我们可以再思考一下。

假如一个组件有多个状态值怎么办?

首先,useState是可以多次调用的,所以我们完全可以这样写:

function ExampleWithManyStates() {
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);

其次,useState接收的初始值没有规定一定要是string/number/boolean这种简单数据类型,它完全可以接收对象或者数组作为参数。唯一需要注意的点是,之前我们的this.setState做的是合并状态后返回一个新状态,而useState是直接替换老状态后返回新状态。最后,react也给我们提供了一个useReducer的hook,如果你更喜欢redux式的状态管理方案的话。

从ExampleWithManyStates函数我们可以看到,useState无论调用多少次,相互之间是独立的。这一点至关重要。为什么这么说呢?

其实我们看hook的“形态”,有点类似之前被官方否定掉的Mixins这种方案,都是提供一种“插拔式的功能注入”的能力。而mixins之所以被否定,是因为Mixins机制是让多个Mixins共享一个对象的数据空间,这样就很难确保不同Mixins依赖的状态不发生冲突。

而现在我们的hook,一方面它是直接用在function当中,而不是class;另一方面每一个hook都是相互独立的,不同组件调用同一个hook也能保证各自状态的独立性。这就是两者的本质区别了。

react是怎么保证多个useState的相互独立的?

还是看上面给出的ExampleWithManyStates例子,我们调用了三次useState,每次我们传的参数只是一个值(如42,‘banana’),我们根本没有告诉react这些值对应的key是哪个,那react是怎么保证这三个useState找到它对应的state呢?

答案是,react是根据useState出现的顺序来定的。我们具体来看一下:

  //第一次渲染
  useState(42);  //将age初始化为42
  useState('banana');  //将fruit初始化为banana
  useState([{ text: 'Learn Hooks' }]); //...

  //第二次渲染
  useState(42);  //读取状态变量age的值(这时候传的参数42直接被忽略)
  useState('banana');  //读取状态变量fruit的值(这时候传的参数banana直接被忽略)
  useState([{ text: 'Learn Hooks' }]); //...

假如我们改一下代码:

let showFruit = true;
function ExampleWithManyStates() {
  const [age, setAge] = useState(42);
  
  if(showFruit) {
    const [fruit, setFruit] = useState('banana');
    showFruit = false;
  }
 
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);

这样一来,

  //第一次渲染
  useState(42);  //将age初始化为42
  useState('banana');  //将fruit初始化为banana
  useState([{ text: 'Learn Hooks' }]); //...

  //第二次渲染
  useState(42);  //读取状态变量age的值(这时候传的参数42直接被忽略)
  // useState('banana');  
  useState([{ text: 'Learn Hooks' }]); //读取到的却是状态变量fruit的值,导致报错

鉴于此,react规定我们必须把hooks写在函数的最外层,不能写在ifelse等条件语句当中,来确保hooks的执行顺序一致。

什么是Effect Hooks?

我们在上一节的例子中增加一个新功能:

import { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // 类似于componentDidMount 和 componentDidUpdate:
  useEffect(() => {
    // 更新文档的标题
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

我们对比着看一下,如果没有hooks,我们会怎么写?

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

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

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

我们写的有状态组件,通常会产生很多的副作用(side effect),比如发起ajax请求获取数据,添加一些监听的注册和取消注册,手动修改dom等等。我们之前都把这些副作用的函数写在生命周期函数钩子里,比如componentDidMount,componentDidUpdate和componentWillUnmount。而现在的useEffect就相当与这些声明周期函数钩子的集合体。它以一抵三。

同时,由于前文所说hooks可以反复多次使用,相互独立。所以我们合理的做法是,给每一个副作用一个单独的useEffect钩子。这样一来,这些副作用不再一股脑堆在生命周期钩子里,代码变得更加清晰。

useEffect做了什么?

我们再梳理一遍下面代码的逻辑:

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

首先,我们声明了一个状态变量count,将它的初始值设为0。然后我们告诉react,我们的这个组件有一个副作用。我们给useEffecthook传了一个匿名函数,这个匿名函数就是我们的副作用。在这个例子里,我们的副作用是调用browser API来修改文档标题。当react要渲染我们的组件时,它会先记住我们用到的副作用。等react更新了DOM之后,它再依次执行我们定义的副作用函数。

这里要注意几点:
第一,react首次渲染和之后的每次渲染都会调用一遍传给useEffect的函数。而之前我们要用两个声明周期函数来分别表示首次渲染(componentDidMount),和之后的更新导致的重新渲染(componentDidUpdate)。

第二,useEffect中定义的副作用函数的执行不会阻碍浏览器更新视图,也就是说这些函数是异步执行的,而之前的componentDidMount或componentDidUpdate中的代码则是同步执行的。这种安排对大多数副作用说都是合理的,但有的情况除外,比如我们有时候需要先根据DOM计算出某个元素的尺寸再重新渲染,这时候我们希望这次重新渲染是同步发生的,也就是说它会在浏览器真的去绘制这个页面前发生。

useEffect怎么解绑一些副作用

这种场景很常见,当我们在componentDidMount里添加了一个注册,我们得马上在componentWillUnmount中,也就是组件被注销之前清除掉我们添加的注册,否则内存泄漏的问题就出现了。

怎么清除呢?让我们传给useEffect的副作用函数返回一个新的函数即可。这个新的函数将会在组件下一次重新渲染之后执行。这种模式在一些pubsub模式的实现中很常见。看下面的例子:

import { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

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

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // 一定注意下这个顺序:告诉react在下次重新渲染组件之后,同时是下次调用ChatAPI.subscribeToFriendStatus之前执行cleanup
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

这里有一个点需要重视!这种解绑的模式跟componentWillUnmount不一样。componentWillUnmount只会在组件被销毁前执行一次而已,而useEffect里的函数,每次组件渲染后都会执行一遍,包括副作用函数返回的这个清理函数也会重新执行一遍。所以我们一起来看一下下面这个问题。

为什么要让副作用函数每次组件更新都执行一遍?

我们先看以前的模式:

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

很清除,我们在componentDidMount注册,再在componentWillUnmount清除注册。但假如这时候props.friend.id变了怎么办?我们不得不再添加一个componentDidUpdate来处理这种情况:

...
  componentDidUpdate(prevProps) {
    // 先把上一个friend.id解绑
    ChatAPI.unsubscribeFromFriendStatus(
      prevProps.friend.id,
      this.handleStatusChange
    );
    // 再重新注册新但friend.id
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
...

看到了吗?很繁琐,而我们但useEffect则没这个问题,因为它在每次组件更新后都会重新执行一遍。所以代码的执行顺序是这样的:

1.页面首次渲染
2.替friend.id=1的朋友注册

3.突然friend.id变成了2
4.页面重新渲染
5.清除friend.id=1的绑定
6.替friend.id=2的朋友注册
...

怎么跳过一些不必要的副作用函数

按照上一节的思路,每次重新渲染都要执行一遍这些副作用函数,显然是不经济的。怎么跳过一些不必要的计算呢?我们只需要给useEffect传第二个参数即可。用第二个参数来告诉react只有当这个参数的值发生改变时,才执行我们传的副作用函数(第一个参数)。

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 只有当count的值发生变化时,才会重新执行`document.title`这一句

当我们第二个参数传一个空数组[]时,其实就相当于只在首次渲染的时候执行。也就是componentDidMount加componentWillUnmount的模式。不过这种用法可能带来bug,少用。

还有哪些自带的Effect Hooks?

除了上文重点介绍的useState和useEffect,react还给我们提供来很多有用的hooks:

useContext
useReducer
useCallback
useMemo
useRef
useImperativeMethods
useMutationEffect
useLayoutEffect

我不再一一介绍,大家自行去查阅官方文档。

怎么写自定义的Effect Hooks?

为什么要自己去写一个Effect Hooks? 这样我们才能把可以复用的逻辑抽离出来,变成一个个可以随意插拔的“插销”,哪个组件要用来,我就插进哪个组件里,so easy!看一个完整的例子,你就明白了。

比如我们可以把上面写的FriendStatus组件中判断朋友是否在线的功能抽出来,新建一个useFriendStatus的hook专门用来判断某个id是否在线。

import { useState, useEffect } from 'react';

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;
}

这时候FriendStatus组件就可以简写为:

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

简直Perfect!假如这个时候我们又有一个朋友列表也需要显示是否在线的信息:

function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

简直Fabulous!

结尾

不知道你阅读完整篇文章的感受如何,或者对hooks有任何角度的看法和思考都欢迎在评论区一起讨论。另外如果你有换工作的打算,我们部门真的很缺人,欢迎私信勾搭~(阿里巴巴,base在深圳的部门)

查看原文

赞 196 收藏 114 评论 10

oceania 关注了标签 · 2019-04-25

react.js

React (sometimes styled React.js or ReactJS) is an open-source JavaScript library for creating user interfaces that aims to address challenges encountered in developing single-page applications. It is maintained by Facebook, Instagram and a community of individual developers and corporations.

关注 69849

oceania 回答了问题 · 2019-03-18

解决求解一道排序算法题

let arr = [1, 2, 3, 4, 5, 6, 6, 7, 8, 9, 10, 10, 10, 11, 12, 12, 4, 5]

//结果二维数组
let result = [[]]
//结果二维数组最后的索引
let resultLastIndex = 0
//当前 不同 0 / 相同 1 标示
let currentFlag = 0

arr.forEach((item, index) => {
  let r = result[resultLastIndex]
  let compare = r[r.length - 1]
  if (item !== compare) {
    if (currentFlag === 0) {
      r.push(item)
    } else {
      currentFlag = 0
      result.push([arr[index - 1], item])
      resultLastIndex++
    }
  } else {
    if (currentFlag === 0) {
      currentFlag = 1
      result.push([arr[index - 1], item])
      resultLastIndex++
    } else {
      r.push(item)
    }
  }
})

console.log(result)

关注 7 回答 6

oceania 关注了标签 · 2019-03-13

es6

ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

标准的制定者有计划,以后每年发布一次标准,使用年份作为版本。因为 ES6 的第一个版本是在 2015 年发布的,所以又称ECMAScript 2015(简称 ES2015)。

2016 年 6 月,小幅修订的《ECMAScript 2016 标准》(简称 ES2016)如期发布。由于变动非常小(只新增了数组实例的includes方法和指数运算符),因此 ES2016 与 ES2015 基本上是同一个标准,都被看作是 ES6。根据计划,2017 年 6 月将发布 ES2017。

标准请参读 ECMAScript® 2015 Language Specification

关注 2666

oceania 回答了问题 · 2019-03-12

解决箭头函数this指向的问题

你可以理解成箭头函数写在哪里,就获取哪里的this
这里明显就直接读取foo的执行环境中的this,而foo被你强制绑定到{ id: 42 }上,所以this自然指向这个对象。

关注 19 回答 17

oceania 回答了问题 · 2019-03-12

解决用const 声明loadMore,怎么会这么引用?不能是变量提升吧

回调推进异步任务队列中了,整体同步代码跑完才会执行,所以访问得到,不是变量提升。

关注 4 回答 2

oceania 回答了问题 · 2019-03-11

解决js判断两个数组的值是否相等 值的顺序可能不同

给你写严格点

function compare(arr1, arr2) {
    //临时数组,用于对比删除项
    let tempArr = [...arr2]
    if (arr1 instanceof Array && arr2 instanceof Array) {
        return arr1.length == arr2.length && arr1.every((item_1) => {
            //临时数组需要删除的索引
            let delIndex = 0
            //是否对比成功
            let result = false
            if (isNaN(item_1)) {
                result = tempArr.some((item_2, index) => {
                    if (isNaN(item_2)) {
                        delIndex = index
                        return true
                    }
                    return false
                })
            } else {
                delIndex = tempArr.indexOf(item_1)
                result = delIndex != -1
            }
            tempArr.splice(delIndex, 1)
            return result
        })
    } else {
        return false
    }
}

var obj = {}
var arr = []
var nan = NaN

var a = [1, 2, 3, obj, arr, nan]
var b = [obj, arr, 3, 2, 1, nan]

console.log(compare(a, b))

关注 8 回答 6

oceania 回答了问题 · 2019-03-07

解决angualr4中html怎么向自定义管道传入两个变量

{{ 要转化的值 | attackType:data.aa:data.bb }}

transform(value,param1,param2){}

value取到的是要转化的值,param1param2取到你传的参数data.aadata.bb

关注 2 回答 1

认证与成就

  • 获得 165 次点赞
  • 获得 87 枚徽章 获得 4 枚金徽章, 获得 26 枚银徽章, 获得 57 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-01-30
个人主页被 2.4k 人浏览