vue雙向綁定實現之源碼解析

參考

本篇文章是基於參考且我的思考後以最簡單的方式寫出: 關於vue依賴收集部分:ustbhuangyi.github.io/vue-analysi… vue源碼解析:github.com/answershuto…javascript

源碼實現

雙向綁定的實現流程以下html

雙向綁定原理

根據上圖(參考自:github.com/DMQ/mvvm) 雙向綁定必需要實現如下幾點:vue

  • 實現一個數據監聽器Observer,可以對數據的全部屬性進行監聽
  • 實現一個訂閱者的容器Dep,可以收集訂閱者,而且數據變動時可以通知訂閱者
  • 實現一個指令解析器Compile,對每一個元素節點的指令進行掃描和解析,根據指令模板替換數據,綁定相應的更新函數,初始化視圖
  • 實現一個觀察者Watcher,做爲鏈接Observer和Compile的橋樑,可以訂閱並收到每一個屬性變更的通知,執行指令對應的回調函數,更新視圖

源代碼執行流程圖: java

雙向綁定

監聽數據的實現

數據劫持監聽的源碼流程圖以下:node

數據劫持源碼流程圖

初始化數據initData

initData初始化數據,監聽數據的變化,使得數據的變化可以響應react

  • 獲取data並判斷
  • 獲取Props,判斷data中的屬性是否在Props中被定義,被定義發出警告
  • 將data上的屬性代理到vm實例上(代理不是取引用,爲vm實例定義相同的屬性,經過getter去獲取data上的值,setter去修改data上的值)
  • observe數據,綁定數據data
function initData (vm: Component) {

  /*獲得data數據*/
  //若是配置中的data是函數,則執行函數,若是是對象,則直接獲取
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}

  /*判斷是不是對象*/
  //若是data不是對象,且不是正式環境的狀況下,發出警告
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }

  // proxy data on instance
  /*遍歷data對象*/
  const keys = Object.keys(data)
  const props = vm.$options.props
  let i = keys.length

  //遍歷data中的數據
  while (i--) {
    /*保證data中的key不與props中的key重複,props優先,若是有衝突會產生warning*/
    if (props && hasOwn(props, keys[i])) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${keys[i]}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(keys[i])) {
      /*判斷是不是保留字段*/

      /*將data上面的屬性代理到了vm實例上,即假設data有屬性a,this.data.a能夠經過this.a被訪問*/
      proxy(vm, `_data`, keys[i])
    }
  }
  // observe data
  /*從這裏開始咱們要observe了,開始對數據進行綁定,這裏有尤大大的註釋asRootData,這步做爲根數據,下面會進行遞歸observe進行對深層對象的綁定。*/
  observe(data, true /* asRootData */)
}
複製代碼

proxy(代理數據)

咱們在訪問vue中data、props、computed、methods的屬性時,都是經過this.propertyName來訪問 在初始化時,咱們也會對這些數據進行區分。假設data中有屬性a,咱們如何經過this.a來訪問this.data.a呢? proxy就是作這件事,幫助咱們把數據代理到vm實例上。git

//target爲代理的實例,proxyObjectName爲被代理的對象名,proxyKey爲被代理對象的屬性
function proxy(target,proxyObjectName,proxyKey){
 Object.defineProperty(target,proxyKey,{
  enumerable:true,
  configurable:true,
  get:function proxyGetter(){
   //注意這裏的this在運行時指向target
   return this[proxyObjectName][proxyKey]
  },
  set:function proxySetter(newVal){
   this[proxyObjectName][proxyKey] = newVal
  }
 })
}
複製代碼

proxy以後,打印target對象看不到被代理對象的屬性,但經過target[proxyKey]卻能訪問到,target[proxyKey]的修改也會對target[proxyObjectName][proxyKey]進行修改,這是和直接複製引用不一樣的地方github

observe

observe函數嘗試建立一個Observer實例(ob),若是成功建立Observer實例則返回新的Observer實例,若是已有Observer實例則返回現有的Observer實例。 Observer實例放在當前對象express

  • 判斷當前數據是否爲對象
  • 判斷__ob__屬性是否存在,存在則直接引用,不存在則建立Observer實例
  • 若是是根數據則計數
