vue3组件更新

上篇文章vue3组件渲染成DOM,分析了组件如何渲染成DOM,挂载到页面上,这篇文章讲一下,组件如何更新?

setupRenderEffect 执行render,收集副作用函数effect

还记得在组件初始挂载的时候,执行了mountComponent:

js 复制代码
const mountComponent = (initialVNode, container, anchor, parentComponent) => {
  // 1. 先创建一个 component instance ,同时放一份在vnode的component属性上
  const instance = (initialVNode.component = createComponentInstance(
    initialVNode,
    parentComponent,
    parentSuspense
  ));

  // 2. 设置组件实例
  setupComponent(instance);

  // 3. 设置并运行带副作用的渲染函数 组件的更新逻辑
  setupRenderEffect(instance, initialVNode, container, anchor);
};

组件更新的秘密就在这个setupRenderEffect 函数中:

js 复制代码
const setupRenderEffect = (instance, initialVNode, container, anchor) => {
  const componentUpdateFn = () => {
    if (!instance.isMounted) {
     // 挂载
     const subTree = (instance.subTree = renderComponentRoot(instance))
    } else {
      // 更新
    }
  };

  // 创建响应式的副作用渲染函数
  const effect = (instance.effect = new ReactiveEffect(
    componentUpdateFn,
    () => queueJob(update),
    instance.scope
  ));

  const update= (instance.update = () => effect.run());

  update();
};

首先,调试一个组件挂载的例子看一下

js 复制代码
// App.vue

<script setup>
import { ref } from '@vue/reactivity'
const msg = ref('vue2')
console.log('msg:', msg)
</script>

<template>
  <div class="App">
    <h1 class="App-text">App组件</h1>
    <span>{{ msg }}</span>
  </div>
</template>

在App组件中定义一个响应式数据,然后在模板中使用它

通过源码调试,我们看到,执行update(),就会执行 effect.run()

js 复制代码
export class ReactiveEffect {
  run() {
    try {
      return this.fn(); 
    } finally {
    }
  }
}

可以看到,执行run,就是执行 ReactiveEffect类中传入的回调componentUpdateFn componentUpdateFn 在挂载的时候,不是执行了一句const subTree = (instance.subTree = renderComponentRoot(instance)) ,这一行代码在上一篇文章中分析过,会执行组件实例 instancerender属性函数,render函数执行读取到里面的响应式数据msg,就会收集相应的副作用函数,其实收集的就是上面的effect ,ReactiveEffect的实例

等响应式数据更新,effect 被拿出来再次执行它的 run 方法,componentUpdateFn 这个函数也会执行,这个时候就会走componentUpdateFn 里面else更新逻辑了

组件更新逻辑

componentUpdateFn

js 复制代码
const componentUpdateFn = () => {
  if (!instance.isMounted) {
    // 初始化组件
  } else {
    // 更新组件
    let { next,  vnode } = instance
    
    // next 新的组件vnode
    if (next) {
      next.el = vnode.el
      updateComponentPreRender(instance, next, optimized) 
    } else {
      next = vnode
    }
   
    // 组件里面所有DOM节点新的vnode
    const nextTree = renderComponentRoot(instance) 
    const prevTree = instance.subTree 
    instance.subTree = nextTree 

    patch(
      prevTree,
      nextTree,
      // parent may have changed if it's in a teleport  // 处理 teleport 相关
      hostParentNode(prevTree.el!)!,
      // anchor may have changed if it's in a fragment  // 处理 fragment 相关
      getNextHostNode(prevTree),
      instance,
      parentSuspense,
      isSVG
    )
    
    next.el = nextTree.el // 缓存更新后的 DOM 节点

  }
}

首先,next变量代表是否有新的组件vnode,有的话,通过updateComponentPreRender ,更新组件实例instance,组件vnode,slot,props等信息,为下一次组件更新提供最新的值;

