Vue 1.0.28 源碼解析

總體概覽

Vue源碼最終是向外部拋出一個Vue的構造函數,見源碼css

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

在源碼最開始,經過installGlobalAPI方法(見源碼)向Vue構造函數添加全局方法,如Vue.extend、Vue.nextTick、Vue.delete等,主要初始化Vue一些全局使用的方法、變量和配置;html

export default function (Vue){
    Vue.options = {
          .....
    }
    Vue.extend = function (extendOptions){
           ......
    }
    Vue.use = function (plugin){
           ......
    }
    Vue.mixin = function (mixin){
           ......
    }
    Vue.extend = function (extendOptions){
           ......
    }
}

實例化Vue

當使用vue時,最基本使用方式以下:vue

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

此時,會調用構造函數實例化一個vue對象,而在構造函數中只有這句代碼this.init(options);而在init中(源碼),主要進行一些變量的初始化、option重組、各類狀態、事件初始化;以下:node

Vue.prototype._init = function (options) {
    options = options || {}
    this.$el = null
    this.$parent = options.parent
    this.$root = this.$parent
      ? this.$parent.$root
      : this
    this.$children = []
    this.$refs = {}       // child vm references
    this.$els = {}        // element references
    this._watchers = []   // all watchers as an array
    this._directives = [] // all directives

    ...... // 更多見源碼

    options = this.$options = mergeOptions(
      this.constructor.options,
      options,
      this
    )

    // set ref
    this._updateRef()

    // initialize data as empty object.
    // it will be filled up in _initData().
    this._data = {}

    // call init hook
    this._callHook('init')

    // initialize data observation and scope inheritance.
    this._initState()

    // setup event system and option events.
    this._initEvents()

    // call created hook
    this._callHook('created')

    // if `el` option is passed, start compilation.
    if (options.el) {
      this.$mount(options.el)
    }
}

在其中經過mergeOptions方法,將全局this.constructor.options與傳入的options及實例化的對象進行合併;而this.constructor.options則是上面初始化vue時進行配置的,其中主要包括一些全局使用的指令、過濾器,如常用的"v-if"、"v-for"、"v-show"、"currency":git

this.constructor.options = {
        directives: {
          bind: {}, // v-bind
          cloak: {}, // v-cloak
          el: {}, // v-el
          for: {}, // v-for
          html: {}, // v-html
          if: {}, // v-if
          for: {}, // v-for
          text: {}, // v-text
          model: {}, // v-model
          on: {}, // v-on
          show: {} // v-show
        },
        elementDirectives: {
          partial: {}, // <partial></partial> api: https://v1.vuejs.org/api/#partial
          slot: {} // <slot></slot>
        },
        filters: {  // api: https://v1.vuejs.org/api/#Filters
          capitalize: function() {}, // {{ msg | capitalize }}  ‘abc’ => ‘Abc’
          currency: funnction() {},
          debounce: function() {},
          filterBy: function() {},
          json: function() {},
          limitBy: function() {},
          lowercase: function() {},
          orderBy: function() {},
          pluralize: function() {},
          uppercase: function() {}
        }
}

而後,會觸發初始化一些狀態、事件、觸發init、create鉤子;而後隨後,會觸發this.$mount(options.el);進行實例掛載,將dom添加到頁面;而this.$mount()方法則包含了絕大部分頁面渲染的代碼量,包括模板的嵌入、編譯、link、指令和watcher的生成、批處理的執行等等,後續會詳細進行說明;github

_compile函數之transclude

在上面說了下,在Vue.prototype.$mount完成了大部分工做,而在$mount方法裏面,最主要的工做量由this._compile(el)承擔;其主要包括transclude(嵌入)、compileRoot(根節點編譯)、compile(頁面其餘的編譯);而在這兒主要說明transclude方法;express

經過對transclude進行網絡翻譯結果是"嵌入";其主要目的是將頁面中自定義的節點轉化爲真實的html節點;如一個組件<hello></hello>其實際dom爲<div><h1>hello {{message}}</h1></div>源碼; 當咱們使用時<div><hello></hello></div>; 會經過transclude將其轉化爲<div><div><h1>hello {{message}}</h1></div></div>,見源碼註釋;json

那transclude具體幹了什麼呢,咱們先看它的源碼:api

