阅读 462

vue Router源码简单分析

前言

在学习VueRouter源码之前我们先来了解一下path-to-regexp 在VueRouter中会有/foo/:id 这样的动态路由,这个库就是解析路由生成与之对应的正则表达式
安装

npm install path-to-regexp --save
复制代码

pathToRegexp

pathToRegexp方法接收三个参数:参数1:目标地址,参数2:用路径中找到的键填充的数组 ,参数3正则配置项 配置项为:

  • sensitive 大小写敏感 (default: false)
  • strict 末尾斜杠是否精确匹配 (default: false)
  • end 全局匹配 (default: true)
  • start 从开始位置展开匹配 (default: true)
  • delimiter 指定其他分隔符 (default: '/')
  • endsWith 指定标准的结束字符
  • whitelist 指定分隔符列表 (default: undefined, any character)
const { pathToRegexp } = require("path-to-regexp");

let keys = []
let re = pathToRegexp('/foo/:bar', keys)
console.log(re);
console.log(keys);
///^\/foo\/([^\/]+?)(?:\/)?$/i
[ { name: 'bar',
    prefix: '/',
    delimiter: '/',
    optional: false,
    repeat: false,
    partial: false,
    pattern: '[^\\/]+?' } 
]

复制代码

exec

作用:是匹配url地址是否和正则规则相符,如果相符返回一个数组,如果不相符返回null

var pathToRegexp = require('path-to-regexp')

var re = pathToRegexp('/foo/:bar');     // 匹配规则
var match1 = re.exec('/test/route');    // url 路径
var match2 = re.exec('/foo/route');     // url 路径

// match1 结果为 null
// match2 结果为 [ '/foo/route', 'route', index: 0, input: '/foo/route' ]

复制代码

match

作用:返回一个将路径转换为参数的函数

const { match } = require("path-to-regexp");
var match1 = match("/user/:id", { decode: decodeURIComponent });
match1("/user/123"); 
//{ path: '/user/123', index: 0, params: { id: '123' } }

复制代码

parse

作用:解析 url 字符串中的参数部分


const { parse } = require("path-to-regexp");
console.log(parse("/foo/:bar"))
// 返回一个数组,数组第二项就可以得到 url 地址携带参数的属性名称(name:bar) 
[
  '/foo',
  {
    name: 'bar',   // 令牌的名称(string表示命名,或number表示未命名索引)
    prefix: '/', //prefix段的前缀字符串(例如"/")
    suffix: '',  // suffix段的后缀字符串(例如"")
    pattern: '[^\\/#\\?]+?', //pattern用于匹配此令牌的RegExp(string)
    modifier: '' //modifier用于段的修饰字符(例如?)
  }
]
复制代码

compile

作用:快速填充url 字符串的参数值

const { compile } = require("path-to-regexp");

const url = '/foo/:bar'

const params = {bar: 123}

compile(url)(params)
//返回/foo/123

复制代码

整体分析

mode区分

VueRouter支持三种模式:hash、history、abstract下面我们来说一下三种模式的区别 hash 使用了hash模式我们在浏览器上的url上看到的是这样的

http://localhost:8080/#/xxx     
复制代码

hash模式的特点:

  1. 浏览器上会出现#符号,浏览器只会把#之前的链接发送给服务端,服务端不需要做处理。
  2. 我们可以使用hashchange事件监听到#后边的改变,这样当他改变之后我们可以调用callback实现我们想做的事情。
  3. 兼容性好。

history 使用了history模式我们在浏览器上的url上看到的是这样的

http://localhost:8080/xxx   
复制代码

history模式的特点:

  1. 浏览器上的url会整个发送给服务端,需要服务端处理url要不然随随便便一个404。
  2. 可以使用popstate事件监听到地址的改变
  3. 对部分浏览器不兼容

abstract abstract是为非浏览器下运行的js,准备的一种路由方式。

代码结构

我们来看一下目录结构主要关注一下src路径下

│  create-matcher.js        // 生成匹配表
│  create-route-map.js      // 生成匹配表
│  index.js                 // 入口文件
│  install.js               // vue安装的文件
│  
├─components            // 提供了俩个组件router-link、router-view
│      link.js
│      view.js
│      
├─history    // 路由模式
│      abstract.js
│      base.js
│      hash.js
│      html5.js
│      
└─util                //功能函数、功能类
        async.js
        dom.js
        errors.js
        location.js
        misc.js
        params.js
        path.js
        push-state.js
        query.js
        resolve-components.js
        route.js
        scroll.js
        state-key.js
        warn.js
复制代码

vuerouter的使用

在阅读代码之前我们来回顾一下在vue中怎么使用vuerouter

// 首先引入vuerouter 然后使用use方法安装
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

// 定义路由
const routes = [
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar }
]

//创建router实例
const router = new VueRouter({
  routes 
})
挂载实例
const app = new Vue({
  router
}).$mount('#app')
复制代码

代码分析

首先我们要使用VueRouter就要先安装一下 Vue.use(MyPlugin),这个方法先会在_installedPlugins中查找,没有没有被安装过才会继续执行,确保插件才会被安装一次。处理传来的参数保证第一个参数是Vue示例,如果参数是function直接执行,如果是对象取其中的install方法执行。

src\install.js

