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)

9. Vue3中如何将虚拟节点渲染成真实节点_第1张图片

  • 在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)
}

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比对
        • 最新的不是数组,说明为空,删除原来的元素的孩子
      • 现在已经排除了旧的是数组的情况,新的是文本的情况,那么下面无论旧的是文本还是空,只需要让其为空,新的无论是空还是数组,只需要按照新的内容赋值即可
        9. Vue3中如何将虚拟节点渲染成真实节点_第2张图片
代码实现
  • 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

  • 创建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
    }
  • umount函数直接利用解构出来的dom操作方法
    • 虚拟节点的el上记录了渲染后的真实节点
   function umount(vnode) {
        hostRemove(vnode.el)
    }
  • 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)
                }
        }
    }
  • isSameVNode函数用来判断两个节点是否有相同的类型和key,如果有,
    • 则认为两节点相同(不考虑属性和孩子不同),因此不需要重新渲染新节点成真实节点,只需要在老的节点渲染的真实节点上复用即可
//判断是否为同一类型节点
export function isSameVNode(n1,n2){
    return n1.type=== n2.type && n1.key === n2.key
}
  • processElement函数实现
    • 如果旧节点为null,则说明不存在之前的真实节点,只需要渲染新节点就行
    • 不为null,则需要对两个节点进行比对,如果能复用就复用,不能复用,就需要删除旧的真实节点,渲染成新的
 function processElement(n1: any, n2: any, container: any, anchor: any) {
        if (n1 == null) {
            //创建节点
            mountElement(n2, container, anchor)
        } else {
            //更新节点

            patchElement(n1, n2)
        }
    }
  • 真实节点创建函数实现
    • 先创建真实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)

   } 
  • 属性更新函数
    • 循环比对新节点属性对象和旧节点属性对象
    • 保证新节点有的添加上,没有的删除掉
  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)
           }
       }
   }
  • 比较完属性的不同之后,开始比较孩子的不同
    • 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)
                }
            }
        }
    }
  • unmountChildren函数
  function unmountChildren(children) {
       children.forEach(child => {
           umount(child)
       })
   }
  • 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)
       }
   }
  • 自此,真实节点的渲染功能已经实现了大部分,还差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秒后颜色和内容都会发生改变
  • 但是以上逻辑还不能处理前后节点都有数组孩子的 情况,下一章会介绍如果利用diff比对结果,渲染数组中的孩子

你可能感兴趣的:(Vue3源码,vue.js,前端,javascript)