jQuery:占用资源 - firebug Time 和 Profile 在时间上不一致

jQuery: Hogging resources - firebug Time and Profile disagree on time

提问人:Eli 提问时间:3/11/2010 最后编辑:Eli 更新时间:3/11/2010 访问量:258

问:

我有几百行jQuery代码,这些代码从数据库中提取项目和子项目的列表,构建一系列div来显示它们,并将它们添加到列表中。

如有必要,我可以展示代码,尽管我无法想象有人会想要完成所有代码,但我遇到的问题是我的一个函数在实际上不应该花费大约 6 秒的时间。

这是一个从返回的数据写入列表的函数。因此,它采用一个模板 div,从一个 JavaScript 对象(大约 15 个字段)向它添加数据,并将其附加到另一个 div 以生成列表。

奇怪的是,同一个函数在 init 运行一次(作为更大代码块的一部分),并且运行良好,但如果我再次运行它,它本身大约需要 6 秒。

如果我将 profile() 和 profileEnd() 与 firebug 一起使用,它说它大约需要 1.1 秒,但是,如果我使用 time() 和 TimeEnd(),它会说(正确)大约 6.7 秒。

这个函数不会从服务器获取任何数据 - 它只使用以前从服务器获取的数据,所以我很确定我们不会等待任何外部资源。此外,在 firefox 运行的 100 秒内,我的处理器使用率为 6%。

有人知道jQuery的任何真正的Gotcha区域吗?我记得有一次我遇到过这样的问题,最终是由于 .find() 不够详细。

我将尝试缩小问题所在的范围,以便我可以展示代码,但肯定对jQuery专家的任何一般想法持开放态度。

谢谢!


编辑二

它似乎与jQuery的.data()有关。

还不知道为什么,但这段代码:

// looping...
console.time('main');
    // SortVal is already in the data, so no need to add it here.
    Parent.data('SortChildrenBy', SectionsAttr[i]);
    Parent.data('SortChildrenDir', 'asc');
console.timeEnd('main');

仅对于这两行代码,每个循环就会产生大约 100 毫秒,而以下代码:

console.time('a');
Parent.data({'SortVal':Parent.data('SortVal'),'SortChildrenBy':SectionsAttr[i], 'SortChildrenDir':'asc'});
console.timeEnd('a');

每个循环的结果约为 1 ms。

也许是因为第一个需要与现有数据进行比较,而第二个只是擦除数据并插入?


编辑

