# vue3 ## 优点 * 打包体积更小 * 内存占用减少 * **更好的支持TS** * **使用Proxy代替defineProperty实现数据响应式** * **重写虚拟DOM的实现和Tree-Shaking** * 组合API (composition api) ,能够更好的组织逻辑,封装逻辑,复用逻辑 ## 新增特性 * **Composition (组合) API** * setup * ref 和 reactive * computed 和 watch * 新的生命周期函数 * provide与inject * ... * 新组件 * Fragment - 文档碎片 * Teleport - 瞬移组件的位置 * Suspense - 异步加载组件的loading界面 * 其它API更新 * 全局API的修改 * 将原来的全局API转移到应用对象 * 模板语法变化 ## 使用vite快速创建项目 开发环境下基于浏览器 [原生 ES 模块](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules), 有兼容性问题: chrome > 61, firefox >60, safair > 10.1 。在生产环境下基于 Rollup 打包。 使用 npm: ```bash # npm 6.x $ npm init vite@latest --template vue # npm 7+,需要加上额外的双短横线 $ npm init vite@latest -- --template vue $ cd $ npm install $ npm run dev ``` pnpm: ```bash $ pnpm create vite -- --template vue $ cd $ pnpm install $ pnpm dev ``` ## Composition API(组合式api) 2.x的叫option API (配置 api) [官方文档](https://composition-api.vuejs.org/zh/api.html) ### 生命周期和钩子函数 创建->挂载->更新->销毁, 每个阶段都有两个**钩子函数** * `setup()`-> ` ``` #### 限制 1. 修改选项配置需要单开一个 script ```ts // Vue 3 SFC 一般会自动从组件的文件名推断出组件的 name。在大多数情况下,不需要明确的 name 声明。 // 唯一需要的情况是当你需要 包含或排除或直接检查组件的选项时,你需要这个名字。 ``` 2. ts兼容 ```ts // 通过解构的方式去导入类型,setup sugar 会进行自动导出, 此时ts会报错 //解决方法: import type { test } from "./test" ``` ### ==vue 2 和 vue 3 的响应式区别== #### vue 2 * 核心: * 对象: 通过 `Object.defineProperty()` 对象的已有属性值的读取和修改进行劫持 (监视/拦截) * 数组: 通过 `重写数组` 更新数组一系列更新元素的 `方法` 来实现元素修改的劫持 ```js Object.defineProperty(data, 'count', { get () {}, set () {} }) ``` * 问题 * 数组直接通过下标替换元素或更新 length, 界面不会自动更新 * `push() pop() shift() unshift() splice() sort() reverse()` 这类会改变原数据的方法, 都进行了包裹 * `this.$set(this.array, index, newValue)` * 对象直接新添加的属性或删除已有属性, 界面不会自动更新 * `this.$set(this.obj, key , newValue)` * `this.$delete(this.obj, key)` * `this.obj=Object.assign({}, this.obj, {a:1, b:2})` #### vue 3 * 核心: * ref:创建的是一个包含 `.value` 属性的对象。对这个 `.value` 属性的访问和修改会触发 `get` 和 `set` 拦截,从而实现响应式(数据劫持)。 * reactive:通过 `Proxy` 来实现响应式(数据劫持),并通过 Reflect 操作原对象内部的数据 * 问题 * 数组用下标更新元素或者更新 length, 同样会丢失响应式 * `push() pop() shift() unshift() splice() sort() reverse()` * 对象使用 `Proxy` 实现响应式, 可以直接添加属性和删除属性 * 对象提取的 `基本类型` 没有响应式 * 用 computed 或者 toRef * 对象 `解构` 和 `展开运算符` 会丢失响应式 * `toRefs` * `reactive` 创建新的响应式对象 * `Object.assign(someReactive, newObj)` * 对象 `整个替换` 会丢失响应式 * 文档: * https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy * https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect ```html Proxy 与 Reflect ``` ### ref 函数 > 常用于简单数据类型定义为响应式数据 * 在 script 中修改值,获取值的时候,需要`.value` * 在模板 template 中使用ref声明的响应式数据,不需要.value * 其实也可以定义复杂数据类型的响应式数据, 如果用ref对象/数组, 内部会自动将对象/数组转换为reactive的代理对象 * 基本数据类型:创建的是一个包含 `.value` 属性的对象。对这个 `.value ` 属性的访问和修改会触发 ` get ` 和 ` set ` 拦截,从而实现响应式追踪和更新。 ```vue ``` * **减少心智负担, 优先 ref** ### toRef 函数 > 转换响应式对象中**某个**属性为单独响应式数据,并且值是关联的。 * 语法:`const name = toRef(person,"name")` 注意: **reactive 对象取出的所有属性值都是非响应式的。** 使用场景: 有一个响应式对象数据,但是模版中只需要使用其中一项数据。 ```vue ``` ### toRefs 函数 > 转换一个响应式对象, 返回一个普通对象, 普通对象的每个属性都是响应式的 * 语法:`const person = toRefs(person)` 使用场景:剥离响应式对象(解构|展开),想使用响应式对象中的**多个**或者**所有**属性做为响应式数据。 ```vue ``` ### isRef 检查值是否为一个 ref 对象。 ### unref 如果参数是一个 `ref`,则返回内部值,否则返回参数本身。这是 `val = isRef(val) ? val.value : val` 的语法糖函数 ### shallowRef函数 shallow: 浅层的 > 创建一个跟踪自身 `.value` 变化的 ref,但不会使其值也变成响应式的 > > 简单理解: 只处理了value的响应式, 不进行对象的reactive处理 > > 使用场景: **有一个对象数据, 后面会产生新的对象来替换(比如: 动态组件)** ```ts let sum = shallowRef(0) const update1 = ()=>{ sum.value++ // 基础数据类型是响应式的 } let sum2 = shallowRef({count: 0}) const update2 = ()=> { sum2.value.count++ // 对象不是响应式的, 视图不更新 } const update3 = ()=>{ sum2.value = {cont: 3} // value是响应式的, 视图会更新 } ``` ### triggerRef函数 > 手动使 shallowRef 的对象响应一次 ```ts let sum2 = shallowRef({count: 0}) const update1 = ()=>{ sum2.value.count++ // 对象不是响应式的, 视图不更新 } const trigger => { triggerRef(sum2) // 视图触发更新一次 } ``` ### customRef > 创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。它需要一个工厂函数,该函数接收 `track` 和 `trigger` 函数作为参数,并且应该返回一个带有 `get` 和 `set` 的对象。 使用 customRef 实现 防抖 的示例 ```ts import {customRef} from 'vue' /* 实现函数防抖的自定义ref */ function useDebouncedRef(value: T, delay = 200) { let timeout: number return customRef((track, trigger) => { return { get() { // 告诉Vue追踪数据 track() return value }, set(newValue: T) { clearTimeout(timeout) timeout = setTimeout(() => { value = newValue // 告诉Vue去触发界面更新 trigger() }, delay) } } }) } const keyword = useDebouncedRef('', 500) ``` ### reactive 函数 > 一般用来定义复杂数据类型成为响应数据 * reactive() 会对对象进行深层响应式转换,但是这种深层转换是**惰性的:只有当访问嵌套属性(触发 getter)时,Vue 才会递归将该属性也转为响应式。 * 注意: 用解构取出的单独属性, 不再是响应式的(用toRef) * 给reactive赋值 {} , 不再是响应式(用ref). 举例: 从接口获取数据, 直接把data赋值给reactive, 会失去相应式. 改为用ref定义, 给ref.value赋值 * 内部基于es6的proxy实现,通过代理对象内部数据进行操作 ```vue ``` * 用proxy实现数据相应式后, 就不再需要v2的this.$set了(proxy响应是深层次的) ```vue ``` * 当将 `ref`分配给 `reactive` property 时,ref 将被自动解包。 ```ts const count = ref(1) const obj = reactive({}) obj.count = count console.log(obj.count) // 1 console.log(obj.count === count.value) // true ``` ### shallowReactive proxy代理有性能问题, https://www.zhihu.com/question/460330154, https://www.cnblogs.com/zmj97/p/10954968.html > 创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (暴露原始值) > 简单理解: 只有第一层是响应式的 > 应用: **有一个对象数据, 结构比较深, 但变化时只是外层属性变化** ```ts const state = shallowReactive({ foo: 1, nested: { bar: 2 } }) // 改变 state 本身的性质是响应式的 state.foo++ // ...但是不转换嵌套对象 isReactive(state.nested) // false state.nested.bar++ // 非响应式 ``` ### readonly函数 > 接受一个对象 (响应式或纯对象) 或 ref 并返回原始对象的只读代理。只读代理是深层的:任何被访问的嵌套 property 也是只读的。 ```ts const original = reactive({ count: 0 }) const copy = readonly(original) original.count++ copy.count++ // 警告! ``` ### shallowReadonly > 创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换 (暴露原始值) ```ts const data = shallowReadonly({ a: 1, b: { c: 2 } }) const update = ()=>{ data.a ++ // error data.b.c++ // 成功 } ``` ### isReactive 检查对象是否是由 [`reactive`](https://v3.cn.vuejs.org/api/basic-reactivity.html#reactive) 创建的响应式代理。 如果该代理是 [`readonly`](https://v3.cn.vuejs.org/api/basic-reactivity.html#readonly) 创建的,但包裹了由 [`reactive`](https://v3.cn.vuejs.org/api/basic-reactivity.html#reactive) 创建的另一个代理,它也会返回 `true`。 ```ts const state = reactive({ name: 'John' }) // 从普通对象创建的只读 proxy const plain = readonly({ name: 'Mary' }) console.log(isReactive(plain)) // -> false // 从响应式 proxy 创建的只读 proxy const stateCopy = readonly(state) console.log(isReactive(stateCopy)) // -> true ``` ### isProxy 检查对象是否是由 [`reactive`](https://v3.cn.vuejs.org/api/basic-reactivity.html#reactive) 或 [`readonly`](https://v3.cn.vuejs.org/api/basic-reactivity.html#readonly) 创建的 proxy。 ### toRaw * 返回由 `reactive` 或 `readonly` 方法转换成响应式代理的普通对象。 * 这是一个还原方法,可用于临时读取,访问不会被代理/跟踪,写入时也不会触发界面更新。 ```ts const state = reactive({ name: 'tom', age: 25, }) const testToRaw = () => { const user = toRaw(state) user.age++ // 界面不会更新 } ``` ### markRaw * 标记一个对象,使其永远不会转换为代理。返回对象本身 * 应用场景: * 有些值不应被设置为响应式的,例如复杂的第三方类实例或 Vue 组件对象。 * 当渲染具有不可变数据源的大列表时,跳过代理转换可以提高性能。 ```ts const state = reactive({ name: 'tom', age: 25, }) const testMarkRaw = ()=>{ const likes = ['a', 'b'] state.likes = markRaw(likes) // likes数组就不再是响应式的了 } ``` **同一性风险**: 因为原始选择退出仅在**根级别**,因此,如果将嵌套在内的、未标记的原始对象添加进响应式对象,然后再次访问该响应式对象,就会得到原始对象被代理后的版本 ```ts const foo = markRaw({ nested: {} }) const bar = reactive({ // 虽然 `foo` 被标记为原始,但 foo.nested 不是。 nested: foo.nested }) console.log(foo.nested === bar.nested) // false ``` ### computed 函数 计算属性 computed函数: * 与computed配置功能一致 * 普通用法: 只有getter * 高级用法: 有getter和setter * 在script中获取computed的值需要`.value`, 在模板中不用 ```vue ``` ### watch 函数 监听 watch 函数: * 与watch配置功能一致 * 监视指定的一个或多个响应式数据, 一旦数据变化, 就自动执行监视回调 * 默认初始时不执行回调, 但可以通过配置 immediate 为 true, 来指定初始时立即执行第一次 * 通过配置deep为true, 来指定深度监视 * ===监听reactive对象中的属性, 必须通过函数返回该属性来指定=== * 回调函数可以接收到两个参数, newValue和oldValue ```vue ``` #### 监听 prop > 第一个参数需要通过箭头函数返回 props 的值 > props 理解成 reactive 对象 ```ts ``` ### watchEffect 函数 * vue3特性 * 不用直接指定要监视的数据, 回调函数中使用的哪些响应式数据就监视哪些响应式数据 * 默认初始时就会执行第一次, 从而可以收集需要监视的数据 * 监视数据发生变化时回调 ```vue ``` | 特性 | watchEffect | watch | | ---------- | --------------- | ------------------------ | | **依赖收集方式** | 自动收集所有回调中的响应式依赖 | 显式指定要监听的源 | | **立即执行** | 立即执行回调 | 默认不立即执行(可配置 `immediate`) | | **返回值处理** | 无法直接访问变化前后的值 | 可以获取新旧值 | | **调试信息** | 调试信息较少 | 提供更丰富的调试信息 | | **性能开销** | 略高(自动依赖收集) | 略低(明确依赖关系) | #### 动态依赖 > 每次运行时都会重新收集依赖 > watch 是静态依赖 * 条件依赖 ```ts watchEffect(() => { if (condition.value) { console.log(activeData.value) // 条件为真时才依赖 activeData } else { console.log(backupData.value) // 条件为假时才依赖 backupData } }) /** 当 condition 变化时,会重新执行并收集依赖 每次执行可能建立不同的依赖关系 */ ``` * 动态依赖 ```ts const activeTab = ref('users') const users = ref([]) const products = ref([]) watchEffect(async () => { if (activeTab.value === 'users') { users.value = await fetchUsers() } else { products.value = await fetchProducts() } }) /** 当 activeTab 从 'users' 变为 'products' 时: 移除对 users 的依赖 新增对 products 的依赖 */ ``` ### ref标签属性 > 绑定DOM或者组件 > > 单个元素:先申明ref响应式数据,返回给模版使用,通过ref绑定数据 注意: 写法和vue2不同, 把一个使用`ref`声明的响应式数据绑定到标签的ref属性上, 使用时要用`.value` ```vue ``` #### v-for中的ref > * 遍历的元素:先定义一个空数组,定一个函数获取元素,返回给模版使用,通过 ref 绑定这个函数 > * 视图更新时, 要清空数组(在onBeforeUpdate ) https://v3.cn.vuejs.org/guide/migration/array-refs.html#frontmatter-title ```js ``` ### 自定义hook函数 > 自定义hook的作用类似于vue2中的mixin技术 > 使用Vue3的组合API封装的可复用的**功能函数** > 文件命名时最好在前面加上`use` > 在setup中能使用的函数,在hook函数中也都可以使用 和mixin的区别 * mixin中的变量和方法是隐式引入(在mixinsp配置中引入), 而hook是要主动调用 * 在一个组件中使用多个mixin可能会出现,函数和变量重名现象,就会导致冲突或覆盖现象。而使用Hook函数时,因为变量和函数是显示引用,我们就可以通过解构赋值,来避免函数和变量重名现象。 * mixins不能传入参数改变它的逻辑 例子1: 收集用户鼠标点击的页面坐标 `hooks/useMousePosition.ts` ```ts import { ref, onMounted, onUnmounted } from 'vue' /* 收集用户鼠标点击的页面坐标 */ export default function useMousePosition () { // 初始化坐标数据 const x = ref(-1) const y = ref(-1) // 用于收集点击事件坐标的函数 const updatePosition = (e: MouseEvent) => { x.value = e.pageX y.value = e.pageY } // 挂载后绑定点击监听 onMounted(() => { document.addEventListener('click', updatePosition) }) // 卸载前解绑点击监听 onUnmounted(() => { document.removeEventListener('click', updatePosition) }) return {x, y} } ``` 使用: ```vue ``` ## v-model [官方文档](https://v3.cn.vuejs.org/guide/migration/v-model.html#%E4%BB%8B%E7%BB%8D) 变化的总体概述: * 非兼容:用于自定义组件时,`v-model` prop 和事件默认名称已更改: * prop:`value` -> `modelValue`; * 事件:`input` -> `update:modelValue`; * **非兼容**:`v-bind` 的 `.sync` 修饰符和组件的 `model` 选项已移除,可在 `v-model` 上加一个参数代替; * **新增**:现在可以在同一个组件上使用多个 `v-model` 绑定; * **新增**:现在可以自定义 `v-model` 修饰符。 ps: vue2.2 引入 `model` 组件选项, 但只允许在组件上使用一个 `v-model` ### prop 和事件名发生变化 父组件 ```vue ``` 子组件 ```vue ``` ### 自定义model名(可以用来替代原来的`.sync`语法糖) 父组件 ```vue ``` 子组件 ```js ``` ### 自定义修饰符 [官方文档](https://v3.cn.vuejs.org/guide/component-custom-events.html#%E5%A4%84%E7%90%86-v-model-%E4%BF%AE%E9%A5%B0%E7%AC%A6) ps: 内置的有`.trim`, `number`, `.lazy` 添加到组件 `v-model` 的修饰符将通过 `modelModifiers` prop 提供给组件 ( 如果`v-model` 自定义model名, prop 名为`自定义名` + `Modifiers` ) * 例子: 把v-model的输入首字母变大写 父组件 ```vue ``` 子组件 ```vue ``` * 例子: 自定义model名 + 自定义修饰符号 父组件 ```vue ``` 子组件 ```vue ``` ## 依赖注入 > 无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。 > 这个特性有两个部分:父组件有一个 `provide` 选项来提供数据,子组件有一个 `inject` 选项来开始使用这些数据。 > ==在 ` ``` ## 新组件 ### Fragment(片段) * 在Vue3中: 组件可以没有根标签, 内部会将多个标签包含在一个Fragment虚拟元素中 * 好处: 减少标签层级, 减小内存占用 ```vue ``` vue 2 必须添加一个根标签把这两个 h2 包住 ### Teleport 传送门 > 一个组件模板的一部分在逻辑上从属于该组件,但从整个应用视图的角度来看,它在 DOM 中应该被渲染在整个 Vue 应用外部的其他地方。 > > 最常见的例子就是全屏的模态框 `` 接收一个 `to prop` 来指定传送的目标。`to` 的值可以是一个 CSS 选择器字符串,也可以是一个 DOM 元素对象。这段代码的作用就是告诉 Vue"把以下模板片段传送到 body 标签下"。 ```js ``` ### Suspense(不确定的) 试验性特性 ## 新指令 ### v-memo > 记录指令下的模板树。该指令期望一个数组,如果数组内的值,都没有发生更新,那么指令下的模板树也不会发生更新。 > > 官网提到`v-memo`仅用于性能关键场景中的微优化,一般使用到的地方应该是渲染大型`v-for`列表(其中`length > 1000`)。 ```vue

