理解Vue内部渲染机制(避免页面性能瓶颈) 您所在的位置:网站首页 vue页面重新渲染机制 理解Vue内部渲染机制(避免页面性能瓶颈)

理解Vue内部渲染机制(避免页面性能瓶颈)

2023-11-24 03:12| 来源: 网络整理| 查看: 265

前言

Vue 提升了我们开发效率和页面性能,但是在某些情况下 Vue 页面的性能甚至大大低于直接使用 jQuery。

我之前写过一篇 虚拟列表 处理大量数据,但是项目中会使用一些第三方 ui 库,或者存在不适用虚拟列表的情况。

最近也详细的看了下 Vue 的渲染机制,下面说说咱们开发者怎么避免一些 Vue 页面的性能瓶颈。

开始

我们先定义一个组件,后面一步一步对这个组件进行优化。

// template 查询 {{ row[key] }} // js export default { data: function() { return { tableData: [], keys: [], columns: [], } }, methods: { search() { const { data, keys } = this.getData() this.keys = keys this.columns = keys.map(key => ({ title: key, key: key, dataIndex: key, scopedSlots: { customRender: key }, })) this.tableData = data }, // 模拟请求的数据 getData() { const arr = [] let i = 1000 while (i--) { const item = { id: i, a0: 1, a1: 1, a2: 1, a3: 1, a4: 1, a5: 1, a6: 1, a7: 1, a8: 1, a9: 1} const data = { id: i, b0: item, b1: item, b2: item, b3: item, b4: item, b5: item, b6: item, b7: item, b8: item, b9: item } arr.push(data) } return { data: arr, keys: Object.keys(arr[0]) } } } } 如何渲染大量数据

这个时候我们已经定义了一个组件,只要点击按钮查询,就会渲染大量的数据。

image.png

性能

查询中创建假数据的时间小到可以忽略不计,剩下可以看到 Vue 对每条数据添加响应式总共用了 2.3s,然后渲染用了 1.26s,我们从点击按钮到看到数据总共花了约 3.6s。

image.png

优化

我们上面 js 连续执行时间过长,会导致页面长时间无法交互,而且渲染时间太慢,用户一次交互反馈的时间过长,我们对这两点进行优化。