// 给vue提供install方法安装
// 通过installed记录是否被安装过如果安装过不再次安装
// 通过混入的方式、在相应的生命周期上挂载路由方法
// 注册RouterView、RouterLink组件
// 注册路由的生命周期函数   
export function install (Vue) {
  if (install.installed && _Vue === Vue) return
  install.installed = true

  _Vue = Vue

  const isDef = v => v !== undefined

  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode // 至少存在一个 VueComponent 时, _parentVnode 属性才存在
    // 如果存在VNode vNode也初始化了attrs: {},hook,on监听事件
    // registerRouteInstance在routerview组件渲染的时候会生成
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }

  Vue.mixin({
    beforeCreate () {
    // 如果选项中传了router 根组件会进入
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this) // 执行router中的init方法
        Vue.util.defineReactive(this, '_route', this._router.history.current) //vue提供的global。没有在官网上说明,详情可以看vue源码src\core\global-api\index.js可以找到声明位置, 这个就是往vue实例中代理个_route属性,属性值是当前路由Dep管理依赖,触发get、set触发依赖
      } else {
      // 组件实例才会进入,通过$parent一级级获取_routerRoot
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this) //注册实例
    },
    destroyed () {
      registerInstance(this) // 销毁实例
    }
  })
    // 在vue原型上挂载$router  这样可以通过this.$router访问了
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })
// 在vue原型上挂载$router 
  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })
// 注册RouterView、RouterLink俩个组件
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  const strats = Vue.config.optionMergeStrategies
  // 路由钩子合并策略
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

复制代码

我们把router传入vue之前需要new VueRouter先初始化一下生成一个实例,然后把实例传入给vue

src\index.js


// 这个文件是VueRouter的入口文件

// 先看点不重要的代码,提给了install方法可以在用script标签的方式引用、错误日志
VueRouter.install = install
VueRouter.version = '__VERSION__'
VueRouter.isNavigationFailure = isNavigationFailure
VueRouter.NavigationFailureType = NavigationFailureType
VueRouter.START_LOCATION = START

if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}

// 下面我们看一下VueRouter 这个类主要做了点什么
// 初始化配置项最后生成当前环境下可用的路由模式,根据模式进一步生成了路由实例
// 创建路由匹配器
// 提供了init方法在,vue.use的使用混入
// 提供了一些方法:init match onReady onError push replace go back forward getMatchedComponents  resolve addRoutes addRoute
// 提供了一些钩子函数: beforeResolve 、afterEach
export default class VueRouter {
  static install: () => void
  static version: string
  static isNavigationFailure: Function
  static NavigationFailureType: any
  static START_LOCATION: Route

  app: any
  apps: Array<any>
  ready: boolean
  readyCbs: Array<Function>
  options: RouterOptions
  mode: string
  history: HashHistory | HTML5History | AbstractHistory
  matcher: Matcher
  fallback: boolean
  beforeHooks: Array<?NavigationGuard>
  resolveHooks: Array<?NavigationGuard>
  afterHooks: Array<?AfterNavigationHook>
// 以上代码可以先忽略,只是做一点了类型限定,不影响我们去看他的原理

