vue面试题(一)

lxf2023-03-22 12:00:02

VUE

Vue2和3对比

脚手架创建项目

之前有个国企,问到了怎么用脚手架创建vue项目。

2.x

npm install -g vue-cli (npm uninstall -g vue-cli 删)

vue init webpack "项目名称"

进入项目 npm run dev

3.x

npm install -g @vue/cli (npm uninstall -g @vue/cli 删)

或者npm install vue@next(最新最稳定版)

npm install -g @vue/cli-init

vue create 项目名称

进入项目 npm run serve

vue3新特性

  1. 支持碎片Fragments,模板可以有多个根元素
  2. 提供了composition api,更好的逻辑复用与代码组织
  3. 响应式数据声明方式改变
  4. 生命周期的改变
  5. 父子组件传参不同
  6. vue3 Teleport瞬移组件(类似于react的Portals传送门)
  7. vue3中v-for与v-if,只会把当前v-if当做v-for中的一个判断语句,不会相互冲突
  8. vue3中移除keyCode作为v-on的修饰符,当然也不支持config.keyCodes
  9. vue3中移除过滤器filter
  10. computed和watch变成组合式的
  11. 数据响应重新实现(ES6的proxy代替Es5的Object.defineProperty)
  12. 源码使用ts重写,更好的类型推导
  13. 虚拟DOM新算法(更快,更小)

一.响应原理对比

vue2使用Object.defineProperty方法实现响应式数据

缺点:

  • 无法检测到对象属性的动态添加和删除
  • 无法检测到数组的下标和length属性的变更
  • 深度监听需要递归到底,性能层面考虑不太好

解决方案

  • vue2提供Vue.$set动态给对象添加属性
  • Vue.$delete动态删除对象属性
  • 也可以通过splice解决数组中的问题,object.assign解决批量添加对象属性的问题

Object.defineProperty还存在一个缺点:不能检测数组变化。2.x中是通过重写数组方法实现的对数组的监听。

vue3使用proxy实现响应式数据  

优点:

  • 可以检测到代理对象属性的动态新增和删除
  • 可以检测数组的下标和length属性的变化

缺点:

  • es6的proxy不支持低版本浏览器 IE11
  • 会针对IE11出一个特殊版本进行支持
  • 无法polyfill

二. Composition API

Vue2使用选项类型API(Options API):选项型API在代码里分割了不同的属性: data,computed属性,methods,等等。

Vue3使用合成型API(Composition API):新的合成型API能让我们用方法(function)来分割,相比于旧的API使用属性来分组,这样代码会更加简便和整洁

 // 2.0
export default {
    props: {
        title: String
    },
    data() {
        return {
            username: '',
            password: ''
        }
    },
    methods: {
        login() {
            // 登陆方法
        }
    },
    components: {
        "buttonComponent": btnComponent
    },
    computed: {
        fullName() {
            return this.firstName + " " + this.lastName;
        }
    }
}

// 3.0
export default {
    props: {
        title: String
    },

    setup() {
        const state = reactive({ //数据
            username: '',
            password: '',
            lowerCaseUsername: computed(() => state.username.toLowerCase()) //计算属性
        })
        //方法
        const login = () => {
            // 登陆方法
        }
        return {
            login,
            state
        }
    }
}
 
composition API 解决了什么问题

options API

  • 条例清晰:相同的放在相同的地方,比如方法都放在methods中,状态都在data中
  • 调用时使用this,逻辑过多时this指向不明确
  • 代码分散:一个功能的代码往往散落在不同的options种,比如data methods等等,这也导致新添加功能的时候需要在各种options中反复横跳,这时候如果代码行数较多那是要命的
  • 逻辑过于复杂的场景可以将某个功能代码抽象出mixin,但是这会导致数据来源不明确,在template中有个count变量,你会不知道他到底是来源于data还是mixin还是vue.prototype设置的全局变量。除此之外,如果存在多个mixin还可能存在同名变量被覆盖的问题。

composition API

  • 将一个功能的代码整合到一起,方便开发的同时也便于代码复用。总之就是更好的代码组织方式和更好的代码复用。
  • 没有对this的使用,避免了指向不明确的情况
  • 全部都是函数,更加方便类型推断

三.建立数据 data

VUE2.0中将数据放入到data属性中,在VUE3.0中使用setup()方法,此方法在组件初始化构造的时候触发。

使用以下三步来建立响应式数据:

  1. 从vue引入reactive
  2. 使用reactive()方法来声名我们的数据为响应性数据
  3. 使用setup()方法来返回我们的响应性数据,从而template可以获取这些响应性数据

可以通过state.username和state.password获得数据的值。

<template>
  <div>
    <h2> {{ state.username }} </h2>
  </div>
</template>

四.碎片

// 2.0
<template>
  <div class='form-element'>
      <h2> {{ title }} </h2>
  </div>
</template>

// 3.0
<template>
  <div class='form-element'>
  </div>
  <h2> {{ title }} </h2>
</template>

五.生命周期钩子函数改变

Vue2--------------vue3
beforeCreate  -> setup()开始创建组件之前,在beforeCreate和created之前执行。创建的是data和method
created       -> setup()
beforeMount   -> onBeforeMount 组件挂载到节点上之前执行的函数。
mounted       -> onMounted 组件挂载完成后执行的函数。
beforeUpdate  -> onBeforeUpdate 组件更新之前执行的函数。
updated       -> onUpdated 组件更新完成之后执行的函数。
beforeDestroy -> onBeforeUnmount 组件卸载之前执行的函数。
destroyed     -> onUnmounted 组件卸载完成后执行的函数
activated     -> onActivated 被包含在中的组件,会多出两个生命周期钩子函								数。被激活时执行 。
deactivated   -> onDeactivated 比如从 A组件,切换到 B 组件,A 组件消失									时执行。

六.父子传参

  1. setup 函数时,它将接受两个参数:(props、context(包含attrs、slots、emit))
  2. setup函数是处于生命周期函数 beforeCreate 和 Created 两个钩子函数之前的函数
  3. 执行 setup 时,组件实例尚未被创建(在 setup() 内部,this 不会是该活跃实例的引用,即不指向vue实例,Vue 为了避免我们错误的使用,直接将 setup函数中的this修改成了 undefined
  4. 与模板一起使用:需要返回一个对象 (在setup函数中定义的变量和方法最后都是需要 return 出去的不然无法在模板中使用)
  5. 使用渲染函数:可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态
  6. setup 函数中的 props 是响应式的,当传入新的 prop 时,它将被更新。但是,因为 props 是响应式的,你不能使用 ES6 解构,因为它会消除 prop 的响应性。如果需要解构 prop,可以通过使用 setup 函数中的toRefs 来完成此操作
父传子

所以当父组件向子组件中传递,和2.x版本中没有太多区别,但是如果需要从props中派生处数据时,需要从setup函数中接收。

// 父组件
<template>
  <Son :msg="state.msg"/>
</template>

<script>
import Son from "@/components/transParams/Son";
import {reactive} from "vue";
export default {
  name: "Parent",
  components: {Son},
  setup(){
    const state =reactive({
      msg:'父组件传递给子组件的参数'
    });
    return {
      state
    }
  }
}
</script>

<style scoped>

</style>

// 子组件
<template>
  <div>这里是子组件啦</div>
  <div>这里是父组件传递过来的值呀:{{msg}}</div>
</template>

<script>
export default {
  name: "Son",
  props:{
    msg:{
      type:String,
      default:''
    }
  },
  setup(props){
    console.log(props) //Proxy {msg: '父组件传递给子组件的参数'}
  }
}
</script>

<style scoped>

</style>
子传父(emit event)

子组件向父组件传递至差别比较大:

//父组件
<template>
  <div>这里是父组件啦</div>
  <div>这里是子组件传递过来的值呀:{{state.sonMsg}}</div>
  <hr/> 
  <!-- 子传父:定义子组件emit时的函数sonSendMsg,并且绑定到父组件中注册的receiveMessageFromSon函数 -->
  <Son @sonSendMsg="receiveMessageFromSon" :msg="state.msg"/>
</template>

<script>
import Son from "@/components/transParams/Son";
import {reactive} from "vue";

export default {
  name: "Parent",
  components: {Son},
  setup(){
    const state =reactive({
      msg:'父组件传递给子组件的参数',
      sonMsg:''
    });
    //子传父:定义接收函数
    const receiveMessageFromSon =(data)=>{
      state.sonMsg=data.sonMsg
    }
    return {
      state,
      receiveMessageFromSon
    }
  }
}
</script>

<style scoped>

</style>

// 子组件
<template>
  <div>这里是子组件啦</div>
  <button @click="sendMsgToParent">向父组件传递数据</button>
  <div>这里是父组件传递过来的值呀:{{msg}}</div>
</template>

<script>
export default {
  name: "Son",
  //差别一:子组件向父组件传值要注册emits
  emits:['sonSendMsg'],
  props:{
    msg:{
      type:String,
      default:''
    }
  },
  setup(props,{ attrs, slots, emit }){
    const sendMsgToParent =()=>{
      // 差别二:向父组件传递参数:不再通过this.$emit触发函数
      emit('sonSendMsg', {
        sonMsg:'子组件传递过来的数据'
      })
    };
    return {
      sendMsgToParent
    }
  }
}
</script>

<style scoped>

</style>

setup函数只能是同步的不能是异步的

七.vue3 Teleport瞬移组件

类比react中的传送门将组件挂载到想挂载的DOM上。

比如创建一个modal组件:

<template>
  <teleport to="#modal">
    <div id="center" v-if="isOpen">
      <h2><slot>this is a modal</slot></h2>
      <button @click="buttonClick">Close</button>
    </div>
  </teleport>
</template>

<script>
export default {
  name: "TeleportComponent",
  props: {
    isOpen: Boolean,
    closeModal:Function
  },
  emits: {
    'closeModal': null
  },
  setup(props, context) {
    const buttonClick = () => {
      context.emit('closeModal')
    }
    return {
      buttonClick
    }
  }
}
</script>

<style scoped>
#center {
  width: 200px;
  height: 200px;
  border: 2px solid black;
  background: white;
  position: fixed;
  left: 50%;
  top: 50%;
  margin-left: -100px;
  margin-top: -100px;
}
</style> 

