成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

Vue源碼詳細(xì)解析:transclude,compile,link,依賴,批處理...一網(wǎng)打盡,全解

yy736044583 / 833人閱讀

摘要:先說遍歷,很簡單,如下行左右代碼就足夠遍歷一個(gè)對象了遇到普通數(shù)據(jù)屬性,直接處理,遇到對象,遍歷屬性之后遞歸進(jìn)去處理屬性,遇到數(shù)組,遞歸進(jìn)去處理數(shù)組元素。這樣改進(jìn)之后我就不需要對數(shù)組元素進(jìn)行響應(yīng)式處理,只是遇到數(shù)組的時(shí)候把數(shù)組的方法變異即可。

用了Vue很久了,最近決定系統(tǒng)性的看看Vue的源碼,相信看源碼的同學(xué)不在少數(shù),但是看的時(shí)候卻發(fā)現(xiàn)挺有難度,Vue雖然足夠精簡,但是怎么說現(xiàn)在也有10k行的代碼量了,深入進(jìn)去逐行查看的時(shí)候感覺內(nèi)容龐雜并且搞不懂代碼的目的,同時(shí)網(wǎng)上的深入去仔細(xì)闡述Vue的compile/link/ expression parse/依賴訂閱和收集/batcher的文章卻不多,我自己讀源碼時(shí),深感在這些環(huán)節(jié)可供參考的資料稀缺。網(wǎng)上較多的文章都在講getter/setter、Mutation Observer和LRU緩存。所以我趁著寒假詳細(xì)的閱讀了Vue構(gòu)建整個(gè)響應(yīng)式過程的代碼,基本包括數(shù)據(jù)observe到模板解析、transclude、compile、link、指令的bind、update、dom批處理更新、數(shù)組diff等等環(huán)節(jié),并用這篇文章詳細(xì)的介紹出來,希望能幫到想學(xué)習(xí)Vue源碼或者想?yún)⑴cVue維護(hù)、提交pr的同學(xué)。

Vue源碼詳解系列文章和配套的我整理的Vue源碼注釋版已經(jīng)在git上開項(xiàng):Vue源碼注釋版及詳解,歡迎大家在git上查看,并配合注釋版源碼使用。訂閱文章更新請watch。
注釋版源碼主要注釋了本文中涉及的部分,依然有很多沒有涉及,我個(gè)人精力有限,歡迎大家提pr,如果您喜歡,多謝您的star~

本文介紹的源碼版本是當(dāng)前(17年2月23日)1.x版本的最新版v1.0.26,2.x版本的源碼我先學(xué)學(xué)虛擬dom之后再進(jìn)行。

源碼整體概覽

