vue 汇总

更新视图原理

  • 数据劫持
  • vue中的数据双向绑定 一个是对象 一个是数组
  • 对象要求数据先存在,否则无法更新视图
  • 数组方法都是 vue内容进行重写了 但是length长度的变化和通过索引是无法更新的
  • 数据变化后更新视图操作是异步执行的 调取vm.$el.innerHTML 值是没有变化的
  • 可以通过vm.$nextTick 等视图更新后 获取的就是变化的值
  • 以下修改 不会更新视图
    • vm.info.address = '1234' //原data下没有这个属性
    • vm.arr.length-- //数组长度变化
  • 以下修改 都会更新视图
    • vm.info = {address:'123'} //赋值对象会 会被监控 可参考Observer.js
    • vm.arr.push(1) // 数组方法的增减
    • 在原有的对象内 新增数据
    • vm.$set(vm.info,'address','22222')
let obj = {
    age:'12',
    data:{
        name:'3333333'
    }
}
function observer(obj){
    if(typeof obj === 'object'){
        for(let key in obj){
            defineReactive(obj,key,obj[key])
        }
    }
}
function defineReactive(obj,key,value){
    // 如果value 是一个对象  则递归
    observer(value)
    Object.defineProperty(obj,key,{
        get(){
            console.log('get')
            return value
        },
        set(item){
            // 如果传入的值 是一个对象,也要拦截下
            observer(item)
            console.log('1数据更新了',item)
            value = item
        }
    })
}
observer(obj)
  • 1、如果 后增加的属性 是不会触发setter访问器的, 同样vue后增加的属性 也不会刷新视图 obj.data.s = 'sssssss' //不会出发数据更新
  • 2、对象情况下
    • 修改obj 下面的属性 就会触发 数据更新了 因为传入的是一个对象 {s:"123"} 也会被监控
  obj.data= {
      s:"123"
  }
  • 3、数组情况下
    • defineProperty 只针对对象有用 数组无效, 这种情况下改写所有的数组, 以push为例
  obj.arr = [1,2,3,4] 
  let arr = ['push','pop','shift','unshfit'] //dengdeng
  for (let i=0;i<arr.length;i++){
      let method = arr[i]
      let oldPush = Array.prototype[method]
      Array.prototype[method] = function(item){
          console.log('2数据更新了');
          oldPush.call(this,item)
      }
  }
  obj.arr[1] = '123'
  obj.arr.push(5) //如不改写,不会触发数据更新 

vue实例上的方法

  • vm.$el
  • vm.$mount() 做单元测试
    • 如果实例化没有收到el项目,可以使用 vm.$mount(elementOrSelector) 手动地挂载一个未挂载的实例。
    • 如果没有提供elementOrSelector参数,模板将被渲染为文档之外的的元素(内存当中),必须使用原生 DOM API 把它插入文档中。
  • vm.$options vue内的属性选项
  • vm.$nextTick(()=>{})
  • vm.$watch('data1',(newValue,oldValue)=>{})

指令

  • v-show 控制的是样式 不支持template v-if 控制的是dom 支持template
  • v-for 放在谁身上就循环谁(可以循环对象也可以数组) 里面key值作用
  • 为啥data里面不能放方法(function) 因为里面的this 是window而不是vue实例
  //:key解析 
  <div v-if="flag">
    <span>测试1</span>
    <input type="text" :key='1' />
  </div>
  <div v-else>
    <span>测试2</span>
    <input type="text" :key='2' />
  </div>

  //如果不加key 那么flag 值变化的时候 input不会更新
  //加key 在diff 比较的时候 以便区别,用来区分元素
  //一般不用index作为key

  // 如果fn不传值 方法里面默认参数就是e, 如果传值 fn($event,a) $event也是e 固定写法 a就是别的参数 
  <input type="text" @input="fn" class="ss">
  • 定义指令
    • Vue.directive有2中写法 第二个参数是对象 和 函数
    • 对象里的钩子
      • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置
      • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)
      • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前
      • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用
      • unbind:只调用一次,指令与元素解绑时调用。
    • 钩子函数参数
      • el:指令所绑定的元素,可以用来直接操作 DOM
      • binding:一个对象,包含以下属性
        • name:指令名,不包括 v- 前缀
        • oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用
        • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"
        • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"
        • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }
      • vnode:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情
      • oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用
  //输入的数值 只要3个
  <input type="text" v-model='msg' v-split.xx='msg' />
    Vue.directive('split',function(el,bindings,vnode){
      let ctx = vnode.context;//获取上下文
      ctx[bindings.expression] = el.value.slice(0,3) 
    }
  })

  <input type="text"  v-split.xx='msg' />
  //下面增加了v-model='msg'
  Vue.directive('split',{
    bind(el,bindings,vnode){
      //el 当前dom bindings申明指令的参数 vnode.context上下文
      let ctx = vnode.context;
      el.addEventListener('input',(e)=>{
        let val = e.target.value.slice(0,3);//输入框中的内容 
        ctx[bindings.expression] = val //将输入的值复制给当前的msg
        el.value = val
      })
      el.value = ctx[bindings.expression].slice(0,3)
    }
  })

  // 获取焦点
  Vue.directive('focus',{
    bind(el){
      Vue.nextTick(()=>{
        el.focus();
      })
    }
  })

select/radio/checkbox

  // select  v-model绑定option中的value   option标签中间的显示的给客户看 value给程序员看 
  <select v-model='selectValue'> 
    <!-- 默认 请选择 -->
    <option value="0" disabled>请选择</option>
    <option v-for='list in lists' :value="list.id">{{list.value}}</option>
  </select>

  //radio 当前的radioValue== 男 那么就会自动选中
  男:<input type="radio" v-model="radioValue" value="">
  女:<input type="radio"  v-model="radioValue" value="">
  {{radioValue}}

  //checkbox checkboxValues初始值为数组  当不给value的时候 默认值 false/true 给了value才是他的值
  游泳: <input type="checkbox" v-model="checkboxValues" value="游泳">
  健身: <input type="checkbox" v-model="checkboxValues" value="健身">
  {{checkboxValues}}

修饰符

  • v-model.lazy 转为在 change 事件_之后_进行同步
  • v-model.number 将用户的输入值转为数值类型
  • v-model.trim 自动过滤用户输入的首尾空白字符
  • @keyup.enter
    • 键盘修饰符 @keyup='fn' input按下键盘就触发fn .enter修饰符就是(按下enter键的时候触发)
<!-- 在“change”时而非“input”时更新 -->
<input v-model.lazy="msg">

过滤器

{{name | capitalize(1)}}
Vue.filter('capitalize',(value,val)=>{
    console.log('canshu',value,val)
    return value
})

属性绑定-class

// class 绑定 对象时 isRed为ture class才为red 数组时 可以显示多个
<div :class="{ red: isRed }"></div>
<div :class="[classA, classB]"></div>
<div :class="[classA, { classB: isB, classC: isC }]">

computed&&watch&&methods

  • methods 只要方法放在页面上 页面上任何数据变化 该方法都会执行一次 没有缓存
  • computed 和 watch 区别 computed不支持异步 watch可以
  • computed 可以缓存
  • watch
  • 可以写成对象也可以写成方法
  • 写成对象主要是传递参数 immediate/deep
 watch:{
  //全写
  firstMsg:{
      handler(){
          console.log('firstMsg 变化了')
      },
      immediate:true,//绑定的时候 立即执行(默认不会)
  },
  //简写
  lastMsg(){
      console.log('lastMsg 变化了')
  }
  },
  • computed
  • 可以写成对象也可以写成方法
  • 一般默认都是调用的get方法
  • 写全就是set和get方法都有(一般做双向绑定才用到,全选等功能)

//全选 用computed做
<input type="checkbox" v-model='checkAll'>
<input type="checkbox" v-for='(check,index) in checks' :key='index' v-model='check.value' >
computed:{
// Object.defineProperty来实现

// 全写 computed的set方法在 checkbox 能用到
// 当checkbox 被点击的时候 触发set()
checkAll:{
    get (){
        return this.checks.every(item => item.value)
    },
    set(val){
        this.checks.forEach(item =>{ item.value = val });
    }
}
},

render

  • 渲染渲染
  • 默认main文件中只支持render方法
  • template和 render 同时存在的时候 以render数据为准
  • 脚手架中要用template 要添加一个vue.config.js
