欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

vue 中Virtual Dom被创建的方法

程序员文章站 2022-08-18 16:03:05
本文将通过解读render函数的源码,来分析vue中的vnode是如何创建的。在vue2.x的版本中,无论是直接书写render函数,还是使用template或el属性,或...

本文将通过解读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被创建的方法,希望对大家有所帮助