使用方法如下:

<template>
  <div id="modal">这是modal即将挂载的元素</div>
  <button @click="showModal">点击我,打开Modal</button>
  <teleport-component :isOpen="isModalOpen" :closeModal="closeModal" />
</template>

<script>
import TeleportComponent from '@/components/TeleportComponent.vue';
import { ref } from 'vue'

export default {
  name: "TeleportPage",
  components:{TeleportComponent},
  setup(){
    const isModalOpen = ref(false)
    const closeModal = function(){
      isModalOpen.value=false
    };
    const showModal = function(){
      isModalOpen.value = true
    }
    return {
      closeModal,isModalOpen,showModal
    }
  }
}
</script>

<style scoped>

</style>

使用方式有两个,第一个是在app.vue中使用,但是这样存在的问题是:modal是在app的 DOM节点之下的,父节点的dom结构和css都会给modal产生影响

为了避免这个问题,我们可以在public文件夹下的index.html中增加一个节点<div id="modal"></div>,这样可以看到modal组件就是没有挂载在app下,不再受app组件的影响了

八.computed和watch

//2.0中
computed:{  //计算属性
            _suming(){
                return parseInt(this.one)+parseInt(this.two)
            },
            dataTimeing(){
                console.log("计算属性方法");
                // return "计算属性方法"+new Date()
                return "普通方法"+this.time
            }
        },

watch: {
            userName: {
                handler(val,res){
                    console.log(val);
                    console.log(res);
                },
                immediate:true,
                deep:true
            },
            
        }

//3.0中
<template>
    <section>my firstName is : {{ nameObj.firstName }}</section>
    <section>my lastName is : {{ nameObj.lastName }}</section>
    <section>my fullName is : {{ fullName }}</section>
    <p>my desc : {{ nameObj.desc }}</p>
    <button @click="changeLastName">click me to add a '!' to lastName</button>
    <p>打开控制台可以查看watch的情况</p>
    <button @click="changeHobby">修改hobby,测试深度监听</button>
</template>

<script>
import { computed, reactive, watch } from 'vue'

export default {
    name: 'ComputedAndWatch',
    setup() {
        const nameObj = reactive({
            firstName: 'Forever',
            lastName: 'Young',
            hobby: {
                ball: {
                    isLike: false
                }
            }
        });
        // 计算属性
        const fullName = computed(() => {
            return `${nameObj.firstName}_${nameObj.lastName}`
        });

        nameObj.desc = computed({
            get() {
                return `my name is ${nameObj.firstName}_${nameObj.lastName}`
            },
            // 此处的set好像没生效
            set(value) {
                console.log('set', value)
            }
        })
        const changeLastName = function () {
            nameObj.lastName = nameObj.lastName + '!'
        }
        // 监听属性
        // 可以深度监听到isLike属性的变化
        watch(nameObj, (newVal) => {
            console.info('watch nameObj 改变了', newVal)
        })
        // 只有更改nameObj.lastName会触发这个watch
        watch(()=>nameObj.lastName, (newVal) => {
            console.info('watch nameObj.lastName', newVal)
        })

        const changeHobby = function () {
            nameObj.hobby.ball.isLike = !nameObj.hobby.ball.isLike
        }
        return {
            nameObj,
            fullName,
            changeLastName,
            changeHobby
        }
    }

}
</script>

如果需要监听深度属性怎么办呢,我们都知道reactive是响应式数据属性,如果这个属性是对象,那么我们就可以开启深度监听

注意:我理解这不应该说成是深度监听,只能说是监听某个属性。由上述代码可以看出直接监听nameObj就可以监听到他内部isLike属性的变化,我觉得这已经足够深度监听了

//第一种
watch(()=> names.job.salary,(newValue,oldValue)=>{
  console.log('names改变了',newValue,oldValue)
})
//第二种
watch(()=> names.job,(newValue,oldValue)=>{
  console.log('names改变了',newValue,oldValue)
},{deep:true})

九.虚拟DOM算法差异

十.defineProperty实现响应式

// 触发更新视图
function updateView() {
    console.log('视图更新啦啦啦')
}

// -------------------------处理数组start-------------------
// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新的对象,原型指向oldArrayProperty,在扩展新的方法不会影响原型
// Array.prototype.push=function(){} 会污染全局
const arrProto = Object.create(oldArrayProperty);
//重写数组方法,弥补不能监听数组变化的缺陷
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodsName => {
    arrProto[methodsName] = function () {
        updateView()//更新视图
        oldArrayProperty[methodsName].call(this, ...arguments)
    }
})
// -------------------------处理数组end-------------------


// 重新定义属性,监听起来
function defineReactive(target, key, value) {
    // 深度监听-递归处理,在性能上有影响
    observer(value)

    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newVal) {
            if (newVal !== value) {
                // 设置新值,深度监听
                observer(newVal)

                value = newVal
                // 触发视图更新
                updateView()
            }
        }
    })

}

// 监听对象属性
function observer(target) {
    
    if (typeof target !== 'object' || target === null) {
        // 不是对象或者数组
        return target
    }
    // -------------------------处理数组start-------------------
    if (Array.isArray(target)) {
        target.__proto__ = arrProto
    }
    // -------------------------处理数组end-------------------

    // 重新定义属性,for-in可以遍历数组
    for (let key in target) { 
        defineReactive(target, key, target[key])
    }

}

// 准备数据
const data = {
    name: 'jerry',
    age: 18,
    info: {
        address: 'beijing'//深度监听
    },
    nums: [1, 2, 3, 4]
}

// 监听数据
observer(data)

// 测试
// data.name = 'tom'
// console.log('更新后名字',data.name)
// data.age = 20

// // 深度监听
// data.info.address = '唐山'

// // 设置新值深度监听
// data.age = { num: 21 }
// data.age.num = 22

// delete data.name// 删除属性监听不到
// data.x = '新增属性'//新增属性监听不到

// 数组监听
data.nums.push(5)

十一.proxy实现响应式

function reactive(target = {}) {
    if (typeof target !== 'object' || target == null) {
        // 不是数组或者对象
        return target
    }

    // 代理配置
    const proxyConf = {
        get(target, key, receiver) {
            // 只处理非原型的属性
            const ownKeys = Reflect.ownKeys(target)
            if (ownKeys.includes(key)) {
                console.log('get', key)
            }

            const result = Reflect.get(target, key, receiver)
            console.log('get', key)
            // return result //返回结果
            //深度监听
            return reactive(result)
        },
        set(target, key, val, receiver) {
            // 不重复修改数据
            const oldVal = target[key]
            if (oldVal === val) {
                return true
            }
            
            // 区别已有的key还是新增的key
            const ownKeys = Reflect.ownKeys(target)
            if (ownKeys.includes(key)) {
                console.log('已有的key', key)
            } else {
                console.log('新增的key', key)
            }

            const result = Reflect.set(target, key, val, receiver)
            console.log('set', key, val)
            console.log('set-result', result)
            return result //是否设置成功
        },
        deleteProperty(target, key) {
            const result = Reflect.deleteProperty(target, key)
            console.log('deleteProperty', key)
            console.log('delete result', result)
            return result //是否删除成功
        }
    }

    // 生成代理对象
    const observed = new Proxy(target, proxyConf)
    return observed
}


// 测试数据
const data = {
    name: 'jerry',
    age: 18,
    info: {
        address: 'beijing'
    }
}
// const data = [1, 2, 3]

const proxyData = reactive(data)

参考链接:

blog.csdn.net/m0_64346035…

blog.csdn.net/duguxueao/a…

blog.csdn.net/weixin_4393…

vue3中ref与reactive

使用ref例子如下:

<template>
    <p>姓名:{{ name }}</p>
    <p>年龄:{{ age }}</p>
    <p>职业:{{ job.occupation }}</p>
    <p>薪资:{{ job.salary }}</p>
    <button @click="change">修改薪资和年龄</button>
    <button @click="deepChange">检测深度监听</button>
</template>

<script>
import { is } from '@babel/types'
import { ref,watch } from 'vue'
export default {
    name: 'RefAndReactive',
    setup() {
        let name = ref('中介')
        let age = ref(18)
        let job = ref({
            occupation: '程序员',
            salary: '10k',
            up:{
                isUp:true
            }
        })
        // 直接监听job监听不到isUp的变化
        watch(()=>job.value.up.isUp,(newVal)=>{
            console.log('watch',newVal)
        })
        //方法
        function change() {
            job.value.salary = '12k'
            age.value = 19
        }
        function deepChange(){
            job.value.up.isUp = !job.value.up.isUp
        }
        return {
            name,
            age,
            job,
            change,
            deepChange
        }
    }
}
</script>

