Vue源碼分析系列五: 響應式原理

原理

  • vue.js採用數據劫持結合發佈者-訂閱者模式的方式,經過Object.defineProperty來觸發各個屬性的getter以及setter,在數據變更時發佈消息給訂閱者,並觸發相應的監聽回調。

具體步驟

  • 第一步
    1. 初始化Vue實例,將Vue實例上綁定 dep 屬性(依賴收集)
    2. 調用Vue原型上的 _observe() 以及 _compile() 方法。、
  • 第二步
    1. 經過 _observe() 方法重寫data對象的setter/getter方法,當咱們對data對象的屬性進行改變的時候,可以發佈消息給訂閱者(Watcher),觸發監聽函數(Watcher原型上的update()方法)
  • 第三步
    1. 經過 _compile() 方法解析模板字符串,即 v-model/v-click/v-html等
    2. 在解析模板的同時,往dep中添加相應的監聽器。
    3. 在這裏操做Vue實例中的 $data
  • 第四步
    1. 經過Watcher構造函數,收集須要監聽的元素
    2. 在構造函數的原型上定義 update()方法,經過數據的改變從而改變視圖。
  • 最後上代碼(刪除註釋說明的話,核心代碼150行不到)
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
    <style>
      body {
        line-height: 120px;
        text-align: center;
        background: #fff;
        color: yellow;
      }
      h1 {
        background: red;
        display: inline-block;
        width: auto;
        padding: 12px 24px;
        margin: 0 auto;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <form>
        <input type="text" v-model="number" />
        <button type="button" v-click="increment">increment</button>
      </form>
      <h1 v-html="number"></h1>
    </div>

    <script>
      function Vue(options) {
        this._init(options);
      }

      Vue.prototype._init = function(options) {
        this.$options = options;
        this.$el = document.querySelector(options.el);
        this.$data = options.data;
        this.$methods = options.methods;

        // 依賴收集: 對dom進行編譯解析(解析指令或模板語法)的時候收集依賴,在數據改變的時候(setter 中)進行更新。
        this.dep = {};
        
        this._observe(this.$data);
        this._compile(this.$el);
      };

      Vue.prototype._observe = function(obj) {
        var value;
        var _this = this;
        for (key in obj) {
          if (obj.hasOwnProperty(key)) {
            // 收集依賴,對全部屬性都進行一個監聽,在這裏是 number
            // 在 dep 對象中添加一個 number 屬性,其值是一個數組,數組中存放的是 Watcher 實例
            // 若是發現 number 發生了改變,就在 setter 中循環遍歷notice,執行 Watcher 實例的 update 方法,統一更新 number
            _this.dep[key] = {
              notice: []
            };
            value = _this.$data[key]; // 將 value 賦值爲最初是的 number 值

            var dep = _this.dep[key];
            Object.defineProperty(_this.$data, key, {
              get() {
                return value;
              },
              set(newVal) {
                value = newVal;
                dep.notice.forEach(item => {
                  // 這裏的item就是Watcher實例,能夠調用update()方法,通知更新
                  // 有幾處用到了 number 屬性,number.notice 就有幾個 Watcher 實例
                  // notice: {
                  //   attr: "number",
                  //   el: Input,
                  //   name: "input",
                  //   value: "value",
                  //   vm: {...}
                  // }
                  item.update();
                });
              }
            });
          }
        }
      };

      Vue.prototype._compile = function(root) {
        // #app 根元素
        var nodes = root.children; // [form, h1]
        var _this = this;
        for (var i = 0, len = nodes.length; i < len; i++) {
          var node = nodes[i];
          if (node.children.length) {
            this._compile(node);
          }

          if (node.hasAttribute("v-click")) {
            // 下面這種方式,有點問題,噹噹即執行函數執行完後,attrVal泄露出去了
            // 致使解析 v-model 的時候,拿到的 attrVal 的值時 increment,而不是number
            // 要注意
            // 用這種方式也能夠實現,那麼在解析'v-model'的時候,須要將當前 (解析'v-model') if語句中var出來的attrVal傳入到當即執行函數中去
            // 或者咱們統一使用ES6中的 let 來聲明 attrVal 變量。

            // var attrVal = node.getAttribute('v-click');
            // node.addEventListener('click', (function () {
            //   return _this.$methods[attrVal].bind(_this.$data);
            // })())

            // 這種方式就是噹噹即執行函數被銷燬以後,var出來的attrVal不會泄露出來,污染別的變量,可是能夠經過閉包能夠訪問獲得。
            node.onclick = (function() {
              var attrVal = node.getAttribute("v-click");
              // 注意:methods方法裏面用的 this,指的是 options 裏面的 data,因此須要將方法的上下文半綁定爲 data
              return _this.$methods[attrVal].bind(_this.$data);
            })();
          }

          if (node.hasAttribute("v-model") && node.tagName === "INPUT") {
            var attrVal = node.getAttribute("v-model");

            node.addEventListener(
              "input",
              (function(i) {
                // 由於 input 用到了 number,因此須要將 dep.number.notice 中添加 Watcher 實例,
                // 在 number 改變時,input 的值就須要改變
                _this.dep[attrVal].notice.push(
                  new Watcher("input", node, _this, attrVal, "value")
                );
                return function() {
                  // 當咱們在 input 裏面輸入數據的時候,就會觸發 number 的 setter 屬性
                  _this.$data[attrVal] = nodes[i].value;
                };
              })(i)
            );
          }

          if (node.hasAttribute("v-html")) {
            var attrVal = node.getAttribute("v-html");
            _this.dep[attrVal].notice.push(
              new Watcher("h1", node, _this, attrVal, "innerHTML")
            );
          }
        }
      };

      class Watcher {
        constructor(name, el, vm, attr, value) {
          // name: input
          // el: current element
          // vm
          // attr: number
          // value: 元素的value (innerHTML, input.value)
          this.name = name;
          this.el = el;
          this.vm = vm;
          this.attr = attr;
          this.value = value;
          this.update();
        }
        update() {
          this.el[this.value] = this.vm.$data[this.attr];
        }
      }

      window.onload = function() {
        let vm = new Vue({
          el: "#app",
          data: {
            number: 0
          },
          methods: {
            increment() {
              this.number++;
            }
          }
        });
      };
    </script>
  </body>
</html>

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