本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力。
前言:本文参与的话题是 Vue 核心知识点和实现原理,话不多说就是搞。
模板转换成视图的过程
- Vue.js通过编译将template 模板转换成渲染函数(render ) ,执行渲染函数就可以得到一个虚拟节点树
- 在对 Model 进行操作的时候,会触发对应 Dep 中的 Watcher 对象。Watcher 对象会调用对应的 update 来修改视图。这个过程主要是将新旧虚拟节点进行差异对比,然后根据对比结果进行DOM操作来更新视图。
我们先对上图几个概念加以解释:
- 渲染函数:渲染函数是用来生成Virtual DOM的。Vue推荐使用模板来构建我们的应用界面,在底层实现中Vue会将模板编译成渲染函数,当然我们也可以不写模板,直接写渲染函数,以获得更好的控制。
- VNode 虚拟节点:它可以代表一个真实的 dom 节点。通过 createElement 方法能将 VNode 渲染成 dom 节点。简单地说,vnode可以理解成节点描述对象,它描述了应该怎样去创建真实的DOM节点。
- patch(也叫做patching算法) :虚拟DOM最核心的部分,它可以将vnode渲染成真实的DOM,这个过程是对比新旧虚拟节点之间有哪些不同,然后根据对比结果找出需要更新的的节点进行更新,其实际作用是在现有DOM上进行修改来实现更新视图的目的。
虚拟DOM
什么是虚拟DOM
Virtual DOM 是一棵以 JavaScript 对象作为基础的树,每一个节点称为 VNode ,用对象属性来描述节点,实际上它是一层对真实 DOM 的抽象,最终可以通过渲染操作使这棵树映射到真实环境上,简单来说 Virtual DOM 就是一个 Js 对象,用以描述整个文档。
<ul id='myId'>
<li>Item 1</li>
<li>Item 2</li>
<ul>
{
tag: 'ul'
attributes: { id: 'myId' }
children: [
//这里是 li
]
};
虚拟DOM优势
-
具备跨平台的优势
由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力。
-
操作 DOM 慢,js运行效率高,提高效率。
因为DOM操作的执行速度远不如Javascript的运算速度快,因此,把大量的DOM操作搬运到Javascript中,运用patching算法来计算出真正需要更新的节点,最大限度地减少DOM操作,从而显著提高性能。Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)
-
提升渲染性能
Virtual DOM的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。
虚拟DOM作用
虚拟DOM的最终目标是将虚拟节点渲染到视图上。但是如果直接使用虚拟节点覆盖旧节点的话,会有很多不必要的DOM操作。例如,一个ul标签下很多个li标签,其中只有一个li有变化,这种情况下如果使用新的ul去替代旧的ul,因为这些不必要的DOM操作而造成了性能上的浪费。
为了避免不必要的DOM操作,虚拟DOM在虚拟节点映射到视图的过程中,将虚拟节点与上一次渲染视图所使用的旧虚拟节点(oldVnode)做对比,找出真正需要更新的节点来进行DOM操作,从而避免操作其他无需改动的DOM。
虚拟DOM在Vue.js主要做了两件事:
- 提供与真实DOM节点所对应的虚拟节点vnode
- 将虚拟节点vnode和旧虚拟节点oldVnode进行对比,然后更新视图
什么是VNode
Vue.js 利用 createElement 方法创建 VNode。就是描述真实节点的js对象
- 其实vnode只是一个名字,本质上其实是Javascript中一个普通的对象,是从VNode类实例化的对象。我们用这个Javascript对象来描述一个真实DOM元素的话,那么该DOM元素上的所有属性在VNode这个对象上都存在对应的属性。
- vnode可以理解成节点描述对象,它描述了应该怎样去创建真实的DOM节点。例如tag表示一个元素节点的名称,text表示一个文本节点的文本,children表示子节点等。
- vnode表示一个真实的DOM元素,所有真实的DOM节点都使用vnode创建并插入到页面中。(vnode-->DOM-->视图)
- vnode和视图是一一对应的。我们可以把vnode理解成Javascript对象版本的DOM元素。
- 渲染视图的过程是先创建vnode,然后再使用vnode去生成真实的DOM元素,最后插入页面渲染视图。
VNode的作用
由于每次渲染视图时都是先创建vnode,然后使用它创建真实DOM插入到页面中,所以可以将上一次渲染视图时所创建的vnode缓存起来,之后每当需要重新渲染视图时,将新创建的vnode和上一次缓存的vnode进行对比,查看它们之间有哪些不一样的地方,找出这些不一样的地方并基于此去修改真实的DOM。
VNode属性含义
tag: 当前节点的标签名
data: 当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息
children: 当前节点的子节点,是一个数组
text: 当前节点的文本
elm: 当前虚拟节点对应的真实dom节点
ns: 当前节点的名字空间
context: 当前节点的编译作用域
functionalContext: 函数化组件作用域
key: 节点的key属性,被当作节点的标志,用以优化
componentOptions: 组件的option选项
componentInstance: 当前节点对应的组件的实例
parent: 当前节点的父节点
raw: 简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false
isStatic: 是否为静态节点
isRootInsert: 是否作为跟节点插入
isComment: 是否为注释节点
isCloned: 是否为克隆节点
isOnce: 是否有v-once指令
栗子🌰
//html
<div class="test">
<span class="demo">hello,VNode</span>
</div>
//vnode
{
tag: 'div'
data: {
class: 'test'
},
children: [
{
tag: 'span',
data: {
class: 'demo'
}
text: 'hello,VNode'
}
]
}
Vue.js
snaddom
// 编译
h('a', { props: { href: 'http://www.baidu.com' }}, 'Hello word');
// 得到
{ "sel": "a", "data": { props: { href: 'http://www.baidu.com' } }, "text": "Hello word" }
// 真实的节点
<a href="http://www.baidu.com">Hello word</a>
Vnode的类型
vnode是Javascript中的一个对象,不同类型的vnode之间其实只是属性不同,准确地说是有效属性不同。因为当使用VNode类创建一个vnode时,通过参数为实例设置属性时,无效的属性会默认被赋值为undefined或false。对于vnode身上无效属性,直接忽略就好。
注释节点
export const createEmptyVNode = text =>{
const node = new VNode();
node.text = text;
node.isComment = true;
return node
}
//(1)一个注释节点只有两个有效属性-----text和isComment,其余属性全是默认的undefined或则false。
//对应的vnode如下:
{
text:"注释节点",
isComment:true
}
// 注释节点
文本节点
export function createTextVNode(val){
return new VNode(undefined,undefined,undefined,String(val))
}
//(1)文本类型的vnode被创建时,它只有一个text属性
//文本类型的vnode
{
text:"Hello Berwin"
}
克隆节点
(1)克隆节点是将现有节点的属性赋值到新节点中,让新创建的节点和被克隆的节点的属性保持一致,从而实现克隆效果。
(2)作用:优化静态节点和插槽节点(slot node)。
(3)以静态节点为例,当组件内的某个状态发生变化后,当前组件会通过虚拟DOM重新渲染视图,静态节点因为它的内容不会改变,所以除了首次渲染需要执行渲染函数获取vnode之外,后续更新不需要执行渲染函数重新生成vnode。因此,这时就会使用创建克隆节点的方法将vnode克隆一份,使用克隆节点进行渲染。这样就不需要重新执行渲染函数生成新的静态节点的vnode,从而提升一定程度的性能。 (4)克隆现有节点时,只需要将现有节点的属性全部赋值到新节点中即可。
(5)克隆节点和被克隆节点之间的唯一区别是isCloned属性,克隆节点的isCloned为true,被克隆的原始节点的isCloned为false。
export function cloneVNode(vnode,deep){
const cloned = new VNode(
vnode.tag,
vnode.data,
vnode.children,
vnode.text,
vnode.elm,
vnode.context,
vnode.componentOptions,
vnode.asyncFactory
)
cloned.ns = vnode.;
cloned.isStatic = vnode.isStatic;
cloned.key = vnode.key;
cloned.isComment = vnode.isComment;
cloned.isCloned = true;
if(deep&&vnode.children){
cloned.chlidren = cloneVNodes(vnode.children);
}
return cloned;
}
元素节点
(1)元素节点通常存在以下4种有效属性。
tag:tag是一个节点的名称,例如ul、p、li和div等。 data:该属性包含了一些节点上的数据,比如attrs、class和style等。 children:当前节点的子节点列表。 context:它是当前组件的Vue.js实例。
// 真实的元素节点
<p><span>Hello</span><span>Berwin</span></p>
// 对应的vnode
{
children:[VNode,VNode],
context:{...},
data:{...},
tag:"p",
....
}
组件节点
componentOptions:组件节点的选项参数,其中包含propsData、tag和children等信息。 componentInstance:组件的实例,也就是Vue.js实例。事实上,在Vue.js种,每个组件都是一个Vue.js实例。
// 一个组件节点
<child></child>
// 对应的vnode如下:
{
componentInstance:{...},
componentOptions:{...},
context:{...},
data:{...},
tag:"vue-component-1-child",
....
}
函数式组件
函数式组件和组件节点类似,它有两个独有的属性functionalContext和functionalOptions。
{
functionalContext:{...},
functionalOptions:{...},
context:{...},
data:{...},
tag:"div",
....
}
Patch
updateChildren
oldStartIdx
、newStartIdx
、oldEndIdx
以及newEndIdx
分别是新老两个VNode
两边的索引,同时oldStartVnode
、newStartVnode
、oldEndVnode
和new EndVnode
分别指向这几个索引对应的vnode
。整个遍历需要在oldStartIdx
小于oldEndIdx
并且newStartIdx
小于newEndIdx
(这里为了简便,称sameVnode
为相似)。
- 当
oldStartVnode
不存在的时候,oldStartVnode
向右移动,oldStartIdx
加1
。 - 当
oldEndVnode
不存在的时候,oldEndVnode
向右移动,oldEndIdx
减1
。 oldStartVnode
和newStartVnode
相似,oldStartVnode
和newStartVnode
都向右移动,oldStartIdx
和newStartIdx
都增加1
。
oldEndVnode
和newEndVnode
相似,oldEndVnode
和newEndVnode
都向左移动,oldEndIdx
和newEndIdx
都减1
。
oldStartVnode
和newEndVnode
相似,则把oldStartVnode.elm
移动到oldEndVnode.elm
的节点后面。然后oldStartIdx
向后移动一位,newEndIdx
向前移动一位。
oldEndVnode
和newStartVnode
相似时,把oldEndVnode.elm
插入到oldStartVnode.elm
前面。同样的,oldEndIdx
向前移动一位,newStartIdx
向后移动一位。
当以上情况都不符合的时候。
生成一个key
与旧vnode
对应的哈希表。
//createKeyToOldIdx 函数,该函数的作用是 当 对比两个子节点数组时,建立 key-index映射代理遍历查找 sameNode.提高性能。
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
最后生成的对象就是以children
的key
为属性,递增的数字为属性值的对象例如
children = [{
key: 'key1'
}, {
key: 'key2'
}]
// 最后生成的map
map = {
key1: 0,
key2: 1,
}
所以oldKeyToIdx
就是key
和旧vnode
的key
对应的哈希表 根据newStartVnode
的key
看能否找到对应的oldVnode
。
-
如果
oldVnode
不存在,就创建一个新节点,newStartVnode
向右移动。 -
如果找到节点:
-
并且和
newStartVnode
相似。将map
表中该位置的赋值undefined
(用于保证key
是唯一的)。同时将newStartVnode.elm
插入啊到oldStartVnode.elm
的前面,然后index
向后移动一位。 -
如果不符合
sameVnode
,只能创建一个新节点插入到parentElm
的子节点中,newStartIdx
向后移动一位。
-
结束循环后
oldStartIdx
又大于oldEndIdx
,就将新节点中没有对比的节点加到队尾中。
如果newStartIdx > newEndIdx
,就说明还存在新节点,就将这些节点进行删除。
点赞支持、手留余香、与有荣焉,动动你发财的小手哟,感谢各位大佬能留下您的足迹。
往期精彩推荐
Git 相关推荐
面试相关推荐
node系列教学及更多精彩详见:个人主页