这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战
更多文章
[vue2]熬夜编写为了让你们通俗易懂的去深入理解nextTick原理
[vue2]熬夜编写为了让你们通俗易懂的去深入理解vuex并手写一个
[vue2]熬夜编写为了让你们通俗易懂的去深入理解双向绑定以及解决监听Array数组变化问题
[vue2]熬夜编写为了让你们通俗易懂的去深入理解v-model原理
熬夜不易,点个赞再走吧
内部实现
router-link
-
默认渲染成a标签,可以通过tag生成别的标签
-
通过绑定click事件,执行VueRouter中的push进行跳转
<router-link to="/about">about</router-link>
router-view
- 当具有层级关系时,每个router-view都有自己的标记,在循环中确认自己的depth,然后在所在的层次中从matched获取组件
const matched = route.matched[depth]
- matched中保存所有父子组件信息,索引从0开始,依次是顶层组件、然后是一层层下来的子组件
初始化
let _Vue;
class VueRouter {
constructor(){}
}
VueRouter.install = (e) => {
_Vue = e
};
// router-link
_Vue.component('router-link', {})
// router-view
_Vue.component('router-view', {})
export default VueRouter;
这个时候我们进行了初始化,在install里设置了全局的_Vue,并初始化了router-link和router-view
router-link
我们需要一个render来渲染, 渲染什么呢
<a href="#/about">about</a>
首先他是一个a标签
然后有一个href
中间的文本用插槽渲染
- 所以我们先用h() 渲染一个虚拟dom
- 第一个参数给一个 'a'
- 第二个参数给一个attrs,包含一个href,利用
#${this.to}
拼接hash链接 - 第三个参数利用插槽获取链接中的文本
// h()返回虚拟dom
render(h) {
return h(
'a',
{
attrs: {
// <a href="#/about">about</a>
href: `#${this.to}`,
},
},
// 通过插槽获取a标签里的文本
this.$slots.default
);
},
接下来看
<router-link to="/about">about</router-link>
标签里的to是一个传参,传的是一个path,利用props来接收,设置一个必传
props: {
to: {
type: String,
require: true,
},
},
于是实现了一个简单的router-link
// router-link
_Vue.component('router-link', {
// <router-link to="/about">about</router-link>
props: {
to: {
type: String,
require: true,
},
},
// h()返回虚拟dom
render(h) {
return h(
'a',
{
attrs: {
// <a href="#/about">about</a>
href: `#${this.to}`,
},
},
// 通过插槽获取a标签里的文本
this.$slots.default
);
},
});
router-view
router-view是怎么实现的呢?
我们总要渲染组件吧,所以先创建一个变量
let component = null
然后我们在 /router/index.js里有一串这样的代码
const routes = [
{
path: '/home',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
];
- 这个routes是一个映射表,之后会挂在this.$router.options里,通过配置去在下面的操作中可以获取对应的组件
- 通过获取的path去找到对应的组件
let current = this.$router.current
let route = this.$router.$options.routes.find((route)=>
route.path==current
)
if(route){
component = route.component
}
- 获取到后渲染
return h(component);
最后是这样
//router-view
_Vue.component('router-view', {
render(h) {
let component = null
// 获取当前路由path
let current = this.$router.current
// 通过获取的path去找到对应的组件
let route = this.$router.$options.routes.find((route)=>
route.path==current
)
if(route){
component = route.component
}
// 获取到后渲染
return h(component);
},
});
其实到这里并没结束,看到这里也许有些疑问,比如this.$router是从哪里来的?
class VueRouter {
constructor(options){
console.log(options)
// 保存这个配置
this.$options = options
})
这里保存了一个option的配置
然后其实在install里我们做了一个这样的操作,利用mixin() 混入在beforeCreate() 的生命周期中去进行了挂载
// 全局混入(如果配置了router,则挂载到根实例上)
// beforeCreate(): 生命周期
_Vue.mixin({
beforeCreate() {
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router;
}
},
});
为什么我们能在这里获取呢?
我们看main.js
import Vue from 'vue'
import App from './App'
import router from './router'
Vue.config.productionTip = false
new Vue({
el: '#app',
// 声明了一个router
router,
components: { App },
template: '<App/>'
})
这里我们在new Vue的时候声明了一个router,而我们在beforeCreate中获取到的this,就是vue的实例,就是new的那个Vue,那就能获取到router,就能通过this.$options.router去进行挂载了
其实到这里也没结束,看到这里也许还有些疑问,比如this.$router.current又是从哪里来的呢?
其实这个时候我们的VueRouter还是空的,存在即合理,那是拿来做什么的呢?就是拿来处理这个current的
我们先默认一个current为"/"
this.current = "/"
监听hash的变化,获取current
当URL的片段标识符更改时,将触发hashchange事件
window.addEventListener('hashchange',()=>{
this.current = window.location.hash.slice(1) || "/"
console.log("hashchange", this.current)
})
其实到这里也没结束,如果调试页面的话会发现current在不断的变化,但是却没有重新渲染,为什么?因为目前current不是一个响应式的,所以只在初始化的时候执行了一次
所以现在要把current变成响应式数据
保证render随着current变化而再次执行
Vue.util.defineReactive定义一个对象的响应属性
Vue.util.defineReactive(obj,key,value,fn)
obj: 目标对象,
key: 目标对象属性;
value: 属性值
fn: 只在node调试环境下set时调用
在this上对current添加响应式的值initial
let initial = window.location.hash.slice(1) || "/"
_Vue.util.defineReactive(this,"current",initial)
完整代码
/router/index.js
import Vue from 'vue'
import VueRouter from './vue-router'
import Home from '../views/Home.vue'
import About from "../views/About.vue"
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/home',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
];
const router = new VueRouter({
routes
})
export default router
/router/vue-router.js
let _Vue;
class VueRouter {
constructor(options){
console.log(options)
// 保存这个配置
this.$options = options
this.current = "/"
// 把current变成响应式数据
// 保证render随着current变化而再次执行
let initial = window.location.hash.slice(1) || "/"
// 在this上对current添加响应式的值initial
_Vue.util.defineReactive(this,"current",initial)
// 监听hash的变化,获取current
// 当URL的片段标识符更改时,将触发hashchange事件
window.addEventListener('hashchange',()=>{
this.current = window.location.hash.slice(1) || "/"
console.log("hashchange", this.current)
})
}
}
VueRouter.install = e => {
_Vue = e;
console.log(_Vue)
// 全局混入(如果配置了router,则挂载到根实例上)
// beforeCreate(): 生命周期
_Vue.mixin({
beforeCreate() {
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router;
}
},
});
// router-link
_Vue.component('router-link', {
// <router-link to="/about">about</router-link>
props: {
to: {
type: String,
require: true,
},
},
// h()返回虚拟dom
render(h) {
return h(
'a',
{
attrs: {
// <a href="#/about">about</a>
href: `#${this.to}`,
},
},
// 通过插槽获取a标签里的文本
this.$slots.default
);
},
});
//router-view
_Vue.component('router-view', {
render(h) {
let component = null
let current = this.$router.current
let route = this.$router.$options.routes.find((route)=>
route.path==current
)
console.log(route)
if(route){
component = route.component
}
console.log(component)
return h(component);
},
});
};
export default VueRouter;
main.js
import Vue from 'vue'
import App from './App'
import router from './router'
Vue.config.productionTip = false
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})