Angular 4奇怪的渲染行为

时间:2018-09-12 15:25:23

标签: javascript angular

我用深层组件树开发复杂的Angular Web应用程序。该应用程序的主要思想是通过视频流显示不同的任务(某种琐事游戏)。视频继续播放时会出现一些任务,而其他任务会暂停视频直到任务完成。但是每个任务都应该在指定的时间准确显示。

问题在于,在某些情况下(我无法弄清此行为确切取决于什么),某些任务会出现明显的延迟(5-10秒)。此行为是不正常的,因此很难捕获和调试其原因。在页面“冷启动”时似乎更经常发生这种行为,而在同一页面上重现它的任何尝试都不会带来任何成功。

以下是已被考虑并丢弃的原因:

  1. 更改检测。我以为Angular不会检测到新的任务外观,也不会运行渲染代码。事实并非如此,因为:

    • 有明确的ChangeDetector.detectChanges()调用出现在任务上;
    • 任务初始化代码也可以播放声音(通过WebAudioApi),我总是在指定的时间内听到声音而没有任何滞后,但是视觉上出现了滞后。
  2. 浏览器负担很多工作,无法及时显示任务。我想事实并非如此,因为我花了几个小时来记录和分析Chrome的性能概况,并且在那里没有发现任何沉重的负担。相反,问题时刻有可疑的闲置。这是示例:

perfomance profile

  1. 浏览器引擎特定的问题。我放弃了此选项,因为我在Chrome,Safari和Firefox(在Windows,MacOS,iOS上)中看到了此行为

以下是与任务外观相关的代码片段:

quest.component.ts

@Component({
    selector: 'quest',
    templateUrl: 'quest.component.html',
    providers: [
        HintsManagerService,
        UserHintService,
        HearNoteSyncService
    ]
})
export class QuestComponent implements OnInit, OnDestroy, AfterViewInit {
    private questPaused = false;
    private subscriptions: Subscription[] = new Array<Subscription>();
    private currentTime: number;
    private maxReward = 0;
    private currentReward = 0;
    private videoProportion: [number, number];
    private wrappedTasks: TaskWrapper[];
    private lastActivatedTask = -1;
    private questPass: QuestPass = new QuestPass();
    private forciblyClosed = false;
    private isFullScreenActivated = false;
    private anticipationTime: number = 0.05;
    private rewindThresholdTime: number = 1;
    private anyTaskTutorial: boolean = false;

    @Input() settings: QuestSettings;
    @Input() quest: Quest;
    @Input() config: QuestConfig;
    @Input() scoreUnit = 'coin';
    @Input() battleId: number;
    @Input() videoChallengeRoundId: number;
    @Output() questFinished = new EventEmitter<{ score: number, forciblyClosed: boolean, questPass: QuestPass }>();
    @ViewChild('player') videoPlayer: PlayerComponent;
    @ViewChild('topPlayer') topPlayer: ElementRef;

    constructor(
        private store: Store<AppState>,
        private playerTimerService: PlayerTimerService,
        private changeDetector: ChangeDetectorRef,
        private hintManagerService: HintsManagerService,
        private tutorialService: TutorialService,
        private soundService: SoundService) {
    }

    ngOnInit() {
        this.subscriptions.push(this.store.select(state => state.gameControl.questEnd)
            .subscribe(questEnd => { if (questEnd) { this.stopQuest(); } }));
        this.subscriptions.push(this.store.select(state => state.gameControl.pause)
            .subscribe(pause => { this.questPaused = pause; }));

        this.wrappedTasks = new Array<TaskWrapper>();
        this.quest.tasks.forEach(value => {
            this.wrappedTasks.push({
                startTime: value.startTime,
                active: false,
                activated: false,
                task: value,
                showTaskTutorial: this.tutorialService.shouldShowTaskTutorial(this.quest, value)
            } as TaskWrapper);
            this.maxReward += value.getTotalReward();
        });
        this.wrappedTasks.sort((a: TaskWrapper, b: TaskWrapper) => a.startTime - b.startTime);
        this.anyTaskTutorial = this.wrappedTasks.some(wt => wt.showTaskTutorial);
        this.hintManagerService.initialize(this.wrappedTasks.map(wt => wt.task), this.quest, this.settings, this.battleId, this.videoChallengeRoundId);
    }

    ngAfterViewInit() {
        this.subscriptions.push(this.videoPlayer.onUserRequestPlayPause
            .subscribe(value => this.tryPlayPause(value)));
        this.subscriptions.push(this.playerTimerService.timerUpdated
            .subscribe(value => {
                if (!this.questPaused) {
                    this.currentTime = value;
                    this.checkActiveTasks();
                }
            }));
    }

    answerClick(task: QuestTask, result: TaskPassResult) {
        if (result.isCorrect) {
            this.currentReward += task.reward;
        }

        let currentScore = 0;
        switch (this.scoreUnit) {
            case 'coin':
                currentScore = Math.round(this.quest.coinReward * this.currentReward / this.maxReward);
                break;
            case 'percent':
                currentScore = Math.round(100 * this.currentReward / this.maxReward);
                break;
        }
        this.videoPlayer.updateScore(currentScore);

        if (result.isCorrect) {
            this.soundService.play('right-answer');
        } else {
            this.soundService.play('wrong-answer');
        }
    }

