從Vue源碼的角度解析面試題[一]

經(jīng)常見到有人問看某某某源碼有沒有用怀吻,從我個(gè)人的經(jīng)歷來說(雖然我的經(jīng)歷也不長)蜈块,我覺得是很有用的蔑穴,而且非常有用忠寻【逶。看一些框架和庫的源碼可以讓我們了解到其中的某些特性是怎么實(shí)現(xiàn)的存和,使我們對這些技術(shù)更加熟悉;另一方面衷旅,看源碼的過程也是個(gè)學(xué)習(xí)的過程捐腿,你可以學(xué)習(xí)整個(gè)項(xiàng)目的架構(gòu),學(xué)習(xí)作者的思路柿顶,學(xué)習(xí)某個(gè)函數(shù)的實(shí)現(xiàn)茄袖,或者代碼風(fēng)格等等,因?yàn)楹芏鄸|西是自己無論如何也想不到的嘁锯,所以我們可以從一些優(yōu)秀的項(xiàng)目的源碼中去學(xué)習(xí)宪祥、去借鑒,然后應(yīng)用到自己的代碼里家乘,這樣就會潛移默化的提升自己的能力蝗羊,我覺得這比知道某些功能是怎么實(shí)現(xiàn)的要更加重要。

其實(shí)我也一直想寫一些 Vue 源碼分析的文章仁锯,但是現(xiàn)在網(wǎng)上分析 Vue 源碼的文章隨便都能找出來百八十篇耀找,實(shí)在是太多了,我也不想再重復(fù)去寫那么多业崖。所以我想了個(gè)辦法野芒,找一些面試題,從源碼的角度來分析双炕,這樣既能多看幾道面試題狞悲,又能鞏固源碼的學(xué)習(xí),豈不是一舉兩得妇斤,所以我打算每周找?guī)讉€(gè) Vue 面試題來分析下摇锋,希望各位小伙伴們看完能有一點(diǎn)收獲。

由于通過面試題分析的話并不會從頭去看源碼趟济,而且我也不會寫的特別細(xì)致乱投,所以本文章適合有基礎(chǔ)的同學(xué)看,否則某些地方可能看不太明白顷编。我這些分析只是輔助戚炫,還是建議大家有時(shí)間的話能完整的看一看源碼,畢竟多看優(yōu)秀的項(xiàng)目才能提升自己的代碼能力媳纬,那廢話不多說双肤,我們開始看題吧施掏。

在使用計(jì)算屬性時(shí),函數(shù)名和 data 數(shù)據(jù)源中的數(shù)據(jù)可以同名嗎茅糜?

答案: 不可以重名七芭,不僅僅是計(jì)算屬性和 data,其他的如 props蔑赘,methods 都不可以重名狸驳,因?yàn)?Vue 會把這些屬性掛載在組件實(shí)例上,直接使用 this 就可以訪問缩赛,如果重名就會導(dǎo)致沖突耙箍。

源碼分析
在組件初始化的時(shí)候會執(zhí)行_init 函數(shù)

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

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

    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      console.log('_isComponent',options)
      // 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 = vmnext
    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')

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

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

這里面執(zhí)行了一個(gè) initState 的方法,這個(gè)方法就是初始化數(shù)據(jù)的關(guān)鍵方法

export function initState(vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe((vm._data = {}), true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

可以看到這里初始化了很多數(shù)據(jù)酥馍,有 props辩昆,methods,data旨袒,computed汁针,watch 這幾個(gè)。對于上面這個(gè)問題砚尽,我們只看 initComputed 這個(gè)方法

function initComputed(vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = (vm._computedWatchers = Object.create(null))
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(`Getter is missing for computed property "${key}".`, vm)
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}

在這個(gè)方法的最底部施无,做了一個(gè)判斷,回去查你定義的 computed 的 key 值在 data 和 props 中有沒有存在尉辑,這個(gè)時(shí)候 props 和 data 都已經(jīng)初始化完成了帆精,且已經(jīng)掛載到了組件實(shí)例上,你的 computed 如果有沖突的話就會報(bào)錯(cuò)了隧魄,其實(shí)這幾個(gè)初始化數(shù)據(jù)的方法內(nèi)部都有做這些檢測卓练。

怎么給 vue 定義全局的方法?

答案:目前一般有兩種做法:一是給直接給 Vue.prototype 上添加购啄,另外一個(gè)是通過 Vue.mixin 注冊襟企。但是為什么 prototype 和 mixin 里面的方法可以在組件訪問到呢?

源碼:由于這里牽扯的比較多狮含,我只說關(guān)鍵點(diǎn)顽悼,可能不是很詳細(xì),希望各位小伙伴們有時(shí)間自己去看一下几迄。
我們都知道 Vue 內(nèi)部很多操作都是通過虛擬節(jié)點(diǎn)進(jìn)行的蔚龙,在初始化時(shí)候會執(zhí)行創(chuàng)建虛擬節(jié)點(diǎn)的操作,這個(gè)就是通過一個(gè)叫 createElement 的函數(shù)進(jìn)行的映胁,就是渲染函數(shù)的第一個(gè)參數(shù)木羹,在 createElement 內(nèi)如果發(fā)現(xiàn)一個(gè)節(jié)點(diǎn)是組件的話,會執(zhí)行 createComponent 函數(shù)

export function createComponent(
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }

  //省略其他邏輯...
  // ......
  const baseCtor = context.$options._base
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  // ......
}

這里是創(chuàng)建了一個(gè)組件的構(gòu)造函數(shù),baseCtor 是 Vue 的構(gòu)造函數(shù)坑填,其實(shí)下面執(zhí)行的就是 Vue.extend()方法抛人,這個(gè)方法是 Vue 構(gòu)造函數(shù)上的一個(gè)靜態(tài)方法,應(yīng)該有不少小伙伴都用過這個(gè)方法脐瑰,我們來看下這個(gè)方法做了什么事情

/**
 * Class inheritance
 */
Vue.extend = function(extendOptions: Object): Function {
  //省略其他邏輯...
  // ......
  const Sub = function VueComponent(options) {
    this._init(options)
  }
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub
  Sub.cid = cid++
  Sub.options = mergeOptions(Super.options, extendOptions)
  // ......
}

創(chuàng)建了一個(gè) Sub 函數(shù)妖枚,并且繼承了將 prototype 指向了Object.create(Super.prototype),是 js 里一個(gè)典型的繼承方法苍在,要知道绝页,最終組件的實(shí)例化是通過這個(gè) Sub 構(gòu)造函數(shù)進(jìn)行的,在組件實(shí)例內(nèi)訪問一個(gè)屬性的時(shí)候忌穿,如果本實(shí)例上沒有的話抒寂,會通過原型鏈向上去查找,這樣我們就可以在組件內(nèi)部訪問到 Vue 的原型掠剑。

那么 mixin 是怎么實(shí)現(xiàn)的呢?其實(shí)上面這段代碼還有一個(gè)是 mergeOptions 的操作郊愧,這個(gè) mergeOptions 函數(shù)做的事情是將兩個(gè) options 合并在一起朴译,這里我就不展開說了,因?yàn)槔锩娴臇|西比較多属铁。這里其實(shí)就是把 Super.options 和我們傳入的 options 合并在一起眠寿,這個(gè) Super 的 options 其實(shí)也就是 Vue 的 options,在我們使用 Vue.mixin 這個(gè)方法的時(shí)候焦蘑,會把我們傳入的 options 添加到 Vue.options 上

export function initMixin(Vue: GlobalAPI) {
  Vue.mixin = function(mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}

這樣 Vue.options 上就會有我們添加到屬性了盯拱,在 extend 的時(shí)候這個(gè)屬性也會擴(kuò)展到組件構(gòu)造函數(shù)的 options 上,

然后在組件初始化的時(shí)候例嘱,會執(zhí)行 init 方法:

Vue.prototype._init = function(options?: Object) {
  //省略其他邏輯
  // ......
  // 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
    )
  }
  // ......
}

里面判斷到是組件時(shí)會執(zhí)行 initInternalComponent 這個(gè)方法

