Re: Mutation events replacement

On Thu, Jul 7, 2011 at 6:38 PM, Jonas Sicking <jonas@sicking.cc> wrote:
> On Thu, Jul 7, 2011 at 5:23 PM, Rafael Weinstein <rafaelw@google.com> wrote:
>>> So yes, my proposal only solves the usecase outside mutation handlers.
>>> However this is arguably better than never solving the use case as in
>>> your proposal. I'm sure people will end up writing buggy code, but
>>> ideally this will be found and fixed fairly easily as the behavior is
>>> consistent. We are at least giving people the tools needed to
>>> implement the synchronous behavior.
>>
>> Ok. Thanks for clarifying. It's helpful to understand this.
>>
>> I'm glad there's mostly common ground on the larger issue. The point
>> of contention is clearly whether accommodating some form of sync
>> mutation actions is a goal or non-goal.
>
> Yup, that seems to be the case.
>
> I think the main reason I'm arguing for allowing synchronous callbacks
> is that I'm concerned that without them people are going to stick to
> mutation events. If I was designing this feature from scratch, I'd be
> much happier to use some sort of async callback. However given that we
> need something that people can migrate to, and we don't really know
> what they're using mutation events for, I'm more conservative.

Ok, here is my updated proposal.

There are two issues at stake here: When to send notifications, and
what they contain. I'll get to when to send them second as that is a
more controversial.

As for what the notification contain, lets first start at how to
register for notifications. Since we want a single callback to contain
information about all mutations that has happened, we need the ability
to choose, for a single callback, which mutations we should tell it
about. Something like this would work:

node.addMutationListener(listener, { childlist: true, attributes:
true, characterdata: true });
node.removeMutationListener(listener);

'listener' above would be a function which receives a single argument
when notifications fire. The value of this argument would be an Array
which could look something like this:

[ { target: node1, type: "childlist", added: [a, b, c, d], removed: [x, y] },
  { target: node1, type: "attributes", changed: ["class", "bgcolor", "href"] },
  { target: node2, type: "characterdata" },
  { target: node3, type: "childlist", added: [r, s, t, x], removed: [z] } ]

A few things to note here:

* There is only ever one entry in the array for a given target+type
pair. If, for example, multiple changes are made to the classlist of a
given node, these changes are added to the added/removed lists.
* For "childlist" changes, you get the full list of which nodes were
added and removed.
* For "attributes" changes you get a full list of which attributes
were changed. However you do not get the new and old value of the
attributes as this could result in significant overhead for attributes
like "style" for example.
* For "characterdata" you don't get the old or new value of the node.
We could also simply add the before/after values here as there
shouldn't be as much serialization overhead involved.


A nice thing with the above approach is that it is very expandable if
we want to introduce more types of notifications in the future. Some
examples that have been mentioned are the ability to be notified about
class changes, text-content changes and changes to individual
attributes. We could do that using:

node.addMutationListener(listener, { class: ["myclass1", "warning"],
textcontent: true, attributes: ["type", "title", "data-foo"] });

We could also add the ability to get notified about microdata changes
or data-prefix-* changes. But for now I think we should start with a
minimal set and see if people use it. But it's good to know that we
have a path forward.

There are of course a few more things that needs to be defined. Here
some of them:

* The notification-objects are added to the list in the order they
happen. With the exception that if there is a notification-object for
the specific target+type then a new object isn't created, but rather
added to the existing one.
* If you call addMutationListener with the same listener multiple
times any new "flags" are added to the existing registration. So
node.addMutationListener(listener, { attributes: true });
node.addMutationListener(listener, { childlist: true });
is equivalent to
node.addMutationListener(listener, { childlist: true, attributes: true });
* For the "childlist" notifications, nodes are added to the
added/removed lists in document order when a whole list of them are
added or removed. For example for .appendChild(docfragment) or
.textContent = "".
* If a node is first added and then removed from a childlist, it
doesn't appear in neither the "added" nor the "removed" lists for the
childlist notification.
* If a node is removed and then readded to a childlist, it appears in
both the "added" and the "removed" lists. This is needed to indicate
that it might have a different location now.


So, this leaves the issue of when to fire these notifications. I had a
very interesting talk with Rafael Weinstein about this last week and
he presented some very strong points.

The two proposals that we have on the table are:

1. Mostly-synchronous. The notifications are dispatched at the end of
any mutating DOM call. The notifications are not fully synchronous
when the mutation happens inside another mutation notification
callback.
2. Almost-asynchronous. The notifications are dispatched at the end of
the task which mutated the the DOM. So before any other scheduled
tasks get a chance to run. The distinction to fully asynchronous is
important since it means that all tasks still see a consistent state,
including tasks that paint.

The advantage of synchronous are fairly obvious. Strictly speaking it
provides a superset of functionality compared to the asynchronous
method. It also has the advantage that it is more similar to the API
we're trying to replace, mutation events, which might make migration
easier.

However, the asynchronous version also has advantages. First of all
it's more consistent in that for a given mutating DOM operation, the
callbacks have never been called by the time the operation returns.
I.e. code that runs inside a mutation handler doesn't see different
behavior from code that runs outside it.

An even bigger advantage however, and this was the convincing argument
for me, is that it's a simpler API to develop against for web
developers. As browser implementors, synchronous events (and other
callbacks) are always a pain in the ass because the world can change
under us by the time the event is done firing. With the
mostly-synchronous option, we create the same situation for web
developers. By the time that they get control again from the
notifications, all sorts of things might have changed under them.

This is especially the case when you have several independent
libraries running on a page, which is exactly the target audience for
mutation notifications.

Hence I'm leaning towards using the almost-asynchronous proposal for
now. If we end up getting the feedback from people that use mutation
events today that they won't be able to solve the same use cases, then
we can consider using the synchronous notifications. However I think
that it would be beneficial to try to go almost-async for now.

/ Jonas

Received on Tuesday, 19 July 2011 23:02:01 UTC