Vue2中MVVM框架

代码仓库地址:https://github.com/141110126/write-vue/tree/main/jvue

一、MVVM框架

MVVM 框架的三要素: 数据响应式、模板引擎及其渲染
1.数据响应式:监听数据变化并在视图中更新
  • Object.defifineProperty()
  • Proxy
2.模版引擎:提供描述视图的模版语法
  • 插值:{{}}
  • 指令:v-bindv-onv-modelv-forv-if
3.渲染:如何将模板转换为 html
  • 模板 => vdom => dom

 

二、Vue响应式原理

利用Object.defineProperty()劫持data数据,里面每个属性都定义了get和set方法 监听属性的变化,

依赖收集:每个属性也都有个数组 保存着谁(视图watcher)依赖了它,当获取属性触发get时,收集了依赖,

依赖更新:当属性变化触发set函数时,通知依赖,通知视图(watcher),视图(watcher)开始更新。

  •  Vue:框架构造函数
  • Observer: 执行数据响应化
  • Compile:编译模板,初始化视图,收集依赖(更新函数、watcher创建)
  • Watcher:执行更新函数
  • Dep:管理多个Watcher,批量更新

 

三、 数据拦截与代理

 Object.defineProperty():在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

      get 值是一个函数,当属性被访问时,会触发 get 函数

      set 值同样是一个函数,当属性被赋值时,会触发 set 函数

 

Vue 是怎么知道数据被调用或改变的?

Vue对data对象中的每个属性进行了get、set监听,当属性被调用时触发get方法,当属性被改变时触发set方法,所以Vue 能知道数据被调用或改变。

1.defineReactive():使用Object.defineProperty()对obj中的某一个属性进行拦截

// 对obj中的属性做拦截
defineReactive(obj, key, val) {
  this.observe(val);
  Object.defineProperty(obj, key, {
      get () {
          return val;
      },
      set (newValue) {
          if(val === newValue) {
              return;
          }
          val = newValue; 
          console.log(val);
      }
  })
}

 2.observe(obj): 当obj中有多个属性要做拦截时,我们需要遍历obj中 的属性

创建Observer类:对传入的数据进行判断,再分类(Array、Object)进行操作。

// 遍历对象,给它的属性做响应式处理
function observe(obj){
  // 判断obj是否为对象,若不是则返回
  if(typeof obj !== "object" || obj == null) {
    return obj;
  }
  // 创建Observer实例,传入obj进行判断 遍历
  new Observer(obj);
}

// 判断是否为对象,若为对象再进行遍历
class Observer {
  constructor(obj) {
    if(Array.isArray(obj)){
      // todo
    } else{
      this.walk(obj);
    }
  }
  walk(obj) {
    // 若是对象,则遍历其属性做拦截处理
    Object.keys(obj).forEach(key => defineReactive(obj,key,obj[key]));
  }
}

3.递归:当obj中的属性的属性值为对象时,需要递归遍历属性值中的属性,对深层的属性进行拦截。

当给属性重新赋值一个对象时,也需要对该对象中的属性做拦截处理

// 对obj中的属性做拦截
function defineReactive(obj, key, val) {
  // 当val是一个对象时,需要递归遍历val中的属性做拦截处理
  observe(val);
  // vue2中用Object.defineProperty()对属性进行跟踪拦截
  Object.defineProperty(obj, key , {
    get() {
      return val;
    },
    set(newVal) {
      val = newVal;
      // 当newVal为对象时,需要遍历newVal中的属性做拦截处理
      observe(newVal);
      // update()
    }
  })
}

4.set():给已有的对象动态添加新的响应式属性

Vue.$sets()实际上就是调用了defineReactive方法

// 给已有的对象动态添加新的响应式属性
function set(obj, key, val) {
  defineReactive(obj, key, val);
}

5.Vue类初始化:获取data中的属性,进行拦截处理,并代理到vue上,这样才能直接从vue中获取和修改属性。

