Vue.js响应式原理,数据双向绑定

响应式原理

数据驱动

  1. 数据响应式
    • 数据模型仅仅是普通的JavaScript对象,而当我们修改数据时,视图会进行更新,避免了繁琐的DOM操作,提高开发效率
  2. 双向绑定
    • 数据改变,视图改变;视图改变,数据也随之改变
    • 我们可以使用v-model在表单元素上创建双向绑定数据
  3. 数据驱动
    • 开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图

Vue2 数据响应式核心原理:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <div id="app"> hello </div> </body> <script> // 模拟Vue中的data选项 let data = { msg: "hello", count: 10, }; // 模拟Vue中的实例 let vm = {}; // 调用封装响应式函数 proxyData(data); function proxyData(data) { // 遍历data对象的所有属性 Object.keys(data).forEach((key) => { // 数据劫持:当访问或者设置vm中的成员的时候,做一些干预操作 Object.defineProperty(vm, key, { // 可枚举 enumerable: true, // 可配置(可以使用delete删除,可以通过defineProperty重新定义) configurable: true, // 当前获取值时候执行 get() { console.log("get:", data[key]); return data[key]; }, // 当设置值时候执行 set(newValue) { console.log("set", newValue); if (newValue === data[key]) { return; } data[key] = newValue; // 数据更改,更新DOM的值 document.querySelector("#app").textContent = data[key]; }, }); }); } // 测试 vm.msg = "Hello World"; console.log(vm.msg); </script> </html>

Vue3 数据响应式原理:

  • Proxy介绍
  • 直接监听对象,而非属性
  • ES6中新增,IE不支持,性能由浏览器优化
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <div id="app">hello</div> </body> <script> // 模拟Vue中的data选项 let data = { msg: "hello", count: 22, }; // 模拟Vue实例 let vm = new Proxy(data, { // 当访问vm的成员时会执行 get(target, key) { console.log("get:", key, target[key]); return target[key]; }, // 当设置 vm 的成员时会执行 set(target, key, newValue) { console.log(console.log("set:", key, newValue)); if (target[key] === newValue) { return; } target[key] = newValue; document.querySelector("#app").textContent = target[key]; }, }); </script> </html>
  • 使用Proxy代理的是整个对象,对象中所有属性在访问或设置时都会触发get,set
  • 使用defineProperty处理多个属性的时候需要循环

发布-订阅模式

  • 发布者
  • 订阅者
  • 信号中心(事件中心)

我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"一个信号,其他任务可以向信号中心"订阅"这个信号,从而知道什么时候自己可以开始自行. 这就叫做"发布-订阅模式"

  • 兄弟组件通信过程
    • 通过事件中心来进行通信
// eventBus.js // 事件中心 const eventHub = new Vue() // ComponentA.vue // 发布者 addTodo: function() { // 发布消息(事件) eventHub.$emit('add-todo',{ text: this.newTodoText }) this.newTodoText = '' } // ComponentB.vue // 订阅者 create: function() { // 订阅消息(事件) eventHub.$on('add-todo', this.addTodo) }
  • 模拟vue自定义事件(事件中心)
    • 可以注册多个事件名称,也可以为一个事件注册多个处理函数
    • 内部以对象的形式存储注册的事件
    • 以下是不考虑传参情况的事件处理中心
    • 有了事件中心之后在兄弟组件之间通信就可以很清晰的了解发布-订阅模式
// 事件触发器 class EventEmitter { constructor() { // 不设置原型的对象方法,可以提高性能 // 对象的形式{ 'click': [fn1, fn2], 'change': [fn]} this.subs = Object.create(null); } // 注册事件(订阅) $on(eventType, handler) { // 确保事件类型的值一定是一个数组 this.subs[eventType] = this.subs[eventType] || []; // 赋值操作,将事件的处理函数赋值给事件名 this.subs[eventType].push(handler); } // 触发事件(发布) $emit(eventType) { if (this.subs[eventType]) { this.subs[eventType].forEach((handler) => { handler(); }); } } } let em = new EventEmitter(); // 注册事件(订阅) em.$on("dataChange", () => { console.log("dataChange1"); }); em.$on("dataChange", () => { console.log("dataChange2"); }); // 触发事件(发布) em.$emit("dataChange");

观察者模式

  • 观察者(订阅者) --Watcher

    • update(): 当事件发生时,具体要做的事情
  • 目标(发布者) --Dep

    • subs数组: 存储所有的观察者
    • addSub(): 添加观察者
    • notify(): 当事件发生,调用所有观察者的update()方法
  • 没有事件中心