module.exports = {
  runtimeCompiler:true
}
  • render
// 多个的话 后面接数组
render:h=>h('h1',{
  class:{a:1},
  style:{color:'red'},
  on:{click(){alret(1)}},
  attrs:{a:1}
},'word')
// 也可以直接接收一个组件或对象 是组件他会将组件解析成对象 App是一个组件
render:h=>h(App)
//  jsx语法 内部自动调用h 名字还只能传递h 尽量不要写箭头函数
render:function(h){
  console.log(h)
  // say方法要提前写好
  return <h1 
    on-click={()=>this.say()}
    class = 'a'
    style={{color:'red'}}
    >点我啊</h1>
}

  • rander组件
export default  {
    functional: true, // 函数式组件 只有render方法不能写template , 设置了他 context才能获取到上下文
    render(h,context){
      // 获取组件内的值  context.slots().default
      console.log(context.slots().default)
      // 获取传递过来的属性 context.props
      return <div>{context.slots().default}</div>
    }
}

lifeCycle

  beforeCreate(){
      //初始化自己的生命周期 获取children 和 parent,并且绑定自己的事件
      //不能操作属性和方法
      console.log(this,this.$data)
  },
  created(){
    // 可以获取数据和调用方法
    // 没有this.$el 不能获取真实dom
      console.log(this.$data,this.data)
  },
  beforeMount(){
      //第一次调用 函数渲染之前
      //会检测template数据,有的话会把template渲染成一个 render函数
      console.log('挂载前')
  },
  // template和 render 同时存在的时候 以render数据为准
  //template会调用render函数 render里面创建一个虚拟dom  最后将虚拟dom渲染成真实的dom
  render(createElement){
    // createElement就是一个虚拟dom
    return createElement('div',{
      attr:{
        id:123
        },
      on:{
        click(){
          console.log('111')
        }
      }
      },'hello')
  }
  mounted(){
      //获取真是的dom this.$el
      console.log('挂载后')
  },
  beforeUpdate(){
      console.log('更新前')
  },
  updated(){//一般不要操作数据 可能会导致死循环
      console.log('更新后')
  },
  beforeDestroy(){
      //当前实例还可以用,这儿一般清除定时器 
      // 移除绑定的方法事件
      console.log('销毁前')
  },
  destroyed(){
      // 实例上的方法 监听都被移除掉
      console.log('销毁后')
  },
  //触发 销毁 第一种路由切换 第二种 vm.$destroy()
  // 销毁 只是针对响应式的数据 绑定的方法事件

components

  • 组件类型
    • 全局组件
    • 局部组件
    • 函数式组件
    • 异步组件
      • 子组件data必须是函数类型,保证数据不会互相影响,通过一个函数返回唯一的对象
// 全局组件 在任何组件中可以直接使用 而且不需要引入 
// 
Vue.component('my-button',{
  data(){
    return{

    }
  }
  template:`<button>点击</button>`
})
let vm = new Vue({el:'#app'})
// 不能写但标签  有bug 而且不符合w3c规范 
<div>
  <my-button></my-button>
</div>
  • 通信
    • 1、props和$emit
    • 2、$attrs和$listeners
      • $attrs子组件没用的属性(props没用接收的)
      • 他可以批量传递属性
    • 3、$parent和$children
    • 4、$refs获取实例
    • 5、父组件中通过provide来提供变量,用Inject接受
      • provide和Inject 和 react的上下文 差不多
      • 组件的数据流 父组件 将数据传递子组件,子组件不能直接更改数据
    • 6、envetBus平级组件数据传递
      • 一个全局的发布订阅方式
      • 适合比较简单的数据流
      • this.emit/this.on (通过全局的vue)
    • 7、vuex状态管理
  • 属性传递
    • msg属性取出来的值 都是String 加: 引号里面是啥就传啥类型 不加:一律按String处理
  <my-button :msg='123' :a='3' :arr="[1]"></my-button>
  • 子组件 this.$attrs 获取所有的父组件传递的属性
    • $attrs子组件没用的属性(props没用接收的)
    • v-bind=$attrs 绑定所有的属性
     <button v-bind='$attrs' @click="btn">触发1</button> 
    
  • props接收的参数会挂载到当前组件的实例上
  • 父级给子集传递参数的时候 会显示在dom上 可以加inheritAttrs:false 隐藏掉
  • 如果接收的属性是对象或者数组 那么必须要用一个函数返回这个对象
  props:{
    msg:{
        type:Number,
        default:123
    },
    arr:{
        type:Array,
        // 数组或者对象 必须写成函数返回的形式
        default:()=>([1,3])
    },
    a:{
        type:Number,
        validator(val){
            //属性校验器,val就是传递过来的值 
            //true说明正常 false说明传值不满足
            return true
        }
    }
  },
  • 方法传递
    • 给组件绑定事件 需要加.native 不加就认为是一个普通的属性, 他会绑定给子组件最外层标签
    • $listeners 获取父组件传递所有的方法
    • v-on=$listeners 绑定所有的方法
  <!-- 父组件 @click='btn' => this.$on('click',btn)-->
  <btn-button a='123' b='sss' @click='btn'></btn-button>
  <!-- 子组件 -->
  <button v-bind='$attrs'>v-bind将父级所有的属性绑定到当前</button> 
  <button @click="$emit('click')">emit触发click方法</button> 
  <button @click="$listeners.click">触发父级click的方法</button> 
  <button v-on="$listeners">v-on将父级所有的方法绑定到当前</button> 

组件通信

  • _uid:每个组件都有唯一的id
  • 平级通信
//子组件 methods
change(){
  this.$parent.cut(this._uid)
}
//父组件 methods
cut(id){
  this.$children.forEach(child => {
    if(child._uid == id){
      //  可以确定是哪一个子组件
      //  对child做处理
    }
  })
}
  • provide通信
    • provide 在上游 提供 inject下游接受 多少层都可以获取到
  //和data 同级
  // 申明
  provide:{
      m1:'根组件提供'
  },
  // 获取
  inject:['m1']
  • ref
    • 如果给dom 就是一个dom
    • 如果给v-for 出来的就是 数组
    • 如果给组件 出来的就是组件的实例
  // 声明
  <div ref="my">dom</div>
  //获取
  this.$refs.my // 当前dom

  // 循环
  <div v-for='item in 3'>
      <div ref='my'>12</div>
  </div>
  this.$refs.my //[div, div, div]

  //组件上添加ref
  <Item ref="my"></Item>
  this.$refs.my //就是当前组件的实例 可以调取里面的方法
  • 父子组件通信
  • this.$emit 发射
  • this.$on 监视
<!-- 父组件 -->
  <div>
  <!-- @updata1 ==> this.$on('updata1',btn1) -->
    <!-- 第一中写法 -->
    <B :s='123' @update='btn1'>
    {{inp}}
    <!-- 第二种写法  将btn1里面的事件 直接写上来 -->
    <B :s='123' @update='value => inp = value'>
    <!-- 第三种  语法糖 -->
    <B :s='123' @update='value => inp = value'>
    
  </div>
  <script>
    methods:{
      btn1(value){
        console.log('value',value)
        this.inp = value
      }
    }
  </script>

<!-- 子组件 -->
  <div>
    <button @click="btn">子传父</button>
  </div>
  <script>
    props:['s'],
    methods:{
      btn(){
        this.$emit('update','dat1')
      }
    },
  </script>
  • 父组组件双向绑定
  • 接收的s和eimt发射的s名字要一样
  • 父组件在传递一个s.sync修饰符过来
  • title1值和s的是就绑定了
<!-- 子组件 -->
<div>
  <button @click="btn">子传父</button>
  {{s}}
</div>
<script>
 props:['s'],
  methods:{
      btn(){
        // update固定写法:后面的s随意更改
        this.$emit('update:s','dat1')
      }
    }
</script>

<!-- 父组件 -->
<div>
  <B :s.sync='title1'></B> 
  {{title1}}  
</div>
<script>
  data(){
      return {
        title1:'ti',
        inp:123,
        isShow : true
      }
  }
</script>
  • v-model 响应式传值
<!-- 父 -->
  <B v-model='s'></B> 
  {{s}} 
<script>
  data(){
      return {
        s:'123',
      }
  },
