Vue0.11版本源碼閱讀系列三:指令編譯

因為vue指令很多,功能也很多,所以會有很多針對一些情況的特殊處理,這些邏輯如果不是對vue很熟悉的話一時間是看不懂的,所以我們只看一些基本邏輯。

compile

創(chuàng)建vue實例時當傳遞了參數(shù)el或者手動調(diào)用$mount方法即可啟動模板編譯過程,$mount方法里調(diào)用了_compile方法,簡化之后其實調(diào)用的是compile(el, options)(this, el),compile也簡化后代碼如下:

function compile (el, options, partial, transcluded) {
  var nodeLinkFn = compileNode(el, options)
  var childLinkFn = el.hasChildNodes()
      ? compileNodeList(el.childNodes, options)
      : null
  
  function compositeLinkFn (vm, el) 
    var childNodes = _.toArray(el.childNodes)
    if (nodeLinkFn) nodeLinkFn(vm.$parent, el)
    if (childLinkFn) childLinkFn(vm.$parent, childNodes)
  }

  return compositeLinkFn
}

該方法會根據(jù)實例的一些狀態(tài)來判斷處理某個部分使用哪個方法,因為代碼極大的簡化了所以不是很明顯。

先來看compileNode方法,這個方法會會對普通節(jié)點和文本節(jié)點調(diào)用不同的方法,只看普通節(jié)點:

function compileElement (el, options) {
  var linkFn, tag, component
  // 檢查是否是自定義元素,也就是子組件
  if (!el.__vue__) {
    tag = el.tagName.toLowerCase()
    component =
      tag.indexOf('-') > 0 &&
      options.components[tag]
    // 是自定義組件則給元素設(shè)置一個屬性標志
    if (component) {
      el.setAttribute(config.prefix + 'component', tag)
    }
  }
   // 如果是自定義組件或者元素有屬性的話
  if (component || el.hasAttributes()) {
    // 檢查 terminal 指令
    linkFn = checkTerminalDirectives(el, options)
    // 如果不是terminal,建立正常的鏈接功能
    if (!linkFn) {
      var dirs = collectDirectives(el, options)
      linkFn = dirs.length
        ? makeNodeLinkFn(dirs)
        : null
    }
  }
  return linkFn
}

terminal 指令有三種:repeatif、'component

var terminalDirectives = [
  'repeat',
  'if',
  'component'
]
function skip () {}
skip.terminal = true
function checkTerminalDirectives (el, options) {
  // v-pre指令是用來告訴vue跳過編譯該元素及其所有子元素
  if (_.attr(el, 'pre') !== null) {
    return skip
  }
  var value, dirName
  for (var i = 0; i < 3; i++) {
    dirName = terminalDirectives[i]
    if (value = _.attr(el, dirName)) {
      return makeTerminalNodeLinkFn(el, dirName, value, options)
    }
  }
}

順便一提的是attr方法,這個方法其實是專門用來獲取vue的自定義屬性的,也就是v-開頭的屬性,為什么我們在模板里寫的帶v-前綴的屬性在最終渲染的元素上沒有呢,就是因為在這個方法里把它給移除了:

exports.attr = function (node, attr) {
  attr = config.prefix + attr
  var val = node.getAttribute(attr)
  // 如果該自定義指令存在,則把它從元素上刪除
  if (val !== null) {
    node.removeAttribute(attr)
  }
  return val
}

makeTerminalNodeLinkFn方法:

function makeTerminalNodeLinkFn (el, dirName, value, options) {
  // 解析指令值
  var descriptor = dirParser.parse(value)[0]
  // 獲取該指令的指令方法,vue內(nèi)置了很多指令處理方法,都在/src/directives/文件夾下
  var def = options.directives[dirName]
  var fn = function terminalNodeLinkFn (vm, el, host) {
    // 創(chuàng)建并把指令綁定到元素
    vm._bindDir(dirName, el, descriptor, def, host)
  }
  fn.terminal = true
  return fn
}

