从0到1实现vue(完整)

203 阅读3分钟

复制可直接运行

<!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>
  </head>
  <body>
    <div id="app">
      <p>a.a: {{ a.a }}</p>
      <p>b: {{ b }}</p>
      <p>{{ add }}</p>
    </div>
    <script>
      // 发布订阅模式  订阅在有发布[fn1,fn2,fn3]
      // 绑定的方法都有update属性
      function Dep() {
        this.subs = [];
      }
      // 把订阅收集起来为后面依次执行准备
      Dep.prototype.addSub = function(sub) {
        this.subs.push(sub);
      };
      // 通知每个订阅执行update方法
      Dep.prototype.notify = function() {
        this.subs.forEach(sub => sub.update());
      };

      // 订阅器
      function Watcher(vm, exp, fn) {
        // exp=>vm下的表达式
        // console.log('Watcher', vm, exp, fn);
        this.fn = fn;
        // 把vm和exp绑到this上,在方法上可以共享并拿到
        this.vm = vm;
        this.exp = exp;
        Dep.target = this; //为了读数据的时候收集Watcher
        // debugger;
        let val = vm;
        let arr = exp.split('.');
        arr.forEach(k => {
          val = val[k];
        });
        Dep.target = null;
      }
      // 执行传进来的函数
      Watcher.prototype.update = function() {
        let val = this.vm;
        let arr = this.exp.split('.');
        arr.forEach(k => {
          val = val[k];
        });
        this.fn(val);
        // this.fn();
      };

      function Vue(options) {
        this.$options = options;
        var data = (this._data = this.$options.data);

        observe(data);
        //data劫持完然后代理给实例vm
        for (let key in data) {
          Object.defineProperty(this, key, {
            numerable: true,
            get() {
              return this._data[key];
            },
            set(newVal) {
              this._data[key] = newVal;
            }
          });
        }
        initCumputed.call(this);
        // 处理完数据开始编译模板 传入要挂载的节点和上下文上下文里有data还有其他东西可以处理
        //  挂载放在最后面
        new Compile(options.el, this);
      }

      function initCumputed() {
        let vm = this;
        // 拿到computed属性
        let computed = this.$options.computed;
        Object.keys(computed).forEach(k => {
          // debugger;
          Object.defineProperty(vm, k, {
            get:
              typeof computed[k] === 'function' ? computed[k] : computed[k].get, //判断是函数还是对象
            set() {}
          });
        });
      }

      function Compile(el, vm) {
        // 挂到vm的$el属性
        vm.$el = document.querySelector(el);
        let fragment = document.createDocumentFragment();
        // 把el内的内容都转移到内存中
        while ((child = vm.$el.firstChild)) {
          fragment.append(child);
        }
        replace(fragment);

        function replace(fragment) {
          // 拿到子节点的类数组然后转成数组进行遍历
          Array.from(fragment.childNodes).forEach(function(node) {
            // 获取文本节点
            let text = node.textContent;
            let reg = /\{\{(.*)\}\}/;
            // 判断是否文本节点
            if (node.nodeType === 3 && reg.test(text)) {
              // 取第一个匹配的值
              // console.log(RegExp.$1); //a.a.a  a.b
              // 因为slint所以左右多出了空格
              let arr = RegExp.$1.trim().split('.'); //[a,a]
              let val = vm;
              arr.forEach(function(key) {
                val = val[key]; //第一次val[k]  第二次val[k][k] 一个个的往下取...
              });
              // 当视图改变要重新刷新视图,数据一遍可以再执行内容的替换
              // 替换之前先订阅下数据,在Watcher构造函数里读取了一遍data会触发get方法,所以在get里把前面缓存的vm=>this给存进收集器
              // 想要知道是哪一个newVal就要取到当前实例vm和当前vm下对应的属性名,然后从属性上取值
              new Watcher(vm, RegExp.$1.trim(), function(newVal) {
                //数据改变了  函数要接收一个新的值
                node.textContent = text.replace(reg, newVal);
              });
              // 替换文本中要替换的内容
              node.textContent = text.replace(reg, val);
            }
            // 如果有子节点就继续替换
            if (node.childNodes) {
              replace(node);
            }
          });
        }

        vm.$el.appendChild(fragment);
      }

      //观察对象给对象增加Object.defineProperty
      function Observe(data) {
        // 实例化一个收集器,
        let dep = new Dep();
        for (let key in data) {
          // debugger;
          let val = data[key];
          // 这边对值进行再劫持
          observe(val);
          //data:{a:1}  => 换Object.defineProperty形式写入data
          Object.defineProperty(data, key, {
            enumerable: true,
            get() {
              // debugger; //因为没有把加在原型上的方法丢到最上面,所以调不到addSub方法,可以实例化是因为变量声明提升
              Dep.target && dep.addSub(Dep.target); //收集[Watcher]
              // console.log(dep);
              return val;
            },
            set(newVal) {
              if (val === newVal) {
                return;
              }
              val = newVal;
              // 新赋值的对象也要加上数据劫持
              observe(newVal);
              // 把新值拿去更新视图
              // console.log(dep.subs);
              dep.notify(); //让所有的update方法执行
            }
          });
        }
      }
      function observe(data) {
        if (typeof data !== 'object') return;
        return new Observe(data);
      }

      // let watcher = new Watcher('', '', function() {
      //   console.log(1);
      // });

      // let dep = new Dep();
      // dep.addSub(watcher); //将watcher放进收集依赖
      // dep.addSub(watcher);
      // dep.addSub(watcher);
      // console.log(dep.subs);

      // dep.notify();

      // vm._data.a= 2;

      var vm = new Vue({
        el: '#app',
        data: {
          a: { a: '1' },
          b: '2'
        },
        computed: {
          add() {
            return this.a.a + this.b;
          }
        }
      });
      console.log(vm._data.a);
    </script>
  </body>
</html>