vue双向绑定实现原理

Vue.js 双向绑定实现原理

1、编译代码,将代码中的特殊模板替换成vue属性值,模板得到初始化。利用DocumentFragment存储dom,完成遍历后一次性添加到原始dom中,只发生1次渲染操作,提高性能。

2、例如input控件text发生变化时需要同步修改data中对应的属性,所以监听input控件的输入事件,如有变化就调用data中属性的set方法,实现属性值更新。为属性添加get、set方法需要使用Object.defineProperty() 覆盖原始属性的实现。这一步骤即为响应式数据绑定

3、如何让文本节点实时变化? 利用订阅发布模式,通知文本节点属性变化。将每一个属性添加一个发布者,编译节点时为模板节点绑定一个watcher,并将watcher添加进属性的发布者模型(dep.subs)中如果属性值发生变化,遍历调用每一个订阅者的watcher.update方法。

以上即为简单的vue双向绑定实现原理。

data

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

<div id='app'>
<input type="text" v-model='text'></input>
<span>{{text}}</span>
</div>

<script>
// https://juejin.im/entry/583bd53ca22b9d006dce11d7
// 劫持节点
function nodeToFragment(node, vm) {

var frag = document.createDocumentFragment();
var child;


while(child = node.firstChild)
{
//方法appendChild调用后会将该child节点从原来的DOM中移除 所以第二次循环时 child是一个新的节点
frag.appendChild(child);
}

// 深度遍历节点
DFS(frag, function(node) {

compile(node, vm);
});

return frag;
}
//遍历node节点所有子节点
function DFS(node, cb) {
let deep = 1;
DFSdom(node, deep, cb)
}

function DFSdom(node, deep, cb) {
if (!node)
return;

cb(node, deep)

if (!node.childNodes.length) {
return;
}

deep++;

Array.from(node.childNodes).forEach(item => DFSdom(item, deep, cb))
}

// 1、 编译node vm 进行node初始化绑定data
function compile(node, vm) {

var reg = /\{\{(.*)\}\}/;

// 节点为元素
if (node.nodeType === Node.ELEMENT_NODE) {
var attr = node.attributes;
//0: type 1: id 2: v - model
/*2: v - model // attr 结构
baseURI: "file:///Users/yuanOK/Desktop/vue.html"
childNodes: NodeList[]
localName: "v-model"
name: "v-model"
nodeName: "v-model"
nodeType: 2
nodeValue: "text"
*/
// 解析属性
for(var i = 0; i < attr.length; i++) {
if (attr[i].nodeName == 'v-model') {
var name = attr[i].nodeValue;
// 3、为input添加监听方法 同步修改model数据
node.addEventListener('input', function (e) {
vm[name] = e.target.value;
})
node.value = vm.data[name];
node.removeAttribute('v-model');
}
}
}
// 节点为text
if (node.nodeType === Node.TEXT_NODE) {
if (reg.test(node.textContent)) {
var name = RegExp.$1; // 获取匹配到的字符串
name = name.trim();
new Watcher(vm, node, name);
// node.nodeValue = vm.data[name]; // 将data的值赋给node
}
}
}
// 2、 响应式数据绑定 实现 input输入的同时修改model数据
// 通过defineProperty 劫持 data中的属性值,为属性设置set get 方法
function defineReactive(obj, key, val) {
var dep = new Dep();
Object.defineProperty(obj, key, {

get: function () {
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set: function (newVal) {
if (newVal == val) return;
val = newVal;
dep.notify();
console.log(val);
}
});
}

function observe(obj, vm) {
Object.keys(obj).forEach(function (key) {
defineReactive(vm, key, obj[key]);
})
}

//4、定义发布者,订阅者模式
function Dep() {
this.subs =[];
}

Dep.prototype = {
addSub: function (sub) {
this.subs.push(sub);
},
notify: function () {
this.subs.forEach(function(sub) {
sub.update();
});
}
}

function Watcher(vm, node, name) {
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.update();
Dep.target = null;
}
Watcher.prototype = {
update: function () {
this.get();
this.node.nodeValue = this.value;
},
get: function () {
this.value = this.vm[this.name];
}
}

function Vue(options) {
this.data = options.data;
var data = this.data;
observe(data, this);
var id = options.el;
var dom = nodeToFragment(document.getElementById(id), this);

document.getElementById(id).appendChild(dom);
}

var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
})

</script>