// 发布者(目标) class Dep { constructor() { // 记录所有的订阅者 this.subs = []; } // 添加订阅者 addSub(sub) { if (sub && sub.update) { this.subs.push(sub); } } // 调用订阅者update方法 notify() { this.subs.forEach((sub) => { sub.update(); }); } } // 订阅者(观察者) class Watcher { update() { console.log("update"); } } // 创建发布者 let dep = new Dep(); // 创建订阅者 let watcher = new Watcher(); // 添加订阅者 dep.addSub(watcher); // 通知所有订阅者,调用订阅者update方法 dep.notify();
  • 总结:
    • 观察者模式是由具体目标调度,比如当事件触发,Dep就会去调用观察者方法,所以观察者模式的订阅者与发布者之间是存在依赖的
    • 发布-订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在.

发布-订阅模式和观察者模式

Vue 响应式原理

一个简易版Vue的核心功能

  1. 负责接收初始化参数
  2. 负责把data中属性注入到Vue实例,转换成getter/setter
  3. 负责调用observer监听data中所有属性的变化
  4. 负责调用compiler解析指令/插值表达式
  5. 通过dep收集依赖(收集观察者),watcher更新视图

Vue响应式流程

  • 下面用代码来具体体现一下

首先创建一个Vue类

结构

  • $options
  • $el
  • $data

  • _proxyData()
// 创建Vue类 class Vue { constructor(options) { // 1.通过属性保存选项数据 this.$options = options || {}; this.$data = options.data || {}; this.$el = typeof options.el === "string" ? document.querySelector(options.el) : options.el; // 2.把data中成员转换成getter和setter,注入到vue实例中 this._proxyData(this.$data); // 3.调用observer对象,监听数据的变化 new Observer(this.$data); // 4.调用compiler对象,解析指令和插值表达式 new Compiler(this); } // 代理数据,让vue实例代理options中data的属性 _proxyData(data) { // 遍历data中所有的属性 Object.keys(data).forEach(key => { // 把data的属性注入vue的实例 Object.defineProperty(this, key, { // 可枚举 enumerable: true, // 可遍历 configurable: true, // 获取值 get() { return data[key]; }, // 设置值 set(newValue) { if (data[key] === newValue) { return; } data[key] = newValue; }, }); }); } }

Observer类(数据劫持)

功能:

  1. 负责把data选项中的属性转换成响应式数据
  2. data中某个属性也是对象,把该属性转换成响应式数据
  3. 数据变化发送通知

结构

  • walk(data)
  • defineReactive(data, key, value)
class Observer { constructor(data) { this.walk(data); } walk(data) { // 判断data是否有值且对象, 如果不是对象则直接返回 if (!data || typeof data !== "object") { return; } // 遍历data对象的所有属性,(如果接收的data是对象则将对象内部的值设置为响应式的) Object.keys(data).forEach(key => { this.defineReactive(data, key, data[key]); }); } // 1. 传入的data 2. 属性 3. 属性对应的值 defineReactive(obj, key, val) { const self = this; // 负责收集依赖, 并发送通知; const dep = new Dep(); // 接收到的值如果是对象则调用walk方法,将val内部属性转换成响应式数据 this.walk(val); // 注意: 这里拦截obj的属性key,如果下面get的时候是获取的obj[key]会陷入一个死循环 Object.defineProperty(obj, key, { // 可枚举 enumerable: true, // 可配置 configurable: true, get() { Dep.target && dep.addSub(Dep.target); // 这里必须是返回val,如果返回自身的obj[key]会无限触发get,陷入死循环 return val; }, set(newValue) { if (newValue === val) { return; } val = newValue; // 把重新赋值的如果是个新对象内部的值也设置为响应式的 self.walk(val); // 发送通知 dep.notify(); }, }); } }

Compiler类(解析指令文本等)

功能:

  1. 负责编译模板,解析指令/插值表达式
  2. 负责页面的首次渲染
  3. 当数据变化后重新渲染视图

结构:

  • el
  • vm

