使用 React Hooks 重构你的小程序

做者:余澈html

背景

一直关注小程序开发的朋友应该会注意到,最开始小程序就是为了微型创新型业务打造的一个框架,最多只能运行 1m 的包。但是后来发现不少厂商把愈来愈多的业务搬到了小程序上,小程序的能力也在不断地开放,变得愈来愈强大。因而后来打包限制上升到了 2m,而后引入了分包,如今已经已经能够上传 8m 的小程序。其实这个体积已经能够实现很是巨型很是复杂的业务了。就从 Taro 的用户来看,例如京东购物小程序和 58 同城小程序无论从代码的数量仍是复杂度都不亚于 PC 端业务,因此咱们能够说前端开发的复杂度正在向小程序端转移。前端

而小程序开发其实也是前端开发的一个子集,在整个前端业界,咱们又是怎么解决复杂度这个问题的呢?vue

首先咱们看看 React:React Core Team 成员,同时也是 Redux 的做者 Dan Abramov 在 2018 年的 ReactConf 向你们首次介绍了 React Hooks。React Hooks 是为了解决 Class Component 的一些问题而引入的:react

  • Class Component 组件间的逻辑难以复用。由于 JavaScript 不像 Go 或 C++ 同样,Class 能够多重继承,类的逻辑的复用就成了一个问题;
  • 复杂组件难以理解。Class Component 常常会在生命周期作一些数据获取事件监听的反作用函数,这样的状况下咱们就很难把组件拆分为更小的力度;
  • Class 使人迷惑。不少新手应该会被 Class 组件绑定事件的 this 迷惑过,绑定事件能够用 bind,能够直接写箭头函数,也能够写类属性函数,但到底哪一种方法才是最好的呢?而到了 ES 2018,class 还有多种语法,例如装饰器,例如 private fileds 这些奇奇怪怪的语法也为新手增长了更多的困惑。

而对于 Vue 而言也有相同的问题,Vue 的做者尤雨溪老师在 VueConf China 2019 也给 Vue 3.0 引入了一个叫 Functional-based API 的概念,它是受 React Hooks 启发而增长的新 API。因为 Vue 2.0 组件组合的模式是对象字面量形式,因此 Functional-based API 能够做为 Mixins 的替代,配合新的响应式 API 做为新的组件组合模式。那么对于 Vue 3.0 咱们还知之甚少,之后的 API 也有可能改变,但或许是英雄所见略同,React 和 Vue 对于下降前端开发复杂度这一问题都不约而同地选择了 Hooks 这一方案,这究竟是为何呢?vue-cli

why_hooks.png

咱们能够一下以前的组件组合方案,首先是 Mixins,红色圈的 Mixins,黄色的是组件,咱们知道 Mixins 其实就是把多个对象组合成一个对象,Mixins 的过程就有点像调用 Object.assgin 方法。那 Mixins 有什么问题呢?首先是命名空间耦合,若是多个对象同名参数,这些参数就会耦合在一块儿;其次因为 Mixins 必须是运行时才能知道具体有什么参数,因此是 TypeScript 是没法作静态检查的;第三是组件参数不清晰,在 Mixins 中组件的 props 和其余参数没什么两样,很容易被其它的 Mixins 覆盖掉。typescript

为了解决 Mixins 的问题,后来发展出了高阶组件(HOC)的方式,高阶组件就和图里同样,一个组件嵌套着另外的组件。它的确解决了 Mixins 的一些问题,例如命名空间解耦,因为每次都会生成新组件,就不存在命名空间问题了;其次它也能很好地作静态检查;但它依然没有办法处理组件 props 的问题,props 仍是有可能会在高阶组件中被更改;并且它还有了新的问题,每多用一次高阶组件,都会多出一个组件实例。redux

