02-Vue Router源码核心

263 阅读4分钟

vue-router实现的核心步骤

Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js的核心深度集成,让构建单页面应用变得易如反 掌。

1. 应用插件

  • use做了什么?

执行了插件的install方法

import Router from 'vue-router'
Vue.use(Router)

2. 创建router实例

export default new Router({...})

3. main.js中配置router实例

  • 为什么需要挂到这里?

获取vue-router实例并挂载到Vue.prototype上,这样在全局的时候可以直接通过this.$router 来获取相应的数据

import router from './router'
new Vue({
  router,
}).$mount("#app");

4. 添加路由视图

  • router-link和router-view是哪来的?
  • router-view做了什么?
<router-view></router-view>
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>

vue-router源码实现

  • 实现vue插件

    • vue提供了插件注册机制是,每个插件都需要实现一个静态的 install方法,当执行 Vue.use 注册插件的时候,就会执行 install 方法,该方法执行的时候第一个参数强制是 Vue对象。
    • vue这种插件机制,使得在编写插件的时候,不需要再引入vue了,注册的时候给插件注入了一个参数就是vue的实例
    • install是static方法:类的静态方法用 static关键字定义,不能在类的实例上调用静态方法,只能够通过类本身调用。这里的install 只能vue-router类调用,他的实例不能调用(防止vue-router的实例在外部调用)。
  • 解析routes选项

  • 监控url变化

    • html5 history api
    • hash index#/login
  • 实现两个全局组件

    • router-link
    • router-view
// mVueRouter.js
let Vue // 保存vue构造函数引用

class mVueRouter {
  constructor (options) {
    this.$options = options
    this.routeMap = {}

    // 当前的url需要时响应式的
    this.app = new Vue({
      data: { current: '/' }
    })
  }

  // 初始化
  init () {
    // 监听hash变化
    this.bindEvents()
    // 解析routes
    this.createRoyteMap()
    // 声明组件
    this.initComponents()
  }

  bindEvents () {
    window.addEventListener('hashchange', this.onHashChange.bind(this))// 事件的回调函数,这边的this会丢失
  }

  onHashChange () {
    this.app.current = window.location.hash.slice(1) || '/'
  }

  createRoyteMap () {
    // 遍历用户配置的路由
    this.$options.routes.forEach(item => {
      this.routeMap[item.path] = item
    })
  }

  initComponents () {
    // router-link转换目标:<a href="#/">xxx</a>
    Vue.component('router-link', {
      props: {
        to: String
      },
      render (h) {
        // h(tag,data,children)
        return h('a', {
          attrs: { href: '#' + this.to }
        }, [this.$slots.default])
        // jsx
        // return <a href={'#' + this.to}>{this.$slots.default}</a>
      }

    })
    // 获取path对应的component,并将其渲染出来
    Vue.component('router-view', {
      render: (h) => {
        // 只要render函数中用到的响应式数据,只要响应式数据发生了变化,render函数就会重新执行
        const component = this.routeMap[this.app.current].component
        return h(component)
      }
    })
  }
}

mVueRouter.install = function (_vue) {
  Vue = _vue

  // 实现一个混入
  Vue.mixin({
    beforeCreate () {
      // 获取mVueRouter实例并挂载到Vue.prototype上
      if (this.$options.router) {
        Vue.prototype.$router = this.$options.router
        // 路由器初始化
        this.$options.router.init()
      }
    }
  })
}

export default mVueRouter

嵌套路由

以下是嵌套路由的核心代码

 Vue.component('router-view', {
      // 函数式组件
      functional: true,
      props: {
        name: {
          type: String,
          default: 'default'
        }
      },
      render (h, { props, children, parent, data }) {
        data.routerView = true
        const name = props.name
        const route = parent.$route
        
        
        let depth = 0
        while (parent && parent._routerRoot !== parent) {
          const vnodeData = parent.$vnode ? parent.$vnode.data : {}
          if (vnodeData.routerView) {
            depth++
          }
          parent = parent.$parent
        }
        
        const matched = route.matched[depth]
        const component = matched && matched.components[name]

        return h(component, data, children)
      }
})

Render函数

简单来说CreateElement就是用来生成Vnode的函数,CreateElement返回的不是真实的DOM元素,是Vnode,render函数所包含的信息会告诉vue页面上需要渲染什么节点及其子节点。

1. createElement 参数

