组件
- 组件是一个抽象的概念,它是对一棵
DOM
树的抽象 - 可以描述组件信息的
JavaScript
对象 - 从表现上来看
- 组件的模板决定了组件生成的
DOM
标签 - 在
Vue.js
内部,一个组件想要真正的渲染生成DOM
- 组件的模板决定了组件生成的
应用程序初始化
- 整个组件树是由根组件开始渲染的
- 为了找到根组件的渲染入口,从应用程序的初始化过程开始分析
- 对比
vue2.0
、vue3.0
入口
// 在 Vue.js 2.x 中,初始化一个应用的方式如下
import Vue from 'vue'
import App from './App'
const app = new Vue({
render: h => h(App)
})
app.$mount('#app')
// 在 Vue.js 3.0 中,初始化一个应用的方式如下
import { createApp } from 'vue'
import App from './app'
const app = createApp(App)
app.mount('#app')
- 在
Vue.js 3.0
中导入了一个createApp
,这是个入口函数,它是Vue.js
对外暴露的一个函数
createApp内部实现
const createApp = ((...args) => {
// 创建 app 对象
const app = ensureRenderer().createApp(...args)
const { mount } = app
// 重写 mount 方法
app.mount = (containerOrSelector) => {
// ...
}
return app
})
-
createApp
主要做了两件事情- 1)创建
app
对象 - 2)重写
app.mount
方法
- 1)创建
创建app对象
-
ensureRenderer().createApp()
来创建app
对象 - 实现了跨平台渲染
const app = ensureRenderer().createApp(...args)
-
ensureRenderer()
用来创建一个渲染器对象
ensureRenderer 内部实现
// 渲染相关的一些配置,比如更新属性的方法,操作 DOM 的方法
const rendererOptions = {
patchProp,
...nodeOps
}
let renderer
// 延时创建渲染器,当用户只依赖响应式包的时候,可以通过 tree-shaking 移除核心渲染逻辑相关的代码
function ensureRenderer() {
return renderer || (renderer = createRenderer(rendererOptions))
}
function createRenderer(options) {
return baseCreateRenderer(options)
}
function baseCreateRenderer(options) {
function render(vnode, container) {
// 组件渲染的核心逻辑
}
return {
render,
createApp: createAppAPI(render)
}
}
function createAppAPI(render) {
// createApp createApp 方法接受的两个参数:根组件的对象和 prop
return function createApp(rootComponent, rootProps = null) {
const app = {
_component: rootComponent,
_props: rootProps,
mount(rootContainer) {
// 创建根组件的 vnode
const vnode = createVNode(rootComponent, rootProps)
// 利用渲染器渲染 vnode
render(vnode, rootContainer)
app._container = rootContainer
return vnode.component.proxy
}
}
return app
}
}
- 首先用
ensureRenderer()
来延时创建渲染器- 好处是当用户只依赖响应式包的时候,就不会创建渲染器
- 可以通过
tree-shaking
的方式移除核心渲染逻辑相关的代码
- 通过
createRenderer
创建一个渲染器- 这个渲染器内部会有一个
createApp
方法- 它是执行
createAppAPI
方法返回的函数 - 接受了
rootComponent
和rootProps
两个参数
- 它是执行
- 我们在应用层面执行
createApp(App)
方法时:- 会把
App
组件对象作为根组件传递给rootComponent
。 - 这样,
createApp
内部就创建了一个app
对象 - 它会提供
mount
方法,这个方法是用来挂载组件的。
- 会把
- 这个渲染器内部会有一个
值得注意的是
-
app
对象创建过程中,Vue.js
利用闭包和函数柯里化的技巧,很好地实现了参数保留
重写app.mount方法
为什么重写?
-
createApp
返回的app
对象已经拥有了mount
方法了,为什么还有在入口重写?- 为了支持跨平台渲染
-
createApp
函数内部的app.mount
方法是一个标准的可跨平台的组件渲染流程:
mount(rootContainer) {
// 创建根组件的 vnode
const vnode = createVNode(rootComponent, rootProps)
// 利用渲染器渲染 vnode
render(vnode, rootContainer)
app._container = rootContainer
return vnode.component.proxy
}
- 主要流程是,先创建
vnode
,再渲染vnode
- 参数
rootContainer
根据平台不同而不同, - 这里面的代码不应该包含任何特定平台相关的逻辑,因此我们需要在外部重写这个方法
app.mount 重写都做了哪些事情?
app.mount = (containerOrSelector) => {
// 标准化容器
const container = normalizeContainer(containerOrSelector)
if (!container)
return
const component = app._component
// 如组件对象没有定义 render 函数和 template 模板,则取容器的 innerHTML 作为组件模板内容
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
// 挂载前清空容器内容
container.innerHTML = ''
// 真正的挂载
return mount(container)
}
- 首先是通过
normalizeContainer
标准化容器(这里可以传字符串选择器或者DOM
对象,但如果是字符串选择器,就需要把它转成DOM
对象,作为最终挂载的容器) - 然后做一个
if
判断,如果组件对象没有定义render
函数和template
模板,则取容器的innerHTML
作为组件模板内容 - 接着在挂载前清空容器内容,最终再调用
app.mount
的方法走标准的组件渲染流程
优势
- 跨平台实现
- 兼容
vue2.0
写法 -
app.mount
既可以传dom
,又可以传字符串选择器
核心渲染流程:创建 vnode 和渲染 vnode
创建 vnode
- 1、
vnode
本质上是用来描述DOM
的JavaScript
对象
它在
Vue.js
中可以描述不同类型的节点,比如普通元素节点、组件节点等
vnode如何描述
// vnode 这样表示
type
属性表示DOM
的标签类型props
属性表示DOM
的一些附加信息,比如style
、class
等children
属性表示DOM
的子节点,它也可以是一个vnode
数组,只不过vnode
可以用字符串表示简单的文本2、
vnode
除了用于描述一个真实的DOM
,也可以用来描述组件
vnode
其实是对抽象事物的描述
// vnode 这样表示
const CustomComponent = {
// 在这里定义组件对象
}
const vnode = {
type: CustomComponent,
props: {
msg: 'test'
}
}
- 3、其他的,还有纯文本
vnode
,注释vnode
- 4、
Vue.js 3.0
中,vnode
的type
,做了更详尽的分类,包括Suspense
、Teleport
等,且把vnode
的类型信息做了编码,以便在后面的patch
阶段,可以根据不同的类型执行相应的处理逻辑
vode优势
- 抽象
- 跨平台
- 但是,和手动修改
dom
对比,并不一定有优势
如何创建vnode
-
app.mount
函数的实现,内部是通过createVNode
函数创建了根组件的vnode
const vnode = createVNode(rootComponent, rootProps)
createVNode 函数的大致实现
function createVNode(type, props = null,children = null) {
if (props) {
// 处理 props 相关逻辑,标准化 class 和 style
}
// 对 vnode 类型信息编码
const shapeFlag = isString(type)
? 1 /* ELEMENT */
: isSuspense(type)
? 128 /* SUSPENSE */
: isTeleport(type)
? 64 /* TELEPORT */
: isObject(type)
? 4 /* STATEFUL_COMPONENT */
: isFunction(type)
? 2 /* FUNCTIONAL_COMPONENT */
: 0
const vnode = {
type,
props,
shapeFlag,
// 一些其他属性
}
// 标准化子节点,把不同数据类型的 children 转成数组或者文本类型
normalizeChildren(vnode, children)
return vnode
}
- 对
props
做标准化处理 - 对
vnode
的类型信息编码 - 创建
vnode
对象 - 标准化子节点
children
渲染 vnode
render(vnode, rootContainer)
const render = (vnode, container) => {
if (vnode == null) {
// 销毁组件
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
// 创建或者更新组件
patch(container._vnode || null, vnode, container)
}
// 缓存 vnode 节点,表示已经渲染
container._vnode = vnode
}
- 如果它的第一个参数
vnode
为空,则执行销毁组件的逻辑 - 否则执行创建或者更新组件的逻辑
patch函数
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false) => {
// 如果存在新旧节点, 且新旧节点类型不同,则销毁旧节点
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
const { type, shapeFlag } = n2
switch (type) {
case Text:
// 处理文本节点
break
case Comment:
// 处理注释节点
break
case Static:
// 处理静态节点
break
case Fragment:
// 处理 Fragment 元素
break
default:
if (shapeFlag & 1 /* ELEMENT */) {
// 处理普通 DOM 元素
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else if (shapeFlag & 6 /* COMPONENT */) {
// 处理组件
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else if (shapeFlag & 64 /* TELEPORT */) {
// 处理 TELEPORT
}
else if (shapeFlag & 128 /* SUSPENSE */) {
// 处理 SUSPENSE
}
}
}
- 这个函数有两个功能:
- 一个是根据
vnode
挂载DOM
- 一个是根据新旧
vnode
更新DOM
。对于初次渲染
- 一个是根据
-
patch
函数入参- 第一个参数
n1
表示旧的vnode
,当n1
为null
的时候,表示是一次挂载的过程; - 第二个参数
n2
表示新的vnode
节点,后续会根据这个vnode
类型执行不同的处理逻辑; - 第三个参数
container
表示DOM
容器,也就是vnode
渲染生成DOM
后,会挂载到container
下面。
- 第一个参数
渲染节点
- 对组件的处理
- 对普通
DOM
元素的处理
对组件的处理
processComponent函数实现
- 用来处理组件
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
if (n1 == null) {
// 挂载组件
mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else {
// 更新组件
updateComponent(n1, n2, parentComponent, optimized)
}
}
- 如果
n1
为null
,则执行挂载组件的逻辑 - 否则执行更新组件的逻辑
mountComponent挂载组件的实现
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
// 创建组件实例
const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense))
// 设置组件实例
setupComponent(instance)
// 设置并运行带副作用的渲染函数
setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized)
}
主要做三件事情
- 1、 创建组件实例
-
Vue.js 3.0
虽然不像Vue.js 2.x
那样通过类的方式去实例化组件,但内部也通过对象的方式去创建了当前渲染的组件实例
-
- 2、 设置组件实例
-
instance
保留了很多组件相关的数据,维护了组件的上下文,包括对props
、插槽,以及其他实例的属性的初始化处理
-
- 3、 设置并运行带副作用的渲染函数(
setupRenderEffect
)
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
// 创建响应式的副作用渲染函数
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {
// 渲染组件生成子树 vnode
const subTree = (instance.subTree = renderComponentRoot(instance))
// 把子树 vnode 挂载到 container 中
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
// 保留渲染生成的子树根 DOM 节点
initialVNode.el = subTree.el
instance.isMounted = true
}
else {
// 更新组件
}
}, prodEffectOptions)
}
- 该函数利用响应式库的
effect
函数创建了一个副作用渲染函数componentEffect
副作用
当组件的数据发生变化时,effect
函数包裹的内部渲染函数componentEffect
会重新执行一遍,从而达到重新渲染组件的目的
- 渲染函数内部也会判断这是一次初始渲染还是组件更新,在初始渲染流程中
初始渲染主要做两件事情:
- 1、 渲染组件生成
subTree
注意,不要弄混subTree
(执行renderComponentRoot
生成的子树vnode
)和initialVNode
(组件vnode
)- 每个组件都有
render
函数,template
也会编译成render
函数 -
renderComponentRoot
函数就是去执行render
函数创建整个组件树内部的vnode
, - 把这个
vnode
再经过内部一层标准化,就得到了该函数的返回结果:subTree
(子树vnode
)
- 每个组件都有
- 2、把
subTree
挂载到container
中- 继续调用
patch
函数把子树vnode
挂载到container
中 - 继续对这个子树
vnode
类型进行判断,此时子树vnode
为普通元素vnode
- 继续调用
对普通 DOM 元素的处理
processElement函数
- 用来处理普通
DOM
元素
const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
isSVG = isSVG || n2.type === 'svg'
if (n1 == null) {
//挂载元素节点
mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else {
//更新元素节点
patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
}
}
- 如果
n1
为null
,走挂载元素节点的逻辑 - 否则走更新元素节点逻辑
mountElement 函数
const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
let el
const { type, props, shapeFlag } = vnode
// 创建 DOM 元素节点
el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is)
if (props) {
// 处理 props,比如 class、style、event 等属性
for (const key in props) {
if (!isReservedProp(key)) {
hostPatchProp(el, key, null, props[key], isSVG)
}
}
}
if (shapeFlag & 8 /* TEXT_CHILDREN */) {
// 处理子节点是纯文本的情况
hostSetElementText(el, vnode.children)
}
else if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
// 处理子节点是数组的情况
mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', optimized || !!vnode.dynamicChildren)
}
// 把创建的 DOM 元素节点挂载到 container 上
hostInsert(el, container, anchor)
}
- 主要做四件事
- 1、 创建
DOM
元素节点
通过hostCreateElement
方法创建,这是一个平台相关的方法,在web
端实现:
// 调用了底层的 DOM API document.createElement 创建元素
function createElement(tag, isSVG, is) {
isSVG ? document.createElementNS(svgNS, tag)
: document.createElement(tag, is ? { is } : undefined)
}
- 2、 处理
props
给这个DOM
节点添加相关的class
、style
、event
等属性,并做相关的处理 - 3、 处理
children
- 子节点是纯文本,则执行
hostSetElementText
方法,它在Web
环境下通过设置DOM
元素的textContent
属性设置文本:
- 子节点是纯文本,则执行
function setElementText(el, text) {
el.textContent = text
}
* 如果子节点是数组,则执行`mountChildren`方法
const mountChildren = (children, container, anchor, parentComponent, parentSuspense, isSVG, optimized, start = 0) => {
for (let i = start; i < children.length; i++) {
// 预处理 child
const child = (children[i] = optimized
? cloneIfMounted(children[i])
: normalizeVNode(children[i]))
// 递归 patch 挂载 child
patch(null, child, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
}
遍历
children
获取到每一个child vnode
mountChildren
函数的第二个参数是container
,传入的是mountElement
时创建的DOM
节点,很好的建立了父子关系通过递归
patch
这种深度优先遍历树的方式,我们就可以构造完整的DOM
树,完成组件的渲染。4、 挂载
DOM
元素到container
上
调用hostInsert
方法
function insert(child, parent, anchor) {
if (anchor) {
parent.insertBefore(child, anchor)
}
else {
parent.appendChild(child)
}
}
嵌套组件的处理
-
mountChildren
的时候递归执行的是patch
函数,而不是mountElement
函数,这是因为子节点可能有其他类型的vnode
,比如组件vnode