├── .vscode
└── settings.json
├── README.md
├── index.html
└── js
├── Compile.js
├── Dependency.js
├── Observer.js
├── Vue.js
└── Watch.js
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "emmet.triggerExpansionOnTab": true
3 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Vue最精华的部分就是双向绑定,在双向绑定的基础上,又写了computed,watch, methods等方法。所以要看懂Vue内核,那第一步肯定就是要了解Vue双向绑定的原理,但是说实话,看了网上很多,好多代码都是经过重构优化后的代码,失去了代码原始的面貌,不太易于理解。所以决定写一个原始一点但是又尽可能简洁一点的,但是原理绝对是Vue双向绑定的原理,确保你看懂这篇文章,就能够了解Vue内核。采用最少的代码,来实现一个个功能。有什么写的不妥的地方,烦请在仓库issue中指出,我好及时修正。
2 | 这个项目的github地址为[build-your-own-vue](https://github.com/jackiewillen/build-your-own-vue) 欢迎[star](https://github.com/jackiewillen/build-your-own-vue)
3 |
4 | 如果你对当前流行的轮子的原理感兴趣,下面还有这些你也可以看看,有疑问欢迎在各个仓库下留言:
5 |
6 | [build-your-own-react](https://github.com/jackiewillen/build-your-own-react)
7 |
8 | [build-your-own-vuex](https://github.com/jackiewillen/build-your-own-vuex)
9 |
10 | [build-your-own-redux](https://github.com/jackiewillen/build-your-own-redux)
11 |
12 | [build-your-own-flux](https://github.com/jackiewillen/build-your-own-flux)
13 |
14 | 接下来所讲的这些就为了实现下面这个简单的双向绑定:
15 | ```javascript
16 |
17 |
18 | {{name}}
19 |
20 |
29 | ```
30 |
31 | 在chrome devtools控制台中通过this.vue.name = 'willen'可以自动更新页面中的name为’willen‘。看看结果:
32 |
33 | 
34 |
35 |
36 | (1)从最容易的Dependency.js开始说。
37 |
38 | 先来看代码:
39 | ```javascript
40 | let Watcher = null; // 用来表明有没有监视器实例,这会你可能不懂,下面会遇到它,然后讲解
41 | class Dep { // 把与一个变量相关的监听器都存在subs这个变量中
42 | constructor() {
43 | this.subs = []; // 定义一个subs容器
44 | }
45 | notify() {
46 | // 执行所有与变量相关的回调函数,容器中的watcher一个个都执行掉(看不懂watcher没关系,第二结中就会讲解)
47 | this.subs.forEach(sub => sub.update());
48 | }
49 | addSub(watcher) { // 将一个一个的watcher放入到sub的容器中(看不懂watcher没关系,第二结中就会讲解)
50 | // 添加与变量相关的订阅回调
51 | this.subs.push(watcher);
52 | }
53 | }
54 | ```
55 | 从代码看下来,Dep就是subs容器,是一个数组,将一个个的watcher都放到subs容器中。watcher就是一个个的回调函数,都放在subs的容器中等待触发。addSub中的this.subs.push(watcher)就是将一个个的watcher回调函数放入到其中。notify就是用来将subs中的watcher都触发掉。watcher中就是一个一个更新页面中对应的变量的函数。这个下面会说到。
56 |
57 |
58 |
59 | (2)接下来就看看这个watcher是什么?
60 | ```javascript
61 |
62 | class Watch {
63 | constructor(vue, exp, cb) {
64 | this.vue = vue; // 将vue实例传入到watcher中
65 | this.exp = exp; // 需要对那个表达式进行监控,比如对上例中的'name'进行监控,那么这里的exp就是'name'
66 | this.cb = cb; // 一但监听到上述exp表达式子的值发生变化,需要通知到的cb(callback)回调函数
67 | this.hasAddedAsSub = false; // 有没有被添加到Dep中的Subscriber中去,有的话就不需要重复添加
68 | this.value = this.get(); // 得到当前vue实例上对应表达式exp的最新的值
69 | }
70 | get() {
71 | Watcher = this; // 这边的Watcher为什么需要放入this,并在下面又置空,你需要继续向下看,暂且先记着,这边把现在的watcher实例放到了Watcher中了。
72 | var value = this.vue[this.exp]; // 得到表达式的值,就是得到'name'表达式的值为‘willen’(通过chrome devtools控制台中通过this.vue.name = 'willen'修改了name为’willen‘。)
73 | Watcher = null; // 将Watcher置空,让给下一个值
74 | return value; // 将获取到的表达式的值返回出去
75 | }
76 | update() {
77 | let value = this.get(); // 通过get()函数得到当前的watcher监听的表达式的值,例如上面的‘willen’
78 | let oldVal = this.value; // 获取旧的值
79 | if(value !== oldVal) { // 对比新旧表达式‘name’的值,发现修改前为'jackieyin',修改后为'willen',说明需要更新页面
80 | this.value = value; // 把现在的值记录下来,用于和下次比较。
81 | this.cb.call(this.vue, value); // 用现在的值willen去执行回调函数,其实就是更新一下页面中的{{name}}从‘jackieyin’ 为‘willen’
82 | }
83 | }
84 | }
85 | ```
86 |
87 | (3) 接下来看一下Observer,这个类是做什么工作的。
88 | ```javascript
89 |
90 | class Observer {
91 | constructor(data) {
92 | this.defineReactive(data); // 将用户自定义的data中的元素都进行劫持观察,从而来实现双向绑定
93 | }
94 | defineReactive(data) { // 开始对用户定义的数据进行劫持
95 | var dep = new Dep(); //这个就是第一节中提及到的Dependency类。用来收集双向绑定的各个数据变化时都有的依赖watcher
96 | Object.keys(data).forEach(key => { // 遍历用户定义的data,其实现在也就一个‘name’字段
97 | var val = data[key]; // 得到data['name']的值为jackieyin
98 | Object.defineProperty(data, key, {
99 | get() { // 使用get对data中的name字段进行劫持
100 | if(Watcher) { // 这个就是第二结中提及的Watcher了,(第二结中Watcher = this赋值后这边才会进入if)
101 | if(!Watcher.hasAddedAsSub) { // 对于已经添加到订阅列表中的监视器则无需再重复添加了,防止将watcher重复添加到subs容器中,没有意义,因为一会儿更新{{name}}从‘jackieyin’到‘willen’,更新两三次也还还是一个结果
102 | dep.addSub(Watcher); // 将监视器watcher添加到subs订阅列表中
103 | Watcher.hasAddedAsSub = true; // 表明这个结果已经添加到subs容器中了
104 | }
105 | }
106 | return val; // 将name中的值返回出去
107 | },
108 | set(newVal) { // 对this.vue.name = 'willen'这个set行为进行劫持
109 | if(newVal === val) { // 新值(例如还是this.vue.name = 'jackieyin')与之前的值相同,不做任何修改
110 | return;
111 | }
112 | val = newVal; // 将vue实例上对应的值(name的值)修改为新的值
113 | dep.notify(); // 通知subs中watcher都触发来对页面进行更新,将页面中的{{name}}处的‘jackieyin’更新为'willen'
114 | }
115 | })
116 | });
117 | }
118 | }
119 | ```
120 |
121 | (4) 最后再一起来看看编译类Compile,这个是用来对{{name}}进行编译,说白了就是在你的实例的data对象中,找到name: 'jackieyin',然后在页面上将{{name}}替换为‘jackieyin’
122 | ```javascript
123 | class Compile {
124 | constructor(el, vue) {
125 | this.$vue = vue; // 拷贝vue实例,之所以加$符号,表示暴露给用户的,经常在Vue中看到这种带$标志的,说明是暴露给用户使用的。
126 | this.$el = document.querySelector(el); // 获取到dom对象,其实就是document.querySelector('#app');
127 | if(this.$el) { // 如果存在可以挂在的实例
128 | // 在$fragment中操作,比this.$el中操作节省很多性能,所以要赋值给fragment
129 | let $fragment = this.node2Fragment(this.$el); // 将获取到的el的地方使用片段替代,这是为了便于在内存中操作,使得更新页面更加快速
130 | this.compileText($fragment.childNodes[0]); // 将模板中的{{}}替换成对应的变量,如{{name}}替换为'jackieyin'
131 | this.$el.appendChild($fragment); // 将el获取到的dom节点使用内存中的片段进行替换
132 | }
133 | }
134 | node2Fragment(el) { // 用来把dom中的节点赋值到内存fragment变量中去
135 | // 将node节点都放到fragment中去
136 | var fragment = document.createDocumentFragment();
137 | fragment.appendChild(el.firstChild);// 将el中的元素放到fragment中去,并删除el中原有的,这个是appendChild自带的功能
138 | return fragment;
139 | }
140 |
141 | compileText(node) {
142 | // 对包含可能出现vue标识的部分进行编译,主要是将{{xxx}}替换成对应的值,这边是用正则表达式检测{{}}进行替换
143 | var reg = /\{\{(.*)\}\}/; // 用来判断有没有vue的双括号的
144 | if(reg.test(node.textContent)) {
145 | let matchedName = RegExp.$1;
146 | node.textContent = this.$vue[matchedName];
147 | new Watch(this.$vue, matchedName, function(value) { // 对当前的表达式‘name’添加watcher监听器,其实后来就是把这个watcher放入到了dep中的subs的数组中了。当'name'更新为‘willen’后,其实就是执行了这边的node.textContent = value就把页面中的jackieyin替换成了willen了。这就是双向绑定了。node其实就是刚才存放在内存中的$fragement的节点,所以相当于直接操作了内存,所以更新页面就比修改DOM更新页面快多了。
148 | node.textContent = value;
149 | });
150 | }
151 | }
152 | }
153 | ```
154 |
155 | (5)这个时候就可以来组装出一个我们自己的小型的Vue了。
156 | ```javascript
157 | class Vue {
158 | constructor(options) {
159 | let data = this._data = options.data || undefined;
160 | this._initData(); // 将data中的数据都挂载到this上去,使得this.name 相当于就是得到了this._data.name
161 | new Observer(data); // 将data中的数据进行劫持
162 | new Compile(options.el, this); // 将{{name}}用data中的’jackieyin‘数据替换掉
163 | }
164 | _initData() {
165 | // 这个函数的功能很简单,就是把用户定义在data中的变量,都挂载到Vue实例(this)上
166 | let that = this;
167 | Object.keys(that._data).forEach((key) => {
168 | Object.defineProperty(that, key, {
169 | get: () => {
170 | return that._data[key];
171 | },
172 | set: (newVal) => {
173 | that._data[key] = newVal;
174 | }
175 | })
176 | });
177 | }
178 | }
179 | ```
180 | (6)大功告成,把我们所写的零件组装在一起试一下我们的小型的vue是否工作正常。
181 | ```html
182 |
183 |
184 |
185 |
186 | Document
187 |
188 |
189 |
190 | {{name}}
191 |
192 |
193 |
194 |
195 |
196 |
197 |
206 |
207 |
208 | ```
209 |
210 | 
211 |
212 | 怎么样,搞定了,其实,这只是Vue的冰山一角(下图中的绿色框框的部分),在这个仓库中还实现了一系列vue的功能,如果你有兴趣可以一个commit一个commit的往上看,每个commit都只实现一个完整的细小的功能,而且代码量都尽可能的少,你如果想看一定能看懂。这仓库都是没有使用虚拟DOM来实现,更新颗粒度细,现在的Vue降低了更新的颗粒度,用了虚拟DOM,但是Vue中双向绑定的原理始终未变,所以这篇文章还是需要看懂的,老弟。以后有时间我再研究研究虚拟DOM写个仓库。
213 |
214 | 
215 |
216 | 如发现文章有什么错误,可以在 [我的github中](https://github.com/jackiewillen/blog/issues/20)进行评论留言。如果你觉的文章写的还可以, [欢迎star](https://github.com/jackiewillen/blog/issues/20)
217 |
218 | [文章vue内核仓库地址](https://github.com/jackiewillen/build-your-own-vue)
219 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Document
6 |
14 |
15 |
16 |
17 | 双向绑定演示:{{str}}
18 | v-model演示:
19 | 多层级属性演示:{{dog.name}}
20 |
21 | 动态class功能的演示
22 |
23 | 计算属性演示:{{getComputedValue}}
24 | 点击事件演示:
25 |
26 |
27 |
28 |
29 |
30 |
31 |
63 |
64 |
--------------------------------------------------------------------------------
/js/Compile.js:
--------------------------------------------------------------------------------
1 | class Compile {
2 | constructor(el, vue) {
3 | this.$vue = vue; // 拷贝vue实例,之所以加$符号,表示暴露给用户的
4 | this.$el = document.querySelector(el);
5 | if(this.$el) {
6 | // 在$fragment中操作,比this.$el中操作节省很多性能,所以要赋值给fragment
7 | let $fragment = this.node2Fragment(this.$el); // 将vue模板的地方使用片段替代,这是为了便于在内存中操作
8 | this.compile($fragment);
9 | this.$el.appendChild($fragment); // 将替换好的片段追加到vue模板中去
10 | }
11 | }
12 |
13 | node2Fragment(el) {
14 | // 将node节点都放到fragment中去
15 | var fragment = document.createDocumentFragment();
16 | var child;
17 | // 将原生节点拷贝到fragment
18 | while (child = el.firstChild) {
19 | fragment.appendChild(child);
20 | }
21 | return fragment;
22 | }
23 |
24 | compile(el) {
25 | var childNodes = el.childNodes;
26 | var that = this;
27 | childNodes.forEach(node => {
28 | var text = node.textContent;
29 | var reg = /\{\{(.*)\}\}/;
30 | if(node.nodeType === 1) {
31 | // 表示当前组件依然是一个html元素
32 | that.compileElement(node);
33 | } else if (node.nodeType === 3 && reg.test(text)) {
34 | // 当前为一个文本节点时,并且文本节点中包含{{}}
35 | that.compileText(node, RegExp.$1);
36 | }
37 | });
38 | }
39 |
40 | compileElement(node) {
41 | // compile使用来对html元素进行编译,因为html上可能被添加了一些vue的属性
42 | var nodeAttrs = node.attributes;
43 | var that = this;
44 | // Element.attributes 属性返回该元素所有属性节点的一个实时集合。该集合是一个 NamedNodeMap 对象,不是一个数组,所以它没有 数组 的方法,其包含的 属性 节点的索引顺序随浏览器不同而不同。更确切地说,attributes 是字符串形式的名/值对,每一对名/值对对应一个属性节点。所以这边不能够使用attributes.foreach
45 | [].slice.call(nodeAttrs).forEach(attr => {
46 | var attrName = attr.name;// 得到属性的名称
47 | var exp = attr.value; // 得到属性的值
48 | var dir = attrName.substring(2); //得到指令,如model,on:click之类的
49 | if(attrName.indexOf('v-') == 0) {
50 | // 当为自定义的控件指令时
51 | if(dir.indexOf('on') === 0) {
52 | // 表示是点击事件的时候
53 | var fn = that.$vue.$options.methods && that.$vue.$options.methods[exp];
54 | node.addEventListener('click', fn.bind(that.$vue), false);
55 | } else if(dir.indexOf('model') === 0) {
56 | // 表示是绑定model值时
57 | new Watch(that.$vue, exp, function(value) {
58 | node.value = value;
59 | });
60 | var val = that._getVueVal(that.$vue, exp); // 得到name在$vue中存放的值
61 | node.value = val; // 绑定值到input上去
62 |
63 | node.addEventListener('input', (e) => {
64 | var newValue = e.target.value;
65 | if (val === newValue) {
66 | return;
67 | }
68 | that._setVueVal(that.$vue, exp, newValue);
69 | val = newValue;
70 | });
71 | node.removeAttribute(attrName);
72 | } else if(dir.indexOf('bind') === 0) {
73 | // 表示是绑定class的样式时
74 | var className = node.className;
75 | var value = this._getVueVal(that.$vue, exp);
76 | var space = className && String(value) ? ' ' : '';
77 | node.className = className + space + value;
78 |
79 | new Watch(that.$vue, exp, function(value, oldValue) {
80 | var className = node.className;
81 | className = className.replace(oldValue, '');
82 | var space = className && String(value) ? ' ' : '';
83 | node.className = className + space + value;
84 | });
85 | }
86 | } else{}
87 | })
88 | }
89 |
90 | compileText(node, matchedName) {
91 | // 对包含可能出现vue标识的部分进行编译,主要是将{{xxx}}翻译成对应的值
92 | node.textContent = this._getVueVal(this.$vue, matchedName);
93 | new Watch(this.$vue, matchedName, function(value) {
94 | node.textContent = value;
95 | });
96 | }
97 |
98 | _getVueVal(vue, exp) {
99 | var val = vue;
100 | exp = exp.split('.');
101 | exp.forEach(function(k) {
102 | val = val[k];
103 | });
104 | return val;
105 | }
106 |
107 | _setVueVal(vue, exp, value) {
108 | var val = vue;
109 | exp = exp.split('.');
110 | exp.forEach(function(k, i) {
111 | // 非最后一个key,更新val的值
112 | if (i < exp.length - 1) {
113 | val = val[k];
114 | } else {
115 | val[k] = value;
116 | }
117 | });
118 | }
119 | }
--------------------------------------------------------------------------------
/js/Dependency.js:
--------------------------------------------------------------------------------
1 | let Watcher = null; // 用来表明有没有监视器实例
2 | var uid = 0; // 表明当前依赖实例的编号,便于区分不同的Dep
3 | class Dep { // 把与一个变量相关的监听器都存在subs这个变量中
4 | constructor() {
5 | this.id = uid++;
6 | this.subs = [];
7 | }
8 | notify() {
9 | // 执行所有与变量相关的回调函数
10 | this.subs.forEach(sub => sub.update());
11 | }
12 | addSub(sub) {
13 | // 添加与变量相关的订阅回调
14 | this.subs.push(sub);
15 | }
16 | }
--------------------------------------------------------------------------------
/js/Observer.js:
--------------------------------------------------------------------------------
1 | class Observer {
2 | constructor(data) {
3 | this.defineReactive(data); // 将用户自定义的data中的元素都进行劫持观察,从而来实现双向绑定
4 | }
5 | defineReactive(data) {
6 | Object.keys(data).forEach(key => {
7 | var val = data[key];
8 | var dep = new Dep(); // 用来收集双向绑定的各个数据上变化时都有的依赖,当前使用一个Dep容器就可以了
9 | if(typeof val === 'object') {
10 | new Observer(val); // 反复检查还有没有内容,向里检查是否还有需要监听的内容
11 | }
12 | Object.defineProperty(data, key, {
13 | get() {
14 | if(Watcher) { // 如果当前所获取的这个变量上面有监视器,那么就需要把监视器放到订阅器中等待触发
15 | dep.addSub(Watcher); // 将监视器添加到订阅列表中
16 | }
17 | return val;
18 | },
19 | set(newVal) {
20 | if(newVal === val) { // set值与之前的值相同,不做任何修改
21 | return;
22 | }
23 | val = newVal; // 将vue实例上对应的值修改为新的值
24 |
25 | dep.notify(); // 通知执行所有与此变量相关的回调函数
26 | }
27 | })
28 | });
29 | }
30 | }
--------------------------------------------------------------------------------
/js/Vue.js:
--------------------------------------------------------------------------------
1 | class Vue {
2 | constructor(options) {
3 | this.$options = options || {};
4 | let data = this._data = options.data || undefined;
5 | this._initData(); // 将data中的数据都挂载到this上去
6 | this._initComputed();// 将data中的计算属性挂载到this上去
7 | new Observer(data); // 将data中的数据都进行双向绑定监控
8 | this._initWatch(); // 添加监控到对应的变量下
9 | new Compile(options.el, this); // 将{{name}}这样的模板,使用data中的数据替换掉
10 | }
11 | _initData() {
12 | // 这个函数的功能很简单,就是把用户定义在data中的变量,都挂载到Vue实例上
13 | let that = this;
14 | Object.keys(that._data).forEach((key) => {
15 | Object.defineProperty(that, key, {
16 | get: () => {
17 | return that._data[key];
18 | },
19 | set: (newVal) => {
20 | that._data[key] = newVal;
21 | }
22 | })
23 | });
24 | }
25 | _initComputed() {
26 | var that = this;
27 | var computed = that.$options.computed || {};
28 | Object.keys(computed).forEach(function(key) {
29 | Object.defineProperty(that, key, {
30 | get: computed[key],
31 | set: function() {}
32 | });
33 | });
34 | }
35 | _initWatch() {
36 | var that = this;
37 | var watch = that.$options.watch;
38 | Object.keys(watch).forEach(function(key) {
39 | new Watch(that, key, watch[key]);
40 | })
41 | }
42 | }
--------------------------------------------------------------------------------
/js/Watch.js:
--------------------------------------------------------------------------------
1 | class Watch {
2 | constructor(vue, exp, cb) {
3 | this.vue = vue;
4 | this.exp = exp;
5 | this.cb = cb;
6 | Watcher = this; // 将当前实例watch放入到Watcher中,移动到这是为了防止update调用get时,反复向Dep依赖中添加
7 | this.value = this.get(); // 得到当前vue实例上对应表达式exp的最新的值
8 | Watcher = null; // 将Watcher置空,让给下一个值
9 | }
10 | get() {
11 | var exps = this.exp.split('.');
12 | var obj = this.vue;
13 | for (var i = 0, len = exps.length; i < len; i++) {
14 | if (!obj) return;
15 | obj = obj[exps[i]];
16 | }
17 | var value = obj;
18 | return value;
19 | }
20 | update() {
21 | let value = this.get();
22 | let oldVal = this.value;
23 | if(value !== oldVal) {
24 | this.value = value;
25 | this.cb.call(this.vue, value); // 将于此变量相关的回调函数全部执行掉
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------