vue框架最全知识点

151 阅读23分钟

vue基础知识

vue指令

vue指令 v-on参数传递问题 v-on修饰符 v-show和v-if的区别 v-model修饰符 v-model修饰符原理 计算属性computed 计算属性的setter和getter 计算属性computed和methods的区别 计算属性computed和watch的区别 过滤器 keep-alive内置组件 watch $nextTick 组件中的data为什么是函数 父子组件访问属性 vue组件通信方式 vue的生命周期函数 vue如何监听键盘事件 vue中key的作用 为什么v-for不建议使用index做key? 直接给一个数组项赋值,Vue 能检测到变化吗? MVC和MVVM区别 真实DOM和虚拟DOM Vue2 vs Vue3 在哪个生命周期内调用异步请求? 父子组件生命周期钩子函数执行顺序? vue-router路由 路由的理解 vue-router原理 vue-router使用 router-link router-link属性 router-view 路由跳转 vue-router参数传递 router和route动态路由 两种路由模式hash和history 路由懒加载 路由导航守卫 全局守卫 路由守卫 组件守卫 应用场景 vue-router面试题 Vuex集中状态管理 vuex作用及管理图例 vuex封装 组件中vuex的使用 读数据 mutations修改数据 actions修改数据 vuex面试题 vuex是不是永久性存储 axios axios封装 vue框架原理 双向数据绑定原理 vue响应式原理 Object.defineProperty() 数据劫持的缺点 defineProperty和proxy diff算法 diff算法对比流程 vue订阅者发布者模式

1、v-bind动态绑定

  • 动态绑定属性:
<img v-bind:src="url" alt="" id="app">
  • 动态绑定class:有对象语法和数组语法
<h2 :class="{active: active, line: line}">{{message}}</h2> 
或者 
<h2 :class="[active]">{{message}}</h2>
  • 动态绑定style:有对象语法和数组语法
:style="{font-size: '50px'}" 
或者
:style="{font-size: finalsize + 'px'}"(其中finalsize是一个变量)

2、v-on动态绑定事件
@click="btnClick"
3、v-once
v-once:数据不允许修改,也就是没有响应式
<div v-once>{{firstname}}</div>
4、v-html:解析html语法
<h2 v-html="url"></h2>
5、v-cloak:在vue解析之前,有属性v-cloak,解析之后没有属性v-cloak

[v-cloak]:{ 
    display:none
}
v-on参数传递问题

总结:方法不加(),会传event,加(),不会传event

<div id="app">
    <!-- 在没有形参时,可以加() 也可以不加() -->
    <button @click="btnClick">按钮1</button>

    <!--结果为undefined -->
    <button @click="btnClick2()" > 按钮2</button >

    < !--结果为浏览器产生的事件MouseEvent -->
    <button @click="btnClick2" > 按钮3</button >

    < !--结果为undefined  undefined-- >
    <button @click="btnClick3()" > 按钮4</button >

    < !--结果为Event  undefined-- >
    <button @click="btnClick3" > 按钮5</button >

    < !--结果为文洁  event-- >
    <button @click="btnClick4(message,$event)" > 按钮6</button >

    总结:不加(),会传event,加(),不会传event

</div >
v-on修饰符
@click.stop:阻止冒泡

@click.prevent:点击提交之后不会自动跳转到另一个页面

@click.once:只能调用一次(再点就没用了)

@keyup="keyUp":键盘弹起的时候调用keyUp函数

@keyup.enter="keyUp":只有回车键弹起的时候调用keyUp函数
v-show和v-if的区别

1、本质区别:v-show是将样式修改为display:none来隐藏元素,而v-if是在DOM树内删除元素
2、编译区别:v-show其实就是在控制css;即使v-show的初始值为false,也只是将display设为none,但它也编译了。v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-if初始值为false,就不会编译了
3、性能区别:v-show只编译一次,而v-if不停的销毁和创建,故v-show性能更好一点
总结:显示与隐藏切换很频繁时,使用v-show,只有一次切换时,使用v-if

v-model修饰符
  • lazy:只有在光标消失或用户回车时,才会更新message
  • number:不加number,会自动把输入的数据转为string
  • trim:会自动去除message两边的空格,在控制台可见,页面不可见
v-model修饰符原理
  • v-model其实是一个语法糖,它的背后本质上是包含两个操作:
    1、v-bind绑定一个value属性
    2、v-on指令给当前元素绑定input事件
    <input type="text" v-model="message">
    等同于
    <input type="text" v-bind:value="message" v-on:input="message = $event.target.value">
计算属性computed
computed: {
    fullname: function() {
        return this.firstname + ' ' + this.secondname
    }
}
计算属性的setter和getter
computed: {
    fullname: {
        //在对属性赋值的时候调用
        set: function() { },
        //在取值的时候调用
        get: function() { }
    }
}
计算属性computed和methods的区别

1、主要区别:当计算属性没有依赖data中的数据时,第一次使用计算属性时,会把第一次的结果进行缓存,后面再次使用计算属性,都会去第一次的结果中进行查找,所以计算属性相当于只执行了一次;而methods每调用一次,执行一次
2、使用上的区别:计算属性在使用时,不加(),methods()方法在调用时,()可加可不加

计算属性computed和watch的区别

computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;

运用场景:

  • 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
  • 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
过滤器

过滤器是一个函数,它会把表达式中的值始终当作函数的第一个参数。 作用:用于过滤数据,改变用户看到的输出,一般用于对文本的格式化。比如保留两位小数,或者想要什么样的日期格式。
使用场景:

  • 需要格式化数据的情况,比如保留两位小数。
  • 比如后端返回一个 年月日的日期字符串,前端需要展示为 多少天前 的数据格式,此时就可以用fliters过滤器来处理数据。
filter: {
    getValue(value){
        value.toFixed(2);//保留两位小数
    }
}
keep-alive内置组件

keep-alive 是 Vue 内置的一个组件,作用是可以使被包含的组件保留状态,避免重新渲染,比如我在首页浏览到中间的时候,跳转到分类的组件,然后想要回到首页的时候,保持刚才浏览的地方,就可以利用keep-alive来保存首页的状态和数据,避免首页这个组件的重新渲染 ,其有以下特性:

  • 提供 include 和 exclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;
  • 对应两个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。

