Vue 的哲学和它所代表的现代前端开发方式确实与早期的“刀耕火种”时代形成了鲜明对比。让我们来进一步探讨 Vue 的优势、为什么它比 jQuery 更适合现代应用开发,以及如何用 Vue 实现一个简单的待办事项(Todos)应用。
前端刀耕火种的时代
在早期,前端开发主要依赖于直接操作 DOM 和事件机制。开发者需要手动管理页面上的每个元素,并且每当数据发生变化时,都需要通过 JavaScript 更新对应的 DOM 节点。这种做法不仅复杂,而且性能低下,因为频繁访问和修改 DOM 会触发浏览器进行回流(reflow)和重绘(repaint),这两个过程都是资源密集型的操作。
频繁访问 DOM 性能差的原因: 回流(Reflow):当 DOM 树结构发生变化时,浏览器需要重新计算所有元素的位置和几何属性,这涉及到整个文档树的重新布局。
重绘(Repaint):回流后通常伴随着重绘,即重新绘制受影响的区域。即使只是样式变化(如颜色改变),也会触发重绘,这是一个耗资源的过程。 JavaScript 引擎和渲染引擎独立运作:JavaScript 引擎(如 V8)负责执行脚本,而渲染引擎处理 HTML 和 CSS 的解析与呈现。两者之间频繁交互会导致性能瓶颈,尤其是在单线程环境下。
如果是早期的前端开发我们来看看我们的前端切图仔是怎么把数据动态渲染在页面的
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h2 id="app"></h2>
<input type="text" id="todo-input">
<script>
var app = document.getElementById('app')
var todoInput = document.getElementById('todo-input')
todoInput.addEventListener('keydown', function(event) {
app.innerHTML = event.target.value
app.innerHTML = todoInput.value
app.innerHTML = this.value//这里我们来展示三种挂载数据的api,这里的二this指向事件对象
})
</script>
</body>
</html>
抛开代码的可读性不谈,单纯大量从浏览器的dom接口获取dom节点也算是性能的浪费,若是业务繁琐,逻辑复杂的情况下,大量的dom操作会极大的影响前端的运作性能,那为什么js get到dom节点会浪费性能呢,我们下面谈谈
- 为了更好地理解 JavaScript 引擎和渲染引擎在单线程环境下的交互以及可能引发的性能瓶颈,我们可以使用一个日常生活中的类比:想象你走进一家只有单一服务员(代表单线程)的餐厅。这位服务员不仅负责接收订单(相当于 JavaScript 引擎执行脚本逻辑、处理事件监听器等),还要确保根据菜单(HTML 和 CSS 文件)准备的食物(页面内容)能够按时上桌(渲染引擎解析并呈现页面)。当一位顾客下单时(用户与页面互动触发 JavaScript 代码执行),服务员需要停下手中的其他工作来记录这个新订单(执行 JavaScript 脚本)。如果这个订单非常复杂或者有特别的要求(长时间运行的 JavaScript 函数),服务员可能会花费很长时间来处理它,在此期间,其他想要点餐或询问问题的顾客只能等待(用户的其他操作被阻塞),即使厨师已经准备好了一些菜品(渲染引擎完成了部分布局计算或绘制),服务员也无暇顾及把这些菜端出去(更新屏幕上的内容),因为他还忙于处理复杂的订单。
例如,在网页上点击按钮触发了一个需要执行大量计算的 JavaScript 函数(如排序大数组或处理复杂动画逻辑)时,由于 JavaScript 是单线程的,在这段代码执行期间,浏览器无法响应用户的其他输入(比如滚动页面或点击其他按钮),渲染引擎也不能更新页面,即使它已经准备好了一些新的内容(比如图片下载完毕或某些元素位置已计算好),其他异步任务(如定时器、网络请求的结果)也无法得到及时处理。这导致页面变得“卡顿”或“无响应”
随着时代的变迁,jqery出现了,帮助我们简化了js获取dom的操作,封装成新的api
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h2 id="app"></h2>
<input type="text" id="todo-input">
<script src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/3.6.0/jquery.min.js"></script>
<script>
$('#todo-input').on('keyup', function(e) {
$('#app').text($(this).val())
})
</script>
</body>
</html>
即使jQuery解决了不同浏览器之间JavaScript API的差异,使得开发者可以编写一次代码在大多数主流浏览器中运行,把原生js封装成框架但还是有一定局限性。
jQuery 的局限性
jQuery 简化了跨浏览器兼容性和 DOM 操作,但它并没有从根本上解决频繁操作 DOM 所带来的性能问题。尽管它提供了链式调用和选择器功能来简化代码编写,但仍然需要开发者显式地去操作 DOM,这对于构建复杂的用户界面来说不够高效。
不但如此,即使api封装好了,某些特定情况还是不如原生的querySelectorAll
或getElementById
等方法高效例如,当仅需获取一个ID元素时,使用document.getElementById('id')
比$('#id')
要快得多。相对vue而言jQery的api也相对抽象,这时vue应运而生。
演变为 Vue/React 现代框架
Vue 和 React 这样的现代框架引入了新的开发模式,它们更注重数据驱动视图更新的理念,而不是直接操作 DOM。
响应式系统:Vue 的核心特性之一是它的响应式数据绑定机制。当你更改数据时,相关的视图会自动更新,反之亦然。这大大减少了手动 DOM 操作的需求。
虚拟 DOM:为了优化性能,Vue 使用了虚拟 DOM 技术。它会在内存中创建一个轻量级的 DOM 树副本,并仅在必要时将差异应用到真实 DOM 上。这样可以减少昂贵的 DOM 操作次数。
组件化架构:现代框架鼓励使用组件化的方式组织代码。每个组件都可以有自己的模板、样式和行为,从而提高了代码的模块性和复用性。
等等我们就不一一细说,我们直接来看vue在构建tool list是如何是实现的
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue Example</title>
<style>
.done {
color: red;
text-decoration: line-through;
}
</style>
</head>
<body>
<div id="app">
<ul>
<li v-for="app in apps" :key="app.title">
<span :class="{ done: app.done }">{{ app.title }}</span>
</li>
</ul>
<input type="text" v-model="newTask" placeholder="Type something..." @keydown.enter="addTodo">
</div>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/vue/3.2.31/vue.global.min.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
newTask: '', // 绑定输入框的值
apps: [
{ title: "吃饭", done: false },
{ title: "睡觉", done: false },
{ title: "打豆豆", done: false }
]
};
},
methods: {
addTodo() {
if (this.newTask.trim() !== '') {
this.apps.push({
title: this.newTask,
done: false
});
this.newTask = ''; // 清空输入框
}
}
}
});
app.mount('#app'); // 挂载Vue应用到id为app的div元素上
</script>
</body>
</html>
比起纯粹的js,Vue 更加关注应用程序的业务逻辑,即数据管理和用户交互,而不是底层的 DOM 操作。它通过响应式系统和组件化的结构让开发者能够专注于构建用户体验良好的应用,而不必担心低级别的细节。这样我们的前端切图仔可以更好的把重心放在与用户交互的页面上
上述简单的toollist我们浅聊几个简单的vue设计感想
首先就是li标签里的v-for语法糖,这种快速的遍历json对象的方式免去了以前在纯粹js里遍历的难读性,我们直接在标签中加上一眼能看懂逻辑的语法糖,vue的语法设计优势迅速的体现出对小白与新手的友好与易懂,这样我们快速的上手框架即可,项目交互逻辑也清晰明了。那为什么vue语法糖中有v-model呢,我们来遵从需求即设计的理念出发
,v-model
是一个语法糖,它结合了 :value
(用于将组件或元素的值绑定到数据属性)和 @input
(用于监听用户输入并更新数据属性) 。 在我们的例子中,v-model="newTask"
将输入框的值与 newTask
数据属性进行了双向绑定。这意味着每当用户在输入框中输入内容时,newTask
会自动更新;同时,如果通过代码改变 newTask
的值,输入框中的显示也会相应地更新。
这样我们即结合了响应式数据,又实现了事件监听的方案,一举两得,而这便是vue v-model设计的精妙哲学之处,一行代码-伟大无需多言。
然而,当项目之庞大,数据内容之繁琐时,难道要使用无穷无尽的ref吗,但如果又有某些值依赖于这些响应数据怎么办,这样单纯的定义响应式数据无疑会增加项目难读性,不急vue的设计者还有这招-computed计算属性,计算属性(Computed Properties)是 Vue.js 中一个非常强大的特性,主要用于简化模板逻辑和优化性能,计算属性会基于其依赖的数据进行缓存。只有当依赖的数据发生变化时,计算属性才会重新评估。这有助于避免不必要的计算,从而提高应用的性能。
而对于视图而言,通过计算属性,可以确保视图始终反映最新的应用状态。计算属性会自动追踪其依赖的数据,并在数据变化时更新视图。
其实计算属性出现的最大好处莫过于这点可读性和维护性,毕竟当你面向别人不良好风格的代码时,无异于噩梦
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>todos</title>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/vue/3.2.31/vue.global.min.js"></script>
<style>
.done {
color: gray;
text-decoration: line-through;
}
</style>
</head>
<body>
<!-- 挂载点,vue 作用范围 -->
<div id="app">
<h2>{{title}}</h2>
<input type="text" v-model="title" @keydown.enter="addTodo" />
<ul>
<li v-for="todo in todos">
<input type="checkbox" v-model="todo.done"/>
<span :class="{done:todo.done}">{{todo.title}}</span>
</li>
</ul>
<div>
全选<input type="checkbox" v-model="allDone"/>
<span >{{active}} / {{all}}</span>
</div>
</div>
<script>
const App = {
data() {
return {
title: 'todos',
todos: [
{title: '吃饭', done: false},
{title: '睡觉', done: true},
]
}
},
methods: {
addTodo() {
this.todos.push({
title: this.title,
done: false
})
this.title = ''
}
},
computed: {
all() {
return this.todos.length
},
active() {
return this.todos.filter(todo => !todo.done).length
},
//get和set两个方法,get是必须的,set可以不写
allDone:{
//this也可以找到计算属性
get() {
return this.active === 0;
},
set(val) {
//数据界面保持一致
this.todos.forEach(todo => {todo.done = val})
}
}
},
}
Vue.createApp(App).mount('#app')
</script>
</body>
</html>
我们把全选的数据逻辑封装在一个计算属性里,每当数据变化时,页面便会跟据数据变化,这样大大提高了大型项目的可读性与维护性
all
和 active
计算属性:用于动态计算所有任务数量和未完成任务数量,利用了 Vue 的依赖追踪机制,只有当相关数据发生变化时才会重新计算,提高了性能。
allDone
计算属性:结合 get
和 set
方法实现了全选/取消全选功能,确保了视图和数据的一致性,并且避免了重复代码。