Vue.js 进阶技巧 - 组件挂载($mount)/继承(extend)与组件渲染函数(render)

1,223 阅读1分钟

Vue 组价挂载的常用方式

常规挂载的弊端

  1. 组件的模板是通过调用接口从服务度获取的,组件需要动态渲染;
  2. 组建挂载的位置在入口组件外,如body层,此时就得用其他挂载方式了;

Vue.extend 与 $mount简介

Vue.extend

Vue.extend其实就是类的继承的意思,是一种寄生组合式的继承,其作用就是基于Vue构造器,创建一个子类,参数和new Vue的基本一样,data要和内部组件一样,是一个函数,再配合$mount就可以让组件进行渲染,并挂载到任意指定节点上;

// 创建构造器
var Profile = Vue.extend({
  template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
  // 一个类肯定可以创建多个实例,而在vue的內部它是直接将传入的option配置对象的data作为数据来源,即状态。为了保持每个Profile实例组件都独立,就得用函数返回一个对象进行独立化
  data: function () {
    return {
      firstName: 'Walter',
      lastName: 'White',
      alias: 'Heisenberg'
    }
  }
})
// 创建 Profile 实例,并挂载到一个元素上。
var profile1 = new Profile().$mount('#mount-point')

var profile2 = new Profile().$mount('#mount-point')

var profile3 = new Profile().$mount('#mount-point')

方式二

import CustomControlBar from '@/componeents/customControlBar.vue'
// CustomControlBar 内部逻辑和常规的Vue组件定义方式一致
const placeholder = document.querySelector('.xgplayer-controls')
const ControlBar = Vue.extend(CustomControlBar)
const Component = new ControlBar({
  propsData: {
    giftShow: this.giftShow,
    danmuShow: this.danmuShow,
    eventBus: this.eventBus,
    isLogin: this.isLogin,
  },
}).$mount()
placeholder.appendChild(Component.$el)

普通的 new 实例化时传入el配置

  • 如果new Vue时候的option的el属性存在,那么它会自动的绑定并渲染到页面上
new Vue({
  el: '#app',
  // el: document.getElementById('app') 
  template: `<div id="app">Lbxin{{ message }}</div>`,
  data () {
    return {
      message: "_11"
    };
  }
})

// main.js 方式挂载
import Vue from 'vue';
import App from './app.vue';
new Vue({
  el: '#app',
  render: h => h(App)
});

new 实例化时不传 el 配置,创建完后手动挂载

var vm = new Vue({ //没有el选项
  template: `<div id="page-container">Lbxin</div>`
})
// 没有传el,模板将被初始化渲染为DOM之外的元素
vm.$mount('#app') //根据mount挂载的配置进行绑定渲染

new 实例化和创建完后手动挂载都不传 el 配置

var vm = new Vue({ //没有el选项
  template: `<div id="page-container">Lbxin</div>`
})

vm.$mount() //没有传递el配置  渲染在内存中  此时可以通过vm.$el进行指定节点的打印  但DOM中不存在
document.body.appendChild(vm.$el)  //通过将其打入DOM中进行绑定渲染

$mount 快捷挂载方式

// 在 $mount ⾥写参数来指定挂载的节点
new AlertComponent().$mount('#app');
// 不⽤ $mount,直接在创建实例时指定 el 选项
new AlertComponent({ el: '#app' });
配合render函数进行渲染后指定挂载

在Vue中是使用模板HTML语法组建页面的,使用render函数就可以用js语言来构建页面DOM;Vue是虚拟DOM,在拿到template模板时也要转义成VNode的函数,当使用render构建DOM时也就免去了Vue的转义过程

import Vue from "vue";
import toast from "./toast.vue";
const props = {
  message: "账号被封禁,请联系客服人员",
  icon: "forbid",
  errno: 80002
}; 
const ToastTem = new Vue({
  render(h) {
    return h(toast, {
      props,
    });
  },
});
const component = ToastTem.$mount();
document.body.appendChild(component.$el);
// 获取组件实例
const toastExam = ToastTem.$children[0]


// render返回多个DOM组件
// render: (h, params) => {
//   var arr = params.row.policyFile.split(";");
//   return h(
//     "ul",
//     arr.map(function (item, index) {
//       return h(
//         "a",
//         {
//           domProps: {
//             href: item,
//             target: "_black",
//           },
//           style: {
//             marginRight: "5px",
//           },
//         },
//         "文件" + parseInt(index + 1)
//       );
//     })
//   );
// };

渲染挂载后进行销毁

