vueRouter简记(下)

简介: 「这是我参与2022首次更文挑战的第16天,活动详情查看:2022首次更文挑战」

「这是我参与2022首次更文挑战的第16天,活动详情查看:2022首次更文挑战


前言


最近抽时间学习了vueRouter源码,基本也就是走马观花式地看了一遍。虽然很多细节和原理没有去深入分析,但还是想通过博客记录下自己从中学习到的点滴。


history


history实现了路由切换及守卫函数执行的逻辑,我们一起看看其中关键逻辑。


构造函数


比较简单,就是初始化了一些实例属性

// router实例
this.router = router
// base的处理
this.base = normalizeBase(base)
// start with a route object that stands for "nowhere"
this.current = START
this.pending = null
this.ready = false
this.readyCbs = []
this.readyErrorCbs = []
this.errorCbs = []
this.listeners = []
复制代码


listen


listen可以传入回调函数,当history修改将调用cb


listen (cb: Function) {
  this.cb = cb
}
复制代码


transitionTo


我们修改路由,无论是通过浏览器前进后退,push方法或者直接修改链接实际都将调用transitionTo。


首先就是通过路径location匹配mather中生成的record

// transitionTo
let route
// catch redirect option https://github.com/vuejs/vue-router/issues/3201
try {
  route = this.router.match(location, this.current)
}
复制代码


然后再调用confirmTransition,我们先看看成功回调

this.confirmTransition(
  route,
  () => {
    // 切换成功后执行的回调函数
    // 更新当前路径即history.current
    // 执行listen中定义的cb
    this.updateRoute(route)
    onComplete && onComplete(route)
    this.ensureURL()
    // 全局后置钩子调用
    this.router.afterHooks.forEach(hook => {
      hook && hook(route, prev)
    })
  }
)
复制代码


confirmTransition


confirmTransition即执行路由切换,伴随路由切换的关键逻辑在于各个组件的导航守卫执行。


首先通过resolveQueue获取组件列表(分为更新组件 失效组件 激活组件)

const { updated, deactivated, activated } = resolveQueue(
  this.current.matched,
  route.matched
)
复制代码


resolveQueue的实现很有意思,利用了前后的matched数组进行对比,仅单次遍历就实现组件获取,应该属于一个双指针算法的实现吧。

let i
const max = Math.max(current.length, next.length)
for (i = 0; i < max; i++) {
  if (current[i] !== next[i]) {
    break
  }
}
return {
  updated: next.slice(0, i),
  activated: next.slice(i),
  deactivated: current.slice(i)
}
复制代码


我们回到刚才,我们上面获取到了updated,deactivated,activated列表。接下来就行执行其中的导航守卫了。

const queue: Array<?NavigationGuard> = [].concat(
  // 实际对应我们守卫的执行顺序
  // 具体的extractLeaveGuards等逻辑就不展开了
  extractLeaveGuards(deactivated),
  this.router.beforeHooks,
  extractUpdateHooks(updated),
  activated.map(m => m.beforeEnter),
  resolveAsyncComponents(activated)
)
复制代码


接下来就是执行runQueue逻辑了。实际就是按顺序执行我们之前定义的queue。为什么需要runQueue呢?


我们在使用的时候可以使用next()来执行下一步,并且可以取消。所以这里不能直接执行所有钩子,必须通过next来确定走到下一步。

runQueue(queue, iterator, () => {
  // wait until async components are resolved before
  // extracting in-component enter guards
  const enterGuards = extractEnterGuards(activated)
  const queue = enterGuards.concat(this.router.resolveHooks)
  runQueue(queue, iterator, () => {
    if (this.pending !== route) {
      return abort(createNavigationCancelledError(current, route))
    }
    this.pending = null
    onComplete(route)
    if (this.router.app) {
      this.router.app.$nextTick(() => {
        handleRouteEntered(route)
      })
    }
  })
})
复制代码


runQueue的定义在asyncJS中,比较经典,大家可以自己去理解下。

export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
  const step = index => {
    if (index >= queue.length) {
      cb()
    } else {
      if (queue[index]) {
        // 关键步骤
        // 将step+1作为callBack传递给fn
        // 实际调用next就是执行step(index + 1)
        // 而执行step(index + 1)则是取执行队列的下一步queue[index + 1]来执行
        fn(queue[index], () => {
          step(index + 1)
        })
      } else {
        step(index + 1)
      }
    }
  }
  step(0)
}
复制代码


matched属性


我们再补充个matched属性的由来。


matched属性记录了从父到子的路由record,对应我们定义的嵌套路由。


我们的record中使用parent记录了父record,因此可以通过递归寻找parent来生成整个嵌套路径的record。实际vueRouter通过formatMatch函数来生成matched数组。

function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
  const res = []
  while (record) {
    res.unshift(record)
    record = record.parent
  }
  return res
}
复制代码


地址更新


