12Vue高级语法补充

44 阅读13分钟

局部自定义指令

在optionsAPI中:

自定义一个v-focus的局部指令

  • 这个自定义指令实现很简单,只需要在组件选项中使用directives即可;
  • 他是一个对象,在对象中我们编写自定义指令的名称(这里不需要加v-)
  • 自定义指令有一个生命周期,是在组件挂载后调用的mounted,我们可以在其中完成操作;

自定义自动聚焦

<input type = "text" v-focus>

<script>
    export default{
        directives:{
            //和v-__后面的名字要求相同!
            focus:{
                //这个对象里面存放自定义指令的生命周期函数,当元素被挂载的时候,就会回调
                mounted(el){
                    console.log('v-focus的元素被挂载了')
                    el?.focus()
                }
            }
        }
    }
</script>

在setup中:

<input type = "text" v-focus>

//通过vXxx驼峰写法来连接自定义指令
const vFocus = {
    mounted(el){
        el?.focus()
    }
}

全局自定义指令

const app = createApp(App)

app.directive('focus',{
    mounted(el){
        el?.focus()
    }
})

app.mount("#app")

全局自定义指令的抽取

由于自定义指令可能会存在很多,全部放在main.js中不太合适,所以进行抽取;

//focus.js

export default function directiveFocus(app){
    app.directive({
    mounted(el){
            el?.focus()
        }
    })
}

------------------------------------------------------
//index.js
import directiveFocus from '...'
export default function useDirectives(app){
    directiveFocus(app)
}

---------------------------------------------------------------------------------
import useDirectives from index.js

const app = createApp(App)

useDirectives(app)

app.mount('#app')

指令的生命周期

一个指令定义的对象,Vue提供了如下的几个钩子函数:

  • created:在绑定元素的属性attribute或事件监听器被应用之前调用;
  • beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用;
  • mounted:在绑定元素的父组件被挂载后调用;
  • beforeUpdate:在更新包含组件的VNode之前被调用;
  • updated:在包含组件的VNode及其子组件的VNode更新后调用;
  • beforeUnmount:在卸载绑定元素的父组件之前被调用;
  • unmounted:当指令与元素接触绑定,且父组件已卸载时,只调用一次;

指令的高级用法:

指令参数-修饰符-值:

<h2 v-why:kobe.abc.cba="message">哈哈</h2>

const message = '你好啊,李银河'

//拿到自定义指令传入的数据
const vWhy = {
    //bindings中存储所有数据,是一个对象,arg存储所有数据
    mounted(el,bindings){
        //将哈哈替换为你好啊,李银河
        //el.textContent是哈哈,bindings.value拿到绑定指令的数据
        el.textContent = bindings.value
    }
}
  • bindings.value拿到绑定指令的数据
  • el.textContent拿到标签中间的数据
  • bindings中还可以拿到修饰符:abc、cba,当然也能拿到值:value

日期格式化指令:

import dayjs from 'dayjs'

export default function directiveUnit(app){
    app.directive('ftime',{
        mounted(el,bingdings){
            // 1.获取时间,并转换为毫秒
            let timestamp = el.textContent
            if(timestamp.length===10){
                timestamp = timestamp*1000
            }
            
            // 2.获取传入的参数,来指定格式化格式
            let value = bindings.value
            //如果是空的
            if(!value){
                value = 'YYYY-MM-DD HH:mm:ss'
            }
            
            // 3.对时间进行格式化
            const formatTime = dayjs(timestamp).format(value)
            el.textContent = formatTime
        }
    })
}

-----------------------------------
<h2 v-ftime='YYYY-MM-DD'>{{1649843135}}</h2>

内置组件

image.png

<template>
    <div class="my-app">
        //让teleport中的内容(h2)挂载到body上去
        <teleport to = "body">
            <h2></h2>
        </teleport>
    </div>
</template>

和组件结合使用:

<template>
    <div class="my-app">
        <teleport to='body'>
            <hello-world message="我是App中的message"/>
        </teleport>
    </div>
</template>

此时hello-world组件被挂载到body上

多个teleport

image.png

异步组件和Suspense

image.png

Suspense:一般用于异步组件

异步组件:会分包处理,该组件会放在单独的js文件中,需要在服务器中单独下载,有可能会存在渲染页面时,该文件还没有被下载出来;

//App.vue

<div class="app">
    <suspense>
    
        //suspense中存在2个插槽,default和fallback
        <template #default>
            <async-home/>
        </template>
        
        //如果async-home没被下载出来,先显示Loading应急组件
        <template #fallback>
            <async-home/>
        </template>
        
    </suspense>
    
