Vue.js响应式原理(四)——模拟vue响应式原理

分析

整体分析模拟最小版本的vue步骤:

  1. 观察Vue基本使用结构

  2. 打印Vue实例观察即将要模拟的成员:

    • 以_开头的是私有成员(例如_data),以 开 头 的 是 公 共 成 员 ( 例 如 ‘ 开头的是公共成员(例如` (data`),我们只模拟公共成员即可。
    • $el可以是选择器,也可以是dom对象,如果是选择器需要我们转换成dom对象进行存储。
    • 最小版本准备模拟$data、$el、$options
  3. 最小版本的vue的整体结构如下,即需要实现的几种类型。
    在这里插入图片描述
    不同类分别的作用如下:

  • Vue:把data中的成员注入到Vue实例,并且把data中的成员转成getter/setter;Vue内部会调用Observer和Compiler。
  • Observer(数据劫持):能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知Dep(发布者)
  • Compiler(解析指令):解析每个元素中的指令及插值表达式,并替换成对应的数据
  • Dep(发布者):添加观察者,当数据发生变化时通知所有观察者
  • Watcher(观察者):有一个update方法,当数据变化负责更新视图。

实现

准备一个html,里面具有需要解析的结构,如下:

<!DOCTYPE html>
<html lang="cn">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Mini Vue</title>
  </head>
  <body>
    <!-- 需要被解析的html start -->
    <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" />
      <input type="text" v-model="count" />
    </div>
    <!-- 需要被解析的html end -->

    <!-- 自定义的类依次引入,需要被依赖的类优先引入 -->
    <!-- <script src="./js/dep.js"></script> -->
    <!-- <script src="./js/watcher.js"></script> -->
    <!--<script src="./js/compiler.js"></script> -->
    <!--<script src="./js/observer.js"></script> -->
    <!--<script src="./js/vue.js"></script> -->
    <script>
      let vm = new Vue({
        el: "#app",
        data: {
          msg: "Hello Vue",
          count: 100,
          person: { name: "zs" },
        },
      });
      console.log(vm.msg);
      // vm.msg = { test: "Hello" };
      vm.test = "abc";
    </script>
  </body>
</html>

Vue类

  • 功能:

    • 负责接收初始化的参数(选项)
    • 负责把 data 中的属性注入到 Vue 实例,转换成 getter/setter
    • 负责调用 observer 监听 data 中所有属性的变化
    • 负责调用 compiler 解析指令/插值表达式
  • 类图结构:
    在这里插入图片描述
    解释说明:

约定以 下划线 开头的成员都是私有成员。
_proxyData():将data的属性转换成getter和setter,注入到vue实例中。

  • 代码:

class Vue {
  constructor(options) {
    // 1.保存选项的数据
    this.$options = options || {};
    this.$data = options.data || {};
    const el = options.el;
    this.$el = typeof el === "string" ? document.querySelector(el) : el;
    // 2.把data中的属性注入到vue实例中,转换成getter和setter
    this._proxyData(this.$data);
    // 3.调用Observer实现数据劫持
    new Observer(this.$data);
    // 4.调用Compiler解析指令/插值表达式等
    new Compiler(this);
  }
  _proxyData(data) {
    Object.keys(data).forEach((key) => {
      // 这里的this是指vue实例,所以直接向其注入转换成getter和setter的属性
      Object.defineProperty(this, key, {
        configurable: true,
        enumerable: true,
        get() {
          return data[key];
        },
        set(newVal) {
          if (newVal === data[key]) return;
          data[key] = newVal;
        },
      });
    });
  }
}

Observer (数据劫持)类

  • 功能

    • 负责把 data 选项中的属性转换成响应式数据
    • data 中的某个属性也是对象,把该属性转换成响应式数据
    • 数据变化发送通知 ----------------?
  • 类图
    在这里插入图片描述
    解释说明:

  1. walk(data):遍历data的所有属性,会调用defineReactive方法进行数据转换。
  2. defineReactive(data,key,value):将数据定义为响应式数据,即getter和setter。
  • 代码:

class Observer {
  constructor(data) {
    this.walk(data);
  }
  // 1.遍历data所有属性
  walk(data) {
    // 加强程序健壮性,做对象判断
    if (!data || typeof data !== "object") return;
    Object.keys(data).forEach((key) => {
      this.defineReactive(data, key, data[key]);
    });
  }
  // 2.将data转换成响应式数据,即getter和setter
  // 特别说明:该方法中需要传入value而不直接使用data[key]的原因是 会造成循环引用的问题。
  defineReactive(data, key, value) {
    const that = this;
    // ______特别加入:为每一个属性增加Dep对象,负责收集依赖,发送通知。
    const dep = new Dep();
    // 特别情况1:当data的某一属性值也是对象时,也需要将其转换为getter和setter
    this.walk(value);
    Object.defineProperty(data, key, {
      configurable: true,
      enumerable: true,
      get() {
        // ______特别加入:在getter中收集观察者:
        // 判断是否有观察者的方式是在watcher类实例化时,向Dep类上挂载一个target属性,用于存储当前的watcher对象,在这里进行target判断,如果有,就使用dep.addSub()添加观察者。
        Dep.target && dep.addSub(Dep.target);
        return value;
      },
      set(newVal) {
        if (newVal === value) return;
        // 特别情况2:当设置data的某一个新属性值为对象时,也需要将其转换为getter和setter。
        // 由于这是在set方法中,this指向并非vue实例,故需要使用外面的that指向vue实例的对象。
        that.walk(newVal);
        value = newVal;
        // ____特别加入:在setter时,需要通知所有的观察者变化。
        dep.notify();
      },
    });
  }
}

Compiler(编译器)类

  • 功能:(操作dom)
    • 负责编译模板,解析指令/插值表达式
    • 负责页面的首次渲染
    • 当数据变化后重新渲染视图

注意:这里没有使用虚拟dom,直接操作的dom。

  • 类图:

在这里插入图片描述

解释说明:

  1. el:传入的dom对象,后面需要渲染dom使用
  2. vm: vue实例,后面需要vm中的数据
  3. compiler(el):遍历el这个dom对象的所有节点,不同的节点调用解析文本节点或者元素节点的方法
  4. compileElement(node):解析元素节点,即解析元素中的指令
  5. compileText(node):解析文本节点,即解析差值表达式
  6. isDirective(attrName):判断元素节点上的属性 是否是 vue指令
  7. isTextNode(node):判断是否是文本节点
  8. isElementNode(node):判断是否是元素节点
  • 代码:

class Compiler {
  constructor(vm) {
    // 方便后面使用,将vm与el存储
    this.vm = vm;
    this.el = vm.$el;
    // 编译模板
    this.compiler(this.el);
  }
  // 1.遍历所有el中的节点
  compiler(el) {
    // 注意:这里使用childNodes可以获取所有子节点,而children只能获取所有子元素。这里只获取一层的子节点。
    const nodes = el.childNodes;
    // nodes是伪数组,需要转换成数组
    Array.from(nodes).forEach((node) => {
      if (this.isElementNode(node)) {
        this.compileElement(node);
      } else if (this.isTextNode(node)) {
        this.compileText(node);
      }
      // 当子节点中还有其子节点时,需要递归调用
      if (node.childNodes && node.childNodes.length) {
        this.compiler(node);
      }
    });
  }

  // 1-1 判断是否文本节点
  isTextNode(node) {
    return node.nodeType === 3;
  }

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

  // 1-3 解析差值表达式
  compileText(node) {
    // 正则匹配 差值表达式
    const reg = /\{\{(.+?)\}\}/;
    // node.textContent和node.nodeValue都是当前节点的内容
    const content = node.textContent;
    if (reg.test(content)) {
      // 使用$1可获取正则表达式中小括号中的值
      const value = RegExp.$1.trim();
      // 差值表达式去掉双括号后,就是变量的名字,由于变量已经都在observer.js中挂载到了vm实例中,故这里取值
      node.textContent = content.replace(reg, this.vm[value]);
      // ____数据响应添加:实例化Watcher对象
      new Watcher(this.vm, value, (newValue) => {
        node.textContent = newValue;
      });
    }
  }

  // 1-4 解析指令
  // 首次渲染——只渲染v-text和v-model
  compileElement(node) {
    const attributes = node.attributes;
    Array.from(attributes).forEach((attr) => {
      let attrName = attr.name;
      if (this.isDirective(attrName)) {
        // 根据不同的指令进行不同的渲染
        // 不采取if语句,使用字符串拼接方法名的方式,便于扩展更多的指令处理方法
        attrName = attrName.substr(2); //从索引2开始截取到最后,获取到v-text指令名去除v-后的字符串。
        // 获取该属性的属性值
        const key = attr.value;
        // 解析不同的指令
        this.update(node, key, attrName);
      }
    });
  }
  // 1-1-1 判断是否vue指令
  isDirective(attrName) {
    return attrName.startsWith("v-");
  }
  // 负责更新Dom,update相当于一个watcher,统一执行不同指令解析的位置
  update(node, key, attrName) {
    const updaterFn = this[attrName + "Updater"];
    updaterFn && updaterFn.call(this, node, this.vm[key], key);
  }
  // v-text解析方法
  textUpdater(node, value, key) {
    node.textContent = value;
    // ____数据响应添加:实例化Watcher对象
    new Watcher(this.vm, key, (newValue) => {
      node.textContent = newValue;
    });
  }
  // v-model解析方法
  modelUpdater(node, value, key) {
    node.value = value;
    // ____数据响应添加:实例化Watcher对象
    new Watcher(this.vm, key, (newValue) => {
      node.value = newValue;
    });
    // ____为了实现双向绑定,页面输入的变化,同步到数据,需要定义input方法
    node.addEventListener("input", () => {
      this.vm[key] = node.value;
    });
  }
}

Dep(发布者)类

在这里插入图片描述

  • 功能:

    • 收集依赖,添加观察者(watcher)
    • 通知所有观察者
  • 类图:

在这里插入图片描述
解释说明:

  • subs:用来存储所有观察者的数组属性

  • addSub(sub):添加观察者的方法

  • notify():通知所有观察者的方法

  • 代码:


class Dep {
  constructor() {
    // 1.初始化subs存储所有观察者的数组
    this.subs = [];
  }
  // 2.添加观察者
  addSub(sub) {
    //判断是否有传递观察者,且约定观察者必须有update方法,方便在被通知后,调用update方法
    if (sub && sub.update) {
      this.subs.push(sub);
    }
  }
  // 3.发送通知给所有观察者
  notify() {
    this.subs.forEach((sub) => {
      sub.update();
    });
  }
}

Watcher(观察者)类

在这里插入图片描述

  • 功能:

    • 当数据变化就触发依赖,dep通知所有的watcher实时更新视图
    • 自身实例化的时候往dep对象中添加自己。
  • 类图:

在这里插入图片描述
解释说明:

update():方法,update中进行更新视图
cb:属性,不同的watcher更新的操作不一样。通过cb交代
vm:属性,通过vue实例获取属性对应的值
key:属性,当前观察的属性名称
oldValue:属性,存储变化之前的值

  • 代码:

class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm;
		this.key = key;
		// 回调函数负责更新视图
    this.cb = cb;
    // 1.实例化时,要把自己加入Dep类中,
    // 1.1 先将自己存储在target静态属性中
    Dep.target = this;
    // 1.2 再通过取值操作,触发执行属性的getter,在observer.js文件中,属性的getter方法中会通过判断Dep.target属性来进行存储当前的watcher实例
    this.oldValue = this.vm[key];
    // 1.3 为了避免重复添加,每一次判断后,需要将Dep的target属性置空。
    Dep.target = null;
	}
	// 2.更新视图
  update() {
    //此时,vue实例对象中data中的key属性值已经变为最新的了
    let newValue = this.vm[this.key];
    if (newValue === this.oldValue) return;
    this.cb(newValue);
  }
}