Vue源碼構(gòu)造實(shí)例的過程就一行this._init(options),用你的參數(shù)對象去執(zhí)行init初始化函數(shù)。init函數(shù)中先進(jìn)行了大量的參數(shù)初始化操作this.xxx = blabla,然后剩下這么幾行代碼(后文所有的英文注釋是尤雨溪所寫,中文是我添加的,英文注釋極其精確、簡潔,請勿忽略

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

基本就是觸發(fā)init鉤子,初始化一些狀態(tài),初始化event,然后觸發(fā)created鉤子,最后掛載到具體的元素上面去。_initState()方法中包含了數(shù)據(jù)的初始化操作,也就是讓數(shù)據(jù)變成響應(yīng)式的,讓Vue能夠監(jiān)聽到數(shù)據(jù)的變動(dòng)。而this.$mount()方法則承載了絕大部分的代碼量,負(fù)責(zé)模板的嵌入、編譯、link、指令和watcher的生成、批處理的執(zhí)行等等。

從數(shù)據(jù)的響應(yīng)化說起

嗯,是的,雖然這個(gè)observe數(shù)據(jù)的部分已經(jīng)被很多文章說爛了,但是我并不只是講getter/setter,這里應(yīng)該會(huì)有你沒看過的部分,比如Vue是如何解決"getter/setter無法監(jiān)聽屬性的添加和刪除"的。

熟悉Vue的同學(xué)都了解Vue的響應(yīng)式特性,對于data對象的幾乎任何更改我們都能夠監(jiān)聽到。這是MVVM的基礎(chǔ),基本思路就是遍歷每一個(gè)屬性,然后使用Object.defineProperty將這個(gè)屬性設(shè)置為響應(yīng)式的(即我能監(jiān)聽到他的改動(dòng))。

先說遍歷,很簡單,如下10行左右代碼就足夠遍歷一個(gè)對象了:

function touch (obj) {
    if (typeof obj === "object")
      if (Array.isArray(obj)) {
        for (let i = 0,l = obj.length; i < l; i++) {
          touch(obj[i])
        }
      } else {
        let keys = Object.keys(obj)
        for (let key of keys) touch(obj[key])
      }
    console.log(obj)
  }

遇到普通數(shù)據(jù)屬性,直接處理,遇到對象,遍歷屬性之后遞歸進(jìn)去處理屬性,遇到數(shù)組,遞歸進(jìn)去處理數(shù)組元素(console.log)。

遍歷完就到處理了,也就是Object.defineProperty部分了,對于一個(gè)對象,我們可以用這個(gè)來改寫它屬性的getter/setter,這樣,當(dāng)你改屬性的值我就有辦法監(jiān)聽到。但是對于數(shù)組就有問題了。

你也許想到可以遍歷當(dāng)前存在的下標(biāo),然后執(zhí)行Object.defineProperty。這種處理方法先不說性能問題,很多時(shí)候我們操作數(shù)組是采用push、pop、splice、unshift等方法來操作的,光是push你就沒辦法監(jiān)聽,更不要說pop后你設(shè)置的getter/setter就直接沒了。

所以,Vue的方法是,改寫數(shù)組的push、pop等8個(gè)方法,讓他們在執(zhí)行之后通知我數(shù)組更新了(這種方法帶來的后果就是你不能直接修改數(shù)組的長度或者通過下標(biāo)去修改數(shù)組。參見官網(wǎng))。這樣改進(jìn)之后我就不需要對數(shù)組元素進(jìn)行響應(yīng)式處理,只是遇到數(shù)組的時(shí)候把數(shù)組的方法變異即可。于是在用戶使用數(shù)組的push、pop等方法會(huì)改變數(shù)組本身的方法時(shí),可以監(jiān)聽到數(shù)組變動(dòng)。

此外,當(dāng)數(shù)組內(nèi)部元素是對象時(shí),設(shè)置getter/setter是可以監(jiān)聽對象的,所以對于數(shù)組元素還是要遍歷一下的。如果不是對象,比如a[0]是字符串、數(shù)字?那就沒辦法了,但是vue為數(shù)組提供了$set和$remove,方便我們可以通過下標(biāo)去響應(yīng)式的改動(dòng)數(shù)組元素,這里后文再說。

我們先說說怎么“變異”數(shù)組的push等方法,并且找出數(shù)組元素中的對象,讓對象響應(yīng)式。我們結(jié)合我的注釋版源碼來看一下。

Vue.prototype._initData = function () {
    // 初始化數(shù)據(jù),其實(shí)一方面把data的內(nèi)容代理到vm實(shí)例上,
    // 另一方面改造data,變成reactive的
    // 即get時(shí)觸發(fā)依賴收集(將訂閱者加入Dep實(shí)例的subs數(shù)組中),set時(shí)notify訂閱者
    var dataFn = this.$options.data
    var data = this._data = dataFn ? dataFn() : {}
   
    var props = this._props
    // proxy data on instance
    var keys = Object.keys(data)
    var i, key
    i = keys.length
    while (i--) {
        key = keys[i]
        // 將data屬性的內(nèi)容代理到vm上面去,使得vm訪問指定屬性即可拿到_data內(nèi)的同名屬性
        // 實(shí)現(xiàn)vm.prop === vm._data.prop,
        // 這樣當(dāng)前vm的后代實(shí)例就能直接通過原型鏈查找到父代的屬性
        // 比如v-for指令會(huì)為數(shù)組的每一個(gè)元素創(chuàng)建一個(gè)scope,這個(gè)scope就繼承自vm或上級數(shù)組元素的scope,
        // 這樣就可以在v-for的作用域中訪問父級的數(shù)據(jù)
        this._proxy(key)
    }
    // observe data
    //重點(diǎn)來了
    observe(data, this)
  }

(注釋里的依賴收集、Dep什么的大家看不懂沒關(guān)系,請?zhí)^,后面會(huì)細(xì)說)

代碼中間做了_proxy操作,注釋里我已經(jīng)寫明原因。_proxy操作也很簡單想了解的話大家自己查看源碼即可。

代理完了之后就開始o(jì)bserve這個(gè)data:

