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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容