ref与reactive区别

  1. ref定义的是基本数据类型
  2. ref通过Object.defineProperty()的get和set实现数据劫持
  3. ref操作数据.value,读取时不需要.value
  4. reactive定义对象或数组数据类型
  5. reactive通过Proxy实现数据劫持
  6. reactive操作和读取数据不需要.value
  7. 监听深度不同

$nextTick 实现原理

参考链接

nextTick存在的原因是Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserversetImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

nextTick具体实现过程为:

  1. 将回调函数添加到callbacks中等待执行
  2. 将执行函数放入到宏任务(setImediate或者setTimeout)或者微任务(promise,mutationObserver)中
  3. 事件循环到了微任务或者宏任务,执行函数依次执行callbacks中的回调

可以看出来nextTick是对setTimeout进行了多种兼容性的处理,宽泛的也可以理解为将回调函数放入setTimeout中执行;不过nextTick优先放入微任务执行,而setTimeout是宏任务,因此nextTick一般情况下总是先于setTimeout执行

源码如下

let pending = false;
let callbacks = []; //存放的是回调函数,存放的第一个回调函数是数据更新的回调函数
let timerFunc = null;

//调用this.$nextTick时执行的函数
function nextTick(cb, ctx) {
  var _resolve;
  // 将回调函数已添加到callbacks
  callbacks.push(function () {
    if (cb) {
      try {
        cb.call(ctx);
      } catch (e) {
        handleError(e, ctx, 'nextTick');
      }
    } else if (_resolve) {
      _resolve(ctx);
    }
  });
  //在数据首次修改时,pending为false,修改后,pending变成true
  if (!pending) {
    pending = true;
    //在这里用到了事件循环
    timerFunc();
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(function (resolve) {
      _resolve = resolve;
    })
  }
}

//定义eventLoop函数---借助promise,mutationObserver,setImediate或者setTimeout将函数添加到事件循环的宏任务或者微任务中
// 判断是否支持promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  var p = Promise.resolve();
  timerFunc = function () {
    p.then(flushCallbacks);

    if (isIOS) { setTimeout(noop); }
  };
  isUsingMicroTask = true;
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  // 判断是否支持MutationObserver
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  var counter = 1;
  var observer = new MutationObserver(flushCallbacks);
  var textNode = document.createTextNode(String(counter));
  observer.observe(textNode, {
    characterData: true
  });
  timerFunc = function () {
    counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
  isUsingMicroTask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // 判断是否支持setImmediate
  timerFunc = function () {
    setImmediate(flushCallbacks);
  };
} else {
  // 前几项都不支持则使用setTimeout
  timerFunc = function () {
    setTimeout(flushCallbacks, 0);
  };
}

//清空事件队列中的回调函数,第一个回调函数是flushSchedulerQueue ()
function flushCallbacks() {
  pending = false;
  var copies = callbacks.slice(0);
  callbacks.length = 0;
  for (var i = 0; i < copies.length; i++) {
    copies[i]();
  }
}

//flushSchedulerQueue的核心代码,执行数据更新操作
function flushSchedulerQueue() {
  currentFlushTimestamp = getNow();
  flushing = true;
  var watcher, id;
  queue.sort(function (a, b) { return a.id - b.id; });
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index];
    if (watcher.before) {
      //调用beforeUpdate()钩子函数
      watcher.before();
    }
    id = watcher.id;
    has[id] = null;
    //执行更新
    watcher.run();
  }
}

v-for中没有Key和有key两种情况,diff算法怎么比较

diff

diff 算法主要进行vDOM的对比,找出差异,减少不必要的dom操作。

两个 js 对象或者两棵树都可以进行diff,但是两棵树的diff复杂度达到o(n^3),基本上不可用。

所以需要优化:

  • 只对比同一层的节点,不跨级比较
  • tag不同则删除重建,不进行深层
  • tag和key都相同,认为是同一节点,不深度比较

用到的重要函数

  • h函数,生成VNode
  • patch函数,接受两个参数,第一个是VNode或者element,第二个是VNode
  • sameVnode函数,接受两个vnode,判断两者的key和sel(即tag)是否相同,返回boolean值

patch函数

  • 第一个参数不是vnode, 创建一个空的vnode并关联到这个element
  • 两个参数都是vnode
    • 两个VNode相同(sameVnode返回true):patchVnode对比节点文本变化或子节点变化
    • 两个VNode不同:创建新的VNode,插入到VDom

pacthVnode

