# vue 学习笔记
# 运⾏环境
- node 12.x
- vue.js 2.6.x
- vue-cli 4.x
# 组件化
vue 组件系统提供了⼀种抽象,让我们可以使⽤独⽴可复⽤的组件来构建⼤型应⽤,任意类型的应⽤界⾯都可以抽象为⼀个组件树。组件化能提⾼开发效率, ⽅便重复使⽤, 简化调试步骤, 提升项⽬可维护性, 便于多⼈协同开发。
# 通信方式
组件通信常⽤⽅式
主要分为三类通信:父子组件,隔代组件,兄弟组件
props
/$emit
(父子组件通信)EventBus
(父子组件,隔代组件,兄弟组件)vuex
(父子组件,隔代组件,兄弟组件)$refs
与$parent/$children
(父子组件)provide
/inject
(隔代组件)$attrs
/$listeners
(隔代组件)
# props/$emit
⽗给⼦传值
// child
props: {
msg: String
}
// parent
;<HelloWorld msg="Welcome to Your Vue.js App" />
2
3
4
5
6
7
⼦给⽗传值
// child this.$emit('add', good) // parent <Cart @add="cartAdd($event)"></Cart>
# EventBus
任意两个组件之间传值常⽤事件总线 或 vuex 的⽅式。
// Bus:事件派发、监听和回调管理
class Bus {
constructor() {
this.callbacks = {}
}
$on(name, fn) {
this.callbacks[name] = this.callbacks[name] || []
this.callbacks[name].push(fn)
}
$emit(name, args) {
if (this.callbacks[name]) {
this.callbacks[name].forEach((cb) => cb(args))
}
}
}
// main.js
Vue.prototype.$bus = new Bus()
// child1
this.$bus.$on('foo', handle)
// child2
this.$bus.$emit('foo')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
实践中通常⽤ Vue 代替 Bus,因为 Vue 已经实现了相应接⼝
# vuex
创建唯⼀的全局数据管理者 store,通过它管理数据并通知组件状态变更。
# $refs
与$parent
/$children
ref 用在普通元素上表示 DOM 元素,用在组件上表示组件实例
兄弟组件之间通信可通过共同祖辈搭桥, $parent
或$root
。
// parent
<HelloWorld ref="hw"/>
mounted() {
this.$refs.hw.xx = 'xxx'
}
2
3
4
5
// brother1
this.$parent.$on('foo', handle)
// brother2
this.$parent.$emit('foo')
2
3
4
⽗组件可以通过$children
访问⼦组件实现⽗⼦通信。
// parent
this.$children[0].xx = 'xxx'
2
注意:
$children
不能保证⼦元素顺序和$refs
有什么区别?//$refs
是对象形式,$chilren
是数组形式
# $attrs
/$listeners
包含了⽗作⽤域中不作为 prop 被识别 (且获取) 的特性绑定 ( class
和 style
除外)。当⼀个组件没有声明任何 prop 时,这⾥会包含所有⽗作⽤域的绑定 ( class
和 style
除外),并且可以通过vbind="$attrs"
传⼊内部组件——在创建⾼级别的组件时⾮常有⽤。
// child:并未在props中声明foo
<p>{{$attrs.foo}}</p>
// parent
<HelloWorld foo="foo"/>
2
3
4
# provide
/inject
能够实现祖先和后代之间传值
// ancestor
provide() {
return {foo: 'foo'}
}
// descendant
inject: ['foo']
2
3
4
5
6
# 插槽
插槽语法是 Vue 实现的内容分发 API,⽤于复合组件开发。该技术在通⽤组件库开发中有⼤量应⽤。
# 匿名插槽
// comp1
<div>
<slot></slot>
</div>
// parent
<comp>hello</comp>
2
3
4
5
6
# 具名插槽
将内容分发到⼦组件指定位置
// comp2
<div>
<slot></slot>
<slot name="content"></slot>
</div>
// parent
<Comp2>
<!-- 默认插槽⽤default做参数 -->
<template v-slot:default>具名插槽</template>
<!-- 具名插槽⽤插槽名做参数 -->
<template v-slot:content>内容...</template>
</Comp2>
2
3
4
5
6
7
8
9
10
11
12
# 作⽤域插槽
分发内容要⽤到⼦组件中的数据
// comp3
<div>
<slot :foo="foo"></slot>
</div>
// parent
<Comp3>
<!-- 把v-slot的值指定为作⽤域上下⽂对象 -->
<template v-slot:default="slotProps">
来⾃⼦组件数据: {{slotProps.foo}}
</template>
</Comp3>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# vue-router3.x 源码解析
# 需求分析
- 作为⼀个插件存在:实现 VueRouter 类和 install ⽅法
- 实现两个全局组件: router-view ⽤于显示匹配组件内容, router-link ⽤于跳转
- 监控 url 变化:监听 hashchange 或 popstate 事件
- 响应最新 url:创建⼀个响应式的属性 current,当它改变时获取对应组件并显示
实现⼀个插件:创建 VueRouter 类和 install ⽅法
# 创建 vue-router.js
let Vue // 引⽤构造函数, VueRouter中要使⽤
// 保存选项
class VueRouter {
constructor(options) {
this.$options = options
}
}
// 插件:实现install⽅法,注册$router
VueRouter.install = function(_Vue) {
// 引⽤构造函数, VueRouter中要使⽤
Vue = _Vue
// 任务1:挂载$router
Vue.mixin({
beforeCreate() {
// 只有根组件拥有router选项
if (this.$options.router) {
// vm.$router
Vue.prototype.$router = this.$options.router
}
}
})
// 任务2:实现两个全局组件router-link和router-view
Vue.component('router-link', Link)
Vue.component('router-view', View)
}
export default VueRouter
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
为什么要⽤混⼊(mixin)⽅式写?
主要原因是 use 代码在前, Router 实例创建在后,⽽ install 逻辑⼜需要⽤到该实例
# 创建 router-view 和 router-link
// router-link.js
export default {
props: {
to: String,
required: true
},
render(h) {
// return <a href={'#'+this.to}>{this.$slots.default}</a>;
return h(
'a',
{
attrs: {
href: '#' + this.to
}
},
[this.$slots.default]
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// router-view.js
export default {
render(h) {
// 暂时先不渲染任何内容
return h(null)
}
}
2
3
4
5
6
7
# 监控 url 变化
定义响应式的 current 属性,监听 hashchange 事件
// vue-router.js
class VueRouter {
constructor(options) {
// current应该是响应式的
// Vue.util.defineReactive(this, 'current', '/')
// 定义响应式的属性current
const initial = window.location.hash.slice(1) || '/'
Vue.util.defineReactive(this, 'current', initial)
// 监听hashchange事件
window.addEventListener('hashchange', this.onHashChange.bind(this))
window.addEventListener('load', this.onHashChange.bind(this))
}
onHashChange() {
this.current = window.location.hash.slice(1)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
动态获取对应组件
// router-view.js
export default {
render(h) {
// 动态获取对应组件
let component = null
this.$router.$options.routes.forEach((route) => {
if (route.path === this.$router.current) {
component = route.component
}
})
return h(component)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
提前处理路由表避免每次都循环
//vue-router.js
class VueRouter {
constructor(options) {
// 缓存path和route映射关系
this.routeMap = {}
this.$options.routes.forEach((route) => {
this.routeMap[route.path] = route
})
}
}
2
3
4
5
6
7
8
9
10
// router-view.js
export default {
render(h) {
const { routeMap, current } = this.$router
const component = routeMap[current] ? routeMap[current].component : null
return h(component)
}
}
2
3
4
5
6
7
8
# vuex3.x 源码解析
Vuex 集中式存储管理应⽤的所有组件的状态,并以相应的规则保证状态以可预测的⽅式发⽣变化
# 核⼼概念
- state 状态、数据
- mutations 更改状态的函数
- actions 异步操作
- store 包含以上概念的容器
# 状态 - state
state 保存应⽤状态
export default new Vuex.Store({
state: { counter: 0 }
})
2
3
# 状态变更 - mutations
mutations ⽤于修改状态, store.js
export default new Vuex.Store({
mutations: {
add(state) {
state.counter++
}
}
})
2
3
4
5
6
7
# 派⽣状态 - getters
从 state 派⽣出新状态,类似计算属性
export default new Vuex.Store({
getters: {
doubleCounter(state) {
// 计算剩余数量
return state.counter * 2
}
}
})
2
3
4
5
6
7
8
# 动作 - actions
添加业务逻辑,类似于 controller
export default new Vuex.Store({
actions: {
add({ commit }) {
setTimeout(() => {
commit('add')
}, 1000)
}
}
})
2
3
4
5
6
7
8
9
测试代码 :
<p @click="$store.commit('add')">counter: {{$store.state.counter}}</p>
<p @click="$store.dispatch('add')">async counter: {{$store.state.counter}}</p>
<p>double: {{$store.getters.doubleCounter}}</p>
2
3
# 任务分析
- 实现⼀个插件:声明 Store 类,挂载$store
- Store 具体实现:
- 创建响应式的 state,保存 mutations、 actions 和 getters
- 实现 commit 根据⽤户传⼊ type 执⾏对应 mutation
- 实现 dispatch 根据⽤户传⼊ type 执⾏对应 action,同时传递上下⽂
- 实现 getters,按照 getters 定义对 state 做派⽣
初始化: Store 声明、 install 实现
// vuex.js
let Vue
class Store {
constructor(options = {}) {
this._vm = new Vue({
data: {
$$state: options.state
}
})
}
get state() {
return this._vm._data.$$state
}
set state(v) {
console.error('please use replaceState to reset state')
}
}
function install(_Vue) {
Vue = _Vue
Vue.mixin({
beforeCreate() {
if (this.$options.store) {
Vue.prototype.$store = this.$options.store
}
}
})
}
export default { Store, install }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
实现 commit:根据⽤户传⼊ type 获取并执⾏对应 mutation
class Store {
constructor(options = {}) {
// 保存⽤户配置的mutations选项
this._mutations = options.mutations || {}
}
commit(type, payload) {
// 获取type对应的mutation
const entry = this._mutations[type]
if (!entry) {
console.error(`unknown mutation type: ${type}`)
return
}
// 指定上下⽂为Store实例
// 传递state给mutation
entry(this.state, payload)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
实现 actions:根据⽤户传⼊ type 获取并执⾏对应 action
class Store {
constructor(options = {}) {
// 保存⽤户编写的actions选项
this._actions = options.actions || {}
// 绑定commit上下⽂否则action中调⽤commit时可能出问题!!
// 同时也把action绑了,因为action可以互调
const store = this
const { commit, action } = store
this.commit = function boundCommit(type, payload) {
commit.call(store, type, payload)
}
this.action = function boundAction(type, payload) {
return action.call(store, type, payload)
}
}
dispatch(type, payload) {
// 获取⽤户编写的type对应的action
const entry = this._actions[type]
if (!entry) {
console.error(`unknown action type: ${type}`)
return
}
// 异步结果处理常常需要返回Promise
return entry(this, payload)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# vue2.x 源码解析
# vue 的设计思想
MVVM 模式
MVVM 框架的三要素: 数据响应式、模板引擎及其渲染
数据响应式:监听数据变化并在视图中更新
- Object.defineProperty()
- Proxy
模版引擎:提供描述视图的模版语法
- 插值: {{}}
- 指令: v-bind, v-on, v-model, v-for, v-if
渲染:如何将模板转换为 html
- 模板 => vdom => dom
# 数据响应式原理
数据变更能够响应在视图中,就是数据响应式。 vue2 中利⽤ Object.defineProperty() 实现变更检测
简单实现
const obj = {}
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`get ${key}:${val}`)
return val
},
set(newVal) {
if (newVal !== val) {
console.log(`set ${key}:${newVal}`)
val = newVal
}
}
})
}
defineReactive(obj, 'foo', 'foo')
obj.foo
obj.foo = 'foooooooooooo'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
结合视图
<!DOCTYPE html>
<html lang="en">
<head></head>
<body>
<div id="app"></div>
<script>
const obj = {}
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`get ${key}:${val}`)
return val
},
set(newVal) {
if (newVal !== val) {
val = newVal
update()
}
}
})
}
defineReactive(obj, 'foo', '')
obj.foo = new Date().toLocaleTimeString()
function update() {
app.innerText = obj.foo
}
setInterval(() => {
obj.foo = new Date().toLocaleTimeString()
}, 1000)
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
遍历需要响应化的对象
// 对象响应化:遍历每个key,定义getter、 setter
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return
}
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key])
})
}
const obj = { foo: 'foo', bar: 'bar', baz: { a: 1 } }
observe(obj)
obj.foo
obj.foo = 'foooooooooooo'
obj.bar
obj.bar = 'barrrrrrrrrrr'
obj.baz.a = 10 // 嵌套对象no ok
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
解决嵌套对象问题
function defineReactive(obj, key, val) {
// 递归处理
observe(val)
Object.defineProperty(obj, key, {
get() {
console.log('get', key)
return val
},
set(newVal) {
if (newVal !== val) {
console.log('set', key, newVal)
observe(newVal) // 新值是对象的情况
val = newVal
}
}
})
}
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return
}
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key])
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
如果添加/删除了新属性⽆法检测
function set(obj, key, val) {
defineReactive(obj, key, val)
}
2
3
defineProperty() 不⽀持数组
# 数据响应化
<--> vue.html </-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<p>{{counter}}</p>
</div>
<script src="node_modules/vue/dist/vue.js"></script>
<script>
const app = new Vue({
el:'#app',
data: {
counter: 1
},
})
setInterval(() => {
app.counter++
}, 1000);
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 原理分析
new Vue() ⾸先执⾏初始化,对 data 执⾏响应化处理,这个过程发⽣在 Observer 中
同时对模板执⾏编译,找到其中动态绑定的数据,从 data 中获取并初始化视图,这个过程发⽣在 Compile 中
同时定义⼀个更新函数和 Watcher,将来对应数据变化时 Watcher 会调⽤更新函数
由于 data 的某个 key 在⼀个视图中可能出现多次,所以每个 key 都需要⼀个管家 Dep 来管理多个 Watcher
将来 data 中数据⼀旦发⽣变化,会⾸先找到对应的 Dep,通知所有 Watcher 执⾏更新函数
涉及类型介绍
- Vue:框架构造函数
- Observer:执⾏数据响应化(分辨数据是对象还是数组)
- Compile:编译模板,初始化视图,收集依赖(更新函数、 watcher 创建)
- Watcher:执⾏更新函数(更新 dom)
- Dep:管理多个 Watcher,批量更新
# Vue
// vue.js
class Vue {
constructor(options) {
// 保存选项
this.$options = options
this.$data = options.data
// 响应化处理
observe(this.$data)
// 代理
proxy(this)
// 编译器
new Compiler('#app', this)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# defineReactive
// 数据响应式
function defineReactive(obj, key, val) {
// 递归处理
observe(val)
// 创建一个Dep实例
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
console.log('get', key)
// 依赖收集: 把watcher和dep关联
// 希望Watcher实例化时,访问一下对应key,同时把这个实例设置到Dep.target上面
Dep.target && dep.addDep(Dep.target)
return val
},
set(newVal) {
if (newVal !== val) {
console.log('set', key, newVal)
observe(newVal)
val = newVal
// 通知更新
dep.notify()
}
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# observe
// 让我们使一个对象所有属性都被拦截
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return
}
// 创建Observer实例:以后出现一个对象,就会有一个Observer实例
new Observer(obj)
}
2
3
4
5
6
7
8
9
# 代理访问
将this.name
代理到this.$data.name
// 代理data中数据
function proxy(vm) {
Object.keys(vm.$data).forEach((key) => {
Object.defineProperty(vm, key, {
get() {
return vm.$data[key]
},
set(v) {
vm.$data[key] = v
}
})
})
}
2
3
4
5
6
7
8
9
10
11
12
13
# 重写数组方法
// 截取改变数组的方法
const originProto = Array.prototype
const arrayProto = Object.create(originProto) //这样调用数组方法时候会顺着原型链先找到我们定义的arrayProto
;['push', 'pop', 'shift', 'unshift'].forEach((method) => {
arrayProto[method] = function() {
console.log('method:' + method)
return Array.prototype[method].apply(this, arguments)
}
})
2
3
4
5
6
7
8
9
# Observer
做数据响应化
class Observer {
constructor(value) {
this.value = value
this.walk(value)
}
// 遍历对象做响应式
walk(obj) {
if (Array.isArray(obj)) {
obj.__proto__ = arrayProto //改变数组的原型,让其能找到我们定义的method
for (const item of obj) {
observe(item)
}
} else {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key])
})
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Compiler
解析模板,找到依赖,并和前面拦截的属性关联起来
- 获取 dom
- 遍历子元素
- 编译节点
- 遍历属性
v-
开头- v-text:处理 textContent
- v-html:处理 innerHTML
- v-model:监听 input
@
开头- 绑定 click
- 遍历属性
- 编译文本
- 编译节点
- 遍历子元素
// new Compiler('#app', vm)
class Compiler {
constructor(el, vm) {
this.$vm = vm
this.$el = document.querySelector(el)
// 执行编译
this.compile(this.$el)
}
compile(el) {
// 遍历这个el
el.childNodes.forEach((node) => {
// 是否是元素
if (node.nodeType === 1) {
// console.log('编译元素', node.nodeName)
this.compileElement(node)
} else if (this.isInter(node)) {
// console.log('编译文本', node.textContent);
this.compileText(node)
}
// 递归
if (node.childNodes) {
this.compile(node)
}
})
}
// 解析绑定表达式{{}}
compileText(node) {
// 获取正则匹配表达式,从vm里面拿出它的值
// node.textContent = this.$vm[RegExp.$1]
this.update(node, RegExp.$1, 'text')
}
// 编译元素
compileElement(node) {
// 处理元素上面的属性,典型的是v-,@开头的
const attrs = node.attributes
Array.from(attrs).forEach((attr) => {
// attr: 指令 {name: 'v-text', value: 'counter'} 事件 {name: '@click', value: 'add'}
const attrName = attr.name
const exp = attr.value
if (attrName.indexOf('v-') === 0) {
// 截取指令名称 text
const dir = attrName.substring(2)
// 看看是否存在对应方法,有则执行
this[dir] && this[dir](node, exp)
}
// 事件处理
if (attrName.indexOf('@') === 0) {
const event = attrName.substring(1)
this.eventHandler(node, event, exp)
}
})
}
// v-text
text(node, exp) {
// node.textContent = this.$vm[exp]
this.update(node, exp, 'text')
}
// v-html
html(node, exp) {
// node.innerHTML = this.$vm[exp]
this.update(node, exp, 'html')
}
// v-model
model(node, exp) {
// value赋值和更新
this.update(node, exp, 'model')
// 绑定input事件
node.addEventListener('input', (e) => {
this.$vm[exp] = e.target.value
})
}
// dir:要做的指令名称
// 一旦发现一个动态绑定,都要做两件事情,首先解析动态值;其次创建更新函数
// 未来如果对应的exp它的值发生变化,执行这个watcher的更新函数
update(node, exp, dir) {
// 初始化
const fn = this[dir + 'Updater']
fn && fn(node, this.$vm[exp])
// 更新,创建一个Watcher实例
new Watcher(this.$vm, exp, (val) => {
fn && fn(node, val)
})
}
textUpdater(node, val) {
node.textContent = val
}
htmlUpdater(node, val) {
node.innerHTML = val
}
modelUpdater(node, val) {
node.value = val
}
// 文本节点且形如{{xx}}
isInter(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
// 事件处理
eventHandler(node, event, exp) {
// @click = "add"
// node DOM元素
// event 事件名称 click
// exp 事件函数名称 add
const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp] //通过vm实例获取函数
node.addEventListener(event, fn.bind(this.$vm)) //绑定this,因为事件处理函数内部很可能用到vm上下文的数据
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# Watcher
管理一个依赖,未来执行更新
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm
this.key = key
this.updateFn = updateFn
// 读一下当前key,触发依赖收集
Dep.target = this
vm[key]
Dep.target = null
}
// 未来会被dep调用
update() {
this.updateFn.call(this.vm, this.vm[this.key])
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Dep
保存所有 watcher 实例,当某个 key 发生变化,通知他们执行更新
实现思路
- defineReactive 时为每⼀个 key 创建⼀个 Dep 实例
- 初始化视图时读取某个 key,例如 name1,创建⼀个 watcher1
- 由于触发 name1 的 getter ⽅法,便将 watcher1 添加到 name1 对应的 Dep 中
- 当 name1 更新, setter 触发时,便可通过对应 Dep 通知其管理所有 Watcher 更新
class Dep {
constructor() {
this.deps = []
}
addDep(watcher) {
this.deps.push(watcher)
}
notify() {
this.deps.forEach((dep) => dep.update())
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 调试源码
# 获取 vue
项目地址:https://github.com/vuejs/vue
迁出项目: git clone https://github.com/vuejs/vue.git
当前版本号:2.6.11
# 文件结构
src 目录
# 调试环境搭建
- 安装依赖: npm i
- 安装 rollup: npm i -g rollup
- 修改 dev 脚本,添加 sourcemap,package.json
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:webfull-dev",
- 运行开发命令: npm run dev
- 引入前面创建的 vue.js,samples/commits/index.html
<script src="../../dist/vue.js"></script>
术语解释:
- runtime:仅包含运行时,不包含编译器
- common:cjs 规范,用于 webpack1
- esm:ES 模块,用于 webpack2+
- umd: universal module definition,兼容 cjs 和 amd,用于浏览器
# 入口
dev 脚本中 -c scripts/config.js
指明配置文件所在参数 TARGET:web-full-dev
指明输出文件配置项,line:123
// Runtime+compiler development build (Browser)
{
"web-full-dev": {
"entry": resolve("web/entry-runtime-with-compiler.js"), // 入口
"dest": resolve("dist/vue.js"), // 目标文件
"format": "umd", // 输出规范
"env": "development",
"alias": { "he": "./entity-decoder" },
banner
}
}
2
3
4
5
6
7
8
9
10
11
# 初始化流程
new Vue()
_init()
$mount
mountComponent()
updateComponent()
render()
update()
new Watcher()
入口 platforms/web/entry-runtime-with-compiler.js
扩展默认$mount
方法:处理 template 或 el 选项
platforms/web/runtime/index.js
安装 web 平台特有指令和组件
定义patch:补丁函数,执行 patching 算法进行更新
定义$mount
:挂载 vue 实例到指定宿主元素(获得 dom 并替换宿主元素)
core/index.js
初始化全局 api 具体如下:
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
initUse(Vue) // 实现Vue.use函数
initMixin(Vue) // 实现Vue.mixin函数
initExtend(Vue) // 实现Vue.extend函数
initAssetRegisters(Vue) // 注册实现Vue.component/directive/filter
2
3
4
5
6
7
core/instance/index.js
Vue 构造函数定义
定义 Vue 实例 API
function Vue(options) {
// 构造函数仅执行了_init
this._init(options)
}
i
nitMixin(Vue) // 实现init函数
stateMixin(Vue) // 状态相关api $data,$props,$set,$delete,$watch
eventsMixin(Vue) // 事件相关api $on,$once,$off,$emit
lifecycleMixin(Vue) // 生命周期api _update,$forceUpdate,$destroy
renderMixin(Vue) // 渲染api _render,$nextTick
2
3
4
5
6
7
8
9
10
core/instance/init.js
创建组件实例,初始化其数据、属性、事件等
initLifecycle(vm) // $parent,$root,$children,$refs
initEvents(vm) // 处理父组件传递的事件和回调
initRender(vm) // $slots,$scopedSlots,_c,$createElement
callHook(vm, 'beforeCreate')
initInjections(vm) // 获取注入数据
initState(vm) // 初始化props,methods,data,computed,watch
initProvide(vm) // 提供数据注入
callHook(vm, 'created')
2
3
4
5
6
7
8
$mount
- mountComponent
执行挂载,获取 vdom 并转换为 dom
- new Watcher()
创建组件渲染 watcher
- updateComponent()
执行初始化或更新
- update()
初始化或更新,将传入 vdom 转换为 dom,初始化时执行的是 dom 创建操作
- render()
src\core\instance\render.js
渲染组件,获取 vdom
整体流程捋一捋
new Vue() => _init() => $mount() => mountComponent() => updateComponent()+new Watcher() => render() => _update()
# 面试题:谈谈 vue 生命周期
- 概念:组件创建、更新和销毁过程
- 用途:生命周期钩子使我们可以在合适的时间做合适的事情
- 分类列举:
- 初始化阶段:beforeCreate、created、beforeMount、mounted
- 更新阶段:beforeUpdate、updated
- 销毁阶段:beforeDestroy、destroyed
- 应用:
- created 时,所有数据准备就绪,适合做数据获取、赋值等数据操作
- mounted 时,$el 已生成,可以获取 dom;子组件也已挂载,可以访问它们
- updated 时,数值变化已作用于 dom,可以获取 dom 最新状态
- destroyed 时,组件实例已销毁,适合取消定时器等操作
# 数据响应式
数据响应式是 MVVM 框架的一大特点,通过某种策略可以感知数据的变化。Vue 中利用了 JS 语言特性 Object.defineProperty(),通过定义对象属性 getter/setter 拦截对属性的访问。
具体实现是在 Vue 初始化时,会调用 initState,它会初始化 data,props 等,这里着重关注 data 初始化,
整体流程
initState (vm: Component) src\core\instance\state.js
初始化数据,包括 props、methods、data、computed 和 watch
initData 核心代码是将 data 数据响应化
function initData (vm: Component) {
// 执行数据响应化
observe(data, true /* asRootData */)
}
2
3
4
core/observer/index.js
observe 方法返回一个 Observer 实例
Observer 对象根据数据类型执行对应的响应化操作
defineReactive 定义对象属性的 getter/setter,getter 负责添加依赖,setter 负责通知更新
core/observer/dep.js
Dep 负责管理一组 Watcher,包括 watcher 实例的增删及通知更新
Watcher
Watcher 解析一个表达式并收集依赖,当数值变化时触发回调函数,常用于$watch API 和指令中。
每个组件也会有对应的 Watcher,数值变化会触发其 update 函数导致重新渲染
export default class Watcher {
constructor() {}
get() {}
addDep(dep: Dep) {}
update() {}
}
2
3
4
5
6
# 数组响应化
数组数据变化的侦测跟对象不同,我们操作数组通常使用 push、pop、splice 等方法,此时没有办法得知数据变化。所以 vue 中采取的策略是拦截这些方法并通知 dep
src\core\observer\array.js
为数组原型中的 7 个可以改变内容的方法定义拦截器
Observer 中覆盖数组原型
if (Array.isArray(value)) {
// 替换数组原型
protoAugment(value, arrayMethods) // value.__proto__ = arrayMethods
this.observeArray(value)
}
2
3
4
5
# 异步更新队列
Vue ⾼效的秘诀是⼀套批量、异步的更新策略
- 事件循环 Event Loop:浏览器为了协调事件处理、脚本执⾏、⽹络请求和渲染等任务⽽制定的⼯作机制。
- 宏任务 Task:代表⼀个个离散的、独⽴的⼯作单元。 浏览器完成⼀个宏任务,在下⼀个宏任务执⾏开始前,会对⻚⾯进⾏重新渲染。主要包括创建⽂档对象、解析 HTML、执⾏主线 JS 代码以及各种事件如⻚⾯加载、输⼊、⽹络事件和定时器等。
- 微任务:微任务是更⼩的任务,是在当前宏任务执⾏结束后⽴即执⾏的任务。 如果存在微任务,浏览器会清空微任务之后再重新渲染。 微任务的例⼦有 Promise 回调函数、 DOM 变化等
# vue 中的具体实现
- 异步:只要侦听到数据变化, Vue 将开启⼀个队列,并缓冲在同⼀事件循环中发⽣的所有数据变更。
- 批量:如果同⼀个 watcher 被多次触发,只会被推⼊到队列中⼀次。去重对于避免不必要的计算和 DOM 操作是⾮常重要的。然后,在下⼀个的事件循环“tick”中, Vue 刷新队列执⾏实际⼯作。
- 异步策略: Vue 在内部对异步队列尝试使⽤原⽣的 Promise.then 、 MutationObserver 或 setImmediate ,如果执⾏环境都不⽀持,则会采⽤ setTimeout 代替。
update() core\observer\watcher.js
dep.notify()之后 watcher 执⾏更新,执⾏⼊队操作
queueWatcher(watcher) core\observer\scheduler.js
执⾏ watcher ⼊队操作
nextTick(flushSchedulerQueue) core\util\next-tick.js
nextTick 按照特定异步策略执⾏队列操作
# 虚拟 DOM
虚拟 DOM(Virtual DOM)是对 DOM 的 JS 抽象表示,它们是 JS 对象,能够描述 DOM 结构和关系。应⽤的各种状态变化会作⽤于虚拟 DOM,最终映射到 DOM 上。
# 体验虚拟 DOM
vue 中虚拟 dom 基于 snabbdom 实现,安装 snabbdom 并体验
<!DOCTYPE html>
<html lang="en">
<head></head>
<body>
<div id="app"></div>
<!--安装并引⼊snabbdom-->
<script src="../../node_modules/snabbdom/dist/snabbdom.js"></script>
<script>
// 之前编写的响应式函数
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
return val
},
set(newVal) {
val = newVal
// 通知更新
update()
}
})
}
// 导⼊patch的⼯⼚init, h是产⽣vnode的⼯⼚
const { init, h } = snabbdom
// 获取patch函数
const patch = init([])
// 上次vnode,由patch()返回
let vnode
// 更新函数,将数据操作转换为dom操作,返回新vnode
function update() {
if (!vnode) {
// 初始化,没有上次vnode,传⼊宿主元素和vnode
vnode = patch(app, render())
} else {
// 更新,传⼊新旧vnode对⽐并做更新
vnode = patch(vnode, render())
}
}
// 渲染函数,返回vnode描述dom结构
function render() {
return h('div', obj.foo)
}
// 数据
const obj = {}
// 定义响应式
defineReactive(obj, 'foo', '')
// 赋⼀个⽇期作为初始值
obj.foo = new Date().toLocaleTimeString()
// 定时改变数据,更新函数会重新执⾏
setInterval(() => {
obj.foo = new Date().toLocaleTimeString()
}, 1000)
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 优点
- 虚拟 DOM 轻量、快速:当它们发⽣变化时通过新旧虚拟 DOM ⽐对可以得到最⼩ DOM 操作量,配合异步更新策略减少刷新频率,从⽽提升性能
patch(vnode, h('div', obj.foo))
- 跨平台:将虚拟 dom 更新转换为不同运⾏时特殊操作实现跨平台
<script src="../../node_modules/snabbdom/dist/snabbdom-style.js"></script>
<script>
// 增加style模块
const patch = init([snabbdom_style.default])
function render() {
// 添加节点样式描述
return h('div', { style: { color: 'red' } }, obj.foo)
}
</script>
2
3
4
5
6
7
8
9
- 兼容性:还可以加⼊兼容性代码增强操作的兼容性
# 必要性
vue 1.0 中有细粒度的数据变化侦测,它是不需要虚拟 DOM 的,但是细粒度造成了⼤量开销,这对于⼤型项⽬来说是不可接受的。因此, vue 2.0 选择了中等粒度的解决⽅案,每⼀个组件⼀个 watcher 实例,这样状态变化时只能通知到组件,再通过引⼊虚拟 DOM 去进⾏⽐对和渲染。
# 整体流程
mountComponent() core/instance/lifecycle.js
渲染、更新组件
// 定义更新函数
const updateComponent = () => {
// 实际调⽤是在lifeCycleMixin中定义的_update和renderMixin中定义的_render
vm._update(vm._render(), hydrating)
}
2
3
4
5
render core/instance/render.js
⽣成虚拟 dom
_update core\instance\lifecycle.js
update 负责更新 dom,转换 vnode 为 dom
__patch__() platforms/web/runtime/index.js
__patch__
是在平台特有代码中指定的
Vue.prototype.__patch__ = inBrowser ? patch : noop
# patch 获取
patch 是 createPatchFunction 的返回值,传递 nodeOps 和 modules 是 web 平台特别实现
export const patch: Function = createPatchFunction({ nodeOps, modules })
platforms\web\runtime\node-ops.js
定义各种原⽣ dom 基础操作⽅法
platforms\web\runtime\modules\index.js
modules 定义了属性更新实现
watcher.run() => componentUpdate() => render() => update() => patch()
# patch 实现
patch core\vdom\patch.js
⾸先进⾏树级别⽐较,可能有三种情况:增删改。
- new VNode 不存在就删;
- old VNode 不存在就增;
- 都存在就执⾏ diff 执⾏更新
# patchVnode
⽐较两个 VNode,包括三种类型操作: 属性更新、⽂本更新、⼦节点更新具体规则如下:
- 新⽼节点均有 children ⼦节点,则对⼦节点进⾏ diff 操作,调⽤ updateChildren
- 如果新节点有⼦节点⽽⽼节点没有⼦节点,先清空⽼节点的⽂本内容,然后为其新增⼦节点
- 当新节点没有⼦节点⽽⽼节点有⼦节点的时候,则移除该节点的所有⼦节点。
- 当新⽼节点都⽆⼦节点的时候,只是⽂本的替换。
例子:
// patchVnode过程分解
// 1.div#demo updateChildren
// 2.h1 updateChildren
// 3.text ⽂本相同跳过
// 4.p updateChildren
// 5.text setTextContent
2
3
4
5
6
# updateChildren
updateChildren 主要作⽤是⽤⼀种较⾼效的⽅式⽐对新旧两个 VNode 的 children 得出最⼩操作补丁。执⾏⼀个双循环是传统⽅式, vue 中针对 web 场景特点做了特别的算法优化,我们看图说话:
在新⽼两组 VNode 节点的左右头尾两侧都有⼀个变量标记,在遍历过程中这⼏个变量都会向中间靠拢。
当 oldStartIdx > oldEndIdx 或者 newStartIdx > newEndIdx 时结束循环。
下⾯是遍历规则:
⾸先, oldStartVnode、 oldEndVnode 与 newStartVnode、 newEndVnode 两两交叉⽐较,共有 4 种⽐较⽅法。
当 oldStartVnode 和 newStartVnode 或者 oldEndVnode 和 newEndVnode 满⾜ sameVnode,直接将该 VNode 节点进⾏ patchVnode 即可,不需再遍历就完成了⼀次循环。如下图,
如果 oldStartVnode 与 newEndVnode 满⾜ sameVnode。说明 oldStartVnode 已经跑到了 oldEndVnode 后⾯去了,进⾏ patchVnode 的同时还需要将真实 DOM 节点移动到 oldEndVnode 的后⾯。
如果 oldEndVnode 与 newStartVnode 满⾜ sameVnode,说明 oldEndVnode 跑到了 oldStartVnode 的前⾯,进⾏ patchVnode 的同时要将 oldEndVnode 对应 DOM 移动到 oldStartVnode 对应 DOM 的前⾯。
如果以上情况均不符合,则在 old VNode 中找与 newStartVnode 相同的节点,若存在执⾏ patchVnode,同时将 elmToMove 移动到 oldStartIdx 对应的 DOM 的前⾯
当然也有可能 newStartVnode 在 old VNode 节点中找不到⼀致的 sameVnode,这个时候会调⽤ createElm 创建⼀个新的 DOM 节点。
⾄此循环结束,但是我们还需要处理剩下的节点。
当结束时 oldStartIdx > oldEndIdx,这个时候旧的 VNode 节点已经遍历完了,但是新的节点还没有。说明了新的 VNode 节点实际上⽐⽼的 VNode 节点多,需要将剩下的 VNode 对应的 DOM 插⼊到真实 DOM 中,此时调⽤ addVnodes(批量调⽤ createElm 接⼝)。
但是,当结束时 newStartIdx > newEndIdx 时,说明新的 VNode 节点已经遍历完了,但是⽼的节点还有剩余,需要从⽂档中将多余的节点删除
# 模板编译
模板编译的主要⽬标是将模板(template)转换为渲染函数(render)
template => render()
# 必要性
Vue 2.0 需要⽤到 VNode 描述视图以及各种交互,⼿写显然不切实际,因此⽤户只需编写类似 HTML 代码的 Vue 模板,通过编译器将模板转换为可返回 VNode 的 render 函数
# 体验模板编译
带编译器的版本中,可以使⽤ template 或 el 的⽅式声明模板
<div id="demo">
<h1>Vue模板<span>编译</span></h1>
<p v-if="foo">{{foo}}</p>
<comp></comp>
</div>
<script>
Vue.component('comp', {
template: '<div>I am comp</div>'
})
// 创建实例
const app = new Vue({
el: '#demo',
data: { foo: 'foo' }
})
// 输出render函数
console.log(app.$options.render)
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
输出的 render 函数
;(function anonymous() {
with (this) {
return _c(
'div',
{ attrs: { id: 'demo' } },
[
_c('h1', [_v('Vue模板编译')]),
_v(' '),
_c('p', [_v(_s(foo))]),
_v(' '),
_c('comp')
],
1
)
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
元素节点使⽤ createElement 创建,别名_c
本⽂节点使⽤ createTextVNode 创建,别名_v
表达式先使⽤ toString 格式化,别名_s
其他渲染
helpers: src\core\instance\render-helpers\index.js
# 整体流程
# compileToFunctions
若指定 template 或 el 选项,则会执⾏编译,
platforms\web\entry-runtime-with-compiler.js
编译过程
编译分为三步:解析、优化和⽣成, src\compiler\index.js
# 解析 - parse
解析器将模板解析为抽象语法树,基于 AST 可以做优化或者代码⽣成⼯作。
调试查看得到的 AST, /src/compiler/parser/index.js
,结构如下:
解析器内部分了 HTML 解析器、 ⽂本解析器和过滤器解析器,最主要是 HTML 解析器
# 优化 - optimize
优化器的作⽤是在 AST 中找出静态⼦树并打上标记。静态⼦树是在 AST 中永远不变的节点,如纯⽂本节点。
标记静态⼦树的好处:
- 每次重新渲染,不需要为静态⼦树创建新节点
- 虚拟 DOM 中 patch 时,可以跳过静态⼦树
<div id="demo">
<!-- 出现静态节点嵌套情况下会做优化标记 -->
<h1>Vue<span>模板编译</span></h1>
<p>{{foo}}</p>
<comp></comp>
</div>
<script>
Vue.component('comp', {
template: '<div>I am comp</div>'
})
// 创建实例
const app = new Vue({
el: '#demo',
data: { foo: 'foo' }
})
// 输出render函数
console.log(app.$options.render)
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
代码实现, src/compiler/optimizer.js - optimize
标记结束
# 代码⽣成 - generate
将 AST 转换成渲染函数中的内容,即代码字符串。 generate ⽅法⽣成渲染函数代码,src/compiler/codegen/index.js
⽣成的 code ⻓这样
_c('div', { attrs: { id: 'demo' } }, [
_c('h1', [_v('Vue.js测试')]),
_c('p', [_v(_s(foo))])
])
2
3
4
# v-if、 v-for
着重观察⼏个结构性指令的解析过程
# 解析 v-if parser/index.js
processIf ⽤于处理 v-if 解析
解析结果:
# 代码⽣成, codegen/index.js
genIfConditions 等⽤于⽣成条件语句相关代码⽣成结果:
"with(this){
return _c(
'div',{
attrs:{"id":"demo"}
},[
(foo) ? _c('h1',[_v(_s(foo))]) : _c('h1',[_v("no title")]),
_v(" "),
_c('abc')
],1)
}"
2
3
4
5
6
7
8
9
10
# 解析 v-for: parser/index.js
processFor ⽤于处理 v-for 指令解析结果:
v-for="item in items" for:'items' alias:'item'
# 代码⽣成, src\compiler\codegen\index.js:
genFor ⽤于⽣成相应代码
⽣成结果
"with(this){
return _c(
'div',{
attrs:{"id":"demo"}
},[
_m(0),
_v(" "),
(foo)?_c('p',[_v(_s(foo))]):_e(),
_v(" "),
_l((arr),function(s){return _c('b',{key:s}, [_v(_s(s))])}),
_v(" "),
_c('comp')
],2)
}"
2
3
4
5
6
7
8
9
10
11
12
13
14
v-if, v-for 这些指令只能在编译器阶段处理,如果我们要在 render 函数处理条件或循环只能使⽤ if 和 for
Vue.component('comp', {
props: ['foo'],
render(h) {
// 渲染内容跟foo的值挂钩,只能⽤if语句
if (this.foo == 'foo') {
return h('div', 'foo')
}
return h('div', 'bar')
}
})
2
3
4
5
6
7
8
9
10
;(function anonymous() {
with (this) {
return _c(
'div',
{
attrs: { id: 'demo' }
},
[
_m(0),
_v(' '),
foo ? _c('p', [_v(_s(foo))]) : _e(),
_v(' '),
_c('comp')
],
1
)
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 组件化机制
# 组件声明: Vue.component()
initAssetRegisters(Vue) src/core/global-api/assets.js
组件注册使⽤ extend ⽅法将配置转换为构造函数并添加到 components 选项
# 组件实例创建及挂载
观察⽣成的渲染函数
"with(this){
return _c(
'div',{
attrs:{"id":"demo"}
},[
_c('h1',[_v("虚拟DOM")]),
_v(" "),
_c('p',[_v(_s(foo))]),
_v(" "),
_c('comp') // 对于组件的处理并⽆特殊之处
],1)
}"
2
3
4
5
6
7
8
9
10
11
12
# 整体流程
⾸先创建的是根实例,⾸次_render()时,会得到整棵树的 VNode 结构,其中⾃定义组件相关的主要有: createComponent() - src/core/vdom/create-component.js
组件 vnode 创建
createComponent() - src/core/vdom/patch.js
创建组件实例并挂载, vnode 转换为 dom
整体流程: new Vue() => $mount() => vm._render() => createElement() => createComponent() => vm._update() => patch() => createElm => createComponent()
# 创建组件 VNode
_createElement - src\core\vdom\create-element.js
_createElement 实际执⾏ VNode 创建的函数,由于传⼊ tag 是⾮保留标签,因此判定为⾃定义组件通过 createComponent 去创建
createComponent - src/core/vdom/create-component.js
创建组件 VNode,保存了上⼀步处理得到的组件构造函数, props,事件等
# 创建组件实例
根组件执⾏更新函数时,会递归创建⼦元素和⼦组件,⼊⼝ createElm
createEle() core/vdom/patch.js line751
⾸次执⾏_update()时, patch()会通过 createEle()创建根元素,⼦元素创建研究从这⾥开始
createComponent core/vdom/patch.js line144
⾃定义组件创建
// 组件实例创建、挂载
if (isDef((i = i.hook)) && isDef((i = i.init))) {
i(vnode, false /* hydrating */)
}
if (isDef(vnode.componentInstance)) {
// 元素引⽤指定vnode.elm,元素属性创建等
initComponent(vnode, insertedVnodeQueue)
// 插⼊到⽗元素
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 事件处理整体流程
- 编译阶段:处理为 data 中的 on
;(function anonymous() {
with (this) {
return _c(
'div',
{
attrs: { id: 'demo' }
},
[
_c('h1', [_v('事件处理机制')]),
_v(' '),
_c('p', { on: { click: onClick } }, [_v('this is p')]),
_v(' '),
_c('comp', { on: { myclick: onMyClick } })
],
1
)
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 初始化阶段:
- 原生事件监听 platforms/web/runtime/modules/events.js
- 事件也是作为属性处理
- 整体流程:patch() => createElm() => invodeCreateHooks() => updateDOMListeners()
- 自定义事件监听 initEvents core/instance/events.js
- 整体流程:patch() => createElm() => createComponent() => hook.init() => createComponent... => _init() => initEvents() => updateComponentListeners()
- 原生事件监听 platforms/web/runtime/modules/events.js
事件监听和派发者均是组件实例,自定义组件中一定伴随着原生事件的监听与处理
# hook
在 Vue 当中,hooks 可以作为一种 event,在 Vue 的源码当中,称之为 hookEvent。
<Table @hook:updated="handleTableUpdated"></Table>
场景:有一个来自第三方的复杂表格组件,表格进行数据更新的时候渲染时间需要 1s,由于渲染时间较长,为了更好的用户体验,我希望在表格进行更新时显示一个 loading 动画。修改源码这个方案很不优雅。
callHook src\core\instance\lifecycle.js
export function callHook(vm: Component, hook: string) {
// ...
// 若包含hook事件,则一并派发
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
}
2
3
4
5
6
7
$on src\core\instance\events.js
const hookRE = /^hook:/
Vue.prototype.$on = function(
event: string | Array<string>,
fn: Function
): Component {
// 若存在hook事件则添加标记
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
2
3
4
5
6
7
8
9
10
# 双向绑定实现机制
v-model 是 mvvm 类型框架中重要功能,给人印象最深刻的就是它,它是怎么实现的?且看下面分解
<div id="demo">
<h1>双向绑定机制</h1>
<!--表单控件绑定-->
<input type="text" v-model="foo" />
<!--自定义事件-->
<comp v-model="foo"></comp>
</div>
<script src="../../dist/vue.js"></script>
<script>
// 声明自定义组件
Vue.component('comp', {
template: `
<input type="text" :value="$attrs.value"
@input="$emit('input', $event.target.value)">
`
})
// 创建实例
const app = new Vue({
el: '#demo',
data: { foo: 'foo' }
})
console.log(app.$options.render)
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
输出的 render 函数
// 生成的渲染函数
;(function anonymous() {
with (this) {
return _c(
'div',
{
attrs: { id: 'demo' }
},
[
_c('h1', [_v('双向绑定机制')]),
_v(' '),
_c('input', {
directives: [
{
name: 'model',
rawName: 'v-model',
value: foo,
expression: 'foo'
}
],
attrs: { type: 'text' },
domProps: { value: foo },
on: {
input: function($event) {
if ($event.target.composing) return
foo = $event.target.value
}
}
}),
_v(' '),
_c('comp', {
model: {
value: foo,
callback: function($$v) {
foo = $$v
},
expression: 'foo'
}
})
],
1
)
}
})
// input
_c('input', {
directives: [
{
name: 'model',
rawName: 'v-model',
value: foo,
expression: 'foo'
}
],
attrs: { type: 'text' },
domProps: { value: foo },
on: {
input: function($event) {
if ($event.target.composing) return
foo = $event.target.value
}
}
})
// comp
_c('comp', {
model: {
value: foo,
callback: function($$v) {
foo = $$v
},
expression: 'foo'
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# 初始化阶段:对节点赋值及事件监听
- 对节点赋值 platforms\web\runtime\modules\dom-props.js
- 事件监听 platforms\web\runtime\modules\events.js
- 额外的 model 指令 platforms\web\runtime\directives\model.js
- 自定义组件会转换为属性和事件 core/vdom/create-component.js
# vue-ssr
服务端渲染:将 vue 实例渲染为 HTML 字符串直接返回,在前端激活为交互程序
优点
- seo
- 首屏内容到达时间
# 基础 http 服务
// nodejs代码
const express = require('express')
// 获取express实例
const server = express()
// 编写路由处理不同url请求
server.get('/', (req, res) => {
res.send('<strong>hello world</strong>')
})
// 监听端口
server.listen(80, () => {
console.log('server running!')
})
2
3
4
5
6
7
8
9
10
11
12
# 基础实现
使用渲染器将 vue 实例成 HTML 字符串并返回
安装 vue-server-renderer
# 确保版本相同且匹配
npm i vue vue-server-renderer -S
2
使用 vue-server-renderer
// 1.创建vue实例
const Vue = require('vue')
const app = new Vue({
template: '<div>hello world</div>'
})
// 2.获取渲染器实例
const { createRenderer } = require('vue-server-renderer')
const renderer = createRenderer()
// 3.用渲染器渲染vue实例
renderer
.renderToString(app)
.then((html) => {
console.log(html)
})
.catch((err) => {
console.log(err)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
整合 express
// nodejs代码
const express = require('express')
// 获取express实例
const server = express()
const Vue = require('vue')
// 2.获取渲染器实例
const { createRenderer } = require('vue-server-renderer')
const renderer = createRenderer()
// 编写路由处理不同url请求
server.get('/', (req, res) => {
// res.send('<strong>hello world</strong>')
// 1.创建vue实例
const app = new Vue({
template: '<div @click="onClick">{{msg}}</div>',
data() {
return { msg: 'vue ssr' }
},
methods: {
onClick() {
console.log('do something')
}
}
})
// 3.用渲染器渲染vue实例
renderer
.renderToString(app)
.then((html) => {
res.send(html)
})
.catch((err) => {
res.status(500)
res.send('Internal Server Error, 500!')
})
})
// 监听端口
server.listen(80, () => {
console.log('server running!')
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 理解 ssr
# 传统 web 开发
网页内容在服务端渲染完成,一次性传输到浏览器
// 客户端渲染,返回给客户端的只是页面骨架,没有实际内容
// 真正的内容是在客户端使用js动态生成的
const express = require('express')
const app = express()
app.get('/', function(req, res) {
const html = `
<div id="app">
<h1>{{title}}</h1>
<p>{{content}}</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
new Vue({
el:'#app',
data:{
title:'wzp',
content:'wzp真不错'
}
})
</script>
`
res.send(html)
})
app.listen(3000)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
打开页面查看源码,浏览器拿到的是全部的 dom 结构
# 单页应用 Single Page App
单页应用优秀的用户体验,使其逐渐成为主流,页面内容由 JS 渲染出来,这种方式称为客户端渲染。
打开页面查看源码,浏览器拿到的仅有宿主元素#app,并没有内容。
SPA 问题:
- seo
- 首屏加载速度
# 服务端渲染 Server Side Render
SSR 解决方案,后端渲染出完整的首屏的 dom 结构返回,前端拿到的内容包括首屏及完整 SPA 结构,应用激活后依然按照 spa 方式运行,这种页面渲染方式被称为服务端渲染 (server side render)
# vue-ssr 实战
# 新建工程
vue-cli 创建工程即可
# 演示项目使用vue-cli 4.x创建
vue create ssr
2
# 安装依赖
# 要确保vue、vue-server-renderer版本一致
npm install vue-server-renderer@2.6.10 -S
2
# 启动脚本
创建一个 express 服务器,将 vue ssr 集成进来
// 创建一个express实例
const express = require('express')
const app = express()
// 导入vue
const Vue = require('vue')
// 创建渲染器
const { createRenderer } = require('vue-server-renderer')
const renderer = createRenderer()
app.get('/', async (req, res) => {
// 创建一个待渲染vue实例
const vm = new Vue({
data: { name: 'wzp真棒' },
template: `
<div>
<h1>{{name}}</h1>
</div>
`
})
try {
// renderToString将vue实例渲染为html字符串,它返回一个Promise
const html = await renderer.renderToString(vm)
// 返回html给客户端
res.send(html)
} catch (error) {
// 渲染出错返回500错误
res.status(500).send('Internal Server Error')
}
})
app.listen(3000)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 路由
路由支持仍然使用 vue-router
# 安装
若未引入 vue-router 则需要安装
npm i vue-router -s
# 创建路由实例
每次请求的 url 委托给 vue-router 处理
// 引入vue-router
const Router = require('vue-router')
Vue.use(Router)
// path修改为通配符
app.get('*', async function(req, res) {
// 每次创建一个路由实例
const router = new Router({
routes: [
{ path: '/', component: { template: '<div>index page</div>' } },
{ path: '/detail', component: { template: '<div>detail page</div>' } }
]
})
const vm = new Vue({
data: { msg: 'wzp真棒' },
// 添加router-view显示内容
template: `
<div>
<router-link to="/">index</router-link>
<router-link to="/detail">detail</router-link>
<router-view></router-view>
</div>`,
router // 挂载
})
try {
// 跳转至对应路由
router.push(req.url)
const html = await renderer.renderToString(vm)
res.send(html)
} catch (error) {
res.status(500).send('渲染出错')
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 同构开发 SSR 应用
对于同构开发,我们依然使用 webpack 打包,我们要解决两个问题:服务端首屏渲染和客户端激活
# 构建流程
目标是生成一个「服务器 bundle」用于服务端首屏渲染,和一个「客户端 bundle」用于客户端激活。
# 代码结构
除了两个不同入口之外,其他结构和之前 vue 应用完全相同
src ├── router ├────── index.js # 路由声明 ├── store ├────── index.js # 全局状态 ├── main.js # 用于创建 vue 实例 ├── entry-client.js # 客户端入口,用于静态内容“激活” └── entry-server.js # 服务端入口,用于首屏内容渲染
# 路由配置
创建@/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
//导出工厂函数
export function createRouter() {
return new Router({
mode: 'history',
routes: [
// 客户端没有编译器,这里要写成渲染函数
{ path: '/', component: { render: (h) => h('div', 'index page') } },
{ path: '/detail', component: { render: (h) => h('div', 'detail page') } }
]
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 主文件
跟之前不同,主文件是负责创建 vue 实例的工厂,每次请求均会有独立的 vue 实例创建。创建 main.js:
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
// 导出Vue实例工厂函数,为每次请求创建独立实例
// 上下文用于给vue实例传递参数
export function createApp(context) {
const router = createRouter()
const app = new Vue({
router,
context,
render: (h) => h(App)
})
return { app, router }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 服务端入口
上面的 bundle 就是 webpack 打包的服务端 bundle,我们需要编写服务端入口文件 src/entry-server.js 它的任务是创建 Vue 实例并根据传入 url 指定首屏
import { createApp } from './main'
// 返回一个函数,接收请求上下文,返回创建的vue实例
export default (context) => {
// 这里返回一个Promise,确保路由或组件准备就绪
return new Promise((resolve, reject) => {
const { app, router } = createApp(context)
// 跳转到首屏的地址
router.push(context.url)
// 路由就绪,返回结果
router.onReady(() => {
resolve(app)
}, reject)
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 客户端入口
客户端入口只需创建 vue 实例并执行挂载,这一步称为激活。创建 entry-client.js:
import { createApp } from './main'
// 创建vue、router实例
const { app, router } = createApp()
// 路由就绪,执行挂载
router.onReady(() => {
app.$mount('#app')
})
2
3
4
5
6
7
# webpack 配置
安装依赖
npm install webpack-node-externals lodash.merge -D
具体配置,vue.config.js
// 两个插件分别负责打包客户端和服务端
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
const nodeExternals = require('webpack-node-externals')
const merge = require('lodash.merge')
// 根据传入环境变量决定入口文件和相应配置项
const TARGET_NODE = process.env.WEBPACK_TARGET === 'node'
const target = TARGET_NODE ? 'server' : 'client'
module.exports = {
css: {
extract: false
},
outputDir: './dist/' + target,
configureWebpack: () => ({
// 将 entry 指向应用程序的 server / client 文件
entry: `./src/entry-${target}.js`,
// 对 bundle renderer 提供 source map 支持
devtool: 'source-map',
// target设置为node使webpack以Node适用的方式处理动态导入,
// 并且还会在编译Vue组件时告知`vue-loader`输出面向服务器代码。
target: TARGET_NODE ? 'node' : 'web',
// 是否模拟node全局变量
node: TARGET_NODE ? undefined : false,
output: {
// 此处使用Node风格导出模块
libraryTarget: TARGET_NODE ? 'commonjs2' : undefined
},
// https://webpack.js.org/configuration/externals/#function
// https://github.com/liady/webpack-node-externals
// 外置化应用程序依赖模块。可以使服务器构建速度更快,并生成较小的打包文件。
externals: TARGET_NODE
? nodeExternals({
// 不要外置化webpack需要处理的依赖模块。
// 可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
// 还应该将修改`global`(例如polyfill)的依赖模块列入白名单
whitelist: [/\.css$/]
})
: undefined,
optimization: {
splitChunks: undefined
},
// 这是将服务器的整个输出构建为单个 JSON 文件的插件。
// 服务端默认文件名为 `vue-ssr-server-bundle.json`
// 客户端默认文件名为 `vue-ssr-client-manifest.json`。
plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
}),
chainWebpack: (config) => {
// cli4项目添加
if (TARGET_NODE) {
config.optimization.delete('splitChunks')
}
config.module
.rule('vue')
.use('vue-loader')
.tap((options) => {
merge(options, {
optimizeSSR: false
})
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 脚本配置
安装依赖
npm i cross-env -D
定义创建脚本,package.json
"scripts": {
"build:client": "vue-cli-service build",
"build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build",
"build": "npm run build:server && npm run build:client"
},
2
3
4
5
执行打包:npm run build
# 宿主文件
最后需要定义宿主文件,修改./public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
# 服务器启动文件
修改服务器启动文件,现在需要处理所有路由,./server/04-ssr.js
// 获取文件路径
const resolve = dir => require('path').resolve(__dirname, dir)
// 第 1 步:开放dist/client目录,关闭默认下载index页的选项,不然到不了后面路由
app.use(express.static(resolve('../dist/client'), {index: false}))
// 第 2 步:获得一个createBundleRenderer
const { createBundleRenderer } = require("vue-server-renderer");
// 第 3 步:服务端打包文件地址
const bundle = resolve("../dist/server/vue-ssr-server-bundle.json");
// 第 4 步:创建渲染器
const renderer = createBundleRenderer(bundle, {
runInNewContext: false, // https://ssr.vuejs.org/zh/api/#runinnewcontext
template: require('fs').readFileSync(resolve("../public/index.html"), "utf-
8"), // 宿主文件
clientManifest: require(resolve("../dist/client/vue-ssr-clientmanifest.json")) // 客户端清单
});
app.get('*', async (req,res)=>{
// 设置url和title两个重要参数
const context = {
title:'ssr test',
url:req.url
} c
onst html = await renderer.renderToString(context);
res.send(html)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 整合 Vuex
安装 vuex
npm install -S vuex
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export function createStore() {
return new Vuex.Store({
state: {
count: 108
},
mutations: {
add(state) {
state.count += 1
}
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
挂载 store,main.js
import { createStore } from './store'
export function createApp(context) {
// 创建实例
const store = createStore()
const app = new Vue({
store, // 挂载
render: (h) => h(App)
})
return { app, router, store }
}
2
3
4
5
6
7
8
9
10
使用,.src/App.vue
<h2 @click="$store.commit('add')">{{$store.state.count}}</h2>
注意事项:注意打包和重启服务
# 数据预取
服务器端渲染的是应用程序的"快照",如果应用依赖于一些异步数据,那么在开始渲染之前,需要先预取和解析好这些数据。异步数据获取,store/index.js
export function createStore() {
return new Vuex.Store({
mutations: {
// 加一个初始化
init(state, count) {
state.count = count
}
},
actions: {
// 加一个异步请求count的action
getCount({ commit }) {
return new Promise((resolve) => {
setTimeout(() => {
commit('init', Math.random() * 100)
resolve()
}, 1000)
})
}
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
组件中的数据预取逻辑,index.vue
export default {
asyncData({ store, route }) {
// 约定预取逻辑编写在预取钩子asyncData中
// 触发 action 后,返回 Promise 以便确定请求结果
return store.dispatch('getCount')
}
}
2
3
4
5
6
7
服务端数据预取,entry-server.js
import { createApp } from "./app";
export default context => {
return new Promise((resolve, reject) => {
// 拿出store和router实例
const { app, router, store } = createApp(context);
router.push(context.url);
router.onReady(() => {
// 获取匹配的路由组件数组
const matchedComponents = router.getMatchedComponents();
// 若无匹配则抛出异常
if (!matchedComponents.length) {
return reject({ code: 404 });
} /
/ 对所有匹配的路由组件调用可能存在的`asyncData()`
Promise.all(
matchedComponents.map(Component => {
if (Component.asyncData) {
return Component.asyncData({
store,
route: router.currentRoute,
});
}
}),
)
.then(() => {
// 所有预取钩子 resolve 后,
// store 已经填充入渲染应用所需状态
// 将状态附加到上下文,且 `template` 选项用于 renderer 时,
// 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
context.state = store.state;
resolve(app);
})
.catch(reject);
}, reject);
});
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
客户端在挂载到应用程序之前,store 就应该获取到状态,entry-client.js
// 导出store
const { app, router, store } = createApp();
// 当使用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态自动嵌入到最
终的 HTML // 在客户端挂载到应用程序之前,store 就应该获取到状态:
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__);
}
2
3
4
5
6
7
客户端数据预取处理,main.js
Vue.mixin({
beforeMount() {
const { asyncData } = this.$options
if (asyncData) {
// 将获取数据操作分配给 promise
// 以便在组件中,我们可以在数据准备就绪后
// 通过运行 `this.dataPromise.then(...)` 来执行其他任务
this.dataPromise = asyncData({
store: this.$store,
route: this.$route
})
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
# TypeScript
# 准备工作
# 新建一个基于 ts 的 vue 项目
# 在已存在项目中安装 typescript
vue add @vue/typescript
请暂时忽略引发的几处 Error,它们不会影响项目运行,我们将在后面处理它们。
# TS 特点
- 类型注解、类型检测
- 类
- 接口
- 泛型
- 装饰器
- 类型声明
# 类型注解和编译时类型检查
使用类型注解约束变量类型,编译器可以做静态类型检查,使程序更加健壮
# 类型基础
// ts-test.ts
let var1: string // 类型注解
var1 = 'wzp' // 正确
var1 = 4 // 错误
// 编译器类型推断可省略这个语法
let var2 = true
// 常见原始类型: string,number,boolean,undefined,null,symbol
// 类型数组
let arr: string[]
arr = ['Tom'] // 或Array<string>
// 任意类型any
let varAny: any
varAny = 'xx'
varAny = 3
// any类型也可用于数组
let arrAny: any[]
arrAny = [1, true, 'free']
arrAny[1] = 100
// 函数中的类型约束
function greet(person: string): string {
return 'hello, ' + person
}
// void类型,常用于没有返回值的函数
function warn(): void {}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 范例
HelloWorld.vue
<template>
<div>
<ul>
<li v-for="feature in features" :key="feature">{{ feature }}</li>
</ul>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
@Component
export default class Hello extends Vue {
features: string[] = ['类型注解', '编译型语言']
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
# 类型别名
使用类型别名自定义类型
// 可以用下面这样方式定义对象类型
const objType: { foo: string; bar: string }
// 使用type定义类型别名,使用更便捷,还能复用
type Foobar = { foo: string; bar: string }
const aliasType: Foobar
2
3
4
5
范例:使用类型别名定义 Feature,types/index.ts
export type Feature = {
id: number
name: string
}
2
3
4
使用自定义类型,HelloWorld.vue
<
template>
<div>
<!--修改模板-->
<li v-for="feature in features" :key="feature.id">{{feature.name}}</li>
</div>
</template>
<script lang='ts'>
// 导入接口
import { Feature } from "@/types";
@Component
export default class Hello extends Vue {
// 修改数据结构
features: Feature[] = [{ id: 1, name: "类型注解" }];
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 联合类型
希望某个变量或参数的类型是多种类型其中之一
let union: string | number
union = '1' // ok
union = 1 // ok
2
3
# 交叉类型
想要定义某种由多种类型合并而成的类型使用交叉类型
type First = { first: number }
type Second = { second: number }
// FirstAndSecond将同时拥有属性first和second
type FirstAndSecond = First & Second
2
3
4
范例:利用交叉类型给 Feature 添加一个 selected 属性
// types/index.ts
type Select = {
selected: boolean
}
export type FeatureSelect = Feature & Select
2
3
4
5
使用这个 FeatureSelect,HelloWorld.vue
features: FeatureSelect[] = [
{ id: 1, name: "类型注解", selected: false },
{ id: 2, name: "编译型语言", selected: true }
];
2
3
4
<li :class="{ selected: feature.selected }">{{feature.name}}</li>
.selected {
background-color: rgb(168, 212, 247);
}
2
3
# 函数
必填参:参数一旦声明,就要求传递,且类型需符合
// 02-function.ts
function greeting(person: string): string {
return 'Hello, ' + person
}
greeting('tom')
2
3
4
5
可选参数:参数名后面加上问号,变成可选参数
function greeting(person: string, msg?: string): string {
return 'Hello, ' + person
}
2
3
默认值
function greeting(person: string, msg = ''): string {
return 'Hello, ' + person
}
2
3
函数重载:以参数数量或类型区分多个同名函数
// 重载1
function watch(cb1: () => void): void
// 重载2
function watch(cb1: () => void, cb2: (v1: any, v2: any) => void): void
// 实现
function watch(cb1: () => void, cb2?: (v1: any, v2: any) => void) {
if (cb1 && cb2) {
console.log('执行watch重载2')
} else {
console.log('执行watch重载1')
}
}
2
3
4
5
6
7
8
9
10
11
12
范例:新增特性,Hello.vue
<div>
<input type="text" placeholder="输入新特性" @keyup.enter="addFeature">
</div>
2
3
addFeature(e: KeyboardEvent) {
// e.target是EventTarget类型,需要断言为HTMLInputElement
const inp = e.target as HTMLInputElement;
const feature: FeatureSelect = {
id: this.features.length + 1,
name: inp.value,
selected: false
} t
his.features.push(feature);
inp.value = "";
}
2
3
4
5
6
7
8
9
10
11
范例:生命周期钩子,Hello.vue
created() {
this.features = [{ id: 1, name: "类型注解" }];
}
2
3
# 类
ts 中的类和 es6 中大体相同,这里重点关注 ts 带来的访问控制等特性
// 03-class.ts
class Parent {
private _foo = 'foo' // 私有属性,不能在类的外部访问
protected bar = 'bar' // 保护属性,可以在子类中访问
// 参数属性:构造函数参数加修饰符,能够定义为成员属性
constructor(public tua = 'tua') {}
// 方法也有修饰符
private someMethod() {}
// 存取器:属性方式访问,可添加额外逻辑,控制读写性
get foo() {
return this._foo
}
set foo(val) {
this._foo = val
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 接口
接口仅约束结构,不要求实现,使用更简单
// 04-interface
// Person接口定义了结构
interface Person {
firstName: string
lastName: string
}
// greeting函数通过Person接口约束参数解构
function greeting(person: Person) {
return 'Hello, ' + person.firstName + ' ' + person.lastName
}
g
reeting({ firstName: 'Jane', lastName: 'User' }) // 正确
greeting({ firstName: 'Jane' }) // 错误
2
3
4
5
6
7
8
9
10
11
12
13
范例:Feature 也可用接口形式约束,./types/index.ts
// 接口中只需定义结构,不需要初始化
export interface Feature {
id: number
name: string
}
2
3
4
5
# 泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。以此增加代码通用性
// 不用泛型
// interface Result {
// ok: 0 | 1;
// data: Feature[];
// }
// 使用泛型
interface Result<T> {
ok: 0 | 1
data: T
}
// 泛型方法
function getResult<T>(data: T): Result<T> {
return { ok: 1, data }
}
// 用尖括号方式指定T为string
getResult<string>('hello')
// 用类型推断指定T为number
getResult(1)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
泛型优点:
- 函数和类可以支持多种类型,更加通用
- 不必编写多条重载,冗长联合类型,可读性好
- 灵活控制类型约束
不仅通用且能灵活控制,泛型被广泛用于通用库的编写。
范例:用 axios 获取数据安装 axios: npm i axios -S
配置一个模拟接口,vue.config.js
module.exports = {
devServer: {
before(app) {
app.get('/api/list', (req, res) => {
res.json([
{ id: 1, name: '类型注解', version: '2.0' },
{ id: 2, name: '编译型语言', version: '1.0' }
])
})
}
}
}
2
3
4
5
6
7
8
9
10
11
12
使用接口,HelloWorld.vue
async mounted() {
console.log("HelloWorld");
const resp = await axios.get<FeatureSelect[]>('/api/list')
this.features = resp.data
}
2
3
4
5
# 声明文件
使用 ts 开发时如果要使用第三方 js 库的同时还想利用 ts 诸如类型检查等特性就需要声明文件,类似 xx.d.ts
同时,vue 项目中还可以在 shims-vue.d.ts 中对已存在模块进行补充
npm i @types/xxx
范例:利用模块补充$axios 属性到 Vue 实例,从而在组件里面直接用
// main.ts
import axios from 'axios'
Vue.prototype.$axios = axios;
// shims-vue.d.ts
import Vue from "vue";
import { AxiosInstance } from "axios";
declare module "vue/types/vue" {
interface Vue {
$axios: AxiosInstance;
}
}
范例:给router/index.js编写声明文件,index.d.ts
```typescript
import VueRouter from "vue-router";
declare const router: VueRouter
export default router
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 装饰器
装饰器用于扩展类或者它的属性和方法。@xxx 就是装饰器的写法
# 属性声明:@Prop
除了在@Component 中声明,还可以采用@Prop 的方式声明组件属性
export default class HelloWorld extends Vue {
// Props()参数是为vue提供属性选项
// !称为明确赋值断言,它是提供给ts的
@Prop({ type: String, required: true })
private msg!: string
}
2
3
4
5
6
# 事件处理:@Emit
新增特性时派发事件通知,Hello.vue
// 通知父类新增事件,若未指定事件名则函数名作为事件名(羊肉串形式)
@Emit()
private addFeature(event: any) {// 若没有返回值形参将作为事件参数
const feature = { name: event.target.value, id: this.features.length + 1 };
this.features.push(feature);
event.target.value = "";
return feature;// 若有返回值则返回值作为事件参数
}
2
3
4
5
6
7
8
# 变更监测:@Watch
@Watch('msg')
onMsgChange(val:string, oldVal:any){
console.log(val, oldVal);
}
2
3
4
状态管理推荐使用:vuex-module-decorators (opens new window)
vuex-module-decorators
通过装饰器提供模块化声明 vuex 模块的方法,可以有效利用 ts 的类型系统。
安装
npm i vuex-module-decorators -D
根模块清空,修改 store/index.ts
export default new Vuex.Store({})
定义 counter 模块,创建 store/counter
import {
Module,
VuexModule,
Mutation,
Action,
getModule
} from 'vuex-moduledecorators'
import store from './index'
// 动态注册模块
@Module({ dynamic: true, store: store, name: 'counter', namespaced: true })
class CounterModule extends VuexModule {
count = 1
@Mutation
add() {
// 通过this直接访问count
this.count++
}
// 定义getters
get doubleCount() {
return this.count * 2
}
@Action
asyncAdd() {
setTimeout(() => {
// 通过this直接访问add
this.add()
}, 1000)
}
}
// 导出模块应该是getModule的结果
export default getModule(CounterModule)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
使用,App.vue
<p @click="add">{{$store.state.counter.count}}</p>
<p @click="asyncAdd">{{count}}</p>
2
import CounterModule from '@/store/counter'
@Component
export default class App extends Vue {
get count() {
return CounterModule.count
}
add() {
CounterModule.add()
}
asyncAdd() {
CounterModule.asyncAdd()
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 装饰器原理
装饰器是工厂函数,它能访问和修改装饰目标。
# 类装饰器
07-decorator.ts
//类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。
function log(target: Function) {
// target是构造函数
console.log(target === Foo) // true
target.prototype.log = function() {
console.log(this.bar)
}
}
@log
class Foo {
bar = 'bar'
}
const foo = new Foo()
// @ts-ignore
foo.log()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 方法装饰器
function rec(target: any, name: string, descriptor: any) {
// 这里通过修改descriptor.value扩展了bar方法
const baz = descriptor.value
descriptor.value = function(val: string) {
console.log('run method', name)
baz.call(this, val)
}
}
class Foo {
@rec
setBar(val: string) {
this.bar = val
}
}
foo.setBar('lalala')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 属性装饰器
// 属性装饰器
function mua(target, name) {
target[name] = 'mua~~~'
}
class Foo {
@mua ns!: string
}
console.log(foo.ns)
2
3
4
5
6
7
8
9
稍微改造一下使其可以接收参数
function mua(param: string) {
return function(target, name) {
target[name] = param
}
}
2
3
4
5
实战一下,复制 HelloWorld.vue
<template>
<div>{{ msg }}</div>
</template>
<script lang="ts">
import { Vue } from 'vue-property-decorator'
function Component(options: any) {
return function(target: any) {
return Vue.extend(options)
}
}
@Component({
props: {
msg: {
type: String,
default: ''
}
}
})
export default class Decor extends Vue {}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
显然 options 中的选项都可以从 Decor 定义中找到
# Vue2.x 项⽬最佳实践
# 项⽬配置策略
基础配置:指定应⽤上下⽂、端⼝号,vue.config.js
const port = 7070
module.exports = {
publicPath: '/best-practice', // 部署应⽤包时的基本 URL
devServer: {
port
}
}
2
3
4
5
6
7
8
配置 webpack: configureWebpack
范例:设置⼀个组件存放路径的别名,vue.config.js
const path = require('path')
module.exports = {
configureWebpack: {
resolve: {
alias: {
comps: path.join(__dirname, 'src/components')
}
}
}
}
2
3
4
5
6
7
8
9
10
11
范例:设置⼀个 webpack 配置项⽤于⻚⾯ title,vue.config.js
module.exports = {
configureWebpack: {
name: 'vue项⽬最佳实践'
}
}
2
3
4
5
在宿主⻚⾯使⽤ lodash 插值语法使⽤它,./public/index.html
<title><%= webpackConfig.name %></title>
webpack-merge 合并出最终选项
范例:基于环境有条件地配置,vue.config.js
// 传递⼀个函数给configureWebpack
// 可以直接修改,或返回⼀个⽤于合并的配置对象
configureWebpack: (config) => {
config.resolve.alias.comps = path.join(__dirname, 'src/components')
if (process.env.NODE_ENV === 'development') {
config.name = 'vue项⽬最佳实践'
} else {
config.name = 'Vue Best Practice'
}
}
2
3
4
5
6
7
8
9
10
配置 webpack:chainWebpack
webpack-chain 称为链式操作,可以更细粒度控制 webpack 内部配置。
范例:svg icon 引⼊
- 下载图标,存⼊ src/icons/svg 中
- 安装依赖:svg-sprite-loader
npm i svg-sprite-loader -D
1
修改规则和新增规则,vue.config.js
// resolve定义⼀个绝对路径获取函数
const path = require('path')
function resolve(dir) {
return path.join(__dirname, dir)
}
//...
chainWebpack(config) {
// 配置svg规则排除icons⽬录中svg⽂件处理
// ⽬标给svg规则增加⼀个排除选项exclude:['path/to/icon']
config.module.rule("svg")
.exclude.add(resolve("src/icons"))
// 新增icons规则,设置svg-sprite-loader处理icons⽬录中的svg
config.module.rule('icons')
.test(/\.svg$/)
.include.add(resolve('./src/icons')).end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({symbolId: 'icon-[name]'})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- 使⽤图标,App.vue
<template>
<svg>
<use xlink:href="#icon-wx" />
</svg>
</template>
<script>
import '@/icons/svg/wx.svg'
</script>
2
3
4
5
6
7
8
⾃动导⼊
创建 icons/index.js
const req = require.context('./svg', false, /\.svg$/) req.keys().map(req)
1
2创建 SvgIcon 组件,components/SvgIcon.vue
<template> <svg :class="svgClass" v-on="$listeners"> <use :xlink:href="iconName" /> </svg> </template> <script> export default { name: 'SvgIcon', props: { iconClass: { type: String, required: true }, className: { type: String, default: '' } }, computed: { iconName() { return `#icon-${this.iconClass}` }, svgClass() { if (this.className) { return 'svg-icon ' + this.className } else { return 'svg-icon' } } } } </script> <style scoped> .svg-icon { width: 1em; height: 1em; vertical-align: -0.15em; fill: currentColor; overflow: hidden; } </style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
环境变量和模式
如果想给多种环境做不同配置,可以利⽤ vue-cli 提供的模式。默认有 development
、 production
、 test
三种模式,对应的,它们的配置⽂件形式是 .env.development
。
范例:定义⼀个开发时可⽤的配置项,创建.env.dev
# 只能⽤于服务端
foo=bar
# 可⽤于客户端
VUE_APP_DONG=dong
2
3
4
修改 mode 选项覆盖模式名称,package.json
"serve": "vue-cli-service serve --mode dev"
# 权限控制
路由分为两种: constantRoutes
和 asyncRoutes
,前者是默认路由可直接访问,后者中定义的路由需要先登录,获取⻆⾊并过滤后动态加⼊到 Router 中。
- 路由定义,router/index.js
- 创建⽤户登录⻚⾯,views/Login.vue
- 路由守卫:创建./src/permission.js,并在 main.js 中引⼊
# ⽤户登录状态维护
维护⽤户登录状态:路由守卫 => ⽤户登录 => 获取 token 并缓存
- 路由守卫:src/permission.js
- 请求登录:components/Login.vue
- user 模块:维护⽤户数据、处理⽤户登录等,store/modules/user.js
- 测试~
# ⽤户⻆⾊获取和权限路由过滤
登录成功后,请求⽤户信息获取⽤户⻆⾊信息,然后根据⻆⾊过滤 asyncRoutes,并将结果动态添加⾄ router
- 维护路由信息,实现动态路由⽣成逻辑,store/modules/permission.js
- 获取⽤户⻆⾊,判断⽤户是否拥有访问权限,permission.js
// 引⼊store
import store from './store'
router.beforeEach(async (to, from, next) => {
// ...
if (hasToken) {
if (to.path === '/login') {
} else {
// 若⽤户⻆⾊已附加则说明权限以判定,动态路由已添加
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
// 说明⽤户已获取过⻆⾊信息,放⾏
next()
} else {
try {
// 先请求获取⽤户信息
web全栈架构师
const { roles } = await store.dispatch('user/getInfo')
// 根据当前⽤户⻆⾊过滤出可访问路由
const accessRoutes = await store.dispatch(
'permission/generateRoutes',
roles
)
// 添加⾄路由器
router.addRoutes(accessRoutes)
// 继续路由切换,确保addRoutes完成
next({ ...to, replace: true })
} catch (error) {
// 出错需重置令牌并重新登录(令牌过期、⽹络错误等原因)
await store.dispatch('user/resetToken')
next(`/login?redirect=${to.path}`)
alert(error || '未知错误')
}
}
}
} else {
// 未登录...
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
异步获取路由表
可以当⽤户登录后向后端请求可访问的路由表,从⽽动态⽣成可访问⻚⾯,操作和原来是相同的,这⾥多了⼀步将后端返回路由表中组件名称和本地的组件映射步骤:
// 前端组件名和组件映射表 const map = { //xx: require('@/views/xx.vue').default // 同步的⽅式 xx: () => import('@/views/xx.vue') // 异步的⽅式 } // 服务端返回的asyncRoutes const asyncRoutes = [ { path: '/xx', component: 'xx',... } ] // 遍历asyncRoutes,将component替换为map[component] function mapComponent(asyncRoutes) { asyncRoutes.forEach(route => { route.component = map[route.component]; if(route.children) { route.children.map(child => mapComponent(child)) } }) } mapComponent(asyncRoutes)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 按钮权限
⻚⾯中某些按钮、链接有时候需要更细粒度权限控制,这时候可以封装⼀个指令 v-permission,放在需要控制的按钮上,从⽽实现按钮级别权限控制
创建指令,src/directives/permission.js
测试,About.vue
该指令只能删除挂载指令的元素,对于那些额外⽣成的和指令⽆关的元素⽆能为⼒,⽐如:
<el-tabs> <el-tab-pane label="⽤户管理" name="first" v-permission="['admin','editor']">⽤户管理</el-tab-pane> <el-tab-pane label="配置管理" name="second" v-permission="['admin', 'editor']">配置管理</el-tab-pane> <el-tab-pane label="⻆⾊管理" name="third" v-permission="['admin']">⻆⾊管理</el-tab-pane> <el-tab-pane label="定时任务补偿" name="fourth" v-permission="['admin', 'editor']">定时任务补偿</el-tab-pane> </el-tabs>
1
2
3
4
5
6此时只能使⽤ v-if 来实现
<template> <el-tab-pane v-if="checkPermission(['admin'])"> </template> <script> export default { methods: { checkPermission(permissionRoles) { return roles.some(role => { return permissionRoles.includes(role); }); } } } </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ⾃动⽣成导航菜单
导航菜单是根据路由信息并结合权限判断⽽动态⽣成的。它需要对应路由的多级嵌套,所以要⽤到递归组件。
- 创建侧边栏组件,components/Sidebar/index.vue
- 创建侧边栏菜项⽬组件,layout/components/Sidebar/SidebarItem.vue
- 创建侧边栏菜单项组件,layout/components/Sidebar/Item.vue
# 数据交互
数据交互流程:
api 服务 => axios 请求 => 本地 mock/线上 mock/服务器 api
# 封装 request
对 axios 做⼀次封装,统⼀处理配置、请求和响应拦截。
安装 axios: npm i axios -S
- 创建@/utils/request.js
- 设置 VUE_APP_BASE_API 环境变量,创建.env.development ⽂件
- 编写服务接⼝,创建@/api/user.js
# 数据 mock
数据模拟两种常⻅⽅式,本地 mock 和线上 esay-mock
本地 mock:利⽤ webpack-dev-server 提供的 before 钩⼦可以访问 express 实例,从⽽定义接⼝
- 修改 vue.config.js,给 devServer 添加相关代码
- 调⽤接⼝,@/store/modules/user.js
# 线上 esay-mock
诸如 easy-mock 这类线上 mock ⼯具优点是使⽤简单,mock ⼯具库也⽐较强⼤,还能根据 swagger 规范⽣成接⼝。
使⽤步骤:
登录easy-mock (opens new window)
若远程不可⽤,可以搭建本地 easy-mock 服务(nvm + node + redis + mongodb)先安装 node 8.x、redis 和 mongodb 启动命令:
- 切 node v8:
nvm list
,nvm use 8.16.0
- 起 redis:
redis-server
- 起 mongodb:
mongod
- 起 easy-mock 项⽬:
npm run dev
- 切 node v8:
创建⼀个项⽬
创建需要的接⼝
// user/login { "code": function({_req}) { const {username} = _req.body; if (username === "admin" || username === "jerry") { return 1 } else { return 10008 } }, "data": function({_req}) { const {username} = _req.body; if (username === "admin" || username === "jerry") { return username } else { return '' } } } // user/info { code: 1, "data": function({_req}) { return _req.headers['authorization'].split(' ')[1] === 'admin' ? ['admin'] : ['editor'] } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28调⽤:修改 base_url,.env.development
VUE_APP_BASE_API = 'http://localhost:7300/mock/5e9032aab92b8c71eb235ad'
1
# 解决跨域
如果请求的接⼝在另⼀台服务器上,开发时则需要设置代理避免跨域问题
添加代理配置,vue.config.js
创建⼀个独⽴接⼝服务器,~/server/index.js
代码下载: https://github.com/57code/vue-study/tree/step-7 命令⽤下⾯这个:
git fetch origin step-7 git reset --hard step-7
1
2注意切换⼀个新分⽀
# 项⽬测试
# 测试分类
常⻅的开发流程⾥,都有测试⼈员,他们不管内部实现机制,只看最外层的输⼊输出,这种我们称为⿊盒测试。⽐如你写⼀个加法的⻚⾯,会设计 N 个⽤例,测试加法的正确性,这种测试我们称之为E2E 测试。
还有⼀种测试叫做⽩盒测试,我们针对⼀些内部核⼼实现逻辑编写测试代码,称之为单元测试。
更负责⼀些的我们称之为集成测试,就是集合多个测试过的单元⼀起测试。
组件的单元测试有很多好处:
- 提供描述组件⾏为的⽂档
- 节省⼿动测试的时间
- 减少研发新特性时产⽣的 bug
- 改进设计
- 促进重构
# 准备⼯作
在 vue-cli 中,预置了 Mocha+Chai 和Jest (opens new window)两套单测⽅案,我们的演示代码使⽤ Jest,它们语法基本⼀致
# 新建 vue 项⽬时
选择特性
Unit Testing
和E2E Testing
单元测试解决⽅案选择: Jest
端到端测试解决⽅案选择:Cypress
# 在已存在项⽬中集成
集成 Jest: vue add @vue/unit-jest
集成 cypress: vue add @vue/e2e-cypress
# 编写单元测试
单元测试(unit testing),是指对软件中的最⼩可测试单元进⾏检查和验证。
- 新建 test/unit/add.spec.js,
*.spec.js
是命名规范
function add(num1, num2) {
return num1 + num2
}
// 测试套件 test suite
describe('Add', () => {
// 测试⽤例 test case
it('测试add函数', () => {
// 断⾔ assert
expect(add(1, 3)).toBe(3)
expect(add(1, 3)).toBe(4)
expect(add(-2, 3)).toBe(1)
})
})
2
3
4
5
6
7
8
9
10
11
12
13
14
# 执⾏单元测试
- 执⾏:
npm run test:unit
# 断⾔ API 简介
describe : 定义⼀个测试套件
it :定义⼀个测试⽤例
expect :断⾔的判断条件
这⾥⾯仅演示了 toBe,更多断⾔ API (opens new window)
# 测试 Vue 组件
vue 官⽅提供了⽤于单元测试的实⽤⼯具库 @vue/test-utils
- 创建⼀个 vue 组件 components/Add.vue
- 测试该组件,test/unit/Add.spec.js
import Add from '@/components/Add.vue'
describe('Add.vue', () => {
// 检查组件选项
it('要求设置created⽣命周期', () => {
expect(typeof Add.created).toBe('function')
})
it('message初始值是vue-test', () => {
// 检查data函数存在性
expect(typeof Add.data).toBe('function')
// 检查data返回的默认值
const defaultData = Add.data()
expect(defaultData.message).toBe('vue-test')
})
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 检查 mounted 之后预期结果
使⽤@vue/test-utils 挂载组件
import { mount } from '@vue/test-utils'
it('mount之后测data是wzp', () => {
const wrapper = mount(Add)
expect(wrapper.vm.message).toBe('wzp')
})
it('按钮点击后', () => {
const wrapper = mount(DemoComp)
wrapper.find('button').trigger('click')
// 测试数据变化
expect(wrapper.vm.message).toBe('按钮点击')
// 测试html渲染结果
expect(wrapper.find('span').html()).toBe('<span>按钮点击</span>')
// 等效的⽅式
expect(wrapper.find('span').text()).toBe('按钮点击')
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 测试覆盖率
Jest ⾃带覆盖率,很容易统计我们测试代码是否全⾯。如果⽤的 mocha,需要使⽤ istanbul 来统计覆盖率。
- package.json ⾥修改 jest 配置
"jest": {
"collectCoverage": true,
"collectCoverageFrom": ["src/**/*.{js,vue}"],
}
2
3
4
若采⽤独⽴配置,则修改 jest.config.js:
module.exports = { collectCoverage: true, collectCoverageFrom: ['src/**/*.{js,vue}'] }
1
2
3
4
- 在此执⾏ npm run test:unit
%stmts 是语句覆盖率(statement coverage):是不是每个语句都执⾏了?
%Branch 分⽀覆盖率(branch coverage):是不是每个 if 代码块都执⾏了?
%Funcs 函数覆盖率(function coverage):是不是每个函数都调⽤了?
%Lines ⾏覆盖率(line coverage):是不是每⼀⾏都执⾏了?
可以看到我们 Demo.vue 的覆盖率是 100%,我们修改⼀下代码
<template>
<div>
<span>{{ message }}</span>
<button @click="changeMsg">点击</button>
</div>
</template>
<script>
export default {
data() {
return {
message: 'vue-text',
count: 0
}
},
created() {
this.message = 'wzp'
},
methods: {
changeMsg() {
if (this.count > 1) {
this.message = 'count⼤于1'
} else {
this.message = '按钮点击'
}
},
changeCount() {
this.count += 1
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
现在的代码,依然是测试没有报错,但是覆盖率只有 66%了,⽽且没有覆盖的代码⾏数,都标记了出来,继续努⼒加测试吧
# E2E 测试
借⽤浏览器的能⼒,站在⽤户测试⼈员的⻆度,输⼊框,点击按钮等,完全模拟⽤户,这个和具体的框架关系不⼤,完全模拟浏览器⾏为。
运⾏ E2E 测试
npm run test:e2e
修改 e2e/spec/test.js
// https://docs.cypress.io/api/introduction/api.html
describe('端到端测试,抢测试⼈员的饭碗', () => {
it('先访问⼀下', () => {
cy.visit('/')
// cy.contains('h1', 'Welcome to Your Vue.js App')
cy.contains('span', 'wzp')
})
})
2
3
4
5
6
7
8
9
测试未通过,因为没有使⽤ Demo.vue,修改 App.vue
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<!-- <HelloWorld msg="Welcome to Your Vue.js App"/> -->
<Add></Add>
</div>
<script>
import Add from './components/Add.vue'
export default {
name: 'app',
components: {
HelloWorld,
Add
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
测试通过~
# 测试⽤户点击
// https://docs.cypress.io/api/introduction/api.html
describe('端到端测试,抢测试⼈员的饭碗', () => {
it('先访问⼀下', () => {
cy.visit('/')
// cy.contains('h1', 'Welcome to Your Vue.js App')
cy.contains('#message', 'wzp')
cy.get('button').click()
cy.contains('span', '按钮点击')
})
})
2
3
4
5
6
7
8
9
10
11
12
# esay-mock 安装
# 安装⼯具
nvm
windows
mac
使⽤ nvm 安装 node 8.x
nvm install 8.16.0
mongodb
- windows
- mac
redis
- windows
- mac
克隆 esay-mock 项⽬
git clone https://github.com/easy-mock/easy-mock.git
1
# 起服务
mongodb
mongod
1redis
redis-server
1esay-mock
npm i npm run dev
1
2
# vue3 初探 + 响应式原理
# 调试环境搭建
迁出 Vue3 源码:
git clone https://github.com/vuejs/vue-next.git
安装依赖:
yarn
生成 sourcemap 文件,package.json
"dev": "node scripts/dev.js --sourcemap"
1编译:
yarn dev
生成结果:packages\vue\dist\vue.global.js
# 源码结构
源码位置是在 package 文件件内,实际上源码主要分为两部分,编译器和运行时环境。
编译器
- compiler-core 核心编译逻辑
- compiler-dom 针对浏览器平台编译逻辑
- compiler-sfc 针对单文件组件编译逻辑
- compiler-ssr 针对服务端渲染编译逻辑
运行时环境
- runtime-core 运行时核心
- runtime-dom 运行时针对浏览器的逻辑
- runtime-test 浏览器外完成测试环境仿真
reactivity 响应式逻辑
template-explorer 模板浏览器
vue 代码入口,整合编译器和运行时
server-renderer 服务器端渲染
share 公用方法
# Vue 3 初探
测试代码,~/packages/samples/01-hello-vue3.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>hello vue3</title>
<script src="../dist/vue.global.js"></script>
</head>
<body>
<div id="app"><h1>{{message}}</h1></div>
<script>
Vue.createApp({
data: { message: 'Hello Vue 3!' }
}).mount('#app')
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Composition API
Composition API (opens new window)字面意思是组合 API,它是为了实现基于函数的逻辑复用机制而产生的。
# 基本使用
数据响应式,创建 02-composition-api.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
<script src="../dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<h1>Composition API</h1>
<div>count: {{ state.count }}</div>
</div>
<script>
const { createApp, reactive } = Vue
// 声明组件
const App = {
// setup是一个新的组件选项,它是组件内使用Composition API的入口
// 调用时刻是初始化属性确定后,beforeCreate之前
setup() {
// 响应化:接收一个对象,返回一个响应式的代理对象
const state = reactive({ count: 0 })
// 返回对象将和渲染函数上下文合并
return { state }
}
}
createApp(App).mount('#app')
</script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
计算属性
<div>doubleCount: {{doubleCount}}</div>
const { computed } = Vue
const App = {
setup() {
const state = reactive({
count: 0,
// computed()返回一个不可变的响应式引用对象
// 它封装了getter的返回值
doubleCount: computed(() => state.count * 2)
})
}
}
2
3
4
5
6
7
8
9
10
11
12
事件处理
<div @click="add">count: {{ state.count }}</div>
const App = {
setup() {
// setup中声明一个add函数
function add() {
state.count++
} // 传入渲染函数上下文
return { state, add }
}
}
2
3
4
5
6
7
8
9
侦听器
const { watch } = Vue
const App = {
setup() {
// state.count变化cb会执行
// 常用方式还有watch(()=>state.count, cb)
watch(() => {
console.log('count变了:' + state.count)
})
}
}
2
3
4
5
6
7
8
9
10
11
引用对象:单个原始值响应化
<div>counter: {{ counter }}</div>
const { ref } = Vue
const App = {
setup() {
// 返回响应式的Ref对象
const counter = ref(1)
setTimeout(() => {
// 要修改对象的value
counter.value++
}, 1000) // 添加counter
return { state, add, counter }
}
}
2
3
4
5
6
7
8
9
10
11
12
体验逻辑组合
03-logic-composition.html
const { createApp, reactive, onMounted, onUnmounted, toRefs } = Vue
// 鼠标位置侦听
function useMouse() {
// 数据响应化
const state = reactive({ x: 0, y: 0 })
const update = (e) => {
state.x = e.pageX
state.y = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
// 转换所有key为响应式数据
return toRefs(state)
}
// 事件监测
function useTime() {
const state = reactive({ time: new Date() })
onMounted(() => {
setInterval(() => {
state.time = new Date()
}, 1000)
})
return toRefs(state)
}
// 逻辑组合
const MyComp = {
template: `
<div>x: {{ x }} y: {{ y }}</div>
<p>time: {{time}}</p>
`,
setup() {
// 使用鼠标逻辑
const { x, y } = useMouse()
// 使用时间逻辑
const { time } = useTime()
// 返回使用
return { x, y, time }
}
}
createApp().mount(MyComp, '#app')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
对比 mixins,好处显而易见:
- x,y,time 来源清晰
- 不会与 data、props 等命名冲突
# Vue3 响应式原理
# Vue2 响应式原理回顾
// 1.对象响应化:遍历每个key,定义getter、setter
// 2.数组响应化:覆盖数组原型方法,额外增加通知逻辑
const originalProto = Array.prototype
const arrayProto = Object.create(originalProto)
;['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(
(method) => {
arrayProto[method] = function() {
originalProto[method].apply(this, arguments)
notifyUpdate()
}
}
)
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return
}
// 增加数组类型判断,若是数组则覆盖其原型
if (Array.isArray(obj)) {
Object.setPrototypeOf(obj, arrayProto)
} else {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
defineReactive(obj, key, obj[key])
}
}
}
function defineReactive(obj, key, val) {
observe(val) // 解决嵌套对象问题
Object.defineProperty(obj, key, {
get() {
return val
},
set(newVal) {
if (newVal !== val) {
observe(newVal) // 新值是对象的情况
val = newVal
notifyUpdate()
}
}
})
}
function notifyUpdate() {
console.log('页面更新!')
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
vue2 响应式弊端:
- 响应化过程需要递归遍历,消耗较大
- 新加或删除属性无法监听
- 数组响应化需要额外实现
- Map、Set、Class 等无法响应式
- 修改语法有限制
# Vue3 响应式原理剖析
vue3 使用 ES6 的 Proxy 特性来解决这些问题。
创建 04-reactivity.js
function reactive(obj) {
if (typeof obj !== 'object' && obj != null) {
return obj
}
// Proxy相当于在对象外层加拦截
// http://es6.ruanyifeng.com/#docs/proxy
const observed = new Proxy(obj, {
get(target, key, receiver) {
// Reflect用于执行对象默认操作,更规范、更友好
// Proxy和Object的方法Reflect都有对应
// http://es6.ruanyifeng.com/#docs/reflect
const res = Reflect.get(target, key, receiver)
console.log(`获取${key}:${res}`)
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
console.log(`设置${key}:${value}`)
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
console.log(`删除${key}:${res}`)
return res
}
})
return observed
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
测试代码
const state = reactive({
foo: 'foo',
bar: { a: 1 }
})
// 1.获取
state.foo // ok
// 2.设置已存在属性
state.foo = 'fooooooo' // ok
// 3.设置不存在属性
state.dong = 'dong' // ok
// 4.删除属性
delete state.dong // ok
2
3
4
5
6
7
8
9
10
11
12
# 嵌套对象响应式
测试:嵌套对象不能响应
// 4.设置嵌套对象属性
react.bar.a = 10 // no ok
// 添加对象类型递归
// 提取帮助方法
const isObject = (val) => val !== null && typeof val === 'object'
function reactive(obj) {
//判断是否对象
if (!isObject(obj)) {
return obj
}
const observed = new Proxy(obj, {
get(target, key, receiver) {
// ...
// 如果是对象需要递归
return isObject(res) ? reactive(res) : res
}
//...
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 避免重复代理
重复代理,比如
reactive(data) // 已代理过的纯对象
reactive(react) // 代理对象
2
解决方式:将之前代理结果缓存,get 时直接使用
const toProxy = new WeakMap() // 形如obj:observed
const toRaw = new WeakMap() // 形如observed:obj
function reactive(obj) {
//...
// 查找缓存,避免重复代理
if (toProxy.has(obj)) {
return toProxy.get(obj)
}
if (toRaw.has(obj)) {
return obj
}
const observed = new Proxy(...)
// 缓存代理结果
toProxy.set(obj, observed)
toRaw.set(observed, obj)
return observed
}
// 测试效果
console.log(reactive(data) === state)
console.log(reactive(state) === state)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 依赖收集
建立响应数据 key 和更新函数之间的对应关系。
# 用法
// 设置响应函数
effect(() => console.log(state.foo))
// 用户修改关联数据会触发响应函数
state.foo = 'xxx'
2
3
4
5
# 设计
实现三个函数:
effect:将回调函数保存起来备用,立即执行一次回调函数触发它里面一些响应数据的 getter
track:getter 中调用 track,把前面存储的回调函数和当前 target,key 之间建立映射关系
trigger:setter 中调用 trigger,把 target,key 对应的响应函数都执行一遍
target,key 和响应函数映射关系
// 大概结构如下所示
// target | depsMap
// obj | key | Dep
// k1 | effect1,effect2...
// k2 | effect3,effect4...
// {target: {key: [effect1,...]}}
2
3
4
5
6
7
# 实现
设置响应函数,创建 effect 函数
// 保存当前活动响应函数作为getter和effect之间桥梁
const effectStack = []
// effect任务:执行fn并将其入栈
function effect(fn) {
const rxEffect = function() {
// 1.捕获可能的异常
try {
// 2.入栈,用于后续依赖收集
effectStack.push(rxEffect)
// 3.运行fn,触发依赖收集
return fn()
} finally {
// 4.执行结束,出栈
effectStack.pop()
}
}
// 默认执行一次响应函数
rxEffect()
// 返回响应函数
return rxEffect
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
依赖收集和触发
function reactive(obj) {
// ...
const observed = new Proxy(obj, {
get(target, key, receiver) {
// ...
// 依赖收集
track(target, key)
return isObject(res) ? reactive(res) : res
},
set(target, key, value, receiver) {
// ...
// 触发响应函数
trigger(target, key)
return res
}
})
}
// 映射关系表,结构大致如下:
// {target: {key: [fn1,fn2]}}
let targetMap = new WeakMap()
function track(target, key) {
// 从栈中取出响应函数
const effect = effectStack[effectStack.length - 1]
if (effect) {
// 获取target对应依赖表
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 获取key对应的响应函数集
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
// 将响应函数加入到对应集合
if (!deps.has(effect)) {
deps.add(effect)
}
}
}
// 触发target.key对应响应函数
function trigger(target, key) {
// 获取依赖表
const depsMap = targetMap.get(target)
if (depsMap) {
// 获取响应函数集合
const deps = depsMap.get(key)
if (deps) {
// 执行所有响应函数
deps.forEach((effect) => {
effect()
})
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59