Angular 4 странное поведение рендеринга - PullRequest
0 голосов
/ 12 сентября 2018

Я разрабатываю сложное угловое веб-приложение с глубоким деревом компонентов.Основная идея этого приложения состоит в том, чтобы показывать различные задачи (какую-то викторину) через видеопоток.Некоторые задачи появляются, когда видео продолжает воспроизводиться, в то время как другие задачи приостанавливают воспроизведение видео, пока задача не будет завершена.Но каждая задача должна появляться точно в указанное время.

Проблема заключается в том, что в некоторых обстоятельствах (я не могу понять, от чего именно это поведение зависит) некоторые задачи появляются со значительным отставанием (5-10 секунд).Такое поведение не является регулярным, поэтому трудно поймать и отладить, в чем причина.Похоже, такое поведение чаще встречается, когда страница «холодный старт», в то время как любая попытка воспроизвести ее на той же странице не приносит успеха.

Вот причины, которые были рассмотрены и отброшены:

  1. Обнаружение изменений.Я думал, что Angular не обнаруживает появление новой задачи и не запускает код рендеринга.Но это не так, потому что:

    • есть явный вызов ChangeDetector.detectChanges () для задачи;
    • код инициализации задачи также воспроизводит звук (через WebAudioApi), и я всегда слышу звук в указанное время без задержки, но визуально элементы отображаются с задержкой.
  2. Браузер сильно загружен какой-то работой и не может показать задачу вовремя.Я полагаю, что это не так, потому что я потратил несколько часов на запись и анализ профиля производительности Chrome и не нашел там большой нагрузки.И наоборот, в проблемные моменты есть подозрительное безделье.Вот пример:

perfomance profile

Проблемы с движком браузера.Я отказался от этой опции, потому что видел это поведение в 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 голосов
/ 11 октября 2018

Наконец я выяснил причину всего этого странного поведения. Причиной является WebAudioApi. Когда я убрал воспроизведение звука из кода инициализации задачи, внешний вид задачи стал плавным и точным. Поэтому я перешел с WebAudioApi на HTML5 Audio, и теперь все в порядке.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...