阅读视图

发现新文章,点击刷新页面。

Vue 任务清单开发:数据驱动 vs 传统 DOM 操作

Vue 任务清单开发:数据驱动 vs 传统 DOM 操作

在前端开发中,任务清单是一个常见的案例,通过这个案例我们可以清晰对比传统 DOM 操作与 Vue 数据驱动开发的差异。本文将结合具体代码,解析 Vue 的核心思想和常用 API。

传统开发方式的局限

传统 JavaScript 开发中,我们需要手动操作 DOM 元素来实现功能。以下代码为例:

<h2 id="app"></h2>
<input type="text" id="todo-input">
<script>
    // 传统方式需要先获取DOM元素
    const app = document.getElementById('app');
    const todoInput = document.getElementById('todo-input');
    
    // 手动绑定事件并操作DOM
    todoInput.addEventListener('change',function(event) {
        const todo = event.target.value.trim();
        if(!todo){
            console.log('请输入任务');
            return ;
        }else{
            // 直接修改DOM内容
            app.innerHTML = todo;
        }
    })
</script>

这种方式的特点是:

  • 需要手动获取 DOM 元素
  • 命令式地操作 DOM 进行更新
  • 业务逻辑与 DOM 操作混杂
  • 随着功能复杂,代码会变得难以维护

Vue 的数据驱动开发理念

Vue 采用了完全不同的思路:开发者只需关注数据本身,而非 DOM 操作。以任务清单为例:

<template>
  <div>
    <!-- 数据绑定 -->
    <h2>{{ title }}</h2>
    <!-- 双向数据绑定 -->
    <input type="text" v-model="title" @keydown.enter="addTodo">
    
    <!-- 条件渲染 -->
    <ul v-if="todos.length">
      <!-- 循环渲染 -->
      <li v-for="todo in todos" :key="todo.id">
        <input type="checkbox" v-model="todo.done">
        <span :class="{done: todo.done}">{{ todo.title }}</span>
      </li>
    </ul>
    <div v-else>暂无计划</div>
  </div>
</template>

Vue 的核心思想是:不再关心页面元素如何操作,只关注数据如何变化。当数据发生改变时,Vue 会自动更新 DOM,开发者无需手动操作。

Vue 常用 API 解析

  1. v-model 双向数据绑定

    <input type="text" v-model="title">
    

    实现表单输入与数据的双向绑定,输入框的变化会自动更新数据,数据的变化也会自动反映到输入框。

  2. v-for 循环渲染

    <li v-for="todo in todos" :key="todo.id">
    

    基于数组渲染列表,:key用于标识每个元素的唯一性,提高渲染性能。

  3. v-if/v-else 条件渲染

    <ul v-if="todos.length">
      ...
    </ul>
    <div v-else>暂无计划</div>
    

    根据条件动态渲染不同的内容,当todos数组为空时显示 "暂无计划"。

  4. :class 动态类绑定

    <span :class="{done: todo.done}">{{ todo.title }}</span>
    

    todo.donetrue时,自动为元素添加done类,实现完成状态的样式变化。

  5. @事件监听

    <input type="text" @keydown.enter="addTodo">
    

    监听键盘回车事件,触发addTodo方法,@v-bind:的缩写。

  6. computed 计算属性

    // 计算未完成的任务数量
    const active = computed(() => {
      return todos.value.filter(todo => !todo.done).length
    })
    
    // 全选功能的实现
    const allDone = computed({
      get(){
        return todos.value.every(todo => todo.done)
      },
      set(val){
        todos.value.forEach(todo => todo.done = val)
      }
    })
    

    计算属性具有缓存特性,只有依赖的数据变化时才会重新计算,相比方法调用更节省性能。全选功能展示了计算属性的高级用法,通过getset实现双向绑定。

  7. ref 响应式数据

    import { ref } from 'vue'
    const title = ref("");
    const todos = ref([...])
    

    创建响应式数据,当这些数据变化时,Vue 会自动更新相关的 DOM。

总结

Vue 通过数据驱动的方式,极大简化了前端开发流程:

  • 开发者可以专注于业务逻辑和数据处理
  • 减少了大量手动 DOM 操作的代码
  • 提供了简洁直观的 API,降低学习成本
  • 内置的性能优化(如计算属性缓存)让应用运行更高效

从后端模板到响应式驱动:界面开发的演进之路

从后端模板到响应式驱动:界面开发的演进之路

在 Web 开发的发展历程中,界面与数据的交互方式经历了多次重要变革。从早期的后端模板渲染到如今的响应式数据驱动,每一次演进都深刻影响着开发模式和用户体验。

一、纯后端套模板时代:MVC 模式的兴起

