手写MVVM框架


手写MVVM框架

创建MVVM.html

<body>
    <div id="app">
      <input type="text" v-model="message" />
      {{message}}
    </div>
    <script src="./compile.js"></script>
    <script src="./MVVM.js"></script>
    <script>
      // 数据一般都挂在在VM上
      // vue中通过 Object.defineProperty 方法 给每一个数据添加get和set
      // vue中实现双向绑定 1. 模板的编译 2. 数据劫持(观察数据变换) 3.Watcher
      let vm = new MVVM({
        el: "#app",
        data: {
          message: "hello wzt",
        },
      });
    </script>
  </body>

创建MVVM.js

class MVVM {
  constructor(options) {
    // 首先 先把可用的东西挂载在实例上
    this.$el = options.el;
    this.$data = optins.data;

    // 通过判断 如果有需要编译的模板就开始编译
    if (this.$el) {
      // 用数据和元素进行编译
      new Compile(this.$el, this);
    }
  }
}

创建compile.js(1)

class Compile {
  constructor(el, vm) {
    // 将参数放置在实例上
    this.el = this.isElementNode(el) ? el : document.querySelector(el); // #app  document
    this.vm = vm;
    if (this.el) {
      // 如果这个元素能获取到  才开始进行编译
      // 1.先把真实的DOM移入到内存中 fragment
      let fragment = this.node2fragment(this.el); // 将div id为app的所有内容放入内存中
      // 2.编译 => 提取想要的元素节点 v-model 和文本节点{{}}

      // 3.把编译好的fragment再塞回到页面中
    }
  }
  // 写一些辅助的方法

  // 判断是否是元素节点
  isElementNode(node) {
    return node.nodeType === 1;
  }
  // 核心的方法
  node2fragment(el) {
    //需要将el中的内容全部放到内存中
    // 文档碎片 内存中的dom节点
    let fragment = document.createDocumentFragment();
    let firstChild;
    while ((firstChild = el.firstChild)) {
      fragment.appendChild(firstChild);
    }
    return fragment; //内存中的节点
  }
}

创建compile.js(2)

  1. 完成判断是否是元素节点,div id为app (创建了 isElementNode()函数)
  2. 如果是元素节点,将真实的DOM移入到内存中 (创建了 node2fragment()函数)
  3. 进而进行编译,从而提取想要的元素节点 (创建了compile()函数 )
    1. 将编译对象分为两类,一种是元素,一种是文本
      1. 编译元素 (创建了 compileElement()函数 )
      2. 编译文本(创建了 compileText()函数 )
class Compile {
  constructor(el, vm) {
    // 将参数放置在实例上
    this.el = this.isElementNode(el) ? el : document.querySelector(el); // #app  document
    this.vm = vm;
    if (this.el) {
      // 如果这个元素能获取到  才开始进行编译
      // 1.先把真实的DOM移入到内存中 fragment
      let fragment = this.node2fragment(this.el); // 将div id为app的所有内容放入内存中
      // 2.编译 => 提取想要的元素节点 v-model 和文本节点{{}}
      this.compile(fragment);
      // 3.把编译好的fragment再塞回到页面中
    }
  }
  /* 

  写一些辅助的方法

 */
  // 判断是否是元素节点
  isElementNode(node) {
    return node.nodeType === 1;
  }

  /* 

  写一些核心的方法

 */
  /* 将节点放置到内存 fragment */
  node2fragment(el) {
    //需要将el中的内容全部放到内存中
    // 文档碎片 内存中的dom节点
    let fragment = document.createDocumentFragment();
    let firstChild;
    while ((firstChild = el.firstChild)) {
      fragment.appendChild(firstChild);
    }
    return fragment; //内存中的节点
  }

  /* 编译方法 */
  compile(fragment) {
    // 需要递归
    let childNodes = fragment.childNodes;
    // console.log(childNodes);
    // 类数组格式需要转换成数组
    Array.from(childNodes).forEach((node) => {
      if (this.isElementNode(node)) {
        // 是元素节点,还需要继续深入检查 使用递归再次调compile()
        console.log("element", node);
        // 这里需要编译元素方法
        // this.compileElement(node);
        this.compile(node);
      } else {
        // 是文本节点
        console.log("text", node);
        // 这里需要编译文本方法
        // this.compileText(node);
      }
    });
  }
}
  • 编译元素方法
