Vue.js响应式原理,数据双向绑定
响应式原理
数据驱动
- 数据响应式
- 数据模型仅仅是普通的JavaScript对象,而当我们修改数据时,视图会进行更新,避免了繁琐的DOM操作,提高开发效率
- 双向绑定
- 数据改变,视图改变;视图改变,数据也随之改变
- 我们可以使用v-model在表单元素上创建双向绑定数据
- 数据驱动
- 开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图
Vue2 数据响应式核心原理:
- defineProperty介绍
- 浏览器兼容IE8以上
<!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的核心功能
- 负责接收初始化参数
- 负责把data中属性注入到Vue实例,转换成getter/setter
- 负责调用observer监听data中所有属性的变化
- 负责调用compiler解析指令/插值表达式
- 通过dep收集依赖(收集观察者),watcher更新视图
- 下面用代码来具体体现一下
首先创建一个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类(数据劫持)
功能:
- 负责把data选项中的属性转换成响应式数据
- data中某个属性也是对象,把该属性转换成响应式数据
- 数据变化发送通知
结构
- 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类(解析指令文本等)
功能:
- 负责编译模板,解析指令/插值表达式
- 负责页面的首次渲染
- 当数据变化后重新渲染视图
结构:
- 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类(收集观察者)
功能:
- 收集依赖,添加观察者(watcher)
- 通知所有观察者
结构:
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也同步更新到了视图上了!!!
此贴备受大伙儿青睐呀~!