核心工程项目精选

# vue部分

# vue

# 核心知识点【最要点】

# 数据驱动分析

  • Vue 实例化
  • Vue 实例挂载
  • render
  • Virtual DOM
  • createElement
  • update

# 组件化分析

  • createComponent
  • patch
  • 合并配置
  • 生命周期
  • 组件注册
  • 异步组件

# 响应式原理分析

  • 响应式对象 / 依赖收集
  • 派发更新 / nextTick
  • 检测变化的注意事项
  • 计算属性 VS 侦听属性
  • 组件更新

# 总体把握

  • template风格、对象配置风格
  • 虚拟dom思想(js对象操作代替dom操作)
  • diff算法思想(同层比较,添加、移动、删除)
  • 组件化思想(组件编译、组件通信)
  • 数据响应式(依赖收集、派发更新,发布订阅)

图示分析

img

  • instance/index.js 真正的Vue的构造函数,并在Vue的原型上扩展方法
  • core/index.js 增加全局API方法
  • runtime/index.js 扩展$mount方法及平台对应的代码

# 响应式

# SPA/MPA

单页面应用(SPA),通俗一点说就是指只有一个主页面的应用,浏览器一开始要加载所有必须的 html, js, css。所有的页面内容都包含在这个所谓的主页面中。但在写的时候,还是会分开写(页面片段),然后在交互的时候由路由程序动态载入,单页面的页面跳转,仅刷新局部资源。多应用于pc端。所有内容都包含在主页面,对每一 个功能模块组件化。单页应用跳转,就是切换相关组件,仅仅刷新局部资源

多页面(MPA),就是指一个应用中有多个页面,每个页面必须重复加载js、css等相关资源。 多页应用跳转,需要整页资源刷新。

优点:

  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
  • 基于上面一点,SPA 相对对服务器压力小; (内容的改变不需要重新加载整个页面)
  • 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;
  • 页面效果会比较炫酷(比如切换页面内容时的专场动画)

缺点:

  • 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;
  • 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理
  • SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。不利于SEO的优化(如果要支持SEO,建议通过服务端来进行渲染组件);
  • 页面复杂度提高很多;

区别:775316ebb4c727f7c8771cc2c06e06dd.jpg

# 如何在单页 Vue 应用(SPA)中实现路由

可以通过官方的 vue-router 库在用 Vue 构建的 SPA 中进行路由。该库提供了全面的功能集,其中包括嵌套路线、路线参数和通配符、过渡、HTML5 历史与哈希模式和自定义滚动行为等功能。 Vue 还支持某些第三方路由器包。

# MVC/MVVM

18-29-43 18-30-14

# 区别

mvc和mvvm其实区别并不大。都是一种设计思想。主要就是mvc中Controller演变成mvvm中的viewModel。mvvm主要解决了mvc中大量的DOM 操作使页面渲染性能降低,加载速度变慢,影响用户体验。和当 Model 频繁发生变化,开发者需要主动更新到View

# VUE示例

Vue是MVVM框架,但是不是严格符合MVVM,因为MVVM规定Model和View不能直接通信,而Vue的ref可以做到这点

通过一个 Vue 实例来说明 MVVM 的具体实现

image-20210302091006131

(1)View 层

<div id="app">
    <p>{{message}}</p>
    <button v-on:click="showMessage()">Click me</button>
</div>
1
2
3
4

(2)ViewModel 层

var app = new Vue({
    el: '#app',
    data: {  // 用于描述视图状态   
        message: 'Hello Vue!', 
    },
    methods: {  // 用于描述视图行为  
        showMessage(){
            let vm = this;
            alert(vm.message);
        }
    },
    created(){
        let vm = this;
        ajax({// Ajax 获取 Model 层的数据
            url: '/your/server/data/api',
            success(res){
                vm.message = res;
            }
        });
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

(3) Model 层

{
    "url": "/your/server/data/api",
    "res": {
        "success": true,
        "name": "samy",
        "domain": "www.baidu.com"
    }
}
1
2
3
4
5
6
7
8

# 渐进式框架

使用渐进式框架的代价很小,从而使现有项目(使用其他技术构建的项目)更容易采用并迁移到新框架Vue.js 是一个渐进式框架因为你可以逐步将其引入现有应用,而不必从头开始重写整个程序。

Vue 的最基本和核心的部分涉及“视图”层,因此可以通过逐步将 Vue 引入程序并替换“视图”实现来开始你的旅程

由于其不断发展的性质,Vue 与其他库配合使用非常好,并且非常容易上手。这与 Angular.js 之类的框架相反,后者要求将现有程序完全重构并在该框架中实现。

# 响应原理/双向数据绑定

# 要点

# 数据劫持部分

vue 双向数据绑定是通过 数据劫持 结合 发布订阅模式 的方式来实现的,也就是说数据和视图同步,数据发生变化,视图跟着变化,视图变化,数据也随之发生改变; 核心:Object.defineProperty() 方法。在回答时可以带出一些相关知识点(比如多层对象是通过递归来实现劫持,顺带提出Vue3中是使用proxy来实现响应式数据

  • 如果是对象则采用Object.defineProperty()的方式定义数据拦截;(只会劫持已经存在的属性data对象里面的数据);
  • 如果是数组,则覆盖数组的7个变更方法实现变更通知; 【PPU S SSR】
# 视图更新部分
  • 由于 Vue 执行一个组件的 render 函数是由 Watcher 去代理执行的,Watcher 在执行前会把 Watcher 自身先赋值给 Dep.target 这个全局变量,等待响应式属性去收集它。
  • 在组件执行render函数时访问了响应式属性,响应式属性就会精确的收集到当前全局存在的 Dep.target 作为自身的依赖。
  • 在响应式属性发生更新时通知 Watcher 去重新调用vm._update(vm._render())进行组件的视图更新,视图更新的时候会通过diff算法对比新老vnode差异,通过patch即时更新DOM

image-20210209222603052

​ Vue 采用 数据劫持 结合 发布者-订阅者 模式的方式,通过 Object.defineProperty() 来劫持各个属性的 setter 以及 getter,在数据发生变化的时候,发布消息给依赖收集器,去通知观察者,做出对应的回调函数去更新视图。在数据变动时发布消息给订阅者,触发相应的监听回调。【要点】

具体就是【要点】:Vue实现数据双向绑定的效果,需要三大模块; MVVM作为绑定的入口,整合Observer,Compile和Watcher三者(OCWD),通过Observe来监听model的变化,通过Compile来解析编译模版指令,最终利用Watcher搭起Observer和Compile之前的通信桥梁,从而达到数据变化 => 更新视图,视图交互变化(input) => 数据model变更的双向绑定效果。【要点】

image-20210411000328434

vue 实现数据双向绑定主要是

​ Vue 采用 数据劫持 结合 发布者-订阅者 模式的方式,

  • 通过 Object.defineProperty() 来劫持各个属性的 setter 以及 getter
  • 在数据发生变化的时候,发布消息给依赖收集器,去通知观察者,做出对应的回调函数去更新视图。
  • 在数据变动时发布消息给订阅者,触发相应的监听回调。【要点】

# 原理

  1. Object.defineProperty 只能劫持对象的属性,因此对每个对象的属性进行遍历时,如果属性值也是对象需要深度遍历,那么就比较麻烦了,所以在比较 Proxy 能完整劫持对象的对比下,选择 Proxy。
  2. Vue 中使用 Object.defineProperty 进行双向数据绑定时,告知使用者是可以监听数组的,但是只是监听了数组的 push()、pop()、unshift()、shift()、splice()、sort()、reverse() 这7种方法,其他数组的属性检测不到。【PPUSSSR】

有一些对属性的操作,使用这种方法无法拦截,比如说通过下标方式修改数组数据或者给对象新增属性,vue 内部通过重写函数解决了这个问题。【目前可以通过set处理】

在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。使用Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为这是 ES6 的语法。

# 对象处理

内部依赖收集是怎样做到的,每个属性都拥有自己的dep属性,存放他所依赖的watcher,当属性变化后会通知自己对应的watcher去更新 (其实后面会讲到每个对象类型自己本身也拥有一个dep属性,这个在$set面试题中在进行讲解)

这里可以引出性能优化相关的内容

  • 对象层级过深,性能就会差 ;
  • 不需要响应数据的内容不要放到data中 ;
  • Object.freeze() 可以冻结数据;
  walk(obj: Object) {
    const keys = Object.keys(obj);
    /*walk方法会遍历对象的每一个属性进行defineReactive绑定*/
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]]);
    }
  }
/*为对象defineProperty上在变化时通知的属性*/
export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: Function
) {
  /*在闭包中定义一个dep对象, 每个属性都增加一个dep 收集依赖*/
  const dep = new Dep();

  const property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return;
  }

  /*如果之前该对象已经预设了getter以及setter函数则将其取出来,新定义的getter/setter中会将其执行,保证不会覆盖之前已经定义的getter/setter。*/
  // cater for pre-defined getter/setters
  const getter = property && property.get;
  const setter = property && property.set;

  /*对象的子对象递归进行observe并返回子节点的Observer对象*/
  let childOb = observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      /*如果原本对象拥有getter方法则执行*/
      const value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        /*进行依赖收集*/
        dep.depend();
        if (childOb) {
          /*子对象进行依赖收集,其实就是将同一个watcher观察者实例放进了两个depend中,一个是正在本身闭包中的depend,另一个是子元素的depend*/
          childOb.dep.depend();// 数组依赖收集
        }
        if (Array.isArray(value)) {
          dependArray(value);
        }
      }
      return value;
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val;
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return;
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== "production" && customSetter) {
        customSetter();
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      /*新的值需要重新进行observe,保证数据响应式*/
      childOb = observe(newVal);
      /*dep对象通知所有的观察者*/
      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
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
# 数组处理

简单来说就是,重写了数组中的那些原生方法,

  • 首先获取到这个数组的__ob__,也就是它的Observer对象,
  • 如果有新的值,就调用observeArray继续对新的值观察变化(也就是通过target__proto__ == arrayMethods来改变了数组实例的型),
  • 然后手动调用notify,通知渲染watcher,执行update。

在Vue中修改数组的索引和长度是无法监控到的**。需要通过以上7种变异方法修改数组才会触发数组对应的watcher进行更新**。数组中如果是对象数据类型也会进行递归劫持。

那如果想更改索引更新数据怎么办?

可以通过Vue.$set()来进行处理 =》 核心内部用的是splice方法

observeArray(value) {
  value.forEach(item => {
    observe(item); // 观测数组中的对象类型
  })
}

// 拿到数组原型上的方法 (原来的方法)
let oldArrayProtoMethods = Array.prototype;
// 继承一下ES5的方法,
export let arrayMethods = Object.create(oldArrayProtoMethods); //继承;__proto__指向;
// 跟源码中的entry-runtime-with-compiler.js的挂载mount处理类似;// 切片 =》 函数劫持
// const mount = Vue.prototype.$mount // 切片 =》 函数劫持
// Vue.prototype.$mount = function (){ return mount.call(this, el, hydrating) }

// 寄生组合继承; extend的实现
// Sub.prototype = Object.create(Super.prototype) // 继承父类,原型链指向父类;
// Sub.prototype.constructor = Sub //自己的原型构造再指回自己;

/**
 * - 如果是对象则采用Object.defineProperty()的方式定义数据拦截;
 * - 如果是数组,则覆盖数组的7个变更方法实现变更通知;
 */
//7种方法重写 PPSUSSR
let methods = ['push','pop','shift','unshift','splice','sort','reverse']
methods.forEach(method => {
  arrayMethods[method] = function (...args) { // this就是observer里的value
    const result = oldArrayProtoMethods[method].apply(this, args);//处理push等相关参数;
    let inserted;
    let ob = this.__ob__;
    switch (method) {
      case 'push': // arr.push({a:1},{b:2})
      case 'unshift': //这两个方法都是追加 追加的内容可能是对象类型,应该被再次进行劫持
        inserted = args;
        break;
      case 'splice': // vue.$set原理
        // slice(2);  截取数组中第三个元素,以及后面所有元素; splice也一样
        //slice(2,5); 截取第三个到第六个前的所有元素
        inserted = args.slice(2); // arr.splice(0,1,{a:1},{a:1},{a:1})
      default:
        break;
    }
    if (inserted) ob.observeArray(inserted); //对新增的每一项进行观测; 给数组新增的值也要进行观测
    ob.dep.notify(); // 通知数组更新
    return result;
  }
});
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

# 快速Mock【推荐】

# 对象观察

观察者模式实现:(源码精简)

三步骤:

  1. 通过 watcher.evaluate() 将自身实例赋值给 Dep.target
  2. 调用 dep.depend() 将dep实例将 watcher 实例 push 到 dep.subs中
  3. 通过数据劫持,在调用被劫持的对象的 set 方法时,调用 dep.subs 中所有的 watcher.update()
class Dep {// 观察者
    constructor() {
        this.subs = []
    }
    addSub(sub) {
        this.subs.push(sub)
    }
    depend() {
        if (Dep.target) { 
            Dep.target.addDep(this);
        }
    }
    notify() {
        this.subs.forEach(sub => sub.update())
    }
}

class Watcher {// 被观察者
    constructor(vm, expOrFn) {
        this.vm = vm;
        this.getter = expOrFn;
        this.value;
    }
    get() {
        Dep.target = this;
        var vm = this.vm;
        var value = this.getter.call(vm, vm);
        return value;
    }
    evaluate() {
        this.value = this.get();
    }
    addDep(dep) {
        dep.addSub(this);
    }
    update() {
        console.log('更新, value:', this.value)
    }
}
// 观察者实例
var dep = new Dep();
//  被观察者实例
var watcher = new Watcher({x: 1}, (val) => val);
watcher.evaluate();//通过 `watcher.evaluate()` 将自身实例赋值给 `Dep.target`

// 观察者监听被观察对象
dep.depend()//调用 `dep.depend()` 将dep实例的 watcher 实例 push 到 dep.subs中
dep.notify()//通过数据劫持,在调用被劫持的对象的set方法时,调用 dep.subs 中所有的 `watcher.update()`
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

js 实现简单的双向绑定

<body>
    <div id="app">
        <input type="text" id="txt">
        <p id="show"></p>
    </div>
    <script>
        window.onload = function() {
            let obj = {};
            Object.defineProperty(obj, "txt", {
                get: function() {
                    return obj;
                },
                set: function(newValue) {
                    document.getElementById("txt").value = newValue;
                    document.getElementById("show").innerHTML  = newValue;
                }
            })
            document.addEventListener("keyup", function(e) {
                obj.txt = e.target.value;
            })
        }
    </script>
</body>
<body>
    <div id="app"></div>
    <script>
        let state = {count:0,a:1,a:{b:{c:{}}}}; // 1.变成响应式的数据
        let active;
        function defineReactive(obj){
            for(let key in obj){
                let value = obj[key]; // 对象对应的值
                let dep = [];
                Object.defineProperty(obj,key,{ // defineProperty
                    get(){
                        if(active){
                            dep.push(active);   // 依赖收集 
                        }
                        return value;
                    },
                    set(newValue){ // 触发更新
                        value = newValue;
                        dep.forEach(watcher =>watcher());
                    }
                });
            }
        }
        defineReactive(state);
        // 插入到页面的功能 需要保存起来
        const watcher = (fn)=>{
            active = fn;
            fn(); // 调用函数
            active = null; // 后续不在watcher中取值 不触发依赖收集
        }
        watcher(()=>{
            app.innerHTML = state.count; // 取值
        })
        watcher(()=>{
            console.log(state.count);
        })
        state.count
        state.count++;
    </script>
</body>
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
# 数组模拟
let state = [1,2,3] // 1.变成响应式的数据
let originalArray = Array.prototype; // 数组原来的方法
let arrayMethods = Object.create(originalArray)// 不是深拷贝
function defineReactive(obj){
    arrayMethods.push = function (...args) {
        originalArray.push.call(this,...args)// 函数劫持
        render(); // 更新视图
    }
    obj.__proto__ = arrayMethods; // 不行可以循环赋值, js中的原型链
}
defineReactive(state); //
// 插入到页面的功能 需要保存起来
function render(){
    app.innerHTML = state;
}
render();
setTimeout(() => {
    state.push(4)
}, 1000);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# data

# 函数问题

因为js本身的特性带来的,如果 data 是一个对象,那么由于对象本身属于引用类型,当我们修改其中的一个属性时,使得所有组件实例共用了一份data,造成了数据污染,会影响到所有Vue实例的数据。

每次使用组件时都会对组件进行实例化操作,并且调用data函数返回一个对象作为组件的数据源。这样可以保证多个组件间数据互不影响;

同一个组件被复用多次,会创建多个实例。这些实例用的是同一个构造函数,如果data是一个对象得话,那么所有组件都共享了一个对象。为了保证组件的数据独立性要求每个组件必须通过data函数返回一个对象作为组件的状态;

/*初始化data*/
function initData(vm: Component) {
  /*得到data数据*/
  let data = vm.$options.data;
  data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};

  /*对对象类型进行严格检查,只有当对象是纯javascript对象的时候返回true*/
  if (!isPlainObject(data)) {
    data = {};
    process.env.NODE_ENV !== "production" &&
      warn(
        "data functions should return an object:\n" +
          "https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function",
        vm
      );
  }
  .....
}

function getData(data: Function, vm: Component): any {
  try {
    return data.call(vm);
  } catch (e) {
    handleError(e, vm, `data()`);
    return {};
  }
}
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
# 原理
/*初始化props、methods、data、computed与watch*/
export function initState(vm: Component) {
  vm._watchers = [];
  const opts = vm.$options;
  /*初始化props*/
  if (opts.props) initProps(vm, opts.props);
  /*初始化方法*/
  if (opts.methods) initMethods(vm, opts.methods);
  /*初始化data*/
  if (opts.data) {
    initData(vm); // 初始化数据
  } else {
    /*该组件没有data的时候绑定一个空对象*/
    observe((vm._data = {}), true /* asRootData */);
  }
  /*初始化computed*/
  if (opts.computed) initComputed(vm, opts.computed);
  /*初始化watchers*/
  if (opts.watch) initWatch(vm, opts.watch);
}
/*初始化data*/
function initData(vm: Component) {
  /*得到data数据*/
  let data = vm.$options.data;
  data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};

  /*对对象类型进行严格检查,只有当对象是纯javascript对象的时候返回true*/
  if (!isPlainObject(data)) {
    data = {};
    process.env.NODE_ENV !== "production" &&
      warn(
        "data functions should return an object:\n" +
          "https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function",
        vm
      );
  }
  // proxy data on instance
  /*遍历data对象*/
  const keys = Object.keys(data);
  const props = vm.$options.props;
  let i = keys.length;

  //遍历data中的数据
  while (i--) {
    /*保证data中的key不与props中的key重复,props优先,如果有冲突会产生warning*/
    if (props && hasOwn(props, keys[i])) {
      process.env.NODE_ENV !== "production" &&
        warn(
          `The data property "${keys[i]}" is already declared as a prop. ` +
            `Use prop default value instead.`,
          vm
        );
    } else if (!isReserved(keys[i])) {
      /*判断是否是保留字段*/

      /*这里是我们前面讲过的代理,将data上面的属性代理到了vm实例上*/
      proxy(vm, `_data`, keys[i]);
    }
  }
  // observe data // 观测数据
  /*从这里开始我们要observe了,开始对数据进行绑定,这里有尤大大的注释asRootData,这步作为根数据,下面会进行递归observe进行对深层对象的绑定。*/
  observe(data, true /* asRootData */);
}

function getData(data: Function, vm: Component): any {
  try {
    return data.call(vm);
  } catch (e) {
    handleError(e, vm, `data()`);
    return {};
  }
}
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
# 重置data

使用Object.assign()

vm.$data可以获取当前状态下的datavm.$options.data可以获取到组件初始化状态下的dataObject.assign(this.$data, this.$options.data());

# 快速Mock
function VueComponent(params) {}
VueComponent.prototype.$options = {
  // data: { name: "samy" },
  data() {
    return { name: "samy" };
  },
};

let vc1 = new VueComponent();
let vc2 = new VueComponent();
// vc1.$options.data = "samy1";
// vc2.$options.data = "samy2";
// console.log(vc1.$options);
// console.log(vc2.$options);
vc1.$options.data().name = "samy1";
vc2.$options.data().name = "samy2";
console.log(vc1.$options.data());
console.log(vc2.$options.data());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Vue{
    constructor(options){
        this.data = options.data();
    }
}
let data = ()=>({a:1})
let d1 = new Vue({data});
let d2 = new Vue({data});
d1.data.a = 100;
console.log(d2); // 1
1
2
3
4
5
6
7
8
9
10
# 初始状态

# 如何将获取data中某一个数据的初始状态?

在开发中,有时候需要拿初始状态去计算。例如

data() {
    return {
      num: 10
  },
mounted() {
    this.num = 1000
  },
methods: {
    howMuch() {
        // 计算出num增加了多少,那就是1000 - 初始值
        // 可以通过this.$options.data().xxx来获取初始值
        console.log(1000 - this.$options.data().num)
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 不需要响应式的数据应该怎么处理?

在我们的Vue开发中,会有一些数据,从始至终都未曾改变过,这种死数据,既然不改变,那也就不需要对他做响应式处理了,不然只会做一些无用功消耗性能,比如一些写死的下拉框,写死的表格数据,这些数据量大的死数据,如果都进行响应式处理,那会消耗大量性能。

// 方法一:将数据定义在data之外
data () {
    this.list1 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    this.list2 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    this.list3 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    this.list4 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    this.list5 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
    return {}
 }
    
// 方法二:Object.freeze()
data () {
    return {
        list1: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
        list2: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
        list3: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
        list4: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
        list5: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
    }
 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 更新检测Vue.set/delete

为什么$set可以触发更新,我们给对象和数组本身都增加了dep属性。当给对象新增不存在的属性则触发对象依赖的watcher去更新,当修改数组索引时我们调用数组本身的splice方法去更新数组

还是由于 JavaScript 的限制,Vue 不能检测对象属性的添加或删除

对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value) 来实现为对象添加响应式属性

# 原理
# set实现
export function set (target: Array | Object, key: any, val: any): any {
    // 1.是开发环境 target 没定义或者是基础类型则报错
    if (process.env.NODE_ENV !== 'production' &&
        (isUndef(target) || isPrimitive(target))
    ) {
        warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
    }
    // 2.如果是数组 Vue.set(array,1,100); 调用我们重写的splice方法 (这样可以更新视图)
    if (Array.isArray(target) && isValidArrayIndex(key)) {
      // 判断谁大谁小
        target.length = Math.max(target.length, key)
        target.splice(key, 1, val)
        return val
    }
    // 3.如果是对象本身的属性,则直接添加即可
    if (key in target && !(key in Object.prototype)) {
        target[key] = val
        return val
    }
    const ob = (target: any).__ob__
    // 4.如果是Vue实例 或 根数据data时 报错;不允许修改;
    if (target._isVue || (ob && ob.vmCount)) {
        process.env.NODE_ENV !== 'production' && warn(
        'Avoid adding reactive properties to a Vue instance or its root $data ' +
        'at runtime - declare it upfront in the data option.'
        )
        return val
    }
    // 5.如果不是响应式的也不需要将其定义成响应式属性
    if (!ob) {
        target[key] = val
        return val
    }
    // 6.将属性定义成响应式的
    defineReactive(ob.value, key, val)
    // 7.通知视图更新
    ob.dep.notify()
    return val
}
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

阅读以上源码可知,vm.$set 的实现原理是

  • 如果目标是数组,直接使用数组的 splice 方法触发相应式
  • 如果目标是对象,会先判断属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)

设置示范

  • Vue.set(info,age,22)
  • Vue.set(array,1,100)
methods: { 
  addObjB () { 
    this.obj.b = 'obj.b' //这种更新方式没有用,设置后不更新页面数据
    console.log(this.obj) 
  },
    addObjB2 () {
      this.$set(this.obj, 'b', 'obj.b')
      console.log(this.obj)
    }
}
1
2
3
4
5
6
7
8
9
10
# delete实现
export function del(target: Array<any> | Object, key: any) {
  if (Array.isArray(target) && typeof key === "number") {
    target.splice(key, 1);
    return;
  }
  const ob = (target: any).__ob__;
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== "production" &&
      warn(
        "Avoid deleting properties on a Vue instance or its root $data " +
        "- just set it to null."
      );
    return;
  }
  if (!hasOwn(target, key)) {
    return;
  }
  delete target[key];
  if (!ob) {
    return;
  }
  ob.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

设置示范

  • target.splice(key, 1);
  • delete target[key];
# 相关问题

# 为什么会出现vue修改数据后页面没有刷新?

受 ES5 的限制,Vue.js 不能检测到对象属性的添加或删除。因为 Vue.js 在初始化实例时将属性转为 getter/setter,所以属性必须在 data 对象上才能让 Vue.js 转换它,才能让它是响应的

# vue不能检测数组或对象变动问题的解决方法有哪些?

  • vm.$set(vm.items, indexOfItem, newValue)
  • vm.items.splice
  • 使用this.$forceupdate强制刷新
  • 使用Proxy
  • 使用立即执行函数

# 对象新属性无法更新视图,删除属性无法更新视图,为什么?怎么办?

  • 原因:Object.defineProperty没有对对象的新属性进行属性劫持
  • 对象新属性无法更新视图:使用Vue.$set(obj, key, value),组件中this.$set(obj, key, value)
  • 删除属性无法更新视图:使用Vue.$delete(obj, key),组件中this.$delete(obj, key)

# 直接arr[index] = xxx无法更新视图怎么办?为什么?怎么办?

  • 原因:Vue没有对数组进行Object.defineProperty的属性劫持,所以直接arr[index] = xxx是无法更新视图的
  • 使用数组的splice方法,arr.splice(index, 1, item)
  • 使用Vue.$set(arr, index, value)

# 生命周期

# 图示

Vue 实例生命周期

# 生命周期过程

Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载 Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是 Vue 的生命周期。

# 生命周期的作用

它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑

# 各个生命周期及描述 (8+2)
生命周期 描述
beforeCreate 组件实例被创建之初,组件的属性生效之前;vue实例的挂载元素$el和数据对象 data都是undefined, 还未初始化; 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。
created 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。但真实 dom 还没有生成,$el 还不可用
beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用;vue实例的$el和data都初始化了
mounted el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子;在 mounted 被调用前,Vue 已经将编译好的模板挂载到页面上,所以在 mounted 中可以访问操作 DOM
beforeUpdate 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前
update 组件数据更新之后;由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
activited keep-alive 专属,组件被激活时调用
deactivated keep-alive 专属,组件被销毁时调用
beforeDestory 组件销毁前调用;在这一步,实例仍然完全可用。
destoryed 组件销毁后调用;调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。

# 生命周期总括【要点】

8 个生命周期,创建前/创建后、挂载前/挂载后、更新前/更新后、销毁前/销毁后。Vue 生命周期的作用方便我们通过它的生命周期,在业务代码中更好地操作数据,实现相关功能。这些函数称为生命周期 hook

8个生命周期过程:Vue 实例(组件)从其初始化到销毁和删除都经历生命周期;

  1. 创建前/后
    • beforeCreated 阶段,Vue 实例的挂载元素 $el 和数据对象 data 以及事件还未初始化。
    • created 阶段,Vue 实例的数据对象 data 以及方法的运算有了,$el 还没有。
  2. 载入前/后
    • beforeMount 阶段,render 函数首次被调用,Vue 实例的 $el 和 data 都初始化了,但还是挂载在虚拟的 DOM 节点上
    • mounted 阶段,Vue 实例挂载到实际的 DOM 操作完成,一般在该过程进行 Ajax 交互
  3. 更新前/后
    • 在数据更新之前调用,即发生在虚拟 DOM 重新渲染和打补丁之前,调用 beforeUpdate
    • 在虚拟 DOM 重新渲染和打补丁之后,会触发 updated 方法。
  4. 销毁前/后
    • 在执行实例销毁之前调用 beforeDestory,此时实例仍然可以调用
    • 在执行 destroy 方法后,对 data 的改变不会再触发周期函数,说明此时 Vue 实例已经解除了事件监听以及和 DOM 的绑定,但是 DOM 结构依然存在

image-20211226163401281

# 补充

  • created 实例已经创建完成,因为它是最早触发的原因可以进行一些数据,资源的请求。(服务端渲染支持created方法)
  • mounted 实例已经挂载完成,可以进行一些DOM操作
  • beforeUpdate 可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程
  • updated 可以执行依赖于 DOM 的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。 该钩子在服务器端渲染期间不被调用
  • destroyed 可以执行一些优化操作,清空定时器,解除绑定事件

# 第一次页面加载会触发哪几个生命周期

第一次页面加载时会触发 beforeCreate, created, beforeMount, mounted 这几个生命周期。

# 生命周期内调用异步请求

created, mounted (一般统一放在这里,但是服务器渲染SSR没有这个生命周期)

可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。但是推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面 loading 时间;
  • ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性

# DOM 渲染在哪个周期就已经完成

挂载前后阶段;在 beforeMounted 时它执行了 render 函数,对 $el 和 data 进行了初始化,但此时还是挂载到虚拟的 DOM 节点,然后它mounted 时就完成了 DOM 渲染,这时候我们一般还进行 Ajax 交互。Vue 在 rendercreateElement 的时候,并不是产生真实的 DOM 元素;

dom是在 mounted 中完成渲染;

# 何时需要使用beforeDestory
  • 可能在当前页面中使用$on方法,那需要在组件销毁前解绑;
  • 清除自己定义的定时器;
  • 清除事件的绑定scroll, mousemove, ...

# 原理

Vue的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法

内部主要是使用callHook方法来调用对应的方法。核心是一个发布订阅模式,将钩子订阅好(内部采用数组的方式存储),在对应的阶段进行发布!

image-20210209223339633

initLifecycle(vm);
/*初始化事件*/
initEvents(vm);
/*初始化render*/
initRender(vm);
/*调用beforeCreate钩子函数并且触发beforeCreate钩子事件*/
callHook(vm, "beforeCreate");
initInjections(vm); // resolve injections before data/props
/*初始化props、methods、data、computed与watch*/
initState(vm);
initProvide(vm); // resolve provide after data/props
/*调用created钩子函数并且触发created钩子事件*/
callHook(vm, "created");

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  /*初始化*/
  this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

 if (vm._isMounted) {
   callHook(vm, "beforeUpdate");
 }

/*调用钩子函数并且触发钩子事件*/
export function callHook(vm: Component, hook: string) {
  const handlers = vm.$options[hook];
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm);
      } catch (e) {
        handleError(e, vm, `${hook} hook`);
      }
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit("hook:" + hook);
  }
}
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
export function mountComponent(vm, el) {
  vm.$el = el;
  callHook(vm, 'beforeMount');
  // 在初始化时,下面两方法已经提取挂载到属性上;调用render方法去渲染 el属性;
  // vm._update(vm._render()); // 先调用render方法创建虚拟节点,在将虚拟节点渲染到页面上; 虚拟Dom ====> 真Dom

  //优化后:
  // 先调用_render方法生成虚拟dom,通过_update方法将虚拟dom创建成真实的dom
  // 默认vue是通过watcher来进行渲染  = 渲染watcher (每一个组件都有一个渲染watcher)
  let updateComponent = () => {
    vm._update(vm._render()); // 虚拟节点; 渲染更新
  }
  // 初始化就会创建watcher;new watcher才出发get;
  new Watcher(vm, updateComponent, () => {
    callHook(vm, 'updated')
  }, true);
  // 要把属性 和 watcher绑定在一起 
  callHook(vm, 'mounted');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 快速Mock

合并参数

// function callHook (vm: Component, hook: string) {
//   pushTarget()
//   const handlers = vm.$options[hook]
//   const info = `${hook} hook`
//   if (handlers) {
//     for (let i = 0, j = handlers.length; i < j; i++) {
//       invokeWithErrorHandling(handlers[i], vm, null, vm, info)
//     }
//   }
//   if (vm._hasHookEvent) {
//     vm.$emit('hook:' + hook)
//   }
//   popTarget()
// }

function mergeHook(parentVal, childValue) {
    if (childValue) {
        if (parentVal) {
            return parentVal.concat(childValue);
        } else {
            return [childValue]
        }
    } else {
        return parentVal;
    }
}
function mergeOptions(parent, child) {
    let opts = {};
    for (let key in child) {
        opts[key] = mergeHook(parent[key], child[key]);
    }
    return opts;
}
function callHook(vm, key) {
    vm.options[key].forEach(hook => hook());
}

function Vue(options) {
    this.options = mergeOptions(this.constructor.options, options);
    callHook(this, 'beforeCreate');
}
Vue.options = {}
new Vue({
    beforeCreate() {
        console.log('before create')
    }
})
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

# Vue的el属性和$mount优先级?

比如下面这种情况,Vue会渲染到哪个节点上

new Vue({
  router,
  store,
  el: '#app',
  render: h => h(App)
}).$mount('#ggg')
1
2
3
4
5
6

这是官方的一张图,可以看出el$mount同时存在时,el优先级 > $mount

image-20211226184408038

原理

export function mountComponent(vm, el) {
  vm.$el = el;
  callHook(vm, 'beforeMount');
  // 在初始化时,下面两方法已经提取挂载到属性上;调用render方法去渲染 el属性;
  // vm._update(vm._render()); // 先调用render方法创建虚拟节点,在将虚拟节点渲染到页面上; 虚拟Dom ====> 真Dom

  //优化后:
  // 先调用_render方法生成虚拟dom,通过_update方法将虚拟dom创建成真实的dom
  // 默认vue是通过watcher来进行渲染  = 渲染watcher (每一个组件都有一个渲染watcher)
  let updateComponent = () => {
    vm._update(vm._render()); // 虚拟节点; 渲染更新
  }
  // 初始化就会创建watcher;new watcher才出发get;
  new Watcher(vm, updateComponent, () => {
    callHook(vm, 'updated')
  }, true);
  // 要把属性 和 watcher绑定在一起 
  callHook(vm, 'mounted');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# computed/watch/method

# 比较

  • watch借助vue响应式原理,默认在取值时将watcher存放到对应属性的dep中,当数据发生变化时通知对应的watcher重新执行;
  • 默认computed也是一个watcher是具备缓存的,只要当依赖的属性发生变化时才会更新视图

# watch、computed与methods的联系和区别【要点】

computed 就是计算属性,其可以当成一个data数据来使用。直接使用,不用像方法那样调用();

watch 就是监听的意思,其专门用来观察和响应Vue实例上的数据的变动

能使用watch属性的场景基本上都可以使用computed属性,而且computed属性开销小,性能高,因此能使用computed就尽量使用computed属性

想要执行异步或昂贵的操作以响应不断变化的数据时,这时watch就派上了大用场。

# 初始化顺序

/*初始化props、methods、data、computed与watch*/
export function initState(vm: Component) {
  vm._watchers = [];
  const opts = vm.$options;
  /*初始化props*/
  if (opts.props) initProps(vm, opts.props);
  /*初始化方法*/
  if (opts.methods) initMethods(vm, opts.methods);
  /*初始化data*/
  if (opts.data) {
    initData(vm); // 初始化数据
  } else {
    /*该组件没有data的时候绑定一个空对象*/
    observe((vm._data = {}), true /* asRootData */);
  }
  /*初始化computed*/
  if (opts.computed) initComputed(vm, opts.computed);
  /*初始化watchers*/
  if (opts.watch) initWatch(vm, opts.watch);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 原理

# computed部分
  • this.lazy = options.lazy; // 如果watcher上有lazy属性 说明是计算属性
  • this.dirty = this.lazy; // dirty代表取值时是否执行用户提供的方法
  • this.dirty = false; // 取过一次值之后 就表示成已经取过值了
/*初始化computed*/
function initComputed(vm: Component, computed: Object) {
  const watchers = (vm._computedWatchers = Object.create(null));
  for (const key in computed) {
    const userDef = computed[key];
    /*
      计算属性可能是一个function,也有可能设置了get以及set的对象。
      可以参考 https://cn.vuejs.org/v2/guide/computed.html#计算-setter
    */
    let getter = typeof userDef === "function" ? userDef : userDef.get;
    if (process.env.NODE_ENV !== "production") {
      /*getter不存在的时候抛出warning并且给getter赋空函数*/
      if (getter === undefined) {
        warn(
          `No getter function has been defined for computed property "${key}".`,
          vm
        );
        getter = noop;
      }
    }
    // create internal watcher for the computed property.
    /*
      为计算属性创建一个内部的监视器Watcher,保存在vm实例的_computedWatchers中
      这里的computedWatcherOptions参数传递了一个lazy为true,会使得watch实例的dirty为true
    */
    watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions);

    /*组件正在定义的计算属性已经定义在现有组件的原型上则不会进行重复定义*/
    if (!(key in vm)) {
      /*定义计算属性*/
      defineComputed(vm, key, userDef);
    } else if (process.env.NODE_ENV !== "production") {
      /*如果计算属性与已定义的data或者props中的名称冲突则发出warning*/
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm);
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(
          `The computed property "${key}" is already defined as a prop.`,
          vm
        );
      }
    }
  }
}

const computedWatcherOptions = { lazy: true };
/*定义计算属性*/
export function defineComputed(
  target: any,
  key: string,
  userDef: Object | Function
) {
  if (typeof userDef === "function") {
    /*创建计算属性的getter*/
    sharedPropertyDefinition.get = createComputedGetter(key);
    /*
      当userDef是一个function的时候是不需要setter的,所以这边给它设置成了空函数。
      因为计算属性默认是一个function,只设置getter。
      当需要设置setter的时候,会将计算属性设置成一个对象。参考:https://cn.vuejs.org/v2/guide/computed.html#计算-setter
    */
    sharedPropertyDefinition.set = noop;
  } else {
    /*get不存在则直接给空函数,如果存在则查看是否有缓存cache,没有依旧赋值get,有的话使用createComputedGetter创建*/
    sharedPropertyDefinition.get = userDef.get
      ? userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop;
    /*如果有设置set方法则直接使用,否则赋值空函数*/
    sharedPropertyDefinition.set = userDef.set ? userDef.set : noop;
  }
  /*defineProperty上getter与setter*/
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

/*创建计算属性的getter*/
function createComputedGetter(key) {
  return function computedGetter() { //取值的时候回调用此方法
    const watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      /*实际是脏检查,在计算属性中的依赖发生改变的时候dirty会变成true,在get的时候重新计算计算属性的输出值*/
      if (watcher.dirty) {
        watcher.evaluate();
      }
      /*依赖收集*/
      if (Dep.target) {
        watcher.depend();
      }
      return watcher.value;
    }
  };
}
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
# watch部分

# watch中的deep:true是如何实现的

当用户指定了watch中的deep属性为true时,如果当前监控的值是数组类型,会对对象中的每一项进行求值,此时会将当前watcher存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新;

ps: computed时不用设置的,默认获取对象中全部数据,再JSON.stringify({})到渲染页面上;

/*初始化watchers*/
function initWatch(vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key];
    /*数组则遍历进行createWatcher*/
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i]);
      }
    } else {
      createWatcher(vm, key, handler);
    }
  }
}

/*创建一个观察者Watcher*/
//这里涉及了watch的三种写法,1.值是对象、2.值是数组、3.值是字符串 (如果是对象可以传入一些watch参数),最终会调用vm.$watch来实现
function createWatcher(vm: Component, key: string, handler: any) {
  let options;
  /*对对象类型进行严格检查,只有当对象是纯javascript对象的时候返回true*/
  // 如果是对象则提取函数 和配置
  if (isPlainObject(handler)) {
    /*
      这里是当watch的写法是这样的时候
      watch: {
          test: {
              handler: function () {},
              deep: true
          }
      }
    */
    options = handler;
    handler = handler.handler;
  }
  if (typeof handler === "string") {
    /*
        当然,也可以直接使用vm中methods的方法
    */
    handler = vm[handler];
  }
  /*用$watch方法创建一个watch来观察该对象的变化*/
  vm.$watch(key, handler, options);
}

 /*
    https://cn.vuejs.org/v2/api/#vm-watch
    $watch方法 用以为对象建立观察者监视变化
  */
 //扩展Vue原型上的方法,都通过mixin的方式来进行添加的。
  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ): Function {
    const vm: Component = this;
    options = options || {};
    options.user = true;
    const watcher = new Watcher(vm, expOrFn, cb, options);
    /*有immediate参数的时候会立即执行*/
    if (options.immediate) {
      cb.call(vm, watcher.value);
    }
    /*返回一个取消观察函数,用来停止触发回调*/
    return function unwatchFn() {
      /*将自身从所有依赖收集订阅列表删除*/
      watcher.teardown();
    };
  };
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
export default class Watcher {
  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ) {
    this.vm = vm;
    /*_watchers存放订阅者实例*/
    vm._watchers.push(this);
    // options
    if (options) {
      this.deep = !!options.deep;
      this.user = !!options.user;
      this.lazy = !!options.lazy;
      this.sync = !!options.sync;
    } else {
      this.deep = this.user = this.lazy = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new Set();
    this.newDepIds = new Set();
    this.expression =
      process.env.NODE_ENV !== "production" ? expOrFn.toString() : "";
    // parse expression for getter
    /*把表达式expOrFn解析成getter*/
    if (typeof expOrFn === "function") {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = function () {};
        process.env.NODE_ENV !== "production" &&
          warn(
            `Failed watching path: "${expOrFn}" ` +
              "Watcher only accepts simple dot-delimited paths. " +
              "For full control, use a function instead.",
            vm
          );
      }
    }
    this.value = this.lazy ? undefined : this.get();
  }
  
  /*获得getter的值并且重新进行依赖收集*/
  get() {
    /*将自身watcher观察者实例设置给Dep.target,用以依赖收集。*/
    pushTarget(this);
    let value;
    const vm = this.vm;

    /*
      执行了getter操作,看似执行了渲染操作,其实是执行了依赖收集。
      在将Dep.target设置为自生观察者实例以后,执行getter操作。
      譬如说现在的的data中可能有a、b、c三个数据,getter渲染需要依赖a跟c,
      那么在执行getter的时候就会触发a跟c两个数据的getter函数,
      在getter函数中即可判断Dep.target是否存在然后完成依赖收集,
      将该观察者对象放入闭包中的Dep的subs中去。
    */
    if (this.user) {
      try {
        value = this.getter.call(vm, vm);
      } catch (e) {
        handleError(e, vm, `getter for watcher "${this.expression}"`);
      }
    } else {
      value = this.getter.call(vm, vm);
    }
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    /*如果存在deep,则触发每个深层对象的依赖,追踪其变化*/
    if (this.deep) {
      /*递归每一个对象或者数组,触发它们的getter,使得对象或数组的每一个成员都被依赖收集,形成一个“深(deep)”依赖关系*/
      traverse(value);
    }

    /*将观察者实例从target栈中取出并设置给Dep.target*/
    popTarget();
    this.cleanupDeps();
    return value;
  }
   
  /*获取观察者的值*/
  evaluate() {
    this.value = this.get(); //详见下面wacther的源码;
    this.dirty = false;
  }
    
/*调度者接口,当依赖发生改变的时候进行回调。*/
  update() {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      /*同步则执行run直接渲染视图*/
      this.run();
    } else {
      /*异步推送到观察者队列中,下一个tick时调用。*/
      queueWatcher(this);
    }
  }
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

# 使用

# computed的使用
let vm = new Vue({
  el: '#app',
  data: {
    name: 'samy',
    firstName:'张',
    lastName:'三'
  },
  computed:{ //内部也使用了defineProperty, 内部有一个变量 dirty
    // computed还是一个watcher,内部依赖的属性会收集这个watcher
    fullName(){
      //  this.firstName ,this.lastName 在求值时, 会记住当前计算属性的watcher
      return this.firstName + this.lastName
    }
  }
});
// firstName,lastName会收集 computed的watcher
// firstName,lastName应该也将渲染watcher收集起来
setTimeout(() => {
  vm.firstName = '李';
}, 1000);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# watch的使用
let vm = new Vue({
  el: '#app',
  data: { a:{a:{a:1}}},
  watch: {
    // 1.直接key value 
    // 2.写成key 和数组的方式
    // 3.监控当前实例上的方法
    // 4.handler的写法 
    'a.a.a':{
      handler(newValue,oldValue){
        console.log(newValue,oldValue, '内部watch')
      }
    },
  }
});
vm.$watch(()=>{
  return vm.a.a.a; // 老值
},(newValue,oldValue)=>{
  console.log(newValue,oldValue,'自己写的$watch')
})
vm.a = {a:{a:'2'}}; // 会触发set方法 =》 set方法会调用watcher.run
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

涉及相关参数

watch: {
  // 监听一个基本数据类型时
  value () {
    // do something
  }
  // 监听一个引用数据类型时
  obj: {
    //handler: 'getList',
    handler () { // 执行回调
      // do something
    },
    deep: true, // 是否进行深度监听
    immediate: true // 是否初始执行handler函数;就不用在初始化created中再调用一次了;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# watch监听一个对象时,如何排除某些属性的监听

下面代码是,params发生改变就重新请求数据,无论是a,b,c,d属性改变

data() {
    return {
      params: {
        a: 1,
        b: 2,
        c: 3,
        d: 4
      },
    };
  },
watch: {
    params: {
      deep: true,
      handler() {
        this.getList;
      },
    },
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

但是如果我只想要a,b改变时重新请求,c,d改变时不重新请求呢?

mounted() {
    Object.keys(this.params)
      .filter((_) => !["c", "d"].includes(_)) // 排除对c,d属性的监听
      .forEach((_) => {
        this.$watch((vm) => vm.params[_], handler, {
          deep: true,
        });
      });
  },
data() {
    return {
      params: {
        a: 1,
        b: 2,
        c: 3,
        d: 4
      },
    };
  },
watch: {
    params: {
      deep: true,
      handler() {
        this.getList;
      },
    },
  }
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
# method的使用

# 计算变量时,methods和computed哪个好?

<div>
    <div>{{howMuch1()}}</div>
    <div>{{howMuch2()}}</div>
    <div>{{index}}</div>
</div>

data: () {
    return {
         index: 0
       }
     }
methods: {
    howMuch1() {
        return this.num + this.price
    }
  }
computed: {
    howMuch2() {
        return this.num + this.price
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

computed会好一些,因为computed会有缓存。例如index由0变成1,那么会触发视图更新,这时候methods会重新执行一次,而computed不会,因为computed依赖的两个变量num和price都没变。

# 异步渲染nextick

大家所熟悉的 Vue API Vue.nextTick 全局方法和 vm.$nextTick 实例方法的内部都是调用 nextTick 函数,该函数的作用可以理解为异步执行传入的函数。

其实nextTick就是一个把回调函数推入任务队列的方法。

# 分析

Vue 是异步修改 DOM 的并且不鼓励开发者直接接触 DOM,但有时候业务需要必须对数据更改--刷新后的 DOM 做相应的处理,这时候就可以使用 Vue.nextTick(callback)这个 api 。

$nextTick 是在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后使用 $nextTick,则可以在回调中获取更新后的 DOM

vue是组件级更新

因为如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染,所以为了性能考虑Vue会在本轮数据更新后,再去异步更新视图;

# 补充回答

vue多次更新数据,最终会进行批处理更新。内部调用的就是nextTick实现了延迟更新,用户自定义的nextTick中的回调会被延迟到更新完成后调用,从而可以获取更新后的DOM。

# 原理

nextTick中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。原理就是异步方法(promise,mutationObserver,setImmediate,setTimeout)经常与事件环一起来问(宏任务和微任务)

延迟调用优先级如下:Promise > MutationObserver > setImmediate > setTimeout【PMSS】

image-20210205213430529

image-20210205221822523

源码位置:src/core/observer/dep.js:34

 /*通知所有订阅者*/
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
1
2
3
4
5
6
7
8

源码位置:src/core/observer/watcher.js:179

   /*调度者接口,当依赖发生改变的时候进行回调。*/
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      /*同步则执行run直接渲染视图*/
      this.run()
    } else {
      /*异步推送到观察者队列中,下一个tick时调用。*/
      queueWatcher(this)
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13

源码位置:src/core/observer/scheduler.js:156

 /*将一个观察者对象push进观察者队列,在队列中已经存在相同的id则该观察者对象将被跳过,除非它是在队列被刷新时推送*/
export function queueWatcher (watcher: Watcher) {
  /*获取watcher的id*/
  const id = watcher.id
  /*检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验*/
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      /*如果没有flush掉,直接push到队列中即可*/
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i >= 0 && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(Math.max(i, index) + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      //调用nextTick方法, 批量的进行更新
      nextTick(flushSchedulerQueue)
    }
  }
}

/*nextTick的回调函数,在下一个tick时flush掉两个队列同时运行watchers*/
function flushSchedulerQueue() {
  flushing = true;
  let watcher, id;
  /*
    给queue排序,这样做可以保证:
    1.组件更新的顺序是从父组件到子组件的顺序,因为父组件总是比子组件先创建。
    2.一个组件的user watchers比render watcher先运行,因为user watchers往往比render watcher更早创建
    3.如果一个组件在父组件watcher运行期间被销毁,它的watcher执行将被跳过。
  */
  queue.sort((a, b) => a.id - b.id);

  /*这里不用index = queue.length;index > 0; index--的方式写是因为不要将length进行缓存,因为在执行处理现有watcher对象期间,更多的watcher对象可能会被push进queue*/
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index];
    id = watcher.id;
    /*将has的标记删除*/
    has[id] = null;
    /*执行watcher*/
    watcher.run();
    // in dev build, check and stop circular updates.
    /*
      在测试环境中,检测watch是否在死循环中
      比如这样一种情况
      watch: {
        test () {
          this.test++;
        }
      }
      持续执行了一百次watch代表可能存在死循环
    */
    if (process.env.NODE_ENV !== "production" && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1;
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          "You may have an infinite update loop " +
            (watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`),
          watcher.vm
        );
        break;
      }
    }
  }

  // keep copies of post queues before resetting state
  /**/
  /*得到队列的拷贝*/
  const activatedQueue = activatedChildren.slice();
  const updatedQueue = queue.slice();

  /*重置调度者的状态*/
  resetSchedulerState();

  // call component updated and activated hooks
  /*使子组件状态都改编成active同时调用activated钩子*/
  callActivatedHooks(activatedQueue);
  /*调用updated钩子*/
  callUpdateHooks(updatedQueue);

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit("flush");
  }
}
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

# Vue.nextTick

Vue.nextTick 内部逻辑; 源码位置:src/core/global-api/index.js:45

在执行 initGlobalAPI(Vue) 初始化 Vue 全局 API 中,这么定义 Vue.nextTick

function initGlobalAPI(Vue) {
    //...
    Vue.nextTick = nextTick;
}
1
2
3
4

可以看出是直接把 nextTick 函数赋值给 Vue.nextTick,就可以了,非常简单。

vm.$nextTick 内部逻辑; 源码位置:src/core/observer/watcher.js:179

Vue.prototype.$nextTick = function (fn) {
    return nextTick(fn, this)
};
1
2
3

源码位置:src/core/util/env.js:78

export const nextTick = (function () {
  /*存放异步执行的回调*/
  const callbacks = [];
  /*一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/
  let pending = false;
  /*一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用*/
  let timerFunc;

  /*下一个tick时的回调*/
  //function flushCallbacks () {
  function nextTickHandler() {
    /*一个标记位,标记等待状态(即函数已经被推入任务队列或者主线程,已经在等待当前栈执行完毕去执行),这样就不需要在push多个回调到callbacks时将timerFunc多次推入任务队列或者主线程*/
    pending = false;
    /*执行所有callback*/
    const copies = callbacks.slice(0);
    callbacks.length = 0;
    for (let i = 0; i < copies.length; i++) {
      copies[i]();
    }
  }

  // the nextTick behavior leverages the microtask queue, which can be accessed
  // via either native Promise.then or MutationObserver.
  // MutationObserver has wider support, however it is seriously bugged in
  // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
  // completely stops working after triggering a few times... so, if native
  // Promise is available, we will use it:
  /* istanbul ignore if */

  /*
    这里解释一下,一共有Promise、MutationObserver以及setTimeout三种尝试得到timerFunc的方法。
    优先使用Promise,在Promise不存在的情况下使用MutationObserver,这两个方法的回调函数都会在microtask中执行,它们会比setTimeout更早执行,所以优先使用。
    如果上述两种方法都不支持的环境则会使用setTimeout,在task尾部推入这个函数,等待调用执行。
  */
  if (typeof Promise !== "undefined" && isNative(Promise)) {
    /*使用Promise*/
    var p = Promise.resolve();
    var logError = (err) => {
      console.error(err);
    };
    timerFunc = () => {
      p.then(nextTickHandler).catch(logError);
      // in problematic UIWebViews, Promise.then doesn't completely break, but
      // it can get stuck in a weird state where callbacks are pushed into the
      // microtask queue but the queue isn't being flushed, until the browser
      // needs to do some other work, e.g. handle a timer. Therefore we can
      // "force" the microtask queue to be flushed by adding an empty timer.
      if (isIOS) setTimeout(noop);
    };
  } else if (
    typeof MutationObserver !== "undefined" &&
    (isNative(MutationObserver) ||
      // PhantomJS and iOS 7.x
      MutationObserver.toString() === "[object MutationObserverConstructor]")
  ) {
    // use MutationObserver where native Promise is not available,
    // e.g. PhantomJS IE11, iOS7, Android 4.4
    /*新建一个textNode的DOM对象,用MutationObserver绑定该DOM并指定回调函数,在DOM变化的时候则会触发回调,该回调会进入主线程(比任务队列优先执行),即textNode.data = String(counter)时便会加入该回调*/
    var counter = 1;
    var observer = new MutationObserver(nextTickHandler);
    var textNode = document.createTextNode(String(counter));
    observer.observe(textNode, {
      characterData: true,
    });
    timerFunc = () => {
      counter = (counter + 1) % 2;
      textNode.data = String(counter);
    };
  } else {
    // fallback to setTimeout
    /*使用setTimeout将回调推入任务队列尾部*/
    timerFunc = () => {
      setTimeout(nextTickHandler, 0);
    };
  }
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

# 快速Mock

let cbs = [];
let pending = false;
function flushCallbacks() {
    cbs.forEach(fn=>fn());
}
function nextTick(fn) {
    cbs.push(fn);
    if (!pending) {
        pending = true;
        setTimeout(() => {
            flushCallbacks();
        }, 0);
    }
}
function render() {
    console.log('rerender');
};
nextTick(render)
nextTick(render)
nextTick(render);
console.log('sync...')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 应用场景

  • 在Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中,原因:是created()钩子函数执行时DOM其实并未进行渲染。
  • 在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的DOM结构的时候,这个操作应该放在Vue.nextTick()的回调函数中,原因:Vue异步执行DOM更新,只要观察到数据变化,Vue将开启一个队列,并缓冲在同一个事件循环中发生的所有数据改变,如果同一个watcher被多次触发,只会被推入到队列中一次
//改变数据
vm.message="changed";
//想要立即使用更新后的DOM,这样不行,因为设置message后DOM还没有更新
console.log(vm.$el.textContent)//并不会得到"changed"
//这样可以,nextTick里面的代码在DOM更新后执行
Vue.nextTick(function(){
    //DOM更新,可以得到"changed"
    console.log(vm.$el.textContent)
})

//作为一个promise使用,不传参数时回调
Vue.nextTick().then(function(){
    //DOM更新时的执行代码
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
new Vue({ // 示例
  methods: {
    example: function () {
      this.message = 'changed'// 修改数据
      this.$nextTick(function () {// DOM 还没有更新
        this.doSomethingElse() // DOM 现在更新了// `this` 绑定到当前实例
      })
    }
  }
})
1
2
3
4
5
6
7
8
9
10

# v-if/show/for/html/model

# v-if与v-show

# 表面分析
  • v-if 如果条件不成立不会渲染当前指令所在节点的 dom 元素;
  • v-show 只是切换当前 dom 的显示或者隐藏;

细分情况:

  • 手段:v-if是动态的向DOM树内添加或者删除DOM元素;v-show是通过设置DOM元素的display样式属性控制显隐;
  • 编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换;
  • 编译条件:v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译; v-show是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且DOM元素保留;
  • 性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;
  • 使用场景:v-if适合运营条件不大可能改变;v-show适合频繁切换。
# 核心答案
  • v-if在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点
  • v-show会被编译成指令,条件不满足时控制样式将对应节点隐藏 (内部其他指令依旧会继续执行)

扩展回答: 频繁控制显示隐藏尽量不使用v-if,v-if和v-for尽量不要连用,要处理的话,使用在外一层控制或者用计算属性处理;

# 原理

# v-if、v-show、v-html 的原理

  • v-if会调用addIfCondition方法,生成vnode的时候会忽略对应节点,render的时候就不会渲染
  • v-show会生成vnode,render的时候也会渲染成真实节点,只是在render过程中会在节点的属性中修改show属性值,也就是常说的display;
  • v-html会先移除节点下的所有节点,调用html方法,通过addProp添加innerHTML属性,归根结底还是设置innerHTML为v-html的值。

v-if源码剖析

/*处理v-if*/
function genIf(el: any): string {
  /*标记位*/
  el.ifProcessed = true; // avoid recursion
  return genIfConditions(el.ifConditions.slice());
}

function genIfConditions (
    conditions: ASTIfConditions,
    state: CodegenState,
    altGen?: Function,
    altEmpty?: string
    ): string {
    if (!conditions.length) {
        return altEmpty || '_e()'
    }
    const condition = conditions.shift()
    if (condition.exp) {   // 如果有表达式
        return `(${condition.exp})?${ // 将表达式作为条件拼接成元素
        genTernaryExp(condition.block)
        }:${
        genIfConditions(conditions, state, altGen, altEmpty)
        }`
    } else {
        return `${genTernaryExp(condition.block)}` // 没有表达式直接生成元素 像v-else
    }
    // v-if with v-once should generate code like (a)?_m(0):_m(1)
    function genTernaryExp (el) {
        return altGen
        ? altGen(el, state)
        : el.once
            ? genOnce(el, state)
            : genElement(el, state)
    }
}
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

v-show源码剖析

v-show会解析成指令,变为display:none;

  bind(el: any, { value }: VNodeDirective, vnode: VNodeWithData) {
    vnode = locateNode(vnode);
    const transition = vnode.data && vnode.data.transition;
    const originalDisplay = (el.__vOriginalDisplay =
      el.style.display === "none" ? "" : el.style.display);//获取原始显示值
    if (value && transition && !isIE9) {
      vnode.data.show = true;
      enter(vnode, () => {
        el.style.display = originalDisplay;
      });
    } else {
      el.style.display = value ? originalDisplay : "none";//根据属性控制显示或者隐藏
    }
  },
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 编译后分析比较

v-if

const VueTemplateCompiler = require('vue-template-compiler'); 
let r1 = VueTemplateCompiler.compile(`
    <div v-if="true"><span v-for="i in 3">hello</span></div>`
); 
/** with(this) { 
 *   return (true) ? _c('div', _l((3), function (i) { return _c('span', [_v("hello")]) }), 0) : _e() 
 * }
**/
1
2
3
4
5
6
7
8

v-show

const VueTemplateCompiler = require('vue-template-compiler'); 
let r1 = VueTemplateCompiler.compile(`<div v-show="true"</div>`); 
// with(this) { 
//   return _c('div', {
//       directives:[{
//           name:"show",
//           rawName:"v-show",
//           value:(true),
//           expression:"true",
//       }]
//   })
// }
1
2
3
4
5
6
7
8
9
10
11
12

# v-ifv-for的优先级

v-for和v-if不要在同一个标签中使用,因为解析时先解析v-for在解析v-if。如果遇到需要同时使用时可以考虑写成计算属性的方式。

扩展回答: 频繁控制显示隐藏尽量不使用v-if,v-if和v-for尽量不要连用,要处理的话,使用在外一层控制或者用计算属性处理;

不推荐同时使用 v-ifv-for; 当 v-ifv-for 一起使用时,v-for 具有比 v-if 更高的优先级。这意味着 v-if 将分别重复运行于每个 v-for 循环中。

v-for 比 v-if 优先级高,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成 computed 属性

# 处理方案

优化方案一:

<li v-for="todo in todos" v-if="!todo.isComplete">
  {{ todo }}
</li>
<!-- 上面的代码将只渲染未完成的 todo -->

<!-- 优化后 -->
<!--而如果你的目的是有条件地跳过循环的执行,那么可以将 v-if 置于外层元素 (<template>)上。-->
<ul v-if="todos.length">
  <li v-for="todo in todos">
    {{ todo }}
  </li>
</ul>
<p v-else>No todos left!</p>
1
2
3
4
5
6
7
8
9
10
11
12
13

优化方案二:

<ul>
  <li
    v-for="user in activeUsers"
    :key="user.id">
    {{ user.name }}
  </li>
</ul>
computed: {
  activeUsers: function () {
    return this.users.filter(function (user) {
   return user.isActive
    })
  }
}
//不推荐:
<ul>
  <li
    v-for="user in users"
    v-if="user.isActive"
    :key="user.id">
    {{ user.name }}
  </li>
</ul>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 原理
/*处理element,分别处理static静态节点、v-once、v-for、v-if、template、slot以及组件或元素*/
function genElement(el: ASTElement): string {
  if (el.staticRoot && !el.staticProcessed) {
    /*处理static静态节点*/
    return genStatic(el);
  } else if (el.once && !el.onceProcessed) {
    /*处理v-once*/
    return genOnce(el);
  } else if (el.for && !el.forProcessed) {
    /*处理v-for*/
    return genFor(el);
  } else if (el.if && !el.ifProcessed) {
    /*处理v-if*/
    return genIf(el);
  } else if (el.tag === "template" && !el.slotTarget) {
    /*处理template*/
    return genChildren(el) || "void 0";
  } else if (el.tag === "slot") {
    /*处理slot*/
    return genSlot(el);
  } else {
    // component or element
    /*处理组件或元素*/
    let code;
    if (el.component) {
      code = genComponent(el.component, el);
    } else {
      const data = el.plain ? undefined : genData(el);
      const children = el.inlineTemplate ? null : genChildren(el, true);
      code = `_c('${el.tag}'${
        data ? `,${data}` : "" // data
      }${
        children ? `,${children}` : "" // children
      })`;
    }
    // module transforms
    for (let i = 0; i < transforms.length; i++) {
      code = transforms[i](el, code);
    }
    return code;
  }
}

/*处理v-if*/
function genIf(el: any): string {
  /*标记位*/
  el.ifProcessed = true; // avoid recursion
  return genIfConditions(el.ifConditions.slice());
}

/*处理if条件*/
function genIfConditions(conditions: ASTIfConditions): string {
  /*表达式不存在*/
  if (!conditions.length) {
    return "_e()";
  }
  const condition = conditions.shift();
  if (condition.exp) {
    return `(${condition.exp})?${genTernaryExp(
      condition.block
    )}:${genIfConditions(conditions)}`;
  } else {
    return `${genTernaryExp(condition.block)}`;
  }

  // v-if with v-once should generate code like (a)?_m(0):_m(1)
  /*v-if与v-once同时存在的时候应该使用三元运算符,譬如说(a)?_m(0):_m(1)*/
  function genTernaryExp(el) {
    return el.once ? genOnce(el) : genElement(el);
  }
}

/*处理v-for循环*/
function genFor(el: any): string {
  const exp = el.for;
  const alias = el.alias;
  const iterator1 = el.iterator1 ? `,${el.iterator1}` : "";
  const iterator2 = el.iterator2 ? `,${el.iterator2}` : "";
  if (
    process.env.NODE_ENV !== "production" &&
    maybeComponent(el) &&
    el.tag !== "slot" &&
    el.tag !== "template" &&
    !el.key
  ) {
    warn(
      `<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
        `v-for should have explicit keys. ` +
        `See https://vuejs.org/guide/list.html#key for more info.`,
      true /* tip */
    );
  }

  /*标记位,避免递归*/
  el.forProcessed = true; // avoid recursion
  return (
    `_l((${exp}),` +
    `function(${alias}${iterator1}${iterator2}){` +
    `return ${genElement(el)}` +
    "})"
  );
}
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
# 编译后分析比较
const VueTemplateCompiler = require('vue-template-compiler'); 
let r1 = VueTemplateCompiler.compile(`
    <div v-if="true"><span v-for="i in 3">hello</span></div>`
); 
/** with(this) { 
 *   return (true) ? _c('div', _l((3), function (i) { return _c('span', [_v("hello")]) }), 0) : _e() 
 * }
**/
1
2
3
4
5
6
7
8

v-for 会比 v-if 的优先级高一些,如果连用的话会把 v-if 给每个元素都添加一下,会造成性能问题 (使用计算属性优化)

const VueTemplateCompiler = require('vue-template-compiler'); 
let r1 = VueTemplateCompiler.compile(`<div v-if="false" v-for="i in 3">hello</div>`); 

/** with(this) { 
 *    return _l((3), function (i) { return (false) ? _c('div', [_v("hello")]) : _e() }) 
 *  }
**/;
1
2
3
4
5
6
7

# v-for要加key

为了在比对过程中进行复用 ; 【最核心那一步比较要用】img

而且key, 不能是index或者随机数;如果是的话,在操作添加或者删除时会错位;

# 原理

patch函数关键代码:

function patch (oldVnode, vnode) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []
    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
      // some code
      }
    }
    return vnode
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

patch函数接收oldVnode和vnode,也就是要比较的新旧节点对象。

首先会用isUndef函数判断传入的两个vnode是否为空对象再做相应处理。当两个都为节点对象时,再用sameVnode来判断是否为同一节点,再判断本次操作是新增、修改、还是移除。

function sameVnode (a, b) {
  return (
    a.key === b.key // key值
    && 
      (
        a.tag === b.tag &&  // 标签名
        a.isComment === b.isComment && // 是否为注释节点
        isDef(a.data) === isDef(b.data) && // 是否都定义了data,data包含一些具体信息,例如onclick , style
        sameInputType(a, b) // 当标签是<input>的时候,type必须相同
      )
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
  • sameVnode通过判断key、标签名、是否为注释、data等是否相等,来判断是否需要进行比较。
  • 值得比较则执行patchVnode,不值得比较则用Vnode替换oldVnode,再渲染真实dom。
  • patchVnode会对oldVnode和vnode进行对比,然后进行DOM更新。这个会在diff算法里再进行说明。

v-for通常都是生成一样的标签,所以key会是patch判断是否相同节点的唯一标识,如果不设置key,它的值就是undefined,则可能永远认为这是两个相同节点,就会去做pathVnode pdateChildren的更新操作,这造成了大量的dom更新操作,所以设置唯一的key是必要的。

# v-html

# v-html会导致哪些问题?

  • 可能会导致xss攻击;
  • v-html会替换掉标签内部的子元素;
# 原理

v-html会先移除节点下的所有节点,调用html方法,通过addProp添加innerHTML属性,归根结底还是设置innerHTML为v-html的值。

image-20210228000842806

源码:platforms\web\compiler\directives\html.js , shared\util.js

import { addProp } from 'compiler/helpers'
export default function html (el: ASTElement, dir: ASTDirective) {
  if (dir.value) {
    addProp(el, 'innerHTML', `_s(${dir.value})`)
  }
}

/*将属性放入ele的props属性中*/
export function addProp (el: ASTElement, name: string, value: string) {
  (el.props || (el.props = [])).push({ name, value })
}

  /*
    内部处理render的函数
    这些函数会暴露在Vue原型上以减小渲染函数大小
  */
  /*处理v-once的渲染函数*/
  Vue.prototype._o = markOnce;
  /*将字符串转化为数字,如果转换失败会返回原字符串*/
  Vue.prototype._n = toNumber;
  /*将val转化成字符串*/
  Vue.prototype._s = toString;
  /*处理v-for列表渲染*/
  Vue.prototype._l = renderList;
  /*处理slot的渲染*/
  Vue.prototype._t = renderSlot;
  /*检测两个变量是否相等*/
  Vue.prototype._q = looseEqual;
  /*检测arr数组中是否包含与val变量相等的项*/
  Vue.prototype._i = looseIndexOf;
  /*处理static树的渲染*/
  Vue.prototype._m = renderStatic;
  /*处理filters*/
  Vue.prototype._f = resolveFilter;
  /*从config配置中检查eventKeyCode是否存在*/
  Vue.prototype._k = checkKeyCodes;
  /*合并v-bind指令到VNode中*/
  Vue.prototype._b = bindObjectProps;
  /*创建一个文本节点*/
  Vue.prototype._v = createTextVNode;
  /*创建一个空VNode节点*/
  Vue.prototype._e = createEmptyVNode;
  /*处理ScopedSlots*/
  Vue.prototype._u = resolveScopedSlots;

/**
 * Convert a value to a string that is actually rendered.
 */
 /*将val转化成字符串*/
export function toString (val: any): string {
  return val == null
    ? ''
    : typeof val === 'object'
      ? JSON.stringify(val, null, 2)
      : String(val)
}
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

源码:platforms\web\runtime\modules\dom-props.js

function updateDOMProps (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  if (isUndef(oldVnode.data.domProps) && isUndef(vnode.data.domProps)) {
    return
  }
  let key, cur
  const elm: any = vnode.elm
  const oldProps = oldVnode.data.domProps || {}
  let props = vnode.data.domProps || {}
  // clone observed objects, as the user probably wants to mutate it
  if (isDef(props.__ob__)) {
    props = vnode.data.domProps = extend({}, props)
  }

  for (key in oldProps) {
    if (isUndef(props[key])) {
      elm[key] = ''
    }
  }
  for (key in props) {
    cur = props[key]
    // ignore children if the node has textContent or innerHTML,
    // as these will throw away existing DOM nodes and cause removal errors
    // on subsequent patches (#3360)
    if (key === 'textContent' || key === 'innerHTML') {
      if (vnode.children) vnode.children.length = 0
      if (cur === oldProps[key]) continue
    }

    if (key === 'value') {
      // store value as _value as well since
      // non-string values will be stringified
      elm._value = cur
      // avoid resetting cursor position when value is the same
      const strCur = isUndef(cur) ? '' : String(cur)
      if (shouldUpdateValue(elm, vnode, strCur)) {
        elm.value = strCur
      }
    } else {
      elm[key] = cur
    }
  }
}
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

# v-model

# 双向绑定原理

vue 双向数据绑定是通过 数据劫持 结合 发布订阅模式 的方式来实现的,也就是说数据和视图同步,数据发生变化,视图跟着变化,视图变化,数据也随之发生改变; 核心:Object.defineProperty() 方法。

v-model本质上是语法糖,v-model在内部为不同的输入元素使用不同的属性并抛出不同的事件;

  • text 和 textarea 元素使用 value 属性和 input 事件
  • checkbox 和 radio 使用 checked 属性和 change 事件
  • select 字段将 value 作为 prop 并将 change 作为事件
# 具体总括
  • 组件:v-model 就是value+input方法的语法糖;会将组件的v-model默认转化成value+input方法的语法糖;

  • 原生:v-model可以看成value+input方法的语法糖;原生的v-model,会根据标签的不同生成不同的事件和属性;

    input , checkbox, select

    • text 和 textarea 元素:使用 value 属性和 input 事件;
    • checkbox 和 radio: 使用 checked 属性和 change 事件;
    • select :value 作为 prop 并将 change 作为事件
# 示范使用

上面是具体实现,内部实现还是:vue数据双向绑定实现原理解析

<input v-model="searchText">
<!--等价于:-->
<input
  v-bind:value="searchText"
  v-on:input="searchText = $event.target.value"
>

<input type="text" v-model="nameInput">
<input type="text" :value="nameInput" @keyup="nameInput = $event.target.value">
1
2
3
4
5
6
7
8
9

组件的v-model就是value+input方法的语法糖;

// 父组件
<aa-input v-model="aa"></aa-input>
// 等价于
<aa-input v-bind:value="aa" v-on:input="aa=$event.target.value"></aa-input>

// 子组件:
<input v-bind:value="aa" v-on:input="onmessage"></aa-input>
props:{value:aa,}
methods:{
    onmessage(e){
        $emit('input',e.target.value)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<custom-input v-model="searchText"></custom-input>
//相当于
<custom-input
  v-bind:value="searchText"
  v-on:input="searchText = $event"
></custom-input>

Vue.component('custom-input', {
  props: ['value'],
  template: `
    <input
      v-bind:value="value"
      v-on:input="$emit('input', $event.target.value)"
    >
  `
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

组件的v-model就是value+input方法的语法糖;

<el-checkbox v-model="check"></el-checkbox>
<el-checkbox :value="" @input=""></el-checkbox>
1
2

可以自定重新定义v-model的含义;

image-20210226002007624

# 原理

**组件:**会将组件的v-model默认转化成value+input方法的语法糖;

image-20210226002310938

源码位置:core/vdom/create-component:232

  data = data || {};
  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data);
  }

// transform component v-model info (value and callback) into
// prop and event handler respectively.
function transformModel(options, data: any) {
  const prop = (options.model && options.model.prop) || "value";
  const event = (options.model && options.model.event) || "input";
  (data.props || (data.props = {}))[prop] = data.model.value;
  const on = data.on || (data.on = {});
  if (isDef(on[event])) {
    on[event] = [data.model.callback].concat(on[event]);
  } else {
    on[event] = data.model.callback;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

**原生:**原生的v-model,会根据标签的不同生成不同的事件和属性

image-20210226002750467

  • 编译时:不同的标签解析出的内容不一样;
  • 运行时:会对元素处理一些关于输入法的问题;

源码位置:platforms\web\compiler\directives\model.js

export default function model (
  el: ASTElement,
  dir: ASTDirective,
  _warn: Function
): ?boolean {
  warn = _warn
  const value = dir.value
  const modifiers = dir.modifiers
  const tag = el.tag
  const type = el.attrsMap.type

  if (process.env.NODE_ENV !== 'production') {
    const dynamicType = el.attrsMap['v-bind:type'] || el.attrsMap[':type']
    if (tag === 'input' && dynamicType) {
      warn(
        `<input :type="${dynamicType}" v-model="${value}">:\n` +
        `v-model does not support dynamic input types. Use v-if branches instead.`
      )
    }
    // inputs with type="file" are read only and setting the input's
    // value will throw an error.
    if (tag === 'input' && type === 'file') {
      warn(
        `<${el.tag} v-model="${value}" type="file">:\n` +
        `File inputs are read only. Use a v-on:change listener instead.`
      )
    }
  }

  if (tag === 'select') {
    genSelect(el, value, modifiers)
  } else if (tag === 'input' && type === 'checkbox') {
    genCheckboxModel(el, value, modifiers)
  } else if (tag === 'input' && type === 'radio') {
    genRadioModel(el, value, modifiers)
  } else if (tag === 'input' || tag === 'textarea') {
    genDefaultModel(el, value, modifiers)
  } else if (!config.isReservedTag(tag)) {
    genComponentModel(el, value, modifiers)
    // component v-model doesn't need extra runtime
    return false
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `<${el.tag} v-model="${value}">: ` +
      `v-model is not supported on this element type. ` +
      'If you are working with contenteditable, it\'s recommended to ' +
      'wrap a library dedicated for that purpose inside a custom component.'
    )
  }

  // ensure runtime directive metadata
  return true
}

function genDefaultModel (
  el: ASTElement,
  value: string,
  modifiers: ?ASTModifiers
): ?boolean {
  const type = el.attrsMap.type
  const { lazy, number, trim } = modifiers || {}
  const needCompositionGuard = !lazy && type !== 'range'
  const event = lazy
    ? 'change'
    : type === 'range'
      ? RANGE_TOKEN
      : 'input'

  let valueExpression = '$event.target.value'
  if (trim) {
    valueExpression = `$event.target.value.trim()`
  }
  if (number) {
    valueExpression = `_n(${valueExpression})`
  }

  let code = genAssignmentCode(value, valueExpression)
  if (needCompositionGuard) {
    code = `if($event.target.composing)return;${code}`
  }

  addProp(el, 'value', `(${value})`)
  addHandler(el, event, code, null, true)
  if (trim || number || type === 'number') {
    addHandler(el, 'blur', '$forceUpdate()')
  }
}

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

源码位置:platforms\web\runtime\directives\model.js

export default {
  inserted(el, binding, vnode) {
    if (vnode.tag === "select") {
      const cb = () => {
        setSelected(el, binding, vnode.context);
      };
      cb();
      /* istanbul ignore if */
      if (isIE || isEdge) {
        setTimeout(cb, 0);
      }
    } else if (
      vnode.tag === "textarea" ||
      el.type === "text" ||
      el.type === "password"
    ) {
      el._vModifiers = binding.modifiers;
      if (!binding.modifiers.lazy) {
        // Safari < 10.2 & UIWebView doesn't fire compositionend when
        // switching focus before confirming composition choice
        // this also fixes the issue where some browsers e.g. iOS Chrome
        // fires "change" instead of "input" on autocomplete.
        el.addEventListener("change", onCompositionEnd);
        if (!isAndroid) {
          el.addEventListener("compositionstart", onCompositionStart);
          el.addEventListener("compositionend", onCompositionEnd);
        }
        /* istanbul ignore if */
        if (isIE9) {
          el.vmodel = true;
        }
      }
    }
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

# 指令/事件/filter

# 常见指令

  • v-once 渲染一次 (可用作优化,但是使用频率极少)
  • v-html 将字符串转化成dom插入到标签中 (会导致xss攻击问题,并且覆盖子元素)
  • v-if/v-else/v-else-if 不满足时dom不存在(可以使用template标签)
  • v-show 不满足时dom隐藏 (不能使用template标签)
  • v-for 循环字符串、对象、数字、数组 (循环时必须加key,尽量不采用索引)
  • v-bind 可以简写成: 属性(style、class...)绑定
  • v-on 可以简写成@ 给元素绑定事件 (常用修饰符 .stop、.prevent、.self、.once、.passive)
  • v-model双向绑定 (支持.trim、.number修饰符)

image-20211226142130346

# 如何设置动态class,动态style?

  • 动态class对象:<div :class="{ 'is-active': true, 'red': isRed }"></div>
  • 动态class数组:<div :class="['is-active', isRed ? 'red' : '' ]"></div>
  • 动态style对象:<div :style="{ color: textColor, fontSize: '18px' }"></div>
  • 动态style数组:<div :style="[{ color: textColor, fontSize: '18px' }, { fontWeight: '300' }]"></div>

# Vue.directive

# 实现原理

把定义的内容进行格式化挂载到Vue.options属性上;

# 格式化挂载

把定义的内容进行格式化挂载到Vue.options属性上; 可见自定义指令中有bind,update属性;

/*选项/资源集合*/
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

ASSET_TYPES.forEach(type => {
    Vue[type] = function ( 
        id: string,
        definition: Function | Object
    ): Function | Object | void {
        if (!definition) {
        return this.options[type + 's'][id]
        } else { // 如果是指令 将指令的定义包装成对象
        if (type === 'directive' && typeof definition === 'function') {
            definition = { bind: definition, update: definition }
        }
        this.options[type + 's'][id] = definition // 将指令的定义绑定在Vue.options上
        return definition
        }
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 指令初始化
# 默认几个内置的指令;
export default {
  inserted(el, binding, vnode) {
    if (vnode.tag === "select") {
      const cb = () => {
        setSelected(el, binding, vnode.context);
      };
      cb();
      /* istanbul ignore if */
      if (isIE || isEdge) {
        setTimeout(cb, 0);
      }
    } else if (
      vnode.tag === "textarea" ||
      el.type === "text" ||
      el.type === "password"
    ) {
      el._vModifiers = binding.modifiers;
      if (!binding.modifiers.lazy) {
        // Safari < 10.2 & UIWebView doesn't fire compositionend when
        // switching focus before confirming composition choice
        // this also fixes the issue where some browsers e.g. iOS Chrome
        // fires "change" instead of "input" on autocomplete.
        el.addEventListener("change", onCompositionEnd);
        if (!isAndroid) {
          el.addEventListener("compositionstart", onCompositionStart);
          el.addEventListener("compositionend", onCompositionEnd);
        }
        /* istanbul ignore if */
        if (isIE9) {
          el.vmodel = true;
        }
      }
    }
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
# 使用示范
Vue.directive('demo', {
  bind: function (el, binding, vnode) {
    el.style.position = 'fixed'
    el.style.top = binding.value + 'px'
  }
})
1
2
3
4
5
6

# 自定义指令【要点】

指令的实现原理,可以从编译原理=>代码生成=>指令钩子实现进行概述

实现步骤

  • 1.在生成ast语法树时,遇到指令会给当前元素添加directives属性
  • 2.通过genDirectives生成指令代码
  • 3.在patch前将指令的钩子提取到cbs中, 在patch过程中调用对应的钩子
  • 4.当执行指令对应钩子函数时,调用对应指令定义的方法
# 原理分析
export function addDirective ( 
    el: ASTElement,
    name: string,
    rawName: string,
    value: string,
    arg: ?string,
    isDynamicArg: boolean,
    modifiers: ?ASTModifiers,
    range?: Range
    ) {
    (el.directives || (el.directives = [])).push(rangeSetItem({ // 给元素添加directives属性
        name,
        rawName,
        value,
        arg,
        isDynamicArg,
        modifiers
    }, range))
    el.plain = false
}   
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function genDirectives (el: ASTElement, state: CodegenState): string | void {
    const dirs = el.directives 
    if (!dirs) return
    let res = 'directives:['
    let hasRuntime = false
    let i, l, dir, needRuntime
    for (i = 0, l = dirs.length; i < l; i++) {
        dir = dirs[i]
        needRuntime = true
        if (needRuntime) {
        hasRuntime = true
        // 将指令生成字符串directives:[{name:'def',rawName:'v-def'}]...
        res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
            dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
        }${
            dir.arg ? `,arg:${dir.isDynamicArg ? dir.arg : `"${dir.arg}"`}` : ''
        }${
            dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
        }},`
        }
    }
    if (hasRuntime) {
        return res.slice(0, -1) + ']'
    }
}
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
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
const { modules, nodeOps } = backend // // modules包含指令对应的hook
for (i = 0; i < hooks.length; ++i) { 
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {  
        // 格式化的结果{create:[hook],update:[hook],destroy:[hook]}
        if (isDef(modules[j][hooks[i]])) {
            cbs[hooks[i]].push(modules[j][hooks[i]])
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
export default { // 无论更新创建销毁调用的都是 updateDirectives方法
    create: updateDirectives, 
    update: updateDirectives,
    destroy: function unbindDirectives (vnode: VNodeWithData) {
       updateDirectives(vnode, emptyNode)
    }
}

function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) {
    if (oldVnode.data.directives || vnode.data.directives) { // 创建更新都调用此方法
        _update(oldVnode, vnode) // 指令的核心方法
    }
}

function _update (oldVnode, vnode) {
    const isCreate = oldVnode === emptyNode
    const isDestroy = vnode === emptyNode
    // 获取指令名称
    const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context)
    const newDirs = normalizeDirectives(vnode.data.directives, vnode.context)

    const dirsWithInsert = []
    const dirsWithPostpatch = []

    let key, oldDir, dir
    for (key in newDirs) {
        oldDir = oldDirs[key]
        dir = newDirs[key]
        if (!oldDir) { // 没有旧的 说明是绑定 调用bind钩子
            // new directive, bind
            callHook(dir, 'bind', vnode, oldVnode)
            if (dir.def && dir.def.inserted) {
                dirsWithInsert.push(dir)
            }
        } else { // 存在指令则是更新操作
            // existing directive, update
            dir.oldValue = oldDir.value
            dir.oldArg = oldDir.arg
            callHook(dir, 'update', vnode, oldVnode)
            if (dir.def && dir.def.componentUpdated) { // 如果有componentUpdated方法
                dirsWithPostpatch.push(dir)
            }
        }
    }

    if (dirsWithInsert.length) { // 如果有insert钩子
        const callInsert = () => { // 生成回调方法
            for (let i = 0; i < dirsWithInsert.length; i++) {
                callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
            }
        }
        if (isCreate) { // 是创建增加insert钩子
            mergeVNodeHook(vnode, 'insert', callInsert)
        } else {
            callInsert()
        }
    }

    if (dirsWithPostpatch.length) { // 如果有componentUpdated在次合并钩子
        mergeVNodeHook(vnode, 'postpatch', () => {
            for (let i = 0; i < dirsWithPostpatch.length; i++) {
                callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode)
            }
        })
    }

    if (!isCreate) { // 否则就是调用卸载钩子
        for (key in oldDirs) {
            if (!newDirs[key]) {
                // no longer present, unbind
                callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)
            }
        }
    }
}
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
# 案例示范
const action = Vue.directive('action', {
  inserted: function (el, binding, vnode) {
    const actionName = binding.arg
    const roles = store.getters.roles
    const elVal = vnode.context.$route.meta.permission
    const permissionId = elVal instanceof String && [elVal] || elVal
    roles.permissions.forEach(p => {
      if (!permissionId.includes(p.permissionId)) {
        return
      }
      if (p.actionList && !p.actionList.includes(actionName)) {
        el.parentNode && el.parentNode.removeChild(el) || (el.style.display = 'none')
        // 因为 v-show 的话,dom其实没有隐藏,用户可以改变display就看到,v-if呢
      }
    })
  }
})

export default action
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//注册
Vue.directive('enterToNext', {
  inserted: function (el) {
    console.log('enterToNext...')
    // let frm = el.querySelector('.el-form');
    const inputs = el.querySelectorAll('input')
    console.log(inputs)
    for (var i = 0; i < inputs.length; i++) {
      inputs[i].setAttribute('keyFocusIndex', i)
      inputs[i].addEventListener('keyup', (ev) => {
        if (ev.keyCode === 13) {
          const targetTo = ev.srcElement.getAttribute('keyFocusTo')
          if (targetTo) {
            this.$refs[targetTo].$el.focus()
          } else {
            var attrIndex = ev.srcElement.getAttribute('keyFocusIndex')
            var ctlI = parseInt(attrIndex)
            if (ctlI < inputs.length - 1) { inputs[ctlI + 1].focus() }
          }
        }
      })
    }
  }
})
//使用
<el-form
ref="postForm"
v-enterToNext="true"
:model="postForm"
:rules="rules"
label-position="left"
label-width="110px"
size="mini"
status-icon
>
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

# 动态指令使用

<template>
    ...
    <aButton @[someEvent]="handleSomeEvent()" :[someProps]="1000" />...
</template>
<script>
  ...
  data(){
    return{
      ...
      someEvent: someCondition ? "click" : "dbclick",
      someProps: someCondition ? "num" : "price"
    }
  },
  methods: {
    handleSomeEvent(){
      // handle some event
    }
  }  
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 事件修饰符

事件修饰符有:.capture、.once、.passive 、.stop、.self、.prevent、

  • .stop:等同于 JavaScript 中的 event.stopPropagation() ,防止事件冒泡;
  • .prevent :等同于 JavaScript 中的 event.preventDefault() ,防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播);
  • .capture :与事件冒泡的方向相反,事件捕获由外到内;
  • .self :只会触发自己范围内的事件,不包含子元素;
  • .once :只会触发一次。

image-20211226141649995

对于 .passive.capture.once 这些事件修饰符, Vue 提供了相应的前缀可以用于 on

修饰符 前缀
.passive &
.capture !
.once ~
.capture.once.once.capture ~!

对于所有其它的修饰符,私有前缀都不是必须的,因为你可以在事件处理函数中使用事件方法:

修饰符 处理函数中的等价操作
.stop event.stopPropagation()
.prevent event.preventDefault()
.self if (event.target !== event.currentTarget) return
按键: .enter, .13 if (event.keyCode !== 13) return (对于别的按键修饰符来说,可将 13 改为另一个按键码 (opens new window))
修饰键: .ctrl, .alt, .shift, .meta if (!event.ctrlKey) return (将 ctrlKey 分别修改为 altKeyshiftKey 或者 metaKey)
# 使用示范
<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>

<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>

<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>

<!-- 只有修饰符 -->
<form v-on:submit.prevent></form>

<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="doThis">...</div>

<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>

<!-- 点击事件将只会触发一次 -->
<a v-on:click.once="doThis"></a>

<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成  -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div v-on:scroll.passive="onScroll">...</div>

<!-- 只有在 `key``Enter` 时调用 `vm.submit()` -->
<input v-on:keyup.enter="submit">
<!--处理函数只会在 $event.key 等于 PageDown 时被调用-->
<input v-on:keyup.page-down="onPageDown">

<!-- Alt + C -->
<input @keyup.alt.67="clear">
<!-- Ctrl + Click -->
<div @click.ctrl="doSomething">Do something</div>

<!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>
<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button @click.exact="onClick">A</button>

//希望使用户能够按下键盘上的Enter键,来将内容提交给名为 “storeComment” 的方法
<textarea @keyup.enter="storeComment"></textarea>
new Vue({
  el: '#app',
  methods: {
    storeComment(event) {
      //using event.target.value or use v-model to bind to a data property
    }
  }
});
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
# 实现原理
  • 生成ast时处理;
  • codegen时处理;
  • 处理on事件;
//①生成ast时处理
export function addHandler ( 
    el: ASTElement,
    name: string,
    value: string,
    modifiers: ?ASTModifiers,
    important?: boolean,
    warn?: ?Function,
    range?: Range,
    dynamic?: boolean
    ) {
    modifiers = modifiers || emptyObject
    // check capture modifier
    if (modifiers.capture) { // 如果是capture 加!
        delete modifiers.capture
        name = prependModifierMarker('!', name, dynamic)
    }
    if (modifiers.once) {  // 如果是once加~
        delete modifiers.once
        name = prependModifierMarker('~', name, dynamic)
    }
    /* istanbul ignore if */
    if (modifiers.passive) { // 如果是passive 加&
        delete modifiers.passive
        name = prependModifierMarker('&', name, dynamic)
    }
}
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
//②codegen时处理
const genGuard = condition => `if(${condition})return null;`
const modifierCode: { [key: string]: string } = {
    stop: '$event.stopPropagation();', // 增加阻止默认事件
    prevent: '$event.preventDefault();', // 阻止默认行为
    self: genGuard(`$event.target !== $event.currentTarget`), // 点击是否是自己
}
for (const key in handler.modifiers) {
    if (modifierCode[key]) { 
        genModifierCode += modifierCode[key]
    }
    if (genModifierCode) {
        code += genModifierCode
    }
    const handlerCode = isMethodPath
    ? `return ${handler.value}($event)`
    : isFunctionExpression
        ? `return (${handler.value})($event)`
        : isFunctionInvocation
        ? `return ${handler.value}`
        : handler.value
    return `function($event){${code}${handlerCode}}`
}

//③处理on事件
for (name in on) {
    def = cur = on[name]
    old = oldOn[name]
    event = normalizeEvent(name) // 处理& ! ~
    if (isTrue(event.once)) {
        cur = on[name] = createOnceHandler(event.name, cur, event.capture)
    }
    add(event.name, cur, event.capture, event.passive, event.params) // 调用addEventListener绑定事件
}
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的事件绑定分为两种:

  • 原生Dom的事件绑定,采用的addEnventListener实现;
  • 组件的事件绑定,采用的是$on实现; 组件中的nativeOn等价于普通元素on, 组件的on会单独处理;
# 事件的编译
let compiler = require("vue-template-compiler");
let r1 = compiler.compile("<div @click='fn()' > xxx </div>");
let r2 = compiler.compile(
  "<my-component @click.native='fn' @onclick='fn1'>xxx </my-component>"
);
console.log(r1.render); 
//with(this){return _c('div',{on:{"click":function($event){return fn()}}},[_v(" xxx ")])}
console.log(r2.render);
// with(this){return _c('my-component',{on:{"onclick":fn1},nativeOn:{"click":function($event){return fn($event)}}},[_v("xxx ")])}
1
2
3
4
5
6
7
8
9
# 将原生事件绑定到组件

总的来说,有三种方法

  • 使用native修饰符
  • 使用$emit分发事件
  • 使用$listeners

三种方法都能使原生事件绑定到组件上,就写法上当然是第一种最简单,第三种更麻烦。但是只要理解了就都挺好写的了。

但是在使用时,还是根据需求来,若是就是想绑定到组件的根标签上,直接使用第一种即可。否则,便使用二或三。

想要在一个组件的根元素上直接监听一个原生事件。这时,你可以使用 v-on.native 修饰符:

<base-input v-on:focus.native="onFocus"></base-input>
1

再升级处理:子组件套了一层;

<label>
  {{ label }}
  <input
    v-bind="$attrs"
    v-bind:value="value"
    v-on:input="$emit('input', $event.target.value)"
  >
</label>
1
2
3
4
5
6
7
8

这时,父级的 .native 监听器将静默失败。它不会产生任何报错,但是 onFocus 处理函数不会如你预期地被调用。为了解决这个问题,Vue 提供了一个 $listeners 属性,它是一个对象,里面包含了作用在这个组件上的所有监听器

Vue.component('base-input', {
  inheritAttrs: false,
  props: ['label', 'value'],
  computed: {
    inputListeners: function () {
      var vm = this
      // `Object.assign` 将所有的对象合并为一个新对象
      return Object.assign({},
        this.$listeners,// 我们从父级添加所有的监听器【处理的核心】
        {// 然后我们添加自定义监听器,或覆写一些监听器的行为
          input: function (event) { // 这里确保组件配合 `v-model` 的工作
            vm.$emit('input', event.target.value)
          }
        }
      )
    }
  },
  template: `
    <label>
      {{ label }}
      <input
        v-bind="$attrs"
        v-bind:value="value"
        v-on="inputListeners"
      >
    </label>
  `
})
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-input> 组件是一个完全透明的包裹器了,也就是说它可以完全像一个普通的 元素一样使用了:所有跟它相同的 attribute 和监听器的都可以工作。

# 源码实现

原生dom的绑定

  • vue在创建dom时会调用createElm,默认会调用invokeCreateHooks;
  • 会遍历当前平台下相对的属性处理代码,其中就有updateComponentListeners方法,内部传入add方法;

源码位置:src/core/vdom/create-component:155

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on;
  // replace with listeners with .native modifier
  data.on = data.nativeOn;

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything other than props & listeners
    data = {};
  }
1
2
3
4
5
6
7
8
9
10

源码位置:src/core/instance/event:37

/*更新组件的监听事件*/
export function updateComponentListeners(
  vm: Component,
  listeners: Object,
  oldListeners: ?Object
) {
  target = vm;
  updateListeners(listeners, oldListeners || {}, add, remove, vm);
}
1
2
3
4
5
6
7
8
9

实践

image-20210225234812694

# sync修饰符

在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父组件和子组件都没有明显的改动来源

推荐以 update:myPropName 的模式触发事件取而代之; 父组件可以监听那个事件并根据需要更新一个本地的数据属性

this.$emit('update:title', newTitle)
<text-document
  v-bind:title="doc.title"
  v-on:update:title="doc.title = $event"
></text-document>

<!-- 为了方便起见,我们为这种模式提供一个缩写,即 .sync 修饰符:-->
<text-document v-bind:title.sync="doc.title"></text-document>
1
2
3
4
5
6
7
8

注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用 (例如 v-bind:title.sync=”doc.title + ‘!’” 是无效的)。取而代之的是,你只能提供你想要绑定的属性名,类似 v-model

当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync 修饰符和 v-bind 配合使用:

<text-document v-bind.sync="doc"></text-document>
1

这样会把 doc 对象中的每一个属性 (如 title) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on 监听器

v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。

# 过滤器fillter

过滤器的作用,如何实现一个过滤器

根据过滤器的名称,过滤器是用来过滤数据的,在Vue中使用filters来过滤数据,filters不会修改数据,而是过滤数据,改变用户看到的输出(计算属性 computed ,方法 methods 都是通过修改数据来处理数据格式的输出显示)。

使用场景:

  • 需要格式化数据的情况,比如需要处理时间、价格等数据格式的输出 / 显示。
  • 比如后端返回一个 年月日的日期字符串,前端需要展示为 多少天前 的数据格式,此时就可以用我们的fliters过滤器来处理数据。

过滤器是一个函数,它会把表达式中的值始终当作函数的第一个参数。过滤器用在**插值表达式 **v-bind表达式中,然后放在操作符“ | ”后面进行指示。

全局安装:或者在创建 Vue 实例之前全局定义过滤器

import Vue from 'vue'
import * as filter from './filter'

Object.keys(filter).forEach(k => Vue.filter(k, filter[k]))
export function formatDate(dataStr = new Date(), type = 1) {
  let pattern;
  switch (type) {
    case 1:
      pattern = "YYYY-MM-DD";
      break;
    case 2:
      pattern = "YYYY-MM-DD HH:mm:ss";
      break;
    case 3:
      pattern = "YYYYMMDD";
      break;
    case 4:
      pattern = "YYYYMM";
      break;
    case 5:
      pattern = "YYYY-MM-DD HH:mm";
      break;
    case 6: //周
      pattern = "YYYYWW";
      break;
  }
  return moment(dataStr).format(pattern);
}
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

html及js中使用

<span class="people">
    {{
    allEntranceNums.mod | numberFormat
    }}
</span>
startTime: this.$options.filters["formatDate"](startTime, 2),
endTime: this.$options.filters["formatDate"](endTime, 2)
1
2
3
4
5
6
7

局部使用:你可以在一个组件的选项中定义本地的过滤器: 当全局过滤器和局部过滤器重名时,会采用局部过滤器

<li>商品价格:{{item.price | filterPrice}}</li>

 filters: {
    filterPrice (price) {
      return price ? ('¥' + price) : '--'
    }
  }
1
2
3
4
5
6
7

# Vue.mixin/extend/extends/use

# component / extend / mixins / extends 的区别

  • Vue.component 注册全局组件,为了方便
  • Vue.extend 创建组件的构造函数,为了复用
  • mixins、extends 为了扩展

如果按照优先级去理解,当你需要继承一个组件时,可以使用Vue.extend().当你需要扩展组件功能的时候,可以使用extends,mixins;

# 比较总结

  • mixins可以混入多个mixin,extends只能继承一个
  • mixins类似于面向切面的编程(AOP),extends类似于面向对象的编程;
  • extend用于创建vue实例;
  • 优先级Vue.extend>extends>mixins;

# 简述 mixin、extends 的覆盖逻辑【要点】

mixin 和 extends mixin 和 extends均是用于合并、拓展组件的,两者均通过 mergeOptions 方法实现合并。

  • mixins 接收一个混入对象的数组,其中混入对象可以像正常的实例对象一样包含实例选项,这些选项会被合并到最终的选项中。Mixin 钩子按照传入顺序依次调用,并在调用组件自身的钩子之前被调用。
  • extends 主要是为了便于扩展单文件组件,接收一个对象或构造函数

bb253b1d177f421741af0e7dd0f52b5e.jpg

# mixin

Vue.mixin用法给组件每个生命周期,函数等都混入一些公共逻辑;

Vue.mixin的作用就是抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用mergeOptions方法进行合并,采用策略模式针对不同的属性进行合并。如果混入的数据和本身组件中的数据冲突,会采用“就近原则”以组件的数据为准

mixin中有很多缺陷 "命名冲突问题"、"依赖问题"、"数据来源问题", 这里强调一下mixin的数据是不会被共享的!

# 原理

mergeOptions 的执行过程

  • 规范化选项(normalizeProps、normalizelnject、normalizeDirectives)
  • 对未合并的选项,进行判断
if(!child._base) {
    if(child.extends) {
        parent = mergeOptions(parent, child.extends, vm)
    }
    if(child.mixins) {
        for(let i = 0, l = child.mixins.length; i < l; i++){
            parent = mergeOptions(parent, child.mixins[i], vm)
        }
    }
}
1
2
3
4
5
6
7
8
9
10
  • 合并处理。根据一个通用 Vue 实例所包含的选项进行分类逐一判断合并,如 props、data、 methods、watch、computed、生命周期等,将合并结果存储在新定义的 options 对象里。
  • 返回合并结果 options。

详细:

import { mergeOptions } from "../util/index";
/*初始化mixin*/
export function initMixin(Vue: GlobalAPI) {
  /*https://cn.vuejs.org/v2/api/#Vue-mixin*/
  Vue.mixin = function (mixin: Object) {
    /*mergeOptions合并optiuons*///将当前定义的属性合并到每个组件中
    this.options = mergeOptions(this.options, mixin); 
  };
}

/*合并两个option对象到一个新的对象中*/
export function mergeOptions(
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== "production") {
    /*检查是否是有效的组件名*/
    checkComponents(child);
  }

  if (typeof child === "function") {
    child = child.options;
  }

  /*确保所有props option序列化成正确的格式*/
  normalizeProps(child);
  /*将函数指令序列化后加入对象*/
  normalizeDirectives(child);
  /*
    https://cn.vuejs.org/v2/api/#extends
    允许声明扩展另一个组件(可以是一个简单的选项对象或构造函数),而无需使用 
    将child的extends也加入parent扩展
  */
  const extendsFrom = child.extends;
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, vm);
  }
  /*child的mixins加入parent中*/
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm);
    }
  }
  const options = {};
  let key;
  for (key in parent) {
    mergeField(key);
  }
  /*合并parent与child*/
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key);
    }
  }
  function mergeField(key) {
    /*strats里面存了options中每一个属性(el、props、watch等等)的合并方法,先取出*/
    const strat = strats[key] || defaultStrat;
    /*根据合并方法来合并两个option*/
    options[key] = strat(parent[key], child[key], vm, key);
  }
  return options;
}

strats.data = function (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    // in a Vue.extend merge, both should be functions
    if (!childVal) {
      return parentVal;
    }
    if (typeof childVal !== "function") {
      process.env.NODE_ENV !== "production" &&
        warn(
          'The "data" option should be a function ' +
            "that returns a per-instance value in component " +
            "definitions.",
          vm
        );
      return parentVal;
    }
    if (!parentVal) {
      return childVal;
    }
    // when parentVal & childVal are both present,
    // we need to return a function that returns the
    // merged result of both functions... no need to
    // check if parentVal is a function here because
    // it has to be a function to pass previous merges.
    return function mergedDataFn() {
      return mergeData(childVal.call(this), parentVal.call(this));
    };
  } else if (parentVal || childVal) {
    return function mergedInstanceDataFn() {
      // instance merge
      const instanceData =
        typeof childVal === "function" ? childVal.call(vm) : childVal;
      const defaultData =
        typeof parentVal === "function" ? parentVal.call(vm) : undefined;
      if (instanceData) {
        return mergeData(instanceData, defaultData);
      } else {
        return defaultData;
      }
    };
  }
};
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
# 快速Mock
Vue.mixin = function (obj) {
	this.options = mergeOptions(this.options,obj);
}
Vue.mixin({
	beforeCreate(){
		console.log('before create ok')
	}
})
1
2
3
4
5
6
7
8
# mixins要点
  • data混入组件数据优先
  • 钩子函数将混合为一个数组,混入对象的钩子将在组件自身钩子之前调用
  • 值为对象的选项,例如 methods, components 和 directives,将被混合为同一个对象。两个对象键名冲突时,取组件对象的键值对。
  • 以上合并策略可以通过Vue.config.optionMergeStrategies修改
# 示范使用

vuex的挂载

export default function applyMixin(Vue) {
  // const version = Number(Vue.version.split('.')[0])
  // 父子组件的beforecreate执行顺序
  Vue.mixin({
    //TODO:了解mixin的原理
    beforeCreate: vuexInit, // 内部会把生命周期函数 拍平成一个数组
    // destory: "xxx"
  });
}

// 组件渲染时从父=》子
// 给所有的组件增加$store 属性 指向我们创建的store实例
function vuexInit() {
  const options = this.$options; // 获取用户所有的选项
  if (options.store) {
    this.$store = options.store; // 根实例;比如:main.js
  } else if (options.parent && options.parent.$store) {
    this.$store = options.parent.$store; // 儿子 或者孙子....
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// mixins示例
var mixin = {
  created: function () { console.log(1) }
}
var vm = new Vue({
  created: function () { console.log(2) },
  mixins: [mixin]
})
// => 1
// => 2
1
2
3
4
5
6
7
8
9
10

# extend

Vue.extend使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。

# 原理
 /*
   使用基础 Vue 构造器,创建一个“子类”。
   其实就是扩展了基础构造器,形成了一个可复用的有指定选项功能的子构造器。
   参数是一个包含组件option的对象。  https://cn.vuejs.org/v2/api/#Vue-extend-options
   */
  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    /*父类的构造*/
    const Super = this
    /*父类的cid*/
    const SuperId = Super.cid
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    /*如果构造函数中已经存在了该cid,则代表已经extend过了,直接返回*/
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production') {
      /*name只能包含字母与连字符*/
      if (!/^[a-zA-Z][\w-]*$/.test(name)) {
        warn(
          'Invalid component name: "' + name + '". Component names ' +
          'can only contain alphanumeric characters and the hyphen, ' +
          'and must start with a letter.'
        )
      }
    }

    /*
      Sub构造函数其实就一个_init方法,这跟Vue的构造方法是一致的,在_init中处理各种数据初始化、生命周期等。
      因为Sub作为一个Vue的扩展构造器,所以基础的功能还是需要保持一致,跟Vue构造器一样在构造函数中初始化_init。
    */
    const Sub = function VueComponent (options) {
      this._init(options)
    }
    /*继承父类*/
    Sub.prototype = Object.create(Super.prototype)
    /*构造函数*/
    Sub.prototype.constructor = Sub
    /*创建一个新的cid*/
    Sub.cid = cid++
    /*将父组件的option与子组件的合并到一起(Vue有一个cid为0的基类,即Vue本身,会将一些默认初始化的option何入)*/
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    /*es6语法,super为父类构造*/
    Sub['super'] = Super
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
# 示范使用
// Vue.extend
// 创建构造器
var Profile = Vue.extend({
  template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
  data: function () {
    return {
      firstName: 'Walter',
      lastName: 'White',
      alias: 'samy'
    }
  }
})
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#mount-point')
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# extends

允许声明扩展另一个组件(可以是一个简单的选项对象或构造函数),而无需使用 Vue.extend。这主要是为了便于扩展单文件组件。这和 mixins 类似。

// extends示例
var CompA = { ... }
 
// 在没有调用 `Vue.extend` 时候继承 CompA
var CompB = {
  extends: CompA,
  ...
}
1
2
3
4
5
6
7
8

# use

Vue.use是用来使用插件的,可以在插件中扩展全局组件、指令、原型方法等

# 原理
Vue.use = function (plugin: Function | Object) {
    // 插件不能重复的加载
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {
        return this
    }
    // additional parameters
    const args = toArray(arguments, 1)
    args.unshift(this)  // install方法的第一个参数是Vue的构造函数,其他参数是Vue.use中除了第一个参数的其他参数
    if (typeof plugin.install === 'function') { // 调用插件的install方法
        plugin.install.apply(plugin, args)  Vue.install = function(Vue,args){}
    } else if (typeof plugin === 'function') { // 插件本身是一个函数,直接让函数执行
        plugin.apply(null, args) 
    }
    installedPlugins.push(plugin) // 缓存插件
    return this
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 快速mock

在vuex中的使用;

// Vue.use 方法会调用插件的install方法,此方法中的参数就是Vue的构造函数
// Vue.use = function (plugin) { // vue中类似use原理实现
//     plugin.install(this);
// }
// 插件的安装 Vue.use(Vuex)

export const install = (_Vue) => {
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== "production") {
      console.error(
        "[vuex] already installed. Vue.use(Vuex) should be called only once."
      );
    }
    return;
  }
  Vue = _Vue; // _Vue 是Vue的构造函数;vue缓存起来再暴露出去
  applyMixin(Vue); // 需要将根组件中注入的store 分派给每一个组件 (子组件) Vue.mixin
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 使用插件

安装 Vue.js 插件。**如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。**install 方法调用时,会将 Vue 作为参数传入。

该方法需要在调用 new Vue() 之前被调用。

当 install 方法被同一个插件多次调用,插件将只会被安装一次。Vue.use 会自动阻止多次注册相同插件,届时即使多次调用也只会注册一次该插件

通过全局方法 Vue.use() 使用插件, 调用 MyPlugin.install(Vue)它需要在你调用 new Vue() 启动应用之前完成

// 调用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin)
new Vue({
  // ...组件选项
})
//也可以传入一个可选的选项对象:
Vue.use(MyPlugin, { someOption: true })
1
2
3
4
5
6
7
# 调用方式
  • 隐式调用:Vue.js 官方提供的一些插件 (例如 vue-router)在检测到 Vue 是可访问的全局变量时会自动调用 Vue.use()

  • 显示调用:然而在像 CommonJS 这样的模块环境中,你应该始终显式地调用 Vue.use()

// 用 Browserify 或 webpack 提供的 CommonJS 模块环境时
var Vue = require('vue')
var VueRouter = require('vue-router')
// 不要忘了调用此方法
Vue.use(VueRouter)
1
2
3
4
5
# 步骤
  1. 采用ES6的import ... from ...语法或CommonJSd的require()方法引入插件

  2. 使用全局方法Vue.use( plugin )使用插件,可以传入一个选项对象Vue.use(MyPlugin, { someOption: true })

import router from './router'
import store from './store'
import ElementUi from 'element-ui'
import * as filters from './filters'
Vue.use(ElementUi)//显示调用
Object.keys(filters).forEach(k => Vue.filter(k, filters[k]))
const app = new Vue({
  el: '#app',
  router,//隐式调用;
  store,
  ...App
})
export default app
1
2
3
4
5
6
7
8
9
10
11
12
13

routet/index.js

import Vue from 'vue'
import Router from 'vue-router'
const login = () => import(/* webpackChunkName: "login" */ '../pages/login')
Vue.use(Router)

router.afterEach(transition => {
  NProgress.done()
})
export default router
1
2
3
4
5
6
7
8
9

# 模板编译

总体流程会将template => ast树 => codegen => 转化为render函数 => 内部调用的就是_c方法 => 虚拟dom

# 核心三步骤

template转行成render函数;【template 到 render 的过程】

  • template=> ast => codegen => with+function 实现生成render方法;

核心步骤:

  • 1.将template模板转换成ast语法树 - parserHTML
  • 2.对静态语法做静态标记 - markUp
  • 3.重新生成代码 -codeGen
const ast = parse(template.trim(), options)//调用parse方法将template转化为ast(抽象语法树)
optimize(ast,options)//对静态节点做优化
const code = generate(ast, options)//生成代码

//generate将ast抽象语法树编译成 render字符串并将静态部分放到 staticRenderFns 中,最后通过 `new Function(`` render``)` 生成render函数。
1
2
3
4
5

如何将template转换成render函数(这里要注意的是我们在开发时尽量不要使用template,因为将template转化成render方法需要在运行时进行编译操作会有性能损耗,同时引用带有compiler包的vue体积也会变大。默认.vue文件中的template处理是通过vue-loader来进行处理的并不是通过运行时的编译 - 后面我们会说到默认vue项目中引入的vue.js是不带有compiler模块的)。

模板引擎的实现原理就是new Function + with来进行实现的;

vue-loader中处理template属性主要靠的是vue-template-compiler模块

let code = generate(root);//根据语法树生产新的代码;
//with会不安全,但是可以帮助我们解决作用域的问题;
let render = `with(this){return ${code}}`;
let renderFu = new Function(render);//包装成函数; 【模板引擎实现原理】
1
2
3
4

# 原理

function baseCompile(
  template: string,
  options: CompilerOptions
): CompiledResult {
  /*parse解析得到ast树*/
  const ast = parse(template.trim(), options);
  /*
    将AST树进行优化
    优化的目标:生成模板AST树,检测不需要进行DOM改变的静态子树。
    一旦检测到这些静态树,我们就能做以下这些事情:
    1.把它们变成常数,这样我们就再也不需要每次重新渲染时创建新的节点了。
    2.在patch的过程中直接跳过。
 */
  optimize(ast, options);
  /*根据ast树生成所需的code(内部包含render与staticRenderFns)*/
  const code = generate(ast, options);
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns,
  };
}

/*新建成Funtion对象*/
function makeFunction(code, errors) {
  try {
    return new Function(code);
  } catch (err) {
    errors.push({ err, code });
    return noop;
  }
}
export function createCompiler(baseOptions: CompilerOptions) {}
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

源码位置: src/core/instance/render.js

 /*处理v-once的渲染函数*/
  Vue.prototype._o = markOnce
  /*将字符串转化为数字,如果转换失败会返回原字符串*/
  Vue.prototype._n = toNumber
  /*将val转化成字符串*/
  Vue.prototype._s = toString
  /*处理v-for列表渲染*/
  Vue.prototype._l = renderList
  /*处理slot的渲染*/
  Vue.prototype._t = renderSlot
  /*检测两个变量是否相等*/
  Vue.prototype._q = looseEqual
  /*检测arr数组中是否包含与val变量相等的项*/
  Vue.prototype._i = looseIndexOf
  /*处理static树的渲染*/
  Vue.prototype._m = renderStatic
  /*处理filters*/
  Vue.prototype._f = resolveFilter
  /*从config配置中检查eventKeyCode是否存在*/
  Vue.prototype._k = checkKeyCodes
  /*合并v-bind指令到VNode中*/
  Vue.prototype._b = bindObjectProps
  /*创建一个文本节点*/
  Vue.prototype._v = createTextVNode
  /*创建一个空VNode节点*/
  Vue.prototype._e = createEmptyVNode
  /*处理ScopedSlots*/
  Vue.prototype._u = resolveScopedSlots
}
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

# 快速Mock

<script src="node_modules/vue-template-compiler/browser.js"></script>
<script>
    // template=> ast => codegen => with+function 实现生成render方法
    let {ast,render } = VueTemplateCompiler.compile(`<div>{{aaa}}</div>`);
    console.log(ast,render)
    // 模板引擎的实现原理 with + new Function
    console.log(new Function(render))
    // render方法执行完毕后生成的是虚拟dom
    // with(this){return _c('div',[_s(aaa)])}
    // 代码生成
</script>
1
2
3
4
5
6
7
8
9
10
11

# template到render的过程

vue的模板编译过程如下:template - ast - render函数

vue在模板编译中执行compileToFunctions将template转化成render函数

compileToFunctions的主要核心点:

1:调用parse方法将template转化为ast树(抽象语法树)

parse的目的:是把template转化为ast树,它是一种用js对象的形式来描述整个模板。

解析过程:利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构成ast树

ast元素节点(type)总共三种类型:普通元素--1,表达式--2,纯文本--3

2:对静态节点做优化 这个过程主要分析出哪些是静态节点,在其做一个标记,为后面的更新渲染可以直接跳过,静态节点做优化

深度遍历AST,查看每个子树的节点元素是否为静态节点或者静态节点根。如果为静态节点,他们生成的dom永远不会改变,这对运行时模板更新起到优化作用

3:生成代码 generate将ast抽象语法树编译成render字符串并将静态部分放到staticRender中,最后通过new Function(render)生成render函数

# 虚拟DOM/diff

首先,我们都知道在前端性能优化的一个秘诀就是尽可能少地操作DOM,不仅仅是DOM相对较慢,更因为频繁变动DOM会造成浏览器的回流或者重回,这些都是性能的杀手,因此我们需要这一层抽象,在patch过程中尽可能地一次性将差异更新到DOM中,这样保证了DOM不会出现性能很差的情况.

其次,现代前端框架的一个基本要求就是无须手动操作DOM,一方面是因为手动操作DOM无法保证程序性能,多人协作的项目中如果review不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动DOM操作可以大大提高开发效率.

# 三步骤

总体流程会将template ast树 => codegen => 转化为render函数 => 内部调用的就是_c方法 => 虚拟dom

虚拟 DOM 的实现原理主要包括以下 3 部分

  • create: 用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象
  • diff 算法 — 比较两棵虚拟 DOM 树的差异;
  • pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树

# Dom抽象

# 原理

减少了对真实DOM的操作;虚拟DOM的实现就是普通对象包含tag、data、children等属性对真实节点的描述。(本质上就是在JS和DOM之间的一个缓存

 constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions
  ) {
    /*当前节点的标签名*/
    this.tag = tag
    /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
    this.data = data
    /*当前节点的子节点,是一个数组*/
    this.children = children
    /*当前节点的文本*/
    this.text = text
    /*当前虚拟节点对应的真实dom节点*/
    this.elm = elm
    /*当前节点的名字空间*/
    this.ns = undefined
    /*当前节点的编译作用域*/
    this.context = context
    /*函数化组件作用域*/
    this.functionalContext = undefined
    /*节点的key属性,被当作节点的标志,用以优化*/
    this.key = data && data.key
    /*组件的option选项*/
    this.componentOptions = componentOptions
    /*当前节点对应的组件的实例*/
    this.componentInstance = undefined
    /*当前节点的父节点*/
    this.parent = undefined
    /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
    this.raw = false
    /*是否为静态节点*/
    this.isStatic = false
    /*是否作为跟节点插入*/
    this.isRootInsert = true
    /*是否为注释节点*/
    this.isComment = false
    /*是否为克隆节点*/
    this.isCloned = false
    /*是否有v-once指令*/
    this.isOnce = 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

源码位置:src/core/vdom/create-element:23

export function createElement(
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode {
  /*兼容不传data的情况*/
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children;
    children = data;
    data = undefined;
  }
  /*如果alwaysNormalize为true,则normalizationType标记为ALWAYS_NORMALIZE*/
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE;
  }
  /*创建虚拟节点*/
  return _createElement(context, tag, data, children, normalizationType);
}

/*创建VNode节点*/
export function _createElement(
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode {
  /*
    如果data未定义(undefined或者null)或者是data的__ob__已经定义(代表已经被observed,上面绑定了Oberver对象),
    https://cn.vuejs.org/v2/guide/render-function.html#约束
    那么创建一个空节点
  */
  if (isDef(data) && isDef((data: any).__ob__)) {
    process.env.NODE_ENV !== "production" &&
      warn(
        `Avoid using observed data object as vnode data: ${JSON.stringify(
          data
        )}\n` + "Always create fresh vnode data objects in each render!",
        context
      );
    return createEmptyVNode();
  }
  /*如果tag不存在也是创建一个空节点*/
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode();
  }
  // support single function children as default scoped slot
  /*默认默认作用域插槽*/
  if (Array.isArray(children) && typeof children[0] === "function") {
    data = data || {};
    data.scopedSlots = { default: children[0] };
    children.length = 0;
  }
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children);
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children);
  }
  let vnode, ns;
  if (typeof tag === "string") {
    let Ctor;
    /*获取tag的名字空间*/
    ns = config.getTagNamespace(tag);
    /*判断是否是保留的标签*/
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      /*如果是保留的标签则创建一个相应节点*/
      vnode = new VNode(
        config.parsePlatformTagName(tag),
        data,
        children,
        undefined,
        undefined,
        context
      );
    } else if (
      isDef((Ctor = resolveAsset(context.$options, "components", tag)))
    ) {
      // component
      /*从vm实例的option的components中寻找该tag,存在则就是一个组件,创建相应节点,Ctor为组件的构造类*/
      vnode = createComponent(Ctor, data, context, children, tag);
    } else {
      /*未知的元素,在运行时检查,因为父组件可能在序列化子组件的时候分配一个名字空间*/
      vnode = new VNode(tag, data, children, undefined, undefined, context);
    }
  } else {
    // direct component options / constructor
    /*tag不是字符串的时候则是组件的构造类*/
    vnode = createComponent(tag, data, context, children);
  }
  if (isDef(vnode)) {
    /*如果有名字空间,则递归所有子节点应用该名字空间*/
    if (ns) applyNS(vnode, ns);
    return vnode;
  } else {
    /*如果vnode没有成功创建则创建空节点*/
    return createEmptyVNode();
  }
}
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
# 快速Mock
function _c(tag, data, ...children) {
  let key = data.key;
  delete data.key;
  children = children.map((children) => {
    if (typeof child === "object") {
      return child;
    } else {
      return vnode(undefined, undefined, undefined, undefined, children);
    }
  });
  return vnode(tag, data, key, children);
}

function vnode(tag, data, key, children, text) {
  return {
    tag, // 当前节点的标签名
    data, // 当前标签上的属性
    key, //唯一表示用户可能传递
    children,
    text,
  };
}

let r = _c("div", { id: "container" }, _c("p", {}, "hello"), "damy");
console.log(r);
// { tag: 'div',
//   data: { id: 'container' },
//   key: undefined,
//   children:
//    [ { tag: undefined,
//        data: undefined,
//        key: undefined,
//        children: undefined,
//        text: [Object] },
//      { tag: undefined,
//        data: undefined,
//        key: undefined,
//        children: undefined,
//        text: 'damy' } ],
//   text: undefined }
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

# diff算法【要点】

Vue的diff算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式 + 双指针的方式进行比较。Vue3中采用最长递增子序列实现diff算法;

# vue有了数据响应式,为何还要diff?

核心原因:粒度

  • React通过setState知道有变化了,但不知道哪里变化了,所以需要通过diff找出变化的地方并更新dom。
  • Vue已经可以通过响应式系统知道哪里发生了变化,但是所有变化都通过响应式会创建大量Watcher,极其消耗性能,因此vue采用的方式是通过响应式系统知道哪个组件发生了变化,然后在组件内部使用diff。这样的中粒度策略,即不会产生大量的Watcher,也使diff的节点减少了,一举两得。
# 时间复杂度

**diff算法的时间复杂度:O(n)**两个树的完全的diff算法是一个时间复杂度为O(n3), vue进行了优化将O(n3)复杂度的问题转换成O(n)复杂度的问题【只比较同级不考虑跨级问题】,在前端当中,很少会跨越层次地移动DOM元素,所以Virtual Dom只会对同一个层次的元素进行对比;

# 主要流程

先同级比较,再比较子节点;

  • 先比较是否是相同节点;
  • 相同节点比较属性,并复用老节点;
    • 先判断一方有儿子,一方没有儿子的情况;
    • 如果老有,新没有,移除老节点的信息,
    • 如果老没有,新有,把新节点的信息替换到老节点上;
  • 比较都有儿子的情况;【最复杂的,子流程】
    • 考虑老节点和新节点儿子的情况;
    • 优化比较:头头、尾尾、头尾、尾头;
    • 比对查找进行复用【体现出key的重要性】;
  • 递归比较子节点;
# 原理
  • Vue2内部采用深度递归的方式 + 双指针的方式进行比较;
  • Vue3中采用最长递增子序列实现diff算法;
/*patch VNode节点*/
  function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    /*两个VNode节点相同则直接返回*/
    if (oldVnode === vnode) {
      return;
    }
    /*
      如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),
      并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),
      那么只需要替换elm以及componentInstance即可。
    */
    if (
      isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.elm = oldVnode.elm;
      vnode.componentInstance = oldVnode.componentInstance;
      return;
    }
    let i;
    const data = vnode.data;
    if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
      /*i = data.hook.prepatch,如果存在的话,见"./create-component componentVNodeHooks"。*/
      i(oldVnode, vnode);
    }
    const elm = (vnode.elm = oldVnode.elm);
    const oldCh = oldVnode.children;
    const ch = vnode.children;
    if (isDef(data) && isPatchable(vnode)) {
      /*调用update回调以及update钩子*/
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
      if (isDef((i = data.hook)) && isDef((i = i.update))) i(oldVnode, vnode);
    }
    /*如果这个VNode节点没有text文本时*/
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        /*新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren*/
        if (oldCh !== ch)
          updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
      } else if (isDef(ch)) {
        /*如果老节点没有子节点而新节点存在子节点,先清空elm的文本内容,然后为当前节点加入子节点*/
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, "");
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
      } else if (isDef(oldCh)) {
        /*当新节点没有子节点而老节点有子节点的时候,则移除所有ele的子节点*/
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
      } else if (isDef(oldVnode.text)) {
        /*当新老节点都无子节点的时候,只是文本的替换,因为这个逻辑中新节点text不存在,所以直接去除ele的文本*/
        nodeOps.setTextContent(elm, "");
      }
    } else if (oldVnode.text !== vnode.text) {
      /*当新老节点text不一样时,直接替换这段文本*/
      nodeOps.setTextContent(elm, vnode.text);
    }
    /*调用postpatch钩子*/
    if (isDef(data)) {
      if (isDef((i = data.hook)) && isDef((i = i.postpatch)))
        i(oldVnode, vnode);
    }
  }

function updateChildren(
    parentElm,
    oldCh,
    newCh,
    insertedVnodeQueue,
    removeOnly
  ) {
    let oldStartIdx = 0;
    let newStartIdx = 0;
    let oldEndIdx = oldCh.length - 1;
    let oldStartVnode = oldCh[0];
    let oldEndVnode = oldCh[oldEndIdx];
    let newEndIdx = newCh.length - 1;
    let newStartVnode = newCh[0];
    let newEndVnode = newCh[newEndIdx];
    let oldKeyToIdx, idxInOld, elmToMove, refElm;

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly;

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx];
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        /*前四种情况其实是指定key的时候,判定为同一个VNode,则直接patchVnode即可,分别比较oldCh以及newCh的两头节点2*2=4种情况*/
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
        oldEndVnode = oldCh[--oldEndIdx];
        newEndVnode = newCh[--newEndIdx];
      } else if (sameVnode(oldStartVnode, newEndVnode)) {
        // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
        canMove &&
          nodeOps.insertBefore(
            parentElm,
            oldStartVnode.elm,
            nodeOps.nextSibling(oldEndVnode.elm)
          );
        oldStartVnode = oldCh[++oldStartIdx];
        newEndVnode = newCh[--newEndIdx];
      } else if (sameVnode(oldEndVnode, newStartVnode)) {
        // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
        canMove &&
          nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
        oldEndVnode = oldCh[--oldEndIdx];
        newStartVnode = newCh[++newStartIdx];
      } else {
        /*
          生成一个key与旧VNode的key对应的哈希表(只有第一次进来undefined的时候会生成,也为后面检测重复的key值做铺垫)
          比如childre是这样的 [{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}]  beginIdx = 0   endIdx = 2  
          结果生成{key0: 0, key1: 1, key2: 2}
        */
        if (isUndef(oldKeyToIdx))
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
        /*如果newStartVnode新的VNode节点存在key并且这个key在oldVnode中能找到则返回这个节点的idxInOld(即第几个节点,下标)*/
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : null;
        if (isUndef(idxInOld)) {
          // New element
          /*newStartVnode没有key或者是该key没有在老节点中找到则创建一个新的节点*/
          createElm(
            newStartVnode,
            insertedVnodeQueue,
            parentElm,
            oldStartVnode.elm
          );
          newStartVnode = newCh[++newStartIdx];
        } else {
          /*获取同key的老节点*/
          elmToMove = oldCh[idxInOld];
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== "production" && !elmToMove) {
            /*如果elmToMove不存在说明之前已经有新节点放入过这个key的Dom中,提示可能存在重复的key,确保v-for的时候item有唯一的key值*/
            warn(
              "It seems there are duplicate keys that is causing an update error. " +
                "Make sure each v-for item has a unique key."
            );
          }
          if (sameVnode(elmToMove, newStartVnode)) {
            /*如果新VNode与得到的有相同key的节点是同一个VNode则进行patchVnode*/
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
            /*因为已经patchVnode进去了,所以将这个老节点赋值undefined,之后如果还有新节点与该节点key相同可以检测出来提示已有重复的key*/
            oldCh[idxInOld] = undefined;
            /*当有标识位canMove实可以直接插入oldStartVnode对应的真实Dom节点前面*/
            canMove &&
              nodeOps.insertBefore(
                parentElm,
                newStartVnode.elm,
                oldStartVnode.elm
              );
            newStartVnode = newCh[++newStartIdx];
          } else {
            // same key but different element. treat as new element
            /*当新的VNode与找到的同样key的VNode不是sameVNode的时候(比如说tag不一样或者是有不一样type的input标签),创建一个新的节点*/
            createElm(
              newStartVnode,
              insertedVnodeQueue,
              parentElm,
              oldStartVnode.elm
            );
            newStartVnode = newCh[++newStartIdx];
          }
        }
      }
    }
    if (oldStartIdx > oldEndIdx) {
      /*全部比较完成以后,发现oldStartIdx > oldEndIdx的话,说明老节点已经遍历完了,新节点比老节点多,所以这时候多出来的新节点需要一个一个创建出来加入到真实Dom中*/
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
      addVnodes(
        parentElm,
        refElm,
        newCh,
        newStartIdx,
        newEndIdx,
        insertedVnodeQueue
      );
    } else if (newStartIdx > newEndIdx) {
      /*如果全部比较完成以后发现newStartIdx > newEndIdx,则说明新节点已经遍历完了,老节点多余新节点,这个时候需要将多余的老节点从真实Dom中移除*/
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
    }
  }
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193

# patch三步骤

  1. patch
  2. patchVnode
  3. updateChildren
function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
    /*vnode不存在则直接调用销毁钩子*/
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue, parentElm, refElm)
    } else {
      /*标记旧的VNode是否有nodeType*/
      /*Github:https://github.com/answershuto*/
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        /*是同一个节点的时候直接修改现有的节点*/
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
   ...
   return vnode.elm

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
function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    // 两个vnode相同,说明不需要diff,直接返回
    if (oldVnode === vnode) {
      return
    }

    // 如果传入了ownerArray和index,可以进行重用vnode,updateChildren里用来替换位置
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // clone reused vnode
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    const elm = vnode.elm = oldVnode.elm

    // 如果oldVnode的isAsyncPlaceholder属性为true时,跳过检查异步组件,return
    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }
    /*
      如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),
      并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),
      那么只需要替换elm以及componentInstance即可。
    */
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    /*如果这个VNode节点没有text文本时*/
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
      // 两个vnode都定义了子节点,并且不相同,就对子节点进行diff
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
      // 如果只有新的vnode定义了子节点,则进行添加子节点的操作
        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(ch)
        }
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
      // 如果只有旧的vnode定义了子节点,则进行删除子节点的操作
        removeVnodes(oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }
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
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // 声明oldCh和newCh的头尾索引和头尾的vnode,
    let oldStartIdx = 0
    let newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm
    
    const canMove = !removeOnly

    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(newCh)
    }

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
        // 判断两边的头是不是相同节点
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
        // 判断尾部是不是相同节点
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
        // 判断旧节点头部是不是与新节点的尾部相同,相同则把头部往右移
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
        // 判断旧节点尾部是不是与新节点的头部相同,相同则把头部往左移
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
       /*
          生成一个key与旧VNode的key对应的哈希表
        */
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    // oldCh或者newCh遍历完,说明剩下的节点不是新增就是删除
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }
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

# 真实DOM和其解析流程

流程图:

img

所有的浏览器渲染引擎工作流程大致分为5步:创建 DOM 树 —> 创建 Style Rules -> 构建 Render 树 —> 布局 Layout -—> 绘制 Painting 【D R 渲布绘】 【DCJ渲布绘】

  • 第一步,构建 DOM 树:用 HTML 分析器,分析 HTML 元素,构建一棵 DOM 树;
  • 第二步,生成样式表:用 CSS 分析器,分析 CSS 文件和元素上的 inline 样式,生成页面的样式表;
  • 第三步,构建 Render 树:将 DOM 树和样式表关联起来,构建一棵 Render 树(Attachment)。每个 DOM 节点都有 attach 方法,接受样式信息,返回一个 render 对象(又名 renderer),这些 render 对象最终会被构建成一棵 Render 树;
  • 第四步,确定节点坐标:根据 Render 树结构,为每个 Render 树上的节点确定一个在显示屏上出现的精确坐标;
  • 第五步,绘制页面:根据 Render 树和节点显示坐标,然后调用每个节点的 paint 方法,将它们绘制出来。

# 组件

# name选项

# 组件中写name选项又哪些好处及作用?

  • 可以通过名字找到对应的组件 (递归组件)
  • 可用通过name属性实现缓存功能 (keep-alive)
  • 可以通过name来识别组件 (跨级组件通信时非常重要)
Vue.extend = function(){
  if (name) {
	Sub.options.components[name] = Sub
  }  
}
1
2
3
4
5

# 初始化

# 渲染和更新过程

  • 渲染组件时,会通过Vue.extend方法构建子组件的构造函数,并进行实例化,最终手动调用$mount()进行挂载;
  • 更新组件时,会进行patchvnode流程,核心就是diff算法;
  1. 父子组件渲染的先后顺序;

  2. 组件是如何渲染到页面上的;

    ①在渲染父组件时会创建父组件的虚拟节点,其中可能包含子组件的标签 ;

    ②在创建虚拟节点时,获取组件的定义使用Vue.extend生成组件的构造函数;

    ③将虚拟节点转化成真实节点时,会创建组件的实例并且调用组件的$mount方法;

    ④所以组件的创建过程是先父后子;

# 原理
// hooks to be invoked on component VNodes during patch
/*被用来在VNode组件patch期间触发的钩子函数集合*/
const componentVNodeHooks = {
  init(
    vnode: VNodeWithData,
    hydrating: boolean,
    parentElm: ?Node,
    refElm: ?Node
  ): ?boolean {
    if (!vnode.componentInstance || vnode.componentInstance._isDestroyed) {
      const child = (vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance,
        parentElm,
        refElm
      ));
      child.$mount(hydrating ? vnode.elm : undefined, hydrating);
    } else if (vnode.data.keepAlive) {
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode; // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode);
    }
  },
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 父子组件生命周期

# 调用顺序

可以归类为以下 4 部分:

  • 加载渲染过程; 这个特别点;后面的mount操作顺序;

    父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

  • 子组件更新过程;

    父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

  • 父组件更新过程;

    父 beforeUpdate -> 父 updated

  • 销毁过程;

    父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

# 理解

组件的调用顺序都是先父后子,渲染完成的顺序肯定是先子后父;

组件的销毁操作先父后子,销毁完成是先子后父;

父监听到子的生命周期

有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,

方式一:以上需要手动通过 $emit 触发父组件的事件; 可以通过以下写法实现:

// Parent.vue
<Child @mounted="doSomething"/>
// Child.vue
mounted() {
  this.$emit("mounted");
}
1
2
3
4
5
6

方式二:更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:

//  Parent.vue
<Child @hook:mounted="doSomething" ></Child>
doSomething() {
   console.log('父组件监听到 mounted 钩子函数 ...');
},
//  Child.vue
mounted(){
   console.log('子组件触发 mounted 钩子函数 ...');
},    
// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...     
1
2
3
4
5
6
7
8
9
10
11
12

当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:created, updated 等都可以监听。

示例优化:

mounted: function () {//优化后
  var picker = new Pikaday({
    field: this.$refs.input,
    format: 'YYYY-MM-DD'
  })
  this.$once('hook:beforeDestroy', function () {
    picker.destroy()
  })
}
1
2
3
4
5
6
7
8
9
# 原理
  /*创建一个组件*/
  function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data;
    if (isDef(i)) {
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
      if (isDef((i = i.hook)) && isDef((i = i.init))) {
        i(vnode, false /* hydrating */, parentElm, refElm);
      }
      // after calling the init hook, if the vnode is a child component
      // it should've created a child instance and mounted it. the child
      // component also has set the placeholder vnode's elm.
      // in that case we can just return the element and be done.
      /*
        在调用了init钩子以后,如果VNode是一个子组件,它应该已经创建了一个子组件实例并挂载它。
        子组件也应该设置了一个VNode占位符,我们直接返回组件实例即可。
        意思就是如果已经存在组件实例,则不需要重新创建一个新的,我们要做的就是初始化组件以及激活组件即可,还是用原来的组件实例。
      */
      if (isDef(vnode.componentInstance)) {
        /*初始化组件*/
        initComponent(vnode, insertedVnodeQueue);
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
        }
        return true;
      }
    }
  }
  /*初始化组件*/
  function initComponent(vnode, insertedVnodeQueue) {
    /*把之前已经存在的VNode队列合并进去*/
    if (isDef(vnode.data.pendingInsert)) {
      insertedVnodeQueue.push.apply(
        insertedVnodeQueue,
        vnode.data.pendingInsert
      );
    }
    vnode.elm = vnode.componentInstance.$el;
    if (isPatchable(vnode)) {
      /*调用create钩子*/
      invokeCreateHooks(vnode, insertedVnodeQueue);
      /*为scoped CSS 设置scoped id*/
      setScope(vnode);
    } else {
      // empty component root.
      // skip all element-related modules except for ref (#3455)
      /*注册ref*/
      registerRef(vnode);
      // make sure to invoke the insert hook
      insertedVnodeQueue.push(vnode);
    }
  }
  function invokeInsertHook(vnode, queue, initial) {
    // delay insert hooks for component root nodes, invoke them after the
    // element is really inserted
    if (isTrue(initial) && isDef(vnode.parent)) {
      vnode.parent.data.pendingInsert = queue;
    } else {
      for (let i = 0; i < queue.length; ++i) {
        queue[i].data.hook.insert(queue[i]);
      }
    }
  }
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
  Vue.prototype.$destroy = function () {
    const vm: Component = this;
    if (vm._isBeingDestroyed) {
      return;
    }
    /* 调用beforeDestroy钩子 */
    callHook(vm, "beforeDestroy");
    /* 标志位 */
    vm._isBeingDestroyed = true;
    // remove self from parent
    const parent = vm.$parent;
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm);
    }
    // teardown watchers
    /* 该组件下的所有Watcher从其所在的Dep中释放 */
    if (vm._watcher) {
      vm._watcher.teardown();
    }
    let i = vm._watchers.length;
    while (i--) {
      vm._watchers[i].teardown();
    }
    // remove reference from data ob
    // frozen object may not have observer.
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--;
    }
    // call the last hook...
    vm._isDestroyed = true;
    // invoke destroy hooks on current rendered tree
    vm.__patch__(vm._vnode, null);//先销毁儿子
    // fire destroyed hook
    /* 调用destroyed钩子 */
    callHook(vm, "destroyed");
    // turn off all instance listeners.
    /* 移除所有事件监听 */
    vm.$off();
    // remove __vue__ reference
    if (vm.$el) {
      vm.$el.__vue__ = null;
    }
    // remove reference to DOM nodes (prevents leak)
    vm.$options._parentElm = vm.$options._refElm = null;
  };
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

# keep-alive

# 生命周期
activited keep-alive 专属,组件被激活时调用
deactivated keep-alive 专属,组件被销毁时调用
# 场景

keep-alive可以实现组件的缓存,当组件切换时不会对单曲最近进行卸载。

常用的2个属性include/exclude,2个生命周期activated/deactivated

keep-alive是一个抽象的组件,缓存的组件不会被mounted,为此提供activateddeactivated钩子函数

在2.1.0 版本后keep-alive新加入了两个属性: include(包含的组件缓存生效) 与 exclude(排除的组件不缓存,优先级大于include) 。

当引入keep-alive的时候,页面第一次进入,钩子的触发顺序 created-> mounted-> activated,退出时触发deactivated。当再次进入(前进或者后退)时,只触发activated

件挂载的方法等,只执行一次的放在 mounted 中;组件每次进去都执行的方法放在 activated 中, activated 中的方法不管是否需要缓存都会执行

# 主要流程
  1. 判断组件 name ,不在 include 或者在 exclude 中,直接返回 vnode,说明该组件不被缓存。
  2. 获取组件实例 key ,如果有获取实例的 key,否则重新生成。
  3. key生成规则,cid +"∶∶"+ tag ,仅靠cid是不够的,因为相同的构造函数可以注册为不同的本地组件。
  4. 如果缓存对象内存在,则直接从缓存对象中获取组件实例给 vnode ,不存在则添加到缓存对象中。
  5. 最大缓存数量,当缓存组件数量超过 max 值时,清除 keys 数组内第一个组件。
# render函数
  1. 会在 keep-alive 组件内部去写自己的内容,所以可以去获取默认 slot 的内容,然后根据这个去获取组件;
  2. keep-alive 只对第一个组件有效,所以获取第一个子组件;
  3. 和 keep-alive 搭配使用的一般有:动态组件 和router-view;
# keep-alive 本身的创建过程和 patch 过程

缓存渲染的时候,会根据 vnode.componentInstance(首次渲染 vnode.componentInstance 为 undefned) 和 keepAlive 属性判断不会执行组件的 created、mounted 等钩子函数,

而是对缓存的组件执行 patch 过程∶ 直接把缓存的 DOM 对象直接插入到目标元素中,完成了数据更新的情况下的渲染过程。

  • 组件的首次渲染∶判断组件的 abstract 属性,才往父组件里面挂载 DOM;
  • 判断当前 keepAlive 和 componentInstance 是否存在来判断是否要执行组件 prepatch 还是执行创建 componentlnstance
  • prepatch 操作就不会在执行组件的 mounted 和 created 生命周期函数,而是直接将 DOM 插入
# 原理

keep-alive主要是缓存,采用的是LRU算法。 最近最久未使用法

  • 通过this.$slots.default拿到插槽组件,也就是keep-alive包裹的组件,getFirstComponentChild获取第一个子组件,获取该组件的name(存在组件名则直接使用组件名,否则会使用tag)。接下来会将这个name通过include与exclude属性进行匹配,匹配不成功(说明不需要进行缓存)则不进行任何操作直接返回vnode,(vnode节点描述对象,vue通过vnode创建真实的DOM)
  • 匹配到了就开始缓存,根据key在this.cache中查找,如果存在则说明之前已经缓存过了,直接将缓存的vnode的componentInstance(组件实例)覆盖到目前的vnode上面。否则将vnode存储在cache中。并且通过remove(keys, key),将当前的key从keys中删除再重新keys.push(key),这样就改变了当前key在keys中的位置。这个是为了实现max的功能,并且遵循缓存淘汰策略。
  • 如果没匹配到,说明没缓存过,这时候需要进行缓存,并且判断当前缓存的个数是否超过max指定的个数,如果超过,则销毁keys里的最后一个组件,并从keys中移除,这个就是LRU(Least Recently Used :最近最少使用 )缓存淘汰算法。
  • 最后返回vnode或者默认插槽的第一个组件进行DOM渲染。
* keep-alive组件 */
export default {
  name: "keep-alive",
  /* 抽象组件 */
  abstract: true,

  props: {
    include: patternTypes,
    exclude: patternTypes,
  },

  created() {
    /* 缓存对象 */
    this.cache = Object.create(null); //创建缓存对象;
    this.keys=[]
  },

  /* destroyed钩子中销毁所有cache中的组件实例和key */
  destroyed() {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache[key]);
    }
  },

  watch: {
    /* 监视include以及exclude属性,在被修改的时候对cache进行修正,进行缓存处理 */
    include(val: string | RegExp) {
      pruneCache(this.cache, this._vnode, (name) => matches(val, name));
    },
    exclude(val: string | RegExp) {
      pruneCache(this.cache, this._vnode, (name) => !matches(val, name));
    },
  },

  render() {
    /* 得到slot插槽中的第一个组件 */
    const vnode: VNode = getFirstComponentChild(this.$slots.default);

    const componentOptions: ?VNodeComponentOptions =
      vnode && vnode.componentOptions;
    if (componentOptions) {
      // check pattern
      /* 获取组件名称,优先获取组件的name字段,否则是组件的tag */
      const name: ?string = getComponentName(componentOptions);
      /* name不在inlcude中或者在exlude中则直接返回vnode(没有取缓存) */
      if (
        name &&
        ((this.include && !matches(this.include, name)) ||
          (this.exclude && matches(this.exclude, name)))
      ) {
        return vnode;
      }
      const key: ?string =
        vnode.key == null
          ? // same constructor may get registered as different local components
            // so cid alone is not enough (#3269)
            componentOptions.Ctor.cid +
            (componentOptions.tag ? `::${componentOptions.tag}` : "")
          : vnode.key;
      /* 如果已经做过缓存了则直接从缓存中获取组件实例给vnode,还未缓存过则进行缓存 */
      if (this.cache[key]) {
        vnode.componentInstance = this.cache[key].componentInstance;
        // remove(keys,key) [a,b,c,d] => [a,c,b] => [b,c,d]   
      } else {
        this.cache[key] = vnode;
      }
      //将组件的keepAlive属性设置为true
      vnode.data.keepAlive=true//判断是否执行组件的created、mounted生命周期函数
    }
    return vnode;
  },
};
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

首次渲染 组件的首次渲染:判断组件的abstract属性,才往父组件里面挂载DOM

    const options=vm.$options
    let parent=options.parent
    if(parent && !options.abstract){//判断组件的abstract属性,才往父组件里面挂载DOM
        while(parent.$options.abstract && parent.$parent){
            parent=parent.$parent
        }
        parent.$children.push(vm)
    }
    vm.$parent=parent
    vm.$root=parent?parent:$root:vm
    vm.$children=[]
    vm.$refs={}
    vm._watcher=null
    vm._inactive=null
    vm._directInactive=false
    vm._isMounted=false
    vm._isDestroyed=false
    vm._isBeingDestoryed=false
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

判断当前keepAlive和componentInstance是否存在来判断是否要执行组件perpatch还是执行创建componentInstance

init(vnode:VNodeWithData,hydrating:boolean):?boolean{
 if(vnode.componentInstance && !vnode.componentInstance.__isDestroyed && vnode.data.keepAlive){//首次渲染 vnode.componentInstance为undefined
        const mounteNode:any=vnode
        componentVNodeHooks.prepatch(mountedNode,mountedNode)//prepatch函数执行的是组件更新的过程
    }else{
        const child=vnode.componentInstance=createComponentInstanceForVnode(vnode,activeInstance)
    }
    child.$mount(hydrating?vode.elm:undefined,hydrating)
}
1
2
3
4
5
6
7
8
9

prepatch操作就不会在执行组件的mounted和created声明周期函数,而是直接将DOM插入;

# LRU缓存策略

LRU缓存策略:从内存找出最久未使用的数据并置换新的数据 LRU(least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是:如果数据最近被访问过,那么将来被访问的几率也更高。最常见的实现是使用一个链表保存缓存数据,

详细算法实现如下

  1. 新数据插入到链表头部
  2. 每当缓存数据被访问,则将数据移到链表头部
  3. 链表满的时候,将链表尾部的数据丢弃
# 使用
<template>
  <div class="app" id="app">
    <keep-alive>
      <router-view v-if="$route.meta.keepAlive"></router-view>
    </keep-alive>
    <router-view v-if="!$route.meta.keepAlive"></router-view>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

<style lang="less"></style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

router.js

{
  path: '/',
  name: 'xxx',
  component: ()=>import('../src/views/xxx.vue'),
  meta:{
    keepAlive: true // 需要被缓存
  }
},
1
2
3
4
5
6
7
8

# 如何保存页面的当前的状态

既然是要保持页面的状态(其实也就是组件的状态),那么会出现以下两种情况:

  • 前组件会被卸载
  • 前组件不会被卸载

那么可以按照这两种情况分别得到以下方法:

组件会被卸载:

  • (1)将状态存储在LocalStorage / SessionStorage;
  • (2)vuex及存储插件,vuex-persist插件;

组件不会被卸载:

(1)单页面渲染 要切换的组件作为子组件全屏渲染,父组件中正常储存页面状态。keepAlive;

# 组件间传值通讯

# 方式总括
  • props和$emit :父组件向子组件传递数据是通过prop传递的,子组件传递数据给父组件是通过$emit触发事件来做到的;
  • $parent,$children: 获取当前组件的父组件和当前组件的子组件;
  • $refs 获取实例的方式调用组件的属性或者方法;
  • 父组件中通过provide来提供变量,然后在子组件中通过inject来注入变量;
  • $attrs$listeners :A->B->C。Vue 2.4 开始提供了$attrs$listeners来解决这个问题;
  • envetBus 平级组件数据传递;这种情况下可以使用中央事件总线的方式;
  • vuex状态管理;
  • 使用浏览器本地缓存,例如localStorage

常见使用场景可以分为三类:

  • 父子通信:props / $emit;$parent / $children/ ref;$attrs/$listeners;provide / inject API;

    父->子props,子->父 $on、$emit

    获取父子组件实例 $parent、$children

    Ref 获取实例的方式调用组件的属性或者方法

    Provide、inject 官方不推荐使用,但是写组件库时很常用

  • 兄弟通信:eventBus; Vuex

  • 跨级通信$emit / $on,Vuex;$attrs/$listeners;provide / inject API

# props / $emit

父组件 A 通过 props 参数向子组件 B 传递数据,B 组件通过 $emit 向 A 组件发送一个事件(携带参数数据),A组件中监听 $emit 触发的事件得到 B 向 A 发送的数据。

  • 父组件向子组件传值 (props)
  • 子组件向父组件传值(通过事件方式this.$emit)
# 示范
// 父组件
<template>
  <div>
    <span>{{ desc }}</span>
    <child1 :msg="msg" @getEmit="getEmit"></child1>
  </div>
</template>
<script>
import child1 from "@/components/child1";
export default {
  components: { child1 },
  data() {
    return {
      msg: "我是父组件的msg",
      desc:"default"
    };
  },
  methods: {
    getEmit(data) {
      this.desc = data;// 这里的得到了子组件的值
    }
  }
};
</script>
// 子组件 => child1
<template>
  <div>
    <span>我是父组件传来的{{ msg }}</span>
    <button @click="getEmit"></button>
  </div>
</template>
<script>
export default {
  props: {
    msg: String
  },
  data() {
    return {
      //msg: "我是父组件的msg",
      childMsg:"我是child1的msg"
    };
  },
  methods: {
    getEmit(data) {
      this.$emit("getEmit"this.childMsg)
    }
  }
};
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

# props怎么自定义验证

props: {
    num: {
      default: 1,
      validator: function (value) {
          // 返回值为false则验证不通过,报错
          return [
            1, 2, 3, 4, 5
          ].indexOf(value) !== -1
    }
    }
  }
1
2
3
4
5
6
7
8
9
10
11
# 原理
export function initState(vm: Component) {
  vm._watchers = [];
  const opts = vm.$options;
  /*初始化props*/
  if (opts.props) initProps(vm, opts.props);
 }
/*初始化props*/
function initProps(vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {};
  const props = (vm._props = {});
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  /*缓存属性的key,使得将来能直接使用数组的索引值来更新props来替代动态地枚举对象*/
  const keys = (vm.$options._propKeys = []);
  /*根据$parent是否存在来判断当前是否是根结点*/
  const isRoot = !vm.$parent;
  // root instance props should be converted
  /*根结点会给shouldConvert赋true,根结点的props应该被转换*/
  observerState.shouldConvert = isRoot;
  for (const key in propsOptions) {
    /*props的key值存入keys(_propKeys)中*/
    keys.push(key);
    /*验证prop,不存在用默认值替换,类型为bool则声称true或false,当使用default中的默认值的时候会将默认值的副本进行observe*/
    const value = validateProp(key, propsOptions, propsData, vm);
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== "production") {
      /*判断是否是保留字段,如果是则发出warning*/
      if (isReservedProp[key] || config.isReservedAttr(key)) {
        warn(
          `"${key}" is a reserved attribute and cannot be used as component prop.`,
          vm
        );
      }
      defineReactive(props, key, value, () => {
        /*
          由于父组件重新渲染的时候会重写prop的值,所以应该直接使用prop来作为一个data或者计算属性的依赖
          https://cn.vuejs.org/v2/guide/components.html#字面量语法-vs-动态语法
        */
        if (vm.$parent && !observerState.isSettingProps) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
              `overwritten whenever the parent component re-renders. ` +
              `Instead, use a data or computed property based on the prop's ` +
              `value. Prop being mutated: "${key}"`,
            vm
          );
        }
      });
    } else {
      defineReactive(props, key, value);
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    /*Vue.extend()期间,静态prop已经在组件原型上代理了,我们只需要在这里进行代理prop*/
    if (!(key in vm)) {
      proxy(vm, `_props`, key);
    }
  }
  observerState.shouldConvert = true;
}
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
# $parent / $children$refs

父子组件通信

  • $parent / $children:访问父 / 子实例
  • $children:当前实例的子组件,它返回的是一个子组件的集合。如果想获取哪个组件属性和方法,可以通过 this.$children[index].子组件属性/f方法
  • ref:获取DOM 元素 和 组件实例来获取组件的属性和方法。
    • 如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;
    • 如果用在子组件上,引用就指向组件实例;

这两种方式都是直接得到组件实例,使用后可以直接调用组件的方法或访问数据。

# 示例
// 父组件
<template>
<div>
  <child1 ref="sendMsg"></child1>
  </div>
</template>
<script>
  import child1 from "@/components/child1";
  export default {
    components: { child1 },
    data() {
      return {
        msg: "我是父组件的msg"};
    },
    mounted() {
      this.refs.sendMsg.handleSendToChild1(this.msg)
    },
    methods: {
      handle() {
        //this.$children是个数组存储所有的子组件
        this.$children[0].flag = !this.$children[0].flag;
        //通过 $children  来获取 子组件的属性和方法
        this.$children[0].getValue(); // 我是 Test1
        this.$children[1].getTest2();  //我是 Test2
        this.$children[1].datas}; //我是Test2
      //可通过给子组件标签加ref快速找到目标子组件
      //<childrenbtn ref='button'></childrenbtn> //this.$refs['button']
      this.$refs.button.flag = !this.$refs.button.flag;
    }
  }
  };
</script>

// 子组件 => child1
<template>
<div>
  <span>{{ msg }} </span>
  </div>
</template>
<script>
  export default {
    data() {
      return {
        msg: "",
      };
    },
    //子组件 通过$parent来获取父组件实例的属性和方法
    created(){
      console.log( this.$parent.obj ) 
      this.$parent.getQuery()
    },
    methods: {
      handleSendToChild1(data) {
        this.msg = data;
      }   
    },
  };
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
# ref/refs

官方文档描述小结:

  • $ref:注册引用信息属性,用来给元素或子组件注册引用信息。// 目前没有用到
  • $refs:一个对象,持有已经注册过ref的所有子组件。

通俗理解:$ref:注册引用信息属性;$refs:注册引用信息对象

注意:refs只会在组件渲染完成之后生效,并且它们不是响应式的。这只意味着一个直接的子组件封装的“逃生舱”——你应该避免在模板或计算属性中访问 $refs

(1)普通DOM元素用法;

新版做法:用ref绑定之后,不需要在获取dom节点了,直接在上面的input上绑定ref , 然后通过ref,然后通过ref,然后通过refs.$ref即可调用,这样就可以减少获取dom节点的消耗

<!-- html代码 -->
<div id="arry">
  <!-- 1.注册引用 -->
  <div ref="one">蜡笔小新</div>
  <button @click="change">点击加背景</button>
</div>
<script>
  var arry= new Vue({
    el:"#arry",
    data:{},
    methods:{
      change(){
        //console.log(this)
        // 2.利用refs找到此元素
        this.$refs.one.style.backgroundColor='blue'
      }
    }
  })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

(2)组件component用法;

案例:利用组件,实现计数器count效果;

<!-- html代码 -->
<div id="arry">
  <!-- 1.注册引用 -->
  <my-count ref="one" @pass-parent="changeparent"></my-count>
  <my-count ref="two" @pass-parent="changeparent"></my-count>
  {{zong}}
</div>
<script>
  Vue.component('my-count', {
    data() {
      return {
        childCount: 0
      }
    },
    template: `<button @click=addCount>{{childCount}}</button>`,
    methods: {
      addCount() {
        this.childCount++;
        this.$emit('pass-parent')
      }
    }
  })
  var arry = new Vue({
    el: "#arry",
    data: {
      samy: 0
    },
    methods: {
      changeparent() {
        console.log(this.$refs.one.childCount)
        console.log(this.$refs.two.childCount) //this.$refs['current']
        this.samy = this.$refs.one.childCount + this.$refs.two.childCount
      }
    }
  })
</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
33
34
35
36
# 原理
/*初始化生命周期*/
export function initLifecycle(vm: Component) {
  const options = vm.$options;

  // locate first non-abstract parent
  /* 将vm对象存储到parent组件中(保证parent组件是非抽象组件,比如keep-alive) */
  let parent = options.parent;
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent;
    }
    parent.$children.push(vm);
  }

  vm.$parent = parent;
  vm.$root = parent ? parent.$root : vm;

  vm.$children = [];
  vm.$refs = {};

  vm._watcher = null;
  vm._inactive = null;
  vm._directInactive = false;
  vm._isMounted = false;
  vm._isDestroyed = false;
  vm._isBeingDestroyed = 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
  prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions;
    const child = (vnode.componentInstance = oldVnode.componentInstance);
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    );
  },
1
2
3
4
5
6
7
8
9
10
11
# 原理ref/refs

真实DOM或者组件实例挂载在当前实例的$refs属性上

export default {
  create(_: any, vnode: VNodeWithData) {
    registerRef(vnode);
  },
  update(oldVnode: VNodeWithData, vnode: VNodeWithData) {
    if (oldVnode.data.ref !== vnode.data.ref) {
      registerRef(oldVnode, true);
      registerRef(vnode);
    }
  },
  destroy(vnode: VNodeWithData) {
    registerRef(vnode, true);
  },
};
/*注册一个ref(即在$refs中添加或者删除对应的Dom实例),isRemoval代表是增加还是移除,*/
export function registerRef(vnode: VNodeWithData, isRemoval: ?boolean) {
  const key = vnode.data.ref;
  if (!key) return;

  const vm = vnode.context;
  const ref = vnode.componentInstance || vnode.elm;
  const refs = vm.$refs;
  if (isRemoval) {
    /*移除一个ref*/
    if (Array.isArray(refs[key])) {
      remove(refs[key], ref);
    } else if (refs[key] === ref) {
      refs[key] = undefined;
    }
  } else {
    /*增加一个ref*/
    if (vnode.data.refInFor) {
      /*如果是在一个for循环中,则refs中key对应的是一个数组,里面存放了所有ref指向的Dom实例*/
      if (Array.isArray(refs[key]) && refs[key].indexOf(ref) < 0) {
        refs[key].push(ref);
      } else {
        refs[key] = [ref];
      }
    } else {
      /*不在一个for循环中则直接放入refs即可,ref指向Dom实例*/
      refs[key] = ref;
    }
  }
}
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
  /*初始化组件*/
  function initComponent(vnode, insertedVnodeQueue) {
    /*把之前已经存在的VNode队列合并进去*/
    if (isDef(vnode.data.pendingInsert)) {
      insertedVnodeQueue.push.apply(
        insertedVnodeQueue,
        vnode.data.pendingInsert
      );
    }
    vnode.elm = vnode.componentInstance.$el;
    if (isPatchable(vnode)) {
      /*调用create钩子*/
      invokeCreateHooks(vnode, insertedVnodeQueue);
      /*为scoped CSS 设置scoped id*/
      setScope(vnode);
    } else {
      // empty component root.
      // skip all element-related modules except for ref (#3455)
      /*注册ref*/
      registerRef(vnode);
      // make sure to invoke the insert hook
      insertedVnodeQueue.push(vnode);
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# $attrs / $listeners

父 => 子 => 孙组件通信

$attrs$listeners获取父组件实例属性和方法(组件嵌套情况下使用); 两者结合使用做到透明组件功能;

其中子组件只做数据传递 多级组件嵌套需要传递数据时,如果仅仅是传递数据,而不做中间处理,可以使用这种方法;

  • $attrs:没有被子组件非属性prop;
  • $listeners:子组件可以触发父组件的 非.native事件;

分析

  1. 使用 Vuex 来进行数据管理,但是使用的 vuex 的问题在于,如果项目比较小,组件间的共享状态比较少,那用 vuex 就好比杀鸡用牛刀
  2. 利用 B 组件做中转站,当 A 组件需要把信息传给 C 组件时,B 接受 A 组件的信息,然后用 props 传给 C 组件, 但是如果嵌套的组件过多,会导致代码繁琐,代码维护比较困难;如果 C 中状态的改变需要传递给 A, 还要使用事件系统一级级往上传递 。

在 Vue2.4 中,为了解决该需求,引入了attrs 和listeners , 新增了 inheritAttrs 选项; ($attrs 的作用,某些情况下需要结合 inheritAttrs 一起使用)

  • $attrs:包含了父作用域中不被认为 (且不预期为) props 的特性绑定 (class 和 style 除外),并且可以通过 v-bind=”$attrs” 传入内部组件。当一个组件没有声明任何 props 时,它包含所有父作用域的绑定 (class 和 style 除外)。
  • $listeners:包含了父作用域中的 (不含 .native 修饰符) v-on 事件监听器。它可以通过 v-on=”$listeners” 传入内部组件。它是一个对象,里面包含了作用在这个组件上的所有事件监听器,相当于子组件继承了父组件的事件。
# 示范

多层嵌套组件的情况下使用,可以避免使用Vuex来做数据处理, **使用v-on="$listeners" v-bind="$attrs"**很方便达到业务数据传递。

// 父组件
<template>
  <div>
    <span>{{ desc }}</span>
    <child1 :msg="msg" @getEmit="getEmit" v-on="$listeners"></child1>
  </div>
</template>
<script>
import child1 from "@/components/child1";
export default {
  components: { child1 },
  data() {
    return {
      msg: "我是父组件的msg",
      desc:""
    };
  },
  methods: {
    getEmit(data) {
      this.desc = data;
    }
  }
};
</script>

// 子组件child1 => 相对于child2组件,该组件为兄弟组件
<template>
  <div>
    <child2 v-bind="$attrs"></child2>
  </div>
</template>
<script>
import child2 from "@/components/child2
export default {
 // inheritAttrs:false,不会把未被注册的 prop 呈现为普通的 HTML 属性 ,也就是:prop="data" prop不会作为html的新属性
  inheritAttrs: false,
  components: { child2 },
  data() {
    return {
      msg: "我是父组件的msg",
      childMsg:"我是child1的msg"
    };
  },
  mounted(){
      this.$emit("getEmit",this.childMsg);
  }
};  
  
// 子组件child2 => 相对比child1组件,该组件为兄弟组件
<template>
  <div>
    <span>{{ msg }} {{ $attrs.msg }}</span>
  </div>
</template>
<script>
export default {
  data() {
    return {
      msg: "该组件拿到父组件通过child1组件传递过来的值 => ",
    };
  },
};
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

而$listeners: 官方文档说的是:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用;示范【推荐】

<template>
  <div class="child1">
    <child2 v-on:upRocket="reciveRocket"></child2>
  </div>
</template>
<script>
const child2 = () => import("./child2.vue");
export default {
  components: {
    child2
  },
  methods: {
    reciveRocket() {
      console.log("reciveRocket success");
    }
  }
};
</script>

// child2.vue
<template>
  <div class="child2">
    <child3 v-bind="$attrs" v-on="$listeners"></child3>
  </div>
</template>
<script>
const child3 = () => import("./child3.vue");
export default {
  components: {
    child3
  },
  created() {
    this.$emit('child2', 'child2-data');
  }
};
</script>

// child3.vue
<template>
  <div class="child3">
    <p @click="startUpRocket">child3</p>
  </div>
</template>
<script>
export default {
  methods: {
    startUpRocket() {
      this.$emit("upRocket");
      console.log("startUpRocket");
    }
  }
};
</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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

这里的结果是,当我们点击 child3 组件的 child3 文字,触发 startUpRocket 事件,child1 组件就可以接收到,并触发 reciveRocket 打印结果如下:

> reciveRocket success
> startUpRocket
1
2
# 原理
export function updateChildComponent (
  vm: Component,
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode,
  renderChildren: ?Array<VNode>
) {
  if (process.env.NODE_ENV !== 'production') {
    isUpdatingChildComponent = true
  }

  // determine whether component has slot children
  // we need to do this before overwriting $options._renderChildren.

  // check if there are dynamic scopedSlots (hand-written or compiled but with
  // dynamic slot names). Static scoped slots compiled from template has the
  // "$stable" marker.
  const newScopedSlots = parentVnode.data.scopedSlots
  const oldScopedSlots = vm.$scopedSlots
  const hasDynamicScopedSlot = !!(
    (newScopedSlots && !newScopedSlots.$stable) ||
    (oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) ||
    (newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key)
  )

  // Any static slot children from the parent may have changed during parent's
  // update. Dynamic scoped slots may also have changed. In such cases, a forced
  // update is necessary to ensure correctness.
  const needsForceUpdate = !!(
    renderChildren ||               // has new static slots
    vm.$options._renderChildren ||  // has old static slots
    hasDynamicScopedSlot
  )

  vm.$options._parentVnode = parentVnode
  vm.$vnode = parentVnode // update vm's placeholder node without re-render

  if (vm._vnode) { // update child tree's parent
    vm._vnode.parent = parentVnode
  }
  vm.$options._renderChildren = renderChildren

  // update $attrs and $listeners hash
  // these are also reactive so they may trigger child update if the child
  // used them during render
  vm.$attrs = parentVnode.data.attrs || emptyObject // 一直查找
  vm.$listeners = listeners || emptyObject
  //<child3 v-bind="$attrs" v-on="$listeners"></child3>

  // update props
  if (propsData && vm.$options.props) {
    toggleObserving(false)
    const props = vm._props
    const propKeys = vm.$options._propKeys || []
    for (let i = 0; i < propKeys.length; i++) {
      const key = propKeys[i]
      const propOptions: any = vm.$options.props // wtf flow?
      props[key] = validateProp(key, propOptions, propsData, vm)
    }
    toggleObserving(true)
    // keep a copy of raw propsData
    vm.$options.propsData = propsData
  }

  // update listeners
  listeners = listeners || emptyObject
  const oldListeners = vm.$options._parentListeners
  vm.$options._parentListeners = listeners
  updateComponentListeners(vm, listeners, oldListeners)

  // resolve slots + force update if has children
  if (needsForceUpdate) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context)
    vm.$forceUpdate()
  }

  if (process.env.NODE_ENV !== 'production') {
    isUpdatingChildComponent = 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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# provide / inject

父 => 子、父 => 孙组件通信

provide/inject 是 Vue2.2.0 新增 API,这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。

provideinject 这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。

provide :

  • 是一个对象或返回一个对象的函数;
  • 该对象包含可注入其子孙的属性。

inject :

  • 是一个字符串数组 或者是一个对象;
  • 用来在子组件或者子孙组件中注入 provide 提供的父组件属性。
# 示范

provide 和 inject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。

provide/inject可以轻松实现跨级访问父组件的数据;

# $attrs是为了解决什么问题出现的?应用场景有哪些?provide/inject 不能解决它能解决的问题吗?

$attrs主要的作用就是实现批量传递数据。provide/inject更适合应用在插件中,主要是实现跨级数据传递

// 父组件
<template>
  <div>
    <span>{{ desc }}</span>
    <child1 :msg="msg"></child1>
  </div>
</template>
<script>
import child1 from "@/components/child1";
export default {
  provide () {
    return {
     desc:this.desc //这里不接收动态传参,但是以直接传this过去,实现动态响应式
     titleFather: '父组件的值'
     reload: this.reload,
    }
  },
  components: { child1 },
  data() {
    return {
      msg: "我是父组件的msg",
      desc:"我是父组件的desc"
    };
  },
  methods: {
    reload () {
      this.isRouterAlive = false
      this.$nextTick(function () {
        this.isRouterAlive = true
      })
    }
  }
};
</script>

// child1部分代码上面已经赘述过多,此处省略
// 孙组件 =>  grandson1
<template>
  <div>
    <span>{{ desc }}</span>
  </div>
</template>
<script>
export default {
  inject:['desc','titleFather'],
  created(){
    console.log(`${this.titleFather}----`)
  },
};
</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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 原理

就是一个响应式的数据;defineReactive

src/core/instance/init.js:74src/core/instance/inject.js:7

initInjections(vm) // resolve injections before data/props
/*初始化props、methods、data、computed与watch*/
initState(vm)
initProvide(vm) // resolve provide after data/props
1
2
3
4
export function initProvide(vm: Component) {
  const provide = vm.$options.provide;
  if (provide) {
    vm._provided = typeof provide === "function" ? provide.call(vm) : provide;
  }
}

export function initInjections(vm: Component) {
  const result = resolveInject(vm.$options.inject, vm);
  if (result) {
    Object.keys(result).forEach((key) => {
      /* istanbul ignore else */
      /*为对象defineProperty上在变化时通知的属性*/
      if (process.env.NODE_ENV !== "production") {
        defineReactive(vm, key, result[key], () => {
          warn(
            `Avoid mutating an injected value directly since the changes will be ` +
              `overwritten whenever the provided component re-renders. ` +
              `injection being mutated: "${key}"`,
            vm
          );
        });
      } else {
        defineReactive(vm, key, result[key]);
      }
    });
  }
}

export function resolveInject(inject: any, vm: Component): ?Object {
  if (inject) {
    // inject is :any because flow is not smart enough to figure out cached
    // isArray here
    const isArray = Array.isArray(inject);
    const result = Object.create(null);
    const keys = isArray
      ? inject
      : hasSymbol
      ? Reflect.ownKeys(inject)
      : Object.keys(inject);

    for (let i = 0; i < keys.length; i++) {
      const key = keys[i];
      const provideKey = isArray ? key : inject[key];
      let source = vm;
      while (source) {
        if (source._provided && provideKey in source._provided) {
          result[key] = source._provided[provideKey];
          break;
        }
        source = source.$parent; // 指定$parent
      }
    }
    return result;
  }
}
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
# EventBus中央事件总线

兄弟组件通信

// 准备条件可以分为两种方式
// 方式一:  npm install vue-bus / main.js中引用
import Vue from 'vue';
import VueBus from 'vue-bus';
Vue.use(VueBus);
//组件中直接使用this.$bus.on()和this.$bus.emit()来做相应的处理   
// 方式二:2.创建bus.js,该文件内写入
import Vue from "vue";
export default new Vue();
1
2
3
4
5
6
7
8
9
// 子组件 => child2 采用了第二种创建bus.js的方法,然后在该组件内引用
<template>
  <div>
    <button @click="handleSendToChild3">{{ msg }}</button>
  </div>
</template>
<script>
import Bus from "@/components/bus.js";
export default {
  data() {
    return {
      msg: "点我向child3组件传参",
      childMsg:"我是child1的msg"
    };
  },
  methods: {
    handleSendToChild3(data) {
      Bus.$emit("getData",this.childMsg)
    }
  }
};

// 子组件 => child3 采用了第二种创建bus.js的方法,然后在该组件内引用
<template>
  <div>
   <span>{{ msg }}</span>
  </div>
</template>
<script>
import Bus from "@/components/bus.js";
export default {
  data() {
    return {
      msg: "",
    };
  },
  methods: {
    handleSendToChild3(data) {// 在mounted中处理监听
      Bus.$on("getData",(data)=>{
          this.msg = data
      })
    }
  }
};
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

其他示例: 直接使用Vue.component注册组件;

// 组件 A
Vue.component('A', {
  template: `
    <div>
      <p>this is A component!</p>
      <input type="text" v-model="mymessage" @input="passData(mymessage)"> 
    </div>
  `,
  data() {
    return {
      mymessage: 'hello brother1'
    }
  },
  methods: {
    passData(val) {
      this.$EventBus.$emit('globalEvent', val)  //触发全局事件globalEvent
    }
  }
});

// 组件 B
Vue.component('B', {
  template:`
    <div>
      <p>this is B component!</p>
      <p>组件A 传递过来的数据:{{brothermessage}}</p>
    </div>
  `,
  data() {
    return {
      mymessage: 'hello brother2',
      brothermessage: ''
    }
  },
  mounted() {
    //绑定全局事件globalEvent
    this.$EventBus.$on('globalEvent', (val) => {
      this.brothermessage = val;
    });
  }
});

//定义中央事件总线
const EventBus = new Vue();
// 将中央事件总线赋值到 Vue.prototype 上,这样所有组件都能访问到了
Vue.prototype.$EventBus = EventBus;

const app = new Vue({
  el: '#app',
  template: `
    <div>
      <A />
      <B />
    </div>
  `
});
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
# $emit / $on

和EventBus类似;这种方式是通过一个类似 App.vue 的实例作为一个模块的事件中心,用它来触发和监听事件,如果把它放在 App.vue 中,就可以很好的实现任何组件中的通信,但是这种方法在项目比较大的时候不太好维护。

假设现在有 4 个组件,Home.vue 和 A/B/C 组件,AB 这三个组件是兄弟组件,Home.vue 相当于父组件 建立一个空的 Vue 实例,将通信事件挂载在该实例上 -

# 示范
//D.js
import Vue from 'vue'
export default new Vue()

// 我们可以在router-view中监听change事件,也可以在mounted方法中监听
// home.vue
<template>
  <div>
    <child-a />
    <child-b />
    <child-c />
  </div>
</template>

// A组件
<template>
  <p @click="dataA">将A组件的数据发送给C组件 - {{name}}</p>
</template>
<script>
import Event from "./D";
export default {
  data() {
    return {
      name: 'Echo'
    }
  },
  components: { Event },
  methods: {
    dataA() {
      Event.$emit('data-a', this.name);
    }
  }
}
</script>

// B组件
<template>
  <p @click="dataB">将B组件的数据发送给C组件 - {{age}}</p>
</template>
<script>
import Event from "./D";
export default {
  data() {
    return {
      age: '18'
    }
  },
  components: { Event },
  methods: {
    dataB() {
      Event.$emit('data-b', this.age);
    }
  }
}
</script>

// C组件
<template>
  <p>C组件得到的数据 {{name}} {{age}}</p>
</template>
<script>
import Event from "./D";
export default {
  data() {
    return {
      name: '',
      age: ''
    }
  },
  components: { Event },
  //在 C 组件的 mounted 事件中监听了 A/B 的 $emit 事件,并获取了它传递过来的参数(由于不确定事件什么时候触发,所以一般在 mounted / created 中监听)
  mounted() {
    Event.$on('data-a', name => {
      this.name = name;
    })
    Event.$on('data-b', age => {
      this.age = age;
    })
  }
}
</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
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
# 原理
/*初始化事件*/
export function initEvents(vm: Component) {
  /*在vm上创建一个_events对象,用来存放事件。*/
  vm._events = Object.create(null);
  /*这个bool标志位来表明是否存在钩子,而不需要通过哈希表的方法来查找是否有钩子,这样做可以减少不必要的开销,优化性能。*/
  vm._hasHookEvent = false;
  // init parent attached events
  /*初始化父组件attach的事件*/
  const listeners = vm.$options._parentListeners;
  if (listeners) {
    updateComponentListeners(vm, listeners);
  }
}
/*更新组件的监听事件*/
export function updateComponentListeners(
  vm: Component,
  listeners: Object,
  oldListeners: ?Object
) {
  target = vm;
  updateListeners(listeners, oldListeners || {}, add, remove, vm);
}

/*为Vue原型加入操作事件的方法*/
export function eventsMixin(Vue: Class<Component>) {}
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
# $boradcast$dispatch

这也是一对成对出现的方法,不过只是在 Vue1.0 中提供了,而 Vue2.0 被废弃了,但是还是有很多开源软件都自己封装了这种组件通信的方式,比如 Mint UI (opens new window)Element UI (opens new window)iView (opens new window) 等。

// broadcast 方法的主逻辑处理方法
function broadcast(componentName, eventName, params) {
    this.$children.forEach(child => {
        const name = child.$options.componentName;
        if (name === componentName) {
            child.$emit.apply(child, [eventName].concat(params));
        } else {
            broadcast.apply(child, [componentName, eventName].concat(params));
        }
    });
}

export default {
    methods: {
        // 定义 dispatch 方法
        dispatch(componentName, eventName, params) {
            let parent = this.$parent;
            let name = parent.$options.componentName;
            while (parent && (!name || name !== componentName)) {
                parent = parent.$parent;
                if (parent) {
                    name = parent.$options.componentName;
                }
            }
            if (parent) {
                parent.$emit.apply(parent, [eventName].concat(params));
            }
        },
        // 定义 broadcast 方法
        broadcast(componentName, eventName, params) {
            broadcast.call(this, componentName, eventName, params);
        }
    }
};
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

上面所示的代码,一般都作为一个 mixins 去混入使用, broadcast 是向特定的父组件触发事件,dispatch 是向特定的子组件触发事件,本质上这种方式还是 onemit 的封装,在一些基础组件中都很实用。

# Vuex状态管理

Vuex 是一个状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 应用的核心是 store(仓库,一个容器),store 包含着应用中大部分的状态 (state);

Vuex 是一个专门为Vue.js开发状态管理软件。 Vuex中有几个核心的概念

  • State 存储状态,类似于data属性
  • Getter 从state中派生出一些属性,类似于computed
  • Mutation 更改state的属性,state属性是无法直接通过赋值进行更改的,需要通过Mutation定义的函数来进行赋值,提交的是state
  • Actions Mutation必须是同步的函数,所以在Actions中处理异步的操作,并且提交的是Mutation`
  • Module 当项目很大的时候,需要根据不同的功能分割成不同的模块,每次需要的时候,只需要引用需要的模块即可

img

# slot插槽

slot又名插槽,是Vue的内容分发机制,组件内部的模板引擎使用slot元素作为承载分发内容的出口。插槽slot是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot又分三类,默认插槽,具名插槽和作用域插槽。

# 普通插槽

普通插槽(模板传入到组件中,数据采用父组件数据)

  • 创建组件虚拟节点时,会将组件的儿子的虚拟节点保存起来。当初始化组件时,通过插槽属性将儿子进行分类;{a:[vnode],b:[vnode]};
  • 渲染组件时会拿到对应的slot属性的节点进行替换操作;【插槽的作用域为父组件
# 作用域插槽

作用域插槽(在父组件中访问子组件数据)

  • 作用域插槽在解析的时候,不会作为组件的孩子节点;
  • 会解析成函数,当子组件渲染时,会调用此函数进行渲染;【插槽的作用域为子组件

普通插槽和作用域插槽的区别:

  • 普通插槽是在父组件编译和渲染阶段生成 vnodes,所以数据的作用域是父组件实例,子组件渲染的时候直接拿到这些渲染好的 vnodes
  • 对于作用域插槽,父组件在编译和渲染阶段并不会直接生成 vnodes,而是在父节点 vnode 的 data 中保留一个 scopedSlots 对象,存储着不同名称的插槽以及它们对应的渲染函数,只有在编译和渲染子组件阶段才会执行这个渲染函数生成 vnodes,由于是在子组件环境执行的,所以对应的数据作用域是子组件实例
  • 简单地说,两种插槽的目的都是让子组件 slot 占位符生成的内容由父组件来决定,但数据的作用域会根据它们 vnodes 渲染时机不同而不同
# 编译分析

const children = $scopedSlots.default ? $scopedSlots.default() : $slots.default;

普通插槽

image-20210302003117080

image-20210302003224946

作用域插槽

image-20210302003359881

image-20210302003426353

# 示范

实现了内容分发,提高了组件自定义的程度,让组件变的更加灵活

默认插槽:

无需name属性,取子组件肚子里第一个元素节点作为默认插槽。

<!-- 子组件,组件名:child-component -->
<div class="child-page">
    <h1>子页面</h1>
    <slot></slot> <!-- 替换为 <p>hello,world!</p> -->
</div>

<!-- 父组件 -->
<div class="parent-page">
    <child-component>
        <p>hello,world!</p>
    </child-component>
</div>

<!-- 渲染结果 -->
<div class="parent-page">
    <div class="child-page">
        <h1>子页面</h1>
        <p>hello,world!</p>
    </div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

具名插槽:【default】

在多个插槽的情况下使用,利用name标识插槽。

<!-- 子组件,组件名:child-component -->
<div class="child-page">
    <h1>子页面</h1>
    <slot name="header"></slot>
    <slot></slot>  <!-- 等价于 <slot name="default"></slot> -->
    <slot name="footer"></slot>
</div>

<!-- 父组件 -->
<div class="parent-page">
    <child-component>
        <template v-slot:header>
          <p>头部</p>  
        </template>
        <template v-slot:footer>
          <p>脚部</p>
        </template>
        <p>身体</p>
    </child-component>
</div>

<!-- 渲染结果 -->
<div class="parent-page">
    <div class="child-page">
        <h1>子页面</h1>
        <p>头部</p>
        <p>身体</p>
        <p>脚部</p>
    </div>
</div>
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

作用域插槽:

子组件给父组件传递数据。

<!-- 子组件,组件名:child-component -->
<div class="child-page">
    <h1>子页面</h1>
    <slot name="header" data="data from child-component."></slot>
</div>

<!-- 父组件 -->
<div class="parent-page">
    <child-component>
        <template v-slot:header="slotProps">
          <p>头部: {{ slotProps.data }}</p>
        </template>
    </child-component>
</div>

<!-- 渲染结果 -->
<div class="parent-page">
    <div class="child-page">
        <h1>子页面</h1>
        <p>头部: data from child-component.</p>
    </div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 异步更新及组件

如果组件功能多打包出的结果会变大,可以采用异步的方式来加载组件。主要依赖import()这个语法,可以实现文件的分割加载;

component: () => import('@/views/login'),  //require([])
1
# 原理
/*创建一个组件节点,返回Vnode节点*/
export function createComponent(
  Ctor: Class<Component> | Function | Object | void,
  data?: VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | void {
  /*没有传组件构造类直接返回*/
  if (isUndef(Ctor)) {
    return;
  }

  /*_base存放了Vue,作为基类,可以在里面添加扩展*/
  const baseCtor = context.$options._base;

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor);
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  /*如果在该阶段Ctor依然不是一个构造函数或者是一个异步组件工厂则直接返回*/
  if (typeof Ctor !== "function") {
    if (process.env.NODE_ENV !== "production") {
      warn(`Invalid Component definition: ${String(Ctor)}`, context);
    }
    return;
  }

  // async component
  /*处理异步组件*/
  if (isUndef(Ctor.cid)) {
    Ctor = resolveAsyncComponent(Ctor, baseCtor, context);
    if (Ctor === undefined) {
      // return nothing if this is indeed an async component
      // wait for the callback to trigger parent update.
      /*如果这是一个异步组件则会不会返回任何东西(undifiened),直接return掉,等待回调函数去触发父组件更新。s*/
      return;
    }
  }

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor);

  data = data || {};

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data);
  }

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag);

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children);
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on;
  // replace with listeners with .native modifier
  data.on = data.nativeOn;

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners
    data = {};
  }

  // merge component management hooks onto the placeholder node
  mergeHooks(data);

  // return a placeholder vnode
  const name = Ctor.options.name || tag;
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ""}`,
    data,
    undefined,
    undefined,
    undefined,
    context,
    { Ctor, propsData, listeners, tag, children }
  );
  return vnode;
}
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
export function resolveAsyncComponent(
  factory: Function,
  baseCtor: Class<Component>,
  context: Component
): Class<Component> | void {
  /*出错组件工厂返回出错组件*/
  if (isTrue(factory.error) && isDef(factory.errorComp)) {
    return factory.errorComp;
  }

  /*resoved时候返回resolved组件*/
  if (isDef(factory.resolved)) {
    return factory.resolved;// 再次渲染时可以拿到获取最新的组件
  }

  /*加载组件*/
  if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
    return factory.loadingComp;
  }

  if (isDef(factory.contexts)) {
    // already pending
    factory.contexts.push(context);
  } else {
    const contexts = (factory.contexts = [context]);
    let sync = true;

    const forceRender = () => {
      for (let i = 0, l = contexts.length; i < l; i++) {
        contexts[i].$forceUpdate();
      }
    };

    const resolve = once((res: Object | Class<Component>) => {
      // cache resolved
      factory.resolved = ensureCtor(res, baseCtor);
      // invoke callbacks only if this is not a synchronous resolve
      // (async resolves are shimmed as synchronous during SSR)
      if (!sync) {
        forceRender();
      }
    });

    const reject = once((reason) => {
      process.env.NODE_ENV !== "production" &&
        warn(
          `Failed to resolve async component: ${String(factory)}` +
            (reason ? `\nReason: ${reason}` : "")
        );
      if (isDef(factory.errorComp)) {
        factory.error = true;
        forceRender(); // 强制更新视图,重新渲染
      }
    });

    const res = factory(resolve, reject);

    if (isObject(res)) {
      if (typeof res.then === "function") {
        // () => Promise
        if (isUndef(factory.resolved)) {
          res.then(resolve, reject);
        }
      } else if (
        isDef(res.component) &&
        typeof res.component.then === "function"
      ) {
        res.component.then(resolve, reject);

        if (isDef(res.error)) {
          factory.errorComp = ensureCtor(res.error, baseCtor);
        }

        if (isDef(res.loading)) {
          factory.loadingComp = ensureCtor(res.loading, baseCtor);
          if (res.delay === 0) {
            factory.loading = true;
          } else {
            setTimeout(() => {
              if (isUndef(factory.resolved) && isUndef(factory.error)) {
                factory.loading = true;
                forceRender();
              }
            }, res.delay || 200);
          }
        }

        if (isDef(res.timeout)) {
          setTimeout(() => {
            reject(
              process.env.NODE_ENV !== "production"
                ? `timeout (${res.timeout}ms)`
                : null
            );
          }, res.timeout);
        }
      }
    }
    sync = false;
    // return in case resolved synchronously
    return factory.loading ? factory.loadingComp : factory.resolved;
  }
}
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
# 示范
const AsyncComponent = () => ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  component: import('./MyComponent.vue'),
  // 异步组件加载时使用的组件
  loading: LoadingComponent,
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
})
1
2
3
4
5
6
7
8
9
10
11
12
13

# 涉及到设计模式

# 工厂模式

工厂模式 - 传入参数即可创建实例 (createElement)

根据传入的参数不同返回不同的实例;创建VNode虚拟节点;

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // ...
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    vnode = createComponent(tag, data, context, children)
  }
  // ....
}
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

# 单例模式

单例模式就是整个程序有且仅有一个实例。

export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    if (__DEV__) {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}
1
2
3
4
5
6
7
8
9
10
11
12

# 观察者模式

  • vue 响应式;watcher&dep的关系
  • DOM事件;

**“Vue 双向绑定实现”;**在 Vue 中,如何实现响应式也是使用了该模式。对于需要实现响应式的对象来说,在 get 的时候会进行依赖收集,当改变了对象的属性时,就会触发派发更新。

简单的观察者模式: (仿 Vue 实现)

class Dep {// 观察者
    constructor() {
        this.subs = []
    }
    addSub(sub) {
        this.subs.push(sub)
    }
    depend() {
        if (Dep.target) { 
            Dep.target.addDep(this);
        }
    }
    notify() {
        this.subs.forEach(sub => sub.update())
    }
}

class Watcher {// 被观察者
    constructor(vm, expOrFn) {
        this.vm = vm;
        this.getter = expOrFn;
        this.value;
    }
    get() {
        Dep.target = this;
        var vm = this.vm;
        var value = this.getter.call(vm, vm);
        return value;
    }
    evaluate() {
        this.value = this.get();
    }
    addDep(dep) {
        dep.addSub(this);
    }
    update() {
        console.log('更新, value:', this.value)
    }
}
// 观察者实例
var dep = new Dep();
//  被观察者实例
var watcher = new Watcher({x: 1}, (val) => val);
watcher.evaluate();//通过 `watcher.evaluate()` 将自身实例赋值给 `Dep.target`

// 观察者监听被观察对象
dep.depend()//调用 `dep.depend()` 将dep实例的 watcher 实例 push 到 dep.subs中
dep.notify()//通过数据劫持,在调用被劫持的对象的set方法时,调用 dep.subs 中所有的 `watcher.update()`
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

# 发布/订阅模式

是观察者模式的变体,比观察者模式多了一个调度中心

  • 发布者发布信息到调度中心;
  • 调度中心和订阅者直接完成订阅和触发事件事件;

使用场景案例:“DOM 的 addEventListener 事件”;

订阅者把自己想订阅的事件注册到调度中心,当该事件触发时候,发布者发布该事件到调度中心,由调度中心统一调度订阅者注册到调度中心的处理代码

Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn)
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn)
      if (hookRE.test(event)) {
        vm._hasHookEvent = true
      }
    }
    return vm
  }
Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    let cbs = vm._events[event]
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      const args = toArray(arguments, 1)
      const info = `event handler for "${event}"`
      for (let i = 0, l = cbs.length; i < l; i++) {
        invokeWithErrorHandling(cbs[i], vm, args, vm, info)
      }
    }
    return 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

# 代理模式

代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。

(防抖和节流) => 返回替代 (例如:Vue3中的proxy)

ES6的 Proxy 相信大家都不会陌生,Vue 3.0 的双向绑定原理就是依赖 ES6 的 Proxy 来实现,给一个简单的例子:

let star = {
    name: 'samy,
    song: '~鸡你太美~'
    age: 40,
    phone: 13089898989
}

let agent = new Proxy(star , {
    get(target , key) {
        if(key == 'phone') {
            // 返回经济人自己的电话
            return 15667096303
        }
        if(key == 'price') {
           return 20000000000
        }
        return target[key]
    },
    set(target , key , val) {
       if(key === 'customPrice') {
          if(val < 100000000) {
              throw new Error('价格太低')
          }
          else {
              target[key] = value;
              return true
          }
       }
    }
})

// agent 对象会根据相应的代理规则,执行相应的操作:
agent.phone // 15667096303  
agent.price // 20000000000 
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中的源码实现mixin;

function initMixin(Vue) {
  Vue.mixin = function (mixin) {
    // Vue.options = {created:[a,b]}
    this.options = mergeOptions(this.options, mixin);// 将属性合并到Vue.options上
    return this;
}}
1
2
3
4
5
6
// 入口:合并参数属性
// {a:1}  {a:2}  => {a:2}
// {a:1}  {}  => {a:1}
// 自定义的策略
// 1.如果父亲有的儿子也有,应该用儿子替换父亲
// 2.如果父亲有值儿子没有,用父亲的
export function mergeOptions(parent, child) {
    // 遍历父亲 ,可能是父亲有 儿子没有 
    const options = {};
    // 父亲和儿子都有
    for (let key in parent) {
        mergeField(key)
    }
    // 儿子有父亲没有
    for (let key in child) {
        //  如果已经合并过了就不需要再次合并了
        if (!parent.hasOwnProperty(key)) { //判断属性过滤
            mergeField(key);
        }
    }

    // 默认的合并策略 但是有些属性 需要有特殊的合并方式 生命周期的合并
    //合并 策略模式; 
    function mergeField(key) {
        // 根据key 不同的策略来进行合并 
        if (strats[key]) {
            return options[key] = strats[key](parent[key], child[key]);
        }
        // else {
        //     options[key] = child[key]// todo默认合并
        // }
        if (isObject(parent[key]) && isObject(child[key])) {
            options[key] = { ...parent[key], ...child[key] } // 儿子和父亲都有的话,整体合并
        } else {
            if (child[key]) {
                options[key] = child[key]; //儿子有的,用儿子的;
            } else {
                options[key] = parent[key];//儿子没有的,用父亲的;
            }
        }
    }
    return options;
}
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

# 中介者模式

中介者是一个行为设计模式,通过提供一个统一的接口让系统的不同部分进行通信。=> vuex

# 涉及到手写实现

# vm.$on

监听当前实例上的自定义事件。事件可以由 vm.$emit 触发。回调函数会接收所有传入事件触发函数的额外参数。

Vue.prototype.$on = function (event, fn) {
    const vm = this
    if (Array.isArray(event)) {
        for (let i = 0; i < event.length; i++) {
            vm.$on(event[i], fn)
        }
    } else {
        (vm._events[event] || (vm._events[event] = [])).push[fn]
    }
    return vm
}
1
2
3
4
5
6
7
8
9
10
11

温馨提示:events是实现初始化的时候定义的,this.events = Object.create(null),所以不要困惑events哪里来的。

vm.$on('test', function (msg) {
  console.log(msg)
})
vm.$emit('test', 'hi')
// => "hi"
1
2
3
4
5

# vm.$off

移除自定义事件监听器。

  • 如果没有提供参数,则移除所有的事件监听器;
  • 如果只提供了事件,则移除该事件所有的监听器;
  • 如果同时提供了事件与回调,则只移除这个回调的监听器。
Vue.prototype.$off = function (event, fn) {
    const vm = this
    if (!arguments.length) {
        vm._events = Object.create(null)
        return vm
    }
    if (Array.isArray(event)) {
        for (let i = 0; i < event.length; i++) {
            vm.$off(event[i], fn)
        }
        return vm
    }
    const cbs = vm._events[event]
    if (!cbs) return vm
    if (arguments.length === 1) {
        vm._events[event] = null
        return vm
    }
    if (fn) {
        let len = cbs.length
        while (len--) {
            let cb = cbs[len]
            if (cb === fn || cb.fn === fn) {
                cbs.splice(len, 1)
                break
            }
        }
    }
    return 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

# vm.$once

监听一个自定义事件,但是只触发一次。一旦触发之后,监听器就会被移除。

Vue.prototype.$once = function (event,fn){
    const vm = this
    function on(){
        vm.$off(event,on)
        fn.apply(vm,arguments)
    }
    on.fn = fn
    vm.$on(event,on)
    return vm
}
1
2
3
4
5
6
7
8
9
10

# vm.$emit

触发当前实例上的事件。附加参数都会传给监听器回调。

Vue.prototype.$emit = function (event, ...params) {
    const vm = this
    let cbs = vm._events[event]
    if (cbs) {
        for (let i = 0; i < cbs.length; i++) {
            cbs[i].apply(vm, params)
        }
    }
    return vm
}
1
2
3
4
5
6
7
8
9
10

# 缓存算法(FIFO/LRU)

# FIFO

最简单的一种缓存算法,设置缓存上限,当达到了缓存上限的时候,按照先进先出的策略进行淘汰,再增加进新的 k-v 。

使用了一个对象作为缓存,一个数组配合着记录添加进对象时的顺序,判断是否到达上限,若到达上限取数组中的第一个元素key,对应删除对象中的键值。

/**
 * FIFO队列算法实现缓存
 * 需要一个对象和一个数组作为辅助
 * 数组记录进入顺序
 */
class FifoCache{
    constructor(limit){
        this.limit = limit || 10
        this.map = {}
        this.keys = []
    }
    set(key,value){
        let map = this.map
        let keys = this.keys
        if (!Object.prototype.hasOwnProperty.call(map,key)) {
            if (keys.length === this.limit) {
                delete map[keys.shift()]//先进先出,删除队列第一个元素
            }
            keys.push(key)
        }
        map[key] = value//无论存在与否都对map中的key赋值
    }
    get(key){
        return this.map[key]
    }
}

module.exports = FifoCache
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
# LRU

LRU(Least recently used,最近最少使用)算法。该算法的观点是,最近被访问的数据那么它将来访问的概率就大,缓存满的时候,优先淘汰最无人问津者

算法实现思路:基于一个双链表的数据结构,在没有满员的情况下,新来的 k-v 放在链表的头部,以后每次获取缓存中的 k-v 时就将该k-v移到最前面,缓存满的时候优先淘汰末尾的

双向链表的特点,具有头尾指针,每个节点都有 prev(前驱) 和 next(后继) 指针分别指向他的前一个和后一个节点。

**关键点:**在双链表的插入过程中要注意顺序问题,一定是在保持链表不断的情况下先处理指针,最后才将原头指针指向新插入的元素,在代码的实现中请注意看我在注释中说明的顺序注意点!

/**
 * LRU队列算法实现缓存
 * 基于一个双链表的数据结构
 * 需要一个对象和一头一尾
 */
class LruCache {
    constructor(limit) {
        this.limit = limit || 10
        //head 指针指向表头元素,即为最常用的元素
        this.head = this.tail = undefined
        this.map = {}
        this.size = 0
    }
    get(key, IfreturnNode) {
        let node = this.map[key]
        // 如果查找不到含有`key`这个属性的缓存对象
        if (node === undefined) return
        // 如果查找到的缓存对象已经是 tail (最近使用过的)
        if (node === this.head) { //判断该节点是不是是第一个节点
            // 是的话,皆大欢喜,不用移动元素,直接返回
            return returnnode ? node : node.value
        }
        // 不是头结点,铁定要移动元素了
        if (node.prev) { //首先要判断该节点是不是有前驱
            if (node === this.tail) { //有前驱,若是尾节点的话多一步,让尾指针指向当前节点的前驱
                this.tail = node.prev
            }
            //把当前节点的后继交接给当前节点的前驱去指向。
            node.prev.next = node.next
        }
        if (node.next) { //判断该节点是不是有后继
            //有后继的话直接让后继的前驱指向当前节点的前驱
            node.next.prev = node.prev
            //整个一个过程就是把当前节点拿出来,并且保证链表不断,下面开始移动当前节点了
        }
        node.prev = undefined //移动到最前面,所以没了前驱
        node.next = this.head //注意!!! 这里要先把之前的排头给接到手!!!!让当前节点的后继指向原排头
        if (this.head) {
            this.head.prev = node //让之前的排头的前驱指向现在的节点
        }
        this.head = node //完成了交接,才能执行此步!不然就找不到之前的排头啦!
        return IfreturnNode ?
            node :
            node.value
    }
    set(key, value) {
        // 之前的算法可以直接存k-v但是现在要把简单的 k-v 封装成一个满足双链表的节点
        //1.查看是否已经有了该节点
        let node = this.get(key, true)
        if (!node) {
            if (this.size === this.limit) { //判断缓存是否达到上限
                //达到了,要删最后一个节点了。
                if (this.tail) {
                    this.tail = this.tail.prev
                    this.tail.prev.next = undefined
                    //平滑断链之后,销毁当前节点
                    this.tail.prev = this.tail.next = undefined
                    this.map[this.tail.key] = undefined
                    //当前缓存内存释放一个槽位
                    this.size--
                }
                node = {
                    key: key
                }
                this.map[key] = node
                if(this.head){//判断缓存里面是不是有节点
                    this.head.prev = node
                    node.next = this.head
                }else{
                    //缓存里没有值,皆大欢喜,直接让head指向新节点就行了
                    this.head = node
                    this.tail = node
                }
                this.size++//减少一个缓存槽位
            }
        }
        //节点存不存在都要给他重新赋值啊
        node.value = value
    }
}

module.exports = LruCache
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

具体的思路就是如果所要get的节点不是头结点(即已经是最近使用的节点了,不需要移动节点位置)要先进行平滑的断链操作,处理好指针指向的关系,拿出需要移动到最前面的节点,进行链表的插入操作。

# 最长递增子序列

function lis(arr) {
  let len = arr.length,
    res = [],
    dp = new Array(len).fill(1);
  // 存默认index
  for (let i = 0; i < len; i++) {
    res.push([i])
  }
  for (let i = len - 1; i >= 0; i--) {
    let cur = arr[i],
      nextIndex = undefined;
    // 如果为-1 直接跳过,因为-1代表的是新节点,不需要进行排序
    if (cur === -1) continue
    for (let j = i + 1; j < len; j++) {
      let next = arr[j]
      // 满足递增条件
      if (cur < next) {
        let max = dp[j] + 1
        // 当前长度是否比原本的长度要大
        if (max > dp[i]) {
          dp[i] = max
          nextIndex = j
        }
      }
    }
    // 记录满足条件的值,对应在数组中的index
    if (nextIndex !== undefined) res[i].push(...res[nextIndex])
  }
  let index = dp.reduce((prev, cur, i, arr) => cur > arr[prev] ? i : prev, dp.length - 1)
  // 返回最长的递增子序列的index
  return result[index]
}
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

# 懒加图片载

**原理:**监听页面滚动事件,做防抖处理,计算图片是否在可视区域,如果在则设置图片src属性,并监听图片加载完成事件,图片加载成功后移除滚动事件监听即可。

  • 使用
<!-- 如果滚动的容器是window -->
<LazyLoadImg src="123.png"/>
<!-- 如果滚动的容器是页面内部某个容器 -->
<LazyLoadImg src="123.png" :container="() => $refs.container"/>
1
2
3
4
  • 源码
<template>
    <img
        ref="image"
        v-if="selfSrc"
        v-show="loaded"
        class="lazy-load-img"
        :src="selfSrc"
        @load="loadImage"
    />
    <div
        ref="contentDom"
        v-else-if="!loaded"
        class="lazy-load-img-container"
    >
        <div class="lazy-load-img-content"><div class="loading"></div></div>
    </div>
</template>

<script>
export default {
    name: 'LazyLoadImg',
    props: {
        src: {
            type: String,
            default: ''
        },
        offset: {
            type: Number,
            default: 50
        },
        container: {
            type: Function,
            default: () => (undefined)
        }
    },
    data() {
        return {
            loaded: false, // 已加载
            isVisible: false, // 是否在可视区域
            selfSrc: '',
        };
    },
    mounted() {
        this.$container = this.container() || document.documentElement || document.body;
        this.contentDom = this.$refs.contentDom;
        this.target = this.container() || window;
        this.target.addEventListener('scroll', this.scrollHandle);
    },
    beforeDestroy() {
        this.removeEvent();
    },
    methods: {
        scrollHandle() {
            // console.log(this.isVisible);
            clearTimeout(this.timeout);
            this.timeout = setTimeout(() => {
                const top = this.contentDom.offsetTop;
                const height = this.contentDom.clientHeight;
                const clientHeight = this.$container.clientHeight;
                const scrollTop = this.$container.scrollTop;
                const viewTop = top - (scrollTop + clientHeight);
                this.isVisible = viewTop < this.offset && (top + height + this.offset > scrollTop);
                // console.log(viewTop, top + height - scrollTop, this.isVisible);
                if (this.isVisible) {
                    this.selfSrc = this.src;
                    this.removeEvent();
                }
            }, 100);
        },
        removeEvent() {
            this.target.removeEventListener('scroll', this.scrollHandle);
        },
        loadImage() {
            this.loaded = true;
        }
    }
}
</script>
<style>
.lazy-load-img-container{
    display: inline-block;
    width: 600px;
    height: 350px;
    background-color: #eee;
    color: #aaa;
}
.lazy-load-img-container .lazy-load-img-content{
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
}
.lazy-load-img-container .loading{
    width: 15px;
    height: 15px;
    border-radius: 100%;
    margin: 2px;
    -webkit-animation-fill-mode: both;
    animation-fill-mode: both;
    border: 3px solid #ddd;
    border-bottom-color: transparent;
    height: 25px;
    width: 25px;
    background: transparent !important;
    display: inline-block;
    -webkit-animation: loadingRotate 0.75s 0s linear infinite;
    animation: loadingRotate 0.75s 0s linear infinite;
}
@keyframes loadingRotate {
    0% {
        -webkit-transform: rotate(0deg);
        transform: rotate(0deg);
    }
    50% {
        -webkit-transform: rotate(180deg);
        transform: rotate(180deg);
    }
    100% {
        -webkit-transform: rotate(360deg);
        transform: rotate(360deg); 
    }
}
</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
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
123
124

# vuex

vuex是专门为vue提供的全局状态管理系统,用于多个组件中数据共享、数据缓存等。(无法持久化、内部核心原理是通过创造一个全局实例 new Vue);

核心方法: replaceStatesubscriberegisterModulenamespace(modules)

# 流程图

img

# 基础使用[SGMA MMP]

主要包括以下几个模块

  • State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。
  • Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。
  • Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
  • Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
  • Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。

这里我们可以进行类比:

  • state 类比为组件的状态 ;
  • getters类比为组件的计算属性 ;
  • mutations类比为组件中的方法(可以更改组件的状态),使用commit;
  • actions用于进行异步操作将结果提交给mutation; 使用dispatch;
  • module 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。

其他辅助函数有:

  • mapState
  • mapGetters
  • mapMutations
  • mapActions

使用示范:(辅助函数的使用详见辅助函数部分)

import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
//不是在生产环境debug为true
const debug = process.env.NODE_ENV !== 'production';
//创建Vuex实例对象
const store = new Vuex.Store({
    strict:debug,//在不是生产环境下都开启严格模式
    state:{
    },
    getters:{
    },
    mutations:{
    },
    actions:{
    }
})
export default store;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import Vue from 'vue';
import App from './App.vue';
import store from './store';
const vm = new Vue({
    store:store,
    render: h => h(App)
}).$mount('#app')
1
2
3
4
5
6
7
const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
  }
})
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      state.count++ // 变更状态
    }
  }
})
store.commit('increment')

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})
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
const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 进阶用法

subscribe的使用 ;store.subscribe((mutation,state)

# persist插件

# vuex中的数据在页面刷新后数据消失

用sessionstorage 或者 localstorage 存储数据

  • 存储: sessionStorage.setItem( '名', JSON.stringify(值) )
  • 使用: sessionStorage.getItem('名') ---得到的值为字符串类型,用JSON.parse()去引号;

也可以引入插件vuex-persist

# 使用第三方库
import VuexPersistence from 'vuex-persist'
const vuexLocal = new VuexPersistence({
    storage: window.localStorage
})
const store = new Vuex.Store({
  state: { ... },
  mutations: { ... },
  actions: { ... },
  plugins: [vuexLocal.plugin]
}) 
1
2
3
4
5
6
7
8
9
10
# 自己实现

自动保存到本地插件; 发布订阅;可自己实现插件功能;挂载到store中的plugins数组上;

简单方式:

export default (store)=>{
    // 用新的状态 替换掉老的状态
  store.replaceState(JSON.parse(localStorage.getItem('state'))|| store.state);
    store.subscribe((mutation,state)=>{ // 订阅每次commit都会触发此函数
        localStorage.setItem('state',JSON.stringify(state));
    });
}
1
2
3
4
5
6
7

类方式:

class VuexPersistence {
  constructor({ storage }) {
    this.storage = storage;
    this.localName = "VUEX-MY";
  }
  plugin = (store) => {
    let localState = JSON.parse(this.storage.getItem(this.localName));
    if (localState) store.replaceState(localState);
    store.subscribe((mutationName, state) => {
      // 这里需要做一个节流  throttle lodash
      this.storage.setItem(this.localName, JSON.stringify(state));
    });
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用插件
const vuexLocal = new VuexPersistence({
  storage: window.localStorage,
});

//import VuexPersistence from 'vuex-persist'
//const vuexLocal = new VuexPersistence({
//    storage: window.localStorage
//})
export default new Vuex.Store({
    state,
    getters,
    mutations,
    actions,
    strict:process.env.NODE_ENV !== 'production',
    modules:{
        teacher
    },
    plugins: [vuexLocal.plugin]
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# logger插件

vuex中自己实现了这个插件;

类的方式:

import deepClone from 'lodash/cloneDeep'
export default (store)=>{
    let prevState = deepClone(store.state);
    store.subscribe((mutation,state)=>{
        console.log('prev',prevState.lesson);
        console.log(mutation);
        console.log('next',state.lesson);
        prevState = deepClone(state);
    });
}
1
2
3
4
5
6
7
8
9
10

变量方式:

// 自己实现logger插件
const logger = () => (store) => {
  let prevState = JSON.stringify(store.state);
  store.subscribe((mutationName, state) => {
    // 监听变化的 ,每次数据变化都会执行此方法 commit 的时候会执行此方法
    console.log("prev" + prevState);
    console.log(mutationName);
    prevState = JSON.stringify(state);
    console.log("next", prevState);
  });
};
1
2
3
4
5
6
7
8
9
10
11
// 使用插件
Vue.use(Vuex); // 使用这个插件  内部会调用Vuex中的install方法
const store = new Vuex.Store({
  // strict: true,
  plugins: [
    logger(),
  ],
 })
1
2
3
4
5
6
7
8

# 对比

# actionmutation的区别

# 原理上

核心:_vm.$watch()

this._committing = false;
 _withCommitting(fn) {
    let committing = this._committing;
    this._committing = true; // 在函数调用前 表示_committing为true
    fn();
    this._committing = committing;
}
1
2
3
4
5
6
7
if (store.strict) {
    // 只要状态一变化会立即执行,在状态变化后同步执行
    store._vm.$watch(() => store._vm._data.$$state, () => {
        console.assert(store._committing, '在mutation之外更改了状态')
    }, { deep: true, sync: true });
}


/* 使能严格模式 */
function enableStrictMode(store) {
  store._vm.$watch(
    function () {
      return this._data.$$state;
    },
    () => {
      if (process.env.NODE_ENV !== "production") {
        /* 检测store中的_committing的值,如果是true代表不是通过mutation的方法修改的 */
        assert(
          store._committing,
          `Do not mutate vuex store state outside mutation handlers.`
        );
      }
    },
    { deep: true, sync: true }
  );
}
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

严格模式下增加同步watcher,监控状态变化

以下三个常规的操作添加监控判断,是否是异步操作,高价函数曝光;

store._withCommitting(() => {
    mutation.call(store, getState(store, path), payload); // 更改状态
})
1
2
3

只有通过mutation更改状态,断言才能通过

replaceState(newState) { // 用最新的状态替换掉
    this._withCommitting(() => {
        this._vm._data.$$state = newState;
    })
}
1
2
3
4
5
store._withCommitting(() => {
    Vue.set(parent, path[path.length - 1], module.state);
})
1
2
3

内部更改状态属于正常更新,所以也需要用_withCommitting进行包裹

# 细分总括【要点】

从下面三个方面说;

  • action 提交的是 mutation,而不是直接变更状态。mutation可以直接变更状态;action 可以包含任意异步操作。mutation只能是同步操作action异步操作,可以获取数据调用mutation提交最终数据;【store.dispatchmutation同步更新数据,(内部会进行示范为异步方式更新数据检测);【store.commit

  • 提交方式不同

    • mutation是用this.$store.commit('SET_NUMBER',10)来提交;
    • action 是用this.$store.dispatch('ACTION_NAME',data)来提交;
  • 接收参数不同mutation第一个参数是state,而action第一个参数是context, [store自己],其包含了;

    {
        state,      // 等同于 `store.state`,若在模块中则为局部状态
        rootState,  // 等同于 `store.state`,只存在于模块中
        commit,     // 等同于 `store.commit`
        dispatch,   // 等同于 `store.dispatch`
        getters,    // 等同于 `store.getters`
        rootGetters // 等同于 `store.getters`,只存在于模块中
    }
    
    1
    2
    3
    4
    5
    6
    7
    8

    示范:

      mutations: {
        SET_TOKEN: (state, token) => {
          state.token = token
        },
      },
    
      actions: {
        Login ({ commit }, userInfo) {
          return new Promise((resolve, reject) => {
            login(userInfo).then(response => {
              const result = response.result
              storage.set(ACCESS_TOKEN, result.token, 7 * 24 * 60 * 60 * 1000)
              commit('SET_TOKEN', result.token)
              resolve()
            }).catch(error => {
              reject(error)
            })
          })
        },
        }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

# ajax请求代码应该写在组件的methods中还是vuex的actions中

如果请求来的数据是不是要被其他组件公用,仅仅在请求的组件内使用,就不需要放入vuex 的state里。

如果被其他地方复用,这个很大几率上是需要的,如果需要,请将请求放入action里,方便复用

# vuex与redux的区别

# 工作流向

vuex的流向:

  • view——>commit——>mutations——>state变化——>view变化(同步操作)
  • view——>dispatch——>actions——>mutations——>state变化——>view变化(异步操作)

redux的流向: view——>dispatch——>actions——>reducer——>state变化——>view变化(同步异步一样)

# 不同点
  • vuex以mutations函数取代redux中的reducer,只需在对应的mutation函数里改变state即可。
  • vuex中的state直接关联到组件实例上,当state变化时自动重新渲染,无需订阅重新渲染函数。redux使用store对象存储整个应用的状态,状态变化时,从最顶层向下传递,每一级都会进行状态比较,从而达到更新
  • vuex支持action异步处理,redux中只支持同步处理,对于异步处理需要借助于redux-thunk和redux-saga实现

# 原理

# 简单实现

实现入口文件,默认导出Store类和install方法

import { Store, install } from './store';
export default {
    Store,
    install
}
export {
    Store,
    install
}
1
2
3
4
5
6
7
8
9
# install方法
import applyMixin from './mixin'
let Vue;
export class Store {
    constructor(options){}
}
export const install = (_Vue) =>{
    Vue = _Vue; // 保留vue的构造函数
    applyMixin(Vue);
}
export default {
  Store,
  install,
};
1
2
3
4
5
6
7
8
9
10
11
12
13

当我们使用插件时默认会执行install方法并传入Vue的构造函数

# mixin方法

将store实例定义在所有的组件实例上;

const applyMixin = (Vue) => {
    Vue.mixin({
        beforeCreate: vuexInit
    })
}
// 需要把根组件中store实例给每个组件都增加一个$store的属性
function vuexInit() {
    const options = this.$options;
    if (options.store) { // 是否是根组件
        this.$store = options.store;// 给根实例增加$store属性
    } else if (options.parent && options.parent.$store) {
        // 子组件 深度优先 父-> 子 -> 孙子
        this.$store = options.parent.$store;// 给组件增加$store属性
    }
}
export default applyMixin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 实现state

vuex核心就是借用了vue的实例 因为vue的实例数据变化 会刷新视图;

什么样的属性可以实现双向 ; 有get 和set new vue({data:{}})

export class Store {
    constructor(options){
        let state = options.state;
        this._vm = new Vue({
            data:{
                $$state:state,//这个数据会放入到_data下
            }
        });
    }
    get state(){ //state别名化简化;注意这里get类的使用
        return this._vm._data.$$state; //优化缩写
    }
}
function forEachValue(obj, callback) {
  //Object.keys(obj).forEach((item) => callback(item, obj[item]));
  Object.keys(obj).forEach((item) => callback(obj[item],item));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

将用户传入的数据定义在vue的实例上 (这个就是vuex核心)产生一个单独的vue实例进行通信,这里要注意的是定义$开头的变量不会被代理到实例上

# 实现getters

通过vue中的computed实现;注意下面的先后顺序;

this.getters = {};
const computed = {}
forEachValue(options.getters, (fn, key) => {
    computed[key] = () => {
        return fn(this.state);//设置时是函数,使用时直接就是属性;
    }
    Object.defineProperty(this.getters,key,{
        get:()=> this._vm[key] //使用时才调用;
    })
});// 注意顺序
this._vm = new Vue({//借助vue的实现动态属性;
    data: {
        $$state: state, // $开头的,不会暴露;用$$state源码也是这样定义的;
    },
    computed // 利用计算属性实现缓存
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 实现mutations

暴露commit方法;注意call传参不同;fn.call(this, this.state, payload)

export class Store {
    constructor(options) {
        this.mutations = {};
        forEachValue(options.mutations, (fn, key) => {
            this.mutations[key] = (payload) => fn.call(this, this.state, payload)
        });
    }
    commit = (type, payload) => { //利用ES6不改变this指向;commit有两种使用方式
        this.mutations[type](payload);
    }
}
1
2
3
4
5
6
7
8
9
10
11
# 实现actions

暴露dispatch方法;注意call传参不同;fn.call(this, this, payload)

export class Store {
    constructor(options) {
        this.actions = {};
        forEachValue(options.actions, (fn, key) => {
            this.actions[key] = (payload) => fn.call(this, this, payload);
        });
    }
    dispatch = (type, payload) => {
        this.actions[type](payload);
    }
}
1
2
3
4
5
6
7
8
9
10
11

image-20201116151817406

# 实现state

vuex核心就是借用了vue的实例 因为vue的实例数据变化 会刷新视图;

将用户传入的数据定义在vue的实例上 (这个就是vuex核心)产生一个单独的vue实例进行通信,这里要注意的是定义$开头的变量不会被代理到实例上;

什么样的属性可以实现双向 ; 有get 和set new vue({data:{}})

export class Store {
    constructor(options){
        let state = options.state;
        this._vm = new Vue({
            data:{
                $$state:state,//这个数据会放入到_data下
            }
        });
    }
    get state(){ //state别名化简化;注意这里get类的使用
        return this._vm._data.$$state; //优化缩写
    }
}
function forEachValue(obj, callback) {
  //Object.keys(obj).forEach((item) => callback(item, obj[item]));
  Object.keys(obj).forEach((item) => callback(obj[item],item));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 实现getters

通过vue中的computed实现;注意下面的先后顺序;

this.getters = {};
const computed = {}
forEachValue(options.getters, (fn, key) => {
    computed[key] = () => {
        return fn(this.state);//设置时是函数,使用时直接就是属性;
    }
    Object.defineProperty(this.getters,key,{
        get:()=> this._vm[key] //使用时才调用;
    })
});// 注意顺序
this._vm = new Vue({//借助vue的实现动态属性;
    data: {
        $$state: state, // $开头的,不会暴露;用$$state源码也是这样定义的;
    },
    computed // 利用计算属性实现缓存
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 模块机制

**核心方法:**三步骤;

  • 模块收集;数据的格式化 格式化成我们想要的结果 (树);this._modules = new ModuleCollection(options);【树状化】
  • 安装模块;根模块的状态中 要将子模块通过模块名定义在根模块上;【展平树结构】installModule(this, state, [], this._modules.root);
  • 重新设置;将状态和getters都定义在当前的vm上;resetStoreVM(this, state);

# 插件机制

涉及到plugin, subcribe, replaceState的方法定义;

**核心方法:**三步骤;

  • plugins;
  • subcribe;
  • replaceState;

1.实现loggervue-persist插件使用方式

// import logger from "vuex/dist/logger";
// import VuexPersistence from "vuex-persist";

/**
 * plugin要实现的方法有;
 * plugins subscribe replaceState
 */
// 自己实现vuex-persist插件
class VuexPersistence {
  constructor({ storage }) {
    this.storage = storage;
    this.localName = "VUEX-MY";
  }
  plugin = (store) => {
    let localState = JSON.parse(this.storage.getItem(this.localName));
    if (localState) store.replaceState(localState);
    store.subscribe((mutationName, state) => {
      // 这里需要优化;做一个节流  throttle lodash
      this.storage.setItem(this.localName, JSON.stringify(state));
    });
  };
}

// 自己实现logger插件
const logger = () => (store) => {
  let prevState = JSON.stringify(store.state);
  store.subscribe((mutationName, state) => {
    // 监听变化的 ,每次数据变化都会执行此方法 commit 的时候会执行此方法
    console.log("prev" + prevState);
    console.log(mutationName);
    prevState = JSON.stringify(state);
    console.log("next", prevState);
  });
};

const vuexLocal = new VuexPersistence({
  storage: window.localStorage,
});
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

2.实现plugin,subscribe,replaceState方法

# 发布订阅模式

// 初始化时,执行插件
  plugins: [
    // 插件从上到下执行
    vuexLocal.plugin, // 放在这里 插件默认就会显执行一次
    logger(),
  ],
//options.plugins.forEach(plugin => plugin(this));
subscribe(fn){
  this._subscribers.push(fn);
}
//还有是在提交commit后执行
 this._subscribes.forEach((fn) => fn({ type, payload }, this.state));
replaceState(state){
  this._vm._data.$$state = state; //直接修改,记得配合下面的replace替换新的数据
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

3.获取最新状态replaceState

外部通过replaceState更新了state, 故要定义获取最新的state方法;

function getState(store, path) {
    let local = path.reduce((newState, current) => {
        return newState[current]; 
    }, store.state);
    return local
}

replaceState(newState) {
    this._withCommiting(() => {
      this._vm._data.$$state = newState; // 替换掉最新的状态
      //可直接提交;但是调用mutation时传入最新状态;
    });
}

module.forEachMutation((mutation, key) => {
    store._mutations[namespace + key] = (store._mutations[namespace + key] || []);
    store._mutations[namespace + key].push((payload) => {
    mutation.call(store, getState(store,path), payload);
    });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

调用mutation时传入最新状态

# 辅助函数

其实mapX就是简化this.$store.X的操作流程 ; vue3后面没有这辅助函数一块;

比如:age(){return this.$store.state.age}

# mapState实现
const mapState = arrList => {
  let obj = {};
  for (let i = 0; i < arrList.length; i++) {
    let stateName = arrList[i];
     //注意这里是高阶函数;
    obj[stateName] = function() {
      return this.$store.state[stateName];
    };
  }
  return obj;
};
1
2
3
4
5
6
7
8
9
10
11
# mapGetters实现
const mapGetters = arrList => {
  let obj = {};
  for (let i = 0; i < arrList.length; i++) {
    let getterName = arrList[i]
    obj[getterName] = function() {
      return this.$store.getters[getterName];
    };  
  }
  return obj;
};
1
2
3
4
5
6
7
8
9
10
# mapMutations实现
const mapMutations = mutationList=>{
    let obj = {};
    for (let i = 0; i < mutationList.length; i++) {
        let type = mutationList[i]
        obj[type] = function(payload){
            this.$store.commit(type,payload);
        }
    }
    return obj
}
1
2
3
4
5
6
7
8
9
10
# mapActions实现
const mapActions = actionList=>{
    let obj = {};
    for (let i = 0; i < actionList.length; i++) {
        let type = actionList[i]
        obj[type] = function(payload){
            this.$store.dispatch(type,payload);
        }
    }
    return obj
}
1
2
3
4
5
6
7
8
9
10
# 引入方式及示范
export * from './helpers';
import {createNamespacedHelpers} from 'vuex';
//import {mapState,mapGetters, mapMutations, mapActions} from 'vuex';
let {mapState as mapState1 ,mapGetters} = createNamespacedHelpers('samy');
export default {
    computed: {
        // 直接取出状态中的结果
        ...mapState(['lesson']),
        // 给状态起名字
        ...mapState({lesson1:'lesson'}),
        // 通过函数的方式获取结果
        ...mapState({lesson2:state=>state.lesson})
        
         // getName(){
        //     return this.$store.getters.getLessonName
        // }
        ...mapGetters(['getLessonName'])//通过辅助函数可以简写很多;
    },
    methods: {
        //this.$store.dispatch('SET_LESSON_NAME');
       ...mapMutations(['SET_LESSON_NAME']), //mapMutations简化方法;
       ...mapActions(['Login', 'Logout']),
       changeName(){
          this['SET_LESSON_NAME']({number:10});
       },
       stepCaptchaCancel () {
        this.Logout().then(() => {
        this.loginBtn = false
        this.stepCaptchaVisible = 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

# 相关联

# Vuex的严格模式是什么,有什么作用,怎么开启?

**在严格模式下,无论何时发生了状态变更且不是由mutation函数引起的,将会抛出错误。**这能保证所有的状态变更都能被调试工具跟踪到。

在Vuex.Store 构造器选项中开启,如下

const store = new Vuex.Store({
    strict:true,
})
1
2
3

# 怎么在组件中批量使用Vuex的getter属性

使用mapGetters辅助函数, 利用对象展开运算符将getter混入computed 对象中

import {mapGetters} from 'vuex'
export default{
    computed:{
        ...mapGetters(['total','discountTotal'])
    }
}
1
2
3
4
5
6

# 组件中重复使用mutation

使用mapMutations辅助函数,在组件中这么使用

import { mapMutations } from 'vuex'
methods:{
    ...mapMutations({
        setNumber:'SET_NUMBER',
    })
}

computed: {
  // 直接取出状态中的结果
  ...mapState(['lesson']),
    // 给状态起名字
    ...mapState({lesson1:'lesson'}),
    // 通过函数的方式获取结果
    ...mapState({lesson2:state=>state.lesson})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

然后调用this.setNumber(10)相当调用this.$store.commit('SET_NUMBER',10)

# 在v-model上怎么用Vuex中state的值?

需要通过computed计算属性来转换。 get/set处理

<input v-model="message">
// ...
computed: {
    message: {
        get () {
            return this.$store.state.message
        },
        set (value) {
            this.$store.commit('updateMessage', value)
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

# vue-router

# 简单实践

import Vue from 'vue'
import Router from 'vue-router'
import routes from './routes';
Vue.use(Router)

export default new Router({
    mode: 'history', // 不使用#方式
    base: process.env.BASE_URL,
    routes
});

// routes.js文件是专门处理路由的 配置影射关系
export default  [
    {
        path:'/',
        redirect: {name:'home'} // 默认访问根路径时 可以重定向到home路由
    },
    {
        name:'home',
        path:'/home', //  实现了路由的懒加载
        component:()=>import('../views/Home.vue')
    },
    {
        path:'/profile',
        name:'profile',
        component:()=>import('../views/Profile.vue')
    },
    {
        path:'/user',
        name:'user',
        component:()=>import('../views/User.vue')
    },
    {
        path:'/login',
        name:'login',
        component:()=>import('../views/Login.vue')
    },
    {
        path:'*',
        component:()=>import('../views/404.vue')
    }
]
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

# 前端路由

# 原理解析

前端路由实现原理很简单,本质上就是检测 URL 的变化,截获 URL 地址,通过解析、匹配路由规则实现 UI 更新

路由描述了 URL 与 UI 之间的映射关系,这种映射是单向的,即 URL 变化引起 UI 更新(无需刷新页面)。前端路由最主要的展示方式有 2 种:

  • 带有 hash 的前端路由:地址栏 URL 中有 #,即 hash 值,不好看,但兼容性高。
  • 不带 hash 的前端路由:地址栏 URL 中没有 #,好看,但部分浏览器不支持,还需要后端服务器支持。

在 vue-router 和 react-router 中,这两种展示形式,被定义成两种模式,即 Hash 模式与 History 模式。路由切换可以重新渲染组件,但是不刷新页面;

# 两种实现原理

Hash

  • 监听hashchange
  • window.location.hashwindow.location.replace

History

  • 监听popstate
  • window.location.pathname, history.pushState({},null,'/x'), history.replaceState
  • pushState不会触发popstate事件,所以需要手动调用渲染函数;
# Hash模式

window对象提供了onhashchange事件来监听hash值的改变,一旦url中的hash值发生改变,便会触发该事件。

# History 模式

HTML5的History API 为浏览器的全局history对象增加的扩展方法。window对象提供了onpopstate事件来监听历史栈的改变,一旦历史栈信息发生改变,便会触发该事件。

需要特别注意的是,调用history.pushState()或history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件。

# 两种模式对比[要点]

对比 Hash History
观赏性
兼容性 >ie8 >ie10
实用性 直接使用 需后端配合
命名空间 同一document 同源
区别 \ mode hash history
监听事件 hashChange popstate
缺点 # 号不好看 子路由刷新404、ie9及以下不兼容
push操作 window.location.assign window.history.pushState
replace操作 window.location.replace window.history.replaceState
访问操作 window.history.go window.history.go
后退操作 window.history.go(-1) window.history.go(-1)
向前操作 window.history.go(1) window.history.go(1)

说明:

  • History新的 URL 可以是任意同源的 URL,而 window.location只能通过改变 hash 值才能保证留在当前 document 中,浏览器不发起请求;
  • 新的 URL 可以是当前 URL,不改变,就可以创建一条新的历史记录项,而 window.location 必须设置不同的 hash 值,才能创建。假如当前URL为 /home.html#foo,使用 window.location 设置 hash 时,hash值必须不能是 #foo,才能创建新的历史记录;
  • 可以通过 data/state 参数在新的历史记录项中添加任何数据,而通过 window.location 改变 hash 的方式,只能将相关的数据转成一个很短的字符串,以 query 的形式放到 hash 值后面;
  • History模式:仅仅将浏览器的 URL 变成了新的 URL,页面不会加载、刷新。pushState 在不刷新页面的情况下修改浏览器 URL 链接,单页面路由的实现也就是利用了这一个特性。【要点】

简单实现:

/**
         *hash模式
        - 监听`hashchange`
        - `window.location.hash`, `window.location.replace`
         */
let fn = function () {
     app.innerHTML = window.location.hash
 };
fn(); //第一次要主动渲染
window.addEventListener('hashchange',fn)

/**
         * history模式
        - 监听`popstate`
        - `window.location.pathname`, `history.pushState({},null,'/x')`, `history.replaceState`
        - pushState不会触发popstate事件,所以需要手动调用渲染函数;
         */
let fn = function () {
  app.innerHTML = window.location.pathname
};
function goA() {
  history.pushState({}, null, '/a');
  fn();
}
function goB() {
  history.pushState({}, null, '/b');
  fn();
}
// 只对浏览器的前进后退进行处理;所以上面方法要重新再设置下;
window.addEventListener('popstate', function () {
  fn();
});
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

# 注意事项

关于 popstate 事件监听路由的局限

history对象的 back(), forward() 和 go() 三个等操作会主动触发 popstate 事件,但是 pushState 和 replaceState 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。

关于子路由刷新的解决方式

history模式子路由刷新会404,因此需要后端配合,将未匹配到的路由默认指向html文件;

location / {
  try_files $uri $uri/ /index.html;
}
1
2
3

# history模式404

  • 当使用history模式时,如果没有进行配置,刷新页面会出现404。

  • 原因是因为history模式的url是真实的url, 服务器会对url的文件路径进行资源查找,找不到资源就会返回404

  • 在以下demo使用webpack-dev-server的里的historyApiFallback属性来支持HTML5 History Mode。

    //historyApiFallback  // 依赖HTML5 history API, 如果设置为true,所有的页面跳转指向index.html
    devServer:{
        contentBase: './src' // 本地服务器所加载的页面所在的目录
        historyApiFallback: true, // 不跳转,默认是true,可不用配置
        inline: true // 实时刷新
    }
    
    //正式环境下nginx配置:try_files $uri $uri/ /index.html;
    server {
      listen 8888;
      root /home/npm/deploy/bdp-website/dist;
      index index.html index.htm index.nginx-debian.html;
      client_max_body_size 500M;
      proxy_connect_timeout       500s;
      proxy_send_timeout          500s;
      proxy_read_timeout          500s;
      send_timeout                500s;
      location / {
          try_files $uri $uri/ =404;
          #try_files $uri $uri/ /index.html;
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

# 手写简单版本

base基类处理:待优化;目前只做了渲染,可做抽象处理;

image-20201018215704133

# Hash
  • 监听hashchange
  • window.location.hashwindow.location.replace
<a href="#/a">a页面</a>
<a href="#/b">b页面</a>
import { BaseRouter } from "./base.js";

export class HashRouter extends BaseRouter {
  constructor(list) {
    super(list);
    this.handler();//第一次要主动渲染
    //监听hash变化事件,hash变化重新渲染
    window.addEventListener("hashchange", (e) => {
      this.handler();
    });
  }
  //渲染
  handler() {
    this.render(this.getState());
  }
  //获取当前hash
  getState() {
    const hash = window.location.hash;
    return hash ? hash.slice(1) : "/";
  }
  //获取完整url
  getUrl(path) {
    const href = window.location.href;
    const i = href.indexOf("#");
    const base = i >= 0 ? href.slice(0, i) : href;
    return `${base}#${path}`;
  }
  //改变hash值 实现压入 功能
  push(path) {
    window.location.hash = path;
  }
  //使用location.replace实现替换 功能
  replace(path) {
    window.location.replace(this.getUrl(path));
  }
  go(n) {
    //window.history.go(n);
  }
}
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
# History
  • 监听popstate
  • window.location.pathname, history.pushState({},null,'/x'), history.replaceState
  • pushState不会触发popstate事件,所以需要手动调用渲染函数;
<a onClick="goA()">a页面</a>
<a onClick="goB()">b页面</a>
import { BaseRouter } from "./base.js";

export class HistoryRouter extends BaseRouter {
  constructor(list) {
    super(list);
    this.handler();
    //监听历史栈信息变化,变化时重新渲染
    window.addEventListener("popstate", (e) => {
      this.handler();
    });
  }
  //渲染
  handler() {
    this.render(this.getState());
  }
  //获取路由路径
  getState() {
    const path = window.location.pathname;
    return path ? path : "/";
  }
  //使用pushState方法实现压入功能
  //PushState不会触发popstate事件,所以需要手动调用渲染函数
  push(path) {
    history.pushState(null, null, path);
    this.handler();
  }
  //使用replaceState实现替换功能
  //replaceState不会触发popstate事件,所以需要手动调用渲染函数
  replace(path) {
    history.replaceState(null, null, path);
    this.handler();
  }
  go(n) {
    window.history.go(n);
  }
}
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
# 简单示例
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <a href="#/a">hash a页面</a>
    <a href="#/b">hash b页面</a> <br />

    <!-- <a onClick="goA()">history a页面</a>
    <a onClick="goB()">history b页面</a> -->

    <div id="app"></div>
    <div id="app2"></div>

    <script>
      // hash模式
      let fn = function () {
        app.innerHTML = window.location.hash;
      };
      fn();
      window.addEventListener("hashchange", fn);

      //   history模式; 要本地服务器模式下,才有用
      let fn2 = function () {
        app2.innerHTML = window.location.pathname;
      };
      function goA() {
        history.pushState({}, null, "/a");
        fn2();
      }
      function goB() {
        history.pushState({}, null, "/b");
        fn2();
      }
      //   只对浏览器的前进后退进行处理
      window.addEventListener("popstate", function () {
        fn2();
      });
    </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

# 比较区别

# router跳转和location.href的区别

  • 使用 location.href= /url来跳转,简单方便,但是刷新了页面;
  • 使用 history.pushState( /url )无刷新页面,静态跳转;
  • 引进 router ,然后使用 router.push( /url ) 来跳转,使用了 diff 算法,实现了按需加载,减少了 dom 的消耗。其实使用 router 跳转和使用 history.pushState() 没什么差别的,因为vue-router就是用了 history.pushState() ,尤其是在history模式下。

# params和query的区别

  • 使用query方法传入的参数使用this.$route.query接受
  • 使用params方式传入的参数使用this.$route.params接受

用法:query要用path来引入,params要用name来引入,接收参数都是类似的,分别是 this.$route.query.namethis.$route.params.name

url地址显示:query更加类似于我们ajax中get传参,params则类似于post,说的再简单一点,前者在浏览器地址栏中显示参数,后者则不显示;

注意query刷新不会丢失query里面的数据 params刷新会丢失 params里面的数据

// 不同方式的传递参数
<td><router-link :to="{name:'userDetail',query:{id:u.id}}">{{u.id}}</router-link></td>
<td><router-link :to="{name:'detail',params:{name:u.username}}"> {{u.username}}</router-link></td>

// 取值方式
 查询字符串 {{this.$route.query && this.$route.query.id}} <br>
 查询参数 {{this.$route.params && this.$route.params.name}} <br>
1
2
3
4
5
6
7

# $route和 $router 的区别

  • $route 是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数;
  • $router 是“路由实例”对象包括了路由的跳转方法,钩子函数等;

vue-router 在注册时,为每个 vue 实例注入了route、router 对象。``route 提供当前激活的路由信息,router为router实例信息,利用push和replace方法实现路由跳转

import router from './router'
export default new Vue({
    el: '#app',
    router,
    render: h => h(App),
})
1
2
3
4
5
6
// 方法1:
<router-link :to="{ name: 'users', params: { uname: samy }}">按钮</router-link
// 方法2:
this.$router.push({name:'users',params:{uname:samy}})
// 方法3:
this.$router.push('/user/' + samy)
1
2
3
4
5
6

# vue-router/react-router

# 注入方式

1. vue-router

vue-router 可以在 vue 项目中全局使用,vue.use() 功不可没。通过 vue.use(),向 VueRouter 对象注入了 Vue 实例,也就是根组件。根组件将 VueRouter 实例一层一层的向下传递,让每个渲染的子组件拥有路由功能

import VueRouter from 'vue-router'
const routes = [
    { path: '/',name: 'home',component: Home,meta:{title:'首页'} }
]
const router = new myRouter({
    mode:'history',
    routes
})
Vue.use(VueRouter)
1
2
3
4
5
6
7
8
9

2. react-router-dom

react-router 的注入方式是在组件树顶层放一个 Router 组件,然后在组件树中散落着很多 Route 组件,顶层的 Router 组件负责分析监听 URL 的变化,在其下面的 Route 组件渲染对应的组件。在完整的单页面项目中,使用 Router 组件将根组件包裹,就能完成保证正常的路由跳转。

import { BrowserRouter as Router, Route } from 'react-router-dom';
class App extends Component {
    render() {
        return (
            <Router>
                <Route path='/' exact component={ Home }></Route>
            </Router>
        )
    }
}
1
2
3
4
5
6
7
8
9
10
# 基础组件

1. vue-router 提供的组件主要有 <router-link/><router-view/>

  • <router-link/>可以操作 DOM 直接进行跳转,定义点击后导航到哪个路径下;对应的组件内容渲染到<router-view/> 中。

2. react-router-dom 常用到的是 <BrowserRouter/><HashRouter/><Route/><Link/><Switch/>

  • <BrowserRouter/><HashRouter/> 组件看名字就知道,用于区分路由模式,并且保证 React 项目具有页面跳转能力。
  • <Link />组件与 vue-router 中的 <router-link/> 组件类似,定义点击后的目标导航路径,对应的组件内容通过 <Route /> 进行渲染。
  • <Switch/> 用来将 react-router 由包容性路由转换为排他性路由,每次只要匹配成功就不会继续向下匹配。vue-router 属于排他性路由。
# 路由模式

1. vue-router 主要分为 hash 和 history 两种模式。在 new VueRouter() 时,通过配置路由选项 mode 实现。

  • Hash 模式:地址栏 URL 中有 #。vue-router 优先判断浏览器是否支持 pushState,若支持,则通过 pushState 改变 hash 值,进行目标路由匹配,渲染组件,popstate 监听浏览器操作,完成导航功能,若不支持,使用 location.hash 设置 hash 值,hashchange 监听 URL 变化完成路由导航。

  • History 模式:地址栏 URL 中没有 #。与 Hash 模式实现导航的思路是一样的。不同的是,vue-router 提供了 fallback 配置,当浏览器不支持 history.pushState 控制路由是否应该回退到 hash 模式。默认值为 true。【兼容处理】

    网上资料对 Hash 路由模式的原理分析大都是通过 location.hash 结合 hashchange 实现,与上述描述的 hash 路由模式的实现方式不同,这也是小编最近阅读 vue-router 源码发现的,鼓励小伙伴们读一下,肯定会收获满满!

2. react-router-dom 常用的 2 种模式是 browserHistory、hashHistory,直接用 <BrowserRouter><HashHistory> 将根组件(通常是 <App> )包裹起来就能实现。

  • react-router 的实现依赖 history.js,history.js 是 JavaScript 库。<BrowserRouter><HashHistory> 分别基于 history.js 的 BrowserHistory 类、HashHistory 类实现。
  • BrowserHistory 类通过 pushState、replaceState 和 popstate 实现,但并没有类似 vue-router 的兼容处理。HashHistory 类则是直接通过 location.hash、location.replace 和 hashchange 实现,没有优先使用 history 新特性的处理。
# 嵌套路由与子路由

1. vue-router 嵌套路由

在 new VueRouter() 配置路由表时,通过定义 Children 实现嵌套路由,无论第几层的路由组件,都会被渲染到父组件 <router-view/> 标识的地方。

router.js
const router = new Router({
    mode:'history',
    routes: [{
        path: '/nest',
        name: 'nest',
        component: Nest,
        children:[{
            path:'first',
            name:'first',
            component:NestFirst
        }]
    }]
})
nest.vue
<div class="nest">
    一级路由 <router-view></router-view>
</div>
first.vue
<div class="nest">
    二级路由 <router-view></router-view>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

/nest 下设置了二级路由 /first,二级对应的组件渲染在一级路由匹配的组件 <router-view/> 标识的地方。在配置子路由时,path 只需要是当前路径即可。

image-20201123234443023

2. react-router 子路由

react-router 根组件会被渲染到 <Router/> 指定的位置,子路由则会作为子组件,由父组件指定该对象的渲染位置。如果想要实现上述 vue-router 嵌套的效果,需要这样设置:

route.js
const Route = () => (
    <HashRouter>
        <Switch>
            <Route path="/nest" component={Nest}/>
        </Switch>
    </HashRouter>
);
nest.js
export default class Nest extends Component {
    render() {
        return (
            <div className="nest">
                一级路由
                <Switch>
                    <Route path="/nest/first" component={NestFirst}/>
                </Switch>
            </div>
        )
    }
}
first.js
export default class NestFirst extends Component {
    render() {
        return (
            <div className="nest">
                二级路由
                <Switch>
                    <Route exact path="/nest/first/second" component={NestSecond}/>
                </Switch>
            </div>
        )
    }
}
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

其中,/nest 为一级路由,/fitst 二级路由匹配的组件,作为一级路由的子组件。react-router 定义子路由 path 时,需要写完整的路径,即父路由的路径要完整。

# hash&history

结合 Vue 的异步组件 (opens new window)和 Webpack 的代码分割功能 (opens new window),轻松实现路由组件的懒加载。

vue-router 有 3 种路由模式:hash、history、abstract,对应的源码如下所示:

switch (mode) {
  case 'history':
	this.history = new HTML5History(this, options.base)
	break
  case 'hash':
	this.history = new HashHistory(this, options.base, this.fallback)
	break
  case 'abstract':
	this.history = new AbstractHistory(this, options.base)
	break
  default:
	if (process.env.NODE_ENV !== 'production') {
	  assert(false, `invalid mode: ${mode}`)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

其中,3 种路由模式的说明如下:

  • hash: 使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器;
  • history : 依赖 HTML5 History API 和服务器配置。具体可以查看 HTML5 History 模式;
  • abstract : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.

# abstract模式

abstract 不涉及和浏览器地址的相关记录。流程跟hash模式一样,通过数组维护模拟浏览器的历史记录栈 服务端下使用。使用一个不依赖于浏览器的浏览器历史虚拟管理后台;

# hash模式

hash + hashChange 兼容性好但是不美观

  • 监听hashchange
  • window.location.hashwindow.location.replace

Hash 模式:地址栏 URL 中有 #。vue-router 优先判断浏览器是否支持 pushState,若支持,则通过 pushState 改变 hash 值,进行目标路由匹配,渲染组件,popstate 监听浏览器操作,完成导航功能,若不支持,使用 location.hash 设置 hash 值,hashchange 监听 URL 变化完成路由导航。

# history模式

historyApi+popState + pushState/replaceState虽然美观,但是刷新会出现404需要后端进行配置;

  • 监听popstate;
  • window.location.pathname, history.pushState({},null,'/x'), history.replaceState;

History 模式:地址栏 URL 中没有 #。与 Hash 模式实现导航的思路是一样的。不同的是,vue-router 提供了 fallback 配置,当浏览器不支持 history.pushState 控制路由是否应该回退到 hash 模式。默认值为 true。【兼容处理】

  • pushState 和 repalceState 两个 API 来操作实现 URL 的变化;
  • 可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染) history.pushState() 或 history.replaceState() 不会触发 popstate 事件,需要手动触发页面跳转(渲染);使用webpack-dev-server的里的historyApiFallback属性来支持HTML5 History Mode。
location / {
  try_files $uri $uri/ /index.html;
}
1
2
3

需要特别注意的是,调用history.pushState()或history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件。

# 总括

hash模式和history模式都是通过window.addEvevtListenter()方法监听 hashchange和popState进行相应路由的操作。可以通过back、foward、go等方法访问浏览器的历史记录栈,进行各种跳转。

而abstract模式是自己维护一个模拟的浏览器历史记录栈的数组。

# 如何获取页面的hash变化

(1)监听$route的变化

// 监听,当路由发生变化的时候执行
watch: {
  $route: {
    handler: function(val, oldVal){
      console.log(val);
    },
    // 深度观察监听
    deep: true
  }
},
1
2
3
4
5
6
7
8
9
10

(2)window.location.hash读取#值

window.location.hash 的值可读可写,读取来判断状态是否改变,写入时可以在不重载网页的前提下,添加一条历史访问记录。

# 跳转及参数传递

# 通过方法跳转

# 编程式导航
<template>
    <div>
        <button class="btn btn-primary" @click="login">登录</button>
    </div>
</template>

<script>
export default {
    methods: {
        login(){
            this.$router.push({name:'login'})
        }
    },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# to方式及参数处理

{
    name:'userDetail',
    path:'userDetail',
    component:()=>import('../views/UserDetail')
},
{
    name:'detail',
    path:'userDetail/:name', // 路径参数 必须通过name跳转
    component:()=>import('../views/UserDetail')
}
// 不同方式的传递参数
<td><router-link :to="{name:'userDetail',query:{id:u.id}}">{{u.id}}</router-link></td>
<td><router-link :to="{name:'detail',params:{name:u.username}}"> {{u.username}}</router-link></td>

// 取值方式
 查询字符串 {{this.$route.query && this.$route.query.id}} <br>
 查询参数 {{this.$route.params && this.$route.params.name}} <br>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# param方式
  • 配置路由格式:/router/:id;
  • 传递的方式:在path后面跟上对应的值;
  • 传递后形成的路径:/router/123

1)路由定义

//在APP.vue中
<router-link :to="'/user/'+userId" replace>用户</router-link>    
//在index.js
{
   path: '/user/:userid',
   component: User,
},
1
2
3
4
5
6
7

2)路由跳转

// 方法1:
<router-link :to="{ name: 'users', params: { uname: samy }}">按钮</router-link
// 方法2:
this.$router.push({name:'users',params:{uname:samy}})
// 方法3:
this.$router.push('/user/' + samy)
1
2
3
4
5
6

3)参数获取 通过 $route.params 获取传递的值

# query方式
  • 配置路由格式:/router,也就是普通配置
  • 传递的方式:对象中使用query的key作为传递方式
  • 传递后形成的路径:/route?id=123

1)路由定义

//方式1:直接在router-link 标签上以对象的形式
<router-link :to="{path:'/profile',query:{name:'why',age:28,height:188}}">档案</router-link>
// 方式2:写成按钮以点击事件形式
<button @click='profileClick'>我的</button>    
profileClick(){
  this.$router.push({
    path: "/profile",
    query: {
        name: "kobi",
        age: "28",
        height: 198
    }
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

2)跳转方法

// 方法1:
<router-link :to="{ name: 'users', query: { uname: james }}">按钮</router-link>
// 方法2:
this.$router.push({ name: 'users', query:{ uname:james }})
// 方法3:
<router-link :to="{ path: '/user', query: { uname:james }}">按钮</router-link>
// 方法4:
this.$router.push({ path: '/user', query:{ uname:james }})
// 方法5:
this.$router.push('/user?uname=' + jsmes)

1
2
3
4
5
6
7
8
9
10
11

3)获取参数; 通过$route.query获取传递的值

# 怎么定义vue-router的动态路由?怎么获取传过来的动态参数?

在router目录下的index.js文件中,对path属性加上/:id。使用router对象的params.id获取动态参数

# 守卫及权限

# 钩子函数种类

路由钩子的执行流程, 钩子函数种类有:全局守卫、路由守卫、组件守卫;

Vue导航守卫(路由生命周期)分类【要点】

确保要调用 next 方法,否则钩子就不会被 resolved。参数: 有to(去的那个路由)、from(离开的路由)、**next(一定要用这个函数才能去到下一个路由,如果不用就拦截)**最常用就这几种。

a. 全局守卫

  • beforeEach — 全局前置钩子(每个路由调用前都会触发,根据 from 和 to 来判断是哪个路由触发)

  • beforeResolve — 全局解析钩子(和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用)

  • afterEach — 全局后置钩子【常用】

    router.afterEach(() => {
      NProgress.done()
    })
    
    1
    2
    3

b. 路由独享守卫

  • 路由配置上可以直接定义 beforeEnter 守卫。

c. 组件内守卫

  • beforeRouteEnter — 在渲染该组件的对应路由被 confirm 前调用,不能获取组件实例 this,因为当守卫执行前,组件实例还没被创建。
  • beforeRouteUpdate — 当前路由改变,但是该组件被复用时调用
  • beforeRouteLeave — 导航离开该组件的对应路由时调用

这三个钩子都有三个参数∶to、from、next

  • beforeRouteEnter∶ 进入组件前触发
  • beforeRouteUpdate∶ 当前地址改变并且改组件被复用时触发,举例来说,带有动态参数的路径foo/∶id,在 /foo/1 和 /foo/2 之间跳转的时候,由于会渲染同样的foa组件,这个钩子在这种情况下就会被调用
  • beforeRouteLeave∶ 离开组件被调用

注意点,beforeRouteEnter组件内还访问不到this,因为该守卫执行前组件实例还没有被创建,需要传一个回调给 next来访问,例如:

beforeRouteEnter(to, from, next) {      
    next(target => {        
        if (from.path == '/classProcess') {          
            target.isFromProcess = true        
        }      
    })    
}
1
2
3
4
5
6
7

# 完整的导航解析流程

runQueue

  • ①导航被触发。
  • ②在失活的组件里调用 beforeRouteLeave 守卫。
  • ③调用全局的 beforeEach 守卫。
  • ④在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  • ⑤在路由配置里调用 beforeEnter
  • ⑥解析异步路由组件。
  • ⑦在被激活的组件里调用 beforeRouteEnter
  • ⑧调用全局的 beforeResolve 守卫 (2.5+)。
  • ⑨导航被确认。
  • ⑩调用全局的 afterEach 钩子。
  • ⑪触发 DOM 更新。
  • ⑫调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

# 实践及验证

# 权限校验

beforeEach上处理;

router.beforeEach((to,from,next)=>{
  let needLogin = to.matched && to.matched.some(({meta})=>{
   return meta && meta.needLogin
  });
  let isLogin = localStorage.getItem('login')
  if(needLogin){
    if(isLogin){
      next();
    }else{
      next({name:'login'});
    }
  }else{
    // 如果不需要登录 并且是登录页面
    if(!(to.name == 'login' && isLogin)){
      next();
    }
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

afterEach (跳转之后滚动条回到顶部)

router.afterEach((to, from) => {  
    window.scrollTo(0,0); // 跳转之后滚动条回到顶部  
});
1
2
3
# 单个路由独享钩子

beforeEnter 如果不想全局配置守卫的话,可以为某些路由单独配置守卫,有三个参数∶ to、from、next

export default [    
    {        
        path: '/',        
        name: 'login',        
        component: login,        
        beforeEnter: (to, from, next) => {          
            console.log('即将进入登录页面')          
            next()        
        }    
    }
]
1
2
3
4
5
6
7
8
9
10
11
# 组件内的钩子
beforeRouteEnter(to,from,next){
  // 在进入路由之前可以对数据校验 ajax获取
  let user = JSON.parse(localStorage.getItem('user')) || [];
  if(user.length){
    next();
  }else{
    next({name:'userAdd'})
  }
},
  beforeRouteLeave (to, from, next) {
    if(this.username){
      let confirm = window.confirm('确认离开吗');
      if(confirm) return next();
    }else{
      next();
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

路由更新变化

watch:{
  $route(){
    alert('参数变化')
  }
}

beforeRouteUpdate(){ // 如果组件被复用 唯一的方式 就是监听路由的更新方法
  console.log('路由更新了')
}
1
2
3
4
5
6
7
8
9

# 导航激活样式

.router-link-active{
  color:red!important
}
.nav-stacked .router-link-active{
  color:blue !important
}
1
2
3
4
5
6

# 路由动画

<div class="container">
      <transition-group name="fade" >
        <router-view key="1"></router-view>
        <router-view key="2" name="name"></router-view>
        <router-view key="3" name="version"></router-view>
      </transition-group>
</div>

.fade-enter{ opacity:0}
.fade-enter-active{ transition:all 0.2s linear;}
.fade-enter-to{opacity:1}
.fade-leave{opacity:1}
.fade-leave-active{transition:all .2s linear}
.fade-leave-to{opacity:0}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 懒加载如何实现

vue 路由懒加载有以下三种方式:

  1. vue 异步组件
  2. ES6 的 import()
  3. webpack 的 require.ensure()

非懒加载:

import List from '@/components/list.vue'
const router = new VueRouter({
  routes: [
    { path: '/list', component: List }
  ]
})
1
2
3
4
5
6

(1)方案一:使用箭头函数+require动态加载;

vue 异步组件 这种方法主要是使用了 resolve 的异步机制,用 require 代替了 import 实现按需加载

const router = new Router({
  routes: [
   {
     path: '/list',
     component: resolve => require(['@/components/list'], resolve)
   }
  ]
})

export default new Router({
  routes: [
    {
      path: '/home',',
      component: (resolve) => require(['@/components/home'], resolve),
    },
    {
      path: '/about',',
      component: (resolve) => require(['@/components/about'], resolve),
    },
  ],
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

(2)方案二(常用):使用箭头函数+import动态加载;

ES6 的 import() vue-router 在官网提供了一种方法,可以理解也是为通过 Promise 的 resolve 机制。因为 Promise 函数返回的 Promise 为 resolve 组件本身,而我们又可以使用 import 来导入组件。

import()方法是由es6提出的,动态加载返回一个Promise对象,then方法的参数是加载到的模块。类似于Node.js的require方法,主要import()方法是异步加载的。

webpack< 2.4{ 
    path:'/', 
    name:'home',
    components:resolve=>require(['@/components/home'],resolve)
} 
webpack> 2.4{ 
    path:'/', 
    name:'home', 
    components:()=>import('@/components/home')
}
1
2
3
4
5
6
7
8
9
10
11
12
const List = () => import('@/components/list.vue')
const router = new VueRouter({
  routes: [
    { path: '/list', component: List }
  ]
})
export default new Router({
  routes: [
    {
      path: '/home',
      component: () => import('@/components/home'),
    },
    {
      path: '/about',
      component: () => import('@/components/home'),
    },
  ],
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

(3)方案三:使用webpack的require.ensure技术,也可以实现按需加载。 这种情况下,多个路由指定相同的chunkName,会合并打包成一个js文件。

webpack 的 require.ensure() 这种模式可以通过参数中的 webpackChunkName 将 js 分开打包。

// r就是resolve
const List = r => require.ensure([], () => r(require('@/components/list')), 'list');
// 路由也是正常的写法  这种是官方推荐的写的 按模块划分懒加载 
const router = new Router({
    routes: [
        {
            path: '/list',
            name: 'list',
            component: List,
        }
    ]
}))

export default new Router({
  routes: [
    {
      path: '/home',
      component: (resolve) => require.ensure([], () => resolve(require('@/components/home')), 'home'),
    },
    {
      path: '/about',
      component: (resolve) => require.ensure([], () => resolve(require('@/components/about')), 'about'),
    },
  ],
})
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

# 手写实现

核心:

  • 理解Vue.use和Vue.mixin的原理;
  • 两个原型上的属性 $route, $router;
  • 两个全局组件router-link ,router-view;
  • 需要将current属性变化成响应式的,后续current变化更新视图;
  • create-matcher暴露了match,addRoutes两个方法;
  • 导航守卫 核心就是把所有方法 组合成一个数组 依次调用; 跟koa中的洋葱模型类似;

# Install/mixin

使用这个插件内部会提供:

  • 两个原型上的属性 $router, $route;

    this._router = options.router;

    Vue.util.defineReactive(this, "_route", this._router.history.current);

  • 两个全局组件router-link , router-view;

注意这里要做响应式的处理;当current变化后 更新_route属性_

import Link from "./components/link";
import View from "./components/view";

export let _Vue;

export default function install(Vue, options) {
  _Vue = Vue;
  Vue.mixin({
    // 给所有组件都混入一个属性$router $route _route
    beforeCreate() {
      // 无论是父组件还是子组件 都可以通过 this._routerRoot._router 获取共同的实例
      const options = this.$options;
      if (options.router) {
        this._routerRoot = this; // this指向的是当前组件的实例;给当前根组件增加一个属性_routerRoot代表的是他自己
        this._router = options.router;
        this._router.init(this); // this就是根实例; mixin调用初始化;初始化监听相关;

        // 获取到current属性 将current属性定义在_route上
        Vue.util.defineReactive(this, "_route", this._router.history.current);
        // _route是响应式的; 当current变化后 更新_route属性; 如果current中的path或者matched的其他属性变化 也是响应式的
      } else {
        this._routerRoot = this.$parent && this.$parent._routerRoot; // 组件渲染;是一层层的渲染
      }
    },
  });

  // 两个原型上的属性 $router $route; // 仅仅是为了更加方便
  // 代表路由中所有的属性;重命名内部中的_routerRoute的_route, _router
  Object.defineProperty(Vue.prototype, "$router", {
    get() {
      return this._routerRoot._router; // 方法 push go repace..
    },
  });
  Object.defineProperty(Vue.prototype, "$route", {
    get() {
      return this._routerRoot._route; // path  matched
    },
  });

  // 插件一般用于定义全局组件 全局指令 过滤器 原型方法....
  // 两个全局组件router-link router-view;
  Vue.component("router-link", Link);
  Vue.component("router-view", View);
}
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

ps:vue.min的原理

Vue-Router 安装最重要的一步就是利用 Vue.mixin 去把 beforeCreatedestroyed 钩子函数注入到每一个组件中Vue.mixin 的定义,在 vue/src/core/global-api/mixin.js 中:

export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}
1
2
3
4
5
6

它的实现实际上非常简单,就是把要混入的对象通过 mergeOptions 合并到 Vueoptions 中,由于每个组件的构造函数都会在 extend 阶段合并 Vue.options 到自身的 options,所以也就相当于每个组件都定义了 mixin 定义的选项。

Vue-Router上增加一个init方法,主要目的初始化监听相关及默认第一次跳转;

matcher内部有match & addRoutes,方便后面路由跳转路由匹配及动态添加路由

import createMatcher from './create-matcher'
import install from './install'
export default class VueRouter{
    constructor(options){
        // 根据用户传递的routes创建匹配关系,this.matcher需要提供两个方法 
        // match:match方法用来匹配规则
        // addRoutes:用来动态添加路由
        this.matcher = createMatcher(options.routes || []);
    }
    init(app){}
}
VueRouter.install = install;
1
2
3
4
5
6
7
8
9
10
11
12

ps:vue插件的原理:

Vue 提供了 Vue.use 的全局 API 来注册这些插件,所以我们先来分析一下它的实现原理,定义在 vue/src/core/global-api/use.js 中:

export function initUse(Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    // 1.如果安装过这个插件直接跳出
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }
    // additional parameters
    // 2.获取参数并在参数中增加Vue的构造函数
    const args = toArray(arguments, 1) //取参数
    args.unshift(this)//[this,{a:1,b:2}]
    // 3.执行install方法
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    // 4.记录安装的插件
    installedPlugins.push(plugin)
    return this
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

Vue.use 接受一个 plugin 参数,并且维护了一个 _installedPlugins 数组,它存储所有注册过的 plugin;接着又会判断 plugin 有没有定义 install 方法,如果有的话则调用该方法,并且该方法执行的第一个参数是 Vue;最后把 plugin 存储到 installedPlugins 中。

可以看到 Vue 提供的插件注册机制很简单,每个插件都需要实现一个静态的 install 方法,当我们执行 Vue.use 注册插件的时候,就会执行这个 install 方法,并且在这个 install 方法的第一个参数我们可以拿到 Vue 对象,这样的好处就是作为插件的编写方不需要再额外去import Vue ,保存vue版本一致。

# 编写createMatcher方法

把外部传递过来的router树结构,扁平化为对象map方式,方便获取;根据用户的配置和当前请求的路径 渲染对应的组件;

创建匹配器可用用于后续的匹配操作; 用户没有传递配置就默认传入一个空数组

  • match通过路由来匹配组件;====》 createMatcher
  • addRoutes 动态添加匹配规则;
export default function createMatcher(routes) {
  let { pathMap, pathList } = createRouteMap(routes); // 扁平化配置; 根据用户的配置创建一个映射表
   // console.log("-----createMatcher-----pathMap-----", pathMap); //pathMap = {'/':Home,'/about':About,'/about/a':'aboutA','/about/b':'aboutB'}
  // console.log("-----createMatcher-----pathList-----", pathList);// ["/", "/about", "/about/a", "/about/b"]
  function match(location) {
    let record = pathMap[location]; // 可能一个路径有多个记录
    if (record) {
      return createRoute(record, {
        path: location,
      });
    }
    //  这个记录可能没有; {path:/,matched:[{},{}]}
    return createRoute(null, {
      path: location,
    });
  }

  function addRoutes(routes) {
    createRouteMap(routes, pathMap, pathList);
  }

  return {
    match, // 用于匹配路径
    addRoutes, // 添加路由,用于动态添加路由
  };
}
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

这里需要创建映射关系,需要createRouteMap方法

# 编写createRouteMap方法

// 先序深度; 将当前路由存储到pathList和pathMap中
// pathMap = {路径:记录}
function addRouteRecord(route, pathMap, pathList, parent) {
  // 当访问/ 时 应该渲染home组件   /  => {Home}
  // 要判断 儿子的路径不是以 /开头的否则不拼接 父路径
  let path = parent ? parent.path + "/" + route.path : route.path;
  // 提取需要的信息
  let record = {
    path,
    parent, // parent指代的就是父记录
    component: route.component,
    name: route.name, //其他相关
    props: route.props,
    params: route.params || {},
    meta: route.meta,
  };
  // 不能定义重复的路由 否则值生效最后一个
  if (!pathMap[path]) {
    pathMap[path] = record;
    pathList.push(path);
  }
  if (route.children) {
    route.children.forEach((childRoute) => {
      // addRouteRecord(childRoute, pathMap, pathList, route); // 在遍历儿子时 将父亲的记录传入进去
      addRouteRecord(childRoute, pathMap, pathList, record); // 在遍历儿子时 将父亲的记录传入进去
    });
  }
}

export default function createRouteMap(routes, oldPathMap, oldPathList) {
  // 当第一次加载的时候没有pathMap和pathList
  // let pathMap = oldPathMap || {}; // 1个参数时初始化 2个参数就是动态添加路由
  let pathMap = oldPathMap || Object.create(null); // 默认没有传递就是直接创建映射关系
  let pathList = oldPathList || [];
  routes.forEach((route) => {
    addRouteRecord(route, pathMap, pathList);
  });
  return {
    pathMap,
    pathList,
  };
}
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

该方法主要是处理路径和不同路径对应的记录

matcher我们先写到这,稍后在来补全match方法的实现

# 编写浏览器历史相关代码

# 核心逻辑current【要点】

最终核心 需要将current属性变化成响应式的 后续current变化会更新视图

在构造时,获取current;

base.js中; this.current = {path:'/',matched:[]}

 constructor(router) {
    this.router = router;
    this.cb = null;
    // 当我们创建完路由后 ,先有一个默认值 路径 和 匹配到的记录做成一个映射表
    // 默认当创建history时 路径应该是/ 并且匹配到的记录是[]
    this.current = createRoute(null, {
      path: "/", // 存放路由状态的
    });
    // /about/a => [/about  /about/a]
    // this.current = {path:'/',matched:[]}
    console.log(this.current);
  }
1
2
3
4
5
6
7
8
9
10
11
12
# 在router初始化逻辑
  init(app) {
    const history = this.history;
    // hash的特色处理; 监听hash值变化 默认跳转到对应的路径中; 切片编程
    const setupHashListener = () => {
      history.setupListener(); // 监听路由变化hashchange;hash的特殊性
    };
    // 初始化会先获得当前hash值进行跳转, 并且监听hash变化
    history.transitionTo(
      history.getCurrentLocation(), // 获取当前的位置
      setupHashListener
    );
    // 改变了 响应式数据
    history.listen((route) => {
      app._route = route; // 每次路径变化都会调用此方法;【订阅】
    });
    // setupListener  放到hash里取
    // transitionTo  放到base中 做成公共的方法
    // getCurrentLocation // 放到自己家里  window.location.hash / window.location.pathname
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这里要分别实现 transitionTo(基类方法)、 getCurrentLocationsetupListener

getCurrentLocation实现

function getHash(){
    return window.location.hash.slice(1);
    //return window.location.pathname;
}
export default class HashHistory extends History{
    // ...
    getCurrentLocation(){
        return getHash();
    }
}
1
2
3
4
5
6
7
8
9
10

setupListener实现

export default class HashHistory extends History{
    // ...
    setupListener(){
        window.addEventListener('hashchange', ()=> {
            this.transitionTo(getHash());
        })
       // window.addEventListener("popstate", () => {
       //     this.transitionTo(this.getCurrentLocation());
       //});
    }
}
1
2
3
4
5
6
7
8
9
10
11

可以看到最核心的还是transitionTo方法

TransitionTo实现【要点】

通过path找到route信息,再处理路由监听,再回调出去页面加载;

export function createRoute(record, location) { // {path:'/',matched:[record,record]}
    let res = [];
    if (record) { // 如果有记录 
        while(record){
            res.unshift(record); // 就将当前记录的父亲放到前面
            record = record.parent
        }
    }
    return {
        ...location,
        matched: res
    }
}
export default class History {
    constructor(router) {
        this.router = router;
        // 根据记录和路径返回对象,稍后会用于router-view的匹配
        this.current = createRoute(null, {
            path: '/'
        })
    }
    // 核心逻辑
    transitionTo(location, onComplete) {
        // 去匹配路径
        let route = this.router.match(location);
        if(
            location === route.path && 
            route.matched.length === this.current.matched.length){
            return 
        }
        this.updateRoute(route); // 更新路由即可
        onComplete && onComplete();
    }
    updateRoute(route){ // 跟新current属性
        this.current =route;
    }
}
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
export default class VueRouter{
	// ...
    match(location){
        return this.matcher.match(location);
    }
}
1
2
3
4
5
6

终于这回可以完善一下刚才没有写完的match方法

function match(location){ // 稍后根据路径找到对应的记录
    let record = pathMap[location]
    if (record) { // 根据记录创建对应的路由
        return createRoute(record,{
            path:location
        })
    }
    // 找不到则返回空匹配
    return createRoute(null, {
        path: location
    })
}
1
2
3
4
5
6
7
8
9
10
11
12

不难发现路径变化时都会更改current属性,可以把current属性变成响应式的,每次current变化刷新视图即可

export let _Vue;
export default function install(Vue) {
    _Vue = Vue;
    Vue.mixin({ // 给所有组件的生命周期都增加beforeCreate方法
        beforeCreate() {
            if (this.$options.router) { // 如果有router属性说明是根实例
                // ...
                Vue.util.defineReactive(this,'_route',this._router.history.current);
            } 
            // ...
        }
    });
}
1
2
3
4
5
6
7
8
9
10
11
12
13

Vue.util.defineReactive 这个方法是vue中响应式数据变化的核心

当路径变化时需要执行此回调更新_route属性, 在init方法中增加监听函数

history.listen((route) => { // 需要更新_route属性
    app._route = route
});
1
2
3
export default class History {
    constructor(router) {
        this.cb = null;
    }
    listen(cb){
        this.cb = cb; // 注册函数
    }
    updateRoute(route){
        this.current =route;
        this.cb && this.cb(route); // 更新current后 更新_route属性
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
# 两种模式比较
  • hash默认时,要匹配/路由;

image-20201129004728395

import HashHistory from './history/hash'
constructor(options){
    this.matcher = createMatcher(options.routes || []);
    // vue路由有三种模式 hash / h5api /abstract ,为了保证调用时方法一致。我们需要提供一个base类,在分别实现子类,不同模式下通过父类调用对应子类的方法
    this.history = new HashHistory(this);
}
1
2
3
4
5
6

这里我们以hash路由为主,创建hash路由实例

import History from './base'
// hash路由
export default class HashHistory extends History{
    constructor(router){
        super(router);
    }
}
// 路由的基类
export default class History {
    constructor(router){
        this.router = router;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

如果是hash路由,打开网站如果没有hash默认应该添加#/

import History from './base';
function ensureSlash(){ 
    if(window.location.hash){
        return 
    }
    window.location.hash = '/'
}
export default class HashHistory extends History{
    constructor(router){
        super(router);
        ensureSlash(); // 确保有hash
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 编写Link及View组件

# router-link组件

默认是<a>,绑定事件处理;再用jsx方便组件库封装;

export default {
    name: "routerLink",
    props:{
        to:{
            type:String,
            required:true
        },
        tag:{
            type:String
        }
    },
    render(h){
        let tag = this.tag || 'a';
        let handler = ()=>{
            this.$router.push(this.to);
        }
        return <tag onClick={handler}>{this.$slots.default}</tag>
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# router-view组件

要考虑路由嵌套,叠加处理;用 【函数式组件】编写;

函数式组件的特点: 性能高,不用创建实例 = react函数组件 new Ctor().$mount()

理解$vnode_vnode的区别:

  • $vnode 代表的是占位符vnode 组件的标签名的虚拟节点
  • _vnode 代表的是组件内部渲染的虚拟节点;

要点:parent.$vnode && parent.$vnode.data.routerView

export default {
  name: "routerView",
  functional: true, // 函数式组件,函数式组件的特点 性能高,不用创建实例,不用new Ctor().$mount()  = react函数组件
  render(h, { parent, data }) {
    // 调用render方法 说明他一定是一个routerView组件
    let route = parent.$route; // 获取 当前对应要渲染的记录// this.current;
    let depth = 0;
    // App.vue 中渲染组件时  默认会调用render函数,父亲中没有data.routerView属性
    // 渲染第一层App.vue,并且标识当前routerView为true
    data.routerView = true; // 自定义属性
    //<router-view> 是支持嵌套的,嵌套关系层级反映到路由配置就是children层级,while循环从调用当前<router-view>组件的父节点(父组件)开始循环,遇到父节点中的<routerView>,
    //就给depth + 1 ,直到vue根节点结束,此时depth表示当前组件嵌套线路中使用<router-view>的数量,也就是层级,这个depth在下面match匹配时会用到
    while (parent) {
      // router-view的父标签
      //  $vnode 代表的是占位符vnode 组件的标签名的虚拟节点
      //  _vnode 代表的是组件内部渲染的虚拟节点
      if (parent.$vnode && parent.$vnode.data.routerView) {
        depth++;
      }
      parent = parent.$parent; // 不停的找父组件
    }
    // 第一层router-view 渲染第一个record,第二个router-view渲染第二个
    // 这里depth用处的地方,会发现depth的值会作为matched数组的索引来获取对应的route
    let record = route.matched[depth]; // 获取对应层级的记录
    if (!record) {
      return h(); // 空的虚拟节点 empty-vnode  注释节点
    }
    //调用 createElement函数 根据component渲染出 当前组件的vonde(真正执行页面的地方)
    return h(record.component, data);
  },
};
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
# install初始化挂载
import Link from "./components/link";
import View from "./components/view";

// 插件一般用于定义全局组件 全局指令 过滤器 原型方法....
// 两个全局组件router-link router-view;
Vue.component("router-link", Link);
Vue.component("router-view", View);
1
2
3
4
5
6
7

# next

# beforeEach实现

导航守卫 核心就是把所有方法 组合成一个数组 依次调用

this.beforeHooks = [];
beforeEach(fn){ // 将fn注册到队列中
    this.beforeHooks.push(fn);
}
1
2
3
4
# 中间件逻辑

在跳转时处理;将用户函数注册到数组中

function runQueue(queue, iterator,cb) { // 迭代queue
    function step(index){
        if(index >= queue.length){
            cb();
        }else{
            let hook = queue[index];
          // 将本次迭代到的hook 传递给iterator函数中,将下次的权限也一并传入
            iterator(hook,()=>{ 
                step(index+1)
            })
        }
    }
    step(0)
}
export default class History {
    transitionTo(location, onComplete) {
        // 跳转到这个路径
        let route = this.router.match(location);
        if (location === this.current.path && route.matched.length === this.current.matched.length) {
            return
        }
        let queue = [].concat(this.router.beforeHooks);
        const iterator = (hook, next) => {
            hook(route,this.current,()=>{ // 分别对应用户 from,to,next参数
                next();
            });
        }
        runQueue(queue, iterator, () => { // 依次执行队列 ,执行完毕后更新路由
            this.updateRoute(route);
            onComplete && onComplete();
        });
    }
    updateRoute(route) {
        this.current = route;
        this.cb && this.cb(route);
    }
    listen(cb) {
        this.cb = cb;
    }
}
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

# express/koa中间件

# 洋葱模型

解题思路:

node本身并无“中间件”的概念,在express/koa中的中间件往往是一个函数,接收request(请求对象)、response(响应对象)、next(express内部传入的一个函数,用于把控制权从上一个中间件传到下一个中间件)作为参数,接收请求,处理响应,然后将控制权交给下一个中间件。

image-20211211162406854

function middlewareA(req, res, next) {
  next();
}
1
2
3

比如一个自定义打印日志中间件:

const app = express();
const myLogger = function (req, res, next) {
  console.log('LOGGED');
  next(); // 将控制权传递给下一个中间件
};
app.use(myLogger); // 注册中间件
1
2
3
4
5
6

koa的执行顺序是这样的:

const middleware = async function (ctx, next) {
  console.log(1)
  await next()
  console.log(6)
}
const middleware2 = async function (ctx, next) {
  console.log(2)
  await next()
  console.log(5)
}
const middleware3 = async function (ctx, next) {
  console.log(3)
  await next()
  console.log(4)
} 
const all = compose([middleware, middleware2, middleware3]);
app.use(all);
//会依次打印1,2,3,4,5,6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Koa-compose的实现:

Koa 中间件的实现原理,也就是洋葱模型的实现原理,核心在于next的实现。next需要依次调用下一个middleware,当到最后一个的时候结束,这样后面middleware的promise先resolve,然后直到第一个,这样的流程也就是洋葱模型的流程了。

Ps: 一个是递归最好做成尾递归的形式,而是用异步递归而不是同步递归,第三就是形式上用函数复合的形式,这样复合之后的中间件还可以继续复合。

function compose(middleware) {
  return function (context, next) {
    let index = -1
    return dispatch(0)

    function dispatch(i) {
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 组合中间件
// 和express中的next函数意义一样
function compose(middlewareList){
	// return function意思是返回一个函数
	return function(ctx,next){
		let index=-1;
		// 各种中间件调用的逻辑
		function dispatch(i){
			if(i<=index) return Promise.reject(new Error('next() called multiple times'))
			index=i
			const fn=middlewareList[i] || next
			if(fn){
				try{
					// koa中都是async,其返回的是一个promise(对象)
					return Promise.resolve(fn(ctx,function next(){
						return dispatch(i+1)
					}))
				}catch(err){
					return Promise.reject(err)
				}
			}else{
				return Promise.resolve()
			}
		}
		return dispatch(0)
	}
}
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

其功能可以表示为这样(非源码):

async function middleware1() {
  //...
  await (async function middleware2() {
    //...
    await (async function middleware3() {
      //...
    });
    //...
  });
  //...
}
1
2
3
4
5
6
7
8
9
10
11

# 兼容性相关

# Vue里面router-link在电脑上有用,在安卓上没反应怎么解决

Vue路由在Android机上有问题,babel问题,安装babel polypill插件解决

# Vue2中注册在router-link上事件无效解决方法

使用@click.native。原因:router-link会阻止click事件,.native指直接监听一个原生事件

# RouterLink在IE和Firefox中不起作用(路由不跳转)的问题

  • 只用a标签,不使用button标签
  • 使用button标签和Router.navigate方法

# axios

# 常用封装

# 构建初始化

// 创建 axios 实例
const request = axios.create({
  baseURL: process.env.VUE_APP_API_BASE_URL, // API 请求的默认前缀,
  timeout: 10000 // 请求超时时间
})
1
2
3
4
5

# 跨域代理

vue.config.js

  devServer: {
    port: process.env.VUE_APP_PORT,
    proxy: {
      [process.env.VUE_APP_BASE_API]: {
        target: process.env.VUE_APP_API_TARGET_URL,
        changeOrigin: true
        // pathRewrite: {
        //   ['^' + process.env.VUE_APP_BASE_API]: ''
        // }
      }
    }
  },
1
2
3
4
5
6
7
8
9
10
11
12

.env.development

NODE_ENV=development
VUE_APP_PREVIEW=true
VUE_APP_PORT=9000
VUE_APP_API_BASE_URL=/api
VUE_APP_API_TARGET_URL=http://test.samyz.cn:8080
1
2
3
4
5

# 请求/响应拦截

request.interceptors.request.use(config => {
  const token = storage.get(ACCESS_TOKEN)
  // 如果 token 存在让每个请求携带自定义 token 请根据实际情况自行修改
  if (token) {
    config.headers['Access-Token'] = token
  }
  return config
}, errorHandler)

request.interceptors.response.use(response => {
  return response.data
}, errorHandler)
1
2
3
4
5
6
7
8
9
10
11
12

# 错误处理

// 异常拦截处理器
const errorHandler = error => {
  if (error.response) {
    const data = error.response.data
    // 从 localstorage 获取 token
    const token = storage.get(ACCESS_TOKEN)
    if (error.response.status === 403) {
      notification.error({
        message: 'Forbidden',
        description: data.message
      })
    }
    if (error.response.status === 401 && !(data.result && data.result.isLogin)) {
      notification.error({
        message: 'Unauthorized',
        description: 'Authorization verification failed'
      })
      if (token) {
        store.dispatch('Logout').then(() => {
          setTimeout(() => {
            window.location.reload()
          }, 1500)
        })
      }
    }
  }
  return Promise.reject(error)
}
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

# 统一解析请求

export function handleResponse(response, isShowSuccessTips = false, isShowErrorTips = true, localSuccessTips = '', localErrorTips = '') {
  const { success, code, data, msg, resultObject, resultMsg, resultCode } = response || {};
  const msgSuccessNew = localSuccessTips || msg || resultMsg || formatMessage({ id: 'comp.operateSuccess', defaultMessage: '操作成功' });
  const msgErrorNew = localErrorTips || msg || resultMsg || formatMessage({ id: 'COMMON_FAILED', defaultMessage: '失败' });
  const dataNew = resultObject || data;
  const codeNew = resultCode || code;
  const isSuccess = success || (!success && codeNew === '0');
  const resp = {
    success: isSuccess,
    isSuccess,
    code: codeNew,
    data: dataNew,
  };
  if (isSuccess) {
    resp.msg = msgSuccessNew;
    if (isShowSuccessTips) Message.success(msgSuccessNew);
  } else {
    resp.msg = msgErrorNew;
    if (isShowErrorTips) Message.error(msgErrorNew);
  }
  return resp;
}

export function delayPms(data, ms = 1000) {
  return new Promise(resolve => setTimeout(() => resolve(data), ms));
}
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

调用封装

const { isSuccess, data } = handleResponse(res);
if (isSuccess && data) {
  
}
1
2
3
4

# 实现取消重复请求

<!DOCTYPE html>
<html lang="zh-cn">
  <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>Axios 取消重复请求示例</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/qs/6.9.6/qs.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.1/axios.min.js"></script>
  </head>
  <body>
    <h3>Axios 取消重复请求示例</h3>
    <button onclick="sendRequest()">发起请求</button>
    <script>
      const pendingRequest = new Map();

      function generateReqKey(config) {
        const { method, url, params, data } = config;
        return [method, url, Qs.stringify(params), Qs.stringify(data)].join(
          "&"
        );
      }

      function addPendingRequest(config) {
        const requestKey = generateReqKey(config);
        config.cancelToken =
          config.cancelToken ||
          new axios.CancelToken((cancel) => {
            if (!pendingRequest.has(requestKey)) {
              pendingRequest.set(requestKey, cancel);
            }
          });
      }

      function removePendingRequest(config) {
        const requestKey = generateReqKey(config);
        if (pendingRequest.has(requestKey)) {
          const cancel = pendingRequest.get(requestKey);
          cancel(requestKey);
          pendingRequest.delete(requestKey);
        }
      }

      axios.interceptors.request.use(
        function (config) {
          removePendingRequest(config); // 检查是否存在重复请求,若存在则取消已发的请求
          addPendingRequest(config); // 把当前请求添加到pendingRequest对象中
          return config;
        },
        (error) => {
          return Promise.reject(error);
        }
      );

      axios.interceptors.response.use(
        (response) => {
          removePendingRequest(response.config); // 从pendingRequest对象中移除请求
          return response;
        },
        (error) => {
          removePendingRequest(error.config || {}); // 从pendingRequest对象中移除请求
          if (axios.isCancel(error)) {
            console.log("已取消的重复请求:" + error.message);
          } else {
            // 添加异常处理
          }
          return Promise.reject(error);
        }
      );

      async function sendRequest() {
        console.dir(pendingRequest);
        const response = await axios.get(
          "https://jsonplaceholder.typicode.com/todos/1"
          //   "https://localhost:3000/todos/1"
        );
        console.log(response.data);
      }
    </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
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

# 实现缓存请求数据

<!DOCTYPE html>
<html lang="zh-cn">
  <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>Axios 缓存请求数据示例</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/qs/6.9.6/qs.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.1/axios.min.js"></script>
  </head>
  <body>
    <h3>Axios 缓存请求数据示例</h3>
    <button onclick="requestWithCache()">使用缓存</button>
    <button onclick="requestWithoutCache()">不使用缓存</button>
    <script>
      const MemoryCache = {
        data: {},
        set(key, value, maxAge) {
          this.data[key] = {
            maxAge: maxAge || 0,
            value,
            now: Date.now(),
          };
        },
        get(key) {
          const cachedItem = this.data[key];
          if (!cachedItem) return null;
          const isExpired = Date.now() - cachedItem.now > cachedItem.maxAge;
          isExpired && this.delete(key);
          return isExpired ? null : cachedItem.value;
        },
        delete(key) {
          return delete this.data[key];
        },
        clear() {
          this.data = {};
        },
      };

      function generateReqKey(config) {
        const { method, url, params, data } = config;
        return [method, url, Qs.stringify(params), Qs.stringify(data)].join(
          "&"
        );
      }

      function isCacheLike(cache) {
        return !!(
          cache.set &&
          cache.get &&
          cache.delete &&
          cache.clear &&
          typeof cache.get === "function" &&
          typeof cache.set === "function" &&
          typeof cache.delete === "function" &&
          typeof cache.clear === "function"
        );
      }

      function cacheAdapterEnhancer(adapter, options) {
        const {
          maxAge,
          enabledByDefault = true,
          cacheFlag = "cache",
          defaultCache = MemoryCache,
        } = options;
        return (config) => {
          const { url, method, params, forceUpdate } = config;
          let useCache =
            config[cacheFlag] !== undefined && config[cacheFlag] !== null
              ? config[cacheFlag]
              : enabledByDefault;
          if (method === "get" && useCache) {
            const cache = isCacheLike(useCache) ? useCache : defaultCache;
            let requestKey = generateReqKey(config);
            let responsePromise = cache.get(requestKey);
            if (!responsePromise || forceUpdate) {
              responsePromise = (async () => {
                try {
                  return await adapter(config);
                } catch (reason) {
                  cache.delete(requestKey);
                  throw reason;
                }
              })();
              cache.set(requestKey, responsePromise, maxAge);
              return responsePromise;
            }
            return responsePromise;
          }
          return adapter(config);
        };
      }

      const http = axios.create({
        baseURL: "https://jsonplaceholder.typicode.com",
        adapter: cacheAdapterEnhancer(axios.defaults.adapter, {
          enabledByDefault: false,
          maxAge: 5000, //设置5s内有效
        }),
      });

      async function requestWithCache() {
        const response = await http.get("/todos/1", { cache: true });
        console.dir(response);
      }

      async function requestWithoutCache() {
        const response = await http.get("/todos/1", { cache: false });
        console.dir(response);
      }
    </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
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

# 实现请求重试

<!DOCTYPE html>
<html lang="zh-cn">
  <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>Axios 请求重试示例(适配器)</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.1/axios.min.js"></script>
  </head>
  <body>
    <h3>Axios 请求重试示例(适配器)</h3>
    <button onclick="requestWithoutRetry()">请求失败不重试</button>
    <button onclick="requestWithRetry()">请求失败重试</button>
    <script>
      function retryAdapterEnhancer(adapter, options) {
        const { times = 0, delay = 300 } = options;
        return async (config) => {
          const { retryTimes = times, retryDelay = delay } = config;
          let __retryCount = 0;
          const request = async () => {
            try {
              return await adapter(config);
            } catch (err) {
              if (!retryTimes || __retryCount >= retryTimes) {
                return Promise.reject(err);
              }
              __retryCount++;
              // 延时处理
              const delay = new Promise((resolve) => {
                setTimeout(() => {
                  resolve();
                }, retryDelay);
              });
              // 重新发起请求
              return delay.then(() => {
                return request();
              });
            }
          };
          return request();
        };
      }

      const http = axios.create({
        baseURL: "http://localhost:3000/",
        adapter: retryAdapterEnhancer(axios.defaults.adapter, {
          retryDelay: 1000,
        }),
      });

      function requestWithoutRetry() {
        http.get("/users");
      }

      function requestWithRetry() {
        http.get("/users", { retryTimes: 2 });//这里总的调用了三次
      }
    </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
55
56
57
58
59
60

# 限制并发请求

/**
 * 封装axios并发请求数
 */
export default class LimitPromise {
  private _max: number;
  private _count: number;
  private _taskQueue: any[];

  constructor(max: number) {
    // 异步任务“并发”上限
    this._max = max || 6;
    // 当前正在执行的任务数量
    this._count = 0;
    // 等待执行的任务队列
    this._taskQueue = [];
  }

  /**
   * 调用器,将异步任务函数和它的参数传入
   * @param caller 异步任务函数,它必须是async函数或者返回Promise的函数
   * @param args 异步任务函数的参数列表
   * @returns {Promise<unknown>} 返回一个Promise
   */
  call(caller: (...arg: any[]) => any) {
    return new Promise((resolve, reject) => {
      const task = this._createTask(caller, resolve, reject);
      if (this._count >= this._max) {
        this._taskQueue.push(task);
      } else {
        task();
      }
    });
  }

  /**
   * 创建一个任务
   * @param caller 实际执行的函数
   * @param args 执行函数的参数
   * @param resolve
   * @param reject
   * @returns {Function} 返回一个任务函数
   * @private
   */
  _createTask(
    caller: (...arg: any[]) => any,
    resolve: (value: any | PromiseLike<any>) => void,
    reject: (reason?: any) => void
  ) {
    return () => {
      // 实际上是在这里调用了异步任务,并将异步任务的返回(resolve和reject)抛给了上层
      caller()
        .then(resolve)
        .catch(reject)
        .finally(() => {
          // 任务队列的消费区,利用Promise的finally方法,在异步任务结束后,取出下一个任务执行
          this._count--;
          if (this._taskQueue.length) {
            const task = this._taskQueue.shift();
            task();
          }
        });
      this._count++;
    };
  }
}
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

# 双重Cookie防御

let csrfToken = Cookies.get('csrfToken');

function csrfSafeMethod(method) {
  // 以下HTTP方法不需要进行CSRF防护
  return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}

// 这里目前用的jq示范
$.ajaxSetup({
  beforeSend: function(xhr, settings) {
    if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
      xhr.setRequestHeader('x-csrf-token', csrfToken);
      // xhr.setRequestHeader('X-XSRF-TOKEN', csrfToken);
    }
  },
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# vue-ssr

# 原理

# vue2/3/react比较

# vue2与vue3对比

# defineProterty/proxy

Object.defineProperty 接收三个参数:对象,属性名,配置对象; 这里使用的是 Object.defineProperty,这是 Vue 2.0 进行双向数据绑定的写法。在 Vue 3.0 中,它使用 Proxy 进行数据劫持。

1.defineProterty是es5的标准,proxy是es6的标准;

2.proxy可以监听到数组索引赋值,改变数组长度的变化;

3.proxy是监听对象,不用深层遍历,defineProterty是监听属性;

4.利用defineProterty实现双向数据绑定(vue2.x采用的核心)

5.利用proxy实现双向数据绑定(vue3.x会采用)

# Proxy 的优势
  • Proxy 可以直接监听对象而非属性;【1】
  • Proxy 可以直接监听数组的变化;【1】
  • Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等Object.defineProperty 不具备的
  • Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;【1】
  • Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;

涉及到多级对象或者多级数组

  //传递两个参数,一个是object, 一个是proxy的handler
  //如果是不是嵌套的object,直接加上proxy返回,如果是嵌套的object,那么进入addSubProxy进行递归。 
  function toDeepProxy(object, handler) {
    if (!isPureObject(object)) addSubProxy(object, handler);
    return new Proxy(object, handler);

    //这是一个递归函数,目的是遍历object的所有属性,如果不是pure object,那么就继续遍历object的属性的属性,如果是pure object那么就加上proxy
    function addSubProxy(object, handler) {
      for (let prop in object) {
        if (typeof object[prop] == 'object') {
          if (!isPureObject(object[prop])) addSubProxy(object[prop], handler);
          object[prop] = new Proxy(object[prop], handler);
        }
      }
      object = new Proxy(object, handler)
    }

    //是不是一个pure object,意思就是object里面没有再嵌套object了
    function isPureObject(object) {
      if (typeof object !== 'object') {
        return false;
      } else {
        for (let prop in object) {
          if (typeof object[prop] == 'object') {
            return false;
          }
        }
      }
      return true;
    }
  }
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
 let object = {
    name: {
      first: {
        four: 5,
        second: {
          third: 'ssss'
        }
      }
    },
    class: 5,
    arr: [1, 2, {
      arr1: 10
    }],
    age: {
      age1: 10
    }
  }
  //这是一个嵌套了对象和数组的数组
  let objectArr = [{
    name: {
      first: 'ss'
    },
    arr1: [1, 2]
  }, 2, 3, 4, 5, 6]

  //这是proxy的handler
  let handler = {
    get(target, property) {
      console.log('get:' + property)
      return Reflect.get(target, property);
    },
    set(target, property, value) {
      console.log('set:' + property + '=' + value);
      return Reflect.set(target, property, value);
    }
  }
  //变成监听对象
  object = toDeepProxy(object, handler);
  objectArr = toDeepProxy(objectArr, handler);

  //进行一系列操作
  console.time('pro')
  objectArr.length
  objectArr[3];
  objectArr[2] = 10
  objectArr[0].name.first = 'ss'
  objectArr[0].arr1[0]
  object.name.first.second.third = 'yyyyy'
  object.class = 6;
  object.name.first.four
  object.arr[2].arr1
  object.age.age1 = 20;
  console.timeEnd('pro')
  // get:length
  // get:3
  // set:2=10
  // get:0
  // get:name
  // set:first=ss
  // get:0
  // get:arr1
  // get:0
  // get:name
  // get:first
  // get:second
  // set:third=yyyyy
  // set:class=6
  // get:name
  // get:first
  // get:four
  // get:arr
  // get:2
  // get:arr1
  // get:age
  // set:age1=20
  // pro: 4.721ms
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
# Object.defineProperty的优势
  • 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。

# 为什么 Vue 3.0 中使用 Proxy

  1. Vue 中使用 Object.defineProperty 进行双向数据绑定时,告知使用者是可以监听数组的,但是只是监听了数组的 push()、pop()、unshift()、shift()、splice()、sort()、reverse() 这7种方法,其他数组的属性检测不到。【PPU S SSR】
  2. Object.defineProperty 只能劫持对象的属性,因此对每个对象的属性进行遍历时,如果属性值也是对象需要深度遍历,那么就比较麻烦了,所以在比较 Proxy 能完整劫持对象的对比下,选择 Proxy。
  3. 为什么 Proxy 在 Vue 2.0 编写的时候出来了,尤大却没有用上去?因为当时 es6 环境不够成熟,兼容性不好,尤其是这个属性无法用 polyfill 来兼容。(polyfill 是一个 js 库,专门用来处理 js 的兼容性问题-js 修补器)因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写

# js监听对象属性的改变比较

(1)在ES5中可以通过Object.defineProperty来实现已有属性的监听

Object.defineProperty(user,'name',{
    setfunction(key,value){
    }
})
1
2
3
4

缺点:如果id不在user对象中,则不能监听id的变化

(2)在ES6中可以通过Proxy来实现

var  user = new Proxy({}{
 setfunction(target,key,value,receiver){
  }
})

let obj = {name:'',age:'',sex:''  }
let handler = {
  get(target, key, receiver) {
    console.log("get", key);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    console.log("set", key, value);
    return Reflect.set(target, key, value, receiver);
  }
};
let proxy = new Proxy(obj, handler);
proxy.name = "李四";
proxy.age = 24;
// set name 李四
// set age 24
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

这样即使有属性在user中不存在,通过user.id来定义也同样可以这样监听这个属性的变化;

Object.defineProperty(user,'name',{
    setfunction(key,value){
    }
})
var  user = new Proxy({}{
  setfunction(target,key,value,receiver){}
})

let obj = {name:'',age:'',sex:''  }
let handler = {
  get(target, key, receiver) {
    console.log("get", key);
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    console.log("set", key, value);
    return Reflect.set(target, key, value, receiver);
  }
};
let proxy = new Proxy(obj, handler);
proxy.name = "李四";
proxy.age = 24;
// set name 李四
// set age 24
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# Vue3Vue2的区别

# 谈谈Vue3你知道有哪些改进?

  • vue3采用了TS来编写;,vue2对TypeScript支持不友好(所有属性都放在了this对象上,难以推倒组件的数据类型)
  • 大量的API挂载在Vue对象的原型上,难以实现TreeShaking
  • 架构层面对跨平台dom渲染开发支持不友好
  • 支持CompositionAPI。受ReactHook启发
  • vue3中响应式数据原理改成proxy;
  • vdom的对比算法更新,只更新vdom的绑定了动态数据的部分;
  • 对虚拟DOM进行了重写、对模板的编译进行了优化操作...

# 与react对比

react整体是函数式的思想,把组件设计成纯组件,状态和逻辑通过参数传入,所以在react中,是单向数据流

vue的思想是响应式的,也就是基于是数据可变的,通过对每一个属性建立Watcher来监听,当属性变化的时候,响应式的更新对应的虚拟dom。

  • vue组件分为全局注册和局部注册,在react中都是通过import相应组件,然后模版中引用;
  • props是可以动态变化的,子组件也实时更新,在react中官方建议props要像纯函数那样,输入输出一致对应,而且不太建议通过props来更改视图;
  • 子组件一般要显示地调用props选项来声明它期待获得的数据。而在react中不必需,另两者都有props校验机制;
  • 每个Vue实例都实现了事件接口,方便父子组件通信,小型项目中不需要引入状态管理机制,而react必需自己实现;
  • vue使用插槽分发内容,使得可以混合父组件的内容与子组件自己的模板;
  • vue多了指令系统,让模版可以实现更丰富的功能,而React只能使用JSX语法;
  • Vue增加的语法糖computedwatch,而在React中需要自己写一套逻辑来实现;
  • react的思路是all in js,通过js来生成html,所以设计了jsx,还有通过js来操作css,社区的styled-component、jss等;而 vue是把html,css,js组合到一起,用各自的处理方式,vue有单文件组件,可以把html、css、js写到一个文件中,html提供了模板引擎来处理。
  • react做的事情很少,很多都交给社区去做,vue很多东西都是内置的,写起来确实方便一些,比如 reduxcombineReducer就对应vuexmodules, 比如reselect就对应vuex的getter和vue组件的computed, vuex的mutation是直接改变的原始数据,而redux的reducer是返回一个全新的state,所以redux结合immutable来优化性能,vue不需要。
  • react是整体的思路的就是函数式,所以推崇纯组件,数据不可变,单向数据流,当然需要双向的地方也可以做到,比如结合redux-form,组件的横向拆分一般是通过高阶组件。而vue是数据可变的,双向绑定,声明式的写法,vue组件的横向拆分很多情况下用mixin

# Vue跟React的异同点?

相同点:

  • 1.都使用了虚拟dom
  • 2.组件化开发
  • 3.都是单向数据流(父子组件之间,不建议子修改父传下来的数据)
  • 4.都支持服务端渲染

不同点:

  • 1.React的JSX,Vue的template
  • 2.数据变化,React手动(setState),Vue自动(初始化已响应式处理,Object.defineProperty)
  • 3.React单向绑定,Vue双向绑定
  • 4.React的Redux,Vue的Vuex

# 和其它框架(jquery)的区别是什么?哪些场景适合?

区别:vue数据驱动,通过数据来显示视图层而不是节点操作。

场景:数据操作比较多的场景,更加便捷

# Vue与AngularJS的区别

  • Angular采用TypeScript开发, 而Vue可以使用javascript也可以使用TypeScript
  • AngularJS依赖对数据做脏检查,所以Watcher越多越慢;Vue.js使用基于依赖追踪的观察并且使用异步队列更新,所有的数据都是独立触发的。
  • AngularJS社区完善, Vue的学习成本较小

# vue3部分

# vue

# defineProperty 和 Proxy 的区别

为什么要用 Proxy 代替 defineProperty ?好在哪里?

  • Object.defineProperty 是 Es5 的方法,Proxy 是 Es6 的方法
  • defineProperty 不能监听到数组下标变化和对象新增属性,Proxy 可以
  • defineProperty 是劫持对象属性,Proxy 是代理整个对象
  • defineProperty 局限性大,只能针对单属性监听,所以在一开始就要全部递归监听。Proxy 对象嵌套属性运行时递归,用到才代理,也不需要维护特别多的依赖关系,性能提升很大,且首次渲染更快
  • defineProperty 会污染原对象,修改时是修改原对象,Proxy 是对原对象进行代理并会返回一个新的代理对象,修改的是代理对象
  • defineProperty 不兼容 IE8,Proxy 不兼容 IE11

# vue2

Object.definePropertygetset来进行数据劫持,修改,从而响应式,但是它有什么缺点呢😶

  • 由于只有get()、set() 方式,所以只能捕获到属性读取和修改操作,当 新增、删除属性时,捕获不到,导致界面也不会更新。
  • 直接通过下标修改数组,界面也不会自动更新。

# vue3

对于vue3中的响应式,我们用到的Proxy,当然,我们在vue2里面知道,Proxy是什么,是代理,当然,并不是只用到了它,还有个Window上的内置对象Reflect(反射)

  • 通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。
  • 通过Reflect(反射): 对源对象的属性进行操作。
const p=new Proxy(data, {
  // 读取属性时调用
  get (target, propName) {
    return Reflect.get(target, propName)
  },
  //修改属性或添加属性时调用
  set (target, propName, value) {
    return Reflect.set(target, propName, value)
  },
  //删除属性时调用
  deleteProperty (target, propName) {
    return Reflect.deleteProperty(target, propName)
  }
}) 
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 核心原理-简写总体【要点】

# 虚拟DOM转真实DOM

先创建一个虚拟节点对象,调用render方法将虚拟节点转化为真实节点。

let { render } = Vue
const state = { count: 0 };
const vnode = {
    tag: 'div',
    props: {
        style: { color: 'red' }
    },
    children: [{
        tag: 'p',
        props: null,
        children: `vue@3- 计数器 ${state.count}`
    }, {
        tag: 'button',
        props: {
            onClick: () => alert(state.count)
        },
        children: '点我啊'
    }]
}
render(vnode, app);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 实现DOM操作方法
export const nodeOps = {
    insert: (child, parent, anchor) => {
        if (anchor) {
            parent.insertBefore(child, anchor);
        } else {
            parent.appendChild(child);
        }
    },
    remove: (child) => {
        const parent = child.parentNode;
        if (parent) {
            parent.removeChild(child);
        }
    },
    createElement: (tag) => document.createElement(tag),
    setElementText: (el, text) => el.textContent = text
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 将虚拟节点转化为真实节点
import { nodeOps } from './runtime-dom';
export function render(vnode, container) {
    // 渲染分为两种 一种是初始化 一种是是diff
    patch(null, vnode, container);
}
function patch(n1, n2, container) {
    if (typeof n2.tag == 'string') {
        // 将虚拟节点挂载到对应的容器中
        mountElement(n2, container);
    }
}
function mountElement(vnode, container) {
    const { tag, props, children } = vnode;
    let el = nodeOps.createElement(tag);
    if (typeof children === 'string') {
        nodeOps.setElementText(el, children);
    } else if (Array.isArray(children)) {
        mountChildren(children, el);
    }
    nodeOps.insert(el, container, null);
}
// 循环挂载子元素
function mountChildren(children, container) {
    for (let i = 0; i < children.length; i++) {
        const child = children[i];
        patch(null, child, container);
    }
}
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
# 处理DOM中的属性
const onRe = /^on[^a-z]/;
export const nodeOps = {
    // ...
    hostPatchProps: (el, key, value) => {
        const isOn = key => onRe.test(key);
        if (isOn(key)) { // 事件添加
            const name = key.slice(2).toLowerCase();
            el.addEventListener(name, value);
        } else {
            if (key === 'style') { // 样式处理
                for (let key in value) {
                    el.style[key] = value[key];
                }
            } else {
                el.setAttribute(key, value);
            }
        }
    }
}
function mountElement(vnode, container) {
    const { tag, props, children } = vnode;
    let el = (vnode.el = nodeOps.createElement(tag));
    if (props) {
        // 循环所有属性添加属性
        for (let key in props) {
            nodeOps.hostPatchProps(el, key, props[key]);
        }
    }
    if (typeof children === 'string') {
        nodeOps.setElementText(el, children);
    } else if (Array.isArray(children)) {
        mountChildren(children, el);
    }
    nodeOps.insert(el, container, null);
}
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

# 组件的实现

const MyComponent = {
    setup() {
        return () => ({
            tag: 'div',
            props: { style: { color: 'blue' } },
            children: '我是一个组件' + state.count
        })
    }
}
1
2
3
4
5
6
7
8
9

Vue3.x中组件拥有setup方法,当组件渲染前会先调用此方法

const vnode = {
    tag: 'div',
    props: {
        style: { color: 'red' }
    },
    children: [{
            tag: 'p',
            props: null,
            children: `vue@3- 计数器 ${state.count}`
        }, {
            tag: 'button',
            props: {
                onClick: () => alert(state.count)
            },
            children: '点我啊'
        },
        { tag: MyComponent, props: null, children: '' },
        { tag: MyComponent, props: null, children: '' }
    ]
}
render(vnode, app);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

将组件同时传入children属性中。内部根据tag类型做不同的初始化操作

function patch(n1, n2, container) {
    if (typeof n2.tag == 'string') {
        // 将虚拟节点挂载到对应的容器中
        mountElement(n2, container);
    }else if (typeof n2.tag === 'object') {
        // 组件的挂载
        mountComponent(n2, container);
    }
}
function mountComponent(vnode, container) {
    const instance = { // 创建元素实例
        vnode,
        tag: vnode.tag,
        render: null, // setup返回的结果
        subTree: null, // 子元素
    }
    const Component = instance.tag;
    instance.render = Component.setup(); // 调用setUp方法
    instance.subTree = instance.render && instance.render();
    patch(null, instance.subTree, container); // 将子树挂载在元素上
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 响应式原理

effect(() => {
    const vnode = {
        tag: 'div',
        props: {
            style: { color: 'red' }
        },
        children: [{
                tag: 'p',
                props: null,
                children: `vue@3- 计数器` + state.count
            }, {
                tag: 'button',
                props: {
                    onClick: () => state.count++
                },
                children: '点我啊'
            },
            { tag: MyComponent, props: null, children: '' },
            { tag: MyComponent, props: null, children: '' }
        ]
    }
    render(vnode, app);
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

注入副作用函数,当数据变化时可以自动重新执行。

let activeEffect;
export function effect(fn) {
    activeEffect = fn;
    fn();
}
export function reactive(target) {
    return new Proxy(target, {
        set(target, key, value, receiver) {
            const res = Reflect.set(target, key, value, receiver);
            activeEffect();
            return res;
        },
        get(target, key, receiver) {
            const res = Reflect.get(target, key, receiver)
            return res;
        }
    })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

通过proxy代理数据当数据更新时,重新执行effect函数。

依赖收集原理

加入依赖收集功能;

let activeEffect;
export function effect(fn) {
    activeEffect = fn;
    fn();
    activeEffect = null; // 当前effect置为空
}
const targetMap = new WeakMap();
function track(target,key){ // 依赖收集
    let depsMap = targetMap.get(target);
    if(!depsMap){ // 属性对应依赖关系
        targetMap.set(target,(depsMap = new Map()));
    }
    let deps = depsMap.get(key);
    if(!deps){ // 设置set 存放effect
        depsMap.set(key,(deps=new Set()));
    }
    if(activeEffect && !deps.has(activeEffect)){
        deps.add(activeEffect);
    }
}
function trigger(target,key,val){
    const depsMap = targetMap.get(target);
    if(!depsMap){
        return;
    }
    const effects = depsMap.get(key);
    effects && effects.forEach(effect =>effect());
}

export function reactive(target) {
    return new Proxy(target, {
        set(target, key, value, receiver) {
            const res = Reflect.set(target, key, value, receiver);
            trigger(target,key,value); // 触发更新
            return res;
        },
        get(target, key, receiver) {
            const res = Reflect.get(target, key, receiver);
            track(target,key); // 收集依赖
            return res;
        }
    })
}
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

# 组件级更新

function mountComponent(vnode, container) {
    const instance = {
        vnode,
        tag: vnode.tag,
        render: null, // setup返回的结果
        subTree: null, // 子元素
    }
    const Component = instance.tag;
    instance.render = Component.setup(); // 调用setUp方法
    effect(()=>{ 
        // 给组件添加effect,组件中的属性变化时只执行对应方法
        instance.subTree = instance.render && instance.render();
        patch(null, instance.subTree, container); // 将子树挂载在元素上
    })
}

const MyComponent = {
    setup() {
        return () => ({
            tag: 'div',
            props: { style: { color: 'blue' } },
            children: [
                {
                    tag: 'h3',
                    children: '姓名:' + state.name
                },
                {
                    tag: 'button',
                    children: '更新',
                    props: {
                        onClick:()=>state.name = 'ysz'
                    }
                }
            ]
        })
    }
}
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

只更新组件对应区域

# diff算法

详见后面分析

# Reactivity实现-简写【要点】

# 配置Webpack开发环境

安装依赖

yarn add webpack webpack-dev-server webpack-cli html-webpack-plugin @babel/core babel-loader @babel/preset-env -D
1

webpack.config.js文件编写

const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
    entry:'./src/index.js',
    output:{
        filename:'bundle.js'
    },
    devtool:'source-map',
    module:{
        rules:[
            {
                test:/\.js/,use:'babel-loader',exclude:/node_modules/
            }
        ]
    },
    plugins:[
        new HtmlWebpackPlugin({
            template:'./public/index.html'
        })
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

配置.babelrc文件

{
    "presets": ["@babel/preset-env"]
}
1
2
3

执行脚本配置

"scripts": {
    "build:dev": "webpack --mode development",
    "serve": "webpack-dev-server"
}
1
2
3
4

使用Vue3响应式模块

import { reactive, effect } from '@vue/reactivity'
let state = reactive({
    name: 'samy',
    age: 11,
});
effect(() => {
    console.log(state.name)
});
1
2
3
4
5
6
7
8

# 目录结构搭建

  • computed.js
  • effect.js
  • index.js
  • reactive.js
  • ref.js

这里我们要实现的方法分别用 reactiveeffectrefcomputed方法。在index文件中统一整合这些方法进行导出

export {computed} from './computed';
export {effect} from './effect';
export {reactive} from './reactive';
export {ref} from './ref';
1
2
3
4

# reactive实现

import { mutableHandlers } from './baseHandlers'; // 代理相关逻辑
import { isObject } from './util'; // 工具方法

export function reactive(target) {
    // 根据不同参数创建不同响应式对象
    return createReactiveObject(
        target,
        mutableHandlers
    )
}
function createReactiveObject(target, baseHandler) {
    if (!isObject(target)) {
        return target;
    }
    const observed = new Proxy(target, baseHandler);
    return observed
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

baseHandlers

import { isObject, hasOwn, hasChanged } from "./util";
import { reactive } from "./reactive";
const get = createGetter();
const set = createSetter();

function createGetter() {
    return function get(target, key, receiver) {
        // 对获取的值进行放射
        const res = Reflect.get(target, key, receiver);
        console.log('属性获取',key)
        if (isObject(res)) { // 如果获取的值是对象类型,则返回当前对象的代理对象
            return reactive(res);
        }
        return res;
    }
}
function createSetter() {
    return function set(target, key, value, receiver) {
        const oldValue = target[key];
        const hadKey = hasOwn(target, key);
        const result = Reflect.set(target, key, value, receiver);
        if (!hadKey) {
            console.log('属性新增',key,value)
        } else if (hasChanged(value, oldValue)) {
            console.log('属性值被修改',key,value)
        }
        return result;
    }
}
export const mutableHandlers = {
    get, // 当获取属性时调用此方法
    set // 当修改属性时调用此方法
}
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

这里我只对最常用到的get和set方法进行处理,还应该处理hasdeletePropertyownKeys。这里为了快速掌握核心流程就先暂且跳过这些实现

使用reactive方法

let { computed, ref, reactive, effect } = Vue;
const proxy = reactive({name:'samy',age:11,lessons:['架构','高级']});
proxy.name = 'samy';
proxy.lessons[0] = '架构';
1
2
3
4

这里当我们获取属性和更改属性值时就可以触发对应的set和get方法

# effect实现

我们再来看effect的实现,默认effect会立即执行,当依赖的值发生变化时effect会重新执行

// 创建effect时可以传递参数,computed也是基于effect来实现的,只是增加了一些参数条件而已
export function effect(fn, options = {}) {
    const effect = createReactiveEffect(fn, options);
    if (!options.lazy) {
        effect(); // 默认effect应该立即被执行
    }
    return effect;
}
let uid = 0;
const effectStack = []; // 存放effect的队列
let activeEffect; // 当前正在执行的effect 
function createReactiveEffect(fn, options) {
    const effect = function reactiveEffect() {
        if (!effectStack.includes(effect)) {
            try {
                effectStack.push(effect); // 将当前effect放到栈中
                activeEffect = effect; // 标记当前运行的effect
                return fn(); // 执行用户定义的方法
            } finally {
                effectStack.pop(); // 执行完毕后出栈
                activeEffect = effectStack[effectStack.length - 1];
            }
        }
    }
    effect.options = options; // effect所有属性
    effect.id = uid++; // effect的标号
    effect.deps = []; // effect函数对应的属性
    return 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

# 依赖收集实现

在effect方法调用时会对属性进行取值,此时可以进行依赖收集。;以下图示分析的不错;

img

// 收集属性对应的effect
export function track(target, type, key) {}
// 触发属性对应effect执行
export function trigger(target, type, key) {}
operations.js
export const TrackOpTypes = {
    GET: 'get'
}
export const TriggerOpTypes = {
    SET: 'set',
    ADD: 'add'
}
1
2
3
4
5
6
7
8
9
10
11
12

定义收集类型和触发类型

function createGetter() {
    return function get(target, key, receiver) {
        const res = Reflect.get(target, key, receiver);
        // 取值时依赖收集
        track(target, TrackOpTypes.GET, key);
        if (isObject(res)) {
            return reactive(res);
        }
        return res;
    }
}
function createSetter() {
    return function set(target, key, value, receiver) {
        const oldValue = target[key];
        const hadKey = hasOwn(target, key);
        const result = Reflect.set(target, key, value, receiver);
        if (!hadKey) {
            // 设置值时触发更新 - ADD
            trigger(target, TriggerOpTypes.ADD, key);
        } else if (hasChanged(value, oldValue)) {
             // 设置值时触发更新 - SET
            trigger(target, TriggerOpTypes.SET, key, value, oldValue);
        }
        return result;
    }
}
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

track的实现

收集属性对应的effect

export function track(target, type, key) {
    if (activeEffect == undefined) {
        return;
    }
    let depsMap = targetMap.get(target);
    if (!depsMap) { // 如果没有map,增加map
        targetMap.set(target, (depsMap = new Map()));
    }
    let dep = depsMap.get(key); // 取对应属性的依赖表
    if (!dep) { // 如果没有则构建set
        depsMap.set(key, (dep = new Set()));
    }
    if(!dep.has(activeEffect)){ 
        dep.add(activeEffect);
        activeEffect.deps.push(dep);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

trigger实现

触发属性对应effect执行

export function trigger(target, type, key) {
    const depsMap = targetMap.get(target);
    if (!depsMap) {
        return;
    }
    const run = (effects) => {
        if (effects) {effects.forEach(effect => effect());}
    }
    // 有key 就找到对应的key的依赖执行
    if (key !== void 0) {
        run(depsMap.get(key));
    }
    // 数组新增属性
    if (type == TriggerOpTypes.ADD) {
        run(depsMap.get(isArray(target) ? 'length' : ''));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# computed实现原理

  • 计算属性也是一个effect,标记effect lazy:truecomputed:true,提供scheduler方法。在依赖数据更新时会调用schedular方法;
  • 计算属性会根据dirty值进行缓存;
  • 计算属性需要标记__v_isRef 说明计算属性取值时,会自动获取value属性;
import { isFunction } from "./util";
import { effect, track,trigger } from './effect'
import { TriggerOpTypes, TrackOpTypes } from "./operations";
export function computed(getterOrOptions) {
    let getter;
    let setter;

    if (isFunction(getterOrOptions)) {
        getter = getterOrOptions;
        setter = () => {}
    } else {
        getter = getterOrOptions.get;
        setter = getterOrOptions.set;
    }
    let dirty = true;
    let computed;
    let value;
    let runner = effect(getter, {
        lazy: true, // 默认不执行
        computed: true, // 计算属性
        scheduler: () => {
            if (!dirty) {
                dirty = true;
                trigger(computed, TriggerOpTypes.SET, 'value')
            }
        }
    })
    computed = {
        __v_isRef: true,
        get value() {
            if (dirty) {
                value = runner(); // 取值时运行effect
                dirty = false;
            }
            track(computed, TrackOpTypes.GET, 'value');
            return value;
        },
        set value(newValue) {
            setter(newValue)
        }
    }
    return computed;
}
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

trigger支持scheduler

当触发更新时需要分开执行计算属性和effect,计算属性的优先级高于effect,确保effect在执行时可以获得正确计算属性对应的结果;

export function trigger(target, type, key, value, oldValue) {
    const depsMap = targetMap.get(target);
    if (!depsMap) {
        return;
    }
    const effects = new Set();
    const computedRunners = new Set();
    const add = (effectsToAdd) => {
        if (effectsToAdd) {
            effectsToAdd.forEach(effect => {
                if (effect.options.computed) {
                    computedRunners.add(effect)
                } else {
                    effects.add(effect)
                }
            })
        }
    }
    if (key !== void 0) {
        add(depsMap.get(key));
    }
    if (TriggerOpTypes.ADD) {
        add(depsMap.get(isArray(target) ? 'length' : ''));
    }
    const run = (effect) => {
        if (effect.options.scheduler) {
            effect.options.scheduler(effect)
        } else {
            effect()
        }
    }
    computedRunners.forEach(run)
    effects.forEach(run)
}
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

# Ref实现原理

ref的原理就是将一个普通值,转化成对象,并且在获取和设置值时可以增加依赖收集和触发更新的功能;

import { isObject } from "lodash";
import { reactive } from "./reactive";
import { track, trigger } from "./effect";
import { hasChanged } from "./util";
import { TriggerOpTypes,TrackOpTypes } from "./operations";
export function ref(value) {
    return createRef(value);
}
function convert(rawValue) {
    return isObject(rawValue) ? reactive(rawValue) : rawValue
}
function createRef(rawValue) {
    let value = convert(rawValue);
    let r = {
        __v_isRef: true,
        get value() { // 取值依赖收集
            track(r, TrackOpTypes.GET, 'value')
            return value
        },
        set value(newVal) { // 设置时触发更新
            if (hasChanged(newVal, rawValue)) {
                rawValue = newVal;
                value = newVal
                trigger(r, TriggerOpTypes.SET, 'value')
            }
        }
    }
    return r
}
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

# 生命周期

方法名调整

除去加上on 之外,销毁钩子有以下转变,使用时需引入,并在setup中

  1. beforeDestroy => onBeforeUnmount
  2. destroyed => onUnmounted

新增两个调试钩子

  1. onRenderTracked
  2. onRenderTriggered

# 新增组件

# Fragment

# Teleport瞬间移动组件

to:渲染的节点:有点类似于是个插槽

# Suspense异步请求组件

<Subpense>
	<template #default>
        <!-- 加载成功之后显示 -->
    </template>
    <template #fallback>
        <!-- 加载之前显示 -->
    </template>
</Subpense>
1
2
3
4
5
6
7
8

# vuex

# vue-router

# vite

# react部分

# react[10题]

# dva[10题]

# react-router[10题]

# hooks[10题]

# 项目总体化

# 模块化

# monorepo/lerna/yarn

# 微前端

# 简介

微服务是面向服务架构(SOA)的一种变体,把应用程序设计成一系列松耦合的细粒度服务,并通过轻量级的通信协议组织起来

具体地,将应用构建成一组小型服务。这些服务都能够独立部署、独立扩展,每个服务都具有稳固的模块边界,甚至允许使用不同的编程语言来编写不同服务,也可以由不同的团队来管理;

微前端就是将不同的功能按照不同的维度拆分成多个子应用。通过主应用来加载这些子应用。

微前端的核心在于, 拆完后在

# 优势

  1. 独立技术栈:主框架不限制接入应用的技术栈,每个应用的技术栈选型可以配合业务团队选择;
  2. 独立开发、独立部署:子应用仓库独立,前后端可独立开发,部署完成后主框架同步更新子应用;
  3. 独立运行:每个子应用之间状态隔离,单独子应用失败不会影响到其他项目;
  4. 数据共享:子应用可以共享主应用数据,亦可共享兄弟应用数据;

# 劣势

  1. 技术团队有没有能力对新架构兜底

    • 独立模块应用独立部署
    • 多技术栈的接入
    • 解决多应用之间的样式隔离与通用、数据隔离与通信
    • 多团队开发是否保持正常协作
  2. 业务是否高度集中、庞大到需要拆分独立业务

    没有耦合的业务或者过小的业务,走微服务反而是一种负担

  3. 团队是否存在多个技术栈,并且不能够统一

    • 没有统一的技术栈,项目基础样式,架构都会存在差异
    • 项目管理交接会存在难度
    • 整体研发成本反而增加

# 接入场景

  1. 项目技术栈过于老旧,相关技能的开发人员少,功能扩展吃力,重构成本高,维护成本高;

    通过微服务拆分将项目拆分掉,渐进式重构、重写、迭代后期功能;

  2. 项目过于庞大,开发,部署效率底下,且出现问题,造成全局崩盘,不好维护;

  3. 项目组存在不同技术栈,但是需要接入同一套主系统中;

# 模式对比

微服务简单点来说就是将一个大综合的 Bundle 拆成多个子 Bundles,再通过父级容器加载各个子 Bundle,达到想要的拆包,合包效果;

img

# 实现分类

img

# Nginx路由分发

通过配置nginx的location来制定不同路由映射的静态资源访问路径,将多个子项目聚合成一体,来配置不同路由的转发代理;

这种通过nginx路由分发也有局限性:

  • web应用之间的复用性差,每个应用都是独立的,无法共享数据和资源;
  • 每个独立的项目之间切换,需要重新加载,容易出现白屏影响用户体验;

img

# iframe

iFrame 是微前端集成的最简单方式之一。可以说iFrame 里的页面是完全独立的,而且iFrame 页面中的静态资源(js、css)都是相互隔离的,互相不干扰,相当于一个独立的环境,具备沙箱隔离,可以让前端应用之间可以相互独立运行,

但是IFrame局限性也很大,主要包括以下几点👇:

  • 子项目需调整,需要隐藏自身页面中的导航(公共区域);
  • iFrame嵌入的视图控制难,有局限性;
  • 刷新无法保存记录,也就意味着当浏览器刷新状态将消失,后退返回无效
  • iframe 阻塞主页面加载;

img

# 模块通讯

目前应用间的通讯,是通过storage,来存储language字段;

  • window.parent.postMessage(e.data, '*');
  • window.addEventListener('message', function (e) {});

postMessage的第一个参数为消息实体,它是一个结构化对象,即可以通过“JSON.stringify和JSON.parse”函数还原的对象;第二个参数为消息发送范围选择器,设置为“/”意味着只发送消息给同源的页面,设置为“*”则发送全部页面

window.addEventListner('message',(e)=>{
  let {data,source,origin} = e;
  source.postMessage('message echo','/');
});

componentDidMount() {
  window.addEventListener('message', this._handleOnMessage, false);
}
componentWillUnmount() {
  window.removeEventListener('message', this._handleOnMessage, false);
  params = {};
  iframeQuery = {};
}
1
2
3
4
5
6
7
8
9
10
11
12
13

实践通讯

主应用接受消息后处理;

_handleOnMessage = e => {
  const {
    comAcctId,
    userLevel,
    menu: { allMenus },
    reloadAppLocale,
  } = this.props;
  const { panes } = this.state;
  const { data: { type = '', data = '', param = '', currentId, search = null } = {} } = e;
  params[data] = param;
  switch (type) {
    case 'openSelfUrl':
      if (data) {
        const actMenu = this.findMenuByUrl(allMenus, data);
        if (_.isEmpty(actMenu)) {
          message.error(
            `${formatMessage({
              id: 'NO_Permission_Access_Page',
              defaultMessage: '您没有访问该页面的权限!',
            })}`
          );
          return false;
        }
        JumpToRouter(data, { query: search || {} }, reloadAppLocale);
      }
      break;
    // 关联主页上需要跳转的页面,
    // 却不希望被配到通用系统门户管理上的页面,故添加此方法
    case 'openNoLimitPermissionSelfUrl':
      if (data) {
        JumpToRouter(data, { query: search || {} }, reloadAppLocale);
      }
      break;
    case 'openUrl':
      if (search && typeof search === 'object') {
        iframeQuery[data] = search;
      } else if (iframeQuery[data]) {
        delete iframeQuery[data];
      }
      if (this.checkOpenedIframe(data)) {
        setTimeout(() => {
          this.handleRefreshIframePage(data);
        }, 100);
      }
      router.push(`/iframeTab?url=${encodeURIComponent(data)}`);
      break;
    case 'closeUrl':
      this.onEdit(data, 'remove');
      break;
    // 关闭当前页面 同时跳转新页面
    case 'closeAndOpenUrl':
      // 此处 currentId 为要关闭页面的 partyId, data为要跳转页面的地址
      this.handleCloseTab(currentId).then(() => {
        router.push(`/iframeTab?url=${encodeURIComponent(data)}`);
      });
      break;
    case 'deleteUrl':
      this.onDeleteUrl(data, 'remove');
      break;
    case 'reloadUrl':
      if (this.iframeRefs[data]) {
        this.handleRefreshIframePage(data);
      }
      break;
    case 'closeAllAddOpenNewUrl':
      this.closeAllKeepingHome(true);
      break;
    case 'logout':
      this.logout({ isRedirect: true });
      break;
    default:
      break;
  }
};
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

子系统

发送消息

  rindClick = item => {
    if (!_.isEmpty(item)) {
      const { urlAppend, menuType } = item;
      if (menuType === 'O') {
        const token = saas.getSubSystemSignatureId();
        window.open(urlAppend, token);
      } else if (urlAppend.startsWith('http')) {
        window.postMessage(
          {
            type: 'openUrl',
            data: urlAppend,
          },
          '*'
        );
      } else {
        window.postMessage(
          {
            type: 'openSelfUrl',
            data: urlAppend,
          },
          '*'
        );
      }
    }
  };
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

# 集成实践

 renderIframe = pane => {
    if (!pane) {
      return null;
    }
    const name = this.getFrameName(pane);
    let url = pane.iframeUrl;
    if (iframeQuery[url]) {
      const search = iframeQuery[url];
      url += url.indexOf('?') === -1 ? '?' : '&';
      const keys = Object.keys(search);
      const searchArr = [];
      keys.forEach(key => {
        searchArr.push(`${key}=${search[key]}`);
      });
      url += searchArr.join('&');
    }
    const sessionId = window.encodeURIComponent(name);
    if (url) {
      url = url.replace(/\$\(window\.name\)/g, sessionId);
    }
    url = this.addIframeVersion(url);
    const iframeProps = {
      url,
      width: '100%',
      className: 'systemIframe',
      display: 'block',
      position: 'relative',
      allowFullScreen: true,
      name,
    };
    const isIframeTab = pane.menuType === 'M';
    if (isIframeTab) {
      return (
        <Iframe
          ref={ref => {
            this.iframeRefs[pane.partyId] = ref;
          }}
          {...iframeProps}
        />
      );
    }
    return null;
  };
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

# qiankun

2018年 Single-SPA诞生了, single-spa是一个用于前端微服务化的JavaScript前端解决方案 (本身没有处理样式隔离,js执行隔离) 实现了路由劫持和应用加载;

2019年 qiankun基于Single-SPA, 提供了更加开箱即用的 APIsingle-spa + sandbox + import-html-entry) 做到了,技术栈无关、并且接入简单(像iframe一样简单);

# Single-spa

官方号称“一个用于前端微服务化的JavaScript前端解决方案”,single-spa 听起来很高大上,它能兼容各种技术栈,并且在同一个页面中可以使用多种技术框架(React, Vue, Angular等任意技术框架),不用考虑因新的技术框架而去重构旧项目的代码;

img

大概的原理是,首先需要一个主应用(容器应用),需要先注册子应用,然后当url匹配到相应的子应用路由后,将会先请求子应用的资源,然后挂载子应用,同理,当url切换出该子应用路由时,将卸载该应用,以此达到切换子应用的效果,通过子应用生命周期boostrap(获取输出的资源文件) 、 mount、unmount的交替 ;

single-spa 一个基于JavaScript的 微前端 框架,可以用于构建可共存的微前端应用,每个前端应用都可以用自己的框架编写,完美支持 Vue React Angular。可以实现 服务注册 事件监听 子父组件通信 等功能。用于 父项目 集成子项目使用;

single-spa-vue 是提供给使用vue子项目使用的npm包。可以快速和sigle-spa父项目集成,并提供了一些比较便携的api。

# 优缺点

优点

  • 各项目独立开发、部署、迭代,互不影响效率高;
  • 开发团队可以选择自己的技术并及时更新技术栈;
  • 相互之间的依赖性大大降低;
  • 有利于CI/CD,更快的交付产品;

缺点

  • singleSpa 缺陷 不够灵活 不能动态加载js文件
  • 样式不隔离 没有js沙箱的机制;
# 2个常用的api

singleSpa.registerApplication:这是注册子项目的方法。参数如下:

  • appName: 子项目名称
  • applicationOrLoadingFn: 子项目注册函数,用户需要返回 single-spa 的生命周期对象。后面我们会介绍single-spa的生命周期机制
  • activityFn: 回调函数入参 location 对象,可以写自定义匹配路由加载规则。

singleSpa.start:这是启动函数。

# 简介

qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统;

应用通信:

  • 基于URL来进行数据传递,但是传递消息能力弱;
  • 使用全局变量、Redux进行通信;
  • 基于CustomEvent实现通信
  • 基于props主子应用间通信

详见下面【父子传值通讯】;

公共依赖:

  • CDN - externals;
  • webpack联邦模块(webpack5);

# 宿主程序【基座】

项目集成实践, 以主vue,子vue/ react 的版本来示例;

# 宿主项目配置
  • 注册 registerApplication; (子应用必须支持跨域)fetch
  • 启动 start; prefetch: false, // 取消预加载
import { registerMicroApps, start, setDefaultMountApp } from "qiankun";
const apps = [
  {
    name: "vueApp", // 应用的名字
    entry: "//localhost:10000", // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
    container: "#vue", // 容器名
    activeRule: "/vue", // 激活的路径
    props: { a: 1 },
  },
  {
    name: "reactApp",
    entry: "//localhost:20000", // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
    container: "#react",
    activeRule: "/react",
  },
];
registerMicroApps(apps); // 注册应用
//setDefaultMountApp("/app-vue"); //默认跳转门户
start({
  prefetch: false, // 取消预加载
}); // 开启

new Vue({
  router,
  render: (h) => h(App),
}).$mount("#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

做相应loader的加载loading及注册监听回调事件处理;

const microApps = [
  {
    name: "sub-vue",
    entry: process.env.VUE_APP_SUB_VUE,// 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
    activeRule: "/sub-vue",// 激活的路径
  },
  {
    name: "sub-react",
    entry: process.env.VUE_APP_SUB_REACT,
    activeRule: "/sub-react",// 激活的路径
  },
];

const instance = new Vue({
  render: (h) => h(App),
}).$mount("#app");

// 定义loader方法,loading改变时,将变量赋值给App.vue的data中的isLoading
function loader(loading) {
  if (instance && instance.$children) {
    // instance.$children[0] 是App.vue,此时直接改动App.vue的isLoading
    instance.$children[0].isLoading = loading;
  }
}

// 给子应用配置加上loader方法
const apps = microApps.map((item) => {
  return {
    ...item,
    loader,
  };
});
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
// registerMicroApps(apps);
//注册的子应用 参数为数组
registerMicroApps(apps, {
  beforeLoad: (app) => {
    console.log("before load app.name====>>>>>", app.name);
  },
  beforeMount: [
    (app) => {
      console.log("[LifeCycle] before mount %c%s", "color: green;", app.name);
    },
  ],
  afterMount: [
    (app) => {
      console.log("[LifeCycle] after mount %c%s", "color: green;", app.name);
    },
  ],
  afterUnmount: [
    (app) => {
      console.log("[LifeCycle] after unmount %c%s", "color: green;", app.name);
    },
  ],
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

监听路由变化

非必须操作项

    bindCurrent() {
      const path = window.location.pathname;
      if (this.microApps.findIndex((item) => item.activeRule === path) >= 0) {
        this.current = path;
      }
    },
    listenRouterChange() {
      const _wr = function (type) {
        const orig = history[type];
        return function () {
          const rv = orig.apply(this, arguments);
          const e = new Event(type);
          e.arguments = arguments;
          window.dispatchEvent(e);
          return rv;
        };
      };
      history.pushState = _wr("pushState");

      window.addEventListener("pushState", this.bindCurrent);
      window.addEventListener("popstate", this.bindCurrent);

      this.$once("hook:beforeDestroy", () => {
        window.removeEventListener("pushState", this.bindCurrent);
        window.removeEventListener("popstate", this.bindCurrent);
      });
    },
  },
  created() {
    this.bindCurrent();
    NProgress.start();
  },
  mounted() {
    this.listenRouterChange();
  },
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
# 父子传值通信【要点】

**通过设置props传递参数;**主应用设置props: { a: 1 },在子应用中打印;

image-20200731002044497

基础api实现; 监听state状态变化;

主应用:

import { initGlobalState, MicroAppStateActions } from 'qiankun';

// 父应用的初始state
// Vue.observable是为了让initialState变成可响应:https://cn.vuejs.org/v2/api/#Vue-observable。
const initialState = Vue.observable({
  user: {
    name: "samy",
  },
});

//定义全局状态,并返回通信方法,建议在主应用使用,微应用通过 props 获取通信方法。
const actions = initGlobalState(initialState);

// 在当前应用监听全局状态,有变更触发 callback,fireImmediately = true 立即触发 callback
actions.onGlobalStateChange((newState, prev) => {
  console.log("main change", JSON.stringify(newState), JSON.stringify(prev)); // newState: 变更后的状态; prev 变更前的状态
  for (const key in newState) {
    initialState[key] = newState[key];
  }
});
// actions.setGlobalState(initialState);
// actions.offGlobalStateChange();

// 定义一个获取state的方法下发到子应用
actions.getGlobalState = (key) => {
  return key ? initialState[key] : initialState; // 有key,表示取globalState下的某个子级对象; 无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

微应用:

// 从生命周期 mount 中获取通信方法,使用方式和 master 一致
export function mount(props) {
  props.onGlobalStateChange((state, prev) => {
    console.log(state, prev);// state: 变更后的状态; prev 变更前的状态
  });
  props.setGlobalState(state);
}
1
2
3
4
5
6
7

store发送commit消息; store.commit("global/setGlobalStat

  instance = new Vue({
    router,
    store,
    render: (h) => h(App),
  }).$mount(container ? container.querySelector("#app") : "#app");
}

// 独立运行时; 为了避免根 id #app 与其他的 DOM 冲突,需要限制查找范围。
if (!window.__POWERED_BY_QIANKUN__) {
  // // 这里是子应用独立运行的环境,实现子应用的登录逻辑
  // // 独立运行时,也注册一个名为global的store module
  // commonStore.globalRegister(store)
  // // 模拟登录后,存储用户信息到global module
  const userInfo = { name: "我是独立运行时名字叫张三" }; // 假设登录后取到的用户信息
  store.commit("global/setGlobalState", { user: userInfo });
  render();
}

export async function bootstrap() {
  console.log("[vue] vue app bootstraped");
}
// 从生命周期 mount 中获取通信方法,使用方式和 master 一致
export async function mount(props) {
  //props 包含主应用传递的参数  也包括为子应用 创建的节点信息
  console.log("[vue] props from main framework", props);
  props.onGlobalStateChange((state, prev) => {
    console.log(state, prev);// state: 变更后的状态; prev 变更前的状态
  });
  props.setGlobalState(state);
  render(props);
}
export async function unmount() {
  instance.$destroy();
  instance.$el.innerHTML = "";
  instance = null;
  router = null;
}
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
# 路由设置
<template>
  <div>
  <el-menu :router="true" mode="horizontal">
      <!--基座中可以放自己的路由-->
      <el-menu-item index="/">Home</el-menu-item> 
       <!--引用其他子应用-->
      <el-menu-item index="/vue">vue应用</el-menu-item>
      <el-menu-item index="/react">react应用</el-menu-item>
  </el-menu>
    <router-view ></router-view>
    <div id="vue"></div>
    <div id="react"></div>
  </div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 构建配置

设置公共模块内置;

module.exports = {
  transpileDependencies: ["common"], // 包依赖公共模块
  chainWebpack: (config) => {
    config.plugin("html").tap((args) => {
      args[0].title = "vue2-main";
      return args;
    });
  },
};
1
2
3
4
5
6
7
8
9

# 子程序【vue/react】

  • 协议规则;botstrap mount unmount;这三个得暴露出去给父容器用;
  • 动态设置子应用publicPath__webpack_public_path__
  • 设置路由模式为history, 及配置子路由基础路径;
  • 需要父应用加载子应用,将子应用打包成一个个的lib去给父应用使用umd模式;【将子模块打包成类库】
  • 配置子应用得设置跨域处理;"Access-Control-Allow-Origin": "*"

vue中的处理:

跟sigleSpa类似,区别就是标识框架的标识变了;还得设置支持跨域处理

react中的处理:

重写react中的webpack配置文件 (config-overrides.js)

# **子vue应用 **

main.js

let instance = null;
function render(props) {
  instance = new Vue({
    router,
    render: (h) => h(App),
  }).$mount("#app"); // 这里是挂载到自己的html中  基座会拿到这个挂载后的html将其插入进去
}

// 独立运行时; 为了避免根 id #app 与其他的 DOM 冲突,需要限制查找范围。
//这里也可以单独配置文件处理,详见micro-app.js
if (!window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; // 动态添加publicPath
} else {
  // // 这里是子应用独立运行的环境,实现子应用的登录逻辑
  // // 独立运行时,也注册一个名为global的store module
  // commonStore.globalRegister(store)
  // // 模拟登录后,存储用户信息到global module
  const userInfo = { name: "我是独立运行时名字叫张三" }; // 假设登录后取到的用户信息
  store.commit("global/setGlobalState", { user: userInfo });
  render(); // 默认独立运行
}
// 子组件的协议;可以通过props传值
export async function bootstrap(props) {}
export async function mount(props) {
  console.log(props);
  render(props);
}
export async function unmount(props) {
  instance.$destroy();
}
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

micro-app.js

(function () {
  if (window.__POWERED_BY_QIANKUN__) {
    if (process.env.NODE_ENV === "development") {
      __webpack_public_path__ = `//localhost:${process.env.VUE_APP_PORT}${process.env.BASE_URL}`;
      return;
    }
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    // __webpack_public_path__ = `${process.env.BASE_URL}/`
  }
})();
1
2
3
4
5
6
7
8
9
10

路由设置 router/index.js

import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";
Vue.use(VueRouter);
const routes = [
  {
    path: "/",
    name: "Home",
    component: Home,
  },
  {
    path: "/about",
    name: "About",
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () =>
      import(/* webpackChunkName: "about" */ "../views/About.vue"),
  },
];
const router = new VueRouter({
  mode: "history",
  base: "/vue", //process.env.BASE_URL
  routes,
});
export default router;
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

打包配置vue.config.js

const { name: packageName } = require("./package.json");
module.exports = {
  devServer: {
    // port: process.env.VUE_APP_PORT,
    port: 10001,
    headers: {
      "Access-Control-Allow-Origin": "*",
    },
  },
  configureWebpack: {
    output: {
      // library: `${packageName}-[name]`,
      library: "vueApp",
      libraryTarget: "umd",
    },
  },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 子react应用

index.js

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";

function render() {
  ReactDOM.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
    document.getElementById("root")
  );
}
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}
export async function bootstrap() {}
export async function mount() {
  render();
}
export async function unmount() {
  ReactDOM.unmountComponentAtNode(document.getElementById("root"));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

打包配置 config-overrides.js; 重写react中的webpack配置文件 (config-overrides.js)

module.exports = {
  webpack: (config) => {
    config.output.library = "reactApp";
    config.output.libraryTarget = "umd";
    config.output.publicPath = "http://localhost:20000/";
    return config;
  },
  devServer: (configFunction) => {
    return function (proxy, allowedHost) {
      const config = configFunction(proxy, allowedHost);
      config.headers = {
        "Access-Control-Allow-Origin": "*",
      };
      return config;
    };
  },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

配置.env文件

PORT=20000
WDS_SOCKET_PORT=20000
1
2

React路由配置

import { BrowserRouter, Route, Link } from "react-router-dom"
const BASE_NAME = window.__POWERED_BY_QIANKUN__ ? "/react" : "";
function App() {
  return (
    <BrowserRouter basename={BASE_NAME}>
      <Link to="/">首页</Link>
      <Link to="/about">关于</Link>
      <Route path="/" exact render={() => <h1>hello home</h1>}></Route>
      <Route path="/about" render={() => <h1>hello about</h1>}></Route>
    </BrowserRouter>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12

启动脚本修改

  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-app-rewired eject"
  },
1
2
3
4
5
6

# CSS隔离方案

# 子应用之间样式隔离

  • Dynamic Stylesheet动态样式表,当应用切换时移除老应用样式,添加新应用样式;

# 主子应用的样式隔离

  • BEM(Block Element Modifier)预定项目前缀;
  • CSS-Modules打包时生成不冲突的选择器名;
  • Shadw Dom 可以实现真正的隔离机制;【推荐】
  • css-in-js

# 影子Dom

# 定义

shadow-dom 其实是浏览器的一种能力,它允许在浏览器渲染文档(document)的时候向其中的 Dom 结构中插入一棵 DOM 元素子树,但是特殊的是,这棵子树(shadow-dom)并不在主 DOM 树中。

  • 直译过来就是影子DOM:他是独立封装的一段html代码块,他所包含的html标签、css样式和js行为都可以隔离、隐藏起来。
  • 与IFrame有点类似,不过IFrame是另外一个独立的html页面,shadow DOM是当前html页面的一个代码片段。
  • 他是由Web Components (opens new window)里提出的一项技术,其他还有Custom elements、HTML templates、HTML Imports这些。
  • shadow DOM并不是一个特别新的概念,html中的video标签就是使用shadow DOM的一个案例。使用它时,你在html只会看到一个video标签,但实际上播放器上还有一系列按钮和其他操作,这些就都是封装到shadow dom中的,对外界是不可见的。
  • 作用:shadow DOM可以把一部分html代码隔离起来,与外部完全不会互相干扰。
# 使用
  • 操作shadow DOM里的元素其实和普通DOM元素的操作一样,例如添加子节点、设置属性,以及为节点添加自己的样式(例如通过 element.style.foo属性),或者为整个 Shadow DOM添加样式(例如在<style>元素内添加样式)。
  • 使用shadow DOM时,首先要找到一个普通的标签元素(部分标签不行,比如button)作为shadow DOM的宿主元素,我们称为shadow host。然后通过host元素调用attachShadow({mode: 'open'})(mode要设为'open',才能后续往shadow DOM添加元素),attachShadow会返回一个元素,我们称为shadow root,它相当于shadow dom中的根元素。host元素有个属性shadowRoot就是指向它的。
  • 需要注意的一点是:如果shadow host下面有其他普通元素,在添加了Shadow Root后,其他普通元素就不会显示了

图示

image-20200730205533618

示例:

 <div>
        <p>hello world</p>
        <div id="shadowHost"></div>
    </div>
    <script>
        // dom的api  const shadowHost = document.querySelector('#shadowHost')
        let shadowDOM = document.getElementById('shadowHost').attachShadow({ mode: 'open' }); // 外界无法访问 shadow dom   【closed】
        let pElm = document.createElement('p');
        pElm.innerHTML = 'samy hello';
        let styleElm = document.createElement('style');
        styleElm.textContent = `
            p{color:red}
        `
        shadowDOM.appendChild(styleElm)
        shadowDOM.appendChild(pElm);
        // document.body.appendChild(pElm);
    </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# JS沙箱机制

单应用切换沙箱创造一个干净的环境给这个子应用使用,当切换时可以选择丢弃属性和恢复属性当运行子应用时应该跑在内部沙箱环境中

  • 快照沙箱,在应用沙箱挂载或卸载时记录快照,在切换时依据快照恢复环境 (无法支持多实例);
  • Proxy 代理沙箱,不影响全局环境;

图示模式:

image-20200730215023228

# 快照沙箱

# 场景

快照沙箱只能针对单实例应用场景,如果是多个实例同时挂载的情况则无法解决,只能通过proxy代理沙箱来实现

# 原理
  • 激活时将当前window属性进行快照处理
  • 失活时用快照中的内容和当前window属性比对;如果属性发生变化保存到modifyPropsMap中,并用快照还原window属性
  • 在次激活时,再次进行快照,并用上次修改的结果还原window
# 模拟实现
class SnapshotSandbox {
  constructor() {
    this.proxy = window; 
    this.modifyPropsMap = {}; // 修改了那些属性
    this.active();
  }
  active() {
    this.windowSnapshot = {}; // window对象的快照
    for (const prop in window) {
      if (window.hasOwnProperty(prop)) {
        this.windowSnapshot[prop] = window[prop]; // 将window上的属性进行拍照
      }
    }
    Object.keys(this.modifyPropsMap).forEach(p => {
      window[p] = this.modifyPropsMap[p];
    });
  }
  inactive() {
    for (const prop in window) { // diff 差异
      if (window.hasOwnProperty(prop)) {
        // 将上次拍照的结果和本次window属性做对比
        if (window[prop] !== this.windowSnapshot[prop]) {
          this.modifyPropsMap[prop] = window[prop]; // 保存修改后的结果
          window[prop] = this.windowSnapshot[prop]; // 还原window
        }
      }
    }
  }
}

let sandbox = new SnapshotSandbox();
((window) => {
  window.a = 1;
  window.b = 2;
  window.c = 3
  console.log(a,b,c)
  sandbox.inactive();
  console.log(a,b,c)
})(sandbox.proxy);
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

# Proxy代理沙箱【要点】

# 场景

每个应用都创建一个proxy来代理window,好处是每个应用都是相对独立,不需要直接更改全局window属性!

# 模拟实现
class ProxySandbox {
  constructor() {
    const rawWindow = window;
    const fakeWindow = {}
    const proxy = new Proxy(fakeWindow, {
      set(target, p, value) {
        target[p] = value;
        return true
      },
      get(target, p) {
        return target[p] || rawWindow[p];
      }
    });
    this.proxy = proxy
  }
}

let sandbox1 = new ProxySandbox();
let sandbox2 = new ProxySandbox();
window.a = 1;
((window) => {
  window.a = 'hello';
  console.log(window.a)
})(sandbox1.proxy);
((window) => {
  window.a = 'world';
  console.log(window.a)
})(sandbox2.proxy);
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

# 自定义实现微前端

# 组件化

# 配套脚手架

# 流程机制

  • 发布上线;
  • 使用及文档库;

# 搭建私服

  • verdaccio
  • nexus

# 组件构建

  • 机制及加载

  • learn&& umd格式;

  • build构建配置;

# 完全自定义组件库

# 常用组件实现

# 样式配置

样式:

@import './common/_var.scss';
@import './mixins/mixins.scss';

@include blockquote(icon){
    width: 20px;
    height:20px;
    vertical-align: middle;
    fill:red
}
1
2
3
4
5
6
7
8
9

引入:

import Icon from './icon.vue'
import '../../style/icon.scss'
// Button组件是可以单独使用
// import {Button} from 'r-ui';
// app.use(Button)

Icon.install = (app) => { // Vue3.0 app  createApp().use().mount()
    app.component(Icon.name,Icon)
}
export default Icon
1
2
3
4
5
6
7
8
9
10

# icon/button/buttonGroup

  • 样式复用处理;
  • 文档库配置;

# message

简单实现:

import { createApp } from 'vue'
import MessageComponent from './message'
const wrapper = document.createElement('div');
let style = {
    position: 'fixed
    top: '10px',
    left: '20%',
    width: '60%'
}
for (let key in style) {
    wrapper.style[key] = style[key];
}
document.body.appendChild(wrapper);

const Message = (options) => { // 最终调用的就是此方法
    // 弹框需要放到body下,防止某个元素 写了overflowHidden  teleport (固定的面板)
    const messageBox = document.createElement('div');
    let app = createApp(MessageComponent, options); // 单元测试的实现
    app.mount(messageBox); // $el
    wrapper.appendChild(messageBox);
    setTimeout(() => {
        app.unmount();
        wrapper.removeChild(messageBox);
    }, options.duraction || 5000);
}

['success', 'info', 'warning', 'error'].forEach(type => {
    Message[type] = function (options) {
        options.type = type;
        return Message(options);
    }
})
//Message.success({message:''})
export default Message;
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

# tree

  • 功能逻辑;
  • 父子组件通讯;
  • 异步加载;
  • 上下级联动选择;
  • 拖拽实现;
<r-tree :data="treeData" ref="tree" :load="loadFn"></r-tree>
function loadFn(data, cb) {
    if (data.id == 1) {
    setTimeout(() => {
        cb([
        {
            id: "1-1",
            name: "菜单1-1",
            children: [],
        },
        ]);
    }, 1000);
    } else if (data.id == "1-1") {
    setTimeout(() => {
        cb([{ id: "1-1-1", name: "菜单1-1-1" }]);
    }, 1000);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
provide('TREE_PROVIDER', {
    treeMethods: methods,
    load:props.load
});
1
2
3
4
let { treeMethods,load } = inject('TREE_PROVIDER');
const methods = {
    handleExpand() {
        if(data.children && data.children.length == 0){// 如果没有儿子是空的
            if(load){ // 有加载方法就进行加载
                data.loading = true; // 正在加载
                load(data,(children)=>{
                    data.children = children;
                    data.loading = false;// 加载完毕
                })
            }
        }
        data.expand = !data.expand;
    },
    handleChange() {
        data.checked = !data.checked;
        treeMethods.updateTreeDown(data, data.checked);
        treeMethods.updateTreeUp(data, data.checked)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const isLoaded = ref(false); // 用来标识加载完毕
const showArrow = computed(() => { // 是否显示箭头  没儿子 而且也加载完了
    return (data.children && data.children.length > 0) || (load && !isLoaded.value)
});

handleExpand() {
    if (data.children && data.children.length == 0) { // 如果没有儿子是空的
        if (load) { // 有加载方法就进行加载
            data.loading = true; // 正在加载
            load(data, (children) => {
                data.children = children;
                data.loading = false; // 加载完毕
                isLoaded.value = true;
            })
        }
    }else{
        isLoaded.value = true;
    }
    data.expand = !data.expand;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

实现tree组件数据的异步加载。这里要注意数据新增后要重新构建父子关系,监控数据变化重新格式化数据。

watch(data,()=>{
    flatMap = flattenTree(data);
});
1
2
3
  • 控制临界点;
  • 鼠标处理;
  • 自动滚动处理;
  • 底部及切换;

# table

  • 头部,尾部;
  • 二次封装,分页,筛选;

# 第三方库再封装

  • bdp-ui
  • bdp-ui-vue

# 工程化

# 构建

# jenkins

详见后面;

# 部署

# nginx

详见后面;

# pm2

# 可视化

# 图标组件

# echart

# g2

# x6

# 低代码

# x-render

# 自动化

# git使用

git pull

git pull --rebase

# linux脚本

# JKS构建

# nginx部署

# 常用操作命令

sudo systemctl enable nginx # 设置开机启动 
sudo service nginx start # 启动 nginx 服务
sudo service nginx restart # 重启 nginx 服务
sudo service nginx stop # 停止 nginx 服务

nginx -s reload 	#重新加载Nginx配置文件,然后以优雅的方式重启Nginx
nginx -s stop   	#强制停止Nginx服务
nginx -s quit   	#优雅地停止Nginx服务(即处理完所有请求后再停止服务)

sudo service nginx -t # 检查语法
sudo service nginx reload # 重新加载配置,一般是在修改过 nginx 配置文件时使用。

ps -ef | grep nginx # 查看服务进程
ps aux | grep nginx # 或者这个命令

tail -n 10 -f /var/log/nginx/access.log # 查看日志
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cd /etc/nginx
sudo vim /sites-available/test-8181.conf
sudo ln -s ../sites-available/test-8181.conf test-8181.conf

sudo nginx -t
sudo nginx -s reload
curl xxxx/api/xxx

sudo service nginx restart
1
2
3
4
5
6
7
8
9

拷贝/下载相关命令scp:

cd insure-adm
#从本地上传服务器
scp -P xx -r public/uploads ubuntu@45.xx.151:/home/ubuntu/deploy/xx-adm/current/public

#从服务器下载到本地
scp -P xx -r ubuntu@45.xx.151:/home/ubuntu/deploy/insure-adm/current/public/uploads public

#从本地上传自己开发机
scp -r public/uploads samyit@192.168.11.xx:/home/samyit/samy/work/xx/code/kInfo/public
#ps:win平台下,要用git bash 终端
pm2 deploy deploy.json stage #setup

tail -n 100 -f  ~/logs/insure-adm/common-error.log
tail -n 100 -f  ~/logs/insure-adm/insure-adm-web-error.log
tail -n 100 -f  /logs/insure-adm/common-error.log

tail -n 100 /Users/samy/logs/master-stderr.log
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 部署

部署结构图

img

# 配置

下面是一个nginx配置文件的基本结构:

模块 功能
main nginx的全局配置,对全局生效。
events 配置影响nginx服务器或与用户的网络连接。
http 可以嵌套多个server,配置代理,缓存,日志定义等绝大多数功能和第三方模块的配置。
server 配置虚拟主机的相关参数,一个http中可以有多个server。
location 配置请求的路由,以及各种页面的处理情况。
upstream 配置后端服务器具体地址,负载均衡配置不可或缺的部分。

简化版配置:

# 全局区   有一个工作子进程,一般设置为CPU数 * 核数
worker_processes  1;
events {
    # 一般是配置nginx进程与连接的特性
    # 如1个work能同时允许多少连接,一个子进程最大允许连接1024个连接
    worker_connections  1024;
}
# 配置HTTP服务器配置段
http {
    # 配置虚拟主机段
    server {
        # 定位,把特殊的路径或文件再次定位。
        location  {
 			...
        }
    }
    server {
        ...
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

基本示例及说明:

# 定义Nginx运行的用户和用户组
user  nginx;
# nginx进程数,建议设置为等于CPU总核心数
worker_processes  1;   
# 错误日志存放目录和类型
error_log  /var/log/nginx/error.log warn;
# 进程文件
pid        /var/run/nginx.pid;

events {
    worker_connections  1024; # 单个进程最大连接数(最大连接数=连接数*进程数)
}

# 设定http服务器
http {
    include       /etc/nginx/mime.types;   # 文件扩展名与类型映射表
    default_type  application/octet-stream;  # 默认文件类型
    #设置日志的格式
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;   # nginx访问日志存放位置
    # 开启高效文件传输模式,sendfile指令指定nginx是否调用sendfile函数来输出文件,对于普通应用设为 on。
    # 如果用来进行下载等应用磁盘IO重负载应用,可设置为off,以平衡磁盘与网络I/O处理速度,降低系统的负载。
    # 注意:如果图片显示不正常把这个改成off。
    sendfile        on;   
    tcp_nopush     on;    # 防止网络阻塞
    keepalive_timeout  65;  # 长连接超时时间,单位是秒
    gzip  on;  # 开启gzip压缩
    include /etc/nginx/conf.d/*.conf; # 包含的子配置项位置和文件,表示conf.d文件夹下.conf后缀的文件都会归入nginx配置中
    # 服务配置
    server{
        listen 80;# 监听端口
        # 根目录下
        location / {
            proxy_pass http://myproject; # 选择哪个服务器列表
        }
    }
}
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
# 配套工具
# pm2
# yarm

# 静态文件

# 管理虚拟主机
server {  
  listen 80;
  #listen  2022; #基于端口
  server_name samyz.cn; #基于域名
  #server_name  192.168.197.142; #基于IP地址
  location / {              
    root    /home/samy/deploy/api-doc;  
    index index.html;                 
  }  
  #vue history模式
  location / {
    root /home/samy/deploy/avue/dist;
    try_files $uri $uri/ /index.html;
    index index.html;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这里推荐一个修改 Host 的工具(Switchhosts!),可以自行百度搜索下载安装,修改好Host文件后,重新用浏览器访问 samyz.cn:2022 (opens new window) ,访问结果会和访问 http://localhost:80 的页面一样。

# 静态资源
server {
    location / {
        root /data/www; # 根目录
        autoindex on; # 自动显示目录下的index.html页面
        expires 30d; # 缓存30天
    }
    location ~* \.(png|gif|jpg|jpeg)$ {
        root    /root/static/;
        autoindex on;
        access_log  off;
        expires     10h;# 设置过期时间为10小时
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

匹配以 png|gif|jpg|jpeg为结尾的请求,并将请求转发到本地路径, root中指定的路径即nginx本地路径。同时也可以进行一些缓存的设置。

# 配置gzip

全部设置:

    gzip                    on;
    gzip_http_version       1.1;
    gzip_comp_level         5;
    gzip_min_length         1000;
    gzip_types text/csv text/xml text/css text/plain text/javascript application/javascript application/x-javascript application/json application/xml;
    gzip_disable    "MSIE [1-6]\.";     # IE6无效
1
2
3
4
5
6

示范:

location ~ .*\. (jpg|png|gif)$ {    
  gzip off; #关闭压缩    
  root /data/www/images;
}
location ~ .*\. (html|js|css)$ {    
  root /data/www/html;
  gzip on; #启用压缩    
  gzip_min_length 1k; # 超过1K的文件才压缩    
  gzip_http_version 1.1; # 启用gzip压缩所需的HTTP最低版本    
  gzip_comp_level 9; # 压缩级别,压缩比率越高,文件被压缩的体积越小    
  gzip_types text/css application/javascript; # 进行压缩的文件类型   
}

location ~* ^.+\.(jpg|jpeg|gif|png|ico|css|js|pdf|txt){
  root /var/www/pro/mall/current/public
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

并不是每个浏览器都支持 gzip的,如何知道客户端是否支持 gzip呢,请求头中的 Accept-Encoding来标识对压缩的支持

image-20211203204827352

# 正反向代理

代理是在服务器和客户端之间假设的一层服务器,代理将接收客户端的请求并将它转发给服务器,然后将服务端的响应转发给客户端。

不管是正向代理还是反向代理,实现的都是上面的功能。

代理有正向代理和反向代理两种,都是用proxy_pass指令来实现。

  • 一句话解释正向代理,正向代理的对象是用户,用户知道访问那个服务器,而服务器不知道是哪个用户访问它。
  • 一句话解释反向代理,反向代理的对象是服务器,用户不知道访问了哪个服务器,而服务器知道那个用户访问它。
# 正向代理

正向代理是为客户端服务的,通过代理客户端的请求,去访问客户端本身无法直接访问的服务器,翻墙工具就是最常见的正向代理。正向代理对于客户端透明,对于服务器端不透明。

nginx配置示例:

server {
    resolver 8.8.8.8;       #指定DNS服务器IP地址 
    listen 80;
    location / {
        proxy_pass http://$host;     #设定代理服务器的协议和地址 
        proxy_set_header HOST $host;
        proxy_buffers 256 4k;
        proxy_max_temp_file_size 0k;
        proxy_connect_timeout 30;
        proxy_send_timeout 60;
        proxy_read_timeout 60;
        proxy_next_upstream error timeout invalid_header http_502;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
resolver 8.8.8.8 # 谷歌的域名解析地址
server { 
    location / {
        # 当客户端请求我的时候,我会把请求转发给它      
        # $http_host 要访问的主机名 $request_uri 请求路径      
        proxy_pass http://$http_host$request_uri;
    }
}
1
2
3
4
5
6
7
8
# 反向代理

反向代理是指为服务端服务的,反向代理接收来自客户端的请求,帮助服务器进行请求转发、负载均衡等。通常解决前端跨域问题也是通过反向代理实现的。反向代理对于服务端是透明的,对于客户端是不透明的。

nginx配置示例:

server {
    listen       80;
    server_name  xx.a.com; // 监听地址
    location / {
        root html;
        proxy_pass xx.b.com; // 请求转向地址
        index index.html; // 设置默认页
    }
}
1
2
3
4
5
6
7
8
9
# 跨域

浏览器同源:如果两个页面的协议,端口(如果有指定)和域名都相同,则两个页面具有相同的源。

现在在 fe.server.comdev.server.com发起请求一定会出现跨域。将 server_name设置为 fe.server.com, 然后设置相应的location以拦截前端需要跨域的请求,最后将请求代理回 dev.server.com。如下面的配置:

server {
    listen       80;
    server_name  fe.server.com;
    location /api {
        proxy_pass dev.server.com;
       # proxy_pass  http://127.0.0.1:3000;
        proxy_redirect     off;
        proxy_set_header   Host             $host;        # 传递域名
        proxy_set_header   X-Real-IP        $remote_addr; # 传递ip
        proxy_set_header   REMOTE-HOST      $remote_addr; # 传递远程host
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
       # proxy_set_header   X-Scheme         $scheme;      # 传递协议
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

实现原理:这样可以完美绕过浏览器的同源策略: dev.server.com访问 nginxfe.server.com属于同源访问,nginx对服务端转发的请求不会触发浏览器的同源策略;

# 负载均衡

负载均衡是指nginx将大量客户端的请求合理地分配到各个服务器上,以达到资服务端资源的充分利用和更快的响应请求。

后端服务器状态

后端服务器支持以下的状态配置:

  • down:当前服务器不参与负载均衡
  • backup:当其他节点都无法使用时的备用服务器
  • max_fails:允许请求失败的次数,若到达就会休眠
  • fail_timeout:经过max_fails次失败后,服务器的暂停时间,默认为10s
  • max_conns:限制每个服务器的最大接收连接数
upstream samyz {
 server 127.0.0.1:66 down;
 server 127.0.0.1:77 backup;
 server 127.0.0.1:88  max_fails=3 fail_timeout=10s;
 server 127.0.0.1:99 max_conns=1000;
}
1
2
3
4
5
6

实现负载均衡有几种方案

  • 轮询(默认),每个请求按照时间顺序轮流分配到不同的后端服务器,如果某台后端服务器宕机,Nginx 轮询列表会自动将它去除掉。
  • weight(加权轮询),轮询的加强版,weight 和访问几率成正比,主要用于后端服务器性能不均的场景。
  • ip_hash,每个请求按照访问 IP 的 hash 结果分配,这样每个访问可以固定访问一个后端服务器。
  • url_hash,按照访问 URL 的 hash 结果来分配请求,使得每个URL定向到同一个后端服务器上,主要应用于后端服务器为缓存时的场景。
  • 自定义hash,基于任意关键字作为 hash key 实现 hash 算法的负载均衡
  • fair,按照后端服务器的响应时间来分配请求,响应时间短则优先分配。

示范:在需要使用负载均衡的server中增加

#user  nobody;
worker_processes  4;
events {
    worker_connections  1024;  # 最大并发数
}
http{
    # 待选服务器列表
    upstream balanceServer {
        server 1.1.2.3:80;
        server 1.1.2.4:80;
        server 1.1.2.5:80;
    }

    server {
        server_name  xx.a.com;
        listen 80;
        location /api {
            proxy_pass http://balanceServer;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 其他相关设置

# 请求限制

对于大流量恶意的访问,会造成带宽的浪费,给服务器增加压力。往往对于同一 IP 的连接数以及并发数进行限制。关于请求限制主要有两种类型:

  • limit_conn_module 连接频率限制
  • limit_req_module 请求频率限制
# $binary_remote_addr 远程IP地址 zone 区域名称 10m内存区域大小
limit_conn_zone $binary_remote_addr zone=coon_zone:10m;
server {
 limit_conn conn_zone 1;# conn_zone 设置对应的共享内存区域 1是限制的数量
}

# $binary_remote_addr 远程IP地址 zone 区域名称 10m内存区域大小 rate 为请求频率 1s 一次
limit_req_zone $binary_remote_addr zone=req_zone:10m rate=1r/s;
server {
    location / {
        # 设置对应的共享内存区域 burst最大请求数阈值 nodelay不希望超过的请求被延迟
        limit_req zone=req_zone burst=5 nodelay;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 访问控制

快速实现简单的访问限制

关于访问控制主要有两种类型:

  • -http_access_module 基于 IP 的访问控制
  • -http_auth_basic_module 基于用户的信任登陆

经常会遇到希望网站让某些特定用户的群体(比如只让公司内网)访问,或者控制某个uri不让人访问。

以下是基于 IP 的访问控制:

server {
 location ~ ^/index.html {
  # 匹配 index.html 页面 除了 127.0.0.1 以外都可以访问
  deny 127.0.0.1;
  allow all;
 }
}
1
2
3
4
5
6
7
location / {
    deny  192.168.1.100;
    allow 192.168.1.10/200;
    allow 10.110.50.16;
    deny  all;
}
1
2
3
4
5
6
# 防盗链处理

因为被大肆盗链使用图片而开启了防盗链处理**,防盗链处理是我们需要开启来保护服务器的一种方式**。

防盗链的原理就是根据请求头中 referer 得到网页来源,从而实现访问控制。这样可以防止网站资源被非法盗用,从而保证信息安全,减少带宽损耗,减轻服务器压力。


location ~ .*\.(jpg|png|gif)$ { # 匹配防盗链资源的文件类型
    # 通过 valid_referers 定义合法的地址白名单 $invalid_referer 不合法的返回403  
    valid_referers none blocked 127.0.0.1;
    if ($invalid_referer) {
        return 403;
    }
}
1
2
3
4
5
6
7
8
server {
  # 匹配所有图片
  location ~* \.(gif|jpg|png|bmp)$ {
    # 验证是否为没有来路,或者有来路与域名或白名单进行匹配,也可以写正则,若匹配到字段为0,否则为1
    valid_referers none blocked server_names www.ite.com;
    # 如果验证不通过则返回403错误
    if ($invalid_referer) {
      return 403; 
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
    location /ssr {
        root /data/sites/ssr/;
        proxy_pass http://127.0.0.1:3000; # 代理到服务器本地3000端口上
        proxy_set_header Host alanyf.ssr.com;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header referer "http://$host";
        break;
    }
1
2
3
4
5
6
7
8
9
# websocket处理

详见示范中的设置;

# 示范【推荐】

worker_processes  1;
events {
    worker_connections  1024;
}
http {
    include       mime.types;
    default_type  application/octet-stream;
    access_log  /usr/local/etc/nginx/logs/access.log;  #nginx请求日志地址
    sendfile        on;
    keepalive_timeout  65;
    server {
        listen       80;
        server_name  localhost;
        location / {
            root   html;
            index  index.html index.htm;
        }
        # 前端
        location /blog {
            root /data/sites/;
            autoindex on;
            expires 30d; # 缓存30天
            if (!-e $request_filename) {
                rewrite ^/(.*) /blog/index.html;
            }
        }
        # 后端,只转发 /blog/api 开头的请求
        location /blog/api {
            proxy_pass  http://127.0.0.1:3000;
            proxy_redirect     off;
            proxy_set_header   Host             $host;        # 传递域名
            proxy_set_header   X-Real-IP        $remote_addr; # 传递ip
            proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
            proxy_set_header   X-Scheme         $scheme;      # 传递协议
        }
        # redirect server error pages to the static page /50x.html
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   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
server {
    listen       80;
    #静态文件及服务器
    root /home/samy/deploy/xx-web/vue/;
    server_name  web.xx.cn;
   #接口处理
    location /api/ {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;
        proxy_pass http://localhost:7002;
        proxy_redirect off;
    }
   #上传文件处理
    location /uploads/ {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;
        proxy_pass http://localhost:7002;
        proxy_redirect off;
    }
  #websocket处理
    location /socket.io/ {
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_pass http://localhost:7002;
    }

    location / {
          # index  index.html;
          try_files $uri $uri/ /index.html;
       }
     location /api/1/statistics/person/ {
          return   500;
      }
    location ~* ^.+\.(jpg|jpeg|gif|png|ico|css|js|pdf|txt){
      root /var/www/pro/mall/current/public
    }
    # 图片样式缓存1年
    location ~* /app.*\.(js|css|png|jpg)${
        access_log off;
        expires    365d;
    }
    # html/xml/json 文件不缓存
    location ~* /app.*\.(?:manifest|appcache|html?|xml|json)${
        expires    -1;
    }
}
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

# 性能测试

# ab命令

ab命令全称为:Apache bench,是 Apache 自带的压力测试工具,也可以测试 Nginx、IIS 等其他 Web 服务器。

  • -n 总共的请求数
  • -c 并发的请求数
ab -n 1000 -c 5000 http://127.0.0.1/
1

# 项目核心优化

# html优化

# webpack优化

# vue优化

# 优化角度分类

  • 编码优化
  • 加载性能优化
  • 打包优化
  • 缓存压缩优化
  • 用户体验优化
  • SEO优化

# 编码优化

  • 不要将所有的数据放在data中,data中的数据都会增加getter和setter,会收集对应的watcher;
  • vuev-for时给每项元素绑定事件需要用事件代理;
  • SPA页面采用keep-alive缓存组件;
  • 拆分组件(提高复用性、增加代码的可维护性、减少不必要的渲染);
  • v-if当值为false时内部指令不会执行,具有阻断功能,很多情况下使用v-if替代v-show;
  • key确保唯一性(默认vue采用就地复用策略);
  • 长列表只显示的话,Object.freeze冻结数据;
  • 合理使用路由懒加载,异步组件;
  • 数据持久化的问题(防抖,节流);
  • 尽量采用runtime运行时版本;

# 加载性能优化TODO

  • 第三方模块按需导入(babel-plugin-component);
  • 滚动到可视区域动态加载(vue-virtual-scroll-list);始终加载上中下屏;
  • 图片懒加载(vue-lazyload);

# 打包优化

  • 使用cdn的方式加载第三方模块;
  • 多线程打包happypack;
  • splitChunks抽离公共文件;
  • sourceMap生成;优化 SourceMap
  • Webpack 对图片进行压缩
  • 减少 ES6 转为 ES5 的冗余代码
  • 模板预编译
  • 提取组件的 CSS
  • 构建结果输出分析
  • Vue 项目的编译优化

# 缓存压缩优化

  • 客户端缓存、服务端缓存;
  • 服务端gzip压缩;
  • CDN 的使用
  • 使用 Chrome Performance 查找性能瓶颈

# 用户体验优化

  • 骨架屏app-skeleton
  • app壳app-shell
  • service workerpwa ;

# SEO优化

  • 预渲染插件 prerender-spa-plugin;
  • 服务器渲染ssr;

# react优化

# 项目实践

# 4个核心项目

  • 综合(ember/angular/jsp/vue)
  • react/脚手架/模板/jks构建脚本
  • vue/nodejs/docker【会议系统】
  • app/h5/weex

# 核心功能

# 扫描登陆的实现

# 普通自己系统登录

三个主体

P:手机端扫码登录端;W:pc浏览器端;F:服务端;

  • 在W端生成含有socketId信息的二维码及设置redis超时二维码过期机制;
  • 通过P端扫码,收入用户及密码登录;发送http请求内容包含socketId及token消息;
  • F端拿到token,及socketId查询缓存,再把消息发送到指定的socketId W端中,消息包含token;
  • 指定W端收到消息后,再调整到首页;
# websocket实现
//使用;io.emit('hi', 'all sockets');
//实践:用户扫描二维码登录功能;注意这里生成的二维码手机端要在加载解析&处理,要不然直接获取有微信加的乱码问题;
  socket.on('loginQR', () => {
    let socketId = socket.id;
    let postTokenUrl = `${global.host}#/Mobile/MobileLogin?socketId=${socketId}&b=onyx`;
    QRCode.toDataURL(postTokenUrl)
    .then(url => {
      let cacheData = {
        socketId: socketId,
        imageUrl: url,
      };
      Cache.set(`loginQR_${socketId}`, cacheData, 60 * 10);//十分钟有效
      socket.emit('loginQRUrl', url);
      // global.socketUser[socketId] = socket;
      Cache.set(`socketUser_${socketId}`, socket.id).then(() => {});
    })
    .catch(err => {
      console.error(err);
    });
  });

//微信端登录扫描 ;登录扫描;微信端登录成功后请求,再发送token给web登录;
export function loginWechatScan(req, res) {
  let resData = req.body;
  let token = resData.token;
  let socketId = resData.socketId;
  return Cache.get(`loginQR_${socketId}`).then(data => {
    if(data) {
      if (socketId) {
        global.io.to(socketId).emit('loginWechatScan', token);
        return res.status(200).end();
      } else {
        return res.status(401).json(errorMsg.qr_overdue_error);
      }
    } else {
      return res.status(403).json(errorMsg.qr_overdue_error);
    }
  })
  .catch(handleError(res));
}
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

# 微信扫码登录

# 流程图

img

# 小程序登陆流程

# 流程

  • 通过 wx.login() 获取到用户的code判断用户是否授权读取用户信息,调用wx.getUserInfo 读取用户数据
  • 由于小程序后台授权域名无法授权微信的域名,所以需要自身后端调用微信服务器获取用户信息
  • 通过 wx.request() 方法请求业务方服务器,后端把 appid , appsecret 和 code 一起发送到微信服务器。appid 和 appsecret 都是微信提供的,可以在管理员后台找到
  • 微信服务器返回了 openid 及本次登录的会话密钥 session_key
  • 后端从数据库中查找 openid ,如果没有查到记录,说明该用户没有注册,如果有记录,则继续往下走
  • session_key 是对用户数据进行加密签名的密钥。为了自身应用安全,session_key 不应该在网络上传输
  • 然后生成 session并返回给小程序
  • 小程序把 session 存到 storage 里面
  • 下次请求时,先从 storage 里面读取,然后带给服务端
  • 服务端对比 session 对应的记录,然后校验有效期; 推荐可以用token方式;

# 流程图

image-20211108004545402

# 扩展

实际业务中,我们还需要登录态是否过期,通常的做法是在登录态(临时令牌)中保存有效期数据,该有效期数据应该在服务端校验登录态时和约定的时间(如服务端本地的系统时间或时间服务器上的标准时间)做对比

这种方法需要将本地存储的登录态发送到小程序的服务端,服务端判断为无效登录态时再返回需重新执行登录过程的消息给小程

另一种方式可以通过调用wx.checkSession检查微信登陆态是否过期:

  • 如果过期,则发起完整的登录流程
  • 如果不过期,则继续使用本地保存的自定义登录态

这种方式的好处是不需要小程序服务端来参与校验,而是在小程序端调用AP,流程如下所示:

img

#

#

上次更新: 2022/04/15, 05:41:33
×