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 相关文章推荐
JS 分号引起的一段调试问题
Jun 18 Javascript
javascript 函数使用说明
Apr 07 Javascript
javascript是怎么继承的介绍
Jan 05 Javascript
判定是否原生方法的JS代码
Nov 12 Javascript
javascript基本类型详解
Nov 28 Javascript
纯javascript移动优先的幻灯片效果
Nov 02 Javascript
JS实现DIV容器赋值的方法
Dec 14 Javascript
Jquery实现的简单轮播效果【附实例】
Apr 19 Javascript
jQuery插件ajaxFileUpload异步上传文件
Oct 19 Javascript
bootstrap Table插件使用demo
Aug 07 Javascript
详解React项目中碰到的IE问题
Mar 14 Javascript
基于Vue+ElementUI的省市区地址选择通用组件
Nov 20 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
实用函数2
2007/11/08 PHP
PHP使用DES进行加密与解密的方法详解
2013/06/06 PHP
浅谈php中fopen不能创建中文文件名文件的问题
2017/02/06 PHP
基于JQuery的类似新浪微博展示信息效果的代码
2012/07/23 Javascript
现如今最流行的JavaScript代码规范
2014/03/08 Javascript
Jquery实现侧边栏跟随滚动条固定(兼容IE6)
2014/04/02 Javascript
NODE.JS加密模块CRYPTO常用方法介绍
2014/06/05 Javascript
JavaScript字符串对象split方法入门实例(用于把字符串分割成数组)
2014/10/16 Javascript
jQuery插件scroll实现无缝滚动效果
2015/04/27 Javascript
jquery中的工具使用方法$.isFunction, $.isArray(), $.isWindow()
2015/08/09 Javascript
js操作cookie保存浏览记录的方法
2015/12/25 Javascript
javascript验证手机号和实现星号(*)代替实例
2016/08/16 Javascript
浅谈js的异步执行
2016/10/18 Javascript
JavaScript ES6中CLASS的使用详解
2016/11/22 Javascript
纯javaScript、jQuery实现个性化图片轮播【推荐】
2017/01/08 Javascript
vue+VeeValidate 校验范围实例详解(部分校验,全部校验)
2018/10/19 Javascript
Node.js 使用axios读写influxDB的方法示例
2018/10/26 Javascript
JS实现拖拽元素时与另一元素碰撞检测
2020/08/27 Javascript
[01:50]《我与DAC》之玩家:iG夺冠时的那面红旗
2018/03/29 DOTA
Python Tkinter基础控件用法
2014/09/03 Python
Python中__new__与__init__方法的区别详解
2015/05/04 Python
Python操作MongoDB详解及实例
2017/05/18 Python
Tensorflow环境搭建的方法步骤
2018/02/07 Python
基于python+selenium的二次封装的实现
2020/01/06 Python
PyQt5中向单元格添加控件的方法示例
2020/03/24 Python
Python新手如何理解循环加载模块
2020/05/29 Python
Python经典五人分鱼实例讲解
2021/01/04 Python
CSS3 Columns分列式布局方法简介
2014/05/03 HTML / CSS
Omio意大利:全欧洲低价大巴、火车和航班搜索和比价
2017/12/02 全球购物
五一家具促销方案
2014/01/10 职场文书
幼儿园小班家长寄语
2014/04/02 职场文书
升国旗演讲稿
2014/09/05 职场文书
合作协议书模板
2014/10/10 职场文书
高效笔记技巧分享:学会这些让你不再困扰
2019/09/04 职场文书
深入理解margin塌陷和margin合并的解决方案
2021/06/26 HTML / CSS
自动在Windows中运行Python脚本并定时触发功能实现
2021/09/04 Python