主要作用是对比两个VNode

  • old === new return
  • new.text === undefined (这基本上意味着新节点的children不为空
    • 旧节点有children:updateChildren
    • 旧节点没有children:旧节点的text设置为空,然后addNodes添加children
    • 旧节点有text:清空text
  • new.text !== undefined(新节点的children不存在)
    • old.text !== new.text 删除旧节点的children,设置新的节点的text

updateChildren

维持四个指针:

  • 新children的开始
  • 新children的结束
  • 旧children的开始
  • 旧children的结束

两两对比(sameVnode):

  • 新的开始和旧的开始
  • 新的开始和旧的结束
  • 新的结束和旧的结束
  • 新的结束和旧的开始
  • 以上四个判断命中的话:pacthVnode 并且移动指针
  • 以上四个均未命中:
    • 对比当前新节点的key是否对应旧节点的某个key
      • 没对应上:创建新的vnode并插入
      • 对应上了:找到对应旧节点,看sel是否相同
        • 不相同就创建新的vnode并且插入
        • 相同:patchVnode

在没有 Key 的情况下。sameVnode 下的 key 都是 undefined ,所以是相同的

答案

先说出diff本来已经存在以及遍历两棵树时,时间复杂度的问题以及优化。

vue维持四个指针相互进行比较,比较过程通过key和tag判断节点是否相同。

有key的情况下:key不同则认为两节点不同,没必要比较子节点直接销毁重建;

没有key时:明明两个节点不同但是由于他们的key都是undefined,此时如果tag相同,那么就会认为是相同节点,会去比较深层的节点

keep-alive实现原理

keep-alive组件的props维持includes,excludes和max分别表示要缓存的组件,不缓存的组件和缓存的最大值。

在created里边初始化cache和keys来存储已经保存的vnode和响应的keys。

在mounted里边监听includes和excludes 来实时更新cache和keys。

渲染的时候首先会找到keep-alive第一个子组件对象以及他的name,然后检查:是否满足不在include或者在exclude中(也就是检查是否满足不需要缓存组件),如果满足的话直接返回vnode,不满足的话(需要缓存)根据tag和ID生成缓存key,检查是否已经被缓存过,如果已经缓存过那就取出缓存并且更新key在keys中的位置,以便达到max时做资源置换,如果没缓存过,那就缓存到cache中,然后检测已缓存的实例对象是否达到max,已经达到的话按照LRU置换策略舍弃掉最近最不常用的那个(index===0的那个),keep-alive设置为true。

vue-router和window.location跳转的区别(对浏览器来讲)

  1. vue-router使用pushState进行路由更新,静态跳转,页面不会重新加载;location.href会触发浏览器页面重新加载一次
  2. vue-router使用diff算法,实现按需加载,减少dom操作
  3. vue-router是路由跳转或同一个页面跳转;location.href是不同页面间跳转;
  4. vue-router是异步加载this.$nextTick(()=>{获取url});location.href是同步加载

Vue如何进行组件封装

组件封装基本上需要考虑三个方面:

  1. props:外部传递给组件的数据
  2. 事件:组件触发外部的方法
  3. slot:外部注入到组件的视图

详情见

具体使用:

  1. 定义组件,像普通页面那样;

  2. 局部使用:在使用的页面中import,之后在components中注册;

    全局使用:在main.js中引入,然后使用Vue.component(组件名,组件)

双向数据绑定的机制

Vuejs的数据驱动是通过MVVM这种框架来实现的。MVVM框架主要包含3个部分:model、view和 viewModel。

  • Model:指的是数据部分,对应到前端就是javascript对象

  • View:指的是视图部分,对应前端就是dom

  • ViewModel:就是连接视图与数据的中间件

ViewModel是实现数据驱动视图的核心,当数据变化的时候,ViewModel能够监听到这种变化,并及时的通知view做出修改。同样的,当页面有事件触发时,ViewModel也能够监听到事件,并通知model进行响应。ViewModel就相当于一个观察者,监控着双方的动作,并及时通知对方进行相应的操作。

首先,vuejs在实例化的过程中,会对遍历传给实例化对象选项中的data 选项,遍历其所有属性并使用 Object.defineProperty 把这些属性全部转为 getter/setter。

同时每一个实例对象都有一个watcher实例对象,他会在模板编译的过程中,用getter去访问data的属性,watcher此时就会把用到的data属性记为依赖,这样就建立了视图与数据之间的联系。当之后我们渲染视图的数据依赖发生改变(即数据的setter被调用)的时候,watcher会对比前后两个的数值是否发生变化,然后确定是否通知视图进行重新渲染。这样就实现了所谓的数据对于视图的驱动。

一、vue-cli工程技术集合介绍

1. 构建的vue-cli工程都用到了哪些技术,它们的作用是什么?

vue.js:vue-cli工程的核心,主要特点是数据双向绑定和组件系统
vue-router:vue官方推荐使用的路由框架
vuex:专为Vue.js应用项目开发的状态管理器,主要用于维护vue组件间共用的一些变量和方
法。
asios:用于发起GETPOST等的http请求,基于Promise 设计
创建一个eenit.js文件,用于vue事件机制的管理
webpack:模块加载和vue-cli工程打包器

2. vue-cli工程中常用的npm命令有哪些?

下载node_modules资源包的命令:npm install

启动vue-cli开发环境的npm命令:npm run dev

vue-cli生成生产环境部署资源的npm命令:npm run build

用于查看vue-cli生产环境部署资源文件大小的npm命令:npm run build --report

会在浏览器上自动弹出一个展示vue-cli工程打包后app.js、manifest.js、vendor.js文件里面所包含代码的页面。可以具此优化vue-cli生产环境部署的静态资源,提升页面的加载速度。

二、vue-cli工程目录结构介绍

1.请说出vue-cli工程中每个文件夹和文件的用处

vue-cli目录解析:

  1. build 文件夹:用于存放 webpack 相关配置和脚本。开发中仅偶尔使用 到此文件夹下webpack.base.conf.js 用于配置 less、sass等css预编译库,或者配置一下 UI 库。
  2. config 文件夹:主要存放配置文件,用于区分开发环境、线上环境的不同。 常用到此文件夹下config.js 配置开发环境的端口号、是否开启热加载或者 设置生产环境的静态资源相对路径、是否开启gzip压缩、npm run build 命令打包生成静态资源的名称和路径等。
  3. dist 文件夹:默认 npm run build 命令打包生成的静态资源文件,用于生产部署。
  4. node_modules:存放npm命令下载的开发环境和生产环境的依赖包。
  5. src: 存放项目源码及需要引用的资源文件。
  6. src下assets:存放项目中需要用到的资源文件,css、js、images等。
  7. src下componets:存放vue开发中一些公共组件:header.vue、footer.vue等。
  8. src下emit:自己配置的vue集中式事件管理机制。
  9. src下router:vue-router vue路由的配置文件。
  10. src下service:自己配置的vue请求后台接口方法。
  11. src下page:存在vue页面组件的文件夹。
  12. src下util:存放vue开发过程中一些公共的.js方法。
  13. src下vuex:存放 vuex 为vue专门开发的状态管理器。
  14. src下app.vue:使用标签渲染整个工程的.vue组件。
  15. src下main.js:vue-cli工程的入口文件。
  16. index.html:设置项目的一些meta头信息和提供用于挂载 vue 节 点。
  17. package.json:用于 node_modules资源部 和 启动、打包项目的 npm 命令管理。

3. 请你详细介绍一些 package.json 里面的配置

  • scripts:npm run xxx 命令调用node执行的 .js 文件
  • dependencies:生产环境依赖包的名称和版本号,即这些依赖包都会打包进生产环境的JS文件里面
  • devDependencies:开发环境依赖包的名称和版本号,即这些依赖包只用于代码开发的时候,不会打包进生产环境js文件里面。

4. public和assets文件夹的区别

相同点:两者中的资源都可以被html使用

不同点:

若把图片放在assets和public中,html页面都可以使用,但是在动态绑定中,assets路径的图片会加载失败(因为webpack使用的是commenJS规范,必须使用require才可以)。

public放不会变动的文件,public建议放一些外部第三方资源。

public目录下的文件并不会被Webpack处理:它们会直接被复制到最终的打包目录(默认是dist/static)下。必须使用绝对路径引用这些文件。

assets放可能会变动的文件, 自己的文件放在assets。

assets目录中的文件会被webpack处理解析为模块依赖,只支持相对路径形式。

通过 webpack 处理会有如下好处:

  • 脚本和样式表会被压缩且打包在一起,从而避免额外的网络请求。
  • 文件丢失会直接在编译时报错,而不是到了用户端才产生 404 错误。
  • 最终生成的文件名包含了内容哈希,因此你不必担心浏览器会缓存它们的老版本。

三、Vue.js核心知识点高频试题一

1. Vue.js的两个核心是什么?

  1. 数据驱动,也叫双向数据绑定

    Vue.js数据观测原理在技术实现上,利用的是 Object.defineProperty和存储器 getter和setter(所以只兼容IE9及以上版本),可称为基于依赖收集的观测机制。核心是VM,即ViewModel,保证数据和视图的一致性。

  2. 组件系统 vue组件的核心选项:

    • 模板(template):模板声明了数据和最终展现给用户的DOM之间的映射关系
    • 初始数据(data):一个组件的初始数据状态。对于可复用的组件来说,这通常是私有的状态。
    • 接受的外部参数(props):组件之间通过参数来进行数据的传递和共享
    • 方法(methods):对数据的改动操作一般都在组建的方法内进行
    • 生命周期钩子函数(lifeCycle hooks):一个组件会出发多个生命周期钩子函数
    • 私有资源(assets):Vue.js当中经用户自定义的指令、过滤器、组件等统称为资源。一个组件可以声明自己的爱有资源。私有资源只有该组件和它的子组件可以调用

2.对于 Vue 是一套构建用户界面的渐进式框架的理解

Vue的核心的功能,是一个视图模板引擎,但这不是说Vue就不能成为一个框架。

在声明式渲染(视图模板引擎)的基础上,我们可以通过添加组件系统、客户端 路由、大规模状态管理来构建一个完整的框架。更重要的是,这些功能相互独立,你可以在核心功能的基础上任意选用其他的部件,不一定要全部整合在一起。可以看到,所说的“渐进式”,其实就是Vue的使用方式,同时也体现了Vue的设计的理念

渐进式代表的含义是:没有多做职责之外的事。

vue.js只提供了 vue-cli 生态中最核心的组件系统和双向数据绑定。

像vuex、vue-router都属于围绕 vue.js开发的库。

比如说,你要使用Angular,必须接受以下东西:

  • 必须使用它的模块机制
  • 必须使用它的依赖注入-
  • 必须使用它的特殊形式定义组件(这一点每个视图框架都有,难以避免)

所以Angular是带有比较强的排它性的,如果你的应用不是从头开始,而是要不断考虑是否跟其他东西集成,这些主张会带来一些困扰。

比如说,你要使用React,你必须理解:

  • 函数式编程的理念,
  • 需要知道什么是副作用,
  • 什么是纯函数,
  • 如何隔离副作用

它的侵入性看似没有Angular那么强,主要因为它是软性侵入。

Vue与React、Angular的不同是,但它是渐进的:

  • 你可以在原有大系统的上面,把一两个组件改用它实现,当jQuery用;
  • 也可以整个用它全家桶开发,当Angular用;
  • 还可以用它的视图,搭配你自己设计的整个下层用。
  • 你可以在底层数据逻辑的地方用OO和设计模式的那套理念,
  • 也可以函数式,都可以,它只是个轻量视图而已,只做了最核心的东西。

3.请说出vue几种常用的指令

  • v-if:根据表达式的值的真假条件渲染元素。在切换时元素及它的数据绑定 / 组件被销毁并重建。
  • v-show:根据表达式之真假值,切换元素的 display CSS 属性。
  • v-for:循环指令,基于一个数组或者对象渲染一个列表,vue 2.0以上必须需配合 key使用。
  • v-bind:动态地绑定一个或多个特性,或一个组件 prop 到表达式。
  • v-on:用于监听指定元素的DOM事件,比如点击事件。绑定事件监听器。
  • v-model:实现表单输入和应用状态之间的双向绑定
  • v-pre:在模板中跳过vue的编译,直接输出原始值。就是在标签中加入v-pre就不会输出vue中的data值了。跳过大量没有指令的节点会加快编译。比如<div v-pre>{{message}}</div>这时并不会输出我们的message值,而是直接在网页中显示{{message}}
  • v-once:只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。

4.请问 v-if 和 v-show 有什么区别

共同点:

v-if 和 v-show 都是动态显示DOM元素。

区别:

  1. 编译过程: v-if 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。v-show 的元素始终会被渲染并保留在 DOM 中。v-show 只是简单地切换元素的 CSS 属性display。

  2. 编译条件: v-if 是惰性的:如果在初始渲染时条件为假,则什么也不做。直到条件第一次变为真时,才会开始渲染条件块。v-show不管初始条件是什么,元素总是会被渲染,并且只是简单地基于CSS 进行切换。

  3. 性能消耗: v-if有更高的切换消耗。v-show有更高的初始渲染消耗。

  4. 应用场景: v-if适合运行时条件很少改变时使用。v-show适合频繁切换。

5.vue常用的修饰符

  • v-on 指令常用修饰符:

    • .stop - 调用 event.stopPropagation(),禁止事件冒泡。
    • .prevent - 调用 event.preventDefault(),阻止事件默认行为。
    • .capture - 添加事件侦听器时使用 capture 模式。
    • .self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。
    • .native - 监听组件根元素的原生事件。
    • .once - 只触发一次回调。
    • .left - (2.2.0) 只当点击鼠标左键时触发。
    • .right - (2.2.0) 只当点击鼠标右键时触发。
    <!-- 阻止单击事件继续传播 -->
    <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">
    

    注意: 如果是在自己封装的组件或者是使用一些第三方的UI库时,会发现并不起效果,这时就需要用.native修饰符了,如:

    //使用示例:
    <el-input
    v-model="inputName"
    placeholder="搜索你的文件"
    @keyup.enter.native="searchFile(params)"
    >
    </el-input>
    
  • v-bind 指令常用修饰符:

    • .sync (2.3.0+) 语法糖,会扩展成一个更新父组件绑定值的 v-on 侦听器。
  • v-model 指令常用修饰符:

    • .lazy - 取代 input 监听 change 事件
    • .number - 输入字符串转为数字
    • .trim - 输入首尾空格过滤

6.v-on可以监听多个方法吗?

v-on可以监听多个方法,例如:

<input type="text" :value="name" @input="onInput" @focus="onFocus"
@blur="onBlur" />

但是同一种事件类型的方法,vue-cli工程会报错,例如:

<a href="javascript:;" @click="methodsOne" @click="methodsTwo"></a>

7.vue中 key 值的作用

key值:用于管理可复用的元素。因为vue会尽可能高效的渲染元素,通常会复用已有元素而不是从头开始渲染。这么做会使Vue变得非常快,但是这样也不总是符合实际需求。

2.2.0+ 的版本里,当在组件中使用 v-for 时,key 现在是必须的。

例如,如果你允许用户在不同的登录方式之间切换:

<template v-if="loginType === 'username'">
    <label>Username</label>
    <input placeholder="Enter your username">
</template>

<template v-else>
    <label>Email</label>
    <input placeholder="Enter your email address">
</template>

那么在上面的代码中切换loginType 将不会清除用户已经输入的内容。因为两个模板使用了相同的元素,</ input>不会被替换掉,仅仅是替换了它的placeholder。

这样也不总是符合实际需求,所以Vue为你提供了一种方式来表达这两个元素是完全独立的,不要复用它们。只需添加一个具有唯一值的 key 属性即可:

<template v-if="loginType === 'username'">
    <label>Username</label>
    <input placeholder="Enter your username" key="username-input">
</template>

<template v-else>
    <label>Email</label>
    <input placeholder="Enter your email address" key="email-input">
</template>

现在,每次切换时,输入框都将被重新渲染。

8.vue事件中如何使用event对象?

如果直接传递具体的值。可以像如下:

<button @click="event('123')">修饰符</button>

event(message){
    console.log(message); //123
}

如果需要访问原始的DOM事件,可以使用特殊变量 $event,使用方法如下:

<button @click="event($event)">修饰符</button>

event(e){
    console(e); //MouseEvent事件
}
<button data-id='event' @click="event($event)">修饰符</button>

event(e){
    console.log(e.srcElement.dataset.id); //event
}

ref的使用:

<div ref="name">
    <button data-id='event' @click="event($event)">修饰符</button>
</div>
 
event(){
    console.log(this.$refs.name)
}

9.什么是$nextTick?

简单回答:因为Vue的异步更新队列,$nextTick是用来知道什么时候DOM更新完成的。

异步更新队列:指的是当状态发生变化时,Vue异步执行DOM更新。

DOM的异步更新:

<template>
    <div>
        <div ref="test">{{test}}</div>
        <button @click="handleClick">tet</button>
    </div>
</template>
export default {
    data () {
        return {
            test: 'begin'
        };
    },
    methods () {
        handleClick () {
            this.test = 'end';
            console.log(this.$refs.test.innerText);//打印“begin”
        }
    }
}

vue官方文档中是这样说的:

可能你还没有注意到,Vue异步执行DOM更新。只要观察到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变化。如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作上非常重要。然后,在下一个事件循环在“tick”中,Vue刷新队列并执行实际(已去重)的工作

简而言之,就是在一个事件循环中发生的所有数据改变都会在下一个事件循环的Tick中来触发视图更新,这也是一个“批处理”的过程。(注意下一个事件循环的Tick有可能是在当前的Tick微任务执行阶段执行,也可能是在下一个Tick执行,主要取决于nextTick函数到底是使用Promise/MutationObserver还是setTimeout)

为什么要异步更新视图

看下面的代码:

<template>
    <div>
        <div>{{test}}</div>
    </div>
</template>

export default {
    data () {
        return {
            test: 0
        };
    },
    mounted () {
        for(let i = 0; i < 1000; i++) {
            this.test++;
        }
    }
}

现在有这样的一种情况,mounted的时候test的值会被++循环执行 1000 次。 每次++时,都会根据响应式触发setter->Dep->Watcher->update->run。 如果这时候没有异步更新视图,那么每次++都会直接操作DOM更新视图,这是非常消耗性能的。

所以Vue实现了一个queue队列,在下一个Tick(或者是当前Tick的微任务阶段)的时候会统一执行queue中Watcher的run。同时,拥有相同id的Watcher不会被重复加入到该queue中去,所以不会执行 1000 次Watcher的run。最终更新视图只会直接将test对应的DOM的 0 变成 1000 。 保证更新视图操作DOM的动作是在当前栈执行完以后下一个Tick(或者是当前Tick的微任务阶段)的时候调用,大大优化了性能。

Vue会根据当前浏览器环境优先使用原生的Promise.then和MutationObservery以及setImmediate,如果都不支持,就会采用setTimeout代替。

应用场景

在操作DOM节点无效的时候,就要考虑操作的实际DOM节点是否存在,或者相应的DOM是否被更新完毕。

  • 在created钩子中涉及DOM节点的操作肯定是无效的,因为此时还没有完成相关DOM的挂载。

    解决的方法就是在nextTick函数中去处理DOM,这样才能保证DOM被成功挂载而有效操作。

  • 在数据变化之后要执行某个操作,而这个操作需要使用随数据改变而改变的DOM时,这个操作应该放进Vue.nextTick。

  • 获取this.$refs.refName 返回undefined时,证明组件可能尚未存在,这时候可以在Vue.nextTick获取。

10.Vue 组件中 data 为什么必须是函数

因为一个组件是可以共享的,但它们的data是私有的,所以每个组件都return一个新的data对象,返回一个唯一的对象,不要和其他组件共用一个对象。

Vue.component('my-component', {
    template: '<div>OK</div>',
    data() {
        return {} // 返回一个唯一的对象,不要和其他组件共用一个对象进行返回
    },
})

实际上,它的使用过程如下:

  • 首先需要创建一个组件构造器
  • 然后注册组件
  • 注册组件的本质其实就是建立一个组件构造器的引用
  • 使用组件才是真正的创建一个组件实例
  • 所以,注册组件其实并不是产生新的组件类,但会产生一个可以用用来实例化的新方式

理解这个过程之后,在理解js的原型链:

var MyComponent = function() {}

MyComponent.prototype.data = {
    a: 1,
    b: 2, 
}

上面是一个虚拟的组件构造器,真实的组件构造器方法很多:

var component1 = new MyComponent()
var component2 = new MyComponent()

上面实例化出来两个组件实例,也就是通过调用,创建的两个实例

component1.data.a === component2.data.a // true
component1.data.b = 5
component2.data.b // 5

从上面的代码可以看出,如果两个实例同时引用一个对象,那么当你修改其中一个属性的时候,另外一个实例也会跟着改。两个实例应该自己各有自己的实例域才对,做更改:

var MyComponent = function() {
    this.data = this.data()
}
MyComponent.prototype.data = function() {
    return {
        a: 1,
        b: 2,
    }
}

这样每一个实例的data属性都是独立的,不会相互影响了。所以,你现在知道为什么vue组件的data必须是函数了吧。这都是因为js本身的特性带来的,跟vue本身设计无关。

11.v-for 与 v-if 的优先级

当它们处于同一节点,v-for的优先级比v-if更高,这意味着 v-if将分别重复运行于每个 v-for循环中。当你想为仅有的一些项渲染节点时,这种优先级的机制会十分有用,如下:

<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo }}
</li>

