普通视图

发现新文章,点击刷新页面。
昨天 — 2025年7月5日首页

vue3+element-plus el-table列的显隐、列宽 持久化

作者 xiguolangzi
2025年7月5日 15:23

element-plus el-table列的显隐、列宽 持久化

1 工具组件

<template>
  <div class="top-right-btn" :style="style">
    <el-row>
      <el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search">
        <el-button circle icon="Search" @click="toggleSearch()" />
      </el-tooltip>
      <el-tooltip class="item" effect="dark" content="刷新" placement="top">
        <el-button circle icon="Refresh" @click="refresh()" />
      </el-tooltip>
      <el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="columns">
        <el-button circle icon="Menu" @click="showColumn()" v-if="showColumnsType == 'transfer'"/>
        <el-dropdown trigger="click" :hide-on-click="false" style="padding-left: 12px" v-if="showColumnsType == 'checkbox'">
          <el-button circle icon="Menu" />
          <template #dropdown>
            <el-dropdown-menu>
              <!-- 全选/反选 按钮 -->
              <el-dropdown-item>
                <el-checkbox :indeterminate="isIndeterminate" v-model="isChecked" @change="toggleCheckAll"> 列展示 </el-checkbox>
              </el-dropdown-item>
              <div class="check-line"></div>
              <template v-for="item in columns" :key="item.key">
                <el-dropdown-item>
                  <el-checkbox v-model="item.visible" @change="checkboxChange($event, item.prop)" :label="item.label" />
                </el-dropdown-item>
              </template>
            </el-dropdown-menu>
          </template>
        </el-dropdown>
      </el-tooltip>
    </el-row>
    <el-dialog :title="title" v-model="open" append-to-body>
      <el-transfer
        :titles="['显示', '隐藏']"
        v-model="value"
        :data="columns"
        @change="dataChange"
      ></el-transfer>
    </el-dialog>
  </div>
</template>

<script setup>
const props = defineProps({
  /* 是否显示检索条件 */
  showSearch: {
    type: Boolean,
    default: true
  },
  /* 显隐列信息 */
  columns: {
    type: Array
  },
  /* 是否显示检索图标 */
  search: {
    type: Boolean,
    default: true
  },
  /* 显隐列类型(transfer穿梭框、checkbox复选框) */
  showColumnsType: {
    type: String,
    default: "checkbox"
  },
  /* 右外边距 */
  gutter: {
    type: Number,
    default: 10
  },
})

const emits = defineEmits(['update:showSearch', 'queryTable','updateStorage'])

// 显隐数据
const value = ref([])
// 弹出层标题
const title = ref("显示/隐藏")
// 是否显示弹出层
const open = ref(false)

const style = computed(() => {
  const ret = {}
  if (props.gutter) {
    ret.marginRight = `${props.gutter / 2}px`
  }
  return ret
})

// 是否全选/半选 状态
const isChecked = computed({
  get: () => props.columns.every(col => col.visible),
  set: () => {}
})
const isIndeterminate = computed(() => props.columns.some((col) => col.visible) && !isChecked.value)

// 搜索
function toggleSearch() {
  emits("update:showSearch", !props.showSearch)
}

// 刷新
function refresh() {
  emits("queryTable")
}

// 右侧列表元素变化
function dataChange(data) {
  for (let item in props.columns) {
    const key = props.columns[item].key
    props.columns[item].visible = !data.includes(key)
  }
}

// 打开显隐列dialog
function showColumn() {
  open.value = true
}

if (props.showColumnsType == 'transfer') {
  // 显隐列初始默认隐藏列
  for (let item in props.columns) {
    if (props.columns[item].visible === false) {
      value.value.push(parseInt(item))
    }
  }
}

// 单勾选
function checkboxChange(event, prop) {
  props.columns.filter(item => item.prop == prop)[0].visible = event
  emits("updateStorage")
}

// 切换全选/反选
function toggleCheckAll() {
  const newValue = !isChecked.value
  props.columns.forEach((col) => (col.visible = newValue))
  emits("updateStorage")
}
</script>