</script>

<!-- 子 -->
  <button @click="btn">子传父</button>
  {{s}}
<script>
 props:['s'],
  methods:{
    btn(){
      this.$emit('input','dat1')
    }
  },
</script>


动画

  • 常见触发动画的操作 v-if v-show v-for 路由切换
  • css 添加动画 animation transition
  • js添加动画 自带钩子 动画库 velocity
  • 动画分为 单个动画 多个动画
  • 动画默认从当前状态 变化 所有 v-enter-to和v-leave 一般用不到

vue动画 用的transition组件 多个的时候 可以给他取名字

<style>
.show{
  border: 1px solid red;
  background: red;
  width: 100px;
  height: 100px;
}
.v-enter {
  opacity: 0;
}
.v-enter-active,.v-leave-active{
  transition: opacity 2s linear
}
.v-leave-to {
  opacity: 0;
}
</style>

<div @click="btn">点击</div>
<transition>  
    <div class="show" v-show="isShow">
    </div>
</transition>
    
<script>    
  data(){
      return {
        isShow : true
      }
    },
  methods:{
    btn(){
      this.isShow = !this.isShow
    }
  }
</script>    

第三方动画库

  • yarn add animate.css
  • 用法
    • 第一步 在需要动画的标签内 添加animated类
    • 第二步 .v-enter-active 设置 animation: bounceIn 1s ease-in (bounceIn效果看官网)
    • 第三步 .v-leave-active 设置 animation: bounceOut 1s ease-in (bounceOut效果看官网)
// <div @click="btn">点击</div>
// <transition>  
//   <!-- 第一步 添加animated类 -->
//     <div class="show animated" v-show="isShow">
//     </div>
// </transition>

  // data(){
  //     return {
  //       isShow : true
  //     }
  //   },
  // methods:{
  //   btn(){
  //     this.isShow = !this.isShow
  //   }
  // }

//   .show{
//   border: 1px solid red;
//   background: red;
//   width: 100px;
//   height: 100px;
// }
// /* .v-enter {
//   opacity: 0;
// } */
// .v-enter-active{
//   animation: bounceIn 1s ease-in
// }
// .v-leave-active{
//   animation: bounceOut 1s ease-in
// }

动画钩子函数

  • cnpm install velocity-animate@beta
<transition
  v-on:before-enter="beforeEnter"
  v-on:enter="enter"
  v-on:after-enter="afterEnter"
  v-on:enter-cancelled="enterCancelled"

  v-on:before-leave="beforeLeave"
  v-on:leave="leave"
  v-on:after-leave="afterLeave"
  v-on:leave-cancelled="leaveCancelled"
>
  <!-- ... -->
</transition>
<script>
methods: {
  // --------
  // 进入中
  // --------

  beforeEnter: function (el) {
    // ...
  },
  // 当与 CSS 结合使用时
  // 回调函数 done 是可选的
  enter: function (el, done) {
    // ...
    done()
  },
  afterEnter: function (el) {
    // ...
  },
  enterCancelled: function (el) {
    // ...
  },

  // --------
  // 离开时
  // --------

  beforeLeave: function (el) {
    // ...
  },
  // 当与 CSS 结合使用时
  // 回调函数 done 是可选的
  leave: function (el, done) {
    // ...
    done()
  },
  afterLeave: function (el) {
    // ...
  },
  // leaveCancelled 只用于 v-show 中
  leaveCancelled: function (el) {
    // ...
  }
}
// 这些钩子函数可以结合 CSS transitions/animations 使用,也可以单独使用。
</script>
  • 一个使用 Velocity.js 的简单例子:
<!--
Velocity 和 jQuery.animate 的工作方式类似,也是用来实现 JavaScript 动画的一个很棒的选择
-->
<div id="example-4">
  <button @click="show = !show">
    Toggle
  </button>
  <transition
    v-on:before-enter="beforeEnter"
    v-on:enter="enter"
    v-on:leave="leave"
    v-bind:css="false"
  >
    <p v-if="show">
      Demo
    </p>
  </transition>
</div>
<script>
new Vue({
  el: '#example-4',
  data: {
    show: false
  },
  methods: {
    beforeEnter: function (el) {
      el.style.opacity = 0
      el.style.transformOrigin = 'left'
    },
    enter: function (el, done) {
      Velocity(el, { opacity: 1, fontSize: '1.4em' }, { duration: 300 })
      Velocity(el, { fontSize: '1em' }, { complete: done })
    },
    leave: function (el, done) {
      Velocity(el, { translateX: '15px', rotateZ: '50deg' }, { duration: 600 })
      Velocity(el, { rotateZ: '100deg' }, { loop: 2 })
      Velocity(el, {
        rotateZ: '45deg',
        translateY: '30px',
        translateX: '30px',
        opacity: 0
      }, { complete: done })
    }
  }
})
</script>

插槽

  • 在html 中不能给标签大写 属性名字必须用驼峰 -
  • 组件中间写了 template (有slot='item' 就是具名插槽) 要是直接写的东西对应slot中
<!-- 子组件 -->
  <div>
      B组件
       <!--slot 将父组件的内容查到那个地方  -->
      <slot name='ab'></slot>
      123
  </div>

<!-- 父组件 -->
  <div>
    A组件 
    <B>
      <template v-slot:ab>
        <!-- 组件_uid 是组件唯一id -->
        <div title='node'>1号组件</div>
        <div title='react'>2号组件</div>
        <div title='vue'>3号组件</div>
    </template>
    </B>
  </div>

作用域插槽

  • 父组件 自定义的插槽可以获取 子组件里面的值
  • v-slot: 是定义名字
  • v-slot:ab='{v}' =后面的是结构赋值 子组件:v传递过来的值
  • slot标签内是不能写内容的 只是一个插槽 但是可以传递给父组件的template template标签内又可以直接获取值
<!-- 父组件 -->
  <div>
    A 
    <B>
      <template v-slot:ab='{v}'>
        <!-- 组件_uid 是组件唯一id -->
        <div title='node'>{{v}}</div>
      </template>
    </B>
  </div>
<!-- 子组件 -->

  <div>
    B 
      <slot name='ab' :v='inp'></slot>
      123
  </div>
  <script>
      export default {
        data(){
            return {
              inp:1211111111113,
            }
          },
      }
  </script>

vue-cli

  // 全局安装脚手架
  cnpm  i @vue/cli -g
  // 零配置 写测试用
  // 创建 .vue文件直接运行
  // 运行vue serve 文件名字/或者不写
  cnpm i @vue/cli-service-global -g

  // 生成项目
  vue create my-project

webpack配置

  • webpack 文件隐藏着
  • 配置他 只需要创建vue.config.js 他会覆盖原有配置
let path = require('path')
// 默认环境变量 NODE_ENV production development
module.exports = {
  //根据环境 设置请求路径
  publicPath:process.env.NODE_ENV === 'production' ? 'http://www.zf.com':'/',
  //打包的资源 集中放到一个独立文件
  assetsDir:'asserts',
  // 输出的目录 默认dist
  outputDir:'./my-dist',
  // 加这个才能使用template(一般都是render)  一般不使用体积会变大 设置false
  runtimeCompiler:true,
  // 打包 不在使用SourceMap 减少体积
  productionSourceMap:false,
  chainWebpack:config=>{
    //  可以获取到webpack的配置 在增加一些自己的功能
    //  配置目录别名 别名叫@   以后引用的@ 就直接代表src目录
    config.resolve.alias.set('@',path.resolve(__dirname,'src'))
  },
  // configureWebpack:{//会自动合并
  //   pluhins:[],
  //   module:{}
  // },
  devServer:{
    //开发 服务时 使用
    proxy:{
      '/api/getUser':{
        target:'http://localhost:3000',
        pathRewrite:{
          //vue中 请求 /api/getUser会找到这儿
          //下面配置 /api不会出现在真是请求路径中
          '/api':''
        }
      }
    }
  },
  // vue add style-resources-loader 会自动注入进来
  pluginOptions: {
    'style-resources-loader': {
      preProcessor: 'scss',
      patterns: [
        path.resolve(__dirname,'src/assets/common.scss')
      ]
    }
  }
}

vue-router

  • this.$route 放的都是属性
  • this.$router 都是方法
    • push 跳转路由
      • 传参数 params/query
      • params 跟在路由/后面 要route文件配置
      • query 跟着?后面 无需配置
    • replace 跟 router.push 很像,但它不会向 history 添加新记录
<script>
// 配置
 {
  //  动态路径参数 以冒号开头
    path: '/about/:id',
    name: 'about',
    component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
  }
</script>
<!-- 声明式跳转 -->
<script>
// 标签跳转
<router-link to='/about/123'>123</router-link>>
</script>
 <!-- 编程式跳转 -->
 <div @click='btn'>跳转</div>
<script>
  methods:{
      btn(){
        this.$router.push({name:'about',params:{ id: '11123' },query:{plan: 'private' }})
        // 或者
        // this.$router.push({path:'about/12',query:{plan: 'private' }})
      }
    }
</script>
    
<script>
// about组件
console.log(this.$route.params) // {id:123}
</script>

基本用法

  • component:()=>import('xx组件') 懒加载
  • router/index.js
  import Vue from 'vue'
  import VueRouter from 'vue-router'
  import routes from './routes'
  // 第三方插件引入后 要使用Vue.use() 

  Vue.use(VueRouter);// 注册了两个全局组件 router-link router-view
  // 会在每个组件上定义两个属性 $router $route this.$router this.$route

  export default new VueRouter({
    mode:'hash', //默认会出现一个#
    routes
  })
  • router/routes.js
import Home from '_v/Home.vue'
import Profile from '_v/Profile.vue'
import User from '_v/User.vue'
// 默认加载首页 其他的组件 在点击时懒加载
// 可能会有白屏
export default = [
  {
    path:'/home',
    name:'home',
    component:{
      default:Home,
      name:Profile,
      version:User
    }
  },
  {
    path:'/login',
    name:'login',
    // import返回的是一个 promise
    component:()=>import('_v/Login.vue')
    // component 其他的组件 在点击时懒加载
  },
  {
    path:'/profile',
    name:'profile',
    component:()=>import('_v/Profile.vue'),
  },
  {
    path:'/user',
    name:'user',
    component:()=>import('_v/User.vue'),
    meta:{ needLogin:true },//路由元信息
    children:[
      {
        path:'',
        component:()=>import('_v/userAdd.vue'),
      },
      {
        // 儿子路径默认不能j加/ 
        path:'add',
        name:'userAdd',
        component:()=>import('_v/userAdd.vue'),
      }
    ]
  },
]

//修改配置别名
chainWebpack:config=>{
  config.resolve.alias.set('_v',path.resolve(__dirname,'src/components'))
}
  • App.vue
<template>
  <div id='app'>
    <ul>
    <!-- 默认是a标签 tag可以设置  -->
      <li><router-link tag="span" :to="{name:'home'}">首页</router-link></li>
      <li><router-link :to="{path:'/profile'}">个人中心</router-link></li>
      <li><router-link>用户</router-link></li>
      <li><router-link>登录</router-link></li>
    <ul> 
    <!--  这个会显示匹配到的路由 -->
    <!-- 没名字 是默认  有名字会根据名字渲染  -->
    <router-view></router-view>
    <router-view name='name'></router-view>
    <router-view name='version'></router-view>
    
  </div>
</template>

