手写简易版vue

目标

实现若干简单指令(含文本替换指令,事件注册指令),实现插值表达式{{}}语法,且实现双向数据绑定。

核心思路


(图片摘自互联网)

实现过程

采取以终为始的方式,由目标倒推实现。

要实现的目标

模板

<div id="app">
      <h1>插值表达式</h1>
      <h3>{{ msg }}</h3>
      <h3>{{ count }}</h3>
      <h1>v-text</h1>
      <div v-text="msg"></div>
      <h1>v-model</h1>
      <input type="text" v-model="msg" v-on:input="onTrigger" />
      <h1>v-html</h1>
      <div v-html="html"></div>
</div>

初始化Vue实例

let vm = new Vue({
        el: '#app',
        data: {
          msg: 'Hello Vue',
          count: 100,
          person: { name: 'zs' },
          test: 'abc', //[1,2,3],
          html: '<strong>7777777</strong>',
        },
        methods: {
          onTrigger() {
            console.log('event trigger!!!');
          },
        },
      });

实现Vue主类

class Vue {
        constructor(options) {
          // 1、通过属性保存选项的数据,默认为空对象
          this.$options = options || {};
          this.$data = options.data || {};
          this.$methods = options.methods;
          // 绑定DOM对象(字符串则通过DOM查询获取DOM)
          this.$el =
            typeof options.el === 'string'
              ? document.querySelector(options.el)
              : options.el;
          // 2、实例化observer对象,监听数据的变化
          new Observer(this.$data);
          // 3、实例化compiler对象,解析指令、插值表达式
          new Compiler(this);
        }

      }

实现Observer类,劫持监听Vue实例中所有属性

class Observer {
        constructor(data) {
          console.log(this);
          this.observe(data);
        }
        observe(data) {
          // 如果设置的数据类型为对象就设置为响应式数据
          if (data && typeof data === 'object') {
            Object.keys(data).forEach((key) => {
              //调用设置响应式数据的方法
              this.definePReactive(data, key, data[key]);
            });
          }
        }
        // 设置属性为响应式数据
        definePReactive(obj, key, value) {
          // 利递归使深层属性转换为 响应式数据
          this.observe(value);
          const that = this; // 保存内部this, 方便内部调用
          // 负责收集依赖并发送通知
          let dep = new Dep();
          console.log('dep', dep);
          Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
              console.log('你访问了', obj, key);
              // 订阅数据变化时, 往Dep中添加观察者, 收集依赖;
              Dep.target && dep.addSub(Dep.target);
              return value;
            },
            set(newVal) {
              //如果新设置的值也为对象, 也转换为响应式数据
              // that.observe(newVal);
              console.log(106, value, newVal);
              if (newVal !== value) {
                value = newVal; // 此处不能写obj[key] = newVal 会出现死递归,将value重置为newVal再次获取时因renturn value;就获取到了最新的属性值,实现属性值更新
                // console.log(110, obj);
              }
              // 发送通知;
              dep.notify();
            },
          });
        }
      }