parse方法用來解析指令的值,請移步文章:vue0.11版本源碼閱讀系列四:詳解指令值解析函數(shù),比如指令值為click: a = a + 1 | uppercase,處理完最后會返回這樣的信息:

{
    arg: 'click',
    expression: 'a = a + 1',
    filters: [
        { name: 'uppercase', args: null }
    ]
}

_bindDir方法會創(chuàng)建一個指令實例:

exports._bindDir = function (name, node, desc, def, host) {
  this._directives.push(
    new Directive(name, node, this, desc, def, host)
  )
}

所以linkFn以及nodeLinkFn就是這個_bindDir的包裝函數(shù)。

對于非terminal指令,調(diào)用的是collectDirectives方法,這個方法會遍歷元素的所有屬性attributes,如果是v-前綴的vue指令會被定義為下列格式的對象:

{
    name: dirName,// 去除了v-前綴的指令名
    descriptors: dirParser.parse(attr.value),// 指令值解析后的數(shù)據(jù)
    def: options.directives[dirName],// 該指令對應(yīng)的處理方法
    transcluded: transcluded
}

vue指令的屬性如果存在動態(tài)綁定,也會進行處理,在該版本vue里的動態(tài)綁定是使用雙大括號插值的,和2.x的使用v-bind不一樣。

如:<div class="{{error}}"></div>,所以會通過正則來匹配判斷是否存在動態(tài)綁定,最終返回下列格式的數(shù)據(jù):

{
    def: options.directives.attr,
    _link: allOneTime// 是否所有屬性都是一次性差值
    ? function (vm, el) {// 一次性的話后續(xù)不需要更新
        el.setAttribute(name, vm.$interpolate(value))
    }
    : function (vm, el) {// 非一次性的話如果依賴的響應(yīng)數(shù)據(jù)變化了也需要改變
        var value = textParser.tokensToExp(tokens, vm)
        var desc = dirParser.parse(name + ':' + value)[0]
        vm._bindDir('attr', el, desc, def)
    }
}

collectDirectives方法最終會返回一個上面對象組成的數(shù)組,然后調(diào)用makeNodeLinkFn為每個指令創(chuàng)建一個綁定函數(shù):

function makeNodeLinkFn (directives) {
  return function nodeLinkFn (vm, el, host) {
    var i = directives.length
    var dir, j, k, target
    while (i--) {
      dir = directives[i]
      if (dir._link) {
        dir._link(vm, el)
      } else {// v-前綴的指令
        k = dir.descriptors.length
        for (j = 0; j < k; j++) {
          vm._bindDir(dir.name, el,
            dir.descriptors[j], dir.def, host)
        }
      }
    }
  }
}

總結(jié)一下compileNode的作用就是遍歷元素上的屬性,分別給其創(chuàng)建一個指令綁定函數(shù),這個指令函數(shù)后續(xù)調(diào)用時會創(chuàng)建一個Directive實例,這個類后續(xù)再看。

如果該元素存在子元素的話會調(diào)用compileNodeList方法,子元素又有子元素的話又會繼續(xù)調(diào)用,其實就是遞歸所有子元素調(diào)用compileNode方法。

compile方法最后返回了compositeLinkFn方法,這個方法被立即執(zhí)行了,這個方法里調(diào)用了剛才生成的nodeLinkFnchildLinkFn方法,執(zhí)行結(jié)果就是會把所有的元素及子元素的指令進行綁定,也就是給元素上的某個屬性或者說指令都創(chuàng)建了一個Directive實例。

Directive

指令這個類主要做的事是把DOM和數(shù)據(jù)綁定起來,實例化的時候會調(diào)用指令的bind方法,同時會實例化一個Watcher實例,后續(xù)數(shù)據(jù)更新的時候會調(diào)用指令的update方法。

function Directive (name, el, vm, descriptor, def, host) {
  this.name = name
  this.el = el
  this.vm = vm
  this.raw = descriptor.raw
  this.expression = descriptor.expression
  this.arg = descriptor.arg
  this.filters = _.resolveFilters(vm, descriptor.filters)
  this._host = host
  this._locked = false
  this._bound = false
  this._bind(def)
}