  constructor (options: RouterOptions = {}) { // 接收使用的传来的options
  // 初始化 保存传来的options
    this.app = null
    this.apps = []
    this.options = options
    this.beforeHooks = [] 
    this.resolveHooks = []
    this.afterHooks = []
    // 创建matcher路由匹配器
    this.matcher = createMatcher(options.routes || [], this) 

    let mode = options.mode || 'hash' // 设置mode 如果什么都不传默认是hash模式
    this.fallback =
      mode === 'history' && !supportsPushState && options.fallback !== false // 如果fallback没有传或者传的值为 true 这里会做一个兼容
      //判断你的模式是否是history,如果符合就判断你当前的浏览器是否支持window.history、是否能调用window.history.pushState。
      // 如果不能会自动兼容到hash模式
    if (this.fallback) {
      mode = 'hash'
    }
    // 如果不能获取到window当前是服务器渲染默认是mode转换为abstract
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode // 保存处理好的mode
    // 根据不同的mode去实例化对应的history类
    switch (mode) {
      case 'history':  //history 方式
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':     //hash 方式
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':    // abstract 方式
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }
  }
 // match方法实际上调用的是匹配器上的方法
  match (raw: RawLocation, current?: Route, redirectedFrom?: Location): Route {
    return this.matcher.match(raw, current, redirectedFrom)
  }
    //getter 得到当前路由  
  get currentRoute (): ?Route {
    return this.history && this.history.current
  }
    //初始化方法在beforeCreate声明周期调用
  init (app: any /* Vue component instance */) {
    process.env.NODE_ENV !== 'production' &&
      assert(
        install.installed,
        `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
          `before creating root instance.`
      )

    this.apps.push(app) // 保存当前组件实例

   // 组件在destroyed生命周期执行的回调,当前组件销毁时,也从app中删除把app重置为null,执行router中的销毁函数
   // $once('hook:destroyed') 这种方式在官网也没有明确写出,可以看vue源码src\core\instance\events.js
    app.$once('hook:destroyed', () => {
        // 如果app入栈过,在销毁之后从apps中删除,然后重置app,确保app是一个实例或者null 不是undefined
      const index = this.apps.indexOf(app)
      if (index > -1) this.apps.splice(index, 1)
      if (this.app === app) this.app = this.apps[0] || null
        // 执行销毁函数
      if (!this.app) this.history.teardown()
    })

     // this.app 有指向,此实例不是根vue实例,返回
    if (this.app) {
      return
    }
    // 此实例是根vue实例
    // 新增一个history,并添加route监听器
    this.app = app

    const history = this.history
    // 如果当前时history模式或者hash模式
    if (history instanceof HTML5History || history instanceof HashHistory) {
      const handleInitialScroll = routeOrError => {
        const from = history.current // 记录当前路径
        const expectScroll = this.options.scrollBehavior // 传递给router中的scrollBehavior
        const supportsScroll = supportsPushState && expectScroll // 检测是否浏览器支持history模式和传入了scrollBehavior事件

        if (supportsScroll && 'fullPath' in routeOrError) {
          handleScroll(this, routeOrError, from, false)
        }
      }
      const setupListeners = routeOrError => {
        history.setupListeners()
        handleInitialScroll(routeOrError)
      }
      history.transitionTo(   // 调用 history 实例的 transitionTo 方法
        history.getCurrentLocation(), // 浏览器 window 地址的 hash 值
        setupListeners, // 成功的回调
        setupListeners  // 失败的回调
      )
    }

    
    // 路由全局监听,维护当前的route
    // 因为_route在install执行时定义为响应式属性,
    // 当route变更时_route更新,后面的视图更新渲染就是依赖于_route
    history.listen(route => {
      this.apps.forEach(app => {
        app._route = route
      })
    })
  }
    //beforeEach钩子
  beforeEach (fn: Function): Function {
    return registerHook(this.beforeHooks, fn)
  }
    //beforeResolve钩子
  beforeResolve (fn: Function): Function {
    return registerHook(this.resolveHooks, fn)
  }
    // afterEach钩子
  afterEach (fn: Function): Function {
    return registerHook(this.afterHooks, fn)
  }
    // 以下api大多都是调用了history实例上的方法或者Matcher上的方法我们先来看一下他们都分别做了什么,然后再来看下下面的api
  onReady (cb: Function, errorCb?: Function) {
    this.history.onReady(cb, errorCb)
  }

  onError (errorCb: Function) {
    this.history.onError(errorCb)
  }

  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    // $flow-disable-line
    if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        this.history.push(location, resolve, reject)
      })
    } else {
      this.history.push(location, onComplete, onAbort)
    }
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    // $flow-disable-line
    if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        this.history.replace(location, resolve, reject)
      })
    } else {
      this.history.replace(location, onComplete, onAbort)
    }
  }

  go (n: number) {
    this.history.go(n)
  }

  back () {
    this.go(-1)
  }

  forward () {
    this.go(1)
  }

  getMatchedComponents (to?: RawLocation | Route): Array<any> {
    const route: any = to
      ? to.matched
        ? to
        : this.resolve(to).route
      : this.currentRoute
    if (!route) {
      return []
    }
    return [].concat.apply(
      [],
      route.matched.map(m => {
        return Object.keys(m.components).map(key => {
          return m.components[key]
        })
      })
    )
  }

  resolve (
    to: RawLocation,
    current?: Route,
    append?: boolean
  ): {
    location: Location,
    route: Route,
    href: string,
    // for backwards compat
    normalizedTo: Location,
    resolved: Route
  } {
    current = current || this.history.current
    const location = normalizeLocation(to, current, append, this)
    const route = this.match(location, current)
    const fullPath = route.redirectedFrom || route.fullPath
    const base = this.history.base
    const href = createHref(base, fullPath, this.mode)
    return {
      location,
      route,
      href,
      // for backwards compat
      normalizedTo: location,
      resolved: route
    }
  }

  getRoutes () {
    return this.matcher.getRoutes()
  }

  addRoute (parentOrRoute: string | RouteConfig, route?: RouteConfig) {
    this.matcher.addRoute(parentOrRoute, route)
    if (this.history.current !== START) {
      this.history.transitionTo(this.history.getCurrentLocation())
    }
  }

  addRoutes (routes: Array<RouteConfig>) {
    if (process.env.NODE_ENV !== 'production') {
      warn(false, 'router.addRoutes() is deprecated and has been removed in Vue Router 4. Use router.addRoute() instead.')
    }
    this.matcher.addRoutes(routes)
    if (this.history.current !== START) {
      this.history.transitionTo(this.history.getCurrentLocation())
    }
  }
}

复制代码

src\create-matcher.js

// 创建路由匹配器
// 收集pathList路由集合、pathMap路由path映射、nameMap路由name映射
// pathList: ['/foo','/bar']  
//pathMap:{'/foo': {"path":"/foo","regex":{"keys":[]},"components":{"default":{"template":"<div>这是foo</div>"}},"alias":[],"instances":{},"enteredCbs":{},"meta":{},"props":{}}}
//nameMap:{'foo':{"path":"/foo","regex":{"keys":[]},"components":{"default":{"template":"<div>这是foo</div>"}},"alias":[],"instances":{},"enteredCbs":{},"meta":{},"props":{}}}
// 返回了addRoutes、addRoute、getRoutes、match方法
// 通过name跳转路由 > 通过path跳转路由
export function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {
  const { pathList, pathMap, nameMap } = createRouteMap(routes)
// 批量添加路由 向原有映射关系中添加新的映射关系
  function addRoutes (routes) {
    createRouteMap(routes, pathList, pathMap, nameMap)
  }
// 添加单个路由
  function addRoute (parentOrRoute, route) {
  // 如果parentOrRoute是routerOPtions 就正常添加,如果是string则从nameMap 映射中查找,
    const parent = (typeof parentOrRoute !== 'object') ? nameMap[parentOrRoute] : undefined
    // $flow-disable-line
    createRouteMap([route || parentOrRoute], pathList, pathMap, nameMap, parent)

    // 如果可以从nameMap映射表中找到,取出name对应的路由,如果有alias选项,重新生成一个alias对应的路由映射
    if (parent && parent.alias.length) {
      createRouteMap(
        // $flow-disable-line route is defined if parent is
        parent.alias.map(alias => ({ path: alias, children: [route] })),
        pathList,
        pathMap,
        nameMap,
        parent
      )
    }
  }
    // 遍历pathList返回pathMap映射出来的新数组
  function getRoutes () {
    return pathList.map(path => pathMap[path])
  }
    // 匹配路由
  function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
  // 拿出路由中的path、params、query、name等参数
    // location 是一个对象类似于
    // {"_normalized":true,"path":"/","query":{},"hash":""}
    const location = normalizeLocation(raw, currentRoute, false, router)
    const { name } = location
    // 如果有name就从nameMap映射表取出来
    if (name) {
      const record = nameMap[name]
      if (process.env.NODE_ENV !== 'production') {
        warn(record, `Route with name '${name}' does not exist`)
      }
       // 如果没有这条路由映射就创建一条路由对象
      if (!record) return _createRoute(null, location)
      // 过滤掉可选参数,取出没有name生成一个参数名数组
      const paramNames = record.regex.keys
        .filter(key => !key.optional)
        .map(key => key.name)
        
     // 经过处理过的location 如果params参数不是object类型 说明没有给他重新初始化一下
      if (typeof location.params !== 'object') {
        location.params = {}
      }
    // 如果当前路由有params参数
    // 遍历当前路由参数给,给新生成的路由参数挂在location.params上
      if (currentRoute && typeof currentRoute.params === 'object') {
        for (const key in currentRoute.params) {
          if (!(key in location.params) && paramNames.indexOf(key) > -1) {
            location.params[key] = currentRoute.params[key]
          }
        }
      }
        // 最终返回一个用于将参数转换为有效路径
      location.path = fillParams(record.path, location.params, `named route "${name}"`)
      return _createRoute(record, location, redirectedFrom)
    } else if (location.path) {
    // 初始化参数
      location.params = {}
      for (let i = 0; i < pathList.length; i++) {
        const path = pathList[i]
        const record = pathMap[path]
        // 如果路由匹配到了 创建新的路由映射
        if (matchRoute(record.regex, location.path, location.params)) {
          return _createRoute(record, location, redirectedFrom)
        }
      }
    }
    // 什么没有匹配到
    return _createRoute(null, location)
  }

  function redirect (
    record: RouteRecord,
    location: Location
  ): Route {
  // 去出路由中的重定向配置是不是一个function,如果是function 将生成冻结的route作为function的参数
    const originalRedirect = record.redirect
    let redirect = typeof originalRedirect === 'function'
      ? originalRedirect(createRoute(record, location, null, router))
      : originalRedirect
    // 如果redirect配置的是字符串封装成对象模式
    if (typeof redirect === 'string') {
      redirect = { path: redirect }
    }

    // 处理错误情况
    if (!redirect || typeof redirect !== 'object') {
      if (process.env.NODE_ENV !== 'production') {
        warn(
          false, `invalid redirect option: ${JSON.stringify(redirect)}`
        )
      }
      return _createRoute(null, location)
    }

    const re: Object = redirect
    const { name, path } = re
    let { query, hash, params } = location
    // 处理参数如果redirect上有则用,没有则用处理过的路由信息
    query = re.hasOwnProperty('query') ? re.query : query
    hash = re.hasOwnProperty('hash') ? re.hash : hash
    params = re.hasOwnProperty('params') ? re.params : params

    if (name) { // 匹配name重定向
      const targetRecord = nameMap[name]
      if (process.env.NODE_ENV !== 'production') {
        assert(targetRecord, `redirect failed: named route "${name}" not found.`)
      }
      // 生成匹配好的路由对象
      return match({
        _normalized: true,
        name,
        query,
        hash,
        params
      }, undefined, location)
    } else if (path) { // 匹配path重定向
      // 解析重定向的路由
      const rawPath = resolveRecordPath(path, record)
      // 解析参数 运用了path-to-regexp中的compile方法,将参数填充到url上
      const resolvedPath = fillParams(rawPath, params, `redirect route with path "${rawPath}"`)
      // 生成匹配好的路由对象
      return match({
        _normalized: true,
        path: resolvedPath,
        query,
        hash
      }, undefined, location)
    } else {
    // 处理错误情况
      if (process.env.NODE_ENV !== 'production') {
        warn(false, `invalid redirect option: ${JSON.stringify(redirect)}`)
      }
      return _createRoute(null, location)
    }
  }

  function alias (
    record: RouteRecord,
    location: Location,
    matchAs: string
  ): Route {
  // 得到一个填充好参数的url
    const aliasedPath = fillParams(matchAs, location.params, `aliased route with path "${matchAs}"`)
    // 生成匹配好的路由对象
    const aliasedMatch = match({
      _normalized: true,
      path: aliasedPath
    })
    // 如果匹配成功
    if (aliasedMatch) {
    // 处理参数重新生成一个冻结的路由对象
      const matched = aliasedMatch.matched
      const aliasedRecord = matched[matched.length - 1]
      location.params = aliasedMatch.params
      return _createRoute(aliasedRecord, location)
    }
     // 如果匹配失败
    return _createRoute(null, location)
  }

  function _createRoute (
    record: ?RouteRecord,
    location: Location,
    redirectedFrom?: Location
  ): Route {
   // 如果路由配置了重定向
    if (record && record.redirect) {
      return redirect(record, redirectedFrom || location)
    }
    // 如果路由配置别名
    if (record && record.matchAs) {
      return alias(record, location, record.matchAs)
    }
    // 上述俩种没有配置走普通的返回一个冻结的route对象
    return createRoute(record, location, redirectedFrom, router)
  }

  return {
    match,
    addRoute,
    getRoutes,
    addRoutes
  }
}

复制代码

小结: matcher中调用createRouteMap方法先生成三个集合pathList, pathMap, nameMap。源代码路径src\create-route-map.js大概原理是循环router生成record指定描述路由的一个对象,如果router中有children递归执行,将*通配符路径放在其他路由最后。matcher提供了 match,addRoute,getRoutes,addRoutes,alias,redirect这么几个方法创建路由addRoute,addRoutes都是在pathList, pathMap, nameMap几个集合中添加新的映射。getRoutes是遍历pathList获取path然后返回pathMap中的path对象。alias,rediret俩个方法是触发别名和重定向的时候才会调用的方法。注意俩者的区别一个是改变路由改变视图、一个是改变视图不改变路由。俩种方法的相同点都是调用了match取匹配路由集合,返回匹配好的路由对象。

路由模式

    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }
复制代码

在生成三种模式路由的时候是创建一个三种模式的实例下面我们逐一分析下。在src\history文件夹中有4个js文件对应着三种路由模式的类和一个基类,三种都是继承这个基类。我们先来看一下基类都干了什么

History

src\history\base.js

export class History {
  router: Router
  base: string
  current: Route
  pending: ?Route
  cb: (r: Route) => void
  ready: boolean
  readyCbs: Array<Function>
  readyErrorCbs: Array<Function>
  errorCbs: Array<Function>
  listeners: Array<Function>
  cleanupListeners: Function
  +go: (n: number) => void
  +push: (loc: RawLocation, onComplete?: Function, onAbort?: Function) => void
  +replace: (
    loc: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) => void
  +ensureURL: (push?: boolean) => void
  +getCurrentLocation: () => string
  +setupListeners: Function
// 上面代码可以直接省略,做了类型限定,字类方法如果有对+function这种有疑惑的小伙伴可以看一下flow文档
// 下面进入主要代码
    // 接受俩个参数,一个是router实例,一个是baseurl
    // 初始化各项
  constructor (router: Router, base: ?string) {
    this.router = router
    // 初始化baseurl
    this.base = normalizeBase(base)
    this.current = START // 默认当前路由是一个/初始路由生成的对象
    this.pending = null
    this.ready = false // 标记路由加载完成
    this.readyCbs = [] // 加载完成回调队列
    this.readyErrorCbs = [] // 加载失败回调队列
    this.errorCbs = [] // 错误回调队列
    this.listeners = [] // 监听队列
  }
    // 监听函数保存传来的回调函数
  listen (cb: Function) {
    this.cb = cb
  }
// onReady事件,如果路由加载完成执行当前的回调,如果没有完成把回调放到队列中,如果传了失败回调放入失败回调队列中
  onReady (cb: Function, errorCb: ?Function) {
    if (this.ready) {
      cb()
    } else {
      this.readyCbs.push(cb)
      if (errorCb) {
        this.readyErrorCbs.push(errorCb)
      }
    }
  }
// 注册一个回调,该回调会在路由导航过程中出错时被调用。
  onError (errorCb: Function) {
    this.errorCbs.push(errorCb)
  }

// 这个方法是比较重要的一个方法,在history.push 、history.replace的底层都是调用了它,它就是路由切换的方法。
//transitionTo 可以接收三个参数 location、onComplete、onAbort,
//分别是目标路径、路经切换成功的回调、路径切换失败的回调。
  transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) {
    let route
    try {
     // 调用 match方法得到匹配的 route对象
      route = this.router.match(location, this.current)
    } catch (e) {
    // 如果发生错误执行errorCbs队列中的回调
      this.errorCbs.forEach(cb => {
        cb(e)
      })
      throw e
    }
    const prev = this.current
     // 过渡处理
    this.confirmTransition(
      route,
      () => {
       // 更新当前的current,有回调执行回调cb
        this.updateRoute(route)
        onComplete && onComplete(route)
        // 更新url地址 hash模式更新hash值 history模式通过pushState/replaceState来更新
        this.ensureURL()
        // 执行afterEach钩子回调
        this.router.afterHooks.forEach(hook => {
            // afterEach钩子函数的参数
          hook && hook(route, prev)
        })

        // 重置ready状态,遍历readyCbs队列执行cb
        if (!this.ready) {
          this.ready = true
          this.readyCbs.forEach(cb => {
            cb(route)
          })
        }
      },
      err => {
        if (onAbort) {
          onAbort(err)
        }
        if (err && !this.ready) {
          // 初始重定向不应将历史记录标记为就绪
          // 因为它是由重定向触发的
          if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {
            this.ready = true
            this.readyErrorCbs.forEach(cb => {
              cb(err)
            })
          }
        }
      }
    )
  }

  confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
    const current = this.current
    this.pending = route
      // 中断跳转路由函数
    const abort = err => {
      if (!isNavigationFailure(err) && isError(err)) {
        if (this.errorCbs.length) {
          this.errorCbs.forEach(cb => {
            cb(err)
          })
        } else {
          warn(false, 'uncaught error during route navigation:')
          console.error(err)
        }
      }
      onAbort && onAbort(err)
    }
    
    const lastRouteIndex = route.matched.length - 1
    const lastCurrentIndex = current.matched.length - 1
    // 如果当前路由和之前路由相同 确认url 直接return
    if (
      isSameRoute(route, current) &&
      lastRouteIndex === lastCurrentIndex &&
      route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
    ) {
      this.ensureURL()
      return abort(createNavigationDuplicatedError(current, route))
    }
 // 通过异步队列来交叉对比当前路由的路由记录和现在的这个路由的路由记录 
    // 为了能准确得到父子路由更新的情况下可以确切的知道 哪些组件需要更新 哪些不需要更新
    const { updated, deactivated, activated } = resolveQueue(
      this.current.matched,
      route.matched
    )
      // 导航守卫数组
    // 通过 queue 这个数组保存相应的路由钩子函数
    const queue: Array<?NavigationGuard> = [].concat(
      // 组件中的beforeRouteLeave 的勾子
      extractLeaveGuards(deactivated),
       // 全局的 beforeEach 的勾子
      this.router.beforeHooks,
         // 在当前路由改变,但是该组件被复用时调用
      extractUpdateHooks(updated),
     // 将要更新的路由的 beforeEnter勾子
      activated.map(m => m.beforeEnter),
      // 异步组件
      resolveAsyncComponents(activated)
    )
    // 队列执行的iterator函数 
    const iterator = (hook: NavigationGuard, next) => {
      // 路由不相等就不跳转路由
      if (this.pending !== route) {
        return abort(createNavigationCancelledError(current, route))
      }
      try {
      // 执行钩子
      // 只有执行了钩子函数中的 next,才会继续执行下一个钩子函数
      // 否则会暂停跳转
      // 以下逻辑是在判断 next() 中的传参
        hook(route, current, (to: any) => {
        
          if (to === false) { // 如果next(false)
            this.ensureURL(true)
            abort(createNavigationAbortedError(current, route))
          } else if (isError(to)) { // 如果next(error)
            this.ensureURL(true)
            abort(to)
          } else if ( // 如果 next('/') or next({ path: '/' })
            typeof to === 'string' ||
            (typeof to === 'object' &&
              (typeof to.path === 'string' || typeof to.name === 'string'))
          ) {
            abort(createNavigationRedirectedError(current, route))
            if (typeof to === 'object' && to.replace) { next({ path: '/',repalce:true  })
              this.replace(to)
            } else {
              this.push(to)
            }
          } else {
           // 这里执行 next
           // 也就是执行下面函数 runQueue 中的 step(index + 1)
            next(to)
          }
        })
      } catch (e) {
        abort(e)
      }
    }
     // runQueue 执行队列 以一种递归回调的方式来启动异步函数队列的执行
    runQueue(queue, iterator, () => {
    // 组件内的钩子
      const enterGuards = extractEnterGuards(activated)
      const queue = enterGuards.concat(this.router.resolveHooks)
    // 当所有异步组件加载完成后,会执行这里的回调,也就是 runQueue 中的 cb()
    // 接下来执行 需要渲染组件的导航守卫钩子
      runQueue(queue, iterator, () => {
       // 跳转完成
        if (this.pending !== route) {
          return abort(createNavigationCancelledError(current, route))
        }
        this.pending = null 
        onComplete(route) // 调用transitionTo中的回调,更新current,执行afterHook回调、更改ready状态并循环执行readyCbs
        if (this.router.app) {
          this.router.app.$nextTick(() => {
            handleRouteEntered(route)
          })
        }
      })
    })
  }

  updateRoute (route: Route) {
    this.current = route
    this.cb && this.cb(route)
  }

// 销毁事件:清空listeners初始化current、pending
  teardown () {
    this.listeners.forEach(cleanupListener => {
      cleanupListener()
    })
    this.listeners = []
    this.current = START
    this.pending = null
  }
}

// 接收俩个参数,当前路由的 matched 和目标路由的 matched。遍历俩个数组对比记录,如果发现记录不一样了,终止循环记下当前下标i
// 对于 next 从0到i和current都是一样的,从i口开始不同,next 从i之后为 activated部分,current从i之后为 deactivated部分,相同部分为 updated,由 resolveQueue 处理之后就能得到路由变更需要更改的部分。紧接着就可以根据路由的变更执行一系列的钩子函数。
function resolveQueue (
  current: Array<RouteRecord>,
  next: Array<RouteRecord>
): {
  updated: Array<RouteRecord>,
  activated: Array<RouteRecord>,
  deactivated: Array<RouteRecord>
} {
  let i
  const max = Math.max(current.length, next.length)
  for (i = 0; i < max; i++) {
   // 当前路由路径和跳转路由路径不同时跳出遍历
    if (current[i] !== next[i]) {
      break
    }
  }
  return {
   // 可复用的组件对应路由
    updated: next.slice(0, i),
     // 需要渲染的组件对应路由
    activated: next.slice(i),
        // 失活的组件对应路由
    deactivated: current.slice(i)
  }
}



复制代码

小结:切换路由的时候调用了transitionTo 函数主要做了两件事:首先根据目标路径 location 和当前的路由对象通过 this.router.match方法去匹配到目标 route 对象。route是这个样子的

{
    fullPath: "/detail/394"
    hash: ""
    matched: [{…}]
    meta: {title: "详情"}
    name: "detail"
    params: {id: "394"}
    path: "/detail/394"
    query: {}
}
复制代码

一个包含了目标路由基本信息的对象。然后执行 confirmTransition方法进行真正的路由切换。因为有一些异步组件,所以回有一些异步操作。然后执行了一些钩子函数,钩子函数的执行书顺序是beforeRouteLeave => beforeEach => beforeRouteUpdate => beforeEnter => beforeRouteEnter => beforeResolve => afterEach

HashHistory

这里先拿HashHistory举例子,HTML5History和他的操作非常的相似在特定的方法中HashHistory做了兼容性的处理。

src\history\hash.js

export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    super(router, base)
    // 能降级处理的重写url
    if (fallback && checkFallback(this.base)) {
      return
    }
    // 检查url的hash有没有#, 如果没有重写url给url追加上#
    ensureSlash()
  }

  // 设置监听器
  setupListeners () {
  // 如果已经有监听器了直接返回
    if (this.listeners.length > 0) {
      return
    }

    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll
//如果支持history模式直接将安装滚动方法放在队列中
    if (supportsScroll) {
      this.listeners.push(setupScroll())
    }
    const handleRoutingEvent = () => {
      const current = this.current
      if (!ensureSlash()) {
        return
      }
      this.transitionTo(getHash(), route => {
      // 如果支持滚动将滚动事件传入,当执行完会在nextTick中改变window.scrollTo从而改变页面的内容位置
        if (supportsScroll) {
          handleScroll(this.router, route, current, true)
        }
        // 如果不支持滚动直接重写url
        if (!supportsPushState) {
          replaceHash(route.fullPath)
        }
      })
    }
    // 监听hashchange事件执行this.transitionTo切换页面
    const eventType = supportsPushState ? 'popstate' : 'hashchange'
    window.addEventListener(
      eventType,
      handleRoutingEvent
    )
    将解除hashchange事件绑定的函数放入队列中
    this.listeners.push(() => {
      window.removeEventListener(eventType, handleRoutingEvent)
    })
  }
//添加路由记录并跳转此时VueRouter会调用this.transitionTo去开始执行一些钩子函数,然后开始resolve组件,在完成这些之后会调用pushHash(route.fullPath)这个方法去改变url,其实pushHash最终调用的就是pushState,只不过replace传入的是false。
  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(
      location,
      route => {
        pushHash(route.fullPath)
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      },
      onAbort
    )
  }
// 替换路由,不会往记录中添加
  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(
      location,
      route => {
        replaceHash(route.fullPath)
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      },
      onAbort
    )
  }
// 调用historyapi
  go (n: number) {
    window.history.go(n)
  }

  ensureURL (push?: boolean) {
    const current = this.current.fullPath
    if (getHash() !== current) {
      push ? pushHash(current) : replaceHash(current)
    }
  }
    // 获取当前url
  getCurrentLocation () {
    return getHash()
  }
}


复制代码

小结:push和replace方法底层都是调用了transitionTo进行路由切换,触发路由生命周期钩子。不同点是push触发的是pushState这里做了容错处理,判定是否存在html5 history API,若支持用history.pushState()操作浏览器历史记录,否则用window.location.hash = path替换文档。注意:调用history.pushState()方法不会触发 popstate 事件,popstate只会在浏览器某些行为下触发, 比如点击后退、前进按钮。

router组件

VueRouter提供了两个组件:router-view和router-link

router-view

router-view是一个函数式组件没有自己的实例,只负责调用父组件上存储的 keepAlive $route.match 等相关的属性/方法来控制路由对应的组件的渲染情况 router-view组件可以嵌套来配合实现嵌套路由,其自身所在的页面位置最终是其匹配上的路由组件所挂载的位置。

src\components\view.js

export default {
  name: 'RouterView',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
    // 标记是routerview组件
    data.routerView = true

// 使用父组件的$createElement这样的好处是我们可以知道命名插槽
    const h = parent.$createElement
    const name = props.name
    const route = parent.$route
    const cache = parent._routerViewCache || (parent._routerViewCache = {})

   // depth标记当前组件深度,作为路由嵌套的个标记
   // inactive记录是不是活动状态
    let depth = 0
    let inactive = false
    // 循环递归遍历组件深度,并读取当前组件是否是活动状态
    while (parent && parent._routerRoot !== parent) {
      const vnodeData = parent.$vnode ? parent.$vnode.data : {}
      if (vnodeData.routerView) {
        depth++
      }
      if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    // 记录当前深度
    data.routerViewDepth = depth

    //如果是keepAlive组件,取缓存中的
    if (inactive) {
      const cachedData = cache[name]
      const cachedComponent = cachedData && cachedData.component
      if (cachedComponent) {
        // pass props
        if (cachedData.configProps) {
          fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
        }
        return h(cachedComponent, data, children)
      } else {
       // 渲染空视图
        return h()
      }
    }
    // 找到匹配的深度组件
    const matched = route.matched[depth]
    const component = matched && matched.components[name]

    // 如果没有找到渲染空节点
    if (!matched || !component) {
      cache[name] = null
      return h()
    }

    // 缓存组件
    cache[name] = { component }
    // 安装VueRouter时混入的
    // 俩个生命周期执行的方法一个是注册一个是销毁
    data.registerRouteInstance = (vm, val) => {
      const current = matched.instances[name]
      if (
        (val && current !== vm) ||
        (!val && current === vm)
      ) {
        matched.instances[name] = val
      }
    }

    ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
      matched.instances[name] = vnode.componentInstance
    }

    data.hook.init = (vnode) => {
      if (vnode.data.keepAlive &&
        vnode.componentInstance &&
        vnode.componentInstance !== matched.instances[name]
      ) {
        matched.instances[name] = vnode.componentInstance
      }

      handleRouteEntered(route)
    }

    const configProps = matched.props && matched.props[name]
    if (configProps) {
      extend(cache[name], {
        route,
        configProps
      })
      fillPropsinData(component, data, route, configProps)
    }
// 渲染组件
    return h(component, data, children)
  }
}

复制代码
router-link

src\components\link.js

export default {
  name: 'RouterLink',
  props: {
    to: {
      type: toTypes,
      required: true
    },
    tag: {
      type: String,
      default: 'a'
    },
    custom: Boolean,
    exact: Boolean,
    exactPath: Boolean,
    append: Boolean,
    replace: Boolean,
    activeClass: String,
    exactActiveClass: String,
    ariaCurrentValue: {
      type: String,
      default: 'page'
    },
    event: {
      type: eventTypes,
      default: 'click'
    }
  },
  render (h: Function) {
  // 获取路由实例、对象
    const router = this.$router
    const current = this.$route
    // 解析新路径
    const { location, route, href } = router.resolve(
      this.to,
      current,
      this.append
    )

    const classes = {}
    // 合并全局class配置
    const globalActiveClass = router.options.linkActiveClass
    const globalExactActiveClass = router.options.linkExactActiveClass
    const activeClassFallback =
      globalActiveClass == null ? 'router-link-active' : globalActiveClass
    const exactActiveClassFallback =
      globalExactActiveClass == null
        ? 'router-link-exact-active'
        : globalExactActiveClass
    const activeClass =
      this.activeClass == null ? activeClassFallback : this.activeClass
    const exactActiveClass =
      this.exactActiveClass == null
        ? exactActiveClassFallback
        : this.exactActiveClass

    // 创建重定向的路由对象
    const compareTarget = route.redirectedFrom
      ? createRoute(null, normalizeLocation(route.redirectedFrom), null, router)
      : route
  // 记录 router-link-active  router-link-exact-active状态
    classes[exactActiveClass] = isSameRoute(current, compareTarget, this.exactPath)
    classes[activeClass] = this.exact || this.exactPath
      ? classes[exactActiveClass]
      : isIncludedRoute(current, compareTarget)

    const ariaCurrentValue = classes[exactActiveClass] ? this.ariaCurrentValue : null
    // 处理event
    const handler = e => {
      if (guardEvent(e)) {
        if (this.replace) {
          router.replace(location, noop)
        } else {
          router.push(location, noop)
        }
      }
    }
    
    const on = { click: guardEvent }
    // 给每个event挂上处理好的事件
    if (Array.isArray(this.event)) {
      this.event.forEach(e => {
        on[e] = handler
      })
    } else {
      on[this.event] = handler
    }

    const data: any = { class: classes }

    const scopedSlot =
      !this.$scopedSlots.$hasNormal &&
      this.$scopedSlots.default &&
      this.$scopedSlots.default({
        href,
        route,
        navigate: handler,
        isActive: classes[activeClass],
        isExactActive: classes[exactActiveClass]
      })
// 处理作用域插槽
    if (scopedSlot) {
      if (process.env.NODE_ENV !== 'production' && !this.custom) {
        !warnedCustomSlot && warn(false, 'In Vue Router 4, the v-slot API will by default wrap its content with an <a> element. Use the custom prop to remove this warning:\n<router-link v-slot="{ navigate, href }" custom></router-link>\n')
        warnedCustomSlot = true
      }
      if (scopedSlot.length === 1) {
        return scopedSlot[0]
      } else if (scopedSlot.length > 1 || !scopedSlot.length) {
        if (process.env.NODE_ENV !== 'production') {
          warn(
            false,
            `<router-link> with to="${
              this.to
            }" is trying to use a scoped slot but it didn't provide exactly one child. Wrapping the content with a span element.`
          )
        }
        return scopedSlot.length === 0 ? h() : h('span', {}, scopedSlot)
      }
    }

    if (process.env.NODE_ENV !== 'production') {
      if ('tag' in this.$options.propsData && !warnedTagProp) {
        warn(
          false,
          `<router-link>'s tag prop is deprecated and has been removed in Vue Router 4. Use the v-slot API to remove this warning: https://next.router.vuejs.org/guide/migration/#removal-of-event-and-tag-props-in-router-link.`
        )
        warnedTagProp = true
      }
      if ('event' in this.$options.propsData && !warnedEventProp) {
        warn(
          false,
          `<router-link>'s event prop is deprecated and has been removed in Vue Router 4. Use the v-slot API to remove this warning: https://next.router.vuejs.org/guide/migration/#removal-of-event-and-tag-props-in-router-link.`
        )
        warnedEventProp = true
      }
    }
    // 处理是a标签的情况
    if (this.tag === 'a') {
      data.on = on
      data.attrs = { href, 'aria-current': ariaCurrentValue }
    } else {
     // 循环递归找默认插槽中有没有a标签
      const a = findAnchor(this.$slots.default)
      if (a) {
       // 标记静态
        a.isStatic = false
        // 复制a标签中的属性
        const aData = (a.data = extend({}, a.data))
        // 复制、初始化事件
        aData.on = aData.on || {}
      // 重新赋值处理过的事件
        for (const event in aData.on) {
          const handler = aData.on[event]
          if (event in on) {
            aData.on[event] = Array.isArray(handler) ? handler : [handler]
          }
        }
      // 将事件赋值在a标签上
        for (const event in on) {
          if (event in aData.on) {
            aData.on[event].push(on[event])
          } else {
            aData.on[event] = handler
          }
        }
        //重新处理href属性
        const aAttrs = (a.data.attrs = extend({}, a.data.attrs))
        aAttrs.href = href
        aAttrs['aria-current'] = ariaCurrentValue
      } else {
        // 如果没有a标签就自己监听事件
        data.on = on
      }
    }

    return h(this.tag, data, this.$slots.default)
  }
}

