Vue-Router 原理

一、背景

当我们在使用vue-router的时候是否会产生疑惑,为什么这个东西能帮助我们建立起url与页面组件之间的映射关系?vue-router的hash模式和history模式有什么区别?以及他们是怎样实现的?一直以来我对于vue-router这方面的了解都不是很深入,仅限于知道如何使用,但是随着技术的深入,我发现这远远不够。我们不应当停留在使用别人的工具的表面,而要去尝试深究其原理。下面大家就跟着我一起去探索吧,由于技术深度有限,个人见解可能会不是很到位,望指出!谢谢大家!

二、相关知识背景

我们在探索vue-router之前,我觉得很有必要去了解一下vue的响应式原理以及其他的相关概念。

1.深入响应式原理

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setterObject.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。这里需要注意的是不同浏览器在控制台打印数据对象时对 getter/setter 的格式化并不同,所以建议安装 vue-devtools 来获取对检查数据更加友好的用户界面。
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
——来源于 vue官网

响应式原理1

Vue支持我们通过data参数传递一个JavaScript对象做为组件数据,然后Vue将遍历此对象属性,使用Object.defineProperty方法设置描述对象,通过存取器函数可以追踪该属性的变更,Vue创建了一层Watcher层,在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知Watcher重新计算,从而使它关联的组件得以更新,如下图:

响应式原理2

作者:kangaroo_v
链接:https://www.jianshu.com/p/7508d2a114d3

2.Vue渲染流程
渲染流程

从上图中,不难发现一个Vue的应用程序是如何运行起来的,模板通过编译生成AST,再由AST生成Vue的render函数(渲染函数),渲染函数结合数据生成Virtual DOM树,Diff和Patch后生成新的UI。从这张图中,可以接触到Vue的一些主要概念:

  • 模板:Vue的模板基于纯HTML,基于Vue的模板语法,我们可以比较方便地声明数据和UI的关系。
  • AST:AST是Abstract Syntax Tree的简称,Vue使用HTML的Parser将HTML模板解析为AST,并且对AST进行一些优化的标记处理,提取最大的静态树,方便Virtual DOM时直接跳过Diff。
  • 渲染函数:渲染函数是用来生成Virtual DOM的。Vue推荐使用模板来构建我们的应用界面,在底层实现中Vue会将模板编译成渲染函数,当然我们也可以不写模板,直接写渲染函数,以获得更好的控制 (这部分是我们今天主要要了解和学习的部分)。
  • Virtual DOM:虚拟DOM树,Vue的Virtual DOM Patching算法是基于Snabbdom的实现,并在些基础上作了很多的调整和改进。
  • Watcher:每个Vue组件都有一个对应的watcher,这个watcher将会在组件render的时候收集组件所依赖的数据,并在依赖有更新的时候,触发组件重新渲染。你根本不需要写shouldComponentUpdate,Vue会自动优化并更新要更新的UI。

上图中,render函数可以作为一道分割线,render函数的左边可以称之为编译期,将Vue的模板转换为渲染函数render函数的右边是Vue的运行时,主要是基于渲染函数生成Virtual DOM树,Diff和Patch。

相关render函数的知识也可以去看下面这位同学的博客,讲得很详细。

作者:kangaroo_v
链接:https://www.jianshu.com/p/7508d2a114d3

3.路由hash和histroy

使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。 hash(#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会滚动到相应位置(绑定了相应的id的dom位置),不会重新加载网页,也就是说hash 出现在 URL 中,但不会被包含在 http 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面;同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置;所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。
与hash相关的属性和方法是,location.hash,与hashchange事件,这是实现路由跳转的关键。

history模式充分利用了html5 history interface 中新增的 pushState() 和 replaceState() 方法。这两个方法应用于浏览器记录栈,在当前已有的 back、forward、go 基础之上,它们提供了对历史记录修改的功能。只是当它们执行修改时,虽然改变了当前的 URL ,但浏览器不会立即向后端发送请求。不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 outsite.com/user/id 就会返回 404,这就不好看了。所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。

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

三、手工实现

有了以上知识的了解我们就可以来敲代码了!
效果图如下:


效果图.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 实现原理模拟</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 : []; // 获取路由注册信息
        this.mode = config ? config.mode : 'hash'; // 获取路由模式 hash/history
        this.currentUrl = ''; // 当前的路径
        this.refresh = this.refresh.bind(this); // 为了事件监听不丢失this 
        window.addEventListener('load', this.refresh, false); // 页面初始化
        window.addEventListener('hashchange', this.refresh, false); // 监听路由变化
    }

    refresh() {
        this.currentUrl = location.hash.slice(1) || '/'; // 获取浏览器当前路径
        const page = this.routes.find((route) => this.currentUrl.match(new RegExp('^' + route.path + '$'))) // 相应的组件进行挂载
        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');

四、验证

为了证明vue-router的路由懒加载是通过匹配路由hash变化然后去执行component函数,我改写了在配置vue-router中的路由的方式


如果不返回对应的组件

这个时候我们通过在vue项目中的浏览器路由中输入相应的'/404'路径是无法获取到页面的,而且路径信息也不会变化,一片空白,但是我们能看到控制台打印了如下内容:


现在的结果

现在证实了vue-router在匹配到相应的路径后会去调用component这个函数,而路径信息没有变化可能是因为没有相应的组件返回,如果有相应的组件返回,那么应该会有一个回调函数来改变当前匹配后的路径。我在vue-router源码里找了很久也没找到这个component的调用地方,下次再找吧。这个component必须为一个组件对象或者返回一个组件对象的函数,他在加载相应的页面时候会调用这个函数,大致是这么个过程。
路由匹配调用component

五、总结

history模式我还没去模拟实现,用空试试,这个hash模式也只是简单地弄了一下,梳理了一下,url和页面之间的映射关系,仅仅是我的理解,可能原理大相径庭,如有错误,我再去查资料。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351