export function initInternalComponent(
  vm: Component,
  options: InternalComponentOptions
) {
  const opts = (vm.$options = Object.create(vm.constructor.options))
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent // 父Vnode,activeInstance
  opts._parentVnode = parentVnode // 占位符Vnode
  opts._parentElm = options._parentElm
  opts._refElm = options._refElm

  const vnodeComponentOptions = parentVnode.componentOptions // componentOptions是createComponent時(shí)候傳入new Vnode()的
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

這里又通過const opts = vm.$options = Object.create(vm.constructor.options)將組件構(gòu)造函數(shù)的 options 賦值給了 vm.$options狡逢,這里的vm.constructor.options就是剛才和 Vue.options 合并后的組件構(gòu)造函數(shù)上的 options,這樣我們就在組件內(nèi)部拿到了 Vue.mixin 定義的方法拼卵。

Vue 中怎么重置 data

答案:使用 Object.assign(this.$data, this.$options.data())即可奢浑。很多人都用過這個(gè)方法,在網(wǎng)上查到的也都是這個(gè)方法腋腮,那么這個(gè)方法的原理是什么雀彼,為什么這樣寫能達(dá)到效果呢?

源碼
這個(gè)方法就是一個(gè)簡單的對象合并的方法即寡,我們都知道this.$options.data()就是在組件內(nèi)部書寫的 data 函數(shù)徊哑,執(zhí)行這個(gè)函數(shù)就會返回一份初始的 data 數(shù)據(jù),那這個(gè)$data 是個(gè)什么呢聪富,它是在什么時(shí)候定義的呢莺丑?其實(shí)第一題里面也說過在初始化的時(shí)候執(zhí)行了一個(gè)叫做 initState 的方法,里面又執(zhí)行了 initData 來初始化數(shù)據(jù):

function initData(vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' &&
      warn(
        'data functions should return an object:\n' +
          'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
        vm
      )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(`Method "${key}" has already been defined as a data property.`, vm)
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' &&
        warn(
          `The data property "${key}" is already declared as a prop. ` +
            `Use prop default value instead.`,
          vm
        )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

我們可以看到這個(gè)方法在最開始執(zhí)行了 data 函數(shù)善涨,然后又把返回值賦值給了 vm._data窒盐,在函數(shù)的最后又執(zhí)行了proxy(vm,_data, key)草则,我們來看下 proxy 方法:

export function proxy(target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter() {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter(val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

這個(gè)方法就是通過Object.defineProperty執(zhí)行了一層代理,這樣我們在組件內(nèi)部訪問一個(gè)屬性時(shí)蟹漓,比如 this.name 其實(shí)訪問的是 this._data.name炕横。到這肯定會有小伙伴有疑問:上面那個(gè)答案是$data,而這里是_data葡粒,這是怎么回事呢份殿?

其實(shí)在 Vue 這個(gè)構(gòu)造函數(shù)初始化的時(shí)候還執(zhí)行了一個(gè)方法

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

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

這里是 Vue 構(gòu)造函數(shù)初始化的過程,我們看下這個(gè) stateMixin 方法嗽交,

export function stateMixin(Vue: Class<Component>) {
  // flow somehow has problems with directly declared definition object
  // when using Object.defineProperty, so we have to procedurally build up
  // the object here.
  const dataDef = {}
  dataDef.get = function() {
    return this._data
  }
  const propsDef = {}
  propsDef.get = function() {
    return this._props
  }
  if (process.env.NODE_ENV !== 'production') {
    dataDef.set = function(newData: Object) {
      warn(
        'Avoid replacing instance root $data. ' +
          'Use nested data properties instead.',
        this
      )
    }
    propsDef.set = function() {
      warn(`$props is readonly.`, this)
    }
  }
  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)
  // 省略其他邏輯...
  //......
}

這里定義了一個(gè) dataDef卿嘲,dataDef 的 get 方法返回了_data,又通過Object.defineProperty將$data指向了dataDef夫壁,這樣我們訪問 $data 的時(shí)候其實(shí)訪問的是_data拾枣,而_data 里保存的就是最終的 data 數(shù)據(jù),所以我們才可以使用Object.assign(this.$data, this.$options.data())來達(dá)到重置數(shù)據(jù)的目的盒让。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末梅肤,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子邑茄,更是在濱河造成了極大的恐慌姨蝴,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肺缕,死亡現(xiàn)場離奇詭異左医,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)同木,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門浮梢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人泉手,你說我怎么就攤上這事黔寇。” “怎么了斩萌?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵缝裤,是天一觀的道長。 經(jīng)常有香客問我颊郎,道長憋飞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任姆吭,我火速辦了婚禮榛做,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己检眯,他們只是感情好厘擂,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著锰瘸,像睡著了一般刽严。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上避凝,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天舞萄,我揣著相機(jī)與錄音,去河邊找鬼管削。 笑死倒脓,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的含思。 我是一名探鬼主播崎弃,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼茸俭!你這毒婦竟也來了吊履?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤调鬓,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后酌伊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體腾窝,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年居砖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了虹脯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡奏候,死狀恐怖循集,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蔗草,我是刑警寧澤咒彤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站咒精,受9級特大地震影響镶柱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜模叙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一歇拆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦故觅、人聲如沸厂庇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽权旷。三九已至,卻和暖如春评也,著一層夾襖步出監(jiān)牢的瞬間炼杖,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工盗迟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留坤邪,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓罚缕,卻偏偏與公主長得像艇纺,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子邮弹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容