<style lang='scss' scoped>
:deep(.el-transfer__button) {
  border-radius: 50%;
  display: block;
  margin-left: 0px;
}
:deep(.el-transfer__button:first-child) {
  margin-bottom: 10px;
}
:deep(.el-dropdown-menu__item) {
  line-height: 30px;
  padding: 0 17px;
}
.check-line {
  width: 90%;
  height: 1px;
  background-color: #ccc;
  margin: 3px auto;
}
</style>


2 封装持久化

// src/utils/storageByUI.js
import localforage from 'localforage'

// 强制使用 IndexedDB 作为存储驱动
localforage.config({
  name: 'okFactura',
  storeName: 'ui_data',
  description: 'Local storage using IndexedDB',
  driver: localforage.INDEXEDDB
})

/** 设置数据 */
const setItem = (key, value) => {
  const raw = JSON.parse(JSON.stringify(value));  // 脱响应式
  return localforage.setItem(key, raw);
}

/** 获取数据 */
const getItem = (key) => {
  return localforage.getItem(key)
}

/** 删除数据 */
const removeItem = (key) => {
  return localforage.removeItem(key)
}

/** 清空数据 */
const clear = () => {
  return localforage.clear()
}

export default {
  setItem,
  getItem,
  removeItem,
  clear
}

3 实现显隐、列宽 持久化

<template>
  <div class="app-container">
      <!-- 1 引用工具组件,常用组件注册到主进程main.js -->
      <right-toolbar 
          v-model:showSearch="showSearch" 
          @queryTable="getList" 
          @updateStorage="updateStorageByColumns" 
          :columns="columns" 
      />
    ... ...
    <el-table class="table-container" v-loading="loading" :data="printConfigList" @selection-change="handleSelectionChange" 
        <!-- 2 border column-resizable 可调整列宽 -->
      size="small" stripe border column-resizable 
        <!-- 3 header-dragend 更改列宽触发 -->
      @header-dragend="handleColumnWidthChange"
    >
      <el-table-column type="selection" width="50" align="center" />
      <el-table-column label="序号" type="index" align="center" width="50" />
      <el-table-column label="模板类型" align="center" prop="templateType"
          <!-- 4 获取持久化列宽 -->
        :width="getColumnWidth(0)" 
          <!-- 5 获取持久化显隐状态 -->
        v-if="columns[0]?.visible"
       >
        <template #default="scope">
          <dict-tag :options="print_template_type" :value="scope.row.templateType"/>
        </template>
      </el-table-column>
      ... ...
    </el-table>
    
    ... ...

  </div>
</template>

<script setup name="PrintConfig">
... ...
// 引入封装的持久化
import storageByUI from "@/utils/storageByUI";
... ...

// ----------------------------------- 列宽设计 start -----------------------------------
const columns = ref([])
const columnsKey = "printConfigColumns"
// 6 prop-控制索引,visible-显隐状态,width-列宽
const initColumns = [
  { key: 0, label: `模板类型`, prop:'templateType', visible: true ,  width: 80 },
  { key: 1, label: `绑定的模板名称`, prop:'templateName', visible: true, width: 120 },
  { key: 2, label: `打印宽度`, prop:'templateWidth', visible: true , width: 80 },
  { key: 3, label: `打印高度`, prop:'templateHeight', visible: true , width: 80 },
  { key: 4, label: `更新时间`, prop:'updateTime', visible: true , width: 120 },
  { key: 5, label: `更新人`, prop:'updateBy', visible: true , width: 80 },
  { key: 6, label: `备注描述`, prop:'remark', visible: true , width: 120 }
]

/** 7 获取持久化列信息 */
function getColumns() { 
  storageByUI.getItem(columnsKey).then(storeColumns => { 
    if(storeColumns) { 
      columns.value = storeColumns;
    } else { 
      columns.value = initColumns;
    }
  })
}
getColumns()

// 8 更新持久化列信息
function updateStorageByColumns(){
  storageByUI.setItem(columnsKey, columns.value);
}

/** 9 修改列宽 */
function handleColumnWidthChange(newWidth, oldWidth, column, event){
  console.log("修改列宽......",column,newWidth)
  // 9.1 找到对应的列索引
  const columnIndex = columns.value.findIndex(col => col.prop === column.property);
  if (columnIndex !== -1) {
    // 9.2 更新列宽
    columns.value[columnIndex].width = newWidth;
    // 9.3 存储更新后的列配置
    updateStorageByColumns();
  }
};

// 10 获取列宽方法
const getColumnWidth = (index) => {
  if (columns.value[index] && columns.value[index].width) {
    return columns.value[index].width;
  }
  return undefined; // 返回undefined让element-ui使用默认宽度
};

// ----------------------------------- 列宽设计 end -----------------------------------
... ...

Vue 模板引用(ref)全面指南:从基础到高级应用

2025年7月4日 14:14

在 Vue 开发中,虽然声明性渲染模型为我们抽象了大部分 DOM 操作,但在某些场景下,我们仍然需要直接访问底层 DOM 元素或子组件实例。Vue 提供的 ref attribute 正是为解决这类需求而生,它允许我们在元素或组件挂载后获取其直接引用,实现更精细的控制。本文将深入探讨 Vue 模板引用的各种用法与最佳实践。

一、ref 的基本概念与核心作用

ref 是 Vue 中一个特殊的 attribute,类似于 v-for 中的 key,其核心功能是为 DOM 元素或子组件实例创建引用标识。当元素或组件被挂载到 DOM 后,我们可以通过这些引用执行以下操作:

  • 对 DOM 元素进行焦点设置、动画控制等底层操作

  • 初始化或操作第三方库(如 Chart.js 图表实例)

  • 访问子组件的实例方法与属性

  • 在 v-for 循环中批量获取元素引用

关键特性

  • 引用仅在组件挂载后可用(初次渲染前为 null
  • 支持字符串命名与函数绑定两种方式
  • 在组合式 API 中通过 useTemplateRef 函数获取引用
  • 子组件引用的访问受 <script setup> 私有性限制

二、在组合式 API 中使用 ref

2.1 获取 DOM 元素引用

在组合式 API 中,获取 DOM 元素引用的标准流程如下:

<script setup>
import { useTemplateRef, onMounted } from 'vue'

// 声明与模板中 ref 值匹配的引用
const inputRef = useTemplateRef('input-element')

// 组件挂载后访问引用
onMounted(() => {
  // 对输入框设置焦点
  inputRef.value.focus()
  
  // 操作 DOM 元素属性
  inputRef.value.placeholder = '已通过 ref 初始化'
})
</script>

<template>
  <input ref="input-element" type="text" />
</template>

2.2 TypeScript 类型推断

Vue 对 TypeScript 提供了良好支持,inputRef.value 的类型会根据匹配的元素自动推断:

<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'

// TypeScript 自动推断 inputRef.value 为 HTMLInputElement 类型
const inputRef = useTemplateRef('input-element')

onMounted(() => {
  // 类型安全的操作
  inputRef.value.addEventListener('input', (e) => {
    console.log(e.target.value)
  })
})
</script>

三、模板引用的高级使用场景

3.1 监听引用变化

由于引用在组件挂载前为 null,且可能随组件卸载而消失,监听时需进行非空判断:

js

import { useTemplateRef, watchEffect } from 'vue'

const inputRef = useTemplateRef('input-element')

// 响应式监听引用变化
watchEffect(() => {
  if (inputRef.value) {
    // 元素已挂载,执行操作
    inputRef.value.style.border = '2px solid blue'
  } else {
    // 元素未挂载或已卸载
    console.log('元素状态变更,当前为 null')
  }
})

3.2 子组件引用与组件通信

当 ref 应用于子组件时,引用值为子组件实例,可访问其属性与方法:

<!-- 父组件 -->
<script setup>
import { useTemplateRef, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'

// 获取子组件实例引用
const childRef = useTemplateRef('child')

onMounted(() => {
  // 调用子组件方法
  childRef.value.doSomething()
  
  // 访问子组件属性
  console.log(childRef.value.dataValue)
})
</script>

<template>
  <ChildComponent ref="child" />
</template>
<!-- 子组件(选项式 API) -->
<script>
export default {
  data() {
    return {
      dataValue: '子组件数据'
    }
  },
  methods: {
    doSomething() {
      console.log('子组件方法被调用')
    }
  }
}
</script>

3.3 <script setup> 组件的引用限制与暴露

使用 <script setup> 的组件默认私有,父组件无法直接访问其属性,需通过 defineExpose 显式暴露:

<!-- 子组件(<script setup>) -->
<script setup>
import { ref } from 'vue'

// 私有属性
const privateData = '不会被暴露'
const exposedData = ref('将被暴露的数据')

// 暴露属性与方法
defineExpose({
  exposedData,
  // ref 会自动解包为原始值
  getExposedValue() {
    return exposedData.value
  }
})
</script>

<template>
  <div>子组件内容</div>
</template>
<!-- 父组件访问暴露的子组件引用 -->
<script setup>
import { useTemplateRef } from 'vue'
import ExposedChild from './ExposedChild.vue'

const childRef = useTemplateRef('exposedChild')

// 组件挂载后访问暴露的属性
console.log(childRef.value.exposedData) // 输出: "将被暴露的数据"
console.log(childRef.value.getExposedValue()) // 输出: "将被暴露的数据"
</script>

四、v-for 中的模板引用(v3.5+)

Vue 3.5 及以上版本支持在 v-for 中获取元素引用数组,这在批量操作元素时非常实用:

<script setup>
import { ref, useTemplateRef, onMounted } from 'vue'

const items = ref([
  { id: 1, name: '项目一' },
  { id: 2, name: '项目二' },
  { id: 3, name: '项目三' }
])

// 获取 v-for 中所有 li 元素的引用数组
const itemRefs = useTemplateRef('listItems')

onMounted(() => {
  // itemRefs.value 是包含所有 li 元素的数组
  console.log('元素数量:', itemRefs.value.length)
  
  // 批量设置样式
  itemRefs.value.forEach((el, index) => {
    el.style.color = index % 2 === 0 ? 'red' : 'blue'
  })
})
</script>

<template>
  <ul>
    <li v-for="item in items" :key="item.id" ref="listItems">
      {{ item.name }}
    </li>
  </ul>
</template>

注意事项

  • ref 数组顺序不一定与源数组完全一致
  • 元素卸载时,对应引用会从数组中移除
  • 可结合 key 提升引用匹配的稳定性

五、函数式模板引用

除了字符串命名,ref 还可绑定为函数,在元素挂载 / 更新 / 卸载时触发:

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

// 声明 ref 变量存储元素引用
const inputEl = ref(null)

// 函数式 ref 绑定
const setInputRef = (el) => {
  // 元素挂载时 el 为 DOM 实例,卸载时为 null
  inputEl.value = el
  
  if (el) {
    // 元素已挂载,初始化操作
    el.placeholder = '函数式 ref 绑定'
  }
}
</script>

<template>
  <!-- 使用 v-bind:ref 绑定函数 -->
  <input v-bind:ref="setInputRef" type="text" />
  
  <!-- 或使用内联函数 -->
  <input :ref="(el) => { if (el) el.focus() }" />
</template>

函数式 ref 的优势

  • 更灵活的引用赋值逻辑
  • 可在元素卸载时执行清理操作
  • 适合动态绑定场景(如条件渲染元素)

六、注意事项

6.1 避免滥用 ref

  • 优先使用声明式编程:Vue 的核心优势在于声明式渲染,应尽量通过数据驱动视图,而非直接操作 DOM
  • 组件通信首选 props/emit:父子组件交互优先使用标准接口,仅在必要时使用 ref 访问子组件
  • 第三方库集成场景:当需要操作库实例(如表单验证库、图表库)时,ref 是合理的选择

6.2 性能考虑

  • 批量操作 DOM:使用 nextTick 确保在 DOM 更新完成后执行批量操作
  • 避免频繁访问 ref:在循环或高频事件中,可先缓存 ref.value 以提升性能
  • v-for 中慎用函数式 ref:函数式 ref 在每次更新时都会被调用,可能影响性能

6.3 类型安全(TypeScript)

  • 显式标注类型:在复杂场景中,可通过泛型为 useTemplateRef 标注精确类型
  • 使用 defineRef 与 ref:确保 ref 变量的类型推导正确
  • 参考官方类型声明:查看 Vue 类型定义文件,了解内置类型的使用方式

七、总结

ref 作为 Vue 中操作底层元素的重要接口,在保持声明式编程优势的同时,为开发者提供了必要的命令式控制能力。从基础的 DOM 焦点设置,到复杂的子组件通信与第三方库集成,ref 的灵活用法贯穿于各类开发场景。

在实际项目中,建议遵循 "声明式优先,命令式保底" 的原则,合理使用 ref 并结合组合式 API 的其他特性(如 watchEffectonMounted),构建既保持 Vue 特性又满足特定需求的应用。

昨天以前首页

Vue响应式原理推导过程

作者 code二极管
2025年7月4日 17:43

Vue响应式原理推导过程

1. 最基础的响应式模型

最初的响应式模型非常简单,就是手动实现函数与对象的绑定关系:

/**
 * 阶段一:函数与对象的绑定
 *
 * 解释:一个或多个函数有该对象的属性,在对象属性发生变化时,重新调用函数,结果也发生变化
 *
 *
 * 缺点:
 *      1.函数需要在对象属性修改后重新手动调用
 *      2.对象属性依赖绑定,某一个属性发生变化,所有函数都要调用
 *      3.多对象依赖绑定,某一对象发生变化,所有对象依赖函数都要调用
 *      4.函数需要一个一个手动调用
 */

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

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

//初始调用
foo() //mike 18

//修改对象属性
obj.name='jack'

//重新调用
foo() //jack 18

存在的问题:

  • 需要手动调用函数
  • 对象属性发生变化时,所有函数都要手动重新调用
  • 多对象依赖混乱
  • 每个函数需要单独调用

优势:

  • 概念简单,容易理解
  • 实现方式直接明了
  • 不需要额外的监听机制,代码量少

2. 集中管理响应函数

改进方向是使用数组收集需要响应的函数,方便统一管理和调用:

/**
 * 阶段二:函数与对象的绑定
 *
 * 改变:1.新增收集函数
 *
 * 解释:一个或多个函数有该对象的属性,在对象属性发生变化时,重新调用函数,结果也发生变化
 *
 *
 * 缺点:
 *      1.函数需要在对象属性修改后重新手动调用
 *      2.对象属性依赖绑定,某一个属性发生变化,所有函数都要调用
 *      3.多对象依赖绑定,某一对象发生变化,所有对象依赖函数都要调用
 *
 * 优点:
 *      1.函数可以一起调用
 */

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


//需要响应式的函数都存入该数组里面
const reactiveFns=[]

//收集响应式函数
function watchFn(fn){
    reactiveFns.push(fn)
}

function foo(){
    console.log('first:'+obj.name)
    console.log('first:'+obj.age)
}

//调用收集函数(第一种调用方法,foo仍然可以手动调用)
watchFn(foo)

//调用收集函数(第二种调用方法,需使用数组才可手动调用)
watchFn(function(){
    console.log('second:'+obj.name)
    console.log('second:'+obj.age)
})

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

// first:mike
// first:18
// second:mike
// second:18


obj.name='jack'

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

// first:jack
// first18
// second:jack
// second:18

改进点:

  • 函数可以统一调用
  • 响应式函数集中管理

仍存在的问题:

  • 属性修改后需要手动触发函数调用
  • 不同对象的依赖函数混在一起

优势:

  • 统一管理所有需要响应的函数
  • 批量执行响应函数,减少重复代码
  • 可以动态添加新的响应函数

3. 使用Depend类管理依赖

为了更好地组织代码,引入Depend类来管理依赖关系:

/**
 * 阶段三:函数与对象的绑定
 *
 * 改变:
 *      1.新增收集函数
 *      2.新增类Depend
 *
 * 解释:一个或多个函数有该对象的属性,在对象属性发生变化时,重新调用函数,结果也发生变化
 *
 *
 * 缺点:
 *      1.函数需要在对象属性修改后重新手动调用
 *      2.对象属性依赖绑定,某一个属性发生变化,所有函数都要调用
 *
 *
 * 优点:
 *      1.函数可以一起调用
 *      2.对象可以分类管理
 */

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

    //收集依赖函数
    addDepend(fn){
        this.reactiveFns.push(fn)
    }

    //调用依赖函数
    notify(){
        this.reactiveFns.forEach(fn=>{
            fn()
        })
    }
}

// ========= 分类管理 obj =============

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

const dep=new Depend()

dep.addDepend(function(){
    console.log('second:'+obj.name)
    console.log('second:'+obj.age)
})

dep.notify()
// second:mike
// second:18

obj.name='jack'

dep.notify()
// second:jack
// second:18

改进点:

  • 对象依赖可以分类管理
  • 代码结构更加清晰

优势:

  • 面向对象的设计,更加结构化
  • 可以为不同对象创建独立的依赖管理
  • 封装了依赖收集和通知逻辑,使用更加灵活
  • 为后续扩展提供了基础设施

4. 使用Object.defineProperty实现自动响应

通过Object.defineProperty监听对象的属性变化,实现自动响应:

/**
 * 阶段四:函数与对象的绑定
 *
 * 改变:
 *      1.新增收集函数
 *      2.新增类Depend
 *      3.使用Object.defineProperty监听对象属性====>vue2
 *
 * 解释:一个或多个函数有该对象的属性,在对象属性发生变化时,重新调用函数,结果也发生变化
 *
 *
 * 缺点:
 *      1.对象属性依赖绑定,某一个属性发生变化,所有函数都要调用
 *
 *
 * 优点:
 *      1.函数可以一起调用
 *      2.对象可以分类管理
 *      3.属性被监听,属性修改时,函数自动调用
 */

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

    //收集依赖函数
    addDepend(fn){
        this.reactiveFns.push(fn)
    }

    //调用依赖函数
    notify(){
        this.reactiveFns.forEach(fn=>{
            fn()
        })
    }
}

