Рефакторинг вашего апплета с помощью React Hooks

Апплет WeChat React.js Taro
Рефакторинг вашего апплета с помощью React Hooks

Автор: Ю Че

задний план

Друзья, которые обращали внимание на разработку мини-программ, должны были заметить, что в самом начале мини-программы создавались как основа для микро-инновационного бизнеса и могут запускать не более 1 млн пакетов. Однако позже выяснилось, что многие производители переводят все больше и больше предприятий на мини-программы, а возможности мини-программ постоянно открываются и становятся все более и более мощными. Так что позже лимит пакетов был увеличен до 2м, а затем были введены подпакеты, и теперь можно загружать 8м апплет. На самом деле этого объема уже можно достичь очень большим и очень сложным бизнесом. С точки зрения пользователей Taro, например, апплет для покупок Jingdong и апплет 58.com не уступают бизнесу на стороне ПК с точки зрения количества кодов и сложности, поэтому мы можем сказать, что сложность фронт- окончательная разработка смещается в сторону апплета.

И разработка апплета на самом деле является подмножеством фронтенд-разработки Во всей фронтенд-индустрии, как мы решаем проблему сложности?

Сначала мы рассмотрим React: член React Core Team и автор Redux Дэн Абрамов впервые представил React Hooks на ReactConf 2018. React Hooks были введены для решения некоторых проблем с компонентом класса:

  • Логику между компонентами класса сложно использовать повторно. Поскольку JavaScript не похож на Go или C++, класс может наследовать несколько раз, и повторное использование логики класса становится проблемой;
  • Сложные компоненты трудно понять. Компонент класса часто выполняет некоторые побочные функции мониторинга событий сбора данных в жизненном цикле.В этом случае нам трудно разделить компонент на более мелкие сильные стороны;
  • Классы сбивают с толку. Многие новички должны быть привязаны к событиям с помощью компонентов класса.thisЗапутавшись, вы можете использовать привязку для привязки событий, вы можете напрямую писать стрелочные функции или вы можете писать функции атрибутов класса, но какой метод лучше? В ES 2018 класс имеет множество синтаксисов, таких как декораторы, такие как частные поля, Эти странные синтаксисы также добавляют новичкам путаницы.

Та же проблема существует и для Vue.Автор Vue, г-н Юси Ю, также представил новую функцию Vue 3.0 на VueConf China 2019 под названиемFunctional-based APIКонцепция нового API, вдохновленная React Hooks. Поскольку шаблон композиции компонентов Vue 2.0 представлен в виде литералов объектов, поэтомуFunctional-based APIЕго можно использовать в качестве альтернативы Mixins с новым адаптивным API в качестве нового режима композиции компонентов. Итак, мы все еще очень мало знаем о Vue 3.0, и API может измениться в будущем, но, возможно, герои видят то же самое.React и Vue оба выбирают хуки для упрощения разработки интерфейса.Почему?

why_hooks.png

Мы можем посмотреть на предыдущую схему комбинации компонентов, перваяMixins, обведено краснымMixins, желтый компонент, мы знаемMixinsПо сути, это объединение нескольких объектов в один объект.MixinsПроцесс немного похож на вызовObject.assginметод. ЭтоMixinsВ чем проблема? Во-первых, это связывание пространства имен.Если несколько объектов имеют параметры с одинаковыми именами, эти параметры будут связаны друг с другом;MixinsЭто должно быть время выполнения, чтобы знать, какие параметры есть, поэтому TypeScript не может выполнять статическую проверку; в-третьих, параметры компонента не ясны.MixinsПропсы компонента ничем не отличаются от других параметров и могут быть легко изменены.Mixinsперезаписать.

чтобы решитьMixinsпроблема, а позже разработали компоненты более высокого порядка (HOC) компоненты более высокого порядка такие же, как на рисунке, один компонент вложен в другие компоненты. это решаетMixinsНекоторые проблемы, такие как развязка пространства имен, потому что каждый раз генерируются новые компоненты, проблемы с пространством имен нет; во-вторых, он также может очень хорошо выполнять статическую проверку; но он все еще не может справиться с проблемой реквизитов компонентов, реквизиты все еще Он может быть изменен в компонентах более высокого порядка, и у него есть новые проблемы, каждый раз, когда используется компонент более высокого порядка, будет дополнительный экземпляр компонента.

Наконец, давайте взглянем на крючки. Фиолетовый кружок — это крючки. Как и на картинке, все крючки находятся в одном компоненте, и крючки также могут вызывать друг друга. Поскольку хуки запускаются в обычном функциональном компоненте, проблем с пространством имен быть не должно.В то же время TypeScript также может выполнять хорошие статические проверки обычных функций, а хуки не могут изменять свойства компонента.Что передается в конце Что доступен; в конце концов, хуки не будут генерировать новые компоненты, поэтому он является однокомпонентным экземпляром.

taroxhooks.png

В Taro 1.3 мы реализовали кучу фич, изюминкой которых стала поддержка React Hooks. Хотя React Hooks долгое время официально не был стабильным, мы считаем, что эта функция может эффективно упростить режим разработки, повысить эффективность разработки и опыт разработки. Даже если экология и передовой опыт хуков еще не совершенны, мы считаем, что хуки станут основным направлением модели разработки React в будущем и окажут глубокое влияние на будущий состав API других фреймворков. Так что в планах Таро мы также поставили Крюков на очень важное место.

Что такое крючки?

Я считаю, что автор так много наговорил, и у каждого должен быть какой-то интерес к хукам.Так что же такое хуки? Короче говоря, хуки — это набор функций, которые запускаются внутри компонентов React, что позволяет вам использовать состояние и другие функции без написания классов. В частности, хуки могут принимать форму:

useStateс внутренним состоянием

Мы можем посмотреть на простой случай родного апплета, простой компонент счетчика, нажать кнопку + 1, я считаю, что каждый друг фронтенд-разработки может легко написать компонент счетчика. Но мы можем немного изменить код и изменить обработчик событий на стрелочную функцию. Если это так, код не будет работать. На самом деле в нативной разработкеthisПроблема постоянна, поэтому нам часто приходится открывать новую переменную, чтобы поместитьthisкешируется, называетсяselfчто-то, чтобы избежать подобных проблем. Мы также упоминали ранее, что если вы используете класс ES6 для организации компонентов, вы также столкнетесь сthisУкажите на неясные вопросы.

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

Давайте взглянем на наш метод написания хуков. Мы ввели функцию под названием useState, которая принимает параметр начального значения и возвращает кортеж. Если вы пишете бэкенд-студентов, вы должны быть знакомы с этим режимом. Так же, как Koa или Go , a Функция возвращает два значения или кортеж, но первый параметр, который мы возвращаем, — это текущее состояние, а другой — функция, которая устанавливает это состояние.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 параметр события может передавать любое допустимое значение, если вы хотите написать стрелочную функцию напрямую или написать Каррирование Функция также полностью без проблем.

Вы можете обнаружить, что хуки, которые мы используем, представляют собой очень простую и обычную функцию.thisнет класса, нет конструктора класса, не болееthis, больше никогдаthis,selfТупо непонятная ситуация.

Вы можете запомнить этот простой компонент счетчика, и многие из случаев, описанных ниже, основаны на этом компоненте.

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>

И наш пример с хуками будет выглядеть так: мы вводимuseEffectфункция. Как мы упоминали ранее, каждый раз, когда мы вызываемuseStateвернутьsetStateФункция повторно вызовет всю функцию, которая на самом деле включаетuseEffectфункция,useEffectПринимает два параметра. Во-первых, это побочный эффект, т.effectфункция, которая не принимает и не возвращает никаких параметров. Второй параметр — это зависимый массив, который вызывается при изменении переменных в массиве, первый параметрeffectфункция.EffectФункция также может возвращать функцию, которая выполняется на следующемeffectКогда функция вызывается или каждый раз, когда компонент выходит из системы или когда он будет вызываться, мы можем здесь очистить некоторые подписки на события или поведение интервалов, которые могут вызвать утечку памяти. В нашем примере, когда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крючки,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Метод передачи callback-функции явно более читабелен. Что действительно необходимо, так это поставить нашintervalПеременная действует как ссылка, и мы помещаем ее в область видимости функции верхнего уровня.intervalв качестве ссылки, чтобы мы могли очистить его в любом месте этой функции, тогда как в исходном коде мы поместили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, а также необходимо задать параметры реквизита в WXML.Можно не менее одного , Не двигайтесь. Мы также заметили, что даже если у дочернего компонента нет никакой бизнес-логики, он должен установитьtriggerEventОбъявления типов для функций и свойств. Этот способ написания, несомненно, очень громоздкий и ограничительный.