export function transclude (el, options) {
  // extract container attributes to pass them down
  // to compiler, because they need to be compiled in
  // parent scope. we are mutating the options object here
  // assuming the same object will be used for compile
  // right after this.
  if (options) {
    // 把el(虛擬節點,如<hello></hello>)元素上的全部attributes抽取出來存放在了選項對象的_containerAttrs屬性上
    // 使用el.attributes 方法獲取el上面,並使用toArray方法,將類數組轉換爲真實數組
    options._containerAttrs = extractAttrs(el)
  }
  // for template tags, what we want is its content as
  // a documentFragment (for fragment instances)
  // 判斷是否爲 template 標籤
  if (isTemplate(el)) {
    // 獲得一段存放在documentFragment裏的真實dom
    el = parseTemplate(el)
  }
  if (options) {
    if (options._asComponent && !options.template) {
      options.template = '<slot></slot>'
    }
    if (options.template) {
      // 將el的內容(子元素和文本節點)抽取出來
      options._content = extractContent(el)
      // 使用options.template 將虛擬節點轉化爲真實html, <hello></hello> => <div><h1>hello {{ msg }}</h1></div>
      // 但不包括未綁定數據, 則上面轉化爲 => <div><h1>hello</h1></div>
      el = transcludeTemplate(el, options)
    }
  }
  // isFragment: node is a DocumentFragment
  // 使用nodeType 爲 11 進行判斷是非爲文檔片斷
  if (isFragment(el)) {
    // 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))
  }
  return el
}

首先先看以下代碼:數組

if (options) {
    // 把el(虛擬節點,如<hello></hello>)元素上的全部attributes抽取出來存放在了選項對象的_containerAttrs屬性上
    // 使用el.attributes 方法獲取el上面,並使用toArray方法,將類數組轉換爲真實數組
    options._containerAttrs = extractAttrs(el)
  }

而extractAttrs方法以下,其主要根據元素nodeType去判斷是否爲元素節點,若是爲元素節點,且元素有相關屬性,則將屬性值取出以後,再轉爲屬性數組;最後將屬性數組放到options._containerAttrs中,爲何要這麼作呢?由於如今的el可能不是真實的元素,而是諸如<hello class="test"></hello>,在後面編譯過程,須要將其替換爲真實的html節點,因此,它上面的屬性值都會先取出來預存起來,後面合併到真實html根節點的屬性上面;

function extractAttrs (el) {
  // 只查找元素節點及有屬性
  if (el.nodeType === 1 && el.hasAttributes()) {
    // attributes 屬性返回指定節點的屬性集合,即 NamedNodeMap, 類數組
    return toArray(el.attributes)
  }
}

下一句,根據元素nodeName是否爲「template」去判斷是否爲<template></template>元素;若是是,則走parseTemplate(el)方法,並覆蓋當前el對象

if (isTemplate(el)) {
    // 獲得一段存放在documentFragment裏的真實dom
    el = parseTemplate(el)
  }

function isTemplate (el) {
  return el.tagName &&
    el.tagName.toLowerCase() === 'template'
}

parseTemplate則主要是將傳入內容生成一段存放在documentFragment裏的真實dom;進入函數,首先判斷傳入是否已是一個文檔片斷,若是已是,則直接返回;不然,判斷傳入是否爲字符串,若是爲字符串, 先判斷是不是"#test"這種選擇器類型,若是是,經過document.getElementById方法取出元素,若是文檔中有此元素,將經過nodeToFragment方式,將其放入一個新的節點片斷中並賦給frag,最後返回到外面;若是不是選擇器類型字符串,則使用stringToFragment將其生成一個新的節點片斷,並返回;若是傳入非字符串而是節點(不論是什麼節點,能夠是元素節點、文本節點、甚至Comment節點等);則直接經過nodeToFragment生成節點片斷並返回;

export function parseTemplate (template, shouldClone, raw) {
  var node, frag

  // if the template is already a document fragment,
  // do nothing
  // 是否爲文檔片斷, nodetype是否爲11
  // https://developer.mozilla.org/zh-CN/docs/Web/API/DocumentFragment
 // 判斷傳入是否已是一個文檔片斷,若是已是,則直接返回
  if (isFragment(template)) {
    trimNode(template)
    return shouldClone
      ? cloneNode(template)
      : template
  }
  // 判斷傳入是否爲字符串
  if (typeof template === 'string') {
    // id selector
    if (!raw && template.charAt(0) === '#') {
      // id selector can be cached too
      frag = idSelectorCache.get(template)
      if (!frag) {
        node = document.getElementById(template.slice(1))
        if (node) {
          frag = nodeToFragment(node)
          // save selector to cache
          idSelectorCache.put(template, frag)
        }
      }
    } else {
      // normal string template
      frag = stringToFragment(template, raw)
    }
  } else if (template.nodeType) {
    // a direct node
    frag = nodeToFragment(template)
  }

  return frag && shouldClone
    ? cloneNode(frag)
    : frag
}