采用$mount手动渲染的组件,如果要进行销毁操作,需要进行DOM级别的removeChild和Vue提供的$destroy进行手动销毁实例,最好将组件的实例进行赋值为null

export default {
  methods: {
    destroyCode() {
      const $target = document.getElementById(this.id);
      if ($target) $target.parentNode.removeChild($target);
      if (this.component) {
        this.$refs.display.removeChild(this.component.$el);
        this.component.$destroy();
        this.component = null;
      }
    },
  },
  beforeDestroy() {
    this.destroyCom();
  },
};

Vue3兼容

Vue的构建方式有两种,独立构建(standalone)和运行时构建(runtime-only),但在Vue3中只有运行时,不允许编译template模板,所以在Vue3中需要单独配置或者转化,通常为配置化进行结局

// vue.config.js
module.exports = {
  runtimeCompiler: true
};
// 此配置会导致应用额外增加10kb左右

render 渲染函数与 function render

在vue2中使用了Virtual DOM来更新DOM组件,提升渲染性能;在常规的vue组件开发中,模板都是写在template中,但在真正编译阶段会被解析为 Virtual DOM(基于JavaScript计算的,开销相对较小);

Virtual DOM运算过程.png

// Virtual DOM
const vNode = {
  tag: "div",
  attributes: {
    id: "app",
  },
  children: [
    // ......
  ],
};

h函数 - Render函数的核心

  • h函数即createElement,是Render的核心,其包括三个参数,分别是
    • 要渲染的元素或组件,可以是一个HTML标签、组件或者一个函数,次配置为必填
    // 1. html 标签
    h("div");
    // 2. 组件选项
    import Top from "../component/top.vue";
    h(Top);
    
    • 组件属性的数据对象,如组件的常规样式、id、props、事件、自定义指令等
      官方配置文档
    h("button", { onClick: () => this.counter++, class: "button" }, "加 1"),
    
    • 子节点,类型是String或Array,同样是一个h函数
    [
      "头部组件",
      h("p", "注册"),
      h(Component, {
        props: {
          userInfo: {},
        },
      }),
    ];
    
虚拟节点是组件或含有组件的slot - 循环和工厂函数

在h函数中,如果vNode是组件或者含有组件的slot,那么vNode必须是唯一的,如需要重复渲染同一个组件元素,可以通过一个循环和工厂函数来解决; 通过循环和工厂函数的使用,将原本相同的组件都克隆一份,包含其中的关键属性的复制

函数组件和插槽

子组件的渲染有时候需要由父组件进行动态DOM或组件相应的传入,这时就需要用到插槽来实现;

// 父组件
<script>
import { h, ref } from "vue";
import Test from "./components/Test.vue"
export default {
    setup() {
        return {
           age: 123
        }
    },
    render() {
        return h(Test, null, {
            // default 对应的是一个函数,default是默认插槽
            default: props => h("span", null, "父组件动态内容传递:" + props.name + "age: " + props.age||this.age)
        })
    }
}
</script>

<script>


// 子组件
import { h } from "vue";
export default {
    render() {
        return h("div", null, [
            h("div", null, "我是子组件"),
            /**
             * 可以使用自定义插槽或者默认的default插槽进行展示
             * 也可以传入一个参数,使用的是 函数传参
             */
            this.$slots.default ? this.$slots.default({ name: "Lbxin" }) : h("div", null, "我是子组件的默认值")
        ])
    }
}
</script>
@vue/babel-plugin-jsx

使用 @vue/babel-plugin-jsx 在Render中使用jsx语法进行编写,通过配置Babel进行语法转义,便于开发;

// babel.config.js
module.exports = {
  presets: ["@/vue/cli-plugin-babel/preset"],
  plugins: ["@vue/babel-plugin-jsx"],
};


// 使用
render() {
  return (
    <div>
      <div>JSX的使用</div>
      <h2>当前数字:{this.counter}</h2>
    </div>
  )
}

Render的使用场景分析

  1. 需要用到多个相同的slot,常规的就是采用命名插槽进行解决,当然也可以使用深度克隆的方法进行解决
  2. 服务端渲染的方式进行渲染界面
  3. 运行时版本的vue中,不允许使用template进行编译渲染
  4. 具有复杂结构或样式时(可以用v-html进行渲染,但是只能解析常规的HTML字符串且存在XSS的分险)、动态的组件渲染中,普通的slot等方法是无法实现的,比如复杂table中的动态表单功能,一种是使用render,另一种是使用多个作用域插槽实现

