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

Re: Mutation events replacement

From: Olli Pettay <Olli.Pettay@helsinki.fi>
Date: Wed, 06 Jul 2011 01:24:30 +0300
Message-ID: <4E138F1E.8050407@helsinki.fi>
To: Rafael Weinstein <rafaelw@google.com>
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 07/06/2011 12:48 AM, Rafael Weinstein wrote:
> On Tue, Jul 5, 2011 at 2:38 PM, Olli Pettay<Olli.Pettay@helsinki.fi>  wrote:
>> On 07/06/2011 12:18 AM, Olli Pettay wrote:
>>>
>>> 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?
>>>
>>
>>
>> What is the reason to require a new mechanism for
>> async handling? Could listeners be handled in
>> a task?
>> Basically, if DOM is mutated during task A, a new
>> task, B, is scheduled and all the mutation listeners will be called
>> there.
>
> It's too late by then. Most importantly, visual artifacts of
> incomplete DOM work may have been seen by the user.


If that is the reason, I don't think it really holds.
The script may force a layout flush right after
DOM mutation and then cause some popup to shows up which
may cause repainting in the main page.
Listeners would be called only after that.
This all might be highly browser engine dependent
(I'll test this on Chrome tomorrow)

-Olli


>
> Mutation observers serve the purpose of doing secondary work on behalf
> of the application script (typically, they create further abstractions
> not implemented by the platform). They are conceptually *a part of*
> the work being done during any given script event. The application
> isn't back to a consistent state until they are 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 22:25:22 GMT

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