W3C home > Mailing lists > Public > public-webapps@w3.org > July to September 2011

Re: Mutation events replacement

From: Rafael Weinstein <rafaelw@google.com>
Date: Tue, 5 Jul 2011 14:06:40 -0700
Message-ID: <CABMdHiRXDiTY7x6PRxYsZcSJYhe++8_yZSYL7ohzJBXzMfJR0A@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>
Respond

On Tue, Jul 5, 2011 at 10:44 AM, Olli Pettay <Olli.Pettay@helsinki.fi> wrote:
> On 07/01/2011 02:17 AM, Rafael Weinstein wrote:
>>
>> 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.
>>
>
>
> You didn't still answer to the question
> 'What is "at the 'end' of the event"?' ;)
>
> I'd really like to understand what that means.

Yes. Sorry. This does need more detail.

I'm not an expert on the HTML spec or implementation, but I've talked
with James Robinson & Tab Atkins and am hopeful that there is a way to
spec and implement it.

Maybe one of them will chime in here with a more precise definition? =-)

Here's how I think about it:

Immediately after the UA finishes executing an outer (more below)
script event, it invokes a "notifyObservers" (1) function which starts
calling back any mutation observers, handing them their list of
mutation records. It continues doing so until all mutation records are
delivered.

Note that mutations *may* occur during this phase. They are simply
added to the list of pending mutations to be delivered. When all
mutations are delivered, this phase exits and the UA returns.

This phase (notifyObservers) is more or less a finalization phase and
would be considered a part of the outer-most script for the purposes
of the UA's poorly-behaved script timer (the work being done here is
the same work that previously would have been done synchronously,
during the event -- now it is more or less put into an inner queue
which runs when outer event's stack winds down to 0).

An "outer" script event is a script event which it NOT the result of
an event dispatch which occurred as a result of a synchronous action
taken by an already running script event. I.e. secondary, sync script
events do not deliver mutation records on exit -- only when control is
about to return the UA to select a new Task to run.

1: One approach for notifyObservers / enqueueMutation:
http://code.google.com/p/mdv/source/browse/trunk/platform/observers.js


>
>
> -Olli
>
>
>
>> 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 Tuesday, 5 July 2011 21:07:07 GMT

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