Right now, in the AngularJS community, the "controller as" syntax is becoming very popular because it disambiguates view-model references within the view. However, with the use of the controller-as syntax, people are also using the "vm" notation within their controllers. This is totally fine; but, it's important to understand why this works and what implications it has for Controller-View coupling and, especially, for $scope.$watch() expressions within your controller and your directive linking functions.
First, let's get rid of some of the magic. The "controller as" syntax isn't doing anything all that complex. It's literally creating a scope-based reference to the Controller instance. In fact, you can easily re-create the controller-as functionality in earlier versions of AngularJS by using a custom directive.
Once you understand how the controller instance is wired into the existing scope, you can start to think about how you want to monitor dirty-data with scope.$watch() expressions. If you follow John Pappa's style guide, the convention is to watch "vm.*" expressions. However, this only works if you use "vm" both internally and externally to the controller. This is not a bad thing; but, it is definitely creating coupling of your Controller to your View since your View now dictates where your Controller has to look for data.
If you don't want to couple your Controller to your View (in this way), you can watch functions using scope.$watch(). This allows the controller to define exactly what data is be observed and where that data is being referenced. And, in reality, this is actually more efficient because is saves AngularJS the overhead of having to parse the expression. The downside to this, of course, is that this approach is more verbose. That said, verbosity can be easy encapsulated behind functions in order to maintain readability.
To see the various approaches side-by-side, I have a demo here in which I increment a view-model value, "fooCount". As we increment the value, we're also going to be watching it with three different $scope.$watch() expressions. The first one demonstrates that that the "vm.*" only works by convention; the second one demonstrates why the "vm.*" approach works when it does work; and, the third one demonstrates how to watch any value without coupling the Controller to the View.
When we run this page and increment the value a few times, we get the following page output:
fn( vm.fooCount ): 0
----> 1 <----
fn( vm.fooCount ): 1
----> 2 <----
fn( vm.fooCount ): 2
----> 3 <----
fn( vm.fooCount ): 3
Notice that "vm.fooCount" never watched the value we were incrementing. This is because watching "vm.*" only works when both the Controller and the View follow the same "controller as" convention (which my demo is not doing).
Watching "appController.fooCount" works because the Controller is using the same alias value that the View is using. Of course, this lets the View dictate the workflow, which reverses the normal flow of dependency.
NOTE: This is the same thing that "vm" does as well; it's just that you probably never stopped to think about it.
And finally, watching a function reference both works and removes this coupling of the Controller to the View. Of course, the downside is the additional complexity and verbosity. That said, watching a function can be incredibly powerful - just look at the "watchDynamicValues" function in my Absolute Grid exploration.
I am not advocating against the use of the "vm.*" convention. I am only trying to clarify how it works; and, that it only works when the Controller is coupled to the View (as opposed to the View being coupled to the Controller). Conventions, after all, are often about these kinds of trade-offs.
Want to use code from this post? Check out the license.