vue

MVVM

model和view層通過中間的vm連接和驅(qū)動。model層數(shù)據(jù)變化會改變視圖美浦,view改變通過事件來修改數(shù)據(jù)项栏。vue參考了MVVM實(shí)現(xiàn)了雙向綁定沼沈,react是MVC,但是vue仍然可以通過ref芽腾、parent等操作dom所以不全是mvvm

vue模板解析

1摊滔、先將代碼轉(zhuǎn)換為AST樹
根據(jù)正則匹配店乐,從第一個字符開始眨八,篩選過的就刪掉繼續(xù)index++向后匹配踪古。
如果匹配開始標(biāo)簽就放入一個stack中券腔,此時如果匹配到結(jié)束標(biāo)簽則出棧對比是否一致纷纫,不一致報(bào)錯
2辱魁、優(yōu)化AST樹
找出靜態(tài)節(jié)點(diǎn)并標(biāo)記诗鸭,之后就不需要diff了
遞歸遍歷ast樹中的節(jié)點(diǎn),如果沒有表達(dá)式强岸、v-if锻弓、v-for等青灼,就標(biāo)記static為true
3杂拨、生成render函數(shù)弹沽、在使用new Function(with() {})包裹
轉(zhuǎn)換成render函數(shù)策橘。編譯結(jié)束亏狰。一定要包裹new Function和with來更改上下文環(huán)境

<div id="app"><p>hello {{name}}</p> hello</div>   ==>
new Function(with(this) {_c("div",{id:app},_c("p",undefined,_v('hello' + _s(name) )),_v('hello'))})

v-if解析出來就是三元表達(dá)式暇唾,v-for解析出來_l((3),..)
4策州、render函數(shù)執(zhí)行后得到的是虛擬dom
ast是需要吧代碼使用正則匹配生成的够挂,然后轉(zhuǎn)換成render孽糖,而虛擬dom則是通過render函數(shù)直接生成一個對象

初始化data中的proxy

將所有的數(shù)據(jù)全部代理到this上

for (let key in data) {
    proxy(vm, '_data', key);
}
function proxy(vm,source,key){
    Object.defineProperty(vm,key,{
        get(){
            return vm[source][key]
        },
        set(newValue){
            vm[source][key] = newValue; 
        }
    })
}

vue的雙向數(shù)據(jù)綁定办悟、響應(yīng)式原理

監(jiān)聽器 Observer 病蛉,用來劫持并監(jiān)聽所有屬性(轉(zhuǎn)變成setter/getter形式)铺然,如果屬性發(fā)生變化,就通知訂閱者
訂閱器 Dep赋铝,用來收集訂閱者柬甥,對監(jiān)聽器 Observer和訂閱者 Watcher進(jìn)行統(tǒng)一管理苛蒲,每一個屬性數(shù)據(jù)都有一個dep記錄保存訂閱他的watcher臂外。
訂閱者 Watcher漏健,可以收到屬性的變化通知并執(zhí)行相應(yīng)的方法蔫浆,從而更新視圖姐叁,每個watcher上都會保存對應(yīng)的dep
解析器 Compile外潜,可以解析每個節(jié)點(diǎn)的相關(guān)指令处窥,對模板數(shù)據(jù)和訂閱器進(jìn)行初始化

image.png

數(shù)據(jù)劫持

利用observe方法遞歸的去劫持谒麦,對外也可以使用這個api。使用defineReactive來劫持?jǐn)?shù)據(jù)

class Observe{
 constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    // 判斷是否為數(shù)組颅悉,如果是數(shù)組則修改__proto__原型剩瓶。會再原來原型和實(shí)例中間增加一層延曙。
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
    //遍歷數(shù)組枝缔,繼續(xù)調(diào)用observe方法愿卸。因?yàn)閿?shù)組中有可能有二維數(shù)組或者對象
      this.observeArray(value)
    } else {
    // 如果是對象則直接綁定響應(yīng)式
      this.walk(value)
    }
  }
}

對象的劫持:不斷的遞歸趴荸,劫持到每一個屬性发钝。在defineReactive中會繼續(xù)遞歸執(zhí)行l(wèi)et childOb = !shallow && observe(val)方法遞歸綁定酝豪,因?yàn)閷ο笾杏锌赡苓€有對象

 walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
    //直接遍歷對象去遞歸攔截get孵淘、set
      defineReactive(obj, keys[i])
    }
  }

數(shù)組的劫持:不劫持下標(biāo),value.proto = arrayMethods,增加一層原型鏈重寫數(shù)組的push余黎、splice等方法來劫持新增的數(shù)據(jù)惧财。在數(shù)組方法中進(jìn)行派發(fā)更新ob.dep.notify()

// 繼續(xù)遍歷數(shù)組,再次執(zhí)行observe來遞歸綁定值乖坠。
 observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }

響應(yīng)式原理數(shù)據(jù)劫持熊泵,首先執(zhí)行observe方法new 一個Observe類顽分,其中會判斷是數(shù)組還是對象卒蘸。
1缸沃、如果數(shù)據(jù)是[1,[2,3],{a:1}]修械,不會去劫持下標(biāo)祠肥。會修改數(shù)組的proto修改原型的方法仇箱。但是其中的[2,3]剂桥,{a:1}并沒有被監(jiān)控权逗,所以繼續(xù)調(diào)用observeArray遞歸調(diào)用斟薇,其中又遞歸調(diào)用了let childOb = !shallow && observe(val)繼續(xù)監(jiān)控
2堪滨、如果數(shù)據(jù)是{a:{b:2},c:3}, 會執(zhí)行walk去遍歷對象執(zhí)行defineReactive攔截key的get、set义矛。其中會去遞歸調(diào)用observe方法繼續(xù)遞歸劫持

依賴收集

渲染watcher的收集:
首次渲染:執(zhí)行完劫持之后凉翻,會走掛載流程會new一個渲染watcher制轰,watcher中會立即執(zhí)行回調(diào)render方法艇挨,方法中會去創(chuàng)建Vnode需要去數(shù)據(jù)中取值缩滨,就會進(jìn)入到屬性的get方法脉漏。會去收集依賴

 Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // 如果有current watcher侧巨,會去收集依賴司忱。Dep.target全局只有一個坦仍。一個時刻只能更新一個watcher繁扎。每次在執(zhí)行watcher時會先pushStack梳玫,等執(zhí)行完后會去popstack
      if (Dep.target) {
        // 收集屬性的依賴提澎,每個屬性獲取后都會有個dep盼忌。這個dep掛在每個屬性上碴犬。
        // 例如直接this.a = xxx修改屬性就可以找到這個屬性上的dep更新watcher
        dep.depend()
        // 如果兒子也是對象或者數(shù)組會去遞歸讓兒子也收集
        if (childOb) { 
          // 這個dep掛在對象或者數(shù)組上。為了給$set或者數(shù)組派發(fā)更新使用偿荷。在 {b:1} 跳纳、[1,2,3]上掛dep
          // 例如新增屬性寺庄,this.$set(obj, b, xxx)或this.arr.push(2)斗塘。
          // a:{b:[1,2,3]}馍盟、a:{b:{c:1}},先收集a的依賴掛在屬性dep上贞岭,因?yàn)閏hildOb又為object瞄桨,需要繼續(xù)收集依賴掛在該對象上
          // 此時如果更新a讲婚,則直接找到屬性上的dep更新筹麸。但是a上如果想新增一個c屬性物赶,則需要使用$set酵紫〗钡兀或者數(shù)組上push一個参歹。
          // 此時是找不到屬性上的dep的犬庇,因?yàn)樵搶傩允切略龅某敉欤瑪?shù)組增加一項(xiàng)需要更新watcher葬荷。所以需要在對象或者數(shù)組的Ob類上掛一個dep方便更新
          childOb.dep.depend()
          // 如果仍然是數(shù)組需要持續(xù)遞歸收集
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },

dep中收集watcher

// dep.js
depend () {
    // Dep.target 是此刻唯一準(zhǔn)備被收集的watcher
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

// watcher.js
addDep (dep: Dep) {
    const id = dep.id
    // 去重闯狱,如果添加過就不需要添加了
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        // dep中保存此watcher
        dep.addSub(this)
      }
    }
  }

派發(fā)更新

在set中派發(fā)更新哄孤,數(shù)組是在劫持的方法中派發(fā)更新瘦陈。會執(zhí)行當(dāng)前所有dep中的watcher的notify方法更新視圖或者數(shù)據(jù)晨逝。

set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // this.a如果直接改了引用,需要重新遞歸劫持屬性趁窃,例如a:{b:1}  this.a = {c:2}
      childOb = !shallow && observe(newVal)
      // 執(zhí)行派發(fā)更新操作
      dep.notify()
    }
  })

dep中派發(fā)更新

notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      // 遍歷執(zhí)行所有watcher的update方法
      subs[i].update()
    }
  }

update () {
    /* istanbul ignore else */
    if (this.lazy) {
      // 如果是computed, 會去執(zhí)行重新取值操作
      this.dirty = true
    } else if (this.sync) {
      // 如果是同步watcher直接run()會去執(zhí)行watcher的get()
      this.run()
    } else {
      // 默認(rèn)watcher都是放入隊(duì)列中異步執(zhí)行的
      queueWatcher(this)
    }
  }

export function queueWatcher (watcher: Watcher) {
  // .......調(diào)用全局的nextTick方法來異步執(zhí)行隊(duì)列

  nextTick(flushSchedulerQueue)
}

watcher都是會異步更新,調(diào)用nexttick去更新刨摩,為了整合多次操作為一次澡刹。提高效率

watch

watch內(nèi)部會調(diào)用$watch創(chuàng)建一個user watcher(設(shè)置user為true)罢浇,等依賴變化執(zhí)行update方法

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    // $watcher最終也會去new 一個watcher,傳入vm實(shí)例和檢測的key(expOrFn)和watcher的回調(diào)(cb)和watcher的設(shè)置(deep等設(shè)置)
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
    // 如果設(shè)置是immediate凌受,則需要立即執(zhí)行一次cb
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

// watcher.js中會去判斷expOrFn 如果是function說明是渲染watcher傳入的回調(diào)胜蛉,
if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // 如果傳入的是字符串誊册,則將取值函數(shù)賦給getter(調(diào)用一次就是取值一次),例如watcher中監(jiān)控的是'a.b.c'則需要從this中一直取到c
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }

依賴收集:會去執(zhí)行watcher的getter方法嘲碱,其實(shí)就是去取值麦锯,此時獲取的值保存起來扶欣。執(zhí)行取值函數(shù)會走屬性的get進(jìn)行依賴收集。
watch初始化時會去取值术陶,為了保存下一次變化時的oldvalue

this.value = this.lazy
      ? undefined
      : this.get()

get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      //如果是user watcher的話梧宫,執(zhí)行的就是取值函數(shù)其實(shí)就是依賴收集過程
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // 如果設(shè)置了deep則需要遍歷獲取子屬性進(jìn)行全部的依賴收集(把子屬性都取值一遍)
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

派發(fā)更新:更新時會執(zhí)行watcher的update方法脓豪,其中如果設(shè)置同步則直接run扫夜,如果沒有默認(rèn)放入隊(duì)列異步更新

update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

run () {
    if (this.active) {
      // run就是重新調(diào)用get執(zhí)行g(shù)etter笤闯,去重新取值颗味,取出來的就是新值
      const value = this.get()
      // 如果是新老值不相同才需要調(diào)用user watcher的回調(diào)
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        // 取老的值并設(shè)置新值
        const oldValue = this.value
        this.value = value
        // 調(diào)用user watcher的回調(diào)
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

computed

computed會設(shè)置lazy為true。并且會執(zhí)行臟檢查晶默,只有當(dāng)這些依賴變化時才會去重新計(jì)算computed的值磺陡,獲取完之后再設(shè)置dirty為false

// 設(shè)置lazy為true表示是computed
const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null)
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    // 如果用戶傳入對象表示自己定義了get函數(shù)則使用用戶的仅政,沒有則直接設(shè)置getter
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    if (!isSSR) {
      // 創(chuàng)建一個computed watcher圆丹, 初始化時其中不會執(zhí)行g(shù)et函數(shù)獲取值辫封。
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

      if (!(key in vm)) {
        // 定義computed倦微,需要去劫持計(jì)算屬性的值進(jìn)行依賴收集。
        defineComputed(vm, key, userDef)
      } else if (process.env.NODE_ENV !== 'production') {
        if (key in vm.$data) {
          warn(`The computed property "${key}" is already defined in data.`, vm)
        } else if (vm.$options.props && key in vm.$options.props) {
          warn(`The computed property "${key}" is already defined as a prop.`, vm)
        }
      }
  }
}

// 定義computed
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
 //  其實(shí)就是重新劫持computed的值拓劝, sharedPropertyDefinition中有定義的get函數(shù)
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

收集依賴: 創(chuàng)建computed時雏逾,需要對computed的變量也進(jìn)行劫持,如果頁面中使用到了這個計(jì)算屬性郑临,則會走下面的createComputedGetter 創(chuàng)建的get方法栖博。之后會去收集依賴。

// 創(chuàng)建劫持computed的get函數(shù)
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // 如果dirty為true才取值厢洞,創(chuàng)建時默認(rèn)第一次是true,會去執(zhí)行g(shù)et方法
      if (watcher.dirty) {
        watcher.evaluate()
      }
      // 如果有target則去收集依賴躺翻。firstName和lastName收集渲染依賴, 計(jì)算屬性上不需要收集渲染watcher丧叽,因?yàn)槿绻撁嬷惺褂玫搅诉@個計(jì)算屬性,計(jì)算屬性是根據(jù)函數(shù)中依賴變化計(jì)算的公你,所以其中任何一個依賴都需要收集一下渲染watcher踊淳,因?yàn)槿魏我粋€變化都有可能導(dǎo)致重新渲染
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

// 取值
evaluate () {
  // 執(zhí)行g(shù)et方法會去取值,例如:return this.firstName + this.lastName,此時也是對依賴firstName和lastName的取值收集依賴的過程省店,那么他們也會將當(dāng)前的computed watcher添加到dep的sub隊(duì)列中嚣崭。取值完置換成false
  this.value = this.get()
  this.dirty = false
}

所以如果計(jì)算屬性中寫了data中其他的值也會使他進(jìn)行收集依賴笨触,浪費(fèi)性能

let vm = new Vue({
    el:'#app',
    data: {
            firstName: 'super',
            lastName: 'kimi',
            kimi: 888
    },
    computed: {
       fullName() {
          // 最后返回沒有kimi但是打印進(jìn)行取值了懦傍,他就會收集computed和渲染watcher
           console.log(this.kimi)
           return `${this.firstName}-${this.lastName}`
       }
    }
 })
// 如果更新了kimi也會讓視圖重新渲染
vm.kimi = 999

派發(fā)更新:如果此時改變了firstName的值,因?yàn)閒irstName之前收集依賴中有依賴他的computed watcher和渲染watcher芦劣,會去執(zhí)行兩個watcher上的update方法

