Skip to content
On this page

简易 vue2 响应、收集依赖、触发更新实现

前言

多年前学习 vue2 源码时,看到 vue2 的响应式实现,感觉很神奇,但是一直没有深入研究,最近在学习 vue3 源码,发现 vue3 的响应式实现和 vue2 的响应式实现有很大的不同,所以想着先从 vue2 的响应式实现开始,一步一步的实现一个简易的 vue2 响应式系统,以此来加深对 vue2 响应式原理的理解。

响应式实现

js
// watcher执行
class Dep {
   constructor() {
      // 这个方法就是用来保存当前watcher的
      this.subs = [];
   }
   addSubs(sub) {
      this.subs.push(sub);
   }
   notify() {
      this.subs.forEach(watcher => {
         watcher.update();
      })
   }
}

// 数据劫持做完需要进行观察
class Wtacher {
   constructor(expr, vm, cb) {
      this.expr = expr;
      this.vm = vm;
      this.cb = cb;
      // 这个类需要进行数据观察 在第一次初始化的时候
      // 三个值 expr 表达式 vm viewModel实体 cb回调 用来将新的值传递回去
      // 设置新值的时候需要先获取一下旧值,这样才能进行对比
      this.oldValue = this.getValue();
   }
   getValue() {
      // 这里是关键  在我们进行观察的时候  将当前watcher实例绑定到全局Dep上
      Dep.target = this;
      let oldValue = CompilerUtils.getValue(this.expr, this.vm);
      // 获取值完毕后,我们再讲全局Dep上的观察者去掉
      Dep.target = null;
      return oldValue;
   }
   update() {
      // 拿取新值,如果两值不相等则通过回调返回
      let newValue = this.getValue();
      if (newValue !== this.oldValue) {
         this.cb && this.cb(newValue);
      }
   }
}

// 数据劫持
class Observer {
   constructor(data) {
      this.observer(data);
   }
   observer(data) {
      // 值劫持object类型的值
      if (data && typeof data === 'object') {
         for (const key in data) {
            if (data.hasOwnProperty(key)) {
               const value = data[key];
               this.defineReactive(data, key, value);
            }
         }
      }
   }
   defineReactive(target, key, value) {
      // 如果值也是对象,则需要再次进行劫持
      this.observer(value);
      // 需要在数据劫持的时候给这个数据创建独一无二的观察者
      let dep = new Dep();
      Object.defineProperty(target, key, {
         get() {
            Dep.target && dep.addSubs(Dep.target);
            // 这里是获取值时
            return value;
         },
         set: (newValue) => {
            // 这里是设置值时  在新值不相等的时候  设置
            if (newValue !== value) {
               value = newValue;
               // 新设置的值也需要进行劫持
               this.observer(value);
               // 在值更新的时候我们执行发布
               dep.notify();
            }
         }
      })
   }
}

// 编译类
class Compiler {
   constructor(el, vm) {
      // 如果el是元素  则设置  如果不是  则获取
      this.el = this.isElementNode(el) ? el : document.querySelector(el);
      this.vm = vm;
      // 现在我们已经拿到元素了  现在拿到处理过后的元素
      let fragment = this.node2fragment(this.el);

      // 对文档碎片进行处理
      this.compiler(fragment);

      // 将碎片添加回源文档
      this.el.appendChild(fragment);
   }
   isElementNode(el) {
      return el.nodeType === 1;
   }
   isDirective(name) {
      return name.startsWith('v-');
   }
   node2fragment(el) {
      // 首先创建一个文档碎片
      let fragment = document.createDocumentFragment();

      // 将el中的元素append到文档碎片中
      let firstChild;
      while (firstChild = el.firstChild) {
         // appendChild具有移动性,可以将文档已有元素移动
         fragment.appendChild(firstChild);
      }
      // 添加进去后,我们返回文档碎片
      return fragment;
   }
   compiler(fragment) {
      // 这里进行文档碎片处理
      let childNodes = fragment.childNodes;
      [...childNodes].forEach(node => {
         // 如果是元素节点,则进行元素节点编译,否则进行文本节点编译
         if (this.isElementNode(node)) {
            this.compilerElement(node);
            // 如果是元素节点,我们需要再次进行编译
            this.compiler(node);
         } else {
            this.compilerText(node);
         }
      })
   }
   compilerElement(node) {
      // 如果是元素节点,我们需要拿到属性 attributes 类似于  v-model="myAdd"这样的
      let attributes = node.attributes;
      [...attributes].forEach(attr => {
         let { name, value: expr } = attr; // name v-model name myAdd
         // 拿到属性后,我们需要去判断 v- 开头的属性
         if (this.isDirective(name)) {
            let [, directive] = name.split('-');
            // 因为v-on:click 格式不一样  所以要进一步进行处理
            let [directiveName, eventName] = directive.split(':');
            // 拿到之后我们使用编译工具进行编译
            CompilerUtils[directiveName](node, expr, this.vm, eventName);
         }
      })
   }
   compilerText(node) {
      // 拿到之后  我们需要  {{}}  这样格式的文本节点
      let textContent = node.textContent;
      if (/\{\{(.+?)\}\}/.test(textContent)) {
         CompilerUtils["text"](node, textContent, this.vm);
      }
   }
}

