Что так разозлило Юду?

JavaScript Vue.js внешний фреймворк
Что так разозлило Юду?

задний план

Это произошло около полугода назад, кто-тоviteрепозиторий упоминает оvue-routerизissue:

angry.jpg

вопрошающий имеет в видуvue-routerВ его предстоящем проекте есть нерешенная ошибка, иvue-routerСопровождающий не смог решить эту проблему, я надеюсь, что Youda поможет ее решить.

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

Ю Да был явно очень недоволен таким поведением, и ответ тоже был властным:

  1. Не поднимайте неуместные проблемы в неуместных репозиториях.

  2. Все очень заняты, если у вас нет времени, у вас просто нет времени, не торопитесь.

  3. Повторные нарушения будут заблокированы.

Причина, по которой спрашивающий пришел сюда, чтобы опубликовать вопрос, заключается в том, что You Da часто активен в сообществе vite. На мой взгляд, Ю Да должен был давно знать об этом вопросе, т.к.vue-routerсопровождающие также являются одними из основных членов Vue, и они, должно быть, говорили об этом в частном порядке.

Итак, почему задержка не решает эту проблему, я думаю, есть две причины:

  1. Саму эту проблему решить непросто, и она может потребовать внесения большого количества изменений в код.

  2. Команда Vue усердно работает над тем, что они считают важным и срочным, и это не является приоритетом.

Так с чем именно столкнулся спрашивающий? Почему я обращаю внимание на этот вопрос? Так как недавно у меня была аналогичная проблема.

аналогичный вопрос

в моемКурс "Разработка корпоративного музыкального приложения Vue3"В области вопросов и ответов студент ответил вопросом:RouterViewСотрудничатьKeepAilveПосле использования компонента страница сведений о певце вторичной маршрутизацииcreatedФункция ловушки выполняется дважды.

После того, как я протестировал его, я обнаружил, что эта проблема существует. Сначала я подозревал, что это Vue3 илиvue-routerОшибка версии, поэтому я поставил Vue3 иvue-routerОба обновились до последней версии и обнаружили, что проблема все еще существует.

Итак, не возникнет ли проблем с написанием моего бизнес-кода? Интуиция подсказывает нет, чтобы найти первопричину проблемы и снизить сложность отладки, я написал минимизацию для воспроизведения проблемы.demo.

На демонстрационной странице есть вторичные маршруты, которые определяются следующим образом:

import { createRouter, createWebHashHistory } from 'vue-router'
const Home = import('../views/Home.vue')
const HomeSub = import('../views/HomeSub.vue')
const Sub = import('../views/Sub.vue')
const About = import('../views/About.vue')

const routes = [
  {
    path: '/',
    redirect: '/home'
  },
  {
    path: '/home',
    name: 'Home',
    component: Home,
    children: [
      {
        path: 'sub',
        component: HomeSub
      }
    ]
  },
  {
    path: '/about',
    name: 'About',
    component: About,
    children: [
      {
        path: 'sub',
        component: Sub
      }
    ]
  }
]

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

export default router

Обратите внимание,Обе основные страницы маршрутизации должны иметь вложенные подмаршруты..

Далее давайте посмотрим на определения нескольких компонентов Vue страницы, среди которыхApp.vueДля компонента входа на страницу:

<template>
  <div id="nav">
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link>
  </div>
  <router-view v-slot="{ Component }">
    <keep-alive>
      <component :is="Component"/>
    </keep-alive>
  </router-view>
</template>

Home.vueявляется компонентом маршрутизации первого уровня:

<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png">
    <button @click="showSub">click me</button>
    <router-view></router-view>
  </div>
</template>

<script>

export default {
  name: 'Home',
  created() {
    console.log('home page created')
  },
  methods: {
    showSub() {
      this.$router.push('/home/sub')
    }
  }
}
</script>

HomeSubдаHomeКомпоненты вторичной маршрутизации в компонентах:

<template>
  <div>This is home sub</div>
</template>

<script>
  export default {
    name: 'HomeSub',
    created() {
      console.log('home sub created')
    }
  }
</script>

About.vueявляется компонентом маршрутизации первого уровня:

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <button @click="showSub">click me</button>
    <router-view></router-view>
  </div>
</template>
<script>
  export default {
    name: 'About',
    created() {
      console.log('about page created')
    },
    methods: {
      showSub() {
        this.$router.push('/about/sub')
      }
    }
  }
</script>

Sub.vueдаAboutКомпоненты вторичной маршрутизации в компонентах:

