Vue源碼解析二——從一個(gè)小例子開始逐步分析

每個(gè)Vue應(yīng)用都是從創(chuàng)建Vue實(shí)例開始的,這里我們就以一個(gè)簡(jiǎn)單的例子為基礎(chǔ),慢慢深究Vue的實(shí)現(xiàn)細(xì)節(jié)。

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

當(dāng)我們重新設(shè)置a屬性時(shí)(vm.a = 2),視圖上顯示的值也會(huì)變成2。這么簡(jiǎn)單的例子大家都知道啦,現(xiàn)在就看看使用Vue構(gòu)造函數(shù)初始化的時(shí)候都發(fā)生了什么。

打開/src/core/instance/index.js文件,看到Vue構(gòu)造函數(shù)的定義如下:

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

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

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

首先是定義了vm,它的值就是this,即當(dāng)前實(shí)例。接著定義了一個(gè)實(shí)例屬性_uid,它是Vue組件的唯一標(biāo)識(shí),每實(shí)例化一個(gè)Vue組件就會(huì)遞增。

接下來(lái)是在非生產(chǎn)環(huán)境下可以測(cè)試性能的一段代碼:

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

省略了中間的代碼。這段代碼的執(zhí)行條件是:非生產(chǎn)環(huán)境,config.performance為true 和 mark都存在的情況下。官方提供了performance的全局API。mark和measure在core/util/perf.js文件中,其實(shí)就是window.performance.mark和window.performance.measure. 組件初始化的性能追蹤就是在代碼的開頭和結(jié)尾分別用mark打上標(biāo)記,然后通過(guò)measure函數(shù)對(duì)兩個(gè)mark進(jìn)行性能計(jì)算。

再看看中間代碼,也就是被性能追蹤的代碼:

// 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')

先是設(shè)置了_isVue實(shí)例屬性,作為一個(gè)標(biāo)志避免Vue實(shí)例被響應(yīng)系統(tǒng)觀測(cè)。

接下來(lái)是合并選項(xiàng)的處理,我們并沒(méi)有使用_isComponent屬性,所以上面的代碼會(huì)走else分支,掛載了實(shí)例屬性$options, 該屬性的生成通過(guò)調(diào)用了mergeOptions方法,接下來(lái)我們看看mergeOptions方法都干了些什么。

mergeOptions 函數(shù)來(lái)自于 core/util/options.js 文件, 該函數(shù)接受三個(gè)參數(shù)。先來(lái)看一下_init函數(shù)中調(diào)用該函數(shù)時(shí)傳遞的參數(shù)分別是什么。

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

后兩個(gè)參數(shù)都好理解,options是我們實(shí)例化時(shí)傳過(guò)來(lái)的參數(shù)

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

vm就是當(dāng)前實(shí)例。

重點(diǎn)看一下第一個(gè)參數(shù),是調(diào)用方法生成的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
}