// 编译工具
let CompilerUtils = {
   getValue(expr, vm) {
      return expr.split('.').reduce((data, cur, curIndex, arr) => {
         return data[cur];
      }, vm.$data);
   },
   setValue(expr, vm, newValue) {
      return expr.split('.').reduce((data, cur, curIndex, arr) => {
         if (curIndex === arr.length - 1) {
            return data[cur] = newValue;
         }
         return data[cur];
      }, vm.$data);
   },
   // 使用name作为key值
   model(node, expr, vm) {
      // 拿到之后  第一步进行 获取值
      let fn = this.updater['model'];
      new Wtacher(expr, vm, (newValue) => {
         // 在这里再次调用设置方法
         fn(node, newValue);
      })
      let value = this.getValue(expr, vm);
      fn(node, value);
      node.addEventListener('input', (e) => {
         this.setValue(expr, vm, e.currentTarget.value);
      })
   },
   getTextContent(expr, vm) {
      return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
         return this.getValue(args[1], vm);
      })
   },
   text(node, expr, vm) {
      // 一样  第一步 获取值 但是文本都是 {{a}}{{b}} 这样的,所以我们要去匹配出来
      let fn = this.updater['text'];
      let value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
         new Wtacher(args[1], vm, () => {
            // 设置文本不一样需要循环 需要对每一个值进行观察
            fn(node, this.getTextContent(expr, vm));
         });
         return this.getValue(args[1], vm);
      })
      fn(node, value);
   },
   html(node, expr, vm) {
      // 拿到之后  第一步进行 获取值
      let fn = this.updater['html'];
      new Wtacher(expr, vm, (newValue) => {
         // 在这里再次调用设置方法
         fn(node, newValue);
      })
      let value = this.getValue(expr, vm);
      fn(node, value);
   },
   on(node, expr, vm, eventName) {
      node.addEventListener(eventName, (e) => {
         vm.$methods[expr].call(vm, e);
      })
   },
   updater: {
      model(node, value) {
         node.value = value;
      },
      text(node, value) {
         node.textContent = value;
      },
      html(node, value) {
         node.innerHTML = value;
      },
   }
}

// 基类
class Vue {
   constructor(options) {
      this.$el = options.el;
      this.$data = options.data;
      this.$computed = options.computed;
      this.$methods = options.methods;

      // 如果存在el则进行编译处理
      if (this.$el) {
         // 在此做数据劫持
         new Observer(this.$data);

         // 代理$data到vm
         this.proxyData();

         // 计算属性
         for (const key in this.$computed) {
            Object.defineProperty(this.$data, key, {
               get: () => {
                  return this.$computed[key].call(this);
               }
            })
         }

         new Compiler(this.$el, this);
      }
   }
   proxyData() {
      for (const key in this.$data) {
         if (this.$data.hasOwnProperty(key)) {
            const value = this.$data[key];
            Object.defineProperty(this, key, {
               get: () => {
                  return this.$data[key];
               },
               set: (newValue) => {
                  if (newValue !== value) {
                     this.$data[key] = newValue;
                  }
               }
            })
         }
      }
   }
}

html 测试部分

html
<body>
   <div class="container">
      <input type="text" v-model="myAdd">
      {{outProp}}
      {{outAge}}
      <div>{{company.name}}</div>
      <div>{{company.age}} {{count}}</div>
      <div>{{myName}}</div>
      <ul>
         <li>1</li>
         <li>2</li>
      </ul>
      {{computedAdd}} {{count}}
      <button v-on:click="change">更新</button>
      <div v-html="message"></div>
   </div>

   <script>
      var vm = new Vue({
         el: '.container',
         data: {
            company: {
               name: 'ABC',
               age: 4
            },
            myName: 'lxh',
            myAdd: '沙河西后街',
            outProp: '外部属性',
            outAge: 12,
            count: 1,
            message: "<h1>你好啊</h1>"
         },
         computed: {
            computedAdd() {
               return this.myAdd.split('').reverse().join('');
            }
         },
         methods: {
            change() {
               this.count++;
               this.myName = this.myName + this.myAdd + this.outProp;
            }
         },
      })
      console.log(vm);
   </script>
</body>