update () {
    // 如果是計(jì)算屬性則設(shè)置dirty為true即可粗俱,之后再去執(zhí)行渲染watcher的update會重新渲染,那就會重新取計(jì)算屬性的值虚吟,到時候就可以取到最新的值了
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

provide寸认、inject

provide是定義在當(dāng)前實(shí)例上,inject會去遍歷$parent找到誰定義了串慰,然后再轉(zhuǎn)成響應(yīng)式掛在當(dāng)前實(shí)例偏塞,只是單向

nextTick

優(yōu)雅降級,先使用promise邦鲫,如果不支持會使用MutationObserver灸叼,不兼容再使用setImmediate,最后降級成setTimeout

slot

普通插槽和作用域插槽的實(shí)現(xiàn)庆捺。它們有一個很大的差別是數(shù)據(jù)作用域古今,普通插槽是在父組件編譯和渲染階段生成 vnodes,所以數(shù)據(jù)的作用域是父組件實(shí)例滔以,子組件渲染的時候直接拿到這些渲染好的 vnodes捉腥。而對于作用域插槽,父組件在編譯和渲染階段并不會直接生成 vnodes你画,而是在父節(jié)點(diǎn) vnode 的 data 中保留一個 scopedSlots 對象抵碟,存儲著不同名稱的插槽以及它們對應(yīng)的渲染函數(shù)桃漾,只有在編譯和渲染子組件階段才會執(zhí)行這個渲染函數(shù)生成 vnodes,由于是在子組件環(huán)境執(zhí)行的立磁,所以對應(yīng)的數(shù)據(jù)作用域是子組件實(shí)例呈队。

Vue.extend

傳入一個vue組件配置,然后創(chuàng)建一個構(gòu)造函數(shù)唱歧,然后進(jìn)行合并配置宪摧,修改指針等操作。生成一個vue的構(gòu)造函數(shù)颅崩,之后進(jìn)行new操作就可以生成一個vue組件實(shí)例几于,然后進(jìn)行vm.$mount可以動態(tài)掛載

Vue.$set

1、對象會重新遞歸添加響應(yīng)式沿后,數(shù)組則會調(diào)用splice方法沿彭,方法已經(jīng)被劫持
2、執(zhí)行ob.dep.notify()尖滚,讓視圖更新

Vue組件化

全局組件:Vue.component內(nèi)部會調(diào)用Vue.extend方法喉刘,將定義掛載到Vue.options.components上。這也說明所有的全局組件最終都會掛載到這個變量上
局部組件:在調(diào)用render時漆弄,也會去調(diào)用Vue.extend方法睦裳,在真正patch時會去new

data.hook = {
    init(vnode){
        let child = vnode.componentInstance = new Ctor({});
        child.$mount(); // 組件的掛載
    }
}

虛擬DOM

用js對象來表示dom節(jié)點(diǎn)。配合diff算法可以提高渲染的效率撼唾。
和ast的區(qū)別:ast是轉(zhuǎn)換語法(js廉邑、html語法轉(zhuǎn)換為ast)兩者很相像

生命周期

組件的渲染生命周期都是先子后父。beforeCreate中拿不到this倒谷。create中可以拿到data蛛蒙,但是沒掛載拿不到$el.
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted

diff算法

1、首先比對標(biāo)簽 <div>...</div> --> <ul></ul>

在diff過程中會先比較標(biāo)簽是否一致渤愁,如果標(biāo)簽不一致用新的標(biāo)簽替換掉老的標(biāo)簽

 // 如果標(biāo)簽不一致說明是兩個不同元素
 if(oldVnode.tag !== vnode.tag){
    oldVnode.el.parentNode.replaceChild(createElm(vnode),oldVnode.el)
 }

如果標(biāo)簽一致牵祟,有可能都是文本節(jié)點(diǎn),那就比較文本的內(nèi)容即可

// 如果標(biāo)簽一致但是不存在則是文本節(jié)點(diǎn)
if(!oldVnode.tag){
    if(oldVnode.text !== vnode.text){
        oldVnode.el.textContent = vnode.text;
    }
}
2抖格、對比屬性<div>...</div> --> <div className=‘a(chǎn)aa’>...</div>

當(dāng)標(biāo)簽相同時诺苹,我們可以復(fù)用老的標(biāo)簽元素,并且進(jìn)行屬性的比對他挎。只需要把新的屬性賦值到老的標(biāo)簽上即可

3筝尾、對比子元素<div><p>a</p></div> -> <div><p>b</p></div>

[1]新老都有孩子需要updateChildren比對
[2]新有老沒有則需要遍歷插入
[3]新沒有老有則需要刪除即可

// 比較孩子節(jié)點(diǎn)
let oldChildren = oldVnode.children || [];
let newChildren = vnode.children || [];
// 新老都有需要比對兒子
if(oldChildren.length > 0 && newChildren.length > 0){
    updateChildren(el, oldChildren, newChildren)
    // 老的有兒子新的沒有清空即可
}else if(oldChildren.length > 0 ){
    el.innerHTML = '';
// 新的有兒子
}else if(newChildren.length > 0){
    for(let i = 0 ; i < newChildren.length ;i++){
        let child = newChildren[i];
        el.appendChild(createElm(child));
    }
}
4、updateChildren 核心

設(shè)置四個index:oldS办桨、oldE筹淫、newS、newE
<1>先比對oldS和newS,通過判斷sameNode()方法比對key和tag等损姜。如果匹配相等則oldS和newS都++饰剥,節(jié)點(diǎn)復(fù)用即可 例如:ABCD -> ABCE

// 優(yōu)化向后追加邏輯
if(isSameVnode(oldStartVnode,newStartVnode)){
    patch(oldStartVnode,newStartVnode); // 遞歸比較兒子
    oldStartVnode = oldChildren[++oldStartIndex];
    newStartVnode = newChildren[++newStartIndex];    
}
image.png

<2>oldS和newS如果不相等再比對oldE和newE,通過判斷sameNode()方法比對key和tag等摧阅。如果匹配相等則oldE和newE都--汰蓉,節(jié)點(diǎn)復(fù)用即可 例如:ABCD -> EBCD

// 優(yōu)化向前追加邏輯
else if(isSameVnode(oldEndVnode,newEndVnode)){ 
    patch(oldEndVnode,newEndVnode); // 遞歸比較孩子 
    oldEndVnode = oldChildren[--oldEndIndex];
    newEndVnode = newChildren[--newEndIndex];
}
image.png

<3>oldE和newE如果不相等再比對oldS和newE,通過判斷sameNode()方法比對key和tag等棒卷。如果匹配相等則oldS++和newE--顾孽,將old節(jié)點(diǎn)插入到最后 例如:ABCD -> BCDA

// 頭移動到尾部 
else if(isSameVnode(oldStartVnode,newEndVnode)){
    patch(oldStartVnode,newEndVnode); // 遞歸處理兒子
    parent.insertBefore(oldStartVnode.el,oldEndVnode.el.nextSibling);
    oldStartVnode = oldChildren[++oldStartIndex];
    newEndVnode = newChildren[--newEndIndex]
}
image.png

<4>oldS和newE如果不相等再比對oldE和newS,通過判斷sameNode()方法比對key和tag等比规。如果匹配相等則oldE--和newS++若厚,將old節(jié)點(diǎn)插入到最前 例如:ABCD -> DABC

// 尾部移動到頭部
else if(isSameVnode(oldEndVnode,newStartVnode)){
    patch(oldEndVnode,newStartVnode);
    parent.insertBefore(oldEndVnode.el,oldStartVnode.el);
    oldEndVnode = oldChildren[--oldEndIndex];
    newStartVnode = newChildren[++newStartIndex]
}
image.png

<5>如果使用index都判斷節(jié)點(diǎn)不相同,則需要建立vnode的key-index map表,然后匹配map表蜒什,如果能匹配上挪到當(dāng)前oldS前面测秸,如果匹配不上則創(chuàng)建新節(jié)點(diǎn)往當(dāng)前oldS前面插入,newS++ 例如:ABCD -> CDME

// 建立key-index的map表
function makeIndexByKey(children) {
    let map = {};
    children.forEach((item, index) => {
        map[item.key] = index
    });
    return map; 
}
let map = makeIndexByKey(oldChildren);

// 在map表中尋找有沒有key匹配的vnode
let moveIndex = map[newStartVnode.key];
if (moveIndex == undefined) { // 老的中沒有將新元素插入
    parent.insertBefore(createElm(newStartVnode), oldStartVnode.el);
} else { // 有的話做移動操作
    let moveVnode = oldChildren[moveIndex]; 
    oldChildren[moveIndex] = undefined;
    parent.insertBefore(moveVnode.el, oldStartVnode.el);
    patch(moveVnode, newStartVnode);
}
newStartVnode = newChildren[++newStartIndex]

圖中第一步比對index都不同灾常,則開始比對key發(fā)現(xiàn)有C相同則把C挪到最前面霎冯,newS++;下來發(fā)現(xiàn)D有相同的把D挪到oldS前面钞瀑,newS++沈撞;接著M找不到則插入oldS前面,newS++仔戈;最后E找不到則插入前面关串,newS++拧廊;


image.png

<6>全部比對完后需要對當(dāng)前index進(jìn)行檢查监徘,因?yàn)橛锌赡苡卸嗷蛘呱俟?jié)點(diǎn)的情況

