- From: Olli Pettay <Olli.Pettay@helsinki.fi>
- Date: Wed, 06 Jul 2011 00:18:21 +0300
- 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: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