/** * Attempt to create an observer instance for a value, * returns the new observer if successfully observed, * or the existing observer if the value already has one. */
 /* 嘗試建立一個Observer實例(__ob__),若是成功建立Observer實例則返回新的Observer實例,若是已有Observer實例則返回現有的Observer實例。 */
export function observe (value: any, asRootData: ?boolean): Observer | void {
  /*判斷是不是一個對象*/
  if (!isObject(value)) {
    return
  }
  let ob: Observer | void

  /*這裏用__ob__這個屬性來判斷是否已經有Observer實例,若是沒有Observer實例則會新建一個Observer實例並賦值給__ob__這個屬性,若是已有Observer實例則直接返回該Observer實例*/
 
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (

    /*這裏的判斷是爲了確保value是單純的對象,而不是函數或者是Regexp等狀況。*/
    observerState.shouldConvert &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {

    /*若是是根數據則計數,後面Observer中的observe的asRootData非true*/
    ob.vmCount++
  }
  return ob
}
複製代碼

Vue的響應式數據都會有一個__ob__的屬性做爲標記,裏面存放了該屬性的觀察器,也就是Observer的實例,防止重複綁定。 因此判斷數據是否可響應,看當前數據是否包含__ob__屬性數組

Observer(數據監聽器)

Observer實例存在於每一個響應式數據的__ob__屬性中,Observer的構造函數遍歷對象的全部屬性,對其進行雙向綁定,使屬性可以響應式。

Observer實例應該具備如下屬性:

  • value:any 保存當前對象的值
  • dep:Dep 保存依賴收集
  • vmCount:number 保存vm實例將當前對象做爲根數據root $data的次數

具備如下方法:

  • walk(obj:Object),對對象類型的數據進行綁定
  • observeArray(array : Array),對數組類型的成員進行綁定(對成員調用observe)

步驟以下:

  • 將Observer實例綁定到目標對象的__ob__屬性
  • 判斷當前目標對象是否爲數組
  • 是數組則監聽數組的方法,而且爲數組的每一個成員嘗試構建一個Observer實例(即調用observe函數)
  • 是對象則響應式對象的屬性
/** * 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. */
 /* Observer類被賦予給每一個響應式的對象,一旦擁有Observer實例,Obsever轉化目標對象屬性的 getter/setters,使得getter可以進行依賴收集,setter可以發佈更新 */
 
export class {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0

    /* 將Observer實例綁定到data的__ob__屬性上面去,以前說過observe的時候會先檢測是否已經有__ob__對象存放Observer實例了,def方法定義能夠參考https://github.com/vuejs/vue/blob/dev/src/core/util/lang.js#L16 */
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      //數組的響應體如今調用方法的時候,因此直接用下標修改數組的成員沒法響應
      /* 若是是數組,將修改後能夠截獲響應的數組方法替換掉該數組的原型中的原生方法,達到監聽數組數據變化響應的效果。 這裏若是當前瀏覽器支持__proto__屬性,則直接覆蓋當前數組對象原型上的原生數組方法,若是不支持該屬性,則直接覆蓋數組對象的原型。 */
      //判斷是否支持__proto__屬性
      //若是支持,則直接覆蓋當前數組對象原型上的數組方法
      //若是不支持,則逐個覆蓋目標數組的方法
      const augment = hasProto 
        ? protoAugment  /*直接覆蓋原型的方法來修改目標對象*/
        : copyAugment   /*定義(覆蓋)目標對象或數組的某一個方法*/
        
      augment(value, arrayMethods, arrayKeys)
      
      //對數組的每個成員進行observe
      /*若是是數組則須要遍歷數組的每個成員進行observe*/
      this.observeArray(value)
      
    } else {

      /*若是是對象則直接walk進行綁定*/
      this.walk(value)
    }
  }

  /** * Walk through each property and convert them into * getter/setters. This method should only be called when * value type is Object. */
  walk (obj: Object) {
    const keys = Object.keys(obj)

    /*walk方法會遍歷對象的每個屬性進行defineReactive綁定*/
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }

  /** * Observe a list of Array items. */
  observeArray (items: Array<any>) {

    /*數組須要遍歷每個成員進行observe*/
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}