上面的代码只传递了未完成的 todos。 而如果你的目的是有条件地跳过循环的执行,那么可以将 v-if置于外层元素 (或 < template>)上。如:

<ul v-if="todos.length">
    <li v-for="todo in todos">
    {{ todo }}
    </li>
</ul>
<p v-else>No todos left!</p>

注意:当你只想条件性的渲染列表中的某些项的时候建议使用computed进行过滤后使用v-for进行渲染,而不是v-for和v-if同时使用。

四、Vue.js核心知识点高频试题二

4.1 组件传参

props/$emit

组件是Vue.js中最强大的功能之一,而组件实例的作用域是相互独立的,这就意味着不同组件之间的数据无法相互引用。一般来说,组件可以有以下几种关系:

  • 父子关系
  • 兄弟关系
  • 个贷关系
父组件向子组件传值

子组件:

<template>
    <div>
        <ul>
            //遍历传递过来的值,然后呈现到页面
            <li v-for="user in users" :key="user">{{user}}</li>
        </ul>
    </div>
</template>
<script>
export default {
    name: "zizujian",
    props: {
        users: { //这个就是父组件中子标签自定义名字
            type: Array,
            require: true
        }
    }
};
</script>

父组件

<template>
    <div>
        <User :users="users"></User>
    </div>
</template>
<script>
import User from "../zizujian";
export default {
    name: "fuzujian",
    components: { User },
    data() {
        return {
            users: ["ma", "nan", "nan"]
        };
    }
};

总结:父组件通过 props向下传递数据给子组件。

注:组件中的数据共有三种形式: data 、 props、 computed

子组件向父组件传值(通过事件形式)

子组件

<template>
    <header>
        <h1 @click="changeTitle">{{title}}</h1>
    </header>
</template>
<script>
export default {
    name: "zizujian",
    data() {
        return {
            title: "Vue.js Demo"
        };
    },
    methods: {
        changeTitle() {
            //自定义事件,传递值“子组件向父组件传值”
            this.$emit("titleChanged", "子组件向父组件传值"); 
        }
    }
}

