在VUE中,码解组件是析计渲染一个非常重要的概念,整个应用的划之e何页面都是通过组件进行渲染实现的,但是组件转为真实我们在编写组件时,它们内部又是码解如何进行工作的呢?从我们开始编写组件,到最终转为真实DOM,析计渲染是划之e何一个怎样的转变过程呢?那么我们应该先来了解vue3中组件时如何渲染的?
组件是一个抽象概念,它是组件转为真实对一棵DOM树的抽象,在页面写一个组件节点:,码解它并不会在页面上渲染这个叫的析计渲染标签。我们在写组件时,划之e何应该内部时这样的组件转为真实:
<template> <div class="test"> <p>hello world</p> </div> </template>那么,一个组件想要真正渲染成DOM需要以下几个步骤:
创建VNode 渲染VNode 生成真实DOM这里的码解VNode是什么,其实就是析计渲染能够描述组件信息的Javascript对象。云服务器提供商
一个组件可以通过"模板+对象描述"的划之e何方式创建组件,创建好后又是如何被调用并进行初始化的呢?
因为整个组件树是从根组件开始进行渲染的,要寻找到根组件的渲染入口,需要从应用程序的初始化过程开始分析。
我们分别看下vue2和vue3初始化应用代码有啥区别,但其实没多大区别。
//vue2 import Vue from "vue"; import App from "./App"; const app = new Vue({ render:h=>h(App); }) app.$mount("#app"); //vue3 import { createApp} from "vue"; import App from "./app"; const app = createApp(App); app.mount("#app");接下来我们看看createApp内部实现:
export const createApp = ((...args) => { //创建app对象 const app = ensureRenderer().createApp(...args) if (__DEV__) { injectNativeTagCheck(app) } const { mount } = app //重写mount方法 app.mount = (containerOrSelector: Element | string): any => { const container = normalizeContainer(containerOrSelector) if (!container) return const component = app._component if (!isFunction(component) && !component.render && !component.template) { component.template = container.innerHTML } // clear content before mounting container.innerHTML = const proxy = mount(container) container.removeAttribute(v-cloak) return proxy } return app }) as CreateAppFunction<Element>我们看到const app = ensureRenderer().createApp(...args)用来创建app对象,那么其内部是如何实现的:
//渲染相关的一些配置,比如:更新属性的方法,操作DOM的方法 const rendererOptions = { patchProp, // 处理 props 属性 ...nodeOps // 处理 DOM 节点操作 } // lazy create the renderer - this makes core renderer logic tree-shakable // in case the user only imports reactivity utilities from Vue. let renderer: Renderer | HydrationRenderer let enabledHydration = false // 我们看到中文翻译就是:延时创建渲染器,当用户只依赖响应式包的时候,不会立即创建渲染器, // 可以通过tree-shakable移除核心渲染逻辑相关的代码 function ensureRenderer() { return renderer || (renderer = createRenderer(rendererOptions)) }渲染器,这是为了跨平台渲染做准备的,简单理解就是:包含平台渲染逻辑的网站模板js对象。我们看到创建渲染器,是通过调用createRenderer来实现的,其通过调用baseCreateRenderer函数进行返回,其中就有我们要找的createApp: createAppAPI(render, hydrate)。
export function createRenderer< HostNode = RendererNode, HostElement = RendererElement >(options: RendererOptions<HostNode, HostElement>) { return baseCreateRenderer<HostNode, HostElement>(options) } // function baseCreateRenderer( options: RendererOptions, createHydrationFns?: typeof createHydrationFunctions ): any { const { insert: hostInsert, remove: hostRemove, patchProp: hostPatchProp, createElement: hostCreateElement, createText: hostCreateText, createComment: hostCreateComment, setText: hostSetText, setElementText: hostSetElementText, parentNode: hostParentNode, nextSibling: hostNextSibling, setScopeId: hostSetScopeId = NOOP, cloneNode: hostCloneNode, insertStaticContent: hostInsertStaticContent } = options // ....此处省略两千行,我们先不管 return { render, hydrate, createApp: createAppAPI(render, hydrate) } }我们看到createAppAPI(render, hydrate)方法接受两个参数:根组件渲染函数render,可选参数hydrate是在SSR场景下应用的,这里先不关注。
export function createAppAPI<HostElement>( render: RootRenderFunction, hydrate?: RootHydrateFunction ): CreateAppFunction<HostElement> { //createApp方法接受的两个参数:根组件的对象和prop return function createApp(rootComponent, rootProps = null) { if (rootProps != null && !isObject(rootProps)) { __DEV__ && warn(`root props passed to app.mount() must be an object.`) rootProps = null } // 创建默认APP配置 const context = createAppContext() const installedPlugins = new Set() let isMounted = false const app: App = { _component: rootComponent as Component, _props: rootProps, _container: null, _context: context, get config() { return context.config }, set config(v) { if (__DEV__) { warn( `app.config cannot be replaced. Modify individual options instead.` ) } }, // 都是一些眼熟的方法 use() { }, mixin() { }, component() { }, directive() { }, //用于挂载组件 mount(rootContainer){ //创建根组件的VNode const vnode = createVNode(rootComponent,rootProps); //利用渲染器渲染VNode render(vnode,rootContainer); app._container = rootComponent; return vnode.component.proxy; } // ... } return app } }在整个app对象的创建过程中,vue.js利用 闭包和函数柯里化 的技巧,很好的实现参数保留。如:在执行app.mount的时候,不需要传入渲染器render,因为在执行createAppAPI的时候,渲染器render参数已经被保留下来。
我们知道在vue源码中已经将mount方法已经进行封装,但是云服务器在我们使用时为什么还要进行重写,而不是直接把相关逻辑放在app对象的mount方法内部实现呢?
重写的目的是:实现既能让用户在使用API时更加灵活,也可以兼容Vue2的写法。
这是因为vue.js不仅仅是为web平台服务的,其设计的目标是"星辰大海"--实现支持跨平台渲染,内部不能够包含任何指定平台的内容,createApp函数内部的app.mount方法是一个标准的可跨平台的组件渲染流程:先创建VNode,再渲染VNode。
mount(rootContainer){ //创建根组件的VNode const vnode = createVNode(rootComponent,rootProps); //利用渲染器渲染VNode render(vnode,rootContainer); app._container = rootComponent; return vnode.component.proxy; }我们看到app.mount重写的代码如下:
//重写mount方法 app.mount = (containerOrSelector: Element | string): any => { //标准化容器 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 } //在挂载前清空容器内容 clear content before mounting container.innerHTML = //实现真正的挂载 const proxy = mount(container) container.removeAttribute(v-cloak) return proxy }vnode本质上用来描述DOM的Javascript对象,它在vue中可以描述不同节点,比如:普通元素节点、组件节点等。
我们可以使用vnode来表示button标签:
type:标签的类型 props:标签的DOM属性信息 children:DOM的子节点,vnode数组 const vnode = { //标签的类型 type:"button", //标签的DOM属性信息 props:{ "class":"btn", style:{ width:"100px", height:"100px" } }, //dom的子节点,vnode数组 children:"确认" }那么,我们可以使用vnode来对抽象事物的描述,比如用来表示组件标签,页面并不会真正渲染一个叫做HelloWorld的标签元素,而是渲染组件内部定义的原生的HTML标签元素。
const HelloWorld = { //定义组件对象信息 } const vnode = { type:HelloWorld, props:{ msg:"test" } }我们在想:vnode到底有什么优势,为什么一定要设计成vnode这样的数据结构?
抽象:引入vnode,可以将渲染过程抽象化,从而使得组件的抽象能力有所提升。 跨平台:因为patch vnode过程不同平台可以有自己的实现,给予vnode再做服务端渲染、weex平台、小程序平台的渲染。但是呢,注意:使用vnode并不意味着不用操作真实DOM。很多人会误认为vnode的性能一定会比手动操作DOM好,但其实并不是一定的。这是因为:
基于vnode实现的MVVM框架,在每次render to vnode过程中,渲染组件会有一定的javascript耗时,尤其是大组件 当我们去更新组件时,可以感觉到明显的卡顿现象。虽然diff算法在减少DOM操作方面足够优秀,但最终还是免不了操作DOM,所以性能并不能说是绝对优势我们前面捋了一遍源码,知道vue中是通过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 }那么在渲染vnode过程中涉及道到的patch补丁函数是如何实现的:
function patch( n1,//旧的vnode,当n1==null时,表示时一次挂载的过程 n2,//新的vnode,后续会根据这个vnode类型执行不同的处理逻辑 container,//表示dom容器,在vnode渲染生成DOM后,会挂载到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 Test: //处理文本节点 break case Comment: //处理注释节点 break case Static: //处理静态节点 break case Fragment: //处理Fragment元素 break default: if(shapeFlag & 1 /*ELEMENT*/){ //处理普通DOM元素 processElemnt( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) }else if(shapeFlag & 64 /*TELEPORT*/){ //处理普通TELEPORT processElemnt( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) }else if(){ }else if(){ }else if(){ } } }我们看下处理组件的parentComponent函数的实现:
function parentComponent( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ){ if(n1==null){ //挂载组件 mountComponent( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) }else{ //更新组件 updateComponent( n1, n2, parentComponent, optimized ) } }关于组件实例:
创建组件实例:内部通过对象的方式创建了当前渲染的组件实例 设置组件实例:instance保留了很多组件相关的数据,维护了组件的上下文,包括对props、插槽以及其他实例的属性的初始化处理初始渲染主要做两件事情:
渲染组件生成subTree 把subTree挂载到container中再回到我们梦开始的地方,我们看到在HelloWorld组件内部,整个DOM节点对应的vnode执行renderComponentRoot渲染生成对应的subTree,我们可以把它成为"子树vnode"。
<template> <div class="test">//test被称为子树vnode <p>hello world</p> </div> </template>如果是其它平台比如weex等,hostCreateElment方法就不再是操作DOM,而是平台相关的API,这些平台相关的方法是在创建渲染器阶段作为参数传入的。
创建完DOM节点后,要判断如果有props,就给这个DOM节点添加相关的class、style、event等属性,并在hostPatchProp函数内部做相关的处理逻辑。
在生产开发中,App和hello组件的例子就是嵌套组件的场景,组件vnode主要维护着组件的定义对象,组件上的各种props,而组件本身是一个抽象节点,它自身的渲染其实是通过执行组件定义的render函数渲染生成的子树vnode完成的,然后再通过patch这种递归方式,无论组件嵌套层级多深,都可以完成整个组件树的渲染。
本文主要分析总结了组件的渲染流程,从入口开始层层分析组件渲染过程的源码,我们知道了一个组件想要真正渲染成DOM需要以下三个步骤:
创建VNode 渲染VNode 生成真实DOM