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"}
]
)
}
上面例子 我们进行渲染结果
数据响应式处理
当前情况下,我们触发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事件,得到以下结果
从上面可以看到,数据响应式效果已经达成。生成三个组件的原因时我们之前在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事件得到下面结果
接下来就是更新逻辑的实现
更新
更新的本质其实就是新旧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的初始化以及更行大致流程
接下来就是进行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的情况更为复杂主要有四种情况
oldChildren
和newChildren
均为 字符串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 初始化的时候实现过,参数后续自己调整