# vue 学习笔记

# 运⾏环境

  1. node 12.x
  2. vue.js 2.6.x
  3. 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" />
1
2
3
4
5
6
7

⼦给⽗传值

// child this.$emit('add', good) // parent <Cart @add="cartAdd($event)"></Cart>
1

# 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')
1
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'
}
1
2
3
4
5
// brother1
this.$parent.$on('foo', handle)
// brother2
this.$parent.$emit('foo')
1
2
3
4

⽗组件可以通过$children访问⼦组件实现⽗⼦通信。

// parent
this.$children[0].xx = 'xxx'
1
2

注意:$children不能保证⼦元素顺序和$refs有什么区别?// $refs是对象形式,$chilren是数组形式

# $attrs/$listeners

包含了⽗作⽤域中不作为 prop 被识别 (且获取) 的特性绑定 ( classstyle 除外)。当⼀个组件没有声明任何 prop 时,这⾥会包含所有⽗作⽤域的绑定 ( classstyle 除外),并且可以通过vbind="$attrs" 传⼊内部组件——在创建⾼级别的组件时⾮常有⽤。

// child:并未在props中声明foo
<p>{{$attrs.foo}}</p>
// parent
<HelloWorld foo="foo"/>
1
2
3
4

# provide/inject

能够实现祖先和后代之间传值

// ancestor
provide() {
    return {foo: 'foo'}
}
// descendant
inject: ['foo']
1
2
3
4
5
6

# 插槽

插槽语法是 Vue 实现的内容分发 API,⽤于复合组件开发。该技术在通⽤组件库开发中有⼤量应⽤。

# 匿名插槽

// comp1
<div>
    <slot></slot>
</div>
// parent
<comp>hello</comp>
1
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>
1
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>
1
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
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

为什么要⽤混⼊(mixin)⽅式写?

主要原因是 use 代码在前, Router 实例创建在后,⽽ install 逻辑⼜需要⽤到该实例

// 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]
    )
  }
}
1
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)
  }
}
1
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)
  }
}
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)
  }
}
1
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
    })
  }
}
1
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)
  }
}
1
2
3
4
5
6
7
8

# vuex3.x 源码解析

Vuex 集中式存储管理应⽤的所有组件的状态,并以相应的规则保证状态以可预测的⽅式发⽣变化

image-20210917204346147

# 核⼼概念

  • state 状态、数据
  • mutations 更改状态的函数
  • actions 异步操作
  • store 包含以上概念的容器

# 状态 - state

state 保存应⽤状态

export default new Vuex.Store({
  state: { counter: 0 }
})
1
2
3

# 状态变更 - mutations

mutations ⽤于修改状态, store.js

export default new Vuex.Store({
  mutations: {
    add(state) {
      state.counter++
    }
  }
})
1
2
3
4
5
6
7

# 派⽣状态 - getters

从 state 派⽣出新状态,类似计算属性

export default new Vuex.Store({
  getters: {
    doubleCounter(state) {
      // 计算剩余数量
      return state.counter * 2
    }
  }
})
1
2
3
4
5
6
7
8

# 动作 - actions

添加业务逻辑,类似于 controller

export default new Vuex.Store({
  actions: {
    add({ commit }) {
      setTimeout(() => {
        commit('add')
      }, 1000)
    }
  }
})
1
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>
1
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 }
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

实现 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)
  }
}
1
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)
  }
}
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

# vue2.x 源码解析

# vue 的设计思想

  • MVVM 模式

    image-20210917205320542

MVVM 框架的三要素: 数据响应式模板引擎及其渲染

数据响应式:监听数据变化并在视图中更新

  • Object.defineProperty()
  • Proxy

模版引擎:提供描述视图的模版语法

  • 插值: {{}}
  • 指令: v-bind, v-on, v-model, v-for, v-if

渲染:如何将模板转换为 html

  • 模板 => vdom => dom

# 数据响应式原理

数据变更能够响应在视图中,就是数据响应式。 vue2 中利⽤ Object.defineProperty() 实现变更检测