if (oldStartIdx > oldEndIdx) {
    // oldNode先掃完說明new有多余,需要添加進(jìn)去
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else if (newStartIdx > newEndIdx) {
    // newNode先掃完說明old有多余吧碾,需要刪除掉
    removeVnodes(oldCh, oldStartIdx, oldEndIdx);
}

diff對開頭凰盔、結(jié)尾插入刪除節(jié)點(diǎn)&頭節(jié)點(diǎn)移到尾部&尾節(jié)點(diǎn)移到頭部有很大的優(yōu)化

key:為了高效復(fù)用

image.png

A B C D E
A B F C D E
如果沒有key:首先比對頭和頭,A倦春、B都復(fù)用户敬,比到C和F時,tag一樣key相同(都為undefined)則會復(fù)用睁本,會成下圖情況
image.png

如果有key:比對到C和F時,C和F的key不相同所以跳過尿庐,此時就該比oldE和newE,EDC都相同呢堰,多下來的F直接插入

image.png

如果key使用index抄瑟,遇到表單元素比如帶checkbox的列表,如果狀態(tài)勾選后枉疼,會復(fù)用勾選狀態(tài)產(chǎn)生bug

keep-alive組件

會將組件緩存到this.cache中皮假,放入內(nèi)存中緩存起來鞋拟。

vue-router

1、install方法注冊全局組件惹资,掛載route\router
// 遞歸給每個子組件實(shí)例上都掛載一個_routerRoot 贺纲、_router屬性,以便于每個組件實(shí)例上都可以取到路由實(shí)例
export default function install(Vue) {
    _Vue = Vue;
    Vue.mixin({ // 給所有組件的生命周期都增加beforeCreate方法
        beforeCreate() {
            if (this.$options.router) { // 如果有router屬性說明是根實(shí)例
                this._routerRoot = this; // 將根實(shí)例掛載在_routerRoot屬性上
                this._router = this.$options.router; // 將當(dāng)前router實(shí)例掛載在_router上

                this._router.init(this); // 初始化路由,這里的this指向的是根實(shí)例
            } else { // 父組件渲染后會渲染子組件
                this._routerRoot = this.$parent && this.$parent._routerRoot;
                // 保證所有子組件都擁有_routerRoot 屬性褪测,指向根實(shí)例
                // 保證所有組件都可以通過 this._routerRoot._router 拿到用戶傳遞進(jìn)來的路由實(shí)例對象
            }
        }
    })
}
// 做一層代理猴誊,方便用戶$route和$router取值
Object.defineProperty(Vue.prototype,'$route',{ // 每個實(shí)例都可以獲取到$route屬性
    get(){
        return this._routerRoot._route;
    }
});
Object.defineProperty(Vue.prototype,'$router',{ // 每個實(shí)例都可以獲取router實(shí)例
    get(){
        return this._routerRoot._router;
    }
})
2、路由先生成map表

addRouter方法其實(shí)就是給路由表中插入對應(yīng)的值即可侮措。

export default function createMatcher(routes) {
    // 收集所有的路由路徑, 收集路徑的對應(yīng)渲染關(guān)系
    // pathList = ['/','/about','/about/a','/about/b']
    // pathMap = {'/':'/的記錄','/about':'/about記錄'...}
    let {pathList,pathMap} = createRouteMap(routes);
    
    // 這個方法就是動態(tài)加載路由的方法
    function addRoutes(routes){
        // 將新增的路由追加到pathList和pathMap中
        createRouteMap(routes,pathList,pathMap);
    }   
    function match(){} // 稍后根據(jù)路徑找到對應(yīng)的記錄
    return {
        addRoutes,
        match
    }
}
3稠肘、三種模式,如果是hash監(jiān)聽onHashChange事件萝毛,hash變化會賦值給this.current项阴,并且利用defineReactive方法定義響應(yīng)式對象_route。
window.addEventListener('hashchange', ()=> {
    // 根據(jù)當(dāng)前hash值 過度到對應(yīng)路徑
    this.transitionTo(getHash());
})
// 核心邏輯
transitionTo(location, onComplete) {
    // 去匹配路徑
    let route = this.router.match(location);
    // 相同路徑不必過渡
    if(
        location === route.path && 
        route.matched.length === this.current.matched.length){
        return 
    }
    this.updateRoute(route); // 更新路由即可
    onComplete && onComplete();
}
updateRoute(route){ // 跟新current屬性
    this.current =route;
}
//使用vue的方法defineReactive將_route變?yōu)轫憫?yīng)式并設(shè)置值為this.current 
Vue.util.defineReactive(this,'_route',this._router.history.current);          
4笆包、router-view拿到$route去使用render函數(shù)渲染其中的組件环揽。(如果/about/a會先渲染about再渲染a)
export default {
    functional:true,
    render(h,{parent,data}){
        // 拿到$route其實(shí)就是拿到了_route,其實(shí)也是設(shè)置的this.current庵佣,此時取值也就相當(dāng)于收集依賴歉胶。收集到渲染watcher
        let route = parent.$route;
        let depth = 0;
        data.routerView = true;
        while(parent){ // 根據(jù)matched 渲染對應(yīng)的router-view
            if (parent.$vnode && parent.$vnode.data.routerView){
                depth++;
            }
            parent = parent.$parent;
        }
        let record = route.matched[depth];
        if(!record){
            return h();
        }
        // 讀取路由表中配置的component(此時已經(jīng)轉(zhuǎn)換成render函數(shù)了),執(zhí)行render
        return h(record.component, data);
    }
}

渲染過程:頁面開始渲染后會去取$route巴粪,會去找內(nèi)部_route通今,之前此屬性已經(jīng)變?yōu)轫憫?yīng)式,所以會進(jìn)行收集依賴操作肛根,添加渲染watcher辫塌。
當(dāng)hash改變時,會修改_route屬性派哲,此時進(jìn)行派發(fā)更新臼氨,執(zhí)行渲染watcher update重新渲染,router-view組件會去重新獲取$route屬性渲染芭届。