<!-- 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')
    }
  }
})

И мы можем посмотреть, как пишутся хуки, сначала мы используемTaro.createContextСоздаватьcontextобъект, в нашемpageкомпонент положить нашcountа такжеsetCountФункция передается как объектContext.Providerизvalueвнутри. тогда в нашемCounterкомпонент, мы можем использоватьuseContextЭти крючки берут нашиcountа такжеsetCountВыньте его, и вы можете использовать его напрямую.

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Компоненты тоже просто как правда, без единой строчки лишней логики. Но оптимизация — не самое большое преимущество. Самым большим преимуществом является то, что вы можете обнаружить, что наш контекст может передавать сложный объект. Студенты, знакомые с собственной разработкой мини-программ, могут знать, что передача всех реквизитов будет сериализована мини-программой. Если передается сложный объект , он в конечном итоге изменится на JSON. Но в контексте Таро такого ограничения нет, вы можете передать объект с помощью функции или передать что-то вродеimutabaleилиobserableтакой сложный объект. В таро 1.3 мы переработали систему реквизита.Контекст таро тот же, что и у реквизита.Нет ограничений на передачу свойств.Вы можете передавать все что хотите.

Еще один момент, на который стоит обратить внимание, это то, что передача контекста может игнорировать стратегию обновления родительского компонента.В этом примере, даже если мы передаемshouldComponentUpdate()запрещенchildобновление компонентов, ноcounterПоскольку его дочерние компоненты все еще могут быть обновлены. Эта функция позволяет нам быть более гибкими при оптимизации производительности.

Хуки на самом деле дерутся в мини-программах

Поговорив об основном использовании хуков, некоторые ученики подумают: «Эй, почему мне кажется, что эти вещи обычные, ничего особенного». Но на самом деле эти базовые Крючки не могут сыграть злую шутку по отдельности, но в сочетании они могут превратиться в мощную силу.

Пользовательские крючки

Каждый может столкнуться с такой потребностью в бизнес-разработке реализовать событие двойного клика.Если вы разрабатывали из 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>
  )
}

В настоящее время мы можем написать собственные хуки, код аналогичен исходному коду,useDoubleClickне принимает аргументов, но когда мы вызываемuseDoubleClickвозвращает файл с именемtextOnDoubleClickфункции, в параметре события компонента Text мы затемtextOnDoubleClickФункция обратного вызова передается в функцию, и эта функция обратного вызова является функцией, когда срабатывает условие двойного щелчка. Когда мы каррируем этот пользовательский хук, мы можем предоставить функцию обратного вызова только тогда, когда используется хук:

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

Функция каррирования кажется немного окольной, но как только мы это сделали, наши пользовательские хуки можно вызывать несколько раз, например:

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


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

Каждый может представить себе, что если мы реализуем его в соответствии с нашими традиционными реквизитами рендеринга, нам придется каждый раз писать еще один компонент-контейнер.Если мы используем для его реализации миксины или компоненты более высокого порядка, это будет более хлопотно. создайте новый на основе каждого другого типа компонента. С помощью хуков любой компонент, который реализует автономный тип, может добиться эффекта двойного щелчка с помощью нашего пользовательского хука, который очень элегантен как с точки зрения его внутренней реализации, так и с помощью предоставляемого им API.

оптимизация производительности

Далее поговорим об оптимизации производительности, я думаю, что у всех была такая ситуация, есть массив, и ему нужно только один раз заставить свой пропс отрендериться, и после этого ему никогда не нужно будет его обновлять. Для традиционного компонента класса мы можем установитьshouldComponentUpdate()вернутьfalse.

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

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

И с функциональными компонентами мы можем сделать то же самое. Таро предоставляет то же, что и ReactTaro.memoAPI, его первый параметр принимает функциональную составляющую, второй параметр и нашshouldComponentUpdate()Таким же образом определите, при каких обстоятельствах компонент нуждается в обновлении. Если второй параметр не передан,Taro.memoЭффект такой же, какTaro.PureComponentТаким же образом выполните поверхностное сравнение старого и нового реквизита и обновите компонент, если поверхностное сравнение не равно.

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

