Vue-Router 原理

一、背景

當(dāng)我們?cè)谑褂胿ue-router的時(shí)候是否會(huì)產(chǎn)生疑惑,為什么這個(gè)東西能幫助我們建立起url與頁(yè)面組件之間的映射關(guān)系?vue-router的hash模式和history模式有什么區(qū)別?以及他們是怎樣實(shí)現(xiàn)的?一直以來(lái)我對(duì)于vue-router這方面的了解都不是很深入,僅限于知道如何使用,但是隨著技術(shù)的深入,我發(fā)現(xiàn)這遠(yuǎn)遠(yuǎn)不夠。我們不應(yīng)當(dāng)停留在使用別人的工具的表面,而要去嘗試深究其原理。下面大家就跟著我一起去探索吧,由于技術(shù)深度有限,個(gè)人見解可能會(huì)不是很到位,望指出!謝謝大家!

二、相關(guān)知識(shí)背景

我們?cè)谔剿鱲ue-router之前,我覺得很有必要去了解一下vue的響應(yīng)式原理以及其他的相關(guān)概念。

1.深入響應(yīng)式原理

當(dāng)你把一個(gè)普通的 JavaScript 對(duì)象傳入 Vue 實(shí)例作為 data 選項(xiàng),Vue 將遍歷此對(duì)象所有的 property,并使用 Object.defineProperty 把這些 property 全部轉(zhuǎn)為 getter/setter。Object.defineProperty 是 ES5 中一個(gè)無(wú)法 shim 的特性,這也就是 Vue 不支持 IE8 以及更低版本瀏覽器的原因。
這些 getter/setter 對(duì)用戶來(lái)說是不可見的,但是在內(nèi)部它們讓 Vue 能夠追蹤依賴,在 property 被訪問和修改時(shí)通知變更。這里需要注意的是不同瀏覽器在控制臺(tái)打印數(shù)據(jù)對(duì)象時(shí)對(duì) getter/setter 的格式化并不同,所以建議安裝 vue-devtools 來(lái)獲取對(duì)檢查數(shù)據(jù)更加友好的用戶界面。
每個(gè)組件實(shí)例都對(duì)應(yīng)一個(gè) watcher 實(shí)例,它會(huì)在組件渲染的過程中把“接觸”過的數(shù)據(jù) property 記錄為依賴。之后當(dāng)依賴項(xiàng)的 setter 觸發(fā)時(shí),會(huì)通知 watcher,從而使它關(guān)聯(lián)的組件重新渲染。
——來(lái)源于 vue官網(wǎng)

響應(yīng)式原理1

Vue支持我們通過data參數(shù)傳遞一個(gè)JavaScript對(duì)象做為組件數(shù)據(jù),然后Vue將遍歷此對(duì)象屬性,使用Object.defineProperty方法設(shè)置描述對(duì)象,通過存取器函數(shù)可以追蹤該屬性的變更,Vue創(chuàng)建了一層Watcher層,在組件渲染的過程中把屬性記錄為依賴,之后當(dāng)依賴項(xiàng)的setter被調(diào)用時(shí),會(huì)通知Watcher重新計(jì)算,從而使它關(guān)聯(lián)的組件得以更新,如下圖:

響應(yīng)式原理2

作者:kangaroo_v
鏈接:http://www.itdecent.cn/p/7508d2a114d3

2.Vue渲染流程
渲染流程

從上圖中,不難發(fā)現(xiàn)一個(gè)Vue的應(yīng)用程序是如何運(yùn)行起來(lái)的,模板通過編譯生成AST,再由AST生成Vue的render函數(shù)(渲染函數(shù)),渲染函數(shù)結(jié)合數(shù)據(jù)生成Virtual DOM樹,Diff和Patch后生成新的UI。從這張圖中,可以接觸到Vue的一些主要概念:

  • 模板:Vue的模板基于純HTML,基于Vue的模板語(yǔ)法,我們可以比較方便地聲明數(shù)據(jù)和UI的關(guān)系。
  • AST:AST是Abstract Syntax Tree的簡(jiǎn)稱,Vue使用HTML的Parser將HTML模板解析為AST,并且對(duì)AST進(jìn)行一些優(yōu)化的標(biāo)記處理,提取最大的靜態(tài)樹,方便Virtual DOM時(shí)直接跳過Diff。
  • 渲染函數(shù):渲染函數(shù)是用來(lái)生成Virtual DOM的。Vue推薦使用模板來(lái)構(gòu)建我們的應(yīng)用界面,在底層實(shí)現(xiàn)中Vue會(huì)將模板編譯成渲染函數(shù),當(dāng)然我們也可以不寫模板,直接寫渲染函數(shù),以獲得更好的控制 (這部分是我們今天主要要了解和學(xué)習(xí)的部分)。
  • Virtual DOM:虛擬DOM樹,Vue的Virtual DOM Patching算法是基于Snabbdom的實(shí)現(xiàn),并在些基礎(chǔ)上作了很多的調(diào)整和改進(jìn)。
  • Watcher:每個(gè)Vue組件都有一個(gè)對(duì)應(yīng)的watcher,這個(gè)watcher將會(huì)在組件render的時(shí)候收集組件所依賴的數(shù)據(jù),并在依賴有更新的時(shí)候,觸發(fā)組件重新渲染。你根本不需要寫shouldComponentUpdate,Vue會(huì)自動(dòng)優(yōu)化并更新要更新的UI。

