Vue2 进阶知识总结

213 阅读5分钟

写在前面: 知识总结于:小马哥尚硅谷

v-if和v-show的区别 (官网解释)

v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。

v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

相比之下,v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。

一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好(eg. 页面上的选项卡、导航栏);如果在运行时条件很少改变,则使用 v-if 较好(eg。登录注册)。

模板解析(compile.js)

1.模板解析的关键对象: compile对象
2.模板解析的基本流程:
        1). 将el的所有子节点取出, 添加到一个新建的文档fragment对象中
        2). 对fragment中的所有层次子节点递归进行编译解析处理
        * 对表达式文本节点进行解析
        * 对元素节点的指令属性进行解析
                * 事件指令解析
                * 一般指令解析
        3). 将解析后的fragment添加到el中显示
3.解析表达式文本节点: textNode.textContent = value
        1). 根据正则对象得到匹配出的表达式字符串: 子匹配/RegExp.$1
        2). 从data中取出表达式对应的属性值
        3). 将属性值设置为文本节点的textContent
4.事件指令解析: elementNode.addEventListener(事件名, 回调函数.bind(vm))
    v-on:click="test"
        1). 从指令名中取出事件名
        2). 根据指令的值(表达式)从methods中得到对应的事件处理函数对象
        3). 给当前元素节点绑定指定事件名和回调函数的dom事件监听
        4). 指令解析完后, 移除此指令属性
5.一般指令解析: elementNode.xxx = value
        1). 得到指令名和指令值(表达式)
        2). 从data中根据表达式得到对应的值
        3). 根据指令名确定需要操作元素节点的什么属性
        * v-text---textContent属性
        * v-html---innerHTML属性
        * v-class--className属性
        4). 将得到的表达式的值设置到对应的属性上
        5). 移除元素的指令属性

数据代理(MVVM.js)

Vue中 数据驱动视图

数据代理 Object.defineProperty

数据代理:通过一个对象代理 对 另一个对象 中属性的操作(读、写)

enumerable:true,//控制属性是否可以枚举,默认false
writable:true,//控制属性是否可以修改,默认false
configurable:true//控制属性是否可以被删除,默认false

getter & setter

当读取person的age属性时,get函数(getter)就会被调用,返回值时age的值

当修改person的age属性时,set函数(setter)就会被调用,且会收到修改后的age的值

<script type="text/javascript">
  let number = 18
  let person = {
    name:'sam',
    gender:'male',
  }

  Object.defineProperty(person,'age',{
    // value:18,
    // enumerable:true,//控制属性是否可以枚举,默认false
    // writable:true,//控制属性是否可以修改,默认false
    // configurable:true,//控制属性是否可以被删除,默认false

    //当读取person的age属性时,get函数(getter)就会被调用,返回值时age的值
    get:function () {
      console.log('读取了age属性了')
      return number
    },
    //当修改person的age属性时,set函数(setter)就会被调用,且会收到修改后的age的值
    set(value) {
      console.log('修改了age属性',value)
      number = value
    }
  })

  console.log(person)
  // console.log(Object.keys(person))
</script>
<!--    数据代理:通过一个对象代理 对 另一个对象 中属性的操作(读、写)-->
    let obj = {x:100}
    let obj2 = {y:200}

    Object.defineProperty(obj2,'x',{
      get() {
        return obj.x
      },
      set(value) {
        obj.x = value
      }
    })
  </script>
</body>

1.png

image.png

image.png image.png

MVC

image.png

MVVM

1.通过一个对象代理对另一个对象中属性的操作(读/写)
2.通过vm对象来代理data对象中所有属性的操作
3.好处: 更方便的操作data中的数据
4.基本实现流程
        1). 通过Object.defineProperty()给vm添加与data对象的属性对应的属性描述符
        2). 所有添加的属性都包含getter/setter
        3). 在getter/setter内部去操作data中对应的属性数据

此时,vue将上面的框提取出来, vue的内部有MVVM的逻辑框架,它只关注与视图层。

image.png

数据绑定