父组件

<template>
    <div>
        <Header @titleChanged="updateTitle"></Header>//与子组件titleChanged自定义事件保持一致
        <h2>{{title}}</h2>
    </div>
</template>
<script>
import Header from "../zizujian";
export default {
    name: "fuzujian",
    components: { Header },
    data() {
        return {
            title: "传递的是一个值"
        };
    },
    methods: {
        updateTitle(e) {
            this.title = e;
        }
    }
};

总结:子组件通过 event给父组件发送消息,实际上就是子组件把自己的数据发送到父组件。

$emit/$on

这种方式通过一个空的Vue实例作为中央事件总线(事件中心),用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、跨级。

当我们的项目比较大时,可以选择更好的转台管理解决方案Vuex

具体实现:

var Event=new Vue();

// 发送数据
Event.$emit(事件名,数据);

// 接收数据
Event.$on(事件名,data => {});

vuex

vuex适用于一个状态需要在多个组件中同时使用的场景。避免传来传去逻辑复杂难以维护。

$attrs/$listeners

多级组件嵌套传递数据,仅仅是传递数据,不做中间处理

$attrs:包含了父作用域中不被 prop所识别(且获取)的特性绑定。当一个组件没有声明任何prop时,这里会包含所有父作用域的绑定,并且可以通过 v-bind="$attrs" 传入内部组件。通常配合inheritAttrs选项一起使用。

$listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件

// index.vue
<template>
    <div>
        <h2>浪里行舟</h2>
        <child-com
        :foo="foo"
        :boo="boo"
        :coo="coo"
        :doo="doo"
        title="前端工匠"
        ></child-com1>
    </div>
</template>

<script>
const childCom1 = () => import("./childCom1.vue");
export default {
    components: { childCom1 },
    data() {
        return {
            foo: "Javascript",
            boo: "Html",
            coo: "CSS",
            doo: "Vue"
        };
    }
};
</script>
// childCom1.vue
<template class="border">
    <div>
        <p>foo: {{ foo }}</p>
        <p>childCom1的$attrs: {{ $attrs }}</p>
        <child-com2 v-bind="$attrs"></child-com2>
    </div>
</template>

<script>
const childCom2 = () => import("./childCom2.vue");
export default {
    components: {childCom},
    inheritAttrs: false, // 可以关闭自动挂载到组件根元素上的没有在props声明的属性
    props: {
        foo: String // foo作为props属性绑定
    },
    created() {
        console.log(this.$attrs);
        // { "boo": "Html", "coo": "CSS", "doo":"Vue", "title": "前端工匠" }
    }
};
</script>
// childCom2.vue
<template>
  <div class="border">
    <p>boo: {{ boo }}</p>
    <p>childCom2: {{ $attrs }}</p>
    <child-com3 v-bind="$attrs"></child-com3>
  </div>
</template>

<script>
const childCom3 = () => import("./childCom3.vue");
export default {
    components: {childCom},
    inheritAttrs: false,
    props: {
        boo: String
    },
    created() {
        console.log(this.$attrs); 
        // { "coo": "CSS", "doo": "Vue", "title":"前端工匠" }
    }
};
</script>
// childCom3.vue
<template>
    <div class="border">
    <p>childCom3: {{ $attrs }}</p>
    </div>
</template>

<script>
export default {
    props: {
        coo: String,
        title: String
    }
};
</script>

Vue2.4提供了$attrs/$listeners 来传递数据与事件,跨级组件之间的通讯变得更简单。

简单来说:$attrs与$listeners 是两个对象,$attrs 里存放的是父组件中绑定的非 Props 属性,$listeners里存放的是父组件中绑定的非原生事件。

provide/inject

Vue2.2.0新增API,这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。一言而蔽之:祖先组件中通过provider来提供变量,然后在子孙组件中通过inject来注入变量。

provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。

假设有两个组件: A.vue 和 B.vue,B 是 A 的子组件

// A.vue
export default {
    provide: {
        name: '浪里行舟'
    }
}

// B.vue
export default {
    inject: ['name'],
    mounted () {
        console.log(this.name); // 浪里行舟
    }
}

需要注意的是:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的

所以,上面 A.vue 的 name 如果改变了,B.vue 的 this.name 是不会改变的,仍然是"浪里行舟"。

provide与 inject如何实现数据响应式

一般来说,有两种办法:

  • provide祖先组件的实例,然后在子孙组件中注入依赖,这样就可以在子孙组件中直接修改祖先组件的实例的属性,不过这种方法有个缺点就是这个实例上挂载很多没有必要的东西比如props,methods
  • 使用2.6最新API Vue.observable 优化响应式 provide(推荐)

我们来看个例子:孙组件D、E和F获取A组件传递过来的color值,并能实现数据响应式变化,即A组件的color变化后,组件D、E、F会跟着变(核心代码如下:)

//A 组件
<div>
    <h1>A 组件</h1>
    <button @click="() => changeColor()">改变color</button>
    <ChildrenB />
    <ChildrenC />
</div>
......
data() {
    return {
        color: "blue"
    };
},
// provide() {
//  return {
//      theme: {
//          color: this.color //这种方式绑定的数据并不是可响应的
//      } // 即A组件的color变化后,组件D、E、F不会跟着变
//  };
// },

provide() {
    return {
        theme: this//方法一:提供祖先组件的实例
    };
},
methods: {
    changeColor(color) {
        if (color) {
            this.color = color;
        } else {
            this.color = this.color === "blue"? "red" : "blue";
        }
    }
}

// 方法二:使用2.6最新API Vue.observable 优化响应式 provide
 provide() {
     this.theme = Vue.observable({
        color: "blue"
     });
     return {
        theme: this.theme
     };
 },
 methods: {
     changeColor(color) {
         if (color) {
            this.theme.color = color;
         } else {
            this.theme.color = this.theme.color === "blue"? "red" :"blue";
         }
     }
 }
 
// F 组件
<template functional>
    <div class="border2">
        <h3 :style="{ color: injections.theme.color }">F 组件</h3>
      </div>
</template>
<script>
export default {
  inject: {
    theme: {
      //函数式组件取值不一样
      default: () => ({})
    }
} };
</script>

虽说provide 和 inject 主要为高阶插件/组件库提供用例,但如果你能在业务中熟练运用,可以达到事半功倍的效果!

$parent / $children与 ref

  • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例

  • $parent / $children:访问父 / 子实例

需要注意的是:这两种都是直接得到组件实例,使用后可以直接调用组件的方法或访问数据。我们先来看个用 ref来访问组件的例子:

// component-a 子组件
export default {
    data () {
        return {
            title: 'Vue.js'
        }
    },
    methods: {
        sayHello () {
            window.alert('Hello');
        }
    }
}

// 父组件
<template>
    <component-a ref="comA"></component-a>
</template>
<script>
export default {
    mounted () {
        const comA = this.$refs.comA;
        console.log(comA.title); // Vue.js
        comA.sayHello(); // 弹窗
    }
}
</script>

不过,这两种方法的弊端是,无法在跨级或兄弟间通信。

// parent.vue
<component-a></component-a>
<component-b></component-b>
<component-b></component-b>

我们想在 component-a 中,访问到引用它的页面中(这里就是parent.vue)的两个 component-b组件,那这种情况下,就得配置额外的插件或工具了,比如 Vuex 和 Bus 的解决方案。

组件传参场景总结

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

  • 父子通信: 父向子传递数据是通过 props,子向父是通过 events($emit);通过父链 / 子链也可以通信($parent / $children);ref 也可以访问组件实例;provide / inject API;$attrs/$listeners
  • 兄弟通信: Bus;Vuex
  • 跨级通信: Bus;Vuex;provide / inject API、$attrs/$listeners

4.2 vue中 keep-alive 组件的作用

keep-alive:主要是用于保留组件状态和避免重新渲染

其是一个抽象组件(或称为功能性组件),实际上不会被渲染在DOM树中。它的作用是在内存中缓存组件(不让组件销毁),等到下次在渲染的时候,还会保持其中的所有状态,并且会触发 activated钩子函数

属性:(属性表示要缓存的组件名,即组件定义时的name属性)

  • include:字符串或正则表达式,只有匹配的组件会被缓存
  • exclude:字符串或正则表达式,任何匹配的组件都不会被缓存

被包含在 keep-alive中创建的组件,会多出两个生命周期的钩子: activated与 decativated

  • activated:在组件被激活时调用,在组件第一次渲染时也会被调用,之后每次 keep-alive激活时被调用
  • decativated:在组件被停用时调用。

注意:只有组件被 keep-alive 包裹时,这两个生命周期才会被调用,如果作为正常组件使用,是不会被调用,以及在 2.1.0 版本之后,使用 exclude 排除之后,就算被包裹在 keep-alive中,这两个钩子依然不会被调用!另外在服务端渲染时此钩子也不会被调用的。

实际应用:(下面的写法是需要将整个路由页面缓存下来的写法)

在App.vue中修改为如下:

<keep-alive>
    <router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>

<router-view v-if="!$route.meta.keepAlive"></router-view>

在router/index.js中为每一个路由添加:

meta:{
    keepAlive:true/false //true代表这个页面(组件)需要缓存,false代表不需要
}

实际应用:(下面的写法是将组件在内存中进行缓存--没有设么意义)

