vue 中Virtual Dom被创建的方法
作者:mrr 发布时间:2024-05-09 10:41:45
本文将通过解读render函数的源码,来分析vue中的vNode是如何创建的。在vue2.x的版本中,无论是直接书写render函数,还是使用template或el属性,或是使用.vue单文件的形式,最终都需要编译成render函数进行vnode的创建,最终再渲染成真实的DOM。 如果对vue源码的目录还不是很了解,推荐先阅读下 深入vue -- 源码目录和编译过程。
01 render函数
render方法定义在文件 src/core/instance/render.js 中
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
// ...
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode
// render self
let vnode
try {
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
} catch (e) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
)
}
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}
_render定义在vue的原型上,会返回vnode,vnode通过代码render.call(vm._renderProxy, vm.$createElement)进行创建。
在创建vnode过程中,如果出现错误,就会执行catch中代码做降级处理。
_render中最核心的代码就是:
vnode = render.call(vm._renderProxy, vm.$createElement)
接下来,分析下这里的render,vm._renderProxy,vm.$createElement分别是什么。
render函数
const { render, _parentVnode } = vm.$options
render方法是从$options中提取的。render方法有两种途径得来:
在组件中开发者直接手写的render函数
通过编译template属性生成
参数 vm._renderProxy
vm._renderProxy定义在 src/core/instance/init.js 中,是call的第一个参数,指定render函数执行的上下文。
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
生产环境:
vm._renderProxy = vm,也就是说,在生产环境,render函数执行的上下文就是当前vue实例,即当前组件的this。
开发环境:
开发环境会执行initProxy(vm),initProxy定义在文件 src/core/instance/proxy.js 中。
let initProxy
// ...
initProxy = function initProxy (vm) {
if (hasProxy) {
// determine which proxy handler to use
const options = vm.$options
const handlers = options.render && options.render._withStripped
? getHandler
: hasHandler
vm._renderProxy = new Proxy(vm, handlers)
} else {
vm._renderProxy = vm
}
}
hasProxy的定义如下
const hasProxy =
typeof Proxy !== 'undefined' && isNative(Proxy)用来判断浏览器是否支持es6的Proxy。
Proxy作用是在访问一个对象时,对其进行拦截,new Proxy的第一个参数表示所要拦截的对象,第二个参数是用来定制拦截行为的对象。
开发环境,如果支持Proxy就会对vm实例进行拦截,否则和生产环境相同,直接将vm赋值给
vm._renderProxy
。具体的拦截行为通过handlers对象指定。当手写render函数时,
handlers = hasHandler
,通过template生成的render函数,handlers = getHandler。 hasHandler代码:
const hasHandler = {
has (target, key) {
const has = key in target
const isAllowed = allowedGlobals(key) ||
(typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
if (!has && !isAllowed) {
if (key in target.$data) warnReservedPrefix(target, key)
else warnNonPresent(target, key)
}
return has || !isAllowed
}
}
getHandler代码
const getHandler = {
get (target, key) {
if (typeof key === 'string' && !(key in target)) {
if (key in target.$data) warnReservedPrefix(target, key)
else warnNonPresent(target, key)
}
return target[key]
}
}hasHandler,getHandler分别是对vm对象的属性的读取和propKey in proxy的操作进行拦截,并对vm的参数进行校验,再调用 warnNonPresent 和 warnReservedPrefix 进行Warn警告。
可见,initProxy方法的主要作用就是在开发时,对vm实例进行拦截发现问题并抛出错误,方便开发者及时修改问题。
参数vm.$createElement
vm.$createElement就是手写render函数时传入的createElement函数,它定义在initRender方法中,initRender在new Vue初始化时执行,参数是实例vm。
export function initRender (vm: Component) {
// ...
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
// ...
}
从代码的注释可以看出: vm.$createElement
是为开发者手写render函数提供的方法,vm._c是为通过编译template生成的render函数使用的方法。它们都会调用createElement方法。
02 createElement方法
createElement方法定义在 src/core/vdom/create-element.js 文件中
const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
// wrapper function for providing a more flexible interface
// without getting yelled at by flow
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方法创建vnode。
下面看一下vue文档中createElement能接收的参数。
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一个 HTML 标签字符串,组件选项对象,或者
// 解析上述任何一种的一个 async 异步函数。必需参数。
'div',
// {Object}
// 一个包含模板相关属性的数据对象
// 你可以在 template 中使用这些特性。可选参数。
{
},
// {String | Array}
// 子虚拟节点 (VNodes),由 `createElement()` 构建而成,
// 也可以使用字符串来生成“文本虚拟节点”。可选参数。
[
'先写一些文字',
createElement('h1', '一则头条'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
文档中除了第一个参数是必选参数,其他都是可选参数。也就是说使用createElement方法的时候,可以不传第二个参数,只传第一个参数和第三个参数。刚刚说的参数处理就是对这种情况做处理。
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}通过判断data是否是数组或者是基础类型,如果满足这个条件,说明这个位置传的参数是children,然后对参数依次重新赋值。这种方式被称为重载。
重载:函数名相同,函数的参数列表不同(包括参数个数和参数类型),至于返回类型可同可不同。
处理好参数后调用_createElement方法创建vnode。下面是_createElement方法的核心代码。
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// ...
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
// ...
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && 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()
}
}
方法开始会做判断,如果data是响应式的数据,component的is属性不是真值的时候,都会去调用createEmptyVNode方法,创建一个空的vnode。 接下来,根据normalizationType的值,调用normalizeChildren或simpleNormalizeChildren方法对参数children进行处理。这两个方法定义在 src/core/vdom/helpers/normalize-children.js 文件下。
// 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
}
normalizeChildren和simpleNormalizeChildren的目的都是将children数组扁平化处理,最终返回一个vnode的一维数组。
simpleNormalizeChildren是针对函数式组件做处理,所以只需要考虑children是二维数组的情况。 normalizeChildren方法会考虑children是多层嵌套的数组的情况。normalizeChildren开始会判断children的类型,如果children是基础类型,直接创建文本vnode,如果是数组,调用normalizeArrayChildren方法,并在normalizeArrayChildren方法里面进行递归调用,最终将children转成一维数组。
接下来,继续看_createElement方法,如果tag参数的类型不是String类型,是组件的话,调用createComponent创建vnode。如果tag是String类型,再去判断tag是否是html的保留标签,是否是不认识的节点,通过调用new VNode(),传入不同的参数来创建vnode实例。
无论是哪种情况,最终都是通过VNode这个class来创建vnode,下面是类VNode的源码,在文件 src/core/vdom/vnode.js 中定义
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 scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
devtoolsMeta: ?Object; // used to store functional render context for devtools
fnScopeId: ?string; // functional scope id support
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag // 标签名
this.data = data // 当前节点数据
this.children = children // 子节点
this.text = text // 文本
this.elm = elm // 对应的真实DOM节点
this.ns = undefined // 命名空间
this.context = context // 当前节点上下文
this.fnContext = undefined // 函数化组件上下文
this.fnOptions = undefined // 函数化组件配置参数
this.fnScopeId = undefined // 函数化组件ScopeId
this.key = data && data.key // 子节点key属性
this.componentOptions = componentOptions // 组件配置项
this.componentInstance = undefined // 组件实例
this.parent = undefined // 父节点
this.raw = false // 是否是原生的HTML片段或只是普通文本
this.isStatic = false // 静态节点标记
this.isRootInsert = true // 是否作为根节点插入
this.isComment = false // 是否为注释节点
this.isCloned = false // 是否为克隆节点
this.isOnce = false // 是否有v-once指令
this.asyncFactory = asyncFactory // 异步工厂方法
this.asyncMeta = undefined // 异步Meta
this.isAsyncPlaceholder = false // 是否异步占位
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child (): Component | void {
return this.componentInstance
}
}
VNode类定义的数据,都是用来描述VNode的。
至此,render函数创建vdom的源码就分析完了,我们简单的总结梳理一下。
_render 定义在 Vue.prototype 上,_render函数执行会调用方法render,在开发环境下,会对vm实例进行代理,校验vm实例数据正确性。render函数内,会执行render的参数createElement方法,createElement会对参数进行处理,处理参数后调用_createElement, _createElement方法内部最终会直接或间接调用new VNode(), 创建vnode实例。
03 vnode && vdom
createElement 返回的vnode并不是真正的dom元素,VNode的全称叫做“虚拟节点 (Virtual Node)”,它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,及其子节点。我们常说的“虚拟 DOM(Virtual Dom)”是对由 Vue 组件树建立起来的整个 VNode 树的称呼。
04 心得
读源码切忌只看源码,一定要结合具体的使用一起分析,这样才能更清楚的了解某段代码的意图。就像本文render函数,如果从来没有使用过render函数,直接就阅读这块源码可能会比较吃力,不妨先看看文档,写个demo,看看具体的使用,再对照使用来分析源码,这样很多比较困惑的问题就迎刃而解了。
总结
以上所述是小编给大家介绍的vue 中Virtual Dom被创建的方法,网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!