複製代碼

def函數實現

/** * Define a property. */
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

複製代碼

數組的響應

若是修改數組的成員,而且該成員是個對象,那隻須要遞歸對數組的成員進行雙向綁定便可。 但若是咱們進行pop、push等操做的時候,push進去的對象沒有進行過雙向綁定,那麼咱們如何監聽數組的成員變化呢?VUE提供的方法是重寫push、pop、shift、unshift、splice、sort、reverse這七個數組方法。 數組的類型也是object,能夠理解爲數組爲具備特定實現方法的object,咱們須要對這些方法進行監聽並響應

重寫的數組(對象)

根據Observer中對數組的響應式處理,若是瀏覽器支持__proto__屬性,則直接修改__proto__爲VUE重寫的數組(對象),若是不支持,則須要覆蓋當前數組的每個方法爲VUE重寫的數組(對象)中的方法,逐個覆蓋。

/** * Augment an target Object or Array by intercepting * the prototype chain using __proto__ */
 /*直接覆蓋原型的方法來修改目標對象或數組*/
function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}

/** * Augment an target Object or Array by defining * hidden properties. */
/* istanbul ignore next */
/*定義(覆蓋)目標對象或數組的某一個方法*/
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

複製代碼

重寫的VUE數組(arrayMethods)實現:

  • 獲取原生數組的原型,根據原生數組的原型建立新的數組對象arrayMethods,防止污染原生數組方法
  • 重寫數組對象的push、pop、shift、unshift、splice、sort、reverse方法

重寫方法的步驟:

  • 調用原生數組方法
  • 根據方法名來獲取新增數據,例如若是是splice只取第三位參數開始的數據
  • 從__ob__屬性中獲取Observer實例,對新增的數組數據進行observe即調用observeArray方法,綁定新增的數組數據(因爲目標(須要響應的)數組的方法最終會繼承或者被重寫數組(arrayMethods)的方法覆蓋,因此在重寫的方法內能夠調用到目標數組的屬性__ob__)
  • 調用Observer實例中訂閱者容器Dep的發佈數據更新的方法notify(),通知全部訂閱當前數據的觀察者
/* * not type checking this file because flow doesn't play well with * dynamically accessing methods on Array prototype */
 //在這裏沒有采用類型檢測,是由於flow這個框架在數組原型方法上表現很差
 //從這裏能夠了解到爲何vue3.0會採用typeScript開發

import { def } from '../util/index'

/*取得原生數組的原型*/
const arrayProto = Array.prototype
/*建立一個新的數組對象,修改該對象上的數組的七個方法,防止污染原生數組方法*/
export const arrayMethods = Object.create(arrayProto)

/** * Intercept mutating methods and emit events */
 /*這裏重寫了數組的這些方法,在保證不污染原生數組原型的狀況下重寫數組的這些方法,截獲數組的成員發生的變化,執行原生數組操做的同時dep通知關聯的全部觀察者進行響應式處理*/
[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  // cache original method
  /*將數組的原生方法緩存起來,後面要調用*/
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator () {
    // avoid leaking arguments:
    // http://jsperf.com/closure-with-arguments
    let i = arguments.length
    const args = new Array(i)
    while (i--) {
      args[i] = arguments[i]
    }
    /*調用原生的數組方法*/
    const result = original.apply(this, args)

    /*數組新插入的元素須要從新進行observe才能響應式*/
    const ob = this.__ob__
    //記錄新插入的元素
    let inserted
    //若是是splice(startIndex,removeNumber,...addItems),則下標爲2開始的爲新增元素
    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
    /*dep通知全部註冊的觀察者進行響應式處理*/
    ob.dep.notify()
    return result
  })
})

複製代碼

若是當前瀏覽器支持__proto__屬性,則能夠直接覆蓋整個屬性爲VUE重寫的數組對象,若是沒有該屬性,則必須經過def對當前數組對象的方法進行覆蓋,效率較低,因此優先使用第一種。

從上述重寫的數組對象能夠看出,若是修改了經過數組下標或者設置length來修改數組,是沒法監聽的,因此沒法爲新增元素進行綁定,可是咱們能夠經過Vue.set或者splice方法

