Vue3 组合式API

1,640 阅读10分钟

微信截图_20210801003927.png

组合式API

01-setup

  1. 我们知道Vue3.0中新增 setup 代替了 beforeCreatecreated,而且 setupbeforeCreatecreated 执行更早
export default {
  beforeCreate () {
    console.log('beforeCreate')
  },

  created () {
    console.log('created')
  },
 
  setup() {
    console.log('setup')
  }
}

image.png

  1. setup函数是 Composition API(组合API)的入口,所有的组合API函数都在此使用, 只在初始化时执行一次

  2. setup函数中定义的属性和方法,最后都是需要 return 出去的 不然无法再模板中使用

  3. 官方给出的setup模板示例

<template>
  <div>{{ readersNumber }} {{ book.title }}</div>
</template>
<script>
  import { ref, reactive } from 'vue'

  export default {
    setup() {
      const readersNumber = ref(0)
      const book = reactive({ title: 'Vue 3 Guide' })

      // 暴露给模板
      return {
        readersNumber,
        book
      }
    }
  }
</script>

参数

setup 函数将接受两个参数 propscontext

props

setup 函数中的 props 是响应式的,当父组件传入新的 props 时,它将被更新

export default {
  props: {
    title: String
  },
  setup(props) {
    console.log(props.title)
  }
}

注意: 因为 props 是响应式的,所以不能使用ES6的解构,他会消除prop的响应性

Context

context 是一个普通的 JavaScript 对象,它暴露组件的三个 property

export default {
  setup(props, context) {
    // Attribute (非响应式对象)
    console.log(context.attrs)

    // 插槽 (非响应式对象)
    console.log(context.slots)

    // 触发事件 (方法)
    console.log(context.emit)
  }
}

或

export default {
  setup(props, { attrs, slots, emit }) {
    ...
  }
}

attrsslots 是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行解构,并始终以 attrs.xslots.x 的方式引用 property

访问组件的 property

执行 setup 时,组件实例尚未被创建。因此,你只能访问以下 property:

  1. props
  2. attrs
  3. slots
  4. emit

this

setup中访问this是undefined,因为 setup() 是在解析其它组件选项之前被调用的,所以 setup() 内部的 this 的行为与其它选项中的 this 完全不同。

02-生命周期

Vue3.0是在 setup () 内部调用生命周期钩子

Vue2.xVue3.x
beforeCreate不再需要
created不再需要
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeDestroyonBeforeUnmount
destroyedonUnmounted
errorCapturedonErrorCaptured

新增的钩子函数

  1. onRenderTracked
  2. onRenderTriggered

如继续使用Vue2.x语法

  1. beforeUnmount 替代了 beforeDestroy
  2. unmounted 替代了 destroyed

03-ref

  1. 作用: 定义一个数据的响应式

  2. 语法: const xxx = ref(initValue);

  3. 在setup内部想要访问和修改count变量,需要通过count.value实现

  4. 模板中操作数据: 不需要.value

  5. 示例

<template>
    {{count}}
</template>
setup {
    const count = ref(0)
    console.log(count.value) // 0
    
    count.value++
    console.log(count.value) // 1
    
    return {count}
}
  1. ref推荐定义基本数据类型,当然也可以使用复杂数据类型,因为ref中如果定义的是对象或数组时,Vue内部会帮我们转成reactive对象

image.png 7. 手写实现ref

// 定义一个ref
function ref (target) {
// 如定义对象类型,reactive函数在下面讲reactive时会实现,如测试基本数据类型可注掉这句代码
  target = reactive(target) 
  return {
    _is_ref: true, // 标识当前的对象是ref对象
    // 保存target数据保存起来
    _value: target,
    get value () {
      console.log('劫持到了读取数据')
      return this._value;
    },
    set value (val) {
      console.log('劫持到了修改数据,准备更新界面', val)
      this._value = val;
    }
  }
}
  1. ref 获取元素,应用场景: 如获取input焦点/调用子组件的函数或访问其属性
<template>
  <h2>App</h2>
  <input type="text">---
  <input type="text" ref="inputRef">
  
  <button @click="open">打开弹窗</button>
  <update ref="updateRef" />
</template>
<script lang="ts">
import { onMounted, ref } from 'vue'
import Update from "./components/update.vue"