  • compile(el)
  • compileElement(node)
  • compileText()
// 1. 负责编译模板,解析指令/插值表达式 // 2. 负责页面的首次渲染 // 3. 当数据变化后重新渲染视图 class Compiler { constructor(vm) { // 模板 this.el = vm.$el; // vue实例 this.vm = vm; this.compile(this.el); } // 编译模板,处理元素节点和文本节点 compile(el) { // 获取所有的子节点。这是一个伪数组 let childNodes = el.childNodes; Array.from(childNodes).forEach((node) => { if (this.isTextNode(node)) { // 处理文本节点 this.compileText(node); } else if (this.isElementNode(node)) { // 处理元素节点 this.compileElement(node); } // 判断node节点是否存在子节点,如果有子节点要递归调用compile if (node.childNodes && node.childNodes.length) { this.compile(node); } }); } // 编译元素节点,处理指令 compileElement(node) { // 获取dom的属性节点 遍历 Array.from(node.attributes).forEach((attr) => { let attrName = attr.name; // 判断节点是否是指令 if (this.isDirective(attrName)) { // 如果用if去判断,指令非常的多,判断起来代码会非常的多 // 以下进行的判断不使用if语句 // v-text -> text 截取前面的v- attrName = attrName.substr(2); // 属性的值就是变量名 let key = attr.value; // 根据不同的指令名称调用对应的处理函数 this.update(node, key, attrName); } }); } // 集中判断是什么指令然后调用相对应的指令函数 update(node, key, attrName) { // 通过截取v-的指令名称匹配相对应的指令处理函数 let updateFn = this[attrName + "Updater"]; // 如果匹配到了这个函数,才执行该函数的调用需要指定调用者 updateFn && updateFn.call(this, node, this.vm[key], key); } // 关联指令的方法 // 处理text指令,传入dom和最后输出的值 textUpdater(node, value, key) { node.textContent = value; // 创建watcher对象,当数据改变更新视图 new Watcher(this.vm, key, (newValue) => { node.textContent = newValue; }); } // v-html处理 htmlUpdater(node, key, value) { const decoder = document.createElement('div'); decoder.innerHTML = value; node.innerHTML = decoder; new Watcher(this.vm, key, (newValue) => { node.innerHTML = newValue; }); } // v-on处理 只考虑了单一事件 onUpdater(node) { // 获取属性 const list = node.attributes; // 遍历 for (let i = 0; i < list.length; i++) { // 取得属性名 let name = list[i]; // 取得属性值 const value = node.getAttribute(name); // 判断以v-on或者@开头 if (/^@|^v-on:/.test(name)) { // 替换@或v-on为空 name = name.replace(/^@|^v-on:/, ''); // 向node节点添加事件 node.addEventListener(name, value); } } } // 更新表单元素 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; }); } // 编译文本节点,处理插值表达式 compileText(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, (newValue) => { node.textContent = value.replace(reg, newValue); }); } } // 判断元素属性是否是指令 isDirective(attrName) { // 指令是v-开头的 return attrName.startsWith("v-"); } // 判断节点是否是文本节点 isTextNode(node) { return node.nodeType === 3; } // 判断节点是否是元素节点 isElementNode(node) { return node.nodeType === 1; } }

Dep类(收集观察者)

功能:

  1. 收集依赖,添加观察者(watcher)
  2. 通知所有观察者

结构:

  • subs

  • addSub(sub)
  • notify()
    // 发布者 class Dep { constructor() { // 存储所有的观察者 this.subs = []; }
// 添加观察者 addSub(sub) { if (sub && sub.update) { this.subs.push(sub); } } // 发送通知 notify() { this.subs.forEach(sub => { sub.update(); }); }

}

### Watcher类(更新视图) >功能 1. 当数据变化触发依赖,dep通知所有的Watcher 实例更新视图 2. 自身实例化的时候往dep对象中添加自己 > >结构: - vm - key - cb - oldValue --- - update() ```javascript class Watcher { constructor(vm, key, cb) { this.vm = vm; // data中的属性名称 this.key = key; // 回调函数, 负责更新视图 this.cb = cb; // 把watcher对象记录到Dep类的静态属性target Dep.target = this; // 触发get方法,在get方法中会调用addSub this.oldValue = this.vm[this.key]; Dep.target = null; } // 当数据发生变化的时候更新视图 update() { // 在Observer类中监听了所有数据的变化,所以一旦值改变这里获取到的就是新值 let newValue = this.vm[this.key]; if (this.oldValue === newValue) { return; } // 更新视图 this.cb(newValue); } }

最终引用使用

  • 接下来我们引入这些文件,创建一个vue实例
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title> 简易版Vue响应式流程 </title> </head> <body> <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> </body> <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/miniVue.js"></script> <script> // 使用vue const vm = new Vue({ el: "#app", data: { msg: "Hello Vue", count: 123, person: { name: "zs" }, }, }); </script> </html>
  • 最终的使用结果如下图

使用后结果

所有定义的属性对象均具备了get,set方法,并且修改了msg也同步更新到了视图上了!!!

讨论数量: 1

此贴备受大伙儿青睐呀~!

4年前

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!
分享你的见解~