export default Taro.memo(Numbers, () => true)

Во втором случае мы можем обратиться к нашему старому другу, компоненту счетчика. Но этот компонент-счетчик отличается от старого друга двумя моментами: во-первых, каждый раз, когда вы нажимаете +1, нужно вызывать счетчикexpensiveФункция выполняет цикл 100 миллионов раз, чтобы получить нужное нам значение, и второй момент заключается в том, что у нее есть еще одно значение.Inputкомпоненты. В нашей реальной разработке бизнеса эта ситуация также очень распространена: нашему компоненту может потребоваться выполнить дорогостоящую обработку данных, чтобы получить конечное желаемое значение, но этот компонент также имеет несколько состояний, которые управляют другими компонентами. В этом случае, если мы нормально напишем бизнес-логику, будут проблемы с производительностью:

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значение, это вызовет повторную визуализацию компонента. То есть этот цикл повторяется 100 миллионов раз.expensive()Функция будет вызвана снова. Такая ситуация явно неприемлема. Для решения этой проблемы мы можем использоватьuseMemoAPI.useMemoподпись иuseEffectВроде как, разница в том, чтоuseMemoПервая функция имеет возвращаемое значение, и значение, возвращаемое этой функцией, такжеuseMemoВозвращаемое значение функции. Второй параметр тоже зависит от массива, только при изменении данных этого массива,useMemoФункция будет пересчитана, если массив не изменился, то данные будут взяты прямо из кеша. В нашем случае нам просто нужноcountИзменение рассчитывается, и нет необходимости рассчитывать изменение входного значения.

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

API двух меморандумов, которые мы только что упомянули, его полное имя на самом делеMemoization. Поскольку хуки запускаются в обычных функциях, мы должны эффективно использовать методы кэширования и запоминания для оптимизации производительности.

В компьютерных науках мемоизация — это метод оптимизации, который увеличивает скорость работы программы. Он экономит время вычислений, сохраняя возвращаемое значение дорогостоящей в вычислительном отношении функции и извлекая результат из кеша, когда он снова понадобится, без необходимости его повторного вычисления.

Масштабное государственное управление

Когда дело доходит до управления состоянием, самым известным инструментом в сообществе React, конечно же, является Redux. существуетreact-redux@7Zhongxin ссылается на три API:

  1. useSelector. это немного похожеconnect()первый параметр функцииmapStateToProps, вывести данные из состояния;
  2. useStore. вернутьstoreсам;
  3. useDispatch. вернутьstore.dispatch.

В Таро вы действительно можете использоватьcreateContextа такжеuseContextнапрямуюuseStoreа такжеuseDispatchДостигнуто. и на основеuseStoreа такжеuseDispatchтак же какuseState,useMemo,useEffectтакже может быть достигнутоuseSelector. то естьreact-redux@7Все новые API представляют собой пользовательские хуки, созданные из обычных хуков. Конечно, мы такжеreact-redux@7новые функции перенесены на@tarojs/redux, вы можете использовать эти API непосредственно в версии Taro 1.3.

Реализация хуков

Теперь у нас есть следующее понимание хуков, юридические хуки должны соответствовать следующим требованиям для выполнения:

  • Может вызываться только из функциональной функции
  • Может вызываться только на верхнем уровне функции
  • нельзя вызывать в условном выражении
  • нельзя вызывать в цикле
  • нельзя вызывать во вложенных функциях

Я хотел бы попросить вас подумать, почему функция Hook должна соответствовать вышеуказанным требованиям? Я хотел бы попросить всех подумать об этой проблеме с точки зрения разработчиков фреймворка, а не оглядываться назад с точки зрения тех, кто вызывает API. Когда вызывается функция-ловушка, внутренняя реализация функции-ловушки должна иметь доступ к выполняющемуся в данный момент компоненту, но входной параметр нашего API-интерфейса не передается в этот компонент, поэтому какова конструкция, позволяющая разрешить? функция получает доступ к исполняющему компоненту и точно определяет свое местонахождение?

