vue3源码学习(7) -- runtime-core(3):更新element(1)

201 阅读5分钟

vue3源码学习(6) -- runtime-core :更新element

前言

runtime-core的流程除了初始化之外,还有一个重要的流程就是更新流程,本文主要学习更新element的流程。

runtime-core主流程还有诸多edge case,后续补充

更新element流程

首先先进行一个update的example

//app.js

import {h,ref} from '../../lib/guide-min-vue.esm.js'    // 使用rollup打包后的文件

export const App = {
    name:'App',
    setup(){
        const count = ref(0)
        const onClick = ()=>{
            count.value ++
        }
        return {
            count,
            onClick
        }   
    }
    
    render(){
        h(
            "div",
            {
                id="root"
            },
            [
                {"div",{},"count:" + this.count},  //依赖收集
                { "button",{ onClick :this.onClick,"click"}
                
            ]    
 
        )
    }

上面例子 我们进行渲染结果

image.png

数据响应式处理

当前情况下,我们触发onClick事件,只会改变count的值,但并不会触发页面的更新,因为我们数据响应式包括两个重要模块:响应式数据effect副作用函数。上面例子中,我们仅仅只有一个响应式数据ref,并没有实现effect函数的应用。也就是说,在getter时,收集的依赖为空,在setter时,也就没有可触发的依赖

那么如何实现页面更新呢?

页面的渲染和更新,本质上就是Vnode节点的渲染和更新,所以当我们onClick事件触发,也就是响应式数据发生更新的时候,我们需要形成新的vnode节点,即重新触发render函数

我们知道在初始化流程的过程中setupRenderEffect函数是用来生成vnode节点,并进行节点渲染的,所以我们需要做的就是 将setupRenderEffect函数中的内容包裹成一个被effect副作用函数包裹匿名函数,这样在初始化时就会将次匿名函数作为依赖进行收集,update的时候,也会重新执行,并进行页面的更新操作`

//renderer.ts

function setupRenderEffect(instance:any,initVnode,container){
    effect(()=>{
        const { proxy } =instance
        
        const subTree = instance.render.call(proxy)
        
        patch(subTree,container,instance)
        
        initVnode.el = subTree.el
        
    
    })

此时,当我们触发click事件,得到以下结果

image.png

从上面可以看到,数据响应式效果已经达成。生成三个组件的原因时我们之前在patch逻辑中只实现了初始化炒作,接下来就是实现更新逻辑

区分初始化和更新 isMounted

instance实例过载一个 isMounted proerty,用于区分更新逻辑和初始化逻辑

//components.ts
 export function createComponentInstance(vnode,parent){
     cosnt component = {
            vnode,
            type:vnode.type,
            setupState: {}, //记录setup函数执行后返回的结果
            
        
            props:{},
            slots:{},
            provides:parent ? parent.provides :{}
            parent,
            emit:()=>{},
            isMounted:false
      }
      component.emit = emit.bind(null,component) as any
      
      return component
      
  }   
  
//renderer.ts
function setupRenderEffect(instance:any,initVnode,container){
   effect(()=>{
       if(instance.isMounted){
           console.log("init")
           
           const { proxy } =instance
      
           const subTree = instance.render.call(proxy)
      
           patch(subTree,container,instance)
      
          initVnode.el = subTree.el
          
          instance.isMounted = true
       }else{
           
         console.log("update")
         
       }  
   }

触发onclick事件得到下面结果

image.png

接下来就是更新逻辑的实现

更新

更新的本质其实就是新旧vnode之间的对比更新,所以在init的时候,我们需要将vnode存储起来,方便之后更新时的对比

//renderer.ts

 function setupRenderEffect(instance:any,initVnode,container){
     effect(()=>{
         if(instance.isMounted){
             console.log("init")
             /*其他代码*/
        
             const subTree = instance.subTree instance.render.call(proxy)   // 生成vnode 并存储在instance实例上
            /*其他代码*/
            
         }else{
             
           console.log("update")
           const { proxy } = instance
           const subTree = instance.render.call(proxy)
           
           const prevSubTree = instance.subTree  //获取之前的subTree
           
           instance.subTree = subTree   // 将新的存储起来,为了之后的再一次更新对比
           
           patch(prevSubTree,subTree,container,instance)
         }  
     }
 

接下来就是patch函数的重新架构,之前我们仅实现了init流程,接下来就是更新逻辑的实现

新增一个参数,需要更新的参数的函数很多,这里就不展示

//renderer.ts 
/*
* @params:  n1: 旧vnode
* @params:  n2: 新vnode
* @params:  container: 渲染容器
* @params:  parentComponent: 父组件
*/
patch(n1,n2,container,parentComponent){
    /*其他代码*/
    //processComponent
    //processElement
}
//renderer.ts 
function  processElement(n1,n2,container,parentComponent){
    if(!n1){  // 如果n1不存在,则初始化逻辑
        mountElement(n2,contianer.parentComponent)
     }else{
         patchElement(n1,n2,contianer)
     }
}
//renderer.ts
 function patchElement(n1,n2,continer){
     //props
     //children
     
}

到此为止,element更新的基本流程已经完成。在进行具体的更新逻辑之前,我们先来捋一遍组件、element的初始化以及更行大致流程

image.png

image.png

接下来就是进行element逻辑的具体实现

更新element的props

更新element的props存在三种情况

  • 之前的值和现在的值不一样 ——修改
  • 之前的值变为null || undefined —— 删除
  • 之前的key在新的里面没有了 —— 删除

编写example

//app.js
import { h ,ref} from "../../lib/mini-vue.esm.js"

export const App = {
    name:"App",
    setup(){
        const count = ref(0)
        const onClick = ()=>{
            count.value ++
         }
         
         //props
         const props = ref({
             foo:"foo",
             bar:"bar"
         })
         //之前的值和现在的值不一样
         const onChangePorpsDemo1 = ()=>{
             props.value.foo = "new-foo"
         }    
         // 之前的值变为undefined
         const onChangePorpsDeom2 = ()=>{
             props.value.foo = "undefined"
         }
         //之前的key在新的里面没有了
         const onChangePorpsDeom3 =()=>{
             props.value = {
                 foo:"foo"
             }
          }
          return {
              count,
              onClick,
              onChangePorpsDemo1,
              onChangePorpsDemo2,
              onChangePorpsDemo3,
          }
       },
       render(){
           return h("div",
           {id = "root",...this.props},
           [
               h("button",{onClick:this.onClick},"click"),
               h("div",{},"count:"+this.count),
               
               //props
               h("button",{onClick:this.onChangePropsDemo1},"changeProps - foo的值改变了")
               h("button",{onClick:this.onChangePropsDemo2},"changeProps - foo的值改变了为undefined")
               h("button",{onClick:this.onChangePropsDemo3},"changeProps - bar没有了")
           ])
       }    
   }       
// renderer.ts
function patchElement(n1,n2,container){
    
    
    // 获取到新旧props
    const oldProps = n1.props || {}
    const newProps = n2.props || {}
    
    const el = n2.el = n1.el
    patchProps(el,oldProps,newProps)
}


function patchProps(oldProps,newProps){
    if(oldPorps !== newPorps){
   
    
        //第一、第二种种情况props只修改
        for(const key in newPorps){
            const prevPorp = oldPorps[key]
            const nextPorp = newPorps[key]
        
            if(prevProp !== nextProp){
                //值不一样 重新挂载此值
                hostPatchProp(el, key, prevVal, nextVal)  //更新
            
            }
        }
        //第三种情况
        for(const key in oldProps){
            if(!(key in newPorps)){
                hostPatchProps(el,key,oldProps[key],null)
        }   
    }    
}
//runtime-dom
function hostPatchProp(el, key, prevVal, nextVal){
      const isOn = (key: string) => /^on[A-Z]/.test(key);
      if(isOn(key)){
          const event = key.slice(2).toLowerCase()
          el.addEventListener(event,nextVal)
       }else{
           if(nextVal === undefined || nextVal == null){
           el.removeAttribute(key)
         }else{
             el.setAttribute(key,nexVal)
       }
 }

重构一下,之前mounElement中挂载props时也可使用hostPorps

function mountElement(vnode,contianer,parentComponent){
    /*其他代码*/
    const el = (vnode.el = hostCreateElement(vnode.type));
    //props
    const { props } = vnode;
    for (const key in props) {
      const val = props[key];
    
     hostPatchProp(el, key, null, val);
    }
}

更新element的children

更新children的情况更为复杂主要有四种情况

  • oldChildrennewChildren均为 字符串
  • oldchildren为字符串,newChildren为数组
  • oldChildren为数组,newChidren为字符串
  • oldchildren为数组,newChildren为数组

Array TO Text

function patchElement(n1,n2,contianer){

    /*其他代码*/
    
    patchChidlren(n1,n2,el)
}

function patchChildren(n1,n2,container){
    const prevShapeFlag = n1.shapeFlag
    const shapeFlag = n2.shapeFlag
    const c2 = n2.children
    if(shapeFlag & shapeFlags.text_children){
        if(shapeFlag & shapeFlags.Array_children){
            // 1、把老的children清空
            unmountChildren(n1.children)
            
            //设置text
            hostSetElementText(contianer,c2)
      }
 }
 function unmountChildren(n1.children){
     for( let i=0 ;i< n1.children.length ; i++){
         const el = children[i].el //获取dom元素
         
         //删除
         hostRemove(el)
     }
 }
 function hostRemove(child){
     const parent = child.parent
     if(parent){
         parent.removeChild(child)
     }
}     
      
function hostSetElementText(el,text){
    el.textContent(text)
}

Text TO Text

function patchChildren(n1,n2,container){
    /*其他代码*/
    const c1 = n1.children
    if(shapeFlag & shapeFlags.text_children){
        if(shapeFlag & shapeFlags.Array_children){
        /*其他代码*/
        }else{
           if(c1 !==c2){
               hostSetElementText(container,c2)
             }
         }
     }
 }

代码重构


function patchChildren(n1,n2,container){
   const prevShapeFlag = n1.shapeFlag
   const shapeFlag = n2.shapeFlag
   const c1 = n1.children
   const c2 = n2.children
   if(shapeFlag & shapeFlags.text_children){
       if(shapeFlag & shapeFlags.Array_children){
           unmountChildren(n1.children)
       }
       if(c1 !== c2){
           hostSetElementText(container,c2)
       }
   }
}

Text TO Array

function patchChildren(n1,n2,container,preComponent){
    /*其他代码*/
    if(shapeFlag & shapeFlags.text_children){
        /*其他代码*/
    }else{
        
        if(preShapeFlag & shapeFlag.text_children){
        //删除旧的chidlren
        hostSetElementText(container, "")
        //创建新的 数组节点节点
        mountChildren(n2,container,parentComponent)
   }
   

mountChildren 初始化的时候实现过,参数后续自己调整

未完待续