image-20210917205457697

简单实现

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'
1
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>
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

遍历需要响应化的对象

// 对象响应化:遍历每个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
1
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])
  })
}
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

如果添加/删除了新属性⽆法检测

function set(obj, key, val) {
  defineReactive(obj, key, val)
}
1
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>
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

# 原理分析

  1. new Vue() ⾸先执⾏初始化,对 data 执⾏响应化处理,这个过程发⽣在 Observer 中

  2. 同时对模板执⾏编译,找到其中动态绑定的数据,从 data 中获取并初始化视图,这个过程发⽣在 Compile 中

  3. 同时定义⼀个更新函数和 Watcher,将来对应数据变化时 Watcher 会调⽤更新函数

  4. 由于 data 的某个 key 在⼀个视图中可能出现多次,所以每个 key 都需要⼀个管家 Dep 来管理多个 Watcher

  5. 将来 data 中数据⼀旦发⽣变化,会⾸先找到对应的 Dep,通知所有 Watcher 执⾏更新函数

image-20210917210601497

涉及类型介绍

  • 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)
  }
}
1
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()
      }
    }
  })
}
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

# observe

// 让我们使一个对象所有属性都被拦截
function observe(obj) {
  if (typeof obj !== 'object' || obj == null) {
    return
  }

  // 创建Observer实例:以后出现一个对象,就会有一个Observer实例
  new Observer(obj)
}
1
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
      }
    })
  })
}
1
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)
  }
})
1
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])
      })
    }
  }
}
1
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上下文的数据
  }
}
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
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])
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# Dep

保存所有 watcher 实例,当某个 key 发生变化,通知他们执行更新

image-20210917212154740

实现思路

  1. defineReactive 时为每⼀个 key 创建⼀个 Dep 实例
  2. 初始化视图时读取某个 key,例如 name1,创建⼀个 watcher1
  3. 由于触发 name1 的 getter ⽅法,便将 watcher1 添加到 name1 对应的 Dep 中
  4. 当 name1 更新, setter 触发时,便可通过对应 Dep 通知其管理所有 Watcher 更新
class Dep {
  constructor() {
    this.deps = []
  }

  addDep(watcher) {
    this.deps.push(watcher)
  }

  notify() {
    this.deps.forEach((dep) => dep.update())
  }
}
1
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

# 文件结构

image-20210917212628843

src 目录

image-20210917212647876

# 调试环境搭建

  • 安装依赖: npm i
  • 安装 rollup: npm i -g rollup
  • 修改 dev 脚本,添加 sourcemap,package.json
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:webfull-dev",
1
  • 运行开发命令: npm run dev
  • 引入前面创建的 vue.js,samples/commits/index.html
<script src="../../dist/vue.js"></script>
1

术语解释:

  • 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
  }
}
1
2
3
4
5
6
7
8
9
10
11

# 初始化流程

  • new Vue()
    • _init()
  • $mount
    • mountComponent()
      • updateComponent()
        • render()
        • update()
      • new Watcher()

image-20210917213752062

入口 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
1
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
1
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')
1
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 */)
}
1
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() {}
}
1
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)
}
1
2
3
4
5

# 异步更新队列

Vue ⾼效的秘诀是⼀套批量、异步的更新策略

image-20210918140854847

  • 事件循环 Event Loop:浏览器为了协调事件处理、脚本执⾏、⽹络请求和渲染等任务⽽制定的⼯作机制。
  • 宏任务 Task:代表⼀个个离散的、独⽴的⼯作单元。 浏览器完成⼀个宏任务,在下⼀个宏任务执⾏开始前,会对⻚⾯进⾏重新渲染。主要包括创建⽂档对象、解析 HTML、执⾏主线 JS 代码以及各种事件如⻚⾯加载、输⼊、⽹络事件和定时器等。
  • 微任务:微任务是更⼩的任务,是在当前宏任务执⾏结束后⽴即执⾏的任务。 如果存在微任务,浏览器会清空微任务之后再重新渲染。 微任务的例⼦有 Promise 回调函数、 DOM 变化等

