Vue 的主要内容

1,046 阅读16分钟

一、vue2.0部分

1、v-show和v-if的区别

  • v-show 通过 css display 控制显示和隐藏的
  • v-if 是组件真正的渲染和销毁,不是显示和隐藏
  • 频换切换的时候使用 v-show , 否则使用 v-if

2、v-for中key的作用

diff 算法中通过 tag 和 key 来判断是否是 sameNode ,减少渲染次数,提升渲染性能。key 必须使用的 不能是 index 和 random 。

3、v-for 和 v-if不能同时使用

v-for 比v-if计算优先级高 而且如果每一次都需要遍历整个数组,将会影响速度, 解决方案 使用计算属性 computed 或者在父级标签判断

4、vue的生命周期(有父子组件的情况)

主要有三个阶段:

  1. 创建阶段(注册实例与挂载): beforeCreate、created、beforeMount、mounted
  2. 更新阶段:beforeUpdate、updated
  3. 注销阶段:beforeDestroy、destroyed

单个组件的生命按正常的顺序执行。这里主要说一下父子组件的情况

  1. 创建阶段: 父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
  2. 子组件更新过程: 父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
  3. 父组件更新过程:父 beforeUpdate -> 父 updated
  4. 销毁过程: 父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

5、vue组件如何通讯

  • 父子组件:父子沟通用的是props,子父沟通用的是 this.$emit
  • 自定义事件: event.no event.off event.$emit
  • vuex

6、对 mvvm 的理解

view -> viewModel -> model view <- viewModel <- model

view: DOM model: javascript Objects viewModel: vue (DOM listeners 和 directives )

7、双向数据绑定v-model 的实现原理

v-model 是一个语法糖真正实现是靠 v-bind绑定响应式数据和触发oninput 事件并传递数据实现,

  • input 元素的 value = this.name
  • 绑定 input 事件 this.name = $event.target.value
  • data 更新出发 re-render

自定义 v-model

  • 定一个 props ,在 model 中 关联 prop:"自己定义的属性默认为 value ", enent:"事件名称默认为 input "

8、$nextTick

异步渲染,将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。

9、slot

1)、什么是插槽?

  • 插槽(Slot)是Vue提出来的一个概念,正如名字一样,插槽用于决定将所携带的内容,插入到指定的某个位置,从而使模板分块,具有模块化的特质和更大的重用性。
  • 插槽显不显示、怎样显示是由父组件来控制的,而插槽在哪里显示就由子组件来进行控制

2)、默认插槽

父组件

<template>
  <div>
    我是父组件
    <slotOne1>
      <p style="color:red">我是父组件插槽内容</p>
    </slotOne1>
  </div>
</template>

在父组件引用的子组件中写入想要显示的内容(可以使用标签,也可以不用)

子组件(slotOne1)

<template>
  <div class="slotOne1">
    <div>我是slotOne1组件</div>
    <slot></slot>
  </div>
</template>

在子组件中写入slot,slot所在的位置就是父组件要显示的内容

3)、具名插槽

子组件

<template>
  <div class="slottwo">
    <div>slottwo</div>
    <slot name="header"></slot>
    <slot></slot>
    <slot name="footer"></slot>
  </div>
</template>

在子组件中定义了三个slot标签,其中有两个分别添加了name属性header和footer

父组件

<template>
  <div>
    我是父组件
    <slot-two>
      <p>啦啦啦,啦啦啦,我是卖报的小行家</p>
      <template slot="header">
          <p>我是name为header的slot</p>
      </template>
      <p slot="footer">我是name为footer的slot</p>
    </slot-two>
  </div>
</template>

在父组件中使用template并写入对应的slot值来指定该内容在子组件中现实的位置(当然也不用必须写到template),没有对应值的其他内容会被放到子组件中没有添加name属性的slot中

4)、作用域插槽

子组件

<template>
  <div>
    我是作用域插槽的子组件
    <slot :data="user"></slot>
  </div>
</template>