最后咱们来看一下 Hooks,紫色的圈圈是 Hooks,就像图里同样,Hooks 都在同一个组件里,Hooks 之间还能够相互调用。由于 Hooks 跑在一个普通函数式组件里,因此他确定是没有命名空间的问题,同时 TypeScript 也能对普通函数作很好的静态检查,并且 Hooks 也不能更改组件的 Props,传入的是啥最后可用的就是啥;最后 Hooks 也不会生成新的组件,因此他是单组件实例。小程序

taroxhooks.png

在 Taro 1.3 版本,咱们实现了一大堆特性,其中的重头戏就是 React Hooks 的支持。虽然 React Hooks 正式稳定的时间并不长,但咱们认为这个特性能有效地简化开发模式,提高开发效率和开发体验。即使 Hooks 的生态和最佳实践还还没有完善,但咱们相信将来 Hooks 会成为 React 开发模式的主流,也会深入地影响其它框架将来的 API 构成。因此在 Taro 的规划中咱们也把 Hooks 放在了很重要的位置。后端

什么是 Hooks?

相信笔者扯了那么多,你们对 Hooks 应该产生了一些兴趣,那什么是 Hooks 呢?简单来讲,Hooks 就是一组在 React 组件中运行的函数,让你在不编写 Class 的状况下使用 state 及其它特性。具体来讲,Hooks 能够表现为如下的形式:数组

useState 与内部状态

咱们能够看一个原生小程序的简单案例,一个简单计数器组件,点击按钮就 + 1,相信每位前端开发朋友均可以轻松地写一个计数器组件。但咱们能够稍微改一下代码,把事件处理函数改成箭头函数。若是是这样代码就跑不了了。事实上在原生开发中 this 的问题是一以贯之的,因此咱们常常要开个新变量把 this 缓存起来,叫作 self 什么的来避免相似的问题。咱们以前也提到过,若是采用 ES6 的 Class 来组织组件一样也会遇到 this 指向不清晰的问题。

Page({
  data: {
    count: 0
  },
  increment: () => { // 这里写箭头函数就跑不了了
    this.setData({
      count: this.data.count + 1
    })
  }
})
复制代码

再来看看咱们的 hooks 写法,咱们引入了一个叫 useState 的函数,它接受一个初始值参数,返回一个元组,若是是写后端的同窗应该对这个模式比较熟悉,就像 Koa 或者 Go 同样,一个函数返回两个值或者说叫一个元组,不过咱们返回的第一个参数是当前的状态,一个是设置这个状态的函数,每次调用这个设置状态的 setState 函数都会使得整个组件被从新渲染。而后用 ES6 的结构语法把它俩解构出来使用。

而后咱们在定义一个增长的函数,把他绑定到 onClick 事件上。

function Counter () {
  // 返回一个值和一个设置值的函数 
  // 每次设置值以后会从新渲染组件
  const [ count, setCount ] = useState(0)

  function increment () {
    setCount(count + 1)
  }

  return (
    <View> <Text>You clicked {count} times</Text> <Button onClick={increment}> Click me </Button> </View>
  )
}
复制代码

一样是很是简单的代码。若是你熟悉 Taro 以前的版本的话就会知道这样的代码在之前的 Taro 是跑不了的,不过 Taro 1.3 以后事件传参能够传入任何合法值,你若是想直接写箭头函数或者写一个柯里化的函数也是彻底没有问题的。

你们能够发现咱们使用的 Hooks 就是一个很是简单很是 normal 的函数,没有 this 没有 class,没有类构造函数,没有了 this,不再会出现那种 thisself 傻傻分不清楚的状况。

你们能够记住这个简单的计数器组件,之后以后讲的不少案例是基于这个组件作的。

useEffect 与反作用

接下来咱们看一个稍微复杂一些的例子,一个倒计时组件,咱们点击按钮就开始倒计时,再点击就中止倒计时。在咱们这个组件里有两个变量,start 用于控制是否开始计时,time 就是咱们的倒计时时间。这里注意咱们须要屡次清除 interval,而在现实业务开发中,这个 touchStart 函数可能会复杂得多,一不当心就会提早清除 interval 或忘记清除。