// 将传入对象中的所有属性代理到指定对象上
function Proxy(vm) {
  // 遍历data中的属性,在vm中添加一样的属性,并做拦截处理
  Object.keys(vm.$data).forEach(key => {
    Object.defineProperty(vm, key, {
      get() {
        console.log(vm.$data[key]);
        return vm.$data[key]
      },
      set(v) {
        // 当修改vm中的属性时,data中的属性也跟着修改
        vm.$data[key] = v
        console.log(vm.$data[key]);
      }
    })
  })
}

class JVue {
  constructor(options) {
    // 1.获取data对象
    this.$options = options;
    this.$data = options.data;
    console.log(this.$data);
    // 2.对data做拦截处理
    observe(this.$data);

    // 3.将data中的属性代理到vm中
    Proxy(this);
  }
}

6.测试

<!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">
    <p>{{count}}</p>
  </div>
<script src="./jvue.js"></script>
<script>
const app = new JVue({
  el: "#app",
  data: {
    count: 1
  }
})
setInterval(() => {
  app.count ++
}, 1000)
</script>
</body>
</html>

7.数组响应式

解析: 数组中有7个方法会改变数组本身,所以在其改变自身时要通知更新,所以需要对这七个方法进行替换。

  • 改变数组自身的7个方法:push,pop,unshift,shift,sort,reverse,splice

思路:创建7个新的方法,当传入的数据为数组时,将其原型替换

// 数组响应式
// 获取Array原型,备份一份,修改备份
const originalProto = Array.prototype;
const arrayProto = Object.create(originalProto);
// 设置七个新的方法
["push", "pop", "shifit", "unshift", "sort", "reverse", "splice"].forEach(
  method => {
    arrayProto[method] = function() {
      // 原始操作
      originalProto[method].apply(this, arguments);
      // 覆盖:通知更新
      console.log("数组执行" + method + "操作");
    };
  }
);

// 判断obj为数组还是对象,分类操作
class Observer {
  constructor(obj) {
    if (Array.isArray(obj)) {
      // 数组类型
      // 覆盖原型
      obj.__proto__ = arrayProto;
      // 递归遍历数组中的子元素
      const keys = Object.keys(obj);
      for (let i = 0; i < obj.length; i++) {
        observe(obj[i]);
      }
    } else {
      // 对象类型
      this.walk(obj);
    }
  }
  walk(obj) {
    // 若是对象,则遍历其属性做拦截处理
    Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]));
  }
}

 

四、compile编译

遍历传入的子元素,分为节点和文本,再进行编译

 

// 编译
class Compile {
  constructor(el, vm) {
    this.$vm = vm;
    const node = document.querySelector(el)
    this.compile(node);
  }
  // 判断是节点还是文本
  compile(node) {
    // 获取el的子节点
    const childNodes = node.childNodes;
    // 遍历子节点并分类判断
    Array.from(childNodes).forEach(n => {
      // 元素节点
      if(this.isElement(n)) {
        // 当子节点中还有子节点时,进行递归
        console.log(n);
        if(n.childNodes.length > 0) { 
          this.compile(n)
        }
        // 编译节点
        this.compileElement(n);

      } else if(this.isInter(n)) {
        // 插值表达式:编译文本
        this.compileText(n)
        console.log('文本',n.textContent);
      }
    })
    
  }
  // 元素节点
  isElement(n) {
    return n.nodeType === 1;
  }
  // 又是文本节点插值, 又是插值表达式,例:{{count}}
  isInter(n) {
    return n.nodeType === 3 && /\{\{(.*)\}\}/.test(n.textContent);
  }
  // 判断是否为j-指令
  isDir(attrName) {
    console.log(attrName);
    return attrName.startsWith('j-');
  }

  // 编译初始化和更新函数
  update(node, exp, dir) {
    // 编译初始化
    const fn = this[dir + 'Updater'];
    fn && fn(node, this.$vm[exp]);
    // 更新函数: watcher是在遍历节点去编译时创建的
    new Watcher(this.$vm, exp, val => {
      // 接收最新的值,更新
      fn && fn(node, val);
    });
  }

