Vue雙向數據綁定原理深度解析


首先,什么是雙向數據綁定?Vue是三大MVVM框架之一,數據綁定簡單來說,就是當數據發生變化時,相應的視圖會進行更新,當視圖更新時,數據也會跟着變化

在分析其原理和代碼的時候,大家首先了解如下幾個js函數的作用:

1. [].slice.call(lis): 將偽數組轉換為真數組
2. node.nodeType: 得到節點類型
3. Object.defineProperty(obj, propertyName, {}): 給對象添加/修改屬性(指定描述符)
    configurable: true/false 是否可以重新define
   enumerable: true/false 是否可以枚舉(for..in / keys())
   value: 指定初始值
   writable: true/false value是否可以修改存取(訪問)描述符
   get: 函數, 用來得到當前屬性值
   set: 函數, 用來監視當前屬性值的變化
4. Object.keys(obj): 得到對象自身可枚舉的屬性名的數組
5. DocumentFragment: 文檔碎片(高效批量更新多個節點)
6. obj.hasOwnProperty(prop): 判斷prop是否是obj自身的屬性

如果想了解這些函數具體使用:請點擊這里

首先,我來看一下如何實現最基礎的數據綁定:

<body>
    <div>請輸入:<input type="text" id="inputId"/></div>
    <div>輸入的值為:<span id="showId"></span></div>
    

</body>
<script>
        var inputValue = document.getElementById('inputId');
        var showValue = document.getElementById('showId');
        var obj = {};

        Object.defineProperty(obj, 'msg', {
            enumerable: true,
            configurable: true,
            set (newVal) {
                showValue.innerHTML = newVal;
            }
        })

        inputValue.addEventListener('input', function(e) {
            obj.msg = e.target.value;
        })
</script>
View Code

 

對於vue來說,Vue.js則是通過數據劫持以及結合發布者-訂閱者來實現的數據綁定,數據劫持是利用ES5的Object.defineProperty(obj, key, val)來劫持各個屬性的的setter以及getter,在數據變動時發布消息給訂閱者,從而觸發相應的回調來更新視圖。

我們來看一下數據雙向綁定的流程圖:

1、實現一個數據監聽器Obverser,對data中的數據進行監聽,若有變化,通知相應的訂閱者。
2、實現一個指令解析器Compile,對於每個元素上的指令進行解析,根據指令替換數據,更新視圖。
3、實現一個Watcher,用來連接Obverser和Compile, 並為每個屬性綁定相應的訂閱者,當數據發生變化時,執行相應的回調函數,從而更新視圖。
4、構造函數 (new MVue({}))

 

我們來看一下對應的js代碼:

一、Obverser.js

function Observer(data) {
    // 保存data對象
    this.data = data;
    // 走起
    this.walk(data);
}

Observer.prototype = {
    walk: function(data) {
        var me = this;
        // 遍歷data中所有屬性
        Object.keys(data).forEach(function(key) {
            // 針對指定屬性進行處理
            me.convert(key, data[key]);
        });
    },
    convert: function(key, val) {
        // 對指定屬性實現響應式數據綁定
        this.defineReactive(this.data, key, val);
    },

    defineReactive: function(data, key, val) {
        // 創建與當前屬性對應的dep對象
        var dep = new Dep();
        // 間接遞歸調用實現對data中所有層次屬性的劫持
        var childObj = observe(val);
        // 給data重新定義屬性(添加set/get)
        Object.defineProperty(data, key, {
            enumerable: true, // 可枚舉
            configurable: false, // 不能再define
            get: function() {
                // 建立dep與watcher的關系
                if (Dep.target) {
                    dep.depend();
                }
                // 返回屬性值
                return val;
            },
            set: function(newVal) {
                if (newVal === val) {
                    return;
                }
                val = newVal;
                // 新的值是object的話,進行監聽
                childObj = observe(newVal);
                // 通過dep
                dep.notify();
            }
        });
    }
};

function observe(value, vm) {
    // value必須是對象, 因為監視的是對象內部的屬性
    if (!value || typeof value !== 'object') {
        return;
    }
    // 創建一個對應的觀察都對象
    return new Observer(value);
};


var uid = 0;