/* 编译元素 */
  compileElement(node) {
    // 带v-model的  v-text ...
    let attrs = node.attributes; // 取出当前节点的属性
    // console.log(attrs);
    Array.from(attrs).forEach((attr) => {
      // console.log(attr); //查看属性  v-model="message"
      // 判断属性名字是否包含v-
      let attrName = attr.name;
      if (this.isDirective(attrName)) {
        // 取到对应的值放到节点中
        let expr = attr.value;
        //node this.vm.$data expr
        //todo ......................后续完善
      }
    });
  }

判断是否是指令

/* 判断是否是指令 */
  isDirective(name) {
    // 判断是否包含'v-'
    return name.includes("v-");
  }

注:node.attributes取出当前节点的属性; node.textContent取文本中的内容

  • 编译文本方法
 /* 编译文本 */
  compileText(node) {
    // 带{{}}的
    let expr = node.textContent; //取文本中的内容
    console.log(expr);
    let reg = /\{\{([^}]+)\}\}/g; //正则
    if (reg.test(expr)) {
      //node this.vm.$data expr
      //todo .......................后续完善
    }
  }
  • 编译工具
/* 编译工具 */
CompileUtil = {
  text(node, vm, expr) {
    // 文本处理
  },
  model(node, vm, expr) {
    // 输入框处理
  },
  updater: {
    // 文本更新
    textUpdater(node, value) {
      node.textContent = value;
    },
    // 输入框更新
    modelUpdater(node, value) {
      node.value = value;
    },
  },
};

至此编译元素和编译方法中的todo替换如下:

编译元素

//  v-model 去除V- 留下model两种方法
        // let type = attrName.slice(2); //法一:从第二个元素开始取
        let [, type] = attrName.split("-"); //法二: 解构赋值

        console.log(type);
        //node this.vm.$data expr
        CompileUtil[type](node, this.vm, expr);
      }

编译文本

CompileUtil["text"](node, this.vm, expr);

上下两部分代码解耦,现在不需要修改上面的代码,只需要在编译工具CompileUtil中完善即可

完成初步创建compile.js

class Compile {
  constructor(el, vm) {
    // 将参数放置在实例上
    this.el = this.isElementNode(el) ? el : document.querySelector(el); // #app  document
    this.vm = vm;
    if (this.el) {
      // 如果这个元素能获取到  才开始进行编译
      // 1.先把真实的DOM移入到内存中 fragment
      let fragment = this.node2fragment(this.el); // 将div id为app的所有内容放入内存中
      // 2.编译 => 提取想要的元素节点 v-model 和文本节点{{}}
      this.compile(fragment);
      // 3.把编译好的fragment再塞回到页面中
      this.el.appendChild(fragment);
    }
  }
  /*

  写一些辅助的方法

 */
  /* 判断是否是元素节点 */
  isElementNode(node) {
    return node.nodeType === 1;
  }

  /* 判断是否是指令 */
  isDirective(name) {
    // 判断是否包含'v-'
    return name.includes("v-");
  }
  /*

  写一些核心的方法

 */
  /* 将节点放置到内存 fragment */
  node2fragment(el) {
    //需要将el中的内容全部放到内存中
    // 文档碎片 内存中的dom节点
    let fragment = document.createDocumentFragment();
    let firstChild;
    while ((firstChild = el.firstChild)) {
      fragment.appendChild(firstChild);
    }
    return fragment; //内存中的节点
  }

  /* 编译方法 */
  compile(fragment) {
    // 需要递归
    let childNodes = fragment.childNodes;
    // console.log(childNodes);
    // 类数组格式需要转换成数组
    Array.from(childNodes).forEach((node) => {
      if (this.isElementNode(node)) {
        // 是元素节点,还需要继续深入检查 使用递归再次调compile()
        // console.log("element", node);
        // 这里需要编译元素方法
        this.compileElement(node);
        this.compile(node);
      } else {
        // 是文本节点
        // console.log("text", node);
        // 这里需要编译文本方法
        this.compileText(node);
      }
    });
  }

  /* 编译元素 */
  compileElement(node) {
    // 带v-model的  v-text ...
    let attrs = node.attributes; // 取出当前节点的属性
    // console.log(attrs);
    Array.from(attrs).forEach((attr) => {
      // console.log(attr); //查看属性  v-model="message"
      // 判断属性名字是否包含v-
      let attrName = attr.name;
      if (this.isDirective(attrName)) {
        // 取到对应的值放到节点中
        let expr = attr.value;
        //  v-model 去除V- 留下model两种方法
        // let type = attrName.slice(2); //法一:从第二个元素开始取
        let [, type] = attrName.split("-"); //法二: 解构赋值

        console.log(type);
        //node this.vm.$data expr
        CompileUtil[type](node, this.vm, expr);
      }
    });
  }

