├── .gitignore ├── .babelrc ├── src ├── index.js └── mog.js ├── package.json ├── LICENSE ├── webpack.config.js ├── index.html └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["es2015", { "modules": false }] 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Mog from './mog.js' 2 | 3 | let template = document.querySelector('#app').innerHTML 4 | 5 | let mog = new Mog({ 6 | template: template, 7 | el: '#app', 8 | data: { 9 | name: 'mog', 10 | lang: 'javascript', 11 | work: 'data binding', 12 | supports: ['String', 'Array', 'Object'], 13 | info: { 14 | author: 'Jrain', 15 | jsVersion: 'Ecma2015' 16 | }, 17 | motto: 'Every dog has his day' 18 | } 19 | }) 20 | 21 | document.querySelector('#set-motto').oninput = (e) => { 22 | mog.$setData(mog.$data, ($d) => { 23 | $d.motto = e.target.value 24 | }) 25 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easy-es6", 3 | "description": "A scion project", 4 | "author": "Jrain Lau ", 5 | "private": true, 6 | "scripts": { 7 | "dev": "cross-env NODE_ENV=development webpack-dev-server --open --inline --hot", 8 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules" 9 | }, 10 | "devDependencies": { 11 | "babel-core": "^6.0.0", 12 | "babel-loader": "^6.0.0", 13 | "babel-preset-es2015": "^6.0.0", 14 | "cross-env": "^3.0.0", 15 | "webpack": "^2.1.0-beta.25", 16 | "webpack-dev-server": "^2.1.0-beta.0" 17 | }, 18 | "dependencies": { 19 | "deep-diff": "^0.3.4" 20 | } 21 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 JrainLau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | 4 | module.exports = { 5 | entry: './src/index.js', 6 | output: { 7 | path: path.resolve(__dirname, './dist'), 8 | publicPath: '/dist/', 9 | filename: 'build.js' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.js$/, 15 | loader: 'babel', 16 | exclude: /node_modules/ 17 | } 18 | ] 19 | }, 20 | devServer: { 21 | historyApiFallback: true, 22 | noInfo: true 23 | }, 24 | devtool: '#eval-source-map' 25 | } 26 | 27 | if (process.env.NODE_ENV === 'production') { 28 | module.exports.devtool = '#source-map' 29 | // http://vue-loader.vuejs.org/en/workflow/production.html 30 | module.exports.plugins = (module.exports.plugins || []).concat([ 31 | new webpack.DefinePlugin({ 32 | 'process.env': { 33 | NODE_ENV: '"production"' 34 | } 35 | }), 36 | new webpack.optimize.UglifyJsPlugin({ 37 | compress: { 38 | warnings: false 39 | } 40 | }), 41 | new webpack.LoaderOptionsPlugin({ 42 | minimize: true 43 | }) 44 | ]) 45 | } 46 | -------------------------------------------------------------------------------- /src/mog.js: -------------------------------------------------------------------------------- 1 | export default class Mog { 2 | constructor (options) { 3 | this.$data = options.data 4 | this.$el = options.el 5 | this.$tpl = options.template 6 | this._render(this.$tpl, this.$data) 7 | } 8 | 9 | $setData (dataObj, fn) { 10 | let self = this 11 | let once = false 12 | let $d = new Proxy(dataObj, { 13 | set (target, property, value) { 14 | if (!once) { 15 | target[property] = value 16 | once = true 17 | self._render(self.$tpl, self.$data) 18 | } 19 | return true 20 | } 21 | }) 22 | fn($d) 23 | } 24 | 25 | _render (tplString, data) { 26 | document.querySelector(this.$el).innerHTML = this._replaceFun(tplString, data) 27 | } 28 | 29 | _replaceFun(str, data) { 30 | let self = this 31 | return str.replace(/{{([^{}]*)}}/g, (a, b) => { 32 | let r = self._getObjProp(data, b); 33 | console.log(a, b, r) 34 | if (typeof r === 'string' || typeof r === 'number') { 35 | return r 36 | } else { 37 | return self._getObjProp(r, b.split('.')[1]) 38 | } 39 | }) 40 | } 41 | 42 | _getObjProp (obj, propsName) { 43 | let propsArr = propsName.split('.') 44 | function rec(o, pName) { 45 | if (!o[pName] instanceof Array && o[pName] instanceof Object) { 46 | return rec(o[pName], propsArr.shift()) 47 | } 48 | return o[pName] 49 | } 50 | return rec(obj, propsArr.shift()) 51 | } 52 | 53 | } 54 | 55 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | easy-es6 6 | 40 | 41 | 42 |
43 |

44 | Hello everyone, my name is {{name}}, I am a mini {{lang}} framework for just {{work}}. I can bind data from {{supports.0}}, {{supports.1}} and {{supports.2}}. What's more, I was created by {{info.author}}, and was written in {{info.jsVersion}}. My motto is "{{motto}}". 45 |

46 |
47 |
48 | Motto: 49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Learn about data binding 2 | --- 3 | I am a frontend egineer, and I've been through too many cases about "data binding". In the early days, I use `jQuery` to do so. However, everytime the data changes, I can only bind it with dom manually, if the number of data is huge, that would be very painful -- all the pain ends till I met `VueJS`. 4 | 5 | One of the sell point about `VueJS` is "data binding". Users doesn't need to care about how the data bind to the dom, but just focus on the data, because `VueJS` would do it automatically. 6 | 7 | Amazing, isn't it? I fell in love with `VueJS` quickly, and used it in my projects. After days, I've been familiar with its usage, and I would like to know the deep of it. 8 | 9 | ## How dose `VueJS` do the data binding? 10 | By looking through the official document, I found that 11 | > When you pass a plain JavaScript object to a Vue instance as its data option, Vue will walk through all of its properties and convert them to getter/setters using Object.defineProperty. 12 | 13 | The keyword is `Object.definProperty`. In the [MDN document](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty), it says 14 | > The Object.defineProperty() method defines a new property directly on an object, or modifies an existing property on an object, and returns the object. 15 | 16 | Let's make an example to test it. 17 | 18 | First, create an Iron Man with a few properties: 19 | ``` 20 | let ironman = { 21 | name: 'Tony Stark', 22 | sex: 'male', 23 | age: '35' 24 | } 25 | ``` 26 | Now, let's using `Object.defineProperty()` to change on of his property, and show out the changes from the console. 27 | 28 | ``` 29 | Object.defineProperty(ironman, 'age', { 30 | set (val) { 31 | console.log(`Set age to ${val}`) 32 | return val 33 | } 34 | }) 35 | ``` 36 | When I change his age, I would see the log: 37 | ``` 38 | ironman.age = '48' 39 | // --> Set age to 48 40 | ``` 41 | 42 | Seems perfect, and if you change `console.log(val)` to `element.innerHTML = val`, the data binding would be done directlly, right? 43 | 44 | Let's change Iron Man's properties a little bit: 45 | ``` 46 | let ironman = { 47 | name: 'Tony Stark', 48 | sex: 'male', 49 | age: '35', 50 | hobbies: ['girl', 'money', 'game'] 51 | } 52 | ``` 53 | Yes, he's actually a playboy. Now, I would like to add some hobbies to him, and I want to see the console output. 54 | ``` 55 | Object.defineProperty(ironman.hobbies, 'push', { 56 | value () { 57 | console.log(`Push ${arguments[0]} to ${this}`) 58 | this[this.length] = arguments[0] 59 | } 60 | }) 61 | 62 | ironman.hobbies.push('wine') 63 | console.log(ironman.hobbies) 64 | 65 | // --> Push wine to girl,money,game 66 | // --> [ 'girl', 'money', 'game', 'wine' ] 67 | ``` 68 | In the previous moment, I used `get()` to watch the object's property changes, but to an array, we can't use `get()` to watch its properties, but use `value()` instead. Though it works, but not best. Are there any idea to simplify the way that how to track the changes of an object or an array? 69 | 70 | ## In ECMA2015, `Proxy` is a good idea 71 | What's `Proxy`? In the [MDN document](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy), it says: 72 | > The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc). 73 | 74 | `Proxy` is a new feature in ECMA2015, it's powerfull and usefull. Today I won't talk too much about it, but only one usefull usage of it. Now let's create a proxy: 75 | ``` 76 | let ironmanProxy = new Proxy(ironman, { 77 | set (target, property, value) { 78 | target[property] = value 79 | console.log('change....') 80 | return true 81 | } 82 | }) 83 | 84 | ironmanProxy.age = '48' 85 | console.log(ironman.age) 86 | 87 | // --> change.... 88 | // --> 48 89 | ``` 90 | It works as exspect. What about `Array`? 91 | ``` 92 | let ironmanProxy = new Proxy(ironman.hobbies, { 93 | set (target, property, value) { 94 | target[property] = value 95 | console.log('change....') 96 | return true 97 | } 98 | }) 99 | 100 | ironmanProxy.push('wine') 101 | console.log(ironman.hobbies) 102 | 103 | // --> change... 104 | // --> change... 105 | // --> [ 'girl', 'money', 'game', 'wine' ] 106 | ``` 107 | It works! But why does it output `change...` twice? The reason is once I trigger function `push()`, both `length` and `body` of this array will be changed. 108 | 109 | ## Real time data binding 110 | Dealing with the core problem of data binding, we can think about the other problems. 111 | 112 | Consider about a template and a data object: 113 | ``` 114 | 115 |

Hello, my name is {{name}}, I enjoy eatting {{hobbies.food}}

116 | 117 | 118 | let ironman = { 119 | name: 'Tony Stark', 120 | sex: 'male', 121 | age: '35', 122 | hobbies: { 123 | food: 'banana', 124 | drink: 'wine' 125 | } 126 | } 127 | ``` 128 | From the code above, we know that if you want to track an object's properties' changing, you should set it as the first param to the `Proxy` instance. Let's create a function to return new `Proxy` instance: 129 | ``` 130 | $setData (dataObj, fn) { 131 | let self = this 132 | let once = false 133 | let $d = new Proxy(dataObj, { 134 | set (target, property, value) { 135 | if (!once) { 136 | target[property] = value 137 | once = true 138 | /* Do something here */ 139 | } 140 | return true 141 | } 142 | }) 143 | fn($d) 144 | } 145 | ``` 146 | 147 | And use it like below: 148 | ``` 149 | $setData(dataObj, ($d) => { 150 | /* 151 | * dataObj.someProps = something 152 | */ 153 | }) 154 | 155 | // or 156 | 157 | $setData(dataObj.arrayProps, ($d) => { 158 | /* 159 | * dataObj.push(something) 160 | */ 161 | }) 162 | ``` 163 | 164 | What's more, we would like the template string point to the data object, then the string like `{{name}}` could be replaced by `Tony Stark`. 165 | ``` 166 | replaceFun(str, data) { 167 | let self = this 168 | return str.replace(/{{([^{}]*)}}/g, (a, b) => { 169 | return data[b] 170 | }) 171 | } 172 | 173 | replaceFun('My name is {{name}}', { name: 'xxx' }) 174 | // --> My name is xxx 175 | ``` 176 | This function works well with monolayer properties like `{ name: 'xx', age: 18 }`, but can't work with multi properties like `{ hobbies: { food: 'apple', drink: 'milk' } }`. 177 | 178 | For example, if the template string is `{{hobbies.food}}`, the code inside `replaceFun()` should return `data['hobbies']['food']`. 179 | 180 | ``` 181 | getObjProp (obj, propsName) { 182 | let propsArr = propsName.split('.') 183 | function rec(o, pName) { 184 | if (!o[pName] instanceof Array && o[pName] instanceof Object) { 185 | return rec(o[pName], propsArr.shift()) 186 | } 187 | return o[pName] 188 | } 189 | return rec(obj, propsArr.shift()) 190 | } 191 | 192 | getObjProp({ data: { hobbies: { food: 'apple', drink: 'milk' } } }, 'hobbies.food') 193 | // --> return { food: 'apple', drink: 'milk' } 194 | ``` 195 | And the final `replaceFun()` should be like this: 196 | ``` 197 | replaceFun(str, data) { 198 | let self = this 199 | return str.replace(/{{([^{}]*)}}/g, (a, b) => { 200 | let r = self._getObjProp(data, b); 201 | console.log(a, b, r) 202 | if (typeof r === 'string' || typeof r === 'number') { 203 | return r 204 | } else { 205 | return self._getObjProp(r, b.split('.')[1]) 206 | } 207 | }) 208 | } 209 | ``` 210 | 211 | ## A data binding instance, names "Mog" 212 | No why, just name it "Mog". 213 | ``` 214 | class Mog { 215 | constructor (options) { 216 | this.$data = options.data 217 | this.$el = options.el 218 | this.$tpl = options.template 219 | this._render(this.$tpl, this.$data) 220 | } 221 | 222 | $setData (dataObj, fn) { 223 | let self = this 224 | let once = false 225 | let $d = new Proxy(dataObj, { 226 | set (target, property, value) { 227 | if (!once) { 228 | target[property] = value 229 | once = true 230 | self._render(self.$tpl, self.$data) 231 | } 232 | return true 233 | } 234 | }) 235 | fn($d) 236 | } 237 | 238 | _render (tplString, data) { 239 | document.querySelector(this.$el).innerHTML = this._replaceFun(tplString, data) 240 | } 241 | 242 | _replaceFun(str, data) { 243 | let self = this 244 | return str.replace(/{{([^{}]*)}}/g, (a, b) => { 245 | let r = self._getObjProp(data, b); 246 | console.log(a, b, r) 247 | if (typeof r === 'string' || typeof r === 'number') { 248 | return r 249 | } else { 250 | return self._getObjProp(r, b.split('.')[1]) 251 | } 252 | }) 253 | } 254 | 255 | _getObjProp (obj, propsName) { 256 | let propsArr = propsName.split('.') 257 | function rec(o, pName) { 258 | if (!o[pName] instanceof Array && o[pName] instanceof Object) { 259 | return rec(o[pName], propsArr.shift()) 260 | } 261 | return o[pName] 262 | } 263 | return rec(obj, propsArr.shift()) 264 | } 265 | 266 | } 267 | ``` 268 | Usage: 269 | ``` 270 | 271 | 272 |
273 |

274 | Hello everyone, my name is {{name}}, I am a mini {{lang}} framework for just {{work}}. I can bind data from {{supports.0}}, {{supports.1}} and {{supports.2}}. What's more, I was created by {{info.author}}, and was written in {{info.jsVersion}}. My motto is "{{motto}}". 275 |

276 |
277 |
278 | Motto: 279 |
280 | ``` 281 | 282 | ``` 283 | 284 | 285 | let template = document.querySelector('#app').innerHTML 286 | 287 | let mog = new Mog({ 288 | template: template, 289 | el: '#app', 290 | data: { 291 | name: 'mog', 292 | lang: 'javascript', 293 | work: 'data binding', 294 | supports: ['String', 'Array', 'Object'], 295 | info: { 296 | author: 'Jrain', 297 | jsVersion: 'Ecma2015' 298 | }, 299 | motto: 'Every dog has his day' 300 | } 301 | }) 302 | 303 | document.querySelector('#set-motto').oninput = (e) => { 304 | mog.$setData(mog.$data, ($d) => { 305 | $d.motto = e.target.value 306 | }) 307 | } 308 | ``` 309 | 310 | You can play it [HERE](http://codepen.io/jrainlau/pen/YpyBBY) 311 | 312 | ## What's more... 313 | `Mog` is only an experiment project for learning data binding, it's not gracefull or functional enough. But this little toy helps me learnt a lot. If you are interest in it, you could fork it from [HERE](https://github.com/jrainlau/mog) and play with your idea. 314 | 315 | Thanks for reading! 316 | --------------------------------------------------------------------------------