  // 编译插值表达式
  compileText(n) {
    // RegExp.$1:与上方正则表达式匹配的表达式,例:count
    // 查找vm中匹配属性的值,赋给该节点
    // n.textContent = this.$vm[RegExp.$1];
    this.update(node, RegExp.$1, "text");
  }
  // 编译节点:属性分类:j-指令, @事件
  compileElement(n) {
    const attrs = n.attributes;
    console.log(n.attributes);
    // 遍历所有属性,进行分类
    Array.from(attrs).forEach(attr => {
      const attrName = attr.name;
      const exp = attr.value;
      // 若为j-指令
      if(this.isDir(attrName)) {
        // 截取j-后的指令,并执行编译对应指令
        const dir = attrName.substring(2); //text,html,...
        this[dir] && this[dir](n, exp);
      } else {
        // 若为@事件
      }
    })
  }

  // 编译j-text
  text(n, exp) {
    this.update(node, exp, 'text');
  }
  // 真正编译j-text的方法
  textUpdater(node, val) {
    node.textContent = val;
  }

  // 编译j-html
  html(n, exp) {
    this.update(node, exp, 'html');
  }
  htmlUpdate(n, exp) {
    n.innerHTML = this.$vm[exp];
  }
}

 

五、依赖收集、更新

1.watcher:依赖

 

实现思路:

1. defineReactive时为每⼀个key创建⼀个Dep实例

2. 初始化视图时读取某个key,例如name1,创建⼀个watcher1

3. 由于触发name1的getter⽅法,便将watcher1添加到name1对应的Dep中

4. 当name1更新,setter触发时,便可通过对应Dep通知其管理所有Watcher更新 

watcher在编译时创建,应包含和哪个key有关,更新函数是谁:

// 负责dom更新
// watcher: watcher在编译时创建,和哪个key有关,更新函数是谁
class Watcher {
  constructor(vm, key, updater) {
    this.vm = vm;
    this.key = key; 
    this.updater = updater; 
  }
  // 将来会被Dep调用
  update() {
    this.updater.call(this.vm, this.vm[this.key]);
  }
}

watcher是在编译时创建的,所以重构前面的Compile中的编译方法

// 编译
class Compile {
  constructor(el, vm) {
    this.$vm = vm;
    const node = document.querySelector(el);
    this.compile(node);
  }
  // 判断是节点还是文本
  compile(node) {
    // 获取el的子节点
    const childNodes = node.childNodes;
    // 遍历子节点并分类判断
    Array.from(childNodes).forEach(n => {
      // 元素节点
      if (this.isElement(n)) {
        // 当子节点中还有子节点时,进行递归
        if (n.childNodes.length > 0) {
          this.compile(n);
        }
        // 编译节点
        this.compileElement(n);
      } else if (this.isInter(n)) {
        // 插值表达式:编译文本
        this.compileText(n);
      }
    });
  }
  // 元素节点
  isElement(n) {
    return n.nodeType === 1;
  }
  // 又是文本节点插值, 又是插值表达式,例:{{count}}
  isInter(n) {
    return n.nodeType === 3 && /\{\{(.*)\}\}/.test(n.textContent);
  }
  // 判断是否为j-指令
  isDir(attrName) {
    return attrName.startsWith("j-");
  }

  // 编译初始化和更新函数
  update(node, exp, dir) {
    // 编译初始化
    const fn = this[dir + "Updater"];
    fn && fn(node, this.$vm[exp]);

    // 更新函数: watcher是在遍历节点去编译时创建的
    new Watcher(this.$vm, exp, val => {
      // 接收最新的值,更新
      fn && fn(node, val);
    });
  }