export function observe (value, vm) {
  if (!value || typeof value !== "object") {
    // 保證只有對象會(huì)進(jìn)入到這個(gè)函數(shù)
    return
  }
  var ob
  if (
    //如果這個(gè)數(shù)據(jù)身上已經(jīng)有ob實(shí)例了,那observe過了,就直接返回那個(gè)ob實(shí)例
    hasOwn(value, "__ob__") &&
    value.__ob__ instanceof Observer
  ) {
    ob = value.__ob__
  } else if (
    shouldConvert &&
    (isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 是對象(包括數(shù)組)的話就深入進(jìn)去遍歷屬性,observe每個(gè)屬性
    ob = new Observer(value)
  }
  if (ob && vm) {
    // 把vm加入到ob的vms數(shù)組當(dāng)中,因?yàn)橛械臅r(shí)候我們會(huì)對數(shù)據(jù)手動(dòng)執(zhí)行$set/$delete操作,
    // 那么就要提示vm實(shí)例這個(gè)行為的發(fā)生(讓vm代理這個(gè)新$set的數(shù)據(jù),和更新界面)
    ob.addVm(vm)
  }
  return ob
}

代碼的執(zhí)行過程一般都是進(jìn)入到那個(gè)else if里,執(zhí)行new Observer(value),至于shouldConvert和后續(xù)的幾個(gè)判斷則是為了防止value不是單純的對象而是Regexp或者函數(shù)之類的,或者是vm實(shí)例再或者是不可擴(kuò)展的,shouldConvert則是某些特殊情況下為false,它的解釋參見源碼里尤雨溪的注釋。

那好,現(xiàn)在就進(jìn)入到拿當(dāng)前的data對象去new Observer(value),現(xiàn)在你可能會(huì)疑惑,遞歸遍歷的過程不是應(yīng)該是純命令式的、面向過程的嗎?怎么代碼跑著跑著跑出來一句new一個(gè)對象了,嗯先不用管,我們先理清代碼執(zhí)行過程,先帶著這個(gè)疑問。同時(shí),我們注意到代碼最后return了ob,結(jié)合代碼,我們可以理解為如果return的是undifned,那么說明傳進(jìn)來的value不是對象,反之return除了一個(gè)ob,則說明這個(gè)value是對象或數(shù)組,他可以添加或刪除屬性,這一點(diǎn)我們先記著,這個(gè)東西后面有用。

我們先看看Observer構(gòu)造函數(shù):

/**
 * Observer class that are attached to each observed
 * object. Once attached, the observer converts target
 * object"s property keys into getter/setters that
 * collect dependencies and dispatches updates.
 *
 * @param {Array|Object} value
 * @constructor
 */

function Observer (value) {
  this.value = value
  this.dep = new Dep()
  def(value, "__ob__", this) //value的__ob__屬性指向這個(gè)Ob實(shí)例
  if (isArray(value)) {
    var augment = hasProto
      ? protoAugment
      : copyAugment
    augment(value, arrayMethods, arrayKeys)
    this.observeArray(value)
  } else {
    // 如果是對象則使用walk遍歷每個(gè)屬性
    this.walk(value)
  }
}
observe一個(gè)數(shù)組

上述代碼中,如果遇到數(shù)組data中的數(shù)組實(shí)例增加了一些“變異”的push、pop等方法,這些方法會(huì)在數(shù)組原本的push、pop方法執(zhí)行后發(fā)出消息,表明發(fā)生了改動(dòng)。聽起來這好像可以用繼承的方式實(shí)現(xiàn): 繼承數(shù)組然后在這個(gè)子類的原型上附加上變異的方法。

但是你需要知道的是在es5及更低版本的js里,無法完美繼承數(shù)組,主要原因是Array.call(this)時(shí),Array根本不是像一般的構(gòu)造函數(shù)那樣對你傳進(jìn)去this進(jìn)行改造,而是直接返回一個(gè)新的數(shù)組。所以一般的繼承方式就沒法實(shí)現(xiàn)了。參見這篇文章,所以出現(xiàn)了新建一個(gè)iframe,然后直接拿那個(gè)iframe里的數(shù)組的原型進(jìn)行修改,添加自定義方法,諸如此類的hack方法,在此按下不表。

但是如果當(dāng)前瀏覽器里存在__proto__這個(gè)非標(biāo)準(zhǔn)屬性的話(大部分都有),那又可以有方法繼承,就是創(chuàng)建一個(gè)繼承自Array.prototype的Object: Object.create(Array.prototype),在這個(gè)繼承了數(shù)組原生方法的對象上添加方法或者覆蓋原有方法,然后創(chuàng)建一個(gè)數(shù)組,把這個(gè)數(shù)組的__proto__指向這個(gè)對象,這樣這個(gè)數(shù)組的響應(yīng)式的length屬性又得以保留,又獲得了新的方法,而且無侵入,不會(huì)改變本來的數(shù)組原型。

Vue就是基于這個(gè)思想,先判斷__proto__能不能用(hasProto),如果能用,則把那個(gè)一個(gè)繼承自Array.prototype的并且添加了變異方法的Object (arrayMethods),設(shè)置為當(dāng)前數(shù)組的__proto__,完成改造,如果__proto__不能用,那么就只能遍歷arrayMethods就一個(gè)個(gè)的把變異方法def到數(shù)組實(shí)例上面去,這種方法效率不高,所以優(yōu)先使用改造__proto__的那個(gè)方法。

源碼里后面那句this.observeArray非常簡單,for遍歷傳進(jìn)去的value,然后對每個(gè)元素執(zhí)行observe,處理之前說的數(shù)組的元素為對象或者數(shù)組的情況。好了,對于數(shù)組的討論先打住,至于數(shù)組的變異方法怎么通知我他進(jìn)行了更改之類的我們不說了,我們先說清楚對象的情況,對象說清楚了,再去看源碼就一目了然了。

observe 對象

對于對象,上面的代碼執(zhí)行this.walk(value),他“游走”對象的每個(gè)屬性,對屬性和屬性值執(zhí)行defineReactive函數(shù)。

function Dep () {
  this.id = uid++
  this.subs = []
}

Dep.prototype.depend = function () {
  Dep.target.addDep(this)
}

Dep.prototype.notify = function () {
  // stablize the subscriber list first
  var subs = toArray(this.subs)
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}

function defineReactive (obj, key, val) {
  // 生成一個(gè)新的Dep實(shí)例,這個(gè)實(shí)例會(huì)被閉包到getter和setter中
  var dep = new Dep()

  var property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  var getter = property && property.get
  var setter = property && property.set
  // 對屬性的值繼續(xù)執(zhí)行observe,如果屬性的值是一個(gè)對象,那么則又遞歸進(jìn)去對他的屬性執(zhí)行defineReactive
  // 保證遍歷到所有層次的屬性
  var childOb = observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val
      // 只有在有Dep.target時(shí)才說明是Vue內(nèi)部依賴收集過程觸發(fā)的getter
      // 那么這個(gè)時(shí)候就需要執(zhí)行dep.depend(),將watcher(Dep.target的實(shí)際值)添加到dep的subs數(shù)組中
      // 對于其他時(shí)候,比如dom事件回調(diào)函數(shù)中訪問這個(gè)變量導(dǎo)致觸發(fā)的getter并不需要執(zhí)行依賴收集,直接返回value即可
      if (Dep.target) {
        dep.depend()
        if (childOb) {
         //如果value是對象,那就讓生成的Observer實(shí)例當(dāng)中的dep也收集依賴
          childOb.dep.depend()
        }
        if (isArray(value)) {
          for (var e, i = 0, l = value.length; i < l; i++) {
            e = value[i]
            //如果數(shù)組元素也是對象,那么他們observe過程也生成了ob實(shí)例,那么就讓ob的dep也收集依賴
            e && e.__ob__ && e.__ob__.dep.depend()
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val
      if (newVal === value) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // observe這個(gè)新set的值
      childOb = observe(newVal)
      // 通知訂閱我這個(gè)dep的watcher們:我更新了
      dep.notify()
    }
  })
}

我們來說說這個(gè)Dep,Dep類的定義極其簡單,一個(gè)id,一個(gè)數(shù)組,他就是一個(gè)很基本的發(fā)布者-觀察者模式的實(shí)現(xiàn),作為一個(gè)發(fā)布者,他的subs屬性用來存放了訂閱他的觀察者,也就是后面我們會(huì)說到的watcher。

defineProperty是用來將對象的屬性轉(zhuǎn)化為響應(yīng)式的getter/setter的,defineProperty函數(shù)執(zhí)行過程中新建了一個(gè)Dep,閉包在了屬性的getter和setter中,因此每個(gè)屬性都有一個(gè)唯一的Dep與其對應(yīng),我們暫且可以把屬性和他對應(yīng)的Dep理解為一體的。

Dep其實(shí)是dependence依賴的縮寫,我之前一直沒能理解依賴、依賴收集是什么,其實(shí)對于我們的一個(gè)模板{{a+b}},我們會(huì)說他的依賴有a和b,其實(shí)就是依賴了data的a和b屬性,更精確的說是依賴了a屬性中閉包的dep實(shí)例和b屬性中閉包的那個(gè)dep實(shí)例。

詳細(xì)來說:我們的這個(gè){{a+b}}在dom里最終會(huì)被"a+b"表達(dá)式的真實(shí)值所取代,所以存在一個(gè)求出這個(gè)“a+b”的表達(dá)式的過程,求值的過程就會(huì)自然的分別觸發(fā)a和b的getter,而在getter中,我們看到執(zhí)行了dep.depend(),這個(gè)函數(shù)實(shí)際上回做dep.addSub(Dep.target),即在dep的訂閱者數(shù)組中存放了Dep.target,讓Dep.target訂閱dep。

那Dep.target是什么?他就是我們后面介紹的Watcher實(shí)例,為什么要放在Dep.target里呢?是因?yàn)間etter函數(shù)并不能傳參,dep可以通過閉包的形式放進(jìn)去,那watcher可就不行了,watcher內(nèi)部存放了a+b這個(gè)表達(dá)式,也是由watcher計(jì)算a+b的值,在計(jì)算前他會(huì)把自己放在一個(gè)公開的地方(Dep.target),然后計(jì)算a+b,從而觸發(fā)表達(dá)式中所有遇到的依賴的getter,這些getter執(zhí)行過程中會(huì)把Dep.target加到自己的訂閱列表中。等整個(gè)表達(dá)式計(jì)算成功,Dep.target又恢復(fù)為null.這樣就成功的讓watcher分發(fā)到了對應(yīng)的依賴的訂閱者列表中,訂閱到了自己的所有依賴。

