Vue 3 Composition API сломан после повторного рендеринга компонента

#vuejs3 #vue-composition-api

#vuejs3 #vue-composition-api

Вопрос:

спасибо, что прочитали мой пост.

Трудно описать мою проблему.

У меня есть реактивный объект, кэшированный глобально (например, хранилище, и давайте назовем его здесь «хранилище»), чтобы сохранить состояния вместе с некоторыми действиями. Изначально все работает хорошо. Но когда я добавил больше страниц и переключался между страницами, компоненты ломались после повторного рендеринга. Компоненты по-прежнему могут корректно считывать данные из хранилища и вызывать действия для обновления данных хранилища и консоли can.зарегистрируйте «отпечаток пальца», чтобы показать, что это тот же магазин. Но обновленные данные в хранилище больше не обновляются в пользовательском интерфейсе. Я могу утешить.зарегистрируйте данные в хранилище, которое выглядит хорошо, но пользовательский интерфейс заморожен с данными с момента последнего размонтирования.

Ниже я привожу код своего компонента.

     <template>
  <div class="min-w-full relative" ref="container">
    <hi-transition slow>
      <center-box
        v-if="loading"
        class="absolute w-full h-full left-0 top-0 text-primary-light opacity-50"
      >
        <hi-spinner class="w-1/6 h-1/6" :stroke="2" />LOADING...
      </center-box>
      <div v-else-if="noResult">
        No Result
      </div>
      <hi-markable
        v-else
        class="w-full h-full divide-y divide-primary-lighter overflow-y-auto block"
        :search="searchValue"
      >
        <div
          v-for="item in displayItems"
          :key="item.id"
          class="hover:bg-primary-lightest transition-colors duration-500 flex items-center block"
          @click="selectItem(item)"
          :style="{
            'min-height': minItemHeight   'px',
            height: itemHeight   'px'
          }"
          :class="{ active: currentItem === item }"
        >
          <slot :item="item" />
        </div>
      </hi-markable>
    </hi-transition>
  </div>
</template>

<script>
import { inject, ref } from "vue";
import HiSpinner from "@/ui/HiSpinner";
import CenterBox from "@/ui/elements/CenterBox";
import HiTransition from "@/ui/HiTransition";
import HiMarkable from "@/ui/HiMarkable";
import { computed } from "vue";

export default {
  name: "HiDataList",
  components: { HiMarkable, HiTransition, CenterBox, HiSpinner },
  props: {
    storeToken: String,
    minItemHeight: [Number, String],
    autoItemsPerPage: Boolean
  },
  emits: ["select"],
  setup(props, { emit }) {
    //this store is a reactive object cached somewhere
    const store = inject(props.storeToken);
    //to make sure it's the same store
    console.log("fingerprint", store.fingerprint);
    const { displayItems, currentItem, loading, searchValue, noResult } = store;

    const container = ref(null);
    const itemHeight = computed(() => {
      return props.autoItemsPerPage
        ? store.autoItemHeight.value
        : props.minItemHeight;
    });
    if (props.autoItemsPerPage) {
      store.autoItemsPerPage(container, props.minItemHeight);
    }
    function selectItem(item) {
      console.log(item);
      emit("select", item);
      store.setCurrentItem(item);
    }
    return {
      displayItems,
      selectItem,
      container,
      itemHeight,
      currentItem,
      loading,
      searchValue,
      noResult
    };
  }
};
</script>

<style scoped>
.active,
.active:hover {
  @apply bg-primary-lighter border-primary-light;
}
</style>
 

Магазин списков

 import { computed, reactive, watch, toRefs } from "vue";
import { useElementSize } from "@vueuse/core";
import { watchProps } from "@/utils/reactiveHelpers";

function filter(item, filters) {
  const f = (key, filter) => {
    const itemVal = item[key];
    if (Array.isArray(itemVal)) return itemVal.indexOf(filter) >= 0;
    else return itemVal === filter;
  };
  for (let key in filters) {
    const filterVal = filters[key];
    if (Array.isArray(filterVal)) {
      for (let i = 0; i < filterVal.length; i  ) {
        if (f(key, filterVal[i])) return true;
      }
    } else {
      return f(key, filterVal);
    }
  }
  return false;
}

const getTime = date => {
  if (date.milliseconds) return date.milliseconds;
  if (date.seconds) return date.seconds;
  if (date.getTime) return date.getTime();
};

