双向数据绑定的实现思路
要想弄懂双向数据绑定,我们需要了解三个概念:
1,对象的访问器属性
2,闭包
3,发布订阅模式
对象的访问器属性
用来实现数据劫持
var obj = {name: 'apple'};
Object.defineProperty(obj, name, {
get(){ // 1
return obj.name;
},
set(newVal){ // 2
obj.name = newVal;
}
})
get/set及obj对象的访问器属性, 当我们执行obj.name来读取name属性的值时,实际执行1, 同理,执行obj.name='banana’来设置name属性,实际执行set函数,即2。
闭包
函数内部能访问外层的变量,
但外层不能访问函数内部的变量。
当函数通过某种方式,让外层函数可以访问到内部变量时,
我们称这个函数形成了闭包。
闭包是很危险的,因为有外层的引用,
这个变量不会被内存释放,
换句话说,
不会被js的自动垃圾回收机制回收。
那么,怎样可以形成一个闭包呢?
常见的是返回函数的形式,如下:
function foo(){
var name;
return function(){
console.log(name);
};
}
foo()(); // 在外层,成功打印了foo内部变量name的值
vue中,是通过上面介绍的访问器属性形成了闭包
defineReactive($data, key, value){
Object.defineProperty($data, key, {
get(){
return value;
},
set(newVal){
if(newVal != value){
value = newVal;
}
}
})
}
value在函数内部相当于私有变量, 外层通过$data.key读取或设置数据,实际都是操作的内部变量value。
我们平时写vue项目,常用的this.xxx,实际都被代理到了$data.xxx。
发布订阅模式
是一种设计模式,想象一种场景:
下雨了:
建筑工:休息一天~
农民工:小苗可以快快长高了
卖伞的:天助我也哈哈哈~
程序员:关我毛事~
天晴了:
建筑工:开工~
农民工:小苗长高高~
卖伞的:哎卖不动了
程序员:关我毛事~
一般会将主动变化因素(天气)视为发布者, 被动变化因素(4位小哥)视为订阅者。
发布者收集所有与之相关的订阅者, 并在发生变化时统一通知他的订阅者们更新状态。
订阅者可以注册到一个发布者, 并实现状态变更。
用代码表示:
1-发布者:
class Dep {
constructor(){
this.deps = [];
}
// 收集
addDep( watcher ){
this.deps.push(watcher);
}
// 通知更新
notify(){
this.deps.forEach( dep => {
dep.update();
})
}
}
2-订阅者:
class Watcher {
constructor(){},
update(){} // 状态变更
}
vue中,每个绑定了数据的节点(包括文本节点/元素节点等)都对应一个watcher, 每个data中的数据属性都对应一个Dep(dependency依赖)。
如:
data: {
name: 'apple'
}
`name属性对应一个Dep, 这个Dep可能对应多个watcher,
如:``
{{name}}
```这就对应了3个watcher。
双向数据绑定的大体流程是这样的:
input标签输入内容,触发input事件,
inputHandler中修改data的属性(this.name=value),
访问器属性劫持到修改,通知watchers去更新(修改dom内容)
好了,概念讲的差不多了,下面我们来上具体代码~
简易版,功能不全,情况考虑也不周全,只是为了体现大体流程,想看具体实现可以去github上拉源码。相信看了这个简易版,源码读起来会更加易懂!
当new一个Vue实例时,vue主要做了这两件事:
1,数据劫持
2,编译dom,解析指令 (compile)
下面来一步步实现:
1:数据劫持
code
// KVue.js
class KVue {
constructor( options ){
this.vm = this;
this.$options = options;
this.$data = options.data;
this.observe( this.$data );
}
observe( obj ){
// 递归结束条件
if(!obj || typeof obj != 'object') return;
Object.keys(obj).forEach( key => {
// 注意:普通非箭头函数 this不指向KVue实例
this.defineReactive(obj, key, obj[key]);
});
}
defineReactive(obj, key, value){
// 递归 劫持 深层数据
this.observe(value);
Object.defineProperty(obj, key, {
get(){ // 劫持到读
return value;
},
set(newVal){ // 劫持到写
if(value != newVal){
vale = newVal;
}
}
})
}
}
2:添加发布订阅模式代码(收集依赖)
code
// KVue.js
class KVue {
constructor( options ){
this.vm = this;
this.$options = options;
this.$data = options.data;
this.observe( this.$data );
}
observe( obj ){
if(!obj || typeof obj != 'object') return;
Object.keys(obj).forEach( key => {
this.defineReactive(obj, key, obj[key]);
});
}
defineReactive(obj, key, value){
this.observe(value);
// 每个属性对应一个Dep实例
var dep = new Dep();
Object.defineProperty(obj, key, {
get(){
// 配合实现watcher实例添加
if(Dep.target){
dep.addDep(Dep.target);
};
return value;
},
set(newVal){
if(value != newVal){
value = newVal;
// 通知更新所有订阅者
dep.notify();
}
}
})
}
}
// 发布者
class Dep {
constructor(){
this.deps = [];
}
addDep( watcher ){
this.deps.push( watcher );
}
notify(){
this.deps.forEach(dep => {
dep.update();
})
}
}
// 订阅者
class Watcher {
constructor(vm, key, cb){
this.cb = cb;
// 注意:这3步 结合 get劫持 实现
// 将该Watcher实例 添加到 vm[key]对应的Dep实例
Dep.target = this; // 通过Dep静态属性
vm[key];
Dep.target = null; // 重置
}
update(){
this.cb && this.cb(); // 这里更新dom,由compile传过来
}
}
3:添加代理,代理this.name到this.$options.data.name
code
// KVue.js
class KVue {
observe(obj){
if(!obj || typeof obj != 'object') return;
Object.keys(obj).forEach(key => {
this.defineReactive(obj, key, obj[key]);
// 这里添加
this.proxy(obj, key, obj[key]);
});
}
proxy(obj, key, value){
Object.defineProperty(this, key, {
get(){
return obj[key];
},
set(newVal){
obj[key] = newVal;
}
})
}
}
4:添加编译过程(解析指令)
编译是vue1.0版本,没有虚拟dom这一块儿
code
// KVue.js
class KVue {
constructor( options ){
this.vm = this;
this.$options = options;
this.$data = options.data;
this.observe( this.$data );
// 添加编译
new Compile(this, this.$options.el);
}
}
// compile.js 循环遍历dom,解析指令
class Compile {
constructor(vm, selector){
this.vm = vm;
this.dom = document.querySelector(selector);
// 转移到fragment,比直接操作dom更高效
this.createFragment();
this.compile(this.$fragment);
this.dom.appendChild(this.$fragment);
}
createFragment(){
this.$fragment = document.createDocumentFragment();
while(this.dom.firstChild){
this.$fragment.appendChild(this.dom.firstChild);
}
}
compile(frag){
Array.from(frag.childNodes).forEach(node => {
if(node.nodeType == 1){// 元素
this.compileElement(node);
// 递归结束条件
if(node.childNodes && node.childNodes.length > 0){
this.compile(node);
};
}else if(node.nodeType == 3){// 文本
this.compileText(node);
};
});
}
compileElement(ndoe){
Array.from(ndoe.attributes).forEach(attr => {
if(attr.name.indexOf('k-') == 0){
var dir = attr.name.slice(2);
this[dir](ndoe, attr.value);
}else if(attr.name.indexOf('@') == 0){
var event = attr.name.slice(1);
this.event(ndoe, event, attr.value);
}
})
}
text(node, key){
node.textContent = this.vm[key];
new Watcher(this.vm, key, function(){
node.textContent = this.vm[key];
});
}
html(node, key){
node.innerHTML = this.vm[key];
new Watcher(this.vm, key, function(){
node.innerHTML = this.vm[key];
})
}
model(node, key){
node.value = this.vm[key];
new Watcher(this.vm, key, function(){
node.value = this.vm[key];
});
node.addEventListener('input', () => {
this.vm[key] = node.value;
})
}
compileText(node){
/**
* 这里用到正则知识点:
* rexExp.test()后,
* 所有捕获到的内容,可以通过RegExp.$1...$9访问到
*/
if(/\{\{(.*)\}\}/.test(node.textContent)){
this.text(node, RegExp.$1);
}
}
event(node, eventName, eventHandler){
if(eventHandler && this.vm.$options.methods[eventHandler]){
node.addEventListener(
eventName,
this.vm.$options.methods[eventHandler].bind(this.vm)
)
}
}
}
(文章错误、不周之处,还请斧正,我们一起探讨共同进步 ^ ^)
原文链接:https://juejin.im/post/5eba043c6fb9a043710eadec
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。