全面了解Vue3的 ref 和相关函数和计算属性
基础类型的响应性 —— ref
在vue3里面,我们可以通过 reactive 来实现引用类型的响应性,那么基础类型的响应性如何来实现呢?
可能你会想到这样来实现:
const count = reactive({value: 0})
count.value += 1
这么做确实可以实现,而且也很像 ref 的使用方式,都是要 .value 嘛。那么 ref内部 是不是这么实现的呢?
我们先定义两个 ref 的实例并且打印看看。
    const refCount = ref(0) // 基础类型
    console.log('refCount ', refCount )
    const refObject = ref({ value: 0 }) // 引用类型
    console.log('refObject ', refObject )
看一下结果:


我们都知道 reactive 是通过 ES6 的 Proxy 来实现的,基础类型的 ref 显然和 Proxy 没啥关系,而引用类型的 ref 是先把原型变成 reactive, 然后再挂到 value 上面。
这样看来,和我们的猜测不太一样呢,那么 ref 到底是如何实现的呢?我们可以看一下 ref 的源码。
ref 的源码
代码来自于 vue.global.js ,调整了一下先后顺序。
  function ref(value) {
      return createRef(value);
  }
  function createRef(rawValue, shallow = false) {
      if (isRef(rawValue)) {
          return rawValue;
      }
      return new RefImpl(rawValue, shallow);
  }
  class RefImpl {
      constructor(_rawValue, _shallow = false) {
          this._rawValue = _rawValue;
          this._shallow = _shallow;
          this.__v_isRef = true;
          this._value = _shallow ? _rawValue : convert(_rawValue); // 深层 ref or 浅层ref
      }
      get value() {
          track(toRaw(this), "get" /* GET */, 'value');
          return this._value;
      }
      set value(newVal) {
          if (hasChanged(toRaw(newVal), this._rawValue)) {
              this._rawValue = newVal;
              this._value = this._shallow ? newVal : convert(newVal);
              trigger(toRaw(this), "set" /* SET */, 'value', newVal);
          }
      }
  }
  const convert = (val) => isObject(val) ? reactive(val) : val;
- ref 
 这是我们使用的函数,里面使用 createRef 来创建一个实例。
- createRef 
 做一些基础判断,然后进入主题,正式创建ref。这里还可以创建 shallowRef。
- RefImpl 
 这个才是主体,显然这是 ES6 的 class,constructor 是初始化函数,依据参数创建一个实例,并且设置实例的属性。这个和上面 ref 的打印结果也是可以对应上的。
 整个class的代码也是非常简单,设置几个“内部”属性,记录需要的数据,然后设置“外部”属性 value,通过setter、getter 实现对 value 的操作拦截,set 里面主要是 trigger 这个函数,由它调用模板的自动刷新的功能。
- convert 
 很显然,判断一下参数是不是 object,如果是的话,变成 reactive 的形式。
 这个就可以解释,引用类型的 ref 是如何实现响应性的,明显是先变成 reactive,然后在挂到 value 上面(挂之前判断一下是不是浅层的)。
ref 和 reactive 的关系
通过打印结果的对比以及分析源码可以发现:
- 基础类型的 ref 和 reactive 没有任何关系。
- 引用类型的 ref ,先把 object 变成 reactive ,即利用 reactive 来实现引用类型的响应性。
关系就是这样的,千万不要再混淆了。
shallowRef
浅层响应式,只监听 .value 的变化,真简单类型的响应式。
function shallowRef(value) {
      return createRef(value, true); // true 浅层
  }
通过源码我们可以发现,在把引用类型挂到 value 之前,先判断一下是不是浅层的,如果是浅层的,并不会变成 reactive,而是直接把原来的对象挂在 value 上面,shallowRef 和 ref 的区别就在于这一点。
我们写几个实例看看效果:
  setup () {
     // 浅层的测试
    // 基础类型
    const srefCount = shallowRef(0)
    console.log('refCount ', srefCount )
    // 引用类型
    const srefObject = shallowRef({ value: 0 })
    console.log('refObject ', srefObject )
    // 嵌套对象
    const srefObjectMore = shallowRef({ info: {a: 'jyk'} })
    console.log('shallowRef ', srefObjectMore )
    // reactive 的 shallowRef
    const ret = reactive({name: 'jyk'})
    const shallowRefRet = shallowRef(ret)
    console.log('shallowRefRet ', shallowRefRet )
    // ==================== 事件 ==================
    // 修改基础类型
    const setNumber = () => {
      srefCount.value = new Date().valueOf()
      console.log('srefCount ', srefCount )
    }
    // 修改引用类型的属性
    const setObjectProp = () => {
      srefObject.value.value = new Date().valueOf()
      console.log('srefObject ', srefObject )
    }
    // 修改引用类型的value
    const setObject = () => {
      srefObject.value = { value: new Date().valueOf() }
      console.log('srefObject ', srefObject )
    }
    // 修改嵌套引用类型的属性
    const setObjectMoreProp = () => {
      srefObjectMore.value.info.a = new Date().valueOf()
      console.log('srefObjectMore ', srefObjectMore )
    }
    // 修改嵌套引用类型的value
    const setObjectMore = () => {
      srefObjectMore.value = { qiantao: 1234567 }
      console.log('srefObjectMore ', srefObjectMore )
    }
    // 修改reactive 的浅层ref
    const setObjectreactive = () => {
      shallowRefRet.value.name = '浅层的reactive'
      console.log('shallowRefRet ', shallowRefRet )
    }
  }
看看结果:

测试了一下响应性:
- 基础类型 srefCount 有响应性;
- 引用类型 srefObject 的属性没有响应性,但是直接修改 .value 是有响应性的。
- 嵌套的引用类型 srefObjectMore ,属性和嵌套属性都是没有响应性的,但是直接修改 .value 是有响应性的。
- reactive 套上 shallowRef ,然后修改 shallowRef.value.属性 = xxx ,也是可以响应的,所以浅层的ref 也不绝对,还要看内部结构。
triggerRef
手动执行与 shallowRef 关联的任何效果。
官网的中文版里面写的很绕,其实就是 让 shallowRef 原本不具有响应性的部分,具有响应性。
shallowRef 是浅层的,深层部分是没有响应性的,那么如果非得让这部分也具有响应性呢?
这时候可以用 triggerRef 来实现。
好吧,目前还没有想到有啥具体的应用场景,因为一般都直接简单粗暴的用 ref 或者 reactive 了,全都自带响应性。
测试了各种情况,发现 triggerRef 并不支持 shallowReactive,还以为能支持呢。(或许是我写的测试代码有问题吧,官网也没提 shallowReactive)
基于上面的例子,在适当的位置加上 triggerRef(xxx)就可以了。
  setup () {
    // 引用类型
    const srefObject = shallowRef({ value: 0 })
    // 嵌套对象
    const srefObjectMore = shallowRef({ value: {a: 'jyk'} })
    // reactive 的 shallowRef
    const ret = reactive({name: 'reactive'})
    const shallowRefRet = shallowRef(ret)
    // 浅层的reactive
    const myShallowReactive = shallowReactive({info:{name:'myShallowReactive'}})
    const setsRet = () => {
      myShallowReactive.info.name = new Date().valueOf()
      triggerRef(myShallowReactive)  // 修改后使用,不支持
   }
    // ==================== 事件 ==================
    // 修改引用类型的属性
    const setObjectProp = () => {
      srefObject.value.value = new Date().valueOf()
      triggerRef(srefObject) // 修改后使用
    }
    // 修改引用类型的value
    const setObject = () => {
      srefObject.value = { value: new Date().valueOf() }
      triggerRef(srefObject)
   }
    // 修改嵌套引用类型的属性
    const setObjectMoreProp = () => {
      srefObjectMore.value.value.a = new Date().valueOf()
      triggerRef(srefObjectMore)
  }
    // 修改嵌套引用类型的value
    const setObjectMore = () => {
      srefObjectMore.value.value = { value: new Date().valueOf() }
      triggerRef(srefObjectMore)
    }
    // 修改reactive 的浅层ref
    const setObjectreactive = () => {
      shallowRefRet.value.name = '浅层的reactive' + new Date().valueOf()
      triggerRef(shallowRefRet)
    }
    return {
      srefObject, // 引用类型
      srefObjectMore, // 嵌套引用类型
      shallowRefRet, // reactive 的浅层ref
      myShallowReactive, // 浅层的reactive
      setsRet, // 修改浅层的reactive
      setObjectProp, // 修改引用类型的属性
      setObject, // 修改引用类型的value
      setObjectMoreProp, // 修改嵌套引用类型的属性
      setObjectMore, // 修改嵌套引用类型的value
      setObjectreactive // 试一试reactive的浅层ref
    }
  }
