Re: Mutation events replacement

On 07/06/2011 12:06 AM, Rafael Weinstein wrote:
> 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.

So "outer script event" (the word 'event' is quite misleading here)
is something like 
http://www.whatwg.org/specs/web-apps/current-work/multipage/webappapis.html#synchronous-section
except that DOM mutations are allowed?


>
> 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:19:32 UTC