Vue 1.0.28 原始碼解析

彭鋒發表於2018-03-07

整體概覽

Vue原始碼最終是向外部丟擲一個Vue的建構函式,見原始碼

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

在原始碼最開始,通過installGlobalAPI方法(見原始碼)向Vue建構函式新增全域性方法,如Vue.extend、Vue.nextTick、Vue.delete等,主要初始化Vue一些全域性使用的方法、變數和配置;

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

例項化Vue

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

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

此時,會呼叫建構函式例項化一個vue物件,而在建構函式中只有這句程式碼this.init(options);而在init中(原始碼),主要進行一些變數的初始化、option重組、各種狀態、事件初始化;如下:

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”:

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的生成、批處理的執行等等,後續會詳細進行說明;

_compile函式之transclude

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

通過對transclude進行網路翻譯結果是”嵌入”;其主要目的是將頁面中自定義的節點轉化為真實的html節點;如一個元件<hello></hello>其實際dom為<div><h1>hello {{message}}</h1></div>原始碼; 當我們使用時<div><hello></hello></div>; 會通過transclude將其轉化為<div><div><h1>hello {{message}}</h1></div></div>,見原始碼註釋;

那transclude具體幹了什麼呢,我們先看它的原始碼:

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`

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

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

相關文章