前端全栈框架-vue

Vue-Router 原理

2020-06-10  本文已影响0人  做一只旅行青蛙

一、背景

当我们在使用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的一些主要概念:

上图中,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和页面之间的映射关系,仅仅是我的理解,可能原理大相径庭,如有错误,我再去查资料。

上一篇 下一篇

猜你喜欢

热点阅读