我們可以看到這是極其精妙的一筆!在一個(gè)表達(dá)式的求值過程中隱式的完成依賴訂閱。

上面完成的是訂閱的過程,而上面setter代碼里的dep.notify就負(fù)責(zé)完成數(shù)據(jù)變動(dòng)時(shí)通知訂閱者的功能。而且數(shù)據(jù)變化時(shí),后文會(huì)說明只有依賴他的那些dom會(huì)精確更新,不會(huì)出現(xiàn)一些介紹mvvm的文章里雖然實(shí)現(xiàn)了訂閱更新但是重新計(jì)算整個(gè)視圖的情況。

于是一整個(gè)對象訂閱、notify的過程就結(jié)束了。

Observer類?

現(xiàn)在我們明白了Dep的作用和收集訂閱依賴的過程,但是對于watcher是什么肯定還是云里霧里的,先別急。我們先解決之前的疑問:為什么命令式的監(jiān)聽過程中出現(xiàn)了個(gè)new Observer()?而且構(gòu)造函數(shù)第一行就創(chuàng)建了一個(gè)dep(這個(gè)dep不是defineReactive里的那個(gè)閉包dep,注意區(qū)分),在defineReactive函數(shù)的getter中還執(zhí)行了childOb.dep.depend(),去完成了這個(gè)dep的watcher添加?

