简单vue如何实现数组的依赖收集 ?

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script>
    function proxy(vm) {
      Object.keys(vm.$data).forEach(key => {
        Object.defineProperty(vm, key, {
          get() {
            return vm.$data[key];
          },
          set(newVal) {
            vm.$data[key] = newVal;
          },
        });
      })
    }

    class Vue1 {
      constructor(options) {
        this.$options = options;
        this.$data = options.data;
        observe(this.$data);
        proxy(this);

        // 用数据和元素进行编译
        new Compiler(options.el, this)
      }
    }

    class Compiler {
      constructor(el, vm) {
        this.$vm = vm;
        this.$el = document.querySelector(el);
        if (this.$el) {
          this.compile(this.$el);
        }
      }

      compile(el) {
        const childNodes = el.childNodes;
        Array.from(childNodes).forEach(node => {
          if (this.isElement(node)) {
            // 编译元素
            this.compileElement(node)
          } else if (this.isInterpolation(node)) {
            // 编译插值文本
            this.compileText(node);
          }
          if (node.childNodes && node.childNodes.length > 0) {
            this.compile(node);
          }
        })
      }

      compileElement(node) {
        let nodeAttrs = node.attributes;
        Array.from(nodeAttrs).forEach(attr => {
          let attrName = attr.name;
          let exp = attr.value;
          if (this.isDirective(attrName)) {
            let dir = attrName.substring(2);
            this[dir] && this[dir](node, exp);
          } else if (this.isEvent(attrName)) {
            const dir = attrName.substring(1)
            this.eventHandler(node, exp, dir)
          }
        });
      }
      eventHandler(node, exp, dir) {
        const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp]
        node.addEventListener(dir, fn.bind(this.$vm))
      }

      compileText(node) {
        this.update(node, RegExp.$1, 'text')
      }

      isElement(node) {
        return node.nodeType == 1;
      }

      // 判断插值:文本且{{}}
      isInterpolation(node) {
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
      }

      isDirective(attr) {
        return attr.indexOf("k-") == 0;
      }

      // 判断是不是@开头的方法
      isEvent(attr) {
        return attr.startsWith('@')
      }

      text(node, exp) {
        node.textContent = this.$vm[exp];
      }

      html(node, exp) {
        node.innerHTML = this.$vm[exp];
      }

      update(node, exp, dir) {
        const fn = this[dir + 'Updater'];
        fn && fn(node, this.$vm[exp]);
        new Watcher(this.$vm, exp, function (val) {
          fn && fn(node, val);
        });
      }

      textUpdater(node, val) {
        node.textContent = val;
      }

      htmlUpdater(node, val) {
        node.innerHTML = val;
      }

      model(node, exp) {
        this.update(node, exp, 'model')

        node.addEventListener('input', (e) => {
          this.$vm[exp] = e.target.value
        })
      }

      modelUpdater(node, val) {
        node.value = val
      }
    }

    function defineReactive(obj, key, val) {
      observe(val);
      const dep = new Dep();
      Object.defineProperty(obj, key, {
        get() {
          Dep.target && dep.addDep(Dep.target);
          return val
        },
        set(newVal) {
          if (newVal !== val) {
            observe(newVal);
            val = newVal
            dep.notify();
          }
        }
      })
    }

    // 对象响应化:遍历每个key,定义getter、setter
    function observe(obj) {
      if (typeof obj !== 'object' || obj == null) {
        return
      }
      if (Array.isArray(obj)) {
        const dep = new Dep();
        // 如果是数组, 重写原型
        obj.__proto__ = arrayProto
        obj.ob = dep;
        // 传入的数据可能是多维度的,也需要执行响应式
        for (let i = 0; i < obj.length; i++) {
          observe(obj[i])
        }
      } else {
        for (const key in obj) {
          // 给对象中的每一个方法都设置响应式
          defineReactive(obj, key, obj[key])
        }
      }
    }

    class Watcher {
      constructor(vm, key, updateFn) {
        this.vm = vm;
        this.key = key;
        this.updateFn = updateFn;
        Dep.target = this;
        // 触发一下getter
        this.vm[this.key];
        Dep.target = null;
      }

      update() {
        this.updateFn.call(this.vm, this.vm[this.key]);
      }
    }

    class Dep {
      constructor() {
        this.deps = [];
      }

      addDep(dep) {
        this.deps.push(dep);
      }

      notify() {
        this.deps.forEach(dep => dep.update());
      }
    }

    const orginalProto = Array.prototype;
    const arrayProto = Object.create(orginalProto); // 先克隆一份Array的原型出来
    const methodsToPatch = [
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse'
    ]
    methodsToPatch.forEach(method => {
      arrayProto[method] = function () {
        // 执行原始操作
        orginalProto[method].apply(this, arguments);
      }
    })
  </script>
</head>

<body>
  <div id="app">
    <div>
      <p>{{name1}}</p>
      <p>{{name2}}</p>
      <p k-text="name1"></p>
      <button @click="click">click</button>
      <p>{{name3}}</p>
      <input k-model="name4" />
      <p>{{name4}}</p>
      <button @click="clickarr">clickarr</button>
      <p>{{arr}}</p>
    </div>
  </div>

  <script>
    new Vue1({
      el: '#app',
      data: {
        name1: 'name1',
        name2: 'name2',
        name3: 1,
        name4: 'name4',
        arr: [],
      },
      methods: {
        click: function () {
          this.name3 = this.name3 + 1;
        },
        clickarr: function () {
          console.log('clickarr', this.arr);
          this.arr.push(this.arr.length);
        }
      },
    });
  </script>
</body>

</html>

这是一个简单的vue,这里是完整的代码,页面上点击操作数组,数组一直在增加,但是页面上却不展示新数组,需要怎么改才能实现数组的响应式显示在页面上?

图片.png

阅读 1.3k
2 个回答

其实实现不难,两个改造点

  1. watcher 中只通过触发 getter 为对象收集了依赖,那么需要添加 数组 的场景

    class Watcher {
     constructor(vm, key, updateFn) {
         this.vm = vm;
         this.key = key;
         this.updateFn = updateFn;
         Dep.target = this;
         // 触发一下getter
         const target = this.vm[this.key];
    
         // add: 为 数组 收集依赖 -----------------
         if (Array.isArray(target)) {
             target.ob.addDep(this);
         }
         // add: end
    
         Dep.target = null;
     }
    
     update() {
         this.updateFn.call(this.vm, this.vm[this.key]);
     }
    }
  2. 在拦截数组的 api 逻辑中触发通知即可

    methodsToPatch.forEach((method) => {
     arrayProto[method] = function () {
         // 执行原始操作
         orginalProto[method].apply(this, arguments);
         
         // add: 触发通知
         this.ob.notify();
         // add: end
     };
    });

你这个版本的实现有点老了,可以去看下 vue3 的响应式系统的实现,和这版差距很大了已经

这个就是vue的数据更新,直接使用$set更新你的数组即可。如果需要再页面打印页可以给相应的html标签加key, 每次set的时候counter++即可

    #set 更新对象或者数组
    this.$set(this.Object, 'b', 2)

    #给打印的元素加key就可以随时跟新内容
    <div :key="counter" v-for="(v,k) in Object">
        {{ v, k }}
    </div>
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题