vue3中v-model的原理示例详解

 更新时间:2025年04月29日 09:50:27   作者:flying robot  
原生 JavaScript 实现,模拟 Vue 3 的 v-model 功能,包括表单元素和组件支持,我们将逐步构建代码,并提供使用示例,对vue v-model原理解析感兴趣的朋友一起看看吧

模拟 Vue 3 的完整 v-model 功能,我们需要实现以下特性:**

  • 表单元素的双向绑定:支持 、、 等表单元素,包括修饰符(如 .lazy、.number、.trim)。
  • 自定义组件的双向绑定:支持 v-model 在组件上的使用,基于 modelValue 和 update:modelValue,并支持多重 v-model(如 v-model:name)。
  • 自定义修饰符:支持组件上的自定义修饰符(如 v-model.capitalize)。
  • 响应式系统:实现一个简化的响应式系统,模拟 Vue 3 的 ref 或 Proxy 行为,确保数据变化触发视图更新。

原生 JavaScript 实现,模拟 Vue 3 的 v-model 功能,包括表单元素和组件支持。我们将逐步构建代码,并提供使用示例。**

实现步骤

1. 响应式系统(模拟 ref)

我们需要一个简单的响应式系统,类似于 Vue 3 的 ref,用于创建响应式数据并在数据变化时通知依赖。

javascript

// 定义 Dep 对象,用于依赖收集
let Dep = {
 target: null // 当前依赖的回调函数
};
// 模拟 Vue 3 的 ref
function ref(initialValue) {
  const deps = new Set();
  let _value = initialValue;
  return {
    get value() {
      // 依赖收集
      if (typeof Dep.target === 'function') {
        deps.add(Dep.target);
      }
      return _value;
    },
    set value(newValue) {
      if (_value !== newValue) {
        _value = newValue;
        // 通知依赖
        deps.forEach((dep) => dep());
      }
    }
  };
}
// 依赖收集的全局变量
Dep.target = null;
// 模拟 watchEffect,用于依赖收集
function watchEffect(callback) {
  Dep.target = callback;
  callback();
  Dep.target = null;
}
  • ref 创建响应式数据,getter 收集依赖,setter 通知依赖更新。
  • watchEffect 模拟 Vue 的依赖收集机制,执行回调并收集依赖。

2. 表单元素的 v-model

表单元素的 v-model:value@input(或 @change)的语法糖。我们需要:

  • 绑定响应式数据到元素的 value(或 checked)。
  • 监听输入事件,更新响应式数据。
  • 支持修饰符(如 .lazy.number**、.trim)。**

javascript

// 处理修饰符逻辑
function applyModifiers(value, modifiers) {
  let result = value;
  if (modifiers.number) {
    result = isNaN(Number(result)) ? result : Number(result);
  }
  if (modifiers.trim && typeof result === 'string') {
    result = result.trim();
  }
  return result;
}
// 表单元素的 v-model
function vModelForm(element, reactiveRef, modifiers = {}) {
  const tag = element.tagName.toLowerCase();
  const type = element.type;
  // 确定事件类型
  const eventName = modifiers.lazy
    ? 'change'
    : tag === 'select' || type === 'checkbox' || type === 'radio'
      ? 'change'
      : 'input';
  // 更新视图
  const updateView = () => {
    if (type === 'checkbox') {
      element.checked = reactiveRef.value;
    } else if (type === 'radio') {
      element.checked = reactiveRef.value === element.value;
    } else {
      element.value = reactiveRef.value;
    }
  };
  // 初始绑定和依赖收集
  Dep.target = updateView;
  updateView();
  Dep.target = null;
  // 监听输入事件
  element.addEventListener(eventName, (event) => {
    let newValue;
    if (type === 'checkbox') {
      newValue = event.target.checked;
    } else if (type === 'radio') {
      if (event.target.checked) {
        newValue = event.target.value;
      } else {
        return; // 未选中时不更新
      }
    } else {
      newValue = event.target.value;
    }
    // 应用修饰符
    newValue = applyModifiers(newValue, modifiers);
    reactiveRef.value = newValue;
  });
  // 数据变化时更新视图
  reactiveRef.deps = reactiveRef.deps || new Set();
  reactiveRef.deps.add(updateView);
}
  • 修饰符处理:applyModifiers 处理 .number 和 .trim 修饰符。
  • 事件选择:根据元素类型(checkbox、radio、select)或修饰符(.lazy)选择 input 或 change 事件。
  • 视图更新:根据元素类型更新 value 或 checked。
  • 事件监听:根据元素类型获取用户输入的值,并应用修饰符后更新响应式数据。