深层部分,不使用  triggerRef 就不会刷新模板,使用了  triggerRef 就可以刷新模板。
话说,为啥会有这个函数?
isRef
通过 __v_isRef 属性 判断是否是 ref 的实例。这个没啥好说的。
vue.global.js 源码:
function isRef(r) {
     return Boolean(r && r.__v_isRef === true);
 }
unref
- 使用.value的语法糖
 unref 是一个语法糖,判断是不是 ref 的,如果是则取.value,不是的话取原型。
 vue.global.js 源码:
  function unref(ref) {
      return isRef(ref) ? ref.value : ref;
  }
- unref 的用途
 普通对象直接.属性即可使用,但是 ref 却需要.value才可以,混合使用的时候容易晕头,尤其在函数内部接收参数的时候,无法确定传入的是 reactive 还是 ref,如果每次都用 isReactive 或者 isRef 来判断类型,然后决定是否用.value,那就太麻烦了。于是有了这个语法糖。
toRef 和 toRefs
toRef 可以用来为源响应式对象上的 property 性创建一个
ref。然后可以将 ref 传递出去,从而保持对其源 property 的响应式连接。
toRefs 将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的ref。
话说,官网的解释为啥总是这么令人费解呢?
我们还是先看看例子
 setup () {
    /**
     * 定义 reactive
     * 直接解构属性,看响应性
     * 使用toRef解构,看响应性
     * 使用toRefs解构,看响应性
     * 按钮只修改reactive
     */
    const person = reactive({
      name: 'jyk',
      age: 18
    })
    console.log('person ', person )
    // 直接获取属性
    const name = person.name
    console.log('name ', name )
    const refName = toRef(person, 'name')
    console.log('refName ', refName )
    const personToRefs = toRefs(person)
    console.log('personToRefs ', personToRefs )
    const update = () => {
      // 修改原型
      person.name = new Date()
    }
    return {
      person, // reactive
      name, // 获取属性
      refName, // 使用toRef
      personToRefs,
      update // 修改属性
    }
  }
当我们修改person的属性值的时候,toRef 和 toRefs 的实例也会自动变化。而直接获取的name属性并不会变化。
toRef 就是想实现直接使用对象的属性名,并且仍然享有响应性的目的。
toRef 就是对reactive 进行解构,然后仍然享有响应性的目的。
其实,说是变成了ref,但是我们看看打印结果就会发现,其实并不完全是ref。


看类名和属性,toRef 和 ref 也是有区别的。
toRef 为啥可以响应
toRef 也是一个语法糖。
如果使用常规的方式对 reactive 进行解构的话就会发现,虽然解构成功了,但是也失去响应性(仅限于基础类型的属性,嵌套对象除外)。
那么如何实现解构后还具有响应性呢?这时候就需要使用 toRef 了。
看上面那个例子,使用 refName 的时候,相当于使用 person['name'],这样就具有响应性了。
你可能会问,就这?对就是这么简单,不信的话,我们来看看源码:
  function toRef(object, key) {
      return isRef(object[key])
          ? object[key]
          : new ObjectRefImpl(object, key);
  }
  class ObjectRefImpl {
      constructor(_object, _key) {
          this._object = _object;
          this._key = _key;
          this.__v_isRef = true;
      }
      get value() {
          return this._object[this._key];  // 相当于 person['name']
      }
      set value(newVal) {
          this._object[this._key] = newVal;
      }
  }
看 get 部分,是不是 相当于 person['name'] ?
另外,虽然 toRef 看起来好像是变成了 ref,但是其实只是变成了 ref (RefImpl)的双胞胎兄弟(ObjectRefImpl),并没有变成 ref(RefImpl)。
ref 是  RefImpl, 而 toRef 是 ObjectRefImpl,这是两个不同的class 。
toRef 可以看做是 ref 同系列的 class,后面还有一个同系列的。
toRefs
了解 toRef 之后,toRefs 就好理解了,从表面上看,可以把 reactive 的所有属性都解构出来,从内部代码来看,就是把多个 toRef 放在了数组(或者对象)里面。
function toRefs(object) {
      if ( !isProxy(object)) {
          console.warn(`toRefs() expects a reactive object but received a plain one.`);
      }
      const ret = isArray(object) ? new Array(object.length) : {};
      for (const key in object) {
          ret[key] = toRef(object, key);
      }
      return ret;
  }
