runtime-core更新的核心流程

500 阅读5分钟

来到我们的App的主要结构

import {h} from '../../lib/guide-mini-vue.esm.js'
export const App = {
    name:"App",
        setup(){
        const count = ref(10)
        window.count = count
        return {
            count
        }
    },
    render(){
        return h('div',{},[h("div",{tId:1},[h("p",{},'主页' +this.count)])
    }
}

在上一节讲到我们在初始化时把实例对象用proxy进行了劫持代理,当我们调用render就可以用this来访问我们的代理对象,并且我们的返回值用ref进行包裹处理时就不需要再进行.value的处理了

const publicPropertiesMap = {
    $el:(i)=> i.vnode.el,
    // $slots
    $slots:(i)=> i.slots,
    $props:(i)=> i.props
    
}
export const PublicInstanceProxyHandlers = {
    get({_:instance},key){
        // 先从setupstate中获取值
        const {setupState,props} = instance
        // if(key in setupState){
        //     return setupState[key]
        // }
        // 将上面的逻辑进行重构 加上我们的props的逻辑
        // const hasOwn = (val,key)=> Object.prototype.hasOwnProperty.call(val,key)
            if(hasOwn(setupState,key)){
                return setupState[key]
            }else if(hasOwn(props,key)){
                return props[key]
            }
        // key=>el
        const publicGetter = publicPropertiesMap[key]
        if(publicGetter){
            return publicGetter(instance)
        }
        // if(key === '$el'){
            // return instance.vnode.el
        // }
    }
}

既然我们可以以this访问到我们setup中的内容,我们可以将this指向window,就可以在控制台中直接调试我们的值的更新

updatecore.jpg 当我们触发更新流程时,就会进到我们的effect中的函数调用,如果mount过了就会进入到我们的else分支,然后调用我们的render函数,获取到我们的新旧虚拟节点树,然后又回到了我们的patch函数的调用,

instance.update =  effect(()=>{
        if(!instance.isMounted){
            console.log('init')
            const {proxy} = instance
            const subTree = instance.subTree =  instance.render.call(proxy);
            // console.log(subTree)
        // vndoeTree => patch
        // vnode => element =>mountElement
        patch(null,subTree,container,instance,anchor)
        
        // 我们这里的subTree就是我们的根节点,我们所要赋值的el可以在subTree上找到
        // 传入我们的虚拟节点
        initialvnode.el = subTree.el
        instance.isMounted = true
    }else{
        console.log('update')
        // 需要一个更新之后的vnode
            const {next,vnode} = instance
            if(next){
                next.el = vnode.el

                updateComponentPreRender(instance,next)
            }
            const {proxy} = instance
            const subTree = instance.render.call(proxy);
            const prevSubTree = instance.subTree

            instance.subTree = subTree
            // console.log('current',subTree)
            // console.log('pre',prevSubTree)
        patch(prevSubTree,subTree,container,instance,anchor)
    }
    },{
        scheduler(){
            console.log('update -- scheduler')
            queueJobs(instance.update)
        }
    })

继续进到我们的更新流程,我们的类型为Element时

function processElement(n1,n2:any,container:any,parentComponent,anchor){
    // console.log('processElement')
    if(!n1){
        // element 主要有初始化init和更新update
        mountElement(n2,container,parentComponent,anchor)
    }else{
        patchElement(n1,n2,container,parentComponent,anchor)
    }
}

function patchElement(n1,n2:any,container,parentComponent,anchor){
    console.log('patchElement')
    console.log('n1',n1)
    console.log('n2',n2)
    console.log('container',container)


    const oldProps = n1.props || {}
    const newProps = n2.props || {}

    const el = (n2.el = n1.el)

    patchChildren(n1,n2,el,parentComponent,anchor)
    patchProps(el,oldProps,newProps)
}

进到我们的props的对比,先将我们新的props进行遍历,然后和老的props进行对比,如果老的props当中没有就需要去进行更新,更新的方法调用的还是我们runtime-dom中的方法

function patchProps(el,oldProps,newProps){
    if(oldProps !== newProps){

        for (const key in newProps) {
            const prevProp = oldProps[key]
        const nextProp = newProps[key]
        if(prevProp !== nextProp){
            hostPatchProp(el,key,prevProp,nextProp)
        }
    }
    if(Object.keys(oldProps).length > 0){

        for (const key in oldProps) {
            if(!(key in newProps)){
                hostPatchProp(el,key,oldProps[key],null)
            }
        }
    }   
}
}
runtime-dom
<=================================================================================>
function patchProp(el,key,prevVal,nextVal){
    // console.log('patchProp---------------')
    const isOn = key=> /^on[A-Z]/.test(key)
        // console.log(key)
        // 如果我们的key是我们的onclick我们就可以给他添加一个点击事件
        if(isOn(key)){
            el.addEventListener(key.slice(2).toLowerCase(),nextVal)
        }else{
            if(nextVal === undefined || nextVal === null){
                el.removeAttribute(key)
            }else{
                el.setAttribute(key,nextVal)
            }
        }
}

另一种情况当老的props中的值在新的props中没有的话就需要进行删除,同样是上面的函数进行处理;了解完我们的props的处理,接下来就到了我们children的处理流程,重磅来袭!

function patchChildren(n1,n2,container,parentComponent,anchor){
    const prevShapeFlag = n1.shapeFlag
    const {shapeFlag} = n2
    const c1 = n1.children
    const c2 = n2.children
    if(shapeFlag & ShapeFlags.TEXT_CHILDREN){
        if(prevShapeFlag & ShapeFlags.ARRAY_CHILDREN){
            // 1.把n1的元素(children)清空
            unmountChildren(n1.children) 
        }
        if(c1 !== c2){
               // 2.设置text
               hostSetElementText(container,c2)
           }
    }else{
        if(prevShapeFlag & ShapeFlags.TEXT_CHILDREN){
            // 清空原来的文本  
            hostSetElementText(container,'')
            // 直接将children进行mount
            mountChildren(c2,container,parentComponent,anchor)
        }else{
            // diff array with array
            patchKeyedChildren(c1,c2,container,parentComponent,anchor)
        }
    }

}

第一种情况将我们的类型是TEXT_CHILDREN时,只需要将文本的值进行改变即可,调用我们的hostSetElementText方法即可

function setElementText(el,text){
    el.textContent = text
}

第二种情况当我们的类型是ARRAY_CHILDRENl类型时,也就是我们的数组的对比清空,这里我们会在patchKeyedChildren中进行详细的处理,也就是来到了我们的地府算法

function patchKeyedChildren(c1,c2,container,parentComponent,parentAnchor){
    const l2 = c2.length
    let i = 0
    let e1 = c1.length -1
    let e2 = l2 -1

    function isSomeVNodeType(n1,n2){
        // type
        // key
        return n1.type === n2.type && n1.key === n2.key
    }
    // 左侧
    while (i<= e1 && i<= e2) {
        const n1 = c1[i]
        const n2 = c2[i]
        if(isSomeVNodeType(n1,n2)){
            patch(n1,n2,container,parentComponent,parentAnchor)
        }else{
            break
        }
        i++
    }
    console.log(i)
    // 右侧
    while (i<= e1 && i<= e2) {
        const n1 = c1[e1]
        const n2 = c2[e2]
        if(isSomeVNodeType(n1,n2)){
            patch(n1,n2,container,parentComponent,parentAnchor)
        }else{
            break
        }
        e1--
        e2--
    }
    // 新的比老的多 需要进行创建 
    if(i>e1){
        if(i<=e2){
            const nextPos = e2 + 1
            const anchor = e2 + 1<l2?c2[nextPos].el:null 
            while (i<=e2) {
            patch(null,c2[i],container,parentComponent,anchor)            
            i++
        }
        }
    } else if(i>e2){ // 老的比新的多 需要删除
            while(i<=e1){
                hostRemove(c1[i].el)
                i++
            }
        }else{
            // 中间乱序对比的部分
            let s1 = i  // e1
            let s2 = i  // e2

            const toBePatched = e2-s2 + 1 // e2中乱序的数量
            let patched = 0  // 记录当前处理的数量
            const keyToNewIndexMap = new Map()

            const newIndexToOldIndexMap = new Array(toBePatched)

            //  判断是否需要进行移动  逻辑优化
            let moved = false
            let maxNewIndexSoFar = 0
            // 重置新节点数组的索引值
           for (let i = 0; i < toBePatched; i++) {
            newIndexToOldIndexMap[i] = 0
           }
           for (let i = s2; i <= e2; i++) {
               const nextChild = c2[i]
               keyToNewIndexMap.set(nextChild.key,i)
           }
           for (let i = s1; i <= e1; i++) {
               const prevChild = c1[i]

               if(patched >= toBePatched){
                   hostRemove(prevChild.el)
                   continue
               }
               // 有key直接找映射表
               let newIndex
               if(prevChild.key !== null){
                   newIndex = keyToNewIndexMap.get(prevChild.key)
               }else{  // 没有key继续遍历
                   for (let j = s2; j <= e2; j++) {
                       // 借助已经封装好的方法
                       if(isSomeVNodeType(prevChild,c2[j])){
                                newIndex = j

                                break
                       }
                   }
               }
            //    新值中没有老值,进行删除
               if(newIndex === undefined){
                   hostRemove(prevChild.el)
               }else{
                   // 新值大于记录的值 重置最大的值
                    if(newIndex>= maxNewIndexSoFar){
                        maxNewIndexSoFar = newIndex
                    }else{
                        // 新值小于记录的值说明进行位置的移动
                        moved = true
                    }

                   // 证明新节点是存在的  在此处将老节点进行遍历对新节点进行重新赋值
                   // 因为此处我们的索引计算包含了前面的部分所以需要减去前面的部分也就是s2
                    // 由于新节点可能在老节点中是不存在的 所以需要考虑到为0的情况 可以将我们的i加1处理
                   newIndexToOldIndexMap[newIndex-s2] = i + 1 
                //    存在继续进行深度对比
                   patch(prevChild,c2[newIndex],container,parentComponent,null)
                   patched++
               }
               
           }
           // 给最长递增子序列算法准备进行处理的数组
           const increasingNewIndexSequence:any = moved? getSequence(newIndexToOldIndexMap) :[] // 需要进行位置的移动时才调用算法,减少不必要的逻辑代码
           let j = increasingNewIndexSequence.length-1
           // 获取到我们的最长递增子序列这是一个数组,需要将我们的老值进行遍历 然后
           // 利用两个指针分别指向我们的最长递增子序列和我们的老值 如果老值没有匹配 则说明需要进行位置移动
           // toBePatched就是我们的新值的中间乱序的长度
          for (let i = toBePatched -1; i >= 0; i--) {
              const nextIndex = i +s2
              const nextChild = c2[nextIndex]
              const anchor = nextIndex + 1<l2?c2[nextIndex + 1].el :null
              if(newIndexToOldIndexMap[i] === 0){
                // 在旧值中找不到新值的映射时就需要新创建
                patch(null,nextChild,container,parentComponent,anchor)
              }else if(moved){ // 需要移动时才进入相关的逻辑判断
                  if( j<0 || i !== increasingNewIndexSequence[j]){
                      console.log('需要进行位置移动')
                      hostInsert(nextChild.el,container,anchor)
                    }else{
                        // 不需要进行移动的话 将j的指针右移
                        j--
                    }
                }
          }
     }
}

我们的地府算法就是对于两个新旧数组的判断,并且VUE3对于VUE2中的算法又进一步的进行了优化.VUE3中主要是采取了双端对比,先解决了简单的两侧增删的逻辑,分别定义了两个指针指向我们两个数组,另外还有一个指针表示跳出对比时的位置,并且把我们的key值进行了一个存储,方便我们后续的排序进行数据准备,(这也是为什么我们在进行for循环时要绑定KEY的原因)这种情况先解决了如下的简单的逻辑

删除的逻辑
function remove(children){
    const parent = children.parentNode
    if(parent){
        parent.removeChild(children)
    }
}
计算位置插入逻辑,这里引入了锚点的概念
function insert(child,parent,anchor){
    // console.log('insert',el,parent)
    // parent.append(el)
    parent.insertBefore(child,anchor || null)
}

diff1.jpg

diff2.jpg 解决了两端的增删之后,就进入到了重点的中间排序的过程

diffMID.jpg 这里面其实在VUE3借助了另外一个算法来进一步提高排序的性能,也就是最长递增子序列的算法,并且做了逻辑优化判断,当我们的映射时的序列是稳定递增时就不需要去调用我们的最长递增子序列算法

// 最长递增子序列算法

function getSequence(arr){
    const p = arr.slice()
    const result  = [0] // 存储长度为i的递增子序列的索引
    let i,j,u,v,c
    const len = arr.length
   for (let i = 0; i < len; i++) {
       const arrI = arr[i]
       if(arrI !== 0){
           // 把j赋值为数组最后一项 
           j = result[result.length-1]
           // result存储的最后一个值小于当前值
           if(arr[j] < arrI){
            //    存储在result更新前的最后一个索引的值
                p[i] = j
                result.push(i)
                continue
           }
           u = 0
           v = result.length -1
           // 二分搜索 查找比arrI小的节点  更新result的值
           while (u<v) {
               c =(u+v) >> 1
               if(arr[result[c]] <arrI){
                   u = c +1
               }else{
                   v = c 
               }
           }
           if(arrI < arr[result[u]]){
               if(u>0){
                   p[i] = result[u-1]
               }
               result[u] = i
           }
        }
    }
    u = result.length
    v = result[u-1]
    // 回溯数组 找到最终的索引
    while (u-- > 0) {
        result[u] = v
        v = p[v]
    }
    return result
}

以上就是我们runtime-core更新的核心流程