從上面可見,在parseTemplate裏面最重要的是nodeToFragment和stringToFragment;那麼,它們又是如何將傳入內容轉化爲新的文檔片斷呢?首先看nodeToFragment:

function nodeToFragment (node) {
  // if its a template tag and the browser supports it,
  // its content is already a document fragment. However, iOS Safari has
  // bug when using directly cloned template content with touch
  // events and can cause crashes when the nodes are removed from DOM, so we
  // have to treat template elements as string templates. (#2805)
  /* istanbul ignore if */
  // 是template元素或者documentFragment,使用stringToFragment轉化並保存節點內容
  if (isRealTemplate(node)) {
    return stringToFragment(node.innerHTML)
  }
  // script template
  if (node.tagName === 'SCRIPT') {
    return stringToFragment(node.textContent)
  }
  // normal node, clone it to avoid mutating the original
  var clonedNode = cloneNode(node)
  var frag = document.createDocumentFragment()
  var child
  /* eslint-disable no-cond-assign */
  while (child = clonedNode.firstChild) {
  /* eslint-enable no-cond-assign */
    frag.appendChild(child)
  }
  trimNode(frag)
  return frag
}

其實看源碼,很容易理解,首先判斷傳入內容是否爲template元素或者documentFragment或者script標籤,若是是,都直接走stringToFragment;後面就是先使用document.createDocumentFragment建立一個文檔片斷,而後將節點進行循環appendChild到建立的文檔片斷中,並返回新的片斷;
那麼,stringToFragment呢?這個就相對複雜一點了,以下:

function stringToFragment (templateString, raw) {
  // try a cache hit first
  var cacheKey = raw
    ? templateString
    : templateString.trim() //trim() 方法會從一個字符串的兩端刪除空白字符
  var hit = templateCache.get(cacheKey)
  if (hit) {
    return hit
  }
  // 建立一個文檔片斷
  var frag = document.createDocumentFragment()
  // tagRE: /<([\w:-]+)/
  // 匹配標籤
  // '<test v-if="ok"></test>'.match(/<([\w:-]+)/) => ["<test", "test", index: 0, input: "<test v-if="ok"></test>"]
  var tagMatch = templateString.match(tagRE)
  // entityRE: /&#?\w+?;/
  var entityMatch = entityRE.test(templateString)
  // commentRE: /<!--/ 
  // 匹配註釋
  var commentMatch = commentRE.test(templateString) 

  if (!tagMatch && !entityMatch && !commentMatch) {
    // text only, return a single text node.
    // 若是都沒匹配到,建立一個文本節點添加到文檔片斷
    frag.appendChild(
      document.createTextNode(templateString)
    )
  } else {
    var tag = tagMatch && tagMatch[1]
    // map, 對標籤進行修正;如是td標籤,則返回"<table><tbody><tr>" + templateString +  "</tr></tbody></table>";
    // map['td'] = [3, "<table><tbody><tr>", "</tr></tbody></table>"]
    var wrap = map[tag] || map.efault
    var depth = wrap[0]
    var prefix = wrap[1]
    var suffix = wrap[2]
    var node = document.createElement('div')

    node.innerHTML = prefix + templateString + suffix

    while (depth--) {
      node = node.lastChild
    }

    var child
    document.body.appendChild(node);
    /* eslint-disable no-cond-assign */
    while (child = node.firstChild) {
    /* eslint-enable no-cond-assign */
      frag.appendChild(child)
    }
  }
  if (!raw) {
    // 移除文檔中空文本節點及註釋節點
    trimNode(frag)
  }
  templateCache.put(cacheKey, frag)
  return frag
}