function Dep() {
    // 標識屬性
    this.id = uid++;
    // 相關的所有watcher的數組
    this.subs = [];
}

Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },

    depend: function() {
        Dep.target.addDep(this);
    },

    removeSub: function(sub) {
        var index = this.subs.indexOf(sub);
        if (index != -1) {
            this.subs.splice(index, 1);
        }
    },

    notify: function() {
        // 通知所有相關的watcher(一個訂閱者)
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};

Dep.target = null;
View Code

1). Observer
    * 用來對data所有屬性數據進行劫持的構造函數
    * 給data中所有屬性重新定義屬性描述(get/set)
    * 為data中的每個屬性創建對應的dep對象
2). Dep(Depend)
    * data中的每個屬性(所有層次)都對應一個dep對象
    * 創建的時機:
    * 在初始化define data中各個屬性時創建對應的dep對象
    * 在data中的某個屬性值被設置為新的對象時
    * 對象的結構
      {
        id, // 每個dep都有一個唯一的id
        subs //包含n個對應watcher的數組(subscribes的簡寫)
       }
     * subs屬性說明
     * 當一個watcher被創建時, 內部會將當前watcher對象添加到對應的dep對象的subs中
     * 當此data屬性的值發生改變時, 所有subs中的watcher都會收到更新的通知, 從而最終更新對應的界面

 

二、模板解析(Compile.js)

function Compile(el, vm) {
  // 保存vm
  this.$vm = vm;
  // 保存el元素
  this.$el = this.isElementNode(el) ? el : document.querySelector(el);
  // 如果el元素存在
  if (this.$el) {
    // 1. 取出el中所有子節點, 封裝在一個framgment對象中
    this.$fragment = this.node2Fragment(this.$el);
    // 2. 編譯fragment中所有層次子節點
    this.init();
    // 3. 將fragment添加到el中
    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 () {
    // 編譯fragment
    this.compileElement(this.$fragment);
  },

  compileElement: function (el) {
    // 得到所有子節點
    var childNodes = el.childNodes,
      // 保存compile對象
      me = this;
    // 遍歷所有子節點
    [].slice.call(childNodes).forEach(function (node) {
      // 得到節點的文本內容
      var text = node.textContent;
      // 正則對象(匹配大括號表達式)
      var reg = /\{\{(.*)\}\}/;  // {{name}}
      // 如果是元素節點
      if (me.isElementNode(node)) {
        // 編譯元素節點的指令屬性
        me.compile(node);
        // 如果是一個大括號表達式格式的文本節點
      } else if (me.isTextNode(node) && reg.test(text)) {
        // 編譯大括號表達式格式的文本節點
        me.compileText(node, RegExp.$1); // RegExp.$1: 表達式   name
      }
      // 如果子節點還有子節點
      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-on:click
      var attrName = attr.name;
      // 判斷是否是指令屬性
      if (me.isDirective(attrName)) {
        // 得到表達式(屬性值): test
        var exp = attr.value;
        // 得到指令名: on:click
        var dir = attrName.substring(2);
        // 事件指令
        if (me.isEventDirective(dir)) {
          // 解析事件指令
          compileUtil.eventHandler(node, me.$vm, exp, dir);
        // 普通指令
        } else {
          // 解析普通指令
          compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
        }

        // 移除指令屬性
        node.removeAttribute(attrName);
      }
    });
  },

  compileText: function (node, exp) {
    // 調用編譯工具對象解析
    compileUtil.text(node, this.$vm, exp);
  },

  isDirective: function (attr) {
    return attr.indexOf('v-') == 0;
  },

  isEventDirective: function (dir) {
    return dir.indexOf('on') === 0;
  },

  isElementNode: function (node) {
    return node.nodeType == 1;
  },

  isTextNode: function (node) {
    return node.nodeType == 3;
  }
};

// 指令處理集合
var compileUtil = {
  // 解析: v-text/{{}}
  text: function (node, vm, exp) {
    this.bind(node, vm, exp, 'text');
  },
  // 解析: v-html
  html: function (node, vm, exp) {
    this.bind(node, vm, exp, 'html');
  },

  // 解析: v-model
  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;
    });
  },

  // 解析: v-class
  class: function (node, vm, exp) {
    this.bind(node, vm, exp, 'class');
  },

  // 真正用於解析指令的方法
  bind: function (node, vm, exp, dir) {
    /*實現初始化顯示*/
    // 根據指令名(text)得到對應的更新節點函數
    var updaterFn = updater[dir + 'Updater'];
    // 如果存在調用來更新節點
    updaterFn && updaterFn(node, this._getVMVal(vm, exp));

    // 創建表達式對應的watcher對象
    new Watcher(vm, exp, function (value, oldValue) {/*更新界面*/
      // 當對應的屬性值發生了變化時, 自動調用, 更新對應的節點
      updaterFn && updaterFn(node, value, oldValue);
    });
  },

  // 事件處理
  eventHandler: function (node, vm, exp, dir) {
    // 得到事件名/類型: click
    var eventType = dir.split(':')[1],
      // 根據表達式得到事件處理函數(從methods中): test(){}
      fn = vm.$options.methods && vm.$options.methods[exp];
    // 如果都存在
    if (eventType && fn) {
      // 綁定指定事件名和回調函數的DOM事件監聽, 將回調函數中的this強制綁定為vm
      node.addEventListener(eventType, fn.bind(vm), false);
    }
  },

  // 得到表達式對應的value
  _getVMVal: function (vm, exp) {
    var val = vm._data;
    exp = exp.split('.');
    exp.forEach(function (k) {
      val = val[k];
    });
    return val;
  },

  _setVMVal: function (vm, exp, value) {
    var val = vm._data;
    exp = exp.split('.');
    exp.forEach(function (k, i) {
      // 非最后一個key,更新val的值
      if (i < exp.length - 1) {
        val = val[k];
      } else {
        val[k] = value;
      }
    });
  }
};

