vue-router v4.x核心原理

router-view视图组件通过监听响应式对象currentRoute实现视图的更新。

router/packages/router/src/router.ts

export function createRouter(options: RouterOptions): Router {
    const currentRoute = shallowRef<RouteLocationNormalizedLoaded>(
        START_LOCATION_NORMALIZED
      )
    const router : Router = {
        install(app: App) {
          const router = this
          app.component('RouterLink', RouterLink)
          app.component('RouterView', RouterView)

          if (
            isBrowser &&
            // used for the initial navigation client side to avoid pushing
            // multiple times when the router is used in multiple apps
            !started &&
            currentRoute.value === START_LOCATION_NORMALIZED
          ) {
            // see above
            started = true
            push(routerHistory.location).catch(err => {
              if (__DEV__) warn('Unexpected error when starting the router:', err)
            })
          }
            ```
          app.provide(routerViewLocationKey, currentRoute)}
        }
    return router
}

router/packages/router/src/RouterView.ts

// router-view组件
export const RouterViewImpl = /*#__PURE__*/ defineComponent({
  name: 'RouterView',
  inheritAttrs: false,
  props: {
    name: {
      type: String as PropType<string>,
      default: 'default',
    },
    route: Object as PropType<RouteLocationNormalizedLoaded>,
  },

  // Better compat for @vue/compat users
  // https://github.com/vuejs/router/issues/1315
   ...
    __DEV__ && warnDeprecatedUsage()

    const injectedRoute = inject(routerViewLocationKey)!
    const routeToDisplay = computed<RouteLocationNormalizedLoaded>(
      () => props.route || injectedRoute.value
    )
    const matchedRouteRef = computed<RouteLocationMatched | undefined>(
      () => routeToDisplay.value.matched[depth.value]
    )

    // watch at the same time the component instance, the route record we are
    // rendering, and the name
    watch(
      () => [viewRef.value, matchedRouteRef.value, props.name] as const,
      ([instance, to, name], [oldInstance, from, oldName]) => {
        // copy reused instances
        if (to) {
          // this will update the instance for new instances as well as reused
          // instances when navigating to a new route
          to.instances[name] = instance
          // the component instance is reused for a different route or name, so
          // we copy any saved update or leave guards. With async setup, the
          // mounting component will mount before the matchedRoute changes,
          // making instance === oldInstance, so we check if guards have been
          // added before. This works because we remove guards when
          // unmounting/deactivating components
          if (from && from !== to && instance && instance === oldInstance) {
            if (!to.leaveGuards.size) {
              to.leaveGuards = from.leaveGuards
            }
            if (!to.updateGuards.size) {
              to.updateGuards = from.updateGuards
            }
          }
        }

        // trigger beforeRouteEnter next callbacks
        if (
          instance &&
          to &&
          // if there is no instance but to and from are the same this might be
          // the first visit
          (!from || !isSameRouteRecord(to, from) || !oldInstance)
        ) {
          ;(to.enterCallbacks[name] || []).forEach(callback =>
            callback(instance)
          )
        }
      },
      { flush: 'post' }
    )

finalizeNavigation方法中会对响应式对象currentRoute重新赋值

function finalizeNavigation(toLocation, from, isPush, replace2, data) {
    const error = checkCanceledNavigation(toLocation, from);
    if (error)
      return error;
    const isFirstNavigation = from === START_LOCATION_NORMALIZED;
    const state = !isBrowser ? {} : history.state;
    if (isPush) {
      if (replace2 || isFirstNavigation)
        routerHistory.replace(toLocation.fullPath, assign({
          scroll: isFirstNavigation && state && state.scroll
        }, data));
      else
        routerHistory.push(toLocation.fullPath, data);
    }
    currentRoute.value = toLocation;
    handleScroll(toLocation, from, isPush, isFirstNavigation);
    markAsReady();
  }

什么情况下会触发finalizeNavigation方法呢?

  • 点击自定义组件 router-link 创建的链接会触发finalizeNavigation方法,调用链如下:

vue-router v4.x核心原理

点击自定义组件 router-link后,也会调用historypushState或者replaceState方法修改url。pushStatereplaceState不会触发popstate事件

 function changeLocation(to, state, replace2) {
    const hashIndex = base.indexOf("#");
    const url = hashIndex > -1 ? (location2.host && document.querySelector("base") ? base : base.slice(hashIndex)) + to : createBaseLocation() + base + to;
    try {
      history2[replace2 ? "replaceState" : "pushState"](state, "", url);
      historyState.value = state;
    } catch (err) {
      if (true) {
        warn("Error with push/replace State", err);
      } else {
        console.error(err);
      }
      location2[replace2 ? "replace" : "assign"](url);
    }
  }
  • 手动修改url或者点击浏览器的回退前进按钮,也会触发finalizeNavigation方法,调用链如下:
    vue-router v4.x核心原理

此时会触发window上绑定的popstate事件的回掉函数,对比vue-router v4.x和 vue-router v3.x发现老版本中hash模式对应的hashchange事件已经没有了。

结果实践后发现改变url上的hash部分也会触发popstate事件。

vue-router v4.x核心原理

router/packages/router/src/history/html5.ts

  window.addEventListener('popstate', popStateHandler)
  
  const popStateHandler: PopStateListener = ({
    state,
  }: {
    state: StateEntry | null
  }) => {
    const to = createCurrentLocation(base, location)
    const from: HistoryLocation = currentLocation.value
    const fromState: StateEntry = historyState.value
    let delta = 0

    if (state) {
      currentLocation.value = to
      historyState.value = state

      // ignore the popstate and reset the pauseState
      if (pauseState && pauseState === from) {
        pauseState = null
        return
      }
      delta = fromState ? state.position - fromState.position : 0
    } else {
      replace(to)
    }
  }

vue-router v3.x的监听事件hashchange和popstate

setupListeners () {
    if (this.listeners.length > 0) {
      return
    }

    const router = this.router
    const eventType = supportsPushState ? 'popstate' : 'hashchange'
    window.addEventListener(
      eventType,
      handleRoutingEvent
    )
    this.listeners.push(() => {
      window.removeEventListener(eventType, handleRoutingEvent)
    })
  }

vue-router v4.x核心原理

原文链接:https://juejin.cn/post/7347668079994241043 作者:yanessa_yu

(0)
上一篇 2024年3月19日 上午11:04
下一篇 2024年3月19日 上午11:14

相关推荐

发表回复

登录后才能评论