  /* 编译文本 */
  compileText(node) {
    // 带{{}}的
    let expr = node.textContent; //取文本中的内容
    // console.log(expr);
    let reg = /\{\{([^}]+)\}\}/g; //正则
    if (reg.test(expr)) {
      //node this.vm.$data expr
      CompileUtil["text"](node, this.vm, expr);
    }
  }
}

/* 编译工具 */
CompileUtil = {
  getVal(vm, expr) {
    // 获取实例上对应的数据
    const exprArr = expr.split("."); // [a,v,c,s]
    const value = exprArr.reduce((prev, next) => {
      // vm.$data.a.v...    将vm.$data指定为prev
      return prev[next];
    }, vm.$data);
    return value;
  },
  getTextVal(vm, expr) {
    //获取编译文本后的结果
    return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
      return this.getVal(vm, arguments[1]);
    });
  },
  text(node, vm, expr) {
    // 文本处理
    let updateFn = this.updater["textUpdater"];
    // {{message.a}} => hello wzt
    let value = this.getTextVal(vm, expr);
    updateFn && updateFn(node, value);
  },
  model(node, vm, expr) {
    // 输入框处理
    let updateFn = this.updater["modelUpdater"];
    updateFn && updateFn(node, this.getVal(vm, expr));
  },
  updater: {
    // 文本更新
    textUpdater(node, value) {
      node.textContent = value;
    },
    // 输入框更新
    modelUpdater(node, value) {
      node.value = value;
    },
  },
};

接下来配置数据劫持 observer.js

配置MVVM.js

新增 new Observer(this.$data);

class MVVM {
  constructor(options) {
    // 首先 先把可用的东西挂载在实例上
    this.$el = options.el;
    this.$data = options.data;

    // 通过判断 如果有需要编译的模板就开始编译
    if (this.$el) {
      //数据劫持  就是把对想的所有属性 改成 get和 set方法
      new Observer(this.$data);
      // 用数据和元素进行编译
      new Compile(this.$el, this);
    }
  }
}

配置observer.js

class Observer {
  constructor(data) {
    this.observe(data);
  }

  observe(data) {
    // 要对这个data数据将原有的属性改成set和get的形式
    if (!data || typeof data !== "object") {
      return;
    }
    // 要将数据一一劫持 先获取到data 的key和value
    Object.keys(data).forEach((key) => {
      // 劫持
      this.defineReactive(data, key, data[key]);
      this.observe(data[key]); //深度递归劫持
    });
  }

  // 定义响应式
  defineReactive(obj, key, value) {
    let that = this;
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        // 当取值是调用的方法
        return value;
      },
      set(newValue) {
        // 当给data属性中设置值的时候 更改获取的属性的值
        if (newValue != value) {
          that.observe(newValue); //如果是对象继续劫持
          value = newValue;
        }
      },
    });
  }
}

修改compile.js

text(node, vm, expr) {
    // 文本处理
    let updateFn = this.updater["textUpdater"];
    // {{message.a}} => hello wzt
    let value = this.getTextVal(vm, expr);
    //{{a}}  {{b}}
    expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
      new Watcher(vm, arguments[1], (newValue) => {
        //如果数据变化了, 文本节点需要重新获取依赖的属性更新文本中的内容
        updateFn && updateFn(node, this.getTextVal());
      });
    });
    updateFn && updateFn(node, value);
  },
  model(node, vm, expr) {
    // 输入框处理
    let updateFn = this.updater["modelUpdater"];
    // 这里应该加一个监控,数据变化了 因该调用这个Watch的callback
    new Watcher(vm, expr, (newValue) => {
      // 当值变化后会调用Cb将新的值传递过来
      updateFn && updateFn(node, this.getVal(vm, expr));
    });
    updateFn && updateFn(node, this.getVal(vm, expr));
  },

完整代码

MVVM.html

