vue3-runtime(四) 组件

115 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第18天,点击查看活动详情

这次讲 组件 的渲染和更新,组件有多种写法,这里用简单的进行举例,还有组件的主动和被动更新机制。

认识props和attribute

cn.vuejs.org/guide/compo…

const Comp = {
  props: ['foo'],
  render(ctx) {
      //由于bar不在props,所以获取不到,id为空
    return h('div', { class: 'a', id: ctx.bar }, ctx.foo);
  },
};
​
const vnodeProps = {
  foo: 'foo',
  bar: 'bar', // 会作为节点属性自动挂载,最常见的例子就是 class、style 和 id。
};
​
const vnode = h(Comp, vnodeProps);
render(vnode, root); // 渲染为<div class="a" bar="bar">foo</div>

Comp.props 决定它接收哪些外部传入的 vnodeProps,把它放入 instance.props,而其他属性会添加进 instance.attrs,作为根节点属性。render 中的 ctx 只会使用 instance.props

updateProps 拆解传入参数

function updateProps(instance, vnode) {
  const { type: Component, props: vnodeProps } = vnode;
  const props = (instance.props = {});
  const attrs = (instance.attrs = {});
  // 拆出attrs和props
  for (const key in vnodeProps) {
    // 遍历传入的props,如果被组件接收,则传入props,如果没有,则给attrs
    if (Component.props?.includes(key)) {
      props[key] = vnodeProps[key];
    } else {
      attrs[key] = vnodeProps[key];
    }
  }
  // toThink: props源码是shallowReactive,确实需要吗?
  // 需要。否则子组件修改props不会触发更新
  instance.props = reactive(instance.props);
}
// 为什么type是Component?
h(type, props = null, children = null) {
   return {
    type,
   }
}

fallThrough 节点继承attrs

// 节点继承attrs
function fallThrough(instance, subTree) {
  if (Object.keys(instance.attrs).length) {
    subTree.props = {
      ...subTree.props, // 这里不清楚有没有必要
      ...instance.attrs,
    };
  }
}

normalizeVNode

render中返回有时候是数组节点有时候是字符串,这里做一下统一兼容,如:

 render(ctx) {
    return [
      h('div', null, ctx.count),
      h('div', null, ctx.count),]
 }
export function normalizeVNode(result) {
    // 如果是数组,如上,作为fragment的子节点即可
  if (Array.isArray(result)) {
    return h(Fragment, null, result);
  }
    // 直接返回 return h('div', { class: 'a', id: ctx.bar }, ctx.foo);
  if (isObject(result)) {
    return result;
  }
    // 文本 数字说明是文本节点
  return h(Text, null, result.toString());
}
//  组件渲染 
normalizeVNode(
          Component.render(instance.ctx)
        ));

例子

1

const Comp = {
  props: ['foo'],
  render(ctx) {
      //由于bar不是props,所以获取不到,id为空
    return h('div', { class: 'a', id: ctx.bar }, ctx.foo);
  },
};
​
const vnodeProps = {
  foo: 'foo',
  bar: 'bar', // 会作为节点属性自动挂载,最常见的例子就是 class、style 和 id。
};
​
const vnode = h(Comp, vnodeProps);
render(vnode, root); // 渲染为<div class="a" bar="bar">foo</div>

2 setup

const Comp ={
  setup() {
    const count = ref(0);
    const add = () => count.value++;
    return {
      count,
      add,
    };
  },
  render(ctx) {
    return [
      h('div', null, ctx.count),
      h(
        'button',
        {
          onClick: ctx.add,
        },
        'add'
      ),
    ];
  },
}
const vnode = h(Comp);
render(vnode, document.body); 

待续

以后
<script setup>
import { ref } from 'vue'const count = ref(0)
</script><template>
  <button @click="count++">{{ count }}</button>
</template>

基础渲染实现

实现例子1