使用场景:
组件切换的时候,如果需要保存当前组件的状态与数据,再次回来后不重新渲染,减少加载时间,提高性能,就可以使用keep-alive。

watch

handler:其值是⼀个回调函数。监听到变化时应该执⾏的函数。
deep:其值是true或false;确认是否深⼊监听。
immediate:其值是true或false;确认是否以当前的初始值执⾏handler的函数。
当需要监听⼀个对象的改变时,普通的watch⽅法⽆法监听到对象内部属性的改变,此时需要deep属性对对象进⾏深度监听

new Vue({
    el: "#first",
    data: { msg: { name: '北京' } },
    watch: {
        msg: {
            handler(newMsg, oldMsg) {
                console.log(newMsg);
            },
            immediate: true,
            deep: true
        }
    }
})
mixin

在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,这些代码的功能相对独立,可以通过 Vue 的 mixin 功能抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用 mergeOptions 方法进行合并,采用策略模式针对不同的属性进行合并。当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。

export default function initMixin(Vue) {
    Vue.mixin = function (mixin) {
        //   合并对象
        this.options = mergeOptions(this.options, mixin)
    };
}
  };

// src/util/index.js
// 定义生命周期
export const LIFECYCLE_HOOKS = [    "beforeCreate",    "created",    "beforeMount",    "mounted",    "beforeUpdate",    "updated",    "beforeDestroy",    "destroyed",];

// 合并策略
const strats = {};
// mixin核心方法
export function mergeOptions(parent, child) {
    const options = {};
    // 遍历父亲
    for (let k in parent) {
        mergeFiled(k);
    }
    // 父亲没有 儿子有
    for (let k in child) {
        if (!parent.hasOwnProperty(k)) {
            mergeFiled(k);
        }
    }

    //真正合并字段方法
    function mergeFiled(k) {
        if (strats[k]) {
            options[k] = strats[k](parent[k], child[k]);
        } else {
            // 默认策略
            options[k] = child[k] ? child[k] : parent[k];
        }
    }
    return options;
}
$nextTick

理解: 在一轮事件循环中,同步执行栈中代码执行完成之后,才会执行异步队列当中的内容,那我们获取DOM的操作是一个同步的呀!!那岂不是虽然我已经把数据改掉了,但是它的更新异步的,而我在获取的时候,它还没有来得及改。所以你放在$nextTick 当中的操作不会立即执行,而是等数据更新、DOM更新完成之后再执行,这样我们拿到的肯定就是最新的了

是vue中的api,官方对nexttick的描述是:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。通俗一点说,vue 在修改数据后,视图不会立刻进行更新,而是要等同一事件循环机制内所有数据变化完成后,再统一进行DOM更新,那么当我们修改data中的数据时,因为数据更新是异步过程,所以并不能立刻通过操作DOM去获取到里面的值,但是可以利用$nexttick获取更新后的数据

原理: 因为Vue中 数据变化 => DOM变化 是异步过程,所以一旦观察到数据变化,Vue就会开启一个任务队列,然后把在同一个事件循环 (Event loop) 中观察到数据变化的 Watcher推送进这个队列。

如果这个watcher被触发多次,只会被推送到队列一次。这种缓冲行为的目的就是有效的去掉重复数据造成的不必要的计算和DOM操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。nextTick的作用是为了在数据变化之后等待 Vue 更新 DOM ,然后可以在数据变化之后立即使用 Vue.nextTick(callback),JS是单线程的,拥有事件循环机制,nextTick的实现就是利用了事件循环的宏任务和微任务。

使用场景:

  • 当我们修改了data里的数据时,并不能立刻通过操作DOM去获取到里面的值,而使用nexttick可以,因为数据变化之后会立即调用vue.nexttick

image.png

  • 在vue生命周期中,如果在created()钩子进行DOM操作,也一定要放在nextTick()的回调函数中。因为在created()钩子函数中,页面的DOM还未渲染,这时候也没办法操作DOM,所以,此时如果想要操作DOM,必须将操作的代码放在nextTick()的回调函数中,等待下一轮事件循环开始的时候,DOM已经挂载好了,挂载好之后会调用mounted钩子函数,然后再去调用nexttick回调函数。
组件中的data为什么是函数

多个组件复用时
如果data是一个对象,那因为对象本身是引用类型,所以每一个组件实例对象的 data 都指向同一块内存,一个组件实例中对data进行修改,会引起其他组件中data的改变,从而造成组件间的数据污染。
如果data是一个函数,每次调用data函数的时候都会return一个新的对象,它们的内存地址都是不一样的,这样就不会相互影响。另外,在实际的开发过程中,不同组件可能由不同的开发人员负责,data中的每个数据的命名极易冲突,所以组件间的数据独立是非常有必要的。

父子组件访问属性

父组件访问子组件:
1、子组件加上ref="aaa"
2、父组件,this.$refs['aaa'].name访问name属性

子组件访问父组件:
1、this.$parent
2、this.$root
一般不建议子访问父:
1、因为子组件具有可复用性,放到另一个地方,可能它的父组件没有这个属性
2、会使组件间的耦合度很高

vue组件通信方式

Vue 组件间通信主要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信。

  • 父子组件通信:props(父传子)、$emit / v-on(子传父)、$children / $parent(父子通信)、ref(子传父)
  • 隔代组件通信:$attrs/$listeners(隔代通信)、provide / inject(隔代通信)
  • 兄弟组件通信:EventBusVuex
  1. 父子组件通信
  • props(父传子)
    父组件通过在子组件标签中传送数据,在子组件中使用props配置项接收数据。
// Parent.vue 传送
<template>
    <child :msg="msg"></child>
</template>

// Child.vue 接收
export default {
  // 写法一 用数组接收
  props:['msg'],
  // 写法二 用对象接收,可以限定接收的数据类型、设置默认值、验证等
  props:{
      msg:{
          type:String,
          default:'这是默认数据'
      }
  },
}
  • $emit / v-on(子传父)
    子组件通过this.$emit()自定义事件派发数据,父组件通过v-on指令响应事件。
