js_脚本之家,js之render函数基础

时间: 2018-12-21阅读: 811标签: vue

本文将通过解读render函数的源码,来分析vue中的vNode是如何创建的。在vue2.x的版本中,无论是直接书写render函数,还是使用template或el属性,或是使用.vue单文件的形式,最终都需要编译成render函数进行vnode的创建,最终再渲染成真实的DOM。 如果对vue源码的目录还不是很了解,推荐先阅读下 深入vue -- 源码目录和编译过程。

刚才翻了一下博客,才发现,距离自己写的第一篇Vue的博客vue.js之绑定class和style(2016-10-30)已经过去一年零两天。这一年里,自己从船厂的普通技术员,成为了一个微型不靠谱创业公司的普通码农。发过一次烧,搬了两次家,没攒下什么钱。好,牢骚发到这里,接下来谈谈传说中接近Vue底层的api==render函数。

代码:

01 render函数

一枚硬币的两面

很久很久以前,前端的数据和视图居住在一起,在强大的jQuery的管理下,他们相处的还算可以。但是随着页面越来越复杂,DOM树的节点越来越多,数据夹杂在DOM中变得越来越难于管理。于是一声炮响,迎来了数据驱动视图的MVVM框架,数据和视图被一条天河划分开来,整个页面的数据状态开始变得整洁起来。而连接数据视图的鹊桥是虚拟DOM,关于虚拟DOM参看全面理解虚拟DOM,实现虚拟DOM。构成DOM的每一个节点在Vue中被称为vnode。(这段不严谨,大胆假设没求证)

在我们生成真实的DOM结构时,可以写一个HTML文件描述文档结构交给浏览器去解析,同时也可以通过DOM 的api innerHTML告诉浏览器结构是什么,还可以用createElement来构建DOM树,以喜闻乐见的hello world为例,html和innerHTML api 对DOM结构的描述都是<h1>hello world</h1>,但是用createElement就变成了这个样子:

var h1 = document.createElement('h1');
var hw = document.createTextNode('hello world')
h1.appendChild(hw);
document.body.appendChild(h1);

这就是描述一个DOM结构的方式,你可以用一个html文件,一个字符串,或者一段js代码,但是他们都是在做同一件事,就是告诉浏览器该怎么渲染你想要的页面。现在我们回头看vue,在构建vue实例时,我们要写一个叫template的属性,里面是一个html一样的字符串。那么,vue对这个字符串做什么了?肯定不是羞羞的事情。事实上,vue拿它构建了虚拟DOM。

Vue.compile这个静态方法给我们展示了一个漂亮的字符串模板是怎么变成一个奇怪的render函数的:

Vue.compile('<h1>hello world</h1>')

//返回
{staticRenderFns: Array(0), render: ƒ}

render属性对应的是一个函数,在Vue的实例的上下文中调用它会得到字符串对应的虚拟DOM节点,可以把下面的代码粘贴到Vue官网的控制台下面看看效果:

let r = Vue.compile('<h1>hello world</h1>');//得到{staticRenderFns: Array(0), render: ƒ}
r.render.call(new Vue({}))

//返回 VNode {tag: "h1", data: undefined, children: Array(1), text: undefined...}

于是我们抽丝剥茧,终于看到了VNode长什么样子,有tag属性,还有children,text...总之咋一看,还真的跟真实的DOM对象有几分相似,真实DOM中有tagName,children,textContent...