構(gòu)造函數(shù)定義一些屬性以及調(diào)用了_bind方法,resolveFilters方法會把過濾器以gettersetter分別收集到一個數(shù)組里,便于后續(xù)循環(huán)調(diào)用:

exports.resolveFilters = function (vm, filters, target) {
  var res = target || {}
  filters.forEach(function (f) {
    var def = vm.$options.filters[f.name]
    if (!def) return
    var args = f.args
    var reader, writer
    if (typeof def === 'function') {
      reader = def
    } else {
      reader = def.read
      writer = def.write
    }
    if (reader) {
      if (!res.read) res.read = []
      res.read.push(function (value) {
        return args
          ? reader.apply(vm, [value].concat(args))
          : reader.call(vm, value)
      })
    }
    if (writer) {
      if (!res.write) res.write = []
      res.write.push(function (value, oldVal) {
        return args
          ? writer.apply(vm, [value, oldVal].concat(args))
          : writer.call(vm, value, oldVal)
      })
    }
  })
  return res
}

_bind方法:

p._bind = function (def) {
  if (typeof def === 'function') {
    this.update = def
  } else {// 這個版本的vue指令有這幾個鉤子方法:bind、update、unbind
    _.extend(this, def)
  }
  this._watcherExp = this.expression
  // 如果該指令存在bind方法,此時進行調(diào)用
  if (this.bind) {
    this.bind()
  }
  if (this._watcherExp && this.update){
    var dir = this
    var update = this._update = function (val, oldVal) {
        dir.update(val, oldVal)
    }
    // 使用原始表達式作為標識符,因為過濾器會讓同一個arg變成不同的觀察者
    var watcher = this.vm._watchers[this.raw]
    if (!watcher) {
      // 該表達式未創(chuàng)建過watcher,則實例化一個
      watcher = this.vm._watchers[this.raw] = new Watcher(
        this.vm,
        this._watcherExp,
        update,
        {
          filters: this.filters
        }
      )
    } else {// 存在則把更新函數(shù)添加進入
      watcher.addCb(update)
    }
    this._watcher = watcher
    if (this._initValue != null) {// 帶初始值的情況,見于v-model的情況
      watcher.set(this._initValue)
    } else if (this.update) {// 其他的會調(diào)用update方法,所以bind方法調(diào)用后緊接著會調(diào)用update方法
      this.update(watcher.value)
    }
  }
  this._bound = true
}

到這里可以知道實例化Directive的時候會調(diào)用指令的bind鉤子函數(shù),一般是做一些初始化工作,然后會對該指令初始化一個Watcher實例,這個實例會用來做依賴收集,最后非v-model的情況會立即調(diào)用指令的update方法,watcher實例化的時候會計算表達式的值,所以此時得到的value就是最新的。

Watcher

Watcher實例用來解析表達式和收集依賴項,并在表達式的值變化時觸發(fā)回調(diào)更新。第一篇里提到的$watch方法也是使用該類實現(xiàn)的。

function Watcher (vm, expression, cb, options) {
  this.vm = vm
  this.expression = expression
  this.cbs = [cb]
  this.id = ++uid
  this.active = true
  options = options || {}
  this.deep = !!options.deep
  this.user = !!options.user
  this.deps = Object.create(null)
  if (options.filters) {
    this.readFilters = options.filters.read
    this.writeFilters = options.filters.write
  }
  // 將表達式解析為getter/setter
  var res = expParser.parse(expression, options.twoWay)
  this.getter = res.get
  this.setter = res.set
  this.value = this.get()
}

構(gòu)造函數(shù)的邏輯很簡單,聲明一些變量、將表達式解析為gettersetter的類型,比如:a.b解析后的get為:

function anonymous(o){
    return o.a.b
}

set為:

function set(obj, val){
    Path.se(obj, path, val)
}

