├── js ├── watcher.js ├── observer.js ├── index.js └── compile.js ├── index.html └── readme.md /js/watcher.js: -------------------------------------------------------------------------------- 1 | function Watcher(vm, exp, cb) { 2 | this.cb = cb; 3 | this.vm = vm; 4 | this.exp = exp; 5 | this.value = this.get(); // 将自己添加到订阅器的操作 6 | } 7 | 8 | Watcher.prototype = { 9 | update: function() { 10 | this.run(); 11 | }, 12 | run: function() { 13 | var value; 14 | if (this.exp.indexOf('.') !== -1) { 15 | var keyArr = this.exp.split('.') 16 | var obj = this.vm.data; 17 | for (var i=0; i 2 | 3 | 4 | 5 | my-vue 6 | 7 | 18 | 19 |
20 |

{{title.msg.text}}

21 | 22 | 23 |

{{name}}

24 | 30 | 31 |
32 | 33 | 34 | 35 | 36 | 37 | 62 | 63 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | function _Vue (options) { 2 | var self = this; 3 | this.data = options.data; 4 | this.methods = options.methods; 5 | 6 | Object.keys(this.data).forEach(function(key) { 7 | self.proxyKeys(key); 8 | if (Array.isArray(self.data[key])) { 9 | self.mutationMethod(key) 10 | } 11 | }); 12 | 13 | observe(this.data); 14 | new Compile(options.el, this); 15 | } 16 | 17 | _Vue.prototype = { 18 | // 19 | proxyKeys: function (key) { 20 | var self = this; 21 | if (typeof key == 'object') { 22 | for (var child in key) 23 | this.proxyKeys(child) 24 | } 25 | Object.defineProperty(this, key, { 26 | enumerable: false, 27 | configurable: true, 28 | get: function getter () { 29 | return self.data[key]; 30 | }, 31 | set: function setter (newVal) { 32 | self.data[key] = newVal; 33 | } 34 | }); 35 | }, 36 | mutationMethod: function (key) { 37 | var self = this; 38 | const aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']; 39 | const arrayAugmentations = []; 40 | aryMethods.forEach((method)=> { 41 | // 这里是原生Array的原型方法 42 | let original = Array.prototype[method]; 43 | 44 | // 将push, pop等封装好的方法定义在对象arrayAugmentations的属性上 45 | // 注意:是属性而非原型属性 46 | arrayAugmentations[method] = function () { 47 | var result = original.apply(this, arguments) 48 | var copyArr = this.slice(0); 49 | copyArr.__proto__ = arrayAugmentations; 50 | 51 | console.log('数组变动了!') 52 | self.data[key] = copyArr 53 | 54 | return result; 55 | }; 56 | }); 57 | this.data[key].__proto__ = arrayAugmentations; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ### 学习vue的双向绑定 2 | 自己动手实现一个简单的todo-list 3 | 4 | > 预览地址: https://fatdong1.github.io/todo-list/index.html 5 | ### 参考链接 6 | - https://github.com/DMQ/mvvm 7 | - https://github.com/youngwind/blog 8 | 9 | ### 预览效果 10 | ![todo-list](https://segmentfault.com/img/bVR7my?w=408&h=460) 11 | 12 | ### 数据代理 13 | #### 1.简单介绍数据代理 14 | 正常情况下,我们都会把数据写在data里面,如下面所示 15 | ```js 16 | var vm = new Vue({ 17 | el: '#app', 18 | data: { 19 | title: 'hello world' 20 | } 21 | methods: { 22 | changeTitle: function () { 23 | this.title = 'hello vue' 24 | } 25 | } 26 | }) 27 | console.log(vm.title) // 'hello world' or 'hello vue' 28 | ``` 29 | 如果没有`数据代理`,而我们又要修改data里面的title的话,methods里面的changeTitle只能这样修改成`this.data.title = 'hello vue'`, 下面的console也只能改成`console.log(vm.data.title)`,数据代理就是这样的功能。 30 | #### 2. 实现原理 31 | 通过遍历data里面的属性,将每个属性通过object.defineProperty()设置getter和setter,将data里面的每个属性都复制到与data同级的对象里。 32 | 33 | (对应上面的示例代码) 34 | 35 | ![clipboard.png](https://segmentfault.com/img/bVR7A2?w=444&h=412) 36 | 37 | 38 | 39 | 40 | 触发这里的getter将会触发data里面对应属性的getter,触发这里的setter将会触发data里面对应属性的setter,从而实现代理。实现代码如下: 41 | ```js 42 | var self = this; // this为vue实例, 即vm 43 | Object.keys(this.data).forEach(function(key) { 44 | Object.defineProperty(this, key, { // this.title, 即vm.title 45 | enumerable: false, 46 | configurable: true, 47 | get: function getter () { 48 | return self.data[key]; //触发对应data[key]的getter 49 | }, 50 | set: function setter (newVal) { 51 | self.data[key] = newVal; //触发对应data[key]的setter 52 | } 53 | }); 54 | } 55 | ``` 56 | 57 | > 对object.defineProperty不熟悉的小伙伴可以在[MDN的文档(链接)](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)学习一下 58 | ### 双向绑定 59 | - 数据变动 ---> 视图更新 60 | - 视图更新(input、textarea) --> 数据变动 61 | 62 | `视图更新 --> 数据变动`这个方向的绑定比较简单,主要通过事件监听来改变数据,比如input可以监听input事件,一旦触发input事件就改变data。下面主要来理解一下`数据变动--->视图更新`这个方向的绑定。 63 | #### 1. 数据劫持 64 | 不妨让我们自己思考一下,如何实现数据变动,对应绑定数据的视图就更新呢? 65 | 答案还是object.defineProperty,通过object.defineProperty遍历设置this.data里面所有属性,在每个属性的setter里面去通知对应的回调函数,这里的回调函数包括dom视图重新渲染的函数、使用$watch添加的回调函数等,这样我们就通过object.defineProperty劫持了数据,当我们对数据重新赋值时,如`this.title = 'hello vue'`,就会触发setter函数,从而触发dom视图重新渲染的函数,实现数据变动,对应视图更新。 66 | #### 2. 发布-订阅模式 67 | 那么问题来了,我们如何在setter里面触发所有绑定该数据的回调函数呢? 68 | 既然绑定该数据的回调函数不止一个,我们就把所有的回调函数放在一个数组里面,一旦触发该数据的setter,就遍历数组触发里面所有的回调函数,我们把这些回调函数称为`订阅者`。数组最好就定义在setter函数的最近的上级作用域中,如下面实例代码所示。 69 | ```js 70 | Object.keys(this.data).forEach(function(key) { 71 | var subs = []; // 在这里放置添加所有订阅者的数组 72 | Object.defineProperty(this.data, key, { // this.data.title 73 | enumerable: false, 74 | configurable: true, 75 | get: function getter () { 76 | console.log('访问数据啦啦啦') 77 | return this.data[key]; //返回对应数据的值 78 | }, 79 | set: function setter (newVal) { 80 | if (newVal === this.data[key]) { 81 | return; // 如果数据没有变动,函数结束,不执行下面的代码 82 | } 83 | this.data[key] = newVal; //数据重新赋值 84 | 85 | subs.forEach(function () { 86 | // 通知subs里面的所有的订阅者 87 | }) 88 | } 89 | }); 90 | } 91 | ``` 92 | 那么问题又来了,怎么把绑定数据的所有回调函数放到一个数组里面呢? 93 | 我们可以在getter里面做做手脚,我们知道只要访问数据就会触发对应数据的getter,那我们可以先设置一个全局变量target,如果我们要在data里面title属性添加一个订阅者(changeTitle函数),我们可以先设置target = changeTitle,把changeTitle函数缓存在target中,然后访问this.title去触发title的getter,在getter里面把target这个全局变量的值添加到subs数组里面,添加完成后再把全局变量target设置为null,以便添加其他订阅者。实例代码如下: 94 | ```js 95 | Object.keys(this.data).forEach(function(key) { 96 | var subs = []; // 在这里放置添加所有订阅者的数组 97 | Object.defineProperty(this.data, key, { // this.data.title 98 | enumerable: false, 99 | configurable: true, 100 | get: function getter () { 101 | console.log('访问数据啦啦啦') 102 | if (target) { 103 | subs.push(target); 104 | } 105 | return this.data[key]; //返回对应数据的值 106 | }, 107 | set: function setter (newVal) { 108 | if (newVal === this.data[key]) { 109 | return; // 如果数据没有变动,函数结束,不执行下面的代码 110 | } 111 | this.data[key] = newVal; //数据重新赋值 112 | 113 | subs.forEach(function () { 114 | // 通知subs里面的所有的订阅者 115 | }) 116 | } 117 | }); 118 | } 119 | ``` 120 | 上面的代码为了方便理解都是通过简化的,实际上我们把订阅者写成一个构造函数watcher,在实例化订阅者的时候去访问对应的数据,触发相应的getter,详细的代码可以阅读[DMQ的自己动手实现MVVM](https://github.com/DMQ/mvvm) 121 | 122 | #### 3. 模板解析 123 | 通过上面的两个步骤我们已经实现一旦数据变动,就会通知对应绑定数据的订阅者,接下来我们来简单介绍一个特殊的订阅者,也就是视图更新函数,几乎每个数据都会添加对应的视图更新函数,所以我们就来简单了解一下视图更新函数。 124 | 125 | 假如说有下面这一段代码,我们怎么把它解析成对应的html呢? 126 | ```html 127 | 128 |

{{title}}

129 |