访问器属性的响应式问题(Vue2)

该文章已生成可运行项目,

问题背景

一开始需求是普通选择时间段,实现如下:

<el-time-picker 
    is-range
    v-model="formData.dutyTime" 
    range-separator="-" 
    start-placeholder="开始时间" 
    end-placeholder="结束时间"
    prefix-icon="123"
    :clearable="false" 
    format="HH:mm" 
    value-format="HH:mm" 
    style="width: 110px; margin-bottom: 4px;"  
    align="center"
/>
​
// formData.dutyTime 输出格式:["00:00", "23:59"]

需求改动:

  1. 时间段可设置多个;

  2. 时间段跨天情况(如:22:00-2:00),仅在开始时间大于结束时间时判定为跨天。

根据需求要求,需要改动 dutyTime 属性为时间段数组,并且开始、结束时间分别由两个 el-time-picker 组件控制。修改如下:

<div v-for="(item, index) in formData.dutyTime" :key="index">
    <el-time-picker 
		prefix-icon="123"
		:clearable="false" 
		format="HH:mm" 
		value-format="HH:mm" 
		style="width: 110px; margin-bottom: 4px;"
		v-model="item[0]" 
		align="center"
	/>
    -
    <el-time-picker 
		prefix-icon="123"
		:clearable="false" 
		format="HH:mm" 
		value-format="HH:mm" 
		style="width: 110px; margin-bottom: 4px;"
		v-model="item[1]" 
		align="center"
	/>
</div>

<script>
    export default {
        data() {
            return {
                formData: {}
            }
        },
    },
    methods: {
        addClick() {
            this.formData = {
                // dutyTime: ['00:00', '23:59'], // 原需求初始值
                dutyTime: [['00:00', '23:59']], // 修改需求后初始值
            };
        },
    }
</script>

一切正常。

因为很多地方都要创建 dutyTime 的对象,所以将 dutyTime 中的对象的创建方式封装。如下:

export class TimeRange {
    timeRange = ['00:00', '23:59'];
    constructor(startTime = '00:00', endTime = '23:59') {;
        this.timeRange[0] = startTime;
        this.timeRange[1] = endTime;
    }
    get startTime() {
        return this.timeRange[0];
    };
    set startTime(time) {
        this.timeRange[0] = time;
    };
    get endTime() {
        return this.timeRange[1];
    };
    set endTime(time) {
        this.timeRange[1] = time;
    };
}

使用 TimeRange 类创建 dutyTime 对象

addClick() {
    this.formData = {
        // dutyTime: ['00:00', '23:59'],   // 原需求初始值
        // dutyTime: [['00:00', '23:59']], // 修改需求后初始值
        dutyTime: [new TimeRange()], // 优化后的初始值
    };
},

好好好~ 问题来了~

访问器数据的值正确,提交时值也是正确的 dutyTime: "00:00-06:17"。但值没有响应给组价,不能实时显示。

解决目标/预期

实现 class 类创建对象中的访问器属性的响应式。

问题分析

√ 用纯 Js 简单模拟,响应正常:

class TimeRange {
    timeRange = ['00:00', '23:59'];
    constructor(startTime = '00:00', endTime = '23:59') {
        this.timeRange[0] = startTime;
        this.timeRange[1] = endTime;
    }
    get startTime() {
        return this.timeRange[0];
    };
    set startTime(time) {
        this.timeRange[0] = time;
    };
    get endTime() {
        return this.timeRange[1];
    };
    set endTime(time) {
        this.timeRange[1] = time;
    };
}


let dutyTime = new TimeRange();

// 模拟数据响应式~
Object.defineProperty(dutyTime, "_startTime", {
    get() {
        return this.startTime;
    },
    set(time) {
        this.startTime = time;
    },
});

dutyTime._startTime = "00:12";
console.log(dutyTime._startTime); //  00:12
dutyTime._startTime = "00:52";
console.log(dutyTime._startTime); //  00:52

vue简化模拟问题出现:

