菜单

js_脚本之家,x中的Render函数详解_vue

2020年3月20日 - 首页

js_脚本之家,x中的Render函数详解_vue。Render函数是Vue2.x版本新增的一个函数;使用虚拟dom来渲染节点提升性能,因为它是基于JavaScript计算。通过使用createElement来创建dom节点。createElement是render的核心方法。其Vue编译的时候会把template里面的节点解析成虚拟dom;

前面的话

什么是虚拟dom?

Vue 推荐在绝大多数情况下使用 template
来创建HTML。然而在一些场景中,真的需要 JavaScript
的完全编程的能力,这就是 render 函数,它比 template
更接近编译器。本文将详细介绍Vue渲染函数

虚拟dom不同于真正的dom,它是一个JavaScript对象。当状态发生变化的时候虚拟dom会进行一个diff判断/运算;然后判断哪些dom是需要被替换的而不是全部重绘,所以性能会比dom操作高很多。

引入

下面看一张Aresn大神总结的一张图:

下面是一个例子,如果要实现类似下面的效果。其中,H标签可替换

Vue2.x版本中VNode属性:

  Hello world! 

tag:当前节点标签名,

在 HTML 层,像下面这样定义来组件接口:

data:当前节点数据对象,

Hello world!

children:子节点数组,

当开始写一个通过 level prop 动态生成 heading
标签的组件,可能很快想到这样实现:

text:当前节点文本,

 <h1 v-if="level === 1"> <slot></slot> </h1> <h2 v-else-if="level === 2"> <slot></slot> </h2> <h3 v-else-if="level === 3"> <slot></slot> </h3> <h4 v-else-if="level === 4"> <slot></slot> </h4> <h5 v-else-if="level === 5"> <slot></slot> </h5> <h6 v-else-if="level === 6"> <slot></slot> </h6>

Vue.component('anchored-heading', { template: '#anchored-heading-template', props: { level: { type: Number, required: true } }})

elm:当前虚拟节点对应的真实dom节点,

在这种场景中使用 template
并不是最好的选择:首先代码冗长,为了在不同级别的标题中插入锚点元素,需要重复地使用

ns:节点的namespace,

虽然模板在大多数组件中都非常好用,但是在这里它就不是很简洁的了。那么,来尝试使用
render 函数重写上面的例子:

content:编译作用域,

 Hello world!Vue.component('anchored-heading', { render: function  { return createElement( 'h' + this.level, // tag name 标签名称 this.$slots.default // 子组件中的阵列 ) }, props: { level: { type: Number, required: true } }}) new Vue

functionalContext:函数化组件的作用域,即全局上下文,

这样的代码精简很多,但是需要非常熟悉 Vue
的实例属性。在这个例子中,需要知道当不使用 slot
属性向组件中传递内容时,比如 anchored-heading 中的 Hello
world!,这些子元素被存储在组件实例中的 $slots.default中

key:节点标识,有利于patch优化,

虚拟DOM

componentOptions:创建组件实例时的options,

在深入渲染函数之前,了解一些浏览器的工作原理是很重要的。以下面这段 HTML
为例:

child:当前节点对应的组件实例,

 My title Some text content 

parent:组件的占位节点,

当浏览器读到这些代码时,它会建立一个“DOM
节点”树来保持追踪,如同会画一张家谱树来追踪家庭成员的发展一样。HTML 的
DOM 节点树如下图所示:

raw:原始html,

每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。一个节点就是页面的一个部分。就像家谱树一样,每个节点都可以有子节点

isStatic:是否是静态节点,

高效的更新所有这些节点会是比较困难的,不过所幸不必再手动完成这个工作了。只需要告诉
Vue 希望页面上的 HTML 是什么,这可以是在一个模板里:

isRootInsert:是否作为跟节点插入,若被包裹的节点,该属性值为false,

{{ blogTitle }}

render: function  { return createElement}

isComment:是否为注释节点,

在这两种情况下,Vue 都会自动保持页面的更新,即便 blogTitle 发生了改变。

isCloned:是否为克隆节点,

Vue 通过建立一个虚拟 DOM 对真实 DOM 发生的变化保持追踪

isOnce:是否只改变一次,或是否有v-once指令;

return createElementcreateElement 到底会返回什么呢?其实不是一个实际的
DOM 元素。它更准确的名字可能是
createNodeDescription,因为它所包含的信息会告诉 Vue
页面上需要渲染什么样的节点,及其子节点。我们把这样的节点描述为“虚拟节点
”,也常简写它为“VNode”。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个
VNode 树的称呼

其中这里面又有几种VNode类型:

createElement

Aresn大神总结的很好:

接下来需要熟悉的是如何在 createElement 函数中生成模板。这里是
createElement 接受的参数:

TextVNode:文本节点,

// @returns {VNode}createElement( // {String | Object | Function} // 一个 HTML 标签字符串,组件选项对象,或者一个返回值类型为 String/Object 的函数,必要参数 'div', // {Object} // 一个包含模板相关属性的数据对象 // 这样,可以在 template 中使用这些属性。可选参数。 { }, // {String | Array} // 子节点 ,由 `createElement()` 构建而成, // 或简单的使用字符串来生成“文本节点”。可选参数。 [ '先写一些文字', createElement, createElement(MyComponent, { props: { someProp: 'foobar' } }) ])

ElementVNode:普通元素节点,

正如在模板语法中,v-bind:class 和 v-bind:style ,会被特别对待一样,在
VNode 数据对象中,下列属性名是级别最高的字段。该对象也允许绑定普通的
HTML 特性,就像 DOM 属性一样,比如 innerHTML

ComponentVNode:组件节点,

{ // 和`v-bind:class`一样的 API 'class': { foo: true, bar: false }, // 和`v-bind:style`一样的 API style: { color: 'red', fontSize: '14px' }, // 正常的 HTML 特性 attrs: { id: 'foo' }, // 组件 props props: { myProp: 'bar' }, // DOM 属性 domProps: { innerHTML: 'baz' }, // 事件监听器基于 `on` // 所以不再支持如 `v-on:keyup.enter` 修饰器 // 需要手动匹配 keyCode。 on: { click: this.clickHandler }, // 仅对于组件,用于监听原生事件,而不是组件内部使用 `vm.$emit` 触发的事件。 nativeOn: { click: this.nativeClickHandler }, // 自定义指令。注意事项:不能对绑定的旧值设值 // Vue 会持续追踪 directives: [ { name: 'my-custom-directive', value: '2', expression: '1 + 1', arg: 'foo', modifiers: { bar: true } } ], // Scoped slots in the form of // { name: props => VNode | Array } scopedSlots: { default: props => createElement }, // 如果组件是其他组件的子组件,需为插槽指定名称 slot: 'name-of-slot', // 其他特殊顶层属性 key: 'myKey', ref: 'myRef'}

EmptyVNode:空节点,或者说是没有内容的注释节点,

有了这些知识,现在可以完成最开始想实现的组件:

CloneVNode:克隆节点,可以是以上任意类型节点

var getChildrenTextContent = function  { return children.map { return node.children ? getChildrenTextContent : node.text }).join}Vue.component('anchored-heading', { render: function  { // create kebabCase id var headingId = getChildrenTextContent .toLowerCase() .replace .replace return createElement( 'h' + this.level, [ createElement('a', { attrs: { name: headingId, href: '#' + headingId } }, this.$slots.default) ] ) }, props: { level: { type: Number, required: true } }})

说了那么多;到底什么时候用虚拟dom才比较好呢?其实
我们使用的单文件组件就已经够好了。但是当某些代码冗余的时候如果写单文件组件的话会有好多重复的内容;

组件树中的所有 VNodes 必须是唯一的。这意味着,下面的 render function
是无效的:

接下来介绍其核心函数;

render: function  { var myParagraphVNode = createElement return createElement('div', [ // 错误-重复的 VNodes myParagraphVNode, myParagraphVNode ])}

createElement:

如果真的需要重复很多次的元素/组件,可以使用工厂函数来实现。例如,下面这个例子
render 函数完美有效地渲染了 20 个重复的段落:

createElement接收3个参数:

render: function  { return createElement('div', Array.apply.map { return createElement}

第一个参数可以是HTML标签名,组件或者函数都可以;此参数是必须的;

JS代替模板

附上一个非常简单的createElement函数demo;

由于使用原生的 JavaScript 来实现某些东西很简单,Vue 的 render
函数没有提供专用的 API。比如,template 中的 v-if 和 v-for:

 var app = new Vue({ el:"#app", render:createElement => { eturn createElement( 'h2', [ createElement( 'a', { domProps:{ href:"#biaoti" } }, "标题" ) ] ); } });

这只是一个createElement函数的使用,而Vue2中VNodeData

No items found.

class: v-bind/:class

这些都会在 render 函数中被 JavaScript 的 if/else 和 map 重写:

style:v-bind/:style

render: function  { if  { return createElement('ul', this.items.map { return createElement } else { return createElement('p', 'No items found.') }}

attrs:dom属性,如id

ender 函数中没有与 v-model 相应的 api,必须自己来实现相应的逻辑:

props:props,

render: function  { var self = this return createElement('input', { domProps: { value: self.value }, on: { input: function  { self.value = event.target.value self.$emit('input', event.target.value) } } })}

nativeOn:原生事件

这就是深入底层要付出的,尽管麻烦了一些,但相对于 v-model
来说,可以更灵活地控制

像这些也可以在render函数中实现,若VNode是组件或含有组件的slot,那么VNode必须是唯一。

对于 .passive、.capture 和 .once事件修饰符,Vue 提供了相应的前缀可以用于
on:

像平常开发过程中单文件组件中template写法更为简单,可读性也高。如果是webpack进行打包的话template也会预编译成render函数。

Modifier Prefix.passive &.capture !.once ~.capture.once or.once.capture ~!

on: { '!click': this.doThisInCapturingMode, '~keyup': this.doThisOnce, `~!mouseover`: this.doThisOnceInCapturingMode}

这里demo栗子暂时还没写那么多。这里只是一个简单的介绍。若有更好的欢迎大家一起学习讨论。

对于其他的修饰符,前缀不是很重要,因为可以直接在事件处理函数中使用事件方法:

Modifier Equivalent in Handler.stop event.stopPropagation().prevent event.preventDefault().self if (event.target !== event.currentTarget) returnKeys:.enter, .13 if  return Modifiers Keys:.ctrl, .alt, .shift, .meta if  return 

下面是一个使用所有修饰符的例子:

on: { keyup: function  { // 如果触发事件的元素不是事件绑定的元素 // 则返回 if (event.target !== event.currentTarget) return // 如果按下去的不是 enter 键或者 // 没有同时按下 shift 键 // 则返回 if (!event.shiftKey || event.keyCode !== 13) return // 阻止 事件冒泡 event.stopPropagation() // 阻止该元素默认的 keyup 事件 event.preventDefault() // ... }}

可以从 this.$slots 获取 VNodes 列表中的静态内容:

render: function  { // `` return createElement('div', this.$slots.default)}

还可以从 this.$scopedSlots 中获得能用作函数的作用域插槽,这个函数返回
VNodes:

render: function  { // `` return createElement('div', [ this.$scopedSlots.default ])}

如果要用渲染函数向子组件中传递作用域插槽,可以利用 VNode 数据中的
scopedSlots 域:

render  { return createElement('div', [ createElement('child', { // pass `scopedSlots` in the data object // in the form of { name: props => VNode | Array } scopedSlots: { default: function  { return createElement } } }) ])}

JSX

如果写了很多 render 函数,可能会觉得痛苦

createElement( 'anchored-heading', { props: { level: 1 } }, [ createElement, ' world!' ])

特别是模板如此简单的情况下:

 Hello world!

这就是为什么会有一个 Babel 插件,用于在 Vue 中使用 JSX
语法的原因,它可以让我们回到更接近于模板的语法上

import AnchoredHeading from './AnchoredHeading.vue'new Vue({ el: '#demo', render  { return (  Hello world!  ) }})

[注意]将 h 作为 createElement 的别名是 Vue
生态系统中的一个通用惯例,实际上也是 JSX 所要求的,如果在作用域中 h
失去作用,在应用中会触发报错

函数式组件

之前创建的锚点标题组件是比较简单,没有管理或者监听任何传递给它的状态,也没有生命周期方法。它只是一个接收参数的函数。

在这个例子中,我们标记组件为 functional,这意味它是无状态 ,无实例

一个 函数式组件 就像这样:

Vue.component('my-component', { functional: true, // 为了弥补缺少的实例 // 提供第二个参数作为上下文 render: function (createElement, context) { // ... }, // Props 可选 props: { // ... }})

[注意]在 2.3.0 之前的版本中,如果一个函数式组件想要接受 props,则
props 选项是必须的。在 2.3.0 或以上的版本中,你可以省略 props
选项,所有组件上的属性都会被自动解析为 props

组件需要的一切都是通过上下文传递,包括:

props:提供 props 的对象children: VNode 子节点的数组slots: slots 对象data:传递给组件的 data 对象parent:对父组件的引用listeners:  一个包含了组件上所注册的 v-on 侦听器的对象。这只是一个指向 data.on 的别名。injections:  如果使用了 inject 选项,则该对象包含了应当被注入的属性。

在添加 functional: true 之后,锚点标题组件的 render 函数之间简单更新增加
context 参数,this.$slots.default 更新为
context.children,之后this.level 更新为 context.props.level。

因为函数式组件只是一个函数,所以渲染开销也低很多。然而,对持久化实例的缺乏也意味着函数式组件不会出现在
Vue devtools 的组件树里。

在作为包装组件时它们也同样非常有用,比如,当需要做这些时:

1、程序化地在多个组件中选择一个

2、在将 children, props, data 传递给子组件之前操作它们

下面是一个依赖传入 props 的值的 smart-list
组件例子,它能代表更多具体的组件:

var EmptyList = { /* ... */ }var TableList = { /* ... */ }var OrderedList = { /* ... */ }var UnorderedList = { /* ... */ }Vue.component('smart-list', { functional: true, render: function (createElement, context) { function appropriateListComponent () { var items = context.props.items if  return EmptyList if (typeof items[0] === 'object') return TableList if (context.props.isOrdered) return OrderedList return UnorderedList } return createElement( appropriateListComponent(), context.data, context.children ) }, props: { items: { type: Array, required: true }, isOrdered: Boolean }})

为什么同时需要 slots.default 不是和 children
类似的吗?在一些场景中,是这样,但是如果是函数式组件和下面这样的
children 呢?

  first  

对于这个组件,children 会给两个段落标签,而 slots().default
只会传递第二个匿名段落标签,slots().foo
会传递第一个具名段落标签。同时拥有 children 和 slots
系统分发或者简单的通过 children 接收,让其他组件去处理

模板编译

Vue 的模板实际是编译成了 render
函数。这是一个实现细节,通常不需要关心。下面是一个使用 Vue.compile
来实时编译模板字符串的简单 demo:

  first  

render:

function anonymous{return _c,?_c]):_c('p',[_v}}

staticRenderFns:

_m: function anonymous{return _c('header',[_c('h1',[_v])}}

以上这篇Vue渲染函数详解就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图