实现Compiler类,解析指令,初始化视图

 class Compiler {
        constructor(vm) {
          this.el = vm.$el;
          this.vm = vm;
          this.compile(this.el); // 构造函数立即执行模板编译
        }
        // 编译模板,处理文本节点和元素节点
        compile(el) {
          let childNodes = el.childNodes;
          // console.log(childNodes);
          // 遍历vm根节点的每个子节点
          Array.from(childNodes).forEach((node) => {
            // 若子节点为文本节点
            if (this.isTextNode(node)) {
              // 处理文本节点
              this.compileText(node);
              // 若子节点为元素节点
            } else if (this.isElementNode(node)) {
              // 处理元素节点
              this.compileElement(node);
            }
            // 处理深层子节点,递归调用
            // 判断node节点是否有子节点,如果有子节点,递归调用complie
            if (node.childNodes && node.childNodes.length) {
              this.compile(node);
            }
          });
        }
        // 编译元素节点,处理指令
        compileElement(node) {
          // 遍历所有的属性节点,判断是否是v-指令
          Array.from(node.attributes).forEach((attr) => {
            let attrName = attr.name;
            // 若属性名为 指令
            if (this.isDirective(attrName)) {
              if (attrName.indexOf(':') != -1) {
                var eventName = attrName.substr(5);
                // 事件注册
                node['on' + eventName] = this.vm.$methods[attr.value];
              } else {
                // v-text --> text
                attrName = attrName.substr(2);
                // 获取属性值(data中的键名)
                let key = attr.value;
                // 更新节点内容
                this.update(node, key, attrName);
              }
            }
          });
        }
        // 辅助方法,根据指令类型更新内容
        update(node, key, attrName) {
          // 根据属性名动态调用对应的更新方法---后面有分类定义的更新方法
          let updateFn = this[attrName + 'Updater'];
          // console.log(updateFn); 触发get方法,在get方法中调用addSub
          updateFn && updateFn.call(this, node, this.vm.$data[key], key);
        }
        // 处理v-text指令
        textUpdater(node, value, key) {
          // 对于v-text指令,将节点内容替换为data中对应的值
          node.textContent = value;
          // 实例化对应的监听器
          new Watcher(this.vm, key, (newValue) => {
            node.textContent = newValue;
          });
        }
        // 处理v-html指令
        htmlUpdater(node, value, key) {
          // 对于v-text指令,将节点内容替换为data中对应的值
          node.innerHTML = value;
          // 实例化对应的监听器
          new Watcher(this.vm, key, (newValue) => {
            node.textContent = newValue;
          });
        }
        // 处理v-model指令
        modelUpdater(node, value, key) {
          // 对于v-model指令, 将节点内容替换为data中对应的值
          node.value = value;
          // 实例化对应的监听器
          new Watcher(this.vm, key, (newValue) => {
            node.value = newValue;
          });
          // 双向绑定,视图更新数据也更新
          node.addEventListener('input', () => {
            console.log('测试185');
            this.vm.$data[key] = node.value;
          });
        }

        // 编译文本节点,处理插值表达式
        compileText(node) {
          // 提取 {{}} 中的 变量 或 表达式
          let reg = /\{\{(.+?)\}\}/;
          let value = node.textContent;
          // 是否可以匹配插值表达式
          if (reg.test(value)) {
            let key = RegExp.$1.trim();
            // 替换成插值表达式对应的值(Vue实例的值) 触发get方法,在get方法中调用addSub
            node.textContent = value.replace(reg, this.vm.$data[key]);
            // 创建watch对象,当数据改变时改变视图
            new Watcher(this.vm, key, (newValue) => {
              console.log(newValue, '======');
              node.textContent = newValue;
            });
          }
        }
        // 判断元素属性是否是指令
        isDirective(attrName) {
          return attrName.startsWith('v-');
        }
        // 判断节点是否是文本节点
        isTextNode(node) {
          return node.nodeType === 3;
        }
        // 判断节点是否是元素节点
        isElementNode(node) {
          return node.nodeType === 1;
        }
      }

实现Dep类,收集依赖,为Watcher类的容器

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

        // 添加观察者
        addSub(sub) {
          // sub存在且有update()方法
          if (sub && sub.update) {
            this.subs.push(sub);
          }
        }

        // 发送通知,遍历并调用每个观察者的update()
        notify() {
          this.subs.forEach((sub) => {
            sub.update();
          });
        }
      }

实现Watcher类,监听视图中数据变化

 class Watcher {
        constructor(vm, key, cb) {
          this.vm = vm;
          // data中的属性名称
          this.key = key;
          // 回调函数负责更新视图
          this.cb = cb;

          // 把watcher对象记录到dep的静态属性target,此时Dep会收集依赖
          Dep.target = this;
          console.log(this);
          // 触发get方法,在get方法中调用addSub
          this.oldValue = vm.$data[key];
          Dep.target = null;
        }

        // 当数据变化的时候,更新视图
        update() {
          console.log('diaoyong');
          let newValue = this.vm.$data[this.key];
          if (this.oldValue === newValue) {
            return;
          }
          this.cb(newValue);
        }
      }
posted @ 2021-12-25 22:22  helloworld777  阅读(58)  评论(0编辑  收藏  举报