Watcher类在Compiler中的实例化
  • 作用:为了实现数据改变=》发送通知=》改变视图,更新视图的时候需要调用update,但在update方法中是通过cb回调函数实现的,所以我们需要实例化Watcher类后,将cb回调函数定义,从而实现该功能。

  • Watcher类实例化的位置:compiler代码中,所有把数据渲染成dom的位置都需要创建一个watcher对象。

  • 代码:(compiler文件中新增的代码)


class Compiler {
  constructor(vm) {
    this.vm = vm;
    this.el = vm.$el;
    this.compiler(this.el);
  }
  compiler(el) {
    const nodes = el.childNodes;
    Array.from(nodes).forEach((node) => {
      if (this.isElementNode(node)) {
        this.compileElement(node);
      } else if (this.isTextNode(node)) {
        this.compileText(node);
      }
      if (node.childNodes && node.childNodes.length) {
        this.compiler(node);
      }
    });
  }

  isTextNode(node) {
    return node.nodeType === 3;
  }

  isElementNode(node) {
    return node.nodeType === 1;
  }

  compileText(node) {
    const reg = /\{\{(.+?)\}\}/;
    const content = node.textContent;
    if (reg.test(content)) {
      const value = RegExp.$1.trim();
      node.textContent = content.replace(reg, this.vm[value]);
      new Watcher(this.vm, value, (newValue) => {
        node.textContent = newValue;
      });
    }
  }