复制代码

总结

路由守卫
全局守卫:

  • beforeEach (to, from, next) 全局前置守卫
  • beforeResolve (to, from, next) 全局解析守卫
  • afterEach (to, from) 全局后置钩子

单个路由守卫:

  • beforeEnter (to, from, next)

组件路由守卫:

  • beforeRouteLeave(to, from, next) 在失活的组件里调用离开守卫
  • beforeRouteUpdate(to, from, next)在重用的组件里调用
  • beforeRouteEnter(to, from, next)在进入对应路由的组件创建前调用

路由导航解析
1.在动态路径下来回跳转例如/foo/:id /foo/1和/foo/2之间跳转
2.触发beforeRouteLeave(失活组件)
3.触发beforeEach(全局路由守卫)
4.触发beforeRouteUpdate(重用组件)
5.触发beforeEnter(单独路由守卫)
6.解析异步组件
7.触发beforeRouterEnter(进入目标组件)
8.触发beforeResolve(全局路由守卫)
9.导航被确认
10.触发afterEach(全局路由守卫)
11.触发dom更新
12.调用beforeRouteEnter中的next回调

路由用法

{
  // 会匹配所有路径
  path: '*'
}
// 当使用通配符路由时,请确保路由的顺序是正确的,也就是说含有通配符的路由应该放在最后。路由 { path: '*' } 通常用于客户端 404 错误。