  // 编译插值表达式
  compileText(node) {
    // RegExp.$1:与上方正则表达式匹配的表达式,例:count
    // 查找vm中匹配属性的值,赋给该节点
    // n.textContent = this.$vm[RegExp.$1];
    this.update(node, RegExp.$1.trim(), "text");
  }
  // 编译节点:属性分类:j-指令, @事件
  compileElement(n) {
    const attrs = n.attributes;
    // 遍历所有属性,进行分类
    Array.from(attrs).forEach(attr => {
      const attrName = attr.name;
      const exp = attr.value;
      // 若为j-指令
      if (this.isDir(attrName)) {
        // 截取j-后的指令,并执行编译对应指令
        const dir = attrName.substring(2); //text,html,...
        this[dir] && this[dir](n, exp);
      } else if(isEvent(attrName)) {
        // 若为@事件
        // 获取事件名
        const dir = attrName.substring(1);
        this.eventHandler(n, exp, dir);
      }
    });
  }

  // 编译j-text
  text(node, exp) {
    this.update(node, exp, "text");
  }
  // 真正编译j-text的方法
  textUpdater(node, val) {
    node.textContent = val;
  }

  // 编译j-html
  html(node, exp) {
    this.update(node, exp, "html");
  }
  htmlUpdater(node, val) {
    node.innerHTML = val;
  }

  // 判断是否为@指令
  isEvent(attrName) {
    return attrName.indexOf("@") == 0;
  }
  // 处理事件
  eventHandler(node, exp, dir) {
    const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp];
    node.addEventListener(dir, fn.bind(this.$vm));
  }

  // 编译j-model:绑定value值,事件监听
  model(node, exp) {
    this.update(node, exp, "model");
    console.log(11111111111);

    node.addEventListener("input", e => {
      // 新的值赋给数据
      this.$vm[exp] = e.target.value;
    });
  }
  modelUpdater(node, value) {
    // 表单元素赋值
    node.value = value;
  }
}

 

 

2.Dep:收集依赖

依赖收集:data中每个属性都会有一个数组作为依赖收集器,当页面使用某个属性(get)时,页面的watcher就会被放到对应属性的依赖收集器中。

依赖更新:通知属性依赖收集器中的watcher进行更新

代码如下: 

// Dep:保存watcher实例的依赖类
class Dep {
  constructor() {
    this.deps = [];
  }
  // 添加依赖:创建依赖关系时调用
  addDep(dep) {
    this.deps.push(dep);
  }
  // 通知deps数组中的依赖进行更新
  notify() {
    this.deps.forEach(dep => dep.update());
  }
}

1.在defineReactive创建dep实例。

2.在getter中收集依赖,即将watcher放到该属性对应的deps数组中。

           watcher如何放到deps中呢?在watcher中初始化时便将watcher放到全局变量Dep.target中,触发对应属性的getter,             getter中将Dep.target放到deps数组中。

3.在setter中调用dep.notify()通知依赖更新

// 对obj中的属性做拦截
function defineReactive(obj, key, val) {
  // 当val是一个对象时,需要递归遍历val中的属性做拦截处理
  observe(val);

  // 创建Dep实例
  const dep = new Dep();

  // vue2中用Object.defineProperty()对属性进行跟踪拦截
  Object.defineProperty(obj, key, {
    get() {
      // 依赖收集
      Dep.target && dep.addDep(Dep.target);
      return val;
    },
    set(newVal) {
      val = newVal;
      // 当newVal为对象时,需要遍历newVal中的属性做拦截处理
      observe(newVal);
      // 通知更新
      dep.notify();
    }
  });
}

// 负责dom更新
// watcher: watcher在编译时创建,和哪个key有关,更新函数是谁
class Watcher {
  constructor(vm, key, updater) {
    this.vm = vm;
    this.key = key;
    this.updater = updater;

    // 将watcher放到Dep.target(任何一个全局变量都行),触发getter,getter中会将该Watcher放到该key所对应的Dep数组中
    Dep.target = this;
    this.vm[this.key];
    Dep.target = null;
  }
  // 将来会被Dep调用
  update() {
    this.updater.call(this.vm, this.vm[this.key]);
  }
}

 

