观察json属性更改的Angular服务,将值复制到其他属性

时间:2015-06-18 15:38:24

标签: angularjs angularjs-service

给出角度服务提供的json数据:

{
  "editableData": {
    "test": {
      "value": "Never gonna give you up"
    }
  },
  "hiddenData": {
    "test": {
      "value": "Never gonna let you down"
    }
  }
}

...我想让用户修改editableData.test.value,但不断将该值与私有hiddenData.test.value同步。让我难倒的部分是确保在第一个值发生变化时始终触发此更新的最佳方法,并且它会尽快发生。

真实世界的并发症

  1. 必须在IE8 +中工作(Angular 1.2.x;允许使用es5-shim),而不是破坏或过度复杂的双向绑定。

  2. ' public'属性由多个控制器,指令和中间服务同时使用,因此根据DRY,在核心服务中处理此属性比在其使用的每个其他位置更好。

  3. 实施一个解决方案是不可取的/不可维护的,这个解决方案涉及为开发人员声明规则,只要这个属性发生变化,你必须记得之后调用xyz()来刷新任何镜像&# 39;

  4. 实际的数据结构要大得多,并且可能有多个必须镜像的属性。任何解决方案都应该相当容易扩展。

  5. 可能的解决方案1:$ rootScope。$ watch()

    在服务中,我可以使用$rootScope.$watch(funcThatRunsOnEveryDigest)触发每个$digest并不断复制值。

    然而,我感到不安,因为:

    1. 我知道当你开始向服务提供$rootScope时,它可能会导致问题...我认为这可能是一种保证它的情况,但这感觉就像是罪。

      < / LI>
    2. 无论属性是否已更改,都会在每个$digest上运行。但是对于任何观察者来说都是这样的,对吧(也就是说,观察者表达/功能总是在运行)?并且比几十个观察者更好,每个镜像属性一个,每次脏检查?

    3. 如果editableData.test.value当前没有$scope在某个地方公开,我是否会遇到问题,但是会因为某些代码运行而在幕后进行修改其他用户操作,或异步操作的解决方案?

    4. 可能的解决方案2:按引用链接

      只需通过引用链接属性:

      //linked at the parent obj containing the .value property
      _data.hiddenData.test = _data.editableData.test;
      

      还有一些需要考虑的进一步影响,包括这个引用可能会被轻易打破,以及它是什么样的偷偷摸摸的&#39;并且似乎可能会让维护开发者感到惊讶。

      更好地回答或深入了解其影响我并未受到高度赞赏!

      http://plnkr.co/edit/mOhFBFfKfqDiHBFEgEpH?p=preview

      &#13;
      &#13;
      (function() {
        "use strict";
        
        angular.module("myApp", ["myServices"])
        .controller("Controller1", ["$scope", "dataServiceFacade1", Controller1])
        .controller("Controller2", ["$scope", "dataServiceFacade2", Controller2])
        .controller("Controller3", ["$scope", "dataServiceCore", Controller3]);
        
        angular.module("myServices", [])
        .service("dataServiceCore", ["$rootScope", DataServiceCore])
        .service("dataServiceFacade1", ["dataServiceCore", DataServiceFacade1])
        .service("dataServiceFacade2", ["dataServiceCore", DataServiceFacade2]);
        
        /* myApp controllers */
        function Controller1($scope, dataServiceFacade1) {
          $scope.data = dataServiceFacade1.data; //using facade1 which returns editableData.test.value as test1.value
        }
        
        function Controller2($scope, dataServiceFacade2) {
          $scope.data = dataServiceFacade2.data; //using facade2 which returns editableData.test.value as test2.value
        }
        
        function Controller3($scope, dataServiceCore) {
          $scope.data = dataServiceCore.data; //no facade, raw data straight from the core
          $scope.isWatching = dataServiceCore.mirrorByRootScopeWatch; // for toggling the $rootScope.$watch on and off
          $scope.isReferencing = dataServiceCore.mirrorByRef; // for toggling ref on and off
          $scope.reset = dataServiceCore.reset;
        }
        
        /* myServices services */
        function DataServiceCore($rootScope) {
          
          var _data,
          _isWatching,
          _watcherDereg,
          _isReferencing;
          
          _init();
          
          //##################################################
          //# Mirroring by updating from within the service, #
          //# listening to every digest...                   #
          //##################################################
          function _watcherFireOnEveryDigest() {
            _data.hiddenData.test.value = _data.editableData.test.value; //mirroring the value
          }
          
          //_isWatching flag getter/setter
          function _mirrorByRootScopeWatch(value) {
            if(typeof value !== "undefined") {
              _isWatching = value;
              
              if(_isWatching) {
                _mirrorByRef(false);
                _watcherDereg = $rootScope.$watch(_watcherFireOnEveryDigest); //no listener function
              } else if(typeof _watcherDereg === "function") {
                _watcherDereg();
                _watcherDereg = null;
              }
            }
            
            return _isWatching;
          }
          
          function _mirrorByRef(value) {
            if(typeof value !== "undefined") {
              _isReferencing = value;
              
              if(_isReferencing) {
                _mirrorByRootScopeWatch(false);
                //##################################################
                //# Mirroring by creating reference from one prop  #
                //# to the other...                                #
                //##################################################
                _data.hiddenData.test = _data.editableData.test; //linking by ref
              } else {
                _data.hiddenData.test = JSON.parse(JSON.stringify(_data.hiddenData.test)); //set to a de-ref'd copy of itself
              }
            }
            
            return _isReferencing;
          }
          
          function _init() {
            if(_data) {
              //if _data already exists, merge (deep copy / recursive extend) so we update without breaking existing ref's
              merge(_data, _getData());
            } else {
              _data =_getData();
            }
            _mirrorByRootScopeWatch(false);
            _mirrorByRef(false);
          }
          
          //return a clone of the original data
          function _getData() {
            return JSON.parse(JSON.stringify({
              "editableData": {
                "test": {
                  "value": "Never gonna give you up"
                }
              },
              "hiddenData": {
                "test": {
                  "value": "Never gonna let you down"
                }
              }
            }));
          }
          
          //merge function adapted from angular.merge (angular 1.4+) as per http://stackoverflow.com/a/29003438/446030
          function merge(dst){
            var slice = [].slice;
            var isArray = Array.isArray;
            function baseExtend(dst, objs, deep) {
              for (var i = 0, ii = objs.length; i < ii; ++i) {
                var obj = objs[i];
                if (!angular.isObject(obj) && !angular.isFunction(obj)) continue;
                var keys = Object.keys(obj);
                for (var j = 0, jj = keys.length; j < jj; j++) {
                  var key = keys[j];
                  var src = obj[key];
                  if (deep && angular.isObject(src)) {
                    if (!angular.isObject(dst[key])) dst[key] = isArray(src) ? [] : {};
                    baseExtend(dst[key], [src], true);
                  } else {
                    dst[key] = src;
                  }
                }
              }
          
              return dst;
            }
            return baseExtend(dst, slice.call(arguments, 1), true);
          }
          
          return {
            data: _data,
            mirrorByRootScopeWatch: _mirrorByRootScopeWatch,
            mirrorByRef: _mirrorByRef,
            reset: _init
          };
        }
        
        function DataServiceFacade1(dataServiceCore) {
          var _data = {
            "test1": dataServiceCore.data.editableData.test
          };
          
          return {
            data: _data
          };
        }
        
        function DataServiceFacade2(dataServiceCore) {
          var _data = {
            "test2": dataServiceCore.data.editableData.test
          };
          
          return {
            data: _data
          };
        }
        
      })();
      &#13;
      <!DOCTYPE html>
      <html>
      
        <head>
          <script data-require="angular.js@*" data-semver="1.2.28" src="https://code.angularjs.org/1.2.28/angular.js"></script>
          <style type="text/css">
            body {font: 0.9em Arial, Verdana, sans-serif;}
            div {margin-bottom: 4px;}
            label {margin-right: 8px;}
            p {font-size: 0.9em; color: #999;}
            code {color:#000; background-color: #eee}
            pre code {display:block;}
          </style>
          <script src="script.js"></script>
        </head>
      
        <body>
          <div ng-app="myApp">
            
            <div ng-controller="Controller1">
              <h4>Controller1</h4>
              <p>This value is linked to <code>editableData.test.value</code> via
              reference in its facade service.</p>
              <label for="test1">test1.value</label>
              <input ng-model="data.test1.value" id="test1" />
              <pre><code>{{data|json}}</code></pre>
            </div>
            
            <div ng-controller="Controller2">
              <h4>Controller2</h4>
              <p>This value is <em>also</em> linked to
              <code>editableData.test.value</code> via reference in its facade
              service.</p>
              <label for="test2">test2.value</label>
              <input ng-model="data.test2.value" id="test2" />
              <pre><code>{{data|json}}</code></pre>
            </div>
            
            <div ng-controller="Controller3">
              <h4>Core Data</h4>
              <p>'Mirroring' the value of <code>editableData.test.value</code> to
              <code>hiddenData.test.value</code> by listening for every
              <code>$rootScope.$digest</code> from within the service, and copying
              between them.</p>
              <p>Enable/Disable mirroring with the button below, and type in the
              input fields above.</p>
              <button ng-click="isWatching(!isWatching());"><strong>
                {{isWatching() ? "Disable" : "Enable"}}</strong> mirroring with
                <code>$rootScope.$watch</code></button>
              <button ng-click="isReferencing(!isReferencing());"><strong>
                {{isReferencing() ? "Disable" : "Enable"}}</strong> mirroring by ref
                </button>
              <button ng-click="reset()">Reset</button>
              <pre><code>{{data|json}}</code></pre>
            </div>
            
          </div>
        </body>
      
      </html>
      &#13;
      &#13;
      &#13;

      更新:根据已接受的答案,对Plnkr的分支进行了一些进一步的修改,以便封装到单独的服务中。实际上比必要的更复杂,所以我仍然可以测试By Ref vs $ rootScope。$ watch():

      http://plnkr.co/edit/agdBWg?p=preview

1 个答案:

答案 0 :(得分:1)

编辑:似乎我误解了这个问题,因为它是关于如何监视数据对象的变化,你可以添加一个单独的服务/控制器,其唯一的工作是观察变化。对我来说,听起来足够分散关注点,使其变得非常好。以下是使用DataWatcher控制器的示例。

function DataWatcher($scope, dataServiceCore) {
  function _watcherHasChanged() {
    return dataServiceCore.data.editableData;
  }
  function _watcherFireOnEveryChange() {
    dataServiceCore.data.hiddenData.test.value = dataServiceCore.data.editableData.test.value; //mirroring the value
  }
  $scope.$watch(_watcherHasChanged, _watcherFireOnEveryChange, true);
}

http://plnkr.co/edit/PWPUPyEXGN5hcc9JANBo?p=preview

老答案:

根据What is the most efficient way to deep clone an object in JavaScript? JSON.parse(JSON.stringify(obj))是克隆对象的最有效方法。这基本上是你在这里尝试做的。

类似

myObject.hiddenData = JSON.parse(JSON.stringify(myObject.editableData);
editableData中更改myObject时执行的

可能是您正在寻找的内容。