3. 自定义组件的 v-model

组件的 v-model 基于 modelValueupdate:modelValue,支持多重 v-model 和自定义修饰符。我们模拟组件为一个简单的 DOM 结构,包含子组件逻辑。

javascript

// 模拟组件的 v-model
function vModelComponent(element, reactiveRef, propName = 'modelValue', modifiers = {}) {
  // 模拟组件的 props 和 emit
  const props = { [propName]: reactiveRef.value };
  const emit = (eventName, value) => {
    if (eventName === `update:${propName}`) {
      // 应用修饰符
      let newValue = value;
      if (modifiers.capitalize && typeof newValue === 'string') {
        newValue = newValue.charAt(0).toUpperCase() + newValue.slice(1);
      }
      reactiveRef.value = newValue;
    }
  };
  // 更新视图
  const updateView = () => {
    element.value = reactiveRef.value; // 模拟组件内部 input 的 value
  };
  // 初始绑定
  Dep.target = updateView;
  updateView();
  Dep.target = null;
  // 模拟组件内部的 input 事件
  element.addEventListener('input', (event) => {
    emit(`update:${propName}`, event.target.value);
  });
  // 数据变化时更新视图
  reactiveRef.deps = reactiveRef.deps || new Set();
  reactiveRef.deps.add(updateView);
}
  • props 和 emit:模拟组件的 props(如 modelValue)和 $emit(如 update:modelValue)。
  • 多重 v-model:通过 propName 支持自定义属性(如 nameage)。
  • 修饰符:支持自定义修饰符(如 .capitalize),在 emit 时处理。
  • 视图更新:模拟组件内部的 ,绑定 value 并监听 input 事件。

4. 统一的 v-model 接口

创建一个统一的 vModel 函数,根据上下文(表单元素或组件)调用不同的实现。

javascript

function vModel(element, reactiveRef, options = {}) {
  const { modifiers = {}, propName } = options;
  const isComponent = propName || element.tagName.toLowerCase() === 'custom-component';
  if (isComponent) {
    vModelComponent(element, reactiveRef, propName || 'modelValue', modifiers);
  } else {
    vModelForm(element, reactiveRef, modifiers);
  }
}
  • 选项modifiers 指定修饰符,propName 指定组件的绑定属性。
  • 上下文判断:根据 propName 或元素标签判断是表单元素还是组件。

5. 完整代码与使用示例

javascript