<template>
  <div>This is sub</div>
</template>

<script>
  export default {
    name: 'Sub',
    created() {
      console.log('sub created')
    }
  }
</script>

Шаги для воспроизведения очень просты, сначала введитеHomeСтраница:

home.jpg

затем нажмитеAboutзапись тегаAboutСтраница:

about.jpg

Затем нажмите кнопку для визуализацииSubДочерний компонент маршрутизации:

about-sub.jpg

Отрисовка страницы нормальная, но мы обнаружили, чтоSubкомпонентcreatedФункция ловушки выполняется дважды и выводится дваждыsub created. Это эквивалентно рендерингу дваждыSubС компонентами явно беда.

анализ ошибок

Я включил метод отладки, вSubкомпонентcreatedКрючок в функции крючкаdebuggerточки останова, а затем пройтись по стеку вызовов функции.

debugger.jpg

Очевидно,debuggerвcreatedВнутри функции хука, а выполнение функции хука находится на этапе монтирования компонента, так какая операция запускает монтирование компонента?

debugger1.jpg

Продолжая просматривать стек вызовов, мы обнаружили, что последней причиной была модификация маршрутизации.currentRoute, сработалsetter, затем срабатываетRouterViewПеренаправления компонента, который в конечном итоге триггерыSubРендеринг компонентов.

так почемуcurrentRouteмодификация триггераRouterViewКак насчет повторного рендеринга компонентов? Это изRouterViewПринцип реализации:

const RouterViewImpl = defineComponent({
  name: 'RouterView',
  inheritAttrs: false,
  props: {
    name: {
      type: String,
      default: 'default',
    },
    route: Object,
  },
  setup(props, { attrs, slots }) {
    (process.env.NODE_ENV !== 'production') && warnDeprecatedUsage()
    const injectedRoute = inject(routerViewLocationKey)
    const routeToDisplay = computed(() => props.route || injectedRoute.value)
    const depth = inject(viewDepthKey, 0)
    const matchedRouteRef = computed(() => routeToDisplay.value.matched[depth])
    provide(viewDepthKey, depth + 1)
    provide(matchedRouteKey, matchedRouteRef)
    provide(routerViewLocationKey, routeToDisplay)
    const viewRef = ref()
    watch(() => [viewRef.value, matchedRouteRef.value, props.name], ([instance, to, name], [oldInstance, from, oldName]) => {
      if (to) {
        to.instances[name] = instance
        if (from && from !== to && instance && instance === oldInstance) {
          if (!to.leaveGuards.size) {
            to.leaveGuards = from.leaveGuards
          }
          if (!to.updateGuards.size) {
            to.updateGuards = from.updateGuards
          }
        }
      }
      if (instance &&
        to &&
        (!from || !isSameRouteRecord(to, from) || !oldInstance)) {
        (to.enterCallbacks[name] || []).forEach(callback => callback(instance))
      }
    }, { flush: 'post' })
    return () => {
      const route = routeToDisplay.value
      const matchedRoute = matchedRouteRef.value
      const ViewComponent = matchedRoute && matchedRoute.components[props.name]
      const currentName = props.name
      if (!ViewComponent) {
        return normalizeSlot(slots.default, { Component: ViewComponent, route })
      }
      const routePropsOption = matchedRoute.props[props.name]
      const routeProps = routePropsOption
        ? routePropsOption === true
          ? route.params
          : typeof routePropsOption === 'function'
            ? routePropsOption(route)
            : routePropsOption
        : null
      const onVnodeUnmounted = vnode => {
        if (vnode.component.isUnmounted) {
          matchedRoute.instances[currentName] = null
        }
      }
      const component = h(ViewComponent, assign({}, routeProps, attrs, {
        onVnodeUnmounted,
        ref: viewRef,
      }))
      return (
        normalizeSlot(slots.default, { Component: component, route }) ||
        component)
    }
  },
})

RouterViewКомпонент реализован на базе Composition API, мы сосредоточимся на его рендеринговой части, т.к.setupВозвращаемое значение функции является функцией, тогда эта функция является ее функцией рендеринга.

RouterViewОсновная идея - идти по путиrouteи текущийRouterViewГлубина вложенности для соответствия соответствующим компонентам маршрутизации в конфигурации маршрутизации и визуализации.

Доступ к вычисляемым свойствам осуществляется на протяжении всего процесса рендеринга.routeToDisplay, который определяется следующим образом:

const injectedRoute = inject(routerViewLocationKey)
const routeToDisplay = computed(() => props.route || injectedRoute.value)

