Vue 源碼分析之二:Vue Class

這段時間折騰了一個vue的日期選擇的組件,爲了達成我一向的使用舒服優先原則,我決定使用directive來實現,可是經過這個實現有一個難點就是我如何把時間選擇的組件插入到dom中,因此問題來了,我是否是又要看Vue的源碼?vue

vue2.0即將到來,改了一大堆,Fragment沒了,因此vue社區中爲數很少的組件又有一批不能在2.0中使用,vue的官方插件也是毀得只剩vuex兼容,因此在我正在折騰個人組件的時候看到這個消息我是崩潰的。。。但沒辦法,仍是得繼續。但願2.0出來以後官方能完善一下文檔,1.0中太多東西根本沒在文檔裏提到,好比Fragment,好比Vue的util方法,這給第三方組件以及插件開發者帶來了無數的瑪法,你只能去看源碼了,費時費力,剛研究透又來個大更新,我真的想哭/(ㄒoㄒ)/~~node

----------迴歸正題--------vuex

Vue Class

vue的核心就是他的Vue class,component到最終其實也就是一個Vue的實例,包含了一些component獨有的屬性而已,咱們來看看這個Class作了什麼:api

function Vue (options) {
  this._init(options)
}

恩,他調用了_init,而在_init裏面就是初始化了一大堆屬性,這些不重要,最重要的是最下面他有這麼一句代碼:app

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

這個el是咱們在調用new Vue({...})時傳入的,即這個vue對象的掛載點,好了,咱們找到辦法去動態得把一個Vue的實例掛載到dom裏面了,因而就有了以下代碼:dom

const vm = new Vue({
    template: '<div>我是天才</div>',
    data: {
        hehe: 'haha'
    }
})
vm.$mount(document.body)

愉快得打開頁面,等等,爲何整個頁面上就剩下我是天才這句很是正確的話呢?哦~原來$mount默認是替換整個整個元素的,呵呵噠this

那麼咱們要如何把節點插入到body裏面呢?這裏有不少辦法,好比你直接調用$mount()不傳任何參數,這個時候他不會執行插入操做,而後你把他編譯過的節點(也就是vm.$el)拿出來手動經過dom操做來進行插入,固然咱們確定不能用這麼low的方法O(∩_∩)O~,繼續擼源碼,很快咱們找到了這麼一個文件:prototype

// instance/api/dom.js
Vue.prototype.$appendTo = function(target, cb, withTransition) {...}
Vue.prototype.$before = function(target, cb, withTransition) {...}

是的,Vue的實例自帶一些dom操做的幫助,那麼咱們隨便選一個用就是了,不細說插件

然而咱們仍是遇到了問題

使用這種方式動態插入的節點會有一個問題,那就是$el並非咱們真正想要的節點,而是一個註釋節點,這是爲啥?仍是看源碼,咱們跟着$mount進去看看他作了什麼:code

Vue.prototype.$mount = function (el) {
    if (this._isCompiled) {
      process.env.NODE_ENV !== 'production' && warn(
        '$mount() should be called only once.'
      )
      return
    }
    el = query(el)
    if (!el) {
      el = document.createElement('div')
    }
    this._compile(el)
    this._initDOMHooks()
    if (inDoc(this.$el)) {
      this._callHook('attached')
      ready.call(this)
    } else {
      this.$once('hook:attached', ready)
    }
    return this
}

顯然咱們的el是沒有的,那麼這裏的el就變成了一個div,而後進行了_compile,再繼續:

// 源碼太長不貼了
// 文件位置:instance/internal/lifecycle.js

這裏面他作了一個el = transclude(el, options),以及this._initElement(el),咱們重點看一下this._initElement(el)

Vue.prototype._initElement = function (el) {
    if (el instanceof DocumentFragment) {
      this._isFragment = true
      this.$el = this._fragmentStart = el.firstChild
      this._fragmentEnd = el.lastChild
      // set persisted text anchors to empty
      if (this._fragmentStart.nodeType === 3) {
        this._fragmentStart.data = this._fragmentEnd.data = ''
      }
      this._fragment = el
    } else {
      this.$el = el
    }
    this.$el.__vue__ = this
    this._callHook('beforeCompile')
}

咱們發現這裏的el以及不是以前咱們可親的div了,那麼他是什麼呢?咱們倒回去看transclude

...
if (options) {
    if (options._asComponent && !options.template) {
      options.template = '<slot></slot>'
    }
    if (options.template) {
      options._content = extractContent(el)
      el = transcludeTemplate(el, options)
    }
}
...

咱們是有template的,因此執行了transcludeTemplate:

function transcludeTemplate (el, options) {
  var template = options.template
  var frag = parseTemplate(template, true)
  if (frag) {
    var replacer = frag.firstChild
    var tag = replacer.tagName && replacer.tagName.toLowerCase()
    if (options.replace) {
      /* istanbul ignore if */
      if (el === document.body) {
        process.env.NODE_ENV !== 'production' && warn(
          'You are mounting an instance with a template to ' +
          '<body>. This will replace <body> entirely. You ' +
          'should probably use `replace: false` here.'
        )
      }
      // there are many cases where the instance must
      // become a fragment instance: basically anything that
      // can create more than 1 root nodes.
      if (
        // multi-children template
        frag.childNodes.length > 1 ||
        // non-element template
        replacer.nodeType !== 1 ||
        // single nested component
        tag === 'component' ||
        resolveAsset(options, 'components', tag) ||
        hasBindAttr(replacer, 'is') ||
        // element directive
        resolveAsset(options, 'elementDirectives', tag) ||
        // for block
        replacer.hasAttribute('v-for') ||
        // if block
        replacer.hasAttribute('v-if')
      ) {
        return frag
      } else {
        options._replacerAttrs = extractAttrs(replacer)
        mergeAttrs(el, replacer)
        return replacer
      }
    } else {
      el.appendChild(frag)
      return el
    }
  } else {
    process.env.NODE_ENV !== 'production' && warn(
      'Invalid template option: ' + template
    )
  }
}

這邊生成了一個Fragment,好吧,咱們最終仍是回到了這裏。。。由於這邊返回的是一個Fragment,因此會執行以下代碼:

if (el instanceof DocumentFragment) {
    // anchors for fragment instance
    // passing in `persist: true` to avoid them being
    // discarded by IE during template cloning
    prepend(createAnchor('v-start', true), el)
    el.appendChild(createAnchor('v-end', true))
}

而後回到剛纔的_initElement裏面,this.$el = this._fragmentStart = el.firstChild,額,好吧。。。我表示無力吐槽

那麼回到咱們剛纔的問題,想要讓$el正確,只須要在new Vue({...})的時候傳入replace: false就好了,可是外面就多包了一層div,怎麼樣都不以爲完美

到這裏咱們基本瞭解了初始化一個Vue對象時的一些方法的執行順序,以及一個組件如何從字符串模板最終到一個節點的過程,講得比較粗糙,建議有興趣的各位仍是自行去看源代碼吧~

相關文章
相關標籤/搜索