美文网首页
Vue源码解析二——从一个小例子开始逐步分析

Vue源码解析二——从一个小例子开始逐步分析

作者: snow_in | 来源:发表于2019-04-03 10:07 被阅读0次

    每个Vue应用都是从创建Vue实例开始的,这里我们就以一个简单的例子为基础,慢慢深究Vue的实现细节。

    <div id="app">{{ a }}</div>
    var vm = new Vue({
      el: '#app',
      data: { a: 1 }
    })
    

    当我们重新设置a属性时(vm.a = 2),视图上显示的值也会变成2。这么简单的例子大家都知道啦,现在就看看使用Vue构造函数初始化的时候都发生了什么。

    打开/src/core/instance/index.js文件,看到Vue构造函数的定义如下:

    function Vue (options) {
      if (process.env.NODE_ENV !== 'production' &&
        !(this instanceof Vue)
      ) {
        warn('Vue is a constructor and should be called with the `new` keyword')
      }
      this._init(options)
    }
    

    由此可知首先执行了this._init(options)代码,_init方法在 src/core/instance/init.js文件中被添加到了Vue原型上,我们看看该方法做了什么。

    const vm: Component = this
    // a uid
    vm._uid = uid++
    

    首先是定义了vm,它的值就是this,即当前实例。接着定义了一个实例属性_uid,它是Vue组件的唯一标识,每实例化一个Vue组件就会递增。

    接下来是在非生产环境下可以测试性能的一段代码:

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        startTag = `vue-perf-start:${vm._uid}`
        endTag = `vue-perf-end:${vm._uid}`
        mark(startTag)
    }
    
    ...
    
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        vm._name = formatComponentName(vm, false)
        mark(endTag)
        measure(`vue ${vm._name} init`, startTag, endTag)
    }
    

    省略了中间的代码。这段代码的执行条件是:非生产环境,config.performance为true 和 mark都存在的情况下。官方提供了performance的全局API。mark和measure在core/util/perf.js文件中,其实就是window.performance.mark和window.performance.measure. 组件初始化的性能追踪就是在代码的开头和结尾分别用mark打上标记,然后通过measure函数对两个mark进行性能计算。

    再看看中间代码,也就是被性能追踪的代码:

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
        // optimize internal component instantiation
        // since dynamic options merging is pretty slow, and none of the
        // internal component options needs special treatment.
        initInternalComponent(vm, options)
    } else {
        vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
        )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
        initProxy(vm)
    } else {
        vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    

    先是设置了_isVue实例属性,作为一个标志避免Vue实例被响应系统观测。

    接下来是合并选项的处理,我们并没有使用_isComponent属性,所以上面的代码会走else分支,挂载了实例属性$options, 该属性的生成通过调用了mergeOptions方法,接下来我们看看mergeOptions方法都干了些什么。

    mergeOptions 函数来自于 core/util/options.js 文件, 该函数接受三个参数。先来看一下_init函数中调用该函数时传递的参数分别是什么。

    vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor), options || {}, vm)
    

    后两个参数都好理解,options是我们实例化时传过来的参数

    {
      el: '#app',
      data: { a: 1 }
    }
    

    vm就是当前实例。

    重点看一下第一个参数,是调用方法生成的resolveConstructorOptions(vm.constructor)

    export function resolveConstructorOptions (Ctor: Class<Component>) {
      let options = Ctor.options
      if (Ctor.super) {
        const superOptions = resolveConstructorOptions(Ctor.super)
        const cachedSuperOptions = Ctor.superOptions
        if (superOptions !== cachedSuperOptions) {
          // super option changed,
          // need to resolve new options.
          Ctor.superOptions = superOptions
          // check if there are any late-modified/attached options (#4976)
          const modifiedOptions = resolveModifiedOptions(Ctor)
          // update base extend options
          if (modifiedOptions) {
            extend(Ctor.extendOptions, modifiedOptions)
          }
          options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
          if (options.name) {
            options.components[options.name] = Ctor
          }
        }
      }
      return options
    }
    

    传的参数是vm.constructor,在我们例子中就是Vue构造函数,因为我们是直接调用的Vue创建的实例。那什么时候不是Vue构造函数呢,在用Vue.extend()去创建子类,再用子类构造实例的时候,vm.constructor就是子类而不是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('#mount-point')
    

    vm.constructor就是Profile。

    再看if语句块,是在Ctor.super为真的情况下执行,super是子类才有的属性,所以在我们的例子中是不执行的,直接返回options,即Vue.options, 它的值如下:

    Vue.options = {
        components: {
            KeepAlive
            Transition,
            TransitionGroup
        },
        directives:{
            model,
            show
        },
        filters: Object.create(null),
        _base: Vue
    }
    

    不记得options是如何形成的可以看一下Vue源码解析一——骨架梳理。现在三个参数已经搞清楚了,就来看看mergeOptions方法发生了什么吧。

    检查组件名是否合法

    mergeOptions方法在core/util/options.js文件中,我们找到该方法,首先看一下方法上方的注释:

    /**
     * Merge two option objects into a new one.
     * Core utility used in both instantiation and inheritance.
     */
    

    合并两个选项对象为一个新的对象。在实例化和继承中使用的核心实用程序。实例化就是调用_init方法的时候,继承也就是使用Vue.extend的时候。现在我们知道了该方法的作用,就来看一下该方法的具体实现吧

    if (process.env.NODE_ENV !== 'production') {
        checkComponents(child)
    }
    

    在非生产环境下,会去校验组件的名字是否合法,checkComponents函数就是用来干这个的,该函数也在当前文件中,找到该函数:

    /**
     * Validate component names
     */
    function checkComponents (options: Object) {
      for (const key in options.components) {
        validateComponentName(key)
      }
    }
    

    一个for in循环遍历options.components,以子组件的名字为参数调用validateComponentName方法,所以该方法才是检测组件名是否合法的具体实现。源码如下:

    export function validateComponentName (name: string) {
      if (!new RegExp(`^[a-zA-Z][\\-\\.0-9_${unicodeLetters}]*$`).test(name)) {
        warn(
          'Invalid component name: "' + name + '". Component names ' +
          'should conform to valid custom element name in html5 specification.'
        )
      }
      if (isBuiltInTag(name) || config.isReservedTag(name)) {
        warn(
          'Do not use built-in or reserved HTML elements as component ' +
          'id: ' + name
        )
      }
    }
    

    该方法由两个if语句块组成,要想组件名合法,必须满足这两个if条件:

    1. 正则表达式/^[a-zA-Z][\\-\\.0-9_${unicodeLetters}]*$/
    2. isBuiltInTag(name) || config.isReservedTag(name) 条件不成立

    对于条件一就是要使用符合html5规范中的有效自定义元素名称

    条件二是使用了两个方法来检测的,isBuiltInTag方法用来检测是否是内置标签,在shared/util.js文件中定义

    /**
     * Check if a tag is a built-in tag.
     */
    export const isBuiltInTag = makeMap('slot,component', true)
    

    isBuiltInTag方法是调用makeMap()生成的,看一下makeMap的定义:

    /**
     * Make a map and return a function for checking if a key
     * is in that map.
     */
    export function makeMap (
      str: string,
      expectsLowerCase?: boolean
    ): (key: string) => true | void {
      const map = Object.create(null)
      const list: Array<string> = str.split(',')
      for (let i = 0; i < list.length; i++) {
        map[list[i]] = true
      }
      return expectsLowerCase
        ? val => map[val.toLowerCase()]
        : val => map[val]
    }
    

    该方法最后返回一个函数,函数接收一个参数,如果参数在map中就返回true,否则返回undefined。map是根据调用makeMap方法时传入的参数生成的,按照来处来看,也就是

    map = { slot: true, component: true }
    

    由此可知slotcomponent 是作为Vue的内置标签而存在的,我们的组件命名不能使用它们。

    还有一个方法config.isReservedTagcore/config.js文件中定义,在platforms/web/runtime/index.js文件中被覆盖

    Vue.config.isReservedTag = isReservedTag
    

    isReservedTag方法在platforms/web/util/element.js文件中,

    export const isReservedTag = (tag: string): ?boolean => {
      return isHTMLTag(tag) || isSVG(tag)
    }
    

    就是检测是否是规定的html标签和svg标签。到此组件名是否合法的检测就结束了。

    if (typeof child === 'function') {
        child = child.options
    }
    

    这里是一个判断,如果child是一个function,就取它的options静态属性。什么函数具有options属性呢?Vue构造函数和使用Vue.extend()创建的'子类',这就允许我们在进行选项合并的时候,去合并一个 Vue 实例构造者的选项了。

    规范化Props

    normalizeProps(child, vm)
    normalizeInject(child, vm)
    normalizeDirectives(child)
    

    这是三个规范化选项的函数调用,分别是针对props, inject, directives。为什么会有规范化选项这一步呢?因为我们在使用选项的时候可以有多种不同的用法,比如props, 既可以是字符串数组也可以是对象:

    props: ['test1', 'test2']
    
    props: {
        test1: String,
        test2: {
            type: String,
            default: ''
        }
    }
    

    这方便了我们使用,但是Vue要对选项进行处理,多种形式定然增加了复杂度,所以要处理成一种格式,这就是该函数的作用。

    我们分别来看具体是怎么规范化的,首先是函数normalizeProps:

    /**
     * Ensure all props option syntax are normalized into the
     * Object-based format.
     */
    function normalizeProps (options: Object, vm: ?Component) {
      const props = options.props
      if (!props) return
      const res = {}
      let i, val, name
      if (Array.isArray(props)) {
        
      } else if (isPlainObject(props)) {
        
      } else if (process.env.NODE_ENV !== 'production') {
        warn(
          `Invalid value for option "props": expected an Array or an Object, ` +
          `but got ${toRawType(props)}.`,
          vm
        )
      }
      options.props = res
    }
    

    根据注释我们知道props最后被规范成对象的形式了。先大体看一下函数的结构:

    • 先是判断props是否存在,如果不存在直接返回
    • if语句处理数组props
    • else if语句块处理对象props
    • 最后如果既不是数组也不对象,还不是生成环境,就发出类型错误的警告

    数组类型的props是如何处理的呢?看一下代码:

    i = props.length
    while (i--) {
        val = props[i]
        if (typeof val === 'string') {
            name = camelize(val)
            res[name] = { type: null }
        } else if (process.env.NODE_ENV !== 'production') {
            warn('props must be strings when using array syntax.')
        }
    }
    

    使用while循环处理每一项,如果是字符串,先用camelize函数转了一下该字符串,然后存储在了res中,其值是{ type: null }camelize函数定义在shared/util.js中,其作用就是把连字符格式的字符串转成驼峰式的。比如:

    test-a // testA
    

    如果不是字符串类型就发出警告,所以数组格式的props中元素必须是字符串。

    数组格式的规范化我们已经了解了,如果我们传的是

    props: ['test-a', 'test2']
    

    规范化之后就变成:

    props: {
        testA: { type: null },
        test2: { type: null }
    }
    

    再来看看对象props是如何规范化的:

    for (const key in props) {
        val = props[key]
        name = camelize(key)
        res[name] = isPlainObject(val)
            ? val
            : { type: val }
    }
    

    我们之前举例说过props是对象的话它的属性值有两种写法,一种属性值直接是类型,还有一种属性值是对象。这里的处理是如果是对象的不做处理,是类型的话就把它作为type的值。所以如果我们传的是:

    props: {
        test1: String,
        test2: {
            type: String,
            default: ''
        }
    }
    

    规范化之后变成:

    props: {
        test1: { type: String },
        test2: {
            type: String,
            default: ''
        }
    }
    

    这样我们就了解了Vue是如何规范化Props的了

    规范化inject

    inject选项不常使用,我们先来看看官方文档的介绍

    // 父级组件提供 'foo'
    var Provider = {
      provide: {
        foo: 'bar'
      },
      // ...
    }
    
    // 子组件注入 'foo'
    var Child = {
      inject: ['foo'],
      created () {
        console.log(this.foo) // => "bar"
      }
      // ...
    }
    

    在子组件中并没有定义foo属性却可以使用,就是因为使用inject注入了这个属性,而这个属性的值是来源于父组件。和props一样,inject既可以是数组也可以是对象:

    inject: ['foo']
    inject: { foo },
    inject: {
        bar: {
            from: 'foo',
            default: '--'
        }
    }
    

    为了方便处理,Vue也把它规范成了一种格式,就是对象:

    /**
     * Normalize all injections into Object-based format
     */
    function normalizeInject (options: Object, vm: ?Component) {
      const inject = options.inject
      if (!inject) return
      const normalized = options.inject = {}
      if (Array.isArray(inject)) {
        
      } else if (isPlainObject(inject)) {
        
      } else if (process.env.NODE_ENV !== 'production') {
        warn(
          `Invalid value for option "inject": expected an Array or an Object, ` +
          `but got ${toRawType(inject)}.`,
          vm
        )
      }
    }
    

    函数开头首先判断inject属性是否存在,如果没有传就直接返回。

    接着是数组类型的处理

    for (let i = 0; i < inject.length; i++) {
        normalized[inject[i]] = { from: inject[i] }
    }
    

    for循环遍历整个数组,将元素的值作为key,{ from: inject[i] }作为值。所以如果是

    inject: ['foo']
    

    规范化之后:

    inject: { foo: { from: 'foo' } }
    

    然后是处理对象类型的inject:

    for (const key in inject) {
        const val = inject[key]
        normalized[key] = isPlainObject(val)
            ? extend({ from: key }, val)
            : { from: val }
    }
    

    使用for in循环遍历对象,依然使用原来的key作为key,值的话要处理一下,如果原来的值是对象,就用extend函数把{ from: key }和val混合一下,否则就用val作为from的值。

    所以如果我们传入的值是:

    inject: {
        foo,
        bar: {
            from: 'foo',
            default: '--'
        }
    }
    

    处理之后变成:

    inject: {
        foo: { from: 'foo' },
        bar: {
            from: 'foo',
            default: '--'
        }
    }
    

    最后,如果传入的既不是数组也不是对象,在非生产环境下就会发出警告。

    规范化Directives

    /**
     * Normalize raw function directives into object format.
     */
    function normalizeDirectives (options: Object) {
      const dirs = options.directives
      if (dirs) {
        for (const key in dirs) {
          const def = dirs[key]
          if (typeof def === 'function') {
            dirs[key] = { bind: def, update: def }
          }
        }
      }
    }
    

    根据官方文档自定义指令的介绍,我们知道注册指令有函数和对象两种形式:

    directives: {
        'color-swatch': function (el, binding) {
            el.style.backgroundColor = binding.value
        },
        'color-swatch1': {
            bind: function (el, binding) {
                el.style.backgroundColor = binding.value
            }
        }
    }
    

    该方法就是要把第一种规范化成对象。

    看一下方法体,for in 循环遍历所有指令,如果值是函数类型,则把该值作为bind和update属性的值。所以第一种形式规范化之后就变成:

    directives: {
        'color-swatch': {
            bind: function (el, binding) {
                el.style.backgroundColor = binding.value
            },
            update: function (el, binding) {
                el.style.backgroundColor = binding.value
            }
        }
    }
    

    现在我们就了解了三个用于规范化选项的函数的作用了。

    规范化选项之后是这样一段代码:

    // Apply extends and mixins on the child options,
    // but only if it is a raw options object that isn't
    // the result of another mergeOptions call.
    // Only merged options has the _base property.
    if (!child._base) {
        if (child.extends) {
          parent = mergeOptions(parent, child.extends, vm)
        }
        if (child.mixins) {
          for (let i = 0, l = child.mixins.length; i < l; i++) {
            parent = mergeOptions(parent, child.mixins[i], vm)
          }
        }
    }
    

    当child是原始选项对象即没有_base属性时,进行extendsmixins选项的处理。

    如果child.extends存在,就递归调用mergeOptions函数将parent和child.extends进行合并,并将返回值赋给parent。

    如果child.mixins存在,for循环遍历child.mixins,也是递归调用mergeOptions函数将parent和每一项元素进行合并,并更新parent。

    mergeOptions函数我们还没有看完,先继续往下看,这里造成的影响先不追究。之前所做的处理都是前奏,还没有涉及选项合并,是为选项合并所做的铺垫。接下来我们来看选项合并的处理

    相关文章

      网友评论

          本文标题:Vue源码解析二——从一个小例子开始逐步分析

          本文链接:https://www.haomeiwen.com/subject/tdcabqtx.html