Vue 响应式

242 阅读5分钟

vue.png

一、数据驱动

  • 数据响应式: 数据模型仅仅是 JavaScript 对象,当数据发生改变时,视图就会更新,避免了繁琐的 dom 操作,提高开发效率.
  • 双向绑定: 数据改变,视图改变;视图改变,数据改变.
  • 数据驱动: 开发过程只需要注重数据本身,不需要关注数据是如何渲染到视图.

二、数据响应式原理

Vue 2.x

  • Vue2.x 深入响应式原理
  • 自定义 obsever 实现响应式,除了针对普通对象属性进行数据劫持之外,也对值为 object 类型的属性进行了递归拦截. 效果如下:

image.png

<!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">
    <h1 class="msg">msg: hello world</h1>
    <h1 class="date">
      <span class="start">start: 2020</span>
      - <span class="end">end: 2021</span>
    </h1>
    <h1 class="talk">talk: say...</h1>
  </div>

  <script>
    let data = {
      msg: "hello world",
      date: {
        start: 2020,
        end: 2021,
      },
      person: {
        skill: {
          talk: "say..."
        }
      }
    };

    obsever(data);

    function obsever(data) {
      // 遍历获取所有的 key
      Object.keys(data).forEach(key => {
        let val = data[key];
        // 当前值为对象,进行递归
        if (Object.prototype.toString.call(val) === "[object Object]") {
          obsever(val);
        } else {
          Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get() {
              console.log("getter...", key);
            },
            set(newVal) {
              if (newVal === val) return;
              console.log("setter...", key);
              document
                .querySelector(`.${key}`)
                .textContent = `${key}: ${newVal}`;
            }
          });
        }

      });
    }
  </script>

</body>

</html>

Vue 3.x

- MDN - Proxy,直接监听对象,而不是属性.

  • ES6 新增,不支持 IE,性能由浏览器优化,所以优化方面要比 defineProperty 更好.
  • proxy 实现响应式,效果如下:

image.png

<!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"></div>

  <script>
    let data = {
      msg: "hello world",
    };

    let proxy = obsever(data);
    function obsever(data) {
      return new Proxy(data, {
        get(target, key, receiver) {
          console.log("getter...", key, Reflect.get(target, key, receiver));
          return target[key];
        },
        set(target, key, newVal, receiver) {
          const oldValue = target[key];
          if (oldValue == newVal) return true;
          let result = Reflect.set(target, key, newVal, receiver);
          console.log('setter...', key, newVal);
          update();
          return result;
        }
      });
    }

    update();
    function update() {
      document.querySelector("#app").innerHTML = `
      <h1 class="msg">msg: ${proxy.msg}</h1>`
    }
  </script>

</body>
</html>

三、发布/订阅模式 & 观察者模式

发布/订阅模式

假定存在一个信号中心,当某个任务完成之后,就向信号中心进行 发布(public) 信号,其他任务可以向信号中心 订阅(subscribe),从而知道自己什么时候开始执行.

  • 订阅者
  • 发布者
  • 信号中心

3.1 Vue 自定义事件

  • Vue 自定义事件
  • 从下图中并不能很好的区分 订阅者、发布者、信号中心 image.png

3.2 兄弟组件通信

  • 假设 AB 两个兄弟组件需要进行通信,我们通过 Vue 创建一个 eventHub,通过它来实现发布和订阅.
  • A 组件 中存在 addTo 方法实现消息的发布;B 组件 中在 created 中实现对消息的订阅.
  • eventHub(信号中心)ComponentA(发布者)ComponentB(订阅者)
 <script>
    // 信号中心
    let eventHub = new Vue({});

    // ComponentA —— 发布者
    addTo() {
      // 发布消息(事件)
      vm.$emit("dataChange", {
        data: 1000
      });
    }

    // ComponentB —— 订阅者
    created() {
      // 订阅消息(事件)
      vm.$on("dataChange", (data) => {
        console.log("dataChange.......",data);
      })
    }
  </script>

3.3 模拟 Vue 自定义事件(发布/订阅)

<script>
    class EventEmitter {
      constructor() {
        this.subs = {}; // 用于存储注册的事件
      }

      //  注册事件(订阅)
      on(type, handle) {
        if (this.subs[type]) { // 对应事件已存在
          this.subs[type].push(handle);
        } else { // 对应事件首次注册
          this.subs[type] = [handle];
        }
      }

      // 触发事件(发布)
      emit(type, params) {
        if (this.subs[type]) {
          this.subs[type].forEach(handle => {
            handle && handle(params);
          });
        }
      }
    }

    let emitter = new EventEmitter();
    emitter.on("click", (data) => {
      console.log("click 1", data);
    });

    emitter.on("click", (data) => {
      console.log("click 2", data);
    });

    emitter.emit("click", {
      text: "hello world"
    });
  </script>

