Angular 2 - Why do I need zone.run()?

2020-02-11 04:54发布

问题:

I am trying to create a component in Angular 2 which displays data from a service. The service basically loads some data from a json file after some input from the user. I've been trying to get the component to update but it does not seem to recognize changes unless I call zone.run() after submitting an event from from my service. My code is as follows for the component...

@Component({
    selector: 'assess-asset-group',
    directives: [AssetComponent, AssetHeaderComponent, NgFor, NgIf],
    template: `

            <div *ngIf="assetService.schema != null">
                <div class="asset-group" *ngFor="#assetTypeName of assetService.schema.assetTypeNames"> 
                    <div class="asset-type-title"><span>{{assetService.schema.assetTypes[assetTypeName].name}}s</span></div> 
                    <table class="asset-group-table" cellpadding=0 cellspacing=0>
                        <thead>
                            <tr assess-asset-header [assetType]="assetService.schema.assetTypes[assetTypeName]"></tr>
                        </thead>
                        <tbody>
                            <tr assess-asset *ngFor="#asset of assetService.assetsForType(assetTypeName)" [asset]="asset"></tr>
                        </tbody>
                    </table>
                    <button class="new-asset-btn" (click)="assetService.addAsset(assetTypeName)">New</button>
                </div>
            </div>`,
    providers: [provide(AssetService, {useValue: injector.get(AssetService)})]
})
export class AssetGroupComponent {

    public assetService: AssetService;
    public zone: NgZone;

    constructor( @Inject(AssetService) assetService: AssetService, zone: NgZone) {
        this.assetService = assetService;
        this.zone = zone;
    }

    ngOnInit() {
        this.assetService.proejectLoadedEmitter.subscribe((e) => { this.zone.run(() => { }) });
    }

    ngOnDestroy() {
        this.assetService.proejectLoadedEmitter.unsubscribe();
    }
}

Am I doing something wrong or is this what I need to do in order to update the view?

UPDATE - AssetService Class

@Injectable()
export class AssetService{
    public assets: Assets.Asset[] = [];
    public assetTypeDefinitions: any = null;

    public schema: Schema = null;
    public assetsAsObj: any = null; // Asset file loaded as object

    @Output() proejectLoadedEmitter: EventEmitter<any> = new EventEmitter();

    constructor(){
    }

    public loadProject(config: Project){
        // Load schema
        // populate AssetTypeDefinitions as object keyed by type
        let data = fs.readFileSync(config.schemaPath, 'utf8');
        if (!data) {
            utils.logError("Error reading schema file");
            return;
        }
        let struc = fs.readFileSync(config.structurePath, 'utf8');
        if (!struc) {
            utils.logError("Error reading structure file");
            return;
        }

        this.schema = new Schema(JSON.parse(data), struc);
        this.readAssets(config.assetFilePath);
    }

    /**
     * @brief Adds a new asset to the assets array 
     * @details Constructs the asset based on the type and populates
     *  its fields with appropreiate default values
     * 
     * @param type The type of the asset - specified in the schema
     */
    public addAsset(type: string): void {
        // Need to make sure there is a loaded type definition for the specified type
        if(!this.schema.assetTypes.hasOwnProperty(type)){
            utils.logError("Error occured during call to addAsset - type \"" + type + "\" is not specified in the loaded schema");
            return;
        }
        // Creeate a new asset object - passing in the type definition from the schema
        this.assets.push(new Assets.Asset(this.schema.assetTypes[type]));
    }   

    /**
     * Write the current assets to a file using the specified format
     * If the outputPasth isn't specied try and load it from the project.json file
     */
    public writeAssets(format:AssetWriteFormat, outputPath?: string) : void {

        var outStructureStr = this.schema.structureStr;
        // insert AS properties from schema into output assets
        this.schema.properties.forEach(prop => {
            outStructureStr = outStructureStr.replace(new RegExp('"' + prop +'"', 'i'), this.retriveValueForSchemaProperty(prop));
        });

        fs.writeFileSync("C:/Projects/Assess/assets.json", outStructureStr);
    }

    public readAssets(inputPath?: string) : void{
        let assetsStr = fs.readFileSync(inputPath, 'utf8');

        let strucToAssetMap = {};
        let strucObj = JSON.parse(this.schema.structureStr);
        this.schema.properties.forEach(p => {
            strucToAssetMap[p] = this.findValueInObject(strucObj, p).reverse();
        });

        // @TODO Load custom properties
        let assetsObj = JSON.parse(assetsStr);
        var c = null;
        strucToAssetMap["AS_ASSETS"].forEach(p => {
            if(c == null){
                c = assetsObj[p];
            }else{
                c = c[p];
            }
        });
        c.forEach((asset) => {
            let a:Assets.Asset = new Assets.Asset(this.schema.assetTypes[asset.type], asset);
            this.assets.push(a);
        });
        console.log(this.assets);
        this.proejectLoadedEmitter.emit(null);
    }

    public assetsForType(type:string): Assets.Asset[]{
        var ret: Assets.Asset[] = [];
        for(let idx in this.assets){
            if(this.assets[idx].definition.type === type){
                ret.push(this.assets[idx]);
            }
        }
        return ret;
    }

    public retriveValueForSchemaProperty(property: string) : string{
        if(AS_SchemaTypes.indexOf(property) != -1){
            switch (property) {
                case "AS_ASSETS":
                    let outAssets = [];
                    this.assets.forEach((asset) => {
                        let outAsset = {};
                        outAsset["type"] = asset.definition.type;

                        for (let key in asset.fields) {
                            outAsset[key] = asset.fields[key].value;
                        }
                        outAssets.push(outAsset);
                    });
                    return JSON.stringify(outAssets, null, "\t");
            }
        }else{
            // @TODO Retrive custom properties
            return '"DDDDDD"';
        }
        return "";
    }

    public findValueInObject(obj: any, property: string, path: any[] = []): any[] {
        for(let x in obj){;
            let val = obj[x];
            if (val == property){
                path.push(x);
                return path;
            }
            else if(val != null && typeof val == 'object'){
                let v = this.findValueInObject(val, property, path);
                if(v != null){
                    path.push(x);   
                    return path;
                }
            }
        }
        return null;
    }
}

回答1:

This would require knowledge about the inner workings of the AssetService you're using.

Angular runs the code of your components within its zone where most async APIs (addEventListener, setTimeout, ...) are patched so the zone can notify Angular when such an async callback has happend. This is when Angular runs change detection.

If you initialized AssetService outside Angular or AssetService by other means executes code outside Angulars zone, then Angular doesn't get notified about happened async callbacks and doesn't run change detection.

With zone.run(...) you explicitely make code execute inside Angulars zone and change detection is run afterwards.



回答2:

Is it possibly caused by referencing assetService, the argument, and not this.assetService, in your views? Maybe that is causing Angular's change detection not to be triggered without calling zone.run().