vue3源码学习(6)--runtime-core(2)--初始化(2)

1,020 阅读5分钟

vue3源码学习(6)--runtime-core(2)--初始化(2)

前言

本篇来实现component以及element初始化过程中的一些具体过程以及部分edge case,是初始化流程更加健壮

实现组件代理对象

实现组件代理对象,即在render函数中通过this来过去setup返回的对象的property。


export const App = {
    render(){
        // 在render函数中,通过this获取setup 返回的对象的property
        return h("div",{},"hello," + this.name)
        //return h('div', { id: 'root', class: 'root' }, [ 
        // h('p', { id: 'p1', class: 'p1' }, 'hello, mini-vue3'),
        // h('p', { id: 'p2', class: 'p2' }, 'this is mini-vue3') 
        // ])


    },
    
   setup(){
   
       return {
           name:'mini-vue3'
       }
    }
}    

我们在实现初始化主流程的时候已经将setup的返回值挂载到instance对象的setupState property,因此在render函数中获取到setup函数返回的对象可以通过instance.setupState来获取。

在初始化有状态的组件即调用setupStatefulComponent函数时,利用Porxy对组件实例对象的proxy property的get进行代理,在获取proxy的property时,若setupState中有该property则返回其值。

setupRenderEffect函数中调用组件实例对象中的render函数时将 this 指向指定为 proxy property。

//runtime-core/component.ts