Page({
  data: {
    time: 60
  },
  start: false,
  toggleStart () {
    this.start = !this.start
    if (this.start) {
      this.interval = setInterval(() => {
        this.setData({
          time: this.data.time - 1
        })
      }, 1000)
    } else {
      clearInterval(this.interval)
    }
  },
  onUnload () {
    clearInterval(this.interval)
  }
})
复制代码
<view>
  <button bindtap="toggleStart">
    {{time}} 
  </button>
</view>
复制代码

而咱们 Hooks 的例子会是这样:咱们引入了一个 useEffect 函数。以前咱们提到过,每次调用 useState 返回的 setState 函数都会从新调用整个函数,其实就包括了 useEffect 函数,useEffect 接受两个参数。第一个就是反作用,也就是 effect 函数,他不接受也不返回任何参数。 第二个参数是依赖数组,当数组中的变量变化时就会调用,第一个参数 effect 函数。 Effect 函数还能够返回一个函数,这个函数在下一次 effect 函数被调用时或每次组件被注销时或者就会调用,咱们能够在这里清楚掉一些事件的订阅或者 interval 之类可能会致使内存泄露的行为。 在咱们这个例子中,当 start 每次变化就会从新跑一次 effect 函数,每隔一秒会设置一次 time 的值让它减一,但这样的写法是有问题的。

function Counter () {
  const [ start, setStart ] = useState(false)
  const [ time, setTime ] = useState(60)
  
  useEffect(() => { // effect 函数,不接受也不返回任何参数
    let interval
    if (start) {
      interval = setInterval(() => {
        // setTime(time - 1) ❌ time 在 effect 闭包函数里是拿不到准确值的
        setTime(t => t -1) // ✅ 在 setTime 的回调函数参数里能够拿到对应 state 的最新值
      }, 1000)
    }
    return () => clearInterval(interval) // clean-up 函数,当前组件被注销时调用
  }, [ start ]) // 依赖数组,当数组中变量变化时会调用 effect 函数
  
  return (
    <View> <Button onClick={() => setStart(!start)}>{time}</Button> </View>
  )
}
复制代码

由于咱们在 setInterval 这个函数的闭包中,咱们捕捉到 time 这个变量的值不能和最新的值对应得上,time 的值有可能在咱们意料以外地被更改了屡次。解决的方案也很简单,以前咱们提到过 useState 返回的 setState 方法,能够接受一个函数做为参数,而这个函数的参数,就是 state 最新的值,因此只要咱们传入一个函数就行了。这是其中一种方法。

还有另外一种方法是使用 useRef Hooks,useRef 能够返回一个可变的引用,它会生成一个对象,对象里这个有 current 属性,而 current 的值是可变的。在咱们这个例子里,每次更改 currentTime.current 都是同步的,并且 currentTime 是一个引用,因此 currentTime.current 必定是可控的。

function Counter () {
  const [ start, setStart ] = useState(false)
  const [ time, setTime ] = useState(60)
  const currentTime = useRef(time) // 生成一个可变引用
  
  useEffect(() => { // effect 函数,不接受也不返回任何参数
    let interval
    if (start) {
      interval = setInterval(() => {
        setTime(currentTime.current--) // currentTime.current 是可变的
      }, 1000)
    }
    return () => clearInterval(interval) // clean-up 函数,当前组件被注销时调用
  }, [ start ]) // 依赖数组,当数组中变量变化时会调用 effect 函数
  
  return (
    <View> <Button onClick={() => setStart(!start)}>{time}</Button> </View>
  )
}
复制代码

虽说咱们能够 useRef 来解决这个问题,可是不必这样作。由于 setTime 传递一个回调函数的方法显然可读性更高。真正有必要的是把咱们的 interval 变量做为一个 ref,咱们在函数最顶层的做用域把 interval 做为一个 ref,这样咱们就能够在这个函数的任何一个地方把他清除,而原来的代码中咱们把 interval 做为一个普通的变量放在 effect 函数里,这样若是咱们有一个事件也须要清除 interval,这就无法作到了。可是用 useRef 生成可变引用就没有这个限制。