</div>

const AsyncHome = defineAsyncComponent(()=>'./AsyncHome.vue')

认识h函数

Vue推荐我们:在绝大多数情况下,使用模板来创建HTML,然后再某些特殊的场景,当我们真的需要JS完全编程,这个时候template就不太灵活了,这个时候可以使用渲染函数,他比模板更接近编译器;

因为本质上,template会被渲染成render函数,生成VNode;如果我们通过渲染函数,直接就会生成render函数,更接近编译器;

  • Vue在生成真实DOM之前,会将我们的节点转换成VNode(虚拟节点),而VNode组合在一块,会形成【树结构】,这个就是虚拟DOM(VDom);
  • 事实上,我们编写的template的HTML最终也是使用【渲染函数】从而生成对应的VNode;
  • 如果我们想要充分的利用JS编程能力,我们可以自己来编写【createVNode】函数,生成对应的VNode;
    • 在template中有很多标签元素,每个元素会通过createVNode函数,创建一个VNode虚拟节点对象,根元素就是根VNode,子元素就是子VNode,形成一个树结构(VDom);最终生成真实DOM

我们现在自己编写createVNode,创建虚拟节点(VNode),那么我们可以通过h()函数,用于创建VNode的一个函数;

其实更准备的命名是createVNode()函数,只不过为了简便,Vue将其简化为h()函数

h()函数的使用

h()函数可以接收3个参数:

  • HTML标签名(String)、一个组件(Object)、一个异步组件或者一个函数式组件(必须的)
  • 和attribute、prop或者事件相对应的对象,我们会在模板中使用(可选的)
  • 子VNodes,使用h()构建,或使用字符串获取文本Vnode或有插槽的对象(可选的)

render()函数h()函数render()函数是大头,是写在组件里面的,只不过其中最核心的功能部分(创建VNode)是由h()函数来完成实现的;

当我们去渲染一个组件的时候,就会调用render()函数,然后他又会通过调用h()函数,返回VNode对象,最终渲染成真实DOM生成在页面上:

<script>
import { h } from 'vue';

// 在OptionAPI中使用render()函数
export default {
    render() {
        // 创建一个div,类名为app,内容是一个数组,数组中又有很多子元素
        return h('div', { class: "app" }, [
            h('h2', { class: 'title' }, '我是标题'),
            h('p', { class: 'content' }, '我是内容哈哈')
        ])
    }
}
</script>

其中h()函数中也是可以来动态书写数据的:暂时先通过OptionApi来实现:计数器案例

<script>
import { h } from 'vue';

// 在OptionAPI中使用render()函数
export default {
    data() {
        return {
            counter: 0
        }
    },
    render() {
        // 计数器案例
        return h('div', { class: "app" }, [
            h('h2', null, `当前计数:${this.counter}`),

            // 这里不再是template模板,不能使用模板指令/语法糖
            h('button', { onClick: this.increment }, '+1'),
            h('button', { onClick: this.decrement }, '-1'),
        ])
    },
    methods: {
        increment() {
            this.counter++
        },
        decrement() {
            this.counter--
        }
    }
}

</script>

渲染组件:

import Home from './Home.vue'

export default {
render() {
        // 渲染home组件
        h(Home)
    },
}

在CompoisitionAPI中使用:

<script>
import { h, ref } from 'vue'
import Home from './Home.vue'

export default {
    setup() {
        const counter = ref(0)

        const increment = () => {
            counter.value++
        }
        const decrement = () => {
            counter.value--
        }

        // 此时return返回的不再是一个对象,而是一个函数:并且不是在模板中了,需要自己手动解包.value
        return () => h('div', { class: "app" }, [
            h('h2', null, `当前计数:${counter.value}`),

            h('button', { onClick: increment }, '+1'),
            h('button', { onClick: decrement }, '-1'),

            // 渲染home组件
            h(Home)
        ])
    },
}
</script>

<style lang="less" scoped>
</style>

setup语法糖的写法:还是需要用到template

<template>
    <render />
</template>

<script setup>
import { ref, h } from 'vue'
import home from './Home.vue'

const counter = ref(0)

const increment = () => {
    counter.value++
}
const decrement = () => {
    counter.value--
}

// 此时return返回的不再是一个对象,而是一个函数:
const render = () => h('div', { class: "app" }, [
    h('h2', null, `当前计数:${counter.value}`),

    h('button', { onClick: increment }, '+1'),
    h('button', { onClick: decrement }, '-1'),

    // 渲染home组件
    h(home)
])