Tasks, microtasks, queues and schedules (opens new window)

# vue 中的具体实现

image-20210918141106246

  • 异步:只要侦听到数据变化, 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 上。

image-20210918141326989

# 体验虚拟 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>
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
44
45
46
47
48
49
50
51
52
53
54

# 优点

  • 虚拟 DOM 轻量、快速:当它们发⽣变化时通过新旧虚拟 DOM ⽐对可以得到最⼩ DOM 操作量,配合异步更新策略减少刷新频率,从⽽提升性能
patch(vnode, h('div', obj.foo))
1
  • 跨平台:将虚拟 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>
1
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)
}
1
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
1
# patch 获取

patch 是 createPatchFunction 的返回值,传递 nodeOps 和 modules 是 web 平台特别实现

export const patch: Function = createPatchFunction({ nodeOps, modules })
1

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 执⾏更新

image-20210918141847146

# patchVnode

⽐较两个 VNode,包括三种类型操作: 属性更新、⽂本更新、⼦节点更新具体规则如下:

  1. 新⽼节点均有 children ⼦节点,则对⼦节点进⾏ diff 操作,调⽤ updateChildren
  2. 如果新节点有⼦节点⽽⽼节点没有⼦节点,先清空⽼节点的⽂本内容,然后为其新增⼦节点
  3. 当新节点没有⼦节点⽽⽼节点有⼦节点的时候,则移除该节点的所有⼦节点。
  4. 当新⽼节点都⽆⼦节点的时候,只是⽂本的替换。

例子: image-20210918141920946

// patchVnode过程分解
// 1.div#demo updateChildren
// 2.h1 updateChildren
// 3.text ⽂本相同跳过
// 4.p updateChildren
// 5.text setTextContent
1
2
3
4
5
6
# updateChildren

updateChildren 主要作⽤是⽤⼀种较⾼效的⽅式⽐对新旧两个 VNode 的 children 得出最⼩操作补丁。执⾏⼀个双循环是传统⽅式, vue 中针对 web 场景特点做了特别的算法优化,我们看图说话:

image-20210918141935972

在新⽼两组 VNode 节点的左右头尾两侧都有⼀个变量标记,在遍历过程中这⼏个变量都会向中间靠拢。

当 oldStartIdx > oldEndIdx 或者 newStartIdx > newEndIdx 时结束循环。

下⾯是遍历规则:

⾸先, oldStartVnode、 oldEndVnode 与 newStartVnode、 newEndVnode 两两交叉⽐较,共有 4 种⽐较⽅法。

当 oldStartVnode 和 newStartVnode 或者 oldEndVnode 和 newEndVnode 满⾜ sameVnode,直接将该 VNode 节点进⾏ patchVnode 即可,不需再遍历就完成了⼀次循环。如下图,

image-20210918141946923

如果 oldStartVnode 与 newEndVnode 满⾜ sameVnode。说明 oldStartVnode 已经跑到了 oldEndVnode 后⾯去了,进⾏ patchVnode 的同时还需要将真实 DOM 节点移动到 oldEndVnode 的后⾯。

image-20210918141955009

如果 oldEndVnode 与 newStartVnode 满⾜ sameVnode,说明 oldEndVnode 跑到了 oldStartVnode 的前⾯,进⾏ patchVnode 的同时要将 oldEndVnode 对应 DOM 移动到 oldStartVnode 对应 DOM 的前⾯。

image-20210918142015255

如果以上情况均不符合,则在 old VNode 中找与 newStartVnode 相同的节点,若存在执⾏ patchVnode,同时将 elmToMove 移动到 oldStartIdx 对应的 DOM 的前⾯

image-20210918142022262

当然也有可能 newStartVnode 在 old VNode 节点中找不到⼀致的 sameVnode,这个时候会调⽤ createElm 创建⼀个新的 DOM 节点。

image-20210918142027558

⾄此循环结束,但是我们还需要处理剩下的节点。