customRef
自定义一个ref,并对其依赖项跟踪和更新触发进行显式控制。它需要一个工厂函数,该函数接收 track 和 trigger 函数作为参数,并应返回一个带有 get 和 set 的对象。
如果上面那段没看懂的话,可以跳过。
简单的说,就是在 ref 原有的 set、get 的基础上,加入我们自己写的代码,以达到一定的目的。
话说,官网写的例子还真是……
反正一开始我是没看懂,后来又反复看,又把代码敲出来运行,又查了一下“debounce”的意思。
最后终于明白了,这是一个防抖(延迟响应)的代码。
一般用户在文本框里输入内容,立即就会响应,但是如果在查询功能里面的话就会有点小郁闷。
用户输入个“1”,立即就去后端查询“1”,
然后用户又输入个“2”,立即又去后端查询“12”,
然后用户又输入个“3”,立即又去后端查询“123”。
……
这个响应是很快,但是有点“折腾”的嫌疑,那么能不能等用户把“123”都输入好了,再去后端查询呢?
官网的例子就是实现这样的功能的,我们把例子完善一下,就很明显了。
const useDebouncedRef = (value, delay = 200) => {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
        track() // vue内部的跟踪函数
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          trigger() // vue内部的自动更新的函数。
        }, delay) // 延迟时间
      }
    }
  })
}
  setup () {
    const text = useDebouncedRef('hello', 1000) // 定义一个 v-model
    console.log('customRef', text)
    const update = () => {
      // 修改后延迟刷新
      text.value = '1111' + new Date().valueOf()
    }
    return {
      text,
      update
    }
  }
  customRef 对象:{{text}} <br><br>
  <input v-model="text" type="text">
- get 
 没有改变,直接用原方法。
- set 
 使用 setTimeout 实现延迟响应的功能,把 Vue 内部的 trigger() 放在 setTimeout 里面就好。
这样就可以了,延迟多长的时间可以自定义,这里是一秒。一秒内用户输入的内容,会一次性更新,而不是每输入一个字符就更新一次。
- v-model="text"
 可以作为 v-model 来使用。
customRef 的 源码
我们再来看看 customRef 内部源码的实现方式。
  function customRef(factory) {
      return new CustomRefImpl(factory);
  }
  class CustomRefImpl {
      constructor(factory) {
          this.__v_isRef = true;
          const { get, set } = factory(() => track(this, "get" /* GET */, 'value'), () => trigger(this, "set" /* SET */, 'value'));
          this._get = get;
          this._set = set;
      }
      get value() {
          return this._get();
      }
      set value(newVal) {
          this._set(newVal);
      }
  }
很简单的是不是,就是先这样,然后在那样,最后就搞定了。
好吧,就是把 factory参数解构出来,分成set和get,做成内部函数,然后在内部的set和get里面调用。
自定义 ref 的实例 —— 写一个自己的计算属性。
一提到计算属性,我们会想到 Vue 提供的 computed,那么如果让我们使用自定义ref 来实现计算属性的功能的话,要如何实现呢?(注意:只是练习用)
我们可以这样来实现:
const myComputed = (_get, _set) => {
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        if (typeof _get === 'function') {
          return _get()
        } else {
          console.warn(`没有设置 get 方法`)
        }
      },
      set(newValue) {
        if (typeof _set === 'function') {
          _set(newValue)
          trigger()
        } else {
          console.warn(`没有设置 set 方法`)
        }
      }
    }
  })
}
setup () {
    const refCount = ref(0)
    const myCom = myComputed(() => refCount.value + 1)
    // const myCom = myComputed(() => refCount.value, (val) => { refCount.value = val})
    const update = () => {
      // 修改原型
      refCount.value = 3
    }
    const setRef = () => {
      // 直接赋值
      refCount.value += 1
    }
    return {
      refCount, // 基础类型
      myCom, // 引用类型
      update, // 修改属性
      setRef // 直接设置
    }
  }
  <div>
      展示 自定义 的 customRef 实现计算属性 <br>
      ref 对象:{{refCount}} <br><br>
      自定义的计算属性 对象:{{myCom}} <br><br>
      <input v-model="myCom" type="text">
      <el-button @click="update" type="primary">修改属性</el-button><br><br>
    </div>
