特点
- 声明式
只需要告诉计算机,你要得到什么样的结果,计算机则会完成你想要的结果,与之相对的是命令式,命令式指的是,用详细的命令机器怎么去处理一件事情以达到你想要的结果。
<div id='app'>
<button @click="count++">
Count:{{ count }}
</button>
</div>
<div id='app'>
<button></button>
</div>
let count = 0;
const btn = document.querSelector('#app button');
btn.textContent = `Count is: ${count}`
btn.addEventListener('click',()=>{
count++;
btn.textContent = `Count is: ${count}`
});
- 组件化
将功能界面拆分为一个个组件,提高代码复用率
<template>
</template>
<script>
export default{
data(){
return{
}
},
methdos:{
}
}
</script>
<style>
</style>
- DIff算法
实现最小化DOM更新
MVVM
- M
- 模型:包含data对象
- 作用:给vue提供数据
- V
- 视图:包含用户界面
- 作用:将VM发送来的data渲染在在页面上,将指令发送给VM
- VM
- 视图模型:vue的核心,是vue的实例对象
- 作用:V和M沟通的桥梁,负责把model的数据同步到view显示出来,还负责把view的修改同步到model
Diff
虚拟dom
使用简单的对象伪装dom元素,里面存储一些dom的重要参数,改变真实dom前,会先比较虚拟dom的数据,如果需要改变,才会将改变应用到真实dom上
// 伪代码实现
var mydiv1{
tagName:'Div',
className:'a'
};
var namediv2{
tagName:'Div',
className:'b'
}
if(mydiv1.className !== mydiv2.className || mydiv1.tagName !== mydiv2.tagName){
change(mydiv)
}
执行过程
深度优先,同级比较
- 数据发生更新时,会调用updateCompontent函数,内部执行**_render方法,生成新的虚拟dom**,把虚拟dom传递给**_update**方法
- _update函数中,首先定义一个变量储存旧的dom,然后再把接受的新的dom放在vm的**_vnode**_属性上,然后调用patch方法对新旧dom进行比较
function Vue(){
const updateComponent = ()=>{
this._update(this._render())
}
}
function _update(vNode){
//将旧虚拟DOM存下来,以便新虚拟DOM赋值vm._vnode
const oldVnode = vm._vnode;
vm._vnode = vNode;
//使用patch函数进行新旧虚拟DOM比较
patch(oldVnode, vNode)
}
- patch函数中,会先调用sameVnode方法查看新旧节点是否值得比较不值得比较的时候,新节点会替代旧节点
patch比较过程
function patch(oldVnode, newVnode) {
// 比较是否为一个类型的节点
if (sameVnode(oldVnode, newVnode)) {
// 是:继续进行深层比较
patchVnode(oldVnode, newVnode)
} else {
// 否
const oldEl = oldVnode.el // 旧虚拟节点的真实DOM节点
const parentEle = api.parentNode(oldEl) // 获取父节点
createEle(newVnode) // 创建新虚拟节点对应的真实DOM节点
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点
// 设置null,释放内存
oldVnode = null
}
}
return newVnode
}
- 首先使用sameVnode方法比较两个节点的tag和key是否一致
funciton sanmeVnode(a,b){
return a.key == b.key && a.tag == b.tag;
}
- 两个节点不同,则直接创建新的,覆盖旧的
- 两个节点相同,就进行打补丁(增删改),本质上就是调用各种更新的函数,更新真实dom上的每个属性,更新的大致思路就是:
- 遍历vnode上的属性,如果不一致,就调用对应的函数进行修改
- 遍历oldVnde上的属性,如果不在vnode上,调用对应的函数进行删除
- 更新完成后,进行子节点比较:
- 先判断是否为文本节点,如果是,就不进行子节点比较
- 新旧节点都具有children,则进入子节点比较(diff)
- 如果新节点有,旧节点没有,则循环创建dom节点
- 如果新节点没有,旧节点有,则循环删除dom节点
function patchVnode (oldVnode, vnode) {
// 新节点引用旧节点的dom
let elm = vnode.elm = oldVnode.elm;
const oldCh = oldVnode.children;
const ch = vnode.children;
// 调用update钩子
if (vnode.data) {
updateAttrs(oldVnode, vnode);
updateClass(oldVnode, vnode);
updateEventListeners(oldVnode, vnode);
updateProps(oldVnode, vnode);
updateStyle(oldVnode, vnode);
}
// 判断是否为文本节点
if (vnode.text == undefined) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) api.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
api.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
api.setTextContent(elm, vnode.text)
}
}
子节点比较(updateChildren)
- 第一步 头头比较。若相似,旧头新头指针后移(即
oldStartIdx++
&&newStartIdx++
),真实dom不变,进入下一次循环;不相似,进入第二步。 - 第二步 尾尾比较。若相似,旧尾新尾指针前移(即
oldEndIdx--
&&newEndIdx--
),真实dom不变,进入下一次循环;不相似,进入第三步。 - 第三步 头尾比较。若相似,旧头指针后移,新尾指针前移(即
oldStartIdx++
&&newEndIdx--
),未确认dom序列中的头移到尾,进入下一次循环;不相似,进入第四步。 - 第四步 尾头比较。若相似,旧尾指针前移,新头指针后移(即
oldEndIdx--
&&newStartIdx++
),未确认dom序列中的尾移到头,进入下一次循环;不相似,进入第五步。 - 第五步 若节点有key且在旧子节点数组中找到sameVnode(tag和key都一致),则将其dom移动到当前真实dom序列的头部,新头指针后移(即
newStartIdx++
);否则,vnode对应的dom(vnode[newStartIdx].elm
)插入当前真实dom序列的头部,新头指针后移(即newStartIdx++
)。 - 但结束循环后,有两种情况需要考虑:
- 新的字节点数组(newCh)被遍历完(
newStartIdx > newEndIdx
)。那就需要把多余的旧dom(oldStartIdx -> oldEndIdx
)都删除,上述例子中就是c,d
; - 新的字节点数组(oldCh)被遍历完(
oldStartIdx > oldEndIdx
)。那就需要把多余的新dom(newStartIdx -> newEndIdx
)都添加。
function updateChildren(el, oldChildren, newChildren) {
let oldStartIndex = 0
let newStartIndex = 0
let oldEndIndex = oldChildren.length - 1
let newEndIndex = newChildren.length - 1
let oldStartVnode = oldChildren[0]
let newStartVnode = newChildren[0]
let oldEndVnode = oldChildren[oldEndIndex]
let newEndVnode = newChildren[newEndIndex]
function makeIndexByKey(children) {
let map = {}
children.forEach((child, index) => {
map[child.key] = index
})
return map
}
// 旧孩子映射表(key-index),用于乱序比对
let map = makeIndexByKey(oldChildren)
// 双方有一方头指针大于尾部指针,则停止循环
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (!oldStartVnode) {
oldStartVnode = oldChildren[++oldStartIndex]
continue
}
if (!oldEndVnode) {
oldEndVnode = oldChildren[--oldEndIndex]
continue
}
// 双端比较_1 - 旧孩子的头 比对 新孩子的头;
// 都从头部开始比对(对应场景:同序列尾部挂载-push、同序列尾部卸载-pop)
if (isSameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode) // 如果是相同节点,则打补丁,并递归比较子节点
oldStartVnode = oldChildren[++oldStartIndex]
newStartVnode = newChildren[++newStartIndex]
}
// 双端比较_2 - 旧孩子的尾 比对 新孩子的尾;
// 都从尾部开始比对(对应场景:同序列头部挂载-unshift、同序列头部卸载-shift)
else if (isSameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode) // 如果是相同节点,则打补丁,并递归比较子节点
oldEndVnode = oldChildren[--oldEndIndex]
newEndVnode = newChildren[--newEndIndex]
}
// 双端比较_3 - 旧孩子的头 比对 新孩子的尾;
// 旧孩子从头部开始,新孩子从尾部开始(对应场景:指针尽可能向内靠拢;极端场景-reverse)
else if (isSameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode)
el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling) // 将 oldStartVnode 移动到 oldEndVnode的后面(把当前节点 移动到 旧列表尾指针指向的节点 后面)
oldStartVnode = oldChildren[++oldStartIndex]
newEndVnode = newChildren[--newEndIndex]
}
// 双端比较_4 - 旧孩子的尾 比对 新孩子的头;
// 旧孩子从尾部开始,新孩子从头部开始(对应场景:指针尽可能向内靠拢;极端场景-reverse)
else if (isSameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode)
el.insertBefore(oldEndVnode.el, oldStartVnode.el) // 将 oldEndVnode 移动到 oldStartVnode的前面(把当前节点 移动到 旧列表头指针指向的节点 前面)
oldEndVnode = oldChildren[--oldEndIndex]
newStartVnode = newChildren[++newStartIndex]
}
// 乱序比对
// 根据旧的列表做一个映射关系,拿新的节点去找,找到则移动;找不到则添加;最后删除多余的旧节点
else {
let moveIndex = map[newStartVnode.key]
// 找的到相同key的老节点,并且是相同节点
if (moveIndex !== undefined && isSameVnode(oldChildren[moveIndex], newStartVnode)) {
let moveVnode = oldChildren[moveIndex] // 复用旧的节点
el.insertBefore(moveVnode.el, oldStartVnode.el) // 将 moveVnode 移动到 oldStartVnode的前面(把复用节点 移动到 旧列表头指针指向的节点 前面)
oldChildren[moveIndex] = undefined // 表示这个旧节点已经被移动过了
patchVnode(moveVnode, newStartVnode) // 比对属性和子节点
}
// 找不到相同key的老节点 or 找的到相同key的老节点但tag不相同
else {
el.insertBefore(createElm(newStartVnode), oldStartVnode.el) // 将 创建的节点 移动到 oldStartVnode的前面(把创建的节点 移动到 旧列表头指针指向的节点 前面)
}
newStartVnode = newChildren[++newStartIndex]
}
}
// 同序列尾部挂载,向后追加
// a b c d
// a b c d e f
// 同序列头部挂载,向前追加
// a b c d
// e f a b c d
if (newStartIndex <= newEndIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
let childEl = createElm(newChildren[i])
// 这里可能是向后追加 ,也可能是向前追加
let anchor = newChildren[newEndIndex + 1] ? newChildren[newEndIndex + 1].el : null // 获取下一个元素
// el.appendChild(childEl);
el.insertBefore(childEl, anchor) // anchor为null的时候等同于 appendChild
}
}
// 同序列尾部卸载,删除尾部多余的旧孩子
// a b c d e f
// a b c d
// 同序列头部卸载,删除头部多余的旧孩子
// e f a b c d
// a b c d
if (oldStartIndex <= oldEndIndex) {
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
if (oldChildren[i]) {
let childEl = oldChildren[i].el
el.removeChild(childEl)
}
}
}
}
- 在新旧虚拟DOM对比更新的时候,默认的diff算法是"就地复用"原则
- “就地复用”:多个子节点比较的时候,如果没有添加key属性,则key属性都是undefined,所以每一个新旧DOM的key都是相同的,所以就会简单的按照节点的顺序依次比较(如果新旧节点的顺序发生变化,vue仍然都是创建新节点删除旧节点)
- 我们可以给每一个节点添加一个key属性,方便Vue跟踪每一个元素的身份,从而在diff算法计算的时候可以按照key确定比较节点
- key的作用:高效的更新渲染虚拟DOM
- 不要使用遍历出来的index作为key(index不是稳定性),key的要求是 唯一性!!! 稳定性!!!