路由鉤子

①導(dǎo)航被觸發(fā)储矩。
②在失活的組件里調(diào)用 beforeRouteLeave 守衛(wèi)。
③調(diào)用全局的 beforeEach 守衛(wèi)褂乍。
④在重用的組件里調(diào)用 beforeRouteUpdate 守衛(wèi) (2.2+)持隧。
⑤在路由配置里調(diào)用 beforeEnter。
⑥解析異步路由組件逃片。
⑦在被激活的組件里調(diào)用 beforeRouteEnter屡拨。
⑧調(diào)用全局的 beforeResolve 守衛(wèi) (2.5+)。
⑨導(dǎo)航被確認(rèn)。
⑩調(diào)用全局的 afterEach 鉤子洁仗。
?觸發(fā) DOM 更新层皱。
?調(diào)用 beforeRouteEnter 守衛(wèi)中傳給 next 的回調(diào)函數(shù),創(chuàng)建好的組件實(shí)例會作為回調(diào)函數(shù)的參數(shù)傳入

Vuex

1赠潦、創(chuàng)建一個Store類叫胖,再導(dǎo)出一個install方法,同樣是利用mixin在beforeCreate鉤子中遞歸注入$store對象
export const install = (_Vue) =>{
    _Vue.mixin({
        beforeCreate() {
          const options = this.$options;
          if (options.store) { 
              // 給根實(shí)例增加$store屬性
              this.$store = options.store;
          } else if (options.parent && options.parent.$store) {
              // 給組件增加$store屬性
              this.$store = options.parent.$store;
          }
       }
    })
}
2她奥、實(shí)現(xiàn)state和getter瓮增。都是利用vue中的data和computed來實(shí)現(xiàn)。這樣可以為每一個store中的數(shù)據(jù)綁定響應(yīng)式哩俭。并做一層代理绷跑,如果用戶調(diào)用this.store.state 或者 this.store.getter會去返回創(chuàng)建的vue實(shí)例上的屬性
// state
export class Store {
    constructor(options){
        let state = options.state;
        this._vm = new Vue({
            data:{
                $$state:state,
            }
        });
    }
    get state(){
        return this._vm._data.$$state
    }
}

