├── .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 | ![双向绑定结果](https://github.com/jackiewillen/blog/blob/master/images/%E5%8F%8C%E5%90%91%E7%BB%91%E5%AE%9A.gif?raw=true) 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 | ![双向绑定结果](https://github.com/jackiewillen/blog/blob/master/images/%E5%8F%8C%E5%90%91%E7%BB%91%E5%AE%9A.gif?raw=true) 211 | 212 | 怎么样,搞定了,其实,这只是Vue的冰山一角(下图中的绿色框框的部分),在这个仓库中还实现了一系列vue的功能,如果你有兴趣可以一个commit一个commit的往上看,每个commit都只实现一个完整的细小的功能,而且代码量都尽可能的少,你如果想看一定能看懂。这仓库都是没有使用虚拟DOM来实现,更新颗粒度细,现在的Vue降低了更新的颗粒度,用了虚拟DOM,但是Vue中双向绑定的原理始终未变,所以这篇文章还是需要看懂的,老弟。以后有时间我再研究研究虚拟DOM写个仓库。 213 | 214 | ![123](https://github.com/jackiewillen/blog/blob/master/images/%E5%B0%8F%E5%9E%8Bvue.png?raw=true) 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 | } --------------------------------------------------------------------------------