// Child.vue 派发
export default {
  data(){
      return { msg: "子给父传的信息" }
  },
  methods: {
      handleClick(){
          this.$emit("sendMsg",this.msg)
      }
  },
}
// Parent.vue 响应
<template>
    <child v-on:sendMsg="getChildMsg"></child>
    // 或 简写
    <child @sendMsg="getChildMsg"></child>
</template>

export default {
    methods:{
        getChildMsg(msg){
            console.log(msg) // 这是父组件接收到的消息
        }
    }
}
  • $children / $parent(父子通信)
    $children:获取到一个包含所有子组件(不包含孙子组件)的 VueComponent 对象数组,可以直接拿到子组件中所有数据和方法等 $parent:获取到一个父节点的 VueComponent 对象,同样包含父节点中所有数据和方法等
// Parent.vue
export default{
    mounted(){
        this.$children[0].someMethod() // 调用第一个子组件的方法
        this.$children[0].name // 获取第一个子组件中的属性
    }
}
// Child.vue
export default{
    mounted(){
        this.$parent.someMethod() // 调用父组件的方法
        this.$parent.name // 获取父组件中的属性
    }
}
  • ref(子传父)
    ref 如果在普通的DOM元素上,引用指向的就是该DOM元素;如果在子组件上,引用的指向就是子组件实例,然后父组件就可以通过 ref 主动获取子组件的属性或者调用子组件的方法
// Child.vue
export default {
    data(){
        return {
            name:"沐华"
        }
    },
    methods:{
        someMethod(msg){
            console.log(msg)
        }
    }
}

// Parent.vue
<template>
    <child ref="child"></child>
</template>
<script>
export default {
    mounted(){
        const child = this.$refs.child
        console.log(child.name) // 沐华
        child.someMethod("调用了子组件的方法")
    }
}
</script>

2、隔代组件通信

  • $attrs/$listeners(隔代通信)
    多级组件嵌套需要传递数据时,通常使用的方法是通过vuex。但如果仅仅是传递数据,而不做中间处理,使用 vuex 处理,未免有点大材小用。为此Vue2.4 版本提供了另一种方法----$attrs/$listeners
    $attrs:包含父作用域里除 classstyle 除外的非 props 属性集合。通过 this.$attrs 获取父作用域中所有符合条件的属性集合,然后通过 v-bind="$attrs"继续传给孙子组件。
    $listeners:包含父作用域里 .native 除外的监听事件集合。如果还要继续传给子组件内部的其他组件,就可以通过 v-on="$linteners"
// Parent.vue
<template>
    <Child :name="qwn" title="姓名" ></Child>
</template

// Child.vue
<template>
    // 继续传给孙子组件
    <sun-child v-bind="$attrs"></sun-child>
</template>

// SonChild.vue
export default{
    props:["name"], // 这里可以接收,也可以不接收
    mounted(){
        // 如果props接收了name就是{ title:"姓名" },否则就是{ name:"qwn", title:"姓名" }
        console.log(this.$attrs)
    }
}
  • provide / inject(隔代通信)
    provide:指定想要提供给后代组件的数据或方法
    inject:在任何后代组件中接收想要添加在这个组件上的数据或方法,不管组件嵌套多深都可以直接拿来用

要注意的是 provide 和 inject 传递的数据不是响应式的,也就是说用 inject 接收来数据后,provide 里的数据改变了,后代组件中的数据不会改变,除非传入的就是一个可监听的对象所以建议还是传递一些常量或者方法。

// 父组件
export default{
    // 方法一 不能获取 this.xxx,只能传写死的
    provide:{
        name:"沐华",
    },
    // 方法二 可以获取 this.xxx
    provide(){
        return {
            name:"qwn",
            msg: this.msg // data 中的属性
            someMethod:this.someMethod // methods 中的方法
        }
    },
    methods:{
        someMethod(){
            console.log("这是注入的方法")
        }
    }
}

// 后代组件
export default{
    inject:["name","msg","someMethod"],
    mounted(){
        console.log(this.msg) // 这里拿到的属性不是响应式的,如果需要拿到最新的,可以在下面的方法中返回
        this.someMethod()
    }
}

3、兄弟组件通信

EventBus

Vuex

vue的生命周期函数

vue的生命周期函数分为:

  1. 创建前:beforeCreate :创建vue实例前调用
    el 和data并未初始化,因此无法访问 methods,data ,computed等方法和数据。
  2. 创建后:created:创建实例之后调用
    这个阶段可以数据请求,但是不能dom操作。
  3. 挂载前:beforeMount:还未将编译生成的html挂载到对应的位置
    把data里面的数据和模板生成html,完成了el和data初始化,注意此时还没有挂载到html页面上。
  4. 挂载后:mounted:挂载完成,HTML已经被渲染到了页面上
    这个阶段可以执行dom操作,可以进行数据请求。
  5. 数据更新前:beforeUpdate:数据更新前调用
    当data的数据发生改变会执行这个钩子 内存中数据是新的 页面是旧的。
  6. 数据更新后:updated:updated执行后,页面和data的数据保持同步,都是最新的。
    当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。注意 updated 不会保证所有的子组件也都一起被重绘。如果你希望等到整个视图都重绘完毕,可以在 updated 里使用 vm.$nextTick。
  7. 销毁前:beforeDestroy:实例销毁之前调用,在这一步还可以做一些释放内存的操作。
  8. 销毁后:destroy:实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有.的事件监听器被移除,所有的子实例也都被销毁。
  9. activated:keep-alive 缓存的组件激活时调用。
  10. deactivated:keep-alive 缓存的组件停用时调用。 image.png
vue框架面试题
vue如何监听键盘事件
  1. keyup方法
  2. addEventListener
    document.addEventListener('keyup', this.handleKey)
vue中修饰符有哪些

事件修饰符

  • .stop 阻止事件继续传播
  • .prevent 阻止标签默认行为
  • .capture 使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理
  • .self 只当在 event.target 是当前元素自身时触发处理函数
  • .once 事件将只会触发一次
  • .passive 告诉浏览器你不想阻止事件的默认行为

v-model 的修饰符

  • .lazy 通过这个修饰符,转变为在 change 事件再同步
  • .number 自动将用户的输入值转化为数值类型
  • .trim 自动过滤用户输入的首尾空格

键盘事件的修饰符

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

系统修饰键

  • .ctrl
  • .alt
  • .shift
  • .meta

鼠标按钮修饰符

  • .left
  • .right
  • .middle
vue中key的作用

尽可能的复用已经有的元素,比如有DOM元素 A B C D E ,当我想把元素变成 B C D E 时,没有key值时,key默认都是undefined,就会按照diff算法的就地复用来进行比较,它会把A更新成B,B更新成C,C更新成D,最后删除E;有唯一的key值时,B C D E全部复用,只删除A
明显可以看出,当没有key值时改变元素会产生许多DOM操作,而DOM操作是非常消耗性能的,那么使用key值就可以降低DOM操作。

  1. v-if 中使用 key
    由于 Vue 会尽可能复用已有元素而不是从头开始渲染。因此当使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。如果是相同的 input 元素,如果不使用key,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。因此可以通过使用 key 来唯一的标识一个元素
  2. v-for 中使用 key
    如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。 key 的作用是为了高效的更新渲染虚拟 DOM
为什么v-for不建议使用index做key?
  • 用index作为key可能会引发的问题:

    • 若对数据进行:逆序添加、逆序删除等破坏顺序操作:会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。
    • 如果结构中还包含输入类的DOM:会产生错误DOM更新 ==> 界面有问题。
  • 开发中如何选择key?

    • 最好使用每条数据的唯一标识作为key, 比如id、手机号、身份证号、学号等唯一值。
    • 如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。
直接给一个数组项赋值,Vue 能检测到变化吗?

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

  • 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
    解决方案:
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// vm.$set,Vue.set的一个别名
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
  • 当你修改数组的长度时,例如:vm.items.length = newLength
    解决方案
// Array.prototype.splice 
vm.items.splice(newLength)
MVC和MVVM区别

MVC: Model(模型) View(视图) Controller(控制器)的缩写,本质上是用一种将数据、界面显示、业务逻辑分离的方法组织代码的软件开发设计典范。

  • Model:数据层,负责操作数据,执行数据的CRUD,职能单一。
  • View:视图层,每当用户操作界面,就需要进行业务的处理,都会通过网络请求去服务端请求服务器。
  • Controller:业务逻辑层,作为中间人负责数据层和视图层的交互。
  1. 视图发生变化触发Controller,并且将数据传递给Controller
  2. Controller拿到更新的数据触发model并将更新的数据传递给model
  3. model拿到数据更新数据并且触发view视图更新

总结: Controller接受View的指令,做出业务逻辑,然后Model进行数据操作,这个过程是单向的
缺点:
1、前后端没办法独立开发,必须要等接口做好了才能往下做
2、前端没有自己的数据中心,太过于依赖后台
MVVM: 是Model(模型) View(视图) ViewModel(调度者)的缩写,是客户端视图层分离的概念,本质上是将其中的View的状态和行为抽象化,让我们将视图UI和业务逻辑分开。与MVC最大的区别就是mvc是单向的,而mvvm是双向的,并且是自动的,也就是数据发生变化自动同步视图,视图发生变化自动同步数据,同时解决了 mvc 中大量的 DOM 操作使页面渲染性能降低,加载速度变慢,影响用户体验。和当 Model 频繁发生变化,开发者需要主动更新到 View

  • Model:数据层,保存每个页面中单独的数据。
  • View:视图层,每个页面中的HTML结构。
  • ViewModel:视图模型层,分离Model和View,每当View需要获取或者保存数据时,都要通过VM做中间的处理。

总结: MVVM实现了View和Model的自动同步,不用手动操作Dom,即Model变化时View可以实时更新,View变化也能改变Model,从而实现数据的双向绑定。

怎样理解vue的单向数据流

据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
如果实在要改变父组件的 prop 值 可以再 data 里面定义一个变量 并用 prop 的值初始化它 之后用$emit 通知父组件去修改

注意:在子组件直接用 v-model 绑定父组件传过来的 prop 这样是不规范的写法 开发环境会报警告

vue中使用了哪些设计模式

1.工厂模式 - 传入参数即可创建实例
虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode
2.单例模式 - 整个程序有且仅有一个实例
vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
3.发布-订阅模式 (vue 事件机制)
4.观察者模式 (响应式数据原理)
5.装饰模式: (@装饰器的用法)
6.策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略

vue模板编译原理

Vue 的编译过程就是将 template 转化为 render 函数的过程 分为以下三步

  1. 第一步是将 模板字符串 转换成 element ASTs(解析器)
  2. 第二步是对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)
  3. 第三步是 使用 element ASTs 生成 render 函数代码字符串(代码生成器)
真实DOM和虚拟DOM

真实的dom是在浏览器通过dom.api操作的复杂的对象
虚拟dom是真实dom的副本
虚拟dom的过程:
1:首先根据数据创建虚拟dom,然后由虚拟dom创建真实的dom树,真实dom树生成之后,在渲染到页面上。
2:如果数据发生改变,创建新的虚拟dom树,比较两个树的区别,调整虚拟dom的内部状态
3:在虚拟dom收集到足够的改变时,再一次性应用到真实dom上,渲染到页面。
虚拟Dom的好处
1.虚拟dom比真实dom体积小,操作相对来说消耗性能少

  • 虚拟Dom不会进行回流和重绘操作
  • 虚拟dom进行频繁的修改,然后一次性比较并修改真实DOM中需要改的部分,最后并在真实DOM中进行回流和重绘,减少过多DOM节点的回流和重绘
  • 真实Dom频繁的回流和重绘效率非常低

2.虚拟dom可能跨端(在服务器端也可以使用vue技术),跨平台,如果直接操作真实的dom,则与浏览器强制绑定在一起

Vue2 vs Vue3

1.监测机制的改变

  • Vue2:对于对象,Vue2通过Object.defineProperty()对对象的每个属性的读取、修改进行拦截(数据劫持)。对于数组,Vue2通过重写更新数组的一系列方法(push,shift,pop,splice,unshift,sort,reverse)来实现拦截。但对象的新增属性、删除属性、直接通过下标修改数组,Vue2都是拦截不到的。
  • Vue3:利用Proxy对整个对象进行代理,从而实现数据劫持,并且可以使用ref或者reactive将数据转化为响应式数据。使用Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为 Proxy 是 ES6 的语法。

2.组合代码的方式不同

  • Vue2采用选项式API:**需要在特定的区域(data,methods,watch,computed...)编写对应的代码。随着业务复杂度越来越高,代码量会不断的加大;由于相关业务的代码需要遵循option的配置写到特定的区域,导致后续维护非常的复杂,代码可复用性也不高。
  • Vue3采用组合式API:我们可以更加优雅的组织我们的代码、函数。让相关功能的代码更加有序的组织在一起。搭配setup语法糖,组件只需引入不用注册,属性和方法也不用return,也不用写setup函数,也不用写export default ,甚至是自定义指令也可以在我们的template中自动获得。

3.生命周期的命名不同

4.diff算法的不同:Vue3对diff算法中的updateChildren方法进行了改进,Vue2采用的是双端对比,Vue3采用的是最长递增子序列配合相同的前置与后置元素的预处理。

在哪个生命周期内调用异步请求?

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

  • 能更快获取到服务端数据,减少页面 loading 时间;
  • ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;
父子组件生命周期钩子函数执行顺序?

Vue 的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分: 加载渲染过程
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
子组件更新过程
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
父组件更新过程
父 beforeUpdate -> 父 updated
销毁过程
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

vue-router路由

路由的理解

在前端技术早期,一个 url 对应一个页面,如果要从 A 页面切换到 B 页面,那么必然伴随着页面的刷新。后来,改变发生了——Ajax 出现了,它允许人们在不刷新页面的情况下发起请求;与之共生的,还有“不刷新页面即可更新页面内容”这种需求。在这样的背景下,出现了 SPA(单页面应用)。 (1)用户请求页面,从静态资源服务器中获取全部的html+css+js
(2)根据地址从js代码中抽取相应页面的js代码,并执行
(3)同上:当执行到js中的数据请求时,对API接口的服务器请求数据
vue中的前端路由就是一个url对应一个组件,通过检测地址栏的变化,切换路由组件
url1:一个组件
url2:另一个组件
url3:另另一个组件

vue-router原理

原理:通过检测地址栏的变化,切换路由组件

vue-router使用

1、下载vue-router
2、vue-router封装

import Router from 'vue-router'
Vue.use(Router)
const router = new Router({
    // 配置路由,即路径和组件间的对应关系
    routes: [
        // 配置默认路由
        {
            path: '',
            name: '/home'
        },
        {
            path: '/home',
            component: Home,
            //路由的嵌套使用
            children: [
                {
                    //注意,这里不需要加/
                    path: 'news',
                    component: Tab
                }
            ]
        }
    ],
    // 修改模式为history
    mode: 'history'
})
export default router

3、在main.js中注册

router-link

router-link会默认被渲染成a标签
<router-link to='/home'>首页</router-link>

router-link属性
  1. to:跳转的页面
  2. tag:tag = 'button':不是渲染成a标签,而是button标签
  3. replace: 不会留下历史记录,也就是不能使用history.back()
  4. active-class='active',将默认的类名修改为active,然后可以添加一些样式
router-view

组件内容渲染作用
router-view在router-link上面,那么内容就在router-link上面显示
router-view在router-link下面,那么内容就在router-link下面显示

路由跳转

1、声明式导航
<router-link to=''>``</router-link>

2、编程式导航
this.$router.push()

vue-router参数传递

1、方式一:params参数
需要占位符,属于路径的一部分

1、在路由中配置占位
{
    path: '/user/:abc',
    component: user
}
2、传入参数
<router-link :to="/path/"+userId></router-link>
3、在组件中使用params参数
data() {
    return {
        return this.$route.params.abc
    }
}

2、方式二:query参数
不需要占位符,跟在?后面,不属于路径

1、传入query参数
<router-link :to="{path: '/home',query:{name:'kobe',age:18}}"></router-link>
设置query后,点击之后链接会加上query的内容
https://...?name=kobe&age=18
2、在组件中获取参数
this.$route.query.name

区别:

  • 用法不同:query要用path来引入,params要用name来引入,接收参数都是类似的,分别是 this.route.query.name 和 this.route.query.name 和 this.route.params.name 。
  • url地址显示不同:query参数会显示在地址栏上,params参数不会显示在地址栏。
  • 参数丢失不同:刷新页面会导致 params参数的丢失,但是不会导致query参数的丢失。
router和route动态路由

1、this.$router:用于路由跳转

this.$router就是router下的index.js文件中的const router,是vue-router的实例对象, 整个应用只有一个$router,通过组件的$router属性可以进行路由的跳转,获取钩子函数等,在整个vue实例中应该都可以访问到$router
this.$router.push('/home')
this.$router.replace('/home')

2、$route:动态路由,一般用于获取参数

  • 哪个路由处于活跃状态,$route就指哪个路由,每个组件都有自己的$route属性,里面存储着自己的路由信息,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数

this.$route.path
this.$route.params
this.$route.query

两种路由模式hash和history

vue-router 有两种模式: hash 和 history 。

  1. hash:把前端路由的路径用井号 # 拼接在真实 url 后面。当井号 # 后面的路径发生变化时,浏览器并不会重新发起请求,而是会触发 onhashchange 事件,来监听 hash 的改变,借此实现无刷新跳转的功能。
  • hash 永远不会提交到 server 端。
  • hash 可以改变 url ,但是不会触发页面重新加载(hash的改变是记录在 window.history 中),即不会刷新页面。也就是说,所有页面的跳转都是在客户端进行操作。因此,这并不算是一次 http 请求。
  • hash 通过 window.onhashchange 的方式,来监听 hash 的改变,借此实现无刷新跳转的功能。
  1. history:history是使用了 H5 提供的pushState() 和 replaceState()的方法向当前路由中添加路径,通过back()可以回退到上一路径,这样前端人员可以直接更改前端路由,并且更新浏览器 URL 地址而不重新发起请求(将url替换并且不刷新页面)
  • 使用 history 模式时,在对当前的页面进行刷新时,此时浏览器会重新发起请求。如果 nginx 没有匹配得到当前的 url ,就会出现 404 的页面。对于 hash 模式来说, 它虽然看着是改变了 url ,但不会被包括在 http 请求中,并不影响服务器端。因此,改变 hash 并没有真正地改变 url,所以页面路径还是之前的路径, nginx 也就不会拦截。
  • 通过 pushState 、 replaceState 来实现无刷新跳转的功能。
