Skip to content
  • props 是一个包装对象,而 props 中的值是父组件传递给子组件的,如果这个值是原始值,则两者没有关系

❓当父组件传递给子组件的 props 是一个 ref,那么 ref 发生变化时候,不是会同时出发父子组件的渲染吗?子组件会有两次渲染:一次是父组件变化引起的子组件的被动更新,一次是子组件的主动更新

看 mini-vue 和 vue3 源码等收获:

  • ref 和 reactive 等函数会处理嵌套包裹的情况,比如说 ref(ref()),并不会出现嵌套包裹;
  • props 的传递其实是传递的原始值,父组件和子组件的 props 并不会是同一个值,这是通过 renderContext 处理的;
    • 这也就解释了上边的问题。

父子组件渲染的流程:

父组件

javascript
export default {
  name: "App",
  setup() {
    const msg = ref("123");
    window.msg = msg

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

    return { msg, changeChildProps };
  },

  render() {
    return h("div", {}, [
      h("div", {}, "你好"),
      h(
        "button",
        {
          onClick: this.changeChildProps,
        },
        "change child props"
      ),
      h(Child, {
        msg: this.msg,
      }),
    ]);
  },
};

子组件

javascript
export default {
  name: "Child",
  setup(props, { emit }) {},
  render(proxy) {
    return h("div", {}, [h("div", {}, "child" + this.$props.msg)]);
  },
};

项目初始化

javascript
const rootContainer = document.querySelector("#app");
createApp(App).mount(rootContainer);

流程:

一些需要理清的概念:

  • 组件
    • 是对虚拟 dom 的一层封装,最后执行渲染的是虚拟 dom
  • 虚拟 dom
  • 组件实例
  • 渲染上下文
  • 渲染函数和模板

几个函数的作用:

  • render
  • patch
  • mountComponent
  • patchComponent

从一个父子组件的渲染来理解 vue 组件实现原理

父组件(根组件)

vue
<script>
import { ref } from 'vue'
import Comp from './Comp.vue';
export default {
  components: { Comp },
  props: {
    foo: String
  },
  setup() {
    const msg = ref('Hello World!')
    return { msg };
  }
}

</script>

<template>
  <h1>{{ msg }}</h1>
  <Comp :bar="msg"></Comp>
</template>

父组件编译后:可以看到返回值是一个 render 函数,这个 render函数接受几个组件实例参数,返回一组虚拟节点

javascript
import { ref } from 'vue'
import Comp from './Comp.vue';
const __sfc__ = {
  components: { Comp },
  props: {
    foo: String
  },
  setup() {
    const msg = ref('Hello World!')
    return { msg };
  }
}

import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, resolveComponent as _resolveComponent, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

function render(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_Comp = _resolveComponent("Comp")

  return (_openBlock(), _createElementBlock(_Fragment, null, [
    _createElementVNode("h1", null, _toDisplayString($setup.msg), 1 /* TEXT */),
    _createVNode(_component_Comp, { bar: $setup.msg }, null, 8 /* PROPS */, ["bar"])
  ], 64 /* STABLE_FRAGMENT */))
}
__sfc__.render = render
__sfc__.__file = "src/App.vue"
export default __sfc__

我们从 createApp 开始看:

javascript
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

createApp函数返回的是一个 app 实例对象,该对象内部有 mount 函数。

debug 下,可以看出这个 app 对象:

我们来看看 mount 函数中关键步骤为:

javascript
// packages/runtime-core/src/apiCreateApp.ts
function mount(rootContainer, isHydrate, isSVG) {
  if (!isMounted) {
    // ... 省略部分不重要的代码
    // 1. 创建根组件的 vnode
    const vnode = createVNode(
      rootComponent,
      rootProps
    )
    
    // 2. 渲染根组件
    render(vnode, rootContainer, isSVG)
    isMounted = true
  }
}

这里创建的 vnode 为:

这个 vnode 中 type 就是组件对象,这里 App 根组件的组件对象就是一个渲染函数。

创建完 App 组件的 vnode 后,就将他传给render 函数来进行渲染,这里也是 vue 组件渲染的入口。

render 函数内部会调用patch函数,patch 函数适用于打补丁的操作,此时我们是第一次渲染,所以可以理解为打全量补丁。

patch函数的核心逻辑是判断传入的新旧节点的类型,针对不同的 vnode 类型,传入不同的处理函数。我们这里是组件类型,所以会进入processComponent函数。

javascript
// packages/runtime-core/src/renderer.ts
function processComponent(n1, n2, container, parentComponent) {
  // 如果 n1 没有值的话,那么就是 mount
  if (!n1) {
    // 初始化 component
    mountComponent(n2, container, parentComponent);
  } else {
    updateComponent(n1, n2, container);
  }
}

由于我们是初次渲染,所以会进入mountComponent函数。

javascript
// packages/runtime-core/src/renderer.ts
function mountComponent(initialVNode, container, parentComponent) {
  // 1. 先创建一个 component instance
  const instance = (initialVNode.component = createComponentInstance(
    initialVNode,
    parentComponent
  ));
  
  // 2. 初始化 instance 上的 props, slots, 执行组件的 setup 函数...
  setupComponent(instance);

  // 3. 设置并运行带副作用的渲染函数
  setupRenderEffect(instance, initialVNode, container);
}

从一个父子组件的渲染来看

vue 中对于嵌套的父子组件的渲染,可以理解为剥洋葱的过程,从外层组件(父组件)开始,一层层的剥开,看懂这个过程也可以更好的理解父子组件的生命周期等内容。

第一趟:App 组件

app 组件的 vnode 中没有太多内容,主要是 type 属性存储组件对象,该组件对象为一个渲染函数。

这个 vnode 经过 patchprocessComponent 等函数进入mountComponent 中,最终在 setupComponentEffect 函数中通过renderComponentRoot转换为 vnode,交给 render 函数进行第二趟处理,此时返回的这个 vnode 为

javascript
{
  type:'div',
  // children: [[...],[...]],
  // ctx: '...',
  props: null,
  // ......
}

第二趟:mountChildren

此时 vnode 的 type 为 div 类型,所以在 patch 函数中会交由processElement处理,

由于 vnode.children 为一个数组,最终会由 mountChildren 函数进行处理:

mountChildren函数中,会遍历调用 patch 函数进行处理

第三趟:子组件渲染

总结

  • vue 项目渲染的过程,可以理解为剥洋葱的过程,从外层组件(跟组件)开始,一层层的剥开,看懂这个过程也可以更好的理解父子组件的生命周期等内容;
  • 我们为组件书写的 props、attrs、emit 等内容,最终是由渲染函数 render 转换为 vnode,最终交由渲染器完成到真实 dom 的转换;
  • 如果我们使用 template 编写的 sfc,vue 转换后的渲染函数 render 接受一些上下文参数:)_ctx, _cache, $props, $setup, $data, $options)
  • 响应式数据、 props 等最终再真实 dom 上是不会体现的,而是当他们发生变化时,重新触发 setupRenderEffect函数执行,重新渲染;