深入亿点点之Vue:数据响应式
2020/4/10 11:02:59
本文主要是介绍深入亿点点之Vue:数据响应式,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
前言
数据响应式原理也是老生常谈了,什么是数据响应式呢?
数据响应式,从官方定义来说,将Model绑定到View,当用代码更新Model时,View会自动更新。
数据响应式强调数据驱动DOM生成,而不是直接操作DOM。
而常常和数据响应式混为一谈的数据双向绑定,则特指v-model
,该指令实现了如果用户更新了View,Model也会随之更新。结合响应式原理,则形成了双向绑定,即双向绑定 = 单向绑定 + UI事件监听
。
本文分为四个部分:
- 一图理解响应式原理
- 手把手教你实现数据双向绑定
- 来个极简实现方案
- 数据响应式的思考
一图理解响应式原理
Vue通过订阅发布者模式来实现,通过三个类Observer
、Dep
、Watcher
来实现,主要关注每个类的功能和类之间的关系。
首先明确的是三个类之间的对应关系:
Observer
与Dep
是一对一的关系,Dep
与Watcher
是多对多的关系。每个类的具体功能如下:
Observer
类:数据的观察者(个人理解上是一个代理发布者),当初始化数据时,遍历数据所有属性,为每一个属性设置一个调度中心(Dep
实例对象),通过Object.defineProperty
把属性转为getter/setter
,注入相关的Dep
调度方法。Watcher
类:数据的订阅者,当接收到调度中心Dep
的更新通知时,Watcher
实例执行回调cb,更新视图。Dep
类:调度中心,作为发布者和订阅者之间的消息传递枢纽。当Observer
类触发getter
时,Dep
收集依赖的Watcher
对象。当Observer
类触发**setter
**时,Observer
将数据更新信息发送给Dep
,Dep
通知订阅者Watcher
更新。这三个类构成了主要的Model到View逻辑。至于View到Model的逻辑,只需要对输入控件进行事件监听,即可实现View到Model。这样就形成了闭环的MVVM模型。
手把手教你实现数据双向绑定
1. 实现订阅-发布者模式架构
这一步骤主要是确定好整个流程框架,参照流程图可以确定:
- 确定初始化需要的数据
- 劫持数据,下发更新
- 编译模板,收集依赖
- 视图更新
class Vue{ constructor(options) { // 1. 初始化数据 this.options = options this.$data = options.data this.$el = document.querySelector(options.el) this._directive = [] // 收集依赖的容器 this.observer() // 2. 数据监测 this.compiler() // 3. 编译模板,收集依赖 } // TODO: 数据劫持,下发更新 observer() {} // TODO: 判断指令,收集依赖 compiler() {} } // 订阅者,主要更新视图 class Watcher { constructor() { this.update() } // TODO: 更新视图 update() {} } var vm = new Vue({ el: '#app', data: { myText: '一开始,只是平平无奇的text', myModel: '普普通通的model', } }) 复制代码
2. 实现M->V,把模型里的数据绑定到视图
在框架上补充具体的逻辑:
- 初始数据:除了记录传入的一些数据外,还需要一个容器来记录订阅者。由于订阅者是对
data
的属性监听的,也就是当data[prop]
更新时,只有订阅prop
属性的订阅者会有更新操作,其他订阅者不会收到更新。所以订阅者容器是一个属性对应一个列表。 - 劫持数据,下发更新:通过
Object.defineProperty
重写get
/set
(Vue3通过ES6的Proxy实现),对数据属性进行劫持监听。当更改数据属性时,下发更新。为什么用Proxy 替代 Object.defineProperty ?
Object.defineProperty
只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。通过递归以及遍历data对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历。
Proxy相比较与Object.defineProperty
,优点如下:
1. 可以劫持整个对象,并返回一个新对象。显然可以大幅度提升性能。
2. z多种劫持操作 - 编译模板,收集依赖:解析html模板,通过检测设定的特定属性如
v-model
,进行依赖收集操作 - 视图更新:订阅者收到更新信息后,对DOM进行操作,更新视图
<body> <div id="app"> <h2>响应式原理实现</h2> <div v-text="myText"></div> <div v-text="myModel"></div> <input v-model="myModel" /> </div> </body> <script> class Vue{ constructor(options) { // 1. 初始化数据 this.options = options this.$data = options.data this.$el = document.querySelector(options.el) this._directive = {} // 收集依赖的容器 // 2. 数据监测 this.observer(this.$data) // 3. 编译模板 this.compiler(this.$el) } observer(data) { for (var key in data) { this._directive[key] = [] // 初始化订阅者容器 let val = data[key] let watchers = this._directive[key] Object.defineProperty(this.$data, key, { get: function() { return val }, set: function(newVal) { // 更新数值,下发更新 val = newVal watchers.forEach(watcher => watcher.update()) }, }) } // es6 // const handler = { // set: function(data, prop, val) { // }, // } // data = new Proxy(data, handler) } compiler(el) { let nodes = el.children for(let i=0; i<nodes.length; i++) { let node = nodes[i] // 如果有子元素,递归调用 if (node.children.length > 0) this.compiler(node) // 判断指令,收集依赖 if(node.hasAttribute("v-model")) { let attrVal = node.getAttribute("v-model") this._directive[attrVal].push(new Watcher(node, this, attrVal, 'value')) } if(node.hasAttribute("v-text")) { let attrVal = node.getAttribute("v-text") this._directive[attrVal].push(new Watcher(node, this, attrVal, 'innerText')) } } } } // 订阅者,主要更新数据 class Watcher { // el: 订阅节点 // vm: vue实例 // exp: 订阅的data属性值 // attr: 不同订阅者更新视图时,修改的属性不同 constructor(el, vm, exp, attr) { this.el = el this.vm = vm this.exp = exp this.attr = attr this.update() } // 更新 update() { this.el[this.attr] = this.vm.$data[this.exp] } } var vm = new Vue({ el: '#app', data: { myText: '一开始,只是平平无奇的text', myModel: '普普通通的model', }, }) setTimeout(function(){ console.log(vm.$data) vm.$data["myText"] = '3秒后,text更新了' }, 3000) </script> 复制代码
3. 实现V->M
对v-model
指令,编译时加入事件监听。
// ... if(node.hasAttribute("v-model")) { let attrVal = node.getAttribute("v-model") this._directive[attrVal].push(new Watcher(node, this, attrVal, 'value')) // V -> M: 加入事件监听 node.addEventListener("input", (function () { return function() { this.$data[attrVal] = node.value } })().bind(this)) } 复制代码
20行代码极简实现
const input = document.getElementById('input') const span = document.getElementById('span') const obj = { text: '文本文本文本' } const handler = { set: function(target, prop, val) { target[prop] = val span.innerText = val input.value = val } } const myText = new Proxy(obj, handler); input.addEventListener('keyup', function(e){ // 赋值触发了set,初始化视图 // myText代理了text属性,当text改变触发set myText.text = e.target.value; }) 复制代码
对数据响应式的一些思考
优点
- 双向绑定把数据变更的操作隐藏在框架内部,调用者并不会直接感知。
- 在表单交互多的情况下,可以简化大量代码。
注意点
- 数据之间互相依赖,由于"黑盒"的存在,在复杂应用中难以追踪数据变化和管理,不如引入如
vuex
的状态管理来得便利。 - 使用
Object.defineProperty
实现数据绑定时,直接添加对象属性obj[prop]
,该属性并非响应式,即无法通过数据驱动视图。对于数组对象也有类似的限制,直接通过索引修改数组也不会驱动视图更新。为了成为响应式属性,需要通过Vue.set
来设置。(Vue3.0使用Proxy实现,属性的添加和删除、数组索引和长度的变更都可以被监听,并可以支持 Map、Set、WeakMap 和 WeakSet,该问题得到解决) - 创造订阅者本身有一定的消耗,且订阅者一直存在于内存中。
参考
[1] 深入响应式原理
[2] 深入理解Vue响应式原理
[3] VUE数据响应式原理
这篇关于深入亿点点之Vue:数据响应式的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-10-04package.json 文件位置在哪?-icode9专业技术文章分享
- 2024-10-01Craco.js学习:从入门到实践指南
- 2024-10-01Create-React-App学习:入门与实践指南
- 2024-10-01CSS-in-JS学习:从入门到实践指南
- 2024-09-30JSX语法学习:从入门到初步掌握
- 2024-09-30Mock.js学习:入门教程与实战演练
- 2024-09-30React Hooks学习:从入门到实践
- 2024-09-30受控组件学习:React中的基础入门教程
- 2024-09-29JS定时器教程:初学者必看指南
- 2024-09-29JS对象教程:初学者的全面指南