Vue2源码梳理:vdom结构与createElement的实现

本文主要是介绍Vue2源码梳理:vdom结构与createElement的实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

vdom 结构

  • 浏览器原生dom对象,本身就是一个非常复杂的对象,单单把 div 这个dom对象拿出来,遍历它的属性,将是一个庞大的存在
  • 因为浏览器的标准就是把这个dom设计的非常复杂,所以当我们去频繁的操作dom的话,一定会有一些性能问题
  • vdom(Virtual DOM), 其实就是用一个原生的js对象去描述一个dom节点,它的创建比创建一个真实的dom的代价要小很多
  • 在vue.js中的 vdom 的定义在 src/core/vdom/vnode.js
    /* @flow */
    export default class VNode {tag: string | void;data: VNodeData | void;children: ?Array<VNode>; // 树形结构text: string | void;elm: Node | void;ns: string | void;context: Component | void; // rendered in this component's scopekey: string | number | void;componentOptions: VNodeComponentOptions | void;componentInstance: Component | void; // component instanceparent: VNode | void; // component placeholder node// strictly internalraw: boolean; // contains raw HTML? (server only)isStatic: boolean; // hoisted static nodeisRootInsert: boolean; // necessary for enter transition checkisComment: boolean; // empty comment placeholder?isCloned: boolean; // is a cloned node?isOnce: boolean; // is a v-once node?asyncFactory: Function | void; // async component factory functionasyncMeta: Object | void;isAsyncPlaceholder: boolean;ssrContext: Object | void;fnContext: Component | void; // real context vm for functional nodesfnOptions: ?ComponentOptions; // for SSR cachingdevtoolsMeta: ?Object; // used to store functional render context for devtoolsfnScopeId: ?string; // functional scope id supportconstructor (tag?: string,data?: VNodeData,children?: ?Array<VNode>,text?: string,elm?: Node,context?: Component,componentOptions?: VNodeComponentOptions,asyncFactory?: Function) {this.tag = tagthis.data = datathis.children = childrenthis.text = textthis.elm = elmthis.ns = undefinedthis.context = contextthis.fnContext = undefinedthis.fnOptions = undefinedthis.fnScopeId = undefinedthis.key = data && data.keythis.componentOptions = componentOptionsthis.componentInstance = undefinedthis.parent = undefinedthis.raw = falsethis.isStatic = falsethis.isRootInsert = truethis.isComment = falsethis.isCloned = falsethis.isOnce = falsethis.asyncFactory = asyncFactorythis.asyncMeta = undefinedthis.isAsyncPlaceholder = false}// DEPRECATED: alias for componentInstance for backwards compat./* istanbul ignore next */get child (): Component | void {return this.componentInstance}
    }
    
  • 上述 VNodeData,定义在 flow/vnode.js 中
    declare interface VNodeData {key?: string | number;slot?: string;ref?: string;is?: string;pre?: boolean;tag?: string;staticClass?: string;class?: any;staticStyle?: { [key: string]: any };style?: string | Array<Object> | Object;normalizedStyle?: Object;props?: { [key: string]: any };attrs?: { [key: string]: string };domProps?: { [key: string]: any };hook?: { [key: string]: Function };on?: ?{ [key: string]: Function | Array<Function> };nativeOn?: { [key: string]: Function | Array<Function> };transition?: Object;show?: boolean; // marker for v-showinlineTemplate?: {render: Function;staticRenderFns: Array<Function>;};directives?: Array<VNodeDirective>;keepAlive?: boolean;scopedSlots?: { [key: string]: Function };model?: {value: any;callback: Function;};
    };
    
  • vdom实际上它比真实的dom对象创建的代价要小很多
  • vdom 是借鉴了一个开源库 snabbdom 的实现
  • 它的设计比较巧,实现的diff算法和react是不太一样的,号称是性能非常高的
  • 除了vuejs,其他的vdom的实现也是基于它的
  • 所以,它是 vue.js 实现的一个基础, 但在它上面又做了很多扩展
  • 总结来说
    • vNode 它其实就是对原生dom的一种抽象的描述
    • 它的核心无非就几个关键属性,如:标签名、数据、子节点、键值等
    • 那其他属性它其实都是来为来扩展 vnode 灵活性以及实现一些特殊feature
    • 由于 vNode 它只是用来映射真实dom渲染的,它不需要包括这些操作dom的方法
    • 所以说它是比较轻量和简单的
    • vdom 除了它的数据结构的定义映射到真实的dom
    • 还有create, diff 和 patch 的过程
    • 在 vNode 的 create 的过程就是通过 createElement 方法
    • 也就是在 render 函数中调用的vm.$createElement 返回的Vnode

