实现双向绑定

主流的双向绑定方法 1.发布-订阅模式 通过使用 get 和 set 的方式获取数据然后更新数据,其原理就是监听页面中某个具体元素的事件,然后将其最新的值手动 set 到 数据中,同时订阅 model 层的改变,然后触发页面的渲染更新

2.脏检测 通过对比数据是否有变更,来决定是否更新视图。最简单的可以通过定时轮询去检测数据的变动。Angular 只有在指定事件触发时进入脏检测:

  • DOM事件,比如用户输入文本点击按钮等(ng-click)
  • XHR响应事件 浏览器 Location 变更
  • Timer事件
  • 执行 $digest() 或 $apply();

脏检查的主要原理是在将数据绑定到 View 的时候,就在监听器列表(scope 作用域中的监听队列 watchList)中插入一条监听器,当触发 UI 事件或者 Ajax 请求时,就会触发脏检查($digest cycle), 在 $digest 流程中,将遍历每个数据变量的 watcher,比较它的新旧值。当新旧值不同时,触发 listener 函数,执行相关的更新逻辑。这个过程将会一直重复,直到所有数据指令的新旧值都相同为止。

脏检查虽然可以达到实现双向绑定,但是当页面中绑定的 watcher 过多时,就会引发性能问题。所以 angular 在进行 $digest 检测时,会限制循环检查的次数最少2次,最多10次,防止无效的检查。

3.数据劫持 通过 ES5 的 Object.defineProperty() 来劫持数据属性的 getter 和 setter, 在数据变动时触发订阅者(watcher),从而触发相应的监听回调更新视图。

  • Observer 对数据的所有属性进行监听其 getter 和 setter
  • Compile 是一个指令解析器,对 MVVM 实例的所有元素指令进行解析,并渲染成 model 中的绑定数据,当数据进行更新时,也能替换为更新后的值。
  • Watcher 作为 Compile 和 Observer 的桥梁,能够订阅数据属性的更新,然后执行相应的监听回调
  • Deps 用于存放监听器数组,主要用来保存 Watcher
  • Updater 执行更新操作,针对不同的指令进行不同的更新操作,如 v-model, v-class, v-html 等类型的指令。
  • MVVM 作为入口函数,整合以上所有的功能。

vue-mvvm

Observer 劫持了所有数据属性的 getter 和 setter,当数据发生改变时,就会通知 deps 中所有 watcher 的更新操作,进而触发页面的重新渲染,这是修改 Model 层从而引发 View 层的重新渲染。 在 Compile 中监听可输入元素的事件,然后将新值更新到 model 的数据中,这是修改 View 层触发的 Model 层的修改。

  • 用户名或者邮箱跟github没有关联上, github认为不是你提交的, 不统计。
  • fork 的项目, 不统计
  • 没有在版本库的master【默认分支】上提交

解读


defineProperty实现

目前支持双向绑定的Vue中的实现就是这种方法。但是这种方法不太好的地方就是对于数组之类的对象,类似修改数组的length,直接用索引设置元素如items[0] = {},以及数组的push,pop等变异方法是无法触发setter的。针对这些,vue中的实现是在Object和Array的原型添加了定制方法来处理这些特殊操作,可以实现上述要求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* bind
* @param {object} target
* @param {object} dom
* @param {object} map
*/
const bind = (target, dom, map) =>
Object.keys(map || target).forEach(key =>
Object.defineProperty(target, key, {
set(value) {
const domKey = map ? map[key] : key
dom[domKey] = value
},
get() {
const domKey = map ? map[key] : key
return dom[domKey]
}
}))
window._alternate = {}

const p = document.getElementById('p')

bind(_alternate, p, {
'value': 'innerHTML'
})

proxy实现

怎么理解reflect reflect 是es6新增的一个全局对象。顾名思义,反射,类似于Java里面的反射机制。在Java里面,反射是个很头疼的概念。简单理解为:通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。对于Java来说,程序中一般的对象的类型都是在编译期就确定下来的,而Java反射机制可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。所以我们可以通过反射机制直接创建对象,即使这个对象的类型在编译期是未知的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* bind
* @param {object} model
* @param {object} map
*/
const bind = (model, map) => new Proxy(map || model, {
get(_, key) {
const mkey = map ? map[key] : key
return Reflect.get(model, mkey)
},
set(_, key, value) {
const mkey = map ? map[key] : key
return Reflect.set(model, mkey, value)
}
})

window.__alternate = bind(document.getElementById('p'), {
'value': 'innerHTML'
})

mvvm

MVVM

mvvm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function MVVM(options) {
this.$el = options.el;
this.$data = options.data;
this.$method = options.method;
if (this.$el) {
// 对所有数据进行劫持
new Observer(this.$data);
// 将数据直接代理到实例中,无需通过vm.$data来操作
this.proxyData(this.$data);
new Compile(this.$el, this);
}
}