實現編譯器

Compile

Compile的主要任務:

  • 解析模板指令,將模板中的變量替換成數據,生成抽象語法樹
  • 初始化渲染視圖
  • 爲每一個指令對應的Node節點綁定更新函數,添加Watcher爲數據的訂閱者
  • Watcher收到數據變更的通知,調用更新視圖

由於遍歷解析的過程有屢次操做dom節點,爲提升性能和效率,會先將vue實例根節點的el轉換成文檔碎片fragment進行解析編譯操做,解析完成,再將fragment添加回原來的真實dom節點中

主要步驟:

  • 把DOM節點轉換成fragment節點
  • 遍歷編譯全部節點,區分元素節點和文本節點編譯
  • 若是是元素節點,遍歷解析元素節點中的指令屬性例如v-text、v-on
  • 根據指令類型,編譯指令(普通指令如v-text、v-if等,事件指令如v-on:click)
  • 普通指令則調用指令處理合集(compileUtil)中的方法,獲取對應指令的視圖更新方法,生成觀察者Wathcer(經過傳入實例化Wathcher的回調函數中,閉包獲取到須要更新的node,與node產生聯繫),綁定視圖更新方法與Watcher的實例。
function Compile(el) {
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);
    if (this.$el) {
        //生成fragment
        this.$fragment = this.node2Fragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
    }
}

Compile.prototype = {
  node2Fragment: function(el) {
      var fragment = document.createDocumentFragment(), child;
      // 將原生節點拷貝到fragment
      while (child = el.firstChild) {
          fragment.appendChild(child);
      }
      return fragment;
  },
	init: function() { 
      this.compileElement(this.$fragment); 
  },
  compileElement: function(el) {
      //遍歷編譯當前結點及其全部子節點
      var childNodes = el.childNodes, me = this;
      [].slice.call(childNodes).forEach(function(node) {
          var text = node.textContent;
          var reg = /\{\{(.*)\}\}/;	// 表達式文本
          if (me.isElementNode(node)) {
              //按元素節點方式編譯
              me.compile(node);
          } else if (me.isTextNode(node) && reg.test(text)) {
              //文本節點編譯
              me.compileText(node, RegExp.$1);
          }
          //遍歷編譯子節點
          if (node.childNodes && node.childNodes.length) {
              me.compileElement(node);
          }
      });
   },
   compile: function(node) {
      //遍歷當前結點的屬性
      var nodeAttrs = node.attributes, me = this;
      [].slice.call(nodeAttrs).forEach(function(attr) {
          // 規定:指令以 v-xxx 命名
          // 如 <span v-text="content"></span> 中指令爲 v-text
          var attrName = attr.name;	// v-text
          // 判斷是否知足v-開頭的屬性
          if (me.isDirective(attrName)) {
              var exp = attr.value; // content
              var dir = attrName.substring(2);	// 取到指令text
              if (me.isEventDirective(dir)) {
                // 編譯事件指令, 如 v-on:click
                  compileUtil.eventHandler(node, me.$vm, exp, dir);
              } else {
                // 編譯普通指令,如v-text
                  compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
              }
          }
      });
    }    
};

複製代碼

compileUtil(指令處理集合)

var compileUtil = {
    text: function(node, vm, exp) {
        this.bind(node, vm, exp, 'text');
    },
    bind: function(node, vm, exp, dir) {
        //這段代碼爲核心,功能爲:初始化視圖,綁定視圖更新函數到Watcher實例中
        var updaterFn = updater[dir + 'Updater'];
        // 第一次初始化視圖
        updaterFn && updaterFn(node, vm[exp]);
        // 實例化觀察者,此操做會在對應的屬性消息訂閱器中添加了該訂閱者watcher
        new Watcher(vm, exp, function(value, oldValue) {
          // 一旦屬性值有變化,會收到通知執行此更新函數,更新視圖
          // 閉包保存node,與當前Watcher產生聯繫
            updaterFn && updaterFn(node, value, oldValue);
        });
    },
    html: function(node, vm, exp) {
        this.bind(node, vm, exp, 'html');
    },

    model: function(node, vm, exp) {
        this.bind(node, vm, exp, 'model');

        var me = this,
            val = this._getVMVal(vm, exp);
	    
        node.addEventListener('input', function(e) {
            var newValue = e.target.value;
            if (val === newValue) {
                return;
            }

            me._setVMVal(vm, exp, newValue);
            val = newValue;
        });
    },

    class: function(node, vm, exp) {
        this.bind(node, vm, exp, 'class');
    },
    // 事件處理
    eventHandler: function(node, vm, exp, dir) {
        var eventType = dir.split(':')[1],
            fn = vm.$options.methods && vm.$options.methods[exp];

        if (eventType && fn) {
            node.addEventListener(eventType, fn.bind(vm), false);
        }
    },
     _getVMVal: function(vm, exp) {
        var val = vm;
        exp = exp.split('.');
        exp.forEach(function(k) {
            val = val[k];
        });
        return val;
    },
    _setVMVal: function(vm, exp, value) {
        var val = vm;
        exp = exp.split('.');
        exp.forEach(function(k, i) {
            // 非最後一個key,更新val的值
            if (i < exp.length - 1) {
                val = val[k];
            } else {
                val[k] = value;
            }
        });
    }
};
複製代碼

updater(指令對應的更新函數)

// 更新函數
var updater = {
    textUpdater: function(node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    },

    htmlUpdater: function(node, value) {
        node.innerHTML = typeof value == 'undefined' ? '' : value;
    },

    classUpdater: function(node, value, oldValue) {
        var className = node.className;
        className = className.replace(oldValue, '').replace(/\s$/, '');

        var space = className && String(value) ? ' ' : '';

        node.className = className + space + value;
    },

    modelUpdater: function(node, value, oldValue) {
        node.value = typeof value == 'undefined' ? '' : value;
    }
};

複製代碼

觀察者Watcher

我的理解,總的來講Watcher就是個橋樑,做用是綁定視圖更新函數與任意被監聽的數據,當被監聽的數據更新時,調用視圖更新的回調

注意這兩個Dep的差異:

  • 屬性的Deps(存放在屬性的__ob__中,是一個Observer實例)會在getter中收集Watcher實例,當數據更新時,Deps會通知全部訂閱當前數據的Watcher實例進行視圖更新
  • Watcher的Deps用於保存當前觀察者依賴數據的訂閱者中心(可能有多個,例如v-text="a&b",在實例化Watcher時會往a、b裏的Dep都添加訂閱,因此須要獲取a、b的DepId防止重複添加訂閱),防止重複添加watcher到依賴數據的訂閱者中心中

值得注意的另外一個點:

  • Watcher類不只被應用到視圖中指令綁定時,還被應用到watch(監聽某個數據)、computed(依賴監聽的數據返回值)當中

Watcher主要函數

  • get():獲取當前表達式/函數的值,觸發依賴收集(這裏是調用Dep的depend方法,depend方法會調用當前Dep.target.addDep方法)
  • addDep():把當前的watcher訂閱到這個數據持有的dep的subs中(這是真正的收集依賴)
  • update():調度者接口,給Dep調用,根據是否lazy來選擇直接調用或者推入異步隊列
  • run():調用get()獲取最新數據,調用視圖更新的回調接口
  • addDep():添加一個Dep到當前Dep容器中
  • cleanupDeps():清理依賴收集,調用當前Watcher全部依賴數據的Dep中的removeSub,清除當前Watcher實例
  • teardown():將自身從全部依賴數據的Dep中清除

Watcher類的源代碼