createElement 的实现

  • 在 render 函数提到的生成vnode方法,也就是它最终会调用这个 createElement 的方法来生成vnode
  • render方法,它最终就会调用 option.render 函数
  • 这个函数的执行分为两种情况
    • 一种情况就是通过把模板编译出来的 render 函数,它内部实际上会调用 vm._c
    • 而用户手写的render函数,最终会调用 vm.$createElement 这这个方法
    • 这两个方法最终都会调用这个 createElement 这个函数
    • 它的函数唯一的区别就是最后一个参数,也就是这六个参数,false 或 true
      // initRender 函数中
      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)
      
  • 进入 createElement,定义在 src/core/vdom/create-element.js 中
    const SIMPLE_NORMALIZE = 1
    const ALWAYS_NORMALIZE = 2export function createElement (context: Component,tag: any,data: any,children: any,normalizationType: any,alwaysNormalize: boolean
    ): VNode | Array<VNode> {// 这个是参数检测,如果符合,说明,第三个参数是 children, 后面的参数前移// 这时候,之前的 data 变成了 children, 之前的 children 变成了 normalizationType// 对 children 做一个data的赋值,并且清理 dataif (Array.isArray(data) || isPrimitive(data)) {normalizationType = childrenchildren = datadata = undefined}// 基于最后一个参数,改变倒数第二个参数if (isTrue(alwaysNormalize)) {normalizationType = ALWAYS_NORMALIZE}return _createElement(context, tag, data, children, normalizationType)
    }
    
    • 它定义支持了六个参数
      • 第一个参数 是 vm 实例
      • 第二个就是 vnode的tag标签
      • 第三个就是data,就是跟vNode相关的一些数据
      • 第四个children是它的一些子节点 vnode, 由此构造出vNode tree, 完美映射 dom tree
    • 内部对参数个数不一致,进行处理
    • 最终调用 _createElement
    • 也就是这个函数,主要是用于处理参数的
    • 进入 _createElement
      export function _createElement (context: Component,tag?: string | Class<Component> | Function | Object,data?: VNodeData,children?: any,normalizationType?: number
      ): VNode | Array<VNode> {// 响应式对象会被添加上 __ob__ 这个属性// 首先对 data 做校验,data是不能是响应式的,否则进行警告if (isDef(data) && isDef((data: any).__ob__)) {process.env.NODE_ENV !== 'production' && warn(`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +'Always create fresh vnode data objects in each render!',context)// 创建一个 空的VNode, 这个 VNode 本质上就是一个 注释 VNodereturn createEmptyVNode()}// 获取 component is// object syntax in v-bindif (isDef(data) && isDef(data.is)) {tag = data.is}if (!tag) {// in case of component :is set to falsy valuereturn createEmptyVNode()}// warn against non-primitive keyif (process.env.NODE_ENV !== 'production' &&isDef(data) && isDef(data.key) && !isPrimitive(data.key)) {if (!__WEEX__ || !('@binding' in data.key)) {warn('Avoid using non-primitive value as key, ' +'use string/number value instead.',context)}}// 接着对插槽进行处理// support single function children as default scoped slotif (Array.isArray(children) &&typeof children[0] === 'function') {data = data || {}data.scopedSlots = { default: children[0] }children.length = 0}// 对 children 做 normalize, 当手写 render 函数时,比如传递一个字符串作为children// 但是 children 实际上是一个数组,每一个数组都是 vnode// 另外在编译的时候,会有不同的情况产生if (normalizationType === ALWAYS_NORMALIZE) {children = normalizeChildren(children)} else if (normalizationType === SIMPLE_NORMALIZE) {children = simpleNormalizeChildren(children)}// 对children进行 normalize 后就能很好处理children了let vnode, ns// 对 tag 做一些判断// tag 可能是 string 也可能是组件if (typeof tag === 'string') {let Ctorns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)// 判断是否是html保留标签if (config.isReservedTag(tag)) {// platform built-in elementsif (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn) && data.tag !== 'component') {warn(`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,context)}// 创建一些平台内建元素实例化的vnodevnode = new VNode(config.parsePlatformTagName(tag), data, children,undefined, undefined, context)// 对组件的解析} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {// componentvnode = 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 / constructorvnode = 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 才是真正创建 vNode 的参数
    • 进入 simpleNormalizeChildren,定义在 src/core/vdom/helpers/normalize-children.js
      /* @flow */import VNode, { createTextVNode } from 'core/vdom/vnode'
      import { isFalse, isTrue, isDef, isUndef, isPrimitive } from 'shared/util'// The template compiler attempts to minimize the need for normalization by
      // statically analyzing the template at compile time.
      //
      // For plain HTML markup, normalization can be completely skipped because the
      // generated render function is guaranteed to return Array<VNode>. There are
      // two cases where extra normalization is needed:// 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.
      // 基础类型,返回text基础类型;否则判断是数组,进行处理,否则是undefined
      export function normalizeChildren (children: any): ?Array<VNode> {return isPrimitive(children)? [createTextVNode(children)]: Array.isArray(children)? normalizeArrayChildren(children): undefined
      }function isTextNode (node): boolean {return isDef(node) && isDef(node.text) && isFalse(node.isComment)
      }// 存储返回的 res 数组,遍历children, children 本身又是 array, 它处理有可能的多层嵌套,递归处理
      // 子节点,像 slot, v-for 可能生成 深层数据结构
      function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {const res = [] // 最终返回的结果集let i, c, lastIndex, last// 遍历 childrenfor (i = 0; i < children.length; i++) {c = children[i]if (isUndef(c) || typeof c === 'boolean') continuelastIndex = res.length - 1last = res[lastIndex]//  nested 对嵌套数据结构进行处理if (Array.isArray(c)) {if (c.length > 0) {// 这里进行递归调用,将结果存入cc = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)// merge adjacent text nodes 合并文本节点// 下次处理的第一个节点和最后一个节点都是文本节点,考虑合并if (isTextNode(c[0]) && isTextNode(last)) {res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)c.shift()}// 最终push cres.push.apply(res, c)}// 是否是基础类型} else if (isPrimitive(c)) {// 匹配文本节点if (isTextNode(last)) {// merge adjacent text nodes// this is necessary for SSR hydration because text nodes are// essentially merged when rendered to HTML stringsres[lastIndex] = createTextVNode(last.text + c)} else if (c !== '') {// convert primitive to vnoderes.push(createTextVNode(c))}} else {if (isTextNode(c) && isTextNode(last)) {// merge adjacent text nodesres[lastIndex] = createTextVNode(last.text + c.text)} else {// default key for nested array children (likely generated by v-for)if (isTrue(children._isVList) &&isDef(c.tag) &&isUndef(c.key) &&isDef(nestedIndex)) {c.key = `__vlist${nestedIndex}_${i}__`}res.push(c)}}}return res
      }
      
      • 其实 normalizeArrayChildren 比 simpleNormalizeChildren 做的更多的是
        • 递归处理很多层,拍平到一维数组中
        • 最后处理节点和新处理节点同样是文本节点,进行合并
        • 最终 normalizeArrayChildren 要把深层数据变成一维vnode数组
  • 以上是 createElement 创建 VNode 的过程
  • 每个 VNode 有 children,children 每个元素也是一个 VNode
  • 这样就形成了一个 VNode Tree,它很好的描述了我们的 DOM Tree

这篇关于Vue2源码梳理:vdom结构与createElement的实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/715077

相关文章

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

【 html+css 绚丽Loading 】000046 三才归元阵

前言:哈喽,大家好,今天给大家分享html+css 绚丽Loading!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏+关注哦 💕 目录 📚一、效果📚二、信息💡1.简介:💡2.外观描述:💡3.使用方式:💡4.战斗方式:💡5.提升:💡6.传说: 📚三、源代码,上代码,可以直接复制使用🎥效果🗂️目录✍️

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo