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>


目录
打赏
0
相关文章
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
206 0
Vue 自定义进度条组件封装及使用方法详解
这是一篇关于自定义进度条组件的使用指南和开发文档。文章详细介绍了如何在Vue项目中引入、注册并使用该组件,包括基础与高级示例。组件支持分段配置(如颜色、文本)、动画效果及超出进度提示等功能。同时提供了完整的代码实现,支持全局注册,并提出了优化建议,如主题支持、响应式设计等,帮助开发者更灵活地集成和定制进度条组件。资源链接已提供,适合前端开发者参考学习。
173 17
Vue 文件批量下载组件封装完整使用方法及优化方案解析
本文详细介绍了批量下载功能的技术实现与组件封装方案。主要包括两种实现方式:**前端打包方案(基于file-saver和jszip)** 和 **后端打包方案**。前者通过前端直接将文件打包为ZIP下载,适合小文件场景;后者由后端生成ZIP文件流返回,适用于大文件或大量文件下载。同时,提供了可复用的Vue组件`BatchDownload`,支持进度条、失败提示等功能。此外,还扩展了下载进度监控和断点续传等高级功能,并针对跨域、性能优化及用户体验改进提出了建议。可根据实际需求选择合适方案并快速集成到项目中。
201 17
Vue 手风琴实现的三种常用方式及长尾关键词解析
手风琴效果是Vue开发中常见的交互组件,可节省页面空间、提升用户体验。本文介绍三种实现方式:1) 原生Vue结合数据绑定与CSS动画;2) 使用Element UI等组件库快速构建;3) 自定义指令操作DOM实现独特效果。每种方式适用于不同场景,可根据项目需求选择。示例包括产品特性页、后台菜单及FAQ展示,灵活满足多样需求。附代码示例与资源链接,助你高效实现手风琴功能。
110 10
Vue 表情包输入组件的实现代码:支持自定义表情库、快捷键发送和输入框联动的聊天表情解决方案
本文详细介绍了在 Vue 项目中实现一个功能完善、交互友好的表情包输入组件的方法,并提供了具体的应用实例。组件设计包含表情分类展示、响应式布局、与输入框的交互及样式定制等功能。通过核心技术实现,如将表情插入输入框光标位置和点击外部关闭选择器,确保用户体验流畅。同时探讨了性能优化策略,如懒加载和虚拟滚动,以及扩展性方案,如自定义主题和国际化支持。最终,展示了如何在聊天界面中集成该组件,为用户提供丰富的表情输入体验。
152 8
Vue 表情包输入组件实现代码及详细开发流程解析
这是一篇关于 Vue 表情包输入组件的使用方法与封装指南的文章。通过安装依赖、全局注册和局部使用,可以快速集成表情包功能到 Vue 项目中。文章还详细介绍了组件的封装实现、高级配置(如自定义表情列表、主题定制、动画效果和懒加载)以及完整集成示例。开发者可根据需求扩展功能,例如 GIF 搜索或自定义表情上传,提升用户体验。资源链接提供进一步学习材料。
89 1
Vue框架中常见指令的应用概述。
通过以上的详细解析,你应该已经初窥Vue.js的指令的威力了。它们是Vue声明式编程模型的核心之一,无论是构建简单的静态网站还是复杂的单页面应用,你都会经常用到。记住,尽管Vue提供了大量预定义的指令,你还可以创建自定义指令以满足特定的需求。为你的Vue应用程序加上这些功能增强器,让编码变得更轻松、更愉快吧!
39 1
如何高效实现 vue 文件批量下载及相关操作技巧
在Vue项目中,实现文件批量下载是常见需求。例如文档管理系统或图片库应用中,用户可能需要一次性下载多个文件。本文介绍了三种技术方案:1) 使用`file-saver`和`jszip`插件在前端打包文件为ZIP并下载;2) 借助后端接口完成文件压缩与传输;3) 使用`StreamSaver`解决大文件下载问题。同时,通过在线教育平台的实例详细说明了前后端的具体实现步骤,帮助开发者根据项目需求选择合适方案。
106 0
Vue 项目中如何自定义实用的进度条组件
本文介绍了如何使用Vue.js创建一个灵活多样的自定义进度条组件。该组件可接受进度段数据数组作为输入,动态渲染进度段,支持动画效果和内容展示。当进度超出总长时,超出部分将以红色填充。文章详细描述了组件的设计目标、实现步骤(包括props定义、宽度计算、模板渲染、动画处理及超出部分的显示),并提供了使用示例。通过此组件,开发者可根据项目需求灵活展示进度情况,优化用户体验。资源地址:[https://pan.quark.cn/s/35324205c62b](https://pan.quark.cn/s/35324205c62b)。
50 0
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问