vue课堂小知识学习记录

108 阅读2分钟

Vue

课程面向对象:有学习、使用过 vue 的前端工程师

课前准备:VsCodeVue 2.6.14 源码

希望可以通过本次课程,让大家不仅会用 vue,而且能了解到它的一部分运行机制

1. 组件通讯

除了常见的 bind / emitvuex 之外还有些比较好用的组件通讯方式

1.1 provide, inject

跨组件层级通讯的利器,由祖先组件 provide ,祖先内任一组件 inject

<script>
// parent
export default {
  provide: {
    ui: {
      size: 'small',
      isTrue: true
    }
  } // 或者是一个 function(此时可以使用内部响应式属性)
}
</script>
<script>
// child
export default {
  inject: ['ui'] // 或者是一个对象 { ui } | { ui: { from: '', default: '' } }
}
</script>

1.2 eventBus

组件通讯中间件

import Vue from 'vue'
const eventBus = new Vue()

export const emit = (eventname, ...args) => {
  eventBus.$emit(eventname, ...args)
}

export const on = (eventname, callback) => {
  eventBus.$on(eventname, callback)
}

export const off = (eventname, callback) => {
  eventBus.$off(eventname, callback)
}

1.3 Vue.observable v2.6.0+

根据原有对象创建并返回一个响应式对象

import Vue from 'vue'

const state = Vue.observable({
  count: 0,
  isTrue: false
})

const mutation = {
  ADD_COUNT: function(state) {
    state.count += 1
  }
}

const commit = (mutation_name, ...args) => mutation[mutation_name](state, ...args)

export default {
  state,
  commit
}

1.4 refs,refs, parent

<template>
	<div>
    <child ref="child" />
    msg from child: {{ msg }}
  </div>
</template>

<script>
import child from './child'
export default {
  name: 'parent',
  components: { child },
  data() {
    return {
      msg: []
    }
  },
  mounted() {
    // $refs
    this.$refs['child'].handleShow()
  },
  methods: {
    onMsg(msg) {
      this.msg.push(msg)
    }
  }
}
</script>
<template>
	<div>
    show: {{ show.toString() }}
    <!-- $parent -->
    <button @click="() => $parent.onMsg('hi!')">send msg</button>
  </div>
</template>

<script>
export default {
  name: 'child',
  data() {
    return {
      show: false
    }
  },
  methods: {
    handleShow() {
      this.show = !this.show
    }
  }
}
</script>

1.5 小结

  • provide, inject 比较适合于跨层级组件传参
  • eventBus 属于事件订阅、派发的工作形式
  • observable 个人认为像是小型的 vuex,区别在于可以直接修改 state
  • $refs / $parent 在具有 父子关系 的组件间使用;更多时候用来调用 父/子 组件的 内部方法,而不应该直接修改内部状态

2. 自定义指令

官方文档: cn.vuejs.org/v2/guide/cu…

/**
 * @param el 指令所绑定的元素,可以用来直接操作 DOM。
 * @param binding
 *  {
 *    name: 指令名,不包括 v- 前缀。
 *    value: 指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。
 *		oldValue: 指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
 *    expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。
 *    arg: 传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。
 *    modifiers: 一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。
 *  }
 * @param vnode Vue 编译生成的虚拟节点。
 * @param oldVnode 上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。
 * @descrip 除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行
 */

export default {
  // 指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置
  bind: function (el, binding) {
    // console.log('binding:', binding)
  },
  // 被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)
  inserted: function (el, binding) {
    el.firstElementChild.focus()
    // console.log('inserted:', binding)
  },
  // 所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有
  // 此时可以通过比较 binding.value 与 binding.oldValue 判断是否更改
  update: function (...args) {
    // console.log('update', ...args)
  },
  // 指令所在组件的 VNode 及其子 VNode 全部更新后调用
  componentUpdated: function (...args) {
    // console.log('componentUpdated', ...args)
  },
  // 只调用一次,指令与元素解绑时调用
  unbind: function (...args) {
    // console.log('unbind', ...args)
  }
}

3. 插槽

在 vue 中分为 匿名插槽具名插槽 。在提供 name 属性后为具名插槽

<!-- 匿名插槽默认命名 default -->
<slot />

<!-- 等同于上面的 -->
<slot name="default" />

插槽默认内容

<!-- 子组件 -->
<slot>
	<span>这是默认内容</span>
</slot>
<!-- 父组件 -->
<child>
	<p
</child>

插槽传参(子传父)

<!-- 子组件 -->
<slot name="yyds" msg="default" :count="count" />
<!-- 父组件 -->
<child>
	<template v-slot:yyds="{msg, count}">
  	<span>{{msg}}</span>
		<span>{{count}}</span>
  </template>
</child>

slots & scopedSlots

render 函数中很常见,常用来拓展组件的功能

  • $slots 是一个 name: component 键值对 对象
  • $scopedSlots 是一个 name: (props) => component(props) 键值对 对象,也就是说可以在 render 函数中传递 props

4. mixins、extends

vue 中, mixinsextendsoptions 采用了一样的合并策略:mergeOptions

extend 通过 js 组合继承实现

const Sub = function VueComponent (options) {
  this._init(options)
}
// 组合继承
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.options = mergeOptions(
  Super.options,
  extendOptions
)
Sub['super'] = Super

// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
  initProps(Sub)
}
if (Sub.options.computed) {
  initComputed(Sub)
}

// allow further extension/mixin/plugin usage
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use

// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
  Sub[type] = Super[type]
})
// enable recursive self-lookup
if (name) {
  Sub.options.components[name] = Sub
}

5. setset、delete

为了弥补 Object.defineProperty() 无法 get 到 对象删除属性、添加新属性、数组的增删改查的问题。

Vue 添加了 set、delete 方法,劫持了数组的一些原生方法

  • 无法响应式监听一个未注册的属性值;无法响应式监听数组项修改

    <template>
      <div>
        <p>
          this.a: {{ JSON.stringify(a) }}<br/><br/>
          this.b: {{ JSON.stringify(b) }}
        </p><br/>
        <p>
          <label>非响应式更新</label>&nbsp;
          <el-button @click="handleChange">更新 a,b</el-button>
        </p><br/>
        <p>
          <label>响应式更新</label>&nbsp;
          <el-button @click="handleActive">更新 a,b</el-button>
        </p><br/>
        <p>
          <label>响应式更新 set delete</label>&nbsp;
          <el-button @click="handleSet">更新 a,b</el-button>
        </p>
      </div>
    </template>
    <script>
    export default {
      name: 'SetDeltete',
      data() {
        return {
          a: { a: 1 },
          b: [1, 2],
          count: 2
        }
      },
      methods: {
        handleChange() {
          if (this.a.b) {
            delete this.a.b
          } else {
            this.a.b = -1
          }
          this.b[1] += 1
        },
        handleActive() {
          this.b.push(-1)
          this.a.a += 1
          this.b.shift()
        },
        handleSet() {
          if (this.a.c) {
            this.$delete(this.a, 'c')
          } else {
            this.$set(this.a, 'c', this.count)
          }
          this.$set(this.b, 0, this.count++)
        }
      }
    }
    </script>
    

6. 灵魂拷问

  • 为什么 provide 提供响应式属性时,可以指定为 dataprops 的内容?

7. 创建 vue 实例的执行顺序

  • import Vue
    • 注册实例的 _init 方法、datadata、props 属性、setset、delete、$watch 方法
    • 注册实例的 onon 、once、offoff、emit、_updateforceUpdateforceUpdate、destroy 方法
    • 注册实例的 $nextTick、_render 方法
    • 注册 Vue 全局方法 set、del、nextTick、observable、全局属性 options、全局方法 use、mixin、extend、component、directive、filter
  • Events & Lifecycle
    • 初始化 options(这里vue会进行一系列的选项合并),selfoptions(这里 vue 会进行一系列的选项合并),`_self`,parent,rootroot, refs, _watcher_inactive_directInactive_isMounted_isDestroyed_isBeingDestroyed_events_hasHookEventslotsslots,scopedSlots,createElementcreateElement,attrs,$listeners
  • Injections & Reactivity
    • 初始化 inject、 props、methods、data(响应式data)、_data、computed、watch、provide
lifecycle

8. 使用 VsCode 调试 vue 源码

  • GitHub 下载一份 vue2 源码
  • 在打包后的源码文件 dist/vue.js 中进行 断点标记
  • 打开examples 文件夹下任意用例的 index.html 文件,修改 vue 引入 scriptsrc 属性为 '../../dist/vue.js'
  • 点击 VsCode 左侧功能面板的 运行和调试 工具

9. 组件封装

在某些业务场景中,实现 可拓展高内聚低耦合 的组件。

  1. 业务场景分析
  2. 模块划分(解耦、提高代码可读性)
  3. 单一功能组件编写(内聚)
  4. 组合功能组件实现满足一定业务场景的组件

render

<script>
export default {
  render(h) {
    return h(
      // html 标签
    	'div',
      // 属性对象,可选参数
      {
        class: 'box',
        on: {
          click: this.onClick
        }
      },
      // 子集虚拟节点(由 h 构建),
      // 也可以直接是一个文本字符串
      [
        h('h1', 'welcome'),
        'hello world'
      ]
    )
  },
  methods: {
    onClick() {
      console.log('clicked!')
    }
  }
}
</script>

render + jsx

个人比较推荐的封装使用方式,尤其在插槽的内容封装时比较好用

<script>
export default {
  render() {
    return (
    	<div class="box" onclick={this.onClick}>
      	<h1>welcome</h1>
        hello world
      </div>
    )
  },
  methods: {
    onClick() {
      console.log('clicked!')
    }
  }
}
</script>