vue-router原理解析

368 阅读7分钟

前言:

路由插件可以说是vue项目必备,用了那么久的vue-router,还是没有明白其中是通过什么来实现跳转,做到页面没有刷新,遂研究了下vue-router的源码,查阅了几篇文章,最后自己也码篇文章记录一下,方便回来温习。

主要分析主流程的实现以及思路,便于理解如果构建一个这样的插件


目录:

  • 基本用法
  • 作为一个插件挂载到vue上使用
    • Vue.use(VueRouter)作用
    • install代码解析
  • 浏览器提供的一些api(新)
  • VueRouter实例化
    • VueRouter类的构造函数
    • 初始化hitory对象(以hash模式为例) base.js
    • 核心函数TransitionTo
    • 路由匹配对象
  • 几种模式区别
    • history模式的不同
    • astro模式的不同
  • push到改变路由的完整流程
  • 如何监听url变化
  • router-link和router-view分析(新)

ok 我们先看看平时使用router的代码

基本用法:

在app.vue里使用 router-view 组件渲染我们的页面

<div id="app">
  <h1>Hello App!</h1>
  <p>
    <!-- 使用 router-link 组件来导航. -->
    <!-- 通过传入 `to` 属性指定链接. -->
    <!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
    <router-link to="/foo">Go to Foo</router-link>
    <router-link to="/bar">Go to Bar</router-link>
  </p>
  <!-- 路由出口 -->
  <!-- 路由匹配到的组件将渲染在这里 -->
  <router-view></router-view>
</div>

在我们main.js引入VueRouter,调用Vue.use(VueRouter),然后按照规则创建routes数据,实例化VueRouter并挂载到Vue实例上。

import Vue from 'vue'
import VueRouter from 'vue-router'
import App from './App'

Vue.use(VueRouter)

// 1. 定义(路由)组件。
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

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

// 3. 创建 router 实例,然后传 `routes` 配置
const router = new VueRouter({
  routes // (缩写)相当于 routes: routes
})

// 4. 创建和挂载根实例。
const app = new Vue({
  el: '#app',
  render(h) {
    return h(App)
  },
  router
})

路由的使用很简单, router-view标签作为渲染的容器,router-link标签作为路由跳转的超链接,通过to来指定路由地址, 在main.js中的初始化我们可以看到,导入VueRouter的作用不仅用于new一个实例挂载进Vue的实例中,他还调用了Vue.use(VueRouter) Vue.use 是vue-router能作为一个插件的关键


浏览器提供的一些api

浏览器的history对象其实提供了很多方法,router就是基于这些方法去实现的

动作

window.history.back() // 返回上一个页面
window.history.forward() // 前进
window.history.go(-1) // 返回
window.history.go(1)  // 前进
window.history.pushState()
window.history.replaceState()

其他都很好理解,主要关注下pushState,pushState方法实际下是像history记录集合里面新增了一个记录,我们看到pushState里面的参数第一个对象包含了一个key值,是每个记录(页面)的唯一标识,存放在。。。 当前页面的唯一标识存放在window.history.state对象里。第二个参数是标题,第三个是url。

replaceState方法和pushState方法的区别就是,替换了当前记录的state对象。

监听

hashchange和popstate

hashchange:当URL的片段标识符更改时,将触发hashchange事件 (跟在#符号后面的URL部分,包括#符号)

监听方式:

window.addEventListener('hashchange', function() {
  console.log('The hash has changed!')
}, false);

popstate:浏览器动作会触发popstate事件,比如前进,后退

值得注意的是,pushstate和replaceState这两个方法,也不能触发popstate浏览器事件


作为一个插件挂载到vue上使用

Vue.use(VueRouter) 的作用

在vue中使用插件,一般全局使用都会执行Vue.use方法,use接收一个plugin参数,调用plugin里的install方法,如果没有定义install方法,会将plugin当作方法直接执行。所以vue-router里一定有一个install方法

install代码解析

install.js

import View from './components/view'
import Link from './components/link'

export let _Vue

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
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }
  // 混入生命周期
  Vue.mixin({
    beforeCreate () {
      console.log('router-beforeCreate')
      // 判断是否是根组件
      if (isDef(this.$options.router)) {
        console.log('根组件', this)
        this._routerRoot = this // 跟组件
        this._router = this.$options.router // 整个router对象
        this._router.init(this) // 初始化根组件路由
        console.log('this._router', this._router)
        // defineReactive(obj, key, val)
        // defineReactive() 就是用于定义响应式数据的工具函数,定义this._route为响应式变量,值为this._router.history.current, _route改变能触发render更新
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 如果不是根组件,关联根组件 this.routerRoot指向根组件
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
        // console.log('子组件:', this)
        // console.log('关联根组件:', this._routerRoot)
      }
      // 注册实例
      registerInstance(this, this)
    },
    destroyed () {
      // 销毁实例
      registerInstance(this)
    }
  })
  // 挂载变量到原型上
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })
  // 挂载变量到原型上
  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })

  // 注册全局组件
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

