[Vue源码]学习一下Vue常用API的源码(一)

549 阅读6分钟

前言

最近阅读Vue源码,从打包的入口文件开始阅读,学到一些API的内部源码,这里分享一下自己的理解。

Vue.use

参数

  • {Object | Function} plugin

用法

用于安装Vue插件,例如vue-routervuex

import Router from 'vue-router'

Vue.use(Router)

如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。

该方法需要在调用 new Vue() 之前被调用。当 install 方法被同一个插件多次调用,插件将只会被安装一次。

源码

src\core\global-api\use.js

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    // 如果Vue._installedPlugins为空,则初始化Vue._installedPlugins用于存放所有安装的插件
    // 声明installedPlugins指向Vue._installedPlugins
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    // 如果插件已被安装,则返回vue实例
    if (installedPlugins.indexOf(plugin) > -1) {
      // 返回实例的目的是实现链式调用,例如Vue.use(Plugin1).filter('filter2',Filter2).component('component2',Component2)
      return this
    }

    // use可以以"Vue.use(plugin,arg1,arg2)"传入参数,arg1、arg2会以形参形式传入到plugin.install方法或plugin方法
    // 此处的toArray把数组中的第一个元素(plugin)去除
    const args = toArray(arguments, 1)
    // 把this(Vue)插入到第一个元素的位置
    // 因为install方法默认第一个传入参数是Vue
    args.unshift(this)
    // 根据传进来的是object(必须有名为install的函数)还是function分开执行
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    installedPlugins.push(plugin)
    return this
  }
}

流程总结:

Vue.filter,Vue.component,Vue.directive

因为Vue.filterVue.component以及Vue.directive都是在源码中都是由同一个函数生成的。而且其传入参数用法都一致。所以这里放在一起说。

参数

  • {string} id
  • {Function | Object} [definition]

用法

注册或获取全局组件。注册还会自动使用给定的 id 设置组件的名称

Vue.component( id, [definition] )

// 注册组件,传入一个扩展过的构造器
Vue.component('my-component', Vue.extend({ /* ... */ }))

// 注册组件,传入一个选项对象 (自动调用 Vue.extend)
Vue.component('my-component', { /* ... */ })

// 获取注册的组件 (始终返回构造器)
var MyComponent = Vue.component('my-component')

Vue.filter( id, [definition] )

// 注册
Vue.filter('my-filter', function (value) {
  // 返回处理后的值
})

// getter,返回已注册的过滤器
var myFilter = Vue.filter('my-filter')

Vue.directive( id, [definition] )

// 注册
Vue.directive('my-directive', {
  bind: function () {},
  inserted: function () {},
  update: function () {},
  componentUpdated: function () {},
  unbind: function () {}
})

// 注册 (指令函数)
Vue.directive('my-directive', function () {
  // 这里将会被 `bind` 和 `update` 调用
})

// getter,返回已注册的指令
var myDirective = Vue.directive('my-directive')

源码

src\core\global-api\assets.js

export function initAssetRegisters (Vue: GlobalAPI) {
  /**
   * 创建静态变量注册方法
   */
  // ASSET_TYPES:['component', 'directive', 'filter']
  // 通过遍历给Vue添加Vue.component,Vue.directive,Vue.filter函数
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      // 无第二参数则代表要返回已注册的变量
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          // 检查组件name的格式
          validateComponentName(id)
        }
        // isPlainObject用于判断definition是否为原始的object对象
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
           // this.options._base即Vue构造函数,
           // 此处调用Vue.extend把组件转换为对应的构造函数
          definition = this.options._base.extend(definition)
        }
        if (type === 'directive' && typeof definition === 'function') {
          // 如果注册'directive'时传入的是函数,则该指令在bind和update周期都调用该函数
          definition = { bind: definition, update: definition }
        }
        // 传进来的'component', 'directive', 'filter'都分别放入
        // 实例的options的'components', 'directives', 'filters'中
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}

流程总结:

Vue.mixin

参数

  • {Object} mixin

用法

全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。插件作者可以使用混入,向组件注入自定义的行为。不推荐在应用代码中使用

源码