export default {
  components: {
    Update
  },
  setup() {
    /**
    * 自动获取input焦点
    */
    const inputRef = ref<HTMLElement|null>(null)
    onMounted(() => {
      inputRef.value && inputRef.value.focus()
    })
    
    /**
    * 自动调用子组件的open函数
    */
    const updateRef = ref(null);
    const open = () => { updateRef.value.open(); };
    return {
      inputRef
    }
  },
}
</script>

04-unref

  1. 如果参数是一个 ref,则返回内部值,否则返回参数本身。
  2. 这是 val = isRef(val) ? val.value : val 的语法糖函数。
  3. 手写实现unref
// 定义一个函数unref,判断当前的对象是不是ref对象
function unref (target) {
  return target && target._is_ref? target.value : target;
}

05-toRef

  1. 可以用来为源响应式对象上的某个 property 新创建一个 ref。然后,ref 可以被传递,它会保持对其源 property 的响应式连接

  2. 同ref区别:

    • ref是 拷贝了一份新的数据值单独操作, 更新时相互不影响
    • toRef它会保持对其源 property 的响应式连接
  3. 示例一 响应式连接


const state = reactive({
  foo: 1,
  bar: 2
})

const fooRef = toRef(state, 'foo')

fooRef.value++
console.log(state.foo) // 2

state.foo++
console.log(fooRef.value) // 3

  1. 示例二 假如: 定义的hook函数需要一个ref对象
// 只接收一个ref对象,返回ref的value值 
function useData(foo: Ref) {
  return foo.value;
}

const component = defineComponent({
  props: {
    foo: {
      type: Number,
      require: true
    }
  },

  setup (props, context) {
    const data = useData(toRef(props, 'foo'))

    return {
      data
    }
  }
})
  1. 手写实现toRef
// 定义一个函数 toRef,可以用来为源响应式对象上的某个 property 新创建一个 ref。然后,ref 可以被传递,它会保持对其源 property 的响应式连接。
function toRef(target, prop) {
  let _target = target[prop];
  let state = target && target._is_reactive;
  return {
    _value: _target,
    get value() {
      this._value = state ? target[prop] : this._value;
      return this._value;
    },
    set value(val) {
      this._value = val;
      target[prop] = state ? val : target[prop];
    },
  };
}

06-toRefs

  1. 将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 propertyref
  2. 当从组合式函数返回响应式对象时,toRefs 非常有用,这样消费组件就可以在不丢失响应性的情况下对返回的对象进行分解/扩散:
  3. toRefs 只会为源对象中包含的 property 生成 ref。如果要为特定的 property 创建 ref,则应当使用 toRef
  4. 示例
setup {
    const data = reactive({
        tableData: [],
        loading: false,
    })
    // 在模板中我们想直接使用tableData和loading属性, 
    // 如通过普通解构 "..." 就会失去响应性, 
    // 在不失去响应性的情况下解构
    return {
        ...toRefs(data),
    }
}
  1. 手写实现toRefs
// 定义一个toRefsHandler处理对象
function toRefsHandler(prop, val) {
  return {
    _value: val,
    __v_isRef: true,
    _key: String(prop),
    get value() {
      return this._value;
    },
    set value(val) {
      this._value = val;
    },
  };
}

