实现一个vue的简易数据双向绑定:
【vue】双向数据绑定(一)–发布订阅模式
【vue】双向数据绑定(二)-- 实现vue的数据劫持与发布订阅
1、实现一个监听器 Observer
,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者;
2、实现一个订阅器 Dep
,用来收集订阅者,对监听器 Observer
和 订阅者 Watcher
进行统一管理;
3、实现一个订阅者 Watcher
,可以收到属性的变化通知并执行相应的方法,从而更新视图;
4、实现一个解析器 Compile
,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化。
每次数据读或写时,我们能感知到数据被读取了或数据被改写了。要使数据变得“可观测”,Vue 2.0
源码中用到 Object.defineProperty()
来劫持各个数据属性的 setter / getter
。
将 Object.defineProperty()
来劫持各个数据属性的 setter / getter
封装在 defineReactive()
函数中
/**
* 循环遍历数据对象的每个属性
*/
function observable(obj) {
if (!obj || typeof obj !== 'object') {
return;
}
let keys = Object.keys(obj);
keys.forEach((key) => {
defineReactive(obj, key, obj[key])
})
return obj;
}
/**
* 将对象的属性用 Object.defineProperty() 进行设置
*/
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`${key}属性被读取了...`);
return val;
},
set(newVal) {
console.log(`${key}属性被修改了...`);
val = newVal;
}
})
}
订阅者 Watcher
在初始化的时候需要将自己添加进订阅器 Dep
中,
负责订阅一些事件,将自己的一些行为交给了 Dep类,放在 Dep数组中。
当以后到了一定时机, Dep类负责对每一个行为进行循环,然后触发。此时 Watcher类就可以收到通知。
实现一个解析器 Compile
来做解析和绑定 Dom,
Vue 主要通过以下 4 个步骤来实现数据双向绑定的:
实现一个监听器 Observer
:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。
实现一个解析器 Compile
:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。
实现一个订阅者 Watcher
:每个组件实例都对应一个 watcher 实例,Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。
Watcher 主要做的事情是: ① 在自身实例化时往属性订阅器(dep)里面添加自己; ② 自身必须有一个update()方法; ③ 待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
实现一个订阅器 Dep
:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。
因为对象最多也就几十个属性,拦截起来数量不多,但是数组可能会有几百几千项,拦截起来非常耗性能,所以直接重写数组原型上的方法,是比较节省性能的方案
数组考虑性能原因没有用 defineProperty 对数组的每一项进行拦截,
而是选择对 7 种数组(push,shift,pop,splice,unshift,sort,reverse)方法进行重写
举例:
//!!!对数组方法进行重写:
//原来的 oldArrayProto用来执行数组上的方法(比如 push,pop,shift)
//新的 newArrProto 用来体现视图的更新
const oldArrayProto = Array.prototype;
const newArrProto = Object.create(oldArrayProto);
['push','pop','shift','unshift','splice','reverse','sort'].forEach(methodName => {
newArrProto[methodName] = function() {
console.log('更新视图 --> 数组');
oldArrayProto[methodName].call(this,...arguments);
}
})
并且在 observer() 函数中,监测是否为数组
//判断是否是数组!!!
if(Array.isArray(target)) {
target.__proto__ = newArrProto;
}
完整代码:
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<h1>Vue响应式h1>
<script>
const data = {
name:'zs',
age:18,
friend:{
friendName:'ls'
},
colors:['red','orange','green']
}
//!!!对数组方法进行重写:
//原来的 oldArrayProto用来执行数组上的方法(比如 push,pop,shift)
//新的 newArrProto 用来体现视图的更新
const oldArrayProto = Array.prototype;
const newArrProto = Object.create(oldArrayProto);
['push','pop','shift','unshift','splice','reverse','sort'].forEach(methodName => {
newArrProto[methodName] = function() {
console.log('更新视图 --> 数组');
oldArrayProto[methodName].call(this,...arguments);
}
})
//变成响应式数据
observer(data);
function observer(target) {
if(typeof target !== 'object' || target === null) {
return target;
}
//判断是否是数组!!!
if(Array.isArray(target)) {
target.__proto__ = newArrProto;
}
for(let key in target) {
defineReactive(target, key, target[key]);
}
}
function defineReactive(target, key, value) {
//深度观察
observer(value);
Object.defineProperty(target, key, {
get(){
return value;
},
set(newValue){
observer(newValue);
if(newValue !== value) {
value = newValue;
console.log('更新视图 --> 对象');
}
}
})
}
data.age = { number: 20 };//监测对象
data.colors.push('blue');//监测数组
console.log(data.colors);
script>
body>
html>
Vue3.x 改用 Proxy 替代 Object.defineProperty。因为 Proxy 可以直接监听对象和数组的变化,并且有多达 13 种拦截方法。
在对一些属性进行操作时,使用 Object.defineProperty 这种方法无法拦截,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作。更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。
在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。使用Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为 Proxy 是 ES6 的语法。
在 Vue2 中, 0bject.defineProperty 会改变原始数据。
而 Proxy 是创建对象的虚拟表示,并提供 set 、get 和 deleteProperty 等处理器,这些处理器可在访问或修改原始对象上的属性时进行拦截。
Proxy 的优势如下:
Object.defineProperty 的优势如下: