首页IT科技vue 页面渲染完成(Vue 页面渲染的流程)

vue 页面渲染完成(Vue 页面渲染的流程)

时间2025-05-05 03:30:28分类IT科技浏览3237
导读:前言 在 Vue 核心中除了响应式原理外,视图渲染也是重中之重。我们都知道每次更新数据,都会走视图渲染的逻辑,而这当中牵扯的逻辑也是十分繁琐。...

前言

在 Vue 核心中除了响应式原理外           ,视图渲染也是重中之重           。我们都知道每次更新数据                ,都会走视图渲染的逻辑      ,而这当中牵扯的逻辑也是十分繁琐                。

本文主要解析的是初始化视图渲染流程     ,你将会了解到从挂载组件开始                ,Vue 是如何构建 VNode           ,又是如何将 VNode 转为真实节点并挂载到页面      。

挂载组件($mount)

Vue 是一个构造函数     ,通过 new 关键字进行实例化     。

// src/core/instance/index.js 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 进行初始化                。

// src/core/instance/init.js Vue.prototype._init = function (options?: Object) { const vm: Component = this // ... if (vm.$options.el) { vm.$mount(vm.$options.el) } }

_init 内会调用 $mount 来挂载组件           ,而 $mount 方法实际调用的是 mountComponent           。

// src/core/instance/lifecycle.js export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el // ... callHook(vm, beforeMount) let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== production && config.performance && mark) { // ... } else { updateComponent = () => { vm._update(vm._render(), hydrating) // 渲染页面函数 } } // we set this to vm._watcher inside the watchers constructor // since the watchers initial patch may call $forceUpdate (e.g. inside child // components mounted hook), which relies on vm._watcher being already defined new Watcher(vm, updateComponent, noop, { // 渲染watcher before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, beforeUpdate) } } }, true /* isRenderWatcher */) hydrating = false // manually mounted instance, call mounted on self // mounted is called for render-created child components in its inserted hook if (vm.$vnode == null) { vm._isMounted = true callHook(vm, mounted) } return vm }

mountComponent 除了调用一些生命周期的钩子函数外,最主要是 updateComponent                ,它就是负责渲染视图的核心方法                ,其只有一行核心代码:

vm._update(vm._render(), hydrating)

vm._render 创建并返回 VNode,vm._update 接受 VNode 将其转为真实节点     。

updateComponent 会被传入 渲染Watcher           ,每当数据变化触发 Watcher 更新就会执行该函数                ,重新渲染视图                。updateComponent 在传入 渲染Watcher 后会被执行一次进行初始化页面渲染           。

所以我们着重分析的是 vm._render 和 vm._update 两个方法      ,这也是本文主要了解的原理——Vue 视图渲染流程。

构建VNode(_render)

首先是 _render 方法           ,它用来构建组件的 VNode                。

// src/core/instance/render.js 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

// src/core/vdom/create-elemenet.js export function createElement ( context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean ): VNode | Array<VNode> { if (Array.isArray(data) || isPrimitive(data)) { normalizationType = children children = data data = undefined } if (isTrue(alwaysNormalize)) { normalizationType = ALWAYS_NORMALIZE } return _createElement(context, tag, data, children, normalizationType) }

createElement 方法实际上是对 _createElement 方法的封装                ,它允许传入的参数更加灵活                。

export function _createElement ( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array<VNode> { if (isDef(data) && isDef(data.is)) { tag = data.is } if (!tag) { // in case of component :is set to falsy value return createEmptyVNode() } // support single function children as default scoped slot if (Array.isArray(children) && typeof children[0] === function ) { data = data || {} data.scopedSlots = { default: children[0] } children.length = 0 } if (normalizationType === ALWAYS_NORMALIZE) { children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) } let vnode, ns if (typeof tag === string) { let Ctor ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) if (config.isReservedTag(tag)) { // platform built-in elements vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if (isDef(Ctor = resolveAsset(context.$options, components, tag))) { // component vnode = createComponent(Ctor, data, context, children, tag) } else { // unknown or unlisted namespaced elements // check at runtime because it may get assigned a namespace when its // parent normalizes children vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { // direct component options / constructor vnode = createComponent(tag, data, context, children) } if (Array.isArray(vnode)) { return vnode } else if (isDef(vnode)) { if (isDef(ns)) applyNS(vnode, ns) if (isDef(data)) registerDeepBindings(data) return vnode } else { return createEmptyVNode() } }

_createElement 参数中会接收 children           ,它表示当前 VNode 的子节点,因为它是任意类型的                ,所以接下来需要将其规范为标准的 VNode 数组;

// 这里规范化 children if (normalizationType === ALWAYS_NORMALIZE) { children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) }

simpleNormalizeChildren 和 normalizeChildren 均用于规范化 children      。由 normalizationType 判断 render 函数是编译生成的还是用户自定义的     。

// 1. When the children contains components - because a functional component // may return an Array instead of a single root. In this case, just a simple // normalization is needed - if any child is an Array, we flatten the whole // thing with Array.prototype.concat. It is guaranteed to be only 1-level deep // because functional components already normalize their own children. export function simpleNormalizeChildren (children: any) { for (let i = 0; i < children.length; i++) { if (Array.isArray(children[i])) { return Array.prototype.concat.apply([], children) } } return children } // 2. When the children contains constructs that always generated nested Arrays, // e.g. <template>, <slot>, v-for, or when the children is provided by user // with hand-written render functions / JSX. In such cases a full normalization // is needed to cater to all possible types of children values. export function normalizeChildren (children: any): ?Array<VNode> { return isPrimitive(children) ? [createTextVNode(children)] : Array.isArray(children) ? normalizeArrayChildren(children) : undefined }

simpleNormalizeChildren 方法调用场景是 render 函数当函数是编译生成的                。normalizeChildren 方法的调用场景主要是 render 函数是用户手写的           。

经过对 children 的规范化                ,children 变成了一个类型为 VNode 的数组     。之后就是创建 VNode 的逻辑                。

// src/core/vdom/patch.js let vnode, ns if (typeof tag === string) { let Ctor ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) if (config.isReservedTag(tag)) { // platform built-in elements vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if (isDef(Ctor = resolveAsset(context.$options, components, tag))) { // component vnode = createComponent(Ctor, data, context, children, tag) } else { // unknown or unlisted namespaced elements // check at runtime because it may get assigned a namespace when its // parent normalizes children vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { // direct component options / constructor vnode = createComponent(tag, data, context, children) }

如果 tag 是 string 类型,则接着判断如果是内置的一些节点           ,创建一个普通 VNode;如果是为已注册的组件名                ,则通过 createComponent 创建一个组件类型的 VNode;否则创建一个未知的标签的 VNode           。

如果 tag 不是 string 类型      ,那就是 Component 类型, 则直接调用 createComponent 创建一个组件类型的 VNode 节点。

最后 _createElement 会返回一个 VNode           ,也就是调用 vm._render 时创建得到的VNode                。之后 VNode 会传递给 vm._update 函数                ,用于生成真实dom                。

生成真实dom(_update)

// src/core/instance/lifecycle.js Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode const prevActiveInstance = activeInstance activeInstance = vm vm._vnode = vnode // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. if (!prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) } activeInstance = prevActiveInstance // update __vue__ reference if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } // if parent is an HOC, update its $el as well if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } // updated hook is called by the scheduler to ensure that children are // updated in a parents updated hook. }

_update 里最核心的方法就是 vm.__patch__ 方法      ,不同平台的 __patch__ 方法的定义会稍有不同     ,在 web 平台中它是这样定义的:

// src/platforms/web/runtime/index.js import { patch } from ./patch // install platform patch function Vue.prototype.__patch__ = inBrowser ? patch : noop

可以看到 __patch__ 实际调用的是 patch 方法。

// src/platforms/web/runtime/patch.js import * as nodeOps from web/runtime/node-ops import { createPatchFunction } from core/vdom/patch import baseModules from core/vdom/modules/index import platformModules from web/runtime/modules/index // the directive module should be applied last, after all // built-in modules have been applied. const modules = platformModules.concat(baseModules) export const patch: Function = createPatchFunction({ nodeOps, modules })

而 patch 方法是由 createPatchFunction 方法创建返回出来的函数           。

// src/core/vdom/patch.js const hooks = [create, activate, update, remove, destroy] export function createPatchFunction (backend) { let i, j const cbs = {} const { modules, nodeOps } = backend for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = [] for (j = 0; j < modules.length; ++j) { if (isDef(modules[j][hooks[i]])) { cbs[hooks[i]].push(modules[j][hooks[i]]) } } } // ... return function patch (oldVnode, vnode, hydrating, removeOnly){} }

这里有两个比较重要的对象 nodeOps 和 modules                。nodeOps 是封装的原生dom操作方法                ,在生成真实节点树的过程中           ,dom相关操作都是调用 nodeOps 内的方法      。

modules 是待执行的钩子函数           。在进入函数时     ,会将不同模块的钩子函数分类放置到 cbs 中                ,其中包括自定义指令钩子函数           ,ref 钩子函数                。在 patch 阶段,会根据操作节点的行为取出对应类型进行调用      。

patch

// initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)

在首次渲染时                ,vm.$el 对应的是根节点 dom 对象                ,也就是我们熟知的 id 为 app 的 div     。它作为 oldVNode 参数传入 patch:

return function patch (oldVnode, vnode, hydrating, removeOnly) { if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { // empty mount (likely as component), create new root element isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } else { if (isRealElement) { // mounting to a real element // check if this is server-rendered content and if we can perform // a successful hydration. if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR) hydrating = true } if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { invokeInsertHook(vnode, insertedVnodeQueue, true) return oldVnode } else if (process.env.NODE_ENV !== production) { warn( The client-side rendered virtual DOM tree is not matching + server-rendered content. This is likely caused by incorrect + HTML markup, for example nesting block-level elements inside + <p>, or missing <tbody>. Bailing hydration and performing + full client-side render. ) } } // either not server-rendered, or hydration failed. // create an empty node and replace it oldVnode = emptyNodeAt(oldVnode) } // replacing existing element const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) // create new node createElm( vnode, insertedVnodeQueue, // extremely rare edge case: do not insert if old element is in a // leaving transition. Only happens when combining transition + // keep-alive + HOCs. (#4590) oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) // update parent placeholder node element, recursively if (isDef(vnode.parent)) { let ancestor = vnode.parent const patchable = isPatchable(vnode) while (ancestor) { for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](ancestor) } ancestor.elm = vnode.elm if (patchable) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, ancestor) } // #6513 // invoke insert hooks that may have been merged by create hooks. // e.g. for directives that uses the "inserted" hook. const insert = ancestor.data.hook.insert if (insert.merged) { // start at index 1 to avoid re-invoking component mounted hook for (let i = 1; i < insert.fns.length; i++) { insert.fns[i]() } } } else { registerRef(ancestor) } ancestor = ancestor.parent } } // destroy old node if (isDef(parentElm)) { removeVnodes([oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm }

通过检查属性 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 的核心方法:

function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { if (isDef(vnode.elm) && isDef(ownerArray)) { // This vnode was used in a previous render! // now its used as a new node, overwriting its elm would cause // potential patch errors down the road when its used as an insertion // reference node. Instead, we clone the node on-demand before creating // associated DOM element for it. vnode = ownerArray[index] = cloneVNode(vnode) } vnode.isRootInsert = !nested // for transition enter check if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } const data = vnode.data const children = vnode.children const tag = vnode.tag if (isDef(tag)) { vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode) setScope(vnode) /* istanbul ignore if */ if (__WEEX__) { // ... } else { createChildren(vnode, children, insertedVnodeQueue) if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } insert(parentElm, vnode.elm, refElm) } if (process.env.NODE_ENV !== production && data && data.pre) { creatingElmInVPre-- } } else if (isTrue(vnode.isComment)) { vnode.elm = nodeOps.createComment(vnode.text) insert(parentElm, vnode.elm, refElm) } else { vnode.elm = nodeOps.createTextNode(vnode.text) insert(parentElm, vnode.elm, refElm) } }

一开始会调用 createComponent 尝试创建组件类型的节点      ,如果成功会返回 true           。在创建过程中也会调用 $mount 进行组件范围内的挂载           ,所以走的还是 patch 这套流程     。

if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return }

如果没有完成创建                ,代表该 VNode 对应的是真实节点      ,往下继续创建真实节点的逻辑                。

vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode)

根据 tag 创建对应类型真实节点     ,赋值给 vnode.elm                ,它作为父节点容器           ,创建的子节点会被放到里面           。

然后调用 createChildren 创建子节点:

function createChildren (vnode, children, insertedVnodeQueue) { if (Array.isArray(children)) { if (process.env.NODE_ENV !== production) { checkDuplicateKeys(children) } for (let i = 0; i < children.length; ++i) { createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i) } } else if (isPrimitive(vnode.text)) { nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text))) } }

内部进行遍历子节点数组     ,再次调用 createElm 创建节点                ,而上面创建的 vnode.elm 作为父节点传入。如此循环           ,直到没有子节点,就会创建文本节点插入到 vnode.elm 中                。

执行完成后出来                ,会调用 invokeCreateHooks                ,它负责执行 dom 操作时的 create 钩子函数,同时将 VNode 加入到 insertedVnodeQueue 中:

function invokeCreateHooks (vnode, insertedVnodeQueue) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, vnode) } i = vnode.data.hook // Reuse variable if (isDef(i)) { if (isDef(i.create)) i.create(emptyNode, vnode) if (isDef(i.insert)) insertedVnodeQueue.push(vnode) } }

