深入理解vue3-Reactivity

959 阅读15分钟

背景

背景 vu3出来有一段时间了,最近刚好有空可以去学习下,直接看源码的话有点难懂,索性就对着文档和源码自己慢慢去实现一把,所以在这背景上,决定从0开始对着api,造一个最精简的依赖收集和响应

依赖收集和响应

实现响应式有2个最关键的步骤:依赖收集和响应,其中Vue是通过数据劫持的方式,vue2因为兼容性原因采用Object.defineProperty,也带来一些问题

  1. 性能差,初始化开始就必须递归将所有数据进行劫持
  2. 只能对对象属性进行劫持,添加、删除属性需要用set函数进行操作 vue3正式采用proxy方式进行劫持
  3. 默认只劫持顶层对象,只有访问过的子对象才会进行进一步劫持
  4. proxy可以针对属性的增、删进行劫持

reactive

vue3中使用 reactive生成响应式对象,实际上就是返回Proxy代理对象,可以尝试自己写一个

const obj = {
    bar: {
        title: 'hello'
    },
    foo: 1
}
const myReactive = (obj) => {
    return new Proxy(obj, {
        get(target, key, receiver) {
            console.log('get!!!!')
            const res = Reflect.get(target, key, receiver)
            return res
        },
        set(target, key, value, receiver) {
            console.log('set!!!')
            target[key] = value
            return true
        }
    })
}

const myReactiveObj = myReactive(obj)
console.log(myReactiveObj.bar)           //get!!!  {title: 'hello'}
myReactiveObj.bar = {                  // set!!!
    title: 'world'
}
myReactiveObj.bar.title = 'world1'     //get!!!

这样一来无论是get/set都能触发相应的拦截函数

嵌套对象如何触发set

上面的例子,执行 myReactiveObj.bar.title = 'world1'时,只触发了get,原因在于只有顶层对象和属性被代理,子对象没有被代理
解决方式也简单,只要在get拦截上再次代理子对象即可

const isObject = (val)=> val !== null && typeof val === 'object'
const obj = {
    bar: {
        title: 'hello'
    },
    foo: 1
}
const myReactive = (obj) => {
    return new Proxy(obj, {
        get(target, key, receiver) {
            console.log('get!!!!')
            const res = Reflect.get(target, key, receiver)
            return isObject(res) ? myReactive(res) : res
        },
        set(target, key, value, receiver) {
            console.log('set!!!')
            target[key] = value
            return true
        }
    })
}

const myReactiveObj = myReactive(obj)
console.log(myReactiveObj.bar)           //get!!!  {title: 'hello'}
myReactiveObj.bar = {                  // set!!!
    title: 'world'
}
myReactiveObj.bar.title = 'world1'     //get!!!  set!!!

effect

定义一个副作用函数,具体的描述可以看下官方文档,主要是将副作用函数以及响应式对象关联起来,执行副作用函数收集依赖,修改响应式对象再执行副作用函数

const isObject = (val)=> val !== null && typeof val === 'object'
const obj = {
    bar: {
        title: 'hello'
    },
    foo: 1
}
const myReactive = (obj) => {
    return new Proxy(obj, {
        get(target, key, receiver) {
            const res = Reflect.get(target, key, receiver)
            track(target, key)        //收集依赖
            return isObject(res) ? myReactive(res) : res
        },
        set(target, key, value, receiver) {
            target[key] = value
            trigger(target, key)    //触发副作用函数
            return true
        }
    })
}

//全局记录收集的依赖对象
const targetMap = new WeakMap()
//收集依赖
const track = (target, key) => {
    const dep = targetMap.get(target)
    if(!dep) {
        targetMap.set(target, [key])
    }
}

//响应
const trigger = (target) => {
    const dep = targetMap.get(target)
    if(dep && activeEffect) {
        activeEffect()
    }
}
//记录当前执行的副作用函数
let activeEffect = null
const myEffect = (fn) => {
    activeEffect = fn
    return fn()
}


const myReactiveObj = myReactive(obj)
myEffect(() => {
    console.log('副作用函数执行')
    console.log(myReactiveObj.bar)
})

myReactiveObj.bar = {
    title: 'world'
}

myReactiveObj.bar = {
    title: 'world1'
}