上圖中,render函數(shù)可以作為一道分割線,render函數(shù)的左邊可以稱之為編譯期,將Vue的模板轉(zhuǎn)換為渲染函數(shù)。render函數(shù)的右邊是Vue的運(yùn)行時(shí),主要是基于渲染函數(shù)生成Virtual DOM樹,Diff和Patch。

相關(guān)render函數(shù)的知識(shí)也可以去看下面這位同學(xué)的博客,講得很詳細(xì)。

作者:kangaroo_v
鏈接:http://www.itdecent.cn/p/7508d2a114d3

3.路由hash和histroy

使用 URL 的 hash 來(lái)模擬一個(gè)完整的 URL,于是當(dāng) URL 改變時(shí),頁(yè)面不會(huì)重新加載。 hash(#)是URL 的錨點(diǎn),代表的是網(wǎng)頁(yè)中的一個(gè)位置,單單改變#后的部分,瀏覽器只會(huì)滾動(dòng)到相應(yīng)位置(綁定了相應(yīng)的id的dom位置),不會(huì)重新加載網(wǎng)頁(yè),也就是說hash 出現(xiàn)在 URL 中,但不會(huì)被包含在 http 請(qǐng)求中,對(duì)后端完全沒有影響,因此改變 hash 不會(huì)重新加載頁(yè)面;同時(shí)每一次改變#后的部分,都會(huì)在瀏覽器的訪問歷史中增加一個(gè)記錄,使用”后退”按鈕,就可以回到上一個(gè)位置;所以說Hash模式通過錨點(diǎn)值的改變,根據(jù)不同的值,渲染指定DOM位置的不同數(shù)據(jù)。
與hash相關(guān)的屬性和方法是,location.hash,與hashchange事件,這是實(shí)現(xiàn)路由跳轉(zhuǎn)的關(guān)鍵。

history模式充分利用了html5 history interface 中新增的 pushState() 和 replaceState() 方法。這兩個(gè)方法應(yīng)用于瀏覽器記錄棧,在當(dāng)前已有的 back、forward、go 基礎(chǔ)之上,它們提供了對(duì)歷史記錄修改的功能。只是當(dāng)它們執(zhí)行修改時(shí),雖然改變了當(dāng)前的 URL ,但瀏覽器不會(huì)立即向后端發(fā)送請(qǐng)求。不過這種模式要玩好,還需要后臺(tái)配置支持。因?yàn)槲覀兊膽?yīng)用是個(gè)單頁(yè)客戶端應(yīng)用,如果后臺(tái)沒有正確的配置,當(dāng)用戶在瀏覽器直接訪問 outsite.com/user/id 就會(huì)返回 404,這就不好看了。所以呢,你要在服務(wù)端增加一個(gè)覆蓋所有情況的候選資源:如果 URL 匹配不到任何靜態(tài)資源,則應(yīng)該返回同一個(gè) index.html 頁(yè)面,這個(gè)頁(yè)面就是你 app 依賴的頁(yè)面。

作者:前端開膛手
鏈接:https://juejin.im/post/5caf0cddf265da03474def8a

三、手工實(shí)現(xiàn)

有了以上知識(shí)的了解我們就可以來(lái)敲代碼了!
效果圖如下:


效果圖.gif
HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>vue-router 實(shí)現(xiàn)原理模擬</title>
    <style type="text/css">
        .nav {
            text-align: center;
            margin-top: 20px;
            height: 60px;
            line-height: 60px;
        }
        a {
            margin: 0 16px;
            color: #aaa;
        }

        a:hover {
            font-size: 120%;
            color: #00e;
        }
    </style>
