First, know Proxy

New Proxy(Target, handler) The target Proxy wraps the target object. It can be any type of object, including built-in arrays, functions or even another proxy object. Handler Is an object whose properties provide the corresponding handlers for certain operations to occur.

Ruan Yifeng ES6 Primer (Proxy and Reflect)

Second, the early adopters

  • Demo:
    function reactive(obj) {
        return new Proxy(obj, {
            get(target, key, receiver) {
                const res = Reflect.get(target, key, receiver)
                console.log('get', key, res)
                return res
            },
            set(target, key, value, receiver) {
                const res = Reflect.set(target, key, value, receiver)
                console.log('set', key, res)
                return res
            },
            deleteProperty(target, key) {
                const res = Reflect.deleteProperty(target, key)
                console.log('delete', key, res)
                return res
            }
        })
    }
    
    const obj = { foo: 'foo'.bar: { a: 1 }, arr: [1.2.3]}const state = reactive(obj)
    
    / / to get
    state.foo
    
    / / set
    state.foo = 'foooooooo'
    
    / / delete
    delete state.foo
    
    // Add attributes
    state.dong = 'dong'
    state.dong
    
    // Nested objects
    state.bar.a = 10
    
    / / array
    state.arr
    state.arr.push(5)
    Copy the code
  • Conclusion: All operations can be intercepted except for nested objects
  • Recursive implementation of depth response:
    const isObject = val= >val ! = =null && typeof val === 'object'
    function reactive(obj) {
        if(! isObject(obj))return obj
        return new Proxy(obj, {
        	get(target, key, receiver) {
                const res = Reflect.get(target, key, receiver)
                console.log('get', key, res)
                returnisObject(res) ? reactive(res) : res }, ... })}...Copy the code

Cache proxy objects to avoid repeated proxy

  1. There are two cases where duplicate agents exist:

    const state = reactive(obj)

    const state2 = reactive(obj)

    const state = reactive(reactive(obj))

  2. That’s it

    // Avoid duplicate proxies
    const toProxy = new WeakMap(a){obj: observed}
    const toRaw = new WeakMap(a){observed: obj}
    
    const observed = new Proxy(obj, {... })/ / cache
    toProxy.set(obj, observed)
    toRaw.set(observed, obj)
    return observed
    Copy the code

4. Responsive inquiry

  • Here we implement VuewatchThe mechanism,computedSimilar.
  • Vue3 usage:effect(() => state.xxx, (val, oldVal) => {}), the first callback indicates the needwatchThe second is the callback function to be processed.
  • Think: ① To listen to the change of an object, is it only necessary toget()Function to set up the mapping relationship between the fields on the line? When will the callback function call be triggered? Is it inset()You can just do it in the function, right? If you have an idea, just do it!
  • The other thing we have to think about is what kind of data structure do we store it inProxy object - attribute - callback functionThe relationship between? In consideration of the fact that these terms can be multiple groups, we use them hereNew WeakMap(), new Map(), new Set()The structure of the.
  • Implement from invocationeffect(getField, callback), the code:
    function effect(getField, callback) {
        try {
            effectStack.push(callback)
            getField() // The get operation is triggered here
        } catch (error) {
          	console.error(error)
        } finally {
          	effectStack.pop()
        }
    }
    Copy the code
  • observedFunction transformation
     const observed = new Proxy(obj, {
       get(target, key, receiver) {
         const res = Reflect.get(target, key, receiver)
         // console.log(' get ', key, res)
         track(target, key) // Set the data binding relationship here
         return isObject(res) ? reactive(res) : res
       },
       set(target, key, value, receiver) {
         const oldVal = Reflect.get(target, key, receiver)
         const res = Reflect.set(target, key, value, receiver)
         // console.log(' setup ', oldVal, res)
         trigger(target, key, value, oldVal) // Trigger the corresponding callback function here
         return res
       },
       deleteProperty(target, key) {
         const oldVal = Reflect.get(target, key)
         const res = Reflect.deleteProperty(target, key)
         // console.log(' delete ', key, res)
         trigger(target, key, undefined, oldVal) // Trigger the corresponding callback function here
         return res
       }
    })
    Copy the code
  • Next implementationtrack(target, key),trigger(target, key, value, oldVal)Two functions, code:
    function track(target, key) {
      const effect = effectStack[effectStack.length - 1]
      if (effect) {
        let depsMap = targetMap.get(target)
        if(! depsMap) { depsMap =new Map()
          targetMap.set(target, depsMap)
        }
    
        let deps = depsMap.get(key)
        if(! deps) { deps =new Set()
          depsMap.set(key, deps)
        }
    
        deps.add(effect)
      }
    }
    
    function trigger(target, key, val, oldVal) {
      const depsMap = targetMap.get(target)
      if (depsMap) {
        const deps = depsMap.get(key)
        if (deps) {
          deps.forEach(effect= > {
            effect(val, oldVal)
          })
        }
      }
    }
    Copy the code
  • Plus the test code:
     effect(() = > state.foo, (val, oldVal) = > {
       console.log(val, oldVal)
     })
    
     effect(() = > state.bar.a, (val, oldVal) = > {
       console.log(val, oldVal)
     })
    Copy the code
  • Congratulations, you passed the test! (~ ▽ ~)~*

Complete code

  const targetMap = new WeakMap(a)// {target, {key: []}}
  const effectStack = []

  const isObject = val= >val ! = =null && typeof val === 'object'

  // Avoid duplicate proxies
  const toProxy = new WeakMap(a){obj: observed}
  const toRaw = new WeakMap(a){observed: obj}

  function reactive(obj) {
    if(! isObject(obj))return obj

    // Avoid executing the agent twice
    if (toProxy.has(obj)) {
      return toProxy.get(obj)
    }

    // Avoid proxying proxied objects
    if (toRaw.has(obj)) {
      return obj
    }

    const observed = new Proxy(obj, {
      get(target, key, receiver) {
        const res = Reflect.get(target, key, receiver)
        // console.log(' get ', key, res)
        track(target, key)
        return isObject(res) ? reactive(res) : res
      },
      set(target, key, value, receiver) {
        const oldVal = Reflect.get(target, key, receiver)
        const res = Reflect.set(target, key, value, receiver)
        // console.log(' setup ', oldVal, res)
        trigger(target, key, value, oldVal)
        return res
      },
      deleteProperty(target, key) {
        const oldVal = Reflect.get(target, key)
        const res = Reflect.deleteProperty(target, key)
        // console.log(' delete ', key, res)
        trigger(target, key, undefined, oldVal)
        return res
      }
    })

    / / cache
    toProxy.set(obj, observed)
    toRaw.set(observed, obj)

    return observed
  }

  function effect(getField, callback) {
    try {
      effectStack.push(callback)
      getField()
    } catch (error) {
      console.error(error)
    } finally {
      effectStack.pop()
    }
  }

  // Create a mapping
  function track(target, key) {
    const effect = effectStack[effectStack.length - 1]
    if (effect) {
      let depsMap = targetMap.get(target)
      if(! depsMap) { depsMap =new Map()
        targetMap.set(target, depsMap)
      }

      let deps = depsMap.get(key)
      if(! deps) { deps =new Set()
        depsMap.set(key, deps)
      }

      deps.add(effect)
    }
  }

  / / triggers
  function trigger(target, key, val, oldVal) {
    const depsMap = targetMap.get(target)
    if (depsMap) {
      const deps = depsMap.get(key)
      if (deps) {
        deps.forEach(effect= > {
          effect(val, oldVal)
        })
      }
    }
  }

  const obj = { foo: 'foo'.bar: { a: 1 }, arr: [1.2.3]}const state = reactive(obj)

  effect(() = > state.foo, (val, oldVal) = > {
    console.log(val, oldVal)
  })

  effect(() = > state.bar.a, (val, oldVal) = > {
    console.log(val, oldVal)
  })

  / / to get
  state.foo

  / / set
  state.foo = 'foooooooo'

  / / delete
  delete state.foo

  // Add attributes
  state.dong = 'dong'
  state.dong

  // Nested objects
  state.bar.a = 10

  / / array
  state.arr
  state.arr.push(5)
Copy the code

Welcome to put forward comments and suggestions, attention, collection to a wave! skr~