routeToDisplayвнутренний доступinjectedRoute,а такжеinjectedRouteвводитсяkeyдляrouterViewLocationKeyДанные.

в исполненииcreateRouterКогда маршрут создан, он будет создан внутриcurrentRouteРеактивные переменные для поддержания текущего пути.

const currentRoute = shallowRef(START_LOCATION_NORMALIZED)

затем выполнениеcreateApp(App).use(router)При установке маршрута он выполнитrouterпредоставленный объектinstallметод, который будетcurrentRouteпройти черезrouterViewLocationKeyдоступны для приложения.

app.provide(routerViewLocationKey, currentRoute)

так рендерингRouterViewПри доступе к компонентуrouteToDisplay, доступ к которому будет осуществляться изнутриinjectedRoute, а затем доступ кcurrentRoute, и из-заcurrentRouteявляется реактивным объектом, который, в свою очередь, запускает процесс сбора зависимостей.

Итак, когда мы выполняемrouterобъектpushКогда метод изменяет путь маршрутизации, он будет выполняться внутренне.finalizeNavigationметод, а затем модифицированныйcurrentRoute, это вызоветвсе RouterViewРендеринг компонента.

По умолчанию с этой логикой проблем нет, так зачем добавлятьKeepAliveЕсть проблема?

Прежде чем ответить на этот вопрос, давайте подумаем над другим вопросом: в примере при нормальных обстоятельствах маршрут изHomeсократить доAboutПосле этого мы его модифицируем.currentRoute, вызоветHomeвнутри компонентаRouterViewперерисовать?

Ответ отрицательный, потому что при маршрутизации изHomeсократить доAbout, это вызоветHomeРазгрузка компонента, что, в свою очередь, приводит к его внутреннемуRouterViewУдаление компонента.

RouterViewВ процессе удаления компонента все зависимости в рамках компонента будут очищены, в том числе, конечноcurrentRouteсобранных компонентовrender effect. Итак, когда мы изменяемcurrentRoute, это не сработаетHomeвнутри компонентаRouterViewКомпонент перерисовывается.

Однако однаждыHomeкомпонент, соответствующийRouterViewодеялоKeepAliveПосле того, как компонент завернут, когда маршрут отHomeсократить доAbout, он не будет выполнятьсяHomeПроцесс удаления компонента не приведет к удалению внутреннегоRouterViewКомпонент, конечно же, не очищает зависимости под своей областью действия.

затем, когда мы изменимcurrentRoute, не только рендеритAboutвнутри компонентаRouterViewкомпонент, который также вызываетHomeвнутри компонентаRouterViewПеререндерить.

из-заHomeвнутри компонентаRouterViewа такжеAboutвнутри компонентаRouterViewявляются вторичными компонентами маршрутизации, согласноRouterViewЛогика рендеринга, на данный моментHomeвнутри компонентаRouterViewтакже будет отображаться какSubкомпоненты, поэтомуSubПричина, по которой компонент отображается дважды.

Сообщить о проблеме для Vue3

Хотя эта ошибка была обнаружена, я какое-то время не мог придумать хорошего решения, поэтому попытался предложить решение для Vue3.issue.

Кстати, вот несколько заметок для поднятия вопросов:

  1. Обычно у некоторых хороших проектов с открытым исходным кодом есть шаблон задачи, и вы можете создать задачу в соответствии с его рекомендациями.

  2. Чтобы позволить специалистам по сопровождению проектов с открытым исходным кодом быстрее и точнее находить проблемы, обычно вам нужно свести к минимуму повторение проблемы и предоставить демонстрацию, которая воспроизводит проблему, а не предоставлять проблемный проект.

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

  4. Если это действительно ошибка и у вас есть возможность ее исправить, вы можете отправить запрос на вытягивание после поднятия проблемы и непосредственно участвовать в совместной разработке проекта с открытым исходным кодом.Этот процесс будет очень полезен для вашего собственного технического роста. .

Но, к моему смущению, проблема была закрыта менее чем через пять минут после того, как я ее подал, потому что она была связана сvue-router-nextодин из проектовissueповторил.

Я провел общее сканирование проблемы и обнаружил, что она была поднята еще 1 декабря 2020 года, и что многие люди сталкивались с подобными проблемами. По этому вопросу можно найти много связанных вопросов, в том числе вопрос, упомянутый в начале статьи, поэтому я могу обратить на него внимание.

vue-routerМейнтейнер . также пытался ее решить, но столкнулся с некоторыми проблемами, подробности см. в его ответе в выпуске.