// 定义一个函数 toRefs,将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的 ref。
function toRefs(target) {
  let result = {};
  if (target && typeof target `= "object") {
    if (Array.isArray(target)) {
      // 数组的数据要进行遍历操作
      target.forEach((item, index) => {
        result[index] = toRefsHandler(index, item);
      });
    } else {
      // 再判断当前的数据是不是对象
      Object.keys(target).forEach((key) => {
        result[key] = toRefsHandler(key, target[key]);
      });
    }
    return result;
  }
}

07-isRef

  1. 检查值是否为一个 ref 对象
  2. 手写实现isRef
function isRef(obj) {
  return obj && obj._is_ref
}

08-shallowRef

  1. 创建一个跟踪自身 .value 变化的 ref,但不会使其值也变成响应式的
  2. 浅劫持(浅监视)----浅响应式
  3. 手写实现shallowRef
function shallowRef(target) {
  const result = {
    _value: target, // 用来保存数据的内部属性
    _is_ref: true, // 用来标识是ref对象
    get value () {
      return this._value
    },
    set value (val) {
      this._value = val
      console.log('set value 数据已更新, 去更新界面')
    }
  }

  return result
}

09-customRef

  1. 创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。
  2. 它需要一个工厂函数,该函数接收 tracktrigger 函数作为参数,并且应该返回一个带有 get 和 set 的对象
  3. 使用自定义 ref 通过 v-model 实现 debounce 的示例:
<input v-model="text" />
function useDebouncedRef(value, delay = 200) {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
        // 对其依赖项跟踪
        track()
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          // 更新触发
          trigger()
        }, delay)
      }
    }
  })
}

export default {
  setup() {
    return {
      text: useDebouncedRef('hello')
    }
  }
}

10-triggerRef

  1. 手动执行与 shallowRef 关联的任何副作用。
const shallow = shallowRef({
  greet: 'Hello, world'
})

// 第一次运行时记录一次 "Hello, world"
watchEffect(() => {
  console.log(shallow.value.greet)
})

// 这不会触发副作用,因为 ref 是浅层的
shallow.value.greet = 'Hello, universe'

// 记录 "Hello, universe"
triggerRef(shallow)

11-reactive

  1. 返回对象的响应式副本
  2. 深度劫持(深监视)----深度响应式
  3. 响应式转换是“深层”的——它影响所有嵌套 property
  4. 内部通过 ProxyReflect 实现
  5. 手写实现reactive
// 定义一个reactiveHandler处理对象
const reactiveHandler = {
  // 获取属性值
  get(target, prop) {
    if (prop = "_is_reactive") return true;
    const result = Reflect.get(target, prop);
    console.log("拦截了读取数据", prop, result);
    return result;
  },
  // 修改属性值或者是添加属性
  set(target, prop, value) {
    const result = Reflect.set(target, prop, value);
    console.log("拦截了修改数据或者是添加属性", prop, value);
    return result;
  },
  // 删除某个属性
  deleteProperty(target, prop) {
    const result = Reflect.deleteProperty(target, prop);
    console.log("拦截了删除数据", prop);
    return result;
  },
};

// 定义一个reactive函数,传入一个目标对象
function reactive(target) {
  // 判断当前的目标对象是不是object类型(对象/数组)
  if (target && typeof target = "object") {
    // 对数组或者是对象中所有的数据进行reactive的递归处理
    // 先判断当前的数据是不是数组
    if (Array.isArray(target)) {
      // 数组的数据要进行遍历操作
      target.forEach((item, index) => {
        target[index] = reactive(item);
      });
    } else {
      // 再判断当前的数据是不是对象
      // 对象的数据也要进行遍历的操作
      Object.keys(target).forEach((key) => {
        target[key] = reactive(target[key]);
      });
    }
    return new Proxy(target, reactiveHandler);
  }
  // 如果传入的数据是基本类型的数据,那么就直接返回
  return target;
}

12-readonly

  1. 接受一个对象 (响应式或纯对象) 或 ref 并返回原始对象的只读代理
  2. 只读代理是深层的:任何被访问的嵌套 property 也是只读的。
  3. reactive 一样,如果任何 property 使用了 ref,当它通过代理访问时,则被自动解包:
const raw = {
  count: ref(123)
}

const copy = readonly(raw)

console.log(raw.count.value) // 123
console.log(copy.count) // 123
  1. 手写实现readonly
// 定义了一个readonlyHandler处理器
const readonlyHandler = {
  get(target, prop) {
    if (prop = "_is_readonly") return true;
    const result = Reflect.get(target, prop);
    console.log("拦截到了读取数据了", prop, result);
    return result;
  },
  set(target, prop, value) {
    console.warn("只能读取数据,不能修改数据或者添加数据");
    return true;
  },
  deleteProperty(target, prop) {
    console.warn("只能读取数据,不能删除数据");
    return true;
  },
};

// 定义一个readonly函数
function readonly(target) {
  // 需要判断当前的数据是不是对象
  if (target && typeof target = "object") {
    // 判断target是不是数组
    if (Array.isArray(target)) {
      // 遍历数组
      target.forEach((item, index) => {
        target[index] = readonly(item);
      });
    } else {
      // 判断target是不是对象
      // 遍历对象
      Object.keys(target).forEach((key) => {
        target[key] = readonly(target[key]);
      });
    }
    return new Proxy(target, readonlyHandler);
  }
  // 如果不是对象或者数组,那么直接返回
  return target;
}

13-isProxy

  1. 检查对象是否是由 reactivereadonly 创建的 proxy
  2. 返回值为boolean
  3. 手写实现isProxy
// 定义一个函数isProxy,判断当前的对象是不是reactive对象或者readonly对象
function isProxy(obj) {
  return isReactive(obj) || isReadonly(obj);
}

14-isReactive

  1. 检查对象是否是由 reactive 创建的响应式代理。
  2. 返回值为boolean类型
  3. 手写实现isReactive
// 定义一个函数isReactive,判断当前的对象是不是reactive对象
function isReactive(obj) {
  return obj && obj._is_reactive;
}

15-isReadonly

  1. 检查对象是否是由 readonly 创建的只读代理
  2. 返回值为boolean类型
  3. 手写实现isReadonly
// 定义一个函数isReadonly,判断当前的对象是不是readonly对象
function isReadonly(obj) {
  return obj && obj._is_readonly;
}

15-toRaw

  1. 返回 reactivereadonly 代理的原始对象。
  2. 把代理对象变成了普通对象了,数据变化,界面不变化
  3. 官方不推荐使用。
const foo = {}
const reactiveFoo = reactive(foo)

console.log(toRaw(reactiveFoo) `= foo) // true

16-markRaw

  1. 标记一个对象,使其永远不会转换为 proxy。返回对象本身
const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false

// 嵌套在其他响应式对象中时也可以使用
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false

17-shallowReactive

  1. 创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (暴露原始值)。
  2. 浅劫持(浅监视)----浅响应式
  3. 官方示例
const state = shallowReactive({
  foo: 1,
  nested: {
    bar: 2
  }
})

// 改变 state 本身的性质是响应式的
state.foo++
// ...但是不转换嵌套对象
isReactive(state.nested) // false
state.nested.bar++ // 非响应式
  1. 手写实现shallowReactive
// 定义一个shallowReactive函数,传入一个目标对象
function shallowReactive(target) {
  // 判断当前的目标对象是不是object类型(对象/数组)
  if (target && typeof target `= "object") {
    return new Proxy(target, reactiveHandler);
  }
  // 如果传入的数据是基本类型的数据,那么就直接返回
  return target;
}

18-shallowReadonly

  1. 创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换 (暴露原始值)
  2. 浅劫持(浅监视)----浅只读
  3. 官方示例
onst state = shallowReadonly({
  foo: 1,
  nested: {
    bar: 2
  }
})

// 改变 state 本身的 property 将失败
state.foo++
// ...但适用于嵌套对象
isReadonly(state.nested) // false
state.nested.bar++ // 适用
  1. 手写实现shallowReadonly
// 定义一个shallowReadonly函数
function shallowReadonly(target) {
  // 需要判断当前的数据是不是对象
  if (target && typeof target `= "object") {
    return new Proxy(target, readonlyHandler);
  }

  return target;
}

19-provide 与 inject

  1. provide和inject提供依赖注入,功能类似 2.x 的provide/inject
  2. 实现跨层级组件(祖孙)间通信
<!--父组件-->
<template>
  <h2>父组件</h2>
  <Son />
</template>
<script lang="ts">
import { provide, ref } from 'vue'
import Son from './Son.vue'

export default {
  name: 'ProvideInject',
  components: {
    Son
  },
  setup() {
    
    const msg = ref('我是父组件的msg')

    provide('msg', msg)

    return {
      msg
    }
  }
}
</script>
<!--子组件Son.vue-->
<template>
  <div>
    <h2>子组件</h2>
    <hr>
    <GrandSon />
  </div>
</template>
<script lang="ts">
import GrandSon from './GrandSon.vue'
export default {
  components: {
    GrandSon
  },
}
</script>
<!--孙子组件-->
<template>
  <h3 :style="{msg}">孙子组件: {{msg}}</h3>
  
</template>

<script lang="ts">
import { inject } from 'vue'
export default {
  setup() {
    const msg = inject('msg')
    return {
      msg
    }
  }
}
</script>

结尾

  • 本篇文章主要为笔者师从杨洪波老师学习Vue3.0的随手笔记
  • 本篇文章结合了Vue3.0官方文档
  • 本篇文章手写部分仅为简单实现,里面复杂逻辑尚未考虑
  • 如有不当敬请斧正