// 定义 Dep 对象,用于依赖收集
const Dep = {
  target: null, // 当前依赖的回调函数
};
// 模拟 Vue 3 的 ref
function ref(initialValue) {
  const deps = new Set();
  let _value = initialValue;
  return {
    get value() {
      // 依赖收集
      if (typeof Dep.target === 'function') {
        deps.add(Dep.target);
      }
      return _value;
    },
    set value(newValue) {
      if (_value !== newValue) {
        _value = newValue;
        // 通知依赖
        deps.forEach((dep) => dep());
      }
    },
  };
}
// 模拟 watchEffect,用于依赖收集
function watchEffect(callback) {
  Dep.target = callback;
  callback();
  Dep.target = null;
}
// 处理修饰符逻辑
function applyModifiers(value, modifiers) {
  let result = value;
  if (modifiers.number) {
    result = isNaN(Number(result)) ? result : Number(result);
  }
  if (modifiers.trim && typeof result === 'string') {
    result = result.trim();
  }
  return result;
}
// 表单元素的 v-model
function vModelForm(element, reactiveRef, modifiers = {}) {
  const tag = element.tagName.toLowerCase();
  const type = element.type;
  const eventName = modifiers.lazy
    ? 'change'
    : tag === 'select' || type === 'checkbox' || type === 'radio'
      ? 'change'
      : 'input';
  const updateView = () => {
    if (type === 'checkbox') {
      element.checked = reactiveRef.value;
    } else if (type === 'radio') {
      element.checked = reactiveRef.value === element.value;
    } else {
      element.value = reactiveRef.value;
    }
  };
  Dep.target = updateView;
  updateView();
  Dep.target = null;
  element.addEventListener(eventName, (event) => {
    let newValue;
    if (type === 'checkbox') {
      newValue = event.target.checked;
    } else if (type === 'radio') {
      if (event.target.checked) {
        newValue = event.target.value;
      } else {
        return;
      }
    } else {
      newValue = event.target.value;
    }
    newValue = applyModifiers(newValue, modifiers);
    reactiveRef.value = newValue;
  });
  reactiveRef.deps = reactiveRef.deps || new Set();
  reactiveRef.deps.add(updateView);
}
// 组件的 v-model
function vModelComponent(
  element,
  reactiveRef,
  propName = 'modelValue',
  modifiers = {},
) {
  const props = { [propName]: reactiveRef.value };
  const emit = (eventName, value) => {
    if (eventName === `update:${propName}`) {
      let newValue = value;
      if (modifiers.capitalize && typeof newValue === 'string') {
        newValue = newValue.charAt(0).toUpperCase() + newValue.slice(1);
      }
      reactiveRef.value = newValue;
    }
  };
  const updateView = () => {
    element.value = reactiveRef.value;
  };
  Dep.target = updateView;
  updateView();
  Dep.target = null;
  element.addEventListener('input', (event) => {
    emit(`update:${propName}`, event.target.value);
  });
  reactiveRef.deps = reactiveRef.deps || new Set();
  reactiveRef.deps.add(updateView);
}
// 统一 v-model 接口
function vModel(element, reactiveRef, options = {}) {
  const { modifiers = {}, propName } = options;
  const isComponent =
    propName || element.tagName.toLowerCase() === 'custom-component';
  if (isComponent) {
    vModelComponent(element, reactiveRef, propName || 'modelValue', modifiers);
  } else {
    vModelForm(element, reactiveRef, modifiers);
  }
}
// 使用示例
// 创建 DOM 元素
const inputText = document.createElement('input');
inputText.placeholder = '输入文本';
const inputNumber = document.createElement('input');
inputNumber.type = 'number';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
const select = document.createElement('select');
select.innerHTML = `
  <option value="option1">选项 1</option>
  <option value="option2">选项 2</option>
 `;
const customComponent = document.createElement('input'); // 模拟组件
customComponent.placeholder = '自定义组件输入';
// 添加到页面
document.body.appendChild(inputText);
document.body.appendChild(document.createElement('br'));
document.body.appendChild(inputNumber);
document.body.appendChild(document.createElement('br'));
document.body.appendChild(checkbox);
document.body.appendChild(document.createElement('br'));
document.body.appendChild(select);
document.body.appendChild(document.createElement('br'));
document.body.appendChild(customComponent);
// 创建响应式数据
const message = ref('Hello');
const number = ref(42);
const checked = ref(false);
const selected = ref('option1');
const componentName = ref('alice');
const componentAge = ref(25);
// 绑定 v-model
vModel(inputText, message, { modifiers: { trim: true } });
vModel(inputNumber, number, { modifiers: { number: true } });
vModel(checkbox, checked);
vModel(select, selected);
vModel(customComponent, componentName, {
  propName: 'name',
  modifiers: { capitalize: true },
});
// 模拟多重 v-model
const componentAgeInput = document.createElement('input');
componentAgeInput.type = 'number';
document.body.appendChild(document.createElement('br'));
document.body.appendChild(componentAgeInput);
vModel(componentAgeInput, componentAge, {
  propName: 'age',
  modifiers: { number: true },
});
// 打印数据变化
watchEffect(() => {
  console.log('message:', message.value);
  console.log('number:', number.value);
  console.log('checked:', checked.value);
  console.log('selected:', selected.value);
  console.log('componentName:', componentName.value);
  console.log('componentAge:', componentAge.value);
});
// 程序化更新数据
setTimeout(() => {
  message.value = 'Updated Text';
  componentName.value = 'bob';
}, 2000);

执行流程图