首先去緩存查看是否已經有,若是有,則直接取緩存數據,減小程序運行;然後,經過正則判斷是否爲元素文本,若是不是,則說明爲正常的文字文本,直接建立文本節點,並放入新建的DocumentFragment中再放入緩存中,並返回最終生成的DocumentFragment;若是是節點文本,則首先對文本進行修正;好比若是傳入的是<td></td>則須要在其外層添加tr、tbody、table後才能直接使用appendChild將節點添加到文檔碎片中,而沒法直接添加td元素到div元素中;在最後返回一個DocumentFragment;

以上就是parseTemplate及其裏面nodeToFragment、stringToFragment的具體實現;而後咱們繼續回到transclude;

在transclude後續中,重要就是transcludeTemplate方法,其主要就是經過此函數,根據option.template將自定義標籤轉化爲真實內容的元素節點;如<hello></hello>這個自定義標籤,會根據此標籤裏面真實元素而轉化爲真實的dom結構;

// app.vue
<hello></hello>

// template: 
<div class="hello" _v-0480c730="">
  <h1 _v-0480c730="">hello {{ msg }} welcome here</h1>
  <h3 v-if="show" _v-0480c730="">this is v-if</h3>
</div>

函數首先會經過上述parseTemplate方法將模版數據轉化爲一個臨時的DocumentFragment,而後根據是否將根元素進行替換,即option.replace是否爲true進行對應處理,而若是須要替換,主要進行將替換元素上的屬性值和模版根元素屬性值進行合併,也就是將替換元素上面的屬性合併並添加到根節點上面,若是兩個上面都有此屬性,則進行合併後的做爲最終此屬性值,若是模板根元素上沒有此屬性而自定義元素上有,則將其設置到根元素上,即:

options._replacerAttrs = extractAttrs(replacer)
        mergeAttrs(el, replacer)

因此,綜上,在compile中,el = transclude(el, options)主要是對元素進行處理,將一個簡單的自定義標籤根據它對應的template模板數據和option的一些配置,進行整合處理,最後返回整理後的元素數據;

_compile函數之compileRoot 與 compile

前面,說了下vue在_compile函數中,首先對el元素進行了處理,主要是處理了自定義標籤元素;將自定義標籤轉化爲真實html元素,並對元素屬性和真實html根節點屬性進行合併;

在這,主要說下對元素根節點的的編譯過程,即var rootLinker = compileRoot(el, options, contextOptions),compileRoot會生成一個最終的linker函數;而最後經過執行生成的linker函數,完成全部編譯過程;

而在源碼,能夠看到還有compile這個方法,也是對元素進行編譯,並生成一個最終的linker函數,那這兩個有什麼區別呢?爲何要分開處理呢?

根據個人理解,compileRoot主要對根節點進行編譯,在這兒的根節點不只包括模板中的根節點,也包括自定義的標籤;以下組件<hello></hello>:

// hello.vue

<template>
  <div class="hello">
    <h1>hello {{ msg }} welcome here</h1>
    <h3 v-if="show" >this is v-if</h3>
  </div>
</template>

// app.vue
<hello class="hello1" :class="{'selected': true}" @click.stop="hello"></hello>

經過compileRoot主要處理<hello>節點和<div class="hello"></div>節點;而compile主要處理整個元素及元素下面的子節點;也包括已經經過compileRoot處理過的節點,只是根節點若是已經處理,在compile中就不會再進行處理;

那爲何會分開進行處理呢,由於咱們在前面說過,對於根節點,它也包含了自定義的標籤節點,即上面的<hello></hello>,全部就分開進行了處理;

而在具體說明compileRoot如何處理以前,咱們先要知道一點,在vue中,基本上全部的dom操做都是經過指令(directive)的方式處理的;如dom屬性的操做(修改class、style)、事件的添加、數據的添加、節點的生成等;而基本大部分的指令都是經過寫在元素屬性上面(如v-bind、v-if、v-show、v-for)等;因此在編譯過程當中,主要是對元素的屬性進行提取、根據不一樣的屬性而後生成對應的Derective的實例;而在執行最終編譯生成的linker函數時,也就是對全部生成的指令實例執行bind;並對其添加響應式處理,也就是watcher;

下面,咱們主要說下具體compileRoot裏面的代碼解析:

//  el(虛擬元素,如<hello></hello>)元素上的全部attributes
//  <hello @click.stop="hello" style="color: red" class="hello" :class="{'selected': true}"></hello>
//  ['@click.stop', 'style', 'class', ':class']
var containerAttrs = options._containerAttrs 