当结束时 oldStartIdx > oldEndIdx,这个时候旧的 VNode 节点已经遍历完了,但是新的节点还没有。说明了新的 VNode 节点实际上⽐⽼的 VNode 节点多,需要将剩下的 VNode 对应的 DOM 插⼊到真实 DOM 中,此时调⽤ addVnodes(批量调⽤ createElm 接⼝)。

image-20210918142035271

但是,当结束时 newStartIdx > newEndIdx 时,说明新的 VNode 节点已经遍历完了,但是⽼的节点还有剩余,需要从⽂档中将多余的节点删除

image-20210918142042005

# 模板编译

模板编译的主要⽬标是将模板(template)转换为渲染函数(render)

image-20210918142955170

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>
1
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
    )
  }
})
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,结构如下:

image-20210918144357183

解析器内部分了 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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

代码实现, src/compiler/optimizer.js - optimize

标记结束

image-20210918144502018

# 代码⽣成 - generate

将 AST 转换成渲染函数中的内容,即代码字符串。 generate ⽅法⽣成渲染函数代码,src/compiler/codegen/index.js

⽣成的 code ⻓这样

_c('div', { attrs: { id: 'demo' } }, [
  _c('h1', [_v('Vue.js测试')]),
  _c('p', [_v(_s(foo))])
])
1
2
3
4

# v-if、 v-for

着重观察⼏个结构性指令的解析过程

# 解析 v-if parser/index.js

processIf ⽤于处理 v-if 解析

解析结果:

image-20210918145008516

# 代码⽣成, 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)
    }"
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'
1

image-20210918145222213

# 代码⽣成, 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)
}"
1
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')
  }
})
1
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
    )
  }
})
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)
}"
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
}
1
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
    )
  }
})
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()

事件监听和派发者均是组件实例,自定义组件中一定伴随着原生事件的监听与处理

# hook

在 Vue 当中,hooks 可以作为一种 event,在 Vue 的源码当中,称之为 hookEvent。

<Table @hook:updated="handleTableUpdated"></Table>
1

场景:有一个来自第三方的复杂表格组件,表格进行数据更新的时候渲染时间需要 1s,由于渲染时间较长,为了更好的用户体验,我希望在表格进行更新时显示一个 loading 动画。修改源码这个方案很不优雅。

callHook src\core\instance\lifecycle.js

export function callHook(vm: Component, hook: string) {
  // ...
  // 若包含hook事件,则一并派发
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
}
1
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
  }
}
1
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>
1
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'
  }
})
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
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!')
})
1
2
3
4
5
6
7
8
9
10
11
12

# 基础实现

使用渲染器将 vue 实例成 HTML 字符串并返回

安装 vue-server-renderer

# 确保版本相同且匹配
npm i vue vue-server-renderer -S
1
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)
  })
1
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!')
})
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

# 理解 ssr

# 传统 web 开发

网页内容在服务端渲染完成,一次性传输到浏览器

image-20210918180351155

// 客户端渲染,返回给客户端的只是页面骨架,没有实际内容
// 真正的内容是在客户端使用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)
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

打开页面查看源码,浏览器拿到的是全部的 dom 结构

image-20210918180849241

# 单页应用 Single Page App

单页应用优秀的用户体验,使其逐渐成为主流,页面内容由 JS 渲染出来,这种方式称为客户端渲染。

image-20210918181111773

打开页面查看源码,浏览器拿到的仅有宿主元素#app,并没有内容。

image-20210918181517823

SPA 问题:

  • seo
  • 首屏加载速度

# 服务端渲染 Server Side Render

SSR 解决方案,后端渲染出完整的首屏的 dom 结构返回,前端拿到的内容包括首屏及完整 SPA 结构,应用激活后依然按照 spa 方式运行,这种页面渲染方式被称为服务端渲染 (server side render)

image-20210918183153138

# vue-ssr 实战

# 新建工程

vue-cli 创建工程即可

# 演示项目使用vue-cli 4.x创建
vue create ssr
1
2

# 安装依赖

# 要确保vue、vue-server-renderer版本一致
npm install vue-server-renderer@2.6.10 -S
1
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)
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