К сожалению, до сих пор проблема также не решена, мейнтейнер опубликовал ее.help wanted, надеясь получить помощь от сообщества.

У Vue2 тоже есть эта проблема?

Поскольку наша компания все еще использует Vue2, меня больше всего беспокоит, есть ли у Vue2 эта проблема.

Итак, я написал такое же демо с Vue2, и я рад, что у Vue2 нет этой ошибки, так в чем же причина?

Поскольку Vue используетvue-router3.x версия , это соответствуетRouterViewРеализация компонента следующая:

var View = {
  name: 'RouterView',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render: function render(_, ref) {
    var props = ref.props
    var children = ref.children
    var parent = ref.parent
    var data = ref.data

    data.routerView = true

    var h = parent.$createElement
    var name = props.name
    var route = parent.$route
    var cache = parent._routerViewCache || (parent._routerViewCache = {})

    var depth = 0
    var inactive = false
    while (parent && parent._routerRoot !== parent) {
      var vnodeData = parent.$vnode ? parent.$vnode.data : {}
      if (vnodeData.routerView) {
        depth++
      }
      if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    data.routerViewDepth = depth
    
    if (inactive) {
      var cachedData = cache[name]
      var cachedComponent = cachedData && cachedData.component
      if (cachedComponent) {
        if (cachedData.configProps) {
          fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
        }
        return h(cachedComponent, data, children)
      } else {
        return h()
      }
    }

    var matched = route.matched[depth]
    var component = matched && matched.components[name]
    
    if (!matched || !component) {
      cache[name] = null
      return h()
    }
    
    cache[name] = { component: component }
    
    data.registerRouteInstance = function(vm, val) {
      var current = matched.instances[name]
      if (
        (val && current !== vm) ||
        (!val && current === vm)
      ) {
        matched.instances[name] = val
      }
    }
    
    (data.hook || (data.hook = {})).prepatch = function(_, vnode) {
      matched.instances[name] = vnode.componentInstance
    }
    
    data.hook.init = function(vnode) {
      if (vnode.data.keepAlive &&
        vnode.componentInstance &&
        vnode.componentInstance !== matched.instances[name]
      ) {
        matched.instances[name] = vnode.componentInstance
      }
      
      handleRouteEntered(route)
    }

    var configProps = matched.props && matched.props[name]
    if (configProps) {
      extend(cache[name], {
        route: route,
        configProps: configProps
      })
      fillPropsinData(component, data, route, configProps)
    }

    return h(component, data, children)
  }
}

RouterViewЛогика отрисовки компонента и новая версияvue-router-nextДостижение согласованности: согласно путиrouteи текущийRouterViewГлубина вложенности для соответствия соответствующим компонентам маршрутизации в конфигурации маршрутизации и визуализации.

Разница в том, что версия 3.xvue-routerобработанныйKeepAliveСитуация: если текущийRouterViewЭкземпляр родительского компонента, в котором находится компонент, находится вKeepAliveв построенном дереве и естьinactiveсостояние, то он будет отображаться только как последний визуализированный вид.

Поэтому здесь есть два ключевых момента: один — уметь судить о текущей среде, а другой — кэшироватьRouterViewПоследний визуализированный вид.

Очевидно, вvue-router-next, соответствующей логики нет, в основном из-за отсутствия хранилища в экземпляре компонентаKeepAliveкомпонент, связанныйinactiveусловие,RouterViewКомпоненты также не имеют возможности узнать свое текущее окружение.

По-моему, еслиvue-router-nextДля решения этой проблемы также могут потребоваться некоторые изменения в Vue3, предоставляющие больше информации и данных, позволяющиеRouterViewКогда компонент визуализируется, он знает свое текущее окружение.

Один из способов решения этой проблемы заключается в том, что в этом вложенном маршруте он не используется.KeepAliveпакетRouterView.

Суммировать

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

Версия Vue2 CSP, используемая нашей компанией, основана на версии Vue.js 2.6.11. Сообщество не предоставляет поддержку, поэтому вам нужно сделать это самостоятельно.

У сопровождающих проектов с открытым исходным кодом, естественно, есть свои планы и соображения.Вы не можете просить сопровождающих решить проблему для вас немедленно, потому что ваш проект срочный. Конечно, если вы действительно хотите обратиться за экстренной помощью, подход с более высоким EQ — это пожертвовать мейнтейнерам с открытым исходным кодом.Платя, вы можете повысить приоритетность исправляемых ошибок.

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

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