详聊vue的diff算法

我们都清楚vue的diff算法很强大,面试官也喜欢问你diff算法的原理,本期文章就带大家认识下这个算法的意义所在,以及其过程

想要聊清楚这个,我们需要先清楚虚拟dom

虚拟dom

我们先看一个情景,v-for去循环遍历出一个数组

xml 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
    <div id="app">
        <ul class="list" id="list">
            <li class="item" v-for="item in list">{{item}}</li>
        </ul>
    </div>
    <script>
        const { createApp, ref} = Vue

        createApp({
            setup() {

                const list = ref(['html', 'css', 'js'])

                return {
                    list
                }

            }
        }).mount('#app')
    </script>
</body>
</html>

里面的li并不是原始的html结构,原生html没有v-for,这是vue里面的template模板,对vue编译器来说这些东西都是字符串,vue编译器会将这些代码编译成真实的html

vue的编译器也就是编译函数compiler会把ul编译成真实的html结构,这就需要先解析,通过各种正则匹配,解析成一个对象,我大概用js对象去模拟下最终的效果,这其实就是虚拟dom

css 复制代码
let Dom = {  // 虚拟dom
        tagName: 'ul',
        props: {
            class: 'list',
            id: 'list'
        },
        children: [
            {
                tagName: 'li',
                props: {
                    class: 'item'
                },
                children: ['html']
            },
            {
                tagName: 'li',
                props: {
                    class: 'item'
                },
                children: ['css']
            }, 
            {
                tagName: 'li',
                props: {
                    class: 'item'
                },
                children: ['js']
            }
        ]
    }

虚拟dom本质上就是一个对象,vue中的编译函数compilertemplate模板代码编译成虚拟dom,再然后将虚拟dom编译成html代码

假设我现在将数据源最后一个元素js更改成vue,那么虚拟dom最后是会变成最后的一个children的文本内容变成vue

我们现在思考这个过程,对于编译器来说,这个过程一定是会重新编译的,也就是数据源一旦变更,编译器就会重新工作,编译出一份新的虚拟dom结构

对于编译器来说,现在有两种方案去解决这个问题,一是直接废除原来的虚拟dom,重新从头开始创建一个新的虚拟dom,二是精准找到哪个属性发生了变更,并去修改它,比如这里仅仅最后一个li发生了变更,那我就去修改原虚拟dom的最后一个li即可

很明显,后者方案性能更优,但是yyx选择了前者,只要任意数据源发生了变更,就会让编译器重新从头编译出一份新的虚拟dom,这会加重编译器的负担,编译过程仅仅是靠v8工作的,我们要知道,v8引擎的性能是很高的,其实是不怕这个负担,如果选择后者这个方案,就需要专门搞一个算法去查找哪个地方需要修改,与其耗费大量人力去弄个算法查找数据源的变更以及虚拟dom特定子元素的修改,不如直接重新编译一份新的虚拟dom

v8:我吃柠檬

虚拟dom最终是会被生成一份真实的dom结构的,真实的dom会被拿到浏览器去渲染,也就是回流重绘,要是谈到回流重绘就要考虑到性能问题,因为重绘是非常占用浏览器性能的

这个时候问题又来了,这样不就是两份虚拟dom吗,一个是老的,一个是新的,如果还是按照前面那样,用新的虚拟dom重新渲染到浏览器是很难受的,此时承担负担的可不再是v8,这可是重绘,重绘是浏览器负责的,而实际上我仅仅是修改了ul中的一个li,那就重绘最后一个li即可,可不能再任性了

安利这个画图网站,相当的niceExcalidraw

因此yyx这个时候必须考虑到这个问题,那就不能重新让浏览器从头渲染了,所以这就需要找到两份虚拟dom哪些内容需要修改,不需要修改的就保留

这就是著名的diff算法,找出两份虚拟dom的不同去修改。diff算法之后会产生一个补丁包path,然后再去拿着这个补丁包path,也就是不同点去html身上求修改,这样就能具体改掉某个子容器

这样就大大降低了浏览器的重绘开销~

diff算法

diff全称就是different,就是找不同,找新老虚拟dom的不同,找到并产生一个补丁包

面试官很喜欢问你diff的查找过程是怎样的

diff算法代码我们不用管,只需要知道原理即可

  1. 同层比较,是不是相同的节点,不相同直接废弃OldVDom
  2. 是相同节点,比较节点上的属性,产生一个补丁包path
  3. 继续比较子节点下一层的子节点,采用双端队列的方式,尽量复用,产生一个补丁包
  4. 同上

前面两点我们可以很好理解,就是一个同层比较,但是第三点的双端队列如何理解,这就考虑到比如我的这里三个li的顺序是不同的,其实是可以进行复用的,采用双端队列就可以很好应对这种情况

双端队列的比较是头头比较,头尾比较,尾尾比较,尾头比较

补丁包其实是一个对象,记录了哪里修改

所以diff算法有个小缺陷,比如这里,我仅仅给ul多套一层div,那么原来的VDom就会废除掉,我们站在上帝视角肯定会觉得可以复用,但是这里如果你发现了第一层不同还去继续比较diff算法的代码量就会指数级增加,显得过于复杂了

面试官:v-for为何不建议使用index作为key

刚刚说了,对于diff算法,只要是子元素的顺序发生了变化其实都是可以进行复用的,这就是第三步骤的双端队列比较,还是上面的那个栗子,我如果用index作为key,然后我添加一个翻转函数,最终是js,css,html,模拟新老dom如下

vbnet 复制代码
let OldDom = [
    {
        tagName: 'li',
        key: 0,
        value: 'html'
    },
    {
        tagName: 'li',
        key: 1,
        value: 'css'
    },
    {
        tagName: 'li',
        key: 2,
        value: 'js'
    },
]

let NewDom = [
    {
        tagName: 'li',
        key: 0,
        value: 'js'
    },
    {
        tagName: 'li',
        key: 1,
        value: 'css'
    },
    {
        tagName: 'li',
        key: 2,
        value: 'html'
    },
]

翻转后,OldDom的最后一个li应该是和NewDom的第一个li相同,但是你会发现,其中的key不同,因此双端比较头尾的时候就会认定不同,因为你用的下标作key,无论位置如何颠倒,下标永远认定第一个位置是0

所以如果用了index作为key,那么像是顺序调到后的子元素是无法进行复用的,因此diff算法本身为你考虑好的双端比较就派不上用场了

既然如此,那我不用key行不行?不用key的话,如果两个子元素相同,比如我现在的listhtml和js,js,那么新老dom如下

ini 复制代码
let OldDom = [
    {
        tagName: 'li',
        value: 'html'
    },
    {
        tagName: 'li',
        value: 'js'
    },
    {
        tagName: 'li',
        value: 'js'
    },
]

let NewDom = [
    {
        tagName: 'li',
        value: 'js'
    },
    {
        tagName: 'li',
        value: 'js'
    },
    {
        tagName: 'li',
        value: 'html'
    },
]

这样就会产生个问题,OldDom有两个一样的,不知道留下哪一个,按道理找到了相同的节点是要留下来进行复用的,这里就不清楚保留第二个还是第三个了,就会导致查找不准确的问题,这就会让diff算法调用额外的手段,占用性能

好了现在你就明白了,原来v-for存在的key的意义就是为了让diff算法比较的时候性能更高,否则可能碰到多个相同的子元素,不清楚保留哪一个

v-for不用index作为key就是因为diff算法已经针对了相同的子元素无论顺序是可以进行复用的,而index只要位置变了就会让整个子元素变,因此无法保留,这样就会导致重新生成子元素,浪费性能

这种时候肯定有小朋友在想,能否用随机数作key,肯定是不行的!Math.random会在你保存代码时候,vue编译器重新执行代码,随机数又会重新变化,因此新老dom永远都不会相等

最后

vue使用的diff算法很强大,考虑到可以复用不同位置的子元素而使用双端队列比较,既然如此,我们就不建议使用index作为key,这样人家辛苦打造的双端diff就无用武之地,浪费性能!key不用也会浪费性能,实际上这个key一般都是使用后端返回的唯一id

如果你对春招感兴趣,可以加我的个人微信:Dolphin_Fung,我和我的小伙伴们有个面试群,可以进群讨论你面试过程中遇到的问题,我们一起解决

另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请"点赞+评论+收藏"一键三连,感谢支持!

相关推荐
桂月二二37 分钟前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
沈梦研2 小时前
【Vscode】Vscode不能执行vue脚本的原因及解决方法
ide·vue.js·vscode
hunter2062062 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb2 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角2 小时前
CSS 颜色
前端·css
轻口味2 小时前
Vue.js 组件之间的通信模式
vue.js
浪浪山小白兔3 小时前
HTML5 新表单属性详解
前端·html·html5
lee5763 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579653 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter
limit for me4 小时前
react上增加错误边界 当存在错误时 不会显示白屏
前端·react.js·前端框架