export default class Watcher {
  vm: Component;      //存放vm實例
  expression: string;
  cb: Function;       //視圖更新的回調函數
  id: number;       
  deep: boolean;      //是否採用深度監聽(用於watch中的deep參數)
  user: boolean;      //是不是一個用戶行爲的監聽(watch、computed),用於判斷放入哪個隊列(有兩條異步隊列),和是否提示警告
  lazy: boolean;      //true 下次觸發時獲取expOrFn當前值;false 當即獲取當前值
  sync: boolean;      //是否爲同步執行回調
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: ISet;
  newDepIds: ISet;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ) {
    this.vm = vm
    /*_watchers存放訂閱者實例*/
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    /*把表達式expOrFn解析成getter*/
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
        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
        )
      }
    }
 
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /** * Evaluate the getter, and re-collect dependencies. */
   /*得到getter的值而且從新進行依賴收集*/
  get () {
    /*將自身watcher觀察者實例設置給Dep.target,用以依賴收集。*/
    pushTarget(this)
    let value
    const vm = this.vm

    /* 執行了getter操做,看似執行了渲染操做,實際上是執行了依賴收集。 在將Dep.target設置爲自身觀察者實例之後,執行getter操做。 譬如說如今的的data中可能有a、b、c三個數據,getter渲染須要依賴a跟c, 那麼在執行getter的時候就會觸發a跟c兩個數據的getter函數, 在getter函數中便可判斷Dep.target是否存在而後完成依賴收集, 將該觀察者對象放入閉包中的Dep的subs中去。 */
    //若是是用戶行爲的監聽,則發出警告
    //調用表達式,這裏的getter指的是當前watcher對應的表達式,但表達式會觸發依賴數據的getter
    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)
    }
    // 這裏用了touch來形容,意味着觸發
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    /*若是存在deep,則觸發每一個深層對象的依賴,追蹤其變化*/
    if (this.deep) {
      /*遞歸每個對象或者數組,觸發它們的getter,使得對象或數組的每個成員都被依賴收集,造成一個「深(deep)」依賴關係*/
      traverse(value)
    }

    /*將觀察者實例從target棧中取出並設置給Dep.target*/
    popTarget()
    this.cleanupDeps()
    return value
  }

  /** * Add a dependency to this directive. */
   /*添加一個依賴關係到Deps集合中*/
  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.addSub(this)
      }
    }
  }

  /** * Clean up for dependency collection. */
   /*清理依賴收集*/
  cleanupDeps () {
    /*移除全部觀察者對象*/
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      //移除舊的Dep在新的Dep中不存在的與當前Watcher的綁定
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

  /** * Subscriber interface. * Will be called when a dependency changes. */
   /* 調度者接口,當依賴發生改變的時候進行回調。 */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      /*同步則執行run直接渲染視圖*/
      this.run()
    } else {
      /*異步推送到觀察者隊列中,由調度者調用。*/
      queueWatcher(this)
    }
  }

  /** * Scheduler job interface. * Will be called by the scheduler. */
   /* 調度者工做接口,將被調度者回調。 */
  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        /* 即使值相同,擁有Deep屬性的觀察者以及在對象/數組上的觀察者應該被觸發更新,由於它們的值可能發生改變。 */
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        /*設置新的值*/
        this.value = value

        /*觸發回調渲染視圖*/
        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)
        }
      }
    }
  }

  /** * Evaluate the value of the watcher. * This only gets called for lazy watchers. */
   /*獲取觀察者的值,僅用於computed watchers*/
  evaluate () {
    if (this.dirty) {
      this.value = this.get()
      this.dirty = false
    }
    return this.value
  }

  /** * Depend on all deps collected by this watcher. */
   /*收集該watcher的全部deps依賴,僅用於Computed Watcher*/
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  /** * Remove self from all dependencies' subscriber list. */
   /*將自身從全部依賴收集訂閱列表刪除*/
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      /*從vm實例的觀察者列表中將自身移除,因爲該操做比較耗費資源,因此若是vm實例正在被銷燬則跳過該步驟。*/
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}
複製代碼

pushTarget與popTarget

export function pushTarget (_target: Watcher) {
  //將上一個Watcher存放到棧中 
  if (Dep.target) targetStack.push(Dep.target)
  //將當前watcher設爲target
  Dep.target = _target
}

export function popTarget (){
 //把 Dep.target 恢復成上一個狀態
  Dep.target = targetStack.pop()
}
複製代碼

依賴收集過程

  • 經過構造函數調用get()收集依賴
  • get調用pushTarget將當前Watcher實例放入Dep.target,觸發當前表達式(也就是觸發表達式或函數內依賴數據的getter)
  • 被依賴數據的getter中經過調用dep.depend方法,dep.depend調用Dep.target.addDep(this)
  • Dep.target.addDep(this)也就是watcher.addDep(dep:Dep)把當前依賴數據的Dep放入Watcher實例中,並調用dep.addSub(this)
  • dep.addSub(this)把當前Watcher實例放入dep的subs數組中,也就是把Watcher當成訂閱者收集起來
  • 若是是deep watch,完成深層遍歷觸發getter以後,popTarget,把Dep.target恢復成上一個狀態,並調用cleanupDeps清空依賴