# 路由

路由支持仍然使用 vue-router

# 安装

若未引入 vue-router 则需要安装

npm i vue-router -s
1

# 创建路由实例

每次请求的 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('渲染出错')
  }
})
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

# 同构开发 SSR 应用

对于同构开发,我们依然使用 webpack 打包,我们要解决两个问题:服务端首屏渲染和客户端激活

# 构建流程

目标是生成一个「服务器 bundle」用于服务端首屏渲染,和一个「客户端 bundle」用于客户端激活。

image-20210918184205776

# 代码结构

除了两个不同入口之外,其他结构和之前 vue 应用完全相同

src ├── router ├────── index.js # 路由声明 ├── store ├────── index.js # 全局状态 ├── main.js # 用于创建 vue 实例 ├── entry-client.js # 客户端入口,用于静态内容“激活” └── entry-server.js # 服务端入口,用于首屏内容渲染
1
# 路由配置

创建@/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') } }
    ]
  })
}
1
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 }
}
1
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)
  })
}
1
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')
})
1
2
3
4
5
6
7
# webpack 配置

安装依赖

npm install webpack-node-externals lodash.merge -D
1

具体配置,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
        })
      })
  }
}
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 脚本配置

安装依赖

npm i cross-env -D
1

定义创建脚本,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"
},
1
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>
1
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)
})
1
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
1
// 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
      }
    }
  })
}
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 }
}
1
2
3
4
5
6
7
8
9
10

使用,.src/App.vue

<h2 @click="$store.commit('add')">{{$store.state.count}}</h2>
1

注意事项:注意打包和重启服务

# 数据预取

服务器端渲染的是应用程序的"快照",如果应用依赖于一些异步数据,那么在开始渲染之前,需要先预取和解析好这些数据。异步数据获取,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)
        })
      }
    }
  })
}
1
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')
  }
}
1
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);
    });
};
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

客户端在挂载到应用程序之前,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__);
}
1
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
      })
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# TypeScript

# 准备工作

# 新建一个基于 ts 的 vue 项目

image-20210918185755312

# 在已存在项目中安装 typescript

vue add @vue/typescript
1

image-20210918185800542

请暂时忽略引发的几处 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 {}
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

# 范例

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>
1
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
1
2
3
4
5

范例:使用类型别名定义 Feature,types/index.ts

export type Feature = {
  id: number
  name: string
}
1
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>
1
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
1
2
3

# 交叉类型

想要定义某种由多种类型合并而成的类型使用交叉类型

type First = { first: number }
type Second = { second: number }
// FirstAndSecond将同时拥有属性first和second
type FirstAndSecond = First & Second
1
2
3
4

范例:利用交叉类型给 Feature 添加一个 selected 属性

// types/index.ts
type Select = {
  selected: boolean
}
export type FeatureSelect = Feature & Select
1
2
3
4
5

使用这个 FeatureSelect,HelloWorld.vue

features: FeatureSelect[] = [
    { id: 1, name: "类型注解", selected: false },
    { id: 2, name: "编译型语言", selected: true }
];
1
2
3
4
<li :class="{ selected: feature.selected }">{{feature.name}}</li>
1
.selected {
  background-color: rgb(168, 212, 247);
}
1
2
3

# 函数

必填参:参数一旦声明,就要求传递,且类型需符合

// 02-function.ts
function greeting(person: string): string {
  return 'Hello, ' + person
}
greeting('tom')
1
2
3
4
5

可选参数:参数名后面加上问号,变成可选参数

function greeting(person: string, msg?: string): string {
  return 'Hello, ' + person
}
1
2
3

默认值

function greeting(person: string, msg = ''): string {
  return 'Hello, ' + person
}
1
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')
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

范例:新增特性,Hello.vue

<div>
    <input type="text" placeholder="输入新特性" @keyup.enter="addFeature">
</div>
1
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 = "";
}
1
2
3
4
5
6
7
8
9
10
11

范例:生命周期钩子,Hello.vue