MVVM.prototype = {
proxyData: function (data) {
Object.keys(data).forEach((key) => {
Object.defineProperty(this, key, {
get() {
return data[key];
},
set(newValue) {
data[key] = newValue;
},
});
});
},
};

observer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
function Observer(data) {
this.data = data;
this.observe(this.data);
}

Observer.prototype = {
/**
* @param {data} 要监听的数据对象
*/
observe: function (data) {
if (!data || typeof data !== "object") return;
// 为每一个属性设置数据监听
Object.keys(data).forEach((key) => {
this.defineReactive(data, key, data[key]);
this.observe(data[key]); // 深度递归劫持属性
});
},

/**
* @param {data} 要监听的数据对象
* @param {key} 要监听的对象属性key值
* @param {value} 要监听的对象属性值
*/
defineReactive: function (data, key, value) {
let dep = new Dep();
let self = this;

// 如果是该属性值是对象类型,则遍历
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get: () => {
// 由于需要在闭包内添加watcher,所有需要 Dep 定义一个全局 target 属性,暂存 watcher ,添加完移除
if (Dep.target) {
// 如果为true,则说明是实例化 watcher 引起的,所以需要添加进消息订阅器中
dep.depend();
}
return value;
},
set: (newVal) => {
if (newVal === value) return;
value = newVal;
// 对新值进行监听
self.observe(newVal);
// 通知所有订阅者
dep.notify();
},
});
},
};

watcher

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
*
* @param {*vm} 双向绑定实例
* @param {*expOrFn} 是表达式还是function
* @param {*cb} 执行更新时的回调函数
*/
function Watcher(vm, expr, cb) {
this.depIds = {}; // 存储deps订阅的依赖
this.vm = vm; // component 实例
this.cb = cb; // 更新数据时的回调函数
this.expr = expr; // 表达式还是function
this.value = this.get(vm, expr); // 在实例化的时候获取老值
}

Watcher.prototype = {
// 暴露给 Dep 类的方法,用于在订阅的数据更新时触发
update: function () {
const newValue = this.get(this.vm, this.expr); // 获取到的新值
const oldValue = this.value; // 获取到的旧值
if (newValue !== oldValue) {
// 判断新旧值是否相等,不相等就执行回调
this.value = newValue;
this.cb(newValue);
}
},

addDep: function (dep) {
// 检查depIds对象是否存在某个实例,避免去查找原型链上的属性
if (!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this); // 在 dep 存储 watcher 监听器
this.depIds[dep.id] = dep; // 在 watcher 存储订阅者 dep
}
},

// 获取data中的值,可能出现 hello.a.b的情况
getVal: function (vm, expr) {
expr = expr.split(".");
return expr.reduce((prev, next) => {
return prev[next];
}, vm.$data);
},

// 获取值
get: function (vm, expr) {
Dep.target = this;
const val = this.getVal(vm, expr);
Dep.target = null;
return val;
},
};

dep

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 订阅事件的唯一标识
let uid = 0;

// 订阅类
function Dep() {
this.id = uid++;
this.subs = [];
}

Dep.prototype = {
addSub: function (sub) {
if (this.subs.indexOf(sub) === -1) {
// 避免重复添加
this.subs.push(sub);
}
},

removeSub: function (sub) {
const index = this.subs.indexOf(sub);
if (index > -1) {
this.subs.splice(index, 1);
}
},

depend: function () {
Dep.target.addDep(this); //执行 watcher 的 addDep 方法
},

notify: function () {
this.subs.forEach((sub) => {
sub.update(); // 执行 watcher 的 update 方法
});
},
};

// Dep 类的全局属性 target,是一个 Watch 实例
Dep.target = null;

compile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
function Compile(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
if (this.el) {
// 将真实DOM移入内存 fragment 中
let fragment = this.node2Fragment(this.el);
this.compile(fragment);
//将编译后的 fragment 再次转化为 DOM 塞回到页面中
this.el.appendChild(fragment);
}
}