六、整体代码

jvue.js:

// 数组响应式
// 获取Array原型,备份一份,修改备份
const originalProto = Array.prototype;
const arrayProto = Object.create(originalProto);
// 设置七个新的方法
["push", "pop", "shifit", "unshift", "sort", "reverse", "splice"].forEach(
  method => {
    arrayProto[method] = function() {
      // 原始操作
      originalProto[method].apply(this, arguments);
      // 覆盖:通知更新
      console.log("数组执行" + method + "操作");
    };
  }
);

// 对象响应式
function defineReactive(obj, key, val) {
  // 当val是一个对象时,需要递归遍历val中的属性做拦截处理
  observe(val);

  // 创建Dep实例
  const dep = new Dep();

  // vue2中用Object.defineProperty()对属性进行跟踪拦截
  Object.defineProperty(obj, key, {
    get() {
      // 依赖收集
      Dep.target && dep.addDep(Dep.target);
      return val;
    },
    set(newVal) {
      val = newVal;
      // 当newVal为对象时,需要遍历newVal中的属性做拦截处理
      observe(newVal);
      // 通知更新
      dep.notify();
    }
  });
}

// 将传入对象中的所有属性代理到指定对象上
function Proxy(vm) {
  // 遍历data中的属性,在vm中添加一样的属性,并做拦截处理
  Object.keys(vm.$data).forEach(key => {
    Object.defineProperty(vm, key, {
      get() {
        return vm.$data[key];
      },
      set(v) {
        // 当修改vm中的属性时,data中的属性也跟着修改
        vm.$data[key] = v;
      }
    });
  });
}

// 遍历对象,给它的属性做响应式处理
function observe(obj) {
  // 判断obj是否为对象,若不是则返回
  if (typeof obj !== "object" || obj == null) {
    return obj;
  }
  // 创建Observer实例,传入obj进行判断 遍历
  new Observer(obj);
}
// 判断obj为数组还是对象,分类操作
class Observer {
  constructor(obj) {
    if (Array.isArray(obj)) {
      // 数组类型
      // 覆盖原型
      obj.__proto__ = arrayProto;
      // 递归遍历数组中的子元素
      const keys = Object.keys(obj);
      for (let i = 0; i < obj.length; i++) {
        observe(obj[i]);
      }
    } else {
      // 对象类型
      this.walk(obj);
    }
  }
  walk(obj) {
    // 若是对象,则遍历其属性做拦截处理
    Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]));
  }
}

class JVue {
  constructor(options) {
    // 1.获取data对象
    this.$options = options;
    this.$data = options.data;
    // 2.对data做拦截处理
    observe(options.data);

    // 3.将data中的属性代理到vm中
    Proxy(this);

    // 4.编译
    new Compile(options.el, this);
  }
}

// 编译
class Compile {
  constructor(el, vm) {
    this.$vm = vm;
    const node = document.querySelector(el);
    this.compile(node);
  }
  // 判断是节点还是文本
  compile(node) {
    // 获取el的子节点
    const childNodes = node.childNodes;
    // 遍历子节点并分类判断
    Array.from(childNodes).forEach(n => {
      // 元素节点
      if (this.isElement(n)) {
        // 当子节点中还有子节点时,进行递归
        if (n.childNodes.length > 0) {
          this.compile(n);
        }
        // 编译节点
        this.compileElement(n);
      } else if (this.isInter(n)) {
        // 插值表达式:编译文本
        this.compileText(n);
      }
    });
  }
  // 元素节点
  isElement(n) {
    return n.nodeType === 1;
  }
  // 又是文本节点插值, 又是插值表达式,例:{{count}}
  isInter(n) {
    return n.nodeType === 3 && /\{\{(.*)\}\}/.test(n.textContent);
  }
  // 判断是否为j-指令
  isDir(attrName) {
    return attrName.startsWith("j-");
  }

