Bulk update of widget attributes

This PR is an optimization in the way widget attributes are updated from the Python side. 
The motivation is the following: Let us say that `chart` is a widget, which has a `width` and a `height` attribute among others. In the JavaScript implementation of the widget, we may have:
```JavaScript
render: function() {
    // ...
    this.model.on("change:width change:height", this.update_layout, this); 
    // ...
}
```
Then, in the Python side, if one wants to change both width and height at once:
```Python
with chart.hold_sync():
    chart.width = 1000
    chart.height = 500
```
With the current behavior, the `update_layout`, which essentially redraws the entire chart is called twice. 

This PR is meant to allow one to do the following in the implementation of the widget:

```JavaScript
render: function() {
    // ...
    this.model.on_bulk_change(["width", "height"], this.update_layout, this); 
    // ...
}
```

A way to test this in a notebook is the following:
In a Python cell
```Python
from IPython.html.widgets import Widget, DOMWidget
from IPython.utils.traitlets import Unicode, Bool
from IPython.display import display
class TestWidget(DOMWidget):
    _view_name = Unicode('TestWidget', sync=True)
    a = Bool(False, sync=True)
    b = Bool(False, sync=True)
```
```JavaScript
%%javascript
var TestWidget = IPython.DOMWidgetView.extend({
    render: function() {
        this.model.on_bulk_change(['a', 'b'], function() {
            console.log('bulk change');
        });
        this.model.on("change:a change:b", function() {
            console.log('normal change');
        });        
    },
});
IPython.WidgetManager.register_widget_view('TestWidget', TestWidget);
```
```Python
wid = TestWidget()
display(wid)
with wid.hold_sync():
    wid.a = True
    wid.b = True
```
You will see that `bulk change` is displayed only once while `normal change` is displayed twice in the JavaScript console.