傳的參數(shù)是vm.constructor,在我們例子中就是Vue構(gòu)造函數(shù),因?yàn)槲覀兪侵苯诱{(diào)用的Vue創(chuàng)建的實(shí)例。那什么時(shí)候不是Vue構(gòu)造函數(shù)呢,在用Vue.extend()去創(chuàng)建子類,再用子類構(gòu)造實(shí)例的時(shí)候,vm.constructor就是子類而不是Vue構(gòu)造函數(shù)了。例如在官方文檔上的例子

// 創(chuàng)建構(gòu)造器
var Profile = Vue.extend({
  template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
  data: function () {
    return {
      firstName: 'Walter',
      lastName: 'White',
      alias: 'Heisenberg'
    }
  }
})
// 創(chuàng)建 Profile 實(shí)例,并掛載到一個(gè)元素上。
new Profile().$mount('#mount-point')

vm.constructor就是Profile。

再看if語(yǔ)句塊,是在Ctor.super為真的情況下執(zhí)行,super是子類才有的屬性,所以在我們的例子中是不執(zhí)行的,直接返回options,即Vue.options, 它的值如下:

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

不記得options是如何形成的可以看一下Vue源碼解析一——骨架梳理?,F(xiàn)在三個(gè)參數(shù)已經(jīng)搞清楚了,就來(lái)看看mergeOptions方法發(fā)生了什么吧。

檢查組件名是否合法

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

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

合并兩個(gè)選項(xiàng)對(duì)象為一個(gè)新的對(duì)象。在實(shí)例化和繼承中使用的核心實(shí)用程序。實(shí)例化就是調(diào)用_init方法的時(shí)候,繼承也就是使用Vue.extend的時(shí)候。現(xiàn)在我們知道了該方法的作用,就來(lái)看一下該方法的具體實(shí)現(xiàn)吧

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

在非生產(chǎn)環(huán)境下,會(huì)去校驗(yàn)組件的名字是否合法,checkComponents函數(shù)就是用來(lái)干這個(gè)的,該函數(shù)也在當(dāng)前文件中,找到該函數(shù):

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

一個(gè)for in循環(huán)遍歷options.components,以子組件的名字為參數(shù)調(diào)用validateComponentName方法,所以該方法才是檢測(cè)組件名是否合法的具體實(shí)現(xiàn)。源碼如下:

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

該方法由兩個(gè)if語(yǔ)句塊組成,要想組件名合法,必須滿足這兩個(gè)if條件:

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

對(duì)于條件一就是要使用符合html5規(guī)范中的有效自定義元素名稱

條件二是使用了兩個(gè)方法來(lái)檢測(cè)的,isBuiltInTag方法用來(lái)檢測(cè)是否是內(nèi)置標(biāo)簽,在shared/util.js文件中定義

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

isBuiltInTag方法是調(diào)用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]
}

該方法最后返回一個(gè)函數(shù),函數(shù)接收一個(gè)參數(shù),如果參數(shù)在map中就返回true,否則返回undefined。map是根據(jù)調(diào)用makeMap方法時(shí)傳入的參數(shù)生成的,按照來(lái)處來(lái)看,也就是

map = { slot: true, component: true }

由此可知slotcomponent 是作為Vue的內(nèi)置標(biāo)簽而存在的,我們的組件命名不能使用它們。

還有一個(gè)方法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)
}

就是檢測(cè)是否是規(guī)定的html標(biāo)簽和svg標(biāo)簽。到此組件名是否合法的檢測(cè)就結(jié)束了。

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

這里是一個(gè)判斷,如果child是一個(gè)function,就取它的options靜態(tài)屬性。什么函數(shù)具有options屬性呢?Vue構(gòu)造函數(shù)和使用Vue.extend()創(chuàng)建的'子類',這就允許我們?cè)谶M(jìn)行選項(xiàng)合并的時(shí)候,去合并一個(gè) Vue 實(shí)例構(gòu)造者的選項(xiàng)了。

規(guī)范化Props

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

這是三個(gè)規(guī)范化選項(xiàng)的函數(shù)調(diào)用,分別是針對(duì)props, inject, directives。為什么會(huì)有規(guī)范化選項(xiàng)這一步呢?因?yàn)槲覀冊(cè)谑褂眠x項(xiàng)的時(shí)候可以有多種不同的用法,比如props, 既可以是字符串?dāng)?shù)組也可以是對(duì)象:

props: ['test1', 'test2']

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

這方便了我們使用,但是Vue要對(duì)選項(xiàng)進(jìn)行處理,多種形式定然增加了復(fù)雜度,所以要處理成一種格式,這就是該函數(shù)的作用。

我們分別來(lái)看具體是怎么規(guī)范化的,首先是函數(shù)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
}

根據(jù)注釋我們知道props最后被規(guī)范成對(duì)象的形式了。先大體看一下函數(shù)的結(jié)構(gòu):

  • 先是判斷props是否存在,如果不存在直接返回
  • if語(yǔ)句處理數(shù)組props
  • else if語(yǔ)句塊處理對(duì)象props
  • 最后如果既不是數(shù)組也不對(duì)象,還不是生成環(huán)境,就發(fā)出類型錯(cuò)誤的警告