观察者模式

  • 观察者 (订阅者) —— Watcher
    • update():当事件发生时,具体要做的事情
  • 目标 (发布者) —— Dep
    • subs 数组:存储所有的观察者
    • addSubs():添加观察者
    • notify():当事件发生时,调用所有的观察者的 update() 方法
  • 没有事件中心

观察者模式发布/订阅模式 的概念非常的相似,注意不要混淆.

实现一个简单的观察者模式:

<script>
    /**
     *  观察者模式
    **/

    // 发布者——目标
    class Dep {
      constructor() {
        this.subs = []; // 存储所有观察者
      }

      addSubs(watcher) { // 添加观察者
        if (watcher && watcher.update) {
          this.subs.push(watcher);
        }
      }

      notify(parms) { // 执行所有的观察者
        this.subs.forEach(watcher => {
          if (watcher) {
            watcher.update(parms);
          }
        });
      }
    }

    // 订阅者——观察者
    class Watcher {
      update(params) { // 需要发布者调用
        console.log("update......", params);
      }
    }

    // 测试
    let dep = new Dep();
    let watcher = new Watcher();

    dep.addSubs(watcher);

    dep.notify({
      data: [1, 2, 3, 4, 5, 6]
    })
  </script>

发布/订阅模式 和 观察者模式 的区别

  • 观察者模式:【目标】 和 【观察者】 是相互依赖的关系
  • 发布/订阅模式:【发布者】和【订阅者】之间需要通过【事件中心】进行通信,减少【发布者】和【订阅者】之间的依赖,更灵活 image.png

四、模拟 Vue 响应式原理

  • vue 基本结构 image.png
  • vue 实例对象

image.png

  • 整体结构
    • Vue: 把 data 转换成 gette/setter,并把 data 中的成员注入到 Vue 实例上.
    • Observer: 能够对数据对象的所有属性进行监听,数据发生变动时会拿到最新值,并通知 Dep , Dep 会通知所有的 Watcher 进行更新.
    • Dep & Watcher: 熟悉的观察者模式,Dep 负责把所有的观察者 Watcher 添加进来,Watcher 中的 update 方法负责视图的更新. image.png

要模拟实现 Vue 的功能

  • 1. 实现 Vue 类
    • 负责接收初始化参数(选项)
    • 负责把 data 中的数据注入到 Vue 实例上,并转换成对应的 gette/setter
    • 负责调用 Observer 监听 data 中所有属性的变化
    • 负责调用 Compile 解析 指令/插值表达式
class Vue {
  constructor(options) {
    // 1. 接收初始化参数(选项) options
    this.$options = options || {};
    this.$data = options.data || {};
    this.$el =
      typeof options.el === "string"
        ? document.querySelector(options.el)
        : options.el;
    // 2. 把 data 中的数据注入到 Vue 实例上,并转换成对应的 gette/setter
    this._proxyData(this.$data);
    // 3. 调用 Observer 监听 data 中所有属性的变化
    new Observer(this.$data);
    // 4. 调用 Compile 解析 指令/插值表达式
    new Compiler(this);
  }

  _proxyData(data) {
    Object.keys(data).forEach((key) => {
      // 将 data 中的数据注入到 this 上
      Object.defineProperty(this, key, {
        configurable: true,
        enumerable: true,
        get() {
          return data[key];
        },
        set(newVal) {
          if (data[key] === newVal) return;
          data[key] = newVal;
        },
      });
    });
  }
}
  • 2. 实现 Observer 类
    • 负责把 data 中的属性转换为响应式
    • 如果 data 中的属性为对象,也要将这个对象转换为响应式
    • 当 data 中数据发生变化时,要发送通知
class Observer {
  constructor(data) {
    this.walk(data);
  }

  walk(data) {
    // 1. 判断 data 不为空 或者 不是一个对象
    if (!data || typeof data !== "object") return;
    // 2. 否则遍历 data 中的所有属性
    Object.keys(data).forEach((key) => {
      this.defineReative(data, key, data[key]);
    });
  }

  // 调用 Object.defineProperty 将属性转换成 getter/setter
  defineReative(obj, key, val) {
    const that = this;
    // 收集依赖,发送通知
    const dep = new Dep();
    // 如果 val 是对象,那么把 val 内部的属性也转换成响应式数据
    this.walk(val);

    Object.defineProperty(obj, key, {
      configurable: true,
      enumerable: true,
      get() {
        // 收集依赖
        Dep.target && dep.addSub(Dep.target);
        return val;
      },
      set(newVal) {
        if (newVal === val) return;
        val = newVal;
        // 防止当前属性被重新赋值为一个新对象时,失去响应式
        that.walk(val);
        // 发送通知
        dep.notify();
      },
    });
  }
}
  • 3. 实现 Compiler 类
    • 负责编译模板,解析指令/插值表达式,实例化 Watcher 实例,触发 get 方法,向 Dep 添加 Watcher 实例
    • 负责页面的首次渲染
    • 当数据变化后重新渲染视图

image.png