    checkActiveTasks() {
        this.hintManagerService.checkTasksIntersection(this.currentTime);
        for (let i = this.lastActivatedTask + 1; i < this.wrappedTasks.length; ++i) {
            if (this.wrappedTasks[i].startTime - this.anticipationTime > this.currentTime) {
                break;
            } else if (this.wrappedTasks[i].startTime - this.anticipationTime <= this.currentTime && !this.wrappedTasks[i].activated && !this.wrappedTasks[i].active) {
                this.wrappedTasks[i].tutorials = this.settings.tutorialEnabled
                    ? this.tutorialService.getTutorialsForTask(this.anyTaskTutorial, this.wrappedTasks[i].showTaskTutorial, this.wrappedTasks[i].task)
                    : [];
                this.wrappedTasks[i].active = true;
                this.wrappedTasks[i].activated = true;
                this.lastActivatedTask = i;
                if (this.wrappedTasks[i].task.pauseRequired) {
                    this.store.dispatch({ type: VIDEO_PAUSE, payload: true });
                    this.videoPlayer.setPlaybackTime(this.wrappedTasks[i].startTime);
                }
                this.changeDetector.detectChanges();
                // if we skipped too much time, then do rewind and stop cycle to prevent simultaneous task activation
                if (this.currentTime - this.wrappedTasks[i].startTime > this.rewindThresholdTime) {
                    // if not yet rewinded
                    if (!this.wrappedTasks[i].task.pauseRequired) {
                        this.videoPlayer.setPlaybackTime(this.wrappedTasks[i].startTime);
                    }
                    break;
                }
            }
        }
    }

    deactivateTask(wrappedTask: TaskWrapper) {
        wrappedTask.active = false;
    }

    closeQuest() {
        this.forciblyClosed = true;
        this.videoPlayer.close();
    }

    stopQuest() {
        this.questFinished.emit({
            score : this.currentReward / this.maxReward,
            forciblyClosed: this.forciblyClosed,
            questPass: this.questPass
        });
    }

    tryPlayPause(paused: boolean): void {
        const canPause = this.wrappedTasks.filter(wt => wt.active && wt.task.type !== TaskType.hear && wt.task.type !== TaskType.note).length === 0;
        if (canPause) {
            this.questPaused = !paused;
            this.store.dispatch({ type: PAUSE });
        }
    }

    ngOnDestroy() {
        this.subscriptions.forEach((subscription: Subscription) => {
            subscription.unsubscribe();
        });
        this.hintManagerService.destroyService();
        this.store.dispatch({ type: RESET });
    }
}

quest.component.html

<ng-container *ngIf="quest">
    <div class="top__player" #topPlayer [ngClass]="topPlayer.offsetHeight | questSize : applySizePipe">
        <player class="player" #player
            [quest]="quest"
            [startTime]="quest?.startTime" 
            [duration]="quest?.duration" 
            [scoreUnit]="scoreUnit" 
            [hintsEnabled]="settings?.hintsEnabled" 
            (onQuestInterrupted)="stopQuest()">
            <ng-container *ngFor="let wrappedTask of wrappedTasks">
                <ng-container [ngSwitch]="wrappedTask.task.type" *ngIf="wrappedTask.active">
                    <quiz-task *ngSwitchCase="'quiz'"
                        [tutorialTypes]="wrappedTask.tutorials"
                        [task]="wrappedTask.task" 
                        (onAnswer)="answerClick(wrappedTask.task, $event)" 
                        (onDeactivate)="deactivateTask(wrappedTask)">
                    </quiz-task>
                    <hidden-area-task *ngSwitchCase="'hidden-area'"
                        [tutorialTypes]="wrappedTask.tutorials"
                        [task]="wrappedTask.task" 
                        (onAnswer)="answerClick(wrappedTask.task, $event)" 
                        (onDeactivate)="deactivateTask(wrappedTask)">
                    </hidden-area-task>
                    <whats-next-task *ngSwitchCase="'whats-next'"
                        [tutorialTypes]="wrappedTask.tutorials"
                        [task]="wrappedTask.task" 
                        (onAnswer)="answerClick(wrappedTask.task, $event)" 
                        (onDeactivate)="deactivateTask(wrappedTask)">
                    </whats-next-task>
                    <hear-note-task *ngSwitchCase="'hear'"
                        [tutorialTypes]="wrappedTask.tutorials"
                        [task]="wrappedTask.task"
                        [taskType]="'hear_box'"
                        [reactionTime]="config.hearReactionTime"
                        (onAnswer)="answerClick(wrappedTask.task, $event)" 
                        (onDeactivate)="deactivateTask(wrappedTask)">
                    </hear-note-task>
                    <hear-note-task *ngSwitchCase="'note'"
                        [tutorialTypes]="wrappedTask.tutorials"
                        [task]="wrappedTask.task" 
                        [taskType]="'note_box'"
                        [reactionTime]="config.noteReactionTime"
                        (onAnswer)="answerClick(wrappedTask.task, $event)" 
                        (onDeactivate)="deactivateTask(wrappedTask)">
                    </hear-note-task>
                    <div *ngSwitchDefault>Unknown type of task</div>
                </ng-container>
            </ng-container>
        </player>
    </div>
</ng-container>

1 个答案:

答案 0 :(得分:0)

最后,我找出了所有这种奇怪行为的原因。原因是WebAudioApi。当我从任务初始化代码中删除播放声音时-任务外观变得平滑且准确。所以我从WebAudioApi切换到HTML5 Audio,现在一切正常。