vue 中Virtual Dom被创建的方法


Posted in Javascript onApril 15, 2019

本文将通过解读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被创建的方法,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

Javascript 相关文章推荐
深入理解JavaScript系列(8) S.O.L.I.D五大原则之里氏替换原则LSP
Jan 15 Javascript
JQuery入门——事件切换之hover()方法应用介绍
Feb 05 Javascript
js函数中onmousedown和onclick的区别和联系探讨
May 19 Javascript
D3.js 从P元素的创建开始(显示可加载数据)
Oct 30 Javascript
简单的JS时钟实例讲解
Jan 13 Javascript
JS排序方法(sort,bubble,select,insert)代码汇总
Jan 30 Javascript
JavaScript九九乘法口诀表的简单实现
Oct 04 Javascript
微信小程序 css使用技巧总结
Jan 09 Javascript
Angular如何引入第三方库的方法详解
Jul 13 Javascript
浅谈Angular HttpClient简单入门
May 04 Javascript
javascript合并两个数组最简单的实现方法
Sep 14 Javascript
JS对日期操作封装代码实例
Nov 08 Javascript
详解jQuery中的getAll()和cleanData()
Apr 15 #jQuery
详解javascript对数组和json数组的操作
Apr 15 #Javascript
详解vue中router-link标签所必备了解的属性
Apr 15 #Javascript
详解小程序设置缓存并且不覆盖原有数据
Apr 15 #Javascript
JavaScript使用ul中li标签实现删除效果
Apr 15 #Javascript
vue 父组件给子组件传值子组件给父组件传值的实例代码
Apr 15 #Javascript
Vuex的actions属性的具体使用
Apr 14 #Javascript
You might like
php实现遍历目录并删除指定文件中指定内容
2015/01/21 PHP
php添加数据到xml文件的简单例子
2016/09/08 PHP
php版交通银行网银支付接口开发入门教程
2016/09/26 PHP
JavaScript为对象原型prototype添加属性的两种方式
2010/08/01 Javascript
鼠标右击事件代码(asp.net后台)
2011/01/27 Javascript
点击进行复制的JS代码实例
2013/08/23 Javascript
经过绑定元素时会多次触发mouseover和mouseout事件
2014/02/28 Javascript
PHPExcel中的一些常用方法汇总
2015/01/23 Javascript
jquery实现的3D旋转木马特效代码分享
2015/08/25 Javascript
详解nodejs异步I/O和事件循环
2017/06/07 NodeJs
vue中七牛插件使用的实例代码
2017/07/28 Javascript
Express系列之multer上传的使用
2017/10/27 Javascript
jQuery获取所有父级元素及同级元素及子元素的方法(推荐)
2018/01/21 jQuery
解决vue-router进行build无法正常显示路由页面的问题
2018/03/06 Javascript
vue 本地服务不能被外部IP访问的完美解决方法
2018/10/29 Javascript
layui问题之模拟table表格中的选中按钮选中事件的方法
2019/09/20 Javascript
vue实现图片按比例缩放问题操作
2020/08/11 Javascript
JavaScript实现网页留言板功能
2020/11/23 Javascript
在Python中使用PIL模块对图片进行高斯模糊处理的教程
2015/05/05 Python
Selenium定时刷新网页的实现代码
2018/10/31 Python
Python适配器模式代码实现解析
2019/08/02 Python
如何让python的运行速度得到提升
2020/07/08 Python
Django路由层URLconf作用及原理解析
2020/09/24 Python
python爬虫 requests-html的使用
2020/11/30 Python
用HTML5制作数字时钟的教程
2015/05/11 HTML / CSS
波兰购物网站:MALL.PL
2019/05/01 全球购物
给领导的致歉信范文
2014/01/13 职场文书
建筑工程质量通病防治方案
2014/06/08 职场文书
党的群众路线教育实践活动教师自我剖析材料
2014/10/09 职场文书
秦始皇兵马俑导游词
2015/02/02 职场文书
小学教师见习总结
2015/06/23 职场文书
运动会通讯稿100字
2015/07/20 职场文书
入党积极分子培养联系人意见
2015/08/12 职场文书
Python中的协程(Coroutine)操作模块(greenlet、gevent)
2022/05/30 Python
Redis实现订单过期删除的方法步骤
2022/06/05 Redis
React如何使用axios请求数据并把数据渲染到组件
2022/08/05 Javascript