</script>

jsx语法

上方的编写还是非常麻烦,通过jsx可以简化书写,jsx在react中发扬光大,我们在这里只稍微提一下:

image.png

<script lang="jsx">
export default {
    data(){
        return{
            counter:1
        }
    },
    methods:{
        increment(){
            this.counter++
        }
    }
    render() {
        // 不再使用h()函数,使用jsx
        return (
            <div class="app">
                <h2>我是标题,哈哈</h2>
                <p>我是内容,嘿嘿</p>
                <span>当前计数{this.counter}</span>
                <button onClick={this.increment}>+1</button>
            </div>
        )
    }
}
</script>

<style lang="less" scoped>
</style>

只要在jsx中绑定数据,就一定会用{}来绑定

安装插件:npm install @vitejs/plugin-vue-jsx -D

image.png

CompositionAPI的使用:

<template>
    <jsx></jsx>
</template>
<script lang="jsx" setup>
import { ref } from 'vue'

const counter = ref(0)

const increment = () => {
    counter.value++
}

const decrement = () => {
    counter.value--
}

const jsx = () => {
    <div class="app">
        <h2>当前计数:{counter.value}</h2>
        <button onClick={increment}>+1</button>
        <button onClick={decrement}>-1</button>
    </div>
}

</script>

<style lang="less" scoped>
</style>

动画

image.png

通过<transition>标签包裹,被包裹的内容就会执行Vue提供的动画效果:v-enter-from、v-enter-to等等;

<template>
    <div class="home">
        <button @click="isShow = !isShow">切换</button>
        <transition>
            <!-- 这个message会自动执行动画 -->
            <h2 v-if="isShow">哈哈哈哈</h2>
        </transition>
    </div>
</template>

<script setup>
import { ref } from 'vue'

const isShow = ref(false)

</script>

<style lang="less" scoped>
// 从哪里离开,一般情况下离开和显示是对应的,因为进入后的地方和准备离开的地方是同一个地方
.v-leave-from,
.v-enter-to {
    // 变成什么状态
    opacity: 1;
    transform: scale(1);
}

// 离开到哪,和从哪里进入的是同一个地方
.v-leave-to,
.v-enter-from {
    opacity: 0;
    transform: scale(0.6);
}

// 进入/离开的过程是什么
.v-leave-active,
.v-enter-active {
    // all就是让所有的动画都生效,如果指定opacity,那么只有opacity动画生效
    transition: all 2s ease;
}
</style>

image.png

image.png

image.png

image.png

如果我们使用的是一个没有name的transition,那么所有的class是以v-作为默认前缀

如果我们添加了一个name属性,比如<transition name = "why">,那么所有的class会以why-开头;

animation动画

image.png

我们在transition动画中,to定义结束动画状态,from定义初始动画状态;而animation是通过帧动画来更好的书写动画效果;

编写好animation(也就是帧动画)时,会要求编写好名字,然后通过<transition>标签,包裹要执行动画的部分,再通过transition中的.v-enter-leave等方法来控制搭配【帧动画】;如果给transition标签定义了name,则可以替换名字:.名字-enter-leave

<template>
    <div class="home">
        <button @click="isShow = !isShow">切换</button>
        <transition name="xh">
            <!-- 这个message会自动执行动画 -->
            <h2 v-if="isShow">哈哈哈哈</h2>
        </transition>
    </div>
</template>

<script setup>
import { ref } from 'vue'

const isShow = ref(false)

</script>

<style lang="less" scoped>
.xh-leave-active{
    animation: xhAnim 2s ease reverse;
}
.xh-enter-active {
    // 指定对应动画的名字、时间、曲线、延迟
    animation: xhAnim 2s ease;
}

@keyframes xhAnim {
    0% {
        transform: scale(0);
        opacity: 0;
    }

    50% {
        transform: scale(1.2);
        opacity: 0.5;
    }

    100% {
        transform: scale(1);
        opacity: 1;
    }
}
</style>

同时设置过渡动画和帧动画(一般不设置)

image.png

其他补充

image.png

mode属性

image.png

通过设置out-in执行完一个后,就离开,然后再执行另一个开始动画;

<template>
    <div class="home">
        <button @click="isShow = !isShow">切换</button>
        <br>
        <transition name="xh" mode="out-in">
            <!-- 这个message会自动执行动画 -->
            <h2 v-if="isShow">哈哈哈哈</h2>
            <h2 v-else>呵呵呵</h2>
        </transition>
    </div>
