vue2 雙向綁定實現原理-續

上一篇大概解釋了雙向綁定實現原理,可是沒有例子,因此可能看起來比較很差理解,vue

這一篇作爲補充,會用一個例子來說解實現過程,首先上例子express

<template>
  <div id="app" class="app">
  
      <label>姓名:</label>
    <input v-model="name"> <!--<input :value="name" @input="name = $event.target.value">-->
     <select v-model="sex"> <option value="1">男</option> <option value="0">女</option> </select> <p>{{infoName}}</p> <p>{{infoSex}}</p> </div> </template> <script> export default { data(){ return { name:"", sex:1, } }, computed:{ infoName(){ return "您的姓名是:" + this.name; }, infoSex(){ return "您的性別是:" + (this.sex == 1 ? "男":"女")); } }, watch:{ sex:function(newSex){ alert("你的性別已經改成"+ this.sex == 1 ? "男":"女"); }, name:function(newName){ alert("你的名字已經改成"+ newName); } }, } </script>

而後下面再上一個上面編譯後的對應的render函數代碼:數組

module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var 

_c=_vm._self._c||_h;
  return _c('div', {
    staticClass: "app",
    attrs: {
      "id": "app"
    }
  }, [_c('label', [_vm._v("姓名:")]), _vm._v(" "), _c('input', {
    domProps: {
      "value": _vm.name
    },
    on: {
      "input": function($event) {
        _vm.name = $event.target.value
      }
    }
  }), _vm._v(" "), _c('select', {
    directives: [{
      name: "model",
      rawName: "v-model",
      value: (_vm.sex),
      expression: "sex"
    }],
    on: {
      "change": function($event) {
        var $$selectedVal = Array.prototype.filter.call($event.target.options, function(o) 

{
          return o.selected
        }).map(function(o) {
          var val = "_value" in o ? o._value : o.value;
          return val
        });
        _vm.sex = $event.target.multiple ? $$selectedVal : $$selectedVal[0]
      }
    }
  }, [_c('option', {
    attrs: {
      "value": "1"
    }
  }, [_vm._v("男")]), _vm._v(" "), _c('option', {
    attrs: {
      "value": "0"
    }
  }, [_vm._v("女")])]), _vm._v(" "), _c('p', [_vm._v(_vm._s(_vm.infoName))]), _vm._v(" "), 

_c('p', [_vm._v(_vm._s(_vm.infoSex))])])
},staticRenderFns: []}

 

當上面組件在建立以後,調用$mount方法進行掛載的時候,會首先建立vm對應的watcher,緩存

在watcher的構造函數實現中,最後會調用一次get方法去初始化value值,而get方法對應的方法爲render(簡化以後),代碼爲:app

var  updateComponent = function () {
          vm._update(vm._render(), hydrating);
       };
 
    vm._watcher = new Watcher(vm, updateComponent, noop);

注:1.update方法是更新組件的方法,這個和vDom(虛擬dom)相關,這裏忽略它,就認爲render就是更新組件的方法dom

  2.其實render只是返回一個vNode,並非真正的更新渲染實現函數

 

而render方法 就是上面編譯後的實現,每一個文件最後都會把template裏面的內容編譯成一個render方法oop

當運行這個render方法的時候,你們能夠看到會去調用_vm.name,_vm.sex,_vm.infoName,_vm.infoSex得到值,性能

而這些屬性值,在vm這個對象裏面,其實只是一個代理,在調用initSate的時候初始化data方法裏面返回的對象,並設置代理到vm,優化

而computed裏面的屬性值,是在建立這個vm對應的構造函數的時候,進行初始化並設置代理到vm的,其方法是 Vue.extend

_vm.name 實際上是多級代理:首先返回this[sourceKey][key], 這裏sourceKey爲_data,key爲name因此實際上是返回_vm._data.name,

而_vm._data.name 會調用上一篇說到的Object.defineProperty定義的get方法,

這個時候,由於Dep.target不爲空(由於這個時候是運行watcher的get方法,因此Dep.target爲當前watcher),那麼name 屬性對應的dep就和這個watcher

創建的關聯了,sex同理,

這裏 說說 vm和infoName設置代理的過程,先上代碼:

Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    const Sub = function VueComponent (options) {
      this._init(options)
    }
    
    Sub['super'] = Super

    
    if (Sub.options.computed) {
      initComputed(Sub)
    }

    // cache constructor
    cachedCtors[SuperId] = Sub
    return Sub
  }
}

function initComputed (Comp) {
  const computed = Comp.options.computed
  for (const key in computed) {
    defineComputed(Comp.prototype, key, computed[key])
  }
}

