Vue
课程面向对象:有学习、使用过 vue 的前端工程师
课前准备:VsCode、Vue 2.6.14 源码
希望可以通过本次课程,让大家不仅会用 vue,而且能了解到它的一部分运行机制
1. 组件通讯
除了常见的 bind / emit 、vuex 之外还有些比较好用的组件通讯方式
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 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 中, mixins 与 extends 的 options 采用了一样的合并策略: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. 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> <el-button @click="handleChange">更新 a,b</el-button> </p><br/> <p> <label>响应式更新</label> <el-button @click="handleActive">更新 a,b</el-button> </p><br/> <p> <label>响应式更新 set delete</label> <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提供响应式属性时,可以指定为data或props的内容?
7. 创建 vue 实例的执行顺序
- import Vue
- 注册实例的
_init方法、props 属性、delete、$watch 方法 - 注册实例的 once、emit、
_update、destroy 方法 - 注册实例的 $nextTick、
_render方法 - 注册 Vue 全局方法 set、del、nextTick、observable、全局属性 options、全局方法 use、mixin、extend、component、directive、filter
- 注册实例的
- Events & Lifecycle
- 初始化 parent,refs,
_watcher,_inactive,_directInactive,_isMounted,_isDestroyed,_isBeingDestroyed,_events,_hasHookEvent,scopedSlots,attrs,$listeners
- 初始化 parent,refs,
- Injections & Reactivity
- 初始化 inject、 props、methods、data(响应式data)、
_data、computed、watch、provide
- 初始化 inject、 props、methods、data(响应式data)、
8. 使用 VsCode 调试 vue 源码
- 从
GitHub下载一份vue2源码 - 在打包后的源码文件
dist/vue.js中进行断点标记 - 打开
examples文件夹下任意用例的index.html文件,修改vue引入script的src属性为'../../dist/vue.js' - 点击
VsCode左侧功能面板的运行和调试工具
9. 组件封装
在某些业务场景中,实现 可拓展 、高内聚 、低耦合 的组件。
- 业务场景分析
- 模块划分(解耦、提高代码可读性)
- 单一功能组件编写(内聚)
- 组合功能组件实现满足一定业务场景的组件
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>