function setupStatefulComponent(instance){

    const Component = instance.type
    
    //利用proxy对组件实例对象的proxy porperty的get进行代理
    
    instance.proxy = new Proxy({},{
        get(target,key){
            // 通过解构赋值获取组件实例对象的 setupState property
            const {setupState} = instance
            
            //若组件实例对象的 setupState property 上有该 property 则返回其值
            if(key in setupState){
                return setupState[key]
             }
        }
        
    })
    
   /*其他代码*/ 
    

修改renderer.ts文件,完善setupRenderEffect函数

//renderer.ts

function setupRenderEffect (instnce,container){
    // 通过解构赋值获取组件实例对象的 proxy property
    const {proxy}  =instance
    // 调用组件实例对象中 render 函数获取 VNode 树,同时将 this 指向指定为 proxy property
     const subTree = instance.render.call(proxy)   
     
     patch(subTree,container)
 }
 

完善之后,在考虑一下render函数中通过this的$el property获取组件的根元素。所以对测试程序做一些改动,用来测试这一功能

//App.js
//用于保存组件的this

window.self = this
export const APP = {
    
    render(){
        window.self = this
        
        return h("div",{},"hello," + this.name")
        
     }
     
     /*其他代码*/
}

首先给Vnode增加el property,用于保存对应组件的根元素

//runtime-core/vnode.ts

export function createVnode(type,props?,children?){
    const vnode = {
        type,
        props,
        children,
        //用于保存对应组件的根元素
        
        el :null
     }
     
     return vnode
     
 }    

在进行Element的初始化即调用mountElement函数的时候,根据Element对应的Vnode的type property创建的DOM元素同时赋值给Vnode的el property,在获取Vnode树并递归调用setupRenderEffect函数的时候,将Vndoe树的el property赋值给Vnode的 el property。

//renderer.ts
function mountElement(vnode,container){
    //根据 Element 对应 VNode 的 type property 创建 DOM 元素并同时赋值给变量 el 和 VNode 的 el property
    const el = vnode.el = document.createElement(vnode.type)
    
    /*其他代码*/
    
}


function mountComponent(vnode,container){
    /*其他代码*/
    
    setupRenderEffect(instance,vnode,container)
}

function setupRenderEffect(instance,vnode,container){
    /*其他代码*/
    
    //将vnode树的el property 赋值给 vnode 的el 
    
    vnode.el = subTree.el
  }

完善组件实例对象的proxy property,在获取$el property时返回vnode的el

// runtime-core/component.ts

function setupStatefulComponent(instance){
    const Component =instance.type
    
    instance.proxy = new Proxy( 
        {},
        { 
            get(target, key) {
                const { setupState } = instance
                if (key in setupState) {
                    return setupState[key] 
                }
                
                //若获取 $el property 则返回 VNode 的 el property
                if(key === "$el"){
                    return instance.vnode.el
                 }
            }     
         }
    )
    /*其他代码*/
}    
    
    

完成功能过后,对代码进行优化重构

//runtime-core/componentPbulicInstance.ts

//用于保存组件实例对象 property及对应的getter
const publicPropertiesMap = {
    $el : i=>i.vnode.el
}

//组件实例对象proxy prperty对应的handlers

export const PublicInstanceHandlers = {
    get({_:instance},key){
        const { setupState } = instance
        
        if(key in setupState){
            return setupState[key]
         }
         
         // 若获取指定 property 则调用对应 getter 并返回其返回值
         const publicGetter = publicPropertiesMap[key]
         if(publicGetter){
             return publicGetter()
         }
    }     
}         
         

修改component.ts文件,对setupStateComponent函数进行重构

//component.ts

function setupStateComponent(instance) {

    const Component = instance.type
    
    instance.proxy = new Proxy({_:instance},PublicInstanceHandlers)
    
    /*其他代码*/
    
    const {setup} = Component
    if(setup){
        const setupResult = setup()
    }
    handleSetupResult(instance,setupResult)
 }

实现shapeFlag

Vue3中,使用shapeFlag用于描述虚拟节点vnode的类型,这部分实现是用来进行代码性能的优化。

目前实现的代码有两处需要进行shapeFlag进行判断

  • rednerer.ts 初始化的时候用于判断vnode的类型,之前我们进行初始化的时候对于vnode的为element还是component类型判断是用的if语句来判断vnode的type属性为string类型还是object类型,现在使用shapeFlag来进行判断
  • renderer.ts``mounteElement方法中判断vnodechildren类型是string还是array。之前同样使用if语句进行判断

首先先实现一个简单的shapeFlags,初始化一个shapeFlag对象,其中的property即为进行判断的内容,property的初始值默认为0

//用于判断vnode的shapeFlag

const shapeFlags = {
    //用于判断Vnode类型是否为Element
    element:0,
    //用于判断vnode类型是否为Component
    stateful_component :0,
    //用于判断children类型是否为string
    text_children :0,
    //用于判断children类型是否是Array
    array_children :0
}

假设Vnode类型为Element则将element property的值为1其他同理

shapeFlags.element = 1 // vnode 的类型是 element
shapeFlags.stateful_component = 1 // vnode 的类型是 component
shapeFlags.text_children = 1 // vnode.children 的类型是 string
shapeFlags.array_chidlren = 1 // vnode.children 的类型是 array

后面判断vnode类型可以直接判断shapeFlags的值

if(shapeFlags.element){
    // vnode 的类型是 element 的情况需要进行的操作
}
if(shapeFlags.stateful_component){
    // vnode 的类型是 component 的情况需要进行的操作
}
if(shapeFlags.text_children){
    // vnode.children 的类型是 string 的情况需要进行的操作
}
if(shapeFlags.array_chidlren){
    // vnode.children 的类型是 array 的情况需要进行的操作
}

这样的实现简单明了,容易理解,到那时不够高效,接下来就是利用位运算进行优化

位运算中包括与运算(&)、或运算(|)和左移运算符(<<):

  • 与运算(&):两位都为1,结果才为1
  • 或运算(|):两位都为0,结果才为0
  • 左移运算符(<<):将二进制全部若以若干位
1&1  // => 1 
1&0  // => 0
0&1  // => 0 
0&0  // => 0 


1|1  // => 1
1|0  // => 1
0|1  // => 1
0|0  // => 0

1101 & 1011    //=> 1001
1010 & 0101    //=> 0000
1101 | 1011    //=> 1111
1010 | 1000    //=> 1010

1 << 1  //=> 10
1 << 2  //=> 100
101 << 1 //=> 1010
101 << 2 //=> 10100


//修改
// 0000
// 0001
// ----
//  0000 | 0001  = 0001

//查找 &
// 0001
// 0001
// ----
// 0001

然后考虑用四位的 VNode shapeFlag property 来表示 VNode 和 children 的类型:

// vnode 为 element 类型,vnode.children 为 string 类型
vnode.shapeFlag === 0101   // element + text_children 
// vnode 为 element 类型,vnode.children 为 array 类型
vnode.shapeFlag === 1001   //  elemetn + array_children
// vnode 为 component 类型,vnode.children 为 string 类型
vnode.shapeFlag === 0110   //  stateful_component + text_children
// vnode 为 component 类型,vnode.children 为 array 类型
vnode.shapeFlag === 1010   // stateful_component + array_children

默认的,四位均为0,若vnode类型为element,chidlren类型为string,则将对应的为设为1,其他同理

vnode.shapeFlag = 0000 

2|=3    //=>   0010 | 0011  = 0011  

//vnode 为 element 类型,vnode.children 为 string 类型
vnode.shapeFlag |= 0101   
// vnode 为 element 类型,vnode.children 为 array 类型
vnode.shapeFlag |= 1001
// vnode 为 component 类型,vnode.children 为 string 类型
vnode.shapeFlag |= 1001
// vnode 为 component 类型,vnode.children 为 array 类型
vnode.shapeFlag |= 1010

若要判断 VNode 类型是否是 Element 则直接判断对应位,其他同理:

if (vnode.shapeFlag & 0001) { } 
if (vnode.shapeFlag & 0010) { } 
if (vnode.shapeFlag & 0100) { } 
if (vnode.shapeFlag & 1000) { }
}

了解了位运算的逻辑,我们来实现代码

// shapeFlags.ts
export const enum shopeFlags  = {
    //用于判断Vnode类型是否为Element
    element:1,  // 0001
    //用于判断vnode类型是否为Component
    stateful_component :1 << 1 ,   // 0010
    //用于判断children类型是否为string
    text_children :1 << 2,    // 0100
    //用于判断children类型是否是Array
    array_children :1 << 3   // 1000
} 

vnode.ts文件中完善createVnode函数,在Vnode中郑家shapeFlag属性,并根据vnode.type和children的类型设置值

//vnode.ts
  import {shopeFlags} from "./ShapeFlags"
   export function createVnode(type,props? ,children?){
      const vnode = {
          type,
          props,
          children,
          
          shapeFlag:getShapeFlag(type)
       }
       
       //根据children的类型设置shapeFlag对应位
       if(typeof children ==="string"){
           vnode.shapeFla |= shapeFalg.text_children    // 
        }else if(Array.isArray(children)){
            vnode.shapeFla |= shapeFalg.array_children
        }
        
        return vnode
   }
   
   function getShapeFlag(type){
       return typeof type ==="string" ? shapeFlags.element : shapeFlags.stateful_component
    }   
}