猜你喜欢
- python如何处理“&#”开头加数字的html字符,比如:风水这类数据。用python
- 一 接口介绍如果说gorountine和channel是支撑起Go语言的并发模型的基石,让Go语言在如今集群化与多核化的时代成为一道亮丽的风
- 前言:1.前面基于Springboot的单体项目介绍已经完结了,至于项目中的其他功能实现我这里就不打算介绍了,因为涉及的知识点不难,而且都是
- 一、判断大型网站的标准1、pv(page views)网页的浏览量概念 一个网站所有的页面,在24小时内被访问的总的次数。千万级别,百万级别
- 写在前面最近在更新我服务器上的python以及pip版本的时候,碰见了令人头痛的问题,就是我执行了升级指令之后,升级也正常的Successf
- 思想:4个数字的排列,加上3个运算符的排列,使用后缀表达式的表现如下:情形一:1,2,3,4,+,-,* => 24*24*4情形二:
- 不论什么语言,我们都需要注意性能优化问题,提高执行效率。选择了脚本语言就要忍受其速度,这句话在某种程度上说明了Python作为脚本语言的不足
- 本文实例讲述了python实现通过pil模块对图片格式进行转换的方法。分享给大家供大家参考。具体分析如下:python的pil模块相当的智能
- 1.官网下载MySQL下载Mysql点击下载mysql. 或点击这里下载下载完成后解压到某一个文件夹(记住这个路径,一会要用到)2.配置初始
- 测试环境Python 3.6.2Win 10 内存 8G,CPU I5 1.6 GHz背景描述这个作品来源于一个日志解析工具的开发,这个开发
- 前言本文主要给大家介绍了关于Django中内置用户认证的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧。认证登陆在
- php获取文件创建时间、修改时间常用代码filemtime ( string filename )返回文件上次被修改的时间,出错时返回 FA
- 为了熟悉Python基础语法,学习了一个经典的案例:飞机大战,最后实现效果如下:实现步骤:①下载64位对应python版本的pygame:p
- 类的定义 类定义有三种基本方法,1、创建并能返回特定类型的对象的函数(工厂函数),例如:function Co(){ var o = new
- MYSQL常用命令1.导出整个数据库mysqldump -u 用户名 -p --default-character-set=latin1 数
- 确定数据库内有多少记录,或者确定有多少记录达到了某些标准,这些用ASP完成并非难事。如果你采用了正确的游标类型,你可以用RecordCoun
- subprocess.Popen用来创建子进程。1)Popen启动新的进程与父进程并行执行,默认父进程不等待新进程结束。def TestPo
- 一般情况下编译安装python环境需要执行以下步骤:下载源码包解压源码包安装配置编译以及编译安装TALK IS CHEAP, SHOW YO
- 深度学习这个词指的是训练神经网络。深代表着非常大的神经网络。那么神经网络到底是什么呢?看了这篇文章后你就会有很直观的认识了。我们从一个房价预
- 1、 HTTP服务器. 2、性能测试工具推荐 3、 图片篇. 4、 如何加载JS,JS应该放在什么位置. 5、 为什么要减少请求数,如何减少