广告位联系
返回顶部
分享到

Vue的diff算法原理介绍

JavaScript 来源:互联网 作者:秩名 发布时间:2022-03-17 21:33:52 人浏览
摘要

思维导图 0. 从常见问题引入 虚拟dom是什么? 如何创建虚拟dom? 虚拟dom如何渲染成真是dom? 虚拟dom如何patch(patch) 虚拟DOM的优势?(性能) Vue中的key到底有什么用,为什么不能用index? Vu

思维导图

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

0. 从常见问题引入

  • 虚拟dom是什么?
  • 如何创建虚拟dom?
  • 虚拟dom如何渲染成真是dom?
  • 虚拟dom如何patch(patch)
  • 虚拟DOM的优势?(性能)
  • Vue中的key到底有什么用,为什么不能用index?
  • Vue中的diff算法实现
  • diff算法是深度还是广度优先遍历

1. 生成虚拟dom

1. h方法实现

virtual dom ,也就是虚拟节点

1.它通过js的Object对象模拟dom中的节点

2.再通过特定的render方法将其渲染成真实的dom节点

eg:

1

2

3

4

<div id="wrapper" class="1">

    <span style="color:red">hello</span>

    world

</div>

如果利用h方法生成虚拟dom的话:

1

h('div', { id: 'wrapper', class: '1' }, h('span', { style: { color: 'red' } }, 'hello'), 'world');

对应的js对象如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

let vd = {

    type: 'div',

    props: { id: 'wrapper', class: '1' },

    children: [

        {

            type: 'span',

            props: { color: 'red' },

            children: [{}]

        },

        {

            type: '',

            props: '',

            text: 'world'

        }

    ]

}

自己实现一个h方法

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

function createElement(type, props = {}, ...children) {

    // 防止没有传值的话就赋值一个初始值

    let key;

    if (props.key) {

        key = props.key

        delete props.key

    }

    // 如果孩子节点有字符串类型的,也需要转化为虚拟节点

    children = children.map(child => {

        if (typeof child === 'string') {

            // 把不是节点类型的子节点包装为虚拟节点

            return vNode(undefined, undefined, undefined, undefined, child)

        } else {

            return child

        }

    })

    return vNode(type, props, key, children)

}

function vNode(type, props, key, children, text = undefined) {

    return {

        type,

        props,

        key,

        children,

        text

    }

}

2. render方法实现

render的作用:把虚拟dom转化为真实dom渲染到container容器中去

1

2

3

4

export function render(vnode, container) {

    let ele = createDomElementFrom(vnode) //通过这个方法转换真实节点

    if (ele) container.appendChild(ele)

}

把虚拟dom转化为真实dom,插入到容器中,如果虚拟dom对象包含type值,说明为元素(createElement),否则为节点类型(createTextnode),并把真实节点赋值给虚拟节点,建立起两者之间的关系

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

function createDomElementFrom(vnode) {

    let { type, key, props, children, text } = vnode

    if (type) {//说明是一个标签

        // 1. 给虚拟元素加上一个domElemnt属性,建立真实和虚拟dom的联系,后面可以用来跟新真实dom

        vnode.domElement = document.createElement(type)

        // 2. 根据当前虚拟节点的属性,去跟新真实dom的值

        updateProperties(vnode)

        // 3. children中方的也是一个个的虚拟节点(就是递归把儿子追加到当前元素里)

        children.forEach(childVnode => render(childVnode, vnode.domElement))

    } else {//说明是一个文本

    }

    return vnode.domElement

 

}

 

function updateProperties(newVnode, oldProps = {}) {

    let domElement = newVnode.domElement //真实dom,

    let newProps = newVnode.props; //当前虚拟节点中的属性

    // 如果老的里面有,新的里面没有,说明这个属性被移出了

    for (let oldPropName in oldProps) {

        if (!newProps[oldPropName]) {

            delete domElement[oldPropName] //新的没有,为了复用这个dom,直接删除

        }

    }

    // 如果新的里面有style,老的里面也有style,style可能还不一样

    let newStyleObj = newProps.style || {}

    let oldStyleObj = oldProps.style || {}

    for (let propName in oldStyleObj) {

        if (!newStyleObj[propName]) {

            domElement.style[propName] = ''

        }

    }

    // 老的里面没有,新的里面有

    for (let newPropsName in newProps) {

        // 直接用新节点的属性覆盖老节点的属性

        if (newPropsName === 'style') {

            let styleObj = newProps.style;

            for (let s in styleObj) {

                domElement.style[s] = styleObj[s]

            }

        } else {

            domElement[newPropsName] = newProps[newPropsName]

        }

    }

 

}

根据当前虚拟节点的属性,去更新真实dom的值
由于还有子节点,所以还需要递归,生成子节点虚拟dom的真实节点,插入当前的真实节点里去

在这里插入图片描述

3. 再次渲染

刚刚可能会有点不解,为什么要把新的节点和老的节点属性进行比对,因为刚刚是首次渲染,现在讲一下二次渲染

比如说现在构建了一个新节点newNode,我们需要和老节点进行对比,然而并不是简单的替换,而是需要尽可能多地进行复用

首先判断父亲节点的类型,如果不一样就直接替换

如果一样

1.文本类型,直接替换文本值即可

2.元素类型,需要根据属性来替换

这就证明了render方法里我们的oldProps的必要性,所以这里把新节点的真实dom赋值为旧节点的真实dom,先复用一波,待会再慢慢修改

updateProperties(newVnode, oldVNode.props)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

export function patch(oldVNode, newVnode) {

    // //判断类型是否一样,不一样直接用新虚拟节点替换老的

    if (oldVNode.type !== newVnode.type) {

        return oldVNode.domElement.parentNode.replaceChild(

            createDomElementFrom(newVnode), oldVNode.domElement

        )

    }

    // 类型相同,且是文本

    if (oldVNode.text) {

        return oldVNode.document.textContent = newVnode.text

    }

    // 类型一样,不是文本,是标签,需要根据新节点的属性更新老节点的属性

    // 1. 复用老节点的真实dom

    let domElement = newVnode.domElement = oldVNode.domElement

    // 2. 根据最新的虚拟节点来更新属性

    updateProperties(newVnode, oldVNode.props)

    // 比较儿子

    let oldChildren = oldVNode.children

    let newChildren = newVnode.children

    // 1. 老的有儿子,新的有儿子

    if (oldChildren.length > 0 && newChildren.length > 0) {

        // 对比两个儿子(很复杂)

    } else if (oldChildren.length > 0) {

        // 2. 老的有儿子,新的没儿子

        domElement.innerHTML = ''

    } else if (newChildren.length > 0) {

        // 3. 新增了儿子

        for (let i = 0; i < newChildren.length; i++) {

            // 把每个儿子加入元素里

            let ele = createDomElementFrom(newChildren[i])

            domElement.appendChild(ele)

        }

    }

 

 

}

2. diff算法

刚刚的渲染方法里,首先是对最外层元素进行对比,对于儿子节点,分为三种情况

1.老的有儿子,新的没儿子(那么直接把真实节点的innerHTML设置为空即可)

2.老的没儿子,新的有儿子(那么遍历新的虚拟节点的儿子列表,把每一个都利用createElementFrom方法转化为真实dom,append到最外层真实dom即可)

3.老的有儿子,新的有儿子,这个情况非常复杂,也就是我们要提及的diff算法

1. 对常见的dom做优化

  • 前后追加元素
  • 正序和倒序元素
  • 中间插入元素

以最常见的ul列表为例子

旧的虚拟dom

1

2

3

4

5

6

let oldNode = h('div', {},

    h('li', { style: { background: 'red' }, key: 'A' }, 'A'),

    h('li', { style: { background: 'blue' }, key: 'B' }, 'A'),

    h('li', { style: { background: 'yellow' }, key: 'C' }, 'C'),

    h('li', { style: { background: 'green' }, key: 'D' }, 'D'),

);

情况1:末尾追加一个元素(头和头相同)

新的虚拟节点

在这里插入图片描述

1

2

3

4

5

6

7

let newVnode = h('div', {},

    h('li', { style: { background: 'red' }, key: 'A' }, 'A'),

    h('li', { style: { background: 'blue' }, key: 'B' }, 'B'),

    h('li', { style: { background: 'yellow' }, key: 'C' }, 'C1'),

    h('li', { style: { background: 'green' }, key: 'D' }, 'D1'),

    h('li', { style: { background: 'black' }, key: 'D' }, 'E'),

);

eg:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

// 比较是否同一个节点

function isSameVnode(oldVnode, newVnode) {

    return oldVnode.key == newVnode.key && oldVnode.type == newVnode.type

}

// diff