依賴清空

Watcher中 this.deps 和 this.newDeps 表示 Watcher 實例持有的 Dep 實例的數組;而 this.depIds 和 this.newDepIds 分別表明 this.deps 和 this.newDeps 的 id Set

Vue的mount過程是經過mountComponent函數,其中有一段比較重要的邏輯,大體以下:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)
複製代碼

在這裏實例化Watcher的時候回調用get()方法,也就是updateComponent方法,在其中會調用render()方法,這個方法會生成渲染VNode,而且在這個過程當中會對 vm 上的數據訪問,這個時候就觸發了數據對象的getter。 考慮到 Vue 是數據驅動的,因此每次數據變化都會從新 render,那麼 vm._render() 方法又會再次執行,並次觸發數據的 getters,因此 Wathcer 在構造函數中會初始化 2 個 Dep 實例數組,newDeps 表示新添加的 Dep 實例數組,而 deps 表示上一次添加的 Dep 實例數組。

那麼爲何須要作 deps 訂閱的移除呢,在添加 deps 的訂閱過程,已經能經過 id 去重避免重複訂閱了。

考慮到一種場景,咱們的模板會根據 v-if 去渲染不一樣子模板 a 和 b,當咱們知足某種條件的時候渲染 a 的時候,會訪問到 a 中的數據,這時候咱們對 a 使用的數據添加了 getter,作了依賴收集,那麼當咱們去修改 a 的數據的時候,理應通知到這些訂閱者。那麼若是咱們一旦改變了條件渲染了 b 模板,又會對 b 使用的數據添加了 getter,若是咱們沒有依賴移除的過程,那麼這時候我去修改 a 模板的數據,會通知 a 數據的訂閱的回調,這顯然是有浪費的。

所以 Vue 設計了在每次添加完新的訂閱,會移除掉舊的訂閱,這樣就保證了在咱們剛纔的場景中,若是渲染 b 模板的時候去修改 a 模板的數據,a 數據訂閱回調已經被移除了,因此不會有任何浪費,真的是很是讚歎 Vue 對一些細節上的處理。

Dep

Dep是訂閱者中心,數組成員是Watcher,在屬性的getter中收集Watcher 須要注意的是,getter中調用Dep的depend方法,而不是直接調用addSub方法 depend方法調用Watcher實例中的addDep,addDep方法將dep放入watcher的dep數組中,再調用dep的addSub方法收集依賴

import type Watcher from './watcher'
import { remove } from '../util/index'

let uid = 0

/** * A dep is an observable that can have multiple * directives subscribing to it. */
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()
    }
  }
}

// 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
const targetStack = []

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

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

複製代碼

defineReactive的實現

/** * Define a reactive property on an Object. */
export function defineReactive ( obj: Object, key: string, val: any, customSetter?: Function ) {
  /*在閉包中定義一個dep對象*/
  const dep = new Dep()

  //這裏能夠關注到,若是屬性是不可配置的,將取消綁定
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  /*若是以前該對象已經預設了getter以及setter函數則將其取出來,新定義的getter/setter中會將其執行,保證不會覆蓋以前已經定義的getter/setter。*/
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set

  /*對象的子對象遞歸進行observe並返回子節點的Observer對象*/
  let childOb = observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {

      /*若是本來對象擁有getter方法則執行*/
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {

        /*進行依賴收集*/
        dep.depend()
        if (childOb) {

          /*子對象進行依賴收集,其實就是將同一個watcher觀察者實例放進了兩個depend中,一個是正在自己閉包中的depend,另外一個是子元素的depend*/
          childOb.dep.depend()
        }
        if (Array.isArray(value)) {

          /*是數組則須要對每個成員都進行依賴收集,若是數組的成員仍是數組,則遞歸。*/
          dependArray(value)
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {

      /*經過getter方法獲取當前值,與新值進行比較,一致則不須要執行下面的操做*/
      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()
      }
      if (setter) {

        /*若是本來對象擁有setter方法則執行setter*/
        setter.call(obj, newVal)
      } else {
        val = newVal
      }

      /*新的值須要從新進行observe,保證數據響應式*/
      childOb = observe(newVal)

      /*dep對象通知全部的觀察者*/
      dep.notify()
    }
  })
}