<!-- 基本 -->
<keep-alive>
    <component :is="view"></component>
</keep-alive>

<!-- 多个条件判断的子组件 -->
<keep-alive>
    <comp-a v-if="a > 1"></comp-a>
    <comp-b v-else></comp-b>
</keep-alive>

<!-- 和 `<transition>` 一起使用 -->
<transition>
    <keep-alive>
        <component :is="view"></component>
    </keep-alive>
</transition>

注意:<keep-alive>是用在其一个直属的子组件被开关的情形。如果你在其中有 v-for 则不会工作。如果有上述的多个条件性的子元素,<keep-alive> 要求同时只有一个子元素被渲染

include 和 exclude 属性的使用

include 和 exclude 属性允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示:

<!-- 逗号分隔字符串 -->
<keep-alive include="a,b">
    <component :is="view"></component>
</keep-alive>


<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
    <component :is="view"></component>
</keep-alive>


<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
    <component :is="view"></component>
</keep-alive>

匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件components 选项的键值)。匿名组件不能被匹配。 不会在函数式组件中正常工作,因为它们没有缓存实例。

4.3 vue中如何编写可复用的组件?

在编写组件的时候,时刻考虑组件是否可复用是有好处的。一次性组件跟其他组件紧密耦合没关系,但是可复用组件一定要定义一个清晰的公开接口。

Vue.js组件 API 来自 三部分:prop、事件、slot:

  • prop 允许外部环境传递数据给组件,在vue-cli工程中也可以使用vuex等传递数据。
  • 事件允许组件触发外部环境的 action
  • slot 允许外部环境将内容插入到组件的视图结构内。

4.4 vue生命周期

1. 什么是vue生命周期和生命周期钩子函数?

vue的生命周期是:vue实例从创建到销毁,也就是从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、卸载等一系列过程。

在这个过程中也会运行一些叫做生命周期钩子的函数,这给用户在不同阶段添加自己的代码的机会。

2. vue生命周期钩子函数有哪些?

  • beforeCreate:在实例初始化之后,数据观测(data observer)和 event/watcher事件配置之前被调用

  • created:在实例创建完成后被立即调用。在这一步,实例已完成以下配置:数据观测(data observer),属性和方法的运算,watch/event事件回调。然而挂载阶段还没有开始,$el属性目前不可见

  • beforeMount:在挂载开始之前,render函数首次被调用

  • mounted:el被新创建的 vm.el替换,并挂载到实例上去之后调用该钩子。如果root实例被挂载了一个文档内元素,当mounted被调用时vm. el也在文档内

  • beforeUpdate:数据更新时调用,发生在虚拟DOM打补丁之前。这里 适合在更新之前访问现有的DOM,比如手动移除已添加的事件监听器。该钩子在服务器端渲染期间不被调用,因为只有出此渲染会在服务器端进行。

  • updated:由于数据更改导致的虚拟DOM重新渲染和打补丁,在这之后会调用该钩子

  • activated:keep-alive组件被激活的时候调用。该钩子在服务器端渲染期间不被调用

  • deactived:keep-alive组件停用的时候调用。该钩子在服务器端渲染期间不被调用

  • beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用。该 钩子在服务器端渲染期间不被调用

  • destroyed:Vue实例销毁后调用。调用后,Vue实例指示的所有东西都会解绑定,所有的事件监听器都会被移除,所有的子实例也会被消除。该钩子在服务器端渲染期间不被调用

  • errorCaptured(2.5.0新增):当捕获一个来自子孙组件的错误时被调用。此钩子会接收三个参数:错误对象,发生错误的组件实例以及一个 包含错误来源信息的字符串。此钩子可以返回false以组织该错误继续向上传播。

注意:

  1. mounted、updated不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用vm.$nextTick 替换掉mounted、updated:
  2. http请求建议在created 生命周期内发出

4.5 vue如何监听键盘事件中的按键

在监听键盘事件时,我们经常需要检查常见的键值。Vue允许为 v-on在监听键盘事件时添加按键修饰符:

<!-- 只有在 `keyCode` 是 13 时调用 `vm.submit()` -->
<input v-on:keyup.13="submit">

记住所有的 keyCode 比较困难,所以 Vue 为最常用的按键提供了别名:

<input v-on:keyup.enter="submit">
<!-- 缩写语法 -->
<input @keyup.enter="submit">

全部的按键别名:

.enter
.tab
.delete (捕获“删除”和“退格”键)
.esc
.space
.up
.down
.left
.right

可以通过全局 config.keyCodes 对象自定义按键修饰符别名:

// 可以使用 `v-on:keyup.f1`
Vue.config.keyCodes.f1 = 112

鼠标按钮修饰符:2.2.0 新增

.left
.right
.middle

这些修饰符会限制处理函数仅响应特定的鼠标按钮。

4.6 vue更新数组时触发视图更新的方法

Vue 包含一组观察数组的变异方法,所以它们也将会触发视图更新。这些方法如下:

push()
pop()
shift()
unshift()
splice()
sort()
reverse()

替换数组:

例如:filter(), concat()和 slice() 。这些不会改变原始数组,但总是返回一个新数组。当使用这些非变异方法时,可以用新数组替换旧数组:

example1.items = example1.items.filter(function (item) {
    return item.message.match(/Foo/)
})

你可能认为这将导致 Vue 丢弃现有 DOM 并重新渲染整个列表。幸运的是,事实并非如此。Vue 为了使得 DOM 元素得到最大范围的重用而实现了一些智能的、启发式的方法,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。

注意事项:

由于 JavaScript 的限制,Vue 不能检测以下变动的数组:

  1. 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength

举个例子:

var vm = new Vue({
    data: {
        items: ['a', 'b', 'c']
    }
})
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的

为了解决第一类问题,以下两种方式都可以实现和vm.items[indexOfItem] = newValue 相同的效果,同时也将触发状态更新:

// Vue.set
Vue.set(vm.items, indexOfItem, newValue)

// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)

你也可以使用vm.$set实例方法,该方法是全局方法 Vue.set 的一个别名:

vm.$set(vm.items, indexOfItem, newValue)

为了解决第二类问题,你可以使用 splice:

vm.items.splice(newLength)

4.7 vue中对象更改检测的注意事项

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

var vm = new Vue({
    data: {
        a: 1
    }
})
// `vm.a` 现在是响应式的

vm.b = 2
// `vm.b` 不是响应式的

对于已经创建的实例,Vue 不能动态添加根级别的响应式属性。但是,可以使用 Vue.set(object,key, value)方法向嵌套对象添加响应式属性。例如,对于:

var vm = new Vue({
    data: {
        userProfile: {
            name: 'Anika'
        }
    }
})

你可以添加一个新的 age 属性到嵌套的 userProfile对象:

Vue.set(vm.userProfile, 'age', 27)

你还可以使用 vm.$set实例方法,它只是全局Vue.set 的别名:

vm.$set(vm.userProfile, 'age', 27)

有时你可能需要为已有对象赋予多个新属性,比如使用 Object.assign()或 _.extend()。在这种情况下,你应该用两个对象的属性创建一个新的对象。所以,如果你想添加新的响应式属性,不要像这样:

Object.assign(vm.userProfile, {
    age: 27,
    favoriteColor: 'Vue Green'
})

应该这样做:

vm.userProfile = Object.assign({}, vm.userProfile, {
    age: 27,
    favoriteColor: 'Vue Green'
})

4.8 如何解决非工程化项目,网速慢时初始化页面闪动问题?

使用 v-cloak指令,v-cloak不需要表达式,它会在Vue实例结束编译时从绑定的HTML元素上移除,经常和CSS的display:none配合使用。

<div id="app" v-cloak>
    {{message}}
</div>
<script>
var app = new Vue({
    el:"#app",
    data:{
        message:"这是一段文本"
    }
})
</script>

这时虽然已经加了指令v-cloak,但其实并没有起到任何作用,当网速较慢、Vue.js 文件还没加载完时,在页面上会显示{{message}}的字样,直到Vue创建实例、编译模版时,DOM才会被替换,所以这个过程屏幕是有闪动的。只要加一句CSS就可以解决这个问题了:

<!--属性选择器-->
[v-cloak]{
    display:none;
}

在一般情况下,v-cloak是一个解决初始化慢导致页面闪动的最佳实践,对于简单的项目很实用。

4.9 v-for产生的列表,如何实现active样式的切换?

通过设置当前 currentIndex + 动态样式实现:

<template>
    <div class="toggleClassWrap">
        <ul>
            <li @click="currentIndex = index" v-bind:class="
            {clicked: index === currentIndex}" v-for="(item, index) in desc"
            :key="index">
                <a href="javascript:;">{{item.ctrlValue}}</a>
            </li>
        </ul>
    </div>
</template>

<script type="text/javascript">
export default{
    data () {
        return {
            desc:[{
                ctrlValue:"test1"
                },{
                ctrlValue:"test2"
                },{
                ctrlValue:"test3"
                },{
                ctrlValue:"test4"
            }],
        
            currentIndex:0
        }
    }
}
</script>
<style type="text/css" lang="less">
.toggleClassWrap{
    .clicked{
        color:red;
    }
}
</style>

4.10 v-model语法糖的使用

使用v-model来进行双向数据绑定的时:

<input v-model="something">

仅仅是一个语法糖:

<input v-bind:value="something" v-on:input="something =
$event.target.value">

所以要组件的v-model生效,它必须:

  • 接受一个value属性
  • 在有新的value时触发input事件

