Virtual Dom 虚拟DOM节点
一般来说,我们的组件的模板都是写在template元素里的,当然也可以写在其他的元素里。不管什么元素,都会被Vue解析成Virtual Dom。
更新DOM的操作要比执行JS代码开销大很多,所以Vue在更新页面的时候,实际上先对Virtual
Dom进行计算,看是否有变化,然后渲染有变化的DOM部分,而不是整个页面重绘,这样的单页面应用要比从服务器获取静态页面速度好很多。
在自定义指令的例子里,打印vnode参数在控制台里看到VNode对象,这个就是Vue的虚拟DOM节点对象。这个对象用一些属性封装了DOM节点的一些内容
其中的一堆属性里比较重要的有两个:
tag
,当前节点的标签名称,像例子里指令绑定的是div元素,就会是div字符串。data
,当前节点的数据对象,Vue源码中对应的类叫做VNodeData类。children
,子节点数组,其中元素也全部是VNode类型text
,当前节点的文本,一般文本和注释节点有此属性elm
,即element,当前虚拟节点对应的真实DOM节点ns
,节点的命名空间content
,编译作用域functionalContext
,函数化组件的作用域key
,节点的key属性,用于唯一标识,方便更新DOMcomponentOptions
,创建组件实例用到的设置child
,当前节点对应的组件实例parent
,组件的占位节点raw
,原始HTMListatic
,是否是静态节点isRootlnsert
,是否作为根节点插入isComment
,是否是注释节点isCloned
,是否是克隆节点isOnce
,当前节点是否存在V-once指令
例子里的data点开之后,由于这是一个指令,能看到其中有一个directives: Array(1),其中存放着指令的参数,表达式等一系列内容。
Vnode可以分为如下几类:
TextVNode
,文本节点ElementVNode
,普通元素节点ComponentVNode
,组件节点EmptyVNode
,无内容的注释节点CloneVNode
,克隆节点,可以是上边四种的任意一个,属性isCloned
为true
通过理论知识可以知道,Vue实际上是把被绑定的所有DOM节点,用自己的方式在后台写了一套对应的虚拟节点。通过操作虚拟节点来更新实际DOM页面,实现前端也有控制器和view的功能。
Render函数的例子
render实际上也是Vue实例的一个属性,与用模板渲染不同,render就是进行渲染的函数,可以把要渲染的内容都使用这个函数来完成,先看一个例子。
这个例子想编写一个组件,传入级别和标题,插槽插入显示的内容,自动生成对应级别的H元素包含的链接,点击之后可以自动添加锚点。
标准的做法,就是编写一个组件,传入level参数用于控制显示哪个级别的标题,然后留个插槽供父组件插入内容,还需要一个文本属性控制锚点的内容:
<div id="app"> <anchor :level="2" title="自定义标题">标题</anchor> </div> <script type="text/x-template" id="mytemp"> <div> <h1 v-if="level === 1"> <a :href="'#' + title"><slot></slot></a> </h1> <h2 v-if="level === 2"> <a :href="'#' + title"><slot></slot></a> </h2> <h3 v-if="level === 3"> <a :href="'#' + title"><slot></slot></a> </h3> <h4 v-if="level === 4"> <a :href="'#' + title"><slot></slot></a> </h4> <h5 v-if="level === 5"> <a :href="'#' + title"><slot></slot></a> </h5> <h6 v-if="level === 6"> <a :href="'#' + title"><slot></slot></a> </h6> </div> </script> <script> Vue.component('anchor',{ template:"#mytemp", props:{ level:{ type:Number }, title:{ type:String } } }); var app = new Vue({ el: "#app" }) </script>
仔细观察一下,其实组件模板的大部分内容都相同,只是v-if控制的不同,如果能直接能拼出一段HTML字符串,那变量其实只有level和title,剩下的都是重复内容。这个时候就可以使用render函数,不再用模板。
修改后的全部代码是:
<div id="app"> <anchor :level="4" title="saner">ttile</anchor> </div> <script> Vue.component('anchor',{ props:{ level:{ type:Number }, title:{ type:String } }, render:function (createElement) { return createElement( 'h' + this.level, [ createElement( 'a', { domProps: { href: '#' + this.title } }, this.$slots.default ) ] ); } }); var app = new Vue({ el: "#app" }) </script>
虽然还没有学,但是能看出来,Render函数内置的createElement参数是其中的关键,可以通过字符串和其他的createElement返回的结果进行嵌套,最后生成一段HTML代码。下边就来具体看createElement。
createElement的参数
在开始之前,首先要想一下,要渲染一个HTML元素,需要一些什么内容。很显然,首先必须要知道渲染一个什么元素,可能是标准的HTML元素,也可能是Vue的自定义组件的元素。之后很显然还需要渲染这个元素的属性,可能是HTML标准属性,也可能是指令。最后很显然,还需要渲染标签中的内容,内容可能是文本,可能是另外一个元素,可能是表达式。
想了这些,再看createElement函数就会比较清晰了。由于这个方法最后是拼出来一段HTML模板,很显然必须知道上边的三个内容才能渲染出来模板。
createElement有三个参数,依次是:
{String | Object | Function)
,一个HTML标签名称字符串,或者组件,或者一个函数。是必须传入的参数。{AttrObject)
,属性数据对象,后边会详细解释,可选参数。{String | Array)
,子节点数组,可选参数。
很显然,这个就与上边一一对应。这里要解释一下第二个属性数据对象,里边其实就封装了渲染为元素属性的各个数据。来看一个渲染一个简单组件的例子:
<div id="app"> <comp>saner</comp> </div> <script> Vue.directive('mydir',{ inserted:function (el,binding) { console.log("bind"); console.log("value的值是 " + binding.expression); } }); Vue.component('comp', { render: function (createElement) { return createElement( 'h1', { class: { title: true, ff: false }, style: { fontSize: "30px", textAlign: "center" }, attrs: { id: "test" }, domProps: { }, props:{ value: }, on:{ click: this.clickHandler }, nativeOn:{ click: this.nativeClickHandler }, directives:[ { name:'mydir', value: 'true', expression:'true', arg:'foo', modifiers:{ bar:true } } ], scopedSlots: { default: props => createElement('span', props.text) }, slot: 'name-of-slot', key: 'myKey', ref: 'myRef', refInFor: true },this.$slots.default ); }, methods:{ clickHandler:function () { console.log("clicked") }, nativeClickHandler: function () { console.log("native clicked") } } }); var app = new Vue({ el: "#app" }) </script>
这里的第一个参数是h1
,表示是一个h1
标题元素,第二个参数就是属性对象,里边包含了所有可以作用在元素上的渲染内容:
class
,类似v-bind:class的属性写法,控制CSS类style
,类似v-bind:style的属性写法,控制内联样式attrs
,标准HTML属性domProps
,DOM节点属性,比如可以设置innerHTML,不过这里要渲染插槽,所以没有插入具体内容。props
,定义组件的props。不过这里我定义了,但是传入似乎没效果。不过由于是组件,可以直接在外边定义。现在还没搞清楚on
,监听事件,可以监听$emit抛出的自定义事件。但是这里不支持直接使用修饰符,必须在事件处理方法里判断。nativeOn
,监听原生事件directives
,自定义指令的相关内容,写了这些就等于在元素上使用了自定义指令。scopedSlots
,作用域插槽,先这么了解一下slot
,如果当前组件还是其他组件的子组件,需要给插槽指定名称。key
,元素的key值,用于唯一标识ref
,元素的ref属性。如果多个元素都使用了同一个ref,$refs.myRef会成为一个数组。refInFor
,这个官方文档也没解释,不知道了。
这里还在第三个参数中写了一个标准的slot插槽元素,这个会成为h1元素的子元素。这个例子渲染完之后的HTML元素是:
<div id="app"> <h1 id="test" class="title" style="font-size: 30px; text-align: center;">saner</h1>、 </div>
在控制台里则打印了自定义指令的内容,点击标题会发现事件也正常得到处理。
所以creatElement的作用,就是渲染出一段像模板一样可以正常供Vue工作的页面。实际上creatElement返回的是一个Vnode对象,不是真正的DOM元素。Vue实例会完成实际渲染的工作。
在组件树中,所有的组件都必须唯一,不能直接简单复用组件,那样只会渲染出一个组件。
但是在Vue的文档
里,提到如下的渲染函数不合法:
render: function (createElement) { var myParagraphVNode = createElement('p', 'hi') return createElement('div', [ // 错误 - 重复的 VNode myParagraphVNode, myParagraphVNode ]) }
但是奇怪的是我竟然可以渲染出两个元素。。。。难道Vue 2.6.10有所改动?
另外这里还展示了render函数可以不传第二个参数,直接传第一个和数组参数,也是可以的。
事件修饰符和按键修饰符
在render里无法使用修饰符,所以必须要通过事件处理函数的event属性来调用。一些事件和按键修饰符对应关系如下:
修饰符 | 调用方法 |
---|---|
.stop |
event.stopPropagation() |
.prevent |
event.preventDefault() |
.self |
if (event.target != event.currentTarget) return |
.enter.13 |
if(event.keyCode != 13) return ,判断按钮 |
.ctrl 、.alt 、.shift 、.meta |
if (!event.ctrlKey) return 根据需要替换ctrlKey 为altK、shiftKey 或metaKey ,也是用于判断按钮 |
对于.capture
,.once
可以使用特殊前缀,写在on的事件名称字符串里即可:
修饰符 | 前缀 |
---|---|
.capture |
! |
.once |
~ |
.capture.once 或.once.capture |
~! |
现在基本上就把Vue的内容都学完了,后边是Webpack和Vue的其他生态了。想要做一个SPA,还真的没那么简单哦。