最后一步就是调用 insert 方法将节点插入到父节点:

function insert (parent, elm, ref) { if (isDef(parent)) { if (isDef(ref)) { if (nodeOps.parentNode(ref) === parent) { nodeOps.insertBefore(parent, elm, ref) } } else { nodeOps.appendChild(parent, elm) } } }

可以看到 Vue 是通过递归调用 createElm 来创建节点树的                。同时也说明最深的子节点会先调用 insert 插入节点。所以整个节点树的插入顺序是“先子后父          ”           。插入节点方法就是原生dom的方法 insertBefore 和 appendChild                。

if (isDef(parentElm)) { removeVnodes([oldVnode], 0, 0) }

createElm 流程走完后           ,构建完成的节点树已经插入到页面上了      。其实 Vue 在初始化渲染页面时                ,并不是把原来的根节点 app 给真正替换掉      ,而是在其后面插入一个新的节点           ,接着再把旧节点给移除掉           。

所以在 createElm 之后会调用 removeVnodes 来移除旧节点                ,它里面同样是调用的原生dom方法 removeChild                。

invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) function invokeInsertHook (vnode, queue, initial) { // delay insert hooks for component root nodes, invoke them after the // element is really inserted if (isTrue(initial) && isDef(vnode.parent)) { vnode.parent.data.pendingInsert = queue } else { for (let i = 0; i < queue.length; ++i) { queue[i].data.hook.insert(queue[i]) } } }

在 patch 的最后就是调用 invokeInsertHook 方法      ,触发节点插入的钩子函数      。

至此整个页面渲染的流程完毕~

总结

初始化调用 $mount 挂载组件     。

_render 开始构建 VNode     ,核心方法为 createElement                ,一般会创建普通的 VNode            ,

遇到组件就创建组件类型的 VNode     ,否则就是未知标签的 VNode                ,构建完成传递给 _update                。

patch 阶段根据 VNode 创建真实节点树           ,核心方法为 createElm,

首先遇到组件类型的 VNode                ,内部会执行 $mount                ,再走一遍相同的流程           。

普通节点类型则创建一个真实节点,如果它有子节点开始递归调用 createElm           ,使用 insert 插入子节点                ,直到没有子节点就填充内容节点     。

最后递归完成后      ,同样也是使用 insert 将整个节点树插入到页面中           ,再将旧的根节点移除                。

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
vue里this指向(Vue——initInjections【九】)