Vue 响应式(下)

简介: Vue 响应式

四、模拟 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>


目录
相关文章
|
7天前
|
缓存 JavaScript 前端开发
vue学习第四章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中计算属性的基本与复杂使用、setter/getter、与methods的对比及与侦听器的总结。如果你觉得有用,请关注我,将持续更新更多优质内容!🎉🎉🎉
vue学习第四章
|
7天前
|
JavaScript 前端开发
vue学习第九章(v-model)
欢迎来到我的博客,我是瑞雨溪,一名热爱JavaScript与Vue的大一学生,自学前端2年半,正向全栈进发。此篇介绍v-model在不同表单元素中的应用及修饰符的使用,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
vue学习第九章(v-model)
|
7天前
|
JavaScript 前端开发 开发者
vue学习第十章(组件开发)
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文深入讲解Vue组件的基本使用、全局与局部组件、父子组件通信及数据传递等内容,适合前端开发者学习参考。持续更新中,期待您的关注!🎉🎉🎉
vue学习第十章(组件开发)
|
13天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
13天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
13天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
13天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
12天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉
|
12天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。自学前端2年半,熟悉JavaScript与Vue,正向全栈方向发展。博客内容涵盖Vue基础、列表展示及计数器案例等,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
|
4天前
|
JavaScript 前端开发 API
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
24 0
下一篇
无影云桌面