export function mountComponent(vnode, container, anchor, patch) {
  const { type: Component } = vnode;
    // createComponentInstance
  const instance = (vnode.component = {
    props: {},
    attrs: {},
    setupState: null,
    ctx: null,
    update: null,
    isMounted: false,
    subTree: null,
    next: null, // 组件更新时,把新vnode暂放在这里
  });
    // 拆解参数 props和attr
  updateProps(instance, vnode);
      // 源码:instance.setupState = proxyRefs(setupResult) 取值ref时可以不用.value去取
    // 如果有setup,返回的值要作为ctx的参数
  instance.setupState = Component.setup?.(instance.props, {
    attrs: instance.attrs,
  });
    // 真实源码中 代理了props,找不到props的属性再去setupState里找
  instance.ctx = {
    ...instance.props,
    ...instance.setupState,
  };
    // 生成子节点
    const subTree = (instance.subTree = normalizeVNode(
          Component.render(instance.ctx)
        ));
    // 获取节点的属性 instance.attr
    fallThrough(instance, subTree);
    // mount 子节点
    patch(null, subTree, container, anchor);
  }

更新实现

实现例子2,会响应式更新

effect包裹render函数,render函数里用到了响应式数据,所以当响应式数据变化时,effect触发执行里面的render函数,从而更新视图

 instance.update = effect(
    () => {
      if (!instance.isMounted) {
        // mount 初始化渲染,和更新区别是更新是有旧节点去patch的
        const subTree = (instance.subTree = normalizeVNode(
          Component.render(instance.ctx)
        ));
          // 如果有属性attr,要作为节点属性传过去。
        fallThrough(instance, subTree);
          // 相对于mount
        patch(null, subTree, container, anchor);
        instance.isMounted = true;
        // 生成渲染后,把el赋值给vnode。
        vnode.el = subTree.el;
      } else {
        // update
​
        const prev = instance.subTree;
        const subTree = (instance.subTree = normalizeVNode(
          Component.render(instance.ctx)
        ));
​
        fallThrough(instance, subTree);
​
        patch(prev, subTree, container, anchor);
        vnode.el = subTree.el;
      }
    }
  );

这个就是主动更新机制的实现,还有被动更新

被动更新

当父节点有属性变动时,重新渲染,导致子节点也重新渲染

const Child = {
  props: ['foo'],
  render(ctx) {
    return h('div', { class: 'a', id: ctx.bar }, ctx.foo);
  },
};
​
const Parent = {
  setup() {
    const vnodeProps = reactive({
      foo: 'foo',
      bar: 'bar',
    });
    return { vnodeProps };
  },
  render(ctx) {
    return h(Child, ctx.vnodeProps);
  },
};
​
render(h(Parent), root);

重渲染,走patch,走processComponent

// vnode用到component属性
h(type, props = null, children = null) {
     return {
         。。。
    component: null, // 组件的instance
  };
}
// 渲染更新Component组件
function processComponent(n1, n2, container, anchor) {
  if (n1 == null) {
    mountComponent(n2, container, anchor, patch);
  } else {
      //走这里
    updateComponent(n1, n2);
  }
}
// 更新组件
function updateComponent(n1, n2) {
    //更新新节点,把旧节点的方法给新节点用
  n2.component = n1.component;
    // 被动更新标志
  n2.component.next = n2;
  n2.component.update();
}
// n2.component.update();
 instance.update = effect(
    () => {
      if (!instance.isMounted) {
。。。
      } else {
        // update
​
        // instance.next存在,代表是被动更新。否则是主动更新
          if (instance.next) {
              // 重新取值最新的父组件传过来的 props
              // 拿到新节点,这里九路十八弯,要跟上车
              vnode = instance.next;
              instance.next = null;
              updateProps(instance, vnode);
              instance.ctx = {
                ...instance.props,
                ...instance.setupState,
              };
          }
         //同上的渲染代码
         const prev = instance.subTree;
        const subTree = (instance.subTree = normalizeVNode(
          Component.render(instance.ctx)
        ));
        fallThrough(instance, subTree);
        patch(prev, subTree, container, anchor);
        vnode.el = subTree.el;
  })
          

源码是有shouldUpdateComponent判断的,vue3是主动提供,不像react那样子,当父组件更新时,子组件会判断传入的参数是否有变化,如果有变化再更新,没有就不继续更新了

createApp

createApp({
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    add() {
      this.count++;
    },
  },
  render(ctx) {
    return [
      h('div', null, ctx.count),
      h(
        'button',
        {
          onClick: ctx.add,
        },
        'add'
      ),
    ];
  },
}).mount('#app');

实现

这种写法是为了和vue2类似

export function createApp(rootComponent) {
  const app = {
    mount(rootContainer) {
    // rootContainer : #app
      if (typeof rootContainer === 'string') {
        rootContainer = document.querySelector(rootContainer);
      }

      render(h(rootComponent), rootContainer);
    },
  };
  return app;
}