</template>

动态组件的切换:

image.png

image.png

列表过渡

image.png

image.png

  • 默认情况下,他不会渲染一个元素的包裹器,但是可以指定元素,并且以tag attribute进行渲染;
  • 并且不能使用mode模式
<template>
  <div class="app">
    <button @click="addNum">添加数字</button>
    <button @click="removeNum">删除数字</button>
    <button @click="shuffleNum">打乱数字</button>
    <transition-group tag="div" name="why">
      <template v-for="item in nums" :key="item">
        <li>{{ item }}</li>
      </template>
    </transition-group>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { shuffle } from 'underscore'

const nums = ref([1, 2, 3, 4, 5, 6, 7, 8, 9])

const addNum = () => {
  nums.value.splice(randomIndex(), 0, nums.value.length)
}

const removeNum = () => {
  nums.value.splice(randomIndex(), 1)
}

const shuffleNum = () => {
  nums.value = shuffle(nums.value)
}

const randomIndex = () => {
  console.log(Math.floor(Math.random() * nums.value.length))
  return Math.floor(Math.random() * nums.value.length)
}

const afterEnter = () => {
  console.log("after-enter")
}

</script>

<style scoped>

  span {
    margin-right: 10px;
    display: inline-block;
  }

  .why-enter-from,
  .why-leave-to {
    opacity: 0;
    transform: translateX(30px);
  }

  .why-move {
    transition: all 2s ease;
  }

  .why-leave-active {
    position: absolute;
  }

  .why-enter-active,
  .why-leave-active {
    transition: all 2s ease;
  }

</style>

响应式原理

当数据发生变化的时候,其他地方用到该数据的时候,也会做出改变;

也就是将template中的数据重新执行一遍,本质就是通过重新执行render()函数,生成新的VNode

image.png

在开发中常见的是对象的响应式:

当第二个文件修改了name值,第一个文件中只要依赖name的,也会跟着重新执行一遍,如果没有依赖name则不需要重新执行,这就是响应式的基本原理;

const obj = {
    name:"why",
    age:18
}

第一个文件用到的代码
console.log(obj.name)
console.log(obj.age)
console.log(obj.age+100)

--------------------------------

第二个文件用到的代码
obj.name = "kobe"

所以到底哪些代码需要重新执行,是需要进行收集起来的,那么这就是响应式的其中一个功能;所以我们将需要重新执行的代码包裹在一个函数中,当我修改了值,就重新执行这个函数;

image.png

响应式依赖收集:

const obj = {
    name: 'why',
    age: 18
}

// 监听当依赖数据发生变化时,自身也要随之变化的函数的一个方法
const reactiveFns = [] //将所有的函数保存在数组中
function watchFn(fn) {
    reactiveFns.push(fn)
    
    // 然后传入进来的函数会先执行一次,和watchEffect类似
    fn()
}

watchFn(function foo() {
    console.log('foo', obj.name);
    console.log('foo', obj.age);
    console.log('foo function');
})

watchFn(function bar() {
    console.log('bar', obj.name + 'hello');
    console.log('bar', obj.age + '10');
    console.log('bar function');
})

// 修改obj的属性
obj.name = 'kobe'

// 此时我们先假设我们知道依赖obj.name的函数有哪些,遍历出来后并再执行一次
reactiveFns.forEach(fn => {
    fn()
})

这里我们先假设已经知道哪些需要变化的函数了,因此我们只将这些保存进reactiveFns中,然后再逐个遍历,并调用执行;

目前我们收集的依赖是放到一个数组中来保存的,但是这里会存在数据管理的问题:

  • 我们在实际开发中会需要监听很多对象的响应式;
  • 这些对象需要监听的不只是一个属性,他们很多属性的变化都会有对应的响应式函数;
  • 我们不可能在全局维护一大堆的数组来保存这些响应函数;

所以我们要设计一个类,这个类用于管理某一个对象的某一个属性的所有响应式函数,相当于替代了原来的简单reactiveFns的数组

class Depend {
    constructor() {
        this.reactiveFns = []
    }

    addDepend(fn) {
        if (fn) {
            this.reactiveFns.push(fn)
        }
    }

    // 数据一旦发生变化,notify就把所有的函数收集起来并执行
    notify() {
        this.reactiveFns.forEach(fn => {
            fn()
        })
    }
}


const obj = {
    name: 'why',
    age: 18
}