function Counter () {
  const [ start, setStart ] = useState(false)
  const [ time, setTime ] = useState(60)
  const interval = useRef() // interval 能够在这个做用域里任何地方清除和设置
  
  useEffect(() => { // effect 函数,不接受也不返回任何参数
    if (start) {
      interval.current = setInterval(() => {
        setTime(t => t - 1) // ✅ 在 setTime 的回调函数参数里能够拿到对应 state 的最新值
      }, 1000)
    }
    return () => clearInterval(interval.current) // clean-up 函数,当前组件被注销时调用
  }, [ start ]) // 依赖数组,当数组中变量变化时会调用 effect 函数
  
  return (
    <View> <Button onClick={() => setStart(!start)}>{time}</Button> </View>
  )
}
复制代码

useContext 与跨组件通讯

接下来咱们再来看一个跨组件通讯的例子,例如咱们有三个组件,page 组件有一个 child 组件,child 组件有一个 counter 组件,而咱们 counter 组件的 count 值和 setCount 函数,是由 page 组件传递下来的。这种状况在一个复杂业务的开发中也常常能遇到,在原生小程序开发中咱们应该怎么作呢?

咱们须要手动的把咱们 counter 的值和函数手动地依次地传递下去,而这样的传递必须是显式的,你须要在 JavaScript 中设置 props 的参数,也须要在 WXML 里设置 props 的参数,一个也不能少,少了就跑不动。咱们还注意到即使 child 组件没有任何业务逻辑,他也必需要设置一个 triggerEvent 的函数和 props 的类型声明。这样的写法无疑是很是麻烦并且限制很大的。

<!-- page.wxml -->

<view>
  <child />
</view>

<!-- child.wxml -->
<view>
  <counter />
</view>

<!-- counter.wxml -->
<view>
  <text>
    You clicked {{count}} times
  </text>
  <butto bindtap="increment">
    Click me
  </button>
</view>
复制代码
// page.js
Page({
  data: {
    count: 0
  },
  increment () {
    this.setData({
      count: this.data.count + 1
    })
  }
})

// child.js
Component({
  properties: {
    count: Number
  },
  methods: {
    increment () {
      this.triggerEvent('increment')
    }
  }
})

// counter.js
Component({
  properties: {
    count: Number
  },
  methods: {
    increment () {
      this.triggerEvent('increment')
    }
  }
})
复制代码

而咱们能够看看 Hooks 的写法,首先咱们用 Taro.createContext 建立一个 context 对象,在咱们 page 组件里把咱们的 countsetCount 函数做为一个对象传入到 Context.Providervalue 里。而后在咱们的 Counter 组件,咱们可使用 useContext 这个 Hooks 把咱们的 countsetCount 取出来,就直接可使用了。

export const CounterContext = Taro.createContext(null);

// page.js
const Page = () => {
  const [ count, setCount ] = useState(0)

  return (
    <CounterContext.Provider value={{ count, setCount }}> <Child /> </CounterContext.Provider> ); }; // child.js const Child = () => ( <View> <Counter /> </View> ); // counter.js const Counter = () => { const { count, setCount } = useContext(CounterContext) return ( <View> <Text> You clicked {count} times </Text> <Button onClick={() => setCount(count + 1)}> Click me </Button> </View> ) } 复制代码

你们能够发现使用 Context 的代码比原来的代码精简了不少,参数不须要一级一级地显式传递,child 组件也和事实同样,没有一行多余的逻辑。但精简不是最大的好处。最大的好处是你们能够发现咱们的 Context 能够传递一个复杂的对象,熟悉小程序原生开发的同窗可能会知道,全部 props 的传递都会被小程序序列化掉,若是传递了一个复杂的对象最终会变成一个 JSON。可是用 Taro 的 context 则没有这层限制,你能够传入一个带有函数的对象,也能够传入像是 imutabale 或者 obserable 这样复杂的对象。在 taro 1.3 咱们对 props 系统进行了一次重构,Taro 的 context 和 props 同样,属性传递没有任何限制,想传啥就传啥。