{ // 刚才的查询方法 search() { // 省略代码。。。 // 我们把赋值操作换到 setData 方法中 // this.tableData = data this.setData(data) }, setData(data) { if (!data.length) return // 我们把数据切割成十份进行批次渲染 requestAnimationFrame(async () => { const num = 100 this.tableData.push(...data.slice(0, num)) this.setData(data.slice(num)) }) }, } 效果

可以看到刚才的任务被分割成十份,每一份的间隙都会进行渲染,这个时候我们减少了 js 的连续运行时间,并且加快了渲染时间,利用加长总运行时间换取了渲染时间,用户既能快速得到反馈,而且不会因为过长时间的 js 运行而无法与页面交互。

image.png

效果详细

我们把区域缩小,发现用户得到反馈的时间用了 3694 - 3160 = 500(ms),比起之前的 3.6s 快了 6 倍!

image.png

总结

我们在 Vue 项目中尽量避免一次性渲染大量数据,采用分批渲染效果会更好。

组件化的重要性

我们在 Vue 项目的开发过程中会不断的封装组件,这样不仅利于我们维护,而且在性能上也会有很大的提升,下面还是接着上面的代码,说一说组件化对性能的影响。

我们在 data 里面定义一个 num: 0,然后在刚才的查询按钮旁边放置一个按钮。

查询 {{ num }}

image.png

性能

当我们页面中出现了大量数据之后,我们点击刚才新创建的按钮,我们会发现页面很卡顿。

我总共点击了两次,每一次大约都会执行 1.7s js,当我点开运行 js 任务时,发现占用时间的就是 Vue 更新当前组件的方法。

因为在 Vue2.x 之后 Vue 的响应式是以组件为粒度进行更新的,只要修改了当前组件中所使用的数据(咱们修改的是 num),组件就会整个去进行更新,我们的 1.7s 中有大量时间是对表格数据去做 diff 算法的对比。

image.png

优化

我们现在要做的是把表格抽离为一个组件。

// MyTable.vue // template {{ row[key] }} // js export default { // 需要的数据 props: ['keys', 'tableData', 'columns'] } // 父组件内 // template // js // 引入封装的组件,并且要注册组件哦。 import MyTable from './MyTable.vue' 效果

我还是点击了两次,你会发现根本没有刚才那么长时间的 js 任务,现在总共才花了 6ms 运行 js,比没封装组件之前快了 573倍(1719 / 3)。

image.png

原因

为什么封装成了组件性能这么高呢,这涉及到 Vue 的更新机制。

Vue 在更新的时候会调用 patch 方法进行虚拟 dom 的对比,path 里面调用了 patchVnode,而每个组件生成的时候会有一个 hook,里面会有一个 prepatch 方法,对比到 MyTable 组件的时候就会进入这个方法。

// 组件的钩子声明 const componentVNodeHooks: { init(vnode: any, hydrating: boolean): boolean | null; prepatch(oldVnode: any, vnode: any): void; insert(vnode: any): void; destroy(vnode: any): void; } function patchVnode ( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly ) { // 省略代码... let i const data = vnode.data if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { // 对比到组件时会进入这个 prepatch 方法 i(oldVnode, vnode) } // 省略代码... }

这个 prepatch 里面调用了 updateChildComponent 方法,这就是我们没封装组件之前,运行时间过长的方法。

prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) { const options = vnode.componentOptions const child = vnode.componentInstance = oldVnode.componentInstance updateChildComponent( child, options.propsData, // updated props options.listeners, // updated listeners vnode, // new parent vnode options.children // new children ) }

我们进入这个方法瞅一眼,会发现有个 update props 这一行,咱们再不进套娃函数了,太深了。

这块的作用是什么,就是子组件初始化的时候会把自己的 props 变成响应式的,在下面的代码中会依次对 props 进行赋值,因为是响应式的,所以只要子组件使用到的 props 里面的值发生了变化,就会触发子组件的更新,但是此时因为 props 里面的值没有发生变化,所以子组件不会触发更新。

export function updateChildComponent ( vm: Component, propsData: ?Object, listeners: ?Object, parentVnode: MountedComponentVNode, renderChildren: ?Array ) { // 省略代码... // update props if (propsData && vm.$options.props) { toggleObserving(false) const props = vm._props const propKeys = vm.$options._propKeys || [] for (let i = 0; i < propKeys.length; i++) { const key = propKeys[i] const propOptions: any = vm.$options.props // wtf flow? props[key] = validateProp(key, propOptions, propsData, vm) } toggleObserving(true) // keep a copy of raw propsData vm.$options.propsData = propsData } // 省略代码... } 总结

这个时候大家应该知道了组件化的重要性,不仅仅是利于我们维护,而且可以大大提升我们项目的性能,所以我们开发过程中还是尽量要组件化。

插槽对组件的影响

插槽也是我们经常使用的,上面说了组件化如何提升性能,还是接着上面的代码,下面就说一说插槽对我们组件化性能的影响。

我们就在刚才的组件里面随便写了点文字,子组件里面写不写默认 slot 都可以。

// 父组件内 // template 我就是一段简单的静态文字 性能

这个时候我再去点击这个数字按钮,发现又突然卡了起来。

image.png

image.png

原因

还是刚才的 updateChildComponent 方法里面,这个时候 needsForceUpdate 的结果是 true,在最下面会触发 MyTable 组件的强制更新,大家可以看 needsForceUpdate 上面的英文注释,大意就是:(在父级更新期间,父级的任何静态插槽子级可能已更改。动态作用域插槽也可能已更改。在这种情况下,需要强制更新以确保正确性)

export function updateChildComponent ( vm: Component, propsData: ?Object, listeners: ?Object, parentVnode: MountedComponentVNode, renderChildren: ?Array ) { // 省略代码... // Any static slot children from the parent may have changed during parent's update. Dynamic scoped slots may also have changed. In such cases, a forced update is necessary to ensure correctness. const needsForceUpdate = !!( renderChildren || // has new static slots vm.$options._renderChildren || // has old static slots hasDynamicScopedSlot ) // resolve slots + force update if has children if (needsForceUpdate) { vm.$slots = resolveSlots(renderChildren, parentVnode.context) vm.$forceUpdate() } // 省略代码... } 动态插槽

从上面咱们可以看到静态插槽的情况,子组件会强制更新保证正确性,上面也说了动态插槽也会导致强制更新,我们来看一下。

// 父组件内 // template 我就是一段简单的静态文字 性能

这个时候继续点这个数字按钮,发现果然还是卡,和上面静态插槽的效果一致。

image.png

image.png

总结

上面的静态插槽和动态插槽一样,为了保证正确性,都会强制更新,大家也可以看下咱们最开始没抽离组件的代码,那个表格就是因为使用了动态插槽,所以会被强制更新。

作用域插槽

对于作用域插槽,是比较特殊的,需要单独来说一说,下面先看下不同插槽的编译结果。

编译对比

大家可以发现,静态插槽内容的生成是在父组件内完成,而作用域插槽里面的内容是封装在一个 fn 的函数里面,这个不同之处就是作用域插槽的内容不会在父组件中生成,而是等待子组件去调用 fn 函数,在子组件里面生成内容,这也就意味着只有当作用域里插槽面所使用的数据发生变化时,子组件才会被通知更新。

静态插槽:

// template 我就是一段简单的静态文字 // 编译后代码 function render() { with(this) { return _c('MyTable', { attrs: { "keys": keys, "tableData": tableData, "columns": columns } }, [_v("\n 我就是一段简单的静态文字\n ")]) } }

作用域插槽:

// template 我就是一段简单的静态文字 // 编译后代码 function render() { with(this) { return _c('MyTable', { attrs: { "keys": keys, "tableData": tableData, "columns": columns }, scopedSlots: _u([{ key: "default", fn: function () { return [_v("\n 我就是一段简单的静态文字\n ")] }, proxy: true }]) }) } } 性能

这个时候我们去把动态插槽变成作用域插槽,试试效果。

// 父组件内 // template 我就是一段简单的静态文字

去点击按钮!

image.png

效果

我点击了两次,发现 js 运行总时间只有 4 ms,效果杠杠的。

image.png

最后

说了这么多,大家应该对 Vue 的组件化有了一定的认识了吧,如果文中有错误,欢迎大家指正!!!



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有