Simply said, Angular keeps a list of watchers that watch elements and check for changes. Changes are as simple as
newValue !== oldValue
The newValue is retrieved via a watcher function (watcherFn). The oldValue is stored for later reference. If the value changes, it calls the listener function (listenerFn), which does something based on the new value. So a watcher is basically just an object with a watcherFn and a listenerFn.
There is a continuous loop that runs and loops over all watchers. This is the $digest loop. You can call the digest loop yourself, but it is safer to call the $apply function. The $apply function gives you a try-finally block that will call the $digest function in the finally block. This way, if there is an exception in any of your watch expressions, the digest will still run for others.
How does Angular know what to watch? That's where the compiler kicks in. The compiler loops over all elements and checks if one of the registered directives is present (like ng-bind). If so, it calls this directive. The directive will use a watcher to know what to watch, and to know what to do when the value of what it is watching has changed.
A watch can look like this:
For an element with a directive like ng-model="name", this will end up calling:
My short post does not do this session any justice, so I cannot recommend the video and code enough. It is very eye-opening to see this unfold in about 30 minutes.