// ========= 分类管理 obj =============

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

const dep=new Depend()

dep.addDepend(function(){
    console.log('second:'+obj.name)
    console.log('second:'+obj.age)
})

Object.keys(obj).forEach(key=>{
    let value=obj[key]

    Object.defineProperty(obj,key,{
        set:(newValue)=>{
            value=newValue
            dep.notify()
        },
        get:()=>{
            return value
        }
    })
})

dep.notify()
// second:mike
// second:18

obj.name='jack'//监听修改后调用dep.notify()
// second:jack
// second:18

改进点:

  • 属性被监听,修改时自动调用依赖函数
  • 无需手动调用notify()

优势:

  • 实现了真正的响应式,属性变化自动触发更新
  • 简化了使用流程,无需手动监听属性变化
  • 提供了更加透明的使用体验
  • 在底层实现变化监听,应用代码更加纯净

5. 完善依赖收集与精确响应

为每个对象的每个属性创建独立的依赖收集器,实现更精确的响应:

/**
 * 阶段五:函数与对象的绑定
 *
 * 改变:
 *      1.新增收集函数
 *      2.新增类Depend
 *      3.使用Object.defineProperty监听对象属性====>vue2
 *      4.新增map结构封装函数,给每个对象和每个属性设置对应的dep实例
 *
 * 解释:一个或多个函数有该对象的属性,在对象属性发生变化时,重新调用函数,结果也发生变化
 *
 *
 *
 *
 * 优点:
 *      1.函数可以一起调用
 *      2.对象可以分类管理
 *      3.属性被监听,属性修改时,函数自动调用
 *      4.对象属性之间减少依赖关系,独立调用对应函数
 */

