vue源码解析-实现一个基础的MVVM框架

基本介绍

vue.js采用数据劫持结合发布-订阅模式的方式,通过Object.defineProperty()来劫持各个属性的getter,setter,在数据变动时发布消息给订阅者,触发响应的监听回调。
在这里插入图片描述

主要功能:

  • 实现一个指令解析器Compile
  • 实现一个数据监听器Observer
  • 实现一个Watcher去更新视图

入口函数和Compile编译类实现

HTML代码示例:

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

    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>

    <body>
      <div id="app">
        <h2>{{person.name}} -- {{person.age}}</h2>
        <h3>{{person.fav}}</h3>
        <ul>
          <li>1</li>
          <li>2</li>
          <li>3</li>
        </ul>
        <h3>{{msg}}</h3>
        <div v-text="msg"></div>
        <div v-text="person.fav"></div>
        <div v-html="htmlStr"></div>
        <input type="test" v-model="msg">
        <button v-on:click="handleClick">点击我on</button>
        <button @click="handleClick">点击我@</button>
        <img v-bind:src="imgUrl" />
        <img :src="imgUrl" />
      </div>
      <script src="./MVue.js"></script>
      <script>
        let vm = new MVue({
          el: '#app',
          data: {
            person: {
              name: '编程哥',
              age: 18,
              fav: '看电影'
            },
            msg: '学习MVVM实现原理',
            htmlStr: '大家学习得怎么样',
            imgUrl: './logo.png'
          },
          methods: {
            handleClick () {
              console.log(this)
            }
          }
        })
      </script>
    </body>

    </html>