簡單的說就是生成兩個函數(shù),一個用來給實例this設(shè)置值,一個用來獲取實例this上的值,具體的解析邏輯比較復(fù)雜,有機會再詳細分析或者可自行閱讀源碼:/src/parsers/path.js。

最后調(diào)用了get方法:

p.get = function () {
  this.beforeGet()
  var vm = this.vm
  var value
  // 調(diào)用取值方法
  value = this.getter.call(vm, vm)
  // “觸摸”每個屬性,以便它們都作為依賴項進行跟蹤,以便進行深入觀察
  if (this.deep) {
    traverse(value)
  }
  // 應(yīng)用過濾器函數(shù)
  value = _.applyFilters(value, this.readFilters, vm)
  this.afterGet()
  return value
}

在調(diào)用取值函數(shù)前調(diào)用了beforeGet方法:

p.beforeGet = function () {
  Observer.target = this
  this.newDeps = {}
}

到這里我們知道了第二篇vue0.11版本源碼閱讀系列二:數(shù)據(jù)觀察里提到的Observer.target是什么了,邏輯也可以串起來,vue在數(shù)據(jù)觀察時對每個屬性進行了攔截,在getter里會判斷Observer.target是否存在,存在的話會把Observer.target對應(yīng)的watcher實例收集到該屬性的依賴對象實例dep里:

if (Observer.target) {
    Observer.target.addDep(dep)
}

beforeGet后緊接著就調(diào)用了該表達式的取值函數(shù),會觸發(fā)對應(yīng)屬性的getter。

addDep方法:

p.addDep = function (dep) {
  var id = dep.id
  if (!this.newDeps[id]) {
    this.newDeps[id] = dep
    if (!this.deps[id]) {
      this.deps[id] = dep
      // 收集該watcher實例到該屬性的依賴對象里
      dep.addSub(this)
    }
  }
}

afterGet用來做一些復(fù)位和清理工作:

p.afterGet = function () {
  Observer.target = null
  for (var id in this.deps) {
    if (!this.newDeps[id]) {// 刪除本次依賴收集時已經(jīng)不依賴的屬性
      this.deps[id].removeSub(this)
    }
  }
  this.deps = this.newDeps
}

traverse方法用來深度遍歷所有嵌套屬性,這樣已轉(zhuǎn)換的所有嵌套屬性都會作為依賴項進行收集,也就是該表達式的watcher會被該屬性及其所有后代屬性的dep對象收集,這樣某個后代屬性的值變了也會觸發(fā)更新:

function traverse (obj) {
  var key, val, i
  for (key in obj) {
    val = obj[key]// 就是這里,獲取一下該屬性即可觸發(fā)getter,此時Observer.target屬性還是該watcher
    if (_.isArray(val)) {
      i = val.length
      while (i--) traverse(val[i])
    } else if (_.isObject(val)) {
      traverse(val)
    }
  }
}

如果某個屬性的值后續(xù)發(fā)生變化根據(jù)第一篇我們知道在屬性setter函數(shù)里會調(diào)用訂閱者的update方法,這個訂閱者就是Watcher實例,看一下這個方法:

p.update = function () {
  if (!config.async || config.debug) {
    this.run()
  } else {
    batcher.push(this)
  }
}

正常情況下是走else分支的,batcher會以異步和批量的方式來更新,但是最后也調(diào)用了run方法,所以先來看一下這個方法:

p.run = function () {
  if (this.active) {
    // 獲取表達式的最新值
    var value = this.get()
    if (
      value !== this.value ||
      Array.isArray(value) ||
      this.deep
    ) {
      var oldValue = this.value
      this.value = value
      var cbs = this.cbs
      for (var i = 0, l = cbs.length; i < l; i++) {
        cbs[i](value, oldValue)
        // 某個回調(diào)刪除了其他的回調(diào)的情況,目前屬實不了解
        var removed = l - cbs.length
        if (removed) {
          i -= removed
          l -= removed
        }
      }
    }
  }
}