双向数据绑定 -应用 - v- model

  1. data中初始化数据
  2. 修改数据: this.key = value
  3. 数据流:
    1. Vue是单项数据流: Model ---> View
    2. Vue中实现了双向数据绑定: v-model
  • 只会体现在UI控件上 只能应用在有value的属性上,如 input

image.png v-model是语法糖,是v-bind + v-on:input 的体现

image.png

双向数据绑定-实现流程

    1). 双向数据绑定是建立在单向数据绑定(model==>View)的基础之上的
    2). 双向数据绑定的实现流程:
    * 在解析v-model指令时, 给当前元素添加input监听
    * 当input的value发生改变时, 将最新的值赋值给当前表达式所对应的data属性
    

VUE数据劫持和代理,data中保存着初始的数据,利用object.defineProperty 的方法来实现对数据的劫持和代理,这个方法中有getset属性,get的属性用于获取数据的属性,set方法用于监视当前的数据,如果要更新原来data中的数据也用了set的方法。

// 模拟Vue中data选项

let data = {
  username: 'A',
  age: 33
}


// 模拟组件的实例
let _this = {

}

// 利用Object.defineProperty()
for(let item in data){
  // console.log(item, data[item]);
  Object.defineProperty(_this, item, {
    // get:用来获取扩展属性值的, 当获取该属性值的时候调用get方法
    get(){
      console.log('get()');
      return data[item]
    },
    // set: 监视扩展属性的, 只要已修改就调用
    set(newValue){
      console.log('set()', newValue);
      // _this.username = newValue; 千万不要在set方中修改修改当前扩展属性的值,会出现死循环
      data[item] = newValue;//可以在这里修改原始data的值
    }
  })
}

console.log(_this);
// 通过Object.defineProperty的get方法添加的扩展属性不能直接对象.属性修改
_this.username = 'B';
console.log(_this.username);

数据绑定 - 原理: 数据劫持

  • 1.数据绑定 v-model (model==>View):

    1). 一旦更新了data中的某个属性数据, 所有界面上直接使用或间接使用了此属性的节点都会更新(更新)

  • 2.数据劫持

    1). 数据劫持是vue中用来实现数据绑定的一种技术

    2). 基本思想: 通过defineProperty()来监视data中所有属性(任意层次)数据的变化, 一旦变化就去更新界面

  • 3.四个重要对象 截屏2021-09-28 14.09.38.png 1). Observer

      * 用来对data所有属性数据进行劫持的构造函数
      * 给data中所有属性重新定义属性描述(get/set)
      * 为data中的每个属性创建对应的dep对象
    

    2). Dep(Depend)

      * data中的每个属性(所有层次)都对应一个dep对象
      * 创建的时机:
              * 在初始化define data中各个属性时创建对应的dep对象
              * 在data中的某个属性值被设置为新的对象时
      * 对象的结构
              {
                id, // 每个dep都有一个唯一的id
                subs //包含n个对应watcher的数组(subscribes的简写)
              }
              * subs属性说明
                      * 当一个watcher被创建时, 内部会将当前watcher对象添加到对应的dep对象的subs中
                      * 当此data属性的值发生改变时, 所有subs中的watcher都会收到更新的通知, 从而最终更新对应的界面
    

    3). Compile

      * 用来解析模板页面的对象的构造函数(一个实例)
      * 利用compile对象解析模板页面
      * 每解析一个表达式(非事件指令)都会创建一个对应的watcher对象, 并建立watcher与dep的关系
      * complie与watcher关系: 一对多的关系
    

4). Watcher

    * 模板中每个非事件指令或表达式都对应一个watcher对象
    * 监视当前表达式数据的变化
    * 创建的时机: 在初始化编译模板时
    * 对象的组成
                    {
              vm,  //vm对象
              exp, //对应指令的表达式
              callback, //当表达式所对应的数据发生改变的回调函数
              value, //表达式当前的值
              depIds //表达式中各级属性所对应的dep对象的集合对象
                      //属性名为dep的id, 属性值为dep
                    }

5). 总结: dep与watcher的关系: 多对多

