W3C home > Mailing lists > Public > public-webapps@w3.org > April to June 2011

Re: Mutation events replacement

From: Rafael Weinstein <rafaelw@google.com>
Date: Thu, 30 Jun 2011 16:17:10 -0700
Message-ID: <CABMdHiTWM5=e5VBGbVupGxFiAp21PP4cJdfb8gTCUaxKWXcNHA@mail.gmail.com>
To: Olli@pettay.fi
Cc: Aryeh Gregor <Simetrical+w3c@gmail.com>, Adam Klein <adamk@google.com>, Jonas Sicking <jonas@sicking.cc>, Anne van Kesteren <annevk@opera.com>, Webapps WG <public-webapps@w3.org>
On Thu, Jun 30, 2011 at 4:05 AM, Olli Pettay <Olli.Pettay@helsinki.fi> wrote:
> On 06/30/2011 12:54 AM, Rafael Weinstein wrote:
>>
>> On Wed, Jun 29, 2011 at 7:13 AM, Aryeh Gregor<Simetrical+w3c@gmail.com>
>>  wrote:
>>>
>>> On Tue, Jun 28, 2011 at 5:24 PM, Jonas Sicking<jonas@sicking.cc>  wrote:
>>>>
>>>> This new proposal solves both these by making all the modifications
>>>> first, then firing all the events. Hence the implementation can
>>>> separate implementing the mutating function from the code that sends
>>>> out notifications.
>>>>
>>>> Conceptually, you simply queue all notifications in a queue as you're
>>>> making modifications to the DOM, then right before returning from the
>>>> function you insert a call like "flushAllPendingNotifications()". This
>>>> way you don't have to care at all about what happens when those
>>>> notifications fire.
>>>
>>> So when exactly are these notifications going to be fired?  In
>>> particular, I hope non-DOM Core specifications are going to have
>>> precise control over when they're fired.  For instance, execCommand()
>>> will ideally want to do all its mutations at once and only then fire
>>> the notifications (which I'm told is how WebKit currently works).  How
>>> will this work spec-wise?  Will we have hooks to say things like
>>> "remove a node but don't fire the notifications yet", and then have to
>>> add an extra line someplace saying to fire all the notifications?
>>> This could be awkward in some cases.  At least personally, I often say
>>> things like "call insertNode(foo) on the range" in the middle of a
>>> long algorithm, and I don't want magic happening at that point just
>>> because DOM Range fires notifications before returning from
>>> insertNode.
>>>
>>> Also, even if specs have precise control, I take it the idea is
>>> authors won't, right?  If a library wants to implement some fancy
>>> feature and be compatible with users of the library firing these
>>> notifications, they'd really want to be able to control when
>>> notifications are fired, just like specs want to.  In practice, the
>>> only reason this isn't an issue with DOM mutation events is because
>>> they can say "don't use them", and in fact people rarely do use them,
>>> but that doesn't seem ideal -- it's just saying library authors
>>> shouldn't bother to be robust.
>>
>> In working on Model Driven Views (http://code.google.com/p/mdv), we've
>> run into exactly this problem, and have developed an approach we think
>> is promising.
>>
>> The idea is to more or less take Jonas's proposal, but instead of
>> firing callbacks immediately before the outer-most mutation returns,
>> mutations are recorded for a given observer and handed to it as an
>> in-order sequence at the "end" of the event.
>
> What is the advantage comparing to Jonas' proposal?

You guys did the conceptual heavy lifting WRT this problem. Jonas's
proposal solves the main problems with current mutation events: (1)
they fire too often, (2) they are expensive because of event
propagation, (3) they are crashy WRT some DOM operations.

If Jonas's proposal is the ultimate solution, I think it's a good
outcome and a big improvement over existing spec or tearing out
mutation events. I'm asking the group to consider a few changes which
I'm hoping are improvements.

I'll be happy if I fail =-).

---

My concern with Jonas's proposal is that its semantics depend on
context (inside vs. outside of a mutation notification). I feel like
this is at least a conceptual problem. That, and I kind of shudder
imagining trying to explain to a webdev why and when mutation
notifications are sync vs async.

The place it seems likely to fall down is when someone designs an
abstraction using mutation events and depends on them firing
synchronously -- then they or someone else attempt to use it inside
another abstraction which uses mutation events. How likely is that? I
don't even have a guess, but I'm pretty surprised at the crazy things
people did with current mutation events.

Our proposal's semantics aren't dependent on context.

Additionally, our proposal makes it clear that handling a mutation
notification is an exercise in dealing with an arbitrary number of
ways the DOM could have changed since you were last called. I.e.
providing the list of changes.

In short, I feel like our proposal is just a small tweak on Jonas's.
It is more direct in its form and API about the actually difficultly
of being a mutation observer.

Also, I'll just note a difference in view: We view it as fundamentally
a bad thing to have more than one actor operating at a time (where
"actor" == event handler, or abstraction which observes mutations). It
seems as though you guys view this as a good thing (i.e. All other
problems aside, mutation events *should* be synchronous).

The example I keep using internally is this: an app which uses

a) A constraint library which manages interactions between form values
(observes data mutations, makes data mutations)
b) A templating library (like MDV) which maps data to DOM (observes
both DOM and data mutations, makes both DOM and data mutations)
c) A widget library (like jQuery) which "extends" elements to widgets
(observers DOM mutations, makes DOM mutations)

Our view is that if libraries *can* be built with good robustness, and
they can interact implicitly via mutation observations (i.e. not have
API dependencies), this kind of usage will become common and
desirable. My own and other's experience in systems that provide
several higher level abstractions like this suggests that it's
preferable to avoid a big pile up with everyone trying to act at once.

> I think one could implement your proposal on top
> of Jonas' proposal - especially since both keep the order
> of the mutations.

It would not be sufficient. The missing bit would be a way to run at
the "end" of the event. Right now the best way to approach this is to
capture and delegate all events. MDV, Angular, Sproutcore (and
probably others) do exactly this. The problem is that

(a) It's hard to capture all events. There's a lot of callback surface
area in the web platform to cover.
(b) It doesn't compose. Only one library can play this trick.

> What is "at the 'end' of the event"? You're not talking about
> DOM event here, but something else.

Another way to think of it is this: If Jonas's proposal treated *all*
event callbacks as already handling a mutation event, then we'd have
exactly the timing semantics we're looking for.

> How is that different comparing to "immediately before the outer-most
> mutation"?
>
>>
>> var observer = window.createMutationObserver(callback);
>
> Why is createMutationObserver needed?

Yeah, it's not. In our proposal, "Observers" behave differently (are
called "later" and with batches of changes) from event listeners. We
thought that if the API looked too much like addEventListener, it'd be
confusing for people. Creating an observer seemed like a way to make
it obvious. It could (and probably should) just be a callback.

>
>
>> document.body.addSubtreeChangedObserver(observer);
>> document.body.addSubtreeAttributeChangedObserver(observer);
>> ...
>> var div = document.createElement('div');
>> document.body.appendChild(div);
>> div.setAttribute('data-foo', 'bar');
>> div.innerHTML = '<b>something</b>  <i>something else</i>';
>> div.removeChild(div.childNodes[1]);
>> ...
>>
>> // mutationList is an array, all the entries added to
>> // |observer| during the preceding script event
>> function callback(mutationList) {
>> // mutationList === [
>> //  { type: 'ChildlistChanged', target: document.body, inserted: [div] },
>> //  { type: 'AttributeChanged', target: div, attrName: 'data-foo' },
>> //  { type: 'ChildlistChanged', target: div, inserted: [b, i] },
>> //  { type: 'ChildlistChanged', target: div, removed: [i] }
>> // ];
>> }
>>
>>>
>>> Maybe this is a stupid question, since I'm not familiar at all with
>>> the use-cases involved, but why can't we delay firing the
>>> notifications until the event loop spins?  If we're already delaying
>>> them such that there are no guarantees about what the DOM will look
>>> like by the time they fire, it seems like delaying them further
>>> shouldn't hurt the use-cases too much more.  And then we don't have to
>>> put further effort into saying exactly when they fire for each method.
>>
>> Agreed.
>>
>> For context, after considering this issue, we've tentatively concluded
>> a few things that don't seem to be widely agreed upon:
>>
>> 1) In terms of when to notify observers: Sync is too soon. Async (add
>> a Task) is too late.
>>
>> - The same reasoning for why firing sync callbacks in the middle of
>> DOM operations is problematic for C++ also applies to application
>> script. Calling mutation observers synchronously can invalidate the
>> assumptions of the code which is making the modifications. It's better
>> to allow one bit of code to finish doing what it needs to and let
>> mutation observers operate "later" over the changes.
>>
>> - Many uses of mutation events would actually *prefer* to not run sync
>> because the "originating" code may be making multiple changes which
>> more or less comprise a "transaction". For consistency and
>> performance, the abstraction which is watching changes would like to
>> operate on the final state.
>>
>> - However, typical uses of mutation events do want to operate more or
>> less "in the same event" because they are trying to create a new
>> consistent state. They'd like to run after the "application code" is
>> finished, but before paint occurs or the next scheduled event runs.
>>
>> 2) Because the system must allow multiple "observers" and allow
>> observers to make further modifications, it's possible for an
>> arbitrary number of mutations to have occurred before any given
>> observer is called. Thus is it preferable to simply record what
>> happened, and provide the list to the observer.
>>
>> - An observer can be naive and assume that only "simple" mutations
>> have occurred. However, it's more likely that an observer is an
>> abstraction (like MDV) which only wants to do work WRT to the net of
>> what has happened. This can be done by creating a projection which
>> takes the sequence of mutations as input.
>>
>
>
Received on Thursday, 30 June 2011 23:17:35 GMT

This archive was generated by hypermail 2.3.1 : Tuesday, 26 March 2013 18:49:45 GMT