我们看到install做了一些事情,

  1. 导入两个组件(router-view,router-link)并声明(后面我们再细说)

  2. 定义了installed防止重复安装

  3. 注册实例

  4. mixin混入两个生命周期 *

    destroyed 销毁实例 beforeCreate: 赋值_routerRoot(根组件), _router(路由对象) 调用router对象中的init方法初始化路由的属性,回头再说下init方法 以及用defineReactive定义了this中的 _route属性为响应式变量,并指定初始值为history.current即为当前组件,也就是根组件 而且 _route的变化会触发render也就是视图更新

  5. 挂载变量($router, $route)到Vue的原型上

** 现在我们大概知道router插件怎么"安装"进Vue里面了。**


VueRouter实例化

VueRouter的构造函数

constructor (options: RouterOptions = {}) {
  this.app = null // 根组件
  this.apps = [] // 根组件数组
  this.options = options // 传入的路由配置
  this.beforeHooks = [] // 钩子函数,后面说
  this.resolveHooks = []
  this.afterHooks = []
  this.matcher = createMatcher(options.routes || [], this) // 匹配路由器,

  let mode = options.mode || 'hash' // 默认模式
  // 不支持history模式时,是否回退到hash模式
  this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
  if (this.fallback) {
    mode = 'hash'
  }
  if (!inBrowser) {
    mode = 'abstract'
  }
  this.mode = mode
  // 根据模式,初始化history对象
  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}`)
      }
  }
}

VueRouter的构造函数,核心就是createMatcher创建了一个匹配对象,路由根据地址或者名字去显示,就是这个对象去匹配的。 还有就是根据不同的mode去实例化history对象,我们先看实例化history对象,最后回来看匹配路由的对象

这里有三种模式 history, hash, abstract, 我们以hash为例看整个流程

初始化history对象

hash.js

export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    super(router, base)
    // check history fallback deeplinking
    if (fallback && checkFallback(this.base)) {
      return
    }
    ensureSlash() // 格式化url,加/
  }
  setupListeners () { // 设置监听
    const router = this.router // 当前路由对象
    const expectScroll = router.options.scrollBehavior // 是否有过滚动记录
    const supportsScroll = supportsPushState && expectScroll // supportsPushState:是否支持pushstate方法

    if (supportsScroll) {
      setupScroll() // 初始化,保存scroll的位置,监听popstate事件:获取popstate事件的key,并且设置到全局变量key里,是页面的唯一标识。有什么用呢?
    }
    // 监听路由变化事件
    window.addEventListener(
      supportsPushState ? 'popstate' : 'hashchange',
      () => {
        const current = this.current // 当前路由对象
        if (!ensureSlash()) {
          return
        }
        this.transitionTo(getHash(), route => {
          if (supportsScroll) { // 是否支持滚动和pushstate
            handleScroll(this.router, route, current, true) // 滚动到记录的位置,监听Vue.$nextTick实现
          }
          if (!supportsPushState) {
            replaceHash(route.fullPath) // 替换url的hash
          }
        })
      }
    )
  }
}

hashhistory继承了history,并执行父类构造方法。setupListeners事件里判断浏览器是否支持pushState,从而选择监听popstate或hashchange事件,然后执行transitionTo(跳转方法)。

HashHistory继承了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>

  // implemented by sub-classes
  +go: (n: number) => void // + 号表示?
  +push: (loc: RawLocation) => void
  +replace: (loc: RawLocation) => void
  +ensureURL: (push?: boolean) => void
  +getCurrentLocation: () => string

  constructor (router: Router, base: ?string) {
    this.router = router // router实例
    this.base = normalizeBase(base) // 
    // start with a route object that stands for "nowhere"
    this.current = START
    this.pending = null
    this.ready = false
    this.readyCbs = []
    this.readyErrorCbs = []
    this.errorCbs = []
  }

  listen (cb: Function) { // 设置更新路由后的回调函数,在index.js里面调用了
    this.cb = cb
  }

  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)
  }

  transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) {
    ...
    
  }

  confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
    ...
  }
  
  // 更新路由函数
  updateRoute (route: Route) {
    const prev = this.current
    this.current = route // 切换当前路由
    this.cb && this.cb(route) // 执行回调,这个回调是listen时设置的
    this.router.afterHooks.forEach(hook => {
      hook && hook(route, prev)
    })
  }
}

History里面提供了几个可调用的函数,最核心的是transitionTo

核心函数transitionTo

transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) {
    // this.current为当前路由
    const route = this.router.match(location, this.current) // 得到即将跳转的路由对象 [name, meta, path, hash,query, params, fullPath, matcched]
    this.confirmTransition( // 确认路由
      route,
      () => { // 确认后回调
        this.updateRoute(route) // 更新路由
        onComplete && onComplete(route) // transitionTo的回调函数
        this.ensureURL() // 替换url

        // fire ready cbs once
        if (!this.ready) {
          this.ready = true
          this.readyCbs.forEach(cb => {
            cb(route) // 准备后的回调事件
          })
        }
      },
      err => {
        if (onAbort) {
          onAbort(err)
        }
        if (err && !this.ready) {
          this.ready = true
          this.readyErrorCbs.forEach(cb => {
            cb(err)
          })
        }
      }
    )
  }

函数的关键是match匹配得到一个route对象,这个对象长这样

  const route: Route = {
    name: location.name || (record && record.name),
    meta: (record && record.meta) || {},
    path: location.path || '/',
    hash: location.hash || '',
    query,
    params: location.params || {},
    fullPath: getFullPath(location, stringifyQuery),
    matched: record ? formatMatch(record) : [] // 匹配到的路由数组,属性和record一致
  }
  

然后调用了确认路由方法confirmTransition(判断是否同个路由等操作),确认完成在回调中执行了updateRoute去更新路由,ensureURL方法更新url。

** updateRoute**

updateRoute (route: Route) {
  const prev = this.current
  this.current = route // 切换当前路由
  this.cb && this.cb(route) // 这个回调是listen时设置的 app._route = route
  this.router.afterHooks.forEach(hook => {
    hook && hook(route, prev)
  })
}

updateRoute关键就是替换当前路由,然后执行cb() 回调函数,在listen中设置了cb,调用则是在main.js中的init中被调用

// 监听route的变化, 更新根组件上的_route, 
history.listen(route => {
  this.apps.forEach((app) => {
    app._route = route
  })
})

ensureURl

作用就是替换页面为新的url

// hash模式:
ensureURL (push?: boolean) { // push表示push方法 或是replace方法
  const current = this.current.fullPath // 当前链接完整路径
  if (getHash() !== current) {
    push ? pushHash(current) : replaceHash(current)
  }
}
function pushHash (path) { // 改变url上的hash
  if (supportsPushState) { // 浏览器支持
    pushState(getUrl(path))
  } else {
    window.location.hash = path // 直接对hash赋值
  }
}

function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}
function getUrl (path) { // 获取一个新的完整url
  const href = window.location.href
  const i = href.indexOf('#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}

当updateRoute更新当前路由,ensureUrl更新浏览器url和历史记录后,整个主线就完成了。

我再来看关键的匹配路由对象,即match

路由匹配对象

createMatcher.js

export function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {
  // 初始化创建路由映射表
  const { pathList, pathMap, nameMap } = createRouteMap(routes)
  // 继续添加路由到映射表中
  function addRoutes (routes) {
    createRouteMap(routes, pathList, pathMap, nameMap)
  }
  // 路由匹配
  function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
    if (location.name) { // 名字匹配
      const record = nameMap[name]
      return _createRoute(record, location, redirectedFrom) // 匹配到的路由再包装
    } else if (location.href) { // 路径匹配
      const record = pathMap[path]
      return _createRoute(record, location, redirectedFrom)
    }
    // 没有匹配到
    return _createRoute(null, location)
    
  }

  return {
    match,
    addRoutes
  }
}

createMatcher返回了对象,通过闭包让我们可以调用里面的两个方法match,addRoutes,match方法大概就是通过名字或者路径在nameMap和pathMap中找对应的record,然后_createRoute方法创建一个完整的route对象。

我们先看看nameMap和pathMap,他们是通过createRouteMap创建的

createRouteMap

export function createRouteMap (
  routes: Array<RouteConfig>,
  oldPathList?: Array<string>,
  oldPathMap?: Dictionary<RouteRecord>,
  oldNameMap?: Dictionary<RouteRecord>
): {
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
  // 路径列表用于控制路径匹配优先级
  const pathList: Array<string> = oldPathList || []
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
  // 遍历路由添加记录 往pathlist,pathMap,nameMap添加route
  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route)
  })

  // 确保通配符*路由始终在末尾 *号有什么用?
  for (let i = 0, l = pathList.length; i < l; i++) {
    if (pathList[i] === '*') {
      pathList.push(pathList.splice(i, 1)[0])
      l--
      i--
    }
  }
  // 返回url数组,映射表
  return {
    pathList,
    pathMap,
    nameMap
  }
}

// 添加路由记录
function addRouteRecord (
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>,
  route: RouteConfig,
  parent?: RouteRecord,
  matchAs?: string
) {
  // 路径和名称
  const { path, name } = route
  const pathToRegexpOptions: PathToRegexpOptions =
    route.pathToRegexpOptions || {}
  // 格式化 url,去掉多余的/和最后一个/ 
  const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)

  if (typeof route.caseSensitive === 'boolean') {
    pathToRegexpOptions.sensitive = route.caseSensitive
  }
  // 记录对象,match返回的对象就是当前record对象
  const record: RouteRecord = {
    path: normalizedPath, // 路由路径
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), // 匹配规则,细看
    components: route.components || { default: route.component }, // 组件模板,细看
    instances: {}, // 
    name,
    parent,
    matchAs, // 
    redirect: route.redirect, // 重定向
    beforeEnter: route.beforeEnter, // 路由独享守卫
    meta: route.meta || {}, // 属性
    props:
      route.props == null
        ? {}
        : route.components
          ? route.props
          : { default: route.props }
  }
  // children递归添加路由记录
  if (route.children) {
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }
  // 更新pathMap,保存了record
  if (!pathMap[record.path]) {
    pathList.push(record.path)
    pathMap[record.path] = record
  }
  // 有别名路由时
  if (route.alias !== undefined) {
    const aliases = Array.isArray(route.alias) ? route.alias : [route.alias]
    for (let i = 0; i < aliases.length; ++i) {
      const alias = aliases[i]
      if (process.env.NODE_ENV !== 'production' && alias === path) {
        warn(
          false,
          `Found an alias with the same value as the path: "${path}". You have to remove that alias. It will be ignored in development.`
        )
        // skip in dev to make it work
        continue
      }
	  // 存入路由的别名属性 为什么?
      const aliasRoute = {
        path: alias,
        children: route.children
      }
      addRouteRecord(
        pathList,
        pathMap,
        nameMap,
        aliasRoute,
        parent,
        record.path || '/' // matchAs
      )
    }
  }

  if (name) { // 有路由名称 则添加到nameMap中
    if (!nameMap[name]) {
      nameMap[name] = record
    } else if (process.env.NODE_ENV !== 'production' && !matchAs) {
      warn(
        false,
        `Duplicate named routes definition: ` +
          `{ name: "${name}", path: "${record.path}" }`
      )
    }
  }
}

createRouteMap返回了一个包含pathList,pathMap,nameMap的一个对象,关键就是遍历routes,执行addRouteRecord方法,往路由映射表pathmap添加{record.path:record}的键值对,往nameMap添加{record.name: record} 往pathlist添加path。

总结一下:pathList是路径的一个集合数组。 pathMap和nameMap则是以path和name作为key,record作为值包含所有路由的数组,record是单个路由对象的一些属性, 这些属性的具体作用我们再细看

执行完方法之后,返回了映射表pathMap,nameMap,这样格式的map,match方法就可以轻松的匹配到当前路由,match方法返回的是 _createRoute(record, location, redirectedFrom),我们具体看一下_createRoute方法

  function _createRoute (
    record: ?RouteRecord,
    location: Location,
    redirectedFrom?: Location
  ): Route {
    // 有重定向
    if (record && record.redirect) {
      // redirect返回route.redirect的路由对象
      return redirect(record, redirectedFrom || location)
    }
    // 有别名
    if (record && record.matchAs) {
      return alias(record, location, record.matchAs)
    }
    return createRoute(record, location, redirectedFrom, router)
  }

export function createRoute (
  record: ?RouteRecord,
  location: Location,
  redirectedFrom?: ?Location,
  router?: VueRouter
): Route {
  const stringifyQuery = router && router.options.stringifyQuery

  let query: any = location.query || {}
  try {
    query = clone(query)
  } catch (e) {}
  // 最后返回的route对象
  const route: Route = {
    name: location.name || (record && record.name),
    meta: (record && record.meta) || {},
    path: location.path || '/',
    hash: location.hash || '',
    query,
    params: location.params || {},
    fullPath: getFullPath(location, stringifyQuery),
    matched: record ? formatMatch(record) : []
  }
  if (redirectedFrom) {
    route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
  }
  return Object.freeze(route)
}

createRoute返回一个重新定义属性的route对象,这个对象即是match函数返回的对象,在transitionTo方法中去调用updateRoute方法,传入route对象,更新_route,触发render


几种路由模式的区别

history模式

export class HTML5History extends History { // history和hash都继承自History
  constructor (router: Router, base: ?string) { // 接受router实例对象, base
    super(router, base)

    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll

    if (supportsScroll) {
      setupScroll()
    }

    const initLocation = getLocation(this.base)
    // 设置监听事件
    window.addEventListener('popstate', e => {
      const current = this.current

      // Avoiding first `popstate` event dispatched in some browsers but first
      // history route not updated since async guard at the same time.
      const location = getLocation(this.base)
      if (this.current === START && location === initLocation) {
        return
      }

      this.transitionTo(location, route => { // 更新路由的过渡方法
        if (supportsScroll) {
          handleScroll(router, route, current, true)
        }
      })
    })
  }
}
  • history监听的事件只有popstate,而hash监听了popstate和hashchange
  • 由于popstate只有触发浏览器前进后退才会执行,所以手动更改url时history模式会触发页面刷新,而hash模式则不会,因为hash只是客户端的一个状态,改变不会重新发送请求。
  • 调用push方法触发跳转时,history只能通过pushstate,hash则可以选择直接替换url的hash值,所以history模式要运行再支持pushstate这个api的浏览器环境。

hash

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(
      location, // 传入的route对象 {path: '', query: {}}
      route => { // route对象
        pushHash(route.fullPath) // 对浏览器hash赋值
        handleScroll(this.router, route, fromRoute, false) // 滚动 还没看
        onComplete && onComplete(route)
      },
      onAbort
    )
  }

function pushHash (path) { // 改变url上的hash
  if (supportsPushState) {
    pushState(getUrl(path))
  } else {
    window.location.hash = path
  }
}

history

 push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      pushState(cleanPath(this.base + route.fullPath))
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }
  • history能做到页面不刷新是因为:pushstate 新的URL可以是任何和当前URL同源的URL。但是设置 window.location 只会在你只设置锚的时候才会使当前的URL。

routerlink & routerview

router-link

在router里承担一个跳转的作用,渲染后是一个a标签,

eg:

<router-link to="/foo">/foo</router-link>

渲染成如下:

<a href="/basic/foo" class="">/foo</a>

源码:

// 注册全局组件
Vue.component('RouterLink', Link)
  

router-view

作为一个渲染容器,渲染当前路由组件

// 注册全局组件
Vue.component('RouterView', View)

// View关键代码
render ({ props, children, parent, data }) {
	const h = parent.$createElement
    const name = props.name
    const route = parent.$route
    const matched = route.matched[0]
	const component = matched.components[name]
	return h(component, data, children)
}

在render中:matched里面包含了components组件集合,获取了当前组件componet,然后渲染,