我們考慮一下這樣的情況,比如我的data:{a:{b:true}},這個(gè)時(shí)候,如果頁面有dom上有個(gè)指令:class="a",而我想響應(yīng)式的刪除data.a的b屬性,此時(shí)我就沒有辦法了,因?yàn)閐efineReactive中的getter/setter都不會(huì)執(zhí)行(他們甚至還會(huì)在delete a.b時(shí)被清空),閉包里的那個(gè)dep就無法通知對應(yīng)的watcher。

這就是getter和setter存在的缺陷:只能監(jiān)聽到屬性的更改,不能監(jiān)聽到屬性的刪除與添加。

Vue的解決辦法是提供了響應(yīng)式的api: vm.$set/vm.$delete/ Vue.set/ Vue.delete /數(shù)組的$set/數(shù)組的$remove。

具體方法是為所有的對象和數(shù)組(只有這倆哥們才可能delete和新建屬性),也創(chuàng)建一個(gè)dep,也完成收集依賴的過程。我們回到源碼defineReactive再看一遍,在執(zhí)行defineReactive(data,"a",{b:true})時(shí),他首先創(chuàng)造了那個(gè)閉包在getter/setter中的dep,然后var childOb = observe(val),val是{b:true},那就會(huì)為這個(gè)對象new Observer(val),并放在val.__ob__上,而這個(gè)ob實(shí)例上存放了一個(gè)Dep實(shí)例。現(xiàn)在我們看到,有兩個(gè)Dep實(shí)例,一個(gè)是閉包里的dep,一個(gè)是為{b:true}創(chuàng)建的ob上的這個(gè)dep。而:class="a"生成的watcher的求值過程中會(huì)觸發(fā)到a的getter,那就會(huì)執(zhí)行:

dep.depend()
if (childOb) {
    //如果value是對象,那就讓生成的Observer實(shí)例當(dāng)中的dep也收集依賴
    childOb.dep.depend()
}

這一步,:class="a"的watcher既會(huì)訂閱閉包dep,也會(huì)訂閱ob的dep。

當(dāng)我們執(zhí)行Vue.delete(this.a,"b"),內(nèi)部會(huì)執(zhí)行del函數(shù),他會(huì)找到要?jiǎng)h除屬性的那個(gè)對象,也是{b:true},它的__ob__屬性存放了ob,現(xiàn)在先刪除屬性,然后執(zhí)行ob.dep.notify,通知所有依賴這個(gè)對象的watcher重新計(jì)算,這個(gè)時(shí)候?qū)傩砸呀?jīng)刪除了,重新計(jì)算的值(為空)就會(huì)刷新到頁面上,完成dom響應(yīng)式更新。參見此處源碼。

不僅對于屬性的刪除這樣,屬性的的添加也是類似的,都是為了彌補(bǔ)getter和setter存在的缺陷,都會(huì)找到這個(gè)dep執(zhí)行notify。不過data的頂級屬性略有不同,涉及到digest,此處不表。

同時(shí)我們再回到之前遍歷數(shù)組的代碼,我們數(shù)組的響應(yīng)化代碼甚至都里沒有g(shù)etter/setter,他連那個(gè)閉包的dep都沒有,代碼只是變異了一下push/pop方法。他有的只是那個(gè)childOb上的dep,所以數(shù)組的響應(yīng)式過程都是notify的這個(gè)dep,不管是數(shù)組的變異方法),還是數(shù)組的$set/$remove里我們都會(huì)看到是在這個(gè)dep上觸發(fā)notify,通知訂閱了整個(gè)數(shù)組的watcher進(jìn)行更新。所以你知道這個(gè)dep的重要性了把。當(dāng)然這也就有問題了,我一個(gè)watcher訂閱整個(gè)數(shù)組,當(dāng)數(shù)組的元素有改動(dòng)我就會(huì)收到消息,但我不知道變動(dòng)的是哪個(gè),難道我要用整個(gè)數(shù)組重新構(gòu)造一下dom?所以這就是數(shù)組diff算法的使用場景了。

