Evan You课程b站地址: https://www.bilibili.com/video/BV1rC4y187Vw?p=7&spm_id_from=pageDriver&vd_source=97f7ca7924dd57017c9bb65f9a9860a1
三个模块:
- 响应式模块:创建JS对象并观察其变化
- 编译器模块: 将HTML模板编译成渲染函数
- 渲染模块: 渲染阶段(调用render返回虚拟DOM节点),挂载阶段(利用虚拟DOM调用DOM API创建页面),补丁阶段(新旧虚拟DOM比较更新变化部分到网页)
一个组件的创建过程:先将template编译成render函数,然后响应式模块初始化响应式数据对象,接着渲染模块调用引用了响应式对象的render函数,返回虚拟DOM节点,在挂载阶段调用mount函数创建网页。当响应式模块观察到数据变化时,渲染器重新调用render函数生成新的虚拟DOM节点,然后将新旧节点发送到patch函数,比较以最小程度更新页面。
Vue3变化:
- 自定义渲染器API,我们可以依赖vue3 runtime-core和自定义的渲染器API自定义渲染器。WebGL渲染器
- render函数
功能型组件:使用渲染函数,获取插槽内容进行渲染
const slot = this.$slots.default && this.$slots.default()
// 作用域插槽,给default传递props
const slot = this.$slots.default && this.$slots.default({...})
const slot = this.$slots.default
? this.$slots.default()
: []
<Stack size="4">
<div>hello</div>
<div>hello</div>
<div>hello</div>
<Stack size="4">
<div>hello</div>
<div>hello</div>
</Stack>
</Stack>
h('div' , { class: 'stack' } , slot.map(child => {
return h('div' , { class="mt-4"} , [
child
])
}))
hoist: 每次节点更新render都会重新调用,将静态节点提升到render外
模板编译阶段的动态属性标记:
对于某个节点不必遍历整个节点对象,只需要关注动态的属性,模板编译器会做标记,这是模板相比于手写渲染函数的优点。
编译器:当进行动态节点的patch flag标记后,会给组件根节点openBlock,将这个flag作为一个属性添加到这个block上(动态子节点),一个额外的动态子节点扁平数组
使用结构指令(如v-if)的节点会将其与其子节点作为一个block,这个block跟踪其父块的所有动态子节点,依然是一个扁平数组。
事件处理程序缓存:避免了事件处理程序引起的大规模组件树的重新渲染。
<div id="app"></div>
<script>
function h(tag, props, children) {
}
function mount(vnode, container) {
}
const vdom = h('div', { class: 'red' }, [
h('span', null, 'hello')
])
mount(vdom, document.querySelector('#app'))
</script>
patch方法最有意思的部分:
the most intersting part: both oldChildren and newChildren are arrays
internally , invue,we have two modes:
keyed mode: the key serves as a hint about the position of a node in child list
move the child node according to their key
两个层次的提示优化:
- 跳过patch中props的比较,跳过children的比较
- block optimize: 直接可以跳过patch调用
手写vue3
https://jsbin.com/fanixek/1/edit?html,output
除虚拟DOM外其他的编译策略:
比如直接根据模板生成指令列表 [ appendChild , createElement , ...]