在这个美好又遗憾的世界里,你我皆是自远方而来的独行者
Vue的核心思想就是数据驱动以及组件化,本文就来讲述一下Vue里面双向数据绑定的原理的两种简单实现。
Object.defineProperty()
在vue3.x之前的版本,vue的双向绑定核心是通过Object.defineProperty()来实现属性的劫持,然后就可以监听数据到数据的变化。它的原理是:
- 数据监听器Observe,能够对数据对象的所有属性进行监听
- 指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更改函数
- Watcher,连接Observe和Compile,订阅并收到每个属性变动,执行相应的回调函数,进而更新视图。
先来一个流程图:
Observe的实现
我们既然可以利用Object.defineProperty()来监听属性变动,那就给Observe的数据所有对象进行遍历,并且添加上setter和getter。
let fans = {name: 'bryant'}
observe(fans)
fans.name = 'chris'
function observe(obj) {
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
function defineReactive(data, key, val) {
observe(val)
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再定义
get: function() {
return val
},
set: function(newVal) {
console.log('哈哈哈,监听到值变化了', val, '--->', newVal)
val = newVal
}
})
}
嘿嘿嘿,这样就监听到每个数据的变化,监听到之后告诉订阅者,我们可以维护一个数组,用来收集订阅者,数据变动触发notify,再调用订阅者的更新方法update。
function defineReactive(data, key, val) {
let dep = new Dep();
observe(val)
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再定义
get: function() {
return val
},
set: function(newVal) {
console.log('哈哈哈,监听到值变化了', val, '--->', newVal)
val = newVal
// 来了,老铁,大喇叭通知所有订阅者
dep.notify()
}
})
}
function dep() {
this.subs = []
}
dep.prototype = {
addSub: function(sub) {
this.subs.push(sub)
},
notify: function () {
this.subs.forEach((sub) => {
sub.update()
})
}
}
订阅者其实就是Watcher,我们需要在defineReactive的getter里面添加dep订阅者
function defineReactive(data, key, val) {
observe(val)
let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再定义
get: function() {
// 通过Dep定义一个全局的target,暂存watcher
Dep.target && dep.addSub(Dep.target);
return val;
},
set: function(newVal) {
console.log('哈哈哈,监听到值变化了', val, '--->', newVal)
val = newVal
// 来了,老铁,大喇叭通知所有订阅者
Dep.notify()
}
})
}
Observe修改后的代码
function Observer(data) {
this.data = data;
this.walk(data);
}
Observer.prototype = {
constructor: Observer,
walk: function(data) {
var me = this;
Object.keys(data).forEach(function(key) {
me.convert(key, data[key]);
});
},
convert: function(key, val) {
this.defineReactive(this.data, key, val);
},
defineReactive: function(data, key, val) {
var dep = new Dep();
var childObj = observe(val);
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
get: function() {
if (Dep.target) {
dep.depend();
}
return val;
},
set: function(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的话,进行监听
childObj = observe(newVal);
// 通知订阅者
dep.notify();
}
});
}
};
function observe(value, vm) {
if (!value || typeof value !== 'object') {
return;
}
return new Observer(value);
};
var uid = 0;
function Dep() {
this.id = uid++;
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
depend: function() {
Dep.target.addDep(this);
},
removeSub: function(sub) {
var index = this.subs.indexOf(sub);
if (index != -1) {
this.subs.splice(index, 1);
}
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};
Dep.target = null;
Compile的实现
Compile干的活是解析模板指令,将模板中的变量替换成数据,然后初始化渲染视图,并将每个指令对应的节点绑定更新函数,监听数据的订阅者,一旦有异动,就去更新视图
因为遍历解析过程中有多次操作dom节点,为了提高效率,会先将根节点el转换成文档碎片fragment进行编译解析,解析完成,再将fragment添加到真实的dom中。
function Compile(el) {
this.$el = this.isElementNode(el) ? el: document.querySelector(el)
if (this.$el) {
this.$fragment = this.node2Fragment(this.$el)
this.init()
this.$el.appendChild(this.$fragment)
}
}
Compile.prototype = {
init: function () {
this.compileElement(this.$fragment)
},
node2Fragment: function(el) {
let fragment = document.createDocumentFragment()
let child = el.firstChild
// 将原生节点copy到fragment
fragment.appendChild(child)
return fragment
}
}
这里的compileElement会遍历所有的节点以及子其节点,进行扫描解析编译,调用对应的渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定
Compile.prototype = {
init: function () {
this.compileElement(this.$fragment)
},
node2Fragment: function(el) {
let fragment = document.createDocumentFragment()
let child = el.firstChild
// 将原生节点copy到fragment
fragment.appendChild(child)
return fragment
},
compileElement: function (el) {
let childNodes = el.childNodes
let self = this
[].slice.call(childNodes).forEach(node => {
const text = node.textContent
const reg = /\{\{(.*)\}\}/ // 表达式文本
if (self.isElementNode(node)) {
self.compile(node)
} else if (self.isTextNode(node) && reg.test(text)) {
self.compileText(node, RegExp.$1) // RegExp.$1是RegExp的一个属性,指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串
}
// 遍历子节点
if (node.childNodes && node.childNodes.length) {
// 递归
self.compileElement(node)
}
})
},
compile: function (node) {
let nodeAttrs = node.attributes
let self = this
[].slice.call(nodeAttrs).forEach(function(attr) {
// 规定:指令以 v-xxx 命名
// 如 <span v-text="content"></span> 中指令为 v-text
var attrName = attr.name; // v-text
if (self.isDirective(attrName)) {
var exp = attr.value; // content
var dir = attrName.substring(2); // text
if (self.isEventDirective(dir)) {
// 事件指令, 如 v-on:click
compileUtil.eventHandler(node, self.$vm, exp, dir);
} else {
// 普通指令
compileUtil[dir] && compileUtil[dir](node, self.$vm, exp);
}
}
});
}
}
let compileUtil = {
text: function (node, vm, exp) {
this.bind(node, vm, 'text')
},
bind: function (node, vm, exp, dir) {
let updateFn = update[dir + 'Update']
// 第一次初始化视图
updateFn && updateFn(node, vm[exp])
// 实例化订阅者
new Watcher(vm, exp, function(value, oldValue) {
updateFn && updateFn(node, value, oldValue)
})
}
}
let update = {
textUpdater: function(node, value) {
node.textContent = typeof value == 'undefined'? '': value
}
}
这里通过递归遍历每个节点以及子节点
完整代码:
function Compile(el, vm) {
this.$vm = vm;
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if (this.$el) {
this.$fragment = this.node2Fragment(this.$el);
this.init();
this.$el.appendChild(this.$fragment);
}
}
Compile.prototype = {
constructor: Compile,
node2Fragment: function(el) {
var fragment = document.createDocumentFragment(),
child;
// 将原生节点拷贝到fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
},
init: function() {
this.compileElement(this.$fragment);
},
compileElement: function(el) {
var childNodes = el.childNodes,
me = this;
[].slice.call(childNodes).forEach(function(node) {
var text = node.textContent;
var reg = /\{\{(.*)\}\}/;
if (me.isElementNode(node)) {
me.compile(node);
} else if (me.isTextNode(node) && reg.test(text)) {
me.compileText(node, RegExp.$1.trim());
}
if (node.childNodes && node.childNodes.length) {
me.compileElement(node);
}
});
},
compile: function(node) {
var nodeAttrs = node.attributes,
me = this;
[].slice.call(nodeAttrs).forEach(function(attr) {
var attrName = attr.name;
if (me.isDirective(attrName)) {
var exp = attr.value;
var dir = attrName.substring(2);
// 事件指令
if (me.isEventDirective(dir)) {
compileUtil.eventHandler(node, me.$vm, exp, dir);
// 普通指令
} else {
compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
}
node.removeAttribute(attrName);
}
});
},
compileText: function(node, exp) {
compileUtil.text(node, this.$vm, exp);
},
isDirective: function(attr) {
return attr.indexOf('v-') == 0;
},
isEventDirective: function(dir) {
return dir.indexOf('on') === 0;
},
isElementNode: function(node) {
return node.nodeType == 1;
},
isTextNode: function(node) {
return node.nodeType == 3;
}
};
// 指令处理集合
var compileUtil = {
text: function(node, vm, exp) {
this.bind(node, vm, exp, 'text');
},
html: function(node, vm, exp) {
this.bind(node, vm, exp, 'html');
},
model: function(node, vm, exp) {
this.bind(node, vm, exp, 'model');
var me = this,
val = this._getVMVal(vm, exp);
node.addEventListener('input', function(e) {
var newValue = e.target.value;
if (val === newValue) {
return;
}
me._setVMVal(vm, exp, newValue);
val = newValue;
});
},
class: function(node, vm, exp) {
this.bind(node, vm, exp, 'class');
},
bind: function(node, vm, exp, dir) {
var updaterFn = updater[dir + 'Updater'];
updaterFn && updaterFn(node, this._getVMVal(vm, exp));
new Watcher(vm, exp, function(value, oldValue) {
updaterFn && updaterFn(node, value, oldValue);
});
},
// 事件处理
eventHandler: function(node, vm, exp, dir) {
var eventType = dir.split(':')[1],
fn = vm.$options.methods && vm.$options.methods[exp];
if (eventType && fn) {
node.addEventListener(eventType, fn.bind(vm), false);
}
},
_getVMVal: function(vm, exp) {
var val = vm;
exp = exp.split('.');
exp.forEach(function(k) {
val = val[k];
});
return val;
},
_setVMVal: function(vm, exp, value) {
var val = vm;
exp = exp.split('.');
exp.forEach(function(k, i) {
// 非最后一个key,更新val的值
if (i < exp.length - 1) {
val = val[k];
} else {
val[k] = value;
}
});
}
};
var updater = {
textUpdater: function(node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
},
htmlUpdater: function(node, value) {
node.innerHTML = typeof value == 'undefined' ? '' : value;
},
classUpdater: function(node, value, oldValue) {
var className = node.className;
className = className.replace(oldValue, '').replace(/\s$/, '');
var space = className && String(value) ? ' ' : '';
node.className = className + space + value;
},
modelUpdater: function(node, value, oldValue) {
node.value = typeof value == 'undefined' ? '' : value;
}
};
Watcher实现
Watcher作为订阅者,主要干的活就是
- 在自身实例化时往属性订阅器(dep)里面添加自己
- 自身必须有一个update()
- 待属性变动dep.notice()通知时,调用自身的update()方法,并触发Compile中绑定回调
function Watcher(vm, exp, cb) {
this.cb = cb
this.vm = vm
this.exp = exp
// 此处为了触发属性的getter,从而在dep添加自己,结合Observer更好理解
this.value = this.get()
}
Watcher.prototype = {
update: function () {
this.run()
},
run: function () {
let value = this.get()
let oldValue = this.value
if (value !== oldValue) {
this.value = value
this.cb.call(this.vm, value, oldValue)
}
},
get: function () {
Dep.target = this // 将当前订阅者指向自己
let value = this.vm[exp] // 触发getter, 添加自己到属性订阅器
Dep.target = null // 添加完毕,重置
return value
}
}
实例化Watcher的时候,调用get(),通过Dep.target = watcherInstance标记订阅者是当前watcher实例,强行触发属性定义的getter方法,getter方法执行的时候,就会在属性的订阅器dep添加当前watcher实例,从而在属性值有变化的时候,watcherInstance就能收到更新通知。
完整代码:
function Watcher(vm, expOrFn, cb) {
this.cb = cb;
this.vm = vm;
this.expOrFn = expOrFn;
this.depIds = {};
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = this.parseGetter(expOrFn.trim());
}
this.value = this.get();
}
Watcher.prototype = {
constructor: Watcher,
update: function() {
this.run();
},
run: function() {
var value = this.get();
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
},
addDep: function(dep) {
// 1. 每次调用run()的时候会触发相应属性的getter
// getter里面会触发dep.depend(),继而触发这里的addDep
// 2. 假如相应属性的dep.id已经在当前watcher的depIds里,说明不是一个新的属性,仅仅是改变了其值而已
// 则不需要将当前watcher添加到该属性的dep里
// 3. 假如相应属性是新的属性,则将当前watcher添加到新属性的dep里
// 如通过 vm.child = {name: 'a'} 改变了 child.name 的值,child.name 就是个新属性
// 则需要将当前watcher(child.name)加入到新的 child.name 的dep里
// 因为此时 child.name 是个新值,之前的 setter、dep 都已经失效,如果不把 watcher 加入到新的 child.name 的dep中
// 通过 child.name = xxx 赋值的时候,对应的 watcher 就收不到通知,等于失效了
// 4. 每个子属性的watcher在添加到子属性的dep的同时,也会添加到父属性的dep
// 监听子属性的同时监听父属性的变更,这样,父属性改变时,子属性的watcher也能收到通知进行update
// 这一步是在 this.get() --> this.getVMVal() 里面完成,forEach时会从父级开始取值,间接调用了它的getter
// 触发了addDep(), 在整个forEach过程,当前wacher都会加入到每个父级过程属性的dep
// 例如:当前watcher的是'child.child.name', 那么child, child.child, child.child.name这三个属性的dep都会加入当前watcher
if (!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this);
this.depIds[dep.id] = dep;
}
},
get: function() {
Dep.target = this;
var value = this.getter.call(this.vm, this.vm);
Dep.target = null;
return value;
},
parseGetter: function(exp) {
if (/[^\w.$]/.test(exp)) return;
var exps = exp.split('.');
return function(obj) {
for (var i = 0, len = exps.length; i < len; i++) {
if (!obj) return;
obj = obj[exps[i]];
}
return obj;
}
}
};
Object.defineProperty的缺陷
- Object.defineProperty监听的都是对象,无法监听数组变化,虽然vue里面也是可以监听数组,但是那是做了一些特别处理的
- Object.defineProperty只能劫持对象的属性,当属性值也是对象的时候就需要深度遍历
Proxy实现的双向绑定
Proxy是ES6的新特性,它其实就是一层拦截器,想访问目标对象,都必须过它这条道,这不是完美解决我们 Object.defineProperty的不足了吗?Vue3.x就使用这一新的特性来替代之前的Object.defineProperty
Proxy可以直接监听对象
来个栗子先
<!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>
<p>请输入:</p>
<input type="text" id="input">
<p id="p"></p>
<script type="text/javascript">
const input = document.getElementById('input')
const p = document.getElementById('p')
const obj = {}
const newObj = new Proxy(obj, {
get: function(target, key, receiver) {
console.log(`getting ${key}`)
return Reflect.get(target, key, receiver) // Reflect.get() 方法的工作方式,就像从对象 ( target[propertyKey] ) 中获取属性,但它是作为一个函数执行的。
},
set: function(target, key, value, receiver) {
console.log(target, key, value, receiver)
if (key === 'text') {
input.value = value
p.innerHTML = value
}
return Reflect.set(target, key, value, receiver)
}
})
input.addEventListener('keyup', function (e) {
newObj.text = e.target.value
})
</script>
</body>
</html>
可以监听数组
当数组进行push,shift,splice等操作和length变化的时候,我们再来一个栗子
<!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>
<ul id="list">
</ul>
<button type="button" name="button" id="btn">添加列表项</button>
<script type="text/javascript">
const list = document.getElementById('list');
const btn = document.getElementById('btn');
// 渲染列表
const Render = {
// 初始化
init: function(arr) {
const fragment = document.createDocumentFragment();
for (let i = 0; i < arr.length; i++) {
const li = document.createElement('li');
li.textContent = arr[i];
fragment.appendChild(li);
}
list.appendChild(fragment);
},
// 我们只考虑了增加的情况,仅作为示例
change: function(val) {
const li = document.createElement('li');
li.textContent = val;
list.appendChild(li);
},
};
// 初始数组
const arr = [1, 2, 3, 4];
// 监听数组
const newArr = new Proxy(arr, {
get: function(target, key, receiver) {
console.log(key);
return Reflect.get(target, key, receiver);
},
set: function(target, key, value, receiver) {
console.log(target, key, value, receiver);
if (key !== 'length') {
Render.change(value);
}
return Reflect.set(target, key, value, receiver);
},
});
// 初始化
window.onload = function() {
Render.init(arr);
}
// push数字
let num = 5
btn.addEventListener('click', function() {
newArr.push(num++);
});
</script>
</body>
</html>