複製代碼

注意到重要的兩點:

  • 若是屬性是沒法配置的,即configurable爲false時,會取消雙向綁定
  • 若是對象含有子對象,會對其進行深層次的雙向綁定,而且會把watcher觀察者實例遞歸放入子元素的dep中,這樣子元素的更改都會引發依賴當前對象的視圖更新

總結

如今再回來看這個執行流程,思路就能清晰了

雙向綁定

從學習雙向綁定源碼的過程,能學習到如下幾點

  • data和props不能重複屬性,props的優先級更高
  • 查看__ob__來判斷數據是否被監聽:查看數據是否被監聽,能夠查看當前數據是否具備__ob__屬性,由於__ob__屬性存放着Obeserver實例,實例化Observer的過程就是數據監聽的過程
  • 數組只能經過七種方法來爲新增元素設置監聽:經過數據監聽的過程,能夠知道對數組的監聽只能經過push、pop、shift、unshift、reverse、sort、splice等方法對修改的元素進行監聽,因此經過數組下表的形式去修改元素,是不會被監聽到的。
  • 不須要雙向綁定時,能夠選擇凍結數據:經過defineReactive使數據可響應的過程,能夠知道當數據被設置了configurable爲false時,是不會進行綁定的,由於Object.defineProperty此時無效,針對這一點,在咱們肯定不須要雙向綁定的該數據的時候(因爲雙向綁定會實例化Observer,當數據量大時,最好取消雙向綁定),能夠設置數據的configurable爲false,這一點能夠經過Object.freeze實現.
  • 子屬性的修改會也會引發視圖更新:經過defineReactive能夠得知,watcher會被綁定到當前依賴數據的Dep中,也會被綁定到依賴數據的子屬性的Dep中,這意味着子屬性被修改,也會引發視圖更新
  • 在初始化階段後新增子屬性沒法被監聽:因爲初始化階段新增子屬性不存在,因此沒法被監聽,只能等到下一次render()函數被執行後,才能被監聽,這也是咱們在設置v-model爲一個新的對象屬性時,會發現它的雙向綁定失效,再反覆的UI操做後,又可以生效,這是render()函數執行後再次綁定的結果。因此咱們要使用官方提供的Vue.$set()爲對象添加新的監聽屬性
  • Watcher的屬性:Watcher做爲綁定視圖更新函數與依賴數據的橋樑,其屬性決定了它的做用。
    • deep屬性決定了該Watcher實例是個深監聽的觀察者,遞歸的觸發子屬性的getter,使得子屬性被觸發後Watcher會被子屬性的Dep收集。
    • sync決定數據更新時,當即調用視圖更新的回調函數仍是異步推送到觀察者隊列queueWatcher()
    • user屬性標誌是否爲一個用戶行爲的Watcher,若是是,在錯誤時進行警告提示,而且用於判斷放入哪一個一個異步隊列

Object.freeze() 方法能夠凍結一個對象(數組也是對象)。一個被凍結的對象不再能被修改;凍結了一個對象則不能向這個對象添加新的屬性,不能刪除已有屬性,不能修改該對象已有屬性的可枚舉性、可配置性、可寫性,以及不能修改已有屬性的值。此外,凍結一個對象後該對象的原型也不能被修改。 要使對象不可變,須要遞歸凍結每一個類型爲對象的屬性(深凍結):

//深凍結函數
function deepFreeze(obj){
   var propNames = Object.getOwnPropertyNames(obj)
   
   // 在凍結自身以前凍結屬性
   propNames.forEach((name)=>{
   	let prop = obj[name]
	//若是prop是個對象,則凍結它
	if(typeof prop === 'object' && prop !== null){
	   deepFreeze(prop)
	}
   })
   //凍結當前對象
   return Object.freeze(obj)

}
複製代碼
相關文章
相關標籤/搜索