源码分析
刀刀
3/20/2025
0 字
0 分钟
主要模块
compiler
编译的过程,将代码做编译处理。
- core:编辑器核心,做初级编译(无关任何平台)
- dom:针对浏览器端的编译
- sfc:针对单文件编译
- ssr:针对服务端的优化
runtime
模块运行时做什么处理,实现什么功能。
- core:无关平台的运行核心
- dom:浏览器端运行
- test:测试运行
reactivity
包含响应式系统,如reactive、ref、computed等。
size-check
查看代码体积。
响应式模块reactivity
reactive实现
响应式核心:proxy
。
// target:进行代理时代理的目标对象;handler:操作时代理对象的行为(get用来获取拦截的对象,读取其属性,set设置属性的捕获器,修改对象时会触发)
const p = new Proxy(target, handler)
原理
- 通过
proxy
拦截数据去代理目标对象 - 定义存储器
getter
接收值、setter
设置值 - 通过
reflect
在对象属性上设置属性进行反射,get
获取数据、set
设置数据
初步实现
定义一个响应式函数 reactive
,形参 target
获取对象,通过 proxy
代理,get
获取值,set
设置值。
const reactive = target => {
return new Proxy(target, {
// get接收值
get(target, key, receiver) {
},
// set设置值
set(target, key, receiver) {
}
})
}
通过 reflect
的 get
和 set
属性反射,返回。
const reactive = target => {
return new Proxy(target, {
// get接收值
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
console.log('get', res);
return res
},
// set设置值
set(target, key, receiver) {
const res = Reflect.set(target, key, receiver)
console.log('set', res);
return res
}
})
}
通过传参并获取属性来触发 get
方法;通过修改属性来触发 set
方法。
const obj = reactive({a:10,b:20})
obj.a = 120
console.log(obj.a);
利用 node
运行查看打印。
effect实现
原理
借助了三个函数实现。
- effect
- tarck:负责收集数据时对数据整理(收集阶段处理依赖)
- trigger:负责数据更新时对数据整理(更新阶段处理依赖)
初步实现
(使用了 reactive
代码连用)
声明定义上方那三个函数,用于依赖的收集处理。
effect
是全阶段处理;tarck
是在收集阶段处理,因此在 get
方法内调用;trigger
是在更新阶段处理,因此在 set
方法内调用。
const reactive = target => {
return new Proxy(target, {
// get接收值
get(target, key, receiver) {
// ...
tarck(target, key)
return res
},
// set设置值
set(target, key, receiver) {
// ....
trigger(target, key)
return res
}
})
}
// tarck与trigger服务于effect,因此要被调用出来执行处理
const effect = effectFn => {
effectFn()
}
const tarck = (target, key) => {}
const trigger = (target, key) => {}
tarck
收集依赖存储到本地或者某个变量中(最好存一个一一映射的关系)。vue3源码使用的是 weakMap
,为了方便这里直接使用 map
即可。
接收 target
目标对象和 key
键,然后做以下操作:
- 定义一个
map
变量targetMap
- 判断当前执行的
effect
函数是否存在,不存在不做任何处理,存在则继续往下操作 - 通过
targetMap.get(target)
方法获取target
,判断该目标是否存在,不存在则定义为map
对象deepMap
,通过targetMap.set()
方法把target
赋值给该deepMap
变量,存在则继续往下操作 - 通过
deepMap.get(key)
获取到映射的值,判断是否存在,不存在则Set
设置,通过set
方法赋值 - 最后用
add
方法添加数据
// 1.建立映射
const targetMap = new Map()
let activeEffect; // 判断是否存在的全局变量
const tarck = (target, key) => {
// 2.判断是否存在,如果不存在,直接结束函数
if(!activeEffect) return
// 3.
let deepMap = targetMap.get(target)
if(!deepMap) {
deepMap = new Map()
targetMap.set(target, deepMap)
}
// 4.
let deps = deepMap.get(key)
if(!deps) {
deps = new Set()
deepMap.set(key, deps)
}
// 5.
deps.add(activeEffect)
}
trigger
- 获取定义的
targetMap.get()
的键值,如果没有,不执行后续操作 depsMap.get(key)
获取键值,没有不执行后续操作- 遍历键值执行函数
const trigger = (target, key) => {
const depsMap = targetMap.get(target)
if(!depsMap) return
const deps = depsMap.get(key)
if(!deps) return
deps.forEach(effectFn => effectFn());
}
effect
处理相应的函数对象,返回函数让外部可以执行,然后自调用执行一次。
const effect = fn => {
const effectFn = () => {
activeEffect = fn
return fn()
}
// 先执行一次
effectFn()
return effectFn
}
调用 effect
,传入一个函数,定义一个对象调用并修改值,例如:
const a = reactive({
count: 0
})
effect(() => {
console.log('执行effect,a:' + a.count);
})
a.count = 10
最后运行效果如下所示:
bug修复
如果定义一个多属性的方法,一个属性使用嵌套的形式输出,一个属性直接输出,如下所示:
const a = reactive({
count: 0,
num: 10
})
effect(() => {
effect(() => {
console.log('count:' + a.count);
})
console.log('num:' + a.num);
})
a.num = 200
a.num = 250
a.num = 500
此时控制台打印的效果如下所示:
可以发现,只输出了嵌套调用的 count
,直接输出的 num
没有了。
这是因为它们是同名函数,内部的函数会覆盖外部的函数。实质上,我们调用的是外部的函数,而运行的是内部的函数,因此就会出现上面的bug。
解决方案:把每一次调用的函数都保存到数组内,存储更变之前的函数,执行完毕后再清除。
const arr = []
// tarck与trigger服务于effect,因此要被调用出来
const effect = fn => {
const effectFn = () => {
try {
arr.push(effectFn)
activeEffect = fn
return fn()
} finally {
arr.pop()
activeEffect = arr[arr.length - 1]
}
}
// 先执行一次
effectFn()
return effectFn
}
ref实现
原理
通过调用并返回一个类,其中,类的 get
方法接收 value
并赋值,set
方法修改 value
值。
使用 tarck
和 trigger
收集依赖。
import {effect, reactive, trigger, tarck} from './effect.js'
export const ref = value => {
return new RefImpl(value)
}
// 声明类,做获取、赋值操作
class RefImpl {
constructor(value) {
this._value = value
}
get value() {
tarck(this, 'value')
return this._value
}
set value(newValue) {
this._value = newValue
trigger(this, 'value')
}
}
修改值,查看控制台的输出
const count = ref(1)
effect(() => {
console.log('count:' + count.value);
})
count.value = 10
答疑解惑
为什么返回 this._value
而调用时是 count.value
?
get
和 set
实际上是一个代理,代理括号前面的那个变量,_value
是它们内部操作的变量。
computed实现
初步实现
照猫画虎,照 ref
实现 computed
。
computed
接收的参数是一个函数,因此要去调整参数,把 value
换为 getter
,类中不是通过 this.
直接赋值,而是通过 effect
函数来收集依赖实现。set
方法也不需要了,通过 effect
来赋值操作。
import {effect, reactive, trigger, tarck} from './effect.js'
import { ref } from "./ref.js";
export const computed = getter => {
return new ComputedRefImpl(getter)
}
class ComputedRefImpl {
constructor(getter) {
this.effect = effect(getter, () => {
trigger(this, 'value')
})
}
get value() {
this._value = this.effect()
tarck(this, 'value')
return this._value
}
}
现在只能接收函数,没有处理函数,因此回到 effect
函数处理函数实现调度。
目前 effect
函数只接收一个函数,声明第二个形参调度器,默认值为 null
。
挂载调度函数,判断当前调度器是否为空,为空则为 effectFn
赋值挂载。
目前为止调度器不为空,去到 trigger
执行调度器,判断当前是否存在调度函数,如果存在则先执行调度函数,没有则执行 effect
本身。
export const effect = (fn, scheduler = null) => {
const effectFn = () => {
try {
arr.push(effectFn)
activeEffect = effectFn
return fn()
} finally {
// ......
}
}
// 先执行一次
effectFn()
if(scheduler) effectFn.scheduler = scheduler
return effectFn
}
export const trigger = (target, key) => {
// ......
deps.forEach(effectFn => {
// 如果有调度函数,优先执行调度函数。没有才执行effectFn函数本身
effectFn.scheduler ? effectFn.scheduler(effectFn) : effectFn()
});
}
调用该方法
const b = ref(1)
const c = computed(() => b.value + 5)
const fn = effect(() => {
console.log('b:' + b.value);
console.log('c:' + c.value);
})
fn()
执行结果如下所示
缓存实现
原理
设置一个锁,如果当前值没有更改,则锁为 true
表示锁上状态,通过 get
返回保存的值;如果值发生更改,即触发更新后,锁的状态改为 false
,此时调用调度函数获取新值。
// ......
class ComputedRefImpl {
constructor(getter) {
this._dirty = true
this.effect = effect(getter, () => {
if(!this._dirty) {
this._dirty = false
trigger(this, 'value')
}
})
}
get value() {
if(this._dirty) {
this._value = this.effect()
this._dirty = false
tarck(this, 'value')
}
return this._value
}
}
模块渲染
vue运用虚拟dom技术来提高性能。虚拟dom技术简而言之指的是先将需要渲染的dom节点在内存中创建好,再通过渲染函数一次性渲染到页面中。
每次dom修改都会引起重绘或回流,大量的dom操作会极大影响性能。使用虚拟dom技术,就可以先在内存中创建好dom节点,操作内存中的dom节点比起直接操作页面dom节点性能要好不少。
原理
位运算符
利用位运算符的操作判断节点类型。
// 判断是属于什么类型的节点
const SHAPEFLAG = {
ELEMENT: 1,
TEXT: 1 << 1,
COMPONENT: 1 << 2,
TEXT_CHILDREN: 1 << 3,
APPY_CHILDREN: 1 << 4
}
实现
创建虚拟节点
通过函数获取传参返回对象的形式创建一个 vnode
虚拟节点。
- 判断是属于什么类型的节点
- 如果类型为字符串,则是元素类型
- 如果类型为
Text
,则是文本类型 - 否则是子节点
- 判断子元素属于什么类型节点
- 如果类型为字符串,则是元素类型
- 如果类型为
Text
,则是文本子类型 - 如果类型为
Array
,则是数组子类型
- 返回包装的对象,包含类型,属性,子元素,标记,空
el
,key
/**
* 创建一个vnode虚拟节点
* @type 虚拟don类型
* @props 属性
* @children 子元素
*/
const h = (type, props, children) => {
let shapeFlag = 0 // 标记:区分类型
// 根据type处理对应的节点类型,把记录的类型保存在shapeFlag中,后续包装成对象返回
if(typeof type === 'string') {
shapeFlag |= SHAPEFLAG.ELEMENT
} else if(type === Text) {
shapeFlag |= SHAPEFLAG.TEXT
} else {
shapeFlag |= SHAPEFLAG.COMPONENT
}
// 根据children处理对应的节点类型,把记录的类型保存在shapeFlag中,后续包装成对象返回
if(typeof children === 'string') {
shapeFlag |= SHAPEFLAG.ELEMENT
} else if(children === 'number') {
shapeFlag |= SHAPEFLAG.TEXT_CHILDREN
} else if(Array.isArray(children)) {
shapeFlag |= SHAPEFLAG.APPY_CHILDREN
}
return {
type,
props,
children,
shapeFlag,
el: null,
key: props && props.key
}
}
传递参数
render(
h(
// 标签,{属性(id,类名,样式,事件)}
'ul', {
id: 'ulId',
class: 'active',
style: {
border: 'none',
color: '#ccc'
},
onClick: () => console.log('click ul')
},
[
h('li', null, 1),
h('li', null, [h(Text, null, '2')]),
h('li', null, 3),
]
)
)
渲染函数执行
定义一个
render
函数,接收vnode
节点和container
内容渲染父节点。一个节点可能会有很多子节点,因此使用递归操作挂载会更好。
container
内容先清空,方便后续挂载- 定义一个
mount
函数用于处理
jsconst render = (vnode, container) => { container.innerHTML = '' mount(vnode, container) }
mount
函数接收一样的参数vnode
中解构获取shapeFlag
标记,判断处理- 如果是文本节点,调用文本节点渲染的函数
- 如果是元素节点,调用元素节点对应的函数
- 如果是子节点,调用子节点对应的函数
都传入
vnode
和container
。jsconst mount = (vnode, container) => { const { shapeFlag } = vnode if (shapeFlag & SHAPEFLAG.TEXT) { // 单独调用文本节点的渲染方法 mountTextNode(vnode, container) } else if (shapeFlag & SHAPEFLAG.ELEMENT) { // 元素节点的挂载方法 mountELEMENT(vnode, container) } else if (shapeFlag & SHAPEFLAG.COMPONENT) { // 子节点 mountCOMPONENT(vnode, container) } }
文本节点挂载函数
- 通过
document.createTextNode
创建文本节点 - 往
container
上挂载刚刚创建的文本节点 - 往
vnode
的el
属性赋值对应属性
jsconst mountTextNode = (vnode, container) => { const textNode = document.createTextNode(vnode.children) container.appendChild(textNode) vnode.el = textNode }
- 通过
元素节点挂载函数
- 解构
vnode
获取类型、属性、子元素与标记 - 创建一个盒子,类型为第一步解构获取到的
type
- 往
container
上挂载元素 - 挂载元素(单独封装为一个函数,传递
props
和el
参数) - 根据不同的子节点类型调用不同的方法来挂载子节点
- 文本节点继续调用文本节点函数渲染
- 子节点则封装一个子节点渲染的函数,接收属性
children
和挂载父节点el
。
- 挂载元素到
container
上 - 把
el
赋值到vnode
的el
上
jsconst mountELEMENT = (vnode, container) => { // 拿数据 const { type, props, children, shapeFlag, } = vnode const el = document.createElement(type) // 挂在属性 mountProps(props, el) // 子节点类型不同,调用不同方法挂载子节点 if (shapeFlag & SHAPEFLAG.TEXT_CHILDREN) { mountTextNode(vnode, el) } else if (shapeFlag & SHAPEFLAG.APPY_CHILDREN) { mountChildren(children, el) } container.appendChild(el) vnode.el = el }
- 解构
子节点渲染的函数
jsconst mountChildren = (children, container) => { children.forEach(element => { mount(element, container) }); }
属性函数渲染
遍历属性对象,通过
switch
判断:- class:类名添加
- style:样式设置
- on:事件绑定
js// 属性遍历 const mountProps = (props, el) => { for (const key in props) { const value = props[key] switch (key) { case 'class': el.className = value break; case 'style': for (const styleName in value) { el.style[styleName] = value[styleName] } break; default: // 判断事件,事件都是以on开头 if(/^on[A-Z]/.test(key)){ // 把事件名的on去掉,转为小写。如:onClick转为click const eventName = key.slice(2).toLowerCase() el.addEventListener(eventName, value) } else { el.setAttribute(key, value) } break; } } }
运行
在 index.html
引入 runtime.js
文件,通过 yarn vite
运行查看页面效果。
注意:
如果出现 'vite' 不是内部或外部命令,也不是可运行的 程序 或批处理文件。 error Command failed with exit code 1 错误,说明你没有装依赖。先
yarn
安装依赖。
页面更新实现
如果 render
函数多次执行更新,则重复清空 cantainer
再创建节点是很消耗性能的。
思路
把旧节点称为 v1,新节点称为 v2.想象以下几点:
- 对比v1和v2,我们需要传入v2,获取v1
- 不能只增不减,因此需要
unmount
删除不需要的节点 - 不同类型节点需要的对比操作不同,如文本节点对比文本内容;元素节点对比标签类型、节点属性、子节点等,因此需要用到不同的函数事件。
- 如果类型相同,只需要去操作节点更新即可,不需要整个替换
步骤
获取旧节点
判断新旧节点是否一样,一样则不需要更新节点,不一样则考虑以下情况:
存在节点,更新(关键在于对比新旧节点的类型
type
)判断,如果类型不一致,销毁节点。获取标记,判断其类型,做不同操作如果存在且为文本类型,判断是否存在新节点。存在则更新文本节点的内容,不存在则调用前面封装好的函数新建文本节点。
如果存在且为元素类型,判断旧节点与新节点是否相同,一样则直接返回,不一样则遍历旧节点的属性对象。
如果对应的新节点该属性为空,更新方法;遍历新节点的属性对象,获取其旧属性的值与新属性的值。
再看看两者是否不等,不等则更新方法。
更新方法的函数接收新旧节点,对应属性的
key
,渲染父节点el
。判断类名、样式、事件的动态修改或移除。jsconst patchDomProps = (prev, next, key, el) => { switch (key) { case 'class': el.className = next || '' break; case 'style': if(next === null) { el.removeAttribute('style') } else { if(prev) { for (const styleName in object) { if(next[styleName] === null) { el.style[styleName] = '' } } } for (const styleName in next) { el.style[styleName] = next[styleName] } } break; default: if (/^on[A-Z]/.test(key)) { // 把事件名的on去掉,转为小写。如:onClick转为click const eventName = key.slice(2).toLowerCase() if(prev) el.removeEventListener(eventName) if(next) el.addEventListener(eventName) } else { if(next === null || next === false) el.removeAttribute(key) if(next) el.setAttribute(key) } break; } }
更新子节点,子节点是数组形式,因此需要获取旧子节点的长度,新子节点的长度和二者最小的值,如果新子节点长度大于旧子节点,说明有新增操作,反之是删除操作
不存在节点,移除旧节点,做销毁操作
jsconst render = (vnode, container) => { const prevNode = container._vnode // 判断旧节点是否存在 if (vnode) { patch(prevNode, vnode, container) } else { prevNode && unmount(prevNode) } container.innerHTML = '' mount(vnode, container) } const patch = (v1, v2, container) => { // 如果类型不一致,直接销毁 if (v1 && v1.type !== v2.type) { unmount(v1) v1 = null } const { shapeFlag } = v2 if (shapeFlag & SHAPEFLAG.TEXT) { processTextNode(v1, v2, container) } else if (shapeFlag & SHAPEFLAG.ELEMENT) { processElement(v1, v2, container) } } const processTextNode = (v1, v2, container) => { if (v1) { v2.el = v1.el v1.el.textContent = v2.children } else { mountTextNode(v2, container) } } const processElement = (v1, v2, container) => { if (v1) { v2.el = v1.el patchProps(v1.props, v2.props, el) patchChildren(v1, v2, v2.el) } else { mountELEMENT(v2, container) } } const patchProps = (old, newProps, el) => { if (old === newProps) { return } for (const key in old) { if (newProps[key] === null) { patchDomProps(old[key], null, key, el) } } for (const key in newProps) { const prew = old[key] const next = newProps[key] if (prew !== next) { patchDomProps(prev, next, key, el) } } } const patchChildren = (v1, v2, el) => { const {shapeFlag: prevShap, children: prevChildren} = v1 const {shapeFlag: nextShap, children: nextChildren} = v2 if(prevShap & SHAPEFLAG.TEXT_CHILDREN) { if(nextShap & SHAPEFLAG.TEXT_CHILDREN) { el.textContent = nextChildren } else if(nextShap & SHAPEFLAG.APPY_CHILDREN) { el.textContent = '' mountChildren(nextChildren, el) } else { el.textContent = '' } } else if(prevShap & SHAPEFLAG.APPY_CHILDREN) { if(nextShap&SHAPEFLAG.TEXT_CHILDREN) { el.textContent = nextChildren } else if(nextShap & SHAPEFLAG.APPY_CHILDREN) { const oldLength = prevChildren.length const newLength = nextChildren.length const commonLength = Math.min(oldLength, newLength) for (let i = 0; i < commonLength; i++) { patch(prevChildren[i], nextChildren[i], el) } if(oldLength > newLength) {} else if(oldLength < newLength) { mountChildren(nextChildren.slice(commonLength), el) } } } } const patchDomProps = (prev, next, key, el) => { switch (key) { case 'class': el.className = next || '' break; case 'style': if(next === null) { el.removeAttribute('style') } else { if(prev) { for (const styleName in object) { if(next[styleName] === null) { el.style[styleName] = '' } } } for (const styleName in next) { el.style[styleName] = next[styleName] } } break; default: if (/^on[A-Z]/.test(key)) { // 把事件名的on去掉,转为小写。如:onClick转为click const eventName = key.slice(2).toLowerCase() if(prev) el.removeEventListener(eventName) if(next) el.addEventListener(eventName) } else { if(next === null || next === false) el.removeAttribute(key) if(next) el.setAttribute(key) } break; } }
Diff算法
思路
有一个场景,我们有5个元素:a、b、c,想要变更为a、d、b、c,通过 diff
算法实现转化。a节点是一样的,不变,直接使用旧节点即可,头指针往后;从后看c节点也是一样的,不变,直接使用旧节点,尾指针往前。
diff
算法核心是先做预处理,处理头指针和尾指针的绑定,中间部分后续做更新处理。
步骤
假设旧节点的 key
和新节点的 key
都存在,定义头指针 i = 0
和旧节点 c1
尾指针 e1 = c1.length - 1
、新节点 c2
尾指针 e2 = c2.length - 1
。
接下来做预处理,判断头指针与尾指针的索引位置:
- 如果头指针小于等于
e1
与小于等于e2
,且c1
的第i
项指针与c2
的第i
项指针相同,则做渲染处理,i
自增1 - 如果尾指针小于等于
e1
与小于等于e2
,且c1
的第e1
项指针与c2
的第e2
项指针相同,则做渲染处理,e1
和e2
自减1
预处理结束,接下来判断新旧节点渲染的情况:如果 i
大于 e1
且小于等于 e2
,这个时候可以认为 i
和 e2
中间部分是需要挂载的新节点;如果 i
大于 e2
且小于等于 e1
,这个时候可以认为 i
和 e1
中间部分是需要卸载的旧节点。
- 如果
i
大于e1
且小于等于e2
,遍历e2
,起始索引为i
,挂载新节点 - 如果
i
大于e2
且小于等于e1
,遍历e1
,起始索引为i
,卸载旧节点
再有一个场景:c1
为a、b、c、d、e;c2
为a、d、f、g、e,只有首尾相同,其他都不等。
首先获取
c1
和c2
的起始索引s1
和s2
等于i
初始化
keyToNewIndex = new Map()
遍历
e2
,初始值为s2
,把索引和对应索引的c2
的key
值作为映射关系保存。定义一个
patched
为0,用于记录当前已经patched
更新的节点数量。定义一个
moved
为false
,用于判断是否需要移动位置。定义一个
maxNewIndexSoFar
为0 ,用于判断是否是上升趋势。定义一个
toBePatch
为e2 - s2 + 1
,用于记录需要被patch
更新的节点数量。根据存的
keyToNewIndex
获取新索引newIndex
根据
newIndex
更新newIndexToOldIndexMap
初始化
newIndexToOldIndexMap
的数据结构new Array(toBePatch).fill(-1)
。遍历
e1
,起始值为s1
,判断patched
是否大于等于toBePatch
,符合要求,则卸载节点。获取该节点的新索引
keyToNewIndexMap.get(c1[i].key)
,如果该索引不存在,说明没有该节点了,销毁;存在,把当前索引i
保存给newIndexToOldIndexMap[newIndex - s2]
,判断其是否为上升趋势newIndex >= maxNewIndexSoFar
,符合要求则把newIndex
赋值给maxNewIndexSoFar
,否则把需要移动的变量moved
改为true
。挂载节点,patched
自增1。移动。根据
newIndexToOldIndexMap
获取其第0项的数据并变为数组的形式res = [newIndexToOldIndexMap[0]]
,定义对应索引pos = [0]
。遍历
newIndexToOldIndexMap
,如果它第i
项为 -1,说明其并没有导入东西,直接pos.push(-1)
即可。如果它第
i
项大于res[res.length - 1]
,res
追加newIndexToOldIndexMap[i]
,pos
追加res.length - 1
;否则遍历res
,判断res[j]
是否大于newIndexToOldIndexMap[i]
,把newIndexToOldIndexMap[i]
赋值给res[j]
。获取处理完的
res
数组最后一项,
组件实现
获取节点类型,节点类型只能是组件节点类型。
创建实例对象
instance
,包含参数props
与attrs
,状态setupState
,ctx
。初始化
props
。定义
props
为当前实例对象中的props
,attrs
为当前实例对象中的attrs
。遍历
props
实例对象,如果组件存在,赋值,不存在,保存。最后通过前面封装好的
reactive
方法转为响应式。为
instance
实例对象通过effect
函数设置update
更新函数,
调度器
定义一个变量 a = ref(0)
,定义一个方法
add = () => {
a.value++
a.value++
}
此时执行操作时会让 a
自增两次1,也就是有两次更新操作,控制台查看效果,发现 log
两次更新,如果变化多的话这性能肯定不合理,最好的方法是把方法封存,等待更新结束后再执行。
使用调度函数,先定义一个队列数组,把所有任务 .push()
进去执行入队操作。
是用一个变量控制是否执行,判断其是否为假。如果为假,改为真,执行回调函数。
createApp
接收一个参数,判断其类型。如果是字符串,则通过 document.querySelector()
获取元素,通过 render(h())
渲染挂载
编译
穷举语法树
设置一个节点类型穷举对象
const NodeTypes = {
ROOT: 'ROOT',
TEXT: 'TEXT',
// ...
}
设置一个元素类型穷举对象
const ElementTypes = {
ELEMENT: 'ELEMENT',
COMPONENT: 'COMPONENT',
// ...
}