import App from './App.vue'new Vue({ render: h = h(App),//2.0渲染App组件,等同于1.0写法:template: 'App/',components: { App }, router,}).$mount('#app') //等同el: '#app',

render方法定义在文件 src/core/instance/render.js 中

render函数

上面我们看到了render函数和模板字符串不同寻常的关系以及通过Vue.compile进行转换,下面来看看render函数的具体构造。要注意的是,编译后得到的不是VNode树,而是生成VNode的函数。

在创建Vue实例的过程中,如果传入的选项中有template和render两个属性,render会有更高的优先级:

new Vue({
    template:'...',
    render:f(){}//优先级高
})

这就表示,Vue在看到你要用render函数描述虚拟DOM时。会很高兴,因为它不用自己编译你给他的模板字符串来得到render函数,省力又省心。同时它会丢给你一个函数,这个函数是你构建虚拟DOM所需要的工具,官网上给他起了个名字叫createElement。还有约定的简写叫h,vm中有一个方法_c,也是这个函数的别名。

下面我们先来说说这个构建虚拟Dom的工具,createElement函数。参考官网createElement-参数,首先思考一个普通的html元素会传递给我们哪些信息,<h1 class='foo'>hello world</h1>,没错,我们可以得到3部分有效信息:

  1. 这个元素的标签名--h1
  2. 这个元素有什么属性/事件,class,style,onclick,name,id...
  3. 这个元素有什么子元素,这里是一个文本节点 'hello world'

上面提到,render函数和模板字符串是描述虚拟DOM树的两种方式,那么用createElement函数来描述就变成了下面这样:

craeteElement('h1', {class,style,on,attrs:{name,id}, 'hello world'})
//这里,第三个参数还有玄机,接收的参数十分灵活,详情参考官网关于这三个参数的描述

看到了吧,之前我们从字符串中得到的有效信息到了函数这边变成了输入的参数,而输出这是一个虚拟DOM节点。我们不妨叫他们createElement三剑客。

render函数是渲染一个视图,然后提供给el挂载,如果没有render那页面什么都不会出来

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  { handleError // 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  { handleError vnode = vm._vnode } } else { vnode = vm._vnode } } // if the returned array contains only a single node, allow it if  && 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 { 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 }

大剑客

参数类型是一个字符串或者一个对象一个函数。像下面这样:

'div'//字符串
{
    data:{},
    methods:{},
    mounted:{}
}//一个组件选项对象
function(){return 'div'}//返回上面两种

vue.2.0的渲染过程:

_render定义在vue的原型上,会返回vnode,vnode通过代码render.call(vm._renderProxy, vm.$createElement)进行创建。

二剑客

一个数据对象,包括对根元素html属性的描述,和组件属性的描述,详情见官网,比方说你要描述这么一个节点:

<man class="color" height="1.4m" weight="50kg" v-on:move="handle" />

需要传入的第二个参数应该是

{
    'class':{color:true},
    props:{height:'1.4m', weight:'50kg'},
    on:{move:function handle(){}}
}

1.首先需要了解这是 es 6 的语法,表示 Vue 实例选项对象的 render 方法作为一个函数,接受传入的参数 h 函数,返回 h(App) 的函数调用结果。

在创建vnode过程中,如果出现错误,就会执行catch中代码做降级处理。

三剑客

三剑客可以是一个字符串或者一个数组,数组就表示这个根元素不止有一个虚拟子节点了。还是举个例子:

<h1> hello world </h1>

要给createElement传入的第三个参数(第二个参数,由于根元素没什么属性,可以省略)应该是:

vm = new Vue({
    render:createElement => createElement('h1',[
        createElement('span',{style:{color:'red'}},'hello'),
        createElement('span','world')
    ])
});
vm.$mount('#logo');//$mount的意思是**附体**

可以把代码复制到vue官网的控制台看效果。有句话说的好,给我一个女人,我能创造一个民族,用到这里是,给我一个createElement函数,我们创造出一课虚拟DOM树。其实render函数和slot还可以擦出不一样的火花,就到下篇介绍了(心虚)。本篇完。

2.其次,Vue 在创建 Vue 实例时,通过调用 render 方法来渲染实例的 DOM 树。

_render中最核心的代码就是:

3.最后,Vue 在调用 render 方法时,会传入一个 createElement 函数作为参数,也就是这里的 h 的实参是 createElement 函数,然后 createElement 会以 APP 为参数进行调用,关于 createElement 函数的参数说明参见:Element-Arguments

vnode = render.call(vm._renderProxy, vm.$createElement)

结合一下官方文档的代码便可以很清晰的了解Vue2.0 render:h = h(App)的渲染过程。

接下来,分析下这里的render,vm._renderProxy,vm.$createElement分别是什么。

render: function (createElement) { return createElement( 'h' + this.level, // tag name 标签名称 this.$slots.default // 子组件中的阵列 ) }

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} else { vm._renderProxy = vm}

生产环境:

vm._renderProxy = vm,也就是说,在生产环境,render函数执行的上下文就是当前vue实例,即当前组件的this。

开发环境:

开发环境会执行initProxy,initProxy定义在文件 src/core/instance/proxy.js 中。

let initProxy// ...initProxy = function initProxy  { // determine which proxy handler to use const options = vm.$options const handlers = options.render && options.render._withStripped ? getHandler : hasHandler vm._renderProxy = new Proxy } else { vm._renderProxy = vm }}

hasProxy的定义如下

const hasProxy = typeof Proxy !== 'undefined' && isNative

用来判断浏览器是否支持es6的Proxy。

Proxy作用是在访问一个对象时,对其进行拦截,new Proxy的第一个参数表示所要拦截的对象,第二个参数是用来定制拦截行为的对象。

开发环境,如果支持Proxy就会对vm实例进行拦截,否则和生产环境相同,直接将vm赋值给vm._renderProxy。具体的拦截行为通过handlers对象指定。

当手写render函数时,handlers = hasHandler,通过template生成的render函数,handlers = getHandler。 hasHandler代码:

const hasHandler = { has  { const has = key in target const isAllowed = allowedGlobals || (typeof key === 'string' && key.charAt === '_' && ! if  { if  warnReservedPrefix else warnNonPresent } return has || !isAllowed }}

getHandler代码

const getHandler = { get  { if (typeof key === 'string' && ! { if  warnReservedPrefix else warnNonPresent } 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  { // ... // 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 =  => createElement(vm, a, b, c, d, false) // normalization is always applied for the public version, used in // user-written render functions. vm.$createElement =  => createElement // ...}

从代码的注释可以看出:vm.$createElement是为开发者手写render函数提供的方法,vm._c是为通过编译template生成的render函数使用的方法。它们都会调用createElement方法。

02 createElement方法

createElement方法定义在 src/core/vdom/create-element.js 文件中

const SIMPLE_NORMALIZE = 1const ALWAYS_NORMALIZE = 2// wrapper function for providing a more flexible interface// without getting yelled at by flowexport function createElement ( context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean): VNode | Array { if  || isPrimitive { normalizationType = children children = data data = undefined } if (isTrue { 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} // 子虚拟节点 ,由 `createElement()` 构建而成, // 也可以使用字符串来生成“文本虚拟节点”。可选参数。 [ '先写一些文字', createElement, createElement(MyComponent, { props: { someProp: 'foobar' } }) ])

文档中除了第一个参数是必选参数,其他都是可选参数。也就是说使用createElement方法的时候,可以不传第二个参数,只传第一个参数和第三个参数。刚刚说的参数处理就是对这种情况做处理。

if  || isPrimitive { normalizationType = children children = data data = undefined}

通过判断data是否是数组或者是基础类型,如果满足这个条件,说明这个位置传的参数是children,然后对参数依次重新赋值。这种方式被称为重载。

重载:函数名相同,函数的参数列表不同,至于返回类型可同可不同。

处理好参数后调用_createElement方法创建vnode。下面是_createElement方法的核心代码。

export function _createElement ( context: Component, tag?: string | Class | Function | Object, data?: VNodeData, children?: any, normalizationType?: number): VNode | Array { // ... if (normalizationType === ALWAYS_NORMALIZE) { children = normalizeChildren } else if (normalizationType === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren } let vnode, ns if (typeof tag === 'string') { let Ctor // ... if (config.isReservedTag { // platform built-in elements vnode = new VNode( config.parsePlatformTagName, data, children, undefined, undefined, context ) } else if  && 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  { return vnode } else if  { if  applyNS if  registerDeepBindings 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  { for (let i = 0; i < children.length; i++) { if (Array.isArray { return Array.prototype.concat.apply } } return children}// 2. When the children contains constructs that always generated nested Arrays,// e.g. , , 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 : ?Array { return isPrimitive ? [createTextVNode] : Array.isArray ? normalizeArrayChildren : 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; 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?  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, 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的全称叫做“虚拟节点 ”,它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,及其子节点。我们常说的“虚拟 DOM”是对由 Vue 组件树建立起来的整个 VNode 树的称呼。

04 心得

读源码切忌只看源码,一定要结合具体的使用一起分析,这样才能更清楚的了解某段代码的意图。就像本文render函数,如果从来没有使用过render函数,直接就阅读这块源码可能会比较吃力,不妨先看看文档,写个demo,看看具体的使用,再对照使用来分析源码,这样很多比较困惑的问题就迎刃而解了。

总结

以上所述是小编给大家介绍的vue 中Virtual Dom被创建的方法,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对脚本之家网站的支持!如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

本文由澳门威斯尼人平台登录发布于Web前端,转载请注明出处:js_脚本之家,js之render函数基础

相关阅读