學習Vue的全家桶vue-router的實現原理和步驟
1、Vue的插件實現寫法
2、render函數的使用
3、數據響應式方法:Vue.util.defineReactive / new Vue ( { data: {} } )
一、預備知識回顧
Vue的插件一般是用來為Vue來添加全局功能的,可以用來添加全局方法或者屬性、添加全局資源(指令/過濾器等等)、通過全局混入來添加一些組件的選項以及可以添加Vue的實例方法。
1MyPlugin.install = function (Vue, options) {
2// 1.添加全局方法或者屬性
3 Vue.globalMethod = function () {}
4// 2.添加全局資源
5 Vue.directive('my-directive', {})
6// 3.混入注入組件選項
7 Vue.mixin({
8 created: function (){}
9 })
10// 4.添加實例方法
11 Vue.prototype.$myMethod = function(methodOptions){}
12}
1插件的使用可以有2種形式
21.第一種那就是在實現的插件代碼裡面加上代碼:
3if (typeof window !== 'undefined' && window.Vue) {
4// 使用插件
5window.Vue.use(MyPlugin)
6}
72.第二種就是可以直接在main.js中直接使用Vue.use(MyPlugin)
Vue推薦在日常使用中應該使用模板來創建HTML,但是有的時候需要JS的能力,那麼這時候就可以使用渲染函數來完成
1render: function (createElement) {
2 // createElement函數返回結果是VNode
3 return createElement(
4 tag, // 標籤名稱
5 data, // 傳遞數據
6 children // 子節點數組
7 )
8}
Why:從例子中我們可以看到我們常見的都是使用的render(h),通過學習發現,其實在Vue的底層在生成虛擬DOM方面其實是借鑑的Snabbdom.js,而在這個裡面他的生成虛擬DOM的函數就叫h,所以其實h就是我們上面看到的createElement
1// 例子heading組件
2// <heading :title="title">{{title}}</heading>
3Vue.component('heading', {
4 props: {
5 title: {
6 type: String,
7 default: ''
8 }
9 },
10 render(h) {
11 return h(
12 'h2', // 參數1:tagname
13 {attrs: {title: this.title} } // 參數2:與模板中屬性對應的數據對象
14 this.$slots.default // 參數3:子節點VNode數組
15 )
16 }
17})
3.數據響應式方法:Vue.util.defineReactive / new Vue ({data: {}})
我們知道Vue最大的亮點之一就是響應式,凡是寫入data裡面的都會被變成響應式數據,而在Vue當中想實現某個數據的響應式有幾種,比如可以藉助Vue工具包util中的defineReactive,來看下具體的用法:
1// defineReactive:定義一個對象的響應屬性
2 Vue.util.defineReactive(obj, key, value, fn)
3 obj:目標對象
4 key:目標對象屬性
5 value:屬性值
6 fn:只在node調試環境下set時調用
其實Vue.util中還有一些函數,如:
二、Vue全家桶之Vue-router的手動實現
1.在我們手動實現vue-router之前,我們先看一下他在Vue中的使用
1import Router from 'vue-router'
2Vue.use(Router)
1export default new Router({...})
1import router from './router'
2new Vue({
3 router
4}).$mount('#app')
1<router-view></router-view>
1<router-link to="/">home</router-link>
2<router-link to="/about">about</router-link>
3
4this.$router.push('/')
5this.$router.push('/about')
6
2.我們發現作為Vue全家桶之一的vue-router使用起來非常方便,幫助我們很好的處理了路由的跳轉的問題,那麼現在我們就來看看怎麼實現一個簡化版的vue-router,從而更加了解他的內部原理
實現一個插件:創建VueRouter類和install方法
我們在使用vue-router的時候我們可以發現,我們是先使用use註冊的,然後再去創建的Router實例,但是我們在寫install方法的時候又需要用到,所以我們只能只用mixin來做延遲處理,等到有了組件實 例之後我們再進行使用
1// router-view
2// 新建一個kvue-router.js
3// 引用構造函數,VueRouter中要使用
4let Vue
5// 保存用戶的選項
6class VueRouter() {
7 constructor(options){
8 this.$options = options
9 }
10}
11// 實現VueRouter的install方法,註冊$router在Vue的原型之上
12VueRouter.install = function(_Vue) {
13 // 引用構造函數,VueRouter要使用
14 Vue = _Vue
15 Vue.mixin({
16 beforeCreate(){
17 if(this.$options.router) {
18 // 將$router掛載到Vue的原型之上,方便所有的組件都可以使用this.$router
19 Vue.prototype.$router = this.$options.router
20 }
21 }
22 })
23 // 實現倆個全局的組件router-link和router-view
24 Vue.component('router-link', RouterLink)
25 Vue.component('router-view', RouterView)
26}
27
28export default VueRouter
創建router-view和router-link
1// router-link
2Vue.component('router-link', {
3 props: {
4 to:{
5 type: String,
6 required: true
7 }
8 },
9 render(h) {
10 return h(
11 'a',
12 attrs: { href: '#'+ this.to },
13 this.$slots.default
14 )
15 }
16})
17
18// router-view
19Vue.component('router-view', {
20 render(h) {
21 return h(null)
22 }
23})
1class VueRouter(){
2 constructor(options) {
3 // 定義一個響應式的數據current表示路由的變化
4 const initial = window.location.hash.slice(1) || '/'
5 // Vue中的響應式方法
6 Vue.defineReactive(this, 'current', initial)
7 // 監聽hashChange事件
8 window.addEventListener('hashChange', this.onHashChange.bind(this))
9 }
10 onHashChange(){
11 this.current = window.location.hash.slice(1)
12 }
13}
1// router-view
2Vue.component('router-view', {
3 render(h) {
4 // 動態的獲取組件,進行內容的渲染
5 let component = null
6 const route = this.$router.$options.routes.find(route => route.path === this.$router.current)
7 if(route) component = route.component
8 return h(component)
9 }
10})
提前處理路由表:到這裡其實我們已經實現了整個vue-router的核心功能,我們去用我們寫好的kvue-router去替換掉vue-router,是可以實現我們想要的功能的
一個疑問:從上一步我們動態獲取對應組件的邏輯裡面我們不難發現,如果直接就這樣寫,我們會發現只要路由一變化,render就會執行一遍,這時候裡面的遍歷循環也會相應執行一遍,那怎麼處理這個問題呢?
回答:這個地方我們需要做一下優化,我們可以這樣想,如果我們可以提前去處理好path和route的映射關係,那麼不就可以避免每次都要循環了嗎
1// router-view
2class VueRouter {
3 constructor(options) {
4 // 緩存path和route映射關係
5 this.routeMap = {}
6 this.$options.routes.forEach(route => {
7 this.routeMap[route.path] = route
8 });
9 }
10}
11
12// router-view
13export default {
14 render(h) {
15 const {routeMap, current} = this.$router
16 const component = routeMap[current] ? routeMap[current].component : null;
17 return h(component);
18 }
19}
截止到這裡,咱們的kvue-router基本核心部分算是完成了,但是回想咱們日常使用的router,會發現現在我們寫的並不支持子路由,也就是說有嵌套的我們現在是不支持的,那怎麼解決這個問題呢,咱們接下來繼續
思考:子路由的嵌套更深層次的含義是什麼?
思路:我們想一下,其實子路由的嵌套在邏輯上是路由的嵌套,但是那在實際上呢?回到代碼中看,那實際上是不是就是router-view相互的嵌套,那麼我們現在需要做的不就是去遞歸遍歷路由表嗎?但是我們要思考如何找到每個子路由對應的組件呢?
源碼思想:通過看源碼發現,先給router-view增加一個routerView的屬性,然後初始化一個深度變量depth,然後去遞歸組件的父級看是否存在routervView為true,如果是則depth++,同時會定義一個響應式的數組matched,並且使用一個遍歷路由表的方法match來進行遞歸遍歷查看是否存在子路由,最後通過matched[depth]找到該層級
代碼實現:
1Vue.component('router-view', {
2 render(h) {
3 // 標記當前router-view的深度
4 this.$vnode.data.routerView = true
5 let depth = 0
6 let parent = this.$parent
7 while(parent) {
8 const vnodeData = parent.$vnode && parent.$vnode.data
9 if (vnodeData) {
10 if (vnodeData.routerView) {
11 depth++
12 }
13 }
14 parent = parent.$parent
15 }
16 }
17})
1class VueRouter {
2 constructor (options) {
3 Vue.util.defineReactive(this, 'matched', [])
4 // match方法可以遞歸遍歷路由表,獲得匹配關係數組
5 this.match()
6 }
7 //match遍歷路由表方法
8 match (routes) {
9 routes = routes || this.$options.routes
10 // 遞歸遍歷
11 for (const route of routes) {
12 if (route.path === '/' && this.current === '/') {
13 this.matched.push(route)
14 return
15 }
16 if (route.path !== '/' && this.current.indexOf(route.path) != -1) {
17 this.matched.push(route)
18 if (route.children) {
19 this.match(route.children)
20 }
21 return
22 }
23 }
24 }
25}
1Vue.component('router-view', {
2 render(h) {
3 ...
4 let component = null
5 const route = this.$router.matched[depth]
6 if (route) {
7 component = route.component
8 }
9 return h(component)
10 }
11})
1onHashChange() {
2 this.current = window.location.hash.slice(1)
3 this.matched = []
4 this.match()
5}
以上便是全部關於手寫kvue-router的內容了,代碼量其實不是很大,主要是關於源碼處理的思路,學會思路更為重要,寫完一遍之後會加深很多對於vue-router的理解,以後用起來也會更加順暢。
最後附上GitHub地址:
https://github.com/guangyingmi/Vue
給個三連唄