// 包含多個用於更新節點方法的對象
var updater = {
  // 更新節點的textContent
  textUpdater: function (node, value) {
    node.textContent = typeof value == 'undefined' ? '' : value;
  },

  // 更新節點的innerHTML
  htmlUpdater: function (node, value) {
    node.innerHTML = typeof value == 'undefined' ? '' : value;
  },

  // 更新節點的className
  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;
  },

  // 更新節點的value
  modelUpdater: function (node, value, oldValue) {
    node.value = typeof value == 'undefined' ? '' : value;
  }
};
View Code

1.模板解析的關鍵對象: compile對象
2.模板解析的基本流程:
   1). 將el的所有子節點取出, 添加到一個新建的文檔fragment對象中
   2). 對fragment中的所有層次子節點遞歸進行編譯解析處理
        * 對表達式文本節點進行解析
        * 對元素節點的指令屬性進行解析
        * 事件指令解析
        * 一般指令解析
   3). 將解析后的fragment添加到el中顯示
3.解析表達式文本節點: textNode.textContent = value
   1). 根據正則對象得到匹配出的表達式字符串: 子匹配/RegExp.$1
   2). 從data中取出表達式對應的屬性值
   3). 將屬性值設置為文本節點的textContent
4.事件指令解析: elementNode.addEventListener(事件名, 回調函數.bind(vm))
v-on:click="test"
   1). 從指令名中取出事件名
   2). 根據指令的值(表達式)從methods中得到對應的事件處理函數對象
   3). 給當前元素節點綁定指定事件名和回調函數的dom事件監聽
   4). 指令解析完后, 移除此指令屬性
5.一般指令解析: elementNode.xxx = value
  1). 得到指令名和指令值(表達式)
  2). 從data中根據表達式得到對應的值
  3). 根據指令名確定需要操作元素節點的什么屬性
      * v-text---textContent屬性
      * v-html---innerHTML屬性
      * v-class--className屬性
  4). 將得到的表達式的值設置到對應的屬性上
  5). 移除元素的指令屬性

所以Compile可以歸納為幾點:

  * 用來解析模板頁面的對象的構造函數(一個實例)
  * 利用compile對象解析模板頁面
  * 每解析一個表達式(非事件指令)都會創建一個對應的watcher對象, 並建立watcher與dep的關系
  * complie與watcher關系: 一對多的關系

 

三、Watcher.js

function Watcher(vm, exp, cb) {
  this.cb = cb;  // callback
  this.vm = vm;
  this.exp = exp;
  this.depIds = {};  // {0: d0, 1: d1, 2: d2}
  this.value = this.get();
}