实现方式非常简单

  1. 执行myEffect传入的函数,触发依赖收集
  2. 在get拦截函数上触发track函数,targetMap保存依赖,key就是当前执行的target,value暂不传入
  3. 修改值时触发set函数,执行2收集的依赖函数(effect)

这样就简单的实现了一个依赖收集和响应的系统,但是存在一些问题

无意义的响应

比如上面的示例改成

myEffect(() => {
    console.log('副作用函数执行')
    console.log(myReactiveObj.bar)
})
myReactiveObj.foo = 2

副作用函数执行了2次,但是函数内只读取了myReactiveObj.bar
原因在于targetMap使用了{target: [effect]}这样的数据结构,.foo.bar都属于myReactiveObj的属性,所以被触发了
解决方式也简单,将key也加入依赖集合的对象

//收集依赖
const track = (target, key) => {
    let deps = targetMap.get(target)   //deps 即为不同key下的依赖集合
    if(!deps) {
        targetMap.set(target, (deps = new Map()))
    }
    let dep = deps.get(key)
    if(!dep) {
        deps.set(key, {})
    }
}
//响应
const trigger = (target, key) => {
    const deps = targetMap.get(target)
    if(!deps) return
    const dep = deps.get(key)
    if(dep && activeEffect) {
        activeEffect()
    }
}

targetMap的数据结构将变成这样

{
    target: {
        key1: {}
        key2: {}
    }
}

响应死循环

如果在effect里面同时进行get/set的话,就会变成死循环,effect -> get/set -> effect -> ... ,比如

myEffect(() => {
    console.log('副作用函数执行')
    console.log(myReactiveObj.bar)    
    myReactiveObj.bar = {
        title: 'world'
    }
})
  1. track阶段记录key关联的effect
  2. trigger阶段判断下当前key关联的effect跟activeEffect是否同一个,触发新的effect时将activeEffect指向到1关联的effect
  3. myEffect执行完成之后释放activeEffect 修改之后如下
let activeEffect = null
const myEffect = (fn) => {
    try {
        activeEffect = fn
        return fn()
    }finally {
        activeEffect = null         //执行完之后需要释放activeEffect
    }
}
//收集依赖
const track = (target, key) => {
    let deps = targetMap.get(target)   //deps 即为不同key下的依赖集合
    if(!deps) {
        targetMap.set(target, (deps = new Map()))
    }
    let dep = deps.get(key)
    if(!dep) {
        deps.set(key, activeEffect)         //依赖收集阶段记录当前执行的effect
    }
}
//响应
const trigger = (target, key) => {
    const deps = targetMap.get(target)
    if(!deps) return
    const dep = deps.get(key)
    if(dep !== activeEffect) {              //判断activeEffect和key记录的effect是否一致
        activeEffect = dep                  // 触发trigger再次记录当前执行的effect
        activeEffect()
    }
}

无法定义多个副作用函数

myEffect(() => {
    console.log('副作用函数1执行')
    console.log(myReactiveObj.bar)
})
myEffect(() => {
    console.log('副作用函数2执行')
    console.log(myReactiveObj.bar)
})

myReactiveObj.bar = {   //set之后,只有副作用函数2被触发
    title: 'world'
}

原因在于,targetMap的数据结构只记录一个activeEffect,再触发trigger响应时自然就只会执行第一个收集的effect,可以改成在 track 阶段关联key和多个effect,trigger阶段循环去执行即可(修改下变量命名)

//收集依赖
const track = (target, key) => {
    let keyMap = targetMap.get(target)   //keyMap 即为不同key下的依赖集合
    if(!keyMap) {
        targetMap.set(target, (keyMap = new Map()))
    }
    let deps = keyMap.get(key) || []
    if(!deps.includes(activeEffect)) {      //避免重复收集
        deps.push(activeEffect)
        keyMap.set(key, deps)         //deps这里变成一个数组,记录多个effect
    }
}
//响应
const trigger = (target, key) => {
    const keyMap = targetMap.get(target)
    if(!keyMap) return
    const deps = keyMap.get(key)
    for(const dep of deps) {
        if(dep !== activeEffect) {              //判断activeEffect和key记录的effect是否一致
            activeEffect = dep                  // 触发trigger再次记录当前执行的effect
            activeEffect()
        }
    }
}

嵌套死循环

示例如下

