经过自己平时的学习,也因为自己现在也是从事的Vue的开发,所以平时对Vue相对来说还是较为熟悉,但是平时自己一直有一个很大的痛点,那就是对Vue的底层实现原理等等这些不够了解,所以很多时候定位错误都可能需要花费很多时间,所以深度学习并且掌握一门框架的核心实现是非常有必要的。
主题:学习Vue的全家桶vue-router的实现原理和步骤
目标:清晰思想,了解步骤,动手实现简化版
预备知识:
一、预备知识回顾
1.Vue的插件实现写法:Vue的插件一般是用来为Vue来添加全局功能的,可以用来添加全局方法或者属性、添加全局资源(指令/过滤器等等)、通过全局混入来添加一些组件的选项以及可以添加Vue的实例方法。
MyPlugin.install = function (Vue, options) { // 1.添加全局方法或者属性 Vue.globalMethod = function () {} // 2.添加全局资源 Vue.directive('my-directive', {}) // 3.混入注入组件选项 Vue.mixin({ created: function (){} }) // 4.添加实例方法 Vue.prototype.$myMethod = function(methodOptions){}}
插件的使用可以有2种形式1.第一种那就是在实现的插件代码里面加上代码:if (typeof window !== 'undefined' && window.Vue) { // 使用插件 window.Vue.use(MyPlugin)}2.第二种就是可以直接在main.js中直接使用Vue.use(MyPlugin)
2.render函数的使用:Vue推荐在日常使用中应该使用模板来创建HTML,但是有的时候需要JS的能力,那么这时候就可以使用渲染函数来完成
render: function (createElement) { // createElement函数返回结果是VNode return createElement( tag, // 标签名称 data, // 传递数据 children // 子节点数组 )}
Why:从例子中我们可以看到我们常见的都是使用的render(h),通过学习发现,其实在Vue的底层在生成虚拟DOM方面其实是借鉴的Snabbdom.js,而在这个里面他的生成虚拟DOM的函数就叫h,所以其实h就是我们上面看到的createElement
// 例子heading组件// {{title}}Vue.component('heading', { props: { title: { type: String, default: '' } }, render(h) { return h( 'h2', // 参数1:tagname {attrs: {title: this.title} } // 参数2:与模板中属性对应的数据对象 this.$slots.default // 参数3:子节点VNode数组 ) }})
3.数据响应式方法:Vue.util.defineReactive / new Vue ({data: {}})
我们知道Vue最大的亮点之一就是响应式,凡是写入data里面的都会被变成响应式数据,而Vue2.x当中实现响应式就是使用的defineReactive,来看下具体的用法:
// defineReactive:定义一个对象的响应属性 Vue.util.defineReactive(obj, key, value, fn) obj:目标对象 key:目标对象属性 value:属性值 fn:只在node调试环境下set时调用
其实Vue.util中还有一些函数,如:
二、Vue全家桶之Vue-router的手动实现
1.在我们手动实现vue-router之前,我们先看一下他在Vue中的使用
vue add router
import Router from 'vue-router'Vue.use(Router)
export default new Router({...})
import router from './router'new Vue({ router}).$mount('#app')
"/">home"/about">aboutthis.$router.push('/')this.$router.push('/about')
2.我们发现作为Vue全家桶之一的vue-router使用起来非常方便,帮助我们很好的处理了路由的跳转的问题,那么现在我们就来看看怎么实现一个简化版的vue-router,从而更加了解他的内部原理
4.代码编写:
我们在使用vue-router的时候我们可以发现,我们是先使用use注册的,然后再去创建的Router实例,但是我们在写install方法的时候又需要用到,所以我们只能只用mixin来做延迟处理,等到有了组件实 例之后我们再进行使用
// 新建一个kvue-router.js// 引用构造函数,VueRouter中要使用let Vue// 保存用户的选项class VueRouter() { constructor(options){ this.$options = options }}// 实现VueRouter的install方法,注册$router在Vue的原型之上VueRouter.install = function(_Vue) { // 引用构造函数,VueRouter要使用 Vue = _Vue Vue.mixin({ beforeCreate(){ if(this.$options.router) { // 将$router挂载到Vue的原型之上,方便所有的组件都可以使用this.$router Vue.prototype.$router = this.$options.router } } }) // 实现俩个全局的组件router-link和router-view Vue.component('router-link', RouterLink) Vue.component('router-view', RouterView)}export default VueRouter
// router-linkVue.component('router-link', { props: { to:{ type: String, required: true } }, render(h) { return h( 'a', attrs: { href: '#'+ this.to }, this.$slots.default ) }})// router-viewVue.component('router-view', { render(h) { return h(null) }})
class VueRouter(){ constructor(options) { // 定义一个响应式的数据current表示路由的变化 const initial = window.location.hash.slice(1) || '/' // Vue中的响应式方法 Vue.defineReactive(this, 'current', initial) // 监听hashChange事件 window.addEventListener('hashChange', this.onHashChange.bind(this)) } onHashChange(){ this.current = window.location.hash.slice(1) }}
// router-viewVue.component('router-view', { render(h) { // 动态的获取组件,进行内容的渲染 let component = null const route = this.$router.$options.routes.find(route => route.path === this.$router.current) if(route) component = route.component return h(component) }})
class VueRouter { constructor(options) { // 缓存path和route映射关系 this.routeMap = {} this.$options.routes.forEach(route => { this.routeMap[route.path] = route }); }}// router-viewexport default { render(h) { const {routeMap, current} = this.$router const component = routeMap[current] ? routeMap[current].component : null; return h(component); }}
四、总结:
通过学习,Vue全家桶之一的Vue-router就算是手撸完成了,虽然只是简单版本的,但是感觉对于自己来说也是一个小的进展,基本原理基本清晰了,而且写完之后再去看源代码也清晰容易了不少,然后现在写完的这个简单的版本其实还差了一块,那就是没有实现子路由的嵌套,子路由的嵌套的逻辑上是路由嵌套,但是物理上其实就是router-view的嵌套,通过后续翻看源码发现,源码的思路是:先给所有的router-view组件的data都设置一个routerView属性,然后设置一个变量depth路由深度,然后去遍历,如果parent的$vnode.data里的routerView属性是true的话,depth就++,然后因为每次URL匹配的时候,结果都是一个数组,从根开始依次按层级放入,然后我们通过route.matched[depth]就可以知道该渲染哪一层的component的了。