Watcher.prototype = {
  update: function () {
    this.run();
  },
  run: function () {
    // 得到最新的值
    var value = this.get();
    // 得到舊值
    var oldVal = this.value;
    // 如果不相同
    if (value !== oldVal) {
      this.value = value;
      // 調用回調函數更新對應的界面
      this.cb.call(this.vm, value, oldVal);
    }
  },
  addDep: function (dep) {
    if (!this.depIds.hasOwnProperty(dep.id)) {
      // 建立dep到watcher
      dep.addSub(this);
      // 建立watcher到dep的關系
      this.depIds[dep.id] = dep;
    }
  },
  get: function () {
    Dep.target = this;
    // 獲取當前表達式的值, 內部會導致屬性的get()調用
    var value = this.getVMVal();

    Dep.target = null;
    return value;
  },

  getVMVal: function () {
    var exp = this.exp.split('.');
    var val = this.vm._data;
    exp.forEach(function (k) {
      val = val[k];
    });
    return val;
  }
};
/*

const obj1 = {id: 1}
const obj12 = {id: 2}
const obj13 = {id: 3}
const obj14 = {id: 4}

const obj2 = {}
const obj22 = {}
const obj23 = {}
// 雙向1對1
// obj1.o2 = obj2
// obj2.o1 = obj1

// obj1: 1:n
obj1.o2s = [obj2, obj22, obj23]

// obj2: 1:n
obj2.o1s = {
  1: obj1,
  2: obj12,
  3: obj13
}
*/
View Code 

  * 模板中每個非事件指令或表達式都對應一個watcher對象
  * 監視當前表達式數據的變化
  * 創建的時機: 在初始化編譯模板時
  * 對象的組成
    {
        vm, //vm對象
        exp, //對應指令的表達式
        cb, //當表達式所對應的數據發生改變的回調函數
        value, //表達式當前的值
       depIds //表達式中各級屬性所對應的dep對象的集合對象
       //屬性名為dep的id, 屬性值為dep
     }

 

四、數據代理(MVVM.js)

/*
相關於Vue的構造函數
 */
function MVVM(options) {
  // 將選項對象保存到vm
  this.$options = options;
  // 將data對象保存到vm和datq變量中
  var data = this._data = this.$options.data;
  //將vm保存在me變量中
  var me = this;
  // 遍歷data中所有屬性
  Object.keys(data).forEach(function (key) { // 屬性名: name
    // 對指定屬性實現代理
    me._proxy(key);
  });

  // 對data進行監視
  observe(data, this);

  // 創建一個用來編譯模板的compile對象
  this.$compile = new Compile(options.el || document.body, this)
}

MVVM.prototype = {
  $watch: function (key, cb, options) {
    new Watcher(this, key, cb);
  },

  // 對指定屬性實現代理
  _proxy: function (key) {
    // 保存vm
    var me = this;
    // 給vm添加指定屬性名的屬性(使用屬性描述)
    Object.defineProperty(me, key, {
      configurable: false, // 不能再重新定義
      enumerable: true, // 可以枚舉
      // 當通過vm.name讀取屬性值時自動調用
      get: function proxyGetter() {
        // 讀取data中對應屬性值返回(實現代理讀操作)
        return me._data[key];
      },
      // 當通過vm.name = 'xxx'時自動調用
      set: function proxySetter(newVal) {
        // 將最新的值保存到data中對應的屬性上(實現代理寫操作)
        me._data[key] = newVal;
      }
    });
  }
};
View Code

1.通過一個對象代理對另一個對象中屬性的操作(讀/寫)
2.通過vm對象來代理data對象中所有屬性的操作
3.好處: 更方便的操作data中的數據
4.基本實現流程
  1). 通過Object.defineProperty()給vm添加與data對象的屬性對應的屬性描述符
  2). 所有添加的屬性都包含getter/setter
  3). 在getter/setter內部去操作data中對應的屬性數據

 

五、總結

 1.dep與watcher的關系: 多對多

    * 一個data中的屬性對應對應一個dep, 一個dep中可能包含多個watcher(模板中有幾個表達式使用到了屬性)
    * 模板中一個非事件表達式對應一個watcher, 一個watcher中可能包含多個dep(表達式中包含了幾個data屬性)
    * 數據綁定使用到2個核心技術
    * defineProperty()
    * 消息訂閱與發布

  2.雙向數據綁定

    1). 雙向數據綁定是建立在單向數據綁定(model==>View)的基礎之上的
    2). 雙向數據綁定的實現流程:
         * 在解析v-model指令時, 給當前元素添加input監聽
         * 當input的value發生改變時, 將最新的值賦值給當前表達式所對應的data屬性


免責聲明!

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



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