<script>
export default {
  name: 'slotthree',
  data () {
    return {
      user: [
        {name: 'Jack', sex: 'boy'},
        {name: 'Jone', sex: 'girl'},
        {name: 'Tom', sex: 'boy'}
      ]
    }
  }
}
</script>

在子组件的slot标签上绑定需要的值

父组件

<template>
  <div>
    我是作用域插槽
    <slot-three>
      <template slot-scope="user">
        <div v-for="(item, index) in user.data" :key="index">
        {{item}}
        </div>
      </template>
    </slot-three>
  </div>
</template>

在父组件上使用slot-scope属性,user.data就是子组件传过来的值

10、keep-alive

  • 缓存组件
  • 频繁切换,不需要重新渲染
  • Vue的性能优化

Props:

  • include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
  • exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
  • max - 数字。最多可以缓存多少组件实例。

使用:

<!-- 基本 -->
<keep-alive>
  <component :is="view"></component>
</keep-alive>

<!-- 多个条件判断的子组件 -->
<keep-alive>
  <comp-a v-if="a > 1"></comp-a>
  <comp-b v-else></comp-b>
</keep-alive>

<!-- 和 `<transition>` 一起使用 -->
<transition>
  <keep-alive>
    <component :is="view"></component>
  </keep-alive>
</transition>

include 和 exclude prop 允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示:

<!-- 逗号分隔字符串 -->
<keep-alive include="a,b">
  <component :is="view"></component>
</keep-alive>

<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
  <component :is="view"></component>
</keep-alive>

<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
  <component :is="view"></component>
</keep-alive>

max:最多可以缓存多少组件实例。一旦这个数字达到了,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉。

<keep-alive :max="10">
  <component :is="view"></component>
</keep-alive>

11、vuex

  • state: 数据仓库,唯一数据源
  • getters: store 的计算属性。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
  • action: Action 提交的是 mutation,而不是直接变更状态。Action 可以包含任意异步操作。
  • mutation: 更改 Vuex 的 store 中的状态的唯一方法

用于组件中的

  • dispatch:
  • commit:
  • mapState:
  • mapGetters:
  • mapActions:
  • mapMutations:

12、vue 响应式原理

核心API: Object.defineProperty

Object.defineProperty 的简单使用:

Object.defineProperty(obj, prop, desc)
  1. obj 需要定义属性的当前对象

  2. prop 当前需要定义的属性名

  3. desc 属性描述符()

     let Person = {}
     let temp = null
     Object.defineProperty(Person, 'name', {
     	get: function () {
     		console.log("get");
     		return temp
     	},
     	set: function (val) {
     		console.log("set",val);
     		temp = val
     	}
     })
    
     Person.name = "风信子"
     console.log(Person.name)
    

Object.defineProperty如何监听数组和对象

// 触发更新视图
function updateView() {
    console.log('视图更新')
}

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
    arrProto[methodName] = function () {
        updateView() // 触发视图更新
        oldArrayProperty[methodName].call(this, ...arguments)
        // Array.prototype.push.call(this, ...arguments)
    }
})

// 重新定义属性,监听起来
function defineReactive(target, key, value) {
    // 深度监听
    observer(value)

    // 核心 API
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (newValue !== value) {
                // 深度监听 (设置新值的时候)
                observer(newValue)

                // 设置新值
                // 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
                value = newValue

                // 触发更新视图
                updateView()
            }
        }
    })
}

// 监听对象属性
function observer(target) {
    if (typeof target !== 'object' || target === null) {
        // 不是对象或数组
        return target
    }

    // 污染全局的 Array 原型
    // Array.prototype.push = function () {
    //     updateView()
    //     ...
    // }

    if (Array.isArray(target)) {
        target.__proto__ = arrProto
    }

    // 重新定义各个属性(for in 也可以遍历数组)
    for (let key in target) {
        defineReactive(target, key, target[key])
    }
}