另一个值得注意的点的是,context 的传递能够无视父级组件的更新策略,在这个例子中即使咱们经过 shouldComponentUpdate() 禁止了 child 组件的更新,但 counter 做为它的子组件依然是能够更新的。这个特性可让咱们作性能优化的时候更为灵活一些。

Hooks 在小程序实战

讲完了 Hooks 的基本使用,有些同窗会以为:咦,我怎么以为你这几个东西感受平平无奇,没什么特别的。但实际上这些基础的 Hooks 单独拿出来看的确不能玩出什么花样,但他们组合起来却能迸发强大的力量。

自定义 Hooks

你们在业务开发可能会遇到这样的需求,实现一个双击事件,若是你是从 H5 开发过来的可能会直接写 onDoubleClick,但很遗憾,小程序组件是没有 doubleClick 这个事件的。固然,若是你使用 Taro 又用了 TypeScript 就不会犯这样的错误,编辑器就回直接给你报错 Text 组件没有这个属性。

因而你就本身实现了一个双击事件,代码大概是这样,有一个上次点击的时间做为状态,每次触发单机事件的时候和上次点击的时间作对比,若是间隔太小,那他就是一个双击事件。代码很是简单,但咱们不由就会产生一个问题问题,每一次给一个组件加单击事件,咱们就每次都加这么一坨代码吗?