ID: {{ item.id }} - selected: {{ item.id === selected }}

...more child nodes

``` ### v-if和v-for优先级 * v-if优先v-for解析(和vue2不同) ## 常见错误 1. 引入组件时, `.vue`后缀不能省略 2. `@` 路径不生效, 在`tsconfig.json`里添加配置 ```json "compilerOptions": { //... "paths": { "@/*": [ "./src/*" ], }, //... } ``` 在`vite.config.ts` ```ts export default defineConfig({ base: "./", resolve: { alias: [ { find: "@", replacement: resolve(__dirname, "./src") // __dirname报错, 则需要安装 `@types/node` npm包 } ] }, // ... }) ``` 3. less支持 ```ts // 1. 先安装less 和 less-loader // 在vite.config.ts export default defineConfig({ css: { preprocessorOptions: { less: { javascriptEnabled: true } } }, // ... }) ``` ## h 函数直接生成虚拟dom 待补充... ## render函数渲染 待补充... ## 动态加载组件 `import()` 函数和 `defineAsyncComponent` ## vue2 vue3 是怎么自动更新页面的 数据劫持/数据代理 + 依赖收集 + 派发更新 vue 2: * 数据劫持: 通过 `Object.definedProperry()` 为数据对象添加 getter 和 setter 方法, 当读取属性的时候, 会触发 getter 方法, 当修改属性值时, 会触发 setter 方法 * 依赖收集: 在 `getter` 方法中会进行依赖收集. 当 Vue 渲染时, 会访问数据对象的属性, getter 方法会将当前的渲染函数和计算属性添加到一个 `依赖列表` 中 * 派发更新: 当数据发生变化时, `setter` 方法会通知所有依赖该属性的地方进行更新. Vue 通过 `Wathcher 观察者模式` 实现 vue 3: * 数据代理: 通过 `Proxy` 代理整个对象, 无须递归劫持 * 依赖收集: 也是在 `getter` 访问属性时收集依赖, 但是用 `WeakMap` (key 是对象, 可以被自动回收) + `Map` 存储依赖关系 * 派发更新: `setter` 方法会调用 `trigger`, 遍历 `effect`(类似 Vue 2 的 `Watcher`), 通过调度器(`Scheduler`)执行 | **对比项** | **Vue 2** (`Object.defineProperty`) | **Vue 3** (`Proxy + WeakMap`) | | ---------- | ----------------------------------- | -------------------------------------------- | | **劫持方式** | 递归使用 `Object.defineProperty` 劫持每个属性 | 使用 `Proxy` 一次性劫持整个对象 | | **动态属性监听** | ❌ 新增属性无法响应(需 `Vue.set`) | ✅ 动态添加/删除属性也能被监听 | | **依赖收集** | `getter` 中手动调用 `Dep` 收集依赖 | 利用 `WeakMap + Map` 存储依赖关系,结构更清晰 | | **依赖存储结构** | 每个属性有自己的 `Dep`,记录对应 `Watcher` | 中心化的 `targetMap: WeakMap -> Map -> Set` 存储结构 | | **派发更新** | `setter` 中触发对应 `Watcher.update()` | 使用 `trigger(target, key)` 精准触发 `effect` 函数 | | **数组监听** | 重写数组方法(如 `push`、`pop`)手动触发更新 | Proxy 能直接监听数组索引和长度变化 | | **性能优化** | 初始化时递归全对象,成本高 | **按需懒劫持**,只在访问时创建响应式,性能更高 | | **调度机制** | 借助 `nextTick()` 实现异步更新 | 提供内建的 `scheduler` 参数,可自定义任务调度 | | **内存管理** | 手动管理引用关系 | `WeakMap` 自动管理依赖,**对象无引用时自动回收** | ## vue 2 和 vue 3 diff 算法的区别 * vue 2 基于标签和 key 识别节点, 需要 `递归遍历` 比较所有节点 * vue 3 增加了 `静态标记`, 静态节点会被跳过, 动态节点会标记哪里会变 * vue 2 使用 `双端比较算法`, 需要同时比较新旧虚拟节点的头部和尾部 * vue 3 先排除首尾不变的部分, 对于变化部分, 使用基于 `最长递增子序列算法` . 举例: ``` A-B-C-D 变成 A-C-B-D 1. 会先去掉首尾不变的部分, 即 B-C-D 变成 C-B-D 2. 基于最新的节点, 生成序列 [0, 1, 2], 对应旧节点, 那么序列为 [1, 0, 2] 3. 最长递增子序列是 [0, 2], 只需要调整 1 的位置就行 ``` | **优化点** | **Vue 2** | **Vue 3** | | ------------------ | ----------------------- | ------------------------- | | **静态节点优化** | 无优化,全部重新渲染 | 编译时标记静态节点,跳过不变的部分 | | **VNode Patch** | 递归比对所有子节点 | `patchFlag` 标记动态部分,跳过静态内容 | | **列表 Diff(v-for)** | O(n) 复杂度遍历整个 `children` | 最长递增子序列(LIS),优化 DOM 操作 | | **Fragment 片段** | 需要额外的 `div` 作为根节点 | 允许多个根节点,减少无意义 Diff | ## 手写api ### shallowReactive 与 reactive ```js const reactiveHandler = { get (target, key) { if (key==='_is_reactive') return true return Reflect.get(target, key) }, set (target, key, value) { const result = Reflect.set(target, key, value) console.log('数据已更新, 去更新界面') return result }, deleteProperty (target, key) { const result = Reflect.deleteProperty(target, key) console.log('数据已删除, 去更新界面') return result }, } /* 自定义shallowReactive */ function shallowReactive(obj) { return new Proxy(obj, reactiveHandler) } /* 自定义reactive */ function reactive (target) { if (target && typeof target==='object') { if (target instanceof Array) { // 数组 target.forEach((item, index) => { target[index] = reactive(item) }) } else { // 对象 Object.keys(target).forEach(key => { target[key] = reactive(target[key]) }) } const proxy = new Proxy(target, reactiveHandler) return proxy } return target } /* 测试自定义shallowReactive */ const proxy = shallowReactive({ a: { b: 3 } }) proxy.a = {b: 4} // 劫持到了 proxy.a.b = 5 // 没有劫持到 /* 测试自定义reactive */ const obj = { a: 'abc', b: [{x: 1}], c: {x: [11]}, } const proxy = reactive(obj) console.log(proxy) proxy.b[0].x += 1 proxy.c.x[0] += 1 ``` ### shallowRef 与 ref ```ts /* 自定义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 } /* 自定义ref */ function ref(target) { if (target && typeof target==='object') { target = reactive(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 } /* 测试自定义shallowRef */ const ref3 = shallowRef({ a: 'abc', }) ref3.value = 'xxx' ref3.value.a = 'yyy' /* 测试自定义ref */ const ref1 = ref(0) const ref2 = ref({ a: 'abc', b: [{x: 1}], c: {x: [11]}, }) ref1.value++ ref2.value.b[0].x++ console.log(ref1, ref2) ``` ### shallowReadonly 与 readonly ```js const readonlyHandler = { get (target, key) { if (key==='_is_readonly') return true return Reflect.get(target, key) }, set () { console.warn('只读的, 不能修改') return true }, deleteProperty () { console.warn('只读的, 不能删除') return true }, } /* 自定义shallowReadonly */ function shallowReadonly(obj) { return new Proxy(obj, readonlyHandler) } /* 自定义readonly */ function readonly(target) { if (target && typeof target==='object') { if (target instanceof Array) { // 数组 target.forEach((item, index) => { target[index] = readonly(item) }) } else { // 对象 Object.keys(target).forEach(key => { target[key] = readonly(target[key]) }) } const proxy = new Proxy(target, readonlyHandler) return proxy } return target } /* 测试自定义readonly */ /* 测试自定义shallowReadonly */ const objReadOnly = readonly({ a: { b: 1 } }) const objReadOnly2 = shallowReadonly({ a: { b: 1 } }) objReadOnly.a = 1 objReadOnly.a.b = 2 objReadOnly2.a = 1 objReadOnly2.a.b = 2 ``` ### isRef, isReactive 与 isReadonly ```js /* 判断是否是ref对象 */ function isRef(obj) { return obj && obj._is_ref } /* 判断是否是reactive对象 */ function isReactive(obj) { return obj && obj._is_reactive } /* 判断是否是readonly对象 */ function isReadonly(obj) { return obj && obj._is_readonly } /* 是否是reactive或readonly产生的代理对象 */ function isProxy (obj) { return isReactive(obj) || isReadonly(obj) } /* 测试判断函数 */ console.log(isReactive(reactive({}))) console.log(isRef(ref({}))) console.log(isReadonly(readonly({}))) console.log(isProxy(reactive({}))) console.log(isProxy(readonly({}))) ```