There are obvious syntax difference between = and @. And one of the features of two-way = binding is that you should proceed with care when attribute value is something other than scope property name, because it wasn't meant for that.
What happens in your example is that '[1,2]' string was parsed to an array and is available as vm.number scope property when controller function is running. The changes that were made with vm.numbers.push(3) were applied to a copy of an anonymous array and they weren't observed anywhere. After controller function was finished, the first digest cycle launches, and vm.number is overwritten with [1, 2] array, again. After that $timeout function kicks in and makes another change with vm.numbers.push(4). The changes in vm.number are observed only since this moment.
That's what happens when anonymous array or object is being fed to bi-directional directive binding. You will also have problems with assigning vm.number to something else.
Since @ binding is good only for text, it isn't an option also. You can do this instead
function myDirective() {
return {
template: '{{vm.numbers}}',
scope: {},
controller: MyController,
controllerAs: 'vm',
bindToController: true,
};
}
function MyController($timeout, $parse, $attrs) {
var vm = this;
vm.numbers = $parse($attrs.numbers)() || [];
vm.numbers.push(3);
$timeout(function() {
vm.numbers.push(4);
});
}