src\core\global-api\mixin.js

export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    // 把两个option对象合并成一个,深度合并
    this.options = mergeOptions(this.options, mixin)
    // 返回实例的目的是实现链式调用
    return this
  }
}

Vue.extend

参数

  • {Object} options

用法

使用基础 Vue 构造器,创建一个对应传入组件的构造函数。传入参数是一个包含组件选项的对象。

// 创建构造器
var Profile = Vue.extend({
  template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
  data: function () {
    return {
      firstName: 'Walter',
      lastName: 'White',
      alias: 'Heisenberg'
    }
  }
})
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#app')

源码

src\core\global-api\extend.js

export function initExtend (Vue: GlobalAPI) {
  /**
   * 每个生成的构造函数(包括Vue)都有一个惟一的cid。
   * 该cid用于作为键缓存生成的构造函数
   */
  // 声明Vue.cid且赋值为0
  Vue.cid = 0
  // 声明cid变量,作为extend生成的构造函数的cid
  let cid = 1

  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    // Super的值取决于xx.extend中的xx
    // 注意:用Vue.extend生成的类也可以调用extend
    const Super = this
    const SuperId = Super.cid
    // 给传入的组件添加_Ctor,用于缓存不同的Super通过Super.extend生成的类
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    // 通过SuperId即Super中的cid缓存
    // 当重复调用相同Super的extend方法时,可以直接从_Ctor中取出结果
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      // 检测组件的name是否合符规范
      validateComponentName(name)
    }
    // 创建Sub构造函数
    const Sub = function VueComponent (options) {
      // 调用Vue.prototype._init初始化
      this._init(options)
    }
    // Sub构造函数继承Super,此处的继承方式使用寄生式组合继承[继承原型、构造函数、静态变量]
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super

    if (Sub.options.props) {
      // 此方法留到下面分析
      // 主要用于把extendOptions的props通过Object.defineProperty到实例上
      initProps(Sub)
    }
    if (Sub.options.computed) {
      // 没看懂,反正就是处理computed吧,之后补上
      initComputed(Sub)
    }

    // 把Super的'extend', 'mixin', 'use'属性复制到Sub
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // ASSET_TYPES:['component', 'directive', 'filter']
    // 把Super的'component', 'directive', 'filter'属性复制到Sub
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    // enable recursive self-lookup
    if (name) {
      Sub.options.components[name] = Sub
    }

    // 在扩展时间保持对Super.options的引用。稍后在实例化时,我们可以检查Super.options是否被更新。
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // 以Super.cid为键存储Sub
    cachedCtors[SuperId] = Sub
    return Sub
  }
}

initProps

源码中,在initProps函数上有以下注释:

对于props和计算属性,我们在扩展时在扩展原型的Vue实例上定义代理getter。这样可以避免对创建的每个实例调用Object.defineProperty。

划出重点:避免对创建的每个实例调用Object.defineProperty。我们带着这个观点分析一下源码:

// src\core\global-api\extend.js
function initProps (Comp) {
  const props = Comp.options.props
  // 把Sub.options.props中的每一个键都传入proxy方法中
  for (const key in props) {
    proxy(Comp.prototype, `_props`, key)
  }
}

// src\core\instance\state.js
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function proxy (target: Object, sourceKey: string, key: string) {
  // 注意:在 get 和 set 方法中,this 指向被访问和修改属性的实例对象。
  // 下面把sourceKey形参的值默认为"_props"分析
  
  // 当读取实例的其中一个与对应组件props中同名的值,相当于读取this._props中的同名属性
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  
  // 在Sub实例化时,props会被设定值,从而会触发set方法,
  // 继而给实例中名为_props的对象添加props中的键名一致的属性
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

上面代码中,在实例里声明一个_props的私有对象,用于给实例调用props中的值时返回。

通过上述proxy方法让Object.defineProperty逻辑在Vue.extend函数执行时就执行,利用setget方法中this指向实例从而达到构造函数实例化时,赋值到props的值会被复制到this._props中。从而避免了Object.defineProperty在创建的每个实例中重复调用。

流程总结:

后记

之后还会继续更新Vue的一些源码。