早期的 Web 开发普遍采用 MVC(Model-View-Controller)开发模式,这种模式将应用程序分为三个核心部分:

  • Model(模型) :负责数据管理,通常与数据库交互例如通过 MySQL 等数据库进行数据抽象存储
  • View(视图) :负责数据展示,以 HTML 模板为载体
  • Controller(控制器) :处理业务逻辑,协调模型和视图

这一时期的后端代码通常直接处理 HTTP 请求并渲染模板,典型的 Node.js 示例如下:

// 简化的后端MVC示例
const http = require('http');
const mysql = require('mysql');

// 模型:数据库连接
const db = mysql.createConnection({
  host: 'localhost',
  user: 'root',
  password: '',
  database: 'todoapp'
});

// 控制器:处理业务逻辑
const getTodos = (req, res) => {
  db.query('SELECT * FROM todos', (err, results) => {
    if (err) throw err;
    // 渲染视图(模板)
    const html = `
      
        
          <ul>
            ${results.map(todo => `<li>${todo.content}</li>`).join('')}
          </ul>
        
      
    `;
    res.end(html);
  });
};

// HTTP服务
http.createServer((req, res) => {
  if (req.url === '/todos') {
    getTodos(req, res);
  } else {
    res.end('Hello World');
  }
}).listen(3000);

在这种模式下,HTML 的静态部分和动态数据部分混合在一起,动态内容通过模板语法(如{{todos}})由后端数据驱动生成。

二、前后端分离:开发职责的解耦

随着 Web 应用复杂度提升,前后端分离架构逐渐成为主流。这种模式将应用拆分为两个独立部分:

  • 前端:专注于 HTML、CSS 和 JavaScript 实现,通过 Ajax 或 Fetch 主动从后端拉取数据
// 前端获取数据示例
fetch('http://localhost:3000/users')
.then(response => response.json())
.then(users => {
  // 处理并展示数据
  const userList = document.getElementById('user-list');
  userList.innerHTML = users.map(user => `<li>${user.name}</li>`).join('');
});

后端:不再返回完整 HTML,而是提供纯粹的数据接口(API)

// 后端API示例
http.createServer((req, res) => {
  if (req.url === '/users') {
    res.setHeader('Content-Type', 'application/json');
    res.end(JSON.stringify([
      { id: 1, name: '张三' },
      { id: 2, name: '李四' }
    ]));
  }
}).listen(3000);

前后端分离带来了显著优势:

  • 前端开发者可专注于数据展示和用户体验
  • 后端开发者可专注于数据处理和系统性能
  • 双方可并行开发,通过 API 契约协作

三、Vue 响应式:数据驱动的革命

Vue 框架的出现彻底改变了前端开发模式,其核心是响应式数据驱动

1. 响应式数据(ref 实现)

Vue 通过ref将普通数据包装为响应式对象:

import { ref } from 'vue';

// 创建响应式数据
const message = ref('Hello Vue');
const todos = ref([
  { id: 1, content: '学习响应式' },
  { id: 2, content: '使用v-for' }
]);

// 修改数据(自动触发界面更新)
message.value = 'Hello Reactive';
todos.value.push({ id: 3, content: '掌握Vue' });

2. 模板语法(数据与界面绑定)

Vue 模板通过{{}}和指令实现数据与界面的自动关联:


  <div>
    
    <p>{{ message }}</p>
    
    
    <ul>
      <li>
        {{ todo.content }}
      </li>
    </ul>
  </div>

3. 完整 Vue 组件示例


  <div>
    <h2>{{ title }}</h2>
    <ul>
      <li>{{ item.name }}</li>
    </ul>
    添加项
  </div>



import { ref } from 'vue';

// 响应式数据
const title = ref('Vue响应式示例');
const list = ref([
  { id: 1, name: '第一项' },
  { id: 2, name: '第二项' }
]);

// 业务逻辑(只操作数据)
const addItem = () => {
  list.value.push({
    id: Date.now(),
    name: `新项${list.value.length + 1}`
  });
};

在 Vue 的响应式体系中,开发者只需关注数据变化,界面更新完全由框架自动处理。这种模式继承了后端模板 "数据驱动界面" 的思想,但通过前端响应式系统实现了更高效、更灵活的开发体验,彻底摆脱了手动 DOM 操作的困扰。

从后端模板到 Vue 响应式,界面开发的演进始终围绕一个核心:让开发者聚焦业务逻辑,而非界面更新细节。Vue 的响应式系统正是这一理念的完美实践。

JavaScript 内存机制与闭包解析

JavaScript 内存机制与闭包解析

