1 <!DOCTYPE html>
2 <html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 <meta http-equiv="X-UA-Compatible" content="ie=edge" />
7 <title>双向数据绑定demo</title>
8 </head>
9 <style>
10 input {
11 border: 1px solid #636336;
12 margin-right: 10px;
13 }
14 </style>
15 <body>
16 <div id="app">
17 <input type="text" v-model="text" />
18 {{ text }}
19 </div>
20 <script>
21 // 遍历data添加数据劫持
22 function observe(obj, vm) {
23 console.log(vm);
24 Object.keys(obj).forEach(function(key) {
25 defineReactive(vm, key, obj[key]);
26 });
27 }
28 // 数据劫持
29 function defineReactive(obj, key, val) {
30 var dep = new Dep();
31 Object.defineProperty(obj, key, {
32 get: function() {
33 // 添加订阅者 watcher 到主题对象 Dep
34 if (Dep.target) {
35 dep.addSub(Dep.target);
36 }
37 return val;
38 },
39 set: function(newVal) {
40 if (newVal === val) return;
41 val = newVal;
42 // 作为发布者发出通知
43 dep.notify();
44 }
45 });
46 }
47 // 劫持dom节点
48 function nodeToFragment(node, vm) {
49 var nodes = document.createDocumentFragment();
50 var child;
51 // appendChild 方法有个隐蔽的地方,就是调用以后 child 会从原来 DOM 中移除
52 // 所以,第二次循环时,node.firstChild 已经不再是之前的第一个子元素了
53 while (child = node.firstChild) {
54 compile(child, vm);
55 nodes.appendChild(child); // 将子节点劫持到文档片段中
56 }
57 /*let nodeList = node.childNodes;
58 for (var i = 0; i < nodeList.length; i++) {
59 compile(nodeList[i], vm);
60 nodes.appendChild(nodeList[i]);
61 }*/
62 return nodes;
63 }
64 // 遍历节点,找出v-model和双花括号
65 function compile(node, vm) {
66 // 节点类型为元素
67 if (node.nodeType === 1) {
68 var attr = node.attributes;
69 // 解析属性
70 for (var i = 0; i < attr.length; i++) {
71 if (attr[i].nodeName == 'v-model') {
72 var name = attr[i].nodeValue; // 获取 v-model 绑定的属性名
73 node.addEventListener('input', function(e) {
74 // 给相应的data属性赋值,进而触发该属性的set方法
75 vm[name] = e.target.value;
76 });
77 node.value = vm[name]; // 将data的值赋给该node
78 node.removeAttribute('v-model'); // 移除v-model属性
79 }
80 }
81 new Watcher(vm, node, name, 'input'); // 输入框节点
82 }
83 var reg = /\{\{(.*)\}\}/; // 匹配双花括号
84 // 节点类型为文本内容
85 if (node.nodeType === 3) {
86 if (reg.test(node.nodeValue)) {
87 var name = RegExp.$1; // 获取匹配到的字符串
88 name = name.trim();
89 new Watcher(vm, node, name, 'text'); // 展示节点
90 }
91 }
92 }
93 class Watcher {
94 constructor (vm, node, name, nodeType) {
95 Dep.target = this;
96 this.name = name;
97 this.node = node;
98 this.vm = vm;
99 this.nodeType = nodeType;
100 this.update();
101 Dep.target = null;
102 }
103 update () {
104 this.get();
105 if (this.nodeType === 'text') {
106 this.node.nodeValue = this.value;
107 }
108 if (this.nodeType === 'input') {
109 this.node.value = this.value;
110 }
111 }
112 // 获取 data 中的属性值
113 get () {
114 this.value = this.vm[this.name]; // 触发相应属性的 get
115 }
116 }
117 /*
118 // 观察者对象
119 function Watcher(vm, node, name, nodeType) {
120 Dep.target = this;
121 this.name = name;
122 this.node = node;
123 this.vm = vm;
124 this.nodeType = nodeType;
125 this.update();
126 Dep.target = null;
127 }
128 Watcher.prototype = {
129 update: function() {
130 this.get();
131 if (this.nodeType === 'text') {
132 this.node.nodeValue = this.value;
133 }
134 if (this.nodeType === 'input') {
135 this.node.value = this.value;
136 }
137 },
138 // 获取 data 中的属性值
139 get: function() {
140 this.value = this.vm[this.name]; // 触发相应属性的 get
141 }
142 };
143 */
144 class Dep {
145 constructor () {
146 this.subs = [];
147 }
148 addSub (sub) {
149 this.subs.push(sub);
150 }
151 notify () {
152 this.subs.forEach(function(sub) {
153 sub.update();
154 });
155 }
156 }
157 /*
158 function Dep() {
159 this.subs = [];
160 }
161 Dep.prototype = {
162 addSub: function(sub) {
163 this.subs.push(sub);
164 },
165 notify: function() {
166 this.subs.forEach(function(sub) {
167 sub.update();
168 });
169 }
170 };
171 */
172 // 创建Vue
173 function Vue(options) {
174 this.data = options.data;
175 var data = this.data;
176 observe(data, this);
177 var id = options.el;
178 var dom = nodeToFragment(document.getElementById(id), this);
179 // 编译完成后,将 dom 返回到 app 中
180 document.getElementById(id).appendChild(dom);
181 }
182 // 实例化Vue
183 var vm = new Vue({
184 el: 'app',
185 data: {
186 text: 'hello world'
187 }
188 });
189 </script>
190 </body>
191 </html>