// getter
this.getters = {};
const computed = {}
forEachValue(options.getters, (fn, key) => {
    computed[key] = () => {
        return fn(this.state);
    }
    Object.defineProperty(this.getters,key,{
        get:()=> this._vm[key]
    })
});
this._vm = new Vue({
    data: {
        $$state: state,
    },
    computed // 利用計(jì)算屬性實(shí)現(xiàn)緩存
});

3、添加mutation和action凡资。其實(shí)就是存儲一個對象砸捏,利用發(fā)布訂閱來保存回調(diào)函數(shù)數(shù)組。
export class Store {
    constructor(options) {
        this.mutations = {};
        forEachValue(options.mutations, (fn, key) => {
            this.mutations[key] = (payload) => fn.call(this, this.state, payload)
        });
    }
    commit = (type, payload) => {
        this.mutations[type](payload);
    }
}

export class Store {
    constructor(options) {
        this.actions = {};
        forEachValue(options.actions, (fn, key) => {
            this.actions[key] = (payload) => fn.call(this, this,payload);
        });
    }
    dispatch = (type, payload) => {
        this.actions[type](payload);
    }
}

整體流程:vuex ->install方法中會去遍歷綁定$store隙赁。所以組件都可以取到-> 格式化用戶配置成一個樹形結(jié)構(gòu)垦藏。->安裝模塊,遞歸把模塊mutation伞访、action掂骏、getter、state都掛在store上厚掷,mutation弟灼、action都是數(shù)組(子模塊和父模塊重名會push都執(zhí)行),getter是對象(子模塊和父模塊重名會覆蓋)冒黑。state也是對象->會new 一個vue將state放到data上田绑、將getter放到computed上利用vue的原理來實(shí)現(xiàn)響應(yīng)式和計(jì)算緩存。

4薛闪、namespace模塊, 其實(shí)就是給安裝的模塊增加了path

1辛馆、如果不寫namespace是沒有作用域的俺陋,調(diào)用根豁延、子模塊的同名mutations都會執(zhí)行修改。
2腊状、狀態(tài)不能和模塊重名诱咏,默認(rèn)會使用模塊, a模塊namespace:true state中也有a
3缴挖、默認(rèn)會找當(dāng)前模塊的namespace袋狞,再向上找父親的。比如父親b有namespace兒子c沒有,會給兒子也加 使用方式:b/c/苟鸯,子c有父b沒有同蜻。則調(diào)用時不需要加父親b。調(diào)用:c/xxx

假設(shè)根模塊下中有a模塊并且都有命名空間
mutation早处、action如果在子模塊和父模塊中都有湾蔓,會都掛到store中的——mutation、action對象中砌梆,其中增加命名空間默责。例如:store.action = {'setA':[()=>{}] ,'a/b/setA':[()=>{}]},如果namespace沒寫的話就都在一個數(shù)組中,不會覆蓋
使用就$store.mutation('a/b/setA')

getter如果在子模塊和父模塊中都有的話咸包,會都掛載到store的_getter對象中桃序,增加命名空間,但是不是數(shù)組烂瘫,重名會覆蓋媒熊,例如:{'getA':()=>{},'a/b/getA':()=>{}},如果namespace沒寫的話就都在一個對象中會覆蓋
使用就$store.getter('a/b/getA')