- myComputed 
 首先定义一个函数,接收两个参数,一个get,一个set。
- customRef 
 返回一个 customRef 的实例,内部设置get 和 set。
- 调用方法 
 调用的时候,可以只传入get函数,也可以传入get、set两个函数。
 修改 refCount.value 的时候,v-model 的 myCom 也会发生变化。
- 实用性 
 那么这种方式有啥使用效果呢?
 在做组件的时候,组件的属性props是不能直接用在内部组件的 v-model 里面的,因为props只读,那么怎么办呢?
可以在组件内部设置一个ref,然后对props做监听,或者用computed来做。
除了上面的几种方法外,也可以用这里的方法来实现,把 refCount 变成 props 的属性就可以了,然后set里面使用 smit 提交。
computed
写完了自己的计算属性后,我们还是来看看 Vue 提供的计算属性。
代码来自于 vue.global.js ,调整了一下先后顺序。
  function computed(getterOrOptions) {
      let getter;
      let setter;
      if (isFunction(getterOrOptions)) {
          getter = getterOrOptions;
          setter =  () => {
                  console.warn('Write operation failed: computed value is readonly');
              }
              ;
      }
      else {
          getter = getterOrOptions.get;
          setter = getterOrOptions.set;
      }
      return new ComputedRefImpl(getter, setter, isFunction(getterOrOptions) || !getterOrOptions.set);
  }
  class ComputedRefImpl {
      constructor(getter, _setter, isReadonly) {
          this._setter = _setter;
          this._dirty = true;
          this.__v_isRef = true;
          this.effect = effect(getter, {
              lazy: true,
              scheduler: () => {
                  if (!this._dirty) {
                      this._dirty = true;
                      trigger(toRaw(this), "set" /* SET */, 'value');
                  }
              }
          });
          this["__v_isReadonly" /* IS_READONLY */] = isReadonly;
      }
      get value() {
          if (this._dirty) {
              this._value = this.effect();
              this._dirty = false;
          }
          track(toRaw(this), "get" /* GET */, 'value');
          return this._value;
      }
      set value(newValue) {
          this._setter(newValue);
      }
  }
- computed 
 暴露给我们用的方法,来定义一个计算属性。只有一个参数,可以是一个函数(function),也可以是一个对象。内部会做一个判断,然后做拆分。
- ComputedRefImpl 
 是不是有点眼熟?这个是 ref 同款系列,都是 RefImpl 风格的,而且内部代码结构也很相似。
 这个是computed 的主体类,也是先定义内部属性,然后设置value的get和set。在get和set里面,调用外部设置的函数。
源码:
https://gitee.com/naturefw/nf-vue-cdn/tree/master/cdn/project-compositionapi
在线演示:
https://naturefw.gitee.io/nf-vue-cdn/cdn/project-compositionapi/
全面了解Vue3的 ref 和相关函数和计算属性的更多相关文章
- 认真总结Vue3中ref与reactive区别和isRef与isReactive 类型判断
		1.什么是ref? 1.ref和reactive-样 也是用来实现响应式数据的方法 由于reactive必须传递一个对象, 所以导致在企业开发中如果我们只想让某个变量实现响应式的时候会非常麻烦 所以V ... 
- VUE3 之 ref、provide、inject 的使用 - 这个系列的教程通俗易懂,适合新手
		1. 概述 首因效应告诉我们: 在日常交往中,第一印象是非常重要的,好的第一印象能让我们在与人相处时事半功倍. 但也从另一面告诉我们,不能仅凭第一印象去判断一个人,有时虚假的第一印象,也有可能蒙蔽我们 ... 
- Vue3源码解析(computed-计算属性)
		作者:秦志英 前言 上一篇文章中我们分析了Vue3响应式的整个流程,本篇文章我们将分析Vue3中的computed计算属性是如何实现的. 在Vue2中我们已经对计算属性了解的很清楚了,在Vue3中提供 ... 