MVue.js代码示例:

    const compileUtil = {
      getValue(expr, vm) {
        return expr.split(".").reduce((pre, cur) => {
          return pre[cur];
        }, vm.$data);
      },
      text(node, expr, vm) {
        // expr:msg 学习MVVM实现原理 // <div v-text="person.fav"></div>
        let value;
        if (expr.indexOf("{{") !== -1) {
          value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            return this.getValue(args[1], vm);
          });
        } else {
          value = this.getValue(expr, vm);
        }
        this.updater.textUpdater(node, value);
      },
      html(node, expr, vm) {
        const value = this.getValue(expr, vm);
        this.updater.htmlUpdater(node, value);
      },
      model(node, expr, vm) {
        const value = this.getValue(expr, vm);
        this.updater.modelUpdater(node, value);
      },
      on(node, expr, vm, eventName) {
        const fn = vm.$options.methods && vm.$options.methods[expr];
        node.addEventListener(eventName, fn.bind(vm), false);
      },
      bind(node, expr, vm, attrName) {
        const value = this.getValue(expr, vm);
        this.updater.bindUpdater(node, attrName, value);
      },
      // 更新的函数
      updater: {
        textUpdater(node, value) {
          node.textContent = value;
        },
        htmlUpdater(node, value) {
          node.innerHTML = value;
        },
        modelUpdater(node, value) {
          node.value = value;
        },
        bindUpdater(node, attrName, value) {
          node.setAttribute(attrName, value);
        },
      },
    };

    class Compiler {
      constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        // 1.获取文档碎片对象,放入内存中会减少页面的回流和重绘
        const fragment = this.node2Fragment(this.el);
        // 2.编译模板
        this.Compiler(fragment);

        // 3.追加子元素到根元素
        this.el.appendChild(fragment);
      }

      /*
      <h2>{{person.name}} -- {{person.age}}</h2>
        <h3>{{person.fav}}</h3>
        <ul>
          <li>1</li>
          <li>2</li>
          <li>3</li>
        </ul>
        <h3>{{msg}}</h3>
        <div v-test="msg"></div>
        <div v-html="htmlStr"></div>
        <input type="test" v-model="msg">
      */
      Compiler(fragment) {
        // 获取子节点
        const childNodes = fragment.childNodes;
        [...childNodes].forEach((child) => {
          // console.log(child);
          if (this.isElementNode(child)) {
            // 是元素节点
            // 编译元素节点
            // console.log("元素节点", child);
            this.compilerElement(child);
          } else {
            // 文本节点
            // 编译文本节点
            // console.log("文本节点", child);
            this.compilerText(child);
          }

          if (child.childNodes && child.childNodes.length) {
            this.Compiler(child);
          }
        });
      }

      compilerElement(node) {
        const attributes = node.attributes;
        [...attributes].forEach((attr) => {
          const { name, value } = attr;
          if (this.isDirective(name)) {
            // 是一个指令 v-text v-html v-model v-on:click v-bind:src
            const [, directive] = name.split("-"); // text html model on:click bind:src
            const [dirName, eventName] = directive.split(":"); // text html model on bind
            // 更新数据,数据驱动视图
            compileUtil[dirName](node, value, this.vm, eventName);

            // 删除有指令的标签上的属性
            node.removeAttribute("v-" + directive);
          } else if (this.isEventName(name)) {
            // @click="handleClick"
            const [, eventName] = name.split("@");
            compileUtil["on"](node, value, this.vm, eventName);
            // 删除标签上的绑定的事件
            node.removeAttribute("@" + eventName);
          } else if (this.isAttrName(name)) {
            // :src="imgUrl"
            const [, attrName] = name.split(":");
            compileUtil["bind"](node, value, this.vm, attrName);
            // 删除标签上绑定的属性
            node.removeAttribute(":" + attrName);
          }
        });
      }
      compilerText(node) {
        // {{}} v-text
        const content = node.textContent;
        if (/\{\{(.+?)\}\}/.test(content)) {
          compileUtil["text"](node, content, this.vm);
        }
      }

      isAttrName(attrName) {
        return attrName.startsWith(":");
      }

      isEventName(attrName) {
        return attrName.startsWith("@");
      }

      isDirective(attrName) {
        return attrName.startsWith("v-");
      }

      node2Fragment(el) {
        // 创建文档碎片
        const f = document.createDocumentFragment();
        let firstChild;
        while ((firstChild = el.firstChild)) {
          f.appendChild(firstChild);
        }
        return f;
      }
      isElementNode(node) {
        // 判断是否是元素节点对象
        return node.nodeType === 1;
      }
    }

    class MVue {
      constructor(options) {
        this.$el = options.el;
        this.$data = options.data;
        this.$options = options;
        if (this.$el) {
          // 1.实现一个数据观察者
          // 2.实现一个指令解析器
          new Compiler(this.$el, this);
        }
      }
    }

实现Observer劫持并监听所有属性

新建一个Observer.js类,示例代码如下:

    class Observer {
      constructor(data) {
        this.observe(data);
      }
      observe(data) {
        if (data && typeof data === "object") {
          Object.keys(data).forEach((key) => {
            this.defineReactive(data, key, data[key]);
          });
        }
      }
      defineReactive(obj, key, value) {
        // 递归遍历劫持
        this.observe(value);
        Object.defineProperty(obj, key, {
          configurable: false,
          enumerable: true,
          get() {
            // 订阅数据变化时,往Dep中添加观察者
            return value;
          },
          set: (newVal) => {
            this.observe(newVal);
            if (newVal !== value) {
              value = newVal;
            }
          },
        });
      }
    }

html代码引用:

    <script src="./Observer.js"></script>
    <script src="./MVue.js"></script>

在Mvue.js类中的调用方式:

    class MVue {
      constructor(options) {
        this.$el = options.el;
        this.$data = options.data;
        this.$options = options;
        if (this.$el) {
          // 1.实现一个数据观察者
          new Observer(this.$data);
          // 2.实现一个指令解析器
          new Compiler(this.$el, this);
        }
      }
    }

实现观察者Watcher和依赖收集器Dep

Observer.js代码中新增类Dep(收集观察者与通知观察者)和Watcher:

class Watcher {
  constructor(vm, expr, cb) {
    this.vm = vm;
    this.expr = expr;
    this.cb = cb;
    // 先把旧值保存起来
    this.oldVal = this.getOldVal();
  }
  getOldVal() {
    Dep.target = this;
    let oldVal = compileUtil.getValue(this.expr, this.vm);
    Dep.target = null;
    return oldVal;
  }
  update() {
    const newVal = compileUtil.getValue(this.expr, this.vm);
    if (newVal !== this.oldVal) {
      this.cb(newVal);
    }
  }
}

class Dep {
  constructor() {
    this.subs = [];
  }
  // 收集观察者
  addSub(watcher) {
    this.subs.push(watcher);
  }
  // 通知观察者去更新
  notify() {
    console.log("观察者", this.subs);
    this.subs.forEach((w) => w.update());
  }
}

class Observer {
  constructor(data) {
    this.observe(data);
  }
  observe(data) {
    if (data && typeof data === "object") {
      Object.keys(data).forEach((key) => {
        this.defineReactive(data, key, data[key]);
      });
    }
  }
  defineReactive(obj, key, value) {
    // 递归遍历劫持
    this.observe(value);
    const dep = new Dep();
    Object.defineProperty(obj, key, {
      configurable: false,
      enumerable: true,
      get() {
        // 订阅数据变化时,往Dep中添加观察者
        Dep.target && dep.addSub(Dep.target);
        return value;
      },
      set: (newVal) => {
        this.observe(newVal);
        if (newVal !== value) {
          value = newVal;
        }
        // 告诉Dep通知变化
        dep.notify();
      }
    });
  }
}

MVue.js代码:

const compileUtil = {
  getValue(expr, vm) {
    return expr.split(".").reduce((pre, cur) => {
      return pre[cur];
    }, vm.$data);
  },
  getContentValue(expr, vm) {
    return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
      return this.getValue(args[1], vm);
    });
  },
  text(node, expr, vm) {
    // expr:msg 学习MVVM实现原理 // <div v-text="person.fav"></div>
    let value;
    if (expr.indexOf("{{") !== -1) {
      // {{person.name}} -- {{person.age}}
      value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
        new Watcher(vm, args[1], () => {
          this.updater.textUpdater(node, this.getContentValue(expr, vm));
        });
        return this.getValue(args[1], vm);
      });
    } else {
      new Watcher(vm, expr, (newVal) => {
        this.updater.textUpdater(node, newVal);
      });
      value = this.getValue(expr, vm);
    }
    this.updater.textUpdater(node, value);
  },
  html(node, expr, vm) {
    const value = this.getValue(expr, vm);
    new Watcher(vm, expr, (newVal) => {
      this.updater.htmlUpdater(node, newVal);
    });
    this.updater.htmlUpdater(node, value);
  },
  model(node, expr, vm) {
    const value = this.getValue(expr, vm);
    // 绑定更新函数 数据=>视图
    new Watcher(vm, expr, (newVal) => {
      this.updater.modelUpdater(node, newVal);
    });
    this.updater.modelUpdater(node, value);
  },
  on(node, expr, vm, eventName) {
    const fn = vm.$options.methods && vm.$options.methods[expr];
    node.addEventListener(eventName, fn.bind(vm), false);
  },
  bind(node, expr, vm, attrName) {
    const value = this.getValue(expr, vm);
    this.updater.bindUpdater(node, attrName, value);
  },
  // 更新的函数
  updater: {
    textUpdater(node, value) {
      node.textContent = value;
    },
    htmlUpdater(node, value) {
      node.innerHTML = value;
    },
    modelUpdater(node, value) {
      node.value = value;
    },
    bindUpdater(node, attrName, value) {
      node.setAttribute(attrName, value);
    }
  }
};