初始化:
  定义 Dep, ref, vModel 等
  创建 DOM 元素 -> 添加到页面
  创建 ref 数据 (message, number, ...)
  vModel 绑定:
    -> vModelForm 或 vModelComponent
       -> 确定事件 (input/change)
       -> 定义 updateView
       -> 初始更新 DOM (触发 getter, 收集 updateView)
       -> 绑定事件监听
       -> 添加 updateView 到 deps
  watchEffect:
    -> 打印初始值 (触发 getter, 收集 watchEffect)
  设置定时器
用户交互 (例如输入 " Hello Vue "):
  input 事件 -> 获取值 " Hello Vue "
  applyModifiers (.trim) -> "Hello Vue"
  message.value = "Hello Vue" -> setter
    -> 更新 _value
    -> 通知 deps:
       -> updateView: inputText.value = "Hello Vue"
       -> watchEffect: 打印所有数据
程序化更新 (2秒后):
  message.value = 'Updated Text' -> setter
    -> 更新 _value
    -> 通知 deps:
       -> updateView: inputText.value = 'Updated Text'
       -> watchEffect: 打印数据
  componentName.value = 'bob' -> setter
    -> 更新 _value
    -> 通知 deps:
       -> updateView: customComponent.value = 'bob'
       -> watchEffect: 打印数据

到此这篇关于vue3中v-model的原理示例的文章就介绍到这了,更多相关vue v-model原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 详解vue-cli快速构建项目以及引入bootstrap、jq

    详解vue-cli快速构建项目以及引入bootstrap、jq

    本篇文章主要介绍了vue-cli快速构建项目以及引入bootstrap、jq,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-05-05
  • 关于element-ui中el-form自定义验证(调用后端接口)

    关于element-ui中el-form自定义验证(调用后端接口)

    这篇文章主要介绍了关于element-ui中el-form自定义验证(调用后端接口),具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-07-07
  • Vuex中actions优雅处理接口请求的方法

    Vuex中actions优雅处理接口请求的方法

    在项目开发中,如果使用到了 vuex,通常我会将所有的接口请求单独用一个文件管理,这篇文章主要介绍了Vuex中actions如何优雅处理接口请求,业务逻辑写在 actions 中,本文给大家分享完整流程需要的朋友可以参考下
    2022-11-11
  • vue将时间戳转换成自定义时间格式的方法

    vue将时间戳转换成自定义时间格式的方法

    下面小编就为大家分享一篇vue将时间戳转换成自定义时间格式的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-03-03
  • 手把手教你使用Vue实现弹窗效果

    手把手教你使用Vue实现弹窗效果

    在vue中弹窗是常用的组件之一,可以用来展示警告、成功提示和错误信息等内容,这篇文章主要给大家介绍了关于如何使用Vue实现弹窗效果的相关资料,需要的朋友可以参考下
    2024-02-02
  • vue element-ui之怎么封装一个自己的组件的详解

    vue element-ui之怎么封装一个自己的组件的详解

    这篇文章主要介绍了vue element-ui之怎么封装一个自己的组件,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-05-05
  • 在Vue+Ts+Vite项目中配置别名指向不同的目录并引用的案例详解

    在Vue+Ts+Vite项目中配置别名指向不同的目录并引用的案例详解

    这篇文章主要介绍了在Vue+Ts+Vite项目中配置别名指向不同的目录并引用,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2024-01-01
  • Vue 组件组织结构及组件注册详情

    Vue 组件组织结构及组件注册详情

    这篇文章主要介绍的是Vue 组件组织结构及组件注册,为了能在模板中使用,这些组件必须先注册以便 Vue 能够识别。这里有两种组件的注册类型:全局注册和局部注册。至此,我们的组件都只是通过 Vue.component 全局注册的,文章学详细内容,需要的朋友可以参考一下
    2021-10-10
  • vue中如何去掉input前后的空格

    vue中如何去掉input前后的空格

    这篇文章主要介绍了vue中如何去掉input前后的空格问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-03-03
  • Vue2和Vue3中常用组件通信用法分享

    Vue2和Vue3中常用组件通信用法分享

    这篇文章主要为大家整理了Vue3的8种和Vue2的12种组件通信的使用方法,文中的示例代码讲解详细,对我们学习Vue有一定的帮助,值得收藏
    2023-04-04

最新评论