手写 mini-vue3 - 实现 component 的更新(十一)

92 阅读1分钟

补充相关的属性

next

next 指向当前组件对应的待更新的虚拟节点

// component.ts
export function createComponentInstance(vnode, parent) {
  const component = {
    next: null, // 如果组件有变化,那么这个 next 指向待更新的虚拟节点
  };
  return component;
}

component

// vnode.ts
export function createVNode(type, props?, children?) {
  const vnode = {
    component: null, // 指组件实例
  };
}
  1. 初始渲染,虚拟节点的 componentmounComponent 的时候更新为组件的实例:
    function mountComponent(initialVNode, container, parentComponent, anchor) {
        // 创建组件实例 instance
        const instance = (
            initialVNode.component = // 虚拟节点的component其实是一个组件实例
            createComponentInstance(initialVNode, parentComponent)
        );
    }
  1. 组件更新的情况下,新虚拟节点的 componentupdateComponent 里面被初始化,instance 的更新需要到 instance.update 执行之后完成。
  function updateComponent(n1, n2) {
    const instance = (n2.component = n1.component); // 新的虚拟节点的 component 初始化
  }

$props 属性

给组件代理对象添加 $props属性,指向组件实例的 props 属性。

// componentPublicInstance.ts
const PublicPropertiesMaps = {
  $props: (i) => i.props,
}

export const PublicInstanceProxyHandlers = {
  get({ _: instance }, key) {

    const publicGetter = PublicPropertiesMaps[key];
    if(publicGetter) {
      return publicGetter(instance);
    }
  }
}

更新 component 的过程

假设 App 组件嵌套有一个子组件 Child

const App = {
    name: 'App',
    setup() {
        const msg = ref('lallallla');
        return {
            msg
        }
    },
    render() {
        return h('div', { class: 'app' }, [
            h(Child, {
                msg: this.msg,
            })
        ])
    }
}
  1. 首先是 effect

    1. App 组件的render函数内有响应式数据 msg 发生变化,会自动触发 effect
    2. 再到 patch->processElement->patchElement->patchChildren->patchKeyedChildren
    3. 再次来到 patch,但是这次 patch 的是 Child 组件
    4. 所以会来到 processComponent->updateComponent,在这里会判断是否 shouldUpdateComponent
    5. shouldUpdateComponent 在新旧节点的 props 值有变化的时候,返回 true
    6. 如果 shouldUpdateComponenttrue,会做两件事:
      • instance.next = n2;
      • instance.update();
      • instance 指的是 Child 组件的实例
  2. instance.update 被调用

    1. Child 组件的 render 被再次调用,
    2. 继续 patch Child 组件的儿子节点,最终完成 Child 组件的更新

patch

  // n1 -> old, n2 -> new
  function patch(n1, n2, container, parentComponent, anchor) {
    const { shapeFlag, type } = n2;
  
    switch (type) {
      case Fragment:
        // ...
        break;
  
      case Text:
        // ...
        break;
  
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          // ...
        } else {
          // 处理组件
          processComponent(n1, n2, container, parentComponent, anchor);
        }
        break;
    }
  }
  
  function processComponent(n1, n2, container, parentComponent, anchor) {
    if(!n1) {
      mountComponent(n2, container, parentComponent, anchor);
    
    // 存在新旧节点,更新
    } else {
      updateComponent(n1, n2);
    }
  }
  
  function updateComponent(n1, n2) {
    const instance = (n2.component = n1.component);

    if(shouldUpdateComponent(n1, n2)) {
      instance.next = n2;
      instance.update();
    } else {
      n2.el = n1.el;
      instance.vnode = n2;
    }
  }

shouldUpdateComponent

// componentUpdateUtils.ts
export function shouldUpdateComponent(prevVNode, nextVNode) {
  const { props: prevProps } = prevVNode;
  const { props: nextProps } = nextVNode;

  for(const key in nextProps) {
    if(nextProps[key] !== prevProps[key]) {
      return true;
    }
  }

  return false;
}

instance.update

function setupRenderEffect(instance, initialVNode, container, anchor) {
    // instance.update 是一个 runner
    instance.update = effect(() => {
      if(!instance.isMounted) {
        console.log('init');
        const { proxy } = instance;
        const subTree = (instance.subTree = instance.render.call(proxy, proxy));
        
        debugger
        patch(null, subTree, container, instance, anchor);
      
        // vnode -> element
        initialVNode.el = subTree.el;

        instance.isMounted = true;
      
      // 更新
      } else {
        console.log('update');

        // 更新 props,需要一个 vnode
        const { next, vnode } = instance;
        if(next) {
          // 设置 新虚拟节点的 el
          next.el = vnode.el;
          updateComponentPreRender(instance, next);
        }

        const { proxy } = instance;
        const subTree = instance.render.call(proxy, proxy);
        const prevSubTree = instance.subTree;
        instance.sbuTree = subTree;

        patch(prevSubTree, subTree, container, instance, anchor);
      }
    });
  }

至此,component 的更新完成。

但是有些疑问:

Element Props 和 Component Props

之前实现的 Element 的 props 更新涉及到 props,这次组件更新也涉及到 props,它们有什么区别呢?