<!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>
  </head>
  <body>
    <div id="app">
      <input type="text" v-model="message.a">
      <div>{{message.a}} {{b}}</div>
      <ul><li></li></ul>
      {{message.a}}
  </div>
    </div>
    <script src="./watcher.js"></script>
    <script src="./observer.js"></script>
    <script src="./compile.js"></script>
    <script src="./MVVM.js"></script>
    <script>
      // 数据一般都挂在在VM上
      // vue中通过 Object.defineProperty 方法 给每一个数据添加get和set
      // vue中实现双向绑定 1. 模板的编译 2. 数据劫持(观察数据变换) 3.Watcher
      let vm = new MVVM({
        el: "#app",
        data: {
          message: {
            a: "hello wzt",
          },
          b: 1,
        },
      });
    </script>
  </body>
</html>

MVVM.js

class MVVM {
  constructor(options) {
    // 首先 先把可用的东西挂载在实例上
    this.$el = options.el;
    this.$data = options.data;

    // 通过判断 如果有需要编译的模板就开始编译
    if (this.$el) {
      //数据劫持  就是把对想的所有属性 改成 get和 set方法
      new Observer(this.$data);
      this.proxyData(this.$data);
      // 用数据和元素进行编译
      new Compile(this.$el, this);
    }
  }
  proxyData(data) {
    Object.keys(data).forEach((key) => {
      Object.defineProperty(this, key, {
        get() {
          return data[key];
        },
        set(newValue) {
          data[key] = newValue;
        },
      });
    });
  }
}

compile.js

class Compile {
  constructor(el, vm) {
    // 将参数放置在实例上
    this.el = this.isElementNode(el) ? el : document.querySelector(el); // #app  document
    this.vm = vm;
    if (this.el) {
      // 如果这个元素能获取到  才开始进行编译
      // 1.先把真实的DOM移入到内存中 fragment
      let fragment = this.node2fragment(this.el); // 将div id为app的所有内容放入内存中
      // 2.编译 => 提取想要的元素节点 v-model 和文本节点{{}}
      this.compile(fragment);
      // 3.把编译好的fragment再塞回到页面中
      this.el.appendChild(fragment);
    }
  }
  /*

  写一些辅助的方法

 */
  /* 判断是否是元素节点 */
  isElementNode(node) {
    return node.nodeType === 1;
  }

  /* 判断是否是指令 */
  isDirective(name) {
    // 判断是否包含'v-'
    return name.includes("v-");
  }
  /*

  写一些核心的方法

 */
  /* 将节点放置到内存 fragment */
  node2fragment(el) {
    //需要将el中的内容全部放到内存中
    // 文档碎片 内存中的dom节点
    let fragment = document.createDocumentFragment();
    let firstChild;
    while ((firstChild = el.firstChild)) {
      fragment.appendChild(firstChild);
    }
    return fragment; //内存中的节点
  }

  /* 编译方法 */
  compile(fragment) {
    // 需要递归
    let childNodes = fragment.childNodes;
    // console.log(childNodes);
    // 类数组格式需要转换成数组
    Array.from(childNodes).forEach((node) => {
      if (this.isElementNode(node)) {
        // 是元素节点,还需要继续深入检查 使用递归再次调compile()
        // console.log("element", node);
        // 这里需要编译元素方法
        this.compileElement(node);
        this.compile(node);
      } else {
        // 是文本节点
        // console.log("text", node);
        // 这里需要编译文本方法
        this.compileText(node);
      }
    });
  }

  /* 编译元素 */
  compileElement(node) {
    // 带v-model的  v-text ...
    let attrs = node.attributes; // 取出当前节点的属性
    // console.log(attrs);
    Array.from(attrs).forEach((attr) => {
      // console.log(attr); //查看属性  v-model="message"
      // 判断属性名字是否包含v-
      let attrName = attr.name;
      if (this.isDirective(attrName)) {
        // 取到对应的值放到节点中
        let expr = attr.value;
        //  v-model 去除V- 留下model两种方法
        // let type = attrName.slice(2); //法一:从第二个元素开始取
        let [, type] = attrName.split("-"); //法二: 解构赋值

        console.log(type);
        //node this.vm.$data expr
        CompileUtil[type](node, this.vm, expr);
      }
    });
  }

  /* 编译文本 */
  compileText(node) {
    // 带{{}}的
    let expr = node.textContent; //取文本中的内容
    // console.log(expr);
    let reg = /\{\{([^}]+)\}\}/g; //正则
    if (reg.test(expr)) {
      //node this.vm.$data expr
      CompileUtil["text"](node, this.vm, expr);
    }
  }
}

