1/* 2 * Copyright (c) 2020 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15import { 16 ObserverStack, 17 SYMBOL_OBSERVABLE, 18 canObserve, 19 defineProp 20} from './utils'; 21 22 23/** 24 * Subject constructor 25 * @param {any} target the target object to be observed 26 */ 27export function Subject(target) { 28 const subject = this; 29 subject._hijacking = true; 30 defineProp(target, SYMBOL_OBSERVABLE, subject); 31 32 if (Array.isArray(target)) { 33 hijackArray(target); 34 } 35 36 Object.keys(target).forEach(key => hijack(target, key, target[key])); 37} 38 39Subject.of = function(target) { 40 if (!target || !canObserve(target)) { 41 return target; 42 } 43 if (target[SYMBOL_OBSERVABLE]) { 44 return target[SYMBOL_OBSERVABLE]; 45 } 46 return new Subject(target); 47}; 48 49Subject.is = function(target) { 50 return target && target._hijacking; 51}; 52Subject.prototype.attach = function(key, observer) { 53 if (typeof key === 'undefined' || !observer) { 54 return void 0; 55 } 56 if (!this._obsMap) { 57 this._obsMap = {}; 58 } 59 if (!this._obsMap[key]) { 60 this._obsMap[key] = new Set(); 61 } 62 const observers = this._obsMap[key]; 63 if (!observers.has(observer)) { 64 observers.add(observer); 65 return function() { 66 observers.delete(observer); 67 }; 68 } 69 return void 0; 70}; 71 72Subject.prototype.notify = function(key) { 73 if ( 74 typeof key === 'undefined' || 75 !this._obsMap || 76 !this._obsMap[key] 77 ) { 78 return void 0; 79 } 80 this._obsMap[key].forEach(observer => observer.update()); 81 return void 1; 82}; 83 84Subject.prototype.setParent = function(parent, key) { 85 this._parent = parent; 86 this._key = key; 87}; 88 89Subject.prototype.notifyParent = function() { 90 this._parent && this._parent.notify(this._key); 91}; 92 93const ObservedMethods = { 94 PUSH: 'push', 95 POP: 'pop', 96 UNSHIFT: 'unshift', 97 SHIFT: 'shift', 98 SORT: 'sort', 99 SPLICE: 'splice', 100 REVERSE: 'reverse' 101}; 102 103const OBSERVED_METHODS = Object.keys(ObservedMethods).map( 104 key => ObservedMethods[key] 105); 106 107/** 108 * observe the change of array 109 * @param {Array} target a plain JavaScript array to be observed 110 */ 111function hijackArray(target) { 112 OBSERVED_METHODS.forEach(key => { 113 const originalMethod = target[key]; 114 115 defineProp(target, key, function() { 116 // eslint-disable-next-line 117 const args = Array.prototype.slice.call(arguments); 118 // eslint-disable-next-line 119 originalMethod.apply(this, args); 120 121 let inserted; 122 if (ObservedMethods.PUSH === key || ObservedMethods.UNSHIFT === key) { 123 inserted = args; 124 } else if (ObservedMethods.SPLICE === key) { 125 inserted = args.slice(2); 126 } 127 128 if (inserted && inserted.length) { 129 inserted.forEach(Subject.of); 130 } 131 132 const subject = target[SYMBOL_OBSERVABLE]; 133 if (subject) { 134 subject.notifyParent(); 135 } 136 }); 137 }); 138} 139 140/** 141 * observe object 142 * @param {any} target the object to be observed 143 * @param {String} key the key to be observed 144 * @param {any} cache the cached value 145 */ 146function hijack(target, key, cache) { 147 const subject = target[SYMBOL_OBSERVABLE]; 148 149 Object.defineProperty(target, key, { 150 enumerable: true, 151 get() { 152 const observer = ObserverStack.top(); 153 if (observer) { 154 observer.subscribe(subject, key); 155 } 156 157 const subSubject = Subject.of(cache); 158 if (Subject.is(subSubject)) { 159 subSubject.setParent(subject, key); 160 } 161 162 return cache; 163 }, 164 set(value) { 165 cache = value; 166 subject.notify(key); 167 } 168 }); 169} 170