虚拟DOM和render()函数

虚拟DOM和render()函数

虚拟DOM

vue在DOM之上增加一个抽象层来解决渲染效率的问题,这就是虚拟DOM。

虚拟DOM使用普通的JavaScript对象描述DOM元素,每一个虚拟节点都是一个VNode实例。vue在更新真实DOM前,会比较更新前后虚拟DOM结构中的有差异的部分,然后采用异步更新队列的方式将差异的部分更新到真实DOM中,从而减少了最终要在真实DOM上执行的操作次数,提高了页面渲染的效率。

render函数

vue推荐在大多数情况下使用模板构建HTML。然而在一些场景中可能需要JavaScript的编程能力,这时可以使用render函数,它比模板更接近编译器。

下面是一个问答页面,用户单击某个问题链接,跳转到对应的回答部分,也可以单击返回顶部链接,回到页面顶部。这是通过a标签锚点实现的。

下面是带有锚点的标题的基础代码:

<h1>
    <a name="hello-world" href="#hello-world">hello world</a>
</h1>

如果采用组件实现上述代码,考虑到标题元素可以变化,我们将标题的级别(1~6)定义成组件的prop,这样在调用组件时就可以通过该prop动态设置标题元素的级别。组件的使用形式如下:

<anchored-heading :level="1">Hello world</anchored-heading>

接下来是组件的实现代码:

const app = vue.createApp({})
app.component('anchored-heading',{
    template:`
		<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>
		`
    props:{
    	level:{
    		type:Number,
    		required:true
			}
		}
})

虽然模板在大多数组件中都好用,但在本例中不太合适,模板代码冗长,且slot元素在每一级标题元素中都重复书写了。当添加锚元素时,我们还必须在每个v-if/v-else-if分支中再次复制slot元素。下面改用render函数重写上面的示例:

<html>
	<head>
		<meta charset="UTF-8">
		<title></title>
	</head>
	<body>
		
		<div id="app">
			<anchored-heading :level="3">
				<a name="hello-world" href="#hello-world">
    			    Hello world!
  			    </a>
			</anchored-heading>
		</div>
		
		<script src="https://unpkg.com/vue@next"></script>
		<script>
			const app = Vue.createApp({})
            app.component('anchored-heading', {
                render() {
                    const { h } = Vue

                    return h(
                        'h' + this.level, // tag name
                        {}, // props/attributes
                        this.$slots.default() // array of children
                    )
                },
                props: {
                    level: {
                        type: Number,
                        required: true
                    }
                }
            })
            app.mount('#app')
		</script>
	</body>
</html>

$slots用于以编程方式访问由插槽分发的内容。每个命名的插槽都有其相应的属性(例如v-slot:foo的内容将在this.$slots.foo()中找到)。this.$slots.default()属性包含了所有未包含在命名插槽的节点或v-slot:default的内容。

render函数最重要的是h()函数,它返回的并不是一个真正的DOM元素,而是一个纯JavaScript对象,其中包含向vue描述应该在页面上渲染的节点类型的信息,包括任何子节点的描述,也就是虚拟节点(VNode)。

h()函数的作用是创建VNode,可以带三个参数,第一个参数是必须的,形式为{String|Object|Function},即该参数可以是字符串(HTML标签名)、对象(组件或一个异步组件)、函数对象(解析前两者的async函数);第二个参数是可选的,形式为{object},表示一个与模板中元素属性对应的数据对象;第三个参数也是可选的,用于生成子虚拟节点,形式为{String|Array|Object},即该参数可以是字符串(文本虚拟节点)、数组(子虚拟节点的数组)、对象(带插槽的对象)。

下面是h函数可以接收的各种参数的形式:

h(
//第一个参数,必填项
'div',
//第二个参数,可选
{},
//第三个参数,可选
[
    'zzd',
    h('h1','一级标题'),
    h(Component,{
        someprop:'zzd'
    })
])

简单来说,h函数的第一个参数是要创建的元素节点的名字或组件;第二个参数是元素的属性集合(包括普通属性、prop、事件属性等),以对象形式给出;第三个参数是子节点的信息,以数组形式给出,如果该元素只有文本子节点,则直接以字符串形式给出即可,如果还有子元素,则继续调用h函数。

下面继续完善anchored-heading组件,将标题元素的子元素a也放到render函数中构建:

<html>
	<head>
		<meta charset="UTF-8">
		<title></title>
	</head>
	<body>
		<div id="app">
			<anchored-heading :level="3">
				Hello world!
			</anchored-heading>
		</div>
		
		<script src="https://unpkg.com/vue@next"></script>
		<script>
		    const app = Vue.createApp({})
		    
			function getChildrenTextContent(children) {
                return children
                    .map(node => {
                        return typeof node.children === 'string'
                            ? node.children
                            : Array.isArray(node.children)
                            ? getChildrenTextContent(node.children)
                            : ''
                    })
                    .join('')
            }

			app.component('anchored-heading', {
                render() {
                    // 从子节点的文本内容创建kebab-case 风格的 ID
                    const headingId = getChildrenTextContent(this.$slots.default())
                      .toLowerCase()
                      .replace(/\W+/g, '-')     // 将非单词字符替换为短划线
                      .replace(/(^-|-$)/g, '')  // 删除前导和尾随的短划线

                    return Vue.h('h' + this.level, [
                        Vue.h(
                            'a',
                            {
                              name: headingId,
                              href: '#' + headingId
                            },
                            this.$slots.default()
                        )
                    ])
                },
                props: {
                    level: {
                      type: Number,
                      required: true
                    }
                }
            })
            app.mount('#app')
		</script>
	</body>
