Vue3 responsive principle

I. Introduction:

The purpose of writing this article is to summarize some of my experience in learning Vue3 and the details that need to be paid attention to, why I should do this, and what is the principle of doing this. This article will outline the Vue3 responsive principle in as concise a language as possible, so some edge cases will be omitted.

Vue version: V3.2.47

The difference between Vue3 and Vue2 responsiveness:

  • In order to be compatible with most browsers, Vue2 uses Object.defineProperty() to implement responsiveness, and Vue3 uses proxy
  • Vue2 cannot directly monitor new attributes, delete attributes, and modify arrays directly through subscripts.

related information:

  • Proxy and Reflect
  • The difference between Map and WeakMap
  • Strong and Weak References
  • garbage collection mechanism
  • Set data structure
  • TypeScript

2. Objectives:

Implement the responsiveness of the following code:

<!-- HTML -->
<body>
  <div id="app">
  </div>
  <div id="star">
  </div>
</body>
//script
const user = {
  name:'林夕',
  age:18
}

const star = {
  name:'张三',
  sex:'男',
  age:42
}

document.querySelector('#app').innerText = `${user.name} - ${user.age}`
document.querySelector('#star').innerText = `${star.name} - ${star.sex} - ${star.age}`

setTimeout(() => {
  user.name = '林夕很帅'
  setTimeout(() => {
    star.age = 20
  }, 1000)
}, 2000)

3. Analysis:

In Vue3, proxy is used to intercept and process data, and reactive is used to package reference type data to achieve responsiveness. The watchEffect function can monitor changes in object data. Let's imitate Vue3's responsive implementation.

We're going to do it this way:

File Directory:

 The js file is generated after compiling the ts file. Note that the module in tsconfig.json here should be changed to 'ES2015', and the global strict mode should be turned off

 index.html:

<body>
  <div id="app">
  </div>
  <div id="star">
  </div>
</body>
<script type="module">
  import { reactive } from './reactive.js'
  import { watchEffect } from './effect.js'
  const user = reactive({
    name: "林夕",
    age: 18
  })
  const star = reactive({
    name: '张三',
    sex: '男',
    age: 42
  })
  watchEffect(() => {
    document.querySelector('#app').innerText = `${user.name} - ${user.age}`
  })

  watchEffect(() => {
    document.querySelector('#star').innerText = `${star.name} - ${star.sex} - ${star.age}`
  })

  setTimeout(() => {
    user.name = '林夕很帅'
    setTimeout(() => {
      star.age = 20
    }, 1000)
  }, 2000)
</script>

Effect:

 

1. He has the following characteristics:

  1. When the properties of the responsive object change, the side effect function can monitor and execute the internal function
  2. When the property of the reactive object changes, the view also changes

2. The data that needs to be used are:

  1. The property (key) of the responsive object
  2. Responsive object (target)
  3. side effect function (effect)

3. The relationship between them:

  1. One attribute (key) can correspond to multiple side effect functions (effect)

  2. A same attribute name (for example: name) may correspond to multiple different responsive objects (user, star)

The attribute key here can be understood as the dep endency of the side effect function, and the attribute value here is used to perform this function, so this side effect function can also be said to be a subscriber (subscriber) it depends on

4. Then we can use the data structure shown in the figure below

WeakMap<target, Map<key, Set<effect>>>

 

Diamonds represent keys, rectangles represent values

  • Purple represents the mapping of the responsive object target, and the data type is WeakMap
  • Blue indicates the mapping of the attribute key of the responsive object, and the data type is Map
  • Green indicates the side effect function effect collection (side effect function bucket), and the data type is Set
  • Yellow indicates the side effect function effect

4. Code implementation

1. watchEffect implementation

let activeEffect
export const watchEffect= (update: Function) => {
  const effect = function () {
    activeEffect = effect
    update()
    activeEffect = null
  }
  effect()
}

2. Reactive implementation

export const reactive = <T extends object>(target: T) => {
  return new Proxy(target, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver)
      track(target, key)
      return res
    },
    set(target, key, value, receiver) {
      const res = Reflect.set(target, key, value, receiver)
      // 这里的trigger()一定要放到反射之后,因为反射完成后,原始对象才会改变,track则没有这种问题
      trigger(target, key)
      return res
    },
  })
}

The track() function here is used to collect side-effect functions, and the trigger() function is used to trigger side-effect functions

3. track implementation

let targetMap = new WeakMap()
export const track = (target, key) => {
  // 响应式对象target的映射
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  // 响应式对象target的key的映射
  let deps = depsMap.get(key)
  if (!deps) {
    deps = new Set()
    depsMap.set(key, deps)
  }
  // 若当前运行的副作用函数不为空,则把副作用函数添加到集合内
  if (activeEffect) {
    deps.add(activeEffect)
  }
}

4. trigger implementation

export const trigger = (target, key) => {
  const depsMap = targetMap.get(target)
  const deps = depsMap.get(key)
  deps.forEach((effect) => effect())
}

So far, we have completed a simple response. But if the property of the reactive object is a reference type, it is currently impossible. At this time, we can consider using recursion. When the obtained value type is a reference type, we can call the reactive function again

5. Recursion

reactive.ts:

// null也是对象
const isObject = (target) => target != null && typeof target == 'object'

export const reactive = <T extends object>(target: T) => {
  return new Proxy(target, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver) as object  //这里使用类型断言
      track(target, key)
      if (isObject(res)) {
        return reactive(res)
      }
      return res
    },
    set(target, key, value, receiver) {
      const res = Reflect.set(target, key, value, receiver)
      // 这里的trigger()一定要放到反射之后,因为反射完成后,原始对象才会改变,track则没有这种问题
      trigger(target, key)

      return res
    },
  })
}

index.html

<body>
  <div id="app">
  </div>
  <div id="star">
  </div>
</body>
<script type="module">
  import { reactive } from './reactive.js'
  import { watchEffect } from './effect.js'
  const user = reactive({
    name: "林夕",
    age: 18
  })
  const star = reactive({
    name: '张三',
    sex: '男',
    info: {
      age: 42,
    }
  })
  watchEffect(() => {
    document.querySelector('#app').innerText = `${user.name} - ${user.age}`
  })

  watchEffect(() => {
    document.querySelector('#star').innerText = `${star.name} - ${star.sex} - ${star.info.age}`
  })

  setTimeout(() => {
    user.name = '林夕很帅'
    setTimeout(() => {
      star.info.age = 20
    }, 1000)
  }, 2000)
</script>

So far, this responsive style can handle most scenarios

5. Thinking

1. Why does targetMap use the WeakMap data structure?

answer:

1. The key of WeakMap can only be a reference type

2. WeakMap is a weak reference, which can be automatically recycled by the garbage collection mechanism without manual deletion

2. Why does the value of depsMap use the Set data structure instead of an array?

Answer: The value in the Set data structure is unique and naturally supports deduplication

Guess you like

Origin blog.csdn.net/Linxi_001/article/details/130742690