// 准备数据
const data = {
    name: 'zhangsan',
    age: 20,
    info: {
        address: '北京' // 需要深度监听
    },
    nums: [10, 20, 30]
}

// 监听数据
observer(data)

// 测试
// data.name = 'lisi'
// data.age = 21
// // console.log('age', data.age)
// data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
// delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
// data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组

Object.defineProperty 的缺点(Vue3.0启用Proxy)

  • 深度监听,需要递归到底,一次性计算量大
  • 无法监听新增属性/删除属性 (没有在data中初始化的属性,需要vue.set(),vue.delete())
  • 无法原生监听数组,需要特殊处理

Proxy 有兼容性问题 无法兼容polyfill

13、虚拟DOM (virtual DOM)

DOM 操作非常耗费性能,之前需手动操作DOM。Vue和React是数据驱动视图,如何操作DOM.

解决方案-VDOM: 项目有了一定复杂度,想减少计算次数比较难。能不能把计算,更多的转移为js计算,因为js执行速度相对很快的。

vdom: 用js模拟DOM结构,计算出最小的变更,操作DOM

1)用js模拟DOM结构原理

// 原html结构
	<div class="content">
			<ul style="font-size:20px;" >
				<li>a</li>
			</ul>
		</div>
//js模拟DOM结构
{
	tag:"div",
	props:{
		className:"content",
		id:"div1"
	},
	childrent:{
		tag:"ul",
		props:{
			style:"font-size:20px;",
		},
		childrent:{
			tag:"li",
			childrent:"a"
		}
	}
}

2)通过 snabbdom 学习 vdom

(1) diff算法概述
  • diff即对比,是一个广泛的概念,如linux diff命令、git diff等
  • 两个js 也可以 diff 对比。如:github.com/cujojs/jiff
  • 两棵树做 diff , 如 vdom diff。

diff算法时间复杂度O(n) o(n): 1千个节点 时间复杂度就是1千。

时间复杂度如何优化到O(n)

  1. 只比较同级,不跨级比较,减少算法复杂度。
  2. tag 不相同,则直接删除重建,不在深度比较。
  3. tag 和 key ,两个相同,则认为是相同节点,不在深度比较。
(2) diff算法总结

钩子方法 :

// 钩子
export interface Hooks {
    // 在 `patch` 开始执行的时候调用
    pre?: PreHook;

    // 在 `createElm`,进入的时候调用init
    // vnode转换为真实DOM节点时触发
    init?: InitHook;

    // 创建真实DOM的时候,调用 create
    create?: CreateHook;

    // 在`patch`方法接近完成的时候,才收集所有的插入节点,遍历调用响应的钩子
    // 可以认为插入到DOM树时触发
    insert?: InsertHook;

    // 在两个节点开始对比前调用
    prepatch?: PrePatchHook;

    // 更新过程中,调用update
    update?: UpdateHook;

    // 两个节点对比完成时候调用
    postpatch?: PostPatchHook;

    // 删除节点的时候调用,包括子节点的destroy也会被触发
    destroy?: DestroyHook;

    // 删除当前节点的时候调用。元素从父节点删除时触发,和destory略有不同,remove只影响到被移除节点中最顶层的节点
    remove?: RemoveHook;

    // 在`patch`方法的最后调用,也就是patch完成后触发
    post?: PostHook;
}

每个 modules 下的 hook 方法提取出来存到 cbs 里面

sameVnode:判断是否是相同的虚拟节点

patch:init 方法最后返回一个 patch 方法 。

patch 方法主要的逻辑如下 :

  • 触发 pre 钩子
  • 如果老节点非 vnode, 则新创建空的 vnode
  • 新旧节点为 sameVnode 的话,则调用 patchVnode 更新 vnode , 否则创建新节点
  • 触发收集到的新元素 insert 钩子
  • 触发 post 钩子

patchVnode 函数:比较两个vnode节点是否相似 相似patch 不同直接进行移除和添加