methods: {
    homeclick() {
        this.$router.push('/test')
        this.$router.replace('/test')
    }
}
路由懒加载

解决的问题:避免进入首页就加载全部的前端资源造成用户等待时间过长的问题
原理:使用ES6新特性——import函数进行动态导入,替代之前的静态导入,然后通过Webpack编译打包后,会把每个路由组件的代码分割成一个个单独的js文件,初始化时不会加载这些js文件,只当激活路由组件才会去加载对应的js文件。

const Home = () => import('../views/home/Home.vue')
const Detail = () => import('../views/detail/Detail.vue')
const routes = [
    {
        path: '',
        redirect: '/home'
    },
    {
        path: '/home',
        component: Home,
    },
    {
        path: '/detail/:iid',
        component: Detail
    }
]
路由导航守卫

作用:对路由进行权限控制,分为全局导航守卫、路由独享守卫和组件内的守卫。

全局守卫
  1. router.beforeEach(全局前置守卫:当一个路由跳转到另一个路由时,调用这个函数) 作用:对用户要跳转的路由做一次检查,符合条件后放行,不符合条件则强制用户跳转登录页面
router.beforeEach((to, from, next) => {
    document.title = to.matched[0].meta.title
    // next()是必须执行的函数
    next()
})

2. router.afterEach(全局后置守卫:当路由跳转时,会先调用beforeEach,再调用afterEach)
作用:跳转之后滚动条回到顶部、修改网页title

router.afterEach((to,from)=>{

})
路由守卫

beforeEnter(如果不想全局配置守卫的话,可以为某些路由单独配置守卫)

组件守卫
  1. beforeRouteEnter(进入组件前触发)
  2. beforeRouteUpdate(当前地址改变并且该组件被复用时触发,举例来说,带有动态参数的路径foo/∶id,在 /foo/1 和 /foo/2 之间跳转的时候,由于会渲染同样的foo组件,这个钩子在这种情况下就会被调用)
  3. beforeRouteLeave(离开组件前触发)
//进入守卫:通过路由规则,进入该组件时被调用
beforeRouteEnter(to, from, next) {
},
//离开守卫:通过路由规则,离开该组件时被调用
beforeRouteLeave(to, from, next) {
}
应用场景
  1. 全局前置守卫应用:当用户登陆时,可以去任意页面;否则控制用户不能去的页面,比如登陆过不能再去登陆页面
// 全局前置守卫(初始化时被调用,每次路由切换之前调用)
router.beforeEach(async (to, from, next) => {
    // to:即将要进入的目标路由  from:当前导航正要离开的路由 next放行函数
    let token = store.state.user.token;
    // 用户信息
    let name = store.state.user.userInfo.name;
    // 当token存在,说明用户登陆过
    if (token) {
        // 当地址栏输入/login 用户登陆了,就不能再去登录页面,只能去首页
        if (to.path == '/login') {
            next('/home')
        } else {
            // 不是去/login,放行
            if (name) {
                // 如果用户信息存在,则放行
                next();
            } else {
                // 用户信息不存在,派发action获取用户信息后再放行
                try {
                    await store.dispatch("getUserInfo")
                    next();
                } catch (error) {
                    // 如果不能获取用户信息,说明token失效,此时需要清除token,重新登录
                    await store.dispatch("userLogout")
                    next('/login');
                }
            }
        }
    } else {
        // 未登录则放行,此处还有其他逻辑 后期再处理
        next();
    }
})

2.路由独享守卫应用:如果不是由指定的路由跳转过来,则不放行,比如如果交易路由不是从购物车路由跳转,则不放行

{
    name: "trade",
    path: "/trade",
    component: Trade,
    // 路由元信息
    meta: {
        show: true
    },
    // 路由独享守卫,
    beforeEnter: (to, from, next) => {
        if (from.path == '/shopcart') {
            // 如果从shopcart路由跳到当前路由trade,则放行
            next();
        } else {
            // 不是从shopcart路由跳到当前路由trade,不放行,还是停留到原来的路由
            next(false);
        }
    }
}
vue-router面试题

1、路由传参的时候(对象写法),path是否可以结合params参数一起使用
不能
path只能和query一起使用,但是name可以和query或者params一起使用

2、如何指定params参数可传可不传
占位符后增加?

3、params参数可传可不传,但是如果传递的是空串,如何解决
使用undefined解决

this.$router.push({
    name: 'search',
    params: {
        key: '' || undefined
    }
})

4、路由组件能不能传递props参数
可以

var routes = [
    ...
    {
        name: 'search',
        // ?表示key参数可传可不传,如果不加?,那么必须要通过params方式传递key参数
        path: '/search/:keyword?',
        component: () => import('../pages/Search'),
        meta: {
            show: true
        },
        // 布尔值写法
        // props: true
        // 对象写法
        // props: {a:1, b:2}
        // 函数写法
        // props: ($route) => {
        //     return {
        //         k1: $route.params.key,
        //         k2: $route.query.keyword
        //     }
        // }
    }
}

Vuex集中状态管理

vuex作用及管理图例

作用:用于集中管理项目中的状态
应用场景:多个组件共享数据或者是跨组件传递数据时
管理图例: image.png state中用于定义状态
mutations用于同步修改state状态
actions用于异步修改state状态,在actions中仍需要commit到mutations,使用mutations修改状态
getters用在需要对vuex中的数据进行处理时,类似计算属性computed
devtools:Vue开发的一个浏览器插件,用于记录每一次修改的记录,这样方便查看是谁修改了state 修改状态只能通过mutations!!!

vuex封装

1、建立模块化文件夹 image.png
2、每个模块建立状态管理

import {reqGetCode} from '@/api'

const state = {
    code: ''
}
// mutations可以修改state,并且带有默认参数
const mutations = {
    GETCODE(state, code) {
        state.code = code
    }
}
// actions异步修改state,形参是store对象,需要解构,如getCode({commit, dispatch, getters})
const actions = {
    async getCode({commit}, phone) {
        const res = await reqGetCode(phone)
        if (res.code == 200) {
            commit('GETCODE', res.data)
        }
    }
}
const getters = {
}
export default {
    state,
    mutations,
    actions,
    getters
}