然后通过renderComponentRoot来获取组件里面DOM节点新的vnode:nextTree,后面就叫组件的子树vnode,它和和组件vnode是不一样的;

最后把新旧组件的子树vnode进行patch

patch

js 复制代码
const patch = (n1, n2, container, anchor = null, parentComponent = null) => {
  const { type, ref, shapeFlag } = n2;
  switch (type) {
    // ...
    default:
      // 进行按位与运算
      if (shapeFlag & ShapeFlags.ELEMENT) {
        // 处理普通 DOM 元素
        processElement(n1, n2, container, anchor, parentComponent);
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        // 处理 component
        processComponent(n1, n2, container, anchor, parentComponent);
      }
  }
};

调试一个更新的例子:

js 复制代码
// App.vue
<script setup>
import { ref } from '@vue/reactivity'
import Child from './Child.vue'

const msg = ref('vue2')
</script>

<template>
  <div class="App">
    <h1 class="App-text" @click="msg = 'vue3'">App组件</h1>
    <span>{{ msg }}</span>
    <p>--------------------------</p>
    <Child :msg="msg" />
  </div>
</template>


// Child.vue
<script setup>
import { ref } from '@vue/reactivity'

defineProps({ msg: String })

const count = ref(10)
</script>

<template>
  <div class="child">
    <h2>Child组件</h2>
    <p>{{ msg }}</p>
    <p>{{ count }}</p>
    <button @click="count++">改变子组件数据</button>
  </div>
</template>

点击更新App.vue中的msg,App组件会进行更新,进入componentUpdateFn

接下来进入对比App的新旧子树vnode patch(prevTree,nextTree,...)

processElement -> patchElement

js 复制代码
const processElement = (n1, n2, container, anchor, parentComponent) => {
  if (n1 == null) {
    // 挂载
  } else {
   // 更新
    patchElement(
      n1,
      n2,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    );
  }
};
js 复制代码
const patchElement = (n1, n2, parentComponent) => {
  const el = (n2.el = n1.el!);
  let { patchFlag, dynamicChildren, dirs } = n2;

  const oldProps = n1.props;
  const newProps = n2.props;

  if (dynamicChildren) {
    // 优化,动态节点different,后续再看
  } else if (!optimized) {
    // full diff 全量diff
    patchChildren(
      n1,
      n2,
      el,
      null,
      parentComponent,
      parentSuspense,
      areChildrenSVG,
      slotScopeIds,
      false
    );
  }

  // patch props
  patchProps(
    el,
    n2,
    oldProps,
    newProps,
    parentComponent,
    parentSuspense,
    isSVG
  );
};

patchChildren 全量diff

js 复制代码
const patchChildren = (n1, n2, container, anchor, parentComponent) => {
  const c1 = n1 && n1.children; // c1 代表旧节点的子节点元素
  const prevShapeFlag = n1 ? n1.shapeFlag : 0;  // 旧节点类型标识
  const c2 = n2.children; // c2 代表新节点的子节点元素

  const { patchFlag, shapeFlag } = n2;

  // children has 3 possibilities: text, array or no children.
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    // text children fast path
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      // 新节点是文本 旧节点是数组
      unmountChildren(c1 as VNode[], parentComponent, parentSuspense); // 卸载旧节点
    }
    if (c2 !== c1) {
      // 新节点是文本 旧节点是文本或空
      hostSetElementText(container, c2 as string);
    }
  } else {
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // prev children was array
        // 新节点是数组  旧节点是数组
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // two arrays, cannot assume anything, do full diff
        patchKeyedChildren(
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        );
      } else {
        // no new children, just unmount old
        // 新节点空  旧节点是数组
        unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true); // 卸载旧节点
      }
    } else {
      // prev children was text OR null
      // new children is array OR null
      if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
        //新节点是数组或空 旧节点是文本
        hostSetElementText(container, ""); // 清空旧节点
      }
      // mount new if array
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // 新节点是数组 旧节点是空
        mountChildren(
          // 挂载新节点
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        );
      }
    }
  }
};