patchVnode 方法主要的逻辑如下 :

  • 触发 prepatch 钩子
  • 触发 update 钩子, 这里主要为了更新对应的 module 内容
  • 非文本节点的情况 , 调用 updateChildren 更新所有子节点
  • 文本节点的情况 , 直接 api.setTextContent(elm, vnode.text as string);

updateChildren 方法:patchVnode 里面最重要的方法,也是整个 diff 里面的最核心方法

updateChildren 主要的逻辑如下:

  1. 优先处理特殊场景,先对比两端。也就是

    • 旧 vnode 头 vs 新 vnode 头
    • 旧 vnode 尾 vs 新 vnode 尾
    • 旧 vnode 头 vs 新 vnode 尾
    • 旧 vnode 尾 vs 新 vnode 头
  2. 拿新节点的key,能否对应上 oldCh 中的某个节点的key。 找不到则新建元素

  3. 如果找到 key,但是,元素选择器变化了,也新建元素

  4. 如果找到 key,并且元素选择没变, 则移动元素

  5. 两个列表对比完之后,清理多余的元素,新增添加的元素

addVnodes 方法:

addVnodes 就比较简单了,主要功能就是添加 Vnodes 到 真实 DOM 中

removeVnodes 方法:

删除 VNodes 的主要逻辑如下:

  • 循环触发 destroy 钩子,递归触发子节点的钩子
  • 触发 remove 钩子,利用 createRmCb , 在所有监听器执行后,才调用 api.removeChild,删除真正的 DOM 节点

createElm 方法:

主要逻辑如下:

  • 触发 init 钩子
  • 处理注释节点
  • 创建元素并设置 id , class
  • 触发模块 create 钩子 。
  • 处理子节点
  • 处理文本节点
  • 触发 vnodeData 的 create 钩子

详细请看系列文章结束:segmentfault.com/a/119000001…

14、模板编译

  • 模板不是html,有指令、插值、js表达式、能实现判断、循环
  • HTML是标签,只有js才能实现判断、循环(图灵完备语言)
  • 模板一定转换为某种js代码,即编译模板

1)、 js 的 with 语法

// 使用 with 能够改变 {} 内自由变量的查找方式
// 将 {} 内自由变量当做 obj 的属性来查找,如果找不到会报错,
// 不建议使用,打破了作用域规则,
  const obj = {
    a: 1000,
    b: 1000
  }

  with(obj){
    console.log(a);
    console.log(b);
    console.log(c); // 报错 c is not defined
  }

2)、vue template complier 将模板编译成 render 函数

const compiler = require('vue-template-compiler')

// 插值
// const template = `<p>{{message}}</p>`
// with(this){return createElement('p',[createTextVNode(toString(message))])}
// h -> vnode
// createElement -> vnode

// // 表达式
// const template = `<p>{{flag ? message : 'no message found'}}</p>`
// // with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}

// // 属性和动态属性
// const template = `
//     <div id="div1" class="container">
//         <img :src="imgUrl"/>
//     </div>
// `
// with(this){return _c('div',
//      {staticClass:"container",attrs:{"id":"div1"}},
//      [
//          _c('img',{attrs:{"src":imgUrl}})])}

// // 条件
// const template = `
//     <div>
//         <p v-if="flag === 'a'">A</p>
//         <p v-else>B</p>
//     </div>
// `
// with(this){return _c('div',[(flag === 'a')?_c('p',[_v("A")]):_c('p',[_v("B")])])}

// 循环
// const template = `
//     <ul>
//         <li v-for="item in list" :key="item.id">{{item.title}}</li>
//     </ul>
// `
// with(this){return _c('ul',_l((list),function(item){return _c('li',{key:item.id},[_v(_s(item.title))])}),0)}

// 事件
// const template = `
//     <button @click="clickHandler">submit</button>
// `
// with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}

// v-model
const template = `<input type="text" v-model="name">`
// 主要看 input 事件
// with(this){return _c('input',{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],attrs:{"type":"text"},domProps:{"value":(name)},on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})}

