js Proxy 应用之响应式数据

903 阅读6分钟
  • 前言

    本文不会长篇大论论述Proxy的基础语法和基本使用,也不会说与文章无关或关系不大的内容,不扯别的,纯应用,有好的实际应用会不定期更新本文章
  • 什么是Proxy对象?

    JavaScript中的Proxy对象是ES6中的一个新特性,可以用来拦截JavaScript对象的底层操作,比如属性访问、属性赋值、函数调用等等。它可以用于创建一个代理对象,这个代理对象可以用来代替原始对象进行操作,从而可以在操作的过程中添加自己的逻辑。使用Proxy对象可以使得代码更加简洁、易于维护。
  • 响应式数据

    • 数据响应的概念

      数据响应是指当数据发生变化时,相应的UI界面也会随之改变的能力。在现代Web开发中,数据响应是一个非常重要的概念,因为它可以使得应用程序更加灵活、交互性更好。

    • 使用Proxy实现数据响应

      在JavaScript中,我们可以使用Proxy对象来实现数据响应的功能。具体地说,我们可以使用Proxy对象来拦截对象的属性访问和属性赋值,从而实现数据响应的功能。以下是一个基础示例代码,展示了如何使用Proxy对象实现数据响应的功能:

        const data = {
          name: '张三',
          age: 18,
        };
      
        const handler = {
          get(target, key, receiver) {
            console.log(`读取了属性${key}`);
            return Reflect.get(target, key, receiver);
          },
      
          set(target, key, value, receiver) {
            console.log(`设置了属性${key}的值为${value}`);
            const result = Reflect.set(target, key, value, receiver);
            return result;
          },
        };
      
        const proxy = new Proxy(data, handler);
      
        proxy.name; // 输出:读取了属性name
        proxy.age = 20; // 输出:设置了属性age的值为20
      

      注意: proxy.age = 20; 只触发了set

      在上述代码中,我们定义了一个普通的JavaScript对象data,然后创建了一个Proxy对象proxy来代理这个普通对象。我们还定义了一个handler对象来拦截Proxy对象的属性访问和属性赋值操作。在handler对象的get方法和set方法中,我们添加了一些打印信息的逻辑,以便更好地理解Proxy对象的工作原理。

      在使用Proxy对象的过程中,我们可以通过添加一些自己的逻辑来实现数据响应的功能。例如,在上述示例代码中,我们可以在set方法中添加一些代码,当数据发生变化时,更新UI界面的内容。

    • 实现简单的数据响应

      在实际开发中,我们通常需要实现更加复杂的数据响应功能,例如当数据发生变化时,自动更新UI界面的内容。以下是一个示例代码,展示了如何使用Proxy对象实现一个简单的数据响应功能:

      // 定义 effect 函数
      const effects = [];
      const watchEffect = (fn) => {
        if(effects.includes(fn)) {
          return
        }
        effects.push(fn)
      };
      
      const trigger = (target, key) => {
        // 触发更新
        effects.forEach(effect => {
          effect();
        });
      };
      
      
      function reactive(target) {
        const handler = {
          get(target, key, receiver) {
            console.log(`读取了属性${key}`)
            return Reflect.get(target, key, receiver);
          },
          set(target, key, value, receiver) {
            console.log(`设置了属性${key}的值为${value}`);
            const result = Reflect.set(target, key, value, receiver);
            // 触发更新
            trigger(target, key);
            return result;
          },
        };
      
        const proxy = new Proxy(target, handler);
        return proxy;
      }
      const reactiveData = reactive({
        name: '张三',
        age: 18,
      });
      // 访问数据对象
      watchEffect(() => {
        console.log(reactiveData.name); // 输出:李四, 李四
      });
      watchEffect(() => {
        console.log(reactiveData.age); // 输出:18,20
      });
      
      // 修改数据对象
      reactiveData.name = '李四';
      reactiveData.age = 20;
      
      

      上述代码中 通过Proxy实现了简单的响应式数据, 通过watchEffect可以监听到数据的变化,但目前还存在很多问题

      • 资源释放问题

        目前watchEffect监听数据变化不支持取消监听,会导致性能问题,比如需要在组件卸载的时候进行资源释放,只需要给watchEffect函数加一个返回一个可以取消监听的函数即可,如下所示:

        const unWatchEffect = (effectFn) => {
            return () => {
              const index = effects.find(val => val == effectFn)
        
              if(index < 0) {
                return
              }
        
              effects.splice(index, 1)
            }
        }
        
        const watchEffect = (fn) => {
            if(effects.includes(fn)) {
              return unWatchEffect(fn)
            }
            effects.push(fn)
            return unWatchEffect(fn)
        };
        
        const unWatch = watchEffect(() => {
            console.log(reactiveData.age); // 输出:20
        });
        // ... 省略
        reactiveData.age = 20;
        unWatch()
        reactiveData.age = 22;
        
      • 对象嵌套问题

        目前访问嵌套对象的属性是无法触发监听器的,因为 Proxy 只能代理对象本身的属性。如果要代理嵌套属性,需要通过递归的方式代理每个嵌套对象的属性,以达到监听嵌套属性的目的。加入递归,代码如下所示:

        function reactive(target) {
          const handler = {
            get(target, key, receiver) {
              console.log(`读取了属性${key}`)
              const result = Reflect.get(target, key, receiver);
              // typeof null 也等于 object
              if(typeof result === "object" && result !== null) {
                // 递归代理
                return reactive(result)
              }
        
              return result;
            },
            set(target, key, value, receiver) {
              console.log(`设置了属性${key}的值为${value}`);
        
              const old = Reflect.get(target, key, receiver);
              const result = Reflect.set(target, key, value, receiver);
        
              if(old !== value) {
                // 触发更新
                trigger(target, key);
              }
        
              return result;
            }
          };
        
          const proxy = new Proxy(target, handler);
        
          return proxy;
        }
        
      • 监听特定key的变化问题

        目前我们实现了监听某个对象任意属性的变化,但无法指定监听特定数据,可以增加watch函数来实现这一个功能,watch函数有两个参数,一个是getter,callback,代码如下:

        function watch(getter, callback) {
          // 获取旧的值
          let oldValue = getter();
        
          console.log(`oldValue: ${oldValue}`);
        
          return watchEffect(() => {
            // 获取最新的值
            const newValue = getter();
            // 如果上一个值跟当前值不相等 则说明数据发生了变化
            if (oldValue !== newValue) {
              callback(newValue, oldValue);
              // 重置 oldValue
              oldValue = newValue;
            }
          });
        }
        
        

        使用watch对特定key进行监听示例:

        const reactiveData = reactive({
          name: '张三',
          age: 18,
          a: {
            b: 1
          },
          arr: [0, 1, 2]
        });
        // 同样可以使用unWatch进行取消监听
        const unWatch = watch(() => reactiveData.a.b, (newVal, oldVal) => {
          // 输出 newVal: 11 oldVal: 1
          console.log(`newVal: ${newVal}  oldVal: ${oldVal}`)
        })
        
        reactiveData.a.b = 11
        unWatch() // 取消监听
        
      • 支持访问源对象

        有时候我们可能会有访问源对象的需求 可以在代理层留个口子

        function reactive(target) {
          const handler = {
            get(target, key, receiver) {
              console.log(`读取了属性${key}`)
              
              if (key === '__original__') { // 如果要获取原始对象,返回 target 对象
                  return target;
              }
              const result = Reflect.get(target, key, receiver);
              // typeof null 也等于 nulll
              if(typeof result === "object" && result !== null) {
                // 递归代理
                return reactive(result)
              }
        
              return result;
            },
            set(target, key, value, receiver) {
              console.log(`设置了属性${key}的值为${value}`);
        
              const old = Reflect.get(target, key, receiver);
              const result = Reflect.set(target, key, value, receiver);
        
              if(old !== value) {
                // 触发更新
                trigger(target, key);
              }
        
              return result;
            }
          };
        
          const proxy = new Proxy(target, handler);
        
          return proxy;
        }
        
      • 使用缓存

        function reactive(target) {
            const deepProxyCacheMap = new Map();
            const handler = {
              get(target, key, receiver) {
                console.log(`读取了属性${key}`)
        
                if (key === '__original__') { // 如果要获取原始对象,返回 target 对象
                    return target;
                }
                const result = Reflect.get(target, key, receiver);
                // typeof null 也等于 object
                if(typeof result === "object" && result !== null) {
                  // 从缓存中拿
                  if (deepProxyCacheMap.has(key)) {
                    return deepProxyCacheMap.get(key);
                  } else {
                    // 递归代理
                    const deepProxyResult = reactive(result);
                    deepProxyCacheMap.set(key, deepProxyResult);
                    return deepProxyResult;
                  }
                }
                // 否则就直接返回不再proxy
                return result;
              },
              set(target, key, value, receiver) {
                console.log(`设置了属性${key}的值为${value}`);
                // 把之前的缓存删除掉
                deepProxyCacheMap.delete(key);
                const old = Reflect.get(target, key, receiver);
                const result = Reflect.set(target, key, value, receiver);
        
                if(old !== value) {
                  // 触发更新
                  trigger();
                }
        
                return result;
              }
            };
        
            const proxy = new Proxy(target, handler);
        
            return proxy;
          }
        
      • set权限问题

        在实际情况中并不是任意情况下都能进行set,set有如下约束,我们需要对这些情况进行相应处理

        1. 当目标属性是一个不可写或者不可配置的数据属性,则不能去改变它的值
        2. 当目标属性没有配置set方法,也就是说 [[Set]] 属性的是 undefined,则不能去设置它的值

        例如: 不可以写入时候在严格模式下会报TypeError

        const data = {
          name: '张三',
        }
        
        Object.defineProperty(data, 'age', {
          value: 18,
          configurable: true,
          // age不可以写 只读
          writable: false, 
          // 同样 如果enumerable为false 在严格模式下也会报错
          enumerable: true
        })
        
        const reactiveData = reactive(data);
        
        watch(() => reactiveData.age, (newVal, oldVal) => {
          console.log(`newVal: ${newVal}  oldVal: ${oldVal}`)
        })
        
        reactiveData.age = 11
        
      • 支持代理多个源对象

        目前reactive已经支持进行多次代理了,但watchEffect目前还是全局的,也就是说,不管reactive代理多少个对象,但任意个代理对象的数据发生变化watchEffect都会触发,将所有AIP封装成一个整体即可,优化如下:

        function reactive(target) {
           const effects = [];
           const unWatchEffect = (effectFn) => {
             return () => {
               const index = effects.find(val => val == effectFn)
        
               if (index < 0) {
                 return
               }
        
               effects.splice(index, 1)
             }
           }
        
           const watchEffect = (fn) => {
             if (effects.includes(fn)) {
               return unWatchEffect(fn)
             }
        
             effects.push(fn)
        
             return unWatchEffect(fn)
           };
        
           function watch(getter, callback) {
             // 获取旧的值
             let oldValue = getter();
        
             console.log(`oldValue: ${oldValue}`);
        
             return watchEffect(() => {
               // 获取最新的值
               const newValue = getter();
               // 如果上一个值跟当前值不相等 则说明数据发生了变化
               if (oldValue !== newValue) {
                 callback(newValue, oldValue);
                 // 重置 oldValue
                 oldValue = newValue;
               }
             });
           }
        
           const trigger = () => {
             // 触发更新
             effects.forEach(effect => effect());
           };
           const observeData = (() => {
             const deepProxyCacheMap = new Map();
             const handler = {
               get(target, key, receiver) {
                 console.log(`读取了属性${key}`)
        
                 if (key === '__original__') {
                   // 如果要获取原始对象,返回 target 对象
                   return target;
                 }
                 const result = Reflect.get(target, key, receiver);
                 // typeof null 也等于 nulll
                 if (typeof result === "object" && result !== null) {
                   // 层缓存中拿
                   if (deepProxyCacheMap.has(key)) {
                     return deepProxyCacheMap.get(key);
                   } else {
                     const deepProxyResult = reactive(result);
                     deepProxyCacheMap.set(key, deepProxyResult);
                     return deepProxyResult;
                   }
                 }
                 // 否则就直接返回不再proxy
                 return result;
               },
               set(target, key, value, receiver) {
                 console.log(`设置了属性${key}的值为${value}`);
                 // 把之前的缓存删除掉
                 deepProxyCacheMap.delete(key);
                 const old = Reflect.get(target, key, receiver);
                 const result = Reflect.set(target, key, value, receiver);
        
                 if (old !== value) {
                   // 触发更新
                   trigger();
                 }
        
                 return result;
               }
             };
        
             const proxy = new Proxy(target, handler);
        
             return proxy;
           })()
        
           return {
             data: observeData,
             watchEffect: watchEffect,
             watch: watch
           }
         }
        

        用法如下:

        const data = {
           name: '张三',
         }
        
         Object.defineProperty(data, 'age', {
           value: 18,
           configurable: true,
           writable: true,
           enumerable: true
         })
        
         const {data: reactiveData, watch, watchEffect} = reactive(data);
        
        
         watch(() => reactiveData.age, (newVal, oldVal) => {
           // 输出 newVal: 11 oldVal: 18
           console.log(`newVal: ${newVal}  oldVal: ${oldVal}`)
         })
        
         reactiveData.age = 11
        
      • 所有代码

        function reactive(target) {
          const effects = [];
          const unWatchEffect = (effectFn) => {
            return () => {
              const index = effects.find(val => val == effectFn)
        
              if (index < 0) {
                return
              }
        
              effects.splice(index, 1)
            }
          }
        
          const watchEffect = (fn) => {
            if (effects.includes(fn)) {
              return unWatchEffect(fn)
            }
        
            effects.push(fn)
        
            return unWatchEffect(fn)
          };
        
          function watch(getter, callback) {
            // 获取旧的值
            let oldValue = getter();
        
            console.log(`oldValue: ${oldValue}`);
        
            return watchEffect(() => {
              // 获取最新的值
              const newValue = getter();
              // 如果上一个值跟当前值不相等 则说明数据发生了变化
              if (oldValue !== newValue) {
                callback(newValue, oldValue);
                // 重置 oldValue
                oldValue = newValue;
              }
            });
          }
        
          const trigger = () => {
            // 触发更新
            effects.forEach(effect => effect());
          };
          const observeData = (() => {
            const deepProxyCacheMap = new Map();
            const handler = {
              get(target, key, receiver) {
                console.log(`读取了属性${key}`)
        
                if (key === '__original__') {
                  // 如果要获取原始对象,返回 target 对象
                  return target;
                }
                const result = Reflect.get(target, key, receiver);
                // typeof null 也等于 nulll
                if (typeof result === "object" && result !== null) {
                  // 层缓存中拿
                  if (deepProxyCacheMap.has(key)) {
                    return deepProxyCacheMap.get(key);
                  } else {
                    const deepProxyResult = reactive(result);
                    deepProxyCacheMap.set(key, deepProxyResult);
                    return deepProxyResult;
                  }
                }
                // 否则就直接返回不再proxy
                return result;
              },
              set(target, key, value, receiver) {
                console.log(`设置了属性${key}的值为${value}`);
                // 把之前的缓存删除掉
                deepProxyCacheMap.delete(key);
                const old = Reflect.get(target, key, receiver);
                const result = Reflect.set(target, key, value, receiver);
        
                if (old !== value) {
                  // 触发更新
                  trigger();
                }
        
                return result;
              }
            };
        
            const proxy = new Proxy(target, handler);
        
            return proxy;
          })()
        
          return {
            data: observeData,
            watchEffect: watchEffect,
            watch: watch
          }
        }
        
        const data = {
          name: '张三',
        }
        
        Object.defineProperty(data, 'age', {
          value: 18,
          configurable: true,
          writable: true,
          enumerable: true
        })
        
        const {data: reactiveData, watch, watchEffect} = reactive(data);
        
        
        watch(() => reactiveData.age, (newVal, oldVal) => {
          console.log(`newVal: ${newVal}  oldVal: ${oldVal}`)
        })
        
        reactiveData.age = 11
        
        
        • 结语

          如有不对之处,欢迎各位道友批评指正