1. Virtual Dom
  2. Render函数例子
  3. createElement
  4. 事件修饰符和按键修饰符

Virtual Dom 虚拟DOM节点

一般来说,我们的组件的模板都是写在template元素里的,当然也可以写在其他的元素里。不管什么元素,都会被Vue解析成Virtual Dom。

更新DOM的操作要比执行JS代码开销大很多,所以Vue在更新页面的时候,实际上先对Virtual
Dom进行计算,看是否有变化,然后渲染有变化的DOM部分,而不是整个页面重绘,这样的单页面应用要比从服务器获取静态页面速度好很多。

在自定义指令的例子里,打印vnode参数在控制台里看到VNode对象,这个就是Vue的虚拟DOM节点对象。这个对象用一些属性封装了DOM节点的一些内容

其中的一堆属性里比较重要的有两个:

  1. tag,当前节点的标签名称,像例子里指令绑定的是div元素,就会是div字符串。
  2. data,当前节点的数据对象,Vue源码中对应的类叫做VNodeData类。
  3. children,子节点数组,其中元素也全部是VNode类型
  4. text,当前节点的文本,一般文本和注释节点有此属性
  5. elm,即element,当前虚拟节点对应的真实DOM节点
  6. ns,节点的命名空间
  7. content,编译作用域
  8. functionalContext,函数化组件的作用域
  9. key,节点的key属性,用于唯一标识,方便更新DOM
  10. componentOptions,创建组件实例用到的设置
  11. child,当前节点对应的组件实例
  12. parent,组件的占位节点
  13. raw,原始HTML
  14. istatic,是否是静态节点
  15. isRootlnsert,是否作为根节点插入
  16. isComment,是否是注释节点
  17. isCloned,是否是克隆节点
  18. isOnce,当前节点是否存在V-once指令

例子里的data点开之后,由于这是一个指令,能看到其中有一个directives: Array(1),其中存放着指令的参数,表达式等一系列内容。

Vnode可以分为如下几类:

  1. TextVNode,文本节点
  2. ElementVNode,普通元素节点
  3. ComponentVNode,组件节点
  4. EmptyVNode,无内容的注释节点
  5. 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有三个参数,依次是:

  1. {String | Object | Function),一个HTML标签名称字符串,或者组件,或者一个函数。是必须传入的参数。
  2. {AttrObject),属性数据对象,后边会详细解释,可选参数。
  3. {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标题元素,第二个参数就是属性对象,里边包含了所有可以作用在元素上的渲染内容:

  1. class,类似v-bind:class的属性写法,控制CSS类
  2. style,类似v-bind:style的属性写法,控制内联样式
  3. attrs,标准HTML属性
  4. domProps,DOM节点属性,比如可以设置innerHTML,不过这里要渲染插槽,所以没有插入具体内容。
  5. props,定义组件的props。不过这里我定义了,但是传入似乎没效果。不过由于是组件,可以直接在外边定义。现在还没搞清楚
  6. on,监听事件,可以监听$emit抛出的自定义事件。但是这里不支持直接使用修饰符,必须在事件处理方法里判断。
  7. nativeOn,监听原生事件
  8. directives,自定义指令的相关内容,写了这些就等于在元素上使用了自定义指令。
  9. scopedSlots,作用域插槽,先这么了解一下
  10. slot,如果当前组件还是其他组件的子组件,需要给插槽指定名称。
  11. key,元素的key值,用于唯一标识
  12. ref,元素的ref属性。如果多个元素都使用了同一个ref,$refs.myRef会成为一个数组。
  13. 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,还真的没那么简单哦。