至于Observer,這個(gè)額外的實(shí)例上存放了一個(gè)dep,這個(gè)dep配合Observer的addVm、removeVm、vms等屬性來一起搞定data的頂級屬性的新增或者刪除,至于為什么不直接在數(shù)據(jù)上存放dep,而是搞個(gè)Observer,并把dep定義在上面,我覺得是Observer的那些方法和vms等屬性,并不是所有的dep都應(yīng)該具有的,作為dep的實(shí)例屬性是不應(yīng)該的,所以就抽象了個(gè)Observer這么個(gè)東東吧,順便把walk、convert之類的函數(shù)變成方法掛在Observer上了,抽象出個(gè)專門用來observe的類而已,這部分純屬個(gè)人臆測。

_compile

介紹完響應(yīng)式的部分,算是開了個(gè)頭了,后面的內(nèi)容很多,但是層層遞進(jìn),最終完成響應(yīng)式精確訂閱和批處理更新的整個(gè)過程,過程比較流程,內(nèi)容耦合度也高,所以我們先來給后文的概覽,介紹一下大體過程。

我們最開始的代碼里提到了Vue處理完數(shù)據(jù)和event之后就到了$mount,而$mount就是在this._compile后觸發(fā)編譯完成的鉤子而已,所以核心就是Vue.prototype._compile。

_compile包含了Vue構(gòu)建的三個(gè)階段,transclude,compile,link。而link階段其實(shí)是放在linkAndCapture里執(zhí)行的,這里又包含了watcher的生成,指令的bind、update等操作。

我先簡單講講什么是指令,雖然Vue文檔里說的指令是v-if,v-for等這種HTML的attribute,其實(shí)在Vue內(nèi)部,只要是被Vue處理的dom上的東西都是指令,比如dom內(nèi)容里的{{a}},最終會(huì)轉(zhuǎn)換成一個(gè)v-text的指令和一個(gè)textNode,而一個(gè)子組件也會(huì)生成指令,還有slot,或者是你自己在元素上寫的attribute比如hello={{you}}也會(huì)被編譯為一個(gè)v-bind指令。我們看到,基本只要是涉及dom的(不是響應(yīng)式的也包含在內(nèi),只要是vue提供的功能),不管是dom標(biāo)簽,還是dom屬性、內(nèi)容,都會(huì)被處理為指令。所以不要有指令就是attribute的慣性思維。

回過頭來,_compile部分大致分為如下幾個(gè)部分

transclude

transclude的意思是內(nèi)嵌,這個(gè)步驟會(huì)把你template里給出的模板轉(zhuǎn)換成一段dom,然后抽取出你el選項(xiàng)指定的dom里的內(nèi)容(即子元素,因?yàn)槟0謇锟赡苡衧lot),把這段模板dom嵌入到el里面去,當(dāng)然,如果replace為true,那他就是直接替換el,而不是內(nèi)嵌。我們大概明白transclude這個(gè)名字的意義了,但其實(shí)更關(guān)鍵的是把template轉(zhuǎn)換為dom的過程(如`

{{a}}

`字符串轉(zhuǎn)為真正的段落元素),這里為后面的編譯準(zhǔn)備好了dom。

compile

compile的的過程具體就是**遍歷模板解析出模板里的指令**。更精確的說是解析后生成了指令描述對象。
同時(shí),compile函數(shù)是一個(gè)高階函數(shù),他執(zhí)行完成之后的返回值是另一個(gè)函數(shù):link,所以compile函數(shù)的第一個(gè)階段是編譯,返回出去的這個(gè)函數(shù)完成另一個(gè)階段:link。

link

compile階段將指令解析成為指令描述對象(descriptor),閉包在了link函數(shù)里,link函數(shù)會(huì)把descriptor傳入Directive構(gòu)造函數(shù),創(chuàng)建出真正的指令實(shí)例。此外link函數(shù)是作為參數(shù)傳入linkAndCaptrue中的,后者負(fù)責(zé)執(zhí)行l(wèi)ink,同時(shí)取出這些新生成的指令,先按照指令的預(yù)置的優(yōu)先級從高到低排好順序,然后遍歷指令執(zhí)行指令的_bind方法,這個(gè)方法會(huì)為指令創(chuàng)建watcher,并計(jì)算表達(dá)式的值,完成前面描述的依賴收集。并最后執(zhí)行對應(yīng)指令的bind和update方法,使指令生效、界面更新。

此外link函數(shù)最終的返回值是unlink函數(shù),負(fù)責(zé)在vm卸載時(shí)取消對應(yīng)的dom到數(shù)據(jù)的綁定。

是時(shí)候回過頭來看看Vue官網(wǎng)這張經(jīng)典的圖了,以前我剛學(xué)Vue時(shí)也是對于Watcher,Directive之類的概念云里霧里。但是現(xiàn)在大家看這圖是不是很清晰明了?

模板中每個(gè)指令/數(shù)據(jù)綁定都有一個(gè)對應(yīng)的 watcher 對象,在計(jì)算過程中它把屬性記錄為依賴。之后當(dāng)依賴的 setter 被調(diào)用時(shí),會(huì)觸發(fā) watcher 重新計(jì)算 ,也就會(huì)導(dǎo)致它的關(guān)聯(lián)指令更新 DOM。 --Vue官網(wǎng)

上代碼:

Vue.prototype._compile = function (el) {
    var options = this.$options

    // transclude and init element
    // transclude can potentially replace original
    // so we need to keep reference; this step also injects
    // the template and caches the original attributes
    // on the container node and replacer node.
    var original = el
    el = transclude(el, options)
    // 在el這個(gè)dom上掛一些參數(shù),并觸發(fā)"beforeCompile"鉤子,為compile做準(zhǔn)備
    this._initElement(el)

    // handle v-pre on root node (#2026)
    // v-pre指令的話就什么都不用做了。
    if (el.nodeType === 1 && getAttr(el, "v-pre") !== null) {
      return
    }

    // root is always compiled per-instance, because
    // container attrs and props can be different every time.
    var contextOptions = this._context && this._context.$options
    var rootLinker = compileRoot(el, options, contextOptions)

    // resolve slot distribution
    // 具體是將各個(gè)slot存儲(chǔ)到vm._slotContents的對應(yīng)屬性里面去,
    // 然后后面的compile階段會(huì)把slot解析為指令然后進(jìn)行處理
    resolveSlots(this, options._content)

    // compile and link the rest
    var contentLinkFn
    var ctor = this.constructor
    // component compilation can be cached
    // as long as it"s not using inline-template
    // 這里是組件的情況才進(jìn)入的,大家先忽略此段代碼
    if (options._linkerCachable) {
      contentLinkFn = ctor.linker
      if (!contentLinkFn) {
        contentLinkFn = ctor.linker = compile(el, options)
      }
    }

    // link phase
    // make sure to link root with prop scope!
    var rootUnlinkFn = rootLinker(this, el, this._scope)
    // compile和link一并做了
    var contentUnlinkFn = contentLinkFn
      ? contentLinkFn(this, el)
      : compile(el, options)(this, el)

    // register composite unlink function
    // to be called during instance destruction
    this._unlinkFn = function () {
      rootUnlinkFn()
      // passing destroying: true to avoid searching and
      // splicing the directives
      contentUnlinkFn(true)
    }

    // finally replace original
    if (options.replace) {
      replace(original, el)
    }

    this._isCompiled = true
    this._callHook("compiled")
  }

尤雨溪的注釋已經(jīng)極盡詳細(xì),上面的代碼很清晰(如果你用過angular,那你會(huì)感覺很熟悉,angular里也是有transclude,compile和link的,雖然實(shí)際差別很大)。我們在具體進(jìn)入各部分代碼前先說說為什么dom的編譯要分成compile和link兩個(gè)phase。

在組件的多個(gè)實(shí)例、v-for數(shù)組等場合,我們會(huì)出現(xiàn)同一個(gè)段模板要綁定不同的數(shù)據(jù)然后分發(fā)到dom里面去的需求。這也是mvvm性能考量的主要場景:大數(shù)據(jù)量的重復(fù)渲染生成。而重復(fù)渲染的模板是一致的,不一致的是他們需要綁定的數(shù)據(jù),因此compile階段找出指令的過程是不用重復(fù)計(jì)算的,只需要link函數(shù)(和里面閉包的指令),而模板生成的dom使用原生的cloneNode方法即可復(fù)制出一份新的dom。現(xiàn)在,復(fù)制出的新dom+ link+具體的數(shù)據(jù)即可完成渲染,所以分離compile、并緩存link使得Vue在渲染時(shí)避免大量重復(fù)的性能消耗。

transclude函數(shù)

這里大家可以考慮一下,我給你一個(gè)空的documentFragment和一段html字符串,讓你把html生成dom放進(jìn)fragment里,你應(yīng)該怎么做?innerHTML?documentFragment可是沒有innerHtml的哦。那先建個(gè)div再innerHTML?那萬一我的html字符串的是tr元素呢?tr并不能直接放進(jìn)div里哦,那直接用outerHTML?沒有parent Node的元素是不能設(shè)置outerHTML的哈(parent是fragment也不行),那我先用正則提取第一個(gè)標(biāo)簽,先createElement這個(gè)標(biāo)簽然后在寫他的innerHTML總可以了吧?并不行,我沒告訴你我給你的這段HTML最外層就一個(gè)元素啊,萬一是個(gè)片段實(shí)例呢(也就是包含多個(gè)頂級元素,如

1

2

),所以我才說給你一個(gè)fragment當(dāng)容器,讓你把dom裝進(jìn)去。