{
  // 会匹配以 `/user-` 开头的任意路径
  path: '/user-*'
}
// 当使用了*通配符$route.params 内会自动添加一个名为 pathMatch 参数

// 给出一个路由 { path: '/user-*' }
this.$router.push('/user-foo')
this.$route.params.pathMatch // 'fo'o'
// 给出一个路由 { path: '*' }
this.$router.push('/non-existing')
this.$route.params.pathMatch // '/non-existing'



// ?可选参数
{ path: '/optional-params/:foo?' }
// 路由跳转是可以设置或者不设置foo参数,可选
<router-link to="/optional-params">/optional-params</router-link>
<router-link to="/optional-params/foo">/optional-params/foo</router-link>


// 零个或多个参数
{ path: '/optional-params/*' }
<router-link to="/number">没有参数</router-link>
<router-link to="/number/foo000">一个参数</router-link>
<router-link to="/number/foo111/fff222">多个参数</router-link>

// 一个或多个参数
{ path: '/optional-params/:foo+' }
<router-link to="/number/foo">一个参数</router-link>
<router-link to="/number/foo/foo111/fff222">多个参数</router-link>

// 自定义匹配参数
// 可以为所有参数提供一个自定义的regexp,它将覆盖默认值([^\/]+)
{ path: '/optional-params/:id(\\d+)' }
{ path: '/optional-params/(foo/)?bar' }

复制代码

结束语

VueRouter我们就先说到这了,有什么地方不对的请各位大佬指正。

有想看vue2源码的同学vue2源码分析

有想看vue1原理的同学手写vue1简版

最后麻烦大家动动小手点个赞!!!

文章分类
前端
文章标签