// render 函数
// 返回 vnode
// patch

// 编译
const res = compiler.compile(template)
console.log(res.render)

// ---------------分割线--------------

// // 从 vue 源码中找到缩写函数的含义
// function installRenderHelpers (target) {
//     target._o = markOnce;
//     target._n = toNumber;
//     target._s = toString;
//     target._l = renderList;
//     target._t = renderSlot;
//     target._q = looseEqual;
//     target._i = looseIndexOf;
//     target._m = renderStatic;
//     target._f = resolveFilter;
//     target._k = checkKeyCodes;
//     target._b = bindObjectProps;
//     target._v = createTextVNode;
//     target._e = createEmptyVNode;
//     target._u = resolveScopedSlots;
//     target._g = bindObjectListeners;
//     target._d = bindDynamicKeys;
//     target._p = prependModifier;
// }

3)、执行 render 函数生成 vnode

VNode表示Virtual DOM,用JavaScript对象来描述真实的DOM把DOM标签,属性,内容都变成对象的属性。

详见上边 js 模拟 Dom 元素

15、渲染和更新过程

回顾知识:vue的三大核心

  1. 响应式:监听 data 属性 getter setter (包括数组)。Object.defineProperty
  2. 模板编译:模板到 render 函数 再到 vnode
  3. vdom: patch(elem,vnode)vnode渲染的新的elem 和 patch(vnode,newVnode) 新的vode更新到旧的vnode。

1)、初次渲染过程

  • 解析模板为 render 函数 (或者在开发环境已完成,vue-loader)
  • 触发响应式,监听 data 属性 getter setter (会触发和模板(视图)有关的变量)
  • 执行 render 函数,生成 vnode, patch(elem,vnode)

2)、更新过程

  • 修改data,触发 setter (此前在 getter 中已被监听)
  • 重新执行 render 函数,生成 newVnode
  • patch(vnode,newVnode)

3)、异步渲染

  • $nextTick 函数
  • 汇总 data 的修改,一次性更新视图
  • 减少 DOM 操作次数,提高性能

16、前端路由原理

1)、hash模式

  • hash 变化会触发网页跳转,即浏览器的前进、后退
  • hash 变化不会刷新页面,spa必须的特点
  • hsah 永远不会提交到 server 端
  • window.onhashchange 监听

2)、 H5 history模式

  • 用url规范的路由,但跳转时不刷新页面(只是刷新刚进来的页面)
  • history.pushState (此方式,跳转浏览器不会刷新页面)
  • window.onpopstate (监听浏览器前进和后退,由history.pushState()或者history.replaceState()形成的历史节点中前进后退)

to B 的系统推荐使用 hash,简单易用,对 url 规范不敏感。

to C 的系统,可以选着 H5 history,对于 seo 优化的使用 H5 history

17、computed 的特点

  • 缓存,data 不变不会重新计算。
  • 提高性能

18、为何组件 data 必须是个函数

data 必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例。如果 data 仍然是一个纯粹的对象,则所有的实例将共享引用同一个数据对象,通过提供 data 函数,每次创建一个新实例后,我们能够调用 data 函数,从而返回初始数据的一个全新副本数据对象。

19、数据请求应该刚在那个生命周期

  • mounted
  • js 是单线程的,数据请求异步获取数据
  • 放在 mounted 之前是没有用的,只会让逻辑更加混乱,在 js 没有渲染完 异步数据也是在查询中不会提前加载。

20、如何将组建所有 props 传递给子组件

  • props (v-bind="props")

21、多个组件有相同的逻辑,如何抽离

使用 mixin

多个组件可以共享数据和方法,在使用mixin的组件中引入后,mixin中的方法和属性也就并入到该组件中,可以直接使用。数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。钩子函数会两个都被调用,mixin中的钩子首先执行。

存在问题:

变量来源不明确,多 mixin 可能造成命名冲突。 mixin 和组件可能存在多对多的关系,业务逻辑复杂。