* 一个data中的属性对应对应一个dep, 一个dep中可能包含多个watcher(模板中有几个表达式使用到了属性)
* 模板中一个非事件表达式对应一个watcher, 一个watcher中可能包含多个dep(表达式中包含了几个data属性)
* 数据绑定使用到2个核心技术
                    * defineProperty()
                    * 消息订阅与发布

写页面步骤

  1. 拆分组件

  2. 编写静态组件

  3. 编写动态组件

    1. 初始化数据,动态显示初始化界面

    2. 实现与用户交互功能

案例todoList

image.png

  1. 1 拆分组件

image.png

  1. 2 编写静态组件

  2. 3 编写动态组件

    1. 初始化数据,动态显示初始化界面 todoList.vue
<template>
   <ul class="todo-main">
    <TodoItem 
      v-for="(todo,index) in todos" 
      :key='index' 
      :todo='todo'
      :index='index'
      /><!---1  这里要将todo,index的值 传给 v-for="(todo,index) in todos" ,
      所以绑定了:todo,:index,也就是向子组件中传递了todo,index这两个属性 -->
       </ul>
</template>

<script>
import TodoItem from './TodoItem.vue'

  export default{
    props: {
      todos: Array,
      deleteTodo: Function
    },
    components: {
      TodoItem
    }
  }
</script>

todoItem.vue

<template>
  <li>
    <label>
      <input type="checkbox" v-model="todo.complete"/>
      <!---3 这里,子组件 双向绑定了todo.complete属性-->
      <span> {{todo.title}}</span>
    </label>
  </li>
</template>

<script>
  export default{
    //2. 这里用props接受了父组件传过来的todo,index;props需要 指定属性名和属性值的类型,
    props: {
        todo: Object,
        index: Number,
      },
    
  }
</script>
2.  实现与用户交互功能(todoHeader 输入框中交互事件,按回车键确认)
    1)给目标元素加监听事件:--- input 输入框 添加 @keyup.enter="addItem",这里的addItem是一个方法。
    2)添加方法的步骤:
     
    //1.检查输入的合法性
   
    //2.根据输入,生成一个todo对象
    
    //3.添加到todos,这一步要复杂一点。
    
    //4.清除输入
    

todoHeader.vue

<template>
  <div class="todo-header">
      <input
      type="text"
      placeholder="请输入你的任务名称,按回车键确认"
      @keyup.enter="addItem"
      v-model="title"
      />
  </div>
</template>

<script>
  export default {
    props: {
      addTodo: Function
    },
    data() {
      return{
        title: ''
      }
    },
    methods: {
      addItem() {
        //1.检查输入的合法性
        const title = this.title.trim();
        if (!title){
          alert('must input something')
          return
        }
        //2.根据输入,生成一个todo对象
        const todo = {
          title: title,
          complete:false,
        }
        //3.添加到todos, 这里需要更新父组件的状态
        //记住:数据在哪个组件,更新数据的行为(方法)就应该定义在哪个组件
        /*
        (1)去父组件App.vue中添加addTodo的方法
        methods: {
        //添加到todos的方法
        addTodo (todo) {
          this.todos.unshift(todo)
        },
        }
        (2)在App.vue中 将addTodo绑定好, 传给TodoHeader 
        <TodoHeader :addTodo="addTodo"/>  
       (3)在todoHeader.vue 中 声明接受addTodo
            props: {
              addTodo: Function
            },
        */
        this.addTodo(todo)
        //4.清除输入
        this.title = ''
      }
    }
  }
</script>

组件通信的5中方式

1 props 父子 间通信的基本方式

  • 1 先父组件v-model绑定自定义属性
  • 2 子组件中使用props接受父组件传递的数据
  • 3 可以在子组件中任意使用

props 传值两种方法

way 1 :声明接收属性:这个属性就会成为组件对象的属性

props:['comments']

way 2: 这种方法指定了属性名和属性值的类型

props: {
        comment: Object
    }

子 ===> 父

  • 1 先父组件v-model绑定自定义属性
  • 2 在子组件中触发原生的事件 在函数中使用emit触发自定义的childHandler,通过this.emit 触发 自定义的childHandler,通过this.emit()去触发

2 vue的V-on自定义事件

子与父组件的通信方式; 取代 function props; 单不适合隔层组件 和 兄弟组件间的通信