- Vue中计算属性、侦听、过滤、自定义指令、ref的操作
		1.计算属性 <div id="app"> <input type="text" v-model="x"> < ... 
- 计算属性、侦听属性、局部与全局组件使用、组件通信(父子互传)、ref属性、动态组件和keep-alive、插槽
		今日内容概要 计算属性 侦听属性 局部组件和全局组件 组件通信之父传子 组件通信之子传父 ref属性(组件间通信) 动态组件和keep-alive 插槽 内容详细 1.计算属性 # 插值的普通函数,只 ... 
- 全面了解Vue3的 reactive 和相关函数
		Vue3的 reactive 怎么用,原理是什么,官网上和reactive相关的那些函数又都是做什么用处的?这里会一一解答. ES6的Proxy Proxy 是 ES6 提供的一个可以拦截对象基础操作 ... 
- 第三十九篇:Vue3 watch(ref和reactive的监视)
		好家伙, 1.vue2中的watch是调用配置项,(只能写一个) vue3中的watch是一个函数(可以写很多个) 2.watch一些用法: 这里是定义的数据 set up(){ let sum =r ... 
- vue3 的 ref、isRef、toRef、toRefs、toRaw 详细介绍
		ref.isRef.toRef.toRefs.toRaw 看着一堆类似的东西,一个头两个大,今天整理一篇文章详细介绍它们的功能及区别. 1.ref ref 属性除了能够获取元素外,也可以使用 ref ... 
- 《Vue3.x+TypeScript实践指南》已出版
		转眼回长沙快2年了,图书本在去年就已经完稿,因为疫情,一直耽搁了,直到这个月才出版!疫情之下,众生皆苦!感觉每天都是吃饭.睡觉.上班.做核酸! 图书介绍 为了紧跟技术潮流,该书聚焦于当下火的Vue3和 ... 
随机推荐
- linux bash which
			linux bash which https://linuxize.com/post/linux-which-command/ Linux which command is used to ident ... 
- CSS Dark Mode
			CSS Dark Mode https://kevq.uk/automatic-dark-mode/ https://kevq.uk/how-to-add-css-dark-mode-to-a-web ... 
- ForkJoin、并行流计算、串行流计算对比
			ForkJoin 什么是 ForkJoin ForkJoin 是一个把大任务拆分为多个小任务来分别计算的并行计算框架 ForkJoin 特点:工作窃取 这里面维护的都是双端队列,因此但其中一个线程完成 ... 
- 02_Mysql用户管理之Navicat下载及安装
			Navicat可以说是最好的Mysql客户端管理软件了,本博客将带你完成Navicat的下载与安装. 1.下载(https://www.navicat.com.cn/products) 
- 使用 Castle 实现 AOP,以及 Autofac 集成 Castle
			Castle 是 2003 年诞生于 Apache Avalon 项目,目的是为了创建一个IOC 框架.发展到现在已经有四个组件: ORM组件:ActiveRecord IOC组件:Windsor 动 ... 
- SpringBoot启动报错 Disconnected from the target VM, address: '127.0.0.1:2227', transport: 'socket'
			今天搭建了一个SpringBoot项目,刚启动就报错 Disconnected from the target VM, address: '127.0.0.1:2227', transport: 's ... 
- CentOS7系统重置root密码
			https://blog.csdn.net/qq_42969074/article/details/88080821 
- Snort + Barbyard2 + Snorby环境搭建
			1.环境 ubuntu-14.04.5 daq-2.0.7 Snort-2.9.15.1 Barbyard2 snorby Mysql Docker 2.架构 3.安装步骤 Ubuntu配置 如果是刚 ... 
- scrapy框架的介绍与安装
			scrapy框架的原理 使用pycharm安装scrapy库 1.打开新建file,然后有个扳手的setings点击进去,如图所示: 2.选择project 然后点击python interprete ... 
- 通过穷举法快速破解excel或word加密文档最高15位密码
			1.打开文件 2.工具 --- 宏 ---- 录制新宏 --- 输入名字如 :aa 3.停止录制 ( 这样得到一个空宏 ) 4.工具 --- 宏 ---- 宏 , 选 aa, 点编辑按钮 5.删除窗口 ... 