JavaScript 作为一门动态弱类型语言,其内存管理机制和闭包特性是理解 JS 运行原理的核心。本文将结合具体代码示例,深入解析 JS 的内存机制以及闭包的工作原理。

一、JavaScript 内存空间划分

JavaScript 的内存空间主要分为三大类:

  1. 代码空间:用于存储执行的代码
  2. 栈内存:主要存储简单数据类型和引用类型的地址
  3. 堆内存:主要存储复杂数据类型(对象等)

2bad8a6ec6a17f7a384612ea3e0cbdcd.png 栈内存的特点是操作速度快、大小固定、存储连续,适合存储体积小的数据;而堆内存空间大,可存储大型复杂对象,但分配和回收内存相对耗时。

二、数据类型的内存存储

1. 简单数据类型的存储

简单数据类型(如数字、字符串、布尔值等)直接存储在栈内存中,赋值时会进行值拷贝:

// 调用栈在栈内存中
// 体积小
function foo() {
    var a = 1; // 赋值,直接存储在栈内存
    var b = a; // 拷贝,在栈内存中创建新值
    a = 2;
    console.log(a); // 2
    console.log(b); // 1
}
foo();

上述代码中,ab是相互独立的变量,修改a的值不会影响b,因为它们在栈内存中占据不同的空间。

2. 复杂数据类型的存储

复杂数据类型(如对象)在栈内存中存储的是指向堆内存的地址,赋值时进行的是引用拷贝:

function foo() {
    var a = {name:"极客时间"}; // 栈内存存储地址,堆内存存储对象
    var b = a; // 引用式拷贝,ba指向堆中同一个对象
    a.name = "极客邦";
    console.log(a); // {name: "极客邦"}
    console.log(b); // {name: "极客邦"}
}
foo();

这里ab在栈内存中存储的是同一个地址,指向堆内存中的同一个对象,因此修改a的属性会影响b

三、JavaScript 的动态弱类型特性

JS 作为动态弱类型语言,变量的类型可以在运行过程中动态改变,不需要预先声明类型:

// JS 是动态弱类型语言
var bar; 
console.log(typeof bar); // undefined
bar = 12; // 变为number类型
console.log(typeof bar); // number
bar = '极客时间'; // 变为string类型
console.log(typeof bar); // string
bar = true; // 变为boolean类型
console.log(typeof bar); // boolean
bar = null;
console.log(typeof bar);  // Object (JS设计的bug)
bar = {name:'极客时间'}  // 变为Object类型
console.log(typeof bar); // Object

四、执行上下文与调用栈

JavaScript 引擎通过调用栈来管理执行上下文,每个函数执行时都会创建一个执行上下文,包含:

  • 变量环境
  • 词法环境
  • outer(词法作用域链)
  • this

lQLPJw1WF0yif-fNAhTNBHawwsioxubOAzAJEmTtM6BTAA_1142_532.png 执行上下文的切换通过调整调用栈的栈顶指针来实现,这也是栈内存需要高效操作的原因。

五、闭包的内存机制

闭包是指内部函数可以访问外部函数作用域中变量的特性,其实现依赖于特殊的内存管理机制。

闭包示例解析

function foo() {
    var myName = "极客时间"
    let test1 = 1
    const test2 = 2
    var innerBar = { 
        setName:function(newName){
            myName = newName
        },
        getName:function(){
            console.log(test1)
            return myName
        }
    }
    return innerBar
}
var bar = foo()
bar.setName("极客邦")
bar.getName() // 输出1
console.log(bar.getName()) // 输出1和"极客邦"

闭包的内存工作原理

  1. 编译阶段:JS 引擎编译foo函数时,会扫描内部函数(setNamegetName
  2. 识别闭包:发现内部函数引用了外部函数的变量(myNametest1),判断形成闭包
  3. 创建闭包对象:在堆内存中创建一个closure(foo)对象,用于保存被内部函数引用的外部变量
  4. 维持引用:当foo函数执行完毕后,虽然其执行上下文出栈,但由于bar仍引用着内部函数,而内部函数又引用着closure(foo),所以这些变量不会被回收
  5. 访问变量:内部函数通过引用closure(foo)来访问外部函数的变量

lQLPKGXzE8qWf4fNAjTNBHawul-TTHmpmmMJEm9r57TIAA_1142_564.png

六、内存管理的特点

  • JavaScript 不需要手动管理内存(不同于 C/C++ 需要使用mallocfree
  • 栈内存的回收通过调整栈指针实现,效率高
  • 堆内存的回收通过垃圾回收机制实现,当对象没有任何引用时会被回收
  • 闭包会延长变量的生命周期,可能导致内存占用增加,需要合理使用
❌