/* 编译工具 */
CompileUtil = {
  getVal(vm, expr) {
    // 获取实例上对应的数据
    const exprArr = expr.split("."); // [a,v,c,s]
    const value = exprArr.reduce((prev, next) => {
      // vm.$data.a.v...    将vm.$data指定为prev
      return prev[next];
    }, vm.$data);
    return value;
  },
  getTextVal(vm, expr) {
    //获取编译文本后的结果
    return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
      // console.log(arguments[0]); //{{message.a}}  {{b}}
      // console.log(arguments[1]); //message.a   b
      return this.getVal(vm, arguments[1]);
    });
  },
  text(node, vm, expr) {
    // 文本处理
    let updateFn = this.updater["textUpdater"];
    // {{message.a}} => hello wzt
    let value = this.getTextVal(vm, expr);
    //{{a}}  {{b}}
    expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
      new Watcher(vm, arguments[1], (newValue) => {
        //如果数据变化了, 文本节点需要重新获取依赖的属性更新文本中的内容
        updateFn && updateFn(node, this.getTextVal(vm, expr));
      });
    });
    updateFn && updateFn(node, value);
  },
  setVal(vm, expr, value) {
    //[message,a]
    expr = expr.split(".");
    return expr.reduce((prev, next, currentIndex) => {
      if (currentIndex === expr.length - 1) {
        return (prev[next] = value);
      }
      return prev[next];
    }, vm.$data);
  },
  model(node, vm, expr) {
    // 输入框处理
    let updateFn = this.updater["modelUpdater"];
    // 这里应该加一个监控,数据变化了 因该调用这个Watch的callback
    new Watcher(vm, expr, (newValue) => {
      // 当值变化后会调用Cb将新的值传递过来
      updateFn && updateFn(node, this.getVal(vm, expr));
    });
    node.addEventListener("input", (e) => {
      let newValue = e.target.value;
      this.setVal(vm, expr, newValue);
    });
    updateFn && updateFn(node, this.getVal(vm, expr));
  },
  updater: {
    // 文本更新
    textUpdater(node, value) {
      node.textContent = value;
    },
    // 输入框更新
    modelUpdater(node, value) {
      node.value = value;
    },
  },
};

observer.js

class Observer {
  constructor(data) {
    this.observe(data);
  }

  observe(data) {
    // 要对这个data数据将原有的属性改成set和get的形式
    if (!data || typeof data !== "object") {
      return;
    }
    // 要将数据一一劫持 先获取到data 的key和value
    Object.keys(data).forEach((key) => {
      // 劫持
      this.defineReactive(data, key, data[key]);
      this.observe(data[key]); //深度递归劫持
    });
  }

  // 定义响应式
  defineReactive(obj, key, value) {
    let that = this;
    let dep = new Dep(); // 每个变化的数据 都会对应一个数组,这个数组是存放所有更新的操作
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        // 当取值是调用的方法
        Dep.target && dep.addSub(Dep.target);
        return value;
      },
      set(newValue) {
        // 当给data属性中设置值的时候 更改获取的属性的值
        if (newValue != value) {
          that.observe(newValue); //如果是对象继续劫持
          value = newValue;
          dep.notify(); //通知所有人数据更新了
        }
      },
    });
  }
}
class Dep {
  constructor() {
    // 订阅的数组
    this.subs = [];
  }
  addSub(watcher) {
    this.subs.push(watcher);
  }
  notify() {
    this.subs.forEach((watcher) => watcher.update());
  }
}

watcher.js

// 观察者的目的就是给需要变化的那个元素增加一个观察者, 当数据变化后执行对应的方法;
class Watcher {
  //cb为回调函数
  constructor(vm, expr, cb) {
    this.vm = vm;
    this.expr = expr;
    this.cb = cb;
    // 先获取老的值
    this.value = this.get();
  }

  getVal(vm, expr) {
    // 获取实例上对应的数据
    const exprArr = expr.split("."); // [a,v,c,s]
    const value = exprArr.reduce((prev, next) => {
      // vm.$data.a.v...    将vm.$data指定为prev
      return prev[next];
    }, vm.$data);
    return value;
  }
  get() {
    Dep.target = this;
    let value = this.getVal(this.vm, this.expr);
    Dep.target = null;
    return value;
  }
  // 对外暴露的方法
  update() {
    let newValue = this.getVal(this.vm, this.expr);
    let oldValue = this.value;
    if (newValue !== oldValue) {
      this.cb(newValue); //对应watch的callback
    }
  }
}
// 用新值和老值进行比对 如果发生变化 就调用更新方法

文章作者: Zetai Wei
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Zetai Wei !
评论
  目录