vue2 雙向綁定實現原理


都知道vue中實現了數據和視圖的雙向綁定,但具體是如何實現的呢?

今天就說說 我閱讀的vue2中的代碼實現的個人所得,如果說錯了,歡迎指正。

注:我閱讀的vue2代碼的版本是v2.2.6,且都是以單文件的方式展示列子, 可以結合下一篇續給出的實際例子結合起來看,更容易理解

 

話不多說,首先上結論,然后再通過圖以及代碼進行詳細解釋

結論:m->v: 利用觀察者模式,當數據改變的時候通知其觀察者進行更新,

     v->m: 利用事件綁定對數據進行設值

 

實現過程:通過Object.defineProperty的方法定義 get和set方法對數據的操作擁有代理權,並且每個屬性都會有一個對應的依賴,

每個vm組件都會有一個或多個watcher,當獲取vm或者data的數據的時候就會調用get方法,如果這個時候有watcher正在運行觀察,

那么就把這個依賴和此watcher都加入到各自的對象下的相關隊列中去

1)model到view:當設置vm或者data對象的數據的時候,就會調用set方法,

如果這次結果 和上次的value不同,就讓此依賴中的所有watcher加入到scheduler(調度者)的更新隊列中去,

等待nextTick更新scheduler的隊列,調用watcher的update方法,然后運行getter方法比較value值和之前保存的value值是否相同,

如果不同,則調用cb(回調函數),對應vm來說getter是vm.update(vm.render()),當調用getter的時候,整個過程就完成了model到view的更新了

2:view到model:一般用v-model方法進行實現,默認的是綁定dom組件的input事件(這個看源碼的實現,如果你設置option.model.event的值的話,

事件就會變成你設置的值),當觸發此事件的時候,會調用你綁定的值進行設置,就又回到1)過程了,當然也可以自己綁定自己關心的事件實現設值的效果

上圖:

 

可以從結論中看到,其中有3個比較重要的部分,data對應的dep和vm對應的watcher以及把它們進行關聯的Object.defineProperty定義的get和set方法,

而實現這一切的基礎就是調用Object.defineProperty對屬性設置get和set代理方法,所以vue2只能支持ie8以上,因為ie8及一下沒有這個方法,也沒法用shim的方式來實現

 

詳細解釋

一. watcher

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: Set;
  newDepIds: Set;
  getter: Function;
  value: any;

  constructor (vm: Component, expOrFn: string | Function, cb: Function, options?: Object) {}

    run(){}
    get(){}
    update(){}
    addDep(){}
    cleanupDeps{}
    evaluate(){}
    teardown(){}
          
}    

注:為了避免太長不方便看,方法的具體實現就沒有帖出來了,如果有興趣的可以去源碼看看

 

簡稱觀察者,通常意義的解釋,就是對某些事物感興趣而去觀察他的人,用在vue2里面就是對一些表達式結果感興趣而進行觀察的對象,

用來統計更新vm的updateComponent方法,computed屬性或者watch屬性中的expOrFn(表達式或者方法)

 

watcher對象有3個比較重要的屬性:

  getter:關心的表達式或者方法

  value: 運行getter的結果,在new watcher的時候如果lazy為false就會調用getter一次來初始化這個值

  cb:當運行getter方法得到的value值和之前的value值不同的時候,會調用cb,對於vm組件來說就是更新組件

還有一些其他屬性:

  id: watcher的唯一標示,當對一個周期內的所有watcher執行更新的時候,會先根據這個id 從小到大進行排序,而且父級的vm的對應的id總是小於子集

  lazy:是否懶更新, 也就是當它關注的表達式中有相關數據改變的時候,它才更新,不然就一直用value值,像computed中的watcher的lazy就是true

  dirty:是否已經臟了,如果臟了,當獲取這個值的時候就重新調用getter獲得新值,否則直接返回value的值,和lazy有關系

  user: cb回調函數是否是用戶傳進來的,如果是的話執行這個回調就會用try catch的方式,以避免出錯

  active: 這個watcher是否是活動的,如果是活動才會去運行getter

  deps:對應的所有依賴,是一個數組

  newDeps:這次更新后,收集到的下一輪更新所相關的依賴

  depIds:所有依賴的id,是一個set對象

  newDepIds: 下一輪的依賴id所對應的set對象

 