<template>
    <div class=''>
        <div v-for="(item, index) in formData.dutyTime" :key="index">
            原始:
            <el-time-picker 
                prefix-icon="123"
                :clearable="false" 
                format="HH:mm" 
                value-format="HH:mm" 
                style="width: 110px; margin-bottom: 4px;"
                v-model="item.startTime" 
                align="center"
                @change="handlePickerChange"
            />
            -
            <el-time-picker 
                prefix-icon="123"
                :clearable="false" 
                format="HH:mm" 
                value-format="HH:mm" 
                style="width: 110px; margin-bottom: 4px;"
                v-model="item.endTime" 
                align="center"
                @change="handlePickerChange"
            />
            {{ formData.dutyTime }}
        </div>

        <div>
            简化:
            <el-time-picker 
                prefix-icon="123"
                :clearable="false" 
                format="HH:mm" 
                value-format="HH:mm" 
                style="width: 110px; margin-bottom: 4px;"
                v-model="dutyTime.startTime" 
                align="center"
                @change="handlePickerChange"
            />
            -
            <el-time-picker 
                prefix-icon="123"
                :clearable="false" 
                format="HH:mm" 
                value-format="HH:mm" 
                style="width: 110px; margin-bottom: 4px;"
                v-model="dutyTime.endTime" 
                align="center"
                @change="handlePickerChange"
            />
            {{ `${dutyTime.startTime}-${dutyTime.endTime}` }}
        </div>
        {{ count }}
        <button @click="handleSubmit" style="display: block;" >submit</button>
    </div>
</template>

<script>
import { TimeRange } from './index.js';

export default {
    data() {
        return {
            count: 0,
            formData: {},
            dutyTime: {},
        };
    },
    methods: {
        handlePickerChange(e) {
            console.log('e', e);
        },

        handleSubmit() {
            // this.count++;
            this.$forceUpdate();
        }
    },
    mounted() {
        this.formData = {
            dutyTime: [new TimeRange()],
        };
        this.dutyTime = new TimeRange();
        console.log('dutyTime', this.dutyTime);
    },
}
</script>

在页面上直接展示 dutyTime 对象,没有反应,证明与数据双向绑定指令 v-model 无关:

执行强制刷新: this.count++this.$forceUpdate(),界面数据刷新,正确:

说明:数据变化没有被监听到。还是响应式的问题!!!!

查看Vue2.6.11响应式实现源码:vue\src\core\observer\index.ts ---> class Observer

Object.keys():静态方法返回一个由给定对象自身的(不包含原型链上的)可枚举的字符串键属性名组成的数组。

所以问题的根本原因还是 字面量 和 class 创建对象的方式不同,导致属性 'startTime', 'endTime' 缺失:

  • 字面量创建对象的形式访问器属性是在自身对象上的

  • class类创建对象的形式访问器属性是在原型对象上的

class TimeRange {
    timeRange = ['00:00', '23:59'];
    constructor(startTime = '00:00', endTime = '23:59') {
        this.timeRange[0] = startTime;
        this.timeRange[1] = endTime;
    }
    get startTime() {
        return this.timeRange[0];
    };
    set startTime(time) {
        this.timeRange[0] = time;
    };
    get endTime() {
        return this.timeRange[1];
    };
    set endTime(time) {
        this.timeRange[1] = time;
    };
}

let dutyTime = new TimeRange();

console.log(Object.keys(dutyTime));        // [ 'timeRange' ]

解决方式

1.class 创建直接返回一个字面量对象

class TimeRange {
    constructor(startTime = '00:00', endTime = '23:59') {;
        // 直接返回字面量对象 方式  √
        return {
            timeRange: [startTime, endTime],
            get startTime() {
                return this.timeRange[0];
            },
            set startTime(time) {
                this.timeRange[0] = time;
            },
            get endTime() {
                return this.timeRange[1];
            },
            set endTime(time) {
                this.timeRange[1] = time;
            },
        }
    }
}

let dutyTime = new TimeRange();
console.log(Object.keys(dutyTime)); // [ 'timeRange', 'startTime', 'endTime' ]

2.使用 Object.defineProperty 给实例对象上添加访问器属性

export class TimeRange {
    timeRange = ['00:00', '23:59'];
    constructor(startTime = '00:00', endTime = '23:59') {
        this.init();
        this.startTime = startTime;
        this.endTime = endTime;
    }

    init() {
        Object.defineProperty(this, "startTime", {
            get: function () {
                return this.timeRange[0];
            },
            set: function (time) {
                this.timeRange[0] = time;
            },
            enumerable: true,   // 默认值为 false(不可遍历)
            configurable: true, // 默认值为 false(不可更改属性配置)
        });
        Object.defineProperty(this, "endTime", {
            get: function () {
                return this.timeRange[1];
            },
            set: function (time) {
                this.timeRange[1] = time;
            },
            enumerable: true,   // 默认值为 false(不可遍历)
            configurable: true, // 默认值为 false(不可更改属性配置)
        });
    }
}

let dutyTime = new TimeRange("12:12");

console.log(Object.keys(dutyTime)); // [ 'timeRange', 'startTime', 'endTime' ]

除了让访问器属性可遍历(enumerable),需要可修改配置(configurable),因为:

一切正常 over~

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值