const createListStore = (source, settings = {}) => {
  const state = reactive({
    source: source,
    currentPage: 0,
    itemsPerPage: 0, //zero means display all
    loading: true,
    ready: false,
    noData: false,
    noResult: false,
    filters: {},
    search: null,
    searchables: settings.searchables || [],
    sortBy: null,
    sortType: "alpha",
    desc: false,
    currentItem: null,
    autoItemHeight: 0,
    fingerprint: Math.random() * 10000000000000000
  });
  // const { itemsPerPage,source,filters,search,searchables,sortBy,sortType,desc } = toRefs(state);
  const {
    itemsPerPage,
    ready,
    loading,
    noResult,
    search,
    autoItemHeight
  } = toRefs(state);
  watchProps(state, "source", v => {
    if (typeof v !== "undefined") {
      state.ready = true;
      state.loading = false;
      if (!v.length) state.noData = true;
    }
  });
  const currentPage = computed(() => state.currentPage   1);
  // const itemsPerPage = computed(() => state.itemsPerPage);

  const totalItems = computed(() => results.asc.length);

  const from = computed(() => {
    if (totalItems.value === 0) return 0;
    return state.currentPage * state.itemsPerPage   1;
  });
  const to = computed(() => {
    const t = from.value   displayItems.value.length - 1;
    return t > totalItems.value ? totalItems.value : t;
  });
  const totalPages = computed(() => {
    if (totalItems.value === 0 || state.itemsPerPage === 0) return 1;
    return Math.ceil(totalItems.value / state.itemsPerPage);
  });
  const gotoPage = page => {
    console.log("gotoPage", page);
    state.currentPage = page - 1;
    console.log(state.currentPage);
  };
  const prevPage = () => {
    if (state.currentPage > 0) state.currentPage--;
  };
  const nextPage = () => {
    if (state.currentPage < totalPages.value) state.currentPage  ;
  };

  const updateFilters = (filter, val) => {
    state.filters[filter] = val;
  };

  /**
   *
   * @param column
   * @param desc
   * @param type "alpha"|"number"|"date"|"time"
   */
  const sortBy = (column, desc = false, type = "alpha") => {
    state.sortBy = column;
    state.desc = desc;
    state.sortType = type;
  };

  function doSearch(item) {
    const searchables = state.searchables;
    for (let i = 0; i < searchables.length; i  ) {
      const key = searchables[i];
      let value = item[key];
      if (value amp;amp; typeof value === "string") {
        value = value.toLowerCase();
      }
      if (value amp;amp; value.indexOf(state.search) >= 0) {
        return true;
      }
    }
    return false;
  }
  const results = reactive({
    desc: [],
    asc: []
  });
  function calcResults() {
    if (!state.ready || state.noData) return null;
    // console.log("re-calc results....");
    const hasFilters = Object.keys(state.filters).length > 0;
    // console.log(Object.keys(state.filters));
    let items = [];
    if (hasFilters || (state.search amp;amp; state.search.length)) {
      //do filter amp; search
      const source = state.source;
      for (let i = 0; i < source.length; i  ) {
        const item = source[i];
        // console.log(filter(item, state.filters));
        if (hasFilters amp;amp; !filter(item, state.filters)) {
          continue;
        }
        if (state.search amp;amp; state.search.length amp;amp; !doSearch(item)) {
          continue;
        }
        items.push(item);
      }
      if (!items.length) {
        results.desc = results.asc = [];
        return null;
      }
    } else {
      items = state.source;
    }

    if (state.sortBy) {
      //do sort
      const sort = state.sortBy;
      // const desc = state.desc ? -1 : 1;
      const type = state.sortType.toLowerCase();
      items.sort((a, b) => {
        a = a[sort];
        b = b[sort];
        if (type === "date" || type === "time") {
          return getTime(a) - getTime(b);
        } else {
          if (typeof a === "string") a = a.trim();
          if (typeof b === "string") b = b.trim();
          if (state.sortType.toLowerCase() === "number") {
            return a - b;
          } else {
            return a.localeCompare(b, "en", { sensitivity: "base" });
          }
        }
      });
    }
    results.asc = items;
    results.desc = [...items].reverse();
    // return items;
  }
  //changed to watch for the wired vue error.
  watchProps(
    state,
    ["source", "filters", "search", "searchables", "sortBy", "sortType"],
    () => {
      calcResults();
      state.noResult = results.asc.length === 0;
    }
  );

  const displayItems = computed(() => {
    if (!results.asc.length) return [];
    const re = state.desc ? results.desc : results.asc;
    if (state.itemsPerPage === 0) return re;

    const from = state.currentPage * state.itemsPerPage;
    const to = from   state.itemsPerPage;
    return re.slice(from, to);
  });
  /**
   *
   * @param elementRef  ref
   * @param minHeight   Number
   * @param itemHeightRef ref
   */
  const autoItemsPerPage = (elementRef, minHeight) => {
    const { height } = useElementSize(elementRef);
    // console.log(elementRef);
    watch(height, v => {
      const items = Math.floor(v / minHeight);
      // itemHeightRef.value = v / items;
      // console.log(v / items);
      state.itemsPerPage = items;
      state.autoItemHeight = v / items;
    });
  };

  const setCurrentItem = item => {
    console.log("set current", state.fingerprint);
    state.currentItem = item;
  };

  const currentItem = computed(() => state.currentItem);

  return {
    currentPage,
    itemsPerPage,
    totalItems,
    displayItems,
    from,
    to,
    totalPages,
    gotoPage,
    prevPage,
    nextPage,
    ready,
    loading,
    updateFilters,
    sortBy,
    search: v => (state.search = v),
    searchValue: search,
    autoItemsPerPage,
    setCurrentItem,
    currentItem,
    noResult,
    autoItemHeight,
    fingerprint: state.fingerprint
  };
};
export { createListStore };

 

Поставщик хранилища

 import { provide } from "vue";
import { createListStore } from "@/ui/storeProvider/listStore";

const storeDepot = {};

export const provideListStore = (token, source, settings = {}) => {
  if (!storeDepot[token]) {
    console.log("create new store", token);
    storeDepot[token] = createListStore(source, settings);
  }
  provide(token, storeDepot[token]);
  return storeDepot[token];
};
 

Комментарии:

1. Можете ли вы опубликовать изображение самого магазина? Чтобы обновить ваш DOM, у вас должны быть ссылки или активные атрибуты в yout store, из этой картинки я не могу сказать, есть ли они / нет

2. Привет, Фелипе, спасибо за ваши комментарии. Пожалуйста, ознакомьтесь с обновленным хранилищем в сообщении. На самом деле дело не в самой реактивности. Код работает нормально во время первоначального монтирования. Странное поведение заключается в том, что при повторном монтировании все состояния компонентов остаются такими же, как и при последнем монтировании. Функция вызывает, но не изменяет никаких данных / состояния.