vue-router 源码解析 | 6k 字 | 多图预警 - 【下】
- 各位好,我是光辉 😎
- 本篇是
vue-router源码解析的下篇,即收尾篇 - 本篇主要介绍了下面几点
- 介绍了
vue-router是如何处理滚动的 view、link组件都怎么实现的?- 路由变化又是怎么触发重新渲染的等等
- 介绍了
- 另外还是要说一下
- 第一次做源码解析,肯定有很多错误或理解不到位的地方,欢迎指正 🤞
- 项目地址
https://github.com/BryanAdamss/vue-router-for-analysis- 如果觉得对你有帮助,记得给我一个
star✨
- uml 图源文件
https://github.com/BryanAdamss/vue-router-for-analysis/blob/dev/vue-router.EAP
- 关联文章链接
滚动处理
- 我们知道
vue-router可以处理一些滚动行为,例如记录页面滚动位置,然后在切换路由时滚到顶部或保持原先位置; - 它主要接收一个
scrollBehavior参数,scrollBehavior有以下玩法
1 |
|
- 既支持滚动到指定位置,也可以滚动页面某个页面锚点位置和异步滚动
- 那它是如何做到呢?具体的逻辑又是怎样的呢?
- 我们前面都知道
HTML5History在初始化时和HashHistory在setupListener时都会调用setupScroll函数,初始化滚动相关的逻辑 - 并在
popstate或hashchange事件触发路由跳转时,调用handleScroll处理滚动行为
1 | // src/history/hash.js |
- 我们先看滚动的初始化
setupScroll
- 代码位于
src/util/scroll.js
1 | // 初始化滚动相关逻辑 |
- 可以看到其利用
History API的来完成位置的保存 - 在
popstate时记录滚动位置并更新状态obj的key - 这个
key是用来在state中标识每个路由用的 - 可以看下
key的存取
1 | // src/util/state-key.js |
- 可以看到声明了一个
_key,其是一个三位的时间戳,更新和读取都是操作这一个_key setupScroll时,首先拷贝了当前的state,并为其生成一个唯一key- 通过
replaceState将添加了key的state保存到当前路由的absolutePath上 - 然后监听
popstate事件,其只能通过浏览器的 前进/后退 按钮触发 - 触发后会保存当前位置,并更新
_key - 这样就可以在路由发生变化触发
popstate时,保存当前位置并设置唯一_key - 看下其是如何存取位置信息的
1 | // src/util/scroll.js |
- 其利用
positionStore对象配合唯一的_key来存取位置 - 在
handleScroll时就可以通过_key取出之前保存的位置
handleScroll
- 处理滚动的代码位于
src/util/scroll.js
1 |
|
- 在
$nextTick中调用getScrollPosition获取之前保存好的位置 - 再调用我们传入的
scrollBehavior查看其返回值来确定是否需要进行滚动 - 还判断了一波是否是异步滚动
- 若是,则等待其
resolved再调用scrollToPosition - 否则直接调用
scrollToPosition getScrollPosition、scrollToPosition代码如下
1 | // src/util/scroll.js |
- 获取滚动位置,是利用
_key从positionStore上读取之前保存的位置信息 scrollToPosition的逻辑很清晰,其处理了滚动到指定dom和直接滚动到特定位置的场景
小结
vue-router处理滚动主要利用了History API可以保存状态的特性实现- 在路由进入前保存滚动位置,并在下次路由变化时,尝试取回之前位置,在
$nextTick中真正的处理滚动 - 其支持滚动到指定位置、指定 DOM、异步滚动等场景
view 组件
vue-router内置了router-view、router-link两个组件- 前者负责在匹配到路由记录后将对应路由组件渲染出来
- 后者支持用户在具有路由功能的应用中 (点击) 导航
- 我们先来看
router-view组件
router-view
router-view的主要职责就是将路由组件渲染出来- 定义位于
src/components/view.js
1 | // src/components/view.js |
- 其被定义成一个函数式组件,这代表它没有状态和实例(this 上下文),只接收了
name来做命名视图 - 我们重点看下
render方法 - 由于其是一个函数式组件,所以很多操作是借助父节点来完成的
- 为了支持解析命名插槽,其没有使用自己的
createElement方法,而是使用父节点的createElement方法 - 由于没有 this 上下文,无法通过
this.$route获得当前路由对象,干脆就直接使用父节点的$route
- 为了支持解析命名插槽,其没有使用自己的
- 可以看到添加了一个标志量
routerView,主要用来在vue-devtools中标识view组件和在查找深度时用 - 然后声明了一个缓存对象
_routerViewCache并赋值给cache变量,用来在keep-alive激活时快速取出被缓存的路由组件 - 开始从当前节点往上查找
Vue根实例,在查找的过程中计算出view组件的深度以及是否被kepp-alive包裹并处于inative状态 depth主要用来获取当前view对应的路由记录- 前面说过,
vue-router是支持嵌套路由的,对应的view也是可以嵌套的 - 而且在匹配路由记录时,有下面的逻辑,
当一个路由记录匹配了,如果其还有父路由记录,则父路由记录肯定也是匹配的,其会一直向上查找,找到一个父记录,就通过unshift塞入route.matched数组中的,所以父记录肯定在前,子记录在后,当前精准匹配的记录在最后- 见
src/util/route.js formatMatch方法
- 见
depth的计算在遇到父view组件时,自增 1,通过不断向上查找,不断自增depth,直到找到Vue根实例才停止- 停止时
route.matched[depth]值就是当前view对应的路由记录 - 有了路由记录,我们就可以从上取出对应的路由组件实例,然后渲染即可
- 关于路由记录和路由组件实例是如何绑定的,我们下面会讲
- 前面说过,
- 我们先看
非inactive状态是如何渲染路由组件实例的- 通过
route.matched[depth]取出当前view匹配的路由记录 - 然后再取出对应的路由组件实例
- 如果路由记录和路由组件实例有一个不存在,则渲染空结点,并重置
cache[name]值 - 如果都能找到,则先把组件实例缓存下来
- 如果有配置动态路由参数,则把路由参数缓存到路由组件实例上,并调用
fillPropsinData填充props
- 如果有配置动态路由参数,则把路由参数缓存到路由组件实例上,并调用
- 调用
h渲染对应的路由组件实例即可
- 通过
- 当组件处于
inactive状态时,我们就可以从cache中取出之前缓存的路由组件实例和路由参数,然后渲染就可以了 - 主流程如上,但还有一个重要的点没提
- 路由记录和路由组件实例是如何绑定的?
- 相信你已经注意到
data.registerRouteInstance方法,没错,他就是用来为路由记录绑定路由组件实例的
registerInstance
- 我们先看下调用的地方
- 主要在
src/install.js的全局混入中
1 | export function install(Vue){ |
- 可以看到其在全局混入的
beforeCreate、destroyed钩子中都有被调用 - 前者传入了两个 vm 实例,后者只传入了一个 vm 实例
- 我们看下实现,代码也位于
src/install.js中
1 | // 为路由记录、router-view关联路由组件 |
- 可以看到其接收一个
vm实例和callVal做为入参 - 然后取了
vm的父节点做为 i 的初值 - 接着一步一步给
i赋值,同时判断i是否定义 - 到最后,
i的值为vm.$options._parentVnode.data.registerRouteInstance - 然后将两个入参传入
i中调用 - 注意,这时的 i 是 vm 父节点上的方法,并不是 vm 上的方法
- 我们全局检索下
registerRouteInstance关键字,发现其只被定义在了view.js中,也就是router-view组件中- 结合上面一条,i 即
registerRouteInstance是vm父节点上的方法,而只有router-view组件定义了registerRouteInstance - 所以,只有当
vm是router-view的子节点时,registerRouteInstance方法才会被调用 i(vm, callVal)可以表达为vm._parentVnode.registerRouteInstance(vm,vm)
- 结合上面一条,i 即
- 看下
registerRouteInstance的实现
1 | // src/components/view.js |
matched保存的是当前匹配到的路由记录,name是命名视图名- 如果
val存在,并且当前路由组件和传入的不同,重新赋值 - 如果
val不存在,且当前路由组件和传入的相同,也重新赋值,但是此时 val 为undefined,相当于解绑 - 可以看到参数数量不同,一个函数实现了绑定和解绑的双重操作
- 通过这个方法就完成了路由记录和路由组件实例的绑定与解绑操作
- 这样就可以在
view组件render时,通过route.matched[depth].components[name]取到路由组件进行渲染 - 还有些场景也需要进行绑定
- 当相同组件在不同路由间复用时,需要为路由记录绑定路由组件
keep-alive组件被激活时,需要为路由记录绑定路由组件
小结
router-view是一个函数式组件,有时需要借助父节点的能力,例如使用父节点的渲染函数来解析命名插槽- 通过
routerView来标识view组件,方便vue-devtools识别出view组件和确定view组件深度 - 通过向上查找,确定当前
view的深度depth,通过depth取到对应的路由记录 - 再取出通过
registerInstance绑定的路由组件实例 - 如果有动态路由参数,则先填充
props然后再渲染 - 如果
view被keep-alive包裹并且处于inactive状态,则从缓存中取出路由组件实例并渲染
如何触发重新渲染
- 在导航解析的章节,我们提过,导航解析成功后
- 会调用
updateRoute方法,重新为全局的_routerRoot._route即$route赋值
1 | // src/history/base.js |
- 在
view组件中,会使用$parent.$route即全局的_routerRoot._route
1 | // src/components/view.js |
- 而在
install.js的全局混入中,将_route定义为响应式的,依赖了_route的地方,在_route发生变化时,都会重新渲染
1 | // src/install.js |
- 这样就完成了渲染的闭环,
view依赖$route,导航解析成功更新$route,触发view渲染 - 看完了
view组件,我们来看下另外一个组件router-link
link 组件
router-link组件被定义在src/components/link.js中- 主要用来支持用户在具有路由功能的应用中 (点击) 导航
router-link
1 | /* @flow */ |
- 其实现就是一个普通的组件,实现了点击时跳转到
to对应的路由功能 - 由于支持点击时需要标识样式类、精准匹配
exact场景,所以通过sameRoute、isIncludedRoute来实现样式类的标识和精准匹配标识 - 在点击时,屏蔽了部分特殊场景,如点击时同时按下
ctrl、alt、shift等control keys时,不做跳转 - 看完组件后,我们再来看看
router还给我们提供哪些实例方法
实例属性、方法
router对外暴露了很多属性和方法- 这些属性和方法在前面的源码部分也都有用过
实例属性
router.app- 配置了 router 的 Vue 根实例
router.mode- 路由使用的模式
router.currentRoute- 当前路由对象,等同于
this.$route
- 当前路由对象,等同于
实例方法
- 用注册全局导航守卫
router.beforeEachrouter.beforeResolverouter.afterEach
- 编程式导航相关
router.pushrouter.replacerouter.gorouter.backrouter.forward
- 服务端渲染相关
router.getMatchedComponents- 返回目标位置或是当前路由匹配的组件数组 (是数组的定义/构造类,不是实例)
router.onReady- 该方法把一个回调排队,在路由完成初始导航时调用,这意味着它可以解析所有的异步进入钩子和路由初始化相关联的异步组件
router.onError- 注册一个回调,该回调会在路由导航过程中出错时被调用
- 动态路由
router.addRoutes- 动态添加路由规则
- 解析
router.resolve- 传入一个对象,尝试解析并返回一个目标位置
总结
- 至此,我们完成了
vue-router@2的所有源码分析 🎉 - 如果您觉得还可以,记得帮我点个赞 👍
参考
- https://github.com/dwqs/blog/issues/53
- https://github.com/dwqs/blog/issues/54
- https://github.com/dwqs/blog/issues/55
- https://github.com/vuejs/vue-router/issues/2184#issuecomment-393484643
- https://juejin.im/post/5e456513f265da573c0c6d4b
- https://juejin.im/post/584040e1ac502e006cbedb23
- https://juejin.im/post/5df0ada0f265da33d56d1a2f
- https://juejin.im/post/5dbed0bef265da4cff701f68
- https://www.jianshu.com/p/29e8214d0bee
- https://ustbhuangyi.github.io/vue-analysis/v2/vue-router/
- https://blog.liuyunzhuge.com/2020/04/08/vue-router 源码:create-matcher/
PS
- 后面还会介绍其余部分,如果觉得还行,可以给个赞哦 ✨
- 个人
github,也总结了一些东西,欢迎 star - 基于 canvas 的绘图板drawing-board
- 前端入门 demo、最佳实践集合 fe-awesome-demos
- 一个自动生成别名的
vue-cli-pluginhttps://www.npmjs.com/package/vue-cli-plugin-auto-alias
npm 包
交流
- 如果有问题,可以加微信交流,共同成长,共同进步~