const dep = new Depend()

function watchFn(fn) {
    dep.addDepend(fn)
    fn()
}

watchFn(function foo() {
    console.log('foo', obj.name);
    console.log('foo', obj.age);
    console.log('foo function');
})

watchFn(function bar() {
    console.log('bar', obj.name + 'hello');
    console.log('bar', obj.age + '10');
    console.log('bar function');
})

console.log('name发生变化-----------');
obj.name = 'kobe'
dep.notify()

但是我们仍然还是手动通知notify(),所以我们希望做出一个可以自动监听对象的方法:

我们可以采用2种方式:

  • 通过Object.defineProperty的方式(vue2采用的方式)
  • 通过new Proxy的方式(vue3采用的方式)

第一种方案

一旦依赖发生变化,自动通知依赖发生变化:

class Depend {
    constructor() {
        this.reactiveFns = []
    }

    addDepend(fn) {
        if (fn) {
            this.reactiveFns.push(fn)
        }
    }

    // 数据一旦发生变化,notify就把所有的函数收集起来并执行
    notify() {
        this.reactiveFns.forEach(fn => {
            fn()
        })
    }
}

const obj = {
    name: 'why',
    age: 18
}

Object.keys(obj).forEach(key=>{
    let value = obj[key]
    
    Object.defineProperty(obj,key,{
        set:function(newValue){
            // 这里不能写obj[key]=newValue,一旦这样写,修改值后又会来调用set,会递归
            value = newValue
            dep.notify()
        }
        get:function(){
            return value
        }
    })
})

const dep = new Depend()

function watchFn(fn) {
    dep.addDepend(fn)
    fn()
}

// 这个函数内部都是依赖obj的,只要有变化就要重新执行
watchFn(function foo() {
    console.log('foo', obj.name);
    console.log('foo', obj.age);
    console.log('foo function');
})
watchFn(function bar(){
    console.log("bar",obj.age+10)
    console.log("bar function")
})

obj.name="kobe"

但是目前我们还存在一个问题,如果只改变obj中的其中一个内容,只让其中一个重新执行即可,而不是全部都执行一次;也就是说,不应该把所有依赖到数据的函数放在reactiveFns数组中;而是要动态决定变化的才放进去;

dep对象存放依赖数据的函数,以及通知操作

因此我们为对象中的每个属性,都创建一个dep对象,如果属性发生改变,那么就找对应属性的dep对象,就可以实现对改变的属性进行重新执行,但是由于存在过多的dep对象,因此不好管理;因此我们将一个obj对象对应一个map对象1,而map对象1中存放是映射关系(对象:key-value),比如name-->dep对象age-->dep对象,除了obj对象还会存在很多的对象关系;

obj对象也不可能存在一个,会有多个这样关系的对象,因此我们将最外层的对象也要设置成Map管理;

Map用来将对象作为key存放在对象中,这里我们不需要强引用,因为存在内存泄漏,所以这里只需要管理即可,使用WeakMap即可;

image.png

如果想要拿到obj对象中的name属性依赖,只需要:objMap.get(obj).get(name),一层一层通过key拿到对应的dep;因为最小的map中存放的是name:dep对象的关系,然后通过dep.notify()通知具体的依赖属性即可;

自动收集:只要用到依赖数据的属性,就应该自动存放在dep中,那么就可以在defineProperty中的get函数中拿到,因为只要依赖数据的地方,就会调用get函数,然后再get函数中拿到具体对象的key,然后通过key拿到dep对象;

class Depend {
    constructor() {
        this.reactiveFns = []
    }

    addDepend(fn) {
        if (fn) {
            this.reactiveFns.push(fn)
        }
    }

    // 数据一旦发生变化,notify就把所有的函数收集起来并执行
    notify() {
        this.reactiveFns.forEach(fn => {
            fn()
        })
    }
}

const obj = {
    name: 'why',
    age: 18
}

// 专门执行响应式函数的一个函数
let reactiveFn = null
function watchFn(fn){
    reactiveFn = fn
    fn()
    reactiveFn = null
}

// 封装一个函数,通过obj的key,获取对应的Depend对象
const objMap = new WeakMap()
function getDepend(obj,key){

    // 1.根据obj对象,找到对应的map对象
    let map = objMap.get(obj)
    
    // 第一个可能拿不到:
    if(!map){
        // 这里key是字符串,不能用weakMap
        map = new Map()
        objMap.set(obj,map)
    }
    
    // 2.根据key,找到对应的dep对象
    let dep = map.get(key)
    
    if(!dep){
        dep = new Depend()
        map.set(key,dep)
    }
    
    return dep
}