myEffect(() => {
    console.log('副作用函数1执行')
    console.log(myReactiveObj.bar)
    myReactiveObj.bar = {
        title: 'world1'
    }
})
myEffect(() => {
    console.log('副作用函数2执行')
    console.log(myReactiveObj.bar)
    myReactiveObj.bar = {
        title: 'world2'
    }
})

通过依赖收集可以看到.bar这个key记录2个effect,里面都修改了.bar的属性,那么按照trigger的逻辑,只会过滤当前执行的activeEffect,还是会执行另外一个effect,从而导致了死循环
解决方式关键在于 activeEffect在每次trigger触发的时候需要记录调用栈,在触发的时候去判断该effect是否存在调用栈中

class ReactiveEffect {
    constructor(fn) {
        this.fn = fn
        this.parent = null
    }
    run() {
        let parent = activeEffect
        while (parent) {
            if (parent === this) {
                return
            }
            parent = parent.parent
        }
        try {
            this.parent = activeEffect          //每次调用时记录当前的执行栈
            activeEffect = this
            return this.fn()
        }finally {
            activeEffect = this.parent
            this.parent = undefined             //一次set的响应结束后需要释放当前的执行栈
        }

    }
}

const myEffect = (fn) => {
    //activeEffect的数据结构就变成activeEffect: ReactiveEffect
    const _effect = new ReactiveEffect(fn)
    return _effect.run()
}

const trigger = (target, key) => {
    const keyMap = targetMap.get(target)
    if(!keyMap) return
    const deps = keyMap.get(key) || []
    for(const dep of deps) {
        if(dep !== activeEffect) {              //判断activeEffect和key记录的effect是否一致
            dep.run()
        }
    }
}

这段逻辑比较绕,可以这样梳理下

  1. 执行effect1时,console.log(myReactiveObj.bar)触发.bar依赖收集,此时的activeEffectdeps数据结构就是
// deps
[
    {                   //ReactiveEffect
        fn: effec1,
        parant: null
    }
]
// activeEffect
{
    fn: effec1,
    parent: null
}

myReactiveObj.bar = xxx时,因为activeEffect.fn === 'bar[0].fn',所以trigger阶段的effect1并没有触发
2. 执行effect2时,console.log(myReactiveObj.bar)触发.bar依赖收集,此时的activeEffectdeps数据结构就是

// deps
[
    {                   //ReactiveEffect
        fn: effec1,
        parant: null
    },
    {                   //ReactiveEffect
        fn: effec2,
        parant: null
    }
]
// activeEffect
{
    fn: effec2,
    parent: null
}

执行到myReactiveObj.bar = xxx时,trigger阶段,循环deps,因为deps[0].fn !== activeEffect.fn,所以触发了deps[0].fn.run
注意!!!此时的activeEffectdeps数据结构已经变成

//deps
[
    {                   //ReactiveEffect
        fn: effec1,
        parant: effec2   //记录了上一次执行的effect
    },
    {                   //ReactiveEffect
        fn: effec2,
        parant: null
    }
]
// activeEffect
{
    fn: effec1,
    parent: effec2    //记录了上一次执行的effect
}
  1. 再执行effect1,get阶段不会再次收集同个依赖(去重),set阶段触发trigger,循环deps,deps[0].fn === activeEffect.fn,故不执行,执行deps[1].fn.run递归判断activeEffect的调用栈有deps[1].fn,不执行effect1,直接返回
  2. 回到2的循环deps中,因为deps[1].fn !== activeEffect.fn,不执行,结束所有的响应

至此,一个基本的依赖收集和响应的雏形就出来了,上面都是通过reactive创建响应式对象进行举例,实际上官方还有很多API创建响应式对象,比如shallowReactiverefshallowRef等,但是实际上依赖收集和响应都差不多,大家可以自己去看下。下面简单介绍下官方的几个API实现吧

reactive

可以使用reactive函数创建一个深响应式对象、数组、Map、Set,最终会返回Proxy代理对象

import { reactive, isReactive } from 'vue'
const obj = {
    bar: {
        title: 'hello'
    }
}
const reactiveObj = reactive(obj)

深响应式的实现方式就是在于访问顶层对象触发get拦截时, 将嵌套的子对象转换为代理对象。同时针对不同数据类型返回了不同的代理对象,objectarray类型数据返回的代理对象 mutableHandlers , MapSetWeakMapWeakSet返回的代理对象 mutableCollectionHandlers ,其余数据类型直接返回原始值

为什么针对MapSetWeakMapWeakSet类型的数据只拦截了get呢?
主要是因为map类型数据都是通过map.set('key', 'value')调用set方法操作,也就是先获取.set,故只需要拦截get操作,在get内部实现 .set 等方法的拦截,即createInstrumentations

shallowReactive

返回浅响应数据的代理对象,实现方式跟reactive差不多,唯一区别在于访问深层次的对象,reactive返回的是代理对象,shallowReactive返回原始对象,相关源码

import { shallowReactive, effect } from 'vue'
const obj = { 
    bar: { 
        title: 'hello' 
    }
}
const shallowObj = shallowReactive(obj)
effect(() => {
    console.log('副作用函数1执行')
    console.log(shallowObj.bar)
})

//因为顶层对象有被代理,所以会触发set
shallowObj.bar = {        
    title: 'world'
}

//对深嵌套对象进行赋值,会先获取顶层对象,触发get,返回原始对象,再继续对返回的对象进行赋值,所以没有触发set
shallowObj.bar.title = 'world'      

readonly

返回只读代理对象,get阶段不进行依赖收集(相关源码),拦截set不改变原始值(相关源码

import { readonly } from 'vue'
const obj = { 
    bar: { 
        title: 'hello' 
    }
}
const readonlyObj = readonly(obj)
//warning: Set operation on key "bar" failed: target is readonly
readonlyObj.bar = {
    title: 'world'
}

console.log(readonlyObj.bar.title)   //hello

shallowReadonly

返回浅响应只读的代理对象,阻止修改且不进行依赖收集,实现原理其实就是只代理了顶层对象,然后拦截了get(不进行依赖收集)、set(阻止修改)

isReactive、isReadonly、isShallow、isProxy

判断数据类型,是否由对应响应式函数创建出来的数据,实现方式非常简单

  1. 尝试访问几个枚举的属性ReactiveFlags相关源码
  2. 触发get拦截,判断特定key === ReactiveFlags枚举,返回true/false,相关源码

有一点副作用,比如原始对象里面刚好包含了枚举值里的key属性,那么也会返回true,比如

import { isReactive } from 'vue'
const obj = { 
    bar: { 
        title: 'hello' 
    }
}
console.log(isReactive({
    ...obj,
    __v_isReactive: true
}))   //true

markRaw

标记某个对象使其不会成功响应式对象,实现原理如下

  1. markRaw函数给对象添加__v_skip属性 相关源码
  2. reactive函数通过判断__v_skip属性,跳过代理直接返回原始对象 相关源码
import { markRaw, reactive } from 'vue'
const obj = { 
    bar: { 
        title: 'hello' 
    }
}
const reactiveObj = reactive(markRaw(obj))    // 直接返回原始对象
console.log(reactiveObj === obj)       // true

其实也可以手动给对象添加__v_skip属性,同样会跳过代理

import { reactive } from 'vue'
const obj = { 
    bar: { 
        title: 'hello' 
    }
    __v_skip: true
}
const reactiveObj = reactive(obj)    // 直接返回原始对象
console.log(reactiveObj === obj)       // true

toRaw

返回创建响应式对象的原始对象,实现原理如下

  1. reactive创建响应式对象时,通过proxyMap记录原始对象(key)和响应式对象(value),缓存原始对象,避免重复创建同个对象的响应式对象 相关源码
  2. toRaw的时候,访问__v_raw,触发get拦截函数,在里面判断特定key,返回proxyMap记录的原始对象 相关源码
import { reactive, toRaw } from 'vue'
const obj = { 
    bar: { 
        title: 'hello' 
    }
}
const reactiveObj = reactive(obj)
console.log(toRaw(reactiveObj) === obj)       // true

ReactiveFlags

内置的枚举值,上面也有说到,简单说明下各自的作用 相关源码

export const enum ReactiveFlags {
  SKIP = '__v_skip',                //跳过代理
  IS_REACTIVE = '__v_isReactive',   //访问该key时返回是否有代理的标记
  IS_READONLY = '__v_isReadonly',   //访问该key时返回是否只读的标记
  IS_SHALLOW = '__v_isShallow',     //访问该key时返回是否为浅响应对象的标记
  RAW = '__v_raw'                   //访问该key时返回原始对象的标记
}

ref

reactive通过Proxy实现代理对象,但是只支持对象类型,故vue3还提供了ref函数,可以针对任意类型的数据生成响应式对象

import { ref } from 'vue'
const obj = { 
    bar: { 
        title: 'hello' 
    }
}
const text = 'hello'
const textRef = ref(text)
const objRef = ref(obj)
console.log(textRef.value)
console.log(objRef.value)
textRef.value = 'world'
objRef.value.bar.title = 'world'

将原始数据包装为一个带 .value property 的 ref 对象,实现方式如下 相关源码

  1. new 一个ref对象,绑定value属性的getter/setter进行读写的拦截
  2. 初始化私有属性_value(getter直接返回该私有属性),如果传入对象、set、map的话,调用reactive返回其代理对象,其它返回原始值
    后续需要读写其响应式对象时,均需要带上value
//Ref内部私有属性
class RefImpl<T> {
  private _value: T           //响应式对象或者原始值
  private _rawValue: T        //原始数据

  public dep?: Dep = undefined
  public readonly __v_isRef = true   //是否已转换过响应式对象的标记
  //xxxxx
}

shallowRef

返回浅响应式对象,实现方式很简单,_value直接返回原始值即可,这样的话,只会拦截.value的getter/setter,访问深层次的对象时操作的是原始值,并不会触发reactive代理对象的get/set拦截函数 相关源码

isRef

判断是否属于Ref对象,通过ref/shallowRef函数转换过的响应式对象均会带上一个内置属性__v_isRef,故只需要判断该属性true/false即可 相关源码
isReactive,如果原始对象就有__v_isRef属性,也会返回true

toRef

在写代码过程中,有一些写法可能会导致响应式丢失的情况,比如解构

import { reactive, toRef } from 'vue'
const obj = { 
    bar: { 
        title: 'hello' 
    }
}
const reactiveObj = reactive(obj)
let {title} = reactiveObj.bar
title = 'world'               //修改title并不会触发响应

这时候需要使用toRef关联上源响应式对象

import { reactive, toRef } from 'vue'
const obj = { 
    bar: { 
        title: 'hello' 
    }
}
const reactiveObj = reactive(obj)
let title = toRef(reactiveObj.bar, 'title')

title.value = 'world'
console.log(reactiveObj.bar.title)   //world

reactiveObj.bar.title = 'hello'
console.log(title.value)  //hello

当然你也可以把一个普通对象的key转换为Ref对象,但是该ref对象进行get/set操作时,并不会触发依赖收集和响应 相关源码

import { toRef, effect } from 'vue'
const obj = { 
    bar: { 
        title: 'hello' 
    }
}
const toRefObj = toRef(obj, 'bar')

//effect只会触发一次
//副作用,后面会讲到
effect(() => {
    console.log(toRefObj.value)
})
toRefObj.value.title = 'world'    //修改不会触发响应

toRefs

toRef只能转换单个key,toRefs将对象的所有key都转换成ref对象 相关源码

import { toRefs, reactive } from 'vue'
const obj = { 
    bar: { 
        title: 'hello' 
    }
}
const reactiveObj = reactive(obj)
const {bar} = toRefs(reactiveObj)           //直接解构不会丢失响应式
bar.value = 'world'
console.log(reactiveObj.bar.title)          //world

比较不一样的是,如果传入的不是一个reactive响应式对象,那么会有告警toRefs() expects a reactive object but received a plain one.(为啥toRef就没有呢?)

proxyRefs

官方文档没有该API的说明,笔者这里猜测应该是下面这些原因
在深嵌套对象中有部分子对象是ref类型的场景下,在操作子对象时候非常麻烦,需要通过isRef判断需不需要加上.value,比如

import { ref, isRef, proxyRefs } from 'vue'
const data = {
    bar: ref(0)
}
if(isRef(data.bar)) {
    console.log(data.bar.value)     
}else{
    console.log(data.bar)
}

为了减少心智负担,提供了proxyRefs包装这类型的对象

import { ref, isRef, proxyRefs } from 'vue'
const data = {
    bar: ref(0)
}
if(isRef(data.bar)) {
    console.log(data.bar.value)     
}else{
    console.log(data.bar)
}

let proxyRefsObj = proxyRefs(data)
proxyRefsObj.bar = 1          //这里可以直接.bar方式修改

console.log(data.bar.value)  //1
console.log(proxyRefsObj.bar)  //1

实现方式很简单,就是给ref对象套一层proxy,在get/set自动加上.value 相关源码

effect

前面实现了基础的响应系统,这里介绍下官方提供的一些能力以及实现的思路

lazy

定义一个副作用函数会立即执行,但是加上lazy之后就不会,需要手动去执行runner

import { effect, reactive } from 'vue'
const obj = {
    bar: {
        title: 'hello'
    }
}
const reactiveObj = reactive(obj)
const runner = effect(() => {
    console.log('副作用函数1执行')
    console.log(reactiveObj.bar)
}, {
    lazy: true
})
reactiveObj.bar = {
    title: 'world'
}
runner()   //输出 "world"

有什么应用场景呢?如果你的响应函数非常耗性能,你希望自己去控制触发,那么你可以加上lazy,在合适的时机触发响应,有点类似小程序的setData

scheduler

副作用函数调度器,如果你希望修改reactive不想频繁执行副作用函数又不希望手动去处理(lazy),那么你可以通过调度器去指定副作用函数如何触发,比如实现一个防抖

import { effect, reactive } from 'vue'
// 使用 reactive() 函数定义响应式数据
const obj = {
    bar: {
        title: 'hello'
    }
}
let timer = null
const reactiveObj = reactive(obj)
const runner = effect(() => {
    console.log('副作用函数1执行')
    console.log(reactiveObj.bar)
}, {
    scheduler: (fn) => {
        clearTimeout(timer)
        timer = setTimeout(() => {
            runner()
        }, 300)
    }
})
reactiveObj.bar = {       
    title: 'world'
}
reactiveObj.bar = {      
    title: 'world1'
}
reactiveObj.bar = {       //world2
    title: 'world2'
}

定义了调度器之后,除了第一次会执行副作用之外,后面所有的trigger都只会执行调度器的函数

scope

给该effect副作用函数创建到一个作用域里面(想不到更好的描述了),便于后续回收、销毁,需要先理解vue3.2之后引用的新API effectScope,如果没有该API,那么你需要自己收集响应,然后在合适的时机进行销毁,比如

const effectList = []
const runner1 = effect(() => {
    console.log('副作用函数1执行')
    console.log(reactiveObj.bar)
})
const runner2 = effect(() => {
    console.log('副作用函数2执行')
    console.log(reactiveObj.bar)
})
effectList.push(runner1)
effectList.push(runner2)

//....

//销毁
effectList.forEach(e => {
    stop(e)
})

有了effectScope之后就可以很方便的进行销毁了

const scope = effectScope()
scope.run(() => {
    effect(() => {
        console.log('副作用函数1执行')
        console.log(reactiveObj.bar)
    }, {
        scope
    })
    effect(() => {
        console.log('副作用函数2执行')
        console.log(reactiveObj.bar)
    }, {
        scope
    })
})

//....

scope.stop()

当前创建effect时也可以指定到某个scope下,比如

effect(() => {
    console.log('副作用函数2执行')
    console.log(reactiveObj.bar)
}, {
    scope
})

allowRecurse

这个配置很有迷惑性,刚看到的时候以为是允许递归调用自身effect(可能会导致死循环,参考响应死循环章节),实际看源码发现不是,跟 scheduler 有关,比如

effect(() => {
    console.log('副作用函数1执行')
    console.log(reactiveObj.bar)
    reactiveObj.bar = {               //修改不会再次触发effect
        title: 'world1'
    }
}, {
    scheduler: () => {                 //也不会触发scheduler
        console.log('scheduler')
    },
})

加上allowRecurse

effect(() => {
    console.log('副作用函数1执行')
    console.log(reactiveObj.bar)
    reactiveObj.bar = {               //修改不会再次触发effect
        title: 'world1'
    }
}, {
    scheduler: () => {                //会触发scheduler
      //  reactiveObj.bar = {           // schedulerz再次修改的话,就变成死循环了,千万要注意
      //      title: 'world1'
      //  }
        console.log('scheduler')
    },
    allowRecurse: true
})

所有 allowRecurse 配置是针对是否允许递归调用 scheduler

onStop

这个配置就很好理解,销毁响应时的回调