Functional Render 函数化组件渲染

Vue.js 提供了⼀个 functional 的布尔值选项,设置为 true 可以使组件⽆状态和⽆实例,也就是没有 data和 this 上下⽂。这样⽤Render 函数返回虚拟节点可以更容易渲染,因为函数化组件(Functional Render)只是⼀个函数,渲染开销要⼩很多。

函数式组件的优点

  1. 渲染开销很小(通过原生js进行渲染,本质上只是函数)
    • 没有状态,不用像vue的响应式需要经过额外的初始化操作
  2. 便于包装组件
    • 程序化的在多个组件中选择一个来代为渲染
    • 在将children、props、data传递给子组件前操作它们

函数式组件的特点

  1. 没有状态
    • 没有响应式数据 - 不用管理任何状态
    • 也不监听任何传递给他的状态
  2. 没有实例
    • 没有this上下文
    • 事件只能通过父组件传递
    on: {
        click: context.listeners.click
    },
    
  3. 没有生命周期函数
  4. 只接受props参数
    可以利用函数式组件的特性,将其做成高阶组件(High order components),即可以生成其他组件的组件
    Functional Render 没有上下文一说,主要用于中转一个组件

函数式组件的适用场景分析

  • 目标是一个简单的展示组件,也就是所谓的dumb组件爱你,如tags、text、静态页面等
  • 高阶组件,用于接受一个组件作为参数,返回一个被包裹过的组件
  • v-for中每项通常都是很好的候选项

函数化组件

  • 使用函数化组件,render函数提供了第二个参数context来提供临时上下文。通过该参数来实现组件间的数据传递,如data、props、slots等;
Vue.component('my-component', {
    functional: true,
    // Props 是可选的
    props: {
        // ...
    },
    // 为了弥补缺少的实例
    // 提供第二个参数作为上下文
    render: function (createElement, context) {
        return createElement(){
            'button',
            on: {
                click: context.listeners.click
            },
            // ...
        }
        
    }
})
  • 函数化组件的使用场景
    • 程序化的在多个组件中选择一个
    • 在将children、props、data传递给子组件之前进行处理
    • 可以定义一个基类的中间文件,进行传统组件的创建逻辑复用;
export default {
  functional: true,
  props: {
    render: Function,
  },
  render: (h, ctx) => {
    return ctx.props.render(h);
  },
};


<!-- my-component.vue -->
<template>
    <div>
        <Render :render="render"></Render>
    </div>
</template>
<script>
import Render from './render.js';
export default {
    components: { Render },
    props: {
        render: Function
    }
}
</script>


<!-- demo.vue -->
<template>
    <div>
        <my-component :render="render"></my-component>
    </div>
</template>
<script>
import myComponent from '../components/my-component.vue';
export default {
    components: { myComponent },
    data() {
        return {
            render: (h) => {
                return h('div', {
                    style: {
                        color: 'red'
                    }
                }, '⾃定义内容');
            }
        }
    }
}
</script>

案例浅析

<div id="app">
  <top :topInfo="topInfo" />
</div>

<script>
  Vue.component('top', {
    functional: true,  // 开启函数组件配置
    props: {
      topInfo: { 
        type: Array, 
        required: true 
      },
      size: {
        required: true, 
        validator(value) {
          return oneOf(value, ["small", "large", "mini"]);
        },
        default: "small"
      }
    },
    render(createElement, context) {
      let userName = createElement(
        "div",
        {
          style: {
            width: "450px",
            height: "30px",
            color: "blue"
          }
        },
        context.props.listData[0].userName
      );
      let userButton = createElement(
        "button",
        {
          style: {
            size: context.props.size,
            border: "1px solid #f5f6f7"
          }
        },
        context.props.listData[0].button
      )
      let userAge = createElement(
        "div",
        {
          style: {
            width: "450px",
            height: "30px",
            color: "#ccc"
          }
        },
        context.props.listData[0].userAge  // content is passed down by context
      );

      return createElement(
        "div",
        {
          style: {
            width: "1000px",
            height: "300px"
          }
        },
        [userName, userButton, userAge]
      )
    }
  })

  const app = new Vue({
    data() {
      return {
        topInfo: [
          {
            userName: "Lbxin",
            userAge: 22,
            button: "logOut"
          },
          {
            shopName: "官方店铺",
            shopCount: 10
          }
        ]
      }
    }
  }).$mount("#app");
</script>

函数式组件-官方文档
深入理解Vue.js实战

参考文献

Vue.js 2.0 独立构建和运行时构建的区别