五、Vue.js核心知识点 高频试题三

5.1 vue-cli工作中如何自定义一个过滤器?

在src目录下创建一个filter目录,在创建一个index.js文件,将过滤器们放在filter/index.js中

这个文件主要是写了过滤器实现的方法,然后export进行导出。

function filterOne(n){
    return n + 10;
}
function filterTwo(n){
    return n + 5;
}
export{
    filterOne,
    filterTwo
}

之后在main.js中:

import * as filters from './filter/filter.js'

//遍历所有导出的过滤器并添加到全局过滤器
Object.keys(filters).forEach((key) => {
    Vue.filter(key, filters[key]);
})

在.vue组件中使用:

{{test | filterOne}}

5.2 vue-cli工作中如何自定义一个指令

1. 什么是vue.js中的自定义指令

自定义一些指令对DOM进行操作

2. 自定义指令的几个钩子函数

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 。
  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
  • unbind:只调用一次,指令与元素解绑时调用。

3. 钩子函数的参数

除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行。

指令钩子函数会被传入以下参数:

  • el:指令所绑定的元素,可以用来直接操作 DOM 。
  • binding:一个对象,包含以下属性:
    • name:指令名,不包括 v- 前缀。
    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2
    • oldValue:指令绑定的前一个值,仅在update和 componentUpdated钩子中可用。无论值是否改变都可用。
    • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。
    • arg:传给指令的参数,可选。例如 v-my-directive:foo中,参数为 "foo"。
    • modiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为{ foo: true, bar: true }。
  • vnode:Vue 编译生成的虚拟节点。
  • oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。

4. 如何在vue-cli中使用自定义指令

文件结构:

├── src
│ ├── directives
│ │ ├── index.js
│ │ ├── modules
│ └── main.js
└── ...

在modules下新建foucs.js下

// 聚焦指令
export default {
    bind (el, binding, vnode) {},
    inserted (el, binding, vnode) {
        el.focus()
    },
    update (el, binding, vnode) {},
    componentUpdated (el, binding, vnode) {},
    unbind (el, binding, vnode) {}
    }

在src/directives/index.js下

import focus from './modules/focus'
export {focus}

在src/main.js下,使用directives自定义指令

//引入自定义指令
import * as directives from './directives'
//注册指令(全局注册)
Object.keys(directives).forEach(k => Vue.directive(k, directives[k]));

在.vue组件中使用

<input v-focus type="text" />

5.3 vue等单页面应用及其优缺点

单页Web应用(single page web application,SPA):就是只有一张Web页面的应用。

单页应用程序 (SPA) 是加载单个HTML 页面并在用户与应用程序交互时动态更新该页面的Web应用程序。

浏览器一开始会加载必需的HTML、CSS和JavaScript,所有的操作都在这张页面上完成,都由JavaScript来控制。因此,对单页应用来说模块化的开发和设计显得相当重要。

单页Web应用的优点:

  1. 良好的用户体验: 单页应用的内容的改变不需要重新加载整个页面,web应用更具响应性和更令人着迷。单页应用没有页面之间的切换,就不会出现“白屏现象”,也不会出现假死并有“闪烁”现象

  2. 单页应用相对服务器压力小,服务器只用出数据就可以,不用管展示逻辑和页面合成,吞吐能力会提高几倍。

  3. 良好的前后端分离。后端不再负责模板渲染、输出页面工作,后端API通用化,即同一套后端程序代码,不用修改就可以用于Web界面、手机、平板等多种客户端。

单页Web应用的缺点:

  1. 不利于SEO,由于是采用前端渲染的方式,搜索引擎不会去解析Js从而只能够抓取首页未渲染的模板,如果需要单页面应用有更好的SEO,那么通常需要使用SSR服务端渲染,搜索引擎爬虫抓取工具可以直接查看完全渲染的页面,但是由于是服务端进行渲染,那么会对服务器造成一定压力,SSR服务端渲染属CPU密集型,当然如果只是需要SEO少数几个页面,可以采用预渲染的方式。
  2. 首次加载速度慢,SPA单页应用通常首次加载页面时就会将相应的HTML、JavaScript、CSS文件全部加载,通常可以通过采取缓存措施以及懒加载即按需加载组件的方式来优化。

5.4 什么是vue的计算属性(computed)和侦听属性(watch)

1. 计算属性

计算属性是自动监听依赖值的变化,从而动态返回内容,监听是一个过程,在监听的值变化时,可以触发一个回调,并做一些事情。它有以下特点:

  • 数据可以进行逻辑处理,减少模板中计算逻辑
  • 对计算属性中的数据进行监视
  • 依赖固定的数据类型(响应式数据)

计算属性由两部分构成:get和set,分别用来获取计算属性和设置计算属性。默认只有get,如果需要set,要自己添加。另外set设置属性,并不是直接修改计算属性,而是修改它的依赖。

computed: {
    fullName: {
        // getter
        get: function () {
            return this.firstName + ' ' + this.lastName
        },
        // setter
        set: function (newValue) {
            //this.fullName = newValue 这种写法会报错
            var names = newValue.split(' ')
            this.firstName = names[0]//对它的依赖进行赋值
            this.lastName = names[names.length - 1]
        }
    }
}

计算属性VS普通属性:

可以像绑定普通属性一样在模板中绑定计算属性,在定义上的区别:计算属性的属性值必须是一个函数,函数必须有返回值,属性值就是函数的返回值

计算属性VS方法:

  1. 计算属性必须返回结果
  2. 计算属性是基于它的依赖缓存的。一个计算属性所依赖的数据发生变化时,它才会重新取值。
  3. 使用计算属性还是methods取决于是否需要缓存,当遍历大数组和做大量计算时,应当使用计算属性,除非你不希望得到缓存。
  4. 计算属性是根据依赖自动执行的,methods需要事件调用。

两者最主要的区别:computed 是可以缓存的,methods 不能缓存;只要相关依赖没有改变,多次访问计算属性得到的值是之前缓存的计算结果,不会多次执行。

网上有种说法就是方法可以传参,而计算属性不能,其实并不准确,计算属性可以通过闭包来实现传参:

:data="closure(item, itemName, blablaParams)"
computed: {
    closure () {
        return function (a, b, c) {
            /** do something */
            return data
        }
    }
}

2. 侦听属性

Vue 提供了一种更通用的方式来观察和响应 Vue 实例上的数据变动:侦听属性watch。watch中可以执行任何逻辑,如函数节流,Ajax异步获取数据,甚至操作 DOM(不建议)。

使用 watch 来监听数据变化的时候除了常用到 handler 回调,其实其还有两个参数,便是:

  • deep 设置为 true 用于监听对象内部值的变化
  • immediate 设置为 true 将立即以表达式的当前值触发回调

3. 两者之间的对比

  • watch:监测的是属性值,只要属性值发生变化,其都会触发执行回调函数来执行一系列操作。

  • computed:监测的是依赖值,依赖值不变的情况下其会直接读取缓存进行复用,变化的情况下才会重新计算。

  • computed:一个数据受多个数据影响

  • watch:一个数据影响多个数据

除此之外,有点很重要的区别是:计算属性不能执行异步任务,计算属性必须同步执行。也就是说计算属性不能向服务器请求或者执行异步任务。如果遇到异步任务,就交给侦听属性。watch也可以检测computed属性。

4. 总结

  • 计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。

  • computed能做的,watch都能做,反之则不行。

  • 能用computed的尽量用computed

5.5 vue-cli开发环境使用全局常量

1.如何在组件中使用全局常量

第一步,在 src 下新建 const 文件夹下 新建 const.js

.
├── src
│ ├── const
│ │ ├── const.js
│ │
│ └── main.js
└── ...

第二步,在 const.js 文件下,设置常量

export default {   
    install(Vue,options){
        Vue.prototype.global = {
            title:'全局',
            isBack: true,
            isAdd: false,
        };
    }
}

第三步,在 main.js 下全局引入:

import constant from './const/const.js'
Vue.use(constant);

第四步,即可在 .vue 组件中使用:

//通过js方式使用:
this.global.title
//或在 html 结构中使用
{{global.title}}

2.在JS中使用常量

第一步,在 src 下新建 const 文件夹下 新建 type.js

.
├── src
│ ├── const
│ │ ├── type.js
│ │
│ └── main.js
└── ...

第二步,在 type.js 文件下,设置常量

export const TEST_INCREMENT='TEST_INCREMENT'
export const TEST_DEREMENT='TEST_DEREMENT'

第三步,在其他 .js 文件下引入并使用:

//以对象的形式引入:
import * as types from '../types'
//使用:
types.TEST_INCREMENT

5.6 父组件如何异步获取动态数据之后传递给子组件

  • v-if:开始的时候让子组件隐藏,然后等数据返回的时候,让子组件显示。(不过这种方式存在问题,当接口返回数据很慢的时候子组件延迟很久显示,这并不符合逻辑)

  • vuex

    • 利用vuex的辅助函数(mapState,mapMutations)mapState是将state里面的数据映射到计算中(computed),mapMutations也是类似,把vuex中mutations的方法映射到组件里面,就可以在组件里面直接使用方法了,在vuex中使用异步(actions)去掉用接口,然后在接口成功的函数里面取触发同步(mutations)里面的方法,把得到数据传给mutations里面的方法里并且给state里面的属性赋值,然后就可以在子组件中使用computed计算中去获取数据并且渲染到页面上