导航守卫-钩子函数

  • 全局的守卫

    • router.beforeEach(to,from,next) 当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中。
    • router.beforeResolve(to,from,next) 全局解析守卫,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。
    • router.afterEach(to,from) 这些钩子不会接受 next 函数也不会改变导航本身。
  • 路由独享的守卫

    • beforeEnter(to, from, next) 在配置路由里面使用的
  • 组件内的守卫

    • beforeRouteEnter (to, from, next)
      • 在渲染该组件的对应路由被 confirm 前调用
      • 不!能!获取组件实例 this,因为当守卫执行前,组件实例还没被创建
      • 但是可以通过next 传递一个vm 他可以访问到 下面两个能获取this 所以next一般没用
    • beforeRouteUpdate (to, from, next)
      • 在当前路由改变,但是该组件被复用时调用,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
      • 可以访问this
    • beforeRouteLeave (to, from, next)
      • 导航离开该组件的对应路由时调用
      • 可以访问this
      • 这个离开守卫通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消。
  • 完整的导航解析流程

  /*
  1、导航被触发。
  2、在失活的组件里调用离开守卫。
  3、调用全局的 beforeEach 守卫。
  4、在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5、在路由配置里调用 beforeEnter。
  6、解析异步路由组件。
  7、在被激活的组件里调用 beforeRouteEnter。
  8、调用全局的 beforeResolve 守卫 (2.5+)。
  9、导航被确认。
  10、调用全局的 afterEach 钩子。
  11、触发 DOM 更新。
  12、用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。
  • 钩子函数
    • 当组件切换时 会触发离开的钩子 beforeRouteLeave
    • 进入到一个新的页面里 组件内部 会触发一个方法 beforeRouteEnter
    • 当属性变化的时候 并没有重新加载组件 会触发beforeRouteUpdate
      • 钩子周期
      • beforeEach 如进入到新的页面
      • beforeEnter 进到路由的配置中
      • beforeRouterEnter 组件进入时的钩子
      • beforeResolve 解析完成
      • afterEach 当前进入完毕 -组件渲染完成以后 会调用当前 beforeRouteEnter 回调方法
  <btton @click='toList'>跳转</btton>

  <script>
    1、组件内
      methods:{
        toList(){
          //vue.use(VueRouter)  里面有2个东西 $router $route 
          //带r存的都是方法  不带r存的都是属性
          this.$router.push('/user/list')
        }
      }
      //钩子函数  与data同级
      beforeRouteLeave(to,from,next){
        // next()//往下走
      }
      beforeRouteEnter(to,from,next){
        // 此方法中不能拿到this
        // from.name //可以拿到name 对应路由的名字
        next(vm=>{
          console.log('实例',vm)// 组件渲染完成后 会调用当前beforceRouteEnter方法
        })
      }
    2、全局 在main.js里面
      router.beforeEach((to,from,next)=>{
        console.log(to.matched)//获取当前路由所有匹配的路径 是一个数组
        console.log(to.meta)//获取路由元信息
        // 查看路由元 信息 是否有needLogin
        let flag = to.matched.some(match=>{
          return match.meta && match.meta.needLogin
        })

        if(flag){ //需要登录
          let isLogin = localStorage.getItem('login')// ajax 看一下用户是否登录过
          if(isLogin){
            //如果用户已经登录 并且访问 的还是登录页面
            next();
          }else{
            next('/login');//没有登录 去登录页面 
          }
        }else{ //不需要登录
          next();
        }

      })
      router.beforeResolve((to,from,next)=>{
        // 当前路由解析后会跳转的钩子
        console.log('xxx')
        next();
      })
      router.afterEach(()=>{
        console.log('xxx')
      })
  </script>

带参数跳转

  • 记住参数或查询的改变并不会触发进入/离开的导航守卫(当?后面的值变化的时候)
  • 用watch和beforeRouterUpdate的组件内守卫
  1、<!-- 问好传递参数 -->
    <li><router-link to="/user/detail?id=1">用户1</router-link></li>

    <script>
      //获取 
      this.$route.query.id

      // 当网页中id值变化的时候 组件不会重新加载
      1、可以用watch监听
        watch:{
          $route(){
            console.log('xx')
          }
        }
      2、beforeRouterUpdate(to,from,next){
          console.log('xx')
          next()
      }
    </script>
  2、<!-- 路由的路径传递 -->
    在routes定义
    {
      path:'detail/id'
    }
    <li><router-link to="/user/detail/1">用户1</router-link></li>

    <script>

      //获取 
      this.$route.params.id

    </script>

警告

const router = new VueRouter({
  mode: 'history',
  routes: [
    { path: '*', component: NotFoundComponent }
  ]
})

vuex

  • 创建
  • store/index.js
  import Vue from 'vue';
  import vuex from 'vuex';

  import actions from './actions'
  import mutations from './mutations'
  import state from './state'
  import getters from './getters'

  import user from './modules/user'

  Vue.use(vuex)
  // 只要页面中注入了store 每个实例上都会存在一个属性 $store
  export default new vuex.Store({
    strict:process.env.NODE_ENV != 'production',// 校验更改状态的合法性
    actions,
    mutations,
    state,
    getters,
    modules:{
      user
    }
  })
  // this.$store.state.lesson
  // this.$store.state.user.userName
  • store/state.js && store/getters.js && store/mutations.js && store/actions.js
  // state
  export default {
    lesson:'测试课程',
    className:'1-1'
  }
  // getters
  export default {
    getNewName(state){
      return '高级'+state.lesson
    }
  }
  // mutations 和 store/modules/user.js类似
  export default {
    
  }
  // actions
  export default {
    
  }
  • store/modules/user.js
export default {
  namespaced: true, //启动 独立的命名空间
  state: {
    userName: 'userName'
  },
  getters: {

  },
  mutations: {
    // 第一个参数永远是state
    change_userName(state,payload) {
      // alert(1)
      state.userName = payload
      console.log(state,payload)
    }
  },
  actions: {
    change_userNameAction({commit},payload){
        setTimeout(()=>{
          commit('change_userName',payload)
          //在action中可以多次触发mutations
        },1000)
    }
  }
}
  • 使用
  // 只要页面中注入了store 每个实例上都会存在一个属性 $store
  import {mapState,mapGetters,mapMutations,mapActions} from 'vuex'
  computed:{
    // 默认的
    ...mapState(['lesson','className']),
    // user 是子模块的名字
    ...mapState('user',['userName']),
    //第二个参数是对象 当前直接 获取 u 
    // ...mapState('user',{u:(state)=>state.userName})
    ...mapGetters(['getNewName'])
  },
  methods: {
    ...mapMutations('user',['change_userName']),
    ...mapActions('user',['change_userNameAction']),
    btn(){
      this['change_userName']('sg')
      this['change_userNameAction']('sgt')
      // this.$store.commit('user/change_userName','jwt')
      // this.$store.dispatch('user/change_userName','jwt')
    }
  },

axios && jwt 使用

  • 搭建server.js
  • jwt 原理
    • 后端: 用户信息 + 密钥(后端存放) + 过期时间(等) = 组成一个加密的token
    • 前端: 登录后获取token,每次发送请求 的时候 将token放在 Authorization 请求头上,后端判断是否有效(过期,正确性等)
let express = require('express')
let app = express()
//jwt jsonwebtoken的库
// jwt.sign 加密 jwt.verify解密
let jwt = require('jsonwebtoken')

var bodyParser = require('body-parser');//解析,用req.body获取post参数
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));

let whitList = ['http://localhost:8080']
app.use((req,res,next)=>{
    let origin = req.headers.origin
     if(whitList.includes(origin)){
        res.setHeader('Access-Control-Allow-Origin',origin)
        res.setHeader('Access-Control-Allow-Headers','name,xx,Authorization,Content-Type')
        res.setHeader('Access-Control-Allow-Methods','PUT')
        res.setHeader('Access-Control-Allow-Credentials',true)
        res.setHeader('Access-Control-Expose-Headers','name,ss,xx')
        res.setHeader('Access-Control-Max-Age',10)
        if(req.method === 'options'){
            res.end()
        }
    }
    next()
})

app.get('/user',(req,res)=>{
  setTimeout(()=>{
    res.json({name:1})
  },2500)
})
const secret = 'cf'
app.post('/login',(req,res)=>{
  let {username} = req.body
  if(username === 'admin'){
    res.json({
      code:0,
      username:'admin',
      //加密 发送给前端
      token:jwt.sign({username:'admin'},secret,{
        expiresIn:5
      })
    })
  }else{
    res.json({
      code:1,
      data:'用户名不存在'
    })
  }
})

app.get('/validate',(req,res)=>{
  let token = req.headers.authorization;
  //解密 获取token
  jwt.verify(token,secret,(err,decode)=>{
    if(err){
      return res.json({
        code:1,
        data:'token失效了'
      })
    }else{
      // 只要用户路由刷新 token就延时 20秒  
      res.json({
        username:decode.username,
        code:0,
        token:jwt.sign({username:'admin'},secret,{
          expiresIn:20
        })
      })
    }
  })
})

app.listen(3000,()=>{
    console.log('listen start')
})
  • axios封装 和 loading -axiosn封装 api抽离
  • libs/ajaxRequest
  import axios from 'axios';
  import store from '../store'
  import {getLocal} from './local'
  // 当第一次请求 显示loding 剩下的时候就不调用了
  // 当都请求完毕后 隐藏loading
  class AjaxRequest{
    constructor(){
      //  请求路径 根据开发和生产区分
      this.baseURL = process.env.NODE_ENV == 'production'?'/':'http://localhost:3000';
      //  超时
      this.timeout = 3000;
      this.queue = {};//存放每次的请求 处理loading
    }
    merge(options){
      return {...options,baseURL:this.baseURL,timeout:this.timeout}
    }
    setInterceptor(instance,url){
      // 每次请求时 都会加一个loading效果 
      // 更改请求头
      instance.interceptors.request.use(config=>{
        // 加请求头 getLocal从本地存储获取token
        config.headers.Authorization=getLocal('token');
        if(Object.keys(this.queue).length === 0){
          store.commit('showLoading')
        }
        this.queue[url] = url
        return config
      })
      instance.interceptors.response.use(res=>{
        delete this.queue[url]; // 每次请求成功后 都删除队列里的路径
        if(Object.keys(this.queue).length === 0){
          store.commit('hideLoading')
        }
        return res.data
      })
    }
    request(options){
      // 返回axios实例
      let instance = axios.create(config);
      // 对请求和拦截做处理
      this.setInterceptor(instance,options.url)
      // 将所有的参数合并
      let config = this.merge(options);
      return instance(config)
    }
  }
export default new AjaxRequest
  • /api/user
import axios from '../libs/ajaxRequest';

export const getUser = () => {
  return axios.request({
    url:'/user',
    method:'get'
  })
};

export const login = (username) => {
  return axios.request({
    url:'/login',
    method:'post',
    data:{
      username
    }
  })
}

export const validate = () => {
  return axios.request({
    url:'/validate',
    method:'get',
  })
}
  • libs/local
export const setLocal = (key,value)=>{
  if(typeof value == 'object'){
    value = JSON.stringify(value);
  }
  localStorage.setItem(key,value)
}

export const getLocal = (key)=>{
 return localStorage.getItem(key)
}
  • store
import {login,validate} from '../api/user'
import {setLocal} from '../libs/local'
export default {
  namespaced: true, //启动 独立的命名空间
  state: {
    isShowLoading:false,
    username:'',
  },
  getters: {

  },
  mutations: {
    // 第一个参数永远是state
    showLoading(state){
      state.isShowLoading = true
    },
    hideLoading(state){
      state.isShowLoading = false
    },
    setUser(state,username){
      state.username = username;
    }
  },
  actions: {
    // 登录获取token
    async toLogin({commit},username){
      let rs = await login(username);
      if(rs.code === 0){
        //成功登录
        commit('setUser',rs.username)
        //将token保存到client 每次请求带上, 服务端校验token 如果token不正确 或者过期 没登录
        setLocal('token',rs.token)
      }else{
        return Promise.reject(rs.data)
      }
    },
    //查看 token是否有效
    async validate({commit}){
      let rs = await validate();
      if(rs.code === 0){
        commit('setUser',rs.username)
        setLocal('token',rs.token)
      }
      return rs.code === 0;//返回用户是否失效
    }
  }
}
  • router.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '_v/Home.vue'
import App from './App'

Vue.use(VueRouter);
export default new VueRouter({
  mode:'hash', //默认会出现一个#
  routes:[
    {
      path:'/',
      name:'App',
      component:App
    },
    {
      path:'/login',
      name:'login',
      // import返回的是一个promise
      component:()=>import('_v/Login.vue')
    },
    {
      path:'/profile',
      name:'profile',
      component:()=>import('_v/Profile.vue'),
    },
    {
      path:'/user',
      name:'user',
      component:()=>import('_v/User.vue'),
      meta:{ needLogin:true },//路由元信息
      children:[
        {
          path:'',
          component:()=>import('_v/userAdd.vue'),//$route.matched
        },
        {
          // 儿子路径默认不能j加/ 
          path:'add',
          name:'userAdd',
          component:()=>import('_v/userAdd.vue'),
        }
      ]
    },
  ]
})
  • main
import Vue from 'vue'
import App from './App.vue'

import store from './store/index'
import router from './router'

Vue.config.productionTip = false

router.beforeEach(async (to,from,next)=>{
  // 根据router 配置的meta是否需要登录 在查看是否有效
  let flag = to.matched.some(match=>{
    return match.meta && match.meta.needLogin
  })

  if(flag){ //需要登录
    let rs =  await store.dispatch('validate')//根据rs返回的数据 查看tikon是否有效
    if(rs){
      //如果用户已经登录 并且访问 的还是登录页面
      next();
    }else{
      next('/login');//没有登录 去登录页面 
    }
  }else{ //不需要登录
    next();
  }
  next()
})

new Vue({
  render: h => h(App),
  store,
  router
}).$mount('#app')

  • app.vue
<span v-if="$store.state.isShowLoading"> 加载中 </span>
<router-view></router-view>
  • login.vue
    <input type="text" v-model="username" >
    <Button @click='login'>登录</Button>
    <span>
      当前登录用户:{{$store.state.username}}
    </span>
  <script>
    import {getUser} from '../api/user.js';
    import {mapActions} from 'vuex'

     methods:{
        ...mapActions(['toLogin']),
        login(){
          this['toLogin'](this.username);
        }
      }
  </script>

api

  • Vue.extend
  • 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
<div id="mount-point"></div>
// 创建构造器
var Profile = Vue.extend({
  template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
  data: function () {
    return {
      firstName: 'Walter',
      lastName: 'White',
      alias: 'Heisenberg'
    }
  }
})
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#mount-point')

输入 => <p>Walter White aka Heisenberg</p>
  • Vue.mixin
  • 全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。(不推荐在应用代码中使用。)

vux原理

  • 基本实现
let Vue;
class Store{ // state getters mutations actions
  constructor(options){
    let state = options.state // {count:100}
    this.getters = {}
    this.mutations = {}
    this.actions = {}
    // vue核心就借用了vue的实例 因为vue的实例数据变化 会刷新视图
    this._vm = new Vue({
      data:{
        state
      }
    });
    if(options.getters){
      let getters = options.getters;// {newCount:fn}
      forEach(getters,(getterName,getterFn)=>{
        Object.defineProperty(this.getters,getterName,{
          get:()=>{
            return getterFn(state)
          }
        })
      });
      let mutations = options.mutations
      forEach(mutations,(mutationName,mutationFn)=>{
        // this.mutations.change = ()=>{ change(state) }
        this.mutations[mutationName] = ()=>{
          mutationFn.call(this,state)
        }
      })
      let actions = options.actions
      forEach(actions,(actionName,actionFn)=>{
        // this.mutations.change = ()=>{ change(state) }
        this.actions[actionName] = ()=>{
          actionFn.call(this,this)
        }
      })
    }
    let {commit,dispatch} = this;
    this.commit = (type) => {
      commit.call(this,type)
    }
    this.dispatch = (type) => {
      dispatch.call(this,type)
    }
  }
  get state(){
    return this._vm.state
  }
  commit(type){
    this.mutations[type]()
  }
  dispatch(type){
    this.actions[type]()
  }
}
function forEach(obj,callback){
  Object.keys(obj).forEach(item=>callback(item,obj[item]))
}


let install = (_Vue) =>{
  Vue = _Vue;// 保留vue的构造函数
  // mixin 全局混入 只要组件(main也是组件)生成 beforeCreate就会执行一次
  // beforeCreate  里面的this就是获取当前组件
  Vue.mixin({ //混合
    beforeCreate() {
      // console.log(' beforeCreate 12',this.$options)
      // 我需要把跟组件中 store实例 给每个组件都增加一个$store的属性
      // 是否是跟组件
      if(this.$options && this.$options.store){
        console.log('1',this.$options.store)
        this.$store = this.$options.store
      }else{ // 子组件 store 深度优先 父->子->孙
        console.log('this.$store',this.$options.data)
        this.$store = this.$parent && this.$parent.$store
      }
    },

  })
}
export default {
  Store,
  install
}
  • vuex 里面的modules state循环
class ModuleCollection{
  constructor(options){// vuex [a,b]
    this.register([],options);
  };
  register(path,rawModule){
    // path 是个空数组 rawModule就是一个对象
    let newModule = {
      _raw:rawModule,// 对象 当前 有state getters 那个对象
      _children:{}, // 表示 他包含的模块
      state:rawModule.state //他自己模块的状态
    }
    if(path.length == 0){
      this.root = newModule; // 根
    }else{
      let parent = path.slice(0,-1).reduce((root,current)=>{
        return root._children[current];
      },this.root)
      // path[path.length-1] 取数组的最后一项
      parent._children[path[path.length-1]] = newModule
    }
    if(rawModule.modules){ // 有子模块
      forEach(rawModule.modules,(childName,module)=>{
        this.register(path.concat(childName),module)
      })
    }
  }
}

class Store{ // state getters mutations actions
  constructor(options){
    let state = options.state // {count:100}
    this.getters = {}
    this.mutations = {}
    this.actions = {}
    // vue核心就借用了vue的实例 因为vue的实例数据变化 会刷新视图
    this._vm = new Vue({
      data:{
        state
      }
    });

    // 把模块之间的关系 进行整理 自己根据用户传入的参数维护了一个对象
    // root._children => a._children => b
    this.modules = new ModuleCollection(options);
    // 无论是子模块 还是孙子 所有的mutation 都是根上的
    。。。。。。。。。。。。。。。。。。。。。。。
  }

nextTick如何实现

  • 定义了一个 macroTimerFunc(宏任务) microTimerFunc (微任务) 他会先采用微任务要是没有就采用宏任务

  • 宏任务

    • 查看是否支持 setImmediate 如果有就将他赋值给macroTimerFunc(宏任务)执行回调
    • 没有的话 查看是否支持 messageChannel 有就用将他赋值给macroTimerFunc(宏任务)执行回调
    • 若上面 上面两个都不支持 直接将setTimeout(callback,0) 赋值给macroTimerFunc(宏任务)执行
  • 微任务

    • 查看知否支持promise 支持就 new promise 将实例.then赋值给微任务执行
    • 要是不支持 就暴力的说 microTimerFunc = macroTimerFunc

日期组件

  • /components/DatePicker.vue
<template>
 <div v-click-outside>
   <input type="text" :value="formatDate" >
   <div  class="pannel" v-if="isVisible">
       <div class="pannel-nav">
           <span>&lt;</span>
           <span @click='prevMonth'>&lt;&lt;</span>
           <span>{{time.year}}年</span>
           <span>{{time.month}}月</span>
           <span  @click='nextMont'>&gt;&gt;</span>
           <span>&gt;</span>
           
       </div>
       <div class="pannel-content">
         <div class='days'>
           <span 
             v-for="j in 7" :key='`_i`+j'
             class="cell"          
           >
             {{weekDays[j-1]}}
           </span>
           <!-- 直接列出一个 6 * 7 一个列表 -->
           <!-- 判断是不是当月 不是当月就变灰色 -->
           <div v-for='i in 6' :key="'_a'+i">
             <span 
               class="cell" 
               :class="[
                 {notCuurentMonth:!isCurrentMoth(visibeDays[(i-1)*7+(j-1)])},
                 {today:isToday(visibeDays[(i-1)*7+(j-1)])},
                 {select:isSelect(visibeDays[(i-1)*7+(j-1)])}
               ]"
               v-for="j in 7" 
               :key="'_b'+j"
               @click="chooseDate(visibeDays[(i-1)*7+(j-1)])"
               >
               {{visibeDays[(i-1)*7+(j-1)].getDate()}}
             </span>
           </div>
         </div>
       </div>
       <div class="pannel-footer">
         今天
       </div>
   </div>
 </div>
</template>

<script>
import {getYearMonthDay,getDate} from '../utils'
export default {
 directives:{
   clickOutside:{//指令的生命周期
     bind(el,bindings,vnode){
       // 把事件绑定给document上 看一下点击的是否是当前这个元素
       let handler = (e)=>{
         if(el.contains(e.target)){
           // 判断一下是否当前面板已经显示出来了
           if(!vnode.context.isVisible){
             vnode.context.focus()
             console.log('包含')
           }
         }else{
           if(vnode.context.isVisible){
             vnode.context.blur()
             console.log('不包含')
           }
         }
       }
       el.handler = handler
       document.addEventListener('click',handler)
       console.log(el,bindings,vnode)
     },
     unbind(){
       document.removeEventListener('click')
     }
   }
 },
 data(){
   let {year,month} = getYearMonthDay(this.value)
   month = month+1
   return{
     weekDays:['日','一','二','三','四','五','六',],
     time:{year,month},
     isVisible:true,//这个变量是用来控制这个面板是否可见
   }
 },
 props:{
   value:{
     type:Date,
     default:()=>new Date()
   }
 },
 methods:{
   focus(){
     this.isVisible = true
   },
   blur(){
     this.isVisible = false
   },
   isCurrentMoth(date){
     // 他是不是当月 比较this.value和date 年月是否相等
     let {year,month} = getYearMonthDay(getDate(this.time.year,this.time.month,1));
     let {year:y,month:m} = getYearMonthDay(date)
     return year === y && month === m 
   },
   isToday(date){
     let {year,month,day} = getYearMonthDay(new Date());
     let {year:y,month:m,day:d} = getYearMonthDay(date);
     return year === y && month === m && day === d
   },
   chooseDate(date){
     this.time = getYearMonthDay(date);
     this.$emit('input',date)
     this.blur()
   },
   isSelect(date){
     let {year,month,day} = getYearMonthDay(getDate(this.time.year,this.time.month,1));
     let {year:y,month:m,day:d} = getYearMonthDay(date);
     return year === y && month === m && day === d
   },
   prevMonth(){
     let d = getDate(this.time.year,this.time.month,1)
     d.setMonth(d.getMonth()-1)
     this.time = getYearMonthDay(d)
   },
   nextMont(){
     let d = getDate(this.time.year,this.time.month,1)
     d.setMonth(d.getMonth()+1)
     this.time = getYearMonthDay(d)
   }
 },
 computed: {
   visibeDays(){
     // 先获取当前是周几
     let {year,month,day} = getYearMonthDay(getDate(this.time.year,this.time.month,1))
     // 获取当前月份的第一天
     let currentFirstDay = getDate(year,month,1)
     // 生成一个 当前 2019 5 18
     // 获取当前是周几  把天数往前移动 几天
     let week = currentFirstDay.getDay();
     // 当前开始的天数, 日期格式 和 和 数字相减得到一个毫秒戳
     let startDay = currentFirstDay - week * 60 * 60 * 1000 * 24
     // 循环42天
     let arr = []
     for(let i=0;i<42;i++){
       // 依次循环出42天
       arr.push(new Date(startDay+i * 60 * 60 * 1000 * 24))
     }
     return arr
   },
   formatDate(){
     let {year,month,day} = getYearMonthDay(this.value)
     console.log(year,month,day)
     this.visibeDays 
     // getFullYear getMonth getDate
     return `${year}-${month}-${day}`
   }
 },
}
</script>
<style lang='scss'>
.pannel{
 position: absolute;
 background: #fff;
 box-shadow: 2px 2px 2px pink, -2px -2px 2px pink;
 .pannel-nav{
     height: 30px;
     display: flex;
     justify-content: space-around;
     span {
       cursor: pointer;
       user-select: none;
     }
 }
 .pannel-content{
     box-sizing: border-box;
   .cell{
     display:  inline-block;
     justify-content: center;
     align-items: center;
     width:50px;
     height: 50px;
     font-weight: bold;
     text-align: center;
     border: 2px solid #fff;
     box-sizing: border-box;
   }
   .cell:hover{
       border: 1px solid pink;
       background: pink;
     }
   .notCuurentMonth{
     color:gray
   }
   .today{
     color: #fff;
     background: red;
     border-radius: 4px;
   }
   .select{
     color: #fff;
     background: red;
     border-radius: 4px;
   }
 }
 .pannel-footer{
   height: 30px;
   text-align: center
 }
 
}
</style>
  • utils
const getYearMonthDay = (date) => {
    let year = date.getFullYear()
    let month = date.getMonth()+1
    let day = date.getDate()
    return {year,month,day}
}
const getDate = (year,month,day)=>{
  return new Date(year,month-1,day)
}
export {
  getYearMonthDay,
  getDate
}
  • 用法
<DatePicker v-model="now"></DatePicker>
<script>
import DatePicker from './components/DatePicker';
export default {
  data(){
    return{
      now : new Date()
    }
  },
  components:{
    DatePicker
  }
}
</script>

插件的编写&&mixin用法

  • 每个插件 内部需要提供一个install方法
  • 使用 Vue.use('插件')
  • mixin 混合
    • vue.mixin 组件原有的方法会和他混合
    • 将实例化的info 对象传递给所有组件
    • 通过beforeCreate方法
    • 判断this.$options里面传递的参数 一个个传入进去 (递归)
    • 这个写了 在每个组件获取info 对象,跟vuex效果等一样实现
  • Message组件 2中方法引入(模仿element-ui)
import Message from './components/Message'
let info = {a:1,b:2}
Vue.use(Message)
new Vue({
  router,
  store,
  info,
  // template:`<div>123</div>`,
  render: h => h(App)
}).$mount('#app')
  • Message.js
import Vue from 'vue';
import MessageComponent from './Message.vue'
// 获取当前组件的实例
let getInstance = ()=>{
  let vm = new Vue({
    render:h=>h(MessageComponent)
  }).$mount(''); //会在内存中进行挂载
  document.body.appendChild(vm.$el);

  // 获取他的儿子,就一个儿子
  let component = vm.$children[0]
  return {
    add(options){
      component.add(options)
    }
  }
  // vm
}
// 单例模式
let instance;
let getInst = ()=>{ //返回一个唯一的实例
  instance = instance || getInstance()
  return instance
}
const Message = {
  info(options){
    getInst().add(options)
  },
  warn(){

  },
  success(){

  },
  error(){

  }
}
export {
  Message
}
let _Vue;
export default{
  install(Vue,options){
    // options 代表的是use的第二个参数
    if(!_Vue){
      // 防止用户多次use
      _Vue = Vue
      let $message = {}
      Object.keys(Message).forEach(type=>{
        $message[type] = Message[type];
      })
      Vue.prototype.$message = $message
    }
    Vue.mixin({
      beforeCreate() {//所有组件都增加了这个方法
        if(this.$options.info){
          this._info = this.$options.info
        }else{
          this._info = this.$parent && this.$parent._info
        }
      },
    })
  }
}
  • Message.vue
<template>
  <div class='message' v-if="messages.length">
    <div v-for='m in messages' :key="m.id">
      {{m.message}}  
    </div>
  </div>  
</template>

<script>
export default {
  data(){
    return{messages:[]}
  },
  mounted(){
    this.id = 0;//表示当前弹层的唯一标识
  },
  methods:{
    add(options){
        let id = this.id ++
        let layer = {...options,id}
        this.messages.push(layer)
        layer.timer = setTimeout(()=>{
           this.remove(layer)
        },options.duration)
    },
    remove(layer){
      this.messages = this.messages.filter(message=>message.id !== layer.id
      )
    }
  }
}
</script>
<style>

</style>
  • 用法
  • App.vue
<template>
  <div id="app">
    <button @click='btn'>点击弹框</button>
  </div>
</template>
<script>
import {Message} from './components/Message'
export default {
  mounted() {
    console.log(this._info)
  },
  methods: {
    btn(){
      // Message.info({
      //   message:'我很帅',
      //   duration:3000
      // })
      this.$message.info({
        message:'我很帅',
        duration:3000
      })
    }
  },
}
</script>

mvvm

观察者模式

1、被观察者供维护观察者的一系列方法
2、观察者提供更新接口
3、观察者把自己注册到被观察者里
4、在被观察者发生变化时候,调用观察者的更新方法
  • 特点
    • 只有观察者和被观察者 2个
    • 被观察者和观察者是耦合的 => 所有的观察者都放到被观察者内
    • 观察者提供一个更新的方法,由被观察者调用
  • 使用场景
    • 1、promise then的时候,里面的函数 用一个列表保存起来 等待resolve执行完成 执行列表里面的每一个函数,这样then就能拿到结构
    • 2、node evnets对象 里面 on 和 emit
    • 3、vue和react里面的声明 周期 只有等待运行这一步的时候(事件触发的时候) 才会取调用

发布订阅

1、有发布者  调度中心 订阅者
2、订阅者把自己想订阅的事件 注册到调度中心
3、当该事件触发的时候,发布者发布该事件到调度中心,由调度中心统一调度订阅者注册到调度中心的代码处理
  • 特点:解耦

  • 模拟代码

class Agent{
  constructor(){
    this._events = {}
  }
  // on
  subscribe(type,listener){
    let listeners = this._events[type]
    if(listeners){
      listeners.push(listener)
    }else{
      this._events[type] = [listener]
    }
  }
  // emit 
  publish(type){
    let listeners = this._events[type]
    let args = Array.prototype.slice.call(arguments,1)
    if(listeners){
      listeners.forEach(listener=>listener(...args))
    }
  }
}
// 房东
class LandLord{
  constructor(name){
    this.name = name
  }
  // 向外出租
  lend(agent,area,money){
    agent.publish('house',area,money)
  }
}
// 租客
class Tenant{
  constructor(name){
    this.name = name
  }
  rent(agent){
    agent.subscribe('house',(area,money)=>{
      console.log(`${this.name}看到中介的新房源${area},${money}`)
    })
  }
}

let agent = new Agent();
let t1 = new Tenant('张三');
let t2 = new Tenant('李四');
t1.rent (agent)
t2.rent (agent)
let landLord = new LandLord();
landLord.lend(agent,60,400)

观察者模式 和 发布订阅 区别

  • 1、观察者模式的订阅者与发布者之间是存在依赖的,而发布/订阅模式则不会。
  • 2、角色量不一样

mvvm 代码


// 观察者(发布订阅)

class Dep{
  constructor(){
    this.subs = [] //存放所有的watcher
  };
  // 订阅
  addSub(watcher){
    // 添加 watcher
    this.subs.push(watcher)
  }
  // 发布
  notify(){
    console.log('this.subs',this.subs)
    this.subs.forEach(watcher => watcher.update())
  } 
}

class Watcher{
  constructor(vm,expr,cb){
    this.vm = vm;
    this.expr = expr;
    this.cb = cb;
    // 默认存放一个老值
    this.oldValue = this.get();
  }

  get(){
    Dep.target = this;//先把自己放在this上
    // 取值 把这个观察者 和 数据关联起来
    let value = CompileUtil.getVal(this.vm,this.expr);
    console.log('this.subs11111',this.subs)
    Dep.target = null;
    return value
  }

  update(){
    //更新操作 数据变化后 会调用观察者的update方法
    let newVal = CompileUtil.getVal(this.vm,this.expr);
    if(newVal !== this.oldValue){
      this.cb(newVal)
    }
  }
}

class Observer{ // 实现数据劫持
  constructor(data){
    this.observer(data);
  }

  observer(data){
    // 如果是对象才观察
    if(data && typeof data == 'object'){
      //如果是对象
      for(let key in data){
        this.defineReactive(data,key,data[key]);
      }
    }
  }

  defineReactive(obj,key,value){
    this.observer(value);
    let dep = new Dep();//给每个属性 都加上一个具有发布订阅的功能
    Object.defineProperty(obj,key,{
      get(){
        //创建watcher时候 会去对应的内容 并且把watcher 放到全局上
        Dep.targer && dep.subs.push(Dep.target)
        return value;
      },
      set:(newVal) => {
        console.log('==',newVal)
        if(newVal != value){
          this.observer(newVal)
          value = newVal
          dep.notify();
        }
      }
    })
  }
}

class Compiler{
  constructor(el,vm){
    // 判断el 属性 是不是一个元素 
    this.el = this.isElementNode(el)?el:document.querySelector(el)
    this.vm = vm
    //把当前节点中的元素 获取到 放到内存中
    let fragment = this.node2fragment(this.el);
    
    //把节点中的内容进行替换

    //编译模板 用数据编译
    this.compile(fragment)

    //把内容塞到页面中
    this.el.appendChild(fragment)
  }

  isDirective(attrName){
    // startsWith() 方法用于检测字符串是否以指定的前缀开始。
    return attrName.startsWith('v-')
  }

  // 编译元素的
  compileElement(node){
    //获取元素属性
    let attributes = node.attributes;// 类数组
    // type='text' v-model='school.name'
    // console.log('==',{...attributes});
      //name就是v-model value 就是school.name 
    [...attributes].forEach(attr => {
      let {name,value:expr} = attr;//
      //判断是不是指令
      if(this.isDirective(name)){
        let [,directive] = name.split('-');
        
        //需要调用不同的指令 来处理
        // node 当前的节点  expr是指令里面的名字  this.vm是当前传递进来的vue参数
        CompileUtil[directive](node,expr,this.vm);
        // console.log('========',node,expr,this.vm)
      }
    })

  }
  // 编译文本的
  compileText(node){
    let content = node.textContent;
    // 判断当前文件节点中的内容是否包含 {{}} 
    if(/\{\{(.+?)\}\}/.test(content)){
      // console.log(content,' 找到所有文本',node)//找到所有文本
      CompileUtil['text'](node,content,this.vm);
    }
  }
  // 核心的编译方法
  compile(node){// 用来编译内存中的dom节点
    let childNodes = node.childNodes;
    [...childNodes].forEach(child=>{
      if(this.isElementNode(child)){
        // 进来的都是元素节点
        this.compileElement(child)
        // 如果是元素的话 需要把自己传递进去 在去便利子元素
        this.compile(child)
      }else{
        this.compileText(child)
      }
    })
  }

  isElementNode(node){//判断是不是文本节点
    return node.nodeType === 1
  }
  //把节点移动到内存中
  node2fragment(node){
    //创建一个文件碎片
    let fragment = document.createDocumentFragment();
    let firstChild;
    while(firstChild = node.firstChild){
      //appendChild增加一个原来的就少一个
      fragment.appendChild(firstChild)
    }
    return fragment
  }
}

CompileUtil = {
  // 根据表达式取到对应的数据
  getVal(vm,expr){//expr格式=> vm.$data  'scholl.name'
   let arr = expr.split('.')
   let rs = vm.$data
   arr.forEach(item=>{
      rs =  rs[item]
   })
   return rs
  },
  model(node,expr,vm){
    // node节点 expr是表达式 vm是当前实例
    // 给输入框赋予value属性
    let fn = this.updater['modelUpdater']
    
    new Watcher(vm,expr,(newVal)=>{
      // 给输入框加入一个观察着
      // 如果稍后数据更新了会触发此方法,会拿新值 给输入框赋予值
      fn(node,newVal);
    })
    
    let value = this.getVal(vm,expr)
    fn(node,value)
  },
  html(){
    //
  },
  getContentValue(vm,expr){
    //遍历表达式 将内容 重新替换成一个完整的内容 返还回去
    return expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
      return this.getVal(vm,rgs[1])
    })
  },
  text(node,expr,vm){
      let fn = this.updater['textUpdater']
      // expr => {{xx}}
      let content = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
        new Watcher(vm,args[1],()=>{
          // 给表达式每{{}}都加上观察者
          fn(node,this.getContentValue(vm,expr));//返回了一个全的字符串 
        })  
        return this.getVal(vm,args[1])
      })
      fn(node,content)
  },
  updater:{
    // 把数据插入到节点中
    modelUpdater(node,value){
      node.value = value
    },
    htmlUpdater(){

    },
    //处理文本节点
    textUpdater(node,value){
      node.textContent = value
    }
  }
}

class Vue{
  constructor(options) {
    this.$el = options.el;
    this.$data = options.data;
    //这个根元素 存在 编译模板
    if(this.$el){
      
      //把数据 全部转换成 object.defineProperty来定义
      new Observer(this.$data)
      new Compiler(this.$el,this)
    }
  }
}

vuex存储和本地存储(localstorage、sessionstorage)的区别

  • 1.最重要的区别:vuex存储在内存,localstorage则以文件的方式存储在本地

  • 2.应用场景:vuex用于组件之间的传值,localstorage则主要用于不同页面之间的传值。

  • 3.永久性:当刷新页面时vuex存储的值会丢失,localstorage不会。

  • 注:很多同学觉得用localstorage可以代替vuex, 对于不变的数据确实可以,但是当两个组件共用一个数据源(对象或数组)时,如果其中一个组件改变了该数据源,希望另一个组件响应该变化时,localstorage无法做到,原因就是区别1。