state會遞歸放入store中坟比,變成一個對象泛释,例如 {name:'kimi', a:{name:'bob'}}代表根節(jié)點(diǎn)中的state name是kimi,模塊a中是bob温算。所以使用的時候$store.state.a.name

vuex 插件

插件會使用發(fā)布訂閱怜校。在每次數(shù)據(jù)mutation更新的時候去發(fā)布。然后提供replaceState方法來替換state注竿,可以寫一個持久化插件茄茁,存到localStorge中,刷新后再從localStorge中取使用replaceState方法替換

function persists(store) { // 每次去服務(wù)器上拉去最新的 session巩割、local
    let local = localStorage.getItem('VUEX:state');
    if (local) {
        store.replaceState(JSON.parse(local)); // 會用local替換掉所有的狀態(tài)
    }
    store.subscribe((mutation, state) => {
        // 這里需要做一個節(jié)流  throttle lodash
        localStorage.setItem('VUEX:state', JSON.stringify(state));
    });
}
plugins: [
    persists
]

內(nèi)部原理實(shí)現(xiàn):

// 執(zhí)行插件
options.plugins.forEach(plugin => plugin(this));
subscribe(fn){
    this._subscribers.push(fn);
}
replaceState(state){
    this._vm._data.$$state = state;
}

registered

也提供動態(tài)注冊模塊功能裙顽,就是重新走 -> 格式化樹形數(shù)據(jù) -> 安裝模塊到store上 -> 重新new vue實(shí)例,此時會銷毀之前的vue實(shí)例

strict模式

如果開啟strict模式宣谈,mutation中只能放同步代碼愈犹,不能放異步。并且不能直接修改state只能通過commit修改state闻丑。
更改屬性時包裹一層切片漩怎,先置換狀態(tài)_commite修改完再改回
也就是說只要是正常操作(不是通過state修改的)都會將_committing改為true

this._committing = false;
_withCommitting(fn) {
    let committing = this._committing;
    this._committing = true; // 在函數(shù)調(diào)用前 表示_committing為true
    fn();
    this._committing = committing;
}

此時修改mutation的值是需要包裹一層_withCommitting

store._withCommitting(() => {
    mutation.call(store, getState(store, path), payload); // 這里更改狀態(tài)
})

嚴(yán)格模式會去利用vue的$watch方法去監(jiān)控state,并且設(shè)置deep嗦嗡,sync為true勋锤,sync代表同步觸發(fā),如果data變了會立即執(zhí)行回調(diào)不會放入queue中nextTick執(zhí)行侥祭。這樣就可以監(jiān)控state變化叁执,如果其中之前的_commite為false說明沒有經(jīng)過commit或者異步更新(fn是異步執(zhí)行茄厘,則此時的_committing已經(jīng)重置回false了)。就可以拋錯

if (store.strict) {
    // 只要狀態(tài)一變化會立即執(zhí)行,在狀態(tài)變化后同步執(zhí)行
    store._vm.$watch(() => store._vm._data.$$state, () => {
        console.assert(store._committing, '在mutation之外更改了狀態(tài)')
    }, { deep: true, sync: true });
}

內(nèi)部正常的操作(不是通過state直接修改的)都需要包裝一層_withCommitting

replaceState(newState) { // 用最新的狀態(tài)替換掉
    this._withCommitting(() => {
        this._vm._data.$$state = newState;
    })
}
store._withCommitting(() => {
    Vue.set(parent, path[path.length - 1], module.state);
})
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末谈宛,一起剝皮案震驚了整個濱河市次哈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌吆录,老刑警劉巖亿乳,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異径筏,居然都是意外死亡葛假,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進(jìn)店門滋恬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來聊训,“玉大人,你說我怎么就攤上這事恢氯〈撸” “怎么了?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵勋拟,是天一觀的道長勋磕。 經(jīng)常有香客問我,道長敢靡,這世上最難降的妖魔是什么挂滓? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮啸胧,結(jié)果婚禮上赶站,老公的妹妹穿的比我還像新娘。我一直安慰自己纺念,他們只是感情好贝椿,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著陷谱,像睡著了一般烙博。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上烟逊,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天渣窜,我揣著相機(jī)與錄音,去河邊找鬼焙格。 笑死图毕,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的眷唉。 我是一名探鬼主播予颤,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼冬阳!你這毒婦竟也來了蛤虐?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤肝陪,失蹤者是張志新(化名)和其女友劉穎驳庭,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體氯窍,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡饲常,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了狼讨。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贝淤。...
    茶點(diǎn)故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖政供,靈堂內(nèi)的尸體忽然破棺而出播聪,到底是詐尸還是另有隱情,我是刑警寧澤布隔,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布离陶,位于F島的核電站,受9級特大地震影響衅檀,放射性物質(zhì)發(fā)生泄漏招刨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一哀军、第九天 我趴在偏房一處隱蔽的房頂上張望计济。 院中可真熱鬧,春花似錦排苍、人聲如沸沦寂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽传藏。三九已至,卻和暖如春彤守,著一層夾襖步出監(jiān)牢的瞬間毯侦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工具垫, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留侈离,地道東北人。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓筝蚕,卻偏偏與公主長得像卦碾,于是被迫代替她去往敵國和親铺坞。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評論 2 345