数据响应式原理也是老生常谈了,什么是数据响应式呢?
数据响应式,从官方定义来讲,将Model绑定到View,当用代码更新Model时,View会自动更新。
数据响应式强调数据驱动DOM生成,而不是直接操做DOM。
而经常和数据响应式混为一谈的数据双向绑定,则特指v-model
,该指令实现了若是用户更新了View,Model也会随之更新。结合响应式原理,则造成了双向绑定,即双向绑定 = 单向绑定 + UI事件监听
。
本文分为四个部分:javascript
Vue经过订阅发布者模式来实现,经过三个类Observer
、Dep
、Watcher
来实现,主要关注每一个类的功能和类之间的关系。
html
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
更新。
这一步骤主要是肯定好整个流程框架,参照流程图能够肯定:vue
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',
}
})
复制代码
在框架上补充具体的逻辑:java
data
的属性监听的,也就是当data[prop]
更新时,只有订阅prop
属性的订阅者会有更新操做,其余订阅者不会收到更新。因此订阅者容器是一个属性对应一个列表。Object.defineProperty
重写get
/set
(Vue3经过ES6的Proxy实现),对数据属性进行劫持监听。当更改数据属性时,下发更新。
为何用Proxy 替代 Object.defineProperty ?
Object.defineProperty
只能劫持对象的属性,所以咱们须要对每一个对象的每一个属性进行遍历。经过递归以及遍历data对象来实现对数据的监控的,若是属性值也是对象那么须要深度遍历。
Proxy相比较与Object.defineProperty
,优势以下:
1. 能够劫持整个对象,并返回一个新对象。显然能够大幅度提高性能。
2. z多种劫持操做node
v-model
,进行依赖收集操做<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>
复制代码
对v-model
指令,编译时加入事件监听。react
// ...
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))
}
复制代码
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,该问题获得解决)