class Depend{
    constructor(){
        this.reactiveFns=new Set() //避免重复添加依赖函数
    }

    //收集依赖函数
    addDepend(fn){
        this.reactiveFns.add(fn)
    }

    //调用依赖函数
    notify(){
        this.reactiveFns.forEach(fn=>{
            fn()
        })
    }
}

//存储响应式函数
let reactiveFn=null

function watchFn(fn){
    reactiveFn=fn
    fn() //激活监听响应
}

//对象map源头,弱引用weakMap
const objMap=new WeakMap()

//map结构的封装函数(分配dep实例)
function getDepend(obj,key){
    //1.根据源头map,找到obj对应的map
    let map=objMap.get(obj)
    //若map不存在
    if(!map){
        map=new Map()
        objMap.set(obj,map)
    }

    //2.根据obj的map对象,找到key对应的depend对象(有一个选择,如果key也是一个对象,是否需要深层响应)
    let dep=map.get(key)
    //若dep不存在
    if(!dep){
        dep=new Depend()
        map.set(key,dep)
    }

    return dep
}


//添加响应式函数
function reactive(obj){
    Object.keys(obj).forEach(key=>{
    let value=obj[key]

    Object.defineProperty(obj,key,{
        set:(newValue)=>{
            value=newValue
            const dep=getDepend(obj,key)
            dep.notify()
        },
        get:()=>{
            const dep=getDepend(obj,key)
            //防止重复添加依赖函数
            dep.addDepend(reactiveFn)
            return value
        }
    })
})
    return obj
}