22、何时使用异步组件

  • 加载大组件
  • 路由异步加载

23、何时使用 keep-alive

  • 缓存组件,不需要重复渲染
  • 多个静态的 tab 页

24、何时使用 beforeDestory

  • 解除自定义事件 event.$off
  • 清除定时器
  • 解除自定义 DOM 事件,如 window scroll 等。

25、请用 vnode 描述一个 DOM 结构

详见 用js模拟DOM结构原理

26、监听data 变化的核心 API 是什么

详见 vue 响应式原理 Object.defineProperty

27、vue 常见性能优化

  • 合理使用 v-show 和 v-if
  • 合理使用computed
  • v-for 时加 key , 避免和 v-if 同时使用
  • 自定义事件个dom 事件及时销毁
  • 合理使用异步组件
  • 合理使用 keep-alive
  • data 层级不要太深(响应式绑定递归遍历比较深)
  • webpack 层面的优化
  • 前端通用性能优化 (图片懒加载等)
  • 使用SSR

二、vue3.0 部分

1)、vue3 升级内容

  • 全部使用 ts 重写 (响应式,vdom,模板编译)
  • 性能提升,打包后代码减少
  • 调整部分API

2)、proxy 重写响应式

(1)、proxy 的基本使用

const data = {
    name: 'zhangsan',
    age: 20,
}

const proxyData = new Proxy(data, {
    get(target, key, receiver) {
        const result = Reflect.get(target, key, receiver)
        console.log('get',key)
        return result // 返回结果
    },
    set(target, key, val, receiver) {
        const result = Reflect.set(target, key, val, receiver)
        console.log('set', key, val)
        return result // 是否设置成功
    },
    deleteProperty(target, key) {
        const result = Reflect.deleteProperty(target, key)
        console.log('delete property', key)
        return result // 是否删除成功
    }
})

Reflect 和 Proxy 是搭配使用的。

Reflect 的作用

  • Reflect 和 Proxy 的能力一一对应。
  • 规范化、标准化、函数式。(如查找一个对象中的 key 一般是 "a" in obj, Reflects是 Reflect.has(obj,"a"), 删除的话 delete obj.a , ReflecteleteProperty(obj,"a"))
  • 代替 Object 上的工具函数

(1)、proxy 实现响应式

// 创建响应式
function reactive(target = {}) {
    if (typeof target !== 'object' || target == null) {
        // 不是对象或数组,则返回
        return target
    }

    // 代理配置
    const proxyConf = {
        get(target, key, receiver) {
            // 只处理本身(非原型的)属性
            const ownKeys = Reflect.ownKeys(target)
            if (ownKeys.includes(key)) {
                console.log('get', key) // 监听
            }
    
            const result = Reflect.get(target, key, receiver)
        
            // 深度监听
            // 性能如何提升的?
            return reactive(result)
        },
        set(target, key, val, receiver) {
            // 重复的数据,不处理
            if (val === target[key]) {
                return true
            }
    
            const ownKeys = Reflect.ownKeys(target)
            if (ownKeys.includes(key)) {
                console.log('已有的 key', key)
            } else {
                console.log('新增的 key', key)
            }

            const result = Reflect.set(target, key, val, receiver)
            console.log('set', key, val)
            // console.log('result', result) // true
            return result // 是否设置成功
        },
        deleteProperty(target, key) {
            const result = Reflect.deleteProperty(target, key)
            console.log('delete property', key)
            // console.log('result', result) // true
            return result // 是否删除成功
        }
    }

    // 生成代理对象
    const observed = new Proxy(target, proxyConf)
    return observed
}

// 测试数据
const data = {
    name: 'zhangsan',
    age: 20,
    info: {
        city: 'beijing',
        a: {
            b: {
                c: {
                    d: {
                        e: 100
                    }
                }
            }
        }
    }
}

const proxyData = reactive(data)
  • 深度监听,不是全部一次性监听,性能更好
  • 可监听 新增/删除 属性
  • 可监听数组变化