注:這里面的deps的作用個人覺得是:當vm銷毀的時候或者運行了一此get方法的時候,從相關的依賴中刪除自己,當某些需要的時候,重新和依賴關系,

  目前只看到computed的watcher 在獲取值的時候,如果watcher的dirty為true,就會重新和deps里面的依賴重新建立關系,其他地方就沒看到有使用了- -!

 

先上一個例子,下面會用到:

<template>
  <div id="app" class="app">
 
      <label>姓名:</label>
      <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,
        resultObj:{
          allInfo:""
        },
      }
    },
    computed:{
      infoName(){
        return "您的姓名是:" + this.name;
      },
      infoSex(){
        return "您的性別是:" + (this.sex == 1 ? "男":"女");
      }
    },
    watch:{
      sex:function(newSex){
        this.resultObj.allInfo = this.name + "|" + this.sex;
        alert("你的性別已經改為"+ (this.sex == 1 ? "男":"女"));
      },
      name:function(newName){
        this.resultObj.allInfo = this.name + "|" + this.sex;
         alert("你的名字已經改為"+ newName);
      },
      "resultObj.allInfo":function(newInfo){
        alert("你新的的所有信息為"+ newInfo);
      }
    },
 }

</script>

注: 上述代碼是為了展示 watcher而強行拼湊的代碼,所以有重復的地方,不要在意這些細節

 

有3個地方會有對應的watcher

1.一個vm組件會對應一個watcher,這個watcher關注的是vm的_render方法,每個vue文件的template里面的內容都會被編譯成一個_render方法,

  並且里面用到的屬性或者computed都會轉化成_vm.name 或者_vm.infoName 的代理形式,具體的轉化過程在dep里面說到

2.computed對象里面的每個屬性,都有一個對應的watcher,如上例:infoName,infoSex都會有一個對應的watcher,並且生成的watcher的lazy為true,

 即它們都是懶更新的,只有里面用到的相關數據出現變化的時候,這個watcher才會執行getter方法

3.watch對象里面的每個屬性,都有一個對應的watcher,如上例:sex,name,"resultObj.allInfo"都會有一個對應的watcher

  "resultObj.allInfo" 這個對應的watcher需要特殊說明下它的getter,先看代碼

if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    this.getter = parsePath(expOrFn);
    if (!this.getter) {
      this.getter = function () {};
}
////////////////////////////////////////
var bailRE = /[^\w.$]/;
function parsePath (path) {
  if (bailRE.test(path)) {
    return
  }
  var segments = path.split('.');
  return function (obj) {
    for (var i = 0; i < segments.length; i++) {
      if (!obj) { return }
      obj = obj[segments[i]];
    }
    return obj
  }
}
///////////////////////////////////////////////
function get(){
  if (this.user) {    try {      value = this.getter.call(vm, vm);    } catch (e) {    handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));   }  } else {    value = this.getter.call(vm, vm);  }
}

如果初始化watcher的時候傳的expOrFn 是function,那么這個getter就直接是這個function,否則就就行轉化,運行parsePath方法得到一個function

當然這個function有一個判斷,是路徑形式的才行,當運行getter方法的時候,傳入vm作為obj,那么其實resultObj.allInfo 得到的是 vm.resultObj.allInfo的值,

當然 不可能直接vm["resultObj.allInfo"]或者vm."resultObj.allInfo"這樣得到,所以用了一個循環來實現

 

這3種對應的watcher創建的時候分別為:

1) 使用$mount方法的時候

Vue$3.prototype.$mount = function (el, hydrating) {
    el = el && inBrowser ? query(el) : undefined;
    return mountComponent(this, el, hydrating)
};

function mountComponent (vm,el,hydrating) {

    var  updateComponent = function () {
          vm._update(vm._render(), hydrating);
       };

    vm._watcher = new Watcher(vm, updateComponent, noop);
}

注:1.有些地方我是從分模塊實現的源碼中拷貝過來的,有些是直接是為了方便查找,就直接在vue.common.js中拷貝過來,所以可能代碼風格有些不一樣,

  而且為了更方便理解,會對某些地方做小小的改動,會刪除一些和本話題無關的代碼實現過程

      2.hydrating 這個屬性和vdom(虛擬dom)實現有關系,在這里暫時不去解釋它的作用, 單詞意思為:保濕

 

通過實現可以看出來,watcher的getter 就是vm的render方法(update是更新組件,和數據沒有關系,所以把getter轉化為render),每個vm都有自己獨立對應的一個watcher,

這個watcher關心的是整個組件的渲染

上面說的是 在使用$mount方法的時候 會創建這個watcher,但是有時候,我們在創建組件的時候,沒有使用$mount ,比如

const app = new Vue({
  el:"#app"
  router: router,
  store:store,
  render: h => h(App)
});

這個時候就沒有用$mount,那么它就不會有watcher了嗎?

先上代碼: 

function Vue (options) {

  this._init(options)
}


Vue.prototype._init = function (options?: Object) {

//...其他初始化實現
if (vm.$options.el) { vm.$mount(vm.$options.el) } }

注: 我吧_init方法里面不相關的代碼都刪除了,只留下了我們關心的地方

在new vue的時候,我們傳入的對象有個el屬性,關鍵就在這里,而在vm初始化的時候會判斷傳進來的對象有沒有這個屬性,如果有就會自動去調用$mount方法,而不需要手動

當然如果又沒有el,又沒有手動調用$mount方法,那么這個組件就不會出現在view上,也不會創建對應的watcher

 

2.解析computed屬性的時候

Vue.prototype._init = function (options?: Object) {
     initState(vm)
}

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch) initWatch(vm, opts.watch)
}

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

  for (const key in computed) {
    const userDef = computed[key]
    let getter = typeof userDef === 'function' ? userDef : userDef.get
    
    // create internal watcher for the computed property.
    watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)
  }
}

注:1.把有些和此話題不相關的代碼都刪除了,所以帖出來的代碼不是完整的

  2.在逛vue論壇的時候,遇到有人提問:在prop屬性設置default為methods里面的方法為什么報空,大家看看initState的初始化順序就明白了

  3.data,computed,watch等都會存儲在vm.$option中

 

在解析computed屬性的時候,會對里面的每個屬性都會有創建一個對應的watcher,並且會用用這個屬性的key作為_computedWatchers 對象的key來保存這個watcher

當你默認傳一個function的時候,會把這個這個function當做watcher的getter,否則就會認為你傳了一個包含了get屬性的對象,並且是一個方法,

這個時候就會用這個get方法當做watcher的getter

 

3.使用$watch方法的時候

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

function createWatcher (vm: Component, key: string, handler: any) {
  let options
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  vm.$watch(key, handler, options)
}

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ): Function {
    const vm: Component = this
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

和使用$mount方法一樣,無論是你初始化的對象包含了watch屬性或者使用$watch 都會創建相對於的watcher

使用$watch會返回一個方法,當你對觀察的expOrFn不在感興趣的時候,可以用來刪除這個觀察者

 

二.dep

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

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

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

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

簡稱依賴,對vm中data返回的每個屬性都會初始化一個依賴,用來收集對這個屬性感興趣的watcher,也就是watcher中運行getter方法會用到的相關屬性,

那么這個屬性對應的依賴就會把watcher加入到dep下的subs隊列中去,也會把本身dep加入到watcher中的deps隊列中去,

當這個屬性的值改變的時候,通知watcher進行更新操作,也就是執行getter方法

 

屬性解釋:

  subs:對某個屬性感興趣的watcher隊列

 

 

dep一般對應vm的中data方法返回的對象以及它的屬性  

export default {
  data(){
    return {
      name:"",
      sex:1,
      items:[]
    }   } }

比如上面的 data會有一個dep,name和sex也分別有一個dep

 

dep的創建過程

function initState (vm) {
  if (opts.data) {
    initData(vm);
  } else {
    observe(vm._data = {}, true /* asRootData */);
  }
}

function initData (vm) {
  var data = vm.$options.data;
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {};
  if (!isPlainObject(data)) {
    data = {};
  }
  
  var keys = Object.keys(data);
  var props = vm.$options.props;
  var i = keys.length;
  while (i--) {
    if (props && hasOwn(props, keys[i])) {
     ///warn
    } else if (!isReserved(keys[i])) {
      proxy(vm, "_data", keys[i]);
    }
  }
  // observe data
  observe(data, true /* asRootData */);
}

function observe (value, asRootData) {
  if (!isObject(value)) {
    return
  }
  var ob;
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    observerState.shouldConvert &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value);
  }

  return ob
}

var Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  def(value, '__ob__', this);
  if (Array.isArray(value)) {
    var augment = hasProto
      ? protoAugment
      : copyAugment;
    augment(value, arrayMethods, arrayKeys);
    this.observeArray(value);
  } else {
    this.walk(value);
  }
};

Observer.prototype.walk = function walk (obj) {
  var keys = Object.keys(obj);
  for (var i = 0; i < keys.length; i++) {
    defineReactive$$1(obj, keys[i], obj[keys[i]]);
  }
};

function defineReactive$$1 (
  obj,
  key,
  val,
  customSetter
) {
  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;

  var childOb = observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
        }
        if (Array.isArray(value)) {
          dependArray(value);
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      var 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();
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = observe(newVal);
      dep.notify();
    }
  });
}

Observer.prototype.observeArray = function observeArray (items) {
  for (var i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
};

注:vm初始化會調用initState方法,所以省略了init的過程

 

整個過程簡而言之就是 

 

1).initData

  在初始化data的時候,是用一個三元運算符,判斷data是否是一個function,如果是的話,就運行 getData方法

function getData (data, vm) {
  try {
    return data.call(vm)
  } catch (e) {
    handleError(e, vm, "data()");
    return {}
  }
}

其實getData 也就是運行一次data方法而已,只是為了安全考慮,加了try catch

如果不是的話,就返回 data || {} 給它,這里有個小技巧就是,如果data不為空就返回data,否則就返回一個{},

然后 在對data里面的屬性設置代理,讓我們能夠用vm.key的方式來獲取或者設置data里面的數據,而不用vm._data.key的方式

在方法的最后面 就是調用observe 來生成一個observer(可以依賴的對象)

 

2).observe

 首先判斷 是不是一個object,當然 這里的object 是一個泛型,是指 typeof 為 "object" 且 != null 的對象,也可以是Array,

function isObject (obj) {
  return obj !== null && typeof obj === 'object'
}

如果不是一個object 那么就會中斷

再判斷 是否已經包含了一個 observer 對象,如果有 則返回這個observer對象

再判斷 

observerState.shouldConvert: 是可以轉換的
!isServerRendering(): 不是服務器渲染
(Array.isArray(value) || isPlainObject(value)) 是 數組 或則 Object 類型
Object.isExtensible(value): 這個對象是可以添加屬性的, 做這個判斷是因為 會在Object上 新增一個__ob__屬性來對應它的Observer對象

!value._isVue: 不能是 vm對象, vm組件 嚴格來說 也是Object對象,但是他不能被觀察,所以有這個判斷

注: 判斷還挺多的。。。。

 

如果上述條件 任何一個不滿足 就會中斷,不過我們只需要關心的是。。我們傳進來的數據滿足是 數組 或者object對象就行了,其他的一般不會觸及

好了 下一步就是肉戲來了,就是創建一個 observer 對象

3). new observer

保存value數據對象,然后最重要的相關核心實現就從這里開始了,

首先創建一個dep依賴(說了這么多終於說到它了),這個dep 就對應的這個value數據對象,

然后 會判斷這個value是不是數組對象,

如果是的話, 先執行

augment(value, arrayMethods, arrayKeys)
而 augment 一般會對應這個方法
function protoAugment (target, src) {
/* eslint-disable no-proto */
target.__proto__ = src;
/* eslint-enable no-proto */
}

這一步比較關鍵,把這個數組對象默認的prototype對應的方法替換成arrayMethods 里面的方法,而arrayMethods 這個對象里面包含了哪些方法呢,請看代碼