后续完善renderer.ts中的patch方法

//renderer.ts

import {shopeFlags} from "./ShapeFlags"
function patch(vnode,container){
    
    //根据 VNode 类型的不同调用不同的函数 
    // 通过 VNode shapeFlag property 与枚举变量 ShapeFlags 进行与运算来判断 VNode 类型
    
    const {shapeFlag} = vnode
    
    if(shapeFlag & shapeFlags.element){     // & 运算  都为1才为1  用来进行查找
           processELment(vnode,contianer)
    }else if(shapeFlag & shapeFlags.stateful_component){
        processComponent(vnode,container)
    }   
 }
 
 /**其他代码*/
 
 
 function mountElement(vnode,container){
     const el = (vnode.el = document.createElement(vnode.type))
     //通过解构赋值获取 Element 对应 VNode 的 props property、shapeFlag property 和 children property
     const { props,shapeFlag, children }  = vnode
     
     if(const key in props){
         const val = props[key]
         el.setAttribute(key,val)
     }
     // 通过 VNode shapeFlag property 与枚举变量 ShapeFlags 进行与运算来判断 children 类型
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
        el.textContent = children 
    } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        mountChildren(children, el) 
    }
    
    container.append(el)

位运算的效率是高于获取和修改对象 property 的,因此 shapeFlags 能够提升性能,但是可以看到,代码的可读性是不如之前的,在开发时应该先考虑实现功能同时保持代码可读性,在之后再考虑对代码进行重构提升性能。

实现注册事件功能

测试代码

//APP.JS
window.self = null

export const App = {
  render() {
    window.self = this

    return h(
      'div',
      {
        id: 'root',
        class: 'root-div',
        // 注册 onClick 事件
        onClick() {
          console.log('you clicked root-div')
        },
        // 注册 onMousedown 事件
        onMousedown() {
          console.log('your mouse down on root-div')
        }
      },
      'hello, ' + this.name
    )
    // return h('div', { id: 'root', class: 'root' }, [
    //   h('p', { id: 'p1', class: 'p1' }, 'hello, mini-vue3'),
    //   h('p', { id: 'p2', class: 'p2' }, 'this is mini-vue3')
    // ])
  }

  /* 其他代码 */
}

在之前实现 Element 初始化的主流程时,mountElement函数中处理了 VNode 的 props:遍历 props,利用Element.setAttribute()将其中的 property 添加到el上, 其中 key 作为el的 attribute 或 prop 名,value 作为 attribute 或 prop 的值。

//renderer.ts

function mountElement(vnode,container){
    const el = (vnode.el = document.createElement(vnode.type))
    const { props, shapeFlag, children } = vnode
    // 遍历 props,利用 Element.setAttribute() 将其中的 property 添加到 el 上 // 其中 key 作为 el 的 attribute 或 property 名,value 作为 attribute 或 property 的值
     if(const key in props){
         const val = props[key]
         el.setAttribute(key,val)
     }
   /*其他代码*/
   
}

而注册事件功能的实现其实就是在遍历props时增加了判断:若 key 以“on”开头,则利用Element.addEventListener()将该方法添加到el上其中 key 去掉前两位(也就是“on”)再转为小写后的字符串作为 event 名,value 作为 listener,否则还按之前的处理方式。

单独封装一个方法用来注册事件功能

//renderer.ts

function mountElement(vnode,container){
    const el = (vnode.el = document.createElement(vnode.type))
    const { props, shapeFlag, children } = vnode
    // 遍历 props,利用 Element.setAttribute() 将其中的 property 添加到 el 上 // 其中 key 作为 el 的 attribute 或 property 名,value 作为 attribute 或 property 的值
     if(const key in props){
         const val = props[key]
         
         
         //通过正则判断该property的key是否已on开头的事件,是则注册事件,否则为 attribute 或 property
         
         const isOn = (key: string) => /^on[A-Z]/.test(key)
         //若为注册事件
         if(isOn(key)){
         // 利用 Element.addEventListener() 将该 property 添加到 el 上 
         // 其中 key 去掉前两位(也就是 on)再转为小写后的字符串作为事件名,value 作为 listener
             const event = key.slice(2).toLowerCase()
             el.addEventListener(event, val)
         }else{
             // 利用 Element.setAttribute() 将该 property 添加到 el 上
             // 其中 key 作为 el 的 attribute 或 property 名,value 作为 attribute 或 property 的值
             el.setAttribute(key,val)
         }    
     }
   /*其他代码*/
   
}

到此 就实现了简单的事件注册功能