改进点:

  • 对象属性与依赖函数的关系更加精确
  • 只触发依赖特定属性的函数
  • 自动收集依赖关系

优势:

  • 细粒度的依赖收集,只有真正依赖某属性的函数才会被触发
  • 使用WeakMap避免内存泄漏问题
  • 使用Set避免重复添加依赖函数
  • 自动在属性获取时收集依赖,无需手动指定依赖关系
  • 提供了完整的reactive函数,封装了响应式逻辑

6. 使用Proxy实现全面的响应式

最终使用ES6的Proxy替代Object.defineProperty,实现更强大的响应式系统:

/**
 * 阶段六:函数与对象的绑定
 *
 * 改变:
 *      1.新增收集函数
 *      2.新增类Depend
 *      3.使用Proxy代理对象属性====>vue3
 *      4.新增map结构封装函数,给每个对象和每个属性设置对应的dep实例
 *
 * 解释:一个或多个函数有该对象的属性,在对象属性发生变化时,重新调用函数,结果也发生变化
 *
 *
 *
 *
 * 优点:
 *      1.函数可以一起调用
 *      2.对象可以分类管理
 *      3.属性被监听,属性修改时,函数自动调用
 *      4.对象属性之间减少依赖关系,独立调用对应函数
 */

//添加响应式函数
function reactive(obj){
    const objProxy=new Proxy(obj,{
        set(target,key,newValue,receiver){
            Reflect.set(target,key,newValue,receiver)
            const dep=getDepend(target,key)
            dep.notify()
        },
        get(target,key,receiver){
            const dep=getDepend(target,key)
            dep.addDepend(reactiveFn)
            return Reflect.get(target,key,receiver)
        }
    })
    return objProxy
}

Proxy的优势:

  1. 可以监听动态添加的属性
  2. 可以监听数组的变化
  3. 可以监听更多种类的操作(不仅限于get/set)
  4. 性能更好,不需要递归遍历对象的所有属性
  5. 返回的是一个新对象,不会修改原始对象
  6. 可以拦截更多的操作,如删除属性、检查属性是否存在等

总结

JavaScript响应式原理的演进经历了以下几个阶段:

  1. 最基础的手动绑定与调用
  2. 集中管理响应函数
  3. 使用Depend类组织依赖关系
  4. Object.defineProperty实现自动响应
  5. 完善依赖收集与精确响应
  6. 使用Proxy实现全面的响应式系统
❌
❌