├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── dist ├── mux.js └── mux.min.js ├── gulpfile.js ├── index.js ├── lib ├── array-hook.js ├── info.js ├── keypath.js ├── message.js ├── mux.js └── util.js ├── package.json └── test ├── index.dist.js ├── index.js ├── spec-base.js ├── spec-global-api.js ├── spec-instance-array.js ├── spec-instance-method.js ├── spec-options-deep.js └── spec-options.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | coverage.html -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | dist 2 | test 3 | coverage.html 4 | .travis.yml 5 | gulpfile.js -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.11' 4 | - '0.10' -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 guankaishe 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](http://switer.qiniudn.com/mux-verti.png?imageView/2/w/110) Muxjs 2 | =========== 3 | [![build](https://travis-ci.org/switer/muxjs.svg?branch=master)](https://travis-ci.org/switer/muxjs) 4 | [![Coverage Status](https://coveralls.io/repos/switer/muxjs/badge.svg?branch=master)](https://coveralls.io/r/switer/muxjs?branch=master) 5 | [![npm version](https://badge.fury.io/js/muxjs.svg)](http://badge.fury.io/js/muxjs) 6 | 7 | Using Muxjs is easy to track the app state. What's state and it's tansition? When look back your codes, often find some logic are described as below: 8 | > if this condition and this other condition are met, then this value should be 'x'. 9 | 10 | Here, *vars* of condition are state, and *condition* is transition, so *'x'* is the transposition's result. 11 | **Muxjs** give the way to subscribe *vars*'s changs and transition's result *'x'*'s changes. 12 | 13 | Let's look at the diagram of an example case of an stateful application: 14 | 15 | ![Case Diagram](http://switer.qiniudn.com/muxjs.png) 16 | 17 | `Left` of diagram is a **view-controller** with 5 state (circle) and 3 **transition** (rhombus).
18 | `Right` of disgram is an **UI Page** with 4 parts, each part depend on one state or transition. 19 | If we can subscribe all changes of state and transition, so we can bind specified DOM opertion when state/transition change, 20 | finally it implement the **data to DOM binding** , event can do more stuff for a completed **MVVM** framework such as [Zect](https://github.com/switer/Zect). 21 | It's usefull, right? 22 | 23 | ## Installation 24 | **browser:** 25 | - [mux.js](https://raw.githubusercontent.com/switer/muxjs/master/dist/mux.js) 26 | - [mux.min.js](https://raw.githubusercontent.com/switer/muxjs/master/dist/mux.min.js) (4.1k when gzipped) 27 | 28 | ```html 29 | 30 | ``` 31 | **node.js:** 32 | ```bash 33 | npm install muxjs --save 34 | ``` 35 | ## Examples: 36 | - [Zect](https://github.com/switer/Zect) Vue.js like 37 | - [virtual-dom-binding](https://github.com/switer/virtual-dom-binding) render view with virtual-dom 38 | - [data-dom-binding](https://github.com/switer/data-dom-binding) zepto do DOM maniputation 39 | 40 | ## API Reference 41 | - **[Gloabal API](#global-api)** 42 | - [Mux(\[props\])](#muxoptions) 43 | - [Mux.extend(\[options\])](#muxextendoptions) 44 | - [Mux.config(\[conf\])](#muxconfigconf) 45 | - [Mux.emitter(\[context\])](#muxemittercontext) 46 | - **[Instance Options](#instance-options)** 47 | - [props](#props) 48 | - [computed](#computed) 49 | - [emitter](#emitter) 50 | - **[Instance Methods](#instance-methods)** 51 | - [$set(\[keyPath, value\] | props)](#setkeypath-value--props) 52 | - [$get(propname)](#computed) 53 | - [$add(\[propname \[, defaultValue\]\] | propnameArray | propsObj)](#addpropname--defaultvalue--propnamearray--propsobj) 54 | - [$computed(\[propname, deps, get, set, enum\] | computedPropsObj)](#computedpropname-deps-get-set-enum--computedpropsobj) 55 | - [$watch(\[propname, \] callback)](#watchpropname--callback) 56 | - [$unwatch(\[propname, \] \[callback\])](#unwatchpropname--callback) 57 | - [$props( )](#props-) 58 | - [$destroy()](#destroy) 59 | - [$destroyed()](#destroyed) 60 | 61 | ## Wiki 62 | - [Deep observe](https://github.com/switer/muxjs/wiki/Deep-observe) 63 | - [The imperfection of "Object.defineProperty"](https://github.com/switer/muxjs/wiki/The-imperfection-of-%22Object.defineProperty%22) 64 | - [Compare defineproperties to looped defineproperty](https://github.com/switer/muxjs/wiki/Compare-defineproperties-to-looped-defineproperty) 65 | - [Can't observe array's indices](https://github.com/switer/muxjs/wiki/The-performance-problem-of-defineProperty-to-array-index) 66 | 67 | ### Global API 68 | ##### `Mux(options)` 69 | 70 | [ :bookmark: API Reference Navigation](#api-reference) 71 | 72 | It is a constructor function that allows you to create Mux instance.*`options`* see: [Instance Options](#instance-options). 73 | 74 | ```js 75 | var author = new Mux({ 76 | props: { 77 | name: 'firstName lastName' 78 | }, 79 | computed: { 80 | firstName: { 81 | deps: ['name'], 82 | fn: function () { 83 | return this.name.split(' ')[0] 84 | } 85 | } 86 | } 87 | }) 88 | assert.equal(author.firstName, 'firstName') 89 | ``` 90 | 91 | ##### `Mux.extend([options])` 92 | - Return: `Function` Class 93 | 94 | [ :bookmark: API Reference Navigation](#api-reference) 95 | 96 | Create a *subclass* of the base Mux constructor. *`options`* see: [Instance Options](#instance-options). 97 | 98 | *Class* can instance with param `propsObj` which will set values to those observered properties of the instance. 99 | 100 | ```js 101 | var Person = Mux.extend({ 102 | props: { 103 | profession: 'programer', 104 | name: '' 105 | } 106 | }) 107 | var author = new Person({ 108 | name: 'switer' 109 | }) 110 | assert.equal(author.profession, 'programer') 111 | assert.equal(author.name, 'switer') 112 | ``` 113 | 114 | ##### `Mux.config([conf])` 115 | 116 | [ :bookmark: API Reference Navigation](#api-reference) 117 | 118 | Global configure. Currently supported configurations: 119 | * warn `Boolean` if value is `false`, don't show any warning log. **Default** is `true` 120 | 121 | ```js 122 | Mux.config({ 123 | warn: false // no warning log 124 | }) 125 | ``` 126 | 127 | ##### `Mux.emitter([context])` 128 | - Params: 129 | * context `Object` binding "this" to `context` for event callbacks. 130 | 131 | [ :bookmark: API Reference Navigation](#api-reference) 132 | 133 | Create a emitter instance. 134 | 135 | ```js 136 | var emitter = Mux.emitter() 137 | emitter.on('change:name', function (name) { 138 | // do something 139 | }) // subscribe 140 | emitter.off('change:name') // unsubscribe 141 | emitter.emitter('change:name', 'switer') // publish message 142 | ``` 143 | 144 | ### Instance Options 145 | ##### `props` 146 | - Type: ` Function` | `Object` 147 | 148 | [ :bookmark: API Reference Navigation](#api-reference) 149 | 150 | Return the initial observed property object for this mux instance.Recommend to using function which return 151 | a object if you don't want to share **props** option's object in each instance: 152 | 153 | ```js 154 | var Person = Mux.extend({ 155 | props: function () { 156 | return { 157 | name: 'mux' 158 | } 159 | } 160 | }) 161 | assert.equal((new person).name, 'mux') 162 | ``` 163 | **props** option could be an object: 164 | 165 | ```js 166 | var Person = new Mux({ 167 | props: { 168 | name: 'mux' 169 | } 170 | }) 171 | assert.equal((new person).name, 'mux') 172 | ``` 173 | 174 | ##### `computed` 175 | - Type: ` Object` 176 | - Options: 177 | - **deps** `Array` property dependencies. 178 | *Restricton:* *`deps`*'s item could be keyPath (contain `.` and `[]`, such as: "post.comments[0]"). 179 | - **fn** `Function` Compute function , using as a getter 180 | - **enum** `Boolean` Whether the computed property enumerable or not 181 | 182 | [ :bookmark: API Reference Navigation](#api-reference) 183 | 184 | Computed properties definition option. `"fn"` will be called if one of dependencies has change, then will emit a change event if `"fn"` returns result has change. 185 | 186 | ```js 187 | var mux = new Mux({ 188 | props: { 189 | items: [1,2,3] 190 | }, 191 | computed: { 192 | count: { 193 | deps: ['items'], 194 | fn: function () { 195 | return this.items.length 196 | } 197 | } 198 | } 199 | }) 200 | assert.equal(mux.cout, 3) 201 | ``` 202 | Watch computed property changes: 203 | 204 | ```js 205 | mux.$watch('count', function (next, pre) { 206 | assert.equal(next, 3) 207 | assert.equal(next, 4) 208 | }) 209 | mux.items.push(4) 210 | assert.equal(mux.count, 4) 211 | ``` 212 | 213 | ##### `emitter` 214 | - Type: ` EventEmitter` 215 | 216 | [ :bookmark: API Reference Navigation](#api-reference) 217 | 218 | Use custom emitter instance. 219 | ```js 220 | var emitter = Mux.emitter() 221 | emitter.on('change:name', function (next) { 222 | next // --> switer 223 | }) 224 | var mux = new Mux({ 225 | emitter: emitter, 226 | props: { 227 | name: '' 228 | } 229 | }) 230 | mux.name = 'switer' 231 | ``` 232 | 233 | ### Instance Methods 234 | ##### `$set([keyPath, value] | props)` 235 | * Params: 236 | - **keyPath** `String` property path , such as: *"items[0].name"* 237 | - **value** *[optional]* 238 | - *or* 239 | - **props** `Object` *[optional]* data structure as below: 240 | ```js 241 | { "propertyName | keyPath": propertyValue } 242 | ``` 243 | * Return: **this** 244 | 245 | [ :bookmark: API Reference Navigation](#api-reference) 246 | 247 | Set value to property by property's keyPath or propertyName, which could trigger change event when value change or value is an object reference (instanceof Object). 248 | **Notice:** PropertyName shouldn't a keyPath (name string without contains *"[", "]", "."* ) 249 | 250 | ```js 251 | var list = new Mux({ 252 | items: [{name: '1'}] 253 | }) 254 | list.$set('items[0].name', '') 255 | ``` 256 | 257 | ##### `$get(propname)` 258 | * Params: 259 | - **propname** `String` only propertyname not keyPath (without contains "[", "]", ".") 260 | * Return: *value* 261 | 262 | [ :bookmark: API Reference Navigation](#api-reference) 263 | 264 | Get property value. It's equal to using "." or "[]" to access value except computed properties. 265 | 266 | ```js 267 | var mux = new Mux({ 268 | props: { 269 | replyUsers: [{ 270 | author: 'switer' 271 | }] 272 | } 273 | }) 274 | assert.equal(mux.$get('replyUses[0].author', 'switer')) 275 | ``` 276 | 277 | **Notice:** Using "." or "[]" to access computed property's value will get a cached result, 278 | so you can use "$get()" to recompute the property's value whithout cache. 279 | 280 | ```js 281 | // define a computed property which use to get the first user of replyUsers 282 | mux.$computed(firstReplyUser, ['replyUsers'], function () { 283 | return this.replyUsers[0].author 284 | }) 285 | 286 | var users = [{ 287 | author: 'switer' 288 | }] 289 | 290 | mux.$set('replyUsers', users) 291 | 292 | user[0].author = 'guankaishe' // modify selft 293 | 294 | assert.equal(post.firstReplyUser, 'switer')) 295 | assert.equal(post.$get('firstReplyUser'), 'guankaishe')) 296 | ``` 297 | 298 | ##### `$add([propname [, defaultValue]] | propnameArray | propsObj)` 299 | * Params: 300 | - **propname** `String` 301 | - **defaultValue** *[optional]* 302 | - *or* 303 | - **propnameArray** `Array` 304 | - *or* 305 | - **propsObj** `Object` 306 | * Return: **this** 307 | 308 | [ :bookmark: API Reference Navigation](#api-reference) 309 | 310 | Define an observerable property or multiple properties. 311 | ```js 312 | mux.$add('name', 'switer') 313 | // or 314 | mux.$add(['name']) // without default value 315 | // or 316 | mux.$add({ 'name': 'switer' }) 317 | ``` 318 | 319 | ##### `$computed([propname, deps, get, set, enum] | computedPropsObj)` 320 | * Params: 321 | - **propname** `String` property name 322 | - **deps** `Array` Property's dependencies 323 | - **get** `Function` Getter function 324 | - **set** `Function` Setter function 325 | - **enum** `Boolean` whether the computed property enumerable or not 326 | - *or* 327 | - **computedPropsObj** `Object` 328 | * Return: **this** 329 | 330 | [ :bookmark: API Reference Navigation](#api-reference) 331 | 332 | Define a computed property. *deps* and *fn* is necessary. If one of **deps** is observable of the instance, emitter a change event after define. 333 | *computedPropsObj* is using to define multiple computed properties in once, 334 | each key of *computedPropsObj* is property's name and value is a object contains "deps", "fn". 335 | Usage as below: 336 | 337 | ```js 338 | // before define computed 339 | assert.equal(mux.commentCount, undefined) 340 | mux.$computed('commentCount', ['comments'], function () { 341 | return this.comments.length 342 | }) 343 | // after define computed 344 | assert.equal(mux.commentCount, 1) 345 | ``` 346 | 347 | ##### `$watch([propname, ] callback)` 348 | * Params: 349 | - **propname** `String` *[optional]* 350 | - **callback** `Function` 351 | * Return: `Function` unwatch handler 352 | 353 | [ :bookmark: API Reference Navigation](#api-reference) 354 | 355 | Subscribe property or computed property changes of the Mux instance. 356 | 357 | 358 | ```js 359 | var unwatch = mux.$watch('title', function (nextValue, preValue) { 360 | // callback when title has change 361 | }) 362 | unwatch() // cancel subscribe 363 | ``` 364 | if *propname* is not present, will watch all property or computed property changes: 365 | 366 | ```js 367 | mux.$watch(function (propname, nextValue, preValue) { 368 | // callback when title has change 369 | }) 370 | ``` 371 | 372 | 373 | ##### `$unwatch([propname, ] [callback])` 374 | * Params: 375 | - **propname** `String` *[optional]* 376 | - **callback** `Function` *[optional]* 377 | * Return: **this** 378 | 379 | [ :bookmark: API Reference Navigation](#api-reference) 380 | 381 | Unsubscribe property or computed property changes of the Mux instance. 382 | ```js 383 | // subscribe 384 | mux.$watch('name', handler) 385 | // unsubscribe 386 | mux.$unwatch('name', handler) 387 | // unsubscribe all of specified propertyname 388 | mux.$unwatch('name') 389 | // unsubscribe all of the Mux instance 390 | mux.$unwatch() 391 | ``` 392 | 393 | ##### `$props( )` 394 | * Return: `Object` 395 | 396 | [ :bookmark: API Reference Navigation](#api-reference) 397 | 398 | Return all properties of the instance. Properties do not contain computed properties(Only observed properties). 399 | ```js 400 | var mux = Mux({ props: { name: 'Muxjs' } }) 401 | mux.$props() // --> {name: 'Muxjs'} 402 | ``` 403 | 404 | ##### `$emitter(emitter)` 405 | * Return: **this** | **emitter** 406 | 407 | [ :bookmark: API Reference Navigation](#api-reference) 408 | 409 | Reset emitter of the mux instance. If arguments is empty, return the emitter of the instance. 410 | ```js 411 | var mux = Mux() 412 | var em = Mux.emitter() 413 | mux.$emitter(em) 414 | 415 | var muxEmitter = mux.$emitter() // equal to em 416 | ``` 417 | 418 | ##### `$destroy()` 419 | 420 | [ :bookmark: API Reference Navigation](#api-reference) 421 | 422 | Destroy the instance, remove all listener of the internal emiter of the instance, free all props references. 423 | 424 | 425 | ##### `$destroyed()` 426 | * Return: **Boolean** 427 | 428 | [ :bookmark: API Reference Navigation](#api-reference) 429 | 430 | Whether the instance is destroyed or not. 431 | 432 | ## Changgelog 433 | 434 | - 2016/10/14 435 | + $set() will not return this. 436 | + $set({ ... }) will emit in batch 437 | 438 | 439 | ## License 440 | 441 | MIT -------------------------------------------------------------------------------- /dist/mux.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mux.js v2.4.18 3 | * (c) 2014 guankaishe 4 | * Released under the MIT License. 5 | */ 6 | (function webpackUniversalModuleDefinition(root, factory) { 7 | if(typeof exports === 'object' && typeof module === 'object') 8 | module.exports = factory(); 9 | else if(typeof define === 'function' && define.amd) 10 | define(factory); 11 | else if(typeof exports === 'object') 12 | exports["Mux"] = factory(); 13 | else 14 | root["Mux"] = factory(); 15 | })(this, function() { 16 | return /******/ (function(modules) { // webpackBootstrap 17 | /******/ // The module cache 18 | /******/ var installedModules = {}; 19 | 20 | /******/ // The require function 21 | /******/ function __webpack_require__(moduleId) { 22 | 23 | /******/ // Check if module is in cache 24 | /******/ if(installedModules[moduleId]) 25 | /******/ return installedModules[moduleId].exports; 26 | 27 | /******/ // Create a new module (and put it into the cache) 28 | /******/ var module = installedModules[moduleId] = { 29 | /******/ exports: {}, 30 | /******/ id: moduleId, 31 | /******/ loaded: false 32 | /******/ }; 33 | 34 | /******/ // Execute the module function 35 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 36 | 37 | /******/ // Flag the module as loaded 38 | /******/ module.loaded = true; 39 | 40 | /******/ // Return the exports of the module 41 | /******/ return module.exports; 42 | /******/ } 43 | 44 | 45 | /******/ // expose the modules object (__webpack_modules__) 46 | /******/ __webpack_require__.m = modules; 47 | 48 | /******/ // expose the module cache 49 | /******/ __webpack_require__.c = installedModules; 50 | 51 | /******/ // __webpack_public_path__ 52 | /******/ __webpack_require__.p = ""; 53 | 54 | /******/ // Load entry module and return exports 55 | /******/ return __webpack_require__(0); 56 | /******/ }) 57 | /************************************************************************/ 58 | /******/ ([ 59 | /* 0 */ 60 | /***/ function(module, exports, __webpack_require__) { 61 | 62 | 'use strict'; 63 | 64 | module.exports = __webpack_require__(1) 65 | 66 | 67 | /***/ }, 68 | /* 1 */ 69 | /***/ function(module, exports, __webpack_require__) { 70 | 71 | 'use strict'; 72 | 73 | /** 74 | * External module's name startof "$" 75 | */ 76 | var $Message = __webpack_require__(2) 77 | var $keypath = __webpack_require__(3) 78 | var $arrayHook = __webpack_require__(4) 79 | var $info = __webpack_require__(5) 80 | var $util = __webpack_require__(6) 81 | var $normalize = $keypath.normalize 82 | var $join = $keypath.join 83 | var $type = $util.type 84 | var $indexOf = $util.indexOf 85 | var $hasOwn = $util.hasOwn 86 | var $warn = $info.warn 87 | 88 | /** 89 | * CONTS 90 | */ 91 | var STRING = 'string' 92 | var ARRAY = 'array' 93 | var OBJECT = 'object' 94 | var FUNCTION = 'function' 95 | var CHANGE_EVENT = 'change' 96 | 97 | var _id = 0 98 | function allotId() { 99 | return _id ++ 100 | } 101 | 102 | /** 103 | * Mux model constructor 104 | * @public 105 | */ 106 | function Mux(options) { 107 | // static config checking 108 | options = options || {} 109 | Ctor.call(this, options) 110 | } 111 | 112 | /** 113 | * Mux model creator 114 | * @public 115 | */ 116 | Mux.extend = function(options) { 117 | return MuxFactory(options || {}) 118 | } 119 | 120 | /** 121 | * Mux global config 122 | * @param conf 123 | */ 124 | Mux.config = function (conf) { 125 | if (conf.warn === false) $info.disable() 126 | else $info.enable() 127 | } 128 | 129 | /** 130 | * Create a emitter instance 131 | * @param `Optional` context use for binding "this" 132 | */ 133 | Mux.emitter = function (context) { 134 | return new $Message(context) 135 | } 136 | 137 | /** 138 | * Expose Keypath API 139 | */ 140 | Mux.keyPath = $keypath 141 | Mux.utils = $util 142 | 143 | /** 144 | * Mux model factory 145 | * @private 146 | */ 147 | function MuxFactory(options) { 148 | 149 | function Class (receiveProps) { 150 | Ctor.call(this, options, receiveProps) 151 | } 152 | Class.prototype = Object.create(Mux.prototype) 153 | return Class 154 | } 155 | /** 156 | * Mux's model class, could instance with "new" operator or call it directly. 157 | * @param receiveProps initial props set to model which will no trigger change event. 158 | */ 159 | function Ctor(options, receiveProps) { 160 | var model = this 161 | var emitter = options.emitter || new $Message(model) // EventEmitter of this model, context bind to model 162 | var _emitter = options._emitter || new $Message(model) 163 | var _computedCtx = $hasOwn(options, 'computedContext') ? options.computedContext : model 164 | var __kp__ = $keypath.normalize(options.__kp__ || '') 165 | var __muxid__ = allotId() 166 | var _isExternalEmitter = !!options.emitter 167 | var _isExternalPrivateEmitter = !!options._emitter 168 | var _destroy // interanl destroyed flag 169 | var _privateProperties = {} 170 | 171 | _defPrivateProperty('__muxid__', __muxid__) 172 | _defPrivateProperty('__kp__', __kp__) 173 | /** 174 | * return current keypath prefix of this model 175 | */ 176 | function _rootPath () { 177 | return __kp__ || '' 178 | } 179 | 180 | /** 181 | * define priavate property of the instance object 182 | */ 183 | function _defPrivateProperty(name, value) { 184 | if (instanceOf(value, Function)) value = value.bind(model) 185 | _privateProperties[name] = value 186 | $util.def(model, name, { 187 | enumerable: false, 188 | value: value 189 | }) 190 | } 191 | 192 | var getter = options.props 193 | 194 | /** 195 | * Get initial props from options 196 | */ 197 | var _initialProps = {} 198 | var _t = $type(getter) 199 | if (_t == FUNCTION) { 200 | _initialProps = getter() 201 | } else if (_t == OBJECT) { 202 | _initialProps = getter 203 | } 204 | // free 205 | getter = null 206 | 207 | var _initialComputedProps = options.computed 208 | var _computedProps = {} 209 | var _computedKeys = [] 210 | var _cptDepsMapping = {} // mapping: deps --> props 211 | var _cptCaches = {} // computed properties caches 212 | var _observableKeys = [] 213 | var _props = {} // all observable properties {propname: propvalue} 214 | 215 | /** 216 | * Observe initial properties 217 | */ 218 | $util.objEach(_initialProps, function (pn, pv) { 219 | _$add(pn, pv, true) 220 | }) 221 | _initialProps = null 222 | 223 | /** 224 | * Define initial computed properties 225 | */ 226 | $util.objEach(_initialComputedProps, function (pn, def) { 227 | _$computed(pn, def.deps, def.get, def.set, def.enum) 228 | }) 229 | _initialComputedProps = null 230 | 231 | 232 | /** 233 | * batch emit computed property change 234 | */ 235 | _emitter.on(CHANGE_EVENT, function (kp) { 236 | var willComputedProps = [] 237 | var mappings = [] 238 | 239 | if (!Object.keys(_cptDepsMapping).length) return 240 | 241 | while(kp) { 242 | _cptDepsMapping[kp] && (mappings = mappings.concat(_cptDepsMapping[kp])) 243 | kp = $keypath.digest(kp) 244 | } 245 | 246 | if (!mappings.length) return 247 | /** 248 | * get all computed props that depend on kp 249 | */ 250 | mappings.reduce(function (pv, cv) { 251 | if (!$indexOf(pv, cv)) pv.push(cv) 252 | return pv 253 | }, willComputedProps) 254 | 255 | willComputedProps.forEach(function (ck) { 256 | $util.patch(_cptCaches, ck, {}) 257 | 258 | var cache = _cptCaches[ck] 259 | var pre = cache.pre = cache.cur 260 | var next = cache.cur = (_computedProps[ck].get || NOOP).call(_computedCtx, model) 261 | if ($util.diff(next, pre)) _emitChange(ck, next, pre) 262 | }) 263 | }, __muxid__/*scope*/) 264 | 265 | 266 | /** 267 | * private methods 268 | */ 269 | function _destroyNotice () { 270 | $warn('Instance already has bean destroyed') 271 | return _destroy 272 | } 273 | // local proxy for EventEmitter 274 | function _emitChange(propname/*, arg1, ..., argX*/) { 275 | var args = arguments 276 | var kp = $normalize($join(_rootPath(), propname)) 277 | args[0] = CHANGE_EVENT + ':' + kp 278 | _emitter.emit(CHANGE_EVENT, kp) 279 | emitter.emit.apply(emitter, args) 280 | 281 | args = $util.copyArray(args) 282 | args[0] = kp 283 | args.unshift('*') 284 | emitter.emit.apply(emitter, args) 285 | } 286 | /** 287 | * Add dependence to "_cptDepsMapping" 288 | * @param propname property name 289 | * @param dep dependency name 290 | */ 291 | function _prop2CptDepsMapping (propname, dep) { 292 | // if ($indexOf(_computedKeys, dep)) 293 | // return $warn('Dependency should not computed property') 294 | $util.patch(_cptDepsMapping, dep, []) 295 | 296 | var dest = _cptDepsMapping[dep] 297 | if ($indexOf(dest, propname)) return 298 | dest.push(propname) 299 | } 300 | /** 301 | * Instance or reuse a sub-mux-instance with specified keyPath and emitter 302 | * @param target instance target, it could be a Mux instance 303 | * @param props property value that has been walked 304 | * @param kp keyPath of target, use to diff instance keyPath changes or instance with the keyPath 305 | */ 306 | function _subInstance (target, props, kp) { 307 | 308 | var ins 309 | var _mux = target.__mux__ 310 | if (_mux && _mux.__kp__ === kp && _mux.__root__ === __muxid__) { 311 | // reuse 312 | ins = target 313 | // emitter proxy 314 | ins._$emitter(emitter) 315 | // a private emitter for communication between instances 316 | ins._$_emitter(_emitter) 317 | } else { 318 | ins = new Mux({ 319 | props: props, 320 | emitter: emitter, 321 | _emitter: _emitter, 322 | __kp__: kp 323 | }) 324 | } 325 | if (!ins.__root__) { 326 | $util.def(ins, '__root__', { 327 | enumerable: false, 328 | value: __muxid__ 329 | }) 330 | } 331 | return ins 332 | } 333 | 334 | /** 335 | * A hook method for setting value to "_props" 336 | * @param name property name 337 | * @param value 338 | * @param mountedPath property's value mouted path 339 | */ 340 | function _walk (name, value, mountedPath) { 341 | var tov = $type(value) // type of value 342 | // initial path prefix is root path 343 | var kp = mountedPath ? mountedPath : $join(_rootPath(), name) 344 | /** 345 | * Array methods hook 346 | */ 347 | if (tov == ARRAY) { 348 | $arrayHook(value, function (self, methodName, nativeMethod, args) { 349 | var pv = $util.copyArray(self) 350 | var result = nativeMethod.apply(self, args) 351 | // set value directly after walk 352 | _props[name] = _walk(name, self, kp) 353 | if (methodName == 'splice') { 354 | _emitChange(kp, self, pv, methodName, args) 355 | } else { 356 | _emitChange(kp, self, pv, methodName) 357 | } 358 | return result 359 | }) 360 | } 361 | 362 | // deep observe into each property value 363 | switch(tov) { 364 | case OBJECT: 365 | // walk deep into object items 366 | var props = {} 367 | var obj = value 368 | if (instanceOf(value, Mux)) obj = value.$props() 369 | $util.objEach(obj, function (k, v) { 370 | props[k] = _walk(k, v, $join(kp, k)) 371 | }) 372 | return _subInstance(value, props, kp) 373 | case ARRAY: 374 | // walk deep into array items 375 | value.forEach(function (item, index) { 376 | value[index] = _walk(index, item, $join(kp, index)) 377 | }) 378 | return value 379 | default: 380 | return value 381 | } 382 | } 383 | 384 | /************************************************************* 385 | Function name start of "_$" are expose methods 386 | *************************************************************/ 387 | /** 388 | * Set key-value pair to private model's property-object 389 | * @param kp keyPath 390 | * @return diff object 391 | */ 392 | function _$sync(kp, value, lazyEmit) { 393 | var parts = $normalize(kp).split('.') 394 | var prop = parts[0] 395 | 396 | if ($indexOf(_computedKeys, prop)) { 397 | // since Mux@2.4.0 computed property support setter 398 | model[prop] = value 399 | return 400 | } 401 | if (!$indexOf(_observableKeys, prop)) { 402 | $warn('Property "' + prop + '" has not been observed') 403 | // return false means sync prop fail 404 | return 405 | } 406 | var pv = $keypath.get(_props, kp) 407 | var isObj = instanceOf(value, Object) 408 | var nKeypath = parts.join('.') 409 | var name = parts.pop() 410 | var parentPath = parts.join('.') 411 | var parent = $keypath.get(_props, parentPath) 412 | var isParentObserved = instanceOf(parent, Mux) 413 | var changed 414 | if (isParentObserved) { 415 | if ($hasOwn(parent, name)) { 416 | changed = parent._$set(name, value, lazyEmit) 417 | } else { 418 | parent._$add(name, value, lazyEmit) 419 | changed = [$keypath.join(_rootPath(), kp), value] 420 | } 421 | } else { 422 | $keypath.set( 423 | _props, 424 | kp, 425 | isObj 426 | ? _walk(name, value, $join(_rootPath(), nKeypath)) 427 | : value 428 | ) 429 | if ($util.diff(value, pv)) { 430 | if (!lazyEmit) { 431 | _emitChange(kp, value, pv) 432 | } else { 433 | changed = [$keypath.join(_rootPath(), kp), value, pv] 434 | } 435 | } 436 | } 437 | return changed 438 | } 439 | 440 | /** 441 | * sync props value and trigger change event 442 | * @param kp keyPath 443 | */ 444 | function _$set(kp, value, lazyEmit) { 445 | if (_destroy) return _destroyNotice() 446 | 447 | return _$sync(kp, value, lazyEmit) 448 | // if (!diff) return 449 | /** 450 | * Base type change of object type will be trigger change event 451 | * next and pre value are not keypath value but property value 452 | */ 453 | // if ( kp == diff.mounted && $util.diff(diff.next, diff.pre) ) { 454 | // var propname = diff.mounted 455 | // // emit change immediately 456 | // _emitChange(propname, diff.next, diff.pre) 457 | // } 458 | } 459 | 460 | /** 461 | * sync props's value in batch and trigger change event 462 | * @param keyMap properties object 463 | */ 464 | function _$setMulti(keyMap) { 465 | if (_destroy) return _destroyNotice() 466 | 467 | if (!keyMap || $type(keyMap) != OBJECT) return 468 | var changes = [] 469 | $util.objEach(keyMap, function (key, item) { 470 | var cg = _$set(key, item, true) 471 | if (cg) changes.push(cg) 472 | }) 473 | 474 | changes.forEach(function (args) { 475 | _emitChange.apply(null, args) 476 | }) 477 | } 478 | 479 | /** 480 | * create a prop observer if not in observer, 481 | * return true if no value setting. 482 | * @param prop property name 483 | * @param value property value 484 | */ 485 | function _$add(prop, value, lazyEmit) { 486 | if (prop.match(/[\.\[\]]/)) { 487 | throw new Error('Propname shoudn\'t contains "." or "[" or "]"') 488 | } 489 | 490 | if ($indexOf(_observableKeys, prop)) { 491 | // If value is specified, reset value 492 | return arguments.length > 1 ? true : false 493 | } 494 | _props[prop] = _walk(prop, $util.copyValue(value)) 495 | _observableKeys.push(prop) 496 | $util.def(model, prop, { 497 | enumerable: true, 498 | get: function() { 499 | return _props[prop] 500 | }, 501 | set: function (v) { 502 | _$set(prop, v) 503 | } 504 | }) 505 | // add peroperty will trigger change event 506 | if (!lazyEmit) { 507 | _emitChange(prop, value) 508 | } else { 509 | return { 510 | kp: prop, 511 | vl: value 512 | } 513 | } 514 | } 515 | 516 | /** 517 | * define computed prop/props of this model 518 | * @param propname property name 519 | * @param deps computed property dependencies 520 | * @param get computed property getter 521 | * @param set computed property setter 522 | * @param enumerable whether property enumerable or not 523 | */ 524 | function _$computed (propname, deps, getFn, setFn, enumerable) { 525 | /** 526 | * property is exist 527 | */ 528 | if ($indexOf(_computedKeys, propname)) return 529 | 530 | _computedKeys.push(propname) 531 | _computedProps[propname] = { 532 | 'deps': deps, 533 | 'get': getFn, 534 | 'set': setFn 535 | } 536 | 537 | /** 538 | * Add to dependence-property mapping 539 | */ 540 | ;(deps || []).forEach(function (dep) { 541 | while(dep) { 542 | _prop2CptDepsMapping(propname, dep) 543 | dep = $keypath.digest(dep) 544 | } 545 | }) 546 | /** 547 | * define getter 548 | */ 549 | $util.patch(_cptCaches, propname, {}) 550 | var dest = _cptCaches[propname] 551 | dest.cur = getFn ? getFn.call(_computedCtx, model):undefined 552 | 553 | $util.def(model, propname, { 554 | enumerable: enumerable === undefined ? true : !!enumerable, 555 | get: function () { 556 | return dest.cur 557 | }, 558 | set: function () { 559 | setFn && setFn.apply(_computedCtx, arguments) 560 | } 561 | }) 562 | // emit change event when define 563 | _emitChange(propname, dest.cur) 564 | } 565 | 566 | /******************************* 567 | define instantiation's methods 568 | *******************************/ 569 | /** 570 | * define observerable prop/props 571 | * @param propname | 572 | * @param defaultValue Optional 573 | * ---------------------------- 574 | * @param propnameArray 575 | * ------------------------ 576 | * @param propsObj 577 | */ 578 | _defPrivateProperty('$add', function(/* [propname [, defaultValue]] | propnameArray | propsObj */) { 579 | var args = arguments 580 | var first = args[0] 581 | var pn, pv 582 | 583 | switch($type(first)) { 584 | case STRING: 585 | // with specified value or not 586 | pn = first 587 | if (args.length > 1) { 588 | pv = args[1] 589 | if (_$add(pn, pv)) { 590 | _$set(pn, pv) 591 | } 592 | } else { 593 | _$add(pn) 594 | } 595 | break 596 | case ARRAY: 597 | // observe properties without value 598 | first.forEach(function (item) { 599 | _$add(item) 600 | }) 601 | break 602 | case OBJECT: 603 | // observe properties with value, if key already exist, reset value only 604 | var resetProps 605 | $util.objEach(first, function (ipn, ipv) { 606 | if (_$add(ipn, ipv)) { 607 | !resetProps && (resetProps = {}) 608 | resetProps[ipn] = ipv 609 | } 610 | }) 611 | if (resetProps) _$setMulti(resetProps) 612 | break 613 | default: 614 | $warn('Unexpect params') 615 | } 616 | return this 617 | }) 618 | _defPrivateProperty('_$add', function (prop, value, lazyEmit) { 619 | var result = _$add(prop, value, !!lazyEmit) 620 | if (result === true) { 621 | return _$set(prop, value, !!lazyEmit) 622 | } 623 | return result 624 | }) 625 | /** 626 | * define computed prop/props 627 | * @param propname property name 628 | * @param deps computed property dependencies 629 | * @param getFn computed property getter 630 | * @param setFn computed property setter 631 | * @param enumerable Optional, whether property enumerable or not 632 | * -------------------------------------------------- 633 | * @param propsObj define multiple properties 634 | */ 635 | _defPrivateProperty('$computed', function (propname/*, deps, getFn, setFn, enumerable | [propsObj]*/) { 636 | if ($type(propname) == STRING) { 637 | _$computed.apply(null, arguments) 638 | } else if ($type(propname) == OBJECT) { 639 | $util.objEach(arguments[0], function (pn, pv /*propname, propnamevalue*/) { 640 | _$computed(pn, pv.deps, pv.get, pv.set, pv.enum) 641 | }) 642 | } else { 643 | $warn('$computed params show be "(String, Array, Function, Function)" or "(Object)"') 644 | } 645 | return this 646 | }) 647 | /** 648 | * subscribe prop change 649 | * change prop/props value, it will be trigger change event 650 | * @param kp 651 | * --------------------- 652 | * @param kpMap 653 | */ 654 | _defPrivateProperty('$set', function( /*[kp, value] | [kpMap]*/ ) { 655 | var args = arguments 656 | var len = args.length 657 | if (len >= 2 || (len == 1 && $type(args[0]) == STRING)) { 658 | return _$set(args[0], args[1]) 659 | } else if (len == 1 && $type(args[0]) == OBJECT) { 660 | return _$setMulti(args[0]) 661 | } else { 662 | $warn('Unexpect $set params') 663 | } 664 | }) 665 | _defPrivateProperty('_$set', function(key, value, lazyEmit) { 666 | return _$set(key, value, !!lazyEmit) 667 | }) 668 | /** 669 | * Get property value by name, using for get value of computed property without cached 670 | * change prop/props value, it will be trigger change event 671 | * @param kp keyPath 672 | */ 673 | _defPrivateProperty('$get', function(kp) { 674 | if ($indexOf(_observableKeys, kp)) 675 | return _props[kp] 676 | else if ($indexOf(_computedKeys, kp)) { 677 | return (_computedProps[kp].get || NOOP).call(_computedCtx, model) 678 | } else { 679 | // keyPath 680 | var normalKP = $normalize(kp) 681 | var parts = normalKP.split('.') 682 | if (!$indexOf(_observableKeys, parts[0])) { 683 | return 684 | } else { 685 | return $keypath.get(_props, normalKP) 686 | } 687 | } 688 | }) 689 | /** 690 | * if params is (key, callback), add callback to key's subscription 691 | * if params is (callback), subscribe any prop change events of this model 692 | * @param key optional 693 | * @param callback 694 | */ 695 | _defPrivateProperty('$watch', function( /*[key, ]callback*/ ) { 696 | var args = arguments 697 | var len = args.length 698 | var first = args[0] 699 | var key, callback 700 | if (len >= 2) { 701 | key = CHANGE_EVENT + ':' + $normalize($join(_rootPath(), first)) 702 | callback = args[1] 703 | } else if (len == 1 && $type(first) == FUNCTION) { 704 | key = '*' 705 | callback = first 706 | } else { 707 | $warn('Unexpect $watch params') 708 | return NOOP 709 | } 710 | emitter.on(key, callback, __muxid__/*scopre*/) 711 | var that = this 712 | // return a unsubscribe method 713 | return function() { 714 | that.$unwatch.apply(that, args) 715 | } 716 | }) 717 | /** 718 | * unsubscribe prop change 719 | * if params is (key, callback), remove callback from key's subscription 720 | * if params is (callback), remove all callbacks from key's subscription 721 | * if params is empty, remove all callbacks of current model 722 | * @param key 723 | * @param callback 724 | */ 725 | _defPrivateProperty('$unwatch', function( /*[key, ] [callback] */ ) { 726 | var args = arguments 727 | var len = args.length 728 | var first = args[0] 729 | var params 730 | var prefix 731 | switch (true) { 732 | case (len >= 2): 733 | params = [args[1]] 734 | case (len == 1 && $type(first) == STRING): 735 | !params && (params = []) 736 | prefix = CHANGE_EVENT + ':' + $normalize($join(_rootPath(), first)) 737 | params.unshift(prefix) 738 | break 739 | case (len == 1 && $type(first) == FUNCTION): 740 | params = ['*', first] 741 | break 742 | case (len === 0): 743 | params = [] 744 | break 745 | default: 746 | $warn('Unexpect param type of ' + first) 747 | } 748 | if (params) { 749 | params.push(__muxid__) 750 | emitter.off.apply(emitter, params) 751 | } 752 | return this 753 | }) 754 | /** 755 | * Return all properties without computed properties 756 | * @return 757 | */ 758 | _defPrivateProperty('$props', function() { 759 | return $util.copyObject(_props) 760 | }) 761 | /** 762 | * Reset event emitter 763 | * @param em emitter 764 | */ 765 | _defPrivateProperty('$emitter', function (em, _pem) { 766 | // return emitter instance if args is empty, 767 | // for share some emitter with other instance 768 | if (arguments.length === 0) return emitter 769 | emitter = em 770 | _walkResetEmiter(this.$props(), em, _pem) 771 | return this 772 | }) 773 | /** 774 | * set emitter directly 775 | */ 776 | _defPrivateProperty('_$emitter', function (em) { 777 | emitter = em 778 | }) 779 | /** 780 | * set private emitter directly 781 | */ 782 | _defPrivateProperty('_$_emitter', function (em) { 783 | instanceOf(em, $Message) && (_emitter = em) 784 | }) 785 | /** 786 | * Call destroy will release all private properties and variables 787 | */ 788 | _defPrivateProperty('$destroy', function () { 789 | // clean up all proto methods 790 | $util.objEach(_privateProperties, function (k, v) { 791 | if ($type(v) == FUNCTION && k != '$destroyed') _privateProperties[k] = _destroyNotice 792 | }) 793 | 794 | if (!_isExternalEmitter) emitter.off() 795 | else emitter.off(__muxid__) 796 | 797 | if (!_isExternalPrivateEmitter) _emitter.off() 798 | else _emitter.off(__muxid__) 799 | 800 | emitter = null 801 | _emitter = null 802 | _computedProps = null 803 | _computedKeys = null 804 | _cptDepsMapping = null 805 | _cptCaches = null 806 | _observableKeys = null 807 | _props = null 808 | 809 | // destroy external flag 810 | _destroy = true 811 | }) 812 | /** 813 | * This method is used to check the instance is destroyed or not 814 | */ 815 | _defPrivateProperty('$destroyed', function () { 816 | return _destroy 817 | }) 818 | /** 819 | * A shortcut of $set(props) while instancing 820 | */ 821 | _$setMulti(receiveProps) 822 | 823 | } 824 | /** 825 | * Reset emitter of the instance recursively 826 | * @param ins 827 | */ 828 | function _walkResetEmiter (ins, em, _pem) { 829 | if ($type(ins) == OBJECT) { 830 | var items = ins 831 | if (instanceOf(ins, Mux)) { 832 | ins._$emitter(em, _pem) 833 | items = ins.$props() 834 | } 835 | $util.objEach(items, function (k, v) { 836 | _walkResetEmiter(v, em, _pem) 837 | }) 838 | } else if ($type(ins) == ARRAY) { 839 | ins.forEach(function (v) { 840 | _walkResetEmiter(v, em, _pem) 841 | }) 842 | } 843 | } 844 | 845 | function NOOP() {} 846 | function instanceOf(a, b) { 847 | return a instanceof b 848 | } 849 | 850 | module.exports = Mux 851 | 852 | /***/ }, 853 | /* 2 */ 854 | /***/ function(module, exports, __webpack_require__) { 855 | 856 | /** 857 | * Simple Pub/Sub module 858 | * @author switer 859 | **/ 860 | 'use strict'; 861 | 862 | var $util = __webpack_require__(6) 863 | var _patch = $util.patch 864 | var _type = $util.type 865 | var _scopeDefault = '__default_scope__' 866 | 867 | function Message(context) { 868 | this._obs = {} 869 | this._context = context 870 | } 871 | var proto = Message.prototype 872 | proto.on = function(sub, cb, scope) { 873 | scope = scope || _scopeDefault 874 | _patch(this._obs, sub, []) 875 | 876 | this._obs[sub].push({ 877 | cb: cb, 878 | scope: scope 879 | }) 880 | } 881 | 882 | /** 883 | * @param subject subscribe type 884 | * @param [cb] callback, Optional, if callback is not exist, 885 | * will remove all callback of that sub 886 | */ 887 | proto.off = function( /*subject, cb, scope*/ ) { 888 | var types 889 | var args = arguments 890 | var len = args.length 891 | var cb, scope 892 | 893 | if (len >= 3) { 894 | // clear all observers of this subject and callback eq "cb" 895 | types = [args[0]] 896 | cb = args[1] 897 | scope = args[2] 898 | } else if (len == 2 && _type(args[0]) == 'function') { 899 | // clear all observers those callback equal "cb" 900 | types = Object.keys(this._obs) 901 | cb = args[0] 902 | scope = args[1] 903 | } else if (len == 2) { 904 | // clear all observers of this subject 905 | types = [args[0]] 906 | scope = args[1] 907 | } else if (len == 1) { 908 | // clear all observes of the scope 909 | types = Object.keys(this._obs) 910 | scope = args[0] 911 | } else { 912 | // clear all observes 913 | this._obs = [] 914 | return this 915 | } 916 | 917 | scope = scope || _scopeDefault 918 | 919 | var that = this 920 | types.forEach(function(sub) { 921 | 922 | var obs = that._obs[sub] 923 | if (!obs) return 924 | var nextObs = [] 925 | if (cb) { 926 | obs.forEach(function(observer) { 927 | if (observer.cb === cb && observer.scope === scope) { 928 | return 929 | } 930 | nextObs.push(observer) 931 | }) 932 | } else { 933 | obs.forEach(function(observer) { 934 | if (observer.scope === scope) return 935 | nextObs.push(observer) 936 | }) 937 | } 938 | // if cb is not exist, clean all observers 939 | that._obs[sub] = nextObs 940 | 941 | }) 942 | 943 | return this 944 | } 945 | proto.emit = function(sub) { 946 | var obs = this._obs[sub] 947 | if (!obs) return 948 | var args = [].slice.call(arguments) 949 | args.shift() 950 | var that = this 951 | obs.forEach(function(item) { 952 | item.cb && item.cb.apply(that._context || null, args) 953 | }) 954 | } 955 | 956 | module.exports = Message 957 | 958 | 959 | /***/ }, 960 | /* 3 */ 961 | /***/ function(module, exports, __webpack_require__) { 962 | 963 | 'use strict'; 964 | 965 | /** 966 | * normalize all access ways into dot access 967 | * @example "person.books[1].title" --> "person.books.1.title" 968 | */ 969 | function _keyPathNormalize(kp) { 970 | return new String(kp).replace(/\[([^\[\]]+)\]/g, function(m, k) { 971 | return '.' + k.replace(/^["']|["']$/g, '') 972 | }) 973 | } 974 | /** 975 | * set value to object by keypath 976 | */ 977 | function _set(obj, keypath, value, hook) { 978 | var parts = _keyPathNormalize(keypath).split('.') 979 | var last = parts.pop() 980 | var dest = obj 981 | parts.forEach(function(key) { 982 | // Still set to non-object, just throw that error 983 | dest = dest[key] 984 | }) 985 | if (hook) { 986 | // hook proxy set value 987 | hook(dest, last, value) 988 | } else { 989 | dest[last] = value 990 | } 991 | return obj 992 | } 993 | /** 994 | * Get undefine 995 | */ 996 | function undf () { 997 | return void(0) 998 | } 999 | function isNon (o) { 1000 | return o === undf() || o === null 1001 | } 1002 | /** 1003 | * get value of object by keypath 1004 | */ 1005 | function _get(obj, keypath) { 1006 | var parts = _keyPathNormalize(keypath).split('.') 1007 | var dest = obj 1008 | parts.forEach(function(key) { 1009 | if (isNon(dest)) return !(dest = undf()) 1010 | dest = dest[key] 1011 | }) 1012 | return dest 1013 | } 1014 | 1015 | /** 1016 | * append path to a base path 1017 | */ 1018 | function _join(pre, tail) { 1019 | var _hasBegin = !!pre 1020 | if(!_hasBegin) pre = '' 1021 | if (/^\[.*\]$/.exec(tail)) return pre + tail 1022 | else if (typeof(tail) == 'number') return pre + '[' + tail + ']' 1023 | else if (_hasBegin) return pre + '.' + tail 1024 | else return tail 1025 | } 1026 | /** 1027 | * remove the last section of the keypath 1028 | * digest("a.b.c") --> "a.b" 1029 | */ 1030 | function _digest(nkp) { 1031 | var reg = /(\.[^\.]+|\[([^\[\]])+\])$/ 1032 | if (!reg.exec(nkp)) return '' 1033 | return nkp.replace(reg, '') 1034 | } 1035 | module.exports = { 1036 | normalize: _keyPathNormalize, 1037 | set: _set, 1038 | get: _get, 1039 | join: _join, 1040 | digest: _digest 1041 | } 1042 | 1043 | 1044 | /***/ }, 1045 | /* 4 */ 1046 | /***/ function(module, exports, __webpack_require__) { 1047 | 1048 | 'use strict'; 1049 | 1050 | var $util = __webpack_require__(6) 1051 | var hookMethods = ['splice', 'push', 'pop', 'shift', 'unshift', 'reverse', 'sort', '$concat'] 1052 | var _push = Array.prototype.push 1053 | var _slice = Array.prototype.slice 1054 | var attachMethods = { 1055 | '$concat': function () { 1056 | var args = _slice.call(arguments) 1057 | var arr = this 1058 | args.forEach(function (items) { 1059 | $util.type(items) == 'array' 1060 | ? items.forEach(function (item) { 1061 | _push.call(arr, item) 1062 | }) 1063 | : _push.call(arr, items) 1064 | }) 1065 | return arr 1066 | } 1067 | } 1068 | var hookFlag ='__hook__' 1069 | 1070 | module.exports = function (arr, hook) { 1071 | hookMethods.forEach(function (m) { 1072 | if (arr[m] && arr[m][hookFlag]) { 1073 | // reset hook method 1074 | arr[m][hookFlag](hook) 1075 | return 1076 | } 1077 | // cached native method 1078 | var nativeMethod = arr[m] || attachMethods[m] 1079 | // method proxy 1080 | $util.def(arr, m, { 1081 | enumerable: false, 1082 | value: function () { 1083 | return hook(arr, m, nativeMethod, arguments) 1084 | } 1085 | }) 1086 | // flag mark 1087 | $util.def(arr[m], hookFlag, { 1088 | enumerable: false, 1089 | value: function (h) { 1090 | hook = h 1091 | } 1092 | }) 1093 | }) 1094 | } 1095 | 1096 | /***/ }, 1097 | /* 5 */ 1098 | /***/ function(module, exports, __webpack_require__) { 1099 | 1100 | 'use strict'; 1101 | 1102 | var _enable = true 1103 | 1104 | module.exports = { 1105 | enable: function () { 1106 | _enable = true 1107 | }, 1108 | disable: function () { 1109 | _enable = false 1110 | }, 1111 | warn: function (msg) { 1112 | if (!_enable) return 1113 | if (console.warn) return console.warn(msg) 1114 | console.log(msg) 1115 | } 1116 | } 1117 | 1118 | /***/ }, 1119 | /* 6 */ 1120 | /***/ function(module, exports, __webpack_require__) { 1121 | 1122 | 'use strict'; 1123 | function hasOwn (obj, prop) { 1124 | return obj && obj.hasOwnProperty(prop) 1125 | } 1126 | var undef = void(0) 1127 | module.exports = { 1128 | type: function (obj) { 1129 | if (obj === null) return 'null' 1130 | else if (obj === undef) return 'undefined' 1131 | var m = /\[object (\w+)\]/.exec(Object.prototype.toString.call(obj)) 1132 | return m ? m[1].toLowerCase() : '' 1133 | }, 1134 | objEach: function (obj, fn) { 1135 | if (!obj) return 1136 | for(var key in obj) { 1137 | if (hasOwn(obj, key)) { 1138 | if(fn(key, obj[key]) === false) break 1139 | } 1140 | } 1141 | }, 1142 | patch: function (obj, prop, defValue) { 1143 | !obj[prop] && (obj[prop] = defValue) 1144 | }, 1145 | diff: function (next, pre, _t) { 1146 | var that = this 1147 | // defult max 4 level 1148 | _t = _t === undefined ? 4 : _t 1149 | 1150 | if (_t <= 0) return next !== pre 1151 | 1152 | if (this.type(next) == 'array' && this.type(pre) == 'array') { 1153 | if (next.length !== pre.length) return true 1154 | return next.some(function(item, index) { 1155 | return that.diff(item, pre[index], _t - 1) 1156 | }) 1157 | } else if (this.type(next) == 'object' && this.type(pre) == 'object') { 1158 | var nkeys = Object.keys(next) 1159 | var pkeys = Object.keys(pre) 1160 | if (nkeys.length != pkeys.length) return true 1161 | 1162 | return nkeys.some(function(k) { 1163 | return (!~pkeys.indexOf(k)) || that.diff(next[k], pre[k], _t - 1) 1164 | }) 1165 | } 1166 | return next !== pre 1167 | }, 1168 | copyArray: function (arr) { 1169 | var len = arr.length 1170 | var nArr = new Array(len) 1171 | while(len --) { 1172 | nArr[len] = arr[len] 1173 | } 1174 | return nArr 1175 | }, 1176 | copyObject: function (obj) { 1177 | var cObj = {} 1178 | this.objEach(obj, function (k, v) { 1179 | cObj[k] = v 1180 | }) 1181 | return cObj 1182 | }, 1183 | copyValue: function (v) { 1184 | var t = this.type(v) 1185 | switch(t) { 1186 | case 'object': return this.copyObject(v) 1187 | case 'array': return this.copyArray(v) 1188 | default: return v 1189 | } 1190 | }, 1191 | def: function () { 1192 | return Object.defineProperty.apply(Object, arguments) 1193 | }, 1194 | indexOf: function (a, b) { 1195 | return ~a.indexOf(b) 1196 | }, 1197 | merge: function (to, from) { 1198 | if (!from) return to 1199 | this.objEach(from, function (k, v) { 1200 | to[k] = v 1201 | }) 1202 | return to 1203 | }, 1204 | hasOwn: hasOwn 1205 | } 1206 | 1207 | /***/ } 1208 | /******/ ]) 1209 | }); 1210 | ; -------------------------------------------------------------------------------- /dist/mux.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mux.js v2.4.18 3 | * (c) 2014 guankaishe 4 | * Released under the MIT License. 5 | */ 6 | !function(a,b){"object"==typeof exports&&"object"==typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):"object"==typeof exports?exports.Mux=b():a.Mux=b()}(this,function(){return function(a){function b(d){if(c[d])return c[d].exports;var e=c[d]={exports:{},id:d,loaded:!1};return a[d].call(e.exports,e,e.exports,b),e.loaded=!0,e.exports}var c={};return b.m=a,b.c=c,b.p="",b(0)}([function(a,b,c){"use strict";a.exports=c(1)},function(a,b,c){"use strict";function d(){return A++}function e(a){a=a||{},g.call(this,a)}function f(a){function b(b){g.call(this,a,b)}return b.prototype=Object.create(e.prototype),b}function g(a,b){function c(){return N||""}function f(a,b){j(b,Function)&&(b=b.bind(J)),R[a]=b,o.def(J,a,{enumerable:!1,value:b})}function g(){return u("Instance already has bean destroyed"),I}function n(a){var b=arguments,d=p(q(c(),a));b[0]=z+":"+d,L.emit(z,d),K.emit.apply(K,b),b=o.copyArray(b),b[0]=d,b.unshift("*"),K.emit.apply(K,b)}function A(a,b){o.patch(Y,b,[]);var c=Y[b];s(c,a)||c.push(a)}function B(a,b,c){var d,f=a.__mux__;return f&&f.__kp__===c&&f.__root__===O?(d=a,d._$emitter(K),d._$_emitter(L)):d=new e({props:b,emitter:K,_emitter:L,__kp__:c}),d.__root__||o.def(d,"__root__",{enumerable:!1,value:O}),d}function C(a,b,d){var f=r(b),g=d?d:q(c(),a);switch(f==w&&m(b,function(b,c,d,e){var f=o.copyArray(b),h=d.apply(b,e);return _[a]=C(a,b,g),"splice"==c?n(g,b,f,c,e):n(g,b,f,c),h}),f){case x:var h={},i=b;return j(b,e)&&(i=b.$props()),o.objEach(i,function(a,b){h[a]=C(a,b,q(g,a))}),B(b,h,g);case w:return b.forEach(function(a,c){b[c]=C(c,a,q(g,c))}),b;default:return b}}function D(a,b,d){var f=p(a).split("."),g=f[0];if(s(X,g))return void(J[g]=b);if(!s($,g))return void u('Property "'+g+'" has not been observed');var h,i=l.get(_,a),k=j(b,Object),m=f.join("."),r=f.pop(),v=f.join("."),w=l.get(_,v),x=j(w,e);return x?t(w,r)?h=w._$set(r,b,d):(w._$add(r,b,d),h=[l.join(c(),a),b]):(l.set(_,a,k?C(r,b,q(c(),m)):b),o.diff(b,i)&&(d?h=[l.join(c(),a),b,i]:n(a,b,i))),h}function E(a,b,c){return I?g():D(a,b,c)}function F(a){if(I)return g();if(a&&r(a)==x){var b=[];o.objEach(a,function(a,c){var d=E(a,c,!0);d&&b.push(d)}),b.forEach(function(a){n.apply(null,a)})}}function G(a,b,c){if(a.match(/[\.\[\]]/))throw new Error('Propname shoudn\'t contains "." or "[" or "]"');return s($,a)?arguments.length>1?!0:!1:(_[a]=C(a,o.copyValue(b)),$.push(a),o.def(J,a,{enumerable:!0,get:function(){return _[a]},set:function(b){E(a,b)}}),c?{kp:a,vl:b}:void n(a,b))}function H(a,b,c,d,e){if(!s(X,a)){X.push(a),W[a]={deps:b,get:c,set:d},(b||[]).forEach(function(b){for(;b;)A(a,b),b=l.digest(b)}),o.patch(Z,a,{});var f=Z[a];f.cur=c?c.call(M,J):void 0,o.def(J,a,{enumerable:void 0===e?!0:!!e,get:function(){return f.cur},set:function(){d&&d.apply(M,arguments)}}),n(a,f.cur)}}var I,J=this,K=a.emitter||new k(J),L=a._emitter||new k(J),M=t(a,"computedContext")?a.computedContext:J,N=l.normalize(a.__kp__||""),O=d(),P=!!a.emitter,Q=!!a._emitter,R={};f("__muxid__",O),f("__kp__",N);var S=a.props,T={},U=r(S);U==y?T=S():U==x&&(T=S),S=null;var V=a.computed,W={},X=[],Y={},Z={},$=[],_={};o.objEach(T,function(a,b){G(a,b,!0)}),T=null,o.objEach(V,function(a,b){H(a,b.deps,b.get,b.set,b["enum"])}),V=null,L.on(z,function(a){var b=[],c=[];if(Object.keys(Y).length){for(;a;)Y[a]&&(c=c.concat(Y[a])),a=l.digest(a);c.length&&(c.reduce(function(a,b){return s(a,b)||a.push(b),a},b),b.forEach(function(a){o.patch(Z,a,{});var b=Z[a],c=b.pre=b.cur,d=b.cur=(W[a].get||i).call(M,J);o.diff(d,c)&&n(a,d,c)}))}},O),f("$add",function(){var a,b,c=arguments,d=c[0];switch(r(d)){case v:a=d,c.length>1?(b=c[1],G(a,b)&&E(a,b)):G(a);break;case w:d.forEach(function(a){G(a)});break;case x:var e;o.objEach(d,function(a,b){G(a,b)&&(!e&&(e={}),e[a]=b)}),e&&F(e);break;default:u("Unexpect params")}return this}),f("_$add",function(a,b,c){var d=G(a,b,!!c);return d===!0?E(a,b,!!c):d}),f("$computed",function(a){return r(a)==v?H.apply(null,arguments):r(a)==x?o.objEach(arguments[0],function(a,b){H(a,b.deps,b.get,b.set,b["enum"])}):u('$computed params show be "(String, Array, Function, Function)" or "(Object)"'),this}),f("$set",function(){var a=arguments,b=a.length;return b>=2||1==b&&r(a[0])==v?E(a[0],a[1]):1==b&&r(a[0])==x?F(a[0]):void u("Unexpect $set params")}),f("_$set",function(a,b,c){return E(a,b,!!c)}),f("$get",function(a){if(s($,a))return _[a];if(s(X,a))return(W[a].get||i).call(M,J);var b=p(a),c=b.split(".");return s($,c[0])?l.get(_,b):void 0}),f("$watch",function(){var a,b,d=arguments,e=d.length,f=d[0];if(e>=2)a=z+":"+p(q(c(),f)),b=d[1];else{if(1!=e||r(f)!=y)return u("Unexpect $watch params"),i;a="*",b=f}K.on(a,b,O);var g=this;return function(){g.$unwatch.apply(g,d)}}),f("$unwatch",function(){var a,b,d=arguments,e=d.length,f=d[0];switch(!0){case e>=2:a=[d[1]];case 1==e&&r(f)==v:!a&&(a=[]),b=z+":"+p(q(c(),f)),a.unshift(b);break;case 1==e&&r(f)==y:a=["*",f];break;case 0===e:a=[];break;default:u("Unexpect param type of "+f)}return a&&(a.push(O),K.off.apply(K,a)),this}),f("$props",function(){return o.copyObject(_)}),f("$emitter",function(a,b){return 0===arguments.length?K:(K=a,h(this.$props(),a,b),this)}),f("_$emitter",function(a){K=a}),f("_$_emitter",function(a){j(a,k)&&(L=a)}),f("$destroy",function(){o.objEach(R,function(a,b){r(b)==y&&"$destroyed"!=a&&(R[a]=g)}),P?K.off(O):K.off(),Q?L.off(O):L.off(),K=null,L=null,W=null,X=null,Y=null,Z=null,$=null,_=null,I=!0}),f("$destroyed",function(){return I}),F(b)}function h(a,b,c){if(r(a)==x){var d=a;j(a,e)&&(a._$emitter(b,c),d=a.$props()),o.objEach(d,function(a,d){h(d,b,c)})}else r(a)==w&&a.forEach(function(a){h(a,b,c)})}function i(){}function j(a,b){return a instanceof b}var k=c(2),l=c(3),m=c(4),n=c(5),o=c(6),p=l.normalize,q=l.join,r=o.type,s=o.indexOf,t=o.hasOwn,u=n.warn,v="string",w="array",x="object",y="function",z="change",A=0;e.extend=function(a){return f(a||{})},e.config=function(a){a.warn===!1?n.disable():n.enable()},e.emitter=function(a){return new k(a)},e.keyPath=l,e.utils=o,a.exports=e},function(a,b,c){"use strict";function d(a){this._obs={},this._context=a}var e=c(6),f=e.patch,g=e.type,h="__default_scope__",i=d.prototype;i.on=function(a,b,c){c=c||h,f(this._obs,a,[]),this._obs[a].push({cb:b,scope:c})},i.off=function(){var a,b,c,d=arguments,e=d.length;if(e>=3)a=[d[0]],b=d[1],c=d[2];else if(2==e&&"function"==g(d[0]))a=Object.keys(this._obs),b=d[0],c=d[1];else if(2==e)a=[d[0]],c=d[1];else{if(1!=e)return this._obs=[],this;a=Object.keys(this._obs),c=d[0]}c=c||h;var f=this;return a.forEach(function(a){var d=f._obs[a];if(d){var e=[];b?d.forEach(function(a){(a.cb!==b||a.scope!==c)&&e.push(a)}):d.forEach(function(a){a.scope!==c&&e.push(a)}),f._obs[a]=e}}),this},i.emit=function(a){var b=this._obs[a];if(b){var c=[].slice.call(arguments);c.shift();var d=this;b.forEach(function(a){a.cb&&a.cb.apply(d._context||null,c)})}},a.exports=d},function(a,b,c){"use strict";function d(a){return new String(a).replace(/\[([^\[\]]+)\]/g,function(a,b){return"."+b.replace(/^["']|["']$/g,"")})}function e(a,b,c,e){var f=d(b).split("."),g=f.pop(),h=a;return f.forEach(function(a){h=h[a]}),e?e(h,g,c):h[g]=c,a}function f(){return void 0}function g(a){return a===f()||null===a}function h(a,b){var c=d(b).split("."),e=a;return c.forEach(function(a){return g(e)?!(e=f()):void(e=e[a])}),e}function i(a,b){var c=!!a;return c||(a=""),/^\[.*\]$/.exec(b)?a+b:"number"==typeof b?a+"["+b+"]":c?a+"."+b:b}function j(a){var b=/(\.[^\.]+|\[([^\[\]])+\])$/;return b.exec(a)?a.replace(b,""):""}a.exports={normalize:d,set:e,get:h,join:i,digest:j}},function(a,b,c){"use strict";var d=c(6),e=["splice","push","pop","shift","unshift","reverse","sort","$concat"],f=Array.prototype.push,g=Array.prototype.slice,h={$concat:function(){var a=g.call(arguments),b=this;return a.forEach(function(a){"array"==d.type(a)?a.forEach(function(a){f.call(b,a)}):f.call(b,a)}),b}},i="__hook__";a.exports=function(a,b){e.forEach(function(c){if(a[c]&&a[c][i])return void a[c][i](b);var e=a[c]||h[c];d.def(a,c,{enumerable:!1,value:function(){return b(a,c,e,arguments)}}),d.def(a[c],i,{enumerable:!1,value:function(a){b=a}})})}},function(a,b,c){"use strict";var d=!0;a.exports={enable:function(){d=!0},disable:function(){d=!1},warn:function(a){return d?console.warn?console.warn(a):void console.log(a):void 0}}},function(a,b,c){"use strict";function d(a,b){return a&&a.hasOwnProperty(b)}var e=void 0;a.exports={type:function(a){if(null===a)return"null";if(a===e)return"undefined";var b=/\[object (\w+)\]/.exec(Object.prototype.toString.call(a));return b?b[1].toLowerCase():""},objEach:function(a,b){if(a)for(var c in a)if(d(a,c)&&b(c,a[c])===!1)break},patch:function(a,b,c){!a[b]&&(a[b]=c)},diff:function(a,b,c){var d=this;if(c=void 0===c?4:c,0>=c)return a!==b;if("array"==this.type(a)&&"array"==this.type(b))return a.length!==b.length?!0:a.some(function(a,e){return d.diff(a,b[e],c-1)});if("object"==this.type(a)&&"object"==this.type(b)){var e=Object.keys(a),f=Object.keys(b);return e.length!=f.length?!0:e.some(function(e){return!~f.indexOf(e)||d.diff(a[e],b[e],c-1)})}return a!==b},copyArray:function(a){for(var b=a.length,c=new Array(b);b--;)c[b]=a[b];return c},copyObject:function(a){var b={};return this.objEach(a,function(a,c){b[a]=c}),b},copyValue:function(a){var b=this.type(a);switch(b){case"object":return this.copyObject(a);case"array":return this.copyArray(a);default:return a}},def:function(){return Object.defineProperty.apply(Object,arguments)},indexOf:function(a,b){return~a.indexOf(b)},merge:function(a,b){return b?(this.objEach(b,function(b,c){a[b]=c}),a):a},hasOwn:d}}])}); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp') 2 | var webpack = require('gulp-webpack') 3 | var uglify = require('gulp-uglifyjs') 4 | var header = require('gulp-header') 5 | var meta = require('./package.json') 6 | 7 | var banner = ['/**', 8 | '* Mux.js v${version}', 9 | '* (c) 2014 ${author}', 10 | '* Released under the ${license} License.', 11 | '*/', 12 | ''].join('\n') 13 | var bannerVars = { 14 | version : meta.version, 15 | author: 'guankaishe', 16 | license: 'MIT' 17 | } 18 | 19 | gulp.task('default', function() { 20 | return gulp.src('index.js') 21 | .pipe(webpack({ 22 | output: { 23 | library: 'Mux', 24 | libraryTarget: 'umd', 25 | filename: 'mux.js' 26 | } 27 | })) 28 | .pipe(header(banner, bannerVars)) 29 | .pipe(gulp.dest('dist/')) 30 | .pipe(uglify('mux.min.js', { 31 | mangle: true, 32 | compress: true 33 | })) 34 | .pipe(header(banner, bannerVars)) 35 | .pipe(gulp.dest('dist/')) 36 | }); 37 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib/mux') 4 | -------------------------------------------------------------------------------- /lib/array-hook.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var $util = require('./util') 4 | var hookMethods = ['splice', 'push', 'pop', 'shift', 'unshift', 'reverse', 'sort', '$concat'] 5 | var _push = Array.prototype.push 6 | var _slice = Array.prototype.slice 7 | var attachMethods = { 8 | '$concat': function () { 9 | var args = _slice.call(arguments) 10 | var arr = this 11 | args.forEach(function (items) { 12 | $util.type(items) == 'array' 13 | ? items.forEach(function (item) { 14 | _push.call(arr, item) 15 | }) 16 | : _push.call(arr, items) 17 | }) 18 | return arr 19 | } 20 | } 21 | var hookFlag ='__hook__' 22 | 23 | module.exports = function (arr, hook) { 24 | hookMethods.forEach(function (m) { 25 | if (arr[m] && arr[m][hookFlag]) { 26 | // reset hook method 27 | arr[m][hookFlag](hook) 28 | return 29 | } 30 | // cached native method 31 | var nativeMethod = arr[m] || attachMethods[m] 32 | // method proxy 33 | $util.def(arr, m, { 34 | enumerable: false, 35 | value: function () { 36 | return hook(arr, m, nativeMethod, arguments) 37 | } 38 | }) 39 | // flag mark 40 | $util.def(arr[m], hookFlag, { 41 | enumerable: false, 42 | value: function (h) { 43 | hook = h 44 | } 45 | }) 46 | }) 47 | } -------------------------------------------------------------------------------- /lib/info.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _enable = true 4 | 5 | module.exports = { 6 | enable: function () { 7 | _enable = true 8 | }, 9 | disable: function () { 10 | _enable = false 11 | }, 12 | warn: function (msg) { 13 | if (!_enable) return 14 | if (console.warn) return console.warn(msg) 15 | console.log(msg) 16 | } 17 | } -------------------------------------------------------------------------------- /lib/keypath.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * normalize all access ways into dot access 5 | * @example "person.books[1].title" --> "person.books.1.title" 6 | */ 7 | function _keyPathNormalize(kp) { 8 | return new String(kp).replace(/\[([^\[\]]+)\]/g, function(m, k) { 9 | return '.' + k.replace(/^["']|["']$/g, '') 10 | }) 11 | } 12 | /** 13 | * set value to object by keypath 14 | */ 15 | function _set(obj, keypath, value, hook) { 16 | var parts = _keyPathNormalize(keypath).split('.') 17 | var last = parts.pop() 18 | var dest = obj 19 | parts.forEach(function(key) { 20 | // Still set to non-object, just throw that error 21 | dest = dest[key] 22 | }) 23 | if (hook) { 24 | // hook proxy set value 25 | hook(dest, last, value) 26 | } else { 27 | dest[last] = value 28 | } 29 | return obj 30 | } 31 | /** 32 | * Get undefine 33 | */ 34 | function undf () { 35 | return void(0) 36 | } 37 | function isNon (o) { 38 | return o === undf() || o === null 39 | } 40 | /** 41 | * get value of object by keypath 42 | */ 43 | function _get(obj, keypath) { 44 | var parts = _keyPathNormalize(keypath).split('.') 45 | var dest = obj 46 | parts.forEach(function(key) { 47 | if (isNon(dest)) return !(dest = undf()) 48 | dest = dest[key] 49 | }) 50 | return dest 51 | } 52 | 53 | /** 54 | * append path to a base path 55 | */ 56 | function _join(pre, tail) { 57 | var _hasBegin = !!pre 58 | if(!_hasBegin) pre = '' 59 | if (/^\[.*\]$/.exec(tail)) return pre + tail 60 | else if (typeof(tail) == 'number') return pre + '[' + tail + ']' 61 | else if (_hasBegin) return pre + '.' + tail 62 | else return tail 63 | } 64 | /** 65 | * remove the last section of the keypath 66 | * digest("a.b.c") --> "a.b" 67 | */ 68 | function _digest(nkp) { 69 | var reg = /(\.[^\.]+|\[([^\[\]])+\])$/ 70 | if (!reg.exec(nkp)) return '' 71 | return nkp.replace(reg, '') 72 | } 73 | module.exports = { 74 | normalize: _keyPathNormalize, 75 | set: _set, 76 | get: _get, 77 | join: _join, 78 | digest: _digest 79 | } 80 | -------------------------------------------------------------------------------- /lib/message.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple Pub/Sub module 3 | * @author switer 4 | **/ 5 | 'use strict'; 6 | 7 | var $util = require('./util') 8 | var _patch = $util.patch 9 | var _type = $util.type 10 | var _scopeDefault = '__default_scope__' 11 | 12 | function Message(context) { 13 | this._obs = {} 14 | this._context = context 15 | } 16 | var proto = Message.prototype 17 | proto.on = function(sub, cb, scope) { 18 | scope = scope || _scopeDefault 19 | _patch(this._obs, sub, []) 20 | 21 | this._obs[sub].push({ 22 | cb: cb, 23 | scope: scope 24 | }) 25 | } 26 | 27 | /** 28 | * @param subject subscribe type 29 | * @param [cb] callback, Optional, if callback is not exist, 30 | * will remove all callback of that sub 31 | */ 32 | proto.off = function( /*subject, cb, scope*/ ) { 33 | var types 34 | var args = arguments 35 | var len = args.length 36 | var cb, scope 37 | 38 | if (len >= 3) { 39 | // clear all observers of this subject and callback eq "cb" 40 | types = [args[0]] 41 | cb = args[1] 42 | scope = args[2] 43 | } else if (len == 2 && _type(args[0]) == 'function') { 44 | // clear all observers those callback equal "cb" 45 | types = Object.keys(this._obs) 46 | cb = args[0] 47 | scope = args[1] 48 | } else if (len == 2) { 49 | // clear all observers of this subject 50 | types = [args[0]] 51 | scope = args[1] 52 | } else if (len == 1) { 53 | // clear all observes of the scope 54 | types = Object.keys(this._obs) 55 | scope = args[0] 56 | } else { 57 | // clear all observes 58 | this._obs = [] 59 | return this 60 | } 61 | 62 | scope = scope || _scopeDefault 63 | 64 | var that = this 65 | types.forEach(function(sub) { 66 | 67 | var obs = that._obs[sub] 68 | if (!obs) return 69 | var nextObs = [] 70 | if (cb) { 71 | obs.forEach(function(observer) { 72 | if (observer.cb === cb && observer.scope === scope) { 73 | return 74 | } 75 | nextObs.push(observer) 76 | }) 77 | } else { 78 | obs.forEach(function(observer) { 79 | if (observer.scope === scope) return 80 | nextObs.push(observer) 81 | }) 82 | } 83 | // if cb is not exist, clean all observers 84 | that._obs[sub] = nextObs 85 | 86 | }) 87 | 88 | return this 89 | } 90 | proto.emit = function(sub) { 91 | var obs = this._obs[sub] 92 | if (!obs) return 93 | var args = [].slice.call(arguments) 94 | args.shift() 95 | var that = this 96 | obs.forEach(function(item) { 97 | item.cb && item.cb.apply(that._context || null, args) 98 | }) 99 | } 100 | 101 | module.exports = Message 102 | -------------------------------------------------------------------------------- /lib/mux.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * External module's name startof "$" 5 | */ 6 | var $Message = require('./message') 7 | var $keypath = require('./keypath') 8 | var $arrayHook = require('./array-hook') 9 | var $info = require('./info') 10 | var $util = require('./util') 11 | var $normalize = $keypath.normalize 12 | var $join = $keypath.join 13 | var $type = $util.type 14 | var $indexOf = $util.indexOf 15 | var $hasOwn = $util.hasOwn 16 | var $warn = $info.warn 17 | 18 | /** 19 | * CONTS 20 | */ 21 | var STRING = 'string' 22 | var ARRAY = 'array' 23 | var OBJECT = 'object' 24 | var FUNCTION = 'function' 25 | var CHANGE_EVENT = 'change' 26 | 27 | var _id = 0 28 | function allotId() { 29 | return _id ++ 30 | } 31 | 32 | /** 33 | * Mux model constructor 34 | * @public 35 | */ 36 | function Mux(options) { 37 | // static config checking 38 | options = options || {} 39 | Ctor.call(this, options) 40 | } 41 | 42 | /** 43 | * Mux model creator 44 | * @public 45 | */ 46 | Mux.extend = function(options) { 47 | return MuxFactory(options || {}) 48 | } 49 | 50 | /** 51 | * Mux global config 52 | * @param conf 53 | */ 54 | Mux.config = function (conf) { 55 | if (conf.warn === false) $info.disable() 56 | else $info.enable() 57 | } 58 | 59 | /** 60 | * Create a emitter instance 61 | * @param `Optional` context use for binding "this" 62 | */ 63 | Mux.emitter = function (context) { 64 | return new $Message(context) 65 | } 66 | 67 | /** 68 | * Expose Keypath API 69 | */ 70 | Mux.keyPath = $keypath 71 | Mux.utils = $util 72 | 73 | /** 74 | * Mux model factory 75 | * @private 76 | */ 77 | function MuxFactory(options) { 78 | 79 | function Class (receiveProps) { 80 | Ctor.call(this, options, receiveProps) 81 | } 82 | Class.prototype = Object.create(Mux.prototype) 83 | return Class 84 | } 85 | /** 86 | * Mux's model class, could instance with "new" operator or call it directly. 87 | * @param receiveProps initial props set to model which will no trigger change event. 88 | */ 89 | function Ctor(options, receiveProps) { 90 | var model = this 91 | var emitter = options.emitter || new $Message(model) // EventEmitter of this model, context bind to model 92 | var _emitter = options._emitter || new $Message(model) 93 | var _computedCtx = $hasOwn(options, 'computedContext') ? options.computedContext : model 94 | var __kp__ = $keypath.normalize(options.__kp__ || '') 95 | var __muxid__ = allotId() 96 | var _isExternalEmitter = !!options.emitter 97 | var _isExternalPrivateEmitter = !!options._emitter 98 | var _destroy // interanl destroyed flag 99 | var _privateProperties = {} 100 | 101 | _defPrivateProperty('__muxid__', __muxid__) 102 | _defPrivateProperty('__kp__', __kp__) 103 | /** 104 | * return current keypath prefix of this model 105 | */ 106 | function _rootPath () { 107 | return __kp__ || '' 108 | } 109 | 110 | /** 111 | * define priavate property of the instance object 112 | */ 113 | function _defPrivateProperty(name, value) { 114 | if (instanceOf(value, Function)) value = value.bind(model) 115 | _privateProperties[name] = value 116 | $util.def(model, name, { 117 | enumerable: false, 118 | value: value 119 | }) 120 | } 121 | 122 | var getter = options.props 123 | 124 | /** 125 | * Get initial props from options 126 | */ 127 | var _initialProps = {} 128 | var _t = $type(getter) 129 | if (_t == FUNCTION) { 130 | _initialProps = getter() 131 | } else if (_t == OBJECT) { 132 | _initialProps = getter 133 | } 134 | // free 135 | getter = null 136 | 137 | var _initialComputedProps = options.computed 138 | var _computedProps = {} 139 | var _computedKeys = [] 140 | var _cptDepsMapping = {} // mapping: deps --> props 141 | var _cptCaches = {} // computed properties caches 142 | var _observableKeys = [] 143 | var _props = {} // all observable properties {propname: propvalue} 144 | 145 | /** 146 | * Observe initial properties 147 | */ 148 | $util.objEach(_initialProps, function (pn, pv) { 149 | _$add(pn, pv, true) 150 | }) 151 | _initialProps = null 152 | 153 | /** 154 | * Define initial computed properties 155 | */ 156 | $util.objEach(_initialComputedProps, function (pn, def) { 157 | _$computed(pn, def.deps, def.get, def.set, def.enum) 158 | }) 159 | _initialComputedProps = null 160 | 161 | 162 | /** 163 | * batch emit computed property change 164 | */ 165 | _emitter.on(CHANGE_EVENT, function (kp) { 166 | var willComputedProps = [] 167 | var mappings = [] 168 | 169 | if (!Object.keys(_cptDepsMapping).length) return 170 | 171 | while(kp) { 172 | _cptDepsMapping[kp] && (mappings = mappings.concat(_cptDepsMapping[kp])) 173 | kp = $keypath.digest(kp) 174 | } 175 | 176 | if (!mappings.length) return 177 | /** 178 | * get all computed props that depend on kp 179 | */ 180 | mappings.reduce(function (pv, cv) { 181 | if (!$indexOf(pv, cv)) pv.push(cv) 182 | return pv 183 | }, willComputedProps) 184 | 185 | willComputedProps.forEach(function (ck) { 186 | $util.patch(_cptCaches, ck, {}) 187 | 188 | var cache = _cptCaches[ck] 189 | var pre = cache.pre = cache.cur 190 | var next = cache.cur = (_computedProps[ck].get || NOOP).call(_computedCtx, model) 191 | if ($util.diff(next, pre)) _emitChange(ck, next, pre) 192 | }) 193 | }, __muxid__/*scope*/) 194 | 195 | 196 | /** 197 | * private methods 198 | */ 199 | function _destroyNotice () { 200 | $warn('Instance already has bean destroyed') 201 | return _destroy 202 | } 203 | // local proxy for EventEmitter 204 | function _emitChange(propname/*, arg1, ..., argX*/) { 205 | var args = arguments 206 | var kp = $normalize($join(_rootPath(), propname)) 207 | args[0] = CHANGE_EVENT + ':' + kp 208 | _emitter.emit(CHANGE_EVENT, kp) 209 | emitter.emit.apply(emitter, args) 210 | 211 | args = $util.copyArray(args) 212 | args[0] = kp 213 | args.unshift('*') 214 | emitter.emit.apply(emitter, args) 215 | } 216 | /** 217 | * Add dependence to "_cptDepsMapping" 218 | * @param propname property name 219 | * @param dep dependency name 220 | */ 221 | function _prop2CptDepsMapping (propname, dep) { 222 | // if ($indexOf(_computedKeys, dep)) 223 | // return $warn('Dependency should not computed property') 224 | $util.patch(_cptDepsMapping, dep, []) 225 | 226 | var dest = _cptDepsMapping[dep] 227 | if ($indexOf(dest, propname)) return 228 | dest.push(propname) 229 | } 230 | /** 231 | * Instance or reuse a sub-mux-instance with specified keyPath and emitter 232 | * @param target instance target, it could be a Mux instance 233 | * @param props property value that has been walked 234 | * @param kp keyPath of target, use to diff instance keyPath changes or instance with the keyPath 235 | */ 236 | function _subInstance (target, props, kp) { 237 | 238 | var ins 239 | var _mux = target.__mux__ 240 | if (_mux && _mux.__kp__ === kp && _mux.__root__ === __muxid__) { 241 | // reuse 242 | ins = target 243 | // emitter proxy 244 | ins._$emitter(emitter) 245 | // a private emitter for communication between instances 246 | ins._$_emitter(_emitter) 247 | } else { 248 | ins = new Mux({ 249 | props: props, 250 | emitter: emitter, 251 | _emitter: _emitter, 252 | __kp__: kp 253 | }) 254 | } 255 | if (!ins.__root__) { 256 | $util.def(ins, '__root__', { 257 | enumerable: false, 258 | value: __muxid__ 259 | }) 260 | } 261 | return ins 262 | } 263 | 264 | /** 265 | * A hook method for setting value to "_props" 266 | * @param name property name 267 | * @param value 268 | * @param mountedPath property's value mouted path 269 | */ 270 | function _walk (name, value, mountedPath) { 271 | var tov = $type(value) // type of value 272 | // initial path prefix is root path 273 | var kp = mountedPath ? mountedPath : $join(_rootPath(), name) 274 | /** 275 | * Array methods hook 276 | */ 277 | if (tov == ARRAY) { 278 | $arrayHook(value, function (self, methodName, nativeMethod, args) { 279 | var pv = $util.copyArray(self) 280 | var result = nativeMethod.apply(self, args) 281 | // set value directly after walk 282 | _props[name] = _walk(name, self, kp) 283 | if (methodName == 'splice') { 284 | _emitChange(kp, self, pv, methodName, args) 285 | } else { 286 | _emitChange(kp, self, pv, methodName) 287 | } 288 | return result 289 | }) 290 | } 291 | 292 | // deep observe into each property value 293 | switch(tov) { 294 | case OBJECT: 295 | // walk deep into object items 296 | var props = {} 297 | var obj = value 298 | if (instanceOf(value, Mux)) obj = value.$props() 299 | $util.objEach(obj, function (k, v) { 300 | props[k] = _walk(k, v, $join(kp, k)) 301 | }) 302 | return _subInstance(value, props, kp) 303 | case ARRAY: 304 | // walk deep into array items 305 | value.forEach(function (item, index) { 306 | value[index] = _walk(index, item, $join(kp, index)) 307 | }) 308 | return value 309 | default: 310 | return value 311 | } 312 | } 313 | 314 | /************************************************************* 315 | Function name start of "_$" are expose methods 316 | *************************************************************/ 317 | /** 318 | * Set key-value pair to private model's property-object 319 | * @param kp keyPath 320 | * @return diff object 321 | */ 322 | function _$sync(kp, value, lazyEmit) { 323 | var parts = $normalize(kp).split('.') 324 | var prop = parts[0] 325 | 326 | if ($indexOf(_computedKeys, prop)) { 327 | // since Mux@2.4.0 computed property support setter 328 | model[prop] = value 329 | return 330 | } 331 | if (!$indexOf(_observableKeys, prop)) { 332 | $warn('Property "' + prop + '" has not been observed') 333 | // return false means sync prop fail 334 | return 335 | } 336 | var pv = $keypath.get(_props, kp) 337 | var isObj = instanceOf(value, Object) 338 | var nKeypath = parts.join('.') 339 | var name = parts.pop() 340 | var parentPath = parts.join('.') 341 | var parent = $keypath.get(_props, parentPath) 342 | var isParentObserved = instanceOf(parent, Mux) 343 | var changed 344 | if (isParentObserved) { 345 | if ($hasOwn(parent, name)) { 346 | changed = parent._$set(name, value, lazyEmit) 347 | } else { 348 | parent._$add(name, value, lazyEmit) 349 | changed = [$keypath.join(_rootPath(), kp), value] 350 | } 351 | } else { 352 | $keypath.set( 353 | _props, 354 | kp, 355 | isObj 356 | ? _walk(name, value, $join(_rootPath(), nKeypath)) 357 | : value 358 | ) 359 | if ($util.diff(value, pv)) { 360 | if (!lazyEmit) { 361 | _emitChange(kp, value, pv) 362 | } else { 363 | changed = [$keypath.join(_rootPath(), kp), value, pv] 364 | } 365 | } 366 | } 367 | return changed 368 | } 369 | 370 | /** 371 | * sync props value and trigger change event 372 | * @param kp keyPath 373 | */ 374 | function _$set(kp, value, lazyEmit) { 375 | if (_destroy) return _destroyNotice() 376 | 377 | return _$sync(kp, value, lazyEmit) 378 | // if (!diff) return 379 | /** 380 | * Base type change of object type will be trigger change event 381 | * next and pre value are not keypath value but property value 382 | */ 383 | // if ( kp == diff.mounted && $util.diff(diff.next, diff.pre) ) { 384 | // var propname = diff.mounted 385 | // // emit change immediately 386 | // _emitChange(propname, diff.next, diff.pre) 387 | // } 388 | } 389 | 390 | /** 391 | * sync props's value in batch and trigger change event 392 | * @param keyMap properties object 393 | */ 394 | function _$setMulti(keyMap) { 395 | if (_destroy) return _destroyNotice() 396 | 397 | if (!keyMap || $type(keyMap) != OBJECT) return 398 | var changes = [] 399 | $util.objEach(keyMap, function (key, item) { 400 | var cg = _$set(key, item, true) 401 | if (cg) changes.push(cg) 402 | }) 403 | 404 | changes.forEach(function (args) { 405 | _emitChange.apply(null, args) 406 | }) 407 | } 408 | 409 | /** 410 | * create a prop observer if not in observer, 411 | * return true if no value setting. 412 | * @param prop property name 413 | * @param value property value 414 | */ 415 | function _$add(prop, value, lazyEmit) { 416 | if (prop.match(/[\.\[\]]/)) { 417 | throw new Error('Propname shoudn\'t contains "." or "[" or "]"') 418 | } 419 | 420 | if ($indexOf(_observableKeys, prop)) { 421 | // If value is specified, reset value 422 | return arguments.length > 1 ? true : false 423 | } 424 | _props[prop] = _walk(prop, $util.copyValue(value)) 425 | _observableKeys.push(prop) 426 | $util.def(model, prop, { 427 | enumerable: true, 428 | get: function() { 429 | return _props[prop] 430 | }, 431 | set: function (v) { 432 | _$set(prop, v) 433 | } 434 | }) 435 | // add peroperty will trigger change event 436 | if (!lazyEmit) { 437 | _emitChange(prop, value) 438 | } else { 439 | return { 440 | kp: prop, 441 | vl: value 442 | } 443 | } 444 | } 445 | 446 | /** 447 | * define computed prop/props of this model 448 | * @param propname property name 449 | * @param deps computed property dependencies 450 | * @param get computed property getter 451 | * @param set computed property setter 452 | * @param enumerable whether property enumerable or not 453 | */ 454 | function _$computed (propname, deps, getFn, setFn, enumerable) { 455 | /** 456 | * property is exist 457 | */ 458 | if ($indexOf(_computedKeys, propname)) return 459 | 460 | _computedKeys.push(propname) 461 | _computedProps[propname] = { 462 | 'deps': deps, 463 | 'get': getFn, 464 | 'set': setFn 465 | } 466 | 467 | /** 468 | * Add to dependence-property mapping 469 | */ 470 | ;(deps || []).forEach(function (dep) { 471 | while(dep) { 472 | _prop2CptDepsMapping(propname, dep) 473 | dep = $keypath.digest(dep) 474 | } 475 | }) 476 | /** 477 | * define getter 478 | */ 479 | $util.patch(_cptCaches, propname, {}) 480 | var dest = _cptCaches[propname] 481 | dest.cur = getFn ? getFn.call(_computedCtx, model):undefined 482 | 483 | $util.def(model, propname, { 484 | enumerable: enumerable === undefined ? true : !!enumerable, 485 | get: function () { 486 | return dest.cur 487 | }, 488 | set: function () { 489 | setFn && setFn.apply(_computedCtx, arguments) 490 | } 491 | }) 492 | // emit change event when define 493 | _emitChange(propname, dest.cur) 494 | } 495 | 496 | /******************************* 497 | define instantiation's methods 498 | *******************************/ 499 | /** 500 | * define observerable prop/props 501 | * @param propname | 502 | * @param defaultValue Optional 503 | * ---------------------------- 504 | * @param propnameArray 505 | * ------------------------ 506 | * @param propsObj 507 | */ 508 | _defPrivateProperty('$add', function(/* [propname [, defaultValue]] | propnameArray | propsObj */) { 509 | var args = arguments 510 | var first = args[0] 511 | var pn, pv 512 | 513 | switch($type(first)) { 514 | case STRING: 515 | // with specified value or not 516 | pn = first 517 | if (args.length > 1) { 518 | pv = args[1] 519 | if (_$add(pn, pv)) { 520 | _$set(pn, pv) 521 | } 522 | } else { 523 | _$add(pn) 524 | } 525 | break 526 | case ARRAY: 527 | // observe properties without value 528 | first.forEach(function (item) { 529 | _$add(item) 530 | }) 531 | break 532 | case OBJECT: 533 | // observe properties with value, if key already exist, reset value only 534 | var resetProps 535 | $util.objEach(first, function (ipn, ipv) { 536 | if (_$add(ipn, ipv)) { 537 | !resetProps && (resetProps = {}) 538 | resetProps[ipn] = ipv 539 | } 540 | }) 541 | if (resetProps) _$setMulti(resetProps) 542 | break 543 | default: 544 | $warn('Unexpect params') 545 | } 546 | return this 547 | }) 548 | _defPrivateProperty('_$add', function (prop, value, lazyEmit) { 549 | var result = _$add(prop, value, !!lazyEmit) 550 | if (result === true) { 551 | return _$set(prop, value, !!lazyEmit) 552 | } 553 | return result 554 | }) 555 | /** 556 | * define computed prop/props 557 | * @param propname property name 558 | * @param deps computed property dependencies 559 | * @param getFn computed property getter 560 | * @param setFn computed property setter 561 | * @param enumerable Optional, whether property enumerable or not 562 | * -------------------------------------------------- 563 | * @param propsObj define multiple properties 564 | */ 565 | _defPrivateProperty('$computed', function (propname/*, deps, getFn, setFn, enumerable | [propsObj]*/) { 566 | if ($type(propname) == STRING) { 567 | _$computed.apply(null, arguments) 568 | } else if ($type(propname) == OBJECT) { 569 | $util.objEach(arguments[0], function (pn, pv /*propname, propnamevalue*/) { 570 | _$computed(pn, pv.deps, pv.get, pv.set, pv.enum) 571 | }) 572 | } else { 573 | $warn('$computed params show be "(String, Array, Function, Function)" or "(Object)"') 574 | } 575 | return this 576 | }) 577 | /** 578 | * subscribe prop change 579 | * change prop/props value, it will be trigger change event 580 | * @param kp 581 | * --------------------- 582 | * @param kpMap 583 | */ 584 | _defPrivateProperty('$set', function( /*[kp, value] | [kpMap]*/ ) { 585 | var args = arguments 586 | var len = args.length 587 | if (len >= 2 || (len == 1 && $type(args[0]) == STRING)) { 588 | return _$set(args[0], args[1]) 589 | } else if (len == 1 && $type(args[0]) == OBJECT) { 590 | return _$setMulti(args[0]) 591 | } else { 592 | $warn('Unexpect $set params') 593 | } 594 | }) 595 | _defPrivateProperty('_$set', function(key, value, lazyEmit) { 596 | return _$set(key, value, !!lazyEmit) 597 | }) 598 | /** 599 | * Get property value by name, using for get value of computed property without cached 600 | * change prop/props value, it will be trigger change event 601 | * @param kp keyPath 602 | */ 603 | _defPrivateProperty('$get', function(kp) { 604 | if ($indexOf(_observableKeys, kp)) 605 | return _props[kp] 606 | else if ($indexOf(_computedKeys, kp)) { 607 | return (_computedProps[kp].get || NOOP).call(_computedCtx, model) 608 | } else { 609 | // keyPath 610 | var normalKP = $normalize(kp) 611 | var parts = normalKP.split('.') 612 | if (!$indexOf(_observableKeys, parts[0])) { 613 | return 614 | } else { 615 | return $keypath.get(_props, normalKP) 616 | } 617 | } 618 | }) 619 | /** 620 | * if params is (key, callback), add callback to key's subscription 621 | * if params is (callback), subscribe any prop change events of this model 622 | * @param key optional 623 | * @param callback 624 | */ 625 | _defPrivateProperty('$watch', function( /*[key, ]callback*/ ) { 626 | var args = arguments 627 | var len = args.length 628 | var first = args[0] 629 | var key, callback 630 | if (len >= 2) { 631 | key = CHANGE_EVENT + ':' + $normalize($join(_rootPath(), first)) 632 | callback = args[1] 633 | } else if (len == 1 && $type(first) == FUNCTION) { 634 | key = '*' 635 | callback = first 636 | } else { 637 | $warn('Unexpect $watch params') 638 | return NOOP 639 | } 640 | emitter.on(key, callback, __muxid__/*scopre*/) 641 | var that = this 642 | // return a unsubscribe method 643 | return function() { 644 | that.$unwatch.apply(that, args) 645 | } 646 | }) 647 | /** 648 | * unsubscribe prop change 649 | * if params is (key, callback), remove callback from key's subscription 650 | * if params is (callback), remove all callbacks from key's subscription 651 | * if params is empty, remove all callbacks of current model 652 | * @param key 653 | * @param callback 654 | */ 655 | _defPrivateProperty('$unwatch', function( /*[key, ] [callback] */ ) { 656 | var args = arguments 657 | var len = args.length 658 | var first = args[0] 659 | var params 660 | var prefix 661 | switch (true) { 662 | case (len >= 2): 663 | params = [args[1]] 664 | case (len == 1 && $type(first) == STRING): 665 | !params && (params = []) 666 | prefix = CHANGE_EVENT + ':' + $normalize($join(_rootPath(), first)) 667 | params.unshift(prefix) 668 | break 669 | case (len == 1 && $type(first) == FUNCTION): 670 | params = ['*', first] 671 | break 672 | case (len === 0): 673 | params = [] 674 | break 675 | default: 676 | $warn('Unexpect param type of ' + first) 677 | } 678 | if (params) { 679 | params.push(__muxid__) 680 | emitter.off.apply(emitter, params) 681 | } 682 | return this 683 | }) 684 | /** 685 | * Return all properties without computed properties 686 | * @return 687 | */ 688 | _defPrivateProperty('$props', function() { 689 | return $util.copyObject(_props) 690 | }) 691 | /** 692 | * Reset event emitter 693 | * @param em emitter 694 | */ 695 | _defPrivateProperty('$emitter', function (em, _pem) { 696 | // return emitter instance if args is empty, 697 | // for share some emitter with other instance 698 | if (arguments.length === 0) return emitter 699 | emitter = em 700 | _walkResetEmiter(this.$props(), em, _pem) 701 | return this 702 | }) 703 | /** 704 | * set emitter directly 705 | */ 706 | _defPrivateProperty('_$emitter', function (em) { 707 | emitter = em 708 | }) 709 | /** 710 | * set private emitter directly 711 | */ 712 | _defPrivateProperty('_$_emitter', function (em) { 713 | instanceOf(em, $Message) && (_emitter = em) 714 | }) 715 | /** 716 | * Call destroy will release all private properties and variables 717 | */ 718 | _defPrivateProperty('$destroy', function () { 719 | // clean up all proto methods 720 | $util.objEach(_privateProperties, function (k, v) { 721 | if ($type(v) == FUNCTION && k != '$destroyed') _privateProperties[k] = _destroyNotice 722 | }) 723 | 724 | if (!_isExternalEmitter) emitter.off() 725 | else emitter.off(__muxid__) 726 | 727 | if (!_isExternalPrivateEmitter) _emitter.off() 728 | else _emitter.off(__muxid__) 729 | 730 | emitter = null 731 | _emitter = null 732 | _computedProps = null 733 | _computedKeys = null 734 | _cptDepsMapping = null 735 | _cptCaches = null 736 | _observableKeys = null 737 | _props = null 738 | 739 | // destroy external flag 740 | _destroy = true 741 | }) 742 | /** 743 | * This method is used to check the instance is destroyed or not 744 | */ 745 | _defPrivateProperty('$destroyed', function () { 746 | return _destroy 747 | }) 748 | /** 749 | * A shortcut of $set(props) while instancing 750 | */ 751 | _$setMulti(receiveProps) 752 | 753 | } 754 | /** 755 | * Reset emitter of the instance recursively 756 | * @param ins 757 | */ 758 | function _walkResetEmiter (ins, em, _pem) { 759 | if ($type(ins) == OBJECT) { 760 | var items = ins 761 | if (instanceOf(ins, Mux)) { 762 | ins._$emitter(em, _pem) 763 | items = ins.$props() 764 | } 765 | $util.objEach(items, function (k, v) { 766 | _walkResetEmiter(v, em, _pem) 767 | }) 768 | } else if ($type(ins) == ARRAY) { 769 | ins.forEach(function (v) { 770 | _walkResetEmiter(v, em, _pem) 771 | }) 772 | } 773 | } 774 | 775 | function NOOP() {} 776 | function instanceOf(a, b) { 777 | return a instanceof b 778 | } 779 | 780 | module.exports = Mux -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | function hasOwn (obj, prop) { 3 | return obj && obj.hasOwnProperty(prop) 4 | } 5 | var undef = void(0) 6 | module.exports = { 7 | type: function (obj) { 8 | if (obj === null) return 'null' 9 | else if (obj === undef) return 'undefined' 10 | var m = /\[object (\w+)\]/.exec(Object.prototype.toString.call(obj)) 11 | return m ? m[1].toLowerCase() : '' 12 | }, 13 | objEach: function (obj, fn) { 14 | if (!obj) return 15 | for(var key in obj) { 16 | if (hasOwn(obj, key)) { 17 | if(fn(key, obj[key]) === false) break 18 | } 19 | } 20 | }, 21 | patch: function (obj, prop, defValue) { 22 | !obj[prop] && (obj[prop] = defValue) 23 | }, 24 | diff: function (next, pre, _t) { 25 | var that = this 26 | // defult max 4 level 27 | _t = _t === undefined ? 4 : _t 28 | 29 | if (_t <= 0) return next !== pre 30 | 31 | if (this.type(next) == 'array' && this.type(pre) == 'array') { 32 | if (next.length !== pre.length) return true 33 | return next.some(function(item, index) { 34 | return that.diff(item, pre[index], _t - 1) 35 | }) 36 | } else if (this.type(next) == 'object' && this.type(pre) == 'object') { 37 | var nkeys = Object.keys(next) 38 | var pkeys = Object.keys(pre) 39 | if (nkeys.length != pkeys.length) return true 40 | 41 | return nkeys.some(function(k) { 42 | return (!~pkeys.indexOf(k)) || that.diff(next[k], pre[k], _t - 1) 43 | }) 44 | } 45 | return next !== pre 46 | }, 47 | copyArray: function (arr) { 48 | var len = arr.length 49 | var nArr = new Array(len) 50 | while(len --) { 51 | nArr[len] = arr[len] 52 | } 53 | return nArr 54 | }, 55 | copyObject: function (obj) { 56 | var cObj = {} 57 | this.objEach(obj, function (k, v) { 58 | cObj[k] = v 59 | }) 60 | return cObj 61 | }, 62 | copyValue: function (v) { 63 | var t = this.type(v) 64 | switch(t) { 65 | case 'object': return this.copyObject(v) 66 | case 'array': return this.copyArray(v) 67 | default: return v 68 | } 69 | }, 70 | def: function () { 71 | return Object.defineProperty.apply(Object, arguments) 72 | }, 73 | indexOf: function (a, b) { 74 | return ~a.indexOf(b) 75 | }, 76 | merge: function (to, from) { 77 | if (!from) return to 78 | this.objEach(from, function (k, v) { 79 | to[k] = v 80 | }) 81 | return to 82 | }, 83 | hasOwn: hasOwn 84 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "muxjs", 3 | "version": "2.4.18", 4 | "description": "Modeling app states with muxjs.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npm run mocha", 8 | "mocha": "mocha test/index.js", 9 | "mocha-dist": "mocha test/index.dist.js", 10 | "dist": "browserify --standalone Mux ./index.js > dist/mux.js && uglifyjs -c hoist_vars=true dist/mux.js > dist/mux.min.js", 11 | "cover": "_mocha --require blanket --reporter mocha-lcov-reporter ./test/index.js | ./node_modules/coveralls/bin/coveralls.js", 12 | "cover-html": "mocha --require blanket --reporter html-cov ./test/index.js > coverage.html", 13 | "release": "git push && git push --tag && npm publish", 14 | "prepublish": "npm test" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/switer/mux.git" 19 | }, 20 | "keywords": [ 21 | "state", 22 | "model" 23 | ], 24 | "author": "switer", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/switer/mux/issues" 28 | }, 29 | "homepage": "https://github.com/switer/mux", 30 | "devDependencies": { 31 | "blanket": "^1.1.6", 32 | "coveralls": "^2.11.2", 33 | "gulp": "^3.8.10", 34 | "gulp-concat": "^2.4.3", 35 | "gulp-header": "^1.2.2", 36 | "gulp-uglify": "^1.1.0", 37 | "gulp-uglifyjs": "^0.5.0", 38 | "gulp-webpack": "^1.1.2", 39 | "istanbul": "^0.3.5", 40 | "mocha": "^2.1.0", 41 | "mocha-lcov-reporter": "0.0.1" 42 | }, 43 | "config": { 44 | "blanket": { 45 | "pattern": "lib" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/index.dist.js: -------------------------------------------------------------------------------- 1 | var Mux = require('../dist/mux.min.js') 2 | var assert = require('assert') 3 | Mux.config({ 4 | warn: false 5 | }) 6 | require('./spec-global-api')(Mux, assert) 7 | require('./spec-instance-method')(Mux, assert) 8 | require('./spec-options')(Mux, assert) 9 | require('./spec-options-deep')(Mux, assert) -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var Mux = require('../index') 2 | var assert = require('assert') 3 | Mux.config({ 4 | warn: false 5 | }) 6 | require('./spec-base')(Mux, assert) 7 | require('./spec-global-api')(Mux, assert) 8 | require('./spec-instance-method')(Mux, assert) 9 | require('./spec-instance-array')(Mux, assert) 10 | require('./spec-options')(Mux, assert) 11 | require('./spec-options-deep')(Mux, assert) -------------------------------------------------------------------------------- /test/spec-base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (Mux, assert) { 4 | describe('Base', function () { 5 | it('Set value', function (done) { 6 | var person = new Mux({ 7 | props: function () { 8 | return { 9 | items: [1,2,3,4], 10 | } 11 | } 12 | }) 13 | person.$watch('items', function (nv) { 14 | assert.equal(nv.length, 0) 15 | }) 16 | person.$watch(function (kp) { 17 | assert.equal(kp, 'items') 18 | done() 19 | }) 20 | person.items = [] 21 | }) 22 | 23 | }) 24 | } -------------------------------------------------------------------------------- /test/spec-global-api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (Mux, assert) { 4 | 5 | var Comment = Mux.extend({ 6 | props: function () { 7 | return { 8 | title: 'comment to me', 9 | author: 'switer', 10 | replyUsers: [] 11 | } 12 | }, 13 | computed: { 14 | replies: { 15 | deps: ['replyUsers'], 16 | get: function () { 17 | return this.replyUsers.length 18 | } 19 | } 20 | } 21 | }) 22 | var comment = new Comment() 23 | 24 | describe('Global API', function () { 25 | var person = new Mux({ 26 | props: function () { 27 | return { 28 | name: 'switer', 29 | github: 'https://github.com/switer' 30 | } 31 | }, 32 | computed: { 33 | nameLength: { 34 | deps: ['name'], 35 | get: function () { 36 | return this.name.length 37 | } 38 | } 39 | } 40 | }) 41 | var another = new Mux({ 42 | props: { 43 | name: 'guankaishe', 44 | email: 'guankaishe@gmail.com' 45 | } 46 | }) 47 | it('Instance of Mux', function () { 48 | assert(person instanceof Mux) 49 | var Clazz = Mux.extend() 50 | var ins = new Clazz() 51 | assert(person instanceof Mux) 52 | assert(ins instanceof Mux) 53 | }) 54 | it('Properties is correct when using Mux instance', function () { 55 | assert.equal(person['name'], 'switer') 56 | assert.equal(person.github, 'https://github.com/switer') 57 | assert.equal(person['nameLength'], 6) 58 | }) 59 | it('Properties is correct when using Mux instance and props is an object', function () { 60 | assert.equal(another['name'], 'guankaishe') 61 | assert.equal(another.email, 'guankaishe@gmail.com') 62 | }) 63 | 64 | it('Has instance methods', function (done) { 65 | person.$unwatch() 66 | person.$add('email', 'guankaishe@gmail.com') 67 | person.$watch('email', function (next) { 68 | assert.equal(next, 'none') 69 | done() 70 | }) 71 | person.$set('email', 'none') 72 | }) 73 | it('Bind ComputedContext correctly', function () { 74 | var ctx = {} 75 | var m = new Mux({ 76 | computedContext: ctx, 77 | props: { 78 | items: [1,2,3] 79 | }, 80 | computed: { 81 | len: { 82 | deps: ['data'], 83 | get: function () { 84 | assert(ctx === this) 85 | return 3 86 | }, 87 | set: function () { 88 | assert(ctx === this) 89 | } 90 | } 91 | } 92 | }) 93 | assert.equal(m.len, 3) 94 | }) 95 | }) 96 | } -------------------------------------------------------------------------------- /test/spec-instance-array.js: -------------------------------------------------------------------------------- 1 | module.exports = function (Mux, assert) { 2 | 3 | var Comment = Mux.extend({ 4 | props: function () { 5 | return { 6 | title: 'comment to me', 7 | author: 'switer', 8 | replyUsers: [0], 9 | arr2d: [[{name: 1}]] 10 | } 11 | }, 12 | computed: { 13 | replies: { 14 | deps: ['replyUsers'], 15 | get: function () { 16 | return this.replyUsers.length 17 | } 18 | } 19 | } 20 | }) 21 | describe('[Array]', function () { 22 | it('Array .push() hook', function (done) { 23 | var comment = new Comment() 24 | comment.$watch('replyUsers', function (next, pre, method) { 25 | assert.equal(method, 'push') 26 | assert.equal(pre.length, 1) 27 | assert.equal(next.length, 2) 28 | done() 29 | }) 30 | comment.replyUsers.push(1) 31 | }) 32 | it('Array .push() hook*2', function (done) { 33 | var comment = new Comment() 34 | comment.$watch(function (kp, next, pre, method) { 35 | assert.equal(kp, 'arr2d.0') 36 | assert.equal(method, 'push') 37 | done() 38 | }) 39 | comment.arr2d[0].push({name: 2}) 40 | }) 41 | it('Array .pop() hook', function (done) { 42 | var comment = new Comment() 43 | comment.$watch('replyUsers', function (next, pre, method) { 44 | assert.equal(method, 'pop') 45 | assert.equal(pre.length, 1) 46 | assert.equal(next.length, 0) 47 | done() 48 | }) 49 | comment.replyUsers.pop() 50 | }) 51 | it('Array .pop() hook*2', function (done) { 52 | var comment = new Comment() 53 | comment.$watch(function (kp, next, pre, method) { 54 | assert.equal(kp, 'arr2d.0') 55 | assert.equal(method, 'pop') 56 | done() 57 | }) 58 | comment.arr2d[0].pop() 59 | }) 60 | it('Array .unshift() hook', function (done) { 61 | var comment = new Comment() 62 | comment.$watch('replyUsers', function (next, pre, method) { 63 | assert.equal(method, 'unshift') 64 | assert.equal(pre.length, 1) 65 | assert.equal(next.length, 2) 66 | done() 67 | }) 68 | comment.replyUsers.unshift(2) 69 | }) 70 | it('Array .unshift() hook*2', function (done) { 71 | var comment = new Comment() 72 | comment.$watch(function (kp, next, pre, method) { 73 | assert.equal(kp, 'arr2d.0') 74 | assert.equal(method, 'unshift') 75 | done() 76 | }) 77 | comment.arr2d[0].unshift() 78 | }) 79 | it('Array .shift() hook', function (done) { 80 | var comment = new Comment() 81 | comment.$watch('replyUsers', function (next, pre, method) { 82 | assert.equal(method, 'shift') 83 | assert.equal(pre.length, 1) 84 | assert.equal(next.length, 0) 85 | done() 86 | }) 87 | comment.replyUsers.shift() 88 | }) 89 | it('Array .shift() hook*2', function (done) { 90 | var comment = new Comment() 91 | comment.$watch(function (kp, next, pre, method) { 92 | assert.equal(kp, 'arr2d.0') 93 | assert.equal(method, 'shift') 94 | done() 95 | }) 96 | comment.arr2d[0].shift() 97 | }) 98 | it('Array .reverse() hook', function (done) { 99 | var comment = new Comment() 100 | comment.$add('nums', [1,2,3,4]) 101 | comment.$watch('nums', function (next, pre, method) { 102 | assert.equal(method, 'reverse') 103 | assert.equal(next[0], 4) 104 | assert.equal(next[1], 3) 105 | assert.equal(next[2], 2) 106 | assert.equal(next[3], 1) 107 | done() 108 | }) 109 | comment.nums.reverse() 110 | }) 111 | it('Array .reverse() hook*2', function (done) { 112 | var comment = new Comment() 113 | comment.$watch(function (kp, next, pre, method) { 114 | assert.equal(kp, 'arr2d.0') 115 | assert.equal(method, 'reverse') 116 | done() 117 | }) 118 | comment.arr2d[0].reverse() 119 | }) 120 | it('Array .$concat() hook', function (done) { 121 | var comment = new Comment() 122 | comment.$add('nums', [1,2]) 123 | comment.$watch('nums', function (next, pre, method) { 124 | assert.equal(method, '$concat') 125 | assert.equal(next[2], 3) 126 | assert.equal(next[3], 4) 127 | done() 128 | }) 129 | comment.nums.$concat(3, [4]) 130 | }) 131 | it('Array .$concat() hook*2', function (done) { 132 | var comment = new Comment() 133 | comment.$watch(function (kp, next, pre, method) { 134 | assert.equal(kp, 'arr2d.0') 135 | assert.equal(method, '$concat') 136 | assert.equal(next.length, 2) 137 | done() 138 | }) 139 | comment.arr2d[0].$concat([{name: 2}]) 140 | }) 141 | }) 142 | } -------------------------------------------------------------------------------- /test/spec-instance-method.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (Mux, assert) { 4 | 5 | var Comment = Mux.extend({ 6 | deep: false, 7 | props: function () { 8 | return { 9 | title: 'comment to me', 10 | author: 'switer', 11 | replyUsers: [] 12 | } 13 | }, 14 | computed: { 15 | replies: { 16 | deps: ['replyUsers'], 17 | get: function () { 18 | return this.replyUsers.length 19 | } 20 | } 21 | } 22 | }) 23 | var comment = new Comment() 24 | 25 | describe('$set && $watch && $unwatch', function () { 26 | it('Get value after set value immediately', function () { 27 | comment.$set('title', 'comment to that') 28 | assert.equal(comment.title, 'comment to that') 29 | }) 30 | it('Set value to an unobserved property using $set', function () { 31 | comment.$set('unknow', 'unknow') 32 | assert.equal(comment.unknow, undefined) 33 | }) 34 | it('Set value to a computed property using $set', function () { 35 | comment.$set('replies', 100) 36 | assert.notEqual(comment.replies, 100) 37 | }) 38 | it('Change callback after set', function (done) { 39 | comment.$unwatch('title') 40 | comment.$set('title','comment to that') 41 | comment.$watch('title', function (next, pre) { 42 | assert.equal(next, 'comment to this') 43 | assert.equal(pre, 'comment to that') 44 | assert.equal(comment.title, 'comment to this') 45 | done() 46 | }) 47 | comment.$set('title','comment to this') 48 | }) 49 | it('Unwatch by watch return method', function () { 50 | comment.$unwatch('title') 51 | var unwatch = comment.$watch('title', function (next, pre) { 52 | assert(false) 53 | }) 54 | unwatch() 55 | comment.$set('title','no callback') 56 | }) 57 | it('Unwatch by name and method', function (done) { 58 | function handler() { 59 | assert(false) 60 | } 61 | function handler2() { 62 | done() 63 | } 64 | comment.$watch('title', handler) 65 | comment.$watch('title', handler2) 66 | comment.$unwatch('title', handler) 67 | comment.$set('title','calback once') 68 | }) 69 | it('Unwatch last and watch again', function (done) { 70 | var count = 0 71 | comment.$unwatch('title') 72 | comment.$set('title', 'comment to that') 73 | assert.equal(comment.title, 'comment to that') 74 | comment.$watch('title', function (next, pre) { 75 | assert.equal(++count, 1) 76 | assert.equal(pre, 'comment to that') 77 | assert.equal(next, 'comment to this') 78 | assert.equal(comment.title, 'comment to this') 79 | done() 80 | }) 81 | comment.$set('title','comment to this') 82 | }) 83 | it('Watch computed property change', function (done) { 84 | comment.$unwatch() 85 | comment.$watch('replies', function () { 86 | assert.equal(this.replies, 2) 87 | done() 88 | }) 89 | comment.replyUsers = [1,2] 90 | }) 91 | it('Unwatch computed property then watch again', function (done) { 92 | comment.$unwatch('replies') 93 | comment.$watch('replies', function () { 94 | assert.equal(this.replies, 3) 95 | done() 96 | }) 97 | comment.$set('replyUsers', [1,2,3]) 98 | }) 99 | it('Watch any properties change', function (done) { 100 | comment.$unwatch() 101 | comment.$watch(function (propname, next) { 102 | if (propname == 'replyUsers') { 103 | assert.equal(this.replyUsers.length, 2) 104 | } 105 | if (propname == 'replies') { 106 | assert.equal(this.replies, 2) 107 | done() 108 | } 109 | }) 110 | comment.$set('replyUsers', [1,2]) 111 | }) 112 | it('Set multiple props', function (done) { 113 | comment.$unwatch() 114 | var count = 0 115 | comment.title = '' 116 | comment.replyUsers = [] 117 | function allCb (propname, next, pre) { 118 | count ++ 119 | if (propname == 'title') { 120 | assert.equal(next, 'reset comment') 121 | assert.equal(pre, '') 122 | } else if (propname == 'replies') { 123 | assert.equal(next, 6) 124 | assert.equal(pre, 0) 125 | } 126 | _done('allCb') 127 | } 128 | function titleCb (next, pre) { 129 | assert.equal(next, 'reset comment') 130 | assert.equal(pre, '') 131 | count ++ 132 | _done() 133 | } 134 | function repliesCb (next, pre) { 135 | assert.equal(next, 6) 136 | assert.equal(this.replies, 6) 137 | count ++ 138 | _done('repliesCb') 139 | } 140 | function _done () { 141 | if (count >= 6) { 142 | comment.$unwatch('title', titleCb) 143 | comment.$unwatch('replies', repliesCb) 144 | comment.$unwatch(allCb) 145 | done() 146 | } 147 | } 148 | comment.$watch(allCb) 149 | comment.$watch('title', titleCb) 150 | comment.$watch('replies', repliesCb) 151 | comment.$set({ 152 | title: 'reset comment', 153 | author: 'mux.js', 154 | replyUsers: [1,2,3,4,5,6] 155 | }) 156 | }) 157 | it('Set value by keyPath', function (done) { 158 | comment.$unwatch() 159 | comment.replyUsers = [{ 160 | author: 'danyan', 161 | comment: 'test' 162 | }] 163 | comment.$watch('replyUsers.0.comment', function () { 164 | assert.equal(this.replyUsers[0].comment, 'test update') 165 | done() 166 | }) 167 | comment.$set('replyUsers[0].comment', 'test update') 168 | }) 169 | it('Set array item\'s value use $set', function (done) { 170 | var a = new Mux({ 171 | props: { 172 | items: [1] 173 | } 174 | }) 175 | a.$watch('items[0]', function (next) { 176 | assert.equal(next, 2) 177 | done() 178 | }) 179 | a.$set('items[0]', 2) 180 | }) 181 | it('Set multiple value by keyPath', function (done) { 182 | comment.$unwatch() 183 | comment.replyUsers = [{ 184 | author: 'danyan' 185 | }, { 186 | author: 'test-user' 187 | }] 188 | comment.post = { 189 | replyUsers: [{ 190 | author: '*' 191 | }] 192 | } 193 | comment.$watch('replyUsers.1.comment', function () { 194 | assert.equal(this.replyUsers[1].comment, 'test2') 195 | done() 196 | }) 197 | comment.$watch('post.replyUsers.1.comment', function () { 198 | assert.equal(this.post.replyUsers[0].comment, 'nothing') 199 | done() 200 | }) 201 | comment.$set('replyUsers[1].comment', 'test2') 202 | comment.$set('post.replyUsers[1].comment', 'nothing') 203 | }) 204 | it('keyPath normalize', function (done) { 205 | var a = new Mux({ 206 | props: { 207 | items: [{ 208 | comment: { 209 | title: 'hello' 210 | }, 211 | post: [{ 212 | content: 'world' 213 | }] 214 | }] 215 | } 216 | }) 217 | var step1 218 | var step2 219 | function _done () { 220 | step1 && step2 && done() 221 | } 222 | a.$watch('items[0].comment["title"]', function (next) { 223 | step1 = true 224 | _done() 225 | }) 226 | a.$watch('items["0"].post[0].content', function (next) { 227 | step2 = true 228 | _done() 229 | }) 230 | a.items[0].comment.title = 'none' 231 | a.items[0].post[0].content = 'none' 232 | }) 233 | }) 234 | describe('$get', function () { 235 | it('$get property value correctly', function () { 236 | comment.$unwatch() 237 | comment.title = 'mux.js' 238 | comment.replyUsers = [1,2,3,4,5] 239 | assert.equal(comment.$get('title'), 'mux.js') 240 | assert.equal(comment.$get('replies'), 5) 241 | }) 242 | it('$get property value by keyPath', function () { 243 | comment.$unwatch() 244 | 245 | comment.$add('person', {name: {first: 'switer'}}) 246 | comment.replyUsers = [{name: {first: 'switer'}}] 247 | assert.equal(comment.$get('person.name.first'), 'switer') 248 | assert.equal(comment.$get('replyUsers[0].name.first'), 'switer') 249 | }) 250 | it('$get computed property', function () { 251 | comment.$unwatch() 252 | comment.replyUsers = [{author: ''}] 253 | comment.$computed('first', ['replyUsers.0.author'], function () { 254 | return this.replyUsers[0].author 255 | }) 256 | comment.replyUsers = [{author: 'switer'}] 257 | assert.equal(comment.first, 'switer') 258 | assert.equal(comment.$get('first'), 'switer') 259 | 260 | comment.replyUsers[0].author = 'switerX' 261 | assert.equal(comment.$get('first'), 'switerX') 262 | comment.replyUsers = comment.replyUsers 263 | assert.equal(comment.first, 'switerX') 264 | }) 265 | it('define computed property not enumerable', function () { 266 | var a = new Mux({ 267 | props: { 268 | nums: [1,2,3] 269 | }, 270 | computed: { 271 | total: { 272 | enum: false, 273 | deps: ['nums'], 274 | get: function () { 275 | return this.nums.length 276 | } 277 | } 278 | } 279 | }) 280 | for (var k in a) { 281 | assert(k != 'total') 282 | } 283 | }) 284 | }) 285 | describe('$add', function () { 286 | it('observe a property', function (done) { 287 | comment.$unwatch() 288 | comment.$add('new') 289 | comment.$watch('new', function (next, pre) { 290 | assert.equal(next, 'new property') 291 | done() 292 | }) 293 | comment.$set('new', 'new property') 294 | assert.equal(comment.new, 'new property') 295 | }) 296 | it('observe a property with value', function (done) { 297 | comment.$unwatch() 298 | comment.$add('withvalue', 'value') 299 | assert.equal(comment.withvalue, 'value') 300 | comment.$watch('withvalue', function (next) { 301 | assert.equal(next, 'value2') 302 | done() 303 | }) 304 | comment.$add('withvalue', 'value2') 305 | }) 306 | it('observe multiple properties array', function (done) { 307 | comment.$unwatch() 308 | comment.$add(['prop1', 'prop2']) 309 | comment.$watch('prop1', function (next, pre) { 310 | assert.equal(next, 'new property 1') 311 | }) 312 | comment.$watch('prop2', function (next, pre) { 313 | assert.equal(next, 'new property 2') 314 | done() 315 | }) 316 | comment.$set('prop1', 'new property 1') 317 | comment.$set('prop2', 'new property 2') 318 | assert.equal(comment['prop1'], 'new property 1') 319 | assert.equal(comment['prop2'], 'new property 2') 320 | }) 321 | it('observe multiple properties object', function (done) { 322 | comment.$unwatch() 323 | comment.$add({ 324 | prop3: 'prop3' 325 | }) 326 | assert.equal(comment.prop3, 'prop3') 327 | comment.$watch('prop3', function (next) { 328 | assert.equal(next, 'prop4') 329 | done() 330 | }) 331 | comment.$add({ 332 | prop3: 'prop4' 333 | }) 334 | }) 335 | }) 336 | describe('$computed', function () { 337 | it('Define a computed property', function (done) { 338 | comment.$unwatch() 339 | comment.$computed('computed1', ['title'], function () { 340 | return 'Say:' + this.title 341 | }) 342 | comment.$watch('computed1', function () { 343 | assert.equal(this.computed1, 'Say:hello') 344 | done() 345 | }) 346 | comment.title = 'hello' 347 | }) 348 | it('Define a computed property with setter', function () { 349 | comment.$unwatch() 350 | comment.$add('namecount', 0) 351 | comment.$computed('computedSetter', [], function () {}, function (v) { 352 | comment.$set('namecount', v) 353 | }) 354 | comment.computedSetter = 10 355 | assert.equal(comment.namecount, 10) 356 | }) 357 | it('Define multiple computed properties', function (done) { 358 | comment.$unwatch() 359 | comment.$computed({ 360 | 'computed2': { 361 | deps:['title'], 362 | get: function () { 363 | return 'Guankaishe say:' + this.title 364 | } 365 | }, 366 | 'computed3': { 367 | deps:['title'], 368 | get: function () { 369 | return 'Switer say:' + this.title 370 | } 371 | } 372 | }) 373 | comment.$watch('computed2', function () { 374 | assert.equal(this.computed2, 'Guankaishe say:world') 375 | }) 376 | comment.$watch('computed3', function () { 377 | assert.equal(this.computed3, 'Switer say:world') 378 | done() 379 | }) 380 | comment.title = 'world' 381 | assert.equal(comment.computed2, 'Guankaishe say:world') 382 | }) 383 | }) 384 | 385 | describe('$props', function () { 386 | it('Get props of model correct without computed props', function () { 387 | var mux = new Mux({ 388 | props: { 389 | name: 'switer' 390 | }, 391 | computed: { 392 | nameLength: { 393 | deps: ['name'], 394 | get: function () { 395 | return this.name.length 396 | } 397 | } 398 | } 399 | }) 400 | 401 | var props = mux.$props() 402 | assert.equal(props.name, 'switer') 403 | assert.equal(props.nameLength, undefined) 404 | }) 405 | }) 406 | 407 | describe('$emitter', function () { 408 | it('Setting custom emitter using $emitter()', function (done) { 409 | var emitter = Mux.emitter() 410 | emitter.on('change:name', function (next) { 411 | assert.equal(next, 'switer') 412 | done() 413 | }) 414 | var mux = new Mux({ 415 | props: { 416 | name: '' 417 | } 418 | }) 419 | mux.$emitter(emitter), 420 | mux.name = 'switer' 421 | }) 422 | it('Deep observe instance trigger change event corrent when reset emitter ', function (done) { 423 | var mux = new Mux({ 424 | deep: true, 425 | props: { 426 | name: {first: 'switer', last: 'guan'} 427 | } 428 | }) 429 | var emitter = Mux.emitter() 430 | emitter.on('change:name.first', function (next) { 431 | assert.equal(next, 'kaishe') 432 | done() 433 | }) 434 | mux.$emitter(emitter), 435 | mux.name.first = 'kaishe' 436 | }) 437 | }) 438 | describe('$destroy', function () { 439 | it('Instance do not work after destroyed', function () { 440 | var mux = new Mux({ 441 | props: { 442 | name: '' 443 | } 444 | }) 445 | mux.$watch('name', function () { 446 | assert(false) 447 | }) 448 | mux.$destroy() 449 | mux.name = 'switer' 450 | }) 451 | it('destroy own scope handler when share emitter', function (done) { 452 | var emiter = Mux.emitter() 453 | 454 | var a = new Mux({ 455 | emitter: emiter, 456 | props: { 457 | name: '' 458 | } 459 | }) 460 | var b = new Mux({ 461 | emitter: emiter, 462 | props: { 463 | name: '' 464 | } 465 | }) 466 | b.$watch('name', function (next) { 467 | assert(false) 468 | }) 469 | a.$watch('name', function (next) { 470 | assert.equal(next, 'switer') 471 | done() 472 | }) 473 | b.$destroy() 474 | a.name = 'switer' 475 | }) 476 | }) 477 | describe('$destroyed', function () { 478 | it('$destroyed return true after destroy', function () { 479 | var mux = new Mux() 480 | assert(!mux.$destroyed()) 481 | mux.$destroy() 482 | assert(mux.$destroyed()) 483 | }) 484 | }) 485 | 486 | } 487 | -------------------------------------------------------------------------------- /test/spec-options-deep.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (Mux, assert) { 4 | 5 | var vm = new Mux({ 6 | props: { 7 | person: {name: 'switer'}, 8 | comments: [{title: 'hello'}, 1], 9 | post: [{ title: '', comments: [{author: 'switer'}] }] 10 | } 11 | }) 12 | var vm2 = new Mux({ 13 | props: { 14 | person: {} 15 | } 16 | }) 17 | 18 | describe('[deep]', function () { 19 | it('change subproperty using $set',function (done) { 20 | var count = 3 21 | vm.$unwatch() 22 | vm.$watch('person.title', function (next, pre) { 23 | assert.equal(next, 'hello') 24 | count -- 25 | !count && done() 26 | }) 27 | vm.$watch('post.0.comments.0.author', function (next, pre) { 28 | assert.equal(next, 'SZ') 29 | count -- 30 | !count && done() 31 | }) 32 | vm.$watch('post.0.title', function (next, pre) { 33 | assert.equal(next, 'demo') 34 | count -- 35 | !count && done() 36 | }) 37 | vm.$set('person.title', 'hello') 38 | vm.$set('post.0.title', 'demo') 39 | vm.$set('post.0.comments.0.author', 'SZ') 40 | }) 41 | it('change sub array property using $set',function (done) { 42 | vm.$unwatch() 43 | vm.$watch('comments[0].title', function (next, pre) { 44 | assert.equal(next, 'world') 45 | done() 46 | }) 47 | vm.$set('comments[0].title', 'world') 48 | }) 49 | it('change sub array items using push',function (done) { 50 | vm.$unwatch() 51 | vm.$watch('comments', function (next, pre) { 52 | assert.equal(next.length, 3) 53 | done() 54 | }) 55 | vm.comments.push(2) 56 | }) 57 | it('change sub array items using pop',function (done) { 58 | vm.$unwatch() 59 | vm.$watch('comments', function (next, pre) { 60 | assert.equal(next.length, 2) 61 | done() 62 | }) 63 | vm.comments.pop() 64 | }) 65 | it('change array property\'s item value',function (done) { 66 | vm.$unwatch() 67 | vm.$watch('comments[0].title', function (next, pre) { 68 | assert.equal(next, 'say') 69 | assert.equal(pre, 'world') 70 | done() 71 | }) 72 | vm.comments[0].title = 'say' 73 | }) 74 | it('create a new property',function (done) { 75 | vm.$unwatch() 76 | vm.$watch('person.email', function (next, pre) { 77 | assert.equal(next, 'guankaishe@gmail.com') 78 | assert.equal(pre, undefined) 79 | done() 80 | }) 81 | vm.$set('person.email', 'guankaishe@gmail.com') 82 | }) 83 | it('use \'=\' to append new property will not trigger change event',function () { 84 | vm.$unwatch() 85 | vm.$watch('person.address', function (next, pre) { 86 | assert.equal(false) 87 | }) 88 | vm.person.address = 'china' 89 | }) 90 | it('move property',function (done) { 91 | vm.$unwatch() 92 | vm.$watch('person.comments', function (next, pre) { 93 | assert.equal(next.length, vm.comments.length) 94 | done() 95 | }) 96 | vm.$set('person.comments', vm.comments) 97 | }) 98 | it('move property to another instance',function (done) { 99 | vm2.$unwatch() 100 | var step1 101 | vm2.$watch('person.comments', function (next, pre) { 102 | assert.equal(next.length, vm2.person.comments.length) 103 | step1 = true 104 | }) 105 | vm.$watch('person.comments', function (next, pre) { 106 | assert(false) 107 | }) 108 | vm.$watch('comments', function (next, pre) { 109 | assert.equal(next.length, 3) 110 | step1 && done() 111 | }) 112 | vm2.$set('person.comments', vm.comments) 113 | vm2.person.comments.push('switer') 114 | vm.comments.push(1) 115 | }) 116 | it('selft property',function (done) { 117 | vm.$unwatch() 118 | vm.$watch('person.parent', function (next, pre) { 119 | assert(next) 120 | done() 121 | }) 122 | vm.$set('person.parent', vm) 123 | }) 124 | }) 125 | } -------------------------------------------------------------------------------- /test/spec-options.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (Mux, assert) { 4 | var Comment = Mux.extend({ 5 | props: function () { 6 | return { 7 | title: 'comment to me', 8 | author: 'switer', 9 | replyUsers: [] 10 | } 11 | }, 12 | computed: { 13 | replies: { 14 | deps: ['replyUsers'], 15 | get: function () { 16 | return this.replyUsers.length 17 | } 18 | } 19 | } 20 | }) 21 | var comment = new Comment() 22 | 23 | describe('[props]', function () { 24 | it('Default property\'s value is correct', function () { 25 | assert.equal(comment.title, 'comment to me') 26 | }) 27 | it('Set value using dot access', function () { 28 | comment.title = 'abc' 29 | assert.equal(comment.title, 'abc') 30 | }) 31 | it('Set value using [] access', function () { 32 | comment['title'] = 123 33 | assert.equal(comment.title, 123) 34 | }) 35 | it('Using "=" operator to set property to an unobserved property to model object', function () { 36 | comment.replies123 = true 37 | assert.equal(comment.replies123, true) 38 | }) 39 | }) 40 | describe('[computed]', function () { 41 | it('Default replies is 0', function () { 42 | assert.equal(comment.replies, 0) 43 | }) 44 | it('Using "=" operator to set property to a computed value', function () { 45 | comment.replies = 10 46 | assert.equal(comment.replies, 0) 47 | }) 48 | it('Callback When dependenies change', function (done) { 49 | assert.equal(comment.replies, 0) 50 | comment.$unwatch() 51 | comment.$watch('replies', function () { 52 | assert.equal(comment.replies, 1) 53 | done() 54 | }) 55 | comment.replyUsers.push(1) 56 | }) 57 | it('Define computed setter', function () { 58 | var mux = new Mux({ 59 | props: { 60 | count: 0 61 | }, 62 | computed: { 63 | replies: { 64 | deps: ['count'], 65 | get: function () { 66 | return this.count 67 | }, 68 | set: function (v) { 69 | this.count = v 70 | } 71 | } 72 | } 73 | }) 74 | assert.equal(mux.replies, 0) 75 | mux.replies = 2 76 | assert.equal(mux.replies, 2) 77 | }) 78 | }) 79 | describe('[emitter]', function () { 80 | it('Passing custom emitter', function (done) { 81 | var em = Mux.emitter() 82 | var initChange = false 83 | em.on('change:name', function () { 84 | initChange = true 85 | }) 86 | var mux = new Mux({ 87 | emitter: em, 88 | props: { 89 | name: '', 90 | email: '' 91 | } 92 | }) 93 | mux.name = '123' 94 | em.on('change:email', function (next) { 95 | assert(initChange) 96 | assert.equal(next, 'guankaishe@gmail.com') 97 | done() 98 | }) 99 | mux.email = 'guankaishe@gmail.com' 100 | }) 101 | }) 102 | } --------------------------------------------------------------------------------