function EditableText ({ title }) {
  const [ lastClickTime, setClickTime ] = useState(0)
  const [ editing, setEditing ] = useState(false)

  return (
    <View> { editing ? <TextInput editing={editing} /> : <Text onClick={e => { const currentTime = e.timeStamp const gap = currentTime - lastClickTime if (gap > 0 && gap < 300) { // double click setEditing(true) } setClickTime(currentTime) }} > {title} </Text> } </View> ) } 复制代码

这个时候咱们就能够写一个自定义 Hooks,代码和原来的代码也差很少,useDoubleClick 不接受任何参数,但当咱们调用 useDoubleClick 时候返回一个名为 textOnDoubleClick 的函数,在在 Text 组件的事件传参中,咱们再在 textOnDoubleClick 函数中传入一个回调函数,这个回调函数就是触发双击条件时候的函数。当咱们给这个自定义 Hooks 作了柯里化以后,咱们就能够作到知道 Hook 使用时才暴露回调函数:

function useDoubleClick () {
  const [ lastClickTime, setClickTime ] = useState(0)

  return (callback) => (e) => {
    const currentTime = e.timeStamp
    const gap = currentTime - lastClickTime
    if (gap > 0 && gap < 300) {
      callback && callback(e)
    }
    setClickTime(currentTime)
  }
}

function EditableText ({ title }) {
  const [ editing, setEditing ] = useState(false)
  const textOnDoubleClick = useDoubleClick()

  return (
    <View> { editing ? <TextInput editing={editing} /> : <Text onClick={textOnDoubleClick(() => setEditing(true) )} > {title} </Text> } </View> ) } 复制代码

柯里化函数好像有一点点绕,但一旦咱们完成了这一步,这种咱们的自定义 hooks 就能够像屡次调用:

function EditableText ({ title }) {
  const textOnDoubleClick = useDoubleClick()
  const buttonOnDoubleClick = useDoubleClick()
  // 任何实现单击类型的组件都有本身独立的双击状态


  return (
    <View> <Text onClick={textOnDoubleClick(...)}> {title} </Text> <Button onClick={buttonOnDoubleClick(...)} /> </View> ) } 复制代码

每个你们不妨试想若是按照咱们传统的 render props 实现,每次都要多写一个 container 组件,若是用 Mixins 或高阶组件来实现就更麻烦,咱们须要基于每一个不一样类型的组件创造一个新的组件。而使用 Hooks,任何一个实现了单机类型的组件均可以经过咱们的自定义 Hook 实现双击效果,无论从它的内部实现来看,仍是从它暴露的 API 来看都是很是优雅的。

性能优化

接下来咱们谈一下性能优化,相信你们也有过这种状况,有一个数组,他只需拿到他的 props 要渲染一次,今后以后他就不再须要更新了。对于传统而言的 Class Component 咱们能够设置 shouldComponentUpdate() 返回 false

class Numbers extends Component {
  shouldComponentUpdate () {
    return false
  }

  render () {
    return <View> { expensive(this.props.array).map(i => <View>{i}</View>) } </View>
  }
}
复制代码

而对于函数式组件而言,咱们也能够作同样的事情。Taro 和 React 同样提供 Taro.memo API,他的第一个参数接受一个函数式组件,第二个参数和咱们的 shouldComponentUpdate() 同样,判断组件在什么样的状况下须要更新。若是第二个参数没有传入的话,Taro.memo 的效果就和 Taro.PureComponent 同样,对新旧 props 作一层浅对比,若是浅对比不相等则更新组件。

function Numbers ({ array }) {
  return (
    <View> { expensive(array).map( i => <View>{i}</View> ) } </View>
  )
}

export default Taro.memo(Numbers, () => true)
复制代码

第二种状况咱们能够看看咱们的老朋友,计数器组件。可是这个计数器组件和老朋友有两点不同:第一是每次点击 + 1,计数器须要调用 expensive 函数循环 1 亿次才能拿到咱们想要的值,第二点是它多了一个 Input 组件。在咱们真实的业务开发中,这种状况也很常见:咱们的组件可能须要进行一次昂贵的数据处理才能获得最终想要的值,但这个组件又还有多个 state 控制其它的组件。在这种状况下,咱们若是正常书写业务逻辑是有性能问题的:

function Counter () {
  const [ count, setCount ] = useState(0)
  const [val, setValue] = useState('')
  function expensive() {
    let sum = 0
    for (let i = 0; i < count * 1e9; i++) {
      sum += i
    }
    return sum
  }

  return (
    <View> <Text>You clicked {expensive()} times}</Text> <Button onClick={() => setCount(count + 1)}> Click me </Button> <Input value={val} onChange={event => setValue(event.detail.value)} /> </View> ) } 复制代码

由于咱们 count 的值跟 Input 的值没有关系,但咱们每次改变 Input 的值,就会触发这个组件从新渲染。也就是说这个循环一亿次的 expensive() 函数就会从新调用。这样状况显然是不能接受的。为了解决这个问题,咱们可使用 useMemo API。useMemo 的签名和 useEffect 有点像,区别就在于 useMemo 的第一个函数有返回值,这个函数返回的值同时也是 useMemo 函数的返回值。而第二个参数一样是依赖数组,只有当这个数组的数据变化时候,useMemo 的函数才会从新计算,若是数组没有变化,那就直接从缓存中取数据。在咱们这个例子里咱们只须要 count 变化才进行计算,而 Input value 变化无需计算。

function Counter () {
  const [ count, setCount ] = useState(0)
  const [val, setValue] = useState('')
  const expensive = useMemo(() => {
    let sum = 0
    for (let i = 0; i < count * 100; i++) {
      sum += i
    }
    return sum
  }, [ count ]) // ✅ 只有 count 变化时,回调函数才会执行

  return (
    <View> <Text>You Clicked {expensive} times</Text> <Button onClick={() => setCount(count + 1)}> Click me </Button> <Input value={val} onChange={event => setValue(event.detail.value)} /> </View> ) } 复制代码

咱们刚才提到的两个 memo 的 API ,他的全称实际上是 Memoization。因为 Hooks 都是在普通函数中运行的,因此咱们要作好性能优化,必定要好好利用缓存和记忆化这一技术。

在计算机科学中,记忆化(Memoization)是一种提升程序运行速度的优化技术。经过储存大计算量函数的返回值,当这个结果再次被须要时将其从缓存提取,而不用再次计算来节省计算时间。

大规模状态管理

提到状态管理,React 社区最有名的工具固然是 Redux。在 react-redux@7 中新引用了三个 API:

  1. useSelector。它有点像 connect() 函数的第一个参数 mapStateToProps,把数据从 state 中取出来;
  2. useStore 。返回 store 自己;
  3. useDispatch。返回 store.dispatch

在 Taro 中其实你也可使用咱们以前提到过的 createContextuseContext 直接就把 useStoreuseDispatch 实现了。而基于 useStoreuseDispatch 以及 useStateuseMemouseEffect 也能够实现 useSelector。也就是说 react-redux@7 的新 API 全都是普通 Hooks 构建而成的自定义 Hooks。固然咱们也把 react-redux@7 的新功能移植到了 @tarojs/redux,在 Taro 1.3 版本你能够直接使用这几个 API。

Hooks 的实现

咱们如今对 Hooks 已经有了如下的了解,一个合法的 Hooks ,必须知足如下需求才能执行:

  • 只能在函数式函数中调用
  • 只能在函数最顶层中调用
  • 不能在条件语句中调用
  • 不能在循环中调用
  • 不能在嵌套函数中调用

我想请你们思考一下,为何一个 Hook 函数须要知足以上的需求呢?我想请你们以能够框架开发者的角度去思考下这个问题,而不是以 API 的调用者的角度去逆向地思考。当一个 Hook 函数被调用的时,这个 Hook 函数的内部实现应该能够访问到当前正在执行的组件,可是咱们的 Hooks API 的入参却没有传入这个组件,那到底是怎么样的设计才可让咱们的 hook 函数访问到正在执行的组件,也可以准确地定位本身呢?

聪明的朋友或许已经猜到了,这些全部线索都指向一个结果,Hooks 必须是一个按顺序执行的函数。也就是说,无论整个组件执行多少次,渲染多少次,组件中 Hooks 的顺序都是不会变的。

咱们还知道另一条规则,Hooks 是 React 函数内部的函数,因而咱们就能够知道,要实现 Hooks 最关键的问题在于两个:

  1. 找到正在执行的 React 函数
  2. 找到正在执行的 Hooks 的顺序。

咱们能够设置一个全局的对象叫 CurrentOwner,它有两个属性,第一个是 current,他是正在执行的 Taro 函数,咱们能够在组件加载和更新时设置它的值,加载或更新完毕以后再设置为 null;第二个属性是 index,它就是 CurrentOwner.current 中 Hooks 的顺序,每次咱们执行一个 Hook 函数就自增 1。

const CurrentOwner: {
  current: null | Component<any, any>,
  index: number
} = {
  // 正在执行的 Taro 函数,
  // 在组件加载和从新渲染前设置它的值
  current: null,
  // Taro 函数中 hooks 的顺序
  // 每执行一个 Hook 自增
  index: 0
}
复制代码

在 React 中其实也有这么一个对象,并且你还可使用它,它叫作 __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,也就是说若是你想给 React 15 实现 Hooks,其实也能够作到的。但也正如它的名字同样,若是你用了说不定就被 fire 了,被优化了,因此更好的方案仍是直接使用咱们 taro。

接下来咱们来实现咱们的 getHook 函数,一样很简单,若是 CurrenOwner.currentnull,那这就不是一个合法的 hook 函数,咱们直接报错。若是知足条件,咱们就把 hook 的 index + 1,接下来咱们把组件的 Hooks 都保存在一个数组里,若是 index 大于 Hooks 的长度,说明 Hooks 没有被创造,咱们就 push 一个空对象,避免以后取值发生 runtime error。而后咱们直接返回咱们的 Hook。

function getHook (): Hook {
  if (CurrentOwner.current === null) {
    throw new Error(`invalid hooks call: hooks can only be called in a taro component.`)
  }
  const index = CurrentOwner.index++ // hook 在该 Taro 函数中的 ID
  const hooks: Hook[] = CurrentOwner.current.hooks // 全部的 hooks
  if (index >= hooks.length) { // 若是 hook 尚未建立
    hooks.push({} as Hook) // 对象就是 hook 的内部状态
  }
  return hooks[index] // 返回正在执行的 hook 状态
}
复制代码

既然咱们已经找到了咱们正在执行的 Hooks,完整地实现 Hooks 也就不难了。以前咱们讨论过 useState 的签名,如今咱们一步一步地看他的实现。

首先若是 initState 是函数,直接执行它。其次调用咱们咱们以前写好的 getHook 函数,它返回的就是 Hook 的状态。接下来就是 useState 的主逻辑,若是 hook 尚未状态的话,咱们就先把正在执行的组件缓存起来,而后 useState 返回的,就是咱们的 hook.state, 其实就是一个数组,第一个值固然就是咱们 initState,第一个参数是一个函数,它若是是一个函数,咱们就执行它,不然就直接把参数赋值给咱们 的 hook.state 第一个值,赋值完毕以后咱们把当前的组件加入到更新队列,等待更新。

function useState<S> (initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>] {
  if (isFunction(initialState)) { // 若是 initialState 是函数
    initialState = initialState() // 就直接执行
  }
  const hook = getHook() as HookState<S> // 找到该函数中对应的 hook
  if (isUndefined(hook.state)) { // 若是 hook 尚未状态
    hook.component = Current.current! // 正在执行的 Taro 函数,缓存起来
    hook.state = [ // hook.state 就是咱们要返回的元组
      initialState,
      (action) => {
        hook.state[0] = isFunction(action) ? action(hook.state[0]) : action
        enqueueRender(hook.component) // 加入更新队列
      }
    ]
  }
  return hook.state // 已经建立 hook 就直接返回
}
复制代码

最后咱们把 hook.state 返回出去就大功告成了。

Taro 的 Hooks 总共有八个 API, useState 的实现你们能够发现很是简单,但其实它的代码量和复杂度是全部 Hooks 的实现中第二高的。因此其实 Hooks 也没有什么黑科技,你们能够放心大胆地使用。

总结与展望

在 2018 年 Ember.js 的做者提出过一个观点,Compilers are the New Frameworks,编译器即框架。什么意思呢?就拿 React 来举例,单单一个 React 其实没什么用,你还须要配合 create-react-app, eslint-plugin-react-hooks, prettier 等等编译相关的工具最终才能构成一个框架,而这些工具也恰巧是 React Core Team 的人创造的。而这样趋势不只仅发生在 React 身上,你们能够发如今2018年,尤雨溪老师的主要工做就是开发 vue-cli。而对一些更激进的框架,例如 svelte,它的框架就是编译器,编译器就是框架。

而到了 2019 年,我想提出一个新概念,叫框架即生态。就拿 Taro 来讲,使用 Taro 你能够复用 React 生态的东西,同时 Taro 还有 taro doctor,Taro 开发者社区,Taro 物料市场,还有腾讯小程序·云开发等等多个合做伙伴一块儿构成了 Taro 生态,而整个 Taro 生态才是框架。在过去的半年,咱们持续改进并优化了 Taro 框架的表现,以上提到的特性与功能在 Taro 1.3 所有均可以正常使用。而在框架以外,咱们也深耕社区,推出了 Taro 物料市场和 Taro 开发者社区,并和腾讯小程序·云开发合做举办了物料开发竞赛。如今,咱们诚挚邀请你一块儿来参与社区贡献,让小程序开发变得更好、更快、更方便:

官方物料市场,邀您参加「Taro x 小程序·云开发」物料开发竞赛

相关文章
相关标签/搜索