每个Vue实例:

(1)用 $on(事件名,回调函数) 来监听事件,(写在父组件上,代替传函数的方式)

//App.vue
<TodoHeader @addTodo="addTodo"/>
<!-- 
      用 `$on(事件名,回调函数)` 来监听事件,(代替传函数的方式)
      将addTodo绑定好, 传给TodoHeader
      -->
      

(2)用$emit(回调事件名,data)来触发事件。

//todoHeader.vue
methods: {
      addItem() {
        //1.检查输入的合法性
        const title = this.title.trim();
        if (!title){
          alert('must input something')
          return
        }
        //2.根据输入,生成一个todo对象
        const todo = {
          title: title,
          complete:false,
        }
        //3.添加到todos, 这里需要更新父组件的状态,
        this.$emit('addTodo',todo);//用`$emit(回调事件名,data)`来触发事件。addTodo是回调函数,todo是 数据
        //4.清除输入
        this.title = ''
      }
    }

3 pubsub 第三方库(父-孙组件)

原理的一样的:

绑定事件监听 ------> 订阅消息(父组件)

触发事件 ----> 发布消息(子孙组件)

(1) 父组件中 引入

import PubSub from "PubSub-js"

(2) 订阅消息,PubSub.subscribe('事件名',回调函数(msg,index){} ), 这里最终要调用下面 deleteTodo(index){},

 mounted () {//执行异步代码
  // PubSub.subscribe('deleteTodo', function(msg,index){
    PubSub.subscribe('deleteTodo', (msg,index) => {//这里的msg就是前面的'deleteTodo',但是命名了msg,也不可以省去。
      this.deleteTodo(index)//考虑到这里的this指向,改为箭头函数,最好所有的回调函数都用 箭头函数
    })
  },

(3)发布消息 PubSub.publish('事件名', 数据的下标),最终会触发订阅消息的function的调用

deleteItem (){
        const {todo, index, deleteTodo} = this
        if(window.confirm(`确认删除${todo.title}么?`)){
          //(3)发布消息 PubSub.publish('事件名', 数据的下标),最终会触发订阅消息的function的调用
          PubSub.publish('deleteTodo', index)
        }
      }

4 slots 传标签数据,可占位

内置组件 作为承载分发内容的出口,

//todoFooter.vue
<template>
  <div class="todo-footer">
        <label>
          <!-- <input type="checkbox" v-model="isAllChecked"/> -->
         <!--  -->
         <slot name="checkAll"></slot>
        </label>
        <span>
          <!--<span>已完成{{completeSize}}</span> / 全部{{todos.length}}-->
         <slot name="count"></slot>          
        </span>
        <!--<button 
          class="btn btn-danger" 
          v-show="completeSize"
          @click="deleteCompleteTodos"
        >清除已完成任务</button>-->
         <slot name="delete"></slot>          
    </div>
</template>
//App.vue
      <todo-footer>
      <input type="checkbox" v-model="isAllChecked" slot='checkAll'/>
      <span slot="count">已完成{{completeSize}} / 全部{{todos.length}}</span>
      <button 
          slot='delete'
          class="btn btn-danger" 
          v-show="completeSize"
          @click="deleteCompleteTodos"
        >清除已完成任务</button>
      </todo-footer>
 //并且把 todo-footer 组件上的方法也传过来了。

5 vuex 状态管理

简单来说: 对应用中组件的状态进行集中式的管理(读/写),
功能比pubsub强大, 更适用于vue项目

image.png

Vuex 状态管理 相对于 超全局变量(加强版的data),可以响应式,

  • 可以携带token,用户的登录状态,类似cookie,session(),多个组件共享的数据
  • 商品的收藏、购物车中的物品
  • 传递的层数太多

1). state

vuex管理的状态对象 它应该是唯一的

/*状态对象模块*/
import storageUtils from '../utils/storageUtils'//这里是用缓存的数据

export default {
  todos: storageUtils.readTodos()
}

2). mutations

包含多个直接更新state的方法(回调函数)的对象

谁来触发: action中的commit('mutation名称')

只能包含同步的代码, 不能写异步代码