根据安德鲁的说法,这是似乎占用大部分时间的方法。

        this.InsertTask = function(Position, task){
        console.time("InsertTask");
            /*
                Find the 'path' where the task will be inserted, based on the sort.
                Each sort item will be a section of the list, or a subsection of a section, with the tasks inserted as the leaves.
                ie: Sorted by DueDateTS, ClientID, UserID
                the task would go in <div id='...[DueDateTS]'> <div id='...[ClientID]'> <div id='...[UserID]'>  </div> </div> </div>

                REMEMBER WE ARE NOT ACTUALLY SORTING HERE, SO DON'T USE UP RESOURCES, JUST ADD THEM TO THE CORRECT SECTION.
                THEY WILL BE SORTED LATER AFTER THEY ARE ALL ADDED.

            */

            var SortItem = [];
            var SectionsAttr = [];
            var SectionsVal = [];
            var SectionsLabel = [];
            var TaskElID = '';

            // Build the sections based on the order by.
            for ( i in this.data.order.by ) {

                SortItem['attr'] = this.data.order.by[i].split(':')[0];
                SortItem['dir'] = this.data.order.by[i].split(':')[1];
                SortItem['label'] = this.data.order.by[i].split(':')[2];

                // Remove any characters that will give us problems using this as part of an html attribute (id in particular) later.
                SectionsAttr[i] = SortItem['attr'];
                SectionsVal[i] = task[SortItem['attr']];
                SectionsLabel[i] = task[SortItem['label']];

                // Force the values to string.
                SectionsAttr[i] = SectionsAttr[i]+"";
                SectionsVal[i] = SectionsVal[i]+"";
                SectionsLabel[i] = SectionsLabel[i]+"";

                // Remove any characters from the attribute and value that will give us problems using this as part of an html attribute (id in particular) later.
                // Remember tha the label is NOT sanitized and should only be used in HTML TEXT.
                SectionsAttr[i] = SectionsAttr[i].replace(/[^0-9a-zA-Z-_.]/,'');
                SectionsVal[i] = SectionsVal[i].replace(/[^0-9a-zA-Z-_.]/,'');


            }

            TaskSectionID ='tl_section_'+SectionsAttr.join('-')+'_'+SectionsVal.join('-');

            // Build the actual section elements for the taskel.
            // Each will be added, and then be the parent to it's sub sections, if there are any.
            // The initial parent is the tasklist itself.
            var Parent = $('#tasklist');
            var ElID ='';
            var ElIDAttr = '';
            var ElIDVal = '';
            var CurrentEl = null;
            var Level = 0;

            for (i in SectionsAttr) {
                // @todo - SOMETHING WRONG WITH THIS SECTION.  SEEMS LIKE THE FIRST HEADER UNDER R&G GOES TO THE WRONG PARENT, WHEN IT'S SORTED BY CLIENT/USER ONLY.
                // Count how many levels down we are.
                Level++;

                // Build the attribute list to be used in the element name for uniqueness.
                if ( ''!=ElIDAttr ) {
                    ElIDAttr +='-';
                    ElIDVal +='-';
                }
                ElIDAttr +=SectionsAttr[i];
                ElIDVal +=SectionsVal[i];
                ElID =  'tl_section_'+ElIDAttr + "_" + ElIDVal;

                // If the section doesn't have an element (div) then create it.
                if ( 0==Parent.children('#'+ElID).length ) {

                    // Set the sort directive for this level, stored in the parent.
                    // The actual value will be stored in the level item.
                    Parent.data('SortChildrenBy', SectionsAttr[i]);
                    Parent.data('SortChildrenDir', 'asc');

                    // Make the section container in the parent.
                    var SectionContainer = $('<div>', {
                        id : ElID,
                        class : 'tl_section',
                    })
                    .data('SortVal', SectionsLabel[i])
                    .appendTo(Parent);

                    //Parent.append( SectionContainer );

                    // Make the header, which will be used for the section summary.
                    HLevel = 4 < Level ? 4 : Level;// 4 is the max level for css...
                    var SectionContainerHeader = $('<h'+HLevel+'>', {
                        id : ''+ElID+'_Header',
                        class : 'tl_section_header tl_section_h'+HLevel,
                        text : SectionsLabel[i]
                    });

                    SectionContainer.append(SectionContainerHeader);

                    if ( !this.settings.showheaders ) {
                        SectionContainerHeader.hide();
                    }

                } else {

                    // The section container is previously created (by another task in it or one of it's sub section containers.)
                    SectionContainer = $('#'+ElID);

                }

                // The section container is considered the parent for the rest of this method, as we are focused on the actual task container being inserted into it.
                Parent = SectionContainer;
            }

            Parent = SectionContainer = null;

            TaskSectionEl = $('#'+TaskSectionID);

            /*
            *   Validate the position where this task is to be inserted.
            *   The inserted task will take the place of an existing task at that position, and push it and all children after it down by one.
            *   They are indexed from zero.  Invalid or out of range goes to the end, but should not generate an error, as the position null or -1 may be used to mean 'append to the end'.
            */

            // The max position is equal to the number of tasks.
            var MaxPosition = $(TaskSectionEl).children('.tasklist_item').length;

            // If the position is an invalid index (not between 0 and max position), then set it to the max position and append it to the list.
            // In the event that max position is 0, then so will be position.
            if ( ( Position < 0 ) || ( MaxPosition < Position ) || ( Position != parseInt(Position) ) ) {
                Position = MaxPosition;
            }

            /*
            *   Create a new task from the template, and append it ot the list in it's proper position.
            *   Be sure not to make it visible until we are done working on it, so the page won't have to reflow.
            */

            // Copy the template, make a jquery handle for it.
            //  Leave it invisible at this point, so it won't cause the page to reflow.
            TaskEl = $('#template_container .task')
                .clone()
                .attr('id', 'tasklist_item_'+task.TaskID)
                .data('TaskID', task.TaskID)
                .removeClass('task')
                .addClass('tasklist_item');

            // Hide the container.
            HeaderTemp = SectionContainerHeader.detach();

            // Insert the new task
            if ( 0 == Position ) {
                $(TaskSectionEl).prepend( TaskEl );
            } else {
                $(TaskSectionEl).children('.tasklist_item').eq(Position-1).after( TaskEl );
            }

            HeaderTemp.prependTo(TaskSectionEl);

            // The title area.
            var TaskTitle = TaskEl.find('.task_title')
                .attr('id', 'task_title_'+task.TaskID)
                .text( task.Title );

            // Add the context menu to pending tasks.
            if ( 0 == task.Completed ) {
                ContextMenuPendingTask(TaskTitle);
            }

            // The subtitle area.

            // Hide the body.
            TaskEl.find('.header_collapse').hide();
            TaskEl.find('.task_bodycontainer').hide();

            // The exp/collapse buttons.
            TaskEl.find('.header_expand').bind('click', function(){
                global_this.ToggleTask( $(this).closest('.tasklist_item'), 'exp' );
            });

            TaskEl.find('.header_collapse').bind('click', function(){
                global_this.ToggleTask( $(this).closest('.tasklist_item'), 'col' );
            });

            // Show the date and time.
            TaskEl.find('.task_duedate')
            .text(task.DueDateV + (null === task.DueTimeV ? "" : " "+task.DueTimeV+""));

            // Tweak for due status.
            switch( task.DueStatus ){
                case 1:/* Nothing if pending.*/break
                case 0:TaskEl.find('.task_duedate').addClass('task_status_due');break;
                case -1:TaskEl.find('.task_duedate').addClass('task_status_pastdue');break;
            }

            // The other subtitle items.
            TaskEl.find('.task_clientname').html(task.ClientName).before('<span> &bull; </span>');

            //$('#'+TaskIdAttrib+'  .task_ownername').html(task.OwnerName);
            // If the user is different from the owner.
            if ( task.OwnerID != task.UserID ) {
                TaskEl.find('.task_username').html(task.UserName).before('<span> &bull; </span>');
            }

            // SubTask Count
            /*
            if ( 0 < task.SubTaskCount ) {
                $('#'+TaskIdAttrib+'  .task_subtaskcount').html("("+task.SubTaskCount+" Subtasks)");
            }
            */

            // Note Count
            if ( 0 < task.NoteCount ) {
                TaskEl.find('.task_notecount').html("("+task.NoteCount+" notes)");
            }

            // Body.
            TaskEl.find('.task_body').html(task.Body);

            // If the task is marked done.
            if ( 0 == task.Completed ) {
                // The task done checkbox.
                TaskEl.find('.task_header .task_done').bind('change', function(){
                    if ($(this).attr('checked')) {
                        global_this.MarkTaskDone($(this).closest('.tasklist_item').data('TaskID'));
                    }
                })
            } else {
                // The task done checkbox.
                TaskEl

                    .find('*')
                    .andSelf()
                    .css({
                        color: '#aaaaaa',
                    })

                    .find('.task_header .task_done')
                    .remove()
                    .end()

                    .find('.task_header .task_title')
                    .after('<br /><span>Done by '+task.CompletedByName+' on '+task.CompletionDateV+'</span>')
                    .end()

                    .find('.subtask_header .subtask_done')
                    .remove()
                    .end()

                    ;

            }

            for ( var i in task.SubTasks ) {

                subtask = task.SubTasks[i];

                ParentSTIDAttrib = null === subtask.ParentST ? TaskEl.attr('id')+' > .task_bodycontainer' : 'SubTask_'+subtask.ParentST;

                SubTaskEl = $('#template_container .subtask')
                    .clone()
                    .attr('id', 'SubTask_'+subtask.SubTaskID)
                    .data('SubTaskID', subtask.SubTaskID)
                    .appendTo('#'+ParentSTIDAttrib+' > .task_subtaskcontainer');

                    SubTaskEl.find('.subtask_title').html(subtask.Title);

                    if ( 0 != subtask.DueDiff ) {
                        SubTaskEl.find('.subtask_duediff').html(" (Due "+subtask.DueDiffDateV+")");
                    }

                    if ( task.OwnerID != subtask.UserID ) {
                        SubTaskEl.find('.subtask_username').html(subtask.UserName).before('<span> - </span>');
                    }

                if ( 0 == subtask.Completed ) {

                    // The sub task done checkbox.
                    SubTaskEl.find('.subtask_header > .subtask_done').bind('change', function(){
                        if ($(this).attr('checked')) {
                            global_this.MarkSubTaskDone($(this).closest('.subtask').data('SubTaskID'));
                        }
                    });

                } else {

                    SubTaskEl
                    .find('.subtask_header > .subtask_done')
                    .attr('disabled', 'disabled')
                    .attr('checked', 'checked')
                    .end()

                    .find('.subtask_header .subtask_title')
                    .after('<span>Done by '+subtask.CompletedByName+' on '+subtask.CompletionDateV+'</span> ')
                    .end();

                }

            }

            for (var i in task.Notes) {

                NoteEl = $('#template_container .note')
                    .clone()
                    .attr('id', 'Note_'+task.Notes[i].NoteID)
                    .appendTo( TaskEl.find('.task_notecontainer') );

                    NoteEl.find('.note_date').html(task.Notes[i].NoteDateV);
                    NoteEl.find('.note_user').html(task.Notes[i].UserName);
                    NoteEl.find('.note_body').html(task.Notes[i].Note);

            }

            // Put the client id into the templates data.
            TaskEl.data('TaskID', task.TaskID);

            console.timeEnd("InsertTask");

            return;
        }
jQuery 优化

评论


答:

1赞 Nick Craver 3/11/2010 #1

为此,我会启动dynaTrace AJAX Edition。这是一个非常详细的、仍然免费的 javascript 分析器。它是特定于IE的,但是如果您有一般的性能问题,并且不仅限于FireFox,那么它是一个很好的工具。

加载页面,运行花费了这么长时间的内容,并启动探查器中的热点,以快速查看哪些代码路径花费了大量时间。

0赞 Andrew Hedges 3/11/2010 #2

如果没有看到代码,很难说,但 DOM 操作对诸如将循环中的节点直接附加到文档片段还是文档片段等问题非常敏感。是否可以发布代码的简化大小写?document.body

评论

0赞 Eli 3/11/2010
它并不完全完善,但我添加了似乎占用最多时间的方法。它有时会对同一数据运行多次,因此列表将被删除,然后重新构建。它将使用一些相同的 ID,但直到它们被删除后才会使用。