邏輯很簡單,遍歷調(diào)用該watcher實例所有指令的update方法,指令會完成頁面的更新工作。

批量更新請移步文章vue0.11版本源碼閱讀系列五:批量更新是怎么做的。

到這里模板編譯的過程就結(jié)束了,接下來以一個指令的視角來看一下具體過程。

以if指令來看一下全過程

模板如下:

<div id="app">
    <div v-if="show">我出來了</div>
</div>

JavaScript代碼如下:

window.vm = new Vue({
    el: '#app',
    data: {
        show: false
    }
})

在控制臺輸入window.vm.show = true這個div就會顯示出來。

根據(jù)上面的分析,我們知道對于v-if這個指令最終肯定調(diào)用了_bindDir方法:

image-20210111194832587

進入Directive后在_bind里調(diào)用了if指令的bind方法,該方法簡化后如下:

{
    bind: function () {
        var el = this.el
        if (!el.__vue__) {
            // 創(chuàng)建了兩個注釋元素把我們要顯示隱藏的div給替換了,效果見下圖
            this.start = document.createComment('v-if-start')
            this.end = document.createComment('v-if-end')
            _.replace(el, this.end)
            _.before(this.start, this.end)
        }
    }
}
image-20210111200014709

可以看到bind方法做的事情是用兩個注釋元素把這個元素從頁面上給替換了。 bind方法之后就是給這個指令創(chuàng)建watcher

image-20210111201925172

接下來在watcher里給Observer.target賦值及進行取值操作,觸發(fā)了show屬性的getter

image-20210112093317760

依賴收集完后會調(diào)用if指令的update方法,看一下這個方法:

{
    update: function (value) {
        if (value) {
            if (!this.unlink) {
                var frag = templateParser.clone(this.template)
                this.compile(frag)
            }
        } else {
            this.teardown()
        }
    }
}

因為我們的初始值為false,所以走else分支調(diào)用了teardown方法:

{
    teardown: function () {
        if (!this.unlink) return
        transition.blockRemove(this.start, this.end, this.vm)
        this.unlink()
        this.unlink = null
    }
}

本次unlink其實并沒有值,所以就直接返回了,但是假如有值的話,teardown方法首先使用會使用transition類來移除元素,然后解除該指令的綁定。

現(xiàn)在讓我們在控制臺輸入window.vm.show = true,這會觸發(fā)showsetter

image-20210112101502685

然后會調(diào)用show屬性的depnotify方法,dep的訂閱者里目前就只有if指令的watcher,所以會調(diào)用watcherupdate方法,最終調(diào)用到if指令的update方法,此時的值為true,所以會走到if分支里,unlink也沒有值,所以會調(diào)用compile方法:

image-20210112102302256
{
    compile: function (frag) {
        var vm = this.vm
        transition.blockAppend(frag, this.end, vm)
    }
}

忽略了部分編譯過程,可以看到使用看transition類來顯示元素。這個過渡類我們將在vue0.11版本源碼閱讀系列六:過渡原理里詳細了解。

總結(jié)

可以發(fā)現(xiàn)在這個早期版本里沒有所謂的虛擬DOM,沒有diff算法,模板編譯就是遍歷元素及元素上的屬性,給每個屬性創(chuàng)建一個指令實例,對同樣的指令表達式創(chuàng)建一個watcher實例,指令實例提供update方法給watcher,watcher會觸發(fā)表達式里所有被觀察屬性的getter,然后watcher就會被這些屬性的依賴收集實例dep收集起來,當屬性值變化時會觸發(fā)setter,在setter里會遍歷dep里所有的watcher,調(diào)用更新方法,也就是指令實例提供的update方法,也就是最終指令對象的update方法完成頁面更新。

當然,這部分的代碼還是比較復(fù)雜的,遠沒有本文所說的這么簡單,各種遞歸調(diào)用,各種函數(shù)重載,反復(fù)調(diào)用,讓人看的云里霧里,有興趣的還請自行閱讀。

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

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

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