//mutation-types.js
/*包含n个 mutation 名称常量 */
export const ADD_TODO = 'add_todo' // 添加todo
/*包含n个用于直接更新状态的方法的对象模块 */

import {ADD_TODO} from './mutation-types'

export default {
  [ADD_TODO] (state, {todo}) {  // 方法名不是ADD_TODO, 而是add_todo
    state.todos.unshift(todo)
  },

3). actions

包含多个事件回调函数的对象

通过执行: commit()来触发mutation的调用, 间接更新state

谁来触发: 组件中: $store.dispatch('action名称') // 'zzz'

可以包含异步代码(定时器, ajax)

/*包含n个用于间接更新状态的方法的对象模块*/
import {ADD_TODO} from './mutation-types'

export default {
  addTodo ({commit}, todo) {
    // 提交一个comutation请求
    commit(ADD_TODO, {todo}) // 传递给mutation的是一个包含数据的对象
  },
}

4). getters

包含多个计算属性(get)的对象

谁来读取: 组件中: $store.getters.xxx

/*包含n个基于state的getter计算属性方法的对象模块*/
export default {
  // 总数量
  totalSize (state) {
    return state.todos.length
  },

5). modules

包含多个module

一个module是一个store的配置对象

与一个组件(包含有共享数据)对应

6). 向外暴露store对象

export default new Vuex.Store({
	state,
	mutations,
	actions,
	getters
})

7). 组件中:

import {mapGetters, mapActions} from 'vuex'
export default {
	computed: mapGetters(['mmm'])
	methods: mapActions(['zzz'])
}

{{mmm}} @click="zzz(data)"

8). 映射store

main.js
import store from './store'
new Vue({
	store
})

9). store对象

  • 1.所有用vuex管理的组件中都多了一个属性$store, 它就是一个store对象

  • 2.属性:

    state: 注册的state对象

    getters: 注册的getters对象

  • 3.方法:

    dispatch(actionName, data): 分发action

Vue中的数据存储

使用LocalStorage 存储数据的功能模块

  1. 函数(对外暴露一个功能,选择函数)

  2. 对象(对外暴露多个功能,选择对象)

步骤

  1. 新建 util/storageUtils.js 文件
//storageUtils.js

//定义一个常量 
const TODOS_KEY = 'todos_key'

export default{
    //写存储,将todos最新的json数据值,保存到localStorage中
    saveTodos (todos) {
       return window.localStorage.setItem(TODOS_KEY, JSON.stringify(todos));
    },
    //读存储 , 这里的数据应该从localStorage中读取todos,一旦发生变化就得监视到
    readTodos () {
        return JSON.parse(localStorage.getItem(TODOS_KEY) || '[]')
    }
}
  1. 在App.vue中引入,并应用
