Vue相关内容
vue是一个渐进式框架,就是说vue有很多功能,你需要哪一部分,就可以使用哪一部分到你的项目中。vue的核心特性是数据双向绑定和组件化。除此之外,vue配套的工具库还有vuex和vue-router,前者做状态管理,后者做路由管理。
经典图片
- 渐进式框架:可以逐渐添加需要使用的功能,不需要使用的可以不添加。
- 声明式渲染:数据发生变化时,视图可以自动更新,不用关心DOM的操作,专心于数据处理。例如:使用
v-for
时,我们只告诉Vue需要重复渲染多少次,并没有告诉Vue每一步该如何操作。这个过程就是声明式渲染,声明新参数的值,如何修改交给Vue去操作。v-if
v-for
v-on / @
v-bind / :
v-model
(v-model
只在表单元素中生效) - Vue的双向绑定原理:
先上图
我们知道,vue 只支持 IE9 及以上浏览器(不包含动画),最主要的一个点是因为一个 ES5 对象的静态方法 Object.defineProperty。Object.defineProperty 可以定义对象属性的 getter/setter,vue 在这对属性里加入了观察者代码,当对象属性被修改时,vue 会追踪变化并告知依赖组件去更新实时的值。具体就是由下面的几个功能模块来实现的:
Observer
数据监听器:能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者,内部采用Object.defineProperty
的getter
和setter
来实现。Compile
指令解析器:它的作用对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。Watcher
订阅者, 作为连接Observer
和Compile
的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数。Dep
消息订阅器,内部维护了一个数组,用来收集订阅者Watcher
,数据变动触发notify
函数,再调用订阅者的update
方法。
- Observer(监听器):作用是检测到数据是否被改变,如果改变就通知订阅者。通过
Object.defineProperty()
中的get
和set
方法实现。当读取该对象属性的数据时触发get
方法,修改对象属性值时触发set
方法。使用Object.defineProperty()
的get
获取输入值,set
来修改渲染值。
<body>
<div id="app"></div>
姓名:<input id="name" type="text" ><br>
年龄:<input id="age" type="number" >
<script>
let obj = {
name : 'zzk',
_name:'zzk',
age : '23',
_age:'23'
}
let app = document.getElementById('app')
app.innerHTML = `我叫:${obj.name},今年:${obj.age}`
//监听name的改变
Object.defineProperty(obj,'name',{
get:function(){
console.log('读取姓名:',this._name)
//返回_name是防止无限递归。返回name时需要去读里面的值,此时又会调用get,直到堆栈益处
return this._name
},
set:function(val){
this._name = val
app.innerHTML = `我叫:${this.name},今年:${this.age}`
console.log('修改姓名:',val);
}
})
//监听age的改变
Object.defineProperty(obj,'age',{
get:function(){
console.log('读取姓名:',this._age)
return this._age
},
set:function(val){
this._age = val
app.innerHTML = `我叫:${this.name},今年:${this.age}`
console.log('修改年龄:',val);
}
})
//获取输入的name
let name = document.getElementById('name')
name.addEventListener('input',function(){
obj.name = name.value
})
//获取输入的age
let age = document.getElementById('age')
age.addEventListener('input',function(){
obj.age = age.value
})
</script>
</body>
- Watcher(订阅者):属性变化时,更新视图。每当数据更新时调用
set
方法,通知Dep(发布者),Dep调用对应参数的watcher的update对页面进行更新。 - Compile(模版编译器):解析dom节点,替换模版数据并初始化视图,将模版指令对应的节点绑定对应的更新函数,创建Watcher。
- Vue的生命周期
Vue实例从创建到销毁的过程。具体过程为创建、初始化数据、编译模板、挂载DOM、渲染=>更新=>渲染、卸载、销毁一系列事件。
Vue更新节点
Virtural DOM
:将真实DOM生成一颗Virtural DOM
,如果某个节点数据改变产生一个新的Vnode
,之前的Virtural DOM
则为oldVnode
,然后对比Vnode
和oldVnode
。Virtural DOM
将真实的DOM数据抽取出来,以对象的形式模拟树结构
真实的节点元素:
<div id="wrap">
<p class="title">Hello world!</p>
</div>
Virtural DOM:
{
tag:'div',
attrs:{
id:'wrap'
},
children:[
{
tag:'p',
text:'Hello world!',
attrs:{
class:'title',
}
}
]
}
diff算法
1.patch
方法:判断节点是继续比较还是直接进行替换。
function patch (oldVnode, vnode) {
// some code
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
} else {
const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点
let parentEle = api.parentNode(oEl) // 父元素
createEle(vnode) // 根据Vnode生成新元素
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点
oldVnode = null
}
}
// some code
return vnode
}
首先判断Vnode
和oldVnode
是否一样,一样则继续比较子节点内容。如果不一样则Vnode
直接替换oldVnode
。
diff算法是逐层进行比较,如果第一层不相同就不继续比较第二层,直接进行替换
sameVnode
方法:判断节点是否相同。
function sameVnode (a, b) {
return (
a.key === b.key && // key值
a.tag === b.tag && // 标签名
a.isComment === b.isComment && // 是否为注释节点
// 是否都定义了data,data包含一些具体信息,例如onclick , style
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) // 当标签是<input>的时候,type必须相同
)
}
3.patchVnode
方法:对Vnode
和oldVnode
做出更新操作。
patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el
let i, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return
if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
api.setTextContent(el, vnode.text)
}else {
updateEle(el, vnode, oldVnode)
if (oldCh && ch && oldCh !== ch) {
updateChildren(el, oldCh, ch)
}else if (ch){
createEle(vnode) //create el's children dom
}else if (oldCh){
api.removeChildren(el)
}
}
}
patchVnode
方法主要功能:
- 找到对应的真实dom,设置为
el
- 判断
Vnode
和oldVnode
是否指向同一个对象,如果是,那么直接返回 - 如果他们都有都有文字节点且不相等,将
oldVnode
的el
的文本节点设置为Vnode
的文本节点
这也就解释了为什么<input></input>
修改后内容不会改变,因为直接复用的父节点的<input></input>
,如果想重新生成<input></input>
的内容需要使用key,下文会讲到 - 如果两者都有字节点,则执行
updateChildren
方法进行更新,下文会讲到 - 如果
Vnode
有子节点,oldVnode
没有子节点,则将Vnode
的子节点真是化之后添加到el
里 - 如果
oldVnode
有子节点,Vnode
没有子节点,则删除el
的字节点
4.updateChildren
方法:对Vnode
和oldVnode
的子节点进行更新。
代码复杂,此处先讲清楚该方法的功能和逻辑
- 将
Vnode
的子节点Vch
和oldVnode
的子节点oldCh
单独提取出来 oldCh
和Vch
各有两个头尾变量StartIdx
和EndIdx
,他们的两个变量相互比较,一共有4种比较方式。(old-StartIdx
:V-StartIdex
/old-StartIdx
:V-EndIdex
/old-EndIdx
:V-StartIdx
/old-EndIdx
:V-EndIdx
)- 如果4种比较都没有匹配,如果设置
key
,就会用key
进行比较 - 在遍历的过程中,变量会往中间移动,一旦
old-StartIdx > old-EndIdx
或V-StartIdex > V-EndIdx
表明oldCh
或Vch
中至少有一个已经完成了遍历,则结束比较
图解updateChildren
粉红色的部分为oldCh和vCh
- 我们将它们取出来并分别用s和e指针指向它们的头child和尾child
- 现在分别对
oldS
、oldE
、S
、E
两两做sameVnode
比较,有四种比较方式,当其中两个能匹配上那么真实dom中的相应节点会移到Vnode
相应的位置,这句话有点绕,打个比方
如果是
oldS
和E
匹配上了,那么真实dom中的第一个节点会移到最后如果是
oldE
和S
匹配上了,那么真实dom中的最后一个节点会移到最前,匹配上的两个指针向中间移动如果四种匹配没有一对是成功的,分为两种情况
- 如果新旧子节点都存在
key
,那么会根据oldChild
的key
生成一张hash
表,用S
的key
与hash
表做匹配,匹配成功就判断S
和匹配节点是否为sameNode
,如果是,就在真实dom中将成功的节点移到最前面,否则,将S
生成对应的节点插入到dom中对应的oldS
位置,S
指针向中间移动,被匹配old
中的节点置为null。 - 如果没有
key
,则直接将S
生成新的节点插入真实DOM(ps:这下可以解释为什么v-for
的时候需要设置key
了,如果没有key
那么就只会做四种匹配,就算指针中间有可复用的节点都不能被复用了)
- 如果新旧子节点都存在
- 再配个图(假设下图中的所有节点都是有key的,且key为自身的值)
第一步
oldS = a, oldE = d;
S = a, E = b;
oldS
和S
匹配,则将dom中的a节点放到第一个,已经是第一个了就不管了,此时dom的位置为:a b d
- 第二步
oldS = b, oldE = d;
S = c, E = b;
oldS
和E
匹配,就将原本的b
节点移动到最后,因为E
是最后一个节点,他们位置要一致,这就是上面说的:当其中两个能匹配上那么真实dom中的相应节点会移到Vnode
相应的位置,此时dom的位置为:a d b
- 第三步
oldS = d, oldE = d;
S = c, E = d;
oldE
和E
匹配,位置不变此时dom的位置为:a d b
- 第四步
oldS++;
oldE--;
oldS > oldE;
遍历结束,说明oldCh
先遍历完。就将剩余的vCh
节点根据自己的的index
插入到真实dom中去,此时dom位置为:a c d b
一次模拟完成。
这个匹配过程的结束有两个条件:
oldS
>oldE
表示oldCh
先遍历完,那么就将多余的vCh
根据index
添加到dom中去(如上图)S
>E
表示vCh
先遍历完,那么就在真实dom中将区间为[oldS, oldE]
的多余节点删掉- 下面再举一个例子,可以像上面那样自己试着模拟一下
- 当这些节点
sameVnode
成功后就会紧接着执行patchVnode
了,可以看一下上面的代码
if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
}
就这样层层递归下去,直到将oldVnode和Vnode中的所有子节点比对完。也将dom的所有补丁都打好。
v-if中key的作用举一个例子,在一个场景中,我们有一个切换按钮,可以切换输入框为邮箱或者姓名,如果在邮箱的输入框中输入结束后再点击切换按钮,会发现所有dom都重新构建但是输入框的内容并没有发生变化:
- 此时为输入邮箱
- 输入邮箱地址
- 点击输入类型切换:
此时已经重新切换了为用户名输入,但是输入框的内容还保留为之前的内容 - 删除输入框的内容可以看到提示语句也已经改变
- 产生的原因是因为上面讲的
patchVnode
方法中有一个判断:如果他们都有都有文字节点且不相等,将oldVnode
的el
的文本节点设置为Vnode
的文本节点。
要避免这个现象只需要设置key
,在Vue渲染的时候会认为此dom不相同会直接进行替换,代码如下:
<template>
<div>
<div v-if="type === 'name'">
<label>用户名:</label>
<input type="text" placeholder="请输入用户名..." key="name"/>
</div>
<div v-else>
<label>邮箱:</label>
<input type="text" placeholder="请输入邮箱..." key="email"/>
</div>
<div>
<button @click="handleToggleClick">切换输入的类型</button>
</div>
</div>
</template>
<script>
export default {
name: "HelloWorld",
data() {
return {
type: "",
};
},
methods: {
handleToggleClick() {
this.type = this.type === "name" ? "mail" : "name";
},
},
};
</script>
父传子:props
- 基础使用方式:
// 父组件
<template>
<div>
parent:下面是我的子组件
<childSon :userName='name'></childSon>
</div>
</template>
<script>
import childSon from './Childs'
export default {
name:'Parent',
components:{
childSon
},
data(){
return{
name:'啊哈'
}
}
}
</script>
//子组件
<template>
<div>
child:这是父组件给我传的数据——{{userName}}
</div>
</template>
<script>
export default {
name:'Childs',
props:['userName'],
data(){
return{
}
}
}
</script>
props
验证:接受指定类型的数据
//父组件
<template>
<div>
parent:下面是我的子组件
<childSon :name='name' :firstName='firstName' :age='18' ></childSon>
</div>
</template>
<script>
import childSon from './Childs'
export default {
name:'Parent',
components:{
childSon
},
data(){
return{
name:'大卫',
firstName:'大华'
}
}
}
</script>
//子组件
<template>
<div>
child:这是父组件给我传的数据——name:{{name}}——firstName:{{firstName}}——lastName:{{lastName}}——age:{{age}}
</div>
</template>
<script>
export default {
name: "Child",
props: {
name: String,
firstName: {
type: String, //规定值的类型
required: true, //必须传值,否则报错
},
lastName: {
type: String,
default: "lastNameDefault", //如果不传值,则为default的值
},
age: {
type: [String, Number], //类型可以是多种
validator: function (value) { //自定义验证,如果不满足条件Console会报错,但是不影响正常值得输出
let num = parseInt(value);
if (num > 0 && num < 10) {
return true;
} else {
return false;
}
},
},
},
data() {
return {};
},
};
</script>
运行结果如下:
如果validator
不满足条件也可以正常输出,但是Console
里会报错
props
传入一个本地变量:父元素<input></input>
里面内容改变,子组件的值也相应改变
//父组件
<template>
<div>
parent:<input type='text' v-model="content">下面是我的子组件
<childSon :content='content' ></childSon>
</div>
</template>
<script>
import childSon from './Childs'
export default {
name:'Parent',
components:{
childSon
},
data() {
return {
content:'er'
};
},
}
</script>
//子组件
<template>
<div>
child:这是父组件给我传的数据——{{con}}
</div>
</template>
<script>
export default {
name:'Childs',
props:['content'],
data(){
return{
con:this.content
}
}
}
</script>
props
传入对象:v-bind
会自动将对象属性给子组件赋值
//父组件
template>
<div>
parent:下面是我的子组件
<childSon v-bind='obj' ></childSon>
</div>
</template>
<script>
import childSon from './Childs'
export default {
name:'Parent',
components:{
childSon
},
data(){
return{
obj: {
name: 'lily',
age: '16'
}
}
}
}
//子组件
<template>
<div>
child:这是父组件给我传的数据——name:{{name}}——age:{{age}}
</div>
</template>
<script>
export default {
name:'Childs',
props:{
name: String,
age: {
type: [String,Number], //类型可以是多种
validator: function(value) { //自定义验证
let num = parseInt(value)
if (num > 0 && num <100) {
return true;
} else {
return false;
}
}
}
}
}
</script>
</script>
子传父:$emit
子组件的值不能传递给父组件,只能通过$emit
将事件发送出去。$emit
方法的第一个参数是事件名称,第二个参数是要传递的值。父组件必须要监听$emit
抛出的事件才能获取到传递的值。
//父组件
<template>
<div>
parent:这是我的子组件传给我的值:{{num}}
<childSon :content='content' @getNum='getMsg'></childSon> //监听子组件抛出的方法,通过函数来获取抛出的值
</div>
</template>
<script>
import childSon from './Childs'
export default {
name:'Parent',
components:{
childSon
},
data() {
return {
content:'er',
num:''
}
},
methods: {
getMsg(num){
this.num = num;
}
}
}
</script>
//子组件
<template>
<div>
child:这是父组件给我传的数据——{{ content }} <br />
<button @click="sendMsgtoParent">点击我可以向父子间传递参数哦</button>
</div>
</template>
<script>
export default {
name: "Child",
props: ["content"],
data() {
return {
num: 0,
};
},
methods: {
sendMsgtoParent() {
this.$emit("getNum", this.num++); //第一个是抛出的事件名称,第二个是参数
},
},
};
</script>
- js中的继承都是通过原型链来实现
- js中只有对象没有类的概念
- class是一种语法糖
- class中的所有方法都是定义在
prototype
上 - class也是一种函数
- class中extends做了两件事:
class B extends A
- 将类
B
对象中的__proto__
指向A
(b
由类A
构建) - 将类
B
的原型对象(prototype
)的__proto__
指向A
如果没有继承关系时如下所示:
- 将类
B
对象中的__proto__
指向Object
- 将类
B
的原型对象(prototype
)的__proto__
指向function
funciton
和class
的不同写法
//函数名和实例化构造名相同且大写(非强制,但这么写有助于区分构造函数和普通函数)
function Person(name,age) {
this.name = name;
this.age=age;
}
Person.prototype.say = function(){
return "我的名字叫" + this.name+"今年"+this.age+"岁了";
}
var obj=new Person("zzk",18);//通过构造函数创建对象,必须使用new 运算符
console.log(obj.say());//我的名字叫zzk今年18岁了
class Person{//定义了一个名字为Person的类
constructor(name,age){//constructor是一个构造方法,用来接收参数
this.name = name;//this代表的是实例对象
this.age=age;
}
say(){//这是一个类的方法,注意千万不要加上function
return "我的名字叫" + this.name+"今年"+this.age+"岁了";
}
}
var obj=new Person("zzk",18);
console.log(obj.say());//我的名字叫zzk今年18岁了