</head>
<body>
    <div class="nav">
        <a href="#/index">index</a>
        <a href="#/home">home</a>
        <a href="#/otherPage">otherPage</a>
        <div id="app"></div>
    </div>

    <script type="text/javascript" src="./index.js"></script>
</body>
</html>
JS
class Router {
    constructor(config) {
        this.routes = config ? config.routes : []; // 獲取路由注冊(cè)信息
        this.mode = config ? config.mode : 'hash'; // 獲取路由模式 hash/history
        this.currentUrl = ''; // 當(dāng)前的路徑
        this.refresh = this.refresh.bind(this); // 為了事件監(jiān)聽不丟失this 
        window.addEventListener('load', this.refresh, false); // 頁(yè)面初始化
        window.addEventListener('hashchange', this.refresh, false); // 監(jiān)聽路由變化
    }

    refresh() {
        this.currentUrl = location.hash.slice(1) || '/'; // 獲取瀏覽器當(dāng)前路徑
        const page = this.routes.find((route) => this.currentUrl.match(new RegExp('^' + route.path + '$'))) // 相應(yīng)的組件進(jìn)行掛載
        page && page.component();
    }

    push(url) {
        location.hash = url;
    }

}

(function init() {
    location.hash = '/index';
}())

const Index = '<p>index page</p>';
const Home = '<p>home page</p>';
const Other = '<p>other page</p>';
const Error = '<p>404 not found</p>'
const APP = document.getElementById("app");

function deleteChild() {
    for(let child = APP.firstElementChild; child; child = APP.firstElementChild) {
        child.remove();
    }
}

const routes = [
    {
        path: '/index',
        name: 'Index',
        component: () => {
            deleteChild();
            const div = document.createElement('div');
            div.innerHTML = Index;
            APP.appendChild(div);
        }
    },
    {
        path: '/home',
        name: 'Home',
        component: () => {
            deleteChild();
            const div = document.createElement('div');
            div.innerHTML = Home;
            APP.appendChild(div);
        }
    },
    {
        path: '/otherPage',
        name: 'OtherPage',
        component: () => {
            deleteChild();
            const div = document.createElement('div');
            div.innerHTML = Other;
            APP.appendChild(div);
        }
    },
    {
        path: '.*',
        name: 'default',
        component: () => {
            deleteChild();
            const div = document.createElement('div');
            div.innerHTML = Error;
            APP.appendChild(div);
        }
    }
]

var route = new Router({ routes });
// route.push('/otherPage');

四、驗(yàn)證

為了證明vue-router的路由懶加載是通過匹配路由hash變化然后去執(zhí)行component函數(shù),我改寫了在配置vue-router中的路由的方式


如果不返回對(duì)應(yīng)的組件

這個(gè)時(shí)候我們通過在vue項(xiàng)目中的瀏覽器路由中輸入相應(yīng)的'/404'路徑是無(wú)法獲取到頁(yè)面的,而且路徑信息也不會(huì)變化,一片空白,但是我們能看到控制臺(tái)打印了如下內(nèi)容:


現(xiàn)在的結(jié)果

現(xiàn)在證實(shí)了vue-router在匹配到相應(yīng)的路徑后會(huì)去調(diào)用component這個(gè)函數(shù),而路徑信息沒有變化可能是因?yàn)闆]有相應(yīng)的組件返回,如果有相應(yīng)的組件返回,那么應(yīng)該會(huì)有一個(gè)回調(diào)函數(shù)來(lái)改變當(dāng)前匹配后的路徑。我在vue-router源碼里找了很久也沒找到這個(gè)component的調(diào)用地方,下次再找吧。這個(gè)component必須為一個(gè)組件對(duì)象或者返回一個(gè)組件對(duì)象的函數(shù),他在加載相應(yīng)的頁(yè)面時(shí)候會(huì)調(diào)用這個(gè)函數(shù),大致是這么個(gè)過程。
路由匹配調(diào)用component

五、總結(jié)

history模式我還沒去模擬實(shí)現(xiàn),用空試試,這個(gè)hash模式也只是簡(jiǎn)單地弄了一下,梳理了一下,url和頁(yè)面之間的映射關(guān)系,僅僅是我的理解,可能原理大相徑庭,如有錯(cuò)誤,我再去查資料。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容