3、主文件中,导入各模块下的状态

import Vue from 'vue'
import Vuex from 'vuex'
import home from './home'
import search from './search'
import detail from './Detail'
import shopCart from './ShopCart'
import user from './User'

Vue.use(Vuex)

export default new Vuex.Store( {
    modules: {
        home,
        search,
        detail,
        shopCart,
        user
    }
})

4、入口文件main.js注册使用

import Vue from 'vue'
// 引入vuex仓库
import store from './store'

new Vue({
  render: h => h(App),
  // 注册vuex仓库
  store
}).$mount('#app')

组件中vuex的使用
读数据

1、直接读取state <span>{{ this.$store.home.bannerList }}</span>

2、使用mapState读取state

import { mapState } from 'vuex'
export default {
  name: 'ListContainer',
  computed: {
    ...mapState({
      bannerList: (state) => state.home.bannerList
    })
  },
  mounted() {
    this.$store.dispatch('bannerList')
  },
}
mutations修改数据

this.$store.commit('方法名')

actions修改数据

this.$store.dispatch('方法名')

vuex面试题
vuex是不是永久性存储

不是,刷新页面后,vuex存储的数据会丢失
解决:为了防止丢失,可以使用本地存储的方式实现数据持久化
实现持久化的方式:
1、利用h5的本地存储(localStorage和sessionStorage)
2、利用第三方封装好的插件,例如vuex-persistedstate

axios

axios封装
export function request(config) {
    // 1、创建axios实例
    const instance = axios.create({
        baseURL: "http://123.207.32.32:8000",
        timeout: 2000
    })

    // 2、axios拦截器
    // 2.1请求成功和请求失败:在向服务器请求的过程中拦截,做些处理,然后继续向服务器请求
    // instance.interceptors.request.use(config => {
    //     console.log(config)
    //     // 请求拦截的作用:
    //     // 1、比如config中的一些信息不符合服务器的要求
    //     // 2、比如每次在发送网络请求时,希望在界面中显示一个请求的图标
    //     // 3、某些网络请求(比如token),必须携带一些特殊的信息

    //     // 拦截之后一定要把拦截的信息返回
    //     return config
    // },err => {
    //     console.log(err)
    // })

    // 2.2响应成功和响应失败:在服务器返回信息的过程中拦截
    instance.interceptors.response.use(res => {
        console.log(res)

        // 也是必须返回结果
        return res.data
    }, err => {
        console.log(err)
    })

    // 3、发送真正的网络请求
    return instance(config)
}

vue框架原理

双向数据绑定原理

Vue 双向数据绑定主要是指:数据变化更新视图,视图变化更新数据。

Vue 采用数据劫持 + 发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter和getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:

  • 实现一个监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。
  • 实现一个解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。
  • 实现一个订阅者 Watcher:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。
  • 实现一个订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。

image.png

vue响应式原理

响应式:数据变化,视图跟着变化

vue2

对象类型:数据劫持 + 观察者模式。

数据劫持的话,利用了Object.defineProperty()本身就能够拦截属性的读取和修改的特性,进一步封装了defineReactive()方法,当有人试图修改对象的属性时,会调用defineProperty中的set()函数,收到修改的具体值,当有人读取对象的某个属性时,会调用defineProperty中的get()函数,并且设置属性值,通过数据劫持,能够实现外部对该属性的读和写操作时能够被内部监听;观察者模式,就是说通过Observer类实现对对象中的每个属性进行defineReactive监听,但是会存在一个问题,就是当对象中的属性值也是对象时,没办法进行深度监听,所以在执行 Object.defineProperty 前先进行递归调用 observe,如果该属性为对象,则 observe 会递归调用 defineReactive,不是则observe直接返回,继续执行Object.defineProperty

核心机理:defineProperty()是js机制带有的方法

主要包括:index.js、observe.js、observer.js和defineReactive.js,在index.js中通过observe()对一个对象进行观察,在observe.js中给对象或者对象的属性添加observer()模式,在observer.js中循环对象的属性,并调用defineReactive()函数,监听对象的属性变化,在defineReactive.js中给对象的每个属性添加observe模式,便于深度监听

视图变化主要通过Dep和Watcher,Dep使用的是发布订阅模式,专门用来管理依赖,每个observer的实例成员中都有一个dep的实例。Dep中使用subs数组存储所有的所有的依赖,那么当有人获取数据时,在defineReactive中的get()函数中通过depend()函数添加依赖,也就是向subs数据中添加Watcher,当有人修改数据时,在defineReactive中的set()函数中通过notify()函数通知所有的依赖更新数据,更新数据通过调用Watcher中的update()函数,将oldValue设置为新的value,

Watcher就是指依赖,

watcher是一个中介,数据发生变化时通过watcher中转,通知组件

总结:

1、observer主要是为了让数据变为响应式,即数据变化时,浏览器的控制台可以检测到变化

2、Dep用于管理依赖,当数据变化时,会循环遍历列表,把所有的Watcher通知一遍

3、Watcher就是依赖,当它收到通知时,会通知组件更新,进行后面的render什么的

数组类型:通过重写更新数组的一系列方法,包括push,shift,pop,splice,unshift,sort,reverse来实现拦截(也只改写了这些方法)。

以Array.prototype为原型,创建了一个叫做arrayMethods对象,然后通过Object.setPrototypeOf()来强制让数组通过__proto__访问arrayMethods上的方法,通过改写arrayMethods上有关数组的方法,侦听数组类型的改变,如果是push,unshift,splice方法,那么会继续监听插入的元素

  • 在Vue修改数组中的某个元素一定要用如下方法:push()、pop()、shift()、unshift()、splice()、sort()、reverse() 或者Vue.set() 或 vm.set()Vue.set(object,propertyName,value)/vm.set()。Vue.set (object, propertyName, value) / vm.set (object, propertyName, value)
  • 新增属性、删除属性, 界面不会更新。
  • 直接通过下标修改数组, 界面不会自动更新。

vue3

  • 通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。
  • 通过Reflect(反射): 对源对象的属性进行操作。

image.png

Object.defineProperty(person, 'age', {
    //当有人读取person的age属性时,get函数(getter)就会被调用,且返回值就是age的值
    get() {
        console.log('有人读取age属性了')
        return number
    },
    //当有人修改person的age属性时,set函数(setter)就会被调用,且会收到修改的具体值
    set(value) {
        console.log('有人修改了age属性,且值是', value)
        number = value
    }
})
Object.defineProperty() 数据劫持的缺点

在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作。更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。

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

defineProperty和proxy

defineProperty
Vue 在实例初始化时遍历 data 中的所有属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。这样当追踪数据发生变化时,setter 会被自动调用。存在以下问题

  • 添加或删除对象的属性时,Vue 检测不到。因为添加或删除的对象没有在初始化进行响应式处理,只能通过Vue.set() 或 vm.$set() 来调用Object.defineProperty()处理。
  • 无法监控到数组下标和长度的变化。

proxy
Vue3 使用 Proxy 来监控数据的变化。Proxy 是 ES6 中提供的功能,其作用为:用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。相对于Object.defineProperty(),其有以下特点:

  • Proxy 直接代理整个对象而非对象属性,这样只需做一层代理就可以监听同级结构下的所有属性变化,包括新增属性和删除属性。
  • Proxy 可以监听数组的变化。
diff算法

Diff算法是一种精细对比算法,对比两者是【旧虚拟DOM】和【新虚拟DOM】,实现最小量更新,比较规则如下:

  • 旧虚拟DOM中找到了与新虚拟DOM相同的key:
    • 若新的虚拟DOM中内容没变, 直接使用之前的真实DOM
    • 若新的虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM。
  • 旧虚拟DOM中未找到与新虚拟DOM相同的key:
    • 创建新的真实DOM,随后渲染到到页面。

因此,Diff算法需要找出是哪个【虚拟DOM】更改了,找出这个【虚拟DOM】,并只更新这个【虚拟DOM】所对应的【真实DOM】,而不用更新其他数据没发生改变的节点,实现精准地更新【真实DOM】,进而提高效率。

diff算法对比流程

当数据改变时,会触发setter,并且通过Dep.notify去通知所有订阅者Watcher,订阅者们就会调用patch方法,给真实DOM打补丁,更新相应的视图

1、patch方法

  • 使用sameVnode方法对比当前同层的虚拟节点是否为同一种类型的标签。
    • 是:只有是同一个虚拟节点,才执行patchVnode方法进行深层比对,如何定义是同一个虚拟节点,即选择器相同且key相同,
    • 否:没必要比对了,会暴力删除旧的,插入新的,比如会将ul删除,添加ol。
  • 需要注意的是,patch方法只进行同层比较,不会进行跨层比较
    • 即使是同一片虚拟节点,但是跨层,也会暴力删除旧的,然后插入新的

2、sameVnode方法

patch关键的一步就是sameVnode方法判断是否为同一类型节点:

3、patchVnode方法

首先会判断newVnode和oldVnode是否指向同一个对象,如果是,那么不用修改,直接return

  • 判断newVnode有没有text属性
    • 有,则判断newVnode的text和oldVnode的text是否相同
      • 不同,则将el的文本节点设置为newVnode的文本节点
      • 相同,则直接return
    • 没有,则说明newVnode有children属性,则需要判断oldVnode有没有children
      • 没有,说明oldVnode有text,则需要清除oldVnode中的text,并且把newVnode中的children添加到DOM中
      • 有,则执行updateChildren函数比较newVnode的children和oldVnode的children

image.png 4、updateChildren方法

此方法就是算法的核心部分,当发现新旧虚拟节点的的子节点都存在时候,我们就需要通过一些方法来判断哪些节点是需要移动的,哪些节点是可以直接复用的,来提高我们整个diff的效率;

优化策略:设置了四个指针,按顺序对比新前与旧前、新后与旧后、新后与旧前、新前与旧后指针,

则会出现以下几种情况:

1、如果是旧节点先循环完毕,说明新节点中剩余的节点是要插入的节点 image.png 2、如果是新节点先循环完毕,说明旧节点中剩余的节点(旧前和旧后中间的节点)是要被删除的 image.png 3、当③命中时,要将旧前指向的节点,移动到旧后之后: image.png 4、当④命中时,要将旧后指向的节点移动到旧前的前面: image.png 如果这四种情况都没有命中,需要在旧节点中循环寻找新前指向的节点,这里它采用了keyMap的用法,将旧节点中的key和下标存储为map,这样当判断新节点中的某个key是否存在的时候,直接用keyMap查位置序号,如果不是undefined,说明不是全新的项,则需要移动,将此节点移动到旧前的前面;如果是undefined,说明是全新的项,则创建节点后,插入到旧前的前面 image.png h函数: 得到虚拟节点
h('a',{props:{href:'http://www.baidu.com'}},'尚硅谷')
对应虚拟节点
{"sel":"a","data":{props:{href:'http://www.baidu.com'}},"text":"尚硅谷"} 虚拟节点(页面不可见,控制台可见): image.png

vue订阅者发布者模式

<div>
    {{ message }}  //李四在用
    {{ name }}  //王五在用
</div>
var obj = {
    message: "哈哈哈",
    name: 'wenjie'
}

Object.keys(obj).foreach(key => {
    let value = obj[key]

    观察者模式,通过Object.defineProperty()实现
    Object.defineProperty(obj, key, {
        set(newvalue) {
            //监听值的改变
            //解析html代码,获取到谁在用
            value = newvalue
        },
        get() {
            //获取对应的值
            return value
        }
    })
})


订阅者模式,将观察者全部添加到订阅者
class Dep {
    constructor() {
        this.subs = []
    }
    addSub(watcher) {
        this.subs.push(watcher)
    }
    notify() {
        this.item.foreach(item => {
            item.update()
        })
    }
}
class Watcher {
    constructor(name) {
        this.name = name
    }
    update() {
        update页面内容
        console.log(this.name + "发生了update")
    }
}

class dep = new Dep()
const watcher1 = new Watcher('张三')
dep.addSub(watcher1)
const watcher2 = new Watcher('李四')
dep.addSub(watcher2)
const watcher3 = new Watcher('王五')
dep.addSub(watcher3)

image.png

参考文章:
1、juejin.cn/post/696122… vue原理文章:
juejin.cn/post/693534…