Angular 服务正在监视 json 属性 更改,正在将值复制到其他 属性

Angular service watching for json property change, copying value to other property

给定 json 由 angular 服务提供的数据:

{
  "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. undesirable/unmaintainable 实施一个涉及为开发人员声明规则的解决方案,例如 'whenever this property gets changed, you must remember to call xyz() afterwards to refresh any mirroring'。

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

可能的解决方案一:$rootScope.$watch()

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

但是,我很不安,因为:

  1. 我知道当您开始提供 $rootScope 服务时可能会导致问题...我认为这可能是一种值得这样做的情况,但感觉就像是一种罪过。

  2. 这将 运行 在每个 $digest 上,无论属性是否已更改。但是任何观察者都会这样,对吧(也就是说,观察者expression/function总是运行)?并且比拥有数十个观察者更好,每个观察者一个镜像 属性,每个脏检查?

  3. 如果 editableData.test.value 当前未在 $scope 某处暴露,但它在幕后被修改,我是否要 运行 解决问题代码 运行ning 以响应某些其他用户操作或异步操作的解决方案?

可能的解决方案 2:Link by reference

通过引用简单地链接属性:

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

还有一些进一步的影响需要考虑,包括这个引用有多容易被破坏,它有点 'sneaky' 并且看起来可能会让维护开发人员感到惊讶。

非常感谢更好的答案或对我未曾考虑过的含义的见解!

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

(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 
    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
    };
  }
  
})();
<!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>

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

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

编辑:看来我误解了这个问题,因为它是关于如何观察数据对象的变化,你可以添加一个单独的 service/controller,它的唯一工作就是观察变化。对我来说,这听起来足以分离关注点,让它变得很好。这是一个使用 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);

每当 editableDatamyObject 中发生更改时执行,可能正是您要查找的内容。