Игорь Шеко, Voximplant

Место стейта в будущем vue.js

ВНИМАНИЕ!

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

function helloWorld(hello, world){
  return hello + " " + word;
}

helloWorld("Hello". "world!")

ВНИМАНИЕ!

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

Поговорим о том, о чем во фронтенде обычно не говорят.

О программировании.

Давным давно, в далекой

далекой галактике...

PubSub

export default class PubSub{
  private listeners:Map<string,EventListener[]> = new Map();
  
  addEventListener(event:string,listener:EventListener){
    const listeners = this.listenerts.get(event)||[];
    this.listenerts.set([...listeners, listener]);
  }
  
  removeEventListener(event:string,listener?:EventListener){
    if(!listener)
      this.listenerts.remove(event);
    this.listenerts.set(this.listeners.filter(item=>item!==listener]);
  }
  
  dispatch(event:string, data?:any){
    const listeners = this.listenerts.get(event)||[];
    listeners.forEach(item=>item.handleEvent(data));
  }
}

И незачем каждый раз наследовать от Vue.

Event "A" received

Component 1

Component 2

Component 3

Component 4

PubSub

Event "A" received
Event "A" dispatched

Component 1

Component 2

Component 3

Component 4

PubSub

Очень быстро
Не реактивно
Надо отписываться

А что если...

Component 1

Component 2

Component 3

Component 4

PubSub

DATA

Event "Change A" received

Component 1

Component 2

Component 3

Component 4

PubSub

DATA

Event "Change A" received
Event "Change A" dispatched

Component 1

Component 2

Component 3

Component 4

PubSub

DATA

Event "Change A" received
Event "Change A" dispatched
Event "Changed A" received

Component 1

Component 2

Component 3

Component 4

PubSub

DATA

Event "Change A" received
Event "Change A" dispatched
Event "Changed A" received
Event "Changed A" dispatched

Component 1

Component 2

Component 3

Component 4

PubSub

DATA

Ничего не напоминает?

Flux-библиотеки похожи на очки: вы будете точно знать, когда они вам понадобятся.

Действие

Состояние

Представление

Строгость

Шаблонность

Краткость

Действие

Состояние

Представление

Компонент

Действие

Состояние

Представление

Компонент

Мутация

Действие

Состояние

Представление

Компонент

Мутация

Состояние

Действие

Состояние

Представление

Компонент

Мутация

Состояние

Геттер

Действие

Состояние

Представление

Компонент

Мутация

Состояние

Геттер

Модуль User

Модуль Auth

Модуль Conversations

Модуль Notifications

Действие

Состояние

Представление

Компонент

Мутация

Состояние

Геттер

Модуль User

Модуль Auth

Модуль Conversations

Модуль Notifications

Middleware

Строгость?

Шаблонность?

Краткость?

Главная проблема - сложность!

import { mapGetters } from 'vuex';

export default {
  // ...
  computed: {
    // смешиваем результат mapGetters с внешним объектом computed
    ...mapGetters([
      'doneTodosCount',
      'anotherGetter'
      // ...
    ])
  }
};
/**
 * Reduce the code which written in Vue.js for getting the getters
 * @param {String} [namespace] - Module's namespace
 * @param {Object|Array} getters
 * @return {Object}
 */
export const mapGetters = normalizeNamespace((namespace, getters) => {
  const res = {}
  normalizeMap(getters).forEach(({ key, val }) => {
    // The namespace has been mutated by normalizeNamespace
    val = namespace + val
    res[key] = function mappedGetter () {
      if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      if (process.env.NODE_ENV !== 'production' && !(val in this.$store.getters)) {
        console.error(`[vuex] unknown getter: ${val}`)
        return
      }
      return this.$store.getters[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})

Вторая проблема - store слабо связан с приложением.

Части store проблематично наследовать.

Но, в любом случае, это лучшее, что у нас есть сейчас

Декомпозиция

не кладем все яйца в один store

Реактивность in the wild

<template>
  <button @click="count++">счётчик — {{count}}</button>
<template>

<script lang="ts">
import {Component, Prop, Vue} from "vue-property-decorator";

@Component({
  components: {},
})
export default class NoopButton extends Vue {
  count:number = 0;
}
</script> 
const state = Vue.observable({ count: 0 })
const Demo = {
  render(h) {
    return h('button', {
      on: { click: () => { state.count++ }}
    }, `счётчик — ${state.count}`)
  }
}
const state = Vue.observable({ count: 0 })
const Demo = {
  render(h) {
    return h('button', {
      on: { click: () => { state.count++ }}
    }, `счётчик — ${state.count}`)
  }
}

const Demo2 = {
  render(h) {
    return h('button', {
      on: { click: () => { state.count++ }}
    }, `счётчик2 — ${state.count}`)
  }
}

Подробнее про разные взаимодействия.

https://www.youtube.com/channel/UCp4bYTGrxZ27ifRxm29U5ow

И понемногу ваше приложение начинает дышать...

И не обязательно все должно быть реактивным.

Proxy

Как работает реактивность.

Сейчас.
В коде.

 let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })

Тоже самое, но с Proxy.

data = new Proxy(data_without_proxy, {  // Override data to have a proxy in the middle
  get(obj, key) {
    deps.get(key).depend(); // <-- Remember the target we're running
    return obj[key]; // call original data
  },

  set(obj, key, newVal) {
    obj[key] = newVal; // Set original data to new value
    deps.get(key).notify(); // <-- Re-run stored functions
    return true;
  }
});

Evan You on Proxies

2.5

3.x

Рендеринг 3000 state-full компонентов

Composition API

<template>
  <form @submit="handleSubmit">
    <label>
      <span>Note:</span>
      <input v-model="currentNote" @input="handleNoteInput">
    </label>
    <button type="submit">Send</button>
  </form>
</template>

<script>
import { ref, watch } from "vue";
export default {
  props: ["divRef"],
  setup(props, context) {
    const currentNote = ref("");
    const handleNoteInput = e => {
      const val = e.target.value && e.target.value.toUpperCase()[0];
      const validNotes = ["A", "B", "C", "D", "E", "F", "G"];
      currentNote.value = validNotes.includes(val) ? val : "";
    };
    const handleSubmit = e => {
      context.emit("note-sent", currentNote.value);
      currentNote.value = "";
      e.preventDefault();
    };

    return {
      currentNote,
      handleNoteInput,
      handleSubmit,
    };
  }
};
</script>
export const outerCount = ref(0)


// где-то в компоненте Example.vue
<template>
  <button @click="onClick">счётчик — {{count}}</button>
<template>

<script>
import {outerCount} from "foo.js" 
const Example = {
  setup(props, { attrs }) {
    const count = outerCount;
    function onClick() {
      count.value++;
    }
  }
}
</script> 
const obj = reactive({ count: 0 })

const count = ref(0)

ref   vs   reactive

// стиль 1: отдельные значения
let x = 0
let y = 0

function updatePosition(e) {
  x = e.pageX
  y = e.pageY
}

// --- сравниваем с ---

// стиль 2: объект
const pos = {
  x: 0,
  y: 0
}

function updatePosition(e) {
  pos.x = e.pageX
  pos.y = e.pageY
}
function useMousePosition() {
  const pos = reactive({
    x: 0,
    y: 0
  })

  // ...
  return pos
}


export default {
  setup() {
    // реактивность потеряна!
    const { x, y } = useMousePosition()
    return {
      x,
      y
    }

    // реактивность потеряна!
    return {
      ...useMousePosition()
    }

    // реактивность не потеряна
    return {
      pos: useMousePosition()
    }
  }
}
function useMousePosition() {
  const pos = reactive({
    x: 0,
    y: 0
  })

  // ...
  return toRefs(pos)
}

// x & y are now refs!
const { x, y } = useMousePosition()
const count = ref(0)

watch(() => console.log(count.value))
// -> logs 0

setTimeout(() => {
  count.value++
  // -> logs 1
}, 100)


watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})


const stop = watch(() => { /* ... */ })

// later
stop()

Всем спасибо

Игорь Шеко

Lead Front end developer @ Voximplant

Future of the state

By Igor Sheko

Future of the state

  • 362