Vue.js是一款渐进式 JavaScript 框架,用于构建用户界面。与其它大型框架不同,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。
这个框架的主要目标是通过尽量简单的 API 提供高效的数据绑定和灵活的组件系统。Vue.js 适用于从单页面应用(SPA)到复杂的前端应用的开发。
Vue 的一些主要特点包括:
vue-router
集成。Vuex
集成。由于 Vue.js 有一个相对简单和灵活的 API,以及丰富的文档和社区支持,因此它很受欢迎,并且得到了大量的开发者和公司的采用。
使用 Vue.js 2 开发应用程序涉及几个基础步骤,下面是一个简单的指南。
通过 CDN 引入
在 HTML 文件中添加以下脚本标签:
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
使用 npm 安装
在终端中运行以下命令:
npm install vue@2
MVVM(Model-View-ViewModel)是一种用于构建用户界面的设计模式,特别适用于复杂的前端应用。下面我们将通过一个简单的 JavaScript 示例来解释 MVVM 的基本概念。
HTML(View)
首先,我们来定义视图(View)。视图是用户与其交互的界面元素。
<!DOCTYPE html>
<html>
<head>
<title>MVVM Demo</title>
</head>
<body>
<button id="increment-btn">点击增加</button>
<p id="counter-text">计数:0</p>
<script src="app.js"></script>
</body>
</html>
JavaScript(Model 和 ViewModel)
然后,我们定义数据模型(Model)和视图模型(ViewModel)。
// Model(模型)
const model = {
count: 0
};
// ViewModel(视图模型)
const viewModel = {
incrementCount: function() {
model.count += 1;
this.updateView();
},
updateView: function() {
document.getElementById('counter-text').innerText = `计数:${model.count}`;
}
};
// 初始化
viewModel.updateView();
// DOM 监听(连接 View 和 ViewModel)
document.getElementById('increment-btn').addEventListener('click', function() {
viewModel.incrementCount();
});
原理解析
Model(模型): 我们定义了一个简单的对象 model
,它有一个属性 count
用于存储计数值。
View(视图): 在 HTML 文件中,我们有一个按钮和一个段落用于展示计数器的值。
ViewModel(视图模型): 在 viewModel
对象中,我们定义了两个方法:
incrementCount
:用于递增 model
的 count
值。updateView
:用于更新视图,展示最新的 count
值。DOM 监听和数据绑定:
viewModel.incrementCount()
,从而改变 Model 的数据。updateView()
方法,自动将最新的数据展示在 View 上。这样,我们就实现了 Model, View 和 ViewModel 的解耦,每个部分有其自己的职责,而 ViewModel 起到了桥梁的作用。这就是 MVVM 设计模式的基本原理。
在 Vue.js 中,声明式渲染是一种表达界面应该如何展示的高级方式,而不是描述如何进行 DOM 操作来达到目的。换句话说,你只需要声明你想要什么,而 Vue.js 会负责如何去做。
vue.html
<!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>
<div id="app"></div>
<script src="vue.js"></script>
<script>
new Vue({
el: '#app',
data: {
message: 'hello'
},
template:'<span>{{message}}</span>'
});
</script>
</body>
</html>
my-vue.js
class Vue {
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.init();
}
init() {
this.proxyData();
this.compile();
}
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
compile() {
const { template } = this.$options;
const html = template.replace(/\{\{(.+?)\}\}/g, (_, key) => {
return this[key];
});
const tempContainer = document.createElement('div');
tempContainer.innerHTML = html;
const newEl = tempContainer.firstChild;
document.body.replaceChild(newEl, this.$el);
}
}
在 Vue.js 中,响应式数据是其中最核心的概念之一。简单来说,响应式意味着当数据变化时,与该数据相关的所有事物都会自动更新。
<!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>
<div id="app"></div>
<script src="vue.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
message: 'hello'
},
template:'<span>{{message}}</span>'
});
+ vm.message = 'world';
</script>
</body>
</html>
my-vue.js
class Vue {
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.init();
}
init() {
this.proxyData();
+ this.observe(this.$data);
this.compile();
}
+ observe(obj) {
+ for (let key in obj) {
+ let value = obj[key];
+ Object.defineProperty(obj, key, {
+ get() {
+ return value;
+ },
+ set: (newValue) => {
+ if (newValue !== value) {
+ value = newValue;
+ this.compile();
+ }
+ }
+ });
+ }
+ }
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
compile() {
const { template } = this.$options;
const html = template.replace(/\{\{(.+?)\}\}/g, (_, key) => {
return this[key];
});
const tempContainer = document.createElement('div');
tempContainer.innerHTML = html;
const newEl = tempContainer.firstChild;
document.body.replaceChild(newEl, this.$el);
this.$el=newEl;
}
}
v-text
是 Vue.js 中用于更新 DOM 元素文本内容的一个指令。这个指令会把指定元素(通常是一个文本节点)的 textContent
属性设置为跟表达式的值一致。
下面是一个简单的例子:
<!-- Vue 组件或者实例的模板 -->
<div v-text="message"></div>
<!-- JavaScript 部分 -->
<script>
new Vue({
el: '#app',
data: {
message: 'Hello, world!'
}
});
</script>
在这个例子中,<div>
标签的文本内容会被设置为 data
对象里 message
的值,即 "Hello, world!"。
这与直接使用插值表达式(即双大括号)基本是等价的:
<div>{{ message }}</div>
然而,v-text
会直接改写元素的 textContent
,因此这个元素内部的其他文本或标记会被覆盖。另外,v-text
更新 DOM 的方式可能比插值表达式更高效,尤其是当你需要更新大量文本节点时。
请注意,v-text
指令本质上不会解析 HTML 标签,只会以纯文本的形式插入。如果你需要解析 HTML,你可以使用 v-html
指令,但要注意这可能会导致跨站脚本攻击(XSS)。
总结:
v-text
用于更新文本内容。<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue</title>
</head>
<body>
<div id="app">
</div>
<script src="my-vue.js"></script>
<script>
var vm = new Vue({
el:'#app',
data:{msg:'hello',content:'world'},
+ template:'<p><span v-text="msg"></span><span v-text="content"></span></p>'
});
</script>
</body>
</html>
class Vue {
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.init();
}
init() {
this.proxyData();
this.observe(this.$data);
this.compile();
}
observe(obj) {
for (let key in obj) {
let value = obj[key];
Object.defineProperty(obj, key, {
get() {
return value;
},
set: (newValue) => {
if (newValue !== value) {
value = newValue;
this.compile();
}
}
});
}
}
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
compile() {
const { template } = this.$options;
const html = template.replace(/\{\{(.+?)\}\}/g, (_, key) => {
return this[key];
});
const tempContainer = document.createElement('div');
tempContainer.innerHTML = html;
+ const hasVTexts = tempContainer.querySelectorAll('[v-text]');
+ if (hasVTexts && hasVTexts.length > 0) {
+ for (const hasVText of hasVTexts) {
+ let textBinding = hasVText.getAttribute('v-text');
+ hasVText.textContent = this[textBinding];
+ }
+ }
const newEl = tempContainer.firstChild;
document.body.replaceChild(newEl, this.$el);
this.$el = newEl;
}
}
v-html
是 Vue.js 框架中的一个指令,用于将 HTML 字符串插入到 DOM(文档对象模型)中。该指令用于绑定一段 HTML 代码到一个 DOM 元素,使该段代码作为元素的 innerHTML
渲染出来。
下面是一个简单的例子:
<template>
<div>
<div v-html="htmlString"></div>
</div>
</template>
<script>
export default {
data() {
return {
htmlString: "<h1>Hello World</h1>"
};
}
};
</script>
在这个例子中,<h1>Hello World</h1>
会被插入到 div
元素内,并显示为一个 H1 大小的 "Hello World" 文字。
注意事项:
安全性问题:由于 v-html
直接将 HTML 字符串插入到 DOM,因此存在 XSS(跨站脚本攻击)的风险。你应当只对可信的内容使用 v-html
,并且尽量避免插入用户生成的内容。
替代方案:如果你只是想插入文本而不是 HTML,可以使用 {{}}
插值表达式。
不保留状态:使用 v-html
插入的 HTML 不会保留任何 Vue 组件状态。这意味着如果你通过 v-html
插入了包含 Vue 组件的 HTML,这些组件将不会被实例化。
限制:v-html
只影响元素的 innerHTML
,不能用来绑定 Vue 模板表达式到整个模板。
不建议频繁使用:频繁使用 v-html
可能会使得应用更难以维护和理解,因为它分散了应用逻辑。
使用 v-html
指令时,务必小心,并确保你了解其潜在的风险和限制。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue</title>
</head>
<body>
<div id="app">
</div>
<script src="my-vue.js"></script>
<script>
var vm = new Vue({
el:'#app',
data:{msg:'<strong>hello</strong>'},
template:'<span v-html="msg"></span>'
});
</script>
</body>
</html>
class Vue {
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.init();
}
init() {
this.proxyData();
this.observe(this.$data);
this.compile();
}
observe(obj) {
for (let key in obj) {
let value = obj[key];
Object.defineProperty(obj, key, {
get() {
return value;
},
set: (newValue) => {
if (newValue !== value) {
value = newValue;
this.compile();
}
}
});
}
}
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
compile() {
const { template } = this.$options;
const html = template.replace(/\{\{(.+?)\}\}/g, (_, key) => {
return this[key];
});
const tempContainer = document.createElement('div');
tempContainer.innerHTML = html;
const hasVTexts = tempContainer.querySelectorAll('[v-text]');
if (hasVTexts && hasVTexts.length > 0) {
for (const hasVText of hasVTexts) {
let textBinding = hasVText.getAttribute('v-text');
hasVText.textContent = this[textBinding];
}
}
+ const hasVHTMLs = tempContainer.querySelectorAll('[v-html]');
+ if (hasVHTMLs && hasVHTMLs.length > 0) {
+ for (const hasVHTML of hasVHTMLs) {
+ let htmlBinding = hasVHTML.getAttribute('v-html');
+ hasVHTML.innerHTML = this[htmlBinding];
+ }
+ }
const newEl = tempContainer.firstChild;
document.body.replaceChild(newEl, this.$el);
this.$el = newEl;
}
}
v-cloak
是一个用于 Vue.js 的特殊指令,其主要目的是在 Vue 实例编译结束、数据绑定完成后才显示该元素。在这之前,元素和其子元素将保持隐藏。这个指令用于防止未编译的 "Mustache 标签"(双大括号语法,如 {{ message }}
)在页面加载时短暂出现。
一般来说,你可能会将 v-cloak
和 CSS 规则结合使用,以在 Vue 完成初始化之前隐藏相关元素:
/* CSS */
[v-cloak] {
display: none;
}
然后,在你的 HTML 中:
<div v-cloak>
{{ message }}
</div>
在这个例子中,只要存在 v-cloak
指令,Vue 还未完成编译,该 div
就会被隐藏。当 Vue 完成初始化和编译后,v-cloak
指令会被自动移除,并且相关的 CSS 规则也不再适用,因此元素将变为可见。
这种机制尤其有用在大型应用或网络状况不佳的情况下,用户可能会看到 "闪烁" 的 {{ message }}
标签,直到 Vue 完成初始化。通过使用 v-cloak
,你可以提供更流畅、更专业的用户体验。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue</title>
<style>
[v-cloak]{
display: none;
}
</style>
</head>
<body>
+ <div id="app" v-cloak>
+ <span>{{msg}}</span>
</div>
<script src="my-vue.js"></script>
<script>
var vm = new Vue({
el:'#app',
data:{msg:'hello'}
});
</script>
</body>
</html>
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
class Vue {
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.init();
}
init() {
this.proxyData();
this.observe(this.$data);
+ this.compile(this.$el);
}
observe(obj) {
for (let key in obj) {
let value = obj[key];
Object.defineProperty(obj, key, {
get() {
return value;
},
set: (newValue) => {
if (newValue !== value) {
value = newValue;
+ this.compile(this.$el);
}
}
});
}
}
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
compile(el) {
+ const { childNodes } = el;
+ Array.from(childNodes).forEach(node => {
+ if (node.nodeType === ELEMENT_NODE) {
+ const attrs = node.attributes;
+ Array.from(attrs).forEach(attr => {
+ if (attr.name === 'v-text') {
+ const expr = attr.value;
+ node.textContent = this[expr];
+ } else if (attr.name == 'v-html') {
+ const expr = attr.value;
+ node.innerHTML = this[expr];
+ }
+ });
+ this.compile(node);
+ } else if (node.nodeType === TEXT_NODE) {
+ const { textContent } = node;
+ node.textContent = textContent.replace(/\{\{(.+?)\}\}/g, (_, key) => {
+ return this[key];
+ });
+ }
+ });
+ el.removeAttribute('v-cloak');
}
}
v-pre
是一个 Vue.js 的指令,用于跳过当前元素和所有子元素的编译过程。这样可以用于显示原始的 Mustache 标签。当 Vue.js 渲染元素和组件时,会解析其中的 Vue 指令和插值表达式(例如,{{ message }}
)。有时,你可能希望 Vue 忽略某个元素及其子元素,不对其进行编译。这是 v-pre
的用途。
例如,考虑以下 HTML:
<div v-pre>
<p>{{ this will not be compiled }}</p>
</div>
由于 v-pre
指令的存在,<div>
元素和其所有子元素都不会被 Vue 编译。因此,页面上会原样显示文本 {{ this will not be compiled }}
。
v-pre
主要用于两种场景:
v-pre
可以减少编译时间。v-pre
。需要注意的是,v-pre
会跳过元素及其所有子元素的编译,包括其中的 Vue 指令。所以,请谨慎使用这个指令。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue</title>
</head>
<body>
<div id="app" >
<span>{{msg}} {{age}}</span>
<span v-pre>{{msg}}</span>
<p v-pre>
<span>{{age}}</span>
</p>
</div>
<script src="my-vue.js"></script>
<script>
var vm = new Vue({
el:'#app',
data:{msg:'hello',age:18}
});
</script>
</body>
</html>
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
class Vue {
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.init();
}
init() {
this.proxyData();
this.observe(this.$data);
this.compile(this.$el);
}
observe(obj) {
for (let key in obj) {
let value = obj[key];
Object.defineProperty(obj, key, {
get() {
return value;
},
set: (newValue) => {
if (newValue !== value) {
value = newValue;
this.compile(this.$el);
}
}
});
}
}
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
compile(el) {
const { childNodes } = el;
Array.from(childNodes).forEach(node => {
if (node.nodeType === ELEMENT_NODE) {
+ if (node.nodeType === 1 && node.hasAttribute('v-pre')) {
+ return;
+ }
const attrs = node.attributes;
Array.from(attrs).forEach(attr => {
if (attr.name === 'v-text') {
const expr = attr.value;
node.textContent = this[expr];
} else if (attr.name == 'v-html') {
const expr = attr.value;
node.innerHTML = this[expr];
}
});
this.compile(node);
} else if (node.nodeType === TEXT_NODE) {
const { textContent } = node;
node.textContent = textContent.replace(/\{\{(.+?)\}\}/g, (_, key) => {
return this[key];
});
}
});
el.removeAttribute('v-cloak');
}
}
v-bind
是 Vue.js 中用于绑定属性或表达式到元素的一个指令。当你需要动态设置 HTML 元素的某个属性值时,你可以使用 v-bind
。它使得数据与视图可以保持同步。
基本语法:
<!-- 绑定一个属性 -->
<a v-bind:href="url">Link</a>
<!-- 使用表达式 -->
<div v-bind:style="{ color: active ? 'red' : 'gray' }"></div>
在这些例子中,url
和 active
是 Vue 实例中的数据或计算属性。
常见用法:
绑定图片链接:
<img v-bind:src="imageSrc">
这里,imageSrc
是一个变量,它的值会被设置为 img
元素的 src
属性。
动态类名:
<div v-bind:class="{ active: isActive }"></div>
当 isActive
为 true
时,这个 div
的类名会包含 active
。
动态样式:
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
这里,activeColor
和 fontSize
是变量,它们的值会分别被设置为 div
的 color
和 fontSize
样式。
绑定动态属性:
<button v-bind:[key]="value"></button>
在这个例子中,key
和 value
是变量。这允许你动态地设置任何属性。
简写:
v-bind
有一个简写,可以直接用 :
表示:
<!-- 完整语法 -->
<a v-bind:href="url">Link</a>
<!-- 简写 -->
<a :href="url">Link</a>
使用 v-bind
指令,你可以创建更加动态和响应式的 web 应用。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue</title>
</head>
<body>
<div id="app">
<div v-bind:title="title">title内容</div>
</div>
<script src="my-vue.js"></script>
<script>
new Vue({
el: '#app',
data: {
title: 'title值'
}
})
</script>
</body>
</html>
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
class Vue {
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.init();
}
init() {
this.proxyData();
this.observe(this.$data);
this.compile(this.$el);
}
observe(obj) {
for (let key in obj) {
let value = obj[key];
Object.defineProperty(obj, key, {
get() {
return value;
},
set: (newValue) => {
if (newValue !== value) {
value = newValue;
this.compile(this.$el);
}
}
});
}
}
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
compile(el) {
const { childNodes } = el;
Array.from(childNodes).forEach(node => {
if (node.nodeType === ELEMENT_NODE) {
if (node.nodeType === 1 && node.hasAttribute('v-pre')) {
return;
}
const attrs = node.attributes;
Array.from(attrs).forEach(attr => {
if (attr.name === 'v-text') {
const expr = attr.value;
node.textContent = this[expr];
} else if (attr.name == 'v-html') {
const expr = attr.value;
node.innerHTML = this[expr];
+ }else if (attr.name.startsWith('v-bind:')) {
+ const attrName = attr.name.slice(7);
+ const key = attr.value;
+ node.setAttribute(attrName, this[key]);
+ }
});
this.compile(node);
} else if (node.nodeType === TEXT_NODE) {
const { textContent } = node;
node.textContent = textContent.replace(/\{\{(.+?)\}\}/g, (_, key) => {
return this[key];
});
}
});
el.removeAttribute('v-cloak');
}
}
v-on
是 Vue.js 中用于监听 DOM 事件并在触发时执行一些 JavaScript 逻辑的一个指令。通过这个指令,你可以将一个方法或者一个内联语句与 DOM 事件绑定。
基本用法
这里有一些例子,以便你更好地理解这个指令:
<!-- 绑定一个方法到 click 事件 -->
<button v-on:click="doSomething">Click me</button>
<!-- 你也可以直接绑定一个内联语句 -->
<button v-on:click="count += 1">Increment</button>
<!-- 使用事件对象 -->
<button v-on:click="showEvent($event)">Show Event</button>
new Vue({
el: '#app',
data: {
count: 0
},
methods: {
doSomething() {
alert('Button was clicked!');
},
showEvent(event) {
console.log(event);
}
}
});
缩写
在 Vue 中,v-on:
可以简写为 @
,让模板看起来更简洁:
<!-- 完整语法 -->
<button v-on:click="doSomething">Click me</button>
<!-- 缩写 -->
<button @click="doSomething">Click me</button>
传递参数
v-on
指令也支持传递参数:
<button @click="say('hi')">Say Hi</button>
<button @click="say('hello')">Say Hello</button>
new Vue({
el: '#app',
methods: {
say(message) {
alert(message);
}
}
});
动态事件名
你也可以绑定动态的事件名:
<button v-on:[eventName]="doSomething">Click me</button>
new Vue({
el: '#app',
data: {
eventName: 'click'
},
methods: {
doSomething() {
alert('Button was clicked!');
}
}
});
在 Vue.js 2 中,v-on
指令用于监听 DOM 事件,并在触发时执行某些 JavaScript 代码。这是一种声明式的方式来处理浏览器事件,如点击 (click
)、输入 (input
)、提交 (submit
) 等。
methods
是 Vue 实例的一个选项,用于定义与该实例关联的方法。这些方法通常用于模板中的事件处理,或者在 Vue 实例中用于计算或操作数据。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue</title>
</head>
<body>
<div id="app">
<button v-on:click="sayHello">Click me</button>
</div>
<script src="my-vue.js"></script>
<script>
new Vue({
el: '#app',
data: {
msg: 'hello'
},
methods: {
sayHello: function () {
console.log(this.msg);
}
}
})
</script>
</body>
</html>
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
class Vue {
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
+ this.$methods = options.methods;
this.init();
}
init() {
this.proxyData();
+ this.proxyMethods();
this.observe(this.$data);
this.compile(this.$el);
}
+ proxyMethods() {
+ for (let key in this.$methods) {
+ this[key] = this.$methods[key].bind(this);
+ }
+ }
observe(obj) {
for (let key in obj) {
let value = obj[key];
Object.defineProperty(obj, key, {
get() {
return value;
},
set: (newValue) => {
if (newValue !== value) {
value = newValue;
this.compile(this.$el);
}
}
});
}
}
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
compile(el) {
const { childNodes } = el;
Array.from(childNodes).forEach(node => {
if (node.nodeType === ELEMENT_NODE) {
if (node.nodeType === 1 && node.hasAttribute('v-pre')) {
return;
}
const attrs = node.attributes;
Array.from(attrs).forEach(attr => {
if (attr.name === 'v-text') {
const expr = attr.value;
node.textContent = this[expr];
} else if (attr.name == 'v-html') {
const expr = attr.value;
node.innerHTML = this[expr];
} else if (attr.name.startsWith('v-bind:')) {
const attrName = attr.name.slice(7);
const key = attr.value;
node.setAttribute(attrName, this[key]);
+ } else if (attr.name.startsWith('v-on:')) {
+ const eventName = attr.name.slice(5);
+ const methodName = attr.value;
+ node.addEventListener(eventName, this[methodName]);
+ }
});
this.compile(node);
} else if (node.nodeType === TEXT_NODE) {
const { textContent } = node;
node.textContent = textContent.replace(/\{\{(.+?)\}\}/g, (_, key) => {
return this[key];
});
}
});
el.removeAttribute('v-cloak');
}
}
在 Vue.js 中,v-on
指令用于监听 DOM 事件,并在触发这些事件时执行一些 JavaScript 代码。$event
是一个特殊的变量,它用于访问原生的 DOM 事件对象。
例如,考虑一个按钮,点击该按钮时触发一个 click
事件:
<!-- HTML模板 -->
<button v-on:click="handleClick($event)">Click Me</button>
// Vue 实例
new Vue({
el: '#app',
methods: {
handleClick(event) {
console.log('Button clicked!', event);
}
}
});
在这个例子中,当用户点击按钮时,handleClick
方法会被调用,并且接收到原生的 click
事件对象作为参数。这个原生的事件对象存储在 $event
变量中。
$event
对象提供了关于该事件的各种信息,包括:
event.target
:触发事件的元素。event.type
:事件的类型(例如 "click")。event.timestamp
:事件的时间戳。有时,你可能想要同时传递 $event
和其他参数。在这种情况下,你可以这样做:
<!-- HTML模板 -->
<button v-on:click="handleClick('someValue', $event)">Click Me</button>
// Vue 实例
new Vue({
el: '#app',
methods: {
handleClick(value, event) {
console.log('Button clicked!', value, event);
}
}
});
在这个例子中,handleClick
方法接收两个参数:一个是字符串 'someValue'
,另一个是原生的 click
事件对象。这样你就可以在方法内部同时访问这两个值。
$event
是非常有用的,特别是当你需要访问事件对象以进行更复杂的操作时,例如阻止事件的默认行为或停止事件的传播。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue</title>
</head>
<body>
<div id="app">
+ <button v-on:click="sayHello(1,'a',$event,msg)">Click me</button>
</div>
<script src="my-vue.js"></script>
<script>
new Vue({
el: '#app',
data: {
msg: 'hello'
},
methods: {
+ sayHello (number,string,event,variable) {
+ console.log(number,string,event,variable);
+ }
}
})
</script>
</body>
</html>
my-vue.js
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
class Vue {
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
this.init();
}
init() {
this.proxyData();
this.proxyMethods();
this.observe(this.$data);
this.compile(this.$el);
}
proxyMethods() {
for (let key in this.$methods) {
this[key] = this.$methods[key].bind(this);
}
}
observe(obj) {
for (let key in obj) {
let value = obj[key];
Object.defineProperty(obj, key, {
get() {
return value;
},
set: (newValue) => {
if (newValue !== value) {
value = newValue;
this.compile(this.$el);
}
}
});
}
}
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
compile(el) {
const { childNodes } = el;
Array.from(childNodes).forEach(node => {
if (node.nodeType === ELEMENT_NODE) {
if (node.nodeType === 1 && node.hasAttribute('v-pre')) {
return;
}
const attrs = node.attributes;
Array.from(attrs).forEach(attr => {
if (attr.name === 'v-text') {
const expr = attr.value;
node.textContent = this[expr];
} else if (attr.name == 'v-html') {
const expr = attr.value;
node.innerHTML = this[expr];
} else if (attr.name.startsWith('v-bind:')) {
const attrName = attr.name.slice(7);
const key = attr.value;
node.setAttribute(attrName, this[key]);
} else if (attr.name.startsWith('v-on:')) {
+ const eventName = attr.name.slice(5);
+ const methodExpression = attr.value.trim();
+ let methodName = methodExpression;
+ let args = [];
+ const parenIndex = methodExpression.indexOf('(');
+ if (parenIndex > -1) {
+ methodName = methodExpression.substring(0, parenIndex);
+ const argsString = methodExpression.substring(
+ parenIndex + 1,
+ methodExpression.length - 1
+ ).trim();
+ args = argsString.split(',').map(arg => arg.trim());
+ }
+ node.addEventListener(eventName, (event) => {
+ const actualArgs = args.map(arg => {
+ if (arg === '$event') return event;
+ if (!isNaN(arg)) return Number(arg);
+ if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
+ return this[arg];
+ });
+ this[methodName](...actualArgs);
+ });
}
});
this.compile(node);
} else if (node.nodeType === TEXT_NODE) {
const { textContent } = node;
node.textContent = textContent.replace(/\{\{(.+?)\}\}/g, (_, key) => {
return this[key];
});
}
});
el.removeAttribute('v-cloak');
}
}
.stop
事件修饰符用于阻止事件冒泡。当你在元素上使用 .stop
修饰符时,如果此元素的事件被触发,那么这个事件不会向上冒泡至父元素。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue</title>
</head>
<body>
<div id="app">
<div id="parent" v-on:click="parentClicked">
<button id="child" v-on:click.stop="childClicked">Click me</button>
</div>
</div>
<script src="my-vue.js"></script>
<script>
new Vue({
el: '#app',
methods: {
parentClicked() {
console.log('Parent clicked!');
},
childClicked() {
console.log('Child clicked!');
}
}
});
</script>
</body>
</html>
.prevent
是一个事件修饰符,用于告诉 Vue 的事件系统,我们希望阻止原生事件的默认行为。这对应于在 JavaScript 中调用 event.preventDefault()
。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue</title>
</head>
<body>
<div id="app" >
<form v-on:submit.prevent="handleSubmit">
<label>用户名</label>
<input/>
<input type="submit"/>
</form>
</div>
<script src="vue.js"></script>
<script>
var vm = new Vue({
el:'#app',
data:{msg:'hello'},
methods:{
handleSubmit(event){
console.log('handleSubmit');
}
}
});
</script>
</body>
</html>
.capture
是一种事件修饰符,它使事件监听器在事件捕获模式下触发,而非默认的冒泡模式。
在解释.capture
修饰符之前,我们首先需要了解事件的冒泡和捕获模式。
事件冒泡和捕获:
冒泡模式:在冒泡模式中,事件首先在触发事件的最深的元素(目标元素)开始,然后逐级向上移动,经过所有的父元素,直到document对象。例如,如果你在一个嵌套的元素中点击,那么冒泡模式会首先触发元素本身的点击事件,然后是其父元素的点击事件,然后是其祖父元素的点击事件,以此类推。
捕获模式:捕获模式则是冒泡模式的相反。事件首先在document对象开始,然后逐级向下移动,经过所有的父元素,直到达到触发事件的最深的元素。这就像是事件先经过所有的关卡,然后才到达目标。
.capture
修饰符就是使Vue在捕获模式下监听DOM事件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue</title>
</head>
<body>
<div id="app">
<div id="parent" v-on:click="parentClicked" v-on:click.capture="parentCaptureClicked">
<button id="child" v-on:click="childClicked" v-on:click.capture="childCaptureClicked">Click me</button>
</div>
</div>
<script src="my-vue.js"></script>
<script>
new Vue({
el: '#app',
methods: {
parentClicked() {
console.log('Parent clicked!');
},
childClicked() {
console.log('Child clicked!');
},
parentCaptureClicked() {
console.log('parentCaptureClicked');
},
childCaptureClicked() {
console.log('childCaptureClicked');
}
}
});
</script>
</body>
</html>
在 Vue2 中,.self
是一个事件修饰符,用于确保只有当事件在该元素本身(而不是子元素)触发时,才会触发事件回调函数。这对应于 JavaScript 中对事件的目标元素(event.target
)和当前元素(event.currentTarget
)的比较。
这个修饰符常用在你希望事件只在特定元素上触发,而不是在其子元素上触发时。
以下是一个简单的例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue</title>
</head>
<body>
<div id="app">
<div id="parent" v-on:click.self="parentClicked" v-on:click.capture="parentCaptureClicked">
Parent
<button id="child" v-on:click="childClicked" v-on:click.capture="childCaptureClicked">Click me</button>
</div>
</div>
<script src="my-vue.js"></script>
<script>
new Vue({
el: '#app',
methods: {
parentClicked() {
console.log('Parent clicked!');
},
childClicked() {
console.log('Child clicked!');
},
parentCaptureClicked() {
console.log('parentCaptureClicked');
},
childCaptureClicked() {
console.log('childCaptureClicked');
}
}
});
</script>
</body>
</html>
.once
修饰符用于指定事件监听器在触发一次之后就自动解除绑定,即这个监听器只会被触发一次。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue</title>
</head>
<body>
<div id="app">
<p>{{counter}}</p>
<button v-on:click.once="handleClick">Click me</button>
</div>
<script src="my-vue2.js"></script>
<script>
new Vue({
el: '#app',
data: {
counter: 0
},
methods: {
handleClick () {
this.counter=this.counter+1;
}
}
});
</script>
</body>
</html>
.passive
是一个事件修饰符,用于增强性能,特别是在移动端。这对应于在 JavaScript 中对事件调用 addEventListener
方法时使用 { passive: true }
选项。
当你添加了 .passive
修饰符后,你就告诉浏览器你不打算阻止事件的默认行为。这样,浏览器就可以在事件处理程序运行的同时做一些性能优化。这对于一些高频触发的事件(例如滚动或触摸事件)特别有用。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue Scrolling</title>
<style>
/* 添加样式以确保 div 具有滚动条 */
div {
width: 300px;
height: 200px;
overflow: auto;
}
/* 添加足够的内容以触发滚动 */
.content {
height: 1000px;
}
</style>
</head>
<body>
<div id="app">
<div v-on:scroll.passive="onScroll" class="scroll-area">
<div class="content">内容</div>
</div>
</div>
<script src="my-vue.js"></script>
<script>
new Vue({
el: '#app',
methods: {
onScroll(event) {
console.log('Scrolled!');
}
}
});
</script>
</body>
</html>
为了处理键盘事件,Vue 提供了一些按键修饰符。这些修饰符允许你在事件触发时更加精确地处理不同的按键。这些修饰符可以在 v-on
或其缩写 @
后使用。
以下是一些常用的按键修饰符:
.enter
:只有在按下 Enter 键时才触发事件处理函数.tab
:只有在按下 Tab 键时才触发事件处理函数.delete
:只有在按下 Delete 或 Backspace 键时才触发事件处理函数.esc
:只有在按下 Esc 键时才触发事件处理函数.space
:只有在按下 Space 键时才触发事件处理函数.up
:只有在按下 Up 键时才触发事件处理函数.down
:只有在按下 Down 键时才触发事件处理函数.left
:只有在按下 Left 键时才触发事件处理函数.right
:只有在按下 Right 键时才触发事件处理函数以下是一个使用 .enter
按键修饰符的示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue</title>
</head>
<body>
<div id="app">
<input v-on:keyup.enter="submit" />
</div>
<script src="my-vue.js"></script>
<script>
new Vue({
el: '#app',
methods: {
submit() {
console.log('Enter key was pressed');
}
}
})
</script>
</body>
</html>
在这个例子中,当在输入框中按下 Enter 键时,submit
方法会被调用。
你也可以使用按键的 ASCII 码作为修饰符,例如 `@keyup.13="submit",其中 13 是 Enter 键的 ASCII 码。但是,使用名字(如
.enter`)通常更容易理解。
请注意,Vue2 也支持系统修饰符(如 .ctrl
、.alt
、.shift
、.meta
),可以与按键修饰符一起使用,例如 `@keydown.ctrl.enter="submit",这样只有在同时按下 Ctrl 键和 Enter 键时,
submit` 方法才会被调用。
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
+class Dep {
+ constructor() {
+ this.subs = [];
+ }
+ addSub(sub) {
+ this.subs.push(sub);
+ }
+ notify() {
+ this.subs.forEach(sub => sub.update());
+ }
+}
+class Watcher {
+ constructor(vm, expr, cb) {
+ this.vm = vm;
+ this.expr = expr;
+ this.cb = cb;
+ Dep.target = this;
+ this.oldValue = this.vm[this.expr];
+ Dep.target = null;
+ }
+ update() {
+ const newValue = this.vm[this.expr];
+ if (this.oldValue !== newValue) {
+ this.cb(newValue);
+ this.oldValue = newValue;
+ }
+ }
+}
class Vue {
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
this.init();
}
init() {
this.proxyData();
this.proxyMethods();
+ this.observe(this.$data);
this.compile(this.$el);
}
proxyMethods() {
for (let key in this.$methods) {
this[key] = this.$methods[key].bind(this);
}
}
+ observe(obj) {
+ for (let key in obj) {
+ let value = obj[key];
+ const dep = new Dep();
+ Object.defineProperty(obj, key, {
+ get() {
+ if (Dep.target) {
+ dep.addSub(Dep.target);
+ }
+ return value;
+ },
+ set: (newValue) => {
+ if (newValue !== value) {
+ value = newValue;
+ dep.notify();
+ }
+ }
+ });
+ }
+ }
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
compile(el) {
const vm = this;
const { childNodes } = el;
Array.from(childNodes).forEach(node => {
if (node.nodeType === ELEMENT_NODE) {
if (node.nodeType === 1 && node.hasAttribute('v-pre')) {
return;
}
const attrs = node.attributes;
Array.from(attrs).forEach(attr => {
if (attr.name === 'v-text') {
const expr = attr.value;
+ new Watcher(vm, expr, newValue => {
+ node.textContent = newValue;
+ });
node.textContent = vm[expr];
} else if (attr.name === 'v-html') {
const expr = attr.value;
+ new Watcher(vm, expr, newValue => {
+ node.innerHTML = newValue;
+ });
node.innerHTML = vm[expr];
} else if (attr.name.startsWith('v-bind:')) {
const attrName = attr.name.slice(7);
const key = attr.value;
+ new Watcher(vm, key, newValue => {
+ node.setAttribute(attrName, newValue);
+ });
node.setAttribute(attrName, vm[key]);
} else if (attr.name.startsWith('v-on:')) {
let [eventName, ...modifiers] = attr.name.slice(5).split('.');
const methodExpression = attr.value.trim();
let methodName = methodExpression;
let args = [];
const parenIndex = methodExpression.indexOf('(');
if (parenIndex > -1) {
methodName = methodExpression.substring(0, parenIndex);
const argsString = methodExpression.substring(
parenIndex + 1,
methodExpression.length - 1
).trim();
args = argsString.split(',').map(arg => arg.trim());
}
const options = {
capture: modifiers.includes('capture'),
once: modifiers.includes('once'),
passive: modifiers.includes('passive')
};
node.addEventListener(eventName, function handler(event) {
+ if (modifiers.includes('stop')) event.stopPropagation();
+ if (modifiers.includes('prevent')) event.preventDefault();
+ if (modifiers.includes('self') && event.target !== event.currentTarget) return;
+ const keyCodes = {
+ enter: 13,
+ tab: 9,
+ delete: [8, 46],
+ esc: 27,
+ space: 32,
+ up: 38,
+ down: 40,
+ left: 37,
+ right: 39
+ };
+ const keyCode = keyCodes[modifiers.find(mod => keyCodes.hasOwnProperty(mod))];
+ if (keyCode) {
+ if (Array.isArray(keyCode)) {
+ if (!keyCode.includes(event.keyCode)) return;
+ } else if (event.keyCode !== keyCode) {
+ return;
+ }
+ }
+ const actualArgs = args.map(arg => {
+ if (arg === '$event') return event;
+ if (!isNaN(arg)) return Number(arg);
+ if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
+ return vm[arg];
+ });
+ vm[methodName](...actualArgs);
+ }, options);
}
});
this.compile(node);
} else if (node.nodeType === TEXT_NODE) {
+ const originalTextContent = node.textContent;
+ const updateTextContent = () => {
+ let text = originalTextContent;
+ text = text.replace(/\{\{(.+?)\}\}/g, (_, key) => {
+ return vm[key.trim()];
+ });
+ node.textContent = text;
+ };
+ updateTextContent();
+ let match;
+ const reg = /\{\{(.+?)\}\}/g;
+ while ((match = reg.exec(originalTextContent)) !== null) {
+ const key = match[1].trim();
+ new Watcher(vm, key, updateTextContent);
+ }
}
});
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app" v-cloak>
{{user.name}}
</div>
<script src="v2.js"></script>
<script>
var vm = new Vue({
el:'#app',
data:{//data的属性都是依赖
user:{
name:'beijing'
}
},
methods:{
}
});
vm.user.name='guangzhou'
</script>
</body>
</html>
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
Dep.target = this;
this.oldValue = this.getVal(this.vm, this.expr);
Dep.target = null;
}
update() {
const newValue = this.getVal(this.vm, this.expr);
if (this.oldValue !== newValue) {
this.cb(this.oldValue, newValue);
this.oldValue = newValue;
}
}
getVal(vm, expr) {
const keys = expr.split('.');
return keys.reduce((prev, next) => {
return prev[next];
}, vm);
}
}
class Vue {
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
this.init();
}
init() {
this.proxyData();
this.proxyMethods();
this.observe(this.$data);
this.compile(this.$el);
}
observe(obj) {
if (!obj || typeof obj !== 'object') {
return;
}
Object.keys(obj).forEach(key => {
this.defineReactive(obj, key, obj[key]);
});
}
defineReactive(obj, key, val) {
this.observe(val);
const dep = new Dep();
const vm = this;
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set(newVal) {
if (newVal === val) {
return;
}
val = newVal;
vm.observe(newVal);
dep.notify();
}
});
}
proxyMethods() {
for (let key in this.$methods) {
this[key] = this.$methods[key].bind(this);
}
}
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
compile(el) {
const vm = this;
const { childNodes } = el;
[...childNodes].forEach(node => {
if (node.nodeType === ELEMENT_NODE && node.hasAttribute('v-pre')) {
return;
}
if (node.nodeType === ELEMENT_NODE) {
// compileAttributes([...node.attributes], node, vm); // 如果需要,编译属性
this.compile(node);
} else if (node.nodeType === TEXT_NODE) {
let { originalTextContent } = node;
if (!originalTextContent) {
originalTextContent = node.textContent;
node.originalTextContent = originalTextContent;
}
let reg = /\{\{(.+?)\}\}/g;
const updateTextContent = () => {
node.textContent = originalTextContent.replace(reg, (_, key) => {
debugger
return this.getVal(vm, key.trim()); // 注意这里使用了 getVal 方法
});
};
updateTextContent();
let match;
while ((match = reg.exec(originalTextContent)) !== null) {
const key = match[1].trim();
new Watcher(vm, key, updateTextContent);
}
}
});
el.removeAttribute('v-cloak');
}
getVal(vm, expr) {
const keys = expr.split('.');
return keys.reduce((prev, next) => {
return prev[next];
}, vm);
}
}
function compileAttributes(attributes, node, vm) {
attributes.forEach(attr => {
if (attr.name === 'v-text') {
handleVText(node, attr, vm);
} else if (attr.name === 'v-html') {
handleVHtml(node, attr, vm);
} else if (attr.name.startsWith('v-bind:') || attr.name.startsWith(':')) {
handleVBind(node, attr, vm);
} else if (attr.name.startsWith('v-on:') || attr.name.startsWith('@')) {
handleEvent(node, attr, vm);
}
});
}
function handleVText(node, attr, vm) {
const expr = attr.value;
node.textContent = vm[expr];
}
function handleVHtml(node, attr, vm) {
const expr = attr.value;
node.innerHTML = vm[expr];
}
function handleVBind(node, attr, vm) {
let attrName = attr.name;
if (attrName.startsWith(':')) {
attrName = `v-bind${attrName}`;
}
attrName = attrName.slice(7);
const key = attr.value;
node.setAttribute(attrName, vm[key]);
}
function handleEvent(node, attr, vm) {
let attrName = attr.name;
if (attrName.startsWith('@')) {
attrName = `v-on:${attrName.slice(1)}`;
}
const [eventName, ...modifiers] = attrName.slice(5).split('.');
const methodExpression = attr.value.trim();
let methodName = methodExpression;
let args = [];
let parenIndex = methodExpression.indexOf('(');
if (parenIndex !== -1) {
methodName = methodExpression.substring(0, parenIndex);
const argsString = methodExpression.substring(parenIndex + 1, methodExpression.length - 1).trim();
args = argsString.split(',').map(arg => arg.trim());
}
let options = {
capture: modifiers.includes('capture'),
once: modifiers.includes('once')
};
node.addEventListener(eventName, event => {
if (modifiers.includes('stop')) event.stopPropagation();
if (modifiers.includes('prevent')) event.preventDefault();
if (modifiers.includes('self')) {
if (event.target !== event.currentTarget) {
return;
}
}
const actualArgs = args.map(arg => {
if (!isNaN(arg)) return arg;
if (arg === '$event') return event;
if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
return vm[arg];
});
vm[methodName](...actualArgs);
}, options);
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app" v-cloak>
{{users[0].name}}
{{users.length}}
</div>
<script src="v3.js"></script>
<script>
var vm = new Vue({
el:'#app',
data:{//data的属性都是依赖
users:[
{id:1,name:'zhangsan'}
]
}
});
vm.users[0].name='lisi'
setTimeout(()=>{
vm.users.push({id:2,name:'wangwu'});
},3000);
</script>
</body>
</html>
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
Dep.target = this;
this.oldValue = this.getVal(this.vm, this.expr);
Dep.target = null;
}
update() {
const newValue = this.getVal(this.vm, this.expr);
if (this.oldValue !== newValue) {
this.cb(this.oldValue, newValue);
this.oldValue = newValue;
}
}
getVal(vm, expr) {
const keys = expr.split(/[\.\[\]]/).filter(Boolean);
return keys.reduce((prev, next) => {
if (next.match(/^\d+$/)) { // 检查是否为数字
return prev[parseInt(next, 10)];
}
return prev[next];
}, vm);
}
}
class Vue {
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
this.init();
}
init() {
this.proxyData();
this.proxyMethods();
this.observe(this.$data);
this.compile(this.$el);
}
observe(obj) {
if (Array.isArray(obj)) {
this.observeArray(obj);
return;
}
if (!obj || typeof obj !== 'object') {
return;
}
Object.keys(obj).forEach(key => {
this.defineReactive(obj, key, obj[key]);
});
}
defineReactive(obj, key, val) {
this.observe(val);
const dep = new Dep();
const _this = this;
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.addSub(Dep.target);
if (Array.isArray(val)) {
val.__dep__ = dep; // 如果 val 是数组,将它的 __dep__ 属性设置为这个依赖
}
}
return val;
},
set(newVal) {
if (newVal === val) {
return;
}
val = newVal;
_this.observe(newVal);
dep.notify();
}
});
}
observeArray(arr) {
arr.__proto__ = this.arrayMethods();
arr.__dep__ = new Dep(); // 添加这行,初始化数组的依赖
for(let i = 0; i < arr.length; i++) {
this.observe(arr[i]);
}
}
arrayMethods() {
let arrProto = Array.prototype;
let arrayMethods = Object.create(arrProto);
let _this = this;
[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
].forEach(method => {
arrayMethods[method] = function () {
const result = arrProto[method].apply(this, arguments);
let inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = arguments;
break;
case 'splice':
inserted = arguments.slice(2);
break;
}
if (inserted) _this.observeArray(inserted);
debugger
if (this.__dep__) {
this.__dep__.notify();
}
return result;
};
});
return arrayMethods;
}
proxyMethods() {
for (let key in this.$methods) {
this[key] = this.$methods[key].bind(this);
}
}
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
compile(el) {
const vm = this;
const {
childNodes
} = el;
[...childNodes].forEach(node => {
if (node.nodeType === ELEMENT_NODE && node.hasAttribute('v-pre')) {
return;
}
if (node.nodeType === ELEMENT_NODE) {
this.compile(node);
} else if (node.nodeType === TEXT_NODE) {
let {
originalTextContent
} = node;
if (!originalTextContent) {
originalTextContent = node.textContent;
node.originalTextContent = originalTextContent;
}
let reg = /\{\{(.+?)\}\}/g;
const updateTextContent = () => {
node.textContent = originalTextContent.replace(reg, (_, key) => {
return this.getVal(vm, key.trim());
});
};
updateTextContent();
let match;
while ((match = reg.exec(originalTextContent)) !== null) {
const key = match[1].trim();
new Watcher(vm, key, updateTextContent);
}
}
});
el.removeAttribute('v-cloak');
}
getVal(vm, expr) {
const keys = expr.split(/[\.\[\]]/).filter(Boolean);
return keys.reduce((prev, next) => {
if (next.match(/^\d+$/)) { // 检查是否为数字
return prev[parseInt(next, 10)];
}
return prev[next];
}, vm);
}
}
function compileAttributes(attributes, node, vm) {
attributes.forEach(attr => {
if (attr.name === 'v-text') {
handleVText(node, attr, vm);
} else if (attr.name === 'v-html') {
handleVHtml(node, attr, vm);
} else if (attr.name.startsWith('v-bind:') || attr.name.startsWith(':')) {
handleVBind(node, attr, vm);
} else if (attr.name.startsWith('v-on:') || attr.name.startsWith('@')) {
handleEvent(node, attr, vm);
}
});
}
function handleVText(node, attr, vm) {
const expr = attr.value;
node.textContent = vm[expr];
}
function handleVHtml(node, attr, vm) {
const expr = attr.value;
node.innerHTML = vm[expr];
}
function handleVBind(node, attr, vm) {
let attrName = attr.name;
if (attrName.startsWith(':')) {
attrName = `v-bind${attrName}`;
}
attrName = attrName.slice(7);
const key = attr.value;
node.setAttribute(attrName, vm[key]);
}
function handleEvent(node, attr, vm) {
let attrName = attr.name;
if (attrName.startsWith('@')) {
attrName = `v-on:${attrName.slice(1)}`;
}
const [eventName, ...modifiers] = attrName.slice(5).split('.');
const methodExpression = attr.value.trim();
let methodName = methodExpression;
let args = [];
let parenIndex = methodExpression.indexOf('(');
if (parenIndex !== -1) {
methodName = methodExpression.substring(0, parenIndex);
const argsString = methodExpression.substring(parenIndex + 1, methodExpression.length - 1).trim();
args = argsString.split(',').map(arg => arg.trim());
}
let options = {
capture: modifiers.includes('capture'),
once: modifiers.includes('once')
};
node.addEventListener(eventName, event => {
if (modifiers.includes('stop')) event.stopPropagation();
if (modifiers.includes('prevent')) event.preventDefault();
if (modifiers.includes('self')) {
if (event.target !== event.currentTarget) {
return;
}
}
const actualArgs = args.map(arg => {
if (!isNaN(arg)) return arg;
if (arg === '$event') return event;
if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
return vm[arg];
});
vm[methodName](...actualArgs);
}, options);
}
v-model
是 Vue.js 中一个非常重要的指令,用于实现数据的双向绑定。这意味着,该指令能让你将输入控件(如 <input>
、<textarea>
或 <select>
)与数据对象的一个属性进行绑定,当其中一方变化时,另一方也会相应地变化。
这是一个基础的使用 v-model
的例子:
<template>
<div>
<input v-model="message">
<p>你输入的内容是:{{ message }}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: ''
};
}
}
</script>
在这个例子中,<input>
元素与 message
数据属性双向绑定。也就是说,当用户在输入框中输入文本时,message
数据属性会自动更新;反过来,如果 message
数据属性变化,输入框中的内容也会跟着变。
如何工作的?
实际上,v-model
是 v-bind
和 v-on
的语法糖。上述的例子等效于:
<template>
<div>
<input :value="message" @input="message = $event.target.value">
<p>你输入的内容是:{{ message }}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: ''
};
}
}
</script>
在这个等效的例子中,:value="message"
使输入框的值与 message
数据属性绑定(单向绑定),而 @input="message = $event.target.value"
监听输入框的 input
事件,并在事件发生时更新 message
数据属性。
应用场景
<input>
、<textarea>
、<select>
等。v-model
。v-model
是 Vue.js 中一个非常实用的指令,它简化了表单和组件与数据之间双向通信的逻辑。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue</title>
</head>
<body>
<div id="app">
<input v-model="msg">
<p>msg is: {{ msg }}</p>
</div>
<script src="my-vue.js"></script>
<script>
new Vue({
el: '#app',
data: {
msg: 'Hello'
}
})
</script>
</body>
</html>
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
Dep.target = this;
this.oldValue = this.vm[this.expr];
Dep.target = null;
}
update() {
const newValue = this.vm[this.expr];
if (this.oldValue !== newValue) {
this.cb(newValue);
this.oldValue = newValue;
}
}
}
class Vue {
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
this.init();
}
init() {
this.proxyData();
this.proxyMethods();
this.observe(this.$data);
this.compile(this.$el);
}
proxyMethods() {
for (let key in this.$methods) {
this[key] = this.$methods[key].bind(this);
}
}
observe(obj) {
for (let key in obj) {
let value = obj[key];
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.addSub(Dep.target);
}
return value;
},
set: (newValue) => {
if (newValue !== value) {
value = newValue;
dep.notify();
}
}
});
}
}
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
compile(el) {
const vm = this;
const { childNodes } = el;
Array.from(childNodes).forEach(node => {
if (node.nodeType === ELEMENT_NODE) {
if (node.nodeType === 1 && node.hasAttribute('v-pre')) {
return;
}
const attrs = node.attributes;
Array.from(attrs).forEach(attr => {
if (attr.name === 'v-text') {
handleVText(node, attr, vm);
} else if (attr.name === 'v-html') {
handleVHtml(node, attr, vm);
} else if (attr.name.startsWith('v-bind:')) {
handleVBind(node, attr, vm);
+ }else if (attr.name.startsWith('@')) {
+ handleEvent(node, attr, vm);
+ }else if (attr.name.startsWith('v-on:')) {
+ handleEvent(node, attr, vm);
+ }else if (attr.name === 'v-model') {
+ handleVModel(node, attr, vm);
+ }
});
this.compile(node);
} else if (node.nodeType === TEXT_NODE) {
const originalTextContent = node.textContent;
const updateTextContent = () => {
let text = originalTextContent;
text = text.replace(/\{\{(.+?)\}\}/g, (_, key) => {
return vm[key.trim()];
});
node.textContent = text;
};
updateTextContent();
let match;
const reg = /\{\{(.+?)\}\}/g;
while ((match = reg.exec(originalTextContent)) !== null) {
const key = match[1].trim();
new Watcher(vm, key, updateTextContent);
}
}
});
}
}
+function handleVModel(node, attr, vm) {
+ const key = attr.value;
+ node.value = vm[key];
+ new Watcher(vm, key, function() {
+ node.value = vm[key];
+ });
+ node.addEventListener('input', function() {
+ vm[key] = node.value;
+ });
+}
function handleVText(node, attr, vm) {
const expr = attr.value;
new Watcher(vm, expr, newValue => {
node.textContent = newValue;
});
node.textContent = vm[expr];
}
function handleVHtml(node, attr, vm) {
const expr = attr.value;
new Watcher(vm, expr, newValue => {
node.innerHTML = newValue;
});
node.innerHTML = vm[expr];
}
function handleVBind(node, attr, vm) {
const attrName = attr.name.slice(7);
const key = attr.value;
new Watcher(vm, key, newValue => {
node.setAttribute(attrName, newValue);
});
node.setAttribute(attrName, vm[key]);
}
function handleEvent(node, attr, vm) {
let attrName = attr.name;
if(attrName.startsWith('@')){
attrName=`v-on:${attr.name.slice(1)}`;
}
let [eventName, ...modifiers] = attrName.slice(5).split('.');
const methodExpression = attr.value.trim();
let methodName = methodExpression;
let args = [];
const parenIndex = methodExpression.indexOf('(');
if (parenIndex > -1) {
methodName = methodExpression.substring(0, parenIndex);
const argsString = methodExpression.substring(
parenIndex + 1,
methodExpression.length - 1
).trim();
args = argsString.split(',').map(arg => arg.trim());
}
const options = {
capture: modifiers.includes('capture'),
once: modifiers.includes('once'),
passive: modifiers.includes('passive')
};
node.addEventListener(eventName, function handler(event) {
if (modifiers.includes('stop')) event.stopPropagation();
if (modifiers.includes('prevent')) event.preventDefault();
if (modifiers.includes('self') && event.target !== event.currentTarget) return;
const keyCodes = {
enter: 13,
tab: 9,
delete: [8, 46],
esc: 27,
space: 32,
up: 38,
down: 40,
left: 37,
right: 39
};
const keyCode = keyCodes[modifiers.find(mod => keyCodes.hasOwnProperty(mod))];
if (keyCode) {
if (Array.isArray(keyCode)) {
if (!keyCode.includes(event.keyCode)) return;
} else if (event.keyCode !== keyCode) {
return;
}
}
const actualArgs = args.map(arg => {
if (arg === '$event') return event;
if (!isNaN(arg)) return Number(arg);
if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
return vm[arg];
});
vm[methodName](...actualArgs);
if (modifiers.includes('once')) {
node.removeEventListener(eventName, handler);
}
}, options);
}
基本用法
<div v-once>
{{ message }}
</div>
在这个例子中,不论 message
如何改变,该 <div>
元素及其内部的内容只会渲染一次。
组件中的使用
您也可以在自定义组件上使用 v-once
。
<my-component v-once></my-component>
使用 v-once
的组件仅会初始化和渲染一次,之后即使传入的 props 改变了,也不会再次渲染。
结合 v-for 使用
如果您有一个不会改变的列表,您也可以结合 v-for
使用 v-once
。
<ul>
<li v-for="item in items" v-once>{{ item }}</li>
</ul>
请注意,在这里使用 v-once
能显著提高列表的渲染性能,因为列表项不会重新渲染。
使用场景
虽然 v-once
在某些性能优化场景中很有用,但请注意不要过度使用它。过度使用可能导致应用逻辑变得难以理解和维护。只有在确定某个元素或组件确实不需要重新渲染时,才应使用 v-once
。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue</title>
</head>
<body>
<div id="app">
<span v-once>永不改变: {{msg}}</span>
</div>
<script src="my-vue.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
msg: 'hello'
}
})
vm.msg = 'world'
</script>
</body>
</html>
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
Dep.target = this;
this.oldValue = this.vm[this.expr];
Dep.target = null;
}
update() {
const newValue = this.vm[this.expr];
if (this.oldValue !== newValue) {
this.cb(newValue);
this.oldValue = newValue;
}
}
}
class Vue {
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
this.init();
}
init() {
this.proxyData();
this.proxyMethods();
this.observe(this.$data);
this.compile(this.$el);
}
proxyMethods() {
for (let key in this.$methods) {
this[key] = this.$methods[key].bind(this);
}
}
observe(obj) {
for (let key in obj) {
let value = obj[key];
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.addSub(Dep.target);
}
return value;
},
set: (newValue) => {
if (newValue !== value) {
value = newValue;
dep.notify();
}
}
});
}
}
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
+ compile(el,once) {
const vm = this;
const { childNodes } = el;
Array.from(childNodes).forEach(node => {
if (node.nodeType === ELEMENT_NODE) {
if (node.nodeType === 1 && node.hasAttribute('v-pre')) {
return;
}
const attrs = node.attributes;
Array.from(attrs).forEach(attr => {
if (attr.name === 'v-text') {
handleVText(node, attr, vm);
} else if (attr.name === 'v-html') {
handleVHtml(node, attr, vm);
} else if (attr.name.startsWith('v-bind:')) {
handleVBind(node, attr, vm);
} else if (attr.name.startsWith('v-on:')) {
handleVOn(node, attr, vm);
} else if (attr.name === 'v-model') {
handleVModel(node, attr, vm);
}
});
+ if (node.hasAttribute('v-once')) {
+ this.compile(node,true);
+ }else{
+ this.compile(node);
}
} else if (node.nodeType === TEXT_NODE) {
const originalTextContent = node.textContent;
const updateTextContent = () => {
let text = originalTextContent;
text = text.replace(/\{\{(.+?)\}\}/g, (_, key) => {
return vm[key.trim()];
});
node.textContent = text;
};
updateTextContent();
+ if (!once) {
let match;
const reg = /\{\{(.+?)\}\}/g;
while ((match = reg.exec(originalTextContent)) !== null) {
const key = match[1].trim();
new Watcher(vm, key, updateTextContent);
}
+ }
}
});
}
}
function handleVModel(node, attr, vm) {
const key = attr.value;
node.value = vm[key];
new Watcher(vm, key, function () {
node.value = vm[key];
});
node.addEventListener('input', function () {
vm[key] = node.value;
});
}
function handleVText(node, attr, vm) {
const expr = attr.value;
new Watcher(vm, expr, newValue => {
node.textContent = newValue;
});
node.textContent = vm[expr];
}
function handleVHtml(node, attr, vm) {
const expr = attr.value;
new Watcher(vm, expr, newValue => {
node.innerHTML = newValue;
});
node.innerHTML = vm[expr];
}
function handleVBind(node, attr, vm) {
const attrName = attr.name.slice(7);
const key = attr.value;
new Watcher(vm, key, newValue => {
node.setAttribute(attrName, newValue);
});
node.setAttribute(attrName, vm[key]);
}
function handleVOn(node, attr, vm) {
let [eventName, ...modifiers] = attr.name.slice(5).split('.');
const methodExpression = attr.value.trim();
let methodName = methodExpression;
let args = [];
const parenIndex = methodExpression.indexOf('(');
if (parenIndex > -1) {
methodName = methodExpression.substring(0, parenIndex);
const argsString = methodExpression.substring(
parenIndex + 1,
methodExpression.length - 1
).trim();
args = argsString.split(',').map(arg => arg.trim());
}
const options = {
capture: modifiers.includes('capture'),
once: modifiers.includes('once'),
passive: modifiers.includes('passive')
};
node.addEventListener(eventName, function handler(event) {
if (modifiers.includes('stop')) event.stopPropagation();
if (modifiers.includes('prevent')) event.preventDefault();
if (modifiers.includes('self') && event.target !== event.currentTarget) return;
const keyCodes = {
enter: 13,
tab: 9,
delete: [8, 46],
esc: 27,
space: 32,
up: 38,
down: 40,
left: 37,
right: 39
};
const keyCode = keyCodes[modifiers.find(mod => keyCodes.hasOwnProperty(mod))];
if (keyCode) {
if (Array.isArray(keyCode)) {
if (!keyCode.includes(event.keyCode)) return;
} else if (event.keyCode !== keyCode) {
return;
}
}
const actualArgs = args.map(arg => {
if (arg === '$event') return event;
if (!isNaN(arg)) return Number(arg);
if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
return vm[arg];
});
vm[methodName](...actualArgs);
if (modifiers.includes('once')) {
node.removeEventListener(eventName, handler);
}
}, options);
}
v-show
是 Vue.js 中用于条件渲染的一个指令。使用 v-show
指令可以基于一个表达式的真假值来显示或隐藏一个元素。与 v-if
类似,v-show
也可以用于条件性地显示或隐藏内容,但两者的工作机制有所不同。
基础用法
<!-- isVisible 是一个布尔值 -->
<div v-show="isVisible">我是可见的</div>
在这个例子中,如果 isVisible
的值为 true
,那么这个 <div>
元素会显示在页面上。如果 isVisible
的值为 false
,这个 <div>
元素会被隐藏。
工作机制
与 v-if
不同的是,v-show
不会从 DOM 中添加或删除元素。而是通过改变 CSS 的 display
属性来控制元素的可见性。当条件为 false
时,v-show
会给元素添加一个 display: none
的样式。
优点
缺点
v-if
。v-show 与 v-if 的选择
v-show
通常是更好的选择。v-if
是更好的选择,因为它真的会从 DOM 中移除元素。注意事项
v-show
不支持 <template>
元素,也不支持 v-else
和 v-else-if
。v-show
在初始化时总是会渲染元素,即使条件为 false
。<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue</title>
</head>
<body>
<div id="app">
<div v-show="isShowing">Hello</div>
<button v-on:click="toggle">toggle</button>
</div>
<script src="my-vue.js"></script>
<script>
new Vue({
el: '#app',
data: {
isShowing: true
},
methods: {
toggle: function () {
this.isShowing = !this.isShowing;
}
}
})
</script>
</body>
</html>
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
Dep.target = this;
this.oldValue = this.vm[this.expr];
Dep.target = null;
}
update() {
const newValue = this.vm[this.expr];
if (this.oldValue !== newValue) {
this.cb(newValue);
this.oldValue = newValue;
}
}
}
class Vue {
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
this.init();
}
init() {
this.proxyData();
this.proxyMethods();
this.observe(this.$data);
this.compile(this.$el);
}
proxyMethods() {
for (let key in this.$methods) {
this[key] = this.$methods[key].bind(this);
}
}
observe(obj) {
for (let key in obj) {
let value = obj[key];
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.addSub(Dep.target);
}
return value;
},
set: (newValue) => {
if (newValue !== value) {
value = newValue;
dep.notify();
}
}
});
}
}
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
compile(el,once) {
const vm = this;
const { childNodes } = el;
Array.from(childNodes).forEach(node => {
if (node.nodeType === ELEMENT_NODE) {
if (node.nodeType === 1 && node.hasAttribute('v-pre')) {
return;
}
const attrs = node.attributes;
Array.from(attrs).forEach(attr => {
if (attr.name === 'v-text') {
handleVText(node, attr, vm);
} else if (attr.name === 'v-html') {
handleVHtml(node, attr, vm);
} else if (attr.name.startsWith('v-bind:')) {
handleVBind(node, attr, vm);
} else if (attr.name.startsWith('v-on:')) {
handleVOn(node, attr, vm);
} else if (attr.name === 'v-model') {
handleVModel(node, attr, vm);
+ }else if (attr.name === 'v-show') {
+ handleVShow(node, attr, vm);
+ }
});
if (node.hasAttribute('v-once')) {
this.compile(node,true);
}else{
this.compile(node);
}
} else if (node.nodeType === TEXT_NODE) {
const originalTextContent = node.textContent;
const updateTextContent = () => {
let text = originalTextContent;
text = text.replace(/\{\{(.+?)\}\}/g, (_, key) => {
return vm[key.trim()];
});
node.textContent = text;
};
updateTextContent();
if (!once) {
let match;
const reg = /\{\{(.+?)\}\}/g;
while ((match = reg.exec(originalTextContent)) !== null) {
const key = match[1].trim();
new Watcher(vm, key, updateTextContent);
}
}
}
});
}
}
+function handleVShow(node, attr, vm) {
+ const expr = attr.value;
+ const update = () => {
+ node.style.display = vm[expr] ? '' : 'none';
+ };
+ update();
+ new Watcher(vm, expr, update);
+}
function handleVModel(node, attr, vm) {
const key = attr.value;
node.value = vm[key];
new Watcher(vm, key, function () {
node.value = vm[key];
});
node.addEventListener('input', function () {
vm[key] = node.value;
});
}
function handleVText(node, attr, vm) {
const expr = attr.value;
new Watcher(vm, expr, newValue => {
node.textContent = newValue;
});
node.textContent = vm[expr];
}
function handleVHtml(node, attr, vm) {
const expr = attr.value;
new Watcher(vm, expr, newValue => {
node.innerHTML = newValue;
});
node.innerHTML = vm[expr];
}
function handleVBind(node, attr, vm) {
const attrName = attr.name.slice(7);
const key = attr.value;
new Watcher(vm, key, newValue => {
node.setAttribute(attrName, newValue);
});
node.setAttribute(attrName, vm[key]);
}
function handleVOn(node, attr, vm) {
let [eventName, ...modifiers] = attr.name.slice(5).split('.');
const methodExpression = attr.value.trim();
let methodName = methodExpression;
let args = [];
const parenIndex = methodExpression.indexOf('(');
if (parenIndex > -1) {
methodName = methodExpression.substring(0, parenIndex);
const argsString = methodExpression.substring(
parenIndex + 1,
methodExpression.length - 1
).trim();
args = argsString.split(',').map(arg => arg.trim());
}
const options = {
capture: modifiers.includes('capture'),
once: modifiers.includes('once'),
passive: modifiers.includes('passive')
};
node.addEventListener(eventName, function handler(event) {
if (modifiers.includes('stop')) event.stopPropagation();
if (modifiers.includes('prevent')) event.preventDefault();
if (modifiers.includes('self') && event.target !== event.currentTarget) return;
const keyCodes = {
enter: 13,
tab: 9,
delete: [8, 46],
esc: 27,
space: 32,
up: 38,
down: 40,
left: 37,
right: 39
};
const keyCode = keyCodes[modifiers.find(mod => keyCodes.hasOwnProperty(mod))];
if (keyCode) {
if (Array.isArray(keyCode)) {
if (!keyCode.includes(event.keyCode)) return;
} else if (event.keyCode !== keyCode) {
return;
}
}
const actualArgs = args.map(arg => {
if (arg === '$event') return event;
if (!isNaN(arg)) return Number(arg);
if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
return vm[arg];
});
vm[methodName](...actualArgs);
if (modifiers.includes('once')) {
node.removeEventListener(eventName, handler);
}
}, options);
}
v-if
, v-else-if
, 和 v-else
是 Vue.js 中用于条件渲染的指令。这些指令允许你根据一些条件动态地添加或移除 DOM 元素。
v-if
v-if
用于条件性地渲染一个块。该块只会在指令的表达式返回 true
时被渲染。
<div v-if="showMessage">
Hello, World!
</div>
new Vue({
data: {
showMessage: true
}
});
在这个例子中,当 showMessage
的值为 true
时,<div>
会被添加到 DOM 中。当 showMessage
的值变为 false
时,该 <div>
会从 DOM 中移除。
v-else-if
v-else-if
用于表示一个“否则,如果”的块,它必须紧跟在一个 v-if
或另一个 v-else-if
块之后。
<div v-if="type === 'A'">
A 类型
</div>
<div v-else-if="type === 'B'">
B 类型
</div>
v-else
v-else
用于表示一个“否则”的块,它必须紧跟在一个 v-if
或 v-else-if
块之后。
<div v-if="type === 'A'">
A 类型
</div>
<div v-else>
非 A 类型
</div>
组合使用
你可以将这三个指令组合在一起,创建一个完整的条件块链。
<div v-if="type === 'A'">
A 类型
</div>
<div v-else-if="type === 'B'">
B 类型
</div>
<div v-else>
其他类型
</div>
注意事项
v-if
是“惰性”的:如果其表达式的初始条件为假,该指令将不做任何事情——直到条件首次变为真并使其工作为止。
v-if
与 v-for
一起使用时要特别小心:使用 v-if
和 v-for
在同一元素上是通常不推荐的,因为其优先级比较复杂,并且可能导致性能问题。
条件块可以通过 template
标签进行分组,这样你就可以在单个 v-if
表达式下有多个元素。
<template v-if="showMessage">
<div>Message 1</div>
<div>Message 2</div>
</template>
当与 v-else
和 v-else-if
一起使用时,v-if
可以用于 template
标签,从而控制多个元素。但是 v-else
和 v-else-if
本身不能应用在 template
标签上。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue</title>
</head>
<body>
<div id="app">
<div v-if="value === 'A'">A</div>
<div v-else-if="value === 'B'">B</div>
<div v-else>C</div>
<button v-on:click="changeValue">changeValue</button>
</div>
<script src="my-vue.js"></script>
<script>
new Vue({
el: '#app',
data: {
value: 'A'
},
methods: {
changeValue: function () {
if (this.value === 'A') {
this.value = 'B';
} else if (this.value === 'B') {
this.value = 'C';
}else {
this.value = 'A';
}
}
}
})
</script>
</body>
</html>
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
class Watcher {
+ constructor(vm, exprOrFn, cb) {
this.vm = vm;
this.cb = cb;
+ if (typeof exprOrFn === 'function') {
+ this.getter = exprOrFn;
+ } else {
+ this.getter = () => {
+ return vm[exprOrFn];
+ };
+ }
Dep.target = this;
+ this.oldValue = this.get();
Dep.target = null;
}
+ get() {
+ return this.getter.call(this.vm);
+ }
update() {
+ const newValue = this.get();
if (this.oldValue !== newValue) {
this.cb(newValue);
this.oldValue = newValue;
}
}
}
class Vue {
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
this.init();
}
init() {
this.proxyData();
this.proxyMethods();
this.observe(this.$data);
this.compile(this.$el);
}
proxyMethods() {
for (let key in this.$methods) {
this[key] = this.$methods[key].bind(this);
}
}
observe(obj) {
for (let key in obj) {
let value = obj[key];
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.addSub(Dep.target);
}
return value;
},
set: (newValue) => {
if (newValue !== value) {
value = newValue;
dep.notify();
}
}
});
}
}
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
compile(el, once) {
const vm = this;
const { childNodes } = el;
+ const conditionalElements = [];
Array.from(childNodes).forEach(node => {
if (node.nodeType === ELEMENT_NODE) {
+ if (
+ node.hasAttribute('v-if') ||
+ node.hasAttribute('v-else-if') ||
+ node.hasAttribute('v-else')
+ ) {
+ conditionalElements.push(node);
+ }
if (node.nodeType === 1 && node.hasAttribute('v-pre')) {
return;
}
const attrs = node.attributes;
Array.from(attrs).forEach(attr => {
if (attr.name === 'v-text') {
handleVText(node, attr, vm);
} else if (attr.name === 'v-html') {
handleVHtml(node, attr, vm);
} else if (attr.name.startsWith('v-bind:')) {
handleVBind(node, attr, vm);
} else if (attr.name.startsWith('v-on:')) {
handleVOn(node, attr, vm);
} else if (attr.name === 'v-model') {
handleVModel(node, attr, vm);
}
});
if (node.hasAttribute('v-once')) {
this.compile(node, true);
} else {
this.compile(node);
}
} else if (node.nodeType === TEXT_NODE) {
const originalTextContent = node.textContent;
const updateTextContent = () => {
let text = originalTextContent;
text = text.replace(/\{\{(.+?)\}\}/g, (_, key) => {
return vm[key.trim()];
});
node.textContent = text;
};
updateTextContent();
if (!once) {
let match;
const reg = /\{\{(.+?)\}\}/g;
while ((match = reg.exec(originalTextContent)) !== null) {
const key = match[1].trim();
new Watcher(vm, key, updateTextContent);
}
}
}
});
+ if (conditionalElements.length > 0)
+ handleConditionals(conditionalElements, vm);
}
}
+function handleConditionals(conditionalElements, vm) {
+ const elementsData = conditionalElements.map(element => {
+ const placeholder = document.createComment('placeholder');
+ element.parentNode.insertBefore(placeholder, element);
+ element.parentNode.removeChild(element);
+ return {
+ placeholder,
+ element,
+ evaluate: getEvaluateFn(element.getAttribute('v-if') || element.getAttribute('v-else-if') || 'true', vm)
+ };
+ });
+ const updateVisibility = () => {
+ let firstVisibleElementInserted = false;
+ elementsData.forEach(({
+ placeholder,
+ element,
+ evaluate
+ }) => {
+ const {
+ parentNode
+ } = placeholder;
+ const shouldDisplay = evaluate();
+ const isDisplayed = parentNode.contains(element);
+ if (shouldDisplay && !firstVisibleElementInserted) {
+ if (!isDisplayed) {
+ parentNode.insertBefore(element, placeholder);
+ }
+ firstVisibleElementInserted = true;
+ } else if (isDisplayed) {
+ parentNode.removeChild(element);
+ }
+ if (shouldDisplay && !firstVisibleElementInserted) {
+ parentNode.insertBefore(element, placeholder);
+ firstVisibleElementInserted = evaluate();
+ }
+ });
+ };
+ updateVisibility();
+ new Watcher(vm,() => elementsData.map(({ evaluate }) => evaluate()),updateVisibility);
+}
+function getEvaluateFn(expr, vm) {
+ return new Function('with(this) { return ' + expr + ' }').bind(vm.$data);
+}
function handleVModel(node, attr, vm) {
const key = attr.value;
node.value = vm[key];
new Watcher(vm, key, function () {
node.value = vm[key];
});
node.addEventListener('input', function () {
vm[key] = node.value;
});
}
function handleVText(node, attr, vm) {
const expr = attr.value;
new Watcher(vm, expr, newValue => {
node.textContent = newValue;
});
node.textContent = vm[expr];
}
function handleVHtml(node, attr, vm) {
const expr = attr.value;
new Watcher(vm, expr, newValue => {
node.innerHTML = newValue;
});
node.innerHTML = vm[expr];
}
function handleVBind(node, attr, vm) {
const attrName = attr.name.slice(7);
const key = attr.value;
new Watcher(vm, key, newValue => {
node.setAttribute(attrName, newValue);
});
node.setAttribute(attrName, vm[key]);
}
function handleVOn(node, attr, vm) {
let [eventName, ...modifiers] = attr.name.slice(5).split('.');
const methodExpression = attr.value.trim();
let methodName = methodExpression;
let args = [];
const parenIndex = methodExpression.indexOf('(');
if (parenIndex > -1) {
methodName = methodExpression.substring(0, parenIndex);
const argsString = methodExpression.substring(
parenIndex + 1,
methodExpression.length - 1
).trim();
args = argsString.split(',').map(arg => arg.trim());
}
const options = {
capture: modifiers.includes('capture'),
once: modifiers.includes('once'),
passive: modifiers.includes('passive')
};
node.addEventListener(eventName, function handler(event) {
if (modifiers.includes('stop')) event.stopPropagation();
if (modifiers.includes('prevent')) event.preventDefault();
if (modifiers.includes('self') && event.target !== event.currentTarget) return;
const keyCodes = {
enter: 13,
tab: 9,
delete: [8, 46],
esc: 27,
space: 32,
up: 38,
down: 40,
left: 37,
right: 39
};
const keyCode = keyCodes[modifiers.find(mod => keyCodes.hasOwnProperty(mod))];
if (keyCode) {
if (Array.isArray(keyCode)) {
if (!keyCode.includes(event.keyCode)) return;
} else if (event.keyCode !== keyCode) {
return;
}
}
const actualArgs = args.map(arg => {
if (arg === '$event') return event;
if (!isNaN(arg)) return Number(arg);
if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
return vm[arg];
});
vm[methodName](...actualArgs);
if (modifiers.includes('once')) {
node.removeEventListener(eventName, handler);
}
}, options);
}
v-for
是 Vue.js 中用于渲染列表的指令。这个指令允许你遍历数组或对象,并为每个项生成一个子模板。
基础用法
最常见的用法是遍历一个数组:
<ul>
<li v-for="item in items">{{ item.text }}</li>
</ul>
对应的 Vue 实例中的 data
对象可能是这样的:
new Vue({
el: '#app',
data: {
items: [
{ text: 'Item 1' },
{ text: 'Item 2' },
{ text: 'Item 3' }
]
}
});
这会生成一个包含三个列表项 (li
) 的无序列表 (ul
)。
遍历对象
除了数组,你也可以使用 v-for
来遍历对象的属性:
<ul>
<li v-for="(value, key) in object">
{{ key }}: {{ value }}
</li>
</ul>
对应的 Vue 实例:
new Vue({
el: '#app',
data: {
object: {
firstName: 'John',
lastName: 'Doe',
age: 30
}
}
});
索引和键
当遍历数组时,你也可以访问当前项的索引:
<ul>
<li v-for="(item, index) in items">{{ index }}: {{ item.text }}</li>
</ul>
当遍历对象时,除了属性值和键,还可以访问当前属性的索引:
<ul>
<li v-for="(value, key, index) in object">
{{ index }}. {{ key }}: {{ value }}
</li>
</ul>
key
属性
当使用 v-for
与 Vue 组件一起使用时,推荐为每个循环项添加一个唯一的 key
属性。这有助于 Vue 更有效地更新 DOM:
<my-component
v-for="(item, index) in items"
:key="item.id"
:item="item"
></my-component>
在范围内遍历
v-for
也可以用于重复一个模板多次(但没有绑定数据):
<span v-for="n in 5">{{ n }} </span>
这将输出 "1 2 3 4 5 "。
注意事项
v-for
和 v-if
。因为 v-for
优先级比 v-if
更高,可能导致不易预料的行为。<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue</title>
</head>
<body>
<div id="app">
<ul >
<li v-for="item in items">
{{ item.message }}
</li>
</ul>
</div>
<script src="my-vue.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
items: [
{ message: 'hello' },
{ message: 'world' }
]
}
})
vm.items = [
{ message: 'aaa' },
{ message: 'bbb' }
];
</script>
</body>
</html>
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
class Watcher {
constructor(vm, exprOrFn, cb) {
this.vm = vm;
this.cb = cb;
if (typeof exprOrFn === 'function') {
this.getter = exprOrFn;
} else {
this.getter = () => {
return vm[exprOrFn];
};
}
Dep.target = this;
this.oldValue = this.get();
Dep.target = null;
}
get() {
return this.getter.call(this.vm);
}
update() {
const newValue = this.get();
if (this.oldValue !== newValue) {
this.cb(newValue);
this.oldValue = newValue;
}
}
}
class Vue {
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
this.init();
}
init() {
this.proxyData();
this.proxyMethods();
this.observe(this.$data);
this.compile(this.$el);
}
proxyMethods() {
for (let key in this.$methods) {
this[key] = this.$methods[key].bind(this);
}
}
observe(obj) {
for (let key in obj) {
let value = obj[key];
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.addSub(Dep.target);
}
return value;
},
set: (newValue) => {
if (newValue !== value) {
value = newValue;
dep.notify();
}
}
});
}
}
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
+ compile(el, once, scope = this) {
+ const vm = scope;
const { childNodes } = el;
const conditionalElements = [];
Array.from(childNodes).forEach(node => {
if (node.nodeType === ELEMENT_NODE) {
if (
node.hasAttribute('v-if') ||
node.hasAttribute('v-else-if') ||
node.hasAttribute('v-else')
) {
conditionalElements.push(node);
}
if (node.nodeType === 1 && node.hasAttribute('v-pre')) {
return;
}
const attrs = node.attributes;
Array.from(attrs).forEach(attr => {
if (attr.name === 'v-text') {
handleVText(node, attr, vm);
} else if (attr.name === 'v-html') {
handleVHtml(node, attr, vm);
} else if (attr.name.startsWith('v-bind:')) {
handleVBind(node, attr, vm);
} else if (attr.name.startsWith('v-on:')) {
handleVOn(node, attr, vm);
} else if (attr.name === 'v-model') {
handleVModel(node, attr, vm);
+ } else if (attr.name === 'v-for') {
+ handleVFor(node, attr, vm,once);
}
});
if (node.hasAttribute('v-once')) {
this.compile(node, true);
} else {
this.compile(node);
}
} else if (node.nodeType === TEXT_NODE) {
const originalTextContent = node.textContent;
const updateTextContent = () => {
let text = originalTextContent;
text = text.replace(/\{\{(.+?)\}\}/g, (_, key) => {
+ return resolveData(vm, key.trim());
});
node.textContent = text;
};
updateTextContent();
if (!once) {
let match;
const reg = /\{\{(.+?)\}\}/g;
while ((match = reg.exec(originalTextContent)) !== null) {
const key = match[1].trim();
new Watcher(vm, key, updateTextContent);
}
}
}
});
if (conditionalElements.length > 0)
handleConditionals(conditionalElements, vm);
}
}
+function resolveData(obj, key) {
+ const keys = key.split('.');
+ let value = obj;
+ for (const k of keys) {
+ if (value == null) return;
+ value = value[k];
+ }
+ return value;
+}
+function handleVFor(node, attr, vm,once) {
+ const originalNode = node.cloneNode(true);
+ const parentNode = node.parentNode;
+ const placeholder = document.createComment('vue-placeholder');
+ parentNode.replaceChild(placeholder, node);
+ const expression = attr.value;
+ const [itemVar, , listKey] = expression.split(' ');
+ const update = () => {
+ while (placeholder.nextSibling) {
+ parentNode.removeChild(placeholder.nextSibling);
+ }
+ const newScope = Object.create(vm);
+ vm[listKey].forEach((itemData, index) => {
+ const clone = originalNode.cloneNode(true);
+ parentNode.appendChild(clone);
+ newScope[itemVar] = itemData;
+ newScope.index = index;
+ vm.compile(clone,once,newScope);
+ });
+ };
+ new Watcher(vm, listKey, update);
+ update();
+}
function handleConditionals(conditionalElements, vm) {
const elementsData = conditionalElements.map(element => {
const placeholder = document.createComment('vue-placeholder');
element.parentNode.insertBefore(placeholder, element);
element.parentNode.removeChild(element);
return {
evaluateCondition: createConditionEvaluator(element, vm),
placeholder,
element,
};
});
const updateVisibility = () => {
let firstVisibleElementInserted = false;
elementsData.forEach(({ evaluateCondition, element, placeholder }) => {
const parentNode = placeholder.parentNode;
const shouldDisplay = evaluateCondition();
const isDisplayed = parentNode.contains(element);
if (shouldDisplay && !firstVisibleElementInserted) {
if (!isDisplayed) {
parentNode.insertBefore(element, placeholder);
}
firstVisibleElementInserted = true;
} else if (isDisplayed) {
parentNode.removeChild(element);
}
});
};
updateVisibility();
new Watcher(vm, () => elementsData.map(({ evaluateCondition }) => evaluateCondition()).join(','), updateVisibility);
}
function createConditionEvaluator(element, vm) {
const vIf = element.getAttribute('v-if');
const vElseIf = element.getAttribute('v-else-if');
const expression = vIf || vElseIf || 'true';
return getEvaluateExpressionFn(expression, vm);
}
function getEvaluateExpressionFn(expr, vm) {
return new Function('with(this) { return ' + expr + ' }').bind(vm.$data);
}
function handleVModel(node, attr, vm) {
const key = attr.value;
node.value = vm[key];
new Watcher(vm, key, function () {
node.value = vm[key];
});
node.addEventListener('input', function () {
vm[key] = node.value;
});
}
function handleVText(node, attr, vm) {
const expr = attr.value;
new Watcher(vm, expr, newValue => {
node.textContent = newValue;
});
node.textContent = vm[expr];
}
function handleVHtml(node, attr, vm) {
const expr = attr.value;
new Watcher(vm, expr, newValue => {
node.innerHTML = newValue;
});
node.innerHTML = vm[expr];
}
function handleVBind(node, attr, vm) {
const attrName = attr.name.slice(7);
const key = attr.value;
new Watcher(vm, key, newValue => {
node.setAttribute(attrName, newValue);
});
node.setAttribute(attrName, vm[key]);
}
function handleVOn(node, attr, vm) {
let [eventName, ...modifiers] = attr.name.slice(5).split('.');
const methodExpression = attr.value.trim();
let methodName = methodExpression;
let args = [];
const parenIndex = methodExpression.indexOf('(');
if (parenIndex > -1) {
methodName = methodExpression.substring(0, parenIndex);
const argsString = methodExpression.substring(
parenIndex + 1,
methodExpression.length - 1
).trim();
args = argsString.split(',').map(arg => arg.trim());
}
const options = {
capture: modifiers.includes('capture'),
once: modifiers.includes('once'),
passive: modifiers.includes('passive')
};
node.addEventListener(eventName, function handler(event) {
if (modifiers.includes('stop')) event.stopPropagation();
if (modifiers.includes('prevent')) event.preventDefault();
if (modifiers.includes('self') && event.target !== event.currentTarget) return;
const keyCodes = {
enter: 13,
tab: 9,
delete: [8, 46],
esc: 27,
space: 32,
up: 38,
down: 40,
left: 37,
right: 39
};
const keyCode = keyCodes[modifiers.find(mod => keyCodes.hasOwnProperty(mod))];
if (keyCode) {
if (Array.isArray(keyCode)) {
if (!keyCode.includes(event.keyCode)) return;
} else if (event.keyCode !== keyCode) {
return;
}
}
const actualArgs = args.map(arg => {
if (arg === '$event') return event;
if (!isNaN(arg)) return Number(arg);
if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
return vm[arg];
});
vm[methodName](...actualArgs);
if (modifiers.includes('once')) {
node.removeEventListener(eventName, handler);
}
}, options);
}
在 Vue.js 中,计算属性(Computed Properties)是一种特殊类型的属性,可以用于处理复杂逻辑和计算。计算属性是基于依赖属性的值进行计算的,并且其结果会被缓存。这意味着,只有当依赖的属性值改变时,计算属性才会重新计算。
基础用法
下面是一个简单的例子,展示了如何使用计算属性:
<template>
<div>
<p>First name: {{ firstName }}</p>
<p>Last name: {{ lastName }}</p>
<p>Full name: {{ fullName }}</p>
</div>
</template>
<script>
export default {
data() {
return {
firstName: 'John',
lastName: 'Doe',
};
},
computed: {
fullName() {
return this.firstName + ' ' + this.lastName;
},
},
};
</script>
在这个例子中,我们定义了一个计算属性 fullName
,它依赖于 firstName
和 lastName
。当 firstName
或 lastName
改变时,fullName
会自动重新计算。
缓存 vs 方法
你可能会问,为什么不直接使用方法来实现相同的功能?实际上,你确实可以使用方法来做这件事,但计算属性有缓存机制。这意味着,如果依赖的数据没有发生变化,计算属性不会重新计算,而是直接返回之前缓存的结果。这在处理复杂计算或大数据集时会更高效。
Getter 和 Setter
计算属性默认只有 getter ,但你也可以提供一个 setter:
<template>
<div>
<p>Full name: {{ fullName }}</p>
<button @click="changeName">Change Name</button>
</div>
</template>
<script>
export default {
data() {
return {
firstName: 'John',
lastName: 'Doe',
};
},
computed: {
fullName: {
get() {
return this.firstName + ' ' + this.lastName;
},
set(newValue) {
const names = newValue.split(' ');
this.firstName = names[0];
this.lastName = names[1];
},
},
},
methods: {
changeName() {
this.fullName = 'Jane Doe';
},
},
};
</script>
在这个例子中,我们添加了一个 setter ,当计算属性 fullName
被设置一个新值时,它会自动地更新 firstName
和 lastName
。
使用计算属性解决问题
计算属性非常适用于下列情况:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue</title>
</head>
<body>
<div id="app">
<input v-model="firstName">
<input v-model="lastName">
<p>你的全名是: {{ fullName }}</p>
</div>
<script src="my-vue.js"></script>
<script>
new Vue({
el: '#app',
data: {
firstName: 'A',
lastName: 'B'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
</script>
</body>
</html>
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
const KeyMap = {
enter: 'Enter',
delete: ['Backspace', 'Delete'],
up: 'ArrowUp',
down: 'ArrowDown',
left: 'ArrowLeft',
right: 'ArrowRight',
space: 'Space'
};
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
class Watcher {
constructor(vm, exprOrFn, cb) {
this.vm = vm;
this.cb = cb;
this.getter = typeof exprOrFn === 'function' ? exprOrFn : getEvaluate(exprOrFn, vm);
Dep.target = this;
this.oldValue = this.getter();
Dep.target = null;
}
update() {
const newValue = this.getter();
if (this.oldValue !== newValue) {
this.cb(this.oldValue, newValue);
this.oldValue = newValue;
}
}
}
class Vue {
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
this.init();
}
init() {
this.proxyData();
this.proxyMethods();
+ this.initComputed();
this.observe(this.$data);
this.compile(this.$el);
}
+ initComputed() {
+ const computed = this.$options.computed;
+ if (computed) {
+ Object.keys(computed).forEach(key => {
+ new Watcher(
+ this,
+ () => computed[key].call(this),
+ (newValue) => {
+ this[key] = newValue;
+ }
+ );
+ Object.defineProperty(this, key, {
+ get: () => {
+ return computed[key].call(this);
+ },
+ set: () => {
+ console.warn('Computed property "' + key + '" cannot be assigned to');
+ },
+ });
+ });
+ }
+ }
observe(obj) {
for (let key in obj) {
let value = obj[key];
let dep = new Dep();
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.addSub(Dep.target);
}
return value;
},
set(newVal) {
if (newVal !== value) {
value = newVal;
dep.notify();
}
}
});
}
}
proxyMethods() {
for (let key in this.$methods) {
this[key] = this.$methods[key].bind(this);
}
}
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
compile(el, isOnce = false, scope) {
const vm = scope || this;
const {
childNodes
} = el;
const conditionalElements = [];
[...childNodes].forEach(node => {
console.log(node);
if (node.nodeType === ELEMENT_NODE && node.hasAttribute('v-pre')) {
return;
}
if (node.nodeType === ELEMENT_NODE) {
if (node.hasAttribute('v-if') || node.hasAttribute('v-else-if') || node.hasAttribute('v-else')) {
conditionalElements.push(node);
}
isOnce = isOnce || node.hasAttribute('v-once');
compileAttributes([...node.attributes], node, vm, isOnce);
if (el.contains(node)) {
vm.compile(node, isOnce);
}
} else if (node.nodeType === TEXT_NODE) {
let {
originalTextContent
} = node;
if (!originalTextContent) {
originalTextContent = node.textContent;
node.originalTextContent = originalTextContent;
}
let reg = /\{\{(.+?)\}\}/g;
const updateTextContent = () => {
node.textContent = originalTextContent.replace(reg, (_, key) => {
return evaluate(key, vm);
});
};
updateTextContent();
if (!isOnce) {
let match;
while ((match = reg.exec(originalTextContent)) !== null) {
const key = match[1].trim();
new Watcher(vm, key, updateTextContent);
}
}
}
});
if (conditionalElements.length > 0) {
handleConditionalElements(conditionalElements, vm);
}
el.removeAttribute('v-cloak');
}
}
function handleConditionalElements(conditionalElements, vm) {
let lastVisibleElement = null;
conditionalElements.forEach(element => {
let expression = element.getAttribute('v-if') || element.getAttribute('v-else-if') || 'true';
let evaluate = getEvaluate(expression, vm);
element.evaluate = evaluate;
const shouldDisplay = evaluate();
if (shouldDisplay && !lastVisibleElement) {
lastVisibleElement = element;
} else {
element.parentNode.removeChild(element);
}
});
const updateVisibility = () => {
const nextVisibleElement = conditionalElements.find(({
evaluate
}) => {
return evaluate();
});
if (nextVisibleElement && nextVisibleElement !== lastVisibleElement) {
lastVisibleElement.parentNode.replaceChild(nextVisibleElement, lastVisibleElement);
lastVisibleElement = nextVisibleElement;
}
};
new Watcher(vm, () => conditionalElements.map(({
evaluate
}) => evaluate()), updateVisibility);
}
function evaluate(expression, vm) {
return new Function(`with(this){return ` + expression + `}`).call(vm);
}
function getEvaluate(expression, vm) {
return new Function(`with(this){return ` + expression + `}`).bind(vm);
}
function compileAttributes(attributes, node, vm, isOnce) {
attributes.forEach(attr => {
if (attr.name === 'v-text') {
handleVText(node, attr, vm, isOnce);
} else if (attr.name === 'v-html') {
handleVHtml(node, attr, vm, isOnce);
} else if (attr.name.startsWith('v-bind:') || attr.name.startsWith(':')) {
handleVBind(node, attr, vm, isOnce);
} else if (attr.name.startsWith('v-on:') || attr.name.startsWith('@')) {
handleEvent(node, attr, vm);
} else if (attr.name === 'v-model') {
handleVModel(node, attr, vm, isOnce);
} else if (attr.name === 'v-show') {
handleVShow(node, attr, vm, isOnce);
} else if (attr.name === 'v-for') {
handleVFor(node, attr, vm, isOnce);
}
});
}
function handleVFor(node, attr, vm, isOnce) {
const originNode = node.cloneNode(true);
const {
parentNode
} = node;
const placeholder = document.createComment('v-for-placeholder');
parentNode.replaceChild(placeholder, node);
const expression = attr.value;
const [itemVar,, listKey] = expression.split(' ');
const update = () => {
while (placeholder.nextSibling) {
parentNode.removeChild(placeholder.nextSibling);
}
evaluate(listKey, vm).forEach(itemData => {
const clonedNode = originNode.cloneNode(true);
parentNode.appendChild(clonedNode);
const newScope = Object.create(vm);
newScope[itemVar] = itemData;
vm.compile(clonedNode, isOnce, newScope);
});
};
update();
new Watcher(vm, listKey, update);
}
function handleVShow(node, attr, vm, isOnce) {
const expr = attr.value;
const originalDisplay = node.style.display;
const update = () => {
node.style.display = evaluate(expr, vm) ? originalDisplay : 'none';
};
update();
if (!isOnce) {
new Watcher(vm, expr, update);
}
}
function handleVModel(node, attr, vm, isOnce) {
const key = attr.value;
node.value = vm[key];
node.addEventListener('input', () => {
vm[key] = node.value;
});
if (!isOnce) {
new Watcher(vm, key, () => {
node.value = vm[key];
});
}
}
function handleVText(node, attr, vm, isOnce) {
const expr = attr.value;
node.textContent = evaluate(expr, vm);
if (!isOnce) {
new Watcher(vm, expr, (oldValue, newValue) => {
node.textContent = newValue;
});
}
}
function handleVHtml(node, attr, vm, isOnce) {
const expr = attr.value;
node.innerHTML = evaluate(expr, vm);
if (!isOnce) {
new Watcher(vm, expr, () => {
node.innerHTML = evaluate(expr, vm);
});
}
}
function handleVBind(node, attr, vm, isOnce) {
let attrName = attr.name;
if (attrName.startsWith(':')) {
attrName = `v-bind${attrName}`;
}
attrName = attrName.slice(7);
const key = attr.value;
node.setAttribute(attrName, vm[key]);
if (!isOnce) {
new Watcher(vm, key, () => {
node.setAttribute(attrName, vm[key]);
});
}
}
function handleEvent(node, attr, vm) {
let attrName = attr.name;
if (attrName.startsWith('@')) {
attrName = `v-on:${attrName.slice(1)}`;
}
const [eventName, ...modifiers] = attrName.slice(5).split('.');
const methodExpression = attr.value.trim();
let methodName = methodExpression;
let args = [];
let parenIndex = methodExpression.indexOf('(');
if (parenIndex !== -1) {
methodName = methodExpression.substring(0, parenIndex);
const argsString = methodExpression.substring(parenIndex + 1, methodExpression.length - 1).trim();
args = argsString.split(',').map(arg => arg.trim());
} else {
args = ['$event'];
}
let options = {
capture: modifiers.includes('capture'),
once: modifiers.includes('once'),
passive: modifiers.includes('passive')
};
node.addEventListener(eventName, event => {
if (modifiers.includes('stop')) event.stopPropagation();
if (modifiers.includes('prevent')) event.preventDefault();
if (modifiers.includes('self')) {
if (event.target !== event.currentTarget) {
return;
}
}
let modifierKeys = modifiers.map(modifier => KeyMap[modifier]).filter(Boolean).flat();
if (modifierKeys.length > 0) {
if (!modifierKeys.includes(event.key)) {
return;
}
}
let actualArgs = args.map(arg => {
if (!isNaN(arg)) return arg;
if (arg === '$event') return event;
if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
return vm[arg];
});
vm[methodName](...actualArgs);
}, options);
}
在 Vue.js 中,watch
选项提供了一种机制来观察和响应 Vue 实例上的数据变动。当某个数据属性发生变化时,watch
会触发一个函数,该函数用于处理这种变化。这样,你可以以声明式的方式进行数据变化的侧效应处理。
下面是一些使用 watch
的基础示例:
基础用法
new Vue({
data: {
message: 'Hello, Vue!'
},
watch: {
message: function (newVal, oldVal) {
console.log('新值:', newVal, '旧值:', oldVal);
}
}
});
在这个例子中,当 message
的值发生变化时,watch
会触发一个函数,该函数接收新值和旧值作为参数。
深度观察
对于对象或数组,你可能需要深度观察。这样做可以通过设置 deep
为 true
:
new Vue({
data: {
obj: {
key: 'value'
}
},
watch: {
obj: {
handler: function (newVal, oldVal) {
console.log('对象改变');
},
deep: true
}
}
});
这样,即使 obj.key
发生变化,watch
也会触发。
立即执行
有时候你可能需要在页面加载时立即执行一次观察函数,可以设置 immediate: true
:
new Vue({
data: {
message: 'Hello, Vue!'
},
watch: {
message: {
handler: function (newVal, oldVal) {
console.log('值改变');
},
immediate: true
}
}
});
清理与取消观察
你还可以通过 watch
返回的对象,调用其 unwatch
方法来停止观察:
const vm = new Vue({
data: {
message: 'Hello, Vue!'
},
watch: {
message: function (newVal, oldVal) {
console.log('值改变');
}
}
});
const unwatch = vm.$watch('message', function (newVal, oldVal) {
console.log('手动观察');
});
// 取消观察
unwatch();
这样可以使你有更多的灵活性来控制何时开始和停止观察。
watch
主要用于执行异步操作或较大的开销操作,而不是在数据变化时直接更新 DOM。对于后者,计算属性(computed
)通常是更好的选择。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue</title>
</head>
<body>
<div id="app">
{{message}}
</div>
<script src="my-vue.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
message: 'hello'
},
watch: {
message(newVal, oldVal) {
console.log('值改变',newVal, oldVal);
}
}
})
vm.message='world';
</script>
</body>
</html>
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
const KeyMap = {
enter: 'Enter',
delete: ['Backspace', 'Delete'],
up: 'ArrowUp',
down: 'ArrowDown',
left: 'ArrowLeft',
right: 'ArrowRight',
space: 'Space'
};
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
class Watcher {
constructor(vm, exprOrFn, cb) {
this.vm = vm;
this.cb = cb;
this.getter = typeof exprOrFn === 'function' ? exprOrFn : getEvaluate(exprOrFn, vm);
Dep.target = this;
this.oldValue = this.getter();
Dep.target = null;
}
update() {
const newValue = this.getter();
if (this.oldValue !== newValue) {
+ this.cb(newValue, this.oldValue);
this.oldValue = newValue;
}
}
}
class Vue {
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
this.init();
}
init() {
this.proxyData();
this.proxyMethods();
this.observe(this.$data);
this.initComputed();
+ this.initWatch();
this.compile(this.$el);
}
+ initWatch() {
+ const watch = this.$options.watch;
+ if (watch) {
+ Object.keys(watch).forEach(key => {
+ new Watcher(
+ this,
+ key,
+ (newValue, oldValue) => {
+ watch[key].call(this, newValue, oldValue);
+ }
+ );
+ });
+ }
+ }
initComputed() {
const computed = this.$options.computed;
if (computed) {
Object.keys(computed).forEach(key => {
new Watcher(
this,
() => computed[key].call(this),
(newValue) => {
this[key] = newValue;
}
);
Object.defineProperty(this, key, {
get: () => {
return computed[key].call(this);
},
set: () => {
console.warn('Computed property "' + key + '" cannot be assigned to');
},
});
});
}
}
observe(obj) {
for (let key in obj) {
let value = obj[key];
let dep = new Dep();
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.addSub(Dep.target);
}
return value;
},
set(newVal) {
if (newVal !== value) {
value = newVal;
dep.notify();
}
}
});
}
}
proxyMethods() {
for (let key in this.$methods) {
this[key] = this.$methods[key].bind(this);
}
}
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
compile(el, isOnce = false, scope) {
const vm = scope || this;
const {
childNodes
} = el;
const conditionalElements = [];
[...childNodes].forEach(node => {
console.log(node);
if (node.nodeType === ELEMENT_NODE && node.hasAttribute('v-pre')) {
return;
}
if (node.nodeType === ELEMENT_NODE) {
if (node.hasAttribute('v-if') || node.hasAttribute('v-else-if') || node.hasAttribute('v-else')) {
conditionalElements.push(node);
}
isOnce = isOnce || node.hasAttribute('v-once');
compileAttributes([...node.attributes], node, vm, isOnce);
if (el.contains(node)) {
vm.compile(node, isOnce);
}
} else if (node.nodeType === TEXT_NODE) {
let {
originalTextContent
} = node;
if (!originalTextContent) {
originalTextContent = node.textContent;
node.originalTextContent = originalTextContent;
}
let reg = /\{\{(.+?)\}\}/g;
const updateTextContent = () => {
node.textContent = originalTextContent.replace(reg, (_, key) => {
return evaluate(key, vm);
});
};
updateTextContent();
if (!isOnce) {
let match;
while ((match = reg.exec(originalTextContent)) !== null) {
const key = match[1].trim();
new Watcher(vm, key, updateTextContent);
}
}
}
});
if (conditionalElements.length > 0) {
handleConditionalElements(conditionalElements, vm);
}
el.removeAttribute('v-cloak');
}
}
function handleConditionalElements(conditionalElements, vm) {
let lastVisibleElement = null;
conditionalElements.forEach(element => {
let expression = element.getAttribute('v-if') || element.getAttribute('v-else-if') || 'true';
let evaluate = getEvaluate(expression, vm);
element.evaluate = evaluate;
const shouldDisplay = evaluate();
if (shouldDisplay && !lastVisibleElement) {
lastVisibleElement = element;
} else {
element.parentNode.removeChild(element);
}
});
const updateVisibility = () => {
const nextVisibleElement = conditionalElements.find(({
evaluate
}) => {
return evaluate();
});
if (nextVisibleElement && nextVisibleElement !== lastVisibleElement) {
lastVisibleElement.parentNode.replaceChild(nextVisibleElement, lastVisibleElement);
lastVisibleElement = nextVisibleElement;
}
};
new Watcher(vm, () => conditionalElements.map(({
evaluate
}) => evaluate()), updateVisibility);
}
function evaluate(expression, vm) {
return new Function(`with(this){return ` + expression + `}`).call(vm);
}
function getEvaluate(expression, vm) {
return new Function(`with(this){return ` + expression + `}`).bind(vm);
}
function compileAttributes(attributes, node, vm, isOnce) {
attributes.forEach(attr => {
if (attr.name === 'v-text') {
handleVText(node, attr, vm, isOnce);
} else if (attr.name === 'v-html') {
handleVHtml(node, attr, vm, isOnce);
} else if (attr.name.startsWith('v-bind:') || attr.name.startsWith(':')) {
handleVBind(node, attr, vm, isOnce);
} else if (attr.name.startsWith('v-on:') || attr.name.startsWith('@')) {
handleEvent(node, attr, vm);
} else if (attr.name === 'v-model') {
handleVModel(node, attr, vm, isOnce);
} else if (attr.name === 'v-show') {
handleVShow(node, attr, vm, isOnce);
} else if (attr.name === 'v-for') {
handleVFor(node, attr, vm, isOnce);
}
});
}
function handleVFor(node, attr, vm, isOnce) {
const originNode = node.cloneNode(true);
const {
parentNode
} = node;
const placeholder = document.createComment('v-for-placeholder');
parentNode.replaceChild(placeholder, node);
const expression = attr.value;
const [itemVar,, listKey] = expression.split(' ');
const update = () => {
while (placeholder.nextSibling) {
parentNode.removeChild(placeholder.nextSibling);
}
evaluate(listKey, vm).forEach(itemData => {
const clonedNode = originNode.cloneNode(true);
parentNode.appendChild(clonedNode);
const newScope = Object.create(vm);
newScope[itemVar] = itemData;
vm.compile(clonedNode, isOnce, newScope);
});
};
update();
new Watcher(vm, listKey, update);
}
function handleVShow(node, attr, vm, isOnce) {
const expr = attr.value;
const originalDisplay = node.style.display;
const update = () => {
node.style.display = evaluate(expr, vm) ? originalDisplay : 'none';
};
update();
if (!isOnce) {
new Watcher(vm, expr, update);
}
}
function handleVModel(node, attr, vm, isOnce) {
const key = attr.value;
node.value = vm[key];
node.addEventListener('input', () => {
vm[key] = node.value;
});
if (!isOnce) {
new Watcher(vm, key, () => {
node.value = vm[key];
});
}
}
function handleVText(node, attr, vm, isOnce) {
const expr = attr.value;
node.textContent = evaluate(expr, vm);
if (!isOnce) {
new Watcher(vm, expr, (oldValue, newValue) => {
node.textContent = newValue;
});
}
}
function handleVHtml(node, attr, vm, isOnce) {
const expr = attr.value;
node.innerHTML = evaluate(expr, vm);
if (!isOnce) {
new Watcher(vm, expr, () => {
node.innerHTML = evaluate(expr, vm);
});
}
}
function handleVBind(node, attr, vm, isOnce) {
let attrName = attr.name;
if (attrName.startsWith(':')) {
attrName = `v-bind${attrName}`;
}
attrName = attrName.slice(7);
const key = attr.value;
node.setAttribute(attrName, vm[key]);
if (!isOnce) {
new Watcher(vm, key, () => {
node.setAttribute(attrName, vm[key]);
});
}
}
function handleEvent(node, attr, vm) {
let attrName = attr.name;
if (attrName.startsWith('@')) {
attrName = `v-on:${attrName.slice(1)}`;
}
const [eventName, ...modifiers] = attrName.slice(5).split('.');
const methodExpression = attr.value.trim();
let methodName = methodExpression;
let args = [];
let parenIndex = methodExpression.indexOf('(');
if (parenIndex !== -1) {
methodName = methodExpression.substring(0, parenIndex);
const argsString = methodExpression.substring(parenIndex + 1, methodExpression.length - 1).trim();
args = argsString.split(',').map(arg => arg.trim());
} else {
args = ['$event'];
}
let options = {
capture: modifiers.includes('capture'),
once: modifiers.includes('once'),
passive: modifiers.includes('passive')
};
node.addEventListener(eventName, event => {
if (modifiers.includes('stop')) event.stopPropagation();
if (modifiers.includes('prevent')) event.preventDefault();
if (modifiers.includes('self')) {
if (event.target !== event.currentTarget) {
return;
}
}
let modifierKeys = modifiers.map(modifier => KeyMap[modifier]).filter(Boolean).flat();
if (modifierKeys.length > 0) {
if (!modifierKeys.includes(event.key)) {
return;
}
}
let actualArgs = args.map(arg => {
if (!isNaN(arg)) return arg;
if (arg === '$event') return event;
if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
return vm[arg];
});
vm[methodName](...actualArgs);
}, options);
}
在 Vue.js 中,过滤器(filter)用于应用一些文本转换和格式化。过滤器可以在两个地方使用:双花括号插值和 v-bind
表达式。
过滤器并不改变原数据,它们仅仅改变数据的输出格式。
基础用法
定义一个全局过滤器
Vue.filter('capitalize', function(value) {
if (!value) return '';
value = value.toString();
return value.charAt(0).toUpperCase() + value.slice(1);
});
在组件中定义局部过滤器
export default {
filters: {
capitalize: function(value) {
if (!value) return '';
value = value.toString();
return value.charAt(0).toUpperCase() + value.slice(1);
}
}
}
在模板中使用过滤器
<!-- 在双花括号中 -->
{{ message | capitalize }}
<!-- 在 `v-bind` 中 -->
<a :href="url | formatUrl"></a>
过滤器可以链式调用
{{ message | filterA | filterB }}
在这种情况下,filterA
会首先应用,然后其结果将传递给 filterB
。
过滤器可以接受参数
Vue.filter('prefix', function(value, prefix) {
if (!value) return '';
value = value.toString();
return prefix + value;
});
{{ message | prefix('Hello: ') }}
在这个例子中,过滤器 prefix
接受一个参数 'Hello: '
,然后将它添加到 message
前面。
总结
过滤器是 Vue.js 中用于文本格式化的一种有效工具,但应当注意的是,它们不应用于复杂的逻辑或副作用,这些应当由计算属性或方法处理。过滤器主要用于格式化输出,使之更易读或更符合要求。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue</title>
</head>
<body>
<div id="app">
{{message | upper | addSuffix }}
</div>
<script src="my-vue.js"></script>
<script>
Vue.filter('upper', function (value) {
return value.toUpperCase();
});
Vue.filter('addSuffix', function (value) {
return value+'$';
});
var vm = new Vue({
el: '#app',
data: {
message: 'hello'
}
})
</script>
</body>
</html>
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
const KeyMap = {
enter: 'Enter',
delete: ['Backspace', 'Delete'],
up: 'ArrowUp',
down: 'ArrowDown',
left: 'ArrowLeft',
right: 'ArrowRight',
space: 'Space'
};
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
class Watcher {
constructor(vm, exprOrFn, cb) {
this.vm = vm;
this.cb = cb;
this.getter = typeof exprOrFn === 'function' ? exprOrFn : getEvaluate(exprOrFn, vm);
Dep.target = this;
this.oldValue = this.getter();
Dep.target = null;
}
update() {
const newValue = this.getter();
if (this.oldValue !== newValue) {
this.cb(newValue, this.oldValue);
this.oldValue = newValue;
}
}
}
class Vue {
+ static filters = {}
+ static filter = function (name, func) {
+ Vue.filters[name] = func;
+ };
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
this.init();
}
init() {
this.proxyData();
this.proxyMethods();
this.observe(this.$data);
this.initComputed();
this.initWatch();
this.compile(this.$el);
}
initWatch() {
const watch = this.$options.watch;
if (watch) {
Object.keys(watch).forEach(key => {
new Watcher(
this,
key,
(newValue, oldValue) => {
watch[key].call(this, newValue, oldValue);
}
);
});
}
}
initComputed() {
const computed = this.$options.computed;
if (computed) {
Object.keys(computed).forEach(key => {
new Watcher(
this,
() => computed[key].call(this),
(newValue) => {
this[key] = newValue;
}
);
Object.defineProperty(this, key, {
get: () => {
return computed[key].call(this);
},
set: () => {
console.warn('Computed property "' + key + '" cannot be assigned to');
},
});
});
}
}
observe(obj) {
for (let key in obj) {
let value = obj[key];
let dep = new Dep();
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.addSub(Dep.target);
}
return value;
},
set(newVal) {
if (newVal !== value) {
value = newVal;
dep.notify();
}
}
});
}
}
proxyMethods() {
for (let key in this.$methods) {
this[key] = this.$methods[key].bind(this);
}
}
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
compile(el, isOnce = false, scope) {
const vm = scope || this;
const {
childNodes
} = el;
const conditionalElements = [];
[...childNodes].forEach(node => {
console.log(node);
if (node.nodeType === ELEMENT_NODE && node.hasAttribute('v-pre')) {
return;
}
if (node.nodeType === ELEMENT_NODE) {
if (node.hasAttribute('v-if') || node.hasAttribute('v-else-if') || node.hasAttribute('v-else')) {
conditionalElements.push(node);
}
isOnce = isOnce || node.hasAttribute('v-once');
compileAttributes([...node.attributes], node, vm, isOnce);
if (el.contains(node)) {
vm.compile(node, isOnce);
}
} else if (node.nodeType === TEXT_NODE) {
let {
originalTextContent
} = node;
if (!originalTextContent) {
originalTextContent = node.textContent;
node.originalTextContent = originalTextContent;
}
let reg = /\{\{(.+?)\}\}/g;
const updateTextContent = () => {
node.textContent = originalTextContent.replace(reg, (_, key) => {
return evaluate(key, vm);
});
};
updateTextContent();
if (!isOnce) {
let match;
while ((match = reg.exec(originalTextContent)) !== null) {
const key = match[1].trim();
new Watcher(vm, key, updateTextContent);
}
}
}
});
if (conditionalElements.length > 0) {
handleConditionalElements(conditionalElements, vm);
}
el.removeAttribute('v-cloak');
}
}
function handleConditionalElements(conditionalElements, vm) {
let lastVisibleElement = null;
conditionalElements.forEach(element => {
let expression = element.getAttribute('v-if') || element.getAttribute('v-else-if') || 'true';
let evaluate = getEvaluate(expression, vm);
element.evaluate = evaluate;
const shouldDisplay = evaluate();
if (shouldDisplay && !lastVisibleElement) {
lastVisibleElement = element;
} else {
element.parentNode.removeChild(element);
}
});
const updateVisibility = () => {
const nextVisibleElement = conditionalElements.find(({
evaluate
}) => {
return evaluate();
});
if (nextVisibleElement && nextVisibleElement !== lastVisibleElement) {
lastVisibleElement.parentNode.replaceChild(nextVisibleElement, lastVisibleElement);
lastVisibleElement = nextVisibleElement;
}
};
new Watcher(vm, () => conditionalElements.map(({
evaluate
}) => evaluate()), updateVisibility);
}
function evaluate(expression, vm) {
return getEvaluate(expression, vm).call(vm);
}
+function getEvaluate(expression, vm) {
+ return function() {
+ const [expr, ...filters] = expression.split('|').map(e => e.trim());
+ let value = new Function(`with(this){return ` + expr + `}`).call(vm);
+ for (const filter of filters) {
+ const filterFunc = Vue.filters[filter];
+ value = filterFunc(value);
+ }
+ return value;
+ }
+}
function compileAttributes(attributes, node, vm, isOnce) {
attributes.forEach(attr => {
if (attr.name === 'v-text') {
handleVText(node, attr, vm, isOnce);
} else if (attr.name === 'v-html') {
handleVHtml(node, attr, vm, isOnce);
} else if (attr.name.startsWith('v-bind:') || attr.name.startsWith(':')) {
handleVBind(node, attr, vm, isOnce);
} else if (attr.name.startsWith('v-on:') || attr.name.startsWith('@')) {
handleEvent(node, attr, vm);
} else if (attr.name === 'v-model') {
handleVModel(node, attr, vm, isOnce);
} else if (attr.name === 'v-show') {
handleVShow(node, attr, vm, isOnce);
} else if (attr.name === 'v-for') {
handleVFor(node, attr, vm, isOnce);
}
});
}
function handleVFor(node, attr, vm, isOnce) {
const originNode = node.cloneNode(true);
const {
parentNode
} = node;
const placeholder = document.createComment('v-for-placeholder');
parentNode.replaceChild(placeholder, node);
const expression = attr.value;
const [itemVar,, listKey] = expression.split(' ');
const update = () => {
while (placeholder.nextSibling) {
parentNode.removeChild(placeholder.nextSibling);
}
evaluate(listKey, vm).forEach(itemData => {
const clonedNode = originNode.cloneNode(true);
parentNode.appendChild(clonedNode);
const newScope = Object.create(vm);
newScope[itemVar] = itemData;
vm.compile(clonedNode, isOnce, newScope);
});
};
update();
new Watcher(vm, listKey, update);
}
function handleVShow(node, attr, vm, isOnce) {
const expr = attr.value;
const originalDisplay = node.style.display;
const update = () => {
node.style.display = evaluate(expr, vm) ? originalDisplay : 'none';
};
update();
if (!isOnce) {
new Watcher(vm, expr, update);
}
}
function handleVModel(node, attr, vm, isOnce) {
const key = attr.value;
node.value = vm[key];
node.addEventListener('input', () => {
vm[key] = node.value;
});
if (!isOnce) {
new Watcher(vm, key, () => {
node.value = vm[key];
});
}
}
function handleVText(node, attr, vm, isOnce) {
const expr = attr.value;
node.textContent = evaluate(expr, vm);
if (!isOnce) {
new Watcher(vm, expr, (oldValue, newValue) => {
node.textContent = newValue;
});
}
}
function handleVHtml(node, attr, vm, isOnce) {
const expr = attr.value;
node.innerHTML = evaluate(expr, vm);
if (!isOnce) {
new Watcher(vm, expr, () => {
node.innerHTML = evaluate(expr, vm);
});
}
}
function handleVBind(node, attr, vm, isOnce) {
let attrName = attr.name;
if (attrName.startsWith(':')) {
attrName = `v-bind${attrName}`;
}
attrName = attrName.slice(7);
const key = attr.value;
node.setAttribute(attrName, vm[key]);
if (!isOnce) {
new Watcher(vm, key, () => {
node.setAttribute(attrName, vm[key]);
});
}
}
function handleEvent(node, attr, vm) {
let attrName = attr.name;
if (attrName.startsWith('@')) {
attrName = `v-on:${attrName.slice(1)}`;
}
const [eventName, ...modifiers] = attrName.slice(5).split('.');
const methodExpression = attr.value.trim();
let methodName = methodExpression;
let args = [];
let parenIndex = methodExpression.indexOf('(');
if (parenIndex !== -1) {
methodName = methodExpression.substring(0, parenIndex);
const argsString = methodExpression.substring(parenIndex + 1, methodExpression.length - 1).trim();
args = argsString.split(',').map(arg => arg.trim());
} else {
args = ['$event'];
}
let options = {
capture: modifiers.includes('capture'),
once: modifiers.includes('once'),
passive: modifiers.includes('passive')
};
node.addEventListener(eventName, event => {
if (modifiers.includes('stop')) event.stopPropagation();
if (modifiers.includes('prevent')) event.preventDefault();
if (modifiers.includes('self')) {
if (event.target !== event.currentTarget) {
return;
}
}
let modifierKeys = modifiers.map(modifier => KeyMap[modifier]).filter(Boolean).flat();
if (modifierKeys.length > 0) {
if (!modifierKeys.includes(event.key)) {
return;
}
}
let actualArgs = args.map(arg => {
if (!isNaN(arg)) return arg;
if (arg === '$event') return event;
if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
return vm[arg];
});
vm[methodName](...actualArgs);
}, options);
}
在 Vue.js 中,:class
是一个特殊的绑定语法,允许你动态地将类添加到 DOM 元素上。这使得元素类名可以根据组件的数据或状态进行更改,从而实现响应式样式。
基础用法
最简单的用法是绑定一个字符串:
<!-- activeClass 是 data 或 computed 属性中定义的 -->
<div :class="activeClass">Hello, world!</div>
data() {
return {
activeClass: 'active'
};
}
对象语法
你也可以传递一个对象来动态地切换类:
<!-- active 会评估为一个布尔值 -->
<div :class="{ active: isActive, 'text-danger': hasError }">Hello, world!</div>
data() {
return {
isActive: true,
hasError: false
};
}
在这个例子里,active
类会在 isActive
为 true
的时候添加到 <div>
元素上,而 text-danger
类会在 hasError
为 true
的时候添加。
数组语法
:class
也可以接受一个数组,以应用多个类:
<div :class="[activeClass, errorClass]">Hello, world!</div>
data() {
return {
activeClass: 'active',
errorClass: 'text-danger'
};
}
数组内部还可以使用对象语法:
<div :class="[isActive ? activeClass : '', { 'text-danger': hasError }]">Hello, world!</div>
使用计算属性
如果类逻辑比较复杂,你可以使用计算属性:
<div :class="classObject">Hello, world!</div>
computed: {
classObject() {
return {
active: this.isActive,
'text-danger': this.hasError
};
}
}
在这个例子中,classObject
是一个计算属性,返回一个根据组件状态动态改变的对象。
在 Vue.js 中,:style
是一个用于动态地绑定样式的特殊 attribute。使用这个 attribute 可以让你以数据绑定的方式动态地设置 HTML 元素的样式。
:style
可以接受一个对象或者数组作为其值,该对象或数组则用于定义样式规则。
对象语法
当使用对象语法时,你需要传递一个对象,其中的键名对应于 CSS 属性名,键值对应于 CSS 属性值。例如:
<template>
<div :style="styleObject"></div>
</template>
<script>
export default {
data() {
return {
styleObject: {
color: 'red',
fontSize: '16px',
},
};
},
};
</script>
在这个例子中,div
元素的文字颜色会被设置为红色,并且字体大小会被设置为 16 像素。
数组语法
当使用数组语法时,你可以传递一个包含多个样式对象的数组,这在你想要根据不同的条件应用多个样式对象时非常有用。例如:
<template>
<div :style="[baseStyles, overridingStyles]"></div>
</template>
<script>
export default {
data() {
return {
baseStyles: {
color: 'red',
fontSize: '16px',
},
overridingStyles: {
color: 'blue',
},
};
},
};
</script>
在这个例子中,div
元素的文字颜色最终会被设置为蓝色(因为 overridingStyles
覆盖了 baseStyles
),并且字体大小会被设置为 16 像素。
使用计算属性
你也可以使用计算属性来动态生成样式对象:
<template>
<div :style="computedStyles"></div>
</template>
<script>
export default {
data() {
return {
isActive: true,
};
},
computed: {
computedStyles() {
return {
color: this.isActive ? 'green' : 'red',
fontSize: this.isActive ? '20px' : '16px',
};
},
},
};
</script>
这里,当 isActive
为 true
时,文字颜色会是绿色并且字体大小为 20 像素;否则,它们会被设置为红色和 16 像素。
<body>
<div id="app">
<div :style="styleObject">style</div>
</div>
<script src="my-vue.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
styleObject: {
color: 'red',
fontSize: '16px',
},
}
})
</script>
</body>
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
const KeyMap = {
enter: 'Enter',
delete: ['Backspace', 'Delete'],
up: 'ArrowUp',
down: 'ArrowDown',
left: 'ArrowLeft',
right: 'ArrowRight',
space: 'Space'
};
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
class Watcher {
constructor(vm, exprOrFn, cb) {
this.vm = vm;
this.cb = cb;
this.getter = typeof exprOrFn === 'function' ? exprOrFn : getEvaluate(exprOrFn, vm);
Dep.target = this;
this.oldValue = this.getter();
Dep.target = null;
}
update() {
const newValue = this.getter();
if (this.oldValue !== newValue) {
this.cb(newValue, this.oldValue);
this.oldValue = newValue;
}
}
}
class Vue {
static filters = {}
static filter = function (name, func) {
Vue.filters[name] = func;
};
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
this.init();
}
init() {
this.proxyData();
this.proxyMethods();
this.observe(this.$data);
this.initComputed();
this.initWatch();
this.compile(this.$el);
}
initWatch() {
const watch = this.$options.watch;
if (watch) {
Object.keys(watch).forEach(key => {
new Watcher(
this,
key,
(newValue, oldValue) => {
watch[key].call(this, newValue, oldValue);
}
);
});
}
}
initComputed() {
const computed = this.$options.computed;
if (computed) {
Object.keys(computed).forEach(key => {
new Watcher(
this,
() => computed[key].call(this),
(newValue) => {
this[key] = newValue;
}
);
Object.defineProperty(this, key, {
get: () => {
return computed[key].call(this);
},
set: () => {
console.warn('Computed property "' + key + '" cannot be assigned to');
},
});
});
}
}
observe(obj) {
for (let key in obj) {
let value = obj[key];
let dep = new Dep();
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.addSub(Dep.target);
}
return value;
},
set(newVal) {
if (newVal !== value) {
value = newVal;
dep.notify();
}
}
});
}
}
proxyMethods() {
for (let key in this.$methods) {
this[key] = this.$methods[key].bind(this);
}
}
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
compile(el, isOnce = false, scope) {
const vm = scope || this;
const {
childNodes
} = el;
const conditionalElements = [];
[...childNodes].forEach(node => {
console.log(node);
if (node.nodeType === ELEMENT_NODE && node.hasAttribute('v-pre')) {
return;
}
if (node.nodeType === ELEMENT_NODE) {
if (node.hasAttribute('v-if') || node.hasAttribute('v-else-if') || node.hasAttribute('v-else')) {
conditionalElements.push(node);
}
isOnce = isOnce || node.hasAttribute('v-once');
compileAttributes([...node.attributes], node, vm, isOnce);
if (el.contains(node)) {
vm.compile(node, isOnce);
}
} else if (node.nodeType === TEXT_NODE) {
let {
originalTextContent
} = node;
if (!originalTextContent) {
originalTextContent = node.textContent;
node.originalTextContent = originalTextContent;
}
let reg = /\{\{(.+?)\}\}/g;
const updateTextContent = () => {
node.textContent = originalTextContent.replace(reg, (_, key) => {
return evaluate(key, vm);
});
};
updateTextContent();
if (!isOnce) {
let match;
while ((match = reg.exec(originalTextContent)) !== null) {
const key = match[1].trim();
new Watcher(vm, key, updateTextContent);
}
}
}
});
if (conditionalElements.length > 0) {
handleConditionalElements(conditionalElements, vm);
}
el.removeAttribute('v-cloak');
}
}
function handleConditionalElements(conditionalElements, vm) {
let lastVisibleElement = null;
conditionalElements.forEach(element => {
let expression = element.getAttribute('v-if') || element.getAttribute('v-else-if') || 'true';
let evaluate = getEvaluate(expression, vm);
element.evaluate = evaluate;
const shouldDisplay = evaluate();
if (shouldDisplay && !lastVisibleElement) {
lastVisibleElement = element;
} else {
element.parentNode.removeChild(element);
}
});
const updateVisibility = () => {
const nextVisibleElement = conditionalElements.find(({
evaluate
}) => {
return evaluate();
});
if (nextVisibleElement && nextVisibleElement !== lastVisibleElement) {
lastVisibleElement.parentNode.replaceChild(nextVisibleElement, lastVisibleElement);
lastVisibleElement = nextVisibleElement;
}
};
new Watcher(vm, () => conditionalElements.map(({
evaluate
}) => evaluate()), updateVisibility);
}
function evaluate(expression, vm) {
return getEvaluate(expression, vm).call(vm);
}
function getEvaluate(expression, vm) {
return function() {
const [expr, ...filters] = expression.split('|').map(e => e.trim());
let value = new Function(`with(this){return ` + expr + `}`).call(vm);
for (const filter of filters) {
const filterFunc = Vue.filters[filter];
value = filterFunc(value);
}
return value;
}
}
function compileAttributes(attributes, node, vm, isOnce) {
attributes.forEach(attr => {
if (attr.name === 'v-text') {
handleVText(node, attr, vm, isOnce);
} else if (attr.name === 'v-html') {
handleVHtml(node, attr, vm, isOnce);
+ } else if (attr.name === 'v-bind:style' || attr.name === ':style') {
+ handleStyleBinding(node, attr, vm);
} else if (attr.name.startsWith('v-bind:') || attr.name.startsWith(':')) {
handleVBind(node, attr, vm, isOnce);
} else if (attr.name.startsWith('v-on:') || attr.name.startsWith('@')) {
handleEvent(node, attr, vm);
} else if (attr.name === 'v-model') {
handleVModel(node, attr, vm, isOnce);
} else if (attr.name === 'v-show') {
handleVShow(node, attr, vm, isOnce);
} else if (attr.name === 'v-for') {
handleVFor(node, attr, vm, isOnce);
}
});
}
+function handleStyleBinding(node, attr, vm) {
+ const updateStyle = () => {
+ const styleObject = evaluate(attr.value, vm);
+ for (const [key, value] of Object.entries(styleObject)) {
+ node.style[key] = value;
+ }
+ };
+ updateStyle();
+ new Watcher(vm, attr.value, updateStyle);
+}
function handleVFor(node, attr, vm, isOnce) {
const originNode = node.cloneNode(true);
const {
parentNode
} = node;
const placeholder = document.createComment('v-for-placeholder');
parentNode.replaceChild(placeholder, node);
const expression = attr.value;
const [itemVar,, listKey] = expression.split(' ');
const update = () => {
while (placeholder.nextSibling) {
parentNode.removeChild(placeholder.nextSibling);
}
evaluate(listKey, vm).forEach(itemData => {
const clonedNode = originNode.cloneNode(true);
parentNode.appendChild(clonedNode);
const newScope = Object.create(vm);
newScope[itemVar] = itemData;
vm.compile(clonedNode, isOnce, newScope);
});
};
update();
new Watcher(vm, listKey, update);
}
function handleVShow(node, attr, vm, isOnce) {
const expr = attr.value;
const originalDisplay = node.style.display;
const update = () => {
node.style.display = evaluate(expr, vm) ? originalDisplay : 'none';
};
update();
if (!isOnce) {
new Watcher(vm, expr, update);
}
}
function handleVModel(node, attr, vm, isOnce) {
const key = attr.value;
node.value = vm[key];
node.addEventListener('input', () => {
vm[key] = node.value;
});
if (!isOnce) {
new Watcher(vm, key, () => {
node.value = vm[key];
});
}
}
function handleVText(node, attr, vm, isOnce) {
const expr = attr.value;
node.textContent = evaluate(expr, vm);
if (!isOnce) {
new Watcher(vm, expr, (oldValue, newValue) => {
node.textContent = newValue;
});
}
}
function handleVHtml(node, attr, vm, isOnce) {
const expr = attr.value;
node.innerHTML = evaluate(expr, vm);
if (!isOnce) {
new Watcher(vm, expr, () => {
node.innerHTML = evaluate(expr, vm);
});
}
}
function handleVBind(node, attr, vm, isOnce) {
let attrName = attr.name;
if (attrName.startsWith(':')) {
attrName = `v-bind${attrName}`;
}
attrName = attrName.slice(7);
const key = attr.value;
node.setAttribute(attrName, vm[key]);
if (!isOnce) {
new Watcher(vm, key, () => {
node.setAttribute(attrName, vm[key]);
});
}
}
function handleEvent(node, attr, vm) {
let attrName = attr.name;
if (attrName.startsWith('@')) {
attrName = `v-on:${attrName.slice(1)}`;
}
const [eventName, ...modifiers] = attrName.slice(5).split('.');
const methodExpression = attr.value.trim();
let methodName = methodExpression;
let args = [];
let parenIndex = methodExpression.indexOf('(');
if (parenIndex !== -1) {
methodName = methodExpression.substring(0, parenIndex);
const argsString = methodExpression.substring(parenIndex + 1, methodExpression.length - 1).trim();
args = argsString.split(',').map(arg => arg.trim());
} else {
args = ['$event'];
}
let options = {
capture: modifiers.includes('capture'),
once: modifiers.includes('once'),
passive: modifiers.includes('passive')
};
node.addEventListener(eventName, event => {
if (modifiers.includes('stop')) event.stopPropagation();
if (modifiers.includes('prevent')) event.preventDefault();
if (modifiers.includes('self')) {
if (event.target !== event.currentTarget) {
return;
}
}
let modifierKeys = modifiers.map(modifier => KeyMap[modifier]).filter(Boolean).flat();
if (modifierKeys.length > 0) {
if (!modifierKeys.includes(event.key)) {
return;
}
}
let actualArgs = args.map(arg => {
if (!isNaN(arg)) return arg;
if (arg === '$event') return event;
if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
return vm[arg];
});
vm[methodName](...actualArgs);
}, options);
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app" v-cloak>
{{user.name}}
</div>
<script src="my-vue.js"></script>
<script>
var vm = new Vue({
el:'#app',
data:{
user:{
name:'beijing'
}
}
});
vm.user.name='guangzhou'
</script>
</body>
</html>
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
const KeyMap = {
enter: 'Enter',
delete: ['Backspace', 'Delete'],
up: 'ArrowUp',
down: 'ArrowDown',
left: 'ArrowLeft',
right: 'ArrowRight',
space: 'Space'
};
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
class Watcher {
constructor(vm, exprOrFn, cb) {
this.vm = vm;
this.cb = cb;
this.getter = typeof exprOrFn === 'function' ? exprOrFn : getEvaluate(exprOrFn, vm);
Dep.target = this;
this.oldValue = this.getter();
Dep.target = null;
}
update() {
const newValue = this.getter();
if (this.oldValue !== newValue) {
this.cb(newValue, this.oldValue);
this.oldValue = newValue;
}
}
}
class Vue {
static filters = {}
static filter = function (name, func) {
Vue.filters[name] = func;
};
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
this.init();
}
init() {
this.proxyData();
this.proxyMethods();
this.observe(this.$data);
this.initComputed();
this.initWatch();
this.compile(this.$el);
}
initWatch() {
const watch = this.$options.watch;
if (watch) {
Object.keys(watch).forEach(key => {
new Watcher(
this,
key,
(newValue, oldValue) => {
watch[key].call(this, newValue, oldValue);
}
);
});
}
}
initComputed() {
const computed = this.$options.computed;
if (computed) {
Object.keys(computed).forEach(key => {
new Watcher(
this,
() => computed[key].call(this),
(newValue) => {
this[key] = newValue;
}
);
Object.defineProperty(this, key, {
get: () => {
return computed[key].call(this);
},
set: () => {
console.warn('Computed property "' + key + '" cannot be assigned to');
},
});
});
}
}
+ observe(obj) {
+ if (!obj || typeof obj !== 'object') {
+ return;
+ }
+ for (let key in obj) {
+ this.defineReactive(obj, key, obj[key]);
+ }
+ }
+ defineReactive(obj, key, value) {
+ this.observe(value);
+ let dep = new Dep();
+ Object.defineProperty(obj, key, {
+ get() {
+ if (Dep.target) {
+ dep.addSub(Dep.target);
+ }
+ return value;
+ },
+ set: (newVal) => {
+ if (newVal !== value) {
+ this.observe(newVal);
+ value = newVal;
+ dep.notify();
+ }
+ }
+ });
+ }
proxyMethods() {
for (let key in this.$methods) {
this[key] = this.$methods[key].bind(this);
}
}
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
compile(el, isOnce = false, scope) {
const vm = scope || this;
const {
childNodes
} = el;
const conditionalElements = [];
[...childNodes].forEach(node => {
console.log(node);
if (node.nodeType === ELEMENT_NODE && node.hasAttribute('v-pre')) {
return;
}
if (node.nodeType === ELEMENT_NODE) {
if (node.hasAttribute('v-if') || node.hasAttribute('v-else-if') || node.hasAttribute('v-else')) {
conditionalElements.push(node);
}
isOnce = isOnce || node.hasAttribute('v-once');
compileAttributes([...node.attributes], node, vm, isOnce);
if (el.contains(node)) {
vm.compile(node, isOnce);
}
} else if (node.nodeType === TEXT_NODE) {
let {
originalTextContent
} = node;
if (!originalTextContent) {
originalTextContent = node.textContent;
node.originalTextContent = originalTextContent;
}
let reg = /\{\{(.+?)\}\}/g;
const updateTextContent = () => {
node.textContent = originalTextContent.replace(reg, (_, key) => {
return evaluate(key, vm);
});
};
updateTextContent();
if (!isOnce) {
let match;
while ((match = reg.exec(originalTextContent)) !== null) {
const key = match[1].trim();
new Watcher(vm, key, updateTextContent);
}
}
}
});
if (conditionalElements.length > 0) {
handleConditionalElements(conditionalElements, vm);
}
el.removeAttribute('v-cloak');
}
}
function handleConditionalElements(conditionalElements, vm) {
let lastVisibleElement = null;
conditionalElements.forEach(element => {
let expression = element.getAttribute('v-if') || element.getAttribute('v-else-if') || 'true';
let evaluate = getEvaluate(expression, vm);
element.evaluate = evaluate;
const shouldDisplay = evaluate();
if (shouldDisplay && !lastVisibleElement) {
lastVisibleElement = element;
} else {
element.parentNode.removeChild(element);
}
});
const updateVisibility = () => {
const nextVisibleElement = conditionalElements.find(({
evaluate
}) => {
return evaluate();
});
if (nextVisibleElement && nextVisibleElement !== lastVisibleElement) {
lastVisibleElement.parentNode.replaceChild(nextVisibleElement, lastVisibleElement);
lastVisibleElement = nextVisibleElement;
}
};
new Watcher(vm, () => conditionalElements.map(({
evaluate
}) => evaluate()), updateVisibility);
}
function evaluate(expression, vm) {
return getEvaluate(expression, vm).call(vm);
}
function getEvaluate(expression, vm) {
return function() {
const [expr, ...filters] = expression.split('|').map(e => e.trim());
let value = new Function(`with(this){return ` + expr + `}`).call(vm);
for (const filter of filters) {
const filterFunc = Vue.filters[filter];
value = filterFunc(value);
}
return value;
}
}
function compileAttributes(attributes, node, vm, isOnce) {
attributes.forEach(attr => {
if (attr.name === 'v-text') {
handleVText(node, attr, vm, isOnce);
} else if (attr.name === 'v-html') {
handleVHtml(node, attr, vm, isOnce);
} else if (attr.name === 'v-bind:style' || attr.name === ':style') {
handleStyleBinding(node, attr, vm);
} else if (attr.name.startsWith('v-bind:') || attr.name.startsWith(':')) {
handleVBind(node, attr, vm, isOnce);
} else if (attr.name.startsWith('v-on:') || attr.name.startsWith('@')) {
handleEvent(node, attr, vm);
} else if (attr.name === 'v-model') {
handleVModel(node, attr, vm, isOnce);
} else if (attr.name === 'v-show') {
handleVShow(node, attr, vm, isOnce);
} else if (attr.name === 'v-for') {
handleVFor(node, attr, vm, isOnce);
}
});
}
function handleStyleBinding(node, attr, vm) {
const updateStyle = () => {
const styleObject = evaluate(attr.value, vm);
for (const [key, value] of Object.entries(styleObject)) {
node.style[key] = value;
}
};
updateStyle();
new Watcher(vm, attr.value, updateStyle);
}
function handleVFor(node, attr, vm, isOnce) {
const originNode = node.cloneNode(true);
const {
parentNode
} = node;
const placeholder = document.createComment('v-for-placeholder');
parentNode.replaceChild(placeholder, node);
const expression = attr.value;
const [itemVar,, listKey] = expression.split(' ');
const update = () => {
while (placeholder.nextSibling) {
parentNode.removeChild(placeholder.nextSibling);
}
evaluate(listKey, vm).forEach(itemData => {
const clonedNode = originNode.cloneNode(true);
parentNode.appendChild(clonedNode);
const newScope = Object.create(vm);
newScope[itemVar] = itemData;
vm.compile(clonedNode, isOnce, newScope);
});
};
update();
new Watcher(vm, listKey, update);
}
function handleVShow(node, attr, vm, isOnce) {
const expr = attr.value;
const originalDisplay = node.style.display;
const update = () => {
node.style.display = evaluate(expr, vm) ? originalDisplay : 'none';
};
update();
if (!isOnce) {
new Watcher(vm, expr, update);
}
}
function handleVModel(node, attr, vm, isOnce) {
const key = attr.value;
node.value = vm[key];
node.addEventListener('input', () => {
vm[key] = node.value;
});
if (!isOnce) {
new Watcher(vm, key, () => {
node.value = vm[key];
});
}
}
function handleVText(node, attr, vm, isOnce) {
const expr = attr.value;
node.textContent = evaluate(expr, vm);
if (!isOnce) {
new Watcher(vm, expr, (oldValue, newValue) => {
node.textContent = newValue;
});
}
}
function handleVHtml(node, attr, vm, isOnce) {
const expr = attr.value;
node.innerHTML = evaluate(expr, vm);
if (!isOnce) {
new Watcher(vm, expr, () => {
node.innerHTML = evaluate(expr, vm);
});
}
}
function handleVBind(node, attr, vm, isOnce) {
let attrName = attr.name;
if (attrName.startsWith(':')) {
attrName = `v-bind${attrName}`;
}
attrName = attrName.slice(7);
const key = attr.value;
node.setAttribute(attrName, vm[key]);
if (!isOnce) {
new Watcher(vm, key, () => {
node.setAttribute(attrName, vm[key]);
});
}
}
function handleEvent(node, attr, vm) {
let attrName = attr.name;
if (attrName.startsWith('@')) {
attrName = `v-on:${attrName.slice(1)}`;
}
const [eventName, ...modifiers] = attrName.slice(5).split('.');
const methodExpression = attr.value.trim();
let methodName = methodExpression;
let args = [];
let parenIndex = methodExpression.indexOf('(');
if (parenIndex !== -1) {
methodName = methodExpression.substring(0, parenIndex);
const argsString = methodExpression.substring(parenIndex + 1, methodExpression.length - 1).trim();
args = argsString.split(',').map(arg => arg.trim());
} else {
args = ['$event'];
}
let options = {
capture: modifiers.includes('capture'),
once: modifiers.includes('once'),
passive: modifiers.includes('passive')
};
node.addEventListener(eventName, event => {
if (modifiers.includes('stop')) event.stopPropagation();
if (modifiers.includes('prevent')) event.preventDefault();
if (modifiers.includes('self')) {
if (event.target !== event.currentTarget) {
return;
}
}
let modifierKeys = modifiers.map(modifier => KeyMap[modifier]).filter(Boolean).flat();
if (modifierKeys.length > 0) {
if (!modifierKeys.includes(event.key)) {
return;
}
}
let actualArgs = args.map(arg => {
if (!isNaN(arg)) return arg;
if (arg === '$event') return event;
if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
return vm[arg];
});
vm[methodName](...actualArgs);
}, options);
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<ul>
<li v-for="user in users">{{user.name}}</li>
</ul>
</div>
<script src="my-vue.js"></script>
<script>
var vm = new Vue({
el:'#app',
data:{
users:[
{id:1,name:'zhangsan'},
{id:2,name:'lisi'}
]
}
});
vm.users.push({id:3,name:'wangwu'});
vm.users[0].name = 'zhaoliu'
vm.users[1].name = 'chenqi'
</script>
</body>
</html>
const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
const KeyMap = {
enter: 'Enter',
delete: ['Backspace', 'Delete'],
up: 'ArrowUp',
down: 'ArrowDown',
left: 'ArrowLeft',
right: 'ArrowRight',
space: 'Space'
};
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
class Watcher {
constructor(vm, exprOrFn, cb) {
this.vm = vm;
this.cb = cb;
this.getter = typeof exprOrFn === 'function' ? exprOrFn : getEvaluate(exprOrFn, vm);
Dep.target = this;
this.oldValue = this.getter();
Dep.target = null;
}
update() {
const newValue = this.getter();
this.cb(newValue, this.oldValue);
this.oldValue = newValue;
}
}
const arrayMethods = Object.create(Array.prototype);
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
const original = arrayMethods[method];
Object.defineProperty(arrayMethods, method, {
value: function mutator(...args) {
const result = original.apply(this, args);
this.__dep__.notify();
return result;
},
writable: true,
configurable: true
});
});
class Vue {
static filters = {};
static filter = (filterName, filterFunc) => {
Vue.filters[filterName] = filterFunc;
};
constructor(options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
this.init();
}
init() {
this.proxyData();
this.observe(this.$data);
this.proxyMethods();
this.initComputed();
this.initWatch();
this.compile(this.$el);
}
initWatch() {
const {
watch
} = this.$options;
if (!watch) return;
const keys = Object.keys(watch);
keys.forEach(key => {
let value = watch[key];
new Watcher(this, key, value.bind(this));
});
}
initComputed() {
const {
computed
} = this.$options;
if (!computed) return;
const keys = Object.keys(computed);
keys.forEach(key => {
const value = computed[key];
let getter = value;
let setter = () => console.warn(`计算属性无法重新赋值!`);
if (typeof value === 'object') {
getter = value.get;
setter = value.set;
}
Object.defineProperty(this, key, {
get: () => {
return getter.call(this);
},
set: newValue => {
setter.call(this, newValue);
}
});
});
}
+ observe(value) {
+ if (!value || typeof value !== 'object') {
+ return;
+ }
+ if (Array.isArray(value)) {
+ value.__proto__ = arrayMethods;
+ this.observeArray(value);
+ } else {
+ this.walk(value);
+ }
+ }
+ observeArray(items) {
+ for (let i = 0, l = items.length; i < l; i++) {
+ this.observe(items[i]);
+ }
+ }
+ walk(obj) {
+ const keys = Object.keys(obj);
+ keys.forEach(key => this.defineReactive(obj, key));
+ }
+ defineReactive(obj, key, value = obj[key]) {
+ let dep = new Dep();
+ this.observe(value);
+ if (Array.isArray(value)) {
+ value.__dep__ = dep;
+ }
+ Object.defineProperty(obj, key, {
+ get() {
+ if (Dep.target) {
+ dep.addSub(Dep.target);
+ }
+ return value;
+ },
+ set(newVal) {
+ if (newVal !== value) {
+ value = newVal;
+ dep.notify();
+ }
+ }
+ });
+ }
proxyMethods() {
for (let key in this.$methods) {
this[key] = this.$methods[key].bind(this);
}
}
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
compile(el, isOnce = false, scope) {
const vm = scope || this;
const {
childNodes
} = el;
const conditionalElements = [];
[...childNodes].forEach(node => {
if (node.nodeType === ELEMENT_NODE && node.hasAttribute('v-pre')) {
return;
}
if (node.nodeType === ELEMENT_NODE) {
if (node.hasAttribute('v-if') || node.hasAttribute('v-else-if') || node.hasAttribute('v-else')) {
conditionalElements.push(node);
}
isOnce = isOnce || node.hasAttribute('v-once');
compileAttributes([...node.attributes], node, vm, isOnce);
if (el.contains(node)) {
vm.compile(node, isOnce);
}
} else if (node.nodeType === TEXT_NODE) {
let {
originalTextContent
} = node;
if (!originalTextContent) {
originalTextContent = node.textContent;
node.originalTextContent = originalTextContent;
}
let reg = /\{\{(.+?)\}\}/g;
const updateTextContent = () => {
node.textContent = originalTextContent.replace(reg, (_, key) => {
return evaluate(key, vm);
});
};
updateTextContent();
if (!isOnce) {
let match;
while ((match = reg.exec(originalTextContent)) !== null) {
const key = match[1].trim();
new Watcher(vm, key, updateTextContent);
}
}
}
});
if (conditionalElements.length > 0) {
handleConditionalElements(conditionalElements, vm);
}
el.removeAttribute('v-cloak');
}
}
function handleConditionalElements(conditionalElements, vm) {
let lastVisibleElement = null;
conditionalElements.forEach(element => {
let expression = element.getAttribute('v-if') || element.getAttribute('v-else-if') || 'true';
let evaluate = getEvaluate(expression, vm);
element.evaluate = evaluate;
const shouldDisplay = evaluate();
if (shouldDisplay && !lastVisibleElement) {
lastVisibleElement = element;
} else {
element.parentNode.removeChild(element);
}
});
const updateVisibility = () => {
const nextVisibleElement = conditionalElements.find(({
evaluate
}) => {
return evaluate();
});
if (nextVisibleElement && nextVisibleElement !== lastVisibleElement) {
lastVisibleElement.parentNode.replaceChild(nextVisibleElement, lastVisibleElement);
lastVisibleElement = nextVisibleElement;
}
};
new Watcher(vm, () => conditionalElements.map(({
evaluate
}) => evaluate()), updateVisibility);
}
function evaluate(expression, vm) {
return getEvaluate(expression, vm).call(vm);
}
function getEvaluate(expression, vm) {
return function () {
const [expr, ...filters] = expression.split('|').map(expr => expr.trim());
let value = new Function(`with(this){return ` + expr + `}`).call(vm);
for (const filterName of filters) {
const filterFunc = Vue.filters[filterName];
value = filterFunc(value);
}
return value;
};
}
function compileAttributes(attributes, node, vm, isOnce) {
attributes.forEach(attr => {
if (attr.name === 'v-text') {
handleVText(node, attr, vm, isOnce);
} else if (attr.name === 'v-html') {
handleVHtml(node, attr, vm, isOnce);
} else if (attr.name === 'v-bind:class' || attr.name === ':class') {
handleVBindClass(node, attr, vm, isOnce);
} else if (attr.name === 'v-bind:style' || attr.name === ':style') {
handleVBindStyle(node, attr, vm, isOnce);
} else if (attr.name.startsWith('v-bind:') || attr.name.startsWith(':')) {
handleVBind(node, attr, vm, isOnce);
} else if (attr.name.startsWith('v-on:') || attr.name.startsWith('@')) {
handleEvent(node, attr, vm);
} else if (attr.name === 'v-model') {
handleVModel(node, attr, vm, isOnce);
} else if (attr.name === 'v-show') {
handleVShow(node, attr, vm, isOnce);
} else if (attr.name === 'v-for') {
handleVFor(node, attr, vm, isOnce);
}
});
}
function handleVBindStyle(node, attr, vm, isOnce) {
let attrName = attr.name;
if (attrName.startsWith(':')) {
attrName = `v-bind${attrName}`;
}
attrName = attrName.slice(7);
function updateStyle() {
const styleObj = evaluate(attr.value, vm);
for (const [key, value] of Object.entries(styleObj)) {
node.style[key] = value;
}
}
updateStyle();
new Watcher(vm, attr.value, updateStyle);
}
function handleVBindClass(node, attr, vm, isOnce) {
let attrName = attr.name;
if (attrName.startsWith(':')) {
attrName = `v-bind${attrName}`;
}
attrName = attrName.slice(7);
const key = attr.value;
function updateClassName() {
let className = '';
let value = evaluate(key, vm);
if (Array.isArray(value)) {
className = value.join(' ');
} else if (typeof value === 'object') {
className = Object.keys(value).filter(className => value[className]).join(' ');
} else if (typeof value === 'string') {
className = value;
}
node.setAttribute('class', className);
}
updateClassName();
if (!isOnce) {
new Watcher(vm, key, updateClassName);
}
}
function handleVFor(node, attr, vm, isOnce) {
const originNode = node.cloneNode(true);
const {
parentNode
} = node;
const placeholder = document.createComment('v-for-placeholder');
parentNode.replaceChild(placeholder, node);
const expression = attr.value;
const [itemVar, , listKey] = expression.split(' ');
const update = () => {
while (placeholder.nextSibling) {
parentNode.removeChild(placeholder.nextSibling);
}
evaluate(listKey, vm).forEach(itemData => {
const clonedNode = originNode.cloneNode(true);
parentNode.appendChild(clonedNode);
const newScope = Object.create(vm);
newScope[itemVar] = itemData;
vm.compile(clonedNode, isOnce, newScope);
});
};
update();
new Watcher(vm, listKey, update);
}
function handleVShow(node, attr, vm, isOnce) {
const expr = attr.value;
const originalDisplay = node.style.display;
const update = () => {
node.style.display = evaluate(expr, vm) ? originalDisplay : 'none';
};
update();
if (!isOnce) {
new Watcher(vm, expr, update);
}
}
function handleVModel(node, attr, vm, isOnce) {
const key = attr.value;
node.value = evaluate(key, vm);
node.addEventListener('input', () => {
vm[key] = node.value;
});
if (!isOnce) {
new Watcher(vm, key, () => {
node.value = evaluate(key, vm);
});
}
}
function handleVText(node, attr, vm, isOnce) {
const expr = attr.value;
node.textContent = evaluate(expr, vm);
if (!isOnce) {
new Watcher(vm, expr, (oldValue, newValue) => {
node.textContent = newValue;
});
}
}
function handleVHtml(node, attr, vm, isOnce) {
const expr = attr.value;
node.innerHTML = evaluate(expr, vm);
if (!isOnce) {
new Watcher(vm, expr, () => {
node.innerHTML = evaluate(expr, vm);
});
}
}
function handleVBind(node, attr, vm, isOnce) {
let attrName = attr.name;
if (attrName.startsWith(':')) {
attrName = `v-bind${attrName}`;
}
attrName = attrName.slice(7);
const key = attr.value;
node.setAttribute(attrName, evaluate(key, vm));
if (!isOnce) {
new Watcher(vm, key, () => {
node.setAttribute(attrName, evaluate(key, vm));
});
}
}
function handleEvent(node, attr, vm) {
let attrName = attr.name;
if (attrName.startsWith('@')) {
attrName = `v-on:${attrName.slice(1)}`;
}
const [eventName, ...modifiers] = attrName.slice(5).split('.');
const methodExpression = attr.value.trim();
let methodName = methodExpression;
let args = [];
let parenIndex = methodExpression.indexOf('(');
if (parenIndex !== -1) {
methodName = methodExpression.substring(0, parenIndex);
const argsString = methodExpression.substring(parenIndex + 1, methodExpression.length - 1).trim();
args = argsString.split(',').map(arg => arg.trim());
} else {
args = ['$event'];
}
let options = {
capture: modifiers.includes('capture'),
once: modifiers.includes('once'),
passive: modifiers.includes('passive')
};
node.addEventListener(eventName, event => {
if (modifiers.includes('stop')) event.stopPropagation();
if (modifiers.includes('prevent')) event.preventDefault();
if (modifiers.includes('self')) {
if (event.target !== event.currentTarget) {
return;
}
}
let modifierKeys = modifiers.map(modifier => KeyMap[modifier]).filter(Boolean).flat();
if (modifierKeys.length > 0) {
if (!modifierKeys.includes(event.key)) {
return;
}
}
let actualArgs = args.map(arg => {
if (!isNaN(arg)) return arg;
if (arg === '$event') return event;
if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
return evaluate(arg, vm);
});
vm[methodName](...actualArgs);
}, options);
}