// 虛擬元素對應真實html根節點全部attributes
// <div class="hello"> ... </div>
// ['class', '_v-b9ed5d18']
var replacerAttrs = options._replacerAttrs

這兩個主要保存着根元素的屬性列表;包括自定義元素和其對應的模板根元素的屬性;而它們在哪兒去提取的呢?就是咱們前面說的transclude方法裏面,若是忘記了能夠回到對應函數裏面去查看;

// 2. container attributes
if (containerAttrs && contextOptions) {
    contextLinkFn = compileDirectives(containerAttrs, contextOptions)
}
// 3. replacer attributes
if (replacerAttrs) {
    replacerLinkFn = compileDirectives(replacerAttrs, options)
}

compileDirectives主要對傳入的attrs和options,經過正則,對一些屬性指令初始化基礎信息,並生成對應的處理函數並返回到外面,而最終處理的是

this._directives.push(
    new Directive(descriptor, this, node, host, scope, frag)
)

也就是上面說的生成對應的指令實例化對象,並保存在this._directives中;

具體compileDirectives裏面的詳細代碼,就不細說,這裏取出一部分進行說下:

// event handlers
// onRE: /^v-on:|^@/ 是否爲事件相關屬性,如「v-on:click」、"@click"
if (onRE.test(name)) {
    arg = name.replace(onRE, '')
    pushDir('on', publicDirectives.on)
}

這個是主要匹配屬性名是不是v-on:類型的,也就是事件相關的,若是是,則取出對應的事件名,而後將其進行指令參數初始化,生成一個指令描述對象:

/**
    指令描述對象,以v-bind:href.literal="mylink"爲例:
      {
        arg:"href",
        attr:"v-bind:href.literal",
        def:Object,// v-bind指令的定義
        expression:"mylink", // 表達式,若是是插值的話,那主要用到的是下面的interp字段
        filters:undefined
        hasOneTime:undefined
        interp:undefined,// 存放插值token
        modifiers:Object, // literal修飾符的定義
        name:"bind" //指令類型
        raw:"mylink"  //未處理前的原始屬性值
      }

    **/
    dirs.push({
      name: dirName,
      attr: rawName,
      raw: rawValue,
      def: def,
      arg: arg,
      modifiers: modifiers,
      // conversion from interpolation strings with one-time token
      // to expression is differed until directive bind time so that we
      // have access to the actual vm context for one-time bindings.
      expression: parsed && parsed.expression,
      filters: parsed && parsed.filters,
      interp: interpTokens,
      hasOneTime: hasOneTimeToken
    })

生成描述對象數組以後,經過下面函數去初始化指令實例化對象:

function makeNodeLinkFn (directives) {
  return function nodeLinkFn (vm, el, host, scope, frag) {
    // reverse apply because it's sorted low to high
    var i = directives.length
    while (i--) {
      vm._bindDir(directives[i], el, host, scope, frag)
    }
  }
}

Vue.prototype._bindDir = function (descriptor, node, host, scope, frag) {

    this._directives.push(
      new Directive(descriptor, this, node, host, scope, frag)
    )
    // console.log(new Directive(descriptor, this, node, host, scope, frag))
  }

那麼,在生成指令數組以後,在哪進行指令的綁定呢?就是下面這兒,在compileRoot返回的最終函數中:

export function compileRoot (el, options, contextOptions) {
 
    // 指令的生成過程
    ......
 
    return function rootLinkFn (vm, el, scope) {
        // link context scope dirs
        var context = vm._context
        var contextDirs
        if (context && contextLinkFn) {
          contextDirs = linkAndCapture(function () {
            contextLinkFn(context, el, null, scope)
          }, context)
        }
    
        // link self
        var selfDirs = linkAndCapture(function () {
          if (replacerLinkFn) replacerLinkFn(vm, el)
        }, vm)
    
    
        // return the unlink function that tearsdown context
        // container directives.
        return makeUnlinkFn(vm, selfDirs, context, contextDirs)
      }
}