class Compiler {
  constructor(vm) {
    this.el = vm.$el;
    this.vm = vm;
    // 页面的初始化渲染
    this.compile(this.el);
  }

  // 编译模板,处理文本节点和元素节点
  compile(el) {
    let childNodes = el.childNodes; // childNodes 伪数组
    Array.from(childNodes).forEach((node) => {
      if (this.isTextNode(node)) {
        // 处理文本节点
        this.compileText(node);
      } else if (this.isElementNode(node)) {
        // 处理元素节点
        this.compileElement(node);
      }

      // 判断 node 是否存在子节点,如果存在,要递归遍历子节点
      if (node.childNodes && node.childNodes.length) {
        this.compile(node);
      }
    });
  }

  // 编译元素节点,处理指令
  compileElement(node) {
    // console.log(node.attributes); // 伪元素
    // 遍历所有的属性节点
    Array.from(node.attributes).forEach((attr) => {
      let attrName = attr.name;
      // 判断是否是指令
      if (this.isDiretive(attrName)) {
        // 去除 v- 前缀,如:v-text ——> text
        attrName = attrName.substr(2);
        let key = attr.value;
        this.update(node, key, attrName);
      }
    });
  }

  // 根据指令调用不同的 updater
  update(node, key, attrName) {
    let updateFunc = this[attrName + "Upater"];
    updateFunc && updateFunc.call(this, node, this.vm[key], key);
  }

  // 处理 v-text 指令
  textUpater(node, value, key) {
    node.textContent = value;
    // 创建 Watcher 对象,当数据改变更新视图
    new Watcher(this.vm, key, (newVlaue) => {
      node.textContent = newVlaue;
    });
  }

  // 处理 v-model 指令
  modelUpater(node, value, key) {
    node.value = value;
    // 创建 Watcher 对象,当数据改变更新视图
    new Watcher(this.vm, key, (newVlaue) => {
      node.value = newVlaue;
    });
    // 注册事件
    node.addEventListener("input", () => {
      this.vm[key] = node.value;
    });
  }

  // 编译文本节点,处理插值表达式
  compileText(node) {
    // console.dir(node); //以对象形式打印文本节点

    // 用于匹配插值表达式,如:{{ msg }}
    let reg = /\{\{(.+?)\}\}/;
    let value = node.textContent;
    if (reg.test(value)) {
      // 用于获取正则表达式中匹配到的分组,并去除匹配内容前后的空格
      let key = RegExp.$1.trim();
      node.textContent = value.replace(reg, this.vm[key]);

      // 创建 Watcher 对象,当数据改变更新视图
      new Watcher(this.vm, key, (newVlaue) => {
        node.textContent = newVlaue;
      });
    }
  }

  // 判断元素属性是否是指令,判断属性是否是 v- 开头
  isDiretive(attrName) {
    return attrName.startsWith("v-");
  }

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

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

  • 4. 实现 Dep(Dependcy) 类
    • 收集依赖,添加观察者(Watcher)
    • 依赖变化,通知观察者更新

image.png

class Dep {
  constructor() {
    this.subs = []; // 存储所有的观察者
  }

  // 添加观察者
  addSub(sub) {
    // 约定 sub 必须为 watcher 类
    if (sub && sub.update) {
      this.subs.push(sub);
    }
  }

  // 通知观察者
  notify() {
    this.subs.forEach((subs) => {
      subs.update();
    });
  }
}
  • 5. 实现 Watcher 类
    • 当数据变化触发依赖,Dep 通知所有的 Watcher 更新视图
    • 在实例化自身时,往 Dep 中添加自己的实例

image.png

class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm; // vue 的实例对象
    this.key = key; // data 中属性的名称
    this.cb = cb; // 回调函数,负责视图更新
    // 1. 把当前的 Whatcher 实例记录在 Dep.target 这个静态属性中
    Dep.target = this;
    // 2. 触发属性的 get 方法,在 get 方法中鬼调用 dep.addSub 方法添加观察者
    this.oldVal = vm[key]; // data 中对应 key 上一次的值
    // 3. 每次添加完 watcher 实例后,清空 Dep.target
    Dep.target = null;
  }

  // 当数据发生变化,更细视图
  update() {
    let newVal = this.vm[this.key];
    if (newVal === this.oldVal) return;
    this.cb(newVal);
  }
}
  • 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>my-vue</title>
</head>

<body>
  <div id="app">
    <h1>text</h1>
    <h2>{{count}}</h2>
    <h2>{{msg}}</h2>
    <hr />
    <h1>v-text</h1>
    <h2 v-text="msg"></h2>
    <hr />
    <h1>v-model</h1>
    <label for="msg"> msg:</label>
    <input id="msg" type="text" v-model="msg">
    <label for="count">count:</label>
    <input id="count" type="text" v-model="count">
  </div>

  <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 world',
        count: 1,
        person: {
          name: '张三',
          age: 30
        }
      }
    });
  </script>

</body>

</html>