int object is not iterable_为啥 useState 返回的是 array 而不是 object 呢?

19bee2197dde927a66489ceaa2568ffb.png

问题

在 react hook 中常用的 useState 和 useRef 的用法如下:

const [count, setCount] = useState(0)
const ref = useRef()

为啥 useState 执行后会返回一个 array, 而 useRef 是 object 呢?为啥不把 useState 设计为返回 object 呢?我们在自定义 hooks 时应该返回什么类型呢?

const { count, setCount } = useState(0)

原因

要回答这个问题得弄明白 ES6 的解构赋值(destructring assignment)语法 , 来看 2 个简单的示例:

  • 数组的解构赋值:
const foo = ['one', 'two', 'three'];

const [red, yellow, green] = foo;
console.log(red); // "one"
console.log(yellow); // "two"
console.log(green); // "three"
  • 对象的解构赋值
const user = {
    id: 42,
    is_verified: true
};

const { id, is_verified } = user;

console.log(id); // 42
console.log(is_verified); // true 

搞清楚了解构赋值,那上面的问题就比较好解释了。

如果 useState 返回数组,那么你可以顺便对数组中的变量命名,代码看起来也比较干净。而如果是对象的话返回的值必须和 useState 内部实现返回的对象同名,这样你只能在 function component 中使用一次,想要多次使用 useState 必须得重命名返回值。

// 第一次使用
const { state, setState } = useState(false)
// 第二次使用
const { state: counter, setState: setCounter} = useState(0)

当然事情总是有两面性的,使用 array 也存在一些问题:

  • 返回值强顺序,灵活性比较低。array[0] 为值,array[1] 为改变值的方法。
  • 返回的值基本都得使用,对于有些返回值不想使用的话代码看起来有些怪,比如只想用 setState, 就得这么写:const [, setState] = useState(false)
  • 返回的参数不能太多,否则处理上面 2 个场景会很麻烦。例如 Apollo useQuery 返回的参数比较多,就采用了 object。

如果遇到以上几个问题的时候,不妨试试返回 object。

简单总结一下

在自定义 hook 的时候可以遵循一个简单原则:当参数大于 2 个的时候返回值的类型返回 object, 否则返回数组。

两者都支持

小孩子才做选择,成年人都要。接下来我们看一下怎么让我们自定义的 hook 既能支持 array 也能支持 object。

让 object 行为像 array 一样

数组是由带有数字下标和一些 prototype 组成的对象,我们在 object 中来模拟一下 array:

const data = {
  foo: 'foo',
  bar: 'bar',
  0: 'foo',
  1: 'bar',
}

let { foo, bar } = data
let [ foo, bar ] = data // ERROR!

当我们来解构数组的时候会抛如下错误:

Uncaught TypeError: data is not iterable

所以,要想让对象具有数组的特性,必须得让对象具有 iterable 特性,也就是可迭代性。

在 ES6 中提供了 Symbol.iterable 可以设置一个对象具有可迭代性,

const data = {
  foo: 'foo',
  bar: 'bar',
  *[Symbol.iterator]() {
    yield 'foo'
    yield 'bar'
  },
}

let { foo, bar } = data
let [ foo, bar ] = data

这个似乎看起来没啥问题,但是当我们对 data 进行解构时,发现是这样的:

let { foo, ...rest } = data

// rest
{
  bar: 'bar',
  Symbol(Symbol.iterator): ƒ*
}

rest 中会包含 Symbol.iterator 额外属性。JS 中能被 解构出来的属性必须是可枚举(enumerable)的,那只要我们将 Symbol.iterator 的 enumerable 设置为 false 就会让其不出现在 rest 中。我们可以使用 Object.difineProperty 来设置。

const data = {
  foo: 'foo',
  bar: 'bar',
}

Object.defineProperty(data, Symbol.iterator, {
  enumerable: false,
  value: function*() {
    yield 'foo'
    yield 'bar'
  },
})

现在就可以将其他额外的属性给隐藏了

let { foo, ...rest } = data

// rest
{
  bar: 'bar'
}

在对数组进行解构时会去执行 Symbol.iterable() 这个函数,该函数返回一个 next 函数。这里我们借助 Generator 的特性,他会返回 next 方法,next 方法又返回值 value 和状态 done。

不使用 Generator

Object.defineProperty(obj, Symbol.iterator, {
  enumerable: false,
  value() {
    let index = 0
    let arr = [foo, bar]
    return {
      next: () => ({
        value: arr[index++],
        done: index > arr.length,
      })
    }
  }
})

最终完整的代码示例:

function createIsomorphicDestructurable(obj, arr) {
  
  const clone = { ...obj }
  
  Object.defineProperty(clone, Symbol.iterator, {
    enumerable: false,
    value() {
      let index = 0
      return {
        next: () => ({
          value: arr[index++],
          done: index > arr.length,
        })
      }
    }
  })

  return clone
}

使用方法

const foo = { name: 'foo' }
const bar = 1024

const obj = createIsomorphicDestructurable(
  { foo, bar },
  [ foo, bar ]
)

let { foo: foo1, bar: bar1 } = obj // foo1 的值为: { name: 'foo'}, bar1 的值为:1024
let [ foo2, bar2 ] = obj // foo2 的值为: { name: 'foo'}, bar2 的值为:1024

总结

在你的 hook 中能将这两种类型都提供给用户自然是最好的,这样用户可以按照自己习惯来自由选择采用哪种方式。但如果不想这么麻烦的话还是建议采用上面说的原则:参数小于 3 个时用 array, 相反用 object。

【完】

参考:

https://www.reddit.com/r/reactjs/comments/drltaq/why_do_hooks_return_an_array_rather_an_object/​www.reddit.com https://antfu.me/posts/destructuring-with-object-or-array/​antfu.me
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值