數(shù)組類型的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循環(huán)處理每一項(xiàng),如果是字符串,先用camelize函數(shù)轉(zhuǎn)了一下該字符串,然后存儲(chǔ)在了res中,其值是{ type: null } 。camelize函數(shù)定義在shared/util.js中,其作用就是把連字符格式的字符串轉(zhuǎn)成駝峰式的。比如:

test-a // testA

如果不是字符串類型就發(fā)出警告,所以數(shù)組格式的props中元素必須是字符串。

數(shù)組格式的規(guī)范化我們已經(jīng)了解了,如果我們傳的是

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

規(guī)范化之后就變成:

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

再來(lái)看看對(duì)象props是如何規(guī)范化的:

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

我們之前舉例說(shuō)過(guò)props是對(duì)象的話它的屬性值有兩種寫法,一種屬性值直接是類型,還有一種屬性值是對(duì)象。這里的處理是如果是對(duì)象的不做處理,是類型的話就把它作為type的值。所以如果我們傳的是:

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

規(guī)范化之后變成:

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

這樣我們就了解了Vue是如何規(guī)范化Props的了

規(guī)范化inject

inject選項(xiàng)不常使用,我們先來(lái)看看官方文檔的介紹

// 父級(jí)組件提供 'foo'
var Provider = {
  provide: {
    foo: 'bar'
  },
  // ...
}

// 子組件注入 'foo'
var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
  // ...
}

在子組件中并沒(méi)有定義foo屬性卻可以使用,就是因?yàn)槭褂?code>inject注入了這個(gè)屬性,而這個(gè)屬性的值是來(lái)源于父組件。和props一樣,inject既可以是數(shù)組也可以是對(duì)象:

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

為了方便處理,Vue也把它規(guī)范成了一種格式,就是對(duì)象:

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

函數(shù)開頭首先判斷inject屬性是否存在,如果沒(méi)有傳就直接返回。

接著是數(shù)組類型的處理

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

for循環(huán)遍歷整個(gè)數(shù)組,將元素的值作為key,{ from: inject[i] }作為值。所以如果是

inject: ['foo']

規(guī)范化之后:

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

然后是處理對(duì)象類型的inject:

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

使用for in循環(huán)遍歷對(duì)象,依然使用原來(lái)的key作為key,值的話要處理一下,如果原來(lái)的值是對(duì)象,就用extend函數(shù)把{ from: key }和val混合一下,否則就用val作為from的值。

所以如果我們傳入的值是:

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

處理之后變成:

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

最后,如果傳入的既不是數(shù)組也不是對(duì)象,在非生產(chǎn)環(huán)境下就會(huì)發(fā)出警告。

規(guī)范化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 }
      }
    }
  }
}

根據(jù)官方文檔自定義指令的介紹,我們知道注冊(cè)指令有函數(shù)和對(duì)象兩種形式:

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

該方法就是要把第一種規(guī)范化成對(duì)象。

看一下方法體,for in 循環(huán)遍歷所有指令,如果值是函數(shù)類型,則把該值作為bind和update屬性的值。所以第一種形式規(guī)范化之后就變成:

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

現(xiàn)在我們就了解了三個(gè)用于規(guī)范化選項(xiàng)的函數(shù)的作用了。

規(guī)范化選項(xiàng)之后是這樣一段代碼:

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

當(dāng)child是原始選項(xiàng)對(duì)象即沒(méi)有_base屬性時(shí),進(jìn)行extendsmixins選項(xiàng)的處理。

如果child.extends存在,就遞歸調(diào)用mergeOptions函數(shù)將parent和child.extends進(jìn)行合并,并將返回值賦給parent。

如果child.mixins存在,for循環(huán)遍歷child.mixins,也是遞歸調(diào)用mergeOptions函數(shù)將parent和每一項(xiàng)元素進(jìn)行合并,并更新parent。

mergeOptions函數(shù)我們還沒(méi)有看完,先繼續(xù)往下看,這里造成的影響先不追究。之前所做的處理都是前奏,還沒(méi)有涉及選項(xiàng)合并,是為選項(xiàng)合并所做的鋪墊。接下來(lái)我們來(lái)看選項(xiàng)合并的處理

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容