上面這個(gè)例子說明了實(shí)際轉(zhuǎn)換dom過程中,可能遇到的一個(gè)小坑,只是想說明字符串轉(zhuǎn)dom并不是看起來那么一行innerHTML的事。

/**
 * Process an element or a DocumentFragment based on a
 * instance option object. This allows us to transclude
 * a template node/fragment before the instance is created,
 * so the processed fragment can then be cloned and reused
 * in v-for.
 *
 * @param {Element} el
 * @param {Object} options
 * @return {Element|DocumentFragment}
 */

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) {
    options._containerAttrs = extractAttrs(el)
  }
  // for template tags, what we want is its content as
  // a documentFragment (for fragment instances)
  if (isTemplate(el)) {
    el = parseTemplate(el)
  }
  if (options) {
    // 如果當(dāng)前是component,并且沒有模板,只有一個(gè)殼
    // 那么只需要處理內(nèi)容的嵌入
    if (options._asComponent && !options.template) {
      options.template = ""
    }
    if (options.template) {
    //基本都會(huì)進(jìn)入到這里
      options._content = extractContent(el)
      el = transcludeTemplate(el, options)
    }
  }
  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
}

我們看上面的代碼,先options._containerAttrs = extractAttrs(el),這樣就把el元素上的所有attributes抽取出來存放在了選項(xiàng)對象的_containerAttrs屬性上。因?yàn)槲覀兦懊嬲f過,這些屬性是vm實(shí)際掛載的根元素上的,如果vm是一個(gè)組件之類的,那么他們應(yīng)該是在父組件的作用于編譯/link的,所以需要預(yù)先提取出來,因?yàn)槿绻鹯eplace為true,el元素會(huì)被模板元素替換,但是他上面的屬性是會(huì)編譯link后merge到模板元素上面去。
然后進(jìn)入到那個(gè)兩層的if里, extractContent(el),將el的內(nèi)容(子元素和文本節(jié)點(diǎn))抽取出來,因?yàn)槿绻0謇镉衧lot,那么他們要分發(fā)到對應(yīng)的slot里。
然后就到el = transcludeTemplate(el, options)

/**
 * Process the template option.
 * If the replace option is true this will swap the $el.
 *
 * @param {Element} el
 * @param {Object} options
 * @return {Element|DocumentFragment}
 */

function transcludeTemplate (el, options) {
  var template = options.template
  var frag = parseTemplate(template, true)
  if (frag) {
    // 對于非片段實(shí)例情況且replace為true的情況下,frag的第一個(gè)子節(jié)點(diǎn)就是最終el元素的替代者
    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 " +
          ". This will replace  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 {
        // 抽取replacer自帶的屬性,他們將在自身作用域下編譯
        options._replacerAttrs = extractAttrs(replacer)
        // 把el的所有屬性都轉(zhuǎn)移到replace上面去,因?yàn)槲覀兒竺鎸⒉粫?huì)再處理el直至他最后被replacer替換
        mergeAttrs(el, replacer)
        return replacer
      }
    } else {
      el.appendChild(frag)
      return el
    }
  } else {
    process.env.NODE_ENV !== "production" && warn(
      "Invalid template option: " + template
    )
  }
}

首先執(zhí)行解析parseTemplate(template, true),得到一段存放在documentFragment里的真實(shí)dom,然后就判斷是否需要replace。(若replace為false)之后判斷是否是片段實(shí)例,官網(wǎng)已經(jīng)講述哪幾種情況對應(yīng)片段實(shí)例,而代碼里那幾個(gè)判斷就是對應(yīng)的處理。若不是,那就進(jìn)入后續(xù)的情況,我已經(jīng)注釋代碼作用,就不再贅述。我們來說說parseTemplate,因?yàn)関ue支持template選項(xiàng)寫#app這樣的HTML選擇符,也支持直接存放模板字符串、document fragment、dom元素等等,所以針對各種情況作了區(qū)分,如果是一個(gè)已經(jīng)好的dom那幾乎不用處理,否則大部分情況下都是執(zhí)行stringToFragment:

function stringToFragment (templateString, raw) {
  // 緩存機(jī)制
  // try a cache hit first
  var cacheKey = raw
    ? templateString
    : templateString.trim()
  var hit = templateCache.get(cacheKey)
  if (hit) {
    return hit
  }
    //這三個(gè)正則分別是/<([w:-]+)/ 和/&#?w+?;/和/
              
  
最新活動(dòng)
閱讀需要支付1元查看
<