回到上面的例子,由于App的新旧子树vnode:nextTree和prevTree里面的children都是数组,所以走到 patchKeyedChildren diff,这里先知道,就是循环children继续调用patch对比, 当循环到children数组中最后一个vnode,就是Child组件vnode,进入patch -> processComponent -> updateComponent

updateComponent 更新子组件

js 复制代码
const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
  const instance = (n2.component = n1.component)!;

  // 根据新老节点判断是否需要更新子组件
  if (shouldUpdateComponent(n1, n2, optimized)) {
    // normal update
    instance.next = n2; // 如果需要更新,则将新节点 vnode 赋值给 next

    // in case the child component is also queued, remove it to avoid
    // double updating the same child component in the same flush.
    invalidateJob(instance.update);

    // instance.update is the reactive effect.
    // 执行前面定义在 instance 上的 update 函数。
    instance.update();
  } else {
    // no update needed. just copy over properties
    n2.el = n1.el;
    instance.vnode = n2;
  }
};

shouldUpdateComponent 函数的内部,主要是通过对比组件vnode中的dirs, props、chidren等属性,来决定子组件是否需要更新。

通过上面的调试,看以看到child组件vnode里面的props更新了,所以肯定是应该更新App的子组件Child,instance.next = n2,Child组件实例intance的next属性赋值最新的组件vnode;

invalidateJob(instance.update) 防止重复更新,如果出现子组件的响应式数据变化,也会导致子组件更新,而这里父组件的状态数据变化也引起子组件更新,就重复更新了,这里就是如果更新队列中有了instance.update就删除它;

instance.update()就相当于Child组件主动更新自己,调用自己的副作用渲染函数,然后调用componentUpdateFn,又会进入patch

可以看到,Child组件实例的next是有值的,就是Child组件vnode

那么如果是Child组件自己的响应式数据变化,会怎么样,点击Child组件的button:

next是null,因为此时App组件不会更新,也就不会走到判断子组件Child是否需要更新的逻辑里面,此时Child的更新跟前面App组件更新原理一样,因为模版里面使用了响应式数据,Child组件的副作用渲染函数被收集了

普通DOM的vnode -> 真实DOM

patch会被反复递归调用,但是不会无限递归,组件vnode是抽象的,最终还是要变成普通DOM元素的vnode,最后转换成真正的DOM。

比如App的子树vnode的children里前3个子元素的普通DOM元素vnode,最后还是会执行到processElement -> patchElement -> patchChildren,patchProps ,该更新属性更新属性,该更新文本更新文本;

最后一个是Child组件vnode,继续patch它,Child的子树vnode的children里面都是普通DOM元素vnode,没有组件vnode,把他们该更新的更新了,也就结束了

相关推荐
梦境之冢28 分钟前
axios 常见的content-type、responseType有哪些?
前端·javascript·http
racerun31 分钟前
vue VueResource & axios
前端·javascript·vue.js
J总裁的小芒果1 小时前
THREE.js 入门(六) 纹理、uv坐标
开发语言·javascript·uv
m0_548514771 小时前
前端Pako.js 压缩解压库 与 Java 的 zlib 压缩与解压 的互通实现
java·前端·javascript
AndrewPerfect1 小时前
xss csrf怎么预防?
前端·xss·csrf
Calm5501 小时前
Vue3:uv-upload图片上传
前端·vue.js
浮游本尊1 小时前
Nginx配置:如何在一个域名下运行两个网站
前端·javascript
m0_748239831 小时前
前端bug调试
前端·bug
m0_748232921 小时前
[项目][boost搜索引擎#4] cpp-httplib使用 log.hpp 前端 测试及总结
前端·搜索引擎
新中地GIS开发老师1 小时前
《Vue进阶教程》(12)ref的实现详细教程
前端·javascript·vue.js·arcgis·前端框架·地理信息科学·地信