</html>

如果真的要用很多重复的元素或组件可以使用工厂函数:

render(){
	return Vue.h('div',
		Array.apply(null,{ length:20 }).map(() => {
    		return Vue.h('p','hi')
			})
	)
}

用普通JavaScript代替模板功能

v-if和v-for

只要普通JavaScript能轻松完成的操作,Vue的render函数就没有提供专有的替代方案。例如在使用v-if和v-for的模板中:

<ul v-if="items.length">
    <li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>no items found</p>

在render函数中可以使用JavaScript的if/else和map实现:

props:['items'],
render(){
    if (this,items.length) {
        return Vue.h('ul',this.items.map((item) => {
            return Vue.h('li',item.name)
        }))
    } else {
        return Vue.h('p','no items found')
    }
}

v-model

在render函数中没有与v-model指令直接对应的实现方案,不过v-model指令在模板编译期间会被扩展为modelValue和onUpdate:modelValue prop,按照v-model的内在逻辑,我们自己实现即可:

props:['modelValue'],
render(){
    return Vue.h(SomeComponent,{
        modelValue:this.modelValue,
        'onUpdate:modelValue':value => this.$emit('update:modelValue',value)
    })
}

v-on

render() {
    return Vue.h('div',{
        onClick:$event => console.log('clicked',$event.target)
    })
}

事件和按键修饰符

对于.passive、.capture和.once这些事件修饰符,可以使用驼峰命名法将它们连接到事件名之后:

render() {
    return Vue.h('input',{
        onClickCapture:this.doThisInCapturingMode,
        onKeyupOnce:this.doThisOnce,
        onMouseoverOnceCapture:this.doThisOnceInCapturingMode,
    })
}

插槽

通过this.$slots可以访问插槽的内容,插槽的内容是VNode数组:

render() {
    //`<div><slot></slot></div>`
    return Vue.h('div',{},this.$slots.default())
}

//访问作用域插槽
props:['message'],
render() {
    //`<div><slot :text="message"></slot></div>`
    return Vue.h('div',{},this.$slots.default({
        text:this.message
    }))
}

实例:使用render函数实现帖子列表

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
	</head>
	<body>
		<div id="app">
			<post-list></post-list>
		</div>
	
	    <script src="https://unpkg.com/vue@next"></script>
		<script>
		    const app = Vue.createApp({})
			// 父组件
			app.component('PostList', {
      			data() {
      				return {
      					posts: [
      						{id: 1, title: '《Servlet/JSP深入详解》怎么样', author: '张三', date: '2019-10-21 20:10:15', vote: 0},
      						{id: 2, title: '《VC++深入详解》观后感', author: '李四', date: '2019-10-10 09:15:11', vote: 0},
      						{id: 3, title: '《Java无难事》怎么样', author: '王五', date: '2020-11-11 15:22:03', vote: 0}
      					]
      				}
      			},
      			methods: {
      				// 自定义事件vote的事件处理器方法
      				handleVote(id){
      					this.posts.map(item => {
      						item.id === id ? {...item, voite: ++item.vote} : item;
      					})
      				}
      			},
      			render(){
      				let postNodes = [];
      				// this.posts.map取代v-for指令,循环遍历posts,
      				// 构造子组件的虚拟节点
      				this.posts.map(post => {
      					let node = Vue.h(Vue.resolveComponent('PostListItem'), {
    								post: post,
    								onVote: () => this.handleVote(post.id)
    							});
      					postNodes.push(node);
      				})
      				return Vue.h('div', [
      						Vue.h('ul',	[
      								postNodes
      							]
      						)
      					]
      				);
      			},
      		});
  		
  		    // 子组件
			app.component('PostListItem', {
				props: {
					post: {
					    type: Object,
					    required: true
					}
				},
				render(){
					return Vue.h('li', [
							Vue.h('p', [
									Vue.h('span',
										// 这是<span>元素的内容
										'标题:'+ this.post.title + ' | 发帖人:' + this.post.author + ' | 发帖时间:' + this.post.date + ' | 点赞数:' + this.post.vote
									),
									Vue.h('button', {
									    onClick: () => this.$emit('vote')
		
										},'赞')
								]
							)
						]
					);
				}
  		    });
  		
			app.mount('#app')
		</script>
	</body>
</html>
posted @ 2021-08-28 21:59  KKKyrie  阅读(251)  评论(0编辑  收藏  举报