实现一个计算属性computed

124 阅读1分钟
  • 😳
  • 解决模板中复杂逻辑运算的问题 (模板与逻辑分离)
  • 计算属性会缓存其依赖数据的上一次计算结果( 缓存到实例)
  • 多次复用一个相同值的结果,计算属性只调用一次
  • 只有在内部逻辑依赖的数据发生变化时,才会被再次调用

模拟

  • 目录

结构.jpg

  • main.js
  • 看代码备注顺序和注释就行🤣
let vm = new Vue({
  el: "#app",
  template: `
    <span>{{a}}</span>
    <span>+</span>
    <span>{{b}}</span>
    <span>=</span>
    <span>total:{{total}}</span>
    <span>||</span>
    <span>num:{{res}}</span>
    `,

  data() {
    return {
      a: 2,
      b: 2,
    };
  },

  computed: {
    total() {
      console.log("computed total");
      return this.a + this.b;
    },

    res() {
        return this.a * this.b
    }
  },
});

console.log(vm);
vm.a = 100
  • index.js
let Vue = (function () {
  let computedData = {}, // 计算属性方法的容器
    domData = {}; // 插值{{}}的节点容器

  console.log(computedData, "computedData");
  console.log(domData, "domData");

  class React {
    constructor(options) {
      this.$el = document.querySelector(options.el);
      this.$data = options.data();

      this._init(this, options.computed, options.template);
    }

    _init(vm, computed, template) {
      // 数据源$data 处理函数
      this.dataReactive(vm);

      // 计算属性处理函数
      this.computedReactive(vm, computed);

      // 渲染函数
      this.render(vm, template);
    }

    // 1:数据源 $data 处理
    dataReactive(vm) {
      let _data = vm.$data;

      for (let key in _data) {
        (function (key) {
          // 将数据源挂载到实例上去
          Object.defineProperty(vm, key, {
            get() {
              return _data[key];
            },

            set(newValue) {
              _data[key] = newValue;

              // 数据更新触发处
              // 7:更新DOM节点(传入实例this和 值)
              this.update(vm, key);

              // 8: 更新依赖(传入实例this和 值)
              this.updateComputedData(vm, key, function (keys) {
                // 更新依赖结果到DOM节点
                vm.update(vm, keys);
              });
            },
          });
        })(key);
      }
    }

    // 2:计算属性处理
    computedReactive(vm, computed) {
      // 初始化
      this._initComputedData(vm, computed);

      // 遍历计算属性容器-将计算属性方法的结果缓存到实例上去
      for (let key in computedData) {
        (function (key) {
          Object.defineProperty(vm, key, {
            get() {
              return computedData[key].value;
            },

            set(newValue) {
              computedData[key].value = newValue;
            },
          });
        })(key);
      }
    }

    // 3:计算属性初始化
    _initComputedData(vm, computed) {
      // 遍历整个computed对象, 通过描述符获取键(计算属性的方法)并配置
      for (let key in computed) {
        /**
         * 获取计算属性方法的时候判断下是直接xxx() 还是 xxx: {get() {}}
         */
        let descriptor = Object.getOwnPropertyDescriptor(computed, key),
          descriptFn = descriptor.value.get
            ? descriptor.value.get
            : descriptor.value;

        // 往容器对象添加键值对: total{} num()
        computedData[key] = {};
        computedData[key].value = descriptFn.call(vm);
        // 添加计算属性的get 函数,访问时返回计算属性方法运行的结果
        computedData[key].get = descriptFn.bind(vm);
        // 计算属性的依赖值
        computedData[key].dep = this._collectDep(descriptFn);
      }
    }

    // 4:渲染函数
    render(vm, template) {
      let container = document.createElement("div"),
        _el = vm.$el;

      container.innerHTML = template;

      // 处理DOM树
      let domTree = this._compivatemplate(vm, container);

      // 将DOM树打入页面
      _el.appendChild(domTree);
    }

    // 5:编译模板
    _compivatemplate(vm, container) {
      // 取出所有节点
      let allNodes = container.getElementsByTagName("*"),
        nodeItem = null;

      for (let i = 0; i < allNodes.length; i++) {
        nodeItem = allNodes[i];

        let matched = nodeItem.textContent.match(/\{\{(.+?)\}\}/g);
        // 如果当前项有插值语法 {{xxx}} 就替换成实例$data对应的变量值
        if (matched) {
          nodeItem.textContent = nodeItem.textContent.replace(
            /\{\{(.+?)\}\}/g,
            function (node, key) {
              // 将有插值括号的元素添加进节点容器 domData
              domData[key.trim()] = nodeItem;
              // 将插值语法的字符变量替换成实例上对应的值
              return vm[key.trim()];
            }
          );
        }
      }

      // 此时5个模板元素的文本对应this.a的值 + this.b的值 = undefined
      return container;
    }

    // 6: 依赖收集
    _collectDep(fn) {
      // 将计算属性的方法转成字符 匹配出内部的依赖数据 如:this.a, this.b
      let _collection = fn.toString().match(/this.(.+?)/g);

      // 遍历依赖数据项
      if (_collection.length > 0) {
        for (let i = 0; i < _collection.length; i++) {
          // 去掉依赖数据的this->提取出原始值,
          // 并返回-> 在初始化函数_initComputedData里丢入依赖dep
          _collection[i] = _collection[i].split(".")[1];
        }
      }

      return _collection;
    }

    // 9: 更新DOM节点
    update(vm, key) {
      domData[key].textContent = vm[key];
    }

    // 依赖更新
    updateComputedData(vm, key, fn) {
      let _dep = null;

      for (let _key in computedData) {
        // 取出计算属性方法的依赖项
        _dep = computedData[_key].dep;

        for (let i = 0; i < _dep.length; i++) {
          // 将依赖项与数据源比对-> 重新运行计算属性方法->更新实例上的缓存结果
          if (_dep[i] === key) {
            vm[_key] = computedData[_key].get();
            // 通过闭包传递更新结果到数据变动-导致跟新触发处
            fn(_key);
          }
        }
      }
    }
  }

  return React;
})();