var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  // cache original method
  var original = arrayProto[method];
  def(arrayMethods, method, function mutator () {
    var arguments$1 = arguments;

    // avoid leaking arguments:
    // http://jsperf.com/closure-with-arguments
    var i = arguments.length;
    var args = new Array(i);
    while (i--) {
      args[i] = arguments$1[i];
    }
    var result = original.apply(this, args);
    var ob = this.__ob__;
    var inserted;
    switch (method) {
      case 'push':
        inserted = args;
        break
      case 'unshift':
        inserted = args;
        break
      case 'splice':
        inserted = args.slice(2);
        break
    }
    if (inserted) { ob.observeArray(inserted); }
    // notify change
    ob.dep.notify();
    return result
  });
});

  

從代碼看:包含了 'push','pop','shift','unshift','splice','sort','reverse' 方法,並且arrayMethods的prototype是Array的prototype,

具體作用在下面回和例子結合說到

然后,再執行 observeArray,

Observer.prototype.observeArray = function observeArray (items) {
  for (var i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
};

其實也就是對數據里面的每個元素再進行觀察, 從observe的判斷條件來看,普通的數據就不要想了,此元素 必須是一個object 或者array,才會生成一個observer對象(這點很重要)

好了,通過上面對數組的實現,可以解釋一下 我們平時 在用數組稍不注意就會犯的一個錯誤, 就是改變一個數組的普通元素的值,但是 界面上卻沒有響應它的改動呢,

比如

<template>
  <div id="app" class="app">
      <p v-for="value of ary">{{value}}</p>
  </div>
    
</template>

<script>

 export default {

    data(){
      return {
        ary:[1,2,3,4]
      }
    },
    mounted(){
      setTimeout(()=>{
        this.ary[2] = 10;
      }, 2000);
    }
 }

  

上述代碼,會在兩秒之后改變界面的顯示嗎? 

不會,因為 this.ary[2] = 10, 這個只是普通的操作,vue2 沒有對這個屬性添加相關的dep(我覺得是為了性能考慮吧,不然的話 數組里面的數據大了,對每個數據都建立一個dep,那就太恐怖了)

這樣的操作,是沒有任何改變的,

而上面vue2 特意自己實現了array 的一些相關方法去實現這個效果,所以如果要改變普通元素的數據的時候我們可以這樣,

mounted(){
      setTimeout(()=>{
        this.ary.splice(2,1,10);
      }, 2000);
    }

感興趣的可以測試下,

起作用的原因在於arrayMethods這個對象,當我們操作splice的時候,不是操作的ary的原生的splice方法,而是操作的arrayMethods里面的splice方法,

而這個方法在執行了原生的對應的方法后,還會去調用ob.dep.notify()方法執行更新

 

以上是如果這個數據類型是數組的時候,當不是的時候,那么執行walk方法,

然后獲取對象的keys 循環對key調用defineReactive方法,然后當這個數據 又是一個object或者array對象的時候,又再一次去觀察它

可以說defineReactive 這個方法里面的實現,是實現雙向綁定最最核心的一個方法了,其他的一系列都是在這基礎上做擴展,或是為了健壯性或者為了解耦等等,

這個方法實現了對data的每個屬性創建一個dep,並且對data的原生屬性的操作利用Object.defineProperty方法定義一個set和get的代理,讓data和dep產生關系,

而且 在set和get具體實現里,也實現了 讓dep和watcher 產生關系,所以說這個是vue2實現雙向綁定最最最核心的方法了,因為這么重要,說就單獨在下一個部分來解釋它

 

三 dep 和 watcher 是如何聯系起來的

function defineReactive$$1 (
  obj,
  key,
  val,
  customSetter
) {
  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;

  var childOb = observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
        }
        if (Array.isArray(value)) {
          dependArray(value);
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      var 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();
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = observe(newVal);
      dep.notify();
    }
  });
}

 

下面就來看看 這個方法的具體實現:

 

首先定義了一個dep,然后在獲取這個屬性的描述對象,用這個對象來判斷這個屬性是不是可以配置的,如果為否,則退出,因為使用Object.defineProperty

這個方法是需要這個屬性是可以配置的

 

然后獲取這個屬性的 get和set方法並緩存,因為Object.defineProperty會重定義這兩個方法,會覆蓋它們,緩存起來以便於調用

 

再然后 let childOb = observe(val)  這句話,就實現生成子對象的observer,這樣就實現了循環調用,對所有子對象都能夠生成observer以及子對象的屬性值的dep