我们前面讲了路由的修改更新,但是没有提到如何同步到浏览器地址栏中,我们来看看其中的逻辑实现。


以hash模式为例,在我们调用push的时候,实际将调用hashHistory中的push方法。

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  const { current: fromRoute } = this
  this.transitionTo(
    location,
    route => {
      pushHash(route.fullPath)
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    },
    onAbort
  )
}
复制代码


可以看到在transitionTo执行完毕后,将执行pushState或直接修改hash来修改浏览器状态。


地址栏响应


我们再来看看修改地址栏的时候是如何触发路由切换的。以hash模式为例,实际在初始化时候会执行setupListeners函数,而其中的关键逻辑在于监听浏览器history


const eventType = supportsPushState ? 'popstate' : 'hashchange'
window.addEventListener(
  eventType,
  handleRoutingEvent
)
复制代码


handleRoutingEvent的主要逻辑则是调用transitionTo实现路由切换。

const handleRoutingEvent = () => {
  const current = this.current
  if (!ensureSlash()) {
    return
  }
  // 通过getHash获取当前浏览器hash
  this.transitionTo(getHash(), route => {
    if (supportsScroll) {
      handleScroll(this.router, route, current, true)
    }
    if (!supportsPushState) {
      replaceHash(route.fullPath)
    }
  })
}
复制代码


routerLink


routerLink的实现就比较简单了,是个组件实现。


其主要逻辑在于渲染a标签,并且监听click函数进行路由切换的控制。


首先通过router.resolve方法获取路径及record,用于实现active等类名添加。

// RouterLink render
const router = this.$router
const current = this.$route
const { location, route, href } = router.resolve(
  this.to,
  current,
  this.append
)
复制代码


再定义处理函数,主要逻辑在于调用路由的push方法并传入需要跳转的链接location

// RouterLink render
const handler = e => {
  if (guardEvent(e)) {
    if (this.replace) {
      router.replace(location, noop)
    } else {
      router.push(location, noop)
    }
  }
}
const on = { click: guardEvent }
if (Array.isArray(this.event)) {
  this.event.forEach(e => {
    on[e] = handler
  })
} else {
  on[this.event] = handler
}
复制代码


最后进行组件的渲染逻辑,执行h方法

// RouterLink render
return h(this.tag, data, this.$slots.default)
复制代码


routerView


routerView的逻辑相对于routerLink会复杂一些。


和routerLink一样,关键逻辑都在render函数中

// 首先会定义routerView属性在后面会使用到
data.routerView = true
const h = parent.$createElement
const name = props.name
// 这边值得一说
// 在这里访问了父组件的$route属性
// 实际会访问到installJS中定义的_router属性
// 而_router被定义为响应式数据
// 因此当路由切换时触发的_router修改将导致渲染函数重新执行
// 非常nice的关联
const route = parent.$route
const cache = parent._routerViewCache || (parent._routerViewCache = {})
复制代码


接下来便是嵌套routerView的实现了,通过前面定义的routerView来标记当前组件为routerView。如此便可以通过组件的parent.$vode.data向上寻找计数routerView的嵌套深度。


// determine current view depth, also check to see if the tree
// has been toggled inactive but kept-alive.
let depth = 0
let inactive = false
while (parent && parent._routerRoot !== parent) {
  const 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
复制代码


而通过depth和matched则可以获取到对应匹配到的同层recored,进而进一步获取组件实例。


const matched = route.matched[depth]
const component = matched && matched.components[name]~
复制代码


接着再调用组件的渲染函数实际渲染成匹配组件的视图component而非routerView。

return h(component, data, children)
复制代码


结语


本篇文章记录了自己在vueRouter源码学习中的一些关键逻辑梳理。可能有些地方不够细节或者有错误地方欢迎指出。



相关文章
|
9月前
|
存储 JavaScript 前端开发
vuex中的辅助函数
vuex中的辅助函数
|
9月前
|
前端开发
react实现步进器
react实现步进器
|
JavaScript 网络架构
vueRouter简记(上)
前言 最近抽时间学习了vueRouter源码,基本也就是走马观花式地看了一遍。虽然很多细节和原理没有去深入分析,但还是想通过博客记录下自己从中学习到的点滴。
|
设计模式 JavaScript
学习Vue3 第二十四章(兄弟组件传参和Bus)
A 组件派发事件通过App.vue 接受A组件派发的事件然后在Props 传给B组件 也是可以实现的缺点就是比较麻烦 ,无法直接通信,只能充当桥梁
92 0
|
JavaScript 前端开发
|
JavaScript 索引
|
JavaScript Go API
大白话理解和初步使用vue-router
大白话理解和初步使用vue-router
416 0
|
JavaScript 前端开发 数据管理
大白话理解和初步使用vuex
大白话理解和初步使用vuex
110 0
|
JavaScript
vue再读84-vue-router嵌套路由
vue再读84-vue-router嵌套路由
51 0
vue再读84-vue-router嵌套路由