Node.js + AngularJS + socket.io: connect state and manually disconnect

后端 未结 2 1481
轻奢々
轻奢々 2021-01-01 03:37

I use nodejs with socket.io and angularjs on client. I picked up angular-socketio example from the Internet and added disconnect method to It.

Socket se

相关标签:
2条回答
  • 2021-01-01 03:46

    [Update]

    $$phase is an internal, private variable to Angular, and thus you should not really depend on it for things like this. Igor describes, in another answer, some suggestions for handling this which should be used instead (I hear he knows a thing or two about Angular. ;)


    When models change and events fire from within the Angular framework, Angular can do dirty tracking as necessary and update any necessary views. When you want to interact with code outside of Angular, you have to wrap the necessary function calls in the $apply method of a scope, so that Angular knows something is happening. That's why the code reads

    $rootScope.$apply(function () {
      callback.apply(socket, args);
    });
    

    and so forth. It's telling Angular, "take this code that normally wouldn't trigger Angular view updates, and treat it like it should."

    The problem is when you call $apply when you're already in an $apply call. For example, the following would throw an $apply already in progress error:

    $rootScope.$apply(function() {
      $rootScope.$apply(function() {
        // some stuff
      });
    });
    

    Based on your stack trace, it looks like some call to emit (which already uses $apply) triggered a call to on (which also uses $apply). To fix this problem, we need to only call $apply if an $apply is not already in progress. Thankfully, there is a property on the scope called $$phase that can tell us if a dirty check is in progress.

    We can easily build a function that takes a scope and a function to run, and then runs the function with $apply only if one isn't already in progress:

    var safeApply = function(scope, fn) {
      if (scope.$$phase) {
        fn(); // digest already in progress, just run the function
      } else {
        scope.$apply(fn); // no digest in progress, run the function with $apply
      }
    };
    

    Now we can replace calls to

    $rootScope.$apply(function...);
    

    to

    safeApply($rootScope, function...);
    

    For example, to modify the code you have above,

    angular.module('app')
      .factory('socket', ['$rootScope', function ($rootScope) {
    
        var safeApply = function(scope, fn) {
          if (scope.$$phase) {
            fn(); // digest already in progress, just run the function
          } else {
            scope.$apply(fn); // no digest in progress, run with $apply
          }
        };
    
        var socket = io.connect();
    
        return {
          on: function (eventName, callback) {
            socket.on(eventName, function () {  
              var args = arguments;
              safeApply($rootScope, function () {
                callback.apply(socket, args);
              });
            });
          },
          emit: function (eventName, data, callback) {
            socket.emit(eventName, data, function () {
              var args = arguments;
              safeApply($rootScope, function () {
                if (callback) {
                  callback.apply(socket, args);
                }
              });
            })
          },
          disconnect: function () {
            socket.disconnect();
          },
          socket: socket
        };
    
      }]);
    
    0 讨论(0)
  • 2021-01-01 03:59

    The core of the problem in this (just like in most of the other cases) is that the on method is called asynchronously most of the time (good!) but also synchronously in some cases (bad!).

    When you call socket.disconnect() from your application (from within a controller which lives in the "angular context") it synchronously fires the disconnect event which then propagates into the on method which is designed to open the boundary into the angular context. But since you are already in the angular context, angular complains with the error you mentioned.

    Since this issue is specific to the disconnect call the best options here are to

    • make the disconnect asynchronous by using setTimeout or $timeout (with the invokeApply arg set to false), or
    • keep an internal flag that will tell you if you are in the disconnect phase, and in that case, skip the $apply

    Example code:

    angular.module('app')
      .factory('socket', ['$rootScope', function ($rootScope, $timeout) {
    
        var socket = io.connect();
    
        return {
          on: function (eventName, callback) {
            socket.on(eventName, function () {  
              var args = arguments;
              $rootScope.$apply(function () {
                callback.apply(socket, args);
              });
            });
          },
          emit: function (eventName, data, callback) {
            socket.emit(eventName, data, function () {
              var args = arguments;
              $rootScope.$apply(function () {
                if (callback) {
                  callback.apply(socket, args);
                }
              });
            })
          },
          disconnect: function () {
            $timeout(socket.disconnect, 0, false);
          },
          socket: socket
        };
    
      }]);
    

    or

    angular.module('app')
      .factory('socket', ['$rootScope', function ($rootScope) {
    
        var socket = io.connect(),
            disconnecting = false;
    
        return {
          on: function (eventName, callback) {
            socket.on(eventName, function () {  
              var args = arguments;
              if (!disconnecting) {
                $rootScope.$apply(function () {
                  callback.apply(socket, args);
                });
              } else {
                callback.apply(socket, args);
              }
            });
          },
          emit: function (eventName, data, callback) {
            socket.emit(eventName, data, function () {
              var args = arguments;
              $rootScope.$apply(function () {
                if (callback) {
                  callback.apply(socket, args);
                }
              });
            })
          },
          disconnect: function () {
            disconnecting = true;
            socket.disconnect();
          },
          socket: socket
        };
    
      }]);
    
    0 讨论(0)
提交回复
热议问题