class Compiler {
  constructor(el, vm) {
    this.el = this.isElementNode(el) ? el : document.querySelector(el);
    this.vm = vm;
    // 1.获取文档碎片对象,放入内存中会减少页面的回流和重绘
    const fragment = this.node2Fragment(this.el);
    // 2.编译模板
    this.Compiler(fragment);

    // 3.追加子元素到根元素
    this.el.appendChild(fragment);
  }

  /*
  <h2>{{person.name}} -- {{person.age}}</h2>
    <h3>{{person.fav}}</h3>
    <ul>
      <li>1</li>
      <li>2</li>
      <li>3</li>
    </ul>
    <h3>{{msg}}</h3>
    <div v-test="msg"></div>
    <div v-html="htmlStr"></div>
    <input type="test" v-model="msg">
  */
  Compiler(fragment) {
    // 获取子节点
    const childNodes = fragment.childNodes;
    [...childNodes].forEach((child) => {
      // console.log(child);
      if (this.isElementNode(child)) {
        // 是元素节点
        // 编译元素节点
        // console.log("元素节点", child);
        this.compilerElement(child);
      } else {
        // 文本节点
        // 编译文本节点
        // console.log("文本节点", child);
        this.compilerText(child);
      }

      if (child.childNodes && child.childNodes.length) {
        this.Compiler(child);
      }
    });
  }

  compilerElement(node) {
    const attributes = node.attributes;
    [...attributes].forEach((attr) => {
      const { name, value } = attr;
      if (this.isDirective(name)) {
        // 是一个指令 v-text v-html v-model v-on:click v-bind:src
        const [, directive] = name.split("-"); // text html model on:click bind:src
        const [dirName, eventName] = directive.split(":"); // text html model on bind
        // 更新数据,数据驱动视图
        compileUtil[dirName](node, value, this.vm, eventName);

        // 删除有指令的标签上的属性
        node.removeAttribute("v-" + directive);
      } else if (this.isEventName(name)) {
        // @click="handleClick"
        const [, eventName] = name.split("@");
        compileUtil["on"](node, value, this.vm, eventName);
        // 删除标签上的绑定的事件
        node.removeAttribute("@" + eventName);
      } else if (this.isAttrName(name)) {
        // :src="imgUrl"
        const [, attrName] = name.split(":");
        compileUtil["bind"](node, value, this.vm, attrName);
        // 删除标签上绑定的属性
        node.removeAttribute(":" + attrName);
      }
    });
  }
  compilerText(node) {
    // {{}} v-text
    const content = node.textContent;
    if (/\{\{(.+?)\}\}/.test(content)) {
      compileUtil["text"](node, content, this.vm);
    }
  }

  isAttrName(attrName) {
    return attrName.startsWith(":");
  }

  isEventName(attrName) {
    return attrName.startsWith("@");
  }

  isDirective(attrName) {
    return attrName.startsWith("v-");
  }

  node2Fragment(el) {
    // 创建文档碎片
    const f = document.createDocumentFragment();
    let firstChild;
    while ((firstChild = el.firstChild)) {
      f.appendChild(firstChild);
    }
    return f;
  }
  isElementNode(node) {
    // 判断是否是元素节点对象
    return node.nodeType === 1;
  }
}

class MVue {
  constructor(options) {
    this.$el = options.el;
    this.$data = options.data;
    this.$options = options;
    if (this.$el) {
      // 1.实现一个数据观察者
      new Observer(this.$data);
      // 2.实现一个指令解析器
      new Compiler(this.$el, this);
    }
  }
}

实现双向的数据绑定和Proxy代理

MVue.js代码:

    const compileUtil = {
      getValue(expr, vm) {
        return expr.split(".").reduce((pre, cur) => {
          return pre[cur];
        }, vm.$data);
      },
      setValue(expr, vm, inputVal) {
        // 将expr的点语法的字符串转换成数组并设置最有一个值为inputVal
        return expr.split(".").reduce((pre, cur, index, arr) => {
          if (index === arr.length - 1) {
            pre[cur] = inputVal;
          }
          return pre[cur];
        }, vm.$data);
      },
      getContentValue(expr, vm) {
        return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
          return this.getValue(args[1], vm);
        });
      },
      text(node, expr, vm) {
        // expr:msg 学习MVVM实现原理 // <div v-text="person.fav"></div>
        let value;
        if (expr.indexOf("{{") !== -1) {
          // {{person.name}} -- {{person.age}}
          value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            new Watcher(vm, args[1], () => {
              this.updater.textUpdater(node, this.getContentValue(expr, vm));
            });
            return this.getValue(args[1], vm);
          });
        } else {
          new Watcher(vm, expr, (newVal) => {
            this.updater.textUpdater(node, newVal);
          });
          value = this.getValue(expr, vm);
        }
        this.updater.textUpdater(node, value);
      },
      html(node, expr, vm) {
        const value = this.getValue(expr, vm);
        new Watcher(vm, expr, (newVal) => {
          this.updater.htmlUpdater(node, newVal);
        });
        this.updater.htmlUpdater(node, value);
      },
      model(node, expr, vm) {
        const value = this.getValue(expr, vm);
        // 绑定更新函数 数据=>视图
        new Watcher(vm, expr, (newVal) => {
          this.updater.modelUpdater(node, newVal);
        });
        // 视图=>数据=>视图
        node.addEventListener("input", (e) => {
          // 设置值
          this.setValue(expr, vm, e.target.value);
        });
        this.updater.modelUpdater(node, value);
      },
      on(node, expr, vm, eventName) {
        const fn = vm.$options.methods && vm.$options.methods[expr];
        node.addEventListener(eventName, fn.bind(vm), false);
      },
      bind(node, expr, vm, attrName) {
        const value = this.getValue(expr, vm);
        this.updater.bindUpdater(node, attrName, value);
      },
      // 更新的函数
      updater: {
        textUpdater(node, value) {
          node.textContent = value;
        },
        htmlUpdater(node, value) {
          node.innerHTML = value;
        },
        modelUpdater(node, value) {
          node.value = value;
        },
        bindUpdater(node, attrName, value) {
          node.setAttribute(attrName, value);
        }
      }
    };

    class Compiler {
      constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        // 1.获取文档碎片对象,放入内存中会减少页面的回流和重绘
        const fragment = this.node2Fragment(this.el);
        // 2.编译模板
        this.Compiler(fragment);

        // 3.追加子元素到根元素
        this.el.appendChild(fragment);
      }

      /*
      <h2>{{person.name}} -- {{person.age}}</h2>
        <h3>{{person.fav}}</h3>
        <ul>
          <li>1</li>
          <li>2</li>
          <li>3</li>
        </ul>
        <h3>{{msg}}</h3>
        <div v-test="msg"></div>
        <div v-html="htmlStr"></div>
        <input type="test" v-model="msg">
      */
      Compiler(fragment) {
        // 获取子节点
        const childNodes = fragment.childNodes;
        [...childNodes].forEach((child) => {
          // console.log(child);
          if (this.isElementNode(child)) {
            // 是元素节点
            // 编译元素节点
            // console.log("元素节点", child);
            this.compilerElement(child);
          } else {
            // 文本节点
            // 编译文本节点
            // console.log("文本节点", child);
            this.compilerText(child);
          }

          if (child.childNodes && child.childNodes.length) {
            this.Compiler(child);
          }
        });
      }

      compilerElement(node) {
        const attributes = node.attributes;
        [...attributes].forEach((attr) => {
          const { name, value } = attr;
          if (this.isDirective(name)) {
            // 是一个指令 v-text v-html v-model v-on:click v-bind:src
            const [, directive] = name.split("-"); // text html model on:click bind:src
            const [dirName, eventName] = directive.split(":"); // text html model on bind
            // 更新数据,数据驱动视图
            compileUtil[dirName](node, value, this.vm, eventName);

            // 删除有指令的标签上的属性
            node.removeAttribute("v-" + directive);
          } else if (this.isEventName(name)) {
            // @click="handleClick"
            const [, eventName] = name.split("@");
            compileUtil["on"](node, value, this.vm, eventName);
            // 删除标签上的绑定的事件
            node.removeAttribute("@" + eventName);
          } else if (this.isAttrName(name)) {
            // :src="imgUrl"
            const [, attrName] = name.split(":");
            compileUtil["bind"](node, value, this.vm, attrName);
            // 删除标签上绑定的属性
            node.removeAttribute(":" + attrName);
          }
        });
      }
      compilerText(node) {
        // {{}} v-text
        const content = node.textContent;
        if (/\{\{(.+?)\}\}/.test(content)) {
          compileUtil["text"](node, content, this.vm);
        }
      }

      isAttrName(attrName) {
        return attrName.startsWith(":");
      }

      isEventName(attrName) {
        return attrName.startsWith("@");
      }

      isDirective(attrName) {
        return attrName.startsWith("v-");
      }

      node2Fragment(el) {
        // 创建文档碎片
        const f = document.createDocumentFragment();
        let firstChild;
        while ((firstChild = el.firstChild)) {
          f.appendChild(firstChild);
        }
        return f;
      }
      isElementNode(node) {
        // 判断是否是元素节点对象
        return node.nodeType === 1;
      }
    }

    class MVue {
      constructor(options) {
        this.$el = options.el;
        this.$data = options.data;
        this.$options = options;
        if (this.$el) {
          // 1.实现一个数据观察者
          new Observer(this.$data);
          // 2.实现一个指令解析器
          new Compiler(this.$el, this);
          this.proxyData(this.$data);
        }
      }

      proxyData(data) {
        for (const key in data) {
          Object.defineProperty(this, key, {
            get() {
              return data[key];
            },
            set(newVal) {
              data[key] = newVal;
            }
          });
        }
      }
    }

