前言
作者简介:Quixn,专一于 Node.js 技术栈分享,前端从 JavaScript 到 Node.js,再到后端数据库,优质文章举荐,【小Q全栈指南】作者,Github 博客开源我的项目 github.com/Quixn…
大家好,我是Quixn。明天简略聊聊 Vue 的整个渲染渲染过程
。嫌文章太长者,可间接到文末看精简版总结
哦。
Vue 的整个渲染过程
Vue
的渲染引擎是基于Virtual DOM
实现的。上面是Vue
的整个渲染过程的简要概述:
1、解析模板:Vue首先会解析模板,并生成一个形象语法树(AST)。
2、生成渲染函数:Vue依据AST生成一个渲染函数,该函数用于生成Virtual DOM树。
3、执行渲染函数:当组件的状态发生变化时,Vue会从新执行渲染函数,生成一个新的Virtual DOM树。
4、比照新旧Virtual DOM树:Vue会比照新旧Virtual DOM树的差别,找出须要更新的局部。
5、更新DOM:Vue会依据差别更新实在的DOM树。
如果你是Vue
的初学者,以下提供一份简略的流程图和一则小故事不便你记忆和了解,也可间接跳过哦。
小故事:
当你应用Vue开发利用时,就好比是你成为了一名餐厅的厨师,你须要将菜单(Vue的模板)转化为美味的佳肴(渲染出的页面)。
首先,你须要将菜单交给服务员(Vue编译器)来翻译,而后把翻译后的菜单(AST)给到你。翻译实现后,你须要依据这份菜单开始烹制食材(Vue的渲染函数)。
你应用菜单上的指令和属性来抉择要应用的食材(渲染函数中的数据)和烹饪办法(Vue的指令)。这些指令和属性通知你哪些食材须要加工,哪些须要展现给客人,哪些不须要展现等等。
接着,你开始烹饪。你遵循菜单上的步骤来解决食材(执行渲染函数),最终你烹制出了一份美味佳肴(生成了Virtual DOM树)。
最初,你须要将这份美味佳肴上菜(渲染到页面上),让客人能够品味到你的作品(浏览器展现渲染后果)。如果客人有一些不喜爱的菜品(须要更新的局部),你须要回到厨房再烹制一份,而后再把新菜品端给客人(更新Virtual DOM树和从新渲染页面)。
这就是Vue的整个渲染过程,将模板转化为AST,而后生成渲染函数,最初渲染到页面上,一次又一次地循环,直到呈现出残缺的页面。
Vue
的渲染过程中,最要害的局部是生成Virtual DOM树
和比照新旧Virtual DOM树的差别
。Vue通过一种叫做Virtual DOM Diff算法
的算法来实现这一过程。该算法会比照新旧Virtual DOM树的构造和属性,找出差别,并将差别利用到实在的DOM树上,从而实现高效的更新。
1、解析模板
当Vue
利用启动时,会将模板传入Vue的编译器
进行解析和编译
。Vue的编译器
将模板解析成一个形象语法树(AST),并将AST
转换为渲染函数
。
上面是Vue
模板解析的根本流程:
首先,Vue会将模板字符串转换为AST树的模式。在这个过程中,Vue
应用了一个叫做htmlparser2
的库来解析模板。
Vue
将模板中的指令
和属性
转换为对应的AST节点
。这些节点被称为指令节点或属性节点。指令节点
蕴含指令名称
和表达式
,属性节点蕴含属性名称和属性值。
在模板解析的过程中,Vue
会将模板中的文本内容
转换为文本节点
。文本节点
中蕴含文本内容
。
2、生成渲染函数
解析完模板后,Vue
会对AST
进行动态剖析
(至于这个过程是如何进行动态剖析的,前面会专门出一篇文章来讲,此处不做剖析),以便优化渲染函数的生成。动态剖析能够检测到不须要更新的节点,从而防止不必要的更新。
最初,Vue
将AST
转换为渲染函数
。渲染函数
是一个JavaScript函数
,用于生成Virtual DOM树并将其渲染到实在的DOM树上。
上面是一个简略的Vue模板和生成的AST节点的示例:
<template>
<div class="container">
<h1>{{ title }}</h1>
<p v-if="showText">{{ text }}</p>
</div>
</template>
解析后的生成的AST节点:
{
type: 1,
tag: 'div',
attrsList: [{ name: 'class', value: 'container' }],
attrsMap: { class: 'container' },
children: [
{
type: 1,
tag: 'h1',
children: [
{
type: 2,
expression: '_s(title)',
text: '{{ title }}'
}
]
},
{
type: 1,
tag: 'p',
if: 'showText',
ifConditions: [
{
exp: 'showText',
block: {
type: 1,
tag: 'p',
children: [
{
type: 2,
expression: '_s(text)',
text: '{{ text }}'
}
]
}
}
]
}
]
}
在这个示例中,解析后的AST树
中蕴含一个div节点
和两个子节点
,一个h1
节点和一个p
节点。h1
节点中蕴含一个文本节点
,它的文本内容是模板中的{{ title }}
。p
节点有一个v-if
指令,Vue会将这个指令转换为一个if
属性,并在ifConditions
数组中保留条件
和对应的节点
。在理论的渲染过程中,Vue会依据if属性的值来判断是否
须要渲染这个节点。
下面咱们曾经将模板转换为AST
树了,接下来就是将AST树转换生成渲染函数。在 Vue
中,将 AST
转换为渲染函数的过程,实际上是在编译阶段实现的。编译器将模板转换为一个蕴含渲染函数的函数,这个函数接管一个参数 render
,执行后会返回一个 VNode
节点。
具体来说,编译器将模板解析为 AST
,而后对 AST
进行动态剖析,进行一系列的优化,最初通过遍历 AST
生成渲染函数。生成渲染函数的过程中,会用到源码里的 codegen.js
文件中定义的一些函数来生成代码。
上面是一个简略的示例,假如咱们有这样一个 Vue
模板:
<template>
<div>
<p>{{ message }}</p>
<button @click="handleClick">Click Me</button>
</div>
</template>
编译器将会把这个模板解析为 AST
,而后遍历 AST
生成一个渲染函数:
function render(_ctx, _cache) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("p", null, _toDisplayString(_ctx.message), 1 /* TEXT */),
_createVNode("button", { onClick: _cache[1] || (_cache[1] = $event => _ctx.handleClick($event)) }, "Click Me")
]))
}
下面的渲染函数中,_ctx
是组件实例的上下文对象,_cache
是一个缓存对象,存储了事件处理函数等。
渲染函数返回的是一个 VNode
节点,示意要渲染的内容。在渲染过程中,Vue
会依据这个 VNode
节点构建实在的 DOM
节点并插入到页面中。
3、执行渲染函数
通过下面两个步骤咱们曾经将模板解析并生成了渲染函数了,接下来Vue
又是如何执行渲染函数
的呢?
在 Vue
中,执行渲染函数的过程是通过调用虚构 DOM
的 patch
办法来实现的。patch
办法接管两个参数
,一个是旧 VNode
,另一个是新 VNode
。它会比拟这两个 VNode
的差别,并将差别利用到实在的 DOM
上。
在组件初始化或数据更新时,Vue
会生成新的 VNode
并与旧的 VNode
进行比拟,找出两者之间的差别,并将这些差别利用到实在的 DOM 上。
上面是一个简略的示例,它演示了如何应用渲染函数来创立一个蕴含一个按钮和一个计数器的组件:
<!-- App.vue -->
<template>
<div>
<button @click="increment">{{ count }}</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
},
render() {
return h('div', [
h('button', { on: { click: this.increment } }, this.count)
])
}
}
</script>
在下面的代码中,咱们应用render
函数来创立 VNode
树,并返回一个根节点。在这个例子中, 函数render
里的h
函数是 Vue
提供的用于创立 VNode
的辅助函数。
在这个例子中,如果用户点击了按钮,Vue
将会从新调用render
函数,并返回一个更新后的 VNode
树。而后,Vue
将会将这个 VNode
树与之前的 VNode
树进行比拟,并计算出须要对 DOM
进行的更改。这些更改将被传递给虚构 DOM
的 patch
办法,该办法将这些更改利用到实在的 DOM
上,从而实现更新视图的成果。
这里只是一个简略的示例,实际上,渲染函数可能更加简单,能够应用计算属性、循环等性能来实现更加弱小的动静 UI
。然而无论如何,渲染函数的基本原理都是通过比拟新旧 VNode
的差别,并将这些差别利用到实在的 DOM
上来实现更新视图的成果。
这个示例的过程就像咱们在玩扑克牌时,每个玩家手中领有一些牌,须要一直地调整牌的地位,直到失去最优的牌型。
假如咱们有两个玩家A
和B
,他们各自手中有5
张牌,每次玩家A会换掉一张手牌,而后将手牌展现给所有玩家看。其余玩家能够察看A
的换牌过程,而后依据A
的牌型和其余玩家手中的牌来决定本人是否换牌。
在这个过程中,A
换掉牌相当于更新了数据,其余玩家察看A
换牌的过程相当于生成新的虚构DOM
树,而后依据新旧虚构DOM
树的变动来决定是否更新视图。每个玩家都能够通过观察A换牌的过程来失去最新的牌型和最优的决策。
类比到Vue
中,每个组件都有本人的数据,当数据发生变化时,Vue
会依据渲染函数生成新的虚构DOM
树,而后通过比对新旧虚构DOM
树来判断哪些节点须要更新。最终,Vue
会将更新的节点通过DOM操作更新到视图上,让用户看到最新的内容。
4、比照新旧Virtual DOM树
下面咱们说到Vue
会依据渲染函数生成新的虚构DOM
树,而后通过比对新旧虚构DOM
树来判断哪些节点须要更新。那Vue
又是通过什么来做这个判断的呢?答案就是Virtual DOM Diff算法
。
Vue
的Virtual DOM Diff算法
是一个用于比拟新旧Virtual DOM
树之间差别
的算法。它通过比照两棵树的节点,找到须要更新、增加和删除的节点,而后对实在DOM
进行最小化的操作。
Vue
的Diff
算法能够大抵分为以下三个步骤:
首先比拟新旧Virtual DOM
的根节点
,如果它们的标签名
、key
和属性
都雷同,则认为这两个节点是雷同的
,能够间接比拟它们的子节点
。如果根节点不同,则间接将新的Virtual DOM
替换旧的Virtual DOM
。
比照子节点,通过遍历新旧Virtual DOM
树的子节点列表,按程序进行比拟。Vue
应用了一些启发式的策略来尽可能地缩小比拟次数,例如只对同级节点进行比拟
,通过节点的key
来疾速定位到雷同的节点等。在比拟过程中,如果有子节点不同
,就将新的子节点插入到旧的子节点的地位上,而后递归
对新旧子节点进行比拟。
比照实现后,失去了须要更新、增加和删除的节点,Vue
会将这些操作打包成一个补丁(patch)
,而后将补丁利用到实在DOM
上,从而实现更新。
上面是一个简略的Vue
组件的代码示例,以阐明Diff
算法的实现形式:
<template>
<div>
<h1>{{ title }}</h1>
<ul>
<li v-for="item in items" :key="item.id">{{ item.text }}</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
title: 'Todo List',
items: [
{ id: 1, text: 'Learn Vue' },
{ id: 2, text: 'Build an app' },
{ id: 3, text: 'Deploy to production' },
],
};
},
methods: {
addItem() {
this.items.push({ id: 4, text: 'Test' });
},
},
};
</script>
在这个例子中,当调用addItem
办法增加新的item
时,Vue
会从新执行渲染函数,生成一个新的Virtual DOM
树。Vue
会比照新旧Virtual DOM
树的差别,并将差别利用到实在的DOM
树上,从而实现高效的更新。
Vue2
和Vue3
的Virtual DOM Diff
算法有一些不同之处,Vue3
对Diff
算法进行了一些优化,以进步渲染性能和缩小内存占用。上面别离介绍Vue2
和Vue3
的Diff
算法。
(1)、Vue2的Diff算法:
Vue2
的Diff
算法应用了一种称为双端指针
的策略,通过同时遍历
新旧Virtual DOM
树,从而缩小比拟次数,进步性能。具体来说,算法的过程如下:
- 1、如果新旧节点是
同一个节点
,间接比拟它们的子节点。 - 2、如果新旧节点
不是同一个节点
,且它们都有子节点,则将旧节点的子节点依照key
映射到一个对象上,而后遍历新节点的子节点列表,通过key
从旧节点的映射对象中找到对应的节点进行比拟,找不到则创立新节点。 - 3、如果新旧节点
不是同一个节点
,且旧节点没有子节点,间接用新节点替换旧节点。 - 4、如果新旧节点
不是同一个节点
,且新节点没有子节点,删除旧节点。
(2)、Vue3的Diff算法:
Vue3
的Diff
算法采纳了一种称为“动静布局”
的策略,通过寻找最长递增子序列
,从而缩小比拟次数,进步性能。具体来说,算法的过程如下:
- 1、如果新旧节点是同一个节点,间接比拟它们的子节点。
- 2、如果新旧节点不是同一个节点,且它们都有子节点,则应用“最长递增子序列”算法对子节点进行匹配,将不须要更新的节点复用,须要更新的节点打上标记。
- 3、如果新旧节点不是同一个节点,且旧节点没有子节点,间接用新节点替换旧节点。
- 4、如果新旧节点不是同一个节点,且新节点没有子节点,删除旧节点。
上面是一个简略的Vue3组件的代码示例,以阐明Diff算法的实现形式:
<template>
<div>
<h1>{{ title }}</h1>
<ul>
<li v-for="item in items" :key="item.id">{{ item.text }}</li>
</ul>
</div>
</template>
<script>
import { reactive } from 'vue';
export default {
setup() {
const state = reactive({
title: 'Todo List',
items: [
{ id: 1, text: 'Learn Vue' },
{ id: 2, text: 'Build an app' },
{ id: 3, text: 'Deploy to production' },
],
});
const addItem = () => {
state.items.push({ id: 4, text: 'Test' });
};
return { state, addItem };
},
};
</script>
在这个例子中,咱们应用了Vue3
的新个性setup()
函数来定义组件。当调用addItem
办法增加新的item
时,Vue
会从新执行setup()
函数,生成一个新的Virtual DOM
树。Vue
会比照新旧Virtual DOM
树的差别,并将差别利用到实在的DOM
树上,从而实现高效的更新。
5、更新DOM
在Vue
执行完渲染函数并且比照新旧的vnode树后,如果发现须要更新DOM,那么就会执行patch
函数来实现DOM更新。
patch
函数定义在src/core/vdom/patch.js
中,其次要作用是比拟新旧节点的差别,而后进行最小化的DOM操作,从而尽可能减少DOM操作的次数,进步渲染效率。
上面是patch函数的简化代码示例:
function patch(oldVnode, vnode) {
if (sameVnode(oldVnode, vnode)) {
// 如果新旧节点雷同,执行updateChildren函数更新子节点
updateChildren(oldVnode, vnode)
} else {
// 如果新旧节点不同,间接替换旧节点
const parent = api.parentNode(oldVnode.elm)
api.insertBefore(parent, createElm(vnode), api.nextSibling(oldVnode.elm))
api.removeChild(parent, oldVnode.elm)
}
}
在代码示例中,updateChildren
函数用于更新新旧节点的子节点,它的实现过程就是通过比拟新旧节点的子节点,找出它们之间的差别,而后进行最小化的DOM
操作。如果新旧节点自身不同,则间接通过DOM
操作替换旧节点。
总之,patch
函数的次要作用就是通过最小化的DOM
操作,将新的vnode
树渲染成实在的DOM
树,并将其插入到HTML
文档中。
总结
本篇文章简要的聊了聊Vue
的整个渲染过程,一共经验了5
个步骤:
1、模板解析,通过htmlparser2
的库来解析模板生成蕴含指令节点、属性节点和文本节点的AST
树。
2、生成渲染函数,经验两个小的步骤,先动态剖析AST
,再转换为渲染函数。
3、执行渲染函数,通过调用虚构 DOM
的 patch
办法来实现。
4、比照新旧虚构 DOM
,介绍了Vue2
和Vue3
diff
算法的异同。
5、最初通过path
函数更新DOM
。
欢送关注,公众号回复【vue的渲染过程
】获取文章的全副脑图资源。
对于我 & Node交换群
大家好,我是 Quixn,专一于 Node.js 技术栈分享,前端从 JavaScript 到 Node.js,再到后端数据库,优质文章举荐。如果你对 Node.js 学习感兴趣的话(后续有打算也能够),能够关注我,加我微信【 Quixn1314 】,拉你进交换群一起交换、学习、共建,或者关注我的公众号【 小Q全栈指南 】。Github 博客开源我的项目 github.com/Quixn…
欢送加我微信【 Quixn1314 】,拉你 进 Node.js 高级进阶群,一起学Node,长期交流学习…
发表回复