持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第29天,点击查看活动详情
🎈大家好,我是
橙橙子,新人初来乍到,请多多关照~📝小小的前端一枚,分享一些日常的学习和项目实战总结~
😜如果本文对你有帮助的话,帮忙点点赞呀!ღ( ´・ᴗ・` )比心~
前言
在 Vue 中,核心除了响应式原理之外,视图渲染也很重要。每次更新数据,都会掉视图渲染逻辑。
本文我们会介绍从挂载组件开始,Vue 是如何构建 VNode,又是如何将 VNode 转为真实节点挂载到页面的。我们一起看看吧~
挂载组件($mount)
Vue 是一个构造函数,通过 new 关键字进行实例化。
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
在实例化时,会调用 _init 进行初始化。
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// ...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
_init 内会调用 $mount 来挂载组件,而 $mount 方法实际调用是 mountComponent。
mountComponent 除了调用一些生命周期的钩子函数外,最主要是 updateComponent,它是负责渲染视图的核心方法
vm._update(vm._render(), hydrating)
vm._render 创建并返回 VNode,vm._update 接受 VNode 将其转为真实节点。
updateComponent 会被传入 渲染Watcher,每当数据变化触发 Watcher 更新就会执行该函数,重新渲染视图。updateComponent 在传入 渲染Watcher 后会被执行一次进行初始化页面渲染。
构建VNode(_render)
首先是 _render 方法,它用来构建组件的 VNode。
Vue.prototype._render = function () {
const { render, _parentVnode } = vm.$options
vnode = render.call(vm._renderProxy, vm.$createElement)
return vnode
}
_render 内部会执行 render 方法并返回构建好的 VNode。render 一般是模板编译后生成的方法,也有可能是用户自定义。
// src/core/instance/render.js
export function initRender (vm) {
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}
initRender 在初始化就会执行为实例上绑定两个方法,分别是 vm._c 和 vm.$createElement。它们两者都是调用 createElement 方法,它是创建 VNode 的核心方法,最后一个参数用于区别是否为用户自定义。
vm._c 应用场景是在编译生成的 render 函数中调用,vm.$createElement 则用于用户自定义 render 函数的场景。就像上面 render 在调用时会传入参数 vm.$createElement,我们在自定义 render 函数接收到的参数就是它。
createElement
createElement 方法实际上是对 _createElement 方法的封装,它允许传入的参数更加灵活。
_createElement 参数中会接收 children,它表示当前 VNode 的子节点,因为它是任意类型的,所以接下来需要将其规范为标准的 VNode 数组;
simpleNormalizeChildren 和 normalizeChildren 均用于规范化 children。由 normalizationType 判断 render 函数是编译生成的还是用户自定义的。
simpleNormalizeChildren 方法调用场景是 render 函数当函数是编译生成的。normalizeChildren 方法的调用场景主要是 render 函数是用户手写的。
经过对 children 的规范化,children 变成了一个类型为 VNode 的数组。之后就是创建 VNode 的逻辑。
如果 tag 是 string 类型,则接着判断如果是内置的一些节点,创建一个普通 VNode;如果是为已注册的组件名,则通过 createComponent 创建一个组件类型的 VNode;否则创建一个未知的标签的 VNode。
如果 tag 不是 string 类型,那就是 Component 类型, 则直接调用 createComponent 创建一个组件类型的 VNode 节点。
最后 _createElement 会返回一个 VNode,也就是调用 vm._render 时创建得到的VNode。之后 VNode 会传递给 vm._update 函数,用于生成真实dom。
生成真实dom(_update)
_update 里最核心的方法就是 vm.__patch__ 方法,不同平台的 __patch__ 方法的定义会稍有不同,在 web 平台中它是这样定义的:
可以看到 __patch__ 实际调用的是 patch 方法。
而 patch 方法是由 createPatchFunction 方法创建返回出来的函数。
这里有两个比较重要的对象 nodeOps 和 modules。nodeOps 是封装的原生dom操作方法,在生成真实节点树的过程中,dom相关操作都是调用 nodeOps 内的方法。
modules 是待执行的钩子函数。在进入函数时,会将不同模块的钩子函数分类放置到 cbs 中,其中包括自定义指令钩子函数,ref 钩子函数。在 patch 阶段,会根据操作节点的行为取出对应类型进行调用。
patch
在首次渲染时,vm.$el 对应的是根节点 dom 对象,也就是我们熟知的 id 为 app 的 div。它作为 oldVNode 参数传入 patch:
通过检查属性 nodeType(真实节点才有的属性), 判断 oldVnode 是否为真实节点。
const isRealElement = isDef(oldVnode.nodeType)
if (isRealElement) {
// ...
oldVnode = emptyNodeAt(oldVnode)
}
很明显第一次的 isRealElement 是为 true,因此会调用 emptyNodeAt 将其转为 VNode:
function emptyNodeAt (elm) {
return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
接着会调用 createElm 方法,它就是将 VNode 转为真实dom 的核心方法:
一开始会调用 createComponent 尝试创建组件类型的节点,如果成功会返回 true。在创建过程中也会调用 $mount 进行组件范围内的挂载,所以走的还是 patch 这套流程。
如果没有完成创建,代表该 VNode 对应的是真实节点,往下继续创建真实节点的逻辑。
根据 tag 创建对应类型真实节点,赋值给 vnode.elm,它作为父节点容器,创建的子节点会被放到里面。
然后调用 createChildren 创建子节点:
内部进行遍历子节点数组,再次调用 createElm 创建节点,而上面创建的 vnode.elm 作为父节点传入。如此循环,直到没有子节点,就会创建文本节点插入到 vnode.elm 中。
执行完成后出来,会调用 invokeCreateHooks,它负责执行 dom 操作时的 create 钩子函数,同时将 VNode 加入到 insertedVnodeQueue 中:
最后一步就是调用 insert 方法将节点插入到父节点:
可以看到 Vue 是通过递归调用 createElm 来创建节点树的。同时也说明最深的子节点会先调用 insert 插入节点。所以整个节点树的插入顺序是“先子后父”。插入节点方法就是原生dom的方法 insertBefore 和 appendChild。
createElm 流程走完后,构建完成的节点树已经插入到页面上了。其实 Vue 在初始化渲染页面时,并不是把原来的根节点 app 给真正替换掉,而是在其后面插入一个新的节点,接着再把旧节点给移除掉。
所以在 createElm 之后会调用 removeVnodes 来移除旧节点,它里面同样是调用的原生dom方法 removeChild。
在 patch 的最后就是调用 invokeInsertHook 方法,触发节点插入的钩子函数。
至此整个页面渲染的流程完毕~
总结
初始化调用 $mount 挂载组件。
_render 开始构建 VNode,核心方法为 createElement,一般会创建普通的 VNode ,遇到组件就创建组件类型的 VNode,否则就是未知标签的 VNode,构建完成传递给 _update。
patch 阶段根据 VNode 创建真实节点树,核心方法为 createElm,首先遇到组件类型的 VNode,内部会执行 $mount,再走一遍相同的流程。普通节点类型则创建一个真实节点,如果它有子节点开始递归调用 createElm,使用 insert 插入子节点,直到没有子节点就填充内容节点。最后递归完成后,同样也是使用 insert 将整个节点树插入到页面中,再将旧的根节点移除。