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
方法中判断vnode
的children
类型是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)
}
}
/*其他代码*/
}
到此 就实现了简单的事件注册功能