export function defineComputed (target: any, key: string, userDef: Object | Function) {
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = createComputedGetter(key)
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.set
      ? userDef.set
      : noop
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

function createComputedGetter (key) {
  return function computedGetter () {
    var watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate();
      }
      if (Dep.target) {
        watcher.depend();
      }
      return watcher.value
    }
  }
}

注: 1.extend方法裏面的實現刪除了不少,這個方法是用來繼承基本vue組件類並返回一個構造函數,而且會緩存起來,當下次調用的時候就直接返回而不用再初始化

       2. 咱們須要關注的執行順序是 extend->initComputed->defineComputed->createComoputederGetter

 

經過實現能夠看出,當這個組件在第一次建立的時候,會利用Vue.extend建立其對應的構造函數,而extend方法會初始化他對應的computed,而後對computed對象裏面的每一個屬性設置一個代理,

當調用vm.infoName的時候,其實的獲取 watcher對應的value值,

 

咱們在看看上面的執行過程,建立vm -> 建立對應的watcher -> 執行render -> 調用_vm.infoName -> 調用computedGetter方法,

而根據 上一篇 computed屬性建立對應的watcher過程,每一個屬性都有一個對應的watcher,而且會用key作對應,並且這個watcher的dirty爲true,在初始化的時候也沒有

調用對應的get方法,這個時候由於dirt爲true,因此爲調用watcher的evaluate強行計算一次,而後dirty爲false,之後若是對應的dep的對應的data值沒有改變,那麼就會一直用這個value值

而再也不從新計算,當涉及到的data值,好比在這個裏面就是name值改變的時候,會通知watcher進行更新,而watcher會把dirty再次設置爲true,這個時候再調用infoName的時候就會從新計算

 

上面就是初始化的過程當中創建關聯的過程,

 

而當咱們在name對應的input的進行輸入的時候,這個時候觸發綁定的input事件,會執行 name = $event.target.value 的操做

這個時候最終會執行Object.defineProperty定義的get方法,這個時候確定不會和以前的value值相同,由於初始化的value值爲"",而新的值確定不會爲"",

會調用dep.notify通知watcher方法更新,執行過程代碼爲:

dep.notify()

notify () {
  
  const subs = this.subs.slice()
  for (let i = 0, l = subs.length; i < l; i++) {
     subs[i].update()
  }
}

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

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i >= 0 && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(Math.max(i, index) + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

function flushSchedulerQueue () {
  flushing = true
  let watcher, id, vm

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    id = watcher.id
    has[id] = null
    watcher.run()
  }
}

  

咱們須要關注的執行流程爲: dep.notify -> watcher.update -> queueWatcher -> nextTick -> flushSchedulerQueue

 

在watcher.update 方法裏面 能夠看到 有3個分支,第一: 若是lazy 那麼就是 只設置爲dirty爲true,等待獲取value值的時候再去從新計算

第二:若是 sync爲true 那麼就當即執行run,

第三:把這個watcher加入到scheduler(調度者)的更新隊列中去,若是當前沒有執行更新,那麼加入數組,等待nextTick(下一個時間片)去更新,

若是當前正在執行更新,那麼就會根據id作一個從小到大的排序,更小的id老是更先更新,由於從id的產生來講,父節點的id老是比子節點的id小,

 

nextTick的源碼實現由於有點多,並且和此話題關係不是很重,我大概講一下nextTick的實現就好了

nextTick方法的主要做用 加入一個function,這個function 會在下一個時間片執行,具體更具環境不一樣而利用不一樣實現,

優先使用Promise,其次使用MutationObserver,若是上述兩種方式都不能使用,那麼就使用setTimeout(functionName,0)的方式來實現

這個方法實現了,講多個watcher的更新集中到一塊兒實現一次更新,而避免每一個watcher更新都直接當即更新,由於watcher的關聯性很大,並且有不少dep對應的watcher多是

同一個watcher,這樣能夠避免避免重複更新,從而大大的優化了性能,好比上面的列子,當我同時改變name 和sex的時候,其實他們的watcher都是同樣,都是vm對應的那個watcher,

那麼若是name改變的時候實行一次render,sex改變的時候又執行一次render,那就有點可怕了,固然這又會照成一個影響,就是我改變了數據以後,dom沒有當即改變,這個時候咱們能夠

利用vue2 提供的nextTick方法來實現咱們的需求

 

在下一個時間片,會調用flushSchedulerQueue方法,首先對隊列裏面的watcher按照id進行升序排序,而後循環隊列,調用watcher.run,而後再運行get方法進行更新

 

上面就是整個通知更新執行過程了

 

總的歸納過程就是, 當調用watcher的get方法進行計算的時候, 獲取data的數據即調用get方法,這個時候會進行進行關聯,

當數據改變的時候執行set方法,會通知dep相關了的watcher進行更新

相關文章
相關標籤/搜索