• 9. Vue3中如何将虚拟节点渲染成真实节点


    render函数

    • 利用h函数或者createVNode函数创建好虚拟节点之后,需要将虚拟节点渲染成真实节点。
    • vue采用双向数据绑定,当节点发生改变,新的虚拟节点需要和旧的虚拟节点进行比对,并重新生成真实节点
    • vue3中采用render函数来实现这一功能
      const {createVNode,render,h} =  VueRuntimeDOM
            render(h('div',{style:{color:'red'}},
                [
                h('p',{key:'a'},'a'),
                    h('p',{key:'b'},'b'),
                    h('p',{key:'c'},'c'),
                    h('p',{key:'d'},'d'),
                    h('p',{key:'e'},'e'),
                    h('p',{key:'q'},'q'),
                    h('p',{key:'f'},'f'),
                    h('p',{key:'g'},'g')
                ]
            ),app)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    • 在runtime-core包中有一个函数为createRenderer
    • 该函数接受一个对象,对象中包含各种dom和属性的操作方法
    • 该函数最终返回一个render函数
    • 这个render函数即是渲染虚拟dom的函数
    • 用户实际操作的时候,可以利用createRender函数,传入自己定义的操作方法,创建一个自己的个性化render
    • vue同样给出了其定义好的render方法,在runtime-dom中,在这个包中,之前定义了很多dom和属性的操作方法,刚好做为createRender的参数,为用户返回一个render函数
    //vue内置的渲染器,也可以通过createRenderer创建一个渲染器
    export function render(vnode,container){
        let {render} = createRenderer(renderOptions)
        return render(vnode,container)
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    createRender函数的创建

    逻辑分析
    • render函数基于createRender函数创建,因此要介绍如何创建createRender函数
    1. 函数接收一个dom操作对象,在函数内部会对改对象解构,获取各种dom和属性操作方法

    2. 返回一个render函数,该函数接收两个参数,分别是虚拟节点vnode,和容器container,改函数在节点更新删除都可以使用,因为都要将虚拟节点渲染成新节点,因为只需要比对传入的新的虚拟节点和原来的旧的虚拟节点,就可以判断出是更新节点还是删除节点

    3. 在render函数内部,判断vnode

      • 如果vnode=null,说明新节点为空,该容器要删除所有旧节点
        • 创建旧节点删除方法umount,删除容器之前挂载的旧节点
      • 如果不为null,则创建patch方法,比对旧节点和新节点
      • 最后要将新节点挂载到container上,作为下一次的旧节点,方便下次更新比对
    4. patch函数,传入四个参数;旧虚拟节点vnode1,新虚拟节点vnode2,节点容器container,以及anchor = null

      • anchor为节点插入的参考节点,要新节点要插入时,要考虑插入到哪个节点前面
      • 在patch函数中,首先判断两个节点是否相同(相同指元素名称相同,孩子和属性可以不同)
        • 如果不同,则直接umount原来的节点
      • 获取新虚拟节点的shapeFlags,通过shapeFlags & ShapeFlags.ELEMENT 判断新的虚拟节点是否为元素节点
      • 若是,则创建processElement函数,进行元素比对
    5. processElement函数中,对vnode进行判断,若为null,说明不存在旧节点,则直接根据新的vnode创建真实节点

      • 元素创建方法采用mountElement函数
      • 若不为null,则调用patchElement函数继续比对两个虚拟节点
    6. 在patchElement函数中,因为两个元素的元素名相同,因此只需要比对属性和孩子

      • 通过patchProps来比对属性
      • 通过patchChildren来比对孩子
        • 对孩子的比对最为复杂,因为前者和后者的孩子都有三种可能,文本、数组、null
        • 如果新孩子是文本
          • 之前的是数组,调用unmountChildren函数删除前者孩子
          • 然后判断如果前者孩子不等于后者孩子(无论前者是文本还是null),那么直接对新的元素的文本赋值
        • 如果旧的是数组
          • 最新的还是数组,则比对两个数组,尽量复用节点,直接进入diff比对
          • 最新的不是数组,说明为空,删除原来的元素的孩子
        • 现在已经排除了旧的是数组的情况,新的是文本的情况,那么下面无论旧的是文本还是空,只需要让其为空,新的无论是空还是数组,只需要按照新的内容赋值即可
          在这里插入图片描述
    代码实现
    • createRenderer接收一个操作对象,并对其进行解构,获取对象内部操作函数
      • 操作对象的实际内容可以参考 第7节(7.vue3渲染模块的domAapi实现)
      let {
            createElement: hostCreateElement,
            createTextNode: hostCreateTextNode,
            insert: hostInsert,
            remove: hostRemove,
            querySelector: hostQuerySelector,
            parentNode: hostParentNode,
            nextSibling: hostNextSibling,
            setText: hostSetText,
            setElementText: hostSetElementText,
            patchProp: hostPatchProp
        } = options
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 创建render函数,判断传入节点,并进行节点比对
      • container._vnode上记录了上一次渲染的虚拟节点
    function render(vnode, container) {
            if (vnode == null) {
                //卸载元素
                if (container._vnode) {
                    umount(container._vnode)
                }
            } else {
    
                //更新元素
                patch(container._vnode || null, vnode, container)
            }
            //将节点保留在容器上
            container._vnode = vnode
        }
        return {
            render
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • umount函数直接利用解构出来的dom操作方法
      • 虚拟节点的el上记录了渲染后的真实节点
       function umount(vnode) {
            hostRemove(vnode.el)
        }
    
    • 1
    • 2
    • 3
    • patch函数实现
      • Text类型和Fragment类型后面再说,这里注重考虑default中的逻辑:
    function patch(n1, n2, container, anchor = null) {
            //判断n1和n2是否相同
            if (n1 && !isSameVNode(n1, n2)) {
                //之前存在节点,但和更新的不同,则删除之前的节点
                umount(n1)
                n1 = null
            }
            const { type, shapeFlags } = n2
            switch (type) {
                case Text:
                    processText(n1, n2, container)
                    break
                case Fragment:
                    processFragment(n1,n2,container)
                default:
                    if (shapeFlags & ShapeFlags.ELEMENT) {
    
                        processElement(n1, n2, container, anchor)
                    }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • isSameVNode函数用来判断两个节点是否有相同的类型和key,如果有,
      • 则认为两节点相同(不考虑属性和孩子不同),因此不需要重新渲染新节点成真实节点,只需要在老的节点渲染的真实节点上复用即可
    //判断是否为同一类型节点
    export function isSameVNode(n1,n2){
        return n1.type=== n2.type && n1.key === n2.key
    }
    
    • 1
    • 2
    • 3
    • 4
    • processElement函数实现
      • 如果旧节点为null,则说明不存在之前的真实节点,只需要渲染新节点就行
      • 不为null,则需要对两个节点进行比对,如果能复用就复用,不能复用,就需要删除旧的真实节点,渲染成新的
     function processElement(n1: any, n2: any, container: any, anchor: any) {
            if (n1 == null) {
                //创建节点
                mountElement(n2, container, anchor)
            } else {
                //更新节点
    
                patchElement(n1, n2)
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 真实节点创建函数实现
      • 先创建真实dom,再比对dom中的属性和孩子节点
       function mountElement(vnode, container, anchor) {
    
           let { type, shapeFlags, props, children } = vnode
           let el = vnode.el = hostCreateElement(type)
           if (props) {
               //存在属性则更新属性
               patchProps(null, props, el)
           }
           //此时渲染children,其不是文本就是数组
           if (shapeFlags & ShapeFlags.TEXT_CHILDREN) {
               hostSetElementText(el, children)
           }
           if (shapeFlags & ShapeFlags.ARRAY_CHILDREN) {
               mountChildren(children, el)
           }
           hostInsert(el, container, anchor)
    
       } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 属性更新函数
      • 循环比对新节点属性对象和旧节点属性对象
      • 保证新节点有的添加上,没有的删除掉
      function patchProps(oldProps, newProps, el) {
           if (oldProps == null) {
               oldProps = {}
           }
           if (newProps == null) {
               newProps = {}
           }
           for (let key in newProps) {
               hostPatchProp(el, key, oldProps[key], newProps[key])
           }
           for (let key in oldProps) {
               if (newProps[key] == null) {
                   hostPatchProp(el, key, oldProps[key], null)
               }
           }
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 比较完属性的不同之后,开始比较孩子的不同
      • unmountChildren函数用来卸载原真实节点中的孩子节点
      • patchKeyedChildren函数用来处理旧节点和新节点的孩子都是数组的情况,需要diff算法进行比对,下一章会介绍
      • mountChildren函数用来渲染孩子节点
       function patchChildren(n1, n2, el) {
            let c1 = n1.children
            let c2 = n2.children
            const prevShapeFlag = n1.shapeFlags
            const shapeFlag = n2.shapeFlags
            //如果新孩子是文本
            if (shapeFlag && shapeFlag & ShapeFlags.TEXT_CHILDREN) {
                //之前是数组
                if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    
                    unmountChildren(c1)
                } if (c1 !== c2) {
                    //之前要么文本,要么是空
                    hostSetElementText(el, c2)
                }
            } else {
                //最新的要么是数组,要么就是空
                if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
                    //前后都是数组.
                    if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
                        //diff算法
                        patchKeyedChildren(c1, c2, el)
                    } else {
                        //说明是空
                        unmountChildren(c1)
                    }
                } else {
                    if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
                        hostSetElementText(el, '')
                    }
                    if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
                        mountChildren(c2, el)
                    }
                }
            }
        }
    
    • 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
    • unmountChildren函数
      function unmountChildren(children) {
           children.forEach(child => {
               umount(child)
           })
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • mountChildren函数
     function normalize(children, i) {
           if (isString(children[i]) || isNumber(children[i])) {
               children[i] = createVNode(Text, null, children[i])
           }
           return children[i]
       }
    
       function mountChildren(children, container) {
           for (let i = 0; i < children.length; ++i) {
               //如果数组为字符串或者数字,则是文本节点数组,其它的则是元素节点数组
               let child = normalize(children, i)
               patch(null, child, container)
           }
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 自此,真实节点的渲染功能已经实现了大部分,还差diff比对的内容,我们可以创建一个元素进行尝试
        const {createVNode,render,h} =  VueRuntimeDOM
             render(h('div',{style:{color:'red'}},'Hello'),app)
             setTimeout( function(){
                render(h('div',{style:{color:'black'}},'World'),app)
             },1000)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 页面1秒后颜色和内容都会发生改变
    • 但是以上逻辑还不能处理前后节点都有数组孩子的 情况,下一章会介绍如果利用diff比对结果,渲染数组中的孩子
  • 相关阅读:
    C++——虚函数、虚析构函数、纯虚函数、抽象类
    Eureka 入门教程
    理解Java程序的执行
    DataBinding原理----双向绑定(4)
    如何利用 Seaborn 实现高级统计图表
    数据结构之图(存储)
    【COSBench系列】1. COSBench认识、使用、结果分析
    反射的作用、应用场景
    Redis哨兵环境搭建
    交通信号标志识别系统 python 深度学习 YOLOv5
  • 原文地址:https://blog.csdn.net/wenyeqv/article/details/126082090