HTML示例代码:

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

    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>

    <body>
      <div id="app">
        <h2>{{person.name}} -- {{person.age}}</h2>
        <h3>{{person.fav}}</h3>
        <ul>
          <li>1</li>
          <li>2</li>
          <li>3</li>
        </ul>
        <h3>{{person.td.msg}}</h3>
        <div v-text="msg"></div>
        <div v-text="person.fav"></div>
        <div v-html="htmlStr"></div>
        <input type="test" v-model="person.td.msg">
        <button v-on:click="handleClick">点击我on</button>
        <button @click="handleClick">点击我@</button>
        <img v-bind:src="imgUrl" />
        <img :src="imgUrl" />
      </div>
      <script src="./Observer.js"></script>
      <script src="./MVue.js"></script>
      <script>
        let vm = new MVue({
          el: '#app',
          data: {
            person: {
              td: {
                msg: '系统td'
              },
              name: '编程哥',
              age: 18,
              fav: '看电影'
            },
            msg: '学习MVVM实现原理',
            htmlStr: '大家学习得怎么样',
            imgUrl: './logo.png'
          },
          methods: {
            handleClick () {
              this.person.name = '学习222';
            }
          }
        })
      </script>
    </body>

    </html>

核心面试题讲解:阐述一下MVVM响应式原理

Vue是采用数据劫持配合发布订阅模式的方式,通过Object.defineProperty()来劫持各个属性的getter和setter,在数据变动时,发布消息给依赖收集器(Dep),去通知观察者(Watcher)做出对应的回调函数,去更新视图。

MVVM作为绑定的入口,整合Observer,Compile和Watcher三者,通过Observer来监听数据变化,通过Compile来解析编译模板指令,最终利用Watcher建立Observer、Compile之间的桥梁,达到数据变化=>视图更新;视图交互变化=>数据model变更的双向绑定效果

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值