- props 是一个包装对象,而 props 中的值是父组件传递给子组件的,如果这个值是原始值,则两者没有关系
❓当父组件传递给子组件的 props 是一个 ref,那么 ref 发生变化时候,不是会同时出发父子组件的渲染吗?子组件会有两次渲染:一次是父组件变化引起的子组件的被动更新,一次是子组件的主动更新
看 mini-vue 和 vue3 源码等收获:
- ref 和 reactive 等函数会处理嵌套包裹的情况,比如说 ref(ref()),并不会出现嵌套包裹;
- props 的传递其实是传递的原始值,父组件和子组件的 props 并不会是同一个值,这是通过 renderContext 处理的;
- 这也就解释了上边的问题。
父子组件渲染的流程:
父组件
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,
}),
]);
},
};
子组件
export default {
name: "Child",
setup(props, { emit }) {},
render(proxy) {
return h("div", {}, [h("div", {}, "child" + this.$props.msg)]);
},
};
项目初始化
const rootContainer = document.querySelector("#app");
createApp(App).mount(rootContainer);
流程:
一些需要理清的概念:
- 组件
- 是对虚拟 dom 的一层封装,最后执行渲染的是虚拟 dom
- 虚拟 dom
- 组件实例
- 渲染上下文
- 渲染函数和模板
几个函数的作用:
- render
- patch
- mountComponent
- patchComponent
从一个父子组件的渲染来理解 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函数接受几个组件实例参数,返回一组虚拟节点
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
开始看:
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
createApp
函数返回的是一个 app 实例对象,该对象内部有 mount 函数。
debug 下,可以看出这个 app 对象:
我们来看看 mount 函数中关键步骤为:
// 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
函数。
// 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
函数。
// 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 经过 patch
、processComponent
等函数进入mountComponent
中,最终在 setupComponentEffect
函数中通过renderComponentRoot
转换为 vnode
,交给 render
函数进行第二趟处理,此时返回的这个 vnode 为
{
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
函数执行,重新渲染;