35-mini-vue 实现组件更新功能

实现组件更新功能

  1. 实现组件更新
  2. 初始化文件
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>
  1. 我们运行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)
      }
    }
  }
  1. 现在初始化可以渲染出来,但点击 change child props 按钮原视图没变化,并在此基础上叠加一个新视图渲染的 props 是 456
  2. 我们需要再次调用 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
}
  1. 我们发现功能已经实现了,但 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
}
  1. 进行测试,我们发现 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 
  }
}
相关推荐
前端达人2 小时前
为什么聪明的工程师都在用TypeScript写AI辅助代码?
前端·javascript·人工智能·typescript·ecmascript
快乐点吧2 小时前
使用 data-属性和 CSS 属性选择器实现状态样式控制
前端·css
EndingCoder2 小时前
属性和参数装饰器
java·linux·前端·ubuntu·typescript
小二·2 小时前
Python Web 开发进阶实战:量子机器学习实验平台 —— 在 Flask + Vue 中集成 Qiskit 构建混合量子-经典 AI 应用
前端·人工智能·python
TTGGGFF2 小时前
控制系统建模仿真(十):实战篇——从工具掌握到工程化落地
前端·javascript·ajax
郝学胜-神的一滴3 小时前
深入解析C/S架构与B/S架构:技术选型与应用实践
c语言·开发语言·前端·javascript·程序人生·架构
s19134838482d3 小时前
javascript练习题
开发语言·javascript·ecmascript
这是个栗子4 小时前
前端开发中的常用工具函数(二)(持续更新中...)
开发语言·前端·javascript
苦藤新鸡4 小时前
38.交换二叉树中所有的左右节点
开发语言·前端·javascript