Object.keys(obj).forEach(key=>{
    let value = obj[key]
    
    Object.defineProperty(obj,key,{
        set:function(newValue){
            value = newValue
            // 拿到属于自己的dep对象
            const dep = getDepend(obj,key)
            dep.notify()
        },
        get:function(){
            // 将依赖传递过去
            const dep = getDepend(obj,key)
            
            // 把函数加进去
            dep.addDepend(reactiveFn)
            
            return value
        }
    })
})

// 当我用到依赖的数据,就会调用get方法,然后生成对应的dep对象
// 当值修改后,就会执行对应的set方法,然后调用dep对象,并通知重新执行
watchFn(function foo(){
    console.log("foo:",obj.name)
    console.log("foo:",obj.age)
})

obj.name = "kobe"

image.png

业务代码:

// 如果对应的值发生改变,那么就执行对应的函数
watchFn(function(){
    console.log(obj.name)
    console.log(obj.age)
    console.log(obj.age)
})

watchFn(function(){
    console.log(obj.age)
})

watchFn(function(){
    console.log(obj.name)
})

obj.name = "kobe"

但是如果在函数中使用了多次同一个属性,那么整个函数则会被重复执行;因为每多一次就会执行一次get函数;然后就会被添加进getDepend中;

class Depend {
    constructor() {
        // 写个set即可
        this.reactiveFns = new Set()
    }

    addDepend(fn) {
        if (fn) {
            // 把push修改成add
            this.reactiveFns.add(fn)
        }
    }
    
    notify() {
        this.reactiveFns.forEach(fn => {
            fn()
        })
    }
}

完善:对任意对象都可以进行依赖收集并响应式,将Object.defineProperty封装成一个函数

function reactive(obj){
  Object.keys(obj).forEach(key=>{
    let value = obj[key]
    
    Object.defineProperty(obj,key,{
        set:function(newValue){
            value = newValue
            // 拿到属于自己的dep对象
            const dep = getDepend(obj,key)
            dep.notify()
        },
        get:function(){
            // 将依赖传递过去
            const dep = getDepend(obj,key)
            
            // 把函数加进去
            dep.addDepend(reactiveFn)
            
            return obj
        }
    })
  })  
}

const obj = reactive({
    name:"why",
    age:18
})

第二种方案

getDepend方法比较关键,一定要加以理解

class Depend {
  constructor() {
    this.reactiveFns = new Set()
  }

  addDepend(fn) {
    if (fn) {
      this.reactiveFns.add(fn)
    }
  }

  depend() {
    if (reactiveFn) {
      this.reactiveFns.add(reactiveFn)
    }
  }

  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}


// 设置一个专门执行响应式函数的一个函数
let reactiveFn = null
function watchFn(fn) {
  reactiveFn = fn
  fn()
  reactiveFn = null
}


// 封装一个函数: 负责通过obj的key获取对应的Depend对象
const objMap = new WeakMap()
function getDepend(obj, key) {
  // 1.根据对象obj, 找到对应的map对象
  let map = objMap.get(obj)
  if (!map) {
    map = new Map()
    objMap.set(obj, map)
  }

  // 2.根据key, 找到对应的depend对象
  let dep = map.get(key)
  if (!dep) {
    dep = new Depend()
    map.set(key, dep)
  }

  return dep
}


// 方式二: new Proxy() -> Vue3
function reactive(obj) {
  const objProxy = new Proxy(obj, {
    set: function(target, key, newValue, receiver) {
      // target[key] = newValue
      Reflect.set(target, key, newValue, receiver)
      const dep = getDepend(target, key)
      dep.notify()
    },
    get: function(target, key, receiver) {
      const dep = getDepend(target, key)
      dep.depend()
      return Reflect.get(target, key, receiver)
    }
  })
  return objProxy
}


// ========================= 业务代码 ========================
const obj = reactive({
  name: "why",
  age: 18,
  address: "广州市"
})

watchFn(function() {
  console.log(obj.name)
  console.log(obj.age)
  console.log(obj.age)
})

// 修改name
console.log("--------------")
// obj.name = "kobe"
obj.age = 20
// obj.address = "上海市"


console.log("=============== user =================")
const user = reactive({
  nickname: "abc",
  level: 100
})

watchFn(function() {
  console.log("nickname:", user.nickname)
  console.log("level:", user.level)
})

user.nickname = "cba"