尤雨溪的B站直播介绍到更新相比于vue2有1.3~2倍的性能优势。那么vue3比vue2块在哪里?
•Proxy取代defineProperty。这个之前的文章已经提过了。•虚拟dom(v-dom)重写--->静态标记:主要体现在纯粹静态节点将被标记•diff算法:vue2是双端比较。vue3加入了最长递增子序列(一种算法)。
或许这个网址能给你一点启示:http://vue-next-template-explorer.netlify.app/。
当我在模板写下这段代码:
<div>
<div>djtao</div>
<div>{{age}}</div>
</div>
看似html的代码经过vue 3编译,其实是一段js。
import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("div", null, "djtao"),
_createVNode("div", null, _toDisplayString(_ctx.age), 1 /* TEXT */)
]))
}
留意到模板代码中存在变量的时候,_createVNode
方法多了第四个参数1
,提示node为文本节点。而模板中的djtao作为纯静态节点,第四个参数不传,就是纯静态节点,在vdom diff的时候,会被直接忽略。
我们再从模板中加一段:
<div :id="aaa">aaa</div>
// 编译后
_createVNode("div", { id: _ctx.aaa }, "aaa", 8 /* PROPS */, ["id"])
节点的动态部分,会维护在一个数组里。
vue3通过_createVNode
方法的第四个参数,可以确定哪些是动态的,diff的时候判断是需要操作text,属性亦或是class。上面的例子中,第四个参数为1表示只需要关心text。第四个参数为8,表示只需要关心节点的id。
想阅读相关代码,可以在源码package/src/shared/src/patchFlags.ts
中找到。
编译就是把看起来像html的模板字符串,转化为js的过程。
在jquery时代,原本就没有“模板字符串”这种说法。JS想要生成html都是非常暴力的html()
操作。到了js库underscore问世之后,就发明了一种奇怪的写法:
•<%=
标记变量•<%
标记js语法
于是你可能从那个时代看到了这种前端代码:
<script type="text/template" id="tpl">
<% _.each(data, function (item) { %>
<div class="outer">
<%= item.title %> - <%= item.url %> - <%= item.film %>
</div>
<% }); %>
</script>
框架通过解析这段字符串,判断哪些是变量,那些是html节点,并通过innerHTML
来生成html代码。
underscore的模板可以说是一种进步,因为前端可以在相对直观的视野之下渲染模版了。但是每当变量变化,整个代码块的内容都会被重新计算innerHTML。但是我们做个实验:
<div id="app"></div>
<script>
const app = document.querySelector('#app');
let arr = [];
for (let k in app) {
arr.push(k);
}
console.log(arr.length, arr);
</script>
单个空div居然有多达293个属性。
实际上,在js只要通过一个对象即可描述上面这个div:
{
type:'div',
props:{id:'app'},
chidren:[]
}
到了MVVM普及的时代,前端开发者都有了共识:
•类似underscore的解决方案,每次渲染的成本太高了!•dom是万恶之源。应极力避免之•编译时,肯定不是全部编译,而应该是部分编译。(按需编译)
这时,mvvm 编译优化就集中在如何更好地按需编译。
vue3 编译的要点在于:
•使用js来描述dom(虚拟dom)•数据修改,通过diff算法求出需要修改的最小部分——再进行修改。相当于加了一层“缓存”。
作为前端,学习编译原理可以去阅读一个库的源码:
the-super-tiny-compiler :https://github.com/starkwang/the-super-tiny-compiler-cn
未来允许会写一下对这个库的解读笔记。
Vue3 的内容和之前差不多,还是:
1.模板字符串->抽象语法树(ast,用对象来描述dom)2.cransform(语意转换)3.codeGenerate:生成代码。
最简单的render比如——我需要把js编译下列html
<ul id="ul">
<li class="item">1</li>
<li class="item">2</li>
<li class="item">3</li>
</ul>
抽象之后的js代码(ast)可能是
const dom = {
type: 'ul',
props: {
id: 'ul'
},
children: [{
type: 'li',
props: {
class: 'item',
},
children: ['1']
}, {
type: 'li',
props: {
class: 'item',
},
children: ['2']
}, {
type: 'li',
props: {
class: 'item',
},
children: ['3']
}]
}
代码是个简单的递归:
const app = document.querySelector('#app');
const render = (dom, parentNode) => {
const { type, props, children } = dom;
const wrap = document.createElement(dom.type);
for (let attr in props) {
wrap.setAttribute(attr, props[attr]);
}
if (children && children.length) {
// if(typeof children == '')
for (let i = 0; i < children.length; i++) {
if (typeof children[i] == 'string') {
wrap.innerHTML = children[i];
} else {
render(children[i], wrap);
}
}
}
parentNode.appendChild(wrap);
}
render(dom, app);
打开packages/compiler-dom/src/index.ts
export function compile(
template: string,
options: CompilerOptions = {}
): CodegenResult {
return baseCompile(
template,
// ...
)
}
上述代码提示template是一个字符串。跳转到baseCompile:
// we name it `baseCompile` so that higher order compilers like
// @vue/compiler-dom can export `compile` while re-exporting everything else.
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
const onError = options.onError || defaultOnError
const isModuleMode = options.mode === 'module'
/* istanbul ignore if */
// ...
// 1.basePaser 抽象语法树(ast)
const ast = isString(template) ? baseParse(template, options) : template
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(
prefixIdentifiers
)
// 2. 语义转换
transform(
ast,
extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {} // user transforms
)
})
)
// 3. 生成代码
return generate(
ast,
extend({}, options, {
prefixIdentifiers
})
)
}
反映了编译的三大过程。
在执行这段代码时,发生了什么?
const App = {
setup(){
// ..
watchEffect(()=>{
// ..
})
}
}
watchEffect内的方法被执行时,意味着数据变化。
这时候响应式就会通知组件更新,具体怎么更新?就会触发vdom的diff算法。
在传统的vdom(react <=15,vue <=2)中,组件每当收到watcher的依赖,虽然能保证自身按照最小规模的方向去更新数据,但是,仍然避免不了递归遍历整棵树。在这种情况,如果计算耗时于33.3ms(30fps情况下),就会导致肉眼可见的卡顿(丢帧)。
再比如上图,反映的是传统vdom的diff流程,一个dom,性能和模板大小正相关,和动态节点的数量无关。那么可能导致一个情况,一个大组件只有少量动态节点的情况下,依然完整的被遍历。
到了vue3,就不需要遍历整棵树了。
vue早就可以支持jsx了。但在vue3写template,可以获得较jsx更好的性能。
这种追求性能极致的灵感,来源于facebook的开源项目prepack(https://prepack.io/)
Prepack是一个JavaScript源代码优化工具:实际上它是一个JavaScript的部分求值器(Partial Evaluator),可在编译时执行原本在运行时的计算过程,并通过重写JavaScript代码来提高其执行效率。Prepack用简单的赋值序列来等效替换JavaScript代码包中的全局代码,从而消除了中间计算过程以及对象分配的操作。对于重初始化的代码,Prepack可以有效缓存JavaScript解析的结果,使得优化效果最佳。
说到性能提升,离不开虚拟dom的历史。
Vue1.x时代是没有虚拟dom的概念的。它的核心只有依赖(depends),观察者(watcher)还有真实dom。
如上图,每个动态的节点,都对应一个watcher。数据变了,直接去改dom。但是当节点越来越大,结构愈发复杂,随着watcher都增多,会造成性能雪崩。
而对于React 16.4及以下版本,创造性的提出了虚拟dom的概念。但是,React本身是没有响应式系统的。它的更新,依赖于虚拟dom树的diff算法:
如图,先后两个状态,比较发现不同,则更新。
vue2吸取了react的虚拟dom的核心优点。于是wathcer不再通知到真实dom,只通知到“组件(vdom)”,再通过组件去diff,再触发更新。这个举措让vue实现了质的飞跃。
但是,老版本的react依然存在弱点:如果diff时间超过16.6ms(60fps所需单位时间),就会造成卡顿。于是react16再次创造了fibber架构。
所谓fibber树,本质上是一个链表。而链表的特性是可以中断的。当渲染任务超过16.6ms,就把控制权还给主线程。待主线程空闲时,再继续。
而对于vue3来说,提升就在于静态标记。也就是前面所提及的内容。
项目地址:https://github.com/dangjingtao/vue2-vs-vue3.git
我们新建一个项目,直接在项目中引入vue3和vue2.并调用loadash的shuffle方法作为乱序依据。
// 模板
const template =
`<div>
<h1>item length: {{datas.length}}</h1>
<p><b>{{action}}</b> tooks {{time}} ms</p><br>
<button @click="shuffle">shuffle</button>
<ul v-for="item in datas" :key="item.index">
<li>{{item.name}}-{{item.index}}</li>
<li>Lorem ipsum dolor sit amet</li>
<li>Lorem ipsum dolor sit amet</li>
<li>Lorem ipsum dolor sit amet</li>
</ul>
</div>`;
// 数据生成器
const getData = (n) => {
let ret = [];
for (let i = 0; i < n; i++) {
ret.push({ name: 'djtao', index: Math.round(1000000 * Math.random()) })
}
return ret;
}
// 以500条为测试数量
const datas = getData(500);
生成50,500,5000,50000条数据文件,其中动态节点约占1/4。为便于比较,均采用options API写法。
<!--vue2 -->
<script>
let s = window.performance.now();
const vm = new Vue({
el: '#app',
template,
data: {
action: 'render',
time: 0,
datas,
},
mounted() {
this.time = window.performance.now() - s;
},
methods: {
shuffle() {
this.action = 'shuffle';
this.datas = _.shuffle(this.datas);
let s = window.performance.now();
this.$nextTick(() => {
this.time = window.performance.now() - s;
})
}
}
})
</script>
Vue3 写法:
<!--vue3 -->
<script>
Vue.createApp({
template,
data() {
return {
action: 'render',
time: 0,
datas
}
},
mounted() {
this.time = window.performance.now() - s;
},
methods: {
shuffle() {
this.action = 'shuffle';
this.datas = _.shuffle(this.datas);
let s = window.performance.now();
this.$nextTick(() => {
this.time = window.performance.now() - s;
})
}
}
}).mount('#app');
</script>
分别测试5次,取平均值。统计如下
数据量(条) | 50 | 500 | 5000 | 50000 |
---|---|---|---|---|
vue2平均渲染(ms) | 18.88 | 46.26 | 225.88 | 1746.78 |
vue3平均渲染(ms) | 23.58 | 40.32 | 137.4 | 900.24 |
vue2平均乱序(ms) | 4.06 | 17.78 | 146.42 | 1935.94 |
vue3平均乱序(ms) | 2.42 | 13.98 | 94.92 | 1328.88 |
由图可见,在5000及以上条数据量时,vue3比vue3要快50%-100%。
在服务端渲染(ssr)场景下,vue3的性能优势更为明显。
在 https://vue-next-template-explorer.netlify.app/ 沙盒,把选项设置为SSR:
先看纯静态节点的渲染:
<div>
<div>djtao</div>
<div>djtao1</div>
<div>djtao2</div>
</div>
编译之后,发现他们全部被转化为了字符串:
// 编译后
import { mergeProps as _mergeProps } from "vue"
import { ssrResolveCssVars as _ssrResolveCssVars, ssrRenderAttrs as _ssrRenderAttrs } from "@vue/server-renderer"
export function ssrRender(_ctx, _push, _parent, _attrs) {
const _cssVars = ssrResolveCssVars({ color: _ctx.color })
_push(`<div${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}><div>djtao</div><div>djtao1</div><div>djtao2</div></div>`)
}
// Check the console for the AST
接下来手写一下vue的ssr。通过express做服务器。以wrk作为压测工具。
Mac 安装wrk(https://github.com/wg/wrk)
brew install wrk
新建项目ssr 2,安装express
/vue
/vue-server-renderer
/vue-template-compiler
。
npm init -y
npm i express vue vue-server-renderer vue-template-compiler -S
新建一个server.js
,
/**
* server side render(SSR)
* seo 首屏渲染的解决方案
*/
// vue3的ssr主要时静态节点字符串,只有一个buffer,不停地推字符串
const App = {
template:`
<div>
<div v-for="n in 1000" :key="n">
<ul>
<li>Lorem ipsum dolor sit amet</li>
<li>Lorem ipsum dolor sit amet</li>
<li>Lorem ipsum dolor sit amet</li>
<li>Lorem ipsum dolor sit amet</li>
<li>Lorem ipsum dolor sit amet</li>
<li>Lorem ipsum dolor sit amet</li>
<li style="color:red;" v-for="todo in todos">{{n}}-{{todo}}</li>
</ul>
</div>
</div>
`,
data(){
return {
todos: ['eating','sleeping'],
}
}
}
const express = require('express');
const app = express();
const Vue = require('vue');
const render = require('vue-server-renderer').createRenderer();
const vue2compiler = require('vue-template-compiler');
App.render = new Function(vue2compiler.ssrCompile(App.template).render)
app.get('/',async (req,res)=>{
let vApp = new Vue(App);
let html = await render.renderToString(vApp);
// vue 组件解析为字符串。
res.send(`
<h1>vue 2 ssr</h1>
${html}
`);
});
app.listen('9001',err=>{
if(!err){
console.log('server started...')
}
});
看到界面:
Vue2 的服务端渲染就完成了。
执行压测(4进程,100并发,持续15秒):
wrk -t4 -c100 -d15 http://localhost:9001
每秒请求大约在162次。
新建项目ssr 3
npm init -y
npm i express vue@next @vue/server-renderer @vue/compiler-ssr -S
新建server.js
/**
* server side render(SSR)
* seo 首屏渲染的解决方案
*/
// vue3的ssr主要时静态节点字符串,只有一个buffer,不停地推字符串
const App = {
template:`
<div>
<div v-for="n in 1000" :key="n">
<ul>
<li>Lorem ipsum dolor sit amet</li>
<li>Lorem ipsum dolor sit amet</li>
<li>Lorem ipsum dolor sit amet</li>
<li>Lorem ipsum dolor sit amet</li>
<li>Lorem ipsum dolor sit amet</li>
<li>Lorem ipsum dolor sit amet</li>
<li style="color:red;" v-for="todo in todos">{{n}}-{{todo}}</li>
</ul>
</div>
</div>
`,
data(){
return {
todos: ['eating','sleeping'],
}
}
}
const express = require('express');
const app = express();
const Vue = require('vue');
const render = require('@vue/server-renderer');
const vue3compiler = require('@vue/compiler-ssr');
App.ssrRender = new Function('require',vue3compiler.compile(App.template).code)(require);
app.get('/',async (req,res)=>{
let vApp = Vue.createApp(App);
let html = await render.renderToString(vApp);
// vue 组件解析为字符串。
res.send(`
<h1>vue 3 ssr</h1>
${html}
`);
});
app.listen('9002',err=>{
if(!err){
console.log('server started...')
}
});
访问本地9002端口,vue3 ssr就访问成功了。
执行压测(4进程,100并发,持续15秒):
wrk -t4 -c100 -d15 http://localhost:9002
vue3 ssr每秒请求大约在374次。vue3 ssr性能是vue2 2倍以上的差距。
vue3的ssr渲染器的逻辑,是尽可能的把虚拟节点转到字符串。
vue3中复杂组件树,ssr场景下会最大化利用node的异步状态,每个组件是一个buffer, 是一个promise 可以直接await, 服务端任何组件节点,都有可能会有异步数据的依赖。