// (1)引入
import storageUtils from './util/storageUtils.js'
export default {
  name: 'App',
  data () {
    return {
      // 这里的数据应该从localStorage中读取todos,一旦发生变化就得监视到,是深度监视
      todos: storageUtils.readTodos()
    }
  },
  watch:{//监视
    todos:{
      deep:true,//深度监视
      // handler:function(newValue){
      //   storageUtils.saveTodos(newValue)
      // }
      //这里还可以精简一些
      handler: storageUtils.saveTodos
      /* 这里有回调函数的一个知识点,相当于
      handler:function(todos){
        window.localStorage.setItem('todos_key', JSON.stringify(todos))
        }
        其中,saveTodos 不需要加(),不用回调,就是将这个方法传过来了
      */ 
    }
  },

vue-axios

axios: 第三方库, 多用于vue2.x

axios使用:


    // 引入模块
    import axios from 'axios'
    
    // 发送ajax请求
    axios.get(url)
      .then(response => {
        console.log(response.data) // 得到返回结果数据
      })
      .catch(error => {
    	console.log(error.message) //错误信息
      })

二次封装的axios的实现(拦截器)

在请求之前(添加一些参数)、请求之后,加入拦截器,可以对公共错误进行集中处理。

  1. 新建 src/api/axios.js
// 拦截器
import axios from 'axios';
//引入config/index.js
import config from '../config/index'
//设置配置 开发环境和生产环境不一样
const baseUrl  = process.env.NODE_ENV ===  'development' ? config.baseUrl.dev :config.baseUrl.pro

class HttpRequest {
    constructor(baseUrl){
        this.baseUrl = baseUrl
    }
    getInsideConfig() { 
        const config = {
            baseUrl:this.baseUrl,
            header:{

            }
        }
        return config
    }
    interceptors(instance) {
        // 添加请求拦截器
        instance.interceptors.request.use(function (config) {
            // 在发送请求之前做些什么
            console.log('拦截处理请求')
            return config;
        }, function (error) {
            // 对请求错误做些什么
            return Promise.reject(error);
        });

        // 添加响应拦截器
        instance.interceptors.response.use(function (response) {
            // 对响应数据做点什么
            console.log('处理响应')
            return response.data;
        }, function (error) {
            // 对响应错误做点什么
            console.log(error)
            return Promise.reject(error);
        });
    }
    // {
    //     baseUrl:'./api-'
    // }
    request(options){//request(option)是调用上面HttpRequest的方法,options是传入一些参数对象
        //请求
        //  /api/getList  /api/getHome
        const instance = axios.create()
        options = {...(this.getInsideConfig()),...options}//技巧
        this.interceptors(instance)
        return instance(options)
    }
}

export default new HttpRequest(baseUrl)

options = {...(this.getInsideConfig()),...options}

  1. 新建src/config/index.js config 这里的事api/axios里面的一些配置

export default{
    title:'admin',
    baseUrl:{
        //开发环境
        dev:'./api/',
        // 生产环境
        pro:''
    }
}

二次封装的axios的用法

  1. 新建 scr/api/data.js
import axios from './axios'

export const getMenu = () => {
    return axios.request({
        url:'menu',
        method:'GET',
    })
}

2 Home.vue 引入并使用

<script>
import {} from "../../api/data.js"


    mounted() {
        getMenu().then((res)=>{
            console.log(res)
        })
    }
}
</script>

vue-router

vue用来实现SPA的插件

使用vue-router

编写路由的3步

  • 1. 定义路由组件    
    
  • 2. 映射路由
    
  • 3. 编写路由2个标签
    
    1. 创建路由器: router/index.js
// router/index.js
      new VueRouter({
        routes: [
          { // 一般路由
            path: '/about',
            component: about
          },
          { // 自动跳转路由
            path: '/', 
            redirect: '/about'
          }
        ]
      })
2. 注册路由器: main.js
//main.js
       import router from './router'
       	new Vue({
       		router
       	})
3. 使用路由组件标签:
       	<router-link to="/xxx">Go to XXX</router-link>
       	<router-view></router-view>
  • 嵌套路由

    children: [
        {
          path: '/home/news',
          component: news
        },
        {
          path: 'message',
          component: message
        }
     ]
  • 向路由组件传递数据
    params: <router-link to="/home/news/abc/123">
    props: <router-view msg='abc'>
  • 缓存路由组件
    <keep-alive>
      <router-view></router-view>
    </keep-alive>
  • 路由的编程式导航

    this.$router.push(path): 相当于点击路由链接(可以返回到当前路由界面)

    this.$router.replace(path): 用新路由替换当前路由(不可以返回到当前路由 界面)

    this.$router.back(): 请求(返回)上一个记录路由

自定义过滤器

对需要显示的数据进行格式化后再显示

1). 定义过滤器

	Vue.filter(filterName, function(value[,arg1,arg2,...]){
	  // 进行一定的数据处理
	  return newValue
	})

2). 使用过滤器

	<div>{{myData | filterName}}</div>
	<div>{{myData | filterName(arg)}}</div>

预渲染Prerendering 和 服务端渲染 SSR

  1. 服务端渲染的过程为:解析执行JS => 构建HTML页面 => 输出给浏览器
  2. 预渲染:直接输出HTML页面给浏览器

Tips

子组件内部: onmouseenter="" onmouseleave=""

父组件外部 onmouseover="" onmouseout=""

数据在哪个组件,更新数据的行为(方法)就应该定义在哪个组件