Compile.prototype = {
// 将 DOM 转化为 fragment
node2Fragment: function (el) {
let fragment = document.createDocumentFragment();
// 每次获取DOM节点树中的第一个元素,直到移除完毕为止
while (el.firstChild) {
fragment.appendChild(el.firstChild);
}
// 返回一个文档碎片容器,存储DOM树的所有节点
return fragment;
},
// 是否是DOM节点元素
isElementNode(node) {
return node.nodeType === 1;
},
//编译函数
compile: function (fragment) {
let childNodes = fragment.childNodes;
Array.from(childNodes).forEach((node) => {
// 是否是元素节点
if (this.isElementNode(node)) {
this.compileElement(node);
this.compile(node);
} else {
// 是否是文本节点
this.compileText(node);
}
});
},

//判断是否是指令
isDirective: function (name) {
// 不能使用 indexOf 的方式,因为可能出现 v-model-v-model,必须以 v- 开头
return name.startsWith("v-");
},

// 是否是事件指令
isEventDirective: function (dir) {
return dir.startsWith("on");
},

//编译节点元素
compileElement: function (node) {
// 带v-model v-text
let attrs = node.attributes; // 取出当前节点的属性
Array.from(attrs).forEach((attr) => {
let attrName = attr.name;
if (this.isDirective(attrName)) {
// 取到指令对应的值放到节点中
let expr = attr.value;
const attrArr = attrName.split("-");
// 说明此时不是 v-model 的这种形式,而是 v-model-v-model
if (attrArr.length !== 2) {
return;
}
let type = attrArr[1]; // 获取指令是哪种类型,比如v-model,v-text
// 如果是事件指令
if (this.isEventDirective(type)) {
CompileUtil.eventHandler(node, this.vm, expr, type);
} else {
// 调用对应的编译方法 编译哪个节点,用数据替换掉表达式
CompileUtil[type](node, this.vm, expr);
}
}
});
},

// 编译文本元素
compileText: function (node) {
let expr = node.textContent; // 取文本中的内容 todo:和 innerHTML 的区别
let reg = /\{\{([^}]+)\}\}/g; // 不能直接检测 {{}} 这种情况,还要考虑这种情况 {{a}} {{b}} {{c}}
if (reg.test(expr)) {
// 调用编译文本的方法 编译哪个节点,用数据替换掉表达式
CompileUtil["text"](node, this.vm, expr);
}
},
};

// 指令处理集合
var CompileUtil = {
// 文本处理
text: function (node, vm, expr) {
let updateFn = Updater["textUpdater"];
let value = this.getTextVal(vm, expr);
expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
// 实例化观察者,添加到发布订阅的数组中
new Watcher(vm, arguments[1], (newValue) => {
// 传入每次的表达式,比如出现 {{a}} {{b}}, 就要分别取获取表达式 a,b 的值,
// 如果直接传入 newValue,则后一个值会覆盖前一个值
updateFn && updateFn(node, this.getTextVal(vm, expr));
});
});
updateFn && updateFn(node, value);
},
// html指令处理
html: function (node, vm, expr) {
let updateFn = Updater["htmlUpdater"];
// 更新渲染
new Watcher(vm, expr, (newValue) => {
updateFn && updateFn(node, newValue);
});
// 初始化渲染
updateFn && updateFn(node, this.getVal(vm, expr));
},

// class 指令处理
class: function (node, vm, expr) {
let updateFn = Updater["classUpdater"];
// 更新渲染
new Watcher(vm, expr, (newValue) => {
updateFn && updateFn(node, newValue);
});
// 初始化渲染
updateFn && updateFn(node, this.getVal(vm, expr));
},

// model指令处理
model: function (node, vm, expr) {
let updateFn = Updater["modelUpdater"];
new Watcher(vm, expr, (newValue) => {
updateFn && updateFn(node, newValue);
});
// 监听输入框的input事件,并将值回填到数据中
node.addEventListener("input", (e) => {
let newValue = e.target.value;
this.setVal(vm, expr, newValue);
});
updateFn && updateFn(node, this.getVal(vm, expr));
},

// 事件指令处理
eventHandler: function (node, vm, expr, type) {
// todo
let eventType = type.split(":")[1],
fn = vm.$method[expr];
if (eventType && fn) {
node.addEventListener(eventType, fn.bind(vm), false);
}
},

// 获取文本的值
getTextVal: function (vm, expr) {
return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
//argument[1] 就是 {{hello}} 里面的 hello 值
return this.getVal(vm, arguments[1]);
});
},
// 获取值
getVal: function (vm, expr) {
expr = expr.split("."); //将 hello.a.b 转化为数组,调用reduce获取最里面的值
return expr.reduce((prev, next) => {
return prev[next];
}, vm.$data);
},
// 设置值
setVal: function (vm, expr, value) {
expr = expr.split(".");
// 将新值回填到数据中,并且回填到最后一个值,如:hello.a.b,就需要把值回填到b中
return expr.reduce((prev, next, index) => {
if (index === expr.length - 1) {
return (prev[next] = value);
}
return prev[next];
}, vm.$data);
},
};

// 更新数据处理集合
var Updater = {
// 文本更新
textUpdater: function (node, value) {
node.textContent = typeof value === "undefined" ? "" : value;
},
// html 更新
htmlUpdater: function (node, value) {
node.innerHTML = typeof value === "undefined" ? "" : value;
},
// class 更新
classUpdater: function (node, value) {
let className = node.className;
className = className.replace(value, "").replace(/\s$/, "");
var space = className && String(value) ? " " : "";
node.className = className + space + value;
},
// model 更新
modelUpdater: function (node, value) {
node.value = typeof value === "undefined" ? "" : value;
},
};
本文结束感谢您的阅读

本文标题:实现双向绑定

文章作者:陈宇(cosyer)

发布时间:2018年11月07日 - 17:11

最后更新:2020年07月11日 - 00:07

原始链接:http://mydearest.cn/%E5%AE%9E%E7%8E%B0%E5%8F%8C%E5%90%91%E7%BB%91%E5%AE%9A.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持原创技术分享,您的支持将鼓励我继续创作!