实现组件更新功能
- 实现组件更新
- 初始化文件
js
复制代码
// App.js
import { h, ref } from '../../lib/guide-mini-vue.esm.js'
import Child from './Child.js'
export const App = {
name: 'App',
setup() {
const msg = ref("123")
const count = ref(1)
window.msg = msg
const changeChildProps = () => {
msg.value = "456"
}
const changeCount = () => {
count.value++
}
return {
msg,
count,
changeChildProps,
changeCount
}
},
render() {
return h("div", {}, [
h("div", {}, "你好"),
h(
"button",
{
onClick: this.changeChildProps,
},
"change child prop"
),
h(Child, {
msg: this.msg
}),
h("button", {
onClick: this.changeCount,
},
"change self count"
),
h("p",{},"count: " + this.count)
])
}
}
js
复制代码
// Child.js
import { h } from '../../lib/guide-mini-vue.esm.js'
export default {
name: "Child",
setup(props, { emit }) {},
render(proxy) {
return h("div",{}, [
h("div",{},"child - prop - msg:" + this.$props.msg)
])
}
}
js
复制代码
// main.js
import { App } from './App.js'
import { createApp } from '../../lib/guide-mini-vue.esm.js'
const root = document.querySelector('#root')
createApp(App).mount(root)
html
复制代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script src="./main.js" type="module"></script>
</body>
</html>
- 我们运行html,发现 msg 报错,Child.js 中的 this.$props 我们没有进行封装,那么我们来进行封装
- 参考之前我们封装的 this.slot,this.slot, this.slot,this.data, 我们是在 componentPublicInstance.js 中处理这块的逻辑,这块不理解可以看前面关于 data,data,data,slots 的章节
js
复制代码
import { hasOwn } from "../shared/index"
const publicPropertiesMap = {
$el:(i)=>i.vnode.el,
$slots:i=>i.slots,
$props:i=>i.props, // ✅ 这里增加一个 props,页面就初始化渲染出来 props
}
export const PublicInstanceProxyHandlers = {
get({_: instance}, key) {
let { setupState, props } = instance
if(hasOwn(setupState,key)) {
return setupState[key]
} else if(hasOwn(props, key)) {
return props[key]
}
const publicGetter = publicPropertiesMap[key]
if(publicGetter) {
return publicGetter(instance)
}
}
}
- 现在初始化可以渲染出来,但点击 change child props 按钮原视图没变化,并在此基础上叠加一个新视图渲染的 props 是 456
- 我们需要再次调用 render 方法,然后进行 patch 对比渲染,而不是叠加渲染
js
复制代码
// renderer.ts
function patch(n1, n2, container, parentComponent, anchor) {
const { type, shapeFlag } = n2
switch (type) {
case Fragment:
processFragment(n1, n2, container, parentComponent, anchor)
break;
case Text:
processText(n1, n2, container)
break;
default:
if (shapeFlag & shapeFlags.ELEMENT) {
processElement(n1, n2, container, parentComponent, anchor)
} else if (shapeFlag & shapeFlags.STATEFUL_COMPONENT) {
processComponent(n1, n2, container, parentComponent, anchor)
}
break;
}
}
function processComponent(n1, n2: any, container: any, parentComponent, anchor) {
if (!n1) {
mountComponent(n2, container, parentComponent, anchor)
} else {
updateComponent(n1, n2) // ✅
}
}
function updateComponent(n1, n2) { // ✅
const instance = n2.component = n1.component
instance.next = n2 // 更新的时候需要更新组件的 props
// 我们下面的 render 方法在 setupRenderEffect 里面写的,里面使用的 effect 返回 runner 函数,调用 renner 函数,就会再次执行 effect 里面传入的 fn 函数逻辑
instance.update() // 更新组件就是调用组件的 render 函数,重新生成虚拟节点,再进行 patch ,再进行对比
}
// vnode.ts
export function createVNode(type, props?, children?) {
const vnode = {
type,
props,
children,
component: null, // ✅ 组件的实例
key: props && props.key,
shapeFlag: getShapeFlag(type),
el: null,
} }
// component.ts
export function createComponentInstance(vnode, parent) {
const component = {
vnode, // ✅ 组件实例上现有的虚拟节点
next: null, // ✅ 组件实例上即将挂载的虚拟节点
type: vnode.type,
setupState: {},
props: {},
emit:()=>{},
slots:{},
provides: parent ? parent.provides : {},
parent,
isMounted: false,
subTree: {},
}
component.emit = emit.bind(null, component) as any // 这里 null 是this指向, component 作为第一个参数传入
return component
}
// renderer.ts
function mountComponent(initialVnode, container, parentComponent, anchor) {
const instance = initialVnode.component = createComponentInstance(initialVnode, parentComponent) // ✅ 将组件虚拟节点挂载在虚拟 dom 上
setupComponent(instance)
setupRenderEffect(instance, initialVnode, container, anchor)
}
function setupRenderEffect(instance, vnode, container, anchor) {
// 我们将 该 effect 的返回的 runner 函数,直接挂载在实例上, 方便组件第二次更新时直接调用
instance.update = effect(() => { // ✅
let { proxy } = instance
if (!instance.isMounted) {
const subTree = instance.subTree = instance.render.call(proxy)
patch(null, subTree, container, instance, anchor)
// 在所有 element 都已经挂载完毕后,才能够拿到 虚拟节点
vnode.el = subTree.el
instance.isMounted = true
} else {
// ✅ 需要一个更新完成之后的 vnode
const { next, vnode } = instance // 这里的 vnode 是我们更新之前的虚拟节点,next是我们下次要更新的虚拟节点
if (next) { // ✅
next.el = vnode.el
updateComponentPreRender(instance, next)
}
const { proxy } = instance
const subTree = instance.render.call(proxy)
const prevSubTree = instance.subTree
instance.subTree = subTree
patch(prevSubTree, subTree, container, instance, anchor)
}
})
}
function updateComponentPreRender(instance, nextVNode) {
instance.vnode = nextVNode
instance.next = null
instance.props = nextVNode.props
}
- 我们发现功能已经实现了,但 count 数值的++,也会造成不相关的组件的渲染, 所以我们在 updateComponent 时,需要判断应不应该更新
js
复制代码
// renderer.ts
function updateComponent(n1, n2, container) {
if(shouldUpdateComponent(n1, n2)) { // ✅
const instance = n2.component = n1.component
instance.next = n2
instance.update()
}
}
// shouldComponentUtils.ts
export function shouldUpdateComponent(prevVNode, nextVNode) { // ✅
const {props: prevProps} = prevVNode
const {props: nextProps} = nextVNode
for(let key in nextProps) {
if(nextProps[key]!==prevProps[key]){
return true
}
}
return false
}
- 进行测试,我们发现 count++ 的问题解决了,但还有一个问题,当不需要组件更新时,我们也要对实例上挂载的 component 和 vnode 进行替换处理。
js
复制代码
function updateComponent(n1, n2, container) {
const instance = n2.component = n1.component // ✅
if(shouldUpdateComponent(n1, n2)) {
instance.next = n2
instance.update()
} else { // ✅
n2.el = n1.el
instance.vnode = n2
}
}