一、 数据双向绑定实现的对比
实现方式的相同之处
Vue2.0 与 Vue3.0 都是采用数据劫持结合发布者-订阅者模式的方式实现的。
区别
- Vue2.0 实现MVVM(双向数据绑定)的原理是通过 Object.defineProperty 来劫持各个属性的 setter、getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
- Vue 3.0 则是通过 new Proxy() 来劫持各个属性的 setter,getter。
二、 数据劫持方式的优劣对比
Vue2.0
- 无法监听数组和对象变化。
- 由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所有属性必须在 data 对象上存在才能让 Vue 将它转换为响应式。
- Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。深度监听需要一次性递归,对性能影响比较大。
- Object.defineProperty 是 ES5 中一个无法 shim 的特性,因此不能兼容 IE8 及以下的版本。
Vue3.0
- 基于 Proxy 和 Reflect,可以原生监听数组,可以监听对象属性的添加和删除。
- 不需要一次性遍历 data 的属性,可以显著提高性能。
- 因为 Proxy 是 ES6 新增的属性,有些浏览器还不支持,只能兼容到 IE11。
三、实现过程
Vue2.0
-
Vue ,初始化实例时,把 data 中的成员转成 getter/setter
-
Observer ,对数据对象的所有属性进行监听,如果变动拿到最新值并通知 Dep(发布者-目标)
-
Watcher ,定义观察者,定义update() 更新函数,当数据发生变动,更新视图
-
Dep ,添加观察者,当数据发生变化的时候,通知所有的观察者,执行观察者的 update() 函数
-
Compiler ,负责编译模板,解析指令/差值表达式,负责页面的首次渲染,当数据变化后更新视图
MVVM 作为数据绑定的入口,通过 Observer 来监听 data 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到双向绑定效果。
// 1. 定义一个 vue 实例,将实例中 data 对象中的数据转成 getter/setter
class Vue {
// 1.1 定义类属性
constructor(options) {
// 1.1.1 通过属性保存data中的数据
this.options = options || Object.create(null);
// 1.1.2 为了防止与内部变量冲突,data 必须是一个函数
if (typeof options.data !== 'function') {
throw ('data must be a function')
}
// 1.1.3 储存data数据
this.data = options.data() || Object.create(null);
// 1.1.4 获取挂载节点
this.el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
// 1.1.5 把data中的成员转换成getter/setter注入到Vue实例中
this.$proxyData(this.data);
// 1.1.6 调用Observer对象,把data变成客观擦者,监听data数据的变化
new Observer(this.data)
// 1.1.7 调用Compiler对象,解析指令和差值表达式
new Compiler(this)
}
// 1.2 私有成员,把data中的属性,转换成getter/setter注入到Vue实例中
$proxyData(data) {
// 1.2.1 如果data不是对象不再执行后续操作
if (!data || typeof data !== 'object') {
return;
}
// 1.2.2 遍历data中的所有属性
Object.keys(data).forEach(item => {
// 1.2.3 使用Object.defineProperty()方法,劫持数据
Object.defineProperty(data, item, {
configurable: true,
enumerable: true,
get() {
// 1.2.4 劫持数据
return data[item];
},
set(val) {
// 1.2.5 如果data的值与变更的值不一致,则更新data的值
if (data[item] !== val) {
data[item] = val;
}
}
})
})
}
}
添加一个 Observer,做数据响应式的处理,包含数据的监听/劫持
// 2. 监听data,做数据响应式处理
class Observer {
constructor(data) {
this.deepDate(data)
}
//只针对对象数据进行响应式处理
deepDate(data) {
if (typeof data !== 'object') {
return
}
// 遍历data数据
Object.keys(data).forEach(key => {
this.proxyData(data, key, data[key])
})
}
// 将data数据转为getter/setter
proxyData(data, key, value) {
// 递归监听所有数据
this.deepDate(value);
// 暂存this
let that = this;
//收集依赖,发送通知
let dep = new Dep();
// 数据劫持
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
if(Dep.target && dep.addSub(Dep.target)){
return value;
}
},
set(val) {
if (value == val) return;
// 赋值最新值
value = val;
that.deepDate(val);
// 数据变化,发送通知
dep.notify(key, val)
}
})
}
}
创建一个 Dep 发布者,收集依赖并添加观察者,当有变更时,通知所有的观察者
// 3. 定义一个发布者
class Dep {
constructor() {
// 3.1 定义一个数组,记录所有的(观察者/订阅者)
this.sub_arr = [];
}
/**
* 添加观察者
* @param {*} sub -- 观察者
*/
addSub(sub) {
// 3.2 每一个观察者都必须包含一个update方法
if (sub && sub.update) this.sub_arr.push(sub);
}
/**
* 通知所有观察者进行更新
* @param { String} key -- 当前变更对象的 key 值
* @param {*} val -- 当前变更的值
*/
notify(key, val) {
// 3.3 遍历观察者数据,通知所有观察者进行更新
this.sub_arr.forEach(sub => sub.update(key, val))
}
}
创建一个 Watcher 观察者,当收到发布者通知更新时,更新视图
// 4. 创建一个订阅者-观察者
class Watcher {
constructor(vm, key, cb) {
// 4.1 存储当前视图
this.vm = vm;
// 4.2 存储当前变更的key
this.key = key;
// 4.3 存储当前回调
this.cb = cb;
// 4.4 把Watcher对象记录到Dep的静态属性target上。触发get方法,在get中会调用addSub
Dep.target = this;
// 4.5 当获取vm[key]的时候会执行getter,记录数据变化之前的值
this.oldValue = vm[key];
// 4.6 当Watcher加到subs之后,重置Dep的target属性
Dep.target = null;
}
/**
* 添加更新方法
* @param {*} key -- 当前data的key
* @param {*} val -- 当前变更的值
* @returns
*/
update(key, val) {
// 4.7 如果当前更新值与之前的值相同,不做更新操作
if (val == this.oldValue) return;
// 4.8 执行Compile中绑定的回调,更新视图
this.cb(key, val)
// 4.9 更新值
this.oldValue = val;
}
}
定义一个 compiler 编译解析器,编译、更新视图
// 5. 定义一个compiler
class Compiler {
// 5.1 构造函数
constructor(vm) {
// 5.1.1 储存当前实例
this.vm = vm;
// 5.1.2 存储DOM对象
this.el = vm.$el;
// 5.1.3 遍历DOM对象所有节点,如果是文本节点,解析差值表达式。如果是元素节点,解析指令。
this.compile(this.el)
}
// 5.1.4 编译模板,处理文本节点和元素节点
compile(el) {
// 5.1.5 存储所有节点
let childNodes = el.childNodes;
// 5.1.6 节点属于伪数组需要通过Array.from()转换成真实数组
Array.from(childNodes).forEach(node => {
if (this.isTextNode(node)) {
// 5.1.7 处理文本节点
this.compileText(node)
} else if (this.isElementNode(node)) {
// 5.1.8 处理元素节点
this.compileElement(node)
}
// 5.1.9 判断node节点,是否有子节点,如果有,递归深度遍历
if (node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
// 5.2 编译元素节点,处理指令
compileElement(node) {
// 5.2.1 遍历所有的属性节点
Array.from(node.attributes).forEach(attr => {
let attrName = attr.name;
// 5.2.2 判断是否是指令
if (this.isDirective(attrName)) {
attrName = attrName.substr(2);
let key = attr.value;
// 5.2.3 如果当前元素含有指令,则需要首次渲染指令对应的内容
this.update(node, key, attrName)
}
})
}
// 5.3 更新视图
update(node, key, attrName) {
let updateFn = this[attrName + 'Update'];
updateFn && updateFn.call(this, node, this.vm[key], key);
}
// 5.4 针对不同的指令编译/更新视图
domUpdate(node, value, key) {
node.textContent = value;
new Watcher(this.vm, key, (k, nv) => {
console.log('创建Watcher ,当数据改变更新视图' + nv)
node.textContent = nv;
})
}
// 5.5 处理v-model 指令
modelUpdate(node, value, key) {
node.value = value;
new Watcher(this.vm, key, (k, nv) => {
console.log('创建Watcher ,当数据改变更新视图' + nv)
node.value = nv;
})
// 5.5.1 设置双向绑定事件
node.addEventListener('input', e => this.vm[key] = node.value)
}
// 5.6 判断元素是否是指令
isDirective(attrName) {
//判断属性是否是v-开头
return attrName.startsWith('v-');
}
// 5.7 判断是否是文本节点
isTextNode(node) {
return node.nodeType === 3;
}
// 5.8 判断是否是元素节点
isElementNode(node) {
return node.nodeType === 1;
}
}
Vue3.0
vue3.0 数据响应式采用 ES6 的 proxy 特性进行数据拦截
proxyData = new Proxy(obj,
{
// 获取
get(data, key) {
return Reflect.get(data, key);
},
// 修改
set(data, key, value) {
return Reflect.set(data, key, value)
},
// 删除
deleteProperty(data, key) {
return Reflect.deleteProperty(data, key)
}
})