根据 vnode.props 更新 Element 的 Props

// renderer.ts 
function processElement(n1, n2, container, parentComponent) { 
    if(!n1) { 
        mountElement(n2, container, parentComponent) 
    } else { 
        patchElement(n1, n2, container);
    }
}
function patchElement(n1, n2, container) { 
    // 获得新旧 props 
    oldProps = n1.props || EMPTY_OBJ; 
    newProps = n2.props || EMPTY_OBJ; 
    // 在这里更新 el 
    el = (n2.el = n1.el); 
    patchChildren(n1, n2, el, parentComponent, anchor);
    patchProps(el, oldProps, newProps);
}
function patchProps(el, oldProps, newProps) {
     // 添加|更新 props
     for (key in newProps) {
         prevProp = oldProps[key];
         nextProp = newProps[key];

         // 对比,是不同的值
         if (prevProp !== nextProp) {
             // 更新 props
             hostPatchProp(el, key, prevProp, nextProp);
         }
     }

     // 移除不存在的旧 props
     if (oldProps !== EMPTY_OBJ) {
         for (key in oldProps) {
             // 新的 props 没有找到旧的 props
             if !(key in newProps) {
                 // 移除旧的 props
                 hostPatchProp(el, key, oldProps[key], null);
             }
         }
     }
 }

根据 vnode.props 判断更新 Component

  function updateComponent(n1, n2) {
    const instance = (n2.component = n1.component);

    if(shouldUpdateComponent(n1, n2)) {
      instance.next = n2;
      instance.update();
    } else {
      n2.el = n1.el;
      instance.vnode = n2;
    }
  }

再次分析上面的代码,我们可以看出一些区别:

  1. 更新 Element 的 Props,其实是实打实地针对标签的属性做更新
h('div', { class: 'red' }, 'Hello World'); // old
h('div', { class: 'blue' }, 'Hello World'); // new 
  1. 更新 Component,props 的作用是用于判断是否需要更新组件,即 props 有变更,则组件需要更新
h(Child, { msg: '1' }); // old
h(Child, {msg: '2'}); // new
  1. 组件的更新,是通过调用 instance.update 去触发的
  2. 组件的 vnode.props 是组件级别的,Element 的 vnode.props 是标签级别的
  3. 组件的 vnode.props 是传给子组件作用于子组件的,Element 的 vnode.props 是作用于本身的

props $props 的区别

// 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');

    window.msg = msg;
    
    const changeChildProps = () => {
      msg.value = '456';
    }

    return {
      msg,
      changeChildProps,
    }
  },
  render() {
    return h('div', { class: 'app' }, [
      h(
        'button',
        {
          onClick: this.changeChildProps,
        },
        'change child props',
      ),
      h(Child, {
        msg: this.msg,
      }),
    ])
  }
}

// Child.js
import { h } from '../../lib/guide-mini-vue.esm.js'

export default {
  name: 'Child',
  setup(props, { emit }) {
    return {
      msg: props.msg
    }
  },
  render({ $props }) {
    return h('div', { class: 'child' }, [
      // 因为在传给 setup 的 props,是只读的,所以这里不会变
      h('div', {}, 'child - setup - props - msg: ' + this.msg),
      h('div', {}, 'child - $props - msg: ' + this.$props.msg),
      h('div', {}, 'child - render - $props - msg: ' + $props.msg),
    ])
  }
}

image.png

在这个例子中,如果我们改变 msg 的数据,在子组件 Child 中的三种 props 的属性取值会是一样吗?

答案是不一样。

来看下结果:

image.png

this.$props.msg$props.msg 是会变的,但是 this.msg 不会变。

通过之前写的文章,我们应该清楚了 this.msg 是访问的 Child 的 setup 的返回值对象 setupState 的属性。

  setup(props, { emit }) {
    return {
      msg: props.msg
    }
  },

这里直接把 props.msg 传给了 msg,但是 setup 是不具有 effect 功能的,所以监听不到 props 的变化,所以这里通过 this.msg 获取的值没变。

this.$props.msg$props.msg 会变,是因为在 render 函数中,实质是获取的 instanceprops 属性。


const PublicPropertiesMaps = {
  $props: (i) => i.props,
}

export const PublicInstanceProxyHandlers = {
  get({ _: instance }, key) {
    const publicGetter = PublicPropertiesMaps[key];
    if(publicGetter) {
      return publicGetter(instance);
    }
  }
}

更新 component 的 props 时机

在什么时候完成组件 props 的更新的呢?

是在 updateComponentPreRender 的时候,更新 Child 组件的儿子节点之前。

updateComponentPreRender 是在执行 Child 组件的 render 之前执行的。

function updateComponentPreRender(instance, nextVNode) {
  // 更新当前实例的 vnode 为新的
  instance.vnode = nextVNode;
  // 当前实例的 next 节点清空 
  instance.next = null; 
  // 更新当前实例的 props 
  instance.props = nextVNode.props;
}

所以在Child 组件的子 DOM 完成更新渲染之前,Child 的 Props 已完成了更新,最终页面上显示的 $props.msgthis.$props.msg 是更新后的。