// link函數的執行過程會生成新的Directive實例,push到_directives數組中
// 而這些_directives並無創建對應的watcher,watcher也沒有收集依賴,
// 一切都還處於初始階段,所以capture階段須要找到這些新添加的directive,
// 依次執行_bind,在_bind裏會進行watcher生成,執行指令的bind和update,完成響應式構建
 function linkAndCapture (linker, vm) {
  /* istanbul ignore if */
  if (process.env.NODE_ENV === 'production') {
    // reset directives before every capture in production
    // mode, so that when unlinking we don't need to splice
    // them out (which turns out to be a perf hit).
    // they are kept in development mode because they are
    // useful for Vue's own tests.
    vm._directives = []
  }
  // 先記錄下數組裏原先有多少元素,他們都是已經執行過_bind的,咱們只_bind新添加的directive
  var originalDirCount = vm._directives.length
  // 在生成的linker中,會對元素的屬性進行指令化處理,並保存到_directives中
  linker()
  // slice出新添加的指令們
  var dirs = vm._directives.slice(originalDirCount)
  // 根據 priority 進行排序
  // 對指令進行優先級排序,使得後面指令的bind過程是按優先級從高到低進行的
  sortDirectives(dirs)
  for (var i = 0, l = dirs.length; i < l; i++) {
    dirs[i]._bind()
  }
  return dirs
}

也就是經過這兒dirs[i]._bind()進行綁定;也就是最終compileRoot生成的最終函數中,當執行此函數,首先會執行linkAndCapture, 而這兒會先去執行傳入的函數,也就是contextLinkFn和replacerLinkFn,經過上面兩個方法,生成指令數組後,再執行循環,並進行_bind()處理;

而對於_bind()具體幹了什麼,會在後面詳細進行說明;其實主要經過指令對元素進行初始化處理和對須要雙向綁定的進行綁定處理;

指令-directive

在上面主要談了下vue整個compile編譯過程,其實最主要任務就是提取節點屬性、根據屬性建立成對應的指令directive實例並保存到this.directives數組中,並在執行生成的linker的時候,將this.directives中新的指令進行初始化綁定_bind;那這兒主要談下directive相關的知識;

在前面說過,自定義組件的渲染其實也是經過指令的方式完成的,那這兒就以組件渲染過程來進行說明,以下組件:

// hello.vue
<template>
  <div class="hello">
    <h1>hello, welcome here</h1>
  </div>
</template>

// app.vue
<hello @click.stop="hello" style="color: red" class="hello1" :class="{'selected': true}"></hello>

對於自定義組件的整個編譯過程,在前面已經說過了,在這就不說了,主要說下如何經過指令將真正的html添加到對應的文檔中;

首先,new directive其實主要是對指令進行初始化配置,就很少談;

主要說下其中this._bind方法,它是指令初始化後綁定到對應元素的方法;

// remove attribute
  if (
    // 只要不是cloak指令那就從dom的attribute裏移除
    // 是cloak指令可是已經編譯和link完成了的話,那也仍是能夠移除的
    // 如移出":class"、":style"等
    (name !== 'cloak' || this.vm._isCompiled) &&
    this.el && this.el.removeAttribute
  ) {
    var attr = descriptor.attr || ('v-' + name)
    this.el.removeAttribute(attr)
  }

這兒主要移出元素上添加的自定義指令,如v-if、v-show等;因此當咱們使用控制檯去查看dom元素時,實際上是看不到寫在代碼中的自定義指令屬性;可是不包括v-cloak,由於這個在css中須要使用;

// html
<div v-cloak>
  {{ message }}
</div>

// css
[v-cloak] {
  display: none;
}
  // copy def properties
  // 不採用原型鏈繼承,而是直接extend定義對象到this上,來擴展Directive實例
  // 將不一樣指令一些特殊的函數或熟悉合併到實例化的directive裏
  var def = descriptor.def
  if (typeof def === 'function') {
    this.update = def
  } else {
    extend(this, def)
  }

這兒主要說下extend(this, def),descriptor主要是指令的一些描述信息:

指令描述對象,以v-bind:href.literal="mylink"爲例:
      {
        arg:"href",
        attr:"v-bind:href.literal",
        def:Object,// v-bind指令的定義
        expression:"mylink", // 表達式,若是是插值的話,那主要用到的是下面的interp字段
        filters:undefined
        hasOneTime:undefined
        interp:undefined,// 存放插值token
        modifiers:Object, // literal修飾符的定義
        name:"bind" //指令類型
        raw:"mylink"  //未處理前的原始屬性值
      }

而,def其實就是指令對應的配置信息;也就是咱們在寫指令時配置的數據,以下指令:

<template>
  <div class="hello">
    <h1 v-demo="demo">hello {{ msg }} welcome here</h1>
    <!-- <h3 v-if="show" >this is v-if</h3> -->
  </div>
</template>

<script>
export default {
  created() {
    setInterval(()=> {
      this.demo += 1;
    }, 1000)
  },
  data () {
    return {
      msg: 'Hello World!',
      show: false,
      demo: 1
    }
  },
  directives: {
    demo: {
      bind: function() {
        this.el.setAttribute('style', 'color: green');
      },
      update: function(value) {
        if(value % 2) {
          this.el.setAttribute('style', 'color: green');
        } else {
          this.el.setAttribute('style', 'color: red');
        }
      }
    }
  }
}
</script>

它對應的descriptor就是:

descriptor = {
    arg: undefined,
    attr: "v-demo",
    def: {
        bind: function() {}, // 上面定義的bind
        update: function() {} // 上面定義的update
    },
    expression:"demo",
    filters: undefined,
    modifiers: {},
    name: 'demo'
}

接着上面的,使用extend(this, def)就將def中定義的方法或屬性就複製到實例化指令對象上面;好供後面使用;

// initial bind
  if (this.bind) {
    this.bind()
  }

這就是執行上面剛剛保存的bind方法;當執行此方法時,上面就會執行

this.el.setAttribute('style', 'color: green');

將字體顏色改成綠色;

// 下面這些判斷是由於許多指令好比slot component之類的並非響應式的,
  // 他們只須要在bind裏處理好dom的分發和編譯/link便可而後他們的使命就結束了,生成watcher和收集依賴等步驟根本沒有
  // 因此根本不用執行下面的處理
if (this.literal) {

} else if (
    (this.expression || this.modifiers) &&
    (this.update || this.twoWay) &&
    !this._checkStatement()
) {

var watcher = this._watcher = new Watcher(
      this.vm,
      this.expression,
      this._update, // callback
      {
        filters: this.filters,
        twoWay: this.twoWay,
        deep: this.deep,
        preProcess: preProcess,
        postProcess: postProcess,
        scope: this._scope
      }
    )
}

而這兒就是對須要添加雙向綁定的指令添加watcher;對應watcher後面再進行詳細說明; 能夠從上看出,傳入了this._update方法,其實也就是當數據變化時,就會執行this._update方法,而:

var dir = this
if (this.update) {
      // 處理一下本來的update函數,加入lock判斷
      this._update = function (val, oldVal) {
        if (!dir._locked) {
          dir.update(val, oldVal)
        }
      }
} else {
      this._update = function() {}
}

其實也就是執行上面的descriptor.def.update方法,因此當值變化時,會觸發咱們自定義指令時定義的update方法,而發生顏色變化;

這是指令最主要的代碼部分;其餘的以下:

// 獲取指令的參數, 對於一些指令, 指令的元素上可能存在其餘的attr來做爲指令運行的參數
  // 好比v-for指令,那麼元素上的attr: track-by="..." 就是參數
  // 好比組件指令,那麼元素上可能寫了transition-mode="out-in", 諸如此類
this._setupParams();

// 當一個指令須要銷燬時,對其進行銷燬處理;此時,若是定義了unbind方法,也會在此刻調用
this._teardown();
而對於每一個指令的處理原理,能夠看其對應源碼;如v-show源碼:

// src/directives/public/show.js

import { getAttr, inDoc } from '../../util/index'
import { applyTransition } from '../../transition/index'

export default {

  bind () {
    // check else block
    var next = this.el.nextElementSibling
    if (next && getAttr(next, 'v-else') !== null) {
      this.elseEl = next
    }
  },

  update (value) {
    this.apply(this.el, value)
    if (this.elseEl) {
      this.apply(this.elseEl, !value)
    }
  },

  apply (el, value) {
    if (inDoc(el)) {
      applyTransition(el, value ? 1 : -1, toggle, this.vm)
    } else {
      toggle()
    }
    function toggle () {
      el.style.display = value ? '' : 'none'
    }
  }
}

能夠從上面看出在初始化頁面綁定時,主要獲取後面兄弟元素是否使用v-else; 若是使用,將元素保存到this.elseEl中,而當值變化執行update時,主要執行了this.apply;而最終只是執行了下面代碼:

el.style.display = value ? '' : 'none'

從而達到隱藏或者展現元素的效果;

未完待續,後續會持續完善......

相關文章
相關標籤/搜索