// @returns {VNode}
createElement(
  // {String | Object | Function}
  // 一个 HTML 标签名、组件选项对象,或者
  // resolve 了上述任何一种的一个 async 函数。必填项。
  'div',

  // {Object}
  // 一个与模板中 attribute 对应的数据对象。可选。
  {
  // 与 `v-bind:class` 的 API 相同,
  // 接受一个字符串、对象或字符串和对象组成的数组
  'class': {
    foo: true,
    bar: false
  },
  // 与 `v-bind:style` 的 API 相同,
  // 接受一个字符串、对象,或对象组成的数组
  style: {
    color: 'red',
    fontSize: '14px'
  },
  // 普通的 HTML attribute
  attrs: {
    id: 'foo'
  },
  // 组件 prop
  props: {
    myProp: 'bar'
  },
  // DOM property
  domProps: {
    innerHTML: 'baz'
  },
  // 事件监听器在 `on` 内,
  // 但不再支持如 `v-on:keyup.enter` 这样的修饰器。
  // 需要在处理函数中手动检查 keyCode。
  on: {
    click: this.clickHandler
  },
  // 仅用于组件,用于监听原生事件,而不是组件内部使用
  // `vm.$emit` 触发的事件。
  nativeOn: {
    click: this.nativeClickHandler
  },
  // 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
  // 赋值,因为 Vue 已经自动为你进行了同步。
  directives: [
    {
      name: 'my-custom-directive',
      value: '2',
      expression: '1 + 1',
      arg: 'foo',
      modifiers: {
        bar: true
      }
    }
  ],
  // 作用域插槽的格式为
  // { name: props => VNode | Array<VNode> }
  scopedSlots: {
    default: props => createElement('span', props.text)
  },
  // 如果组件是其它组件的子组件,需为插槽指定名称
  slot: 'name-of-slot',
  // 其它特殊顶层 property
  key: 'myKey',
  ref: 'myRef',
  // 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
  // 那么 `$refs.myRef` 会变成一个数组。
  refInFor: true
},

  // {String | Array}
  // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
  // 也可以使用字符串来生成“文本虚拟节点”。可选。
  [
    '先写一些文字',
    createElement('h1', '一则头条'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)

函数式组件

之前创建的锚点标题组件是比较简单,没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法。实际上,它只是一个接受一些 prop 的函数。在这样的场景下,我们可以将组件标记为 functional,这意味它无状态 (没有响应式数据),也没有实例 (没有 this 上下文)。

一个函数式组件就像这样:

1. render函数提供了第二个参数作为上下文

Vue.component('my-component', {
  functional: true,
  // Props 是可选的
  props: {
    // ...
  },
  // 为了弥补缺少的实例
  // 提供第二个参数作为上下文
  render: function (createElement, context) {
    // ...
  }
})

2. context上下文包含的字段对象

组件需要的一切都是通过 context 参数传递,它是一个包括如下字段的对象:

  • props:提供所有 prop 的对象
  • children:VNode 子节点的数组
  • slots:一个函数,返回了包含所有插槽的对象
  • scopedSlots:(2.6.0+) 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。
  • data:传递给组件的整个数据对象,作为 createElement 的第二个参数传入组件
  • parent:对父组件的引用
  • listeners:(2.3.0+) 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是 data.on 的一个别名。
  • injections:(2.3.0+) 如果使用了 inject 选项,则该对象包含了应当被注入的 property。

在添加 functional: true 之后,需要更新我们的锚点标题组件的渲染函数,为其增加 context 参数,并将 this.$slots.default 更新为 context.children,然后将 this.level 更新为 context.props.level。

因为函数式组件只是函数,所以渲染开销也低很多。

在作为包装组件时它们也同样非常有用。比如,当你需要做这些时:

程序化地在多个组件中选择一个来代为渲染; 在将 children、props、data 传递给子组件之前操作它们。

var EmptyList = { /* ... */ }
var TableList = { /* ... */ }
var OrderedList = { /* ... */ }
var UnorderedList = { /* ... */ }

Vue.component('smart-list', {
  functional: true,
  props: {
    items: {
      type: Array,
      required: true
    },
    isOrdered: Boolean
  },
  render: function (createElement, context) {
    function appropriateListComponent () {
      var items = context.props.items

      if (items.length === 0)           return EmptyList
      if (typeof items[0] === 'object') return TableList
      if (context.props.isOrdered)      return OrderedList

      return UnorderedList
    }

    return createElement(
      appropriateListComponent(),
      context.data,
      context.children
    )
  }
})

3. vue-router:router-view官方是怎么获取参数的

// 获取path对应的component,并将其渲染出来
Vue.component('router-view', {
  // hack的方式:箭头函数取到了vue-router的实例,可是如果要使用当前router-view的this则无法拿到
  // render: (h) => {
  //     // 只要render函数中用到的响应式数据,只要响应式数据发生了变化,render函数就会重新执行
  //     const component = this.routeMap[this.app.current].component
  //     return h(component)
  //  }
  
  // 函数式组件
  functional: true,
  render (h, { parent }) {
    const router = parent.$router
    const component = router.routeMap[router.app.current].component
    return h(component)
  }
})

参考

vue-router源码