  // 编译初始化和更新函数
  update(node, exp, dir) {
    // 编译初始化
    const fn = this[dir + "Updater"];
    fn && fn(node, this.$vm[exp]);

    // 更新函数: watcher是在遍历节点去编译时创建的
    new Watcher(this.$vm, exp, val => {
      // 接收最新的值,更新
      fn && fn(node, val);
    });
  }

  // 编译插值表达式
  compileText(node) {
    // RegExp.$1:与上方正则表达式匹配的表达式,例:count
    // 查找vm中匹配属性的值,赋给该节点
    // n.textContent = this.$vm[RegExp.$1];
    this.update(node, RegExp.$1.trim(), "text");
  }
  // 编译节点:属性分类:j-指令, @事件
  compileElement(n) {
    const attrs = n.attributes;
    // 遍历所有属性,进行分类
    Array.from(attrs).forEach(attr => {
      const attrName = attr.name;
      const exp = attr.value;
      // 若为j-指令
      if (this.isDir(attrName)) {
        // 截取j-后的指令,并执行编译对应指令
        const dir = attrName.substring(2); //text,html,...
        this[dir] && this[dir](n, exp);
      } else if (this.isEvent(attrName)) {
        // 若为@事件
        const dir = attrName.substring(1);
        this.eventHandler(n, exp, dir);
      }
    });
  }

  // 编译j-text
  text(node, exp) {
    this.update(node, exp, "text");
  }
  // 真正编译j-text的方法
  textUpdater(node, val) {
    node.textContent = val;
  }

  // 编译j-html
  html(node, exp) {
    this.update(node, exp, "html");
  }
  htmlUpdater(node, val) {
    node.innerHTML = val;
  }

  // 判断是否为@指令
  isEvent(attrName) {
    return attrName.indexOf("@") == 0;
  }
  // 处理事件
  eventHandler(node, exp, dir) {
    const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp];
    node.addEventListener(dir, fn.bind(this.$vm));
  }

  // 编译j-model:绑定value值,事件监听
  model(node, exp) {
    this.update(node, exp, "model");
    console.log(11111111111);

    node.addEventListener("input", e => {
      // 新的值赋给数据
      this.$vm[exp] = e.target.value;
    });
  }
  modelUpdater(node, value) {
    // 表单元素赋值
    node.value = value;
  }
}

// 负责dom更新
// watcher: watcher在编译时创建,和哪个key有关,更新函数是谁
class Watcher {
  constructor(vm, key, updater) {
    this.vm = vm;
    this.key = key;
    this.updater = updater;

    // 将watcher放到Dep.target(任何一个全局变量都行),触发getter,getter中会将该Watcher放到该key所对应的Dep数组中
    Dep.target = this;
    this.vm[this.key];
    Dep.target = null;
  }
  // 将来会被Dep调用
  update() {
    this.updater.call(this.vm, this.vm[this.key]);
  }
}

// Dep:保存watcher实例的依赖类
class Dep {
  constructor() {
    this.deps = [];
  }
  // 添加依赖:创建依赖关系时调用
  addDep(dep) {
    this.deps.push(dep);
  }
  // 通知deps数组中的依赖进行更新
  notify() {
    this.deps.forEach(dep => dep.update());
  }
}

jvue.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">
      <p>{{ count }}</p>
      <p j-text="count"></p>
      <p j-html="exp"></p>
      <button @click="add">增加</button>
      <input j-model="value" />
      <p>{{ value }}</p>
    </div>
    <script src="./jvue.js"></script>
    <script>
      const app = new JVue({
        el: "#app",
        data: {
          count: 1,
          exp: "<h3>j-html指令</h3>",
          arr: [1, 2, 3],
          value: ""
        },
        methods: {
          add() {
            this.count++;
          }
        }
      });
      // setInterval(() => {
      //   app.count++;
      //   app.arr.push(app.count);
      //   console.log(app.arr);
      // }, 1000);
    </script>
  </body>
</html>

代码仓库地址:https://github.com/141110126/write-vue/tree/main/jvue

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值