  compileElement(node) {
    const attributes = node.attributes;
    Array.from(attributes).forEach((attr) => {
      let attrName = attr.name;
      if (this.isDirective(attrName)) {
        attrName = attrName.substr(2); 
        const key = attr.value;
        this.update(node, key, attrName);
      }
    });
  }
  
  isDirective(attrName) {
    return attrName.startsWith("v-");
  }
 
  update(node, key, attrName) {
    const updaterFn = this[attrName + "Updater"];
    updaterFn && updaterFn.call(this, node, this.vm[key], key);
  }

  textUpdater(node, value, key) {
    node.textContent = value;
    // ____数据响应添加:实例化Watcher对象
    new Watcher(this.vm, key, (newValue) => {
      node.textContent = newValue;
    });
  }
  modelUpdater(node, value, key) {
    node.value = value;
    // ____数据响应添加:实例化Watcher对象
    new Watcher(this.vm, key, (newValue) => {
      node.value = newValue;
    });
    // ____为了实现双向绑定,页面输入的变化,同步到数据,需要定义input方法
    node.addEventListener("input", () => {
      this.vm[key] = node.value;
    });
  }
}

特别说明:

  1. 在写Dep类和Watcher类的时候,需要随之修改两个文件中的类
  • 一是observer.js中的Observer类(因为在这个类中为data中的每一个属性转换为getter和setter形式)

原因是:考虑到需要为每一个data对象中的属性添加一个Dep实例,在getter方法中添加观察者(这里也会涉及辅助Watcher类中实例的存储),在setter方法中通知所有观察者做修改。

  • 二是compiler.js中的Compiler类 (因为在这个类中主要做dom操作,数据的双向绑定都需要在这里进行修改)

原因是:Watcher类的实例化位置需要在Compiler类中,因为需要实现数据的变化。

整体流程

在这里插入图片描述
当页面首次加载的时候,是通过Compiler更新视图;
当页面数据变化的时候,是通过Watcher更新视图。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值