Re: Mutation events replacement

On Wed, Jul 20, 2011 at 5:20 AM, Olli Pettay <Olli.Pettay@helsinki.fi> wrote:
> On 07/20/2011 02:01 AM, Jonas Sicking wrote:
>>
>> 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.
>
> I don't understand this.
> There are cases when event loop may spin while handling a task.
> How should the almost-async listeners be handled in that case - at which
> point would they be called?

Spinning the event loop breaks things in both models. If you do a sync
XHR from within a mutation handler in the mostly-synchronous case, you
end up with the same situation.

But yes, in the almost-asynchronous solution there are larger code
paths where spinning the event loop results in badness.

In either solution, what we'd do if there are pending notifications
and someone spins the event loop, we'd stash away the current set of
notifications and clear the list of pending notifications. Inner tasks
behave like normal, unaffected by the stashed away set of
notifications. Once spinning the event loop finishes we restore the
list of notifications from the stashed list and continue as normal.

Again, this is something we have to do in both proposals.

>> The advantage of synchronous are fairly obvious.
>
> Indeed. So I'm quite surprised that you are leaning towards async.
>
>> 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.
>
> How so? Mostly-sync is in general close to how event handling works, so
> web devs should be familiar with it.
>
>> 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.
>
> I disagree.

I had hoped for a bit more of an explanation than that ;-)

Such as why do you not think that synchronous events will be a problem
for web developers just like they have been for us?

/ Jonas

Received on Wednesday, 20 July 2011 15:47:35 UTC