function updateChildren(parent, oldChildren, newChildren) {

    // 1. 创建旧节点开头指针和结尾

    let oldStartIndex = 0

    let oldStartVnode = oldChildren[oldStartIndex];

    let oldEndIndex = oldChildren.length - 1

    let oldEndVnode = oldChildren[oldEndIndex];

    // 2. 创建新节点的指针

    let newStartIndex = 0

    let newStartVnode = newChildren[newStartIndex];

    let newEndIndex = newChildren.length - 1

    let newEndVnode = newChildren[newEndIndex];

    // 1. 当从后面插入节点的时候,希望判断老的孩子和新的孩子 循环的时候,谁先结束就停止循环

    while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {

        // 注意:比较对象是否相等,你不能用==,因为指向的位置可能不一样,可以用type和key

        if (isSameVnode(oldStartVnode, newStartVnode)) {

            //patch比对更新

            patch(oldStartVnode, newStartVnode)

            // 移动指针

            oldStartVnode = oldChildren[++oldStartIndex]

            newStartVnode = newChildren[++newStartIndex]

        }

    }

    if (newStartIndex <= newEndIndex) {

        for (let i = newStartIndex; i <= newEndIndex; i++) {

            parent.appendChild(createDomElementFrom(newChildren[i]))

        }

    }

}

在这里插入图片描述

情况2:队首添加一个节点(尾和尾)

在这里插入图片描述

在这里插入图片描述

头和头+尾和尾的处理方法:

我们通过parent.insertBefore(createDomElementFrom(newChildren[i]), beforeElement)使得末尾添加和头部添加采用同一种处理方法

1

2

3

4

5

6

7

8

// 如果是从前往后遍历说明末尾新增了节点,会比原来的儿子后面新增了几个

// 也可以时从后往前遍历,说明比原来的儿子前面新增了几个

if (newStartIndex <= newEndIndex) {

    for (let i = newStartIndex; i <= newEndIndex; i++) {

        // 取得第一个值,null代表末尾

        let beforeElement = newChildren[newEndIndex + 1] == null ? null : newChildren[newEndIndex + 1].domElement   parent.insertBefore(createDomElementFrom(newChildren[i]), beforeElement)

    }

}

图解:

在这里插入图片描述

MVVM=>数据一变,就调用patch

情况3:翻转类型(头和尾)

在这里插入图片描述

尾和头就不画图了

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

else if (isSameVnode(oldStartVnode, newEndVnode)) {

                // 头和尾巴都不一样,拿老的头和新的尾巴比较

                patch(oldStartVnode, newEndVnode)

                // 把旧节点的头部插入到旧节点末尾指针指向的节点之后一个

                parent.insertBefore(oldStartVnode.domElement, oldEndVnode.domElement.nextSibling)

                // 移动指针

                oldStartVnode = oldChildren[++oldStartIndex]

                newEndVnode = newChildren[--newEndIndex]

            } else if (isSameVnode(oldEndVnode, newStartVnode)) {

                // 头和尾巴都不一样,拿老的头和新的尾巴比较

                patch(oldEndVnode, newStartVnode)

                // 把旧节点的头部插入到旧节点末尾指针指向的节点之后一个

                parent.insertBefore(oldEndVnode.domElement, oldStartVnode.domElement)

                // 移动指针

                oldEndVnode = oldChildren[--oldEndIndex]

                newStartVnode = newChildren[++newStartIndex]

            } else {

情况4: 暴力比对复用

在这里插入图片描述

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

else {

                // 都不一样,就暴力比对

                // 需要先拿到新的节点去老的节点查找是否存在相同的key,存在则复用,不存在就创建插入即可

                // 1. 先把老的哈希

                let index = map[newStartVnode.key]//看看新节点的key在不在这个map里

                console.log(index);

                if (index == null) {//没有相同的key

                    // 直接创建一个,插入到老的前面即可

                    parent.insertBefore(createDomElementFrom(newStartVnode),

                        oldStartVnode.domElement)

                } else {//有,可以复用

                    let toMoveNode = oldChildren[index]

                    patch(toMoveNode, newStartVnode)//复用要先patch一下

                    parent.insertBefore(toMoveNode.domElement, oldStartVnode.domElement)

                    oldChildren[index] = undefined

                    // 移动指正

                }

                newStartVnode = newChildren[++newStartIndex]

            }

// 写一个方法,做成一个哈希表{a:0,b:1,c:2}

function createMapToIndex(oldChildren) {

    let map = {}

    for (let i = 0; i < oldChildren.length; i++) {

        let current = oldChildren[i]

        if (current.key) {

            map[current.key] = i

        }

    }

    return map

}

对于key的探讨

1. 为什么不能没有key

在这里插入图片描述

2. 为什么key不能是index

在这里插入图片描述

3. diff的遍历方式

采用的是深度优先,只会涉及到dom树同层的比较,先对比父节点是否相同,然后对比儿子节点是否相同,相同的话对比孙子节点是否相同

在这里插入图片描述


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 : https://blog.csdn.net/darabiuz/article/details/123465698
相关文章
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计