created() {
    this.features = [{ id: 1, name: "类型注解" }];
}
1
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
  }
}
1
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' }) // 错误
1
2
3
4
5
6
7
8
9
10
11
12
13

范例:Feature 也可用接口形式约束,./types/index.ts

// 接口中只需定义结构,不需要初始化
export interface Feature {
  id: number
  name: string
}
1
2
3
4
5

Interface vs type aliases (opens new window)

# 泛型

泛型(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)
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' }
        ])
      })
    }
  }
}
1
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
}
1
2
3
4
5

# 声明文件

使用 ts 开发时如果要使用第三方 js 库的同时还想利用 ts 诸如类型检查等特性就需要声明文件,类似 xx.d.ts

同时,vue 项目中还可以在 shims-vue.d.ts 中对已存在模块进行补充

npm i @types/xxx
1

范例:利用模块补充$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
1
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
}
1
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;// 若有返回值则返回值作为事件参数
}
1
2
3
4
5
6
7
8

# 变更监测:@Watch

@Watch('msg')
onMsgChange(val:string, oldVal:any){
    console.log(val, oldVal);
}
1
2
3
4

状态管理推荐使用:vuex-module-decorators (opens new window)

vuex-module-decorators 通过装饰器提供模块化声明 vuex 模块的方法,可以有效利用 ts 的类型系统。

安装

npm i vuex-module-decorators -D
1

根模块清空,修改 store/index.ts

export default new Vuex.Store({})
1

定义 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)
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

使用,App.vue

<p @click="add">{{$store.state.counter.count}}</p>
<p @click="asyncAdd">{{count}}</p>
1
2
import CounterModule from '@/store/counter'

@Component
export default class App extends Vue {
  get count() {
    return CounterModule.count
  }
  add() {
    CounterModule.add()
  }
  asyncAdd() {
    CounterModule.asyncAdd()
  }
}
1
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()
1
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')
1
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)
1
2
3
4
5
6
7
8
9

稍微改造一下使其可以接收参数

function mua(param: string) {
  return function(target, name) {
    target[name] = param
  }
}
1
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>
1
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
  }
}
1
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')
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11

范例:设置⼀个 webpack 配置项⽤于⻚⾯ title,vue.config.js

module.exports = {
  configureWebpack: {
    name: 'vue项⽬最佳实践'
  }
}
1
2
3
4
5

在宿主⻚⾯使⽤ lodash 插值语法使⽤它,./public/index.html

<title><%= webpackConfig.name %></title>
1

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'
  }
}
1
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]'})
}
1
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>
1
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 提供的模式。默认有 developmentproductiontest 三种模式,对应的,它们的配置⽂件形式是 .env.development

范例:定义⼀个开发时可⽤的配置项,创建.env.dev

# 只能⽤于服务端
foo=bar
# 可⽤于客户端
VUE_APP_DONG=dong
1
2
3
4

修改 mode 选项覆盖模式名称,package.json

"serve": "vue-cli-service serve --mode dev"
1

# 权限控制

路由分为两种: constantRoutesasyncRoutes ,前者是默认路由可直接访问,后者中定义的路由需要先登录,获取⻆⾊并过滤后动态加⼊到 Router 中。

image-20210920112423135

  • 路由定义,router/index.js
  • 创建⽤户登录⻚⾯,views/Login.vue
  • 路由守卫:创建./src/permission.js,并在 main.js 中引⼊

# ⽤户登录状态维护

维护⽤户登录状态:路由守卫 => ⽤户登录 => 获取 token 并缓存

image-20210920112423135

  • 路由守卫:src/permission.js
  • 请求登录:components/Login.vue
  • user 模块:维护⽤户数据、处理⽤户登录等,store/modules/user.js
  • 测试~

# ⽤户⻆⾊获取和权限路由过滤

登录成功后,请求⽤户信息获取⽤户⻆⾊信息,然后根据⻆⾊过滤 asyncRoutes,并将结果动态添加⾄ router

image-20210920112423135

  • 维护路由信息,实现动态路由⽣成逻辑,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 {
    // 未登录...
  }
})
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
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 规范⽣成接⼝。

使⽤步骤:

  1. 登录easy-mock (opens new window)

    若远程不可⽤,可以搭建本地 easy-mock 服务(nvm + node + redis + mongodb)先安装 node 8.x、redis 和 mongodb 启动命令:

    • 切 node v8: nvm listnvm use 8.16.0
    • 起 redis: redis-server
    • 起 mongodb: mongod
    • 起 easy-mock 项⽬: npm run dev
  2. 创建⼀个项⽬

  3. 创建需要的接⼝

    // 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
  4. 调⽤:修改 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 TestingE2E Testing

    image-20210920114413051

  • 单元测试解决⽅案选择: Jest

    image-20210920114426408

  • 端到端测试解决⽅案选择:Cypress

    image-20210920114434636

# 在已存在项⽬中集成

集成 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)
  })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 执⾏单元测试

  • 执⾏: npm run test:unit

image-20210921010209278

# 断⾔ 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')
  })
})
1
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('按钮点击')
})
1
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}"],
}
1
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>
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

现在的代码,依然是测试没有报错,但是覆盖率只有 66%了,⽽且没有覆盖的代码⾏数,都标记了出来,继续努⼒加测试吧

Vue 组件单元测试 cookbook (opens new window)

Vue Test Utils 使⽤指南 (opens new window)

# E2E 测试

借⽤浏览器的能⼒,站在⽤户测试⼈员的⻆度,输⼊框,点击按钮等,完全模拟⽤户,这个和具体的框架关系不⼤,完全模拟浏览器⾏为。

运⾏ E2E 测试

npm run test:e2e
1

修改 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')
  })
})
1
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>
1
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', '按钮点击')
  })
})
1
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
1
  • mongodb

    • windows
    • mac
  • redis

    • windows
    • mac
  • 克隆 esay-mock 项⽬

     git clone https://github.com/easy-mock/easy-mock.git
    
    1

# 起服务

  • mongodb

    mongod
    
    1
  • redis

    redis-server
    
    1
  • esay-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

# 源码结构

image-20210920232649969

源码位置是在 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>
1
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>
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

计算属性

<div>doubleCount: {{doubleCount}}</div>
1
const { computed } = Vue

const App = {
  setup() {
    const state = reactive({
      count: 0,
      // computed()返回一个不可变的响应式引用对象
      // 它封装了getter的返回值
      doubleCount: computed(() => state.count * 2)
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

事件处理

<div @click="add">count: {{ state.count }}</div>
1
const App = {
  setup() {
    // setup中声明一个add函数
    function add() {
      state.count++
    } // 传入渲染函数上下文
    return { state, add }
  }
}
1
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)
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11

引用对象:单个原始值响应化

<div>counter: {{ counter }}</div>
1
const { ref } = Vue
const App = {
  setup() {
    // 返回响应式的Ref对象
    const counter = ref(1)
    setTimeout(() => {
      // 要修改对象的value
      counter.value++
    }, 1000) // 添加counter
    return { state, add, counter }
  }
}
1
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')
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
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('页面更新!')
}
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
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
}
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

测试代码

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
1
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
    }
    //...
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 避免重复代理

重复代理,比如

reactive(data) // 已代理过的纯对象
reactive(react) // 代理对象
1
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)
1
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'
1
2
3
4
5
# 设计

实现三个函数:

effect:将回调函数保存起来备用,立即执行一次回调函数触发它里面一些响应数据的 getter

track:getter 中调用 track,把前面存储的回调函数和当前 target,key 之间建立映射关系

trigger:setter 中调用 trigger,把 target,key 对应的响应函数都执行一遍

image-20210921001407937

target,key 和响应函数映射关系

// 大概结构如下所示
//    target | depsMap
//      obj  |   key  |  Dep
//               k1   |  effect1,effect2...
//               k2   |  effect3,effect4...

// {target: {key: [effect1,...]}}
1
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
}
1
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()
      })
    }
  }
}
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
最近更新: 4 小时前