proxy

<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>
    <body>

        <script>
            <!-- <b>:<t></t><r name='input-radio' data-text= class=input-既往史></r> -->
            document.body.innerHTML = "既往史<div>111</div><input/>"
            // proxy 是一种封装,能去拦截并改变js引擎的底层操作,比如一些不可枚举、不可写入的属性。
            // 通过调用new Proxy(),可以创建一个代理去替代另一个对象(目标对象),
            // 这时,代理对目标对象进行了虚拟,因此,该代理和目标对象在表面上可以当做统一对象来看。
            // Proxy代理允许拦截目标对象的底层操作,而这本来是js引擎的内部操作。
            // 拦截的行为是个函数,可以修改js对象的内置行为,用于响应拦截的特定操作,我们称为陷阱。
            // 创建代理 
            // var proxy = new Proxy(target, handler);
            // 参数:target参数表示所要拦截的目标对象,
            // handler参数也是一个或多个陷阱函数的对象,用来定制拦截行为。若没有提供陷阱函数,则代理采取默认行为操作。
            // new Proxy( ) 表示生成一个Proxy实例
            let arr = ["1", 2, "3"];
            arr.length = 3;
            arr.length = 1;
            console.log(arr) //["1"],数组剩下一个元素
            // 在proxy之前,我们无法去模拟数组的这种行为。

            let target = {}
            let proxyObj = new Proxy(target, {})
            proxyObj.name = "proxyName" //给proxy代理属性赋值

            console.log(proxyObj.name) //proxyName 
            console.log(target.name) //proxyName  

            target.name = "targeName" //给目标对象属性赋值
            console.log(proxyObj.name) //targeName 
            console.log(target.name) //targeName

            let targetarr = [1, 2, 3, 4, 5]
            let proxyarr = new Proxy(targetarr, [])
            proxyarr.length = 1 // 在proxy之后,我们可以去去模拟数组的这种行为。
            console.log("proxyarr", targetarr)
            // get陷阱函数(添加属性且能够读取存在的属性,而读取不存在属性会报错。)
            // 读取对象不存在的属性,会显示undefined,而不会报错。
            // let target = {}
            // console.log(target.name) // undefined
            // 若我们想读取对象不存在的属性,会报错,该如何设置?
            // get陷阱函数在读取属性时被调用,即使对象不存在此属性,也可以接受参数。
            // get陷阱函数有三,分别为
            // trapTarget:被读取属性的对象(代理的目标对象)
            // key:被读取属性的键
            // receiver:操作发生的对象(代理的对象)
            // 注:1)Reflect.get()方法 接受参数和get陷阱函数相同。
            //   2)set陷阱函数的参数有四个(trapTarget、key、value、receiver), 而get陷阱函数没有使用value参数,
            // 是因为get陷阱函数不需要设置属性值。
            // 举个例子来具体说明下: 
            // eg: 读取目标属性不存在的情况下,报错
            // 复制代码
            var target2 = {
                name: 'targetName',
            }
            let proxy2 = new Proxy(target2, {
                //获取属性几次执行几次 set就是设置几次就执行几次 has陷阱函数也是
                get(trapTarget, key, receiver) {
                    console.log('trapTarget--------', trapTarget);
                    console.log('key--------', key);
                    console.log('receiver--------', receiver);
                    //用于检查 trapTarget 对象中是否存在名为 key 的属性,并不是for in循环
                    if (!(key in trapTarget)) {
                        //提示读取不存在属性会报错(proxy2添加属性但是没有赋值就会报错,如下面的proxy2.sex没有赋值)。
                        console.log("属性" + key + " doesn't exist.")
                    }
                    console.log("Reflect", Reflect.get(trapTarget, key, receiver))
                    return Reflect.get(trapTarget, key, receiver);
                }
            })
            console.log(proxy2.name) // "targetName"
            // 添加属性的功能正常
            proxy2.place = "北京";
            // console.log(proxy2.place) // "北京"
            // let obj = {}
            // console.log("obj",obj.a)
            // 读取不存在属性会报错
            // console.log(proxy2.sex) // 报错
            // 复制代码
            // 由于我们是读取对象的属性,只需要使用get陷阱函数。
            // 在本例中,通过in运算符来判断receiver对象上是否存在已有的属性,从而进行拦截操作。
            // 以上看出,可以添加属性且能够读取存在的属性,而读取不存在属性会报错。
            // get陷阱函数
            // 读取对象不存在的属性,会显示undefined,而不会报错。
            // let target = {}
            // console.log(target.name) // undefined
            // 若我们想读取对象不存在的属性,会报错,该如何设置?
            // get陷阱函数在读取属性时被调用,即使对象不存在此属性,也可以接受参数。

            // get陷阱函数有三,分别为
            // trapTarget:被读取属性的对象(代理的目标对象)
            // key:被读取属性的键
            // receiver:操作发生的对象(代理的对象)
            // 注:1)Reflect.get()方法 接受参数和get陷阱函数相同。
            //   2)set陷阱函数的参数有四个(trapTarget、key、value、receiver), 而get陷阱函数没有使用value参数,是因为get陷阱函数不需要设置属性值。
            // 举个例子来具体说明下: 
            // eg: 读取目标属性不存在的情况下,报错

            // 复制代码
            var target6 = {
                name: 'targetName'
            }
            let proxy6 = new Proxy(target6, {
                get(trapTarget, key, receiver) {
                    if (!(key in trapTarget)) {
                        // throw new TypeError("属性" + key + " doesn't exist.");
                    }
                    return Reflect.get(trapTarget, key, receiver);
                }
            })
            console.log(proxy6.name) // "targetName"
            // 添加属性的功能正常
            proxy6.place = "北京";
            console.log(proxy6.place) // "北京"
            //正常访问一个对象不存在的属性是不会报错的,直接返回 undefined
            // 读取不存在属性会报错
            console.log(proxy6.sex) // 报错
            // 复制代码
            // 由于我们是读取对象的属性,只需要使用get陷阱函数。

            // 在本例中,通过in运算符来判断receiver对象上是否存在已有的属性,从而进行拦截操作。

            // 以上看出,可以添加属性且能够读取存在的属性,而读取不存在属性会报错。

            // set陷阱函数
            // set陷阱函数有四个参数,分别为

            // trapTarget:被接受属性的对象(代理的目标对象)
            // key:被写入属性的键
            // value:被写入属性的值
            // receiver:操作发生的对象(代理的对象)
            // set()在写入属性成功返回true,否则返回false。
            // 同样的,Reflect.set()参数和set陷阱函数一致,且Reflect.set()依据操作的不同返回相应的结果。
            // 举个例子来说明下,
            // eg: 创建对象,且属性值只能是num类型,若类型不符,则报错。需要用set陷阱函数去重新属性值的默认行为。
            // 复制代码
            let target4 = {
                name: "target"
            }
            let proxy4 = new Proxy(target4, {
                set(trapTarget, key, value, receiver) {
                    console.log("settrapTarget", trapTarget)
                    console.log("setkey", key)
                    console.log("setvalue", value)
                    console.log("setreceiver", receiver)
                    // proxy.count = 1          打印结果为 {name: "target"}           "count"       1       Proxy {name: "target"}       
                    // proxy.name = "proxyName” 打印结果为 {name: "target", count: 1} "name"   "proxyName"  Proxy {name: "target", count: 1} 
                    // 忽略已有属性,避免影响它们
                    if (!trapTarget.hasOwnProperty(key)) {
                        if (isNaN(value)) {
                            // throw new TypeError("Property must be a number.");
                        }
                    }
                    // 添加属性(创建属性)
                    return Reflect.set(trapTarget, key, value, receiver);
                }
            })
            // 添加一个新属性
            proxy4.count = 1
            console.log(proxy4.count) // l
            console.log(target4.count) // l
            // 你可以为name 赋一个非数值类型的值,因为该属性已存在 
            proxy4.name = "proxy"
            console.log(proxy4.name) // "proxyName"
            console.log(target4.name) // "proxyName"
            // // 抛出错误
            // proxy.anotherName = "proxyOtherName" 
            // 复制代码
            // 当执行 proxy.count = 1时,set陷阱函数被调用,此时trapTarget的値等于target对象,key的値是字符串"count" ’,value的値是1 。

            // target对象上尚不存在名为count的属性,因此代理将 value参数传递给isNaN()方法进行验证;

            // 如果验证结果是NaN ,表示传入的属性値不是 一个数値,需要拋出错误;

            // 但由于这段代码将count参数设置为1 ,验证通过,代理使用一致的四个参数去调用Reflect.set()方法,从而创建了一个新的属性。

            // 当proxy.name被赋值为字符串时,操作成功完成。这是因为target对象已经拥有一个 name属性,

            // 因此验证时通过调用trapTarget.hasOwnProperty()会忽略该属性,这就确保允 许在该对象的已有属性上使用非数値的属性値。

            // 当proxy.anotherName被紙値为字符串时,抛出了一个错误。这是因为该对象上并不存在 anotherName属性,因此该属性的値必须被验证,

            // 而因为提供的値不是一个数値,验证过程 就会抛出错误。

            // has陷阱函数
            // in 运算符判断对象是否存在某个属性,无论该属性是对象自身属性,还是其原型属性


            // val是自身属性,toString是 原型属性,

            //  has陷阱函数参数有两个,分别为

            // trapTarget:需要读取属性的对象(代理的目标对象)
            // key:需要检查的属性的键
            // Reflect.has()方法接受与之相同的参数,并向in运算符返回默认响应结果。

            // 使用has 陷阱函数以及Reflect.has()方法,允许你修改部分属性在接受in检測时的行为,但保留其他属性的默认行为。

            // 举个例子来说明:

            // eg: 只想要隐藏value属性

            // 复制代码
            let target3 = {
                name: "target",
                value: 42
            }
            let proxy = new Proxy(target3, {
                has(trapTarget, key) { //key指的是 value" in proxy 的value,并不是trapTarget的键
                    console.log(11111, key)
                    if (key === "value") {
                        return false
                    } else {
                        return Reflect.has(trapTarget, key);
                    }
                }
            })

            console.log("value" in proxy); // false
            console.log("name" in proxy); // true
            console.log("tost ring" in proxy); // false

            // 使用了 has陷牌函数,用于检查key値是否为"value"。如果是,则 返回false,否则通过调用Reflect.has()方法来返回默认的结果。
            const targetObj = {
                name: '小浪',
                age: 22,
            }

            const handler = {
                get(targetObj, key, receiver) {
                    console.log(`获取对象属性${key}值`, Reflect.get(targetObj, key, receiver))
                    //Reflect.get(targetObj, key, receiver)返回获取的值
                    return Reflect.get(targetObj, key, receiver)
                },
                set(targetObj, key, value, receiver) {
                    console.log(`设置对象属性${key}值`, Reflect.set(targetObj, key, value, receiver))
                    // Reflect.set(targetObj, key, value, receiver) 返回true/false 
                    return Reflect.set(targetObj, key, value, receiver)
                },
                deleteProperty(targetObj, key) {
                    console.log(`删除对象属性${key}值`, Reflect.deleteProperty(targetObj, key))
                    //Reflect.deleteProperty(targetObj, key)返回true/false
                    return Reflect.deleteProperty(targetObj, key)
                },
            }
            // target:参数表示所要拦截的目标对象
            // handler:参数也是一个对象,用来定制拦截行为
            // 注意:
            // this 关键字表示的是代理的 handler 对象,所以不能使用this而是要用 receiver传递,
            // receiver代表当前proxy对象 或者 继承proxy的对象,它保证传递正确的 this 给 getter,setter
            // set  和 deleteProperty 也需要返回(添加return ),返回的是一个布尔值,设置/删除成功返回true,反之返回false
            const proxyCopy = new Proxy(targetObj, handler)
            console.log(proxyCopy.age) //22
            proxyCopy.age = 22
            console.log(delete proxyCopy.age); //删除age属性
            console.log(proxyCopy) //22
            // 代码片段 delete 是一个 JavaScript 操作,用于删除对象的属性。如果 proxy 是一个对象,那么 delete proxy 会尝试删除 
            // proxy 对象本身。 这将导致 proxy 对象被销毁, 并且它所占用的内存将被释放。
            // 如果 proxy 不是一个对象,那么 delete proxy 将是一个无效操作,不会产生任何效果。这种情况下,
            // proxy 可能是一个变量, 它的值可能是一个基本类型( 如数字、 字符串、 布尔值等) 或者是一个对象的引用。 对于基本类型,
            // delete 操作没有意义, 因为它们不是对象, 也没有属性。 对于对象引用, delete 操作应该作用于引用的对象, 而不是变量本身。
            // 如果你想删除 proxy 对象的某个属性,可以使用 delete proxy[key] 的形式,其中 key 是要删除的属性的名称。
            // 这将从 proxy 对象中移除指定的属性, 使它不再可用。

            // 了解了上面的Proxy和Reflect,我们来看一下reactive的实现,reactive,
            // 返回proxy对象,这个reactive可以深层次递归,如果发现子元素存在引用类型,递归处理。
            // 判断是否为对象 ,注意 null 也是对象
            // 判断是否为对象 ,注意 null 也是对象
            const isObject = val => val !== null && typeof val === 'object'
            // 判断key是否存在
            const hasOwn = (target, key) => Object.prototype.hasOwnProperty.call(target, key)

            function reactive(target) {
                // 首先先判断是否为对象
                if (!isObject(target)) return target
                const handler = {
                    get(target, key, receiver) {
                        console.log(`获取对象属性${key}值`)
                        // ... 这里还需要收集依赖,先空着
                        const result = Reflect.get(target, key, receiver)
                        // 递归判断的关键, 如果发现子元素存在引用类型,递归处理。
                        if (isObject(result)) {
                            return reactive(result)
                        }
                        return result
                    },

                    set(target, key, value, receiver) {
                        console.log(`设置对象属性${key}值`)
                        // 首先先获取旧值
                        const oldValue = Reflect.get(target, key, reactive)
                        // set 是需要返回 布尔值的
                        let result = true
                        // 判断新值和旧值是否一样来决定是否更新setter
                        if (oldValue !== value) {
                            result = Reflect.set(target, key, value, receiver)
                            // 更新操作 等下再补
                        }
                        return result
                    },

                    deleteProperty(target, key) {
                        console.log(`删除对象属性${key}值`)
                        // 先判断是否有key
                        const hadKey = hasOwn(target, key)
                        const result = Reflect.deleteProperty(target, key)

                        if (hadKey && result) {
                            // 更新操作 等下再补
                        }

                        return result
                    },
                }
                return new Proxy(target, handler);
            }
            const objCopy = {
                name: '小浪',
                age: 22,
                test: {
                    test1: {
                        test2: 21,
                    },
                },
            }

            const proxyReactive = reactive(objCopy)
            console.log("proxyReactive.age", proxyReactive.age)
            proxyReactive.test.test1.test2 = 22
            console.log(delete proxyReactive.age)
            console.log("objCopy", objCopy);

            //javascript
            // WeakMap 是 JavaScript 中的一种数据结构,它是一种与对象键相关联的键/值对集合。与普通的 Map 相比,
            // WeakMap 的键只能是对象,并且在没有其他引用时会被自动垃圾回收。
            // 下面是 WeakMap 的一些特点和用法:
            // 键只能是对象:WeakMap 的键只能是对象,而值可以是任意类型的数据。
            // 弱引用:当键对象没有其他引用时,它会被自动从 WeakMap 中移除,这样有助于避免内存泄漏。
            // 无法迭代:由于键的弱引用特性,WeakMap 不提供像 Map 那样的迭代方法,所以无法通过 WeakMap 实例直接获取键或值的列表。
            // 适用场景:WeakMap 主要用于需要存储对象键并且不希望阻止这些键被垃圾回收的场景,比如在一些框架或库内部用来存储私有数据。

            let wm = new WeakMap();
            let key = {}; // 创建一个新对象作为键
            let value = "some value";
            wm.set(key, value); // 将键值对存储到 WeakMap 中
            console.log(wm.get(key)); // 输出 "some value"

            key = null; // 将原来的键置为 null

            // 此时,由于 key 不再被引用,它会被自动从 WeakMap 中移除
            // 这时再尝试获取值,将返回 undefined
            console.log(wm.get(key)); // 输出 undefined
            // 总之,WeakMap 在需要使用对象作为键并且希望在键对象失去引用后能够被垃圾回收时非常有用。
        </script>

        <div>
            <div>
                响应式顺序:effect > track > trigger > effect
            </div>
            <div>
                在组件渲染过程中,一个 effect 会会触发get,从而对值进行 track,当值发生改变,就会进行 trigge,执行 effect 来完成一个响应
            </div>
            <!-- 那么先来实现 effect  effect的实现很简单-->
            <!--        
            activeEffect 表示当前正在走的 effect这个 activeEffect 相当于  value.__ob__  value.__ob__ 存在值说明已被观察,
            let activeEffect = null
            export function effect(callback) {
                activeEffect = callback
                callback()
                activeEffect = null
            }
            
            // targetMap 表里每个key都是一个普通对象 对应他们的 depsMap
            这段代码描述了创建一个新的 Map 对象并存储到另一个 Map 对象中,下面是详细分析。
            set(target, value) 是一个 WeakMap 对象的方法,它允许你将一个 value 与 target 对象关联,并且是弱引用。
            这意味着如果 target 对象没有其他强引用,它可以被垃圾回收。
            new Map() 创建了一个新的映射对象用于存储依赖关系。depsMap 变量被赋值为这个新建的 Map。
            综合以上两点,targetMap.set(target, (depsMap = new Map())) 表示将 target 对象作为键,
            一个新的 Map 对象作为值存储在 targetMap 中。
            这样,你可以将 target 对象与一个依赖关系对象 depsMap 关联,用于跟踪与 target 对象相关的特定数据或状态。
            dep = new Set():
            
            这行代码创建了一个新的 Set 对象,并将其赋值给变量 dep。Set 对象是一种集合数据结构,
            它可以存储唯一的值,在许多情况下用于去重或集合操作。
            通过初始化 dep 为新的 Set 对象,确保每次访问或更新特定键的依赖关系时,
            都会有一个干净的集合实例以供使用,可能用于存储依赖项的唯一标识符或其他相关信息。
            这种模式在软件开发中很常见,用于管理对象间的复杂依赖关系。通过这种方式,
            你可以轻松追踪和操作与特定对象相关的多种依赖关系。
            let targetMap = new WeakMap()
            然后就是对 track 的实现,track就是添加依赖
            export function track(target, key) {
                // 如果当前没有effect就不执行追踪
                if (!activeEffect) return
                // 获取当前对象的依赖图
                let depsMap = targetMap.get(target)
                // 不存在就新建
                if (!depsMap) {
                    targetMap.set(target, (depsMap = new Map()))
                }
                // 根据key 从 依赖图 里获取到到 effect 集合
                let dep = depsMap.get(key)
                // 不存在就新建
                if (!dep) {
                    depsMap.set(key, (dep = new Set()))
                }
                // 如果当前effectc 不存在,才注册到 dep里
                  // Dep.target(wather)是一个我们指定的全局的位置,用window.target也行,只要是全局唯一,没有歧义就行

                if (!dep.has(activeEffect)) {
                    dep.add(activeEffect)// 相当于 if (Dep.target) this.addSub(Dep.target)
                }
            }
            最后添加到hander 里 get 中
            get(target, key, receiver) {
                // ...
                // 收集依赖
                track(target, key)
            
                // ...
            },
            触发更新
            通过上面的图,我们知道在set中使用trigger函数来触发更新,我们来实现一下吧
            // trigger 响应式触发
            export function trigger(target, key) {
                // 拿到 依赖图,判断是否已经添加到依赖
                const depsMap = targetMap.get(target)
                if (!depsMap) {
                    // 没有被追踪,直接 return
                    return
                }
                // 拿到了 视图渲染effect 就可以进行排队更新 effect 了
                const dep = depsMap.get(key)
            
                // 遍历 dep 集合执行里面 effect 副作用方法
                if (dep) {
                    dep.forEach(effect => {
                        //相当于调用watcher update去通知更新
                        effect()
                    })
                }
            }
            最后添加到hander 的 set 和 deleteProperty 中
            set(target, key, value, receiver) {
                // ...
                if (oldValue !== value) {
                    result = Reflect.set(target, key, value, receiver)
                    trigger(target, key)
                }
                // ...
            },

            deleteProperty(target, key) {
                // ...
                if (hadKey && result) {
                    // 更新操作
                    trigger(target, key)
                }
                // ...
            }
            
            
            ref
            // 判断是否是一个对象,是就用 reactive 来代理
            const convert = val => (isObject(val) ? reactive(val) : val)
            
            class RefImpl {
                constructor(_rawValue) {
                    this._rawValue = _rawValue
                    this.__v_isRef = true
                    // 判断 _rawValue 是否是一个对象
                    // 如果是对象调用reactive使用 proxy来代理
                    // 不是返回 _rawValue 本身
                    this._value = convert(_rawValue)
                }
                // 使用get/set 存取器,来进行追踪和触发
                get value() {
                    // 追踪依赖
                    track(this, 'value')
                    // 当然 get 得返回 this._value
                    return this._value
                }
                set value(newValue) {
                    // 判断旧值和新值是否一直
                    if (newValue !== this._value) {
                        this._rawValue = newValue
                        // 设置新值的时候也得使用 convert 处理一下,判断新值是否是对象
                        this._value = convert(this._rawValue)
                        // 触发依赖
                        trigger(this, 'value')
                    }
                }
            }
            
            export function ref(rawValue) {
                // __v_isRef 用来标识是否是 一个 ref 如果是直接返回,不用再转
                if (isObject(rawValue) && rawValue.__v_isRef) return rawValue
            
                return new RefImpl(rawValue)
            }



            toRef
            toRef传入两个参数,目标对象,对象当中的属性名,它的返回结果就是属性名的可响应式数据,就是将对象中的某个值转化为响应式数据 toRef(obj,key)

            那么简单来实现一下
            class ObjectRefImpl {
                constructor(proxy, _key) {
                    this._proxy = proxy
                    this._key = _key
                    // __v_isRef 用来标识是否是 一个 ref
                    this.__v_isRef = true
                }
                get value() {
                    // 这里不用收集依赖
                    // this._proxy 就是响应式对象,当访问[this._key]时,this._proxy里面会去自动收集依赖
                    return this._proxy[this._key]
                }
                set value(newVal) {
                    // 这里不用收集依赖
                    // this._proxy 响应式对象,会在this._proxy里面set去调用trigger
                    this._proxy[this._key] = newVal
                }
            }
            
            // 暴露出去的方法
            export function toRef(proxy, key) {
                return new ObjectRefImpl(proxy, key)
            }
            
            测试

            import { ref, effect, toRef, reactive } from '../src/reactive.js'
            const obj = reactive({
                name: '小浪',
            })
            const age = toRef(obj, 'age')
            age.value = 21
            console.log(obj) // 
            effect(() => {
                age.value = 22
            })
            console.log(obj)
            原来的obj对象没有 age 属性,使用 toRef 添加了 age, 并且是响应式的
            
            
            区别于ref:
            
            ref是对原始数据的拷贝,当修改ref数据时,模板中的视图会发生改变,但是原始数据并不会改变。
            toRef是对原始数据的引用,修改toRef数据时,原始数据也会发生改变,但是视图并不会更新。
            toRefs
            
            想必这个大家经常使用吧,平时如果使用reactive创建对象,我们不能直接进行解构,我们要使用 toRefs 帮助我们进行解构, 把整个 reactive 创建的对象变成 普通对象, 然后把每个属性变成 ref 响应式对象。那么直接上手写一下吧, 其实它的核心还是使用了 toRef
            export function toRefs(proxy) {
                // 判断 当前 proxy 是 proxy 数组, 还是 proxy 对象
                const ret = proxy instanceof Array ? new Array(proxy.length) : {}

                for (const key in proxy) {
                    // 内部还是调用 toRef 进行转为 响应式
                    ret[key] = toRef(proxy, key)
                }

                return ret
            }
            测试
            import { reactive, toRefs } from '../src/reactive.js'
            const obj = reactive({
                name: '小浪',
                age: 22,
            })

            const { name, age } = toRefs(obj)
            console.log(obj)
            name.value = '小云'
            age.value = 21
            console.log(obj)
            测试成功,toRefs解构后的属性也是响应式
             -->

            "toRef是对原始数据的引用,修改toRef数据时,原始数据也会发生改变,但是视图并不会更新。" 这句有问题吧...toRef的设计初衷和toRefs一样,
            都是为了解决响应式对象解构时响应式丢失的问题,修改toRef.value,视图也会更新。



            <br>
            Object.defineProperty 可以监听数组索引好吧,只是无法监听pop,push等方法
            <img src="./assets/renative.png" />
            <img src="./assets/11111.png" />
            <img src="./assets/22222.png" />
        </div>


    </body>
</html>
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容