Умные друзья могли догадаться, что все эти подсказки указывают на результат,Хуки должны быть функцией, которая выполняется по порядку.. Другими словами, независимо от того, сколько раз весь компонент будет выполняться и отображаться, порядок хуков в компоненте не изменится.

Мы также знаем еще одно правило: хуки — это функции внутри функций React, поэтому мы можем знать, что наиболее важными проблемами для реализации хуков являются две:

  1. Найдите исполняемую функцию React
  2. Найдите порядок, в котором выполняются хуки.

Мы можем установить глобальный объект с именемCurrentOwner, у него есть два свойства, первоеcurrent, он выполняетTaroфункцию, мы можем установить ее значение, когда компонент загружается и обновляется, а затем установить его вnull; второе свойствоindex, этоCurrentOwner.currentПорядок хуков в хуке увеличивается на 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, вы действительно можете это сделать. Но, как и в случае с его названием, если вы его используете, вас могут уволить и оптимизировать, поэтому лучшим решением будет использовать наше таро напрямую.

Далее реализуем нашуgetHookфункция, которая также очень проста, еслиCurrenOwner.currentдаnull, то это не легальная функция хука, мы напрямую сообщаем об ошибке. Если условие выполнено, ставим крючокindex+ 1, то сохраняем Хуки компонента в массив, еслиindexЕсли она больше длины хуков, значит, хуки не были созданы, поэтому мы пушим пустой объект, чтобы избежать ошибок времени выполнения при взятии значений позже. Затем возвращаемся непосредственно к нашему Крюку.

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 状态
}

Теперь, когда мы нашли хуки, которые мы реализуем, нетрудно реализовать хуки целиком. мы обсуждали раньшеuseState, теперь рассмотрим его реализацию пошагово.

Сначала, еслиinitStateявляется функцией, выполните ее напрямую. Во-вторых, позвоните нам, что мы написали ранееgetHookфункция, которая возвращает состояние хука. Далее основная логика useState.Если у хука нет состояния, то мы сначала кэшируем исполняемый компонент, а потом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Вернитесь назад, и все готово.

Крючки Таро имеют в общей сложности восемь API,useStateРеализация хуков может показаться очень простой, но на самом деле ее объем кода и сложность являются вторыми по величине среди всех реализаций хуков. Так что на самом деле никаких черных технологий у Hooks нет, и каждый может с уверенностью пользоваться ими.

Резюме и перспективы

В 2018 году автор Ember.js отметил,Compilers are the New Frameworks, компилятор — это фреймворк. Что это значит? Возьмите React в качестве примера, один React на самом деле бесполезен, вам также нужно сотрудничатьcreate-react-app, eslint-plugin-react-hooks, prettierТаким образом, инструменты, связанные с компиляцией, в конечном итоге сформируют основу, и эти инструменты также созданы людьми из основной команды React. И эта тенденция характерна не только для React, вы можете обнаружить, что в 2018 году основной задачей Юси Ю является разработкаvue-cli. в то время как для некоторых более агрессивных фреймворков, таких какsvelte, его фреймворк — это компилятор, а компилятор — это фреймворк.

А в 2019 году я хочу предложить новую концепцию под названием «фреймворк экология». Возьмем в качестве примера Taro.С помощью Taro можно повторно использовать вещи экосистемы React.При этом у Taro также естьtaro doctor, сообщество разработчиков Taro, рынок материалов Taro, разработка облачных апплетов Tencent и другие партнеры вместе составляют экосистему Taro, а вся экосистема Taro является основой. В течение последних шести месяцев мы продолжали улучшать и оптимизировать производительность фреймворка Taro.Все упомянутые выше возможности и функции могут нормально использоваться в Taro 1.3. В дополнение к фреймворку мы также глубоко взрастили сообщество, запустили рынок материалов Taro и сообщество разработчиков Taro, а также провели конкурс по разработке материалов в сотрудничестве с Tencent Mini Program Cloud Development. Теперь мы искренне приглашаем вас присоединиться к сообществу, чтобы внести свой вклад в разработку мини-программы лучше, быстрее и удобнее:

Официальный рынок материалов приглашает вас принять участие в конкурсе разработчиков материалов "Taro x Mini Program Cloud Development".