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>

复制代码
相关文章
相关标签/搜索