當然,這個子對象得是 Object 或者 array

 

請注意:核心來了,使用Object.defineProperty方法 定義set 和get方法

我們看set方法實現:

首先得到value值,這里用三元運算符來判斷之前緩存的get方法是否存在,如果存在就調用這個get方法否則直接返回val值

然后再 判斷 Dep.target 是否存在,我們看看源碼里面的注釋對這個屬性的解釋

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null

這個屬性表示當前正在計算的watcher, 並且在用一個時間內整個全局內只會同時存在一個

就是說:正在運行 get方法的watcher,而get是一個方法,而js是單線程,不會同一時間運行多個方法,那當運行這個get方法過程中又可能存在調用其他watcher的get方法的情況,

比如一個vm組件的render方法里面包含computed的屬性的時候,這又是如何處理的呢,請看代碼

const targetStack = []

export function pushTarget (_target: Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  Dep.target = targetStack.pop()
}

 

首先定義了一個target棧,是通過一個數組實現的,

運行watcher的get方法的時候 會先調用pushTarget方法,方法里面會 判斷Dep.target,如果存在則把它壓進棧里面,然后把新的taget賦值給Dep.target

watcher的個體方法后面會運行popTarget,把target棧的最后一個元素賦值給Dep.target,當然如果不存在,那么Dep.target為空

通過代碼可以看出來,Dep永遠是對應當前正在計算的watcher,如果還不懂的話也沒關系,最后會通過一個例子把整個相關的流程都解釋一遍,

 

我們回到set方法實現:

判斷Dep.target是否存在,如果存在那么表示,我獲取這個屬性的值的是在運行watcher的get方法過程中(因為只有運行watcher的get方法,才會pushTarget,

而且在get方法后面還會運行popTarget把Dep.target重置),那么代表這個屬性值是和這個watcher是有關聯的,

好,既然是有關聯的,那么dep 就和watcher要進行關聯操作了,即執行dep.depend方法,這個方法實現了,把dep放進watcher的newDeps里面,為什么是newDeps

而不是deps呢,因為deps代表當前的依賴,而newDeps是表示新的依賴,即運行完這次更新之后,這個watcher的相關依賴,而運行get方法的時候會清楚當前的deps里面的依賴,

把newDeps里面的依賴復制到deps里面去

 

如果有childOb的話,也會把childOb同watcher關聯,因為如果有childOb,那么這個value肯定是一個對象,操作這個對象有兩個方式,要么獲取value對象的子屬性,要么設置

這個對象為新對象,這和childOb都有關系,所以需要把childOb同watcher關聯

 

再判斷value值是否為array,如果是的話,就去關聯array下面的對象

 

以上就是整個dep 和watcher 關聯的部分了

 

而set實現就是實現通知關聯的watcher進行更新,因為只有數據改變的時候,才會需要更新,所以把通知更新實現放在set方法里面

 

set具體實現:

當改變一個屬性對應的值的時候, 先獲取現在的值並做比較,如果相同就退出

然后不同的話,那么就進行復制操作

然后再執行更新操作,而在watcher部分說過,watcher的getter有3個類型:1:vm的render方法,2:computed的屬性,3:watch的屬性

這個時候,大多數情況是執行vm。render進行更新,當然這個render只是返回一個vNode(虛擬節點)然后調用vm的update進行真正的更新,

這里為了簡化理解,所以這樣說

 

上面就是整個關聯的部分,在總結一下過程就是:在創建watcher進行的時候,1和3方式的會立即去運行get方法,然后和相關運算用到的屬性對應的dep建立關聯,

而2對應的computed的屬性因為是懶更新的,所以在創建watcher的時候並不會立即執行關聯,而是在調用的時候才會用到,而一般是在vm.render方法里面會有調用

 

雙向綁定到這里就解釋完了,本來還想繼續在這里用一個例子來說明整個過程,但是發現貌似這篇文字已經很多了,那就放在下一篇《雙向綁定續》里面來解釋

注:因為是第一次寫博客,所以可能一些東西沒考慮完善,或者錯誤的地方,請指正,謝謝

 

 

 

 
        

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM