Vue基础知识和原理

352 阅读10分钟

思考🤔

  • v-showv-if的区别
    • v-show: 节点一直存在,只是通过css display 来显示或隐藏,不会重新渲染和更新
    • v-if: 节点会根据值来动态创建和销毁
    • 使用场景
      • 组件频繁切换显示状态时,用v-show,可以降低渲染消耗组件创建以后;
      • 不需要频繁切换显示状态的用v-if
  • 为何v-for中要用key
  • Vue组件的生命周期调用顺序(有父子组件的情况)
  • Vue组件如何通讯
    • 父子组件,使用属性和触发事件
    • 组件之间无关或者层级较深,使用自定义事件
    • 使用Vuex通讯
  • 描述组件渲染和更新的过程
  • 描述数据绑定v-mode的实现原理

Vue 使用

  • 基本使用 组件使用 ---- 常用 必须会
  • 高级特性 ---- 不常用 可以体现深度
  • Vuex 和 Vue-router 常用

Vue3.0出来后 Vue 和 React 越来越接近

  • Vue3 Options API对应React Class Component
  • Vue3 Composition API 对应 React Hooks

Vue基本使用

  • 插值、表达式
  • 指令、动态属性
  • v-html: 会有 XSS 风险,会覆盖子组件
<template>
  <div>
    <p>插入值: {{ message }}</p>
    <p>JS 表达式 {{ flag ? "yes" : "no" }} (只能是表达式,不能是 js 语句)</p>
    <p>一条语句执行一个动作,一个表达式产生一个值</p>

	<p :id="dynamicId">动态属性 id</p>

    <p v-html="rawHtml"></p>

    <!-- <p v-html="rawHtml"> -->
    <!-- <span>有 xss 风险</span> -->
    <!-- <span>「注意」使用 v-html 之后 将会覆盖子元素</span> -->
    <!-- </p> -->
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: "hello vue",
      flag: true,
      rawHtml: "指令 - html <b>加粗</b>",
      dynamicId: `id-${Date.now()}`,
    };
  },
};
</script>
  • computed 有缓存, data 不变不会重新计算
<template>
  <p>num {{ num }}</p>
  <p>aDouble {{ aDouble }}</p>
  <input v-model="aPlus" />
</template>

<script>
export default {
  data() {
    return { num: 20 };
  },
  // computed有缓存,缓存可以提高性能
  computed: {
    // 仅读取
    aDouble() {
      return this.num * 2;
    },
    // 读取和设置
    aPlus: {
      get: function () {
        return this.num * 1;
      },
      set: function (v) {
        this.num = v / 2;
      },
    },
  },
};
</script>

watch 监听

  • watch 如何深度监听
  • watch 监听引用类型,拿不到oldVal,指向同一个指针
<template>
  <div>
    <input v-model="name" />
    <input v-model="info.city" />
  </div>
</template>

<script>
export default {
  data() {
    return {
      name: "aki",
      info: {
        city: "珠海",
      },
    };
  },
  watch: {
    name(oldValue, val) {
      console.log("watch name: ", oldValue, val); // 值类型,可以正常拿到 oldValue val
    },
    info: {
      handler(oldValue, val) {
        console.log("watch info: ", oldValue, val); // 引用类型,拿不到 oldVal 。因为指针相同,此时已经指向了新的 val
      },
      deep: true,// 开启深度监听可以 就可以拿到值
    },
  },
};
</script>

image.png

class和style

  • 使用动态属性
  • 使用驼峰式写法
<template>
    <div>
        <p :class="{ black: isBlack, yellow: isYellow }">使用 class</p>
        
        <p :class="[black, yellow]">使用 class (数组)</p>
        
        <p :style="styleData">使用 style</p>
    </div>
</template>

<script>
export default {
    data() {
        return {
            isBlack: true,
            isYellow: false,

            black: 'black',
            yellow: 'yellow',

            styleData: {
                fontSize: '40px', // 转换为驼峰式
                color: 'red',
                backgroundColor: '#ccc' // 转换为驼峰式
            }
        }
    }
}
</script>

<style scoped>
    .black {
        background-color: #999;
    }
    .yellow {
        color: yellow;
    }
</style>

v-if & v-show

  • v-if v-else-if v-else 用法 可使用变量,也可以使用 === 表达式
  • v-if 和 v-show 的区别
    • v-if 不会渲染
    • v-show 会渲染 但是会用display: none;来隐藏
    image.png
  • v-if 和 v-show 的使用场景
    • 从性能考虑,频繁销毁就是用v-show,一次性的话就是用v-if
<template>
  <div>
    <p v-if="type === 'a'">A</p>
    <p v-else-if="type === 'b'">B</p>
    <p v-else>other</p>

	<p v-show="type === 'a'">v-show A</p>
	<p v-show="type === 'b'">v-show B</p>

	
  </div>
</template>
<script>
export default {
  data() {
    return { type: "b" };
  },
};
</script>

event

  • 基本使用 传参
  • 观察事件绑定在哪里 与react对比
<template>
  <p>{{ num }}</p>
  <button @click="increment1">+1</button>
  <button @click="increment2(2, $event)">+2</button>
</template>

<script>
export default {
  data() {
    return {
      num: 1,
    };
  },
  methods: {
    increment1(event) {
      console.log("event :>> ", event);
      console.log("event.currentTarget :>> ", event.currentTarget); // 注意,事件是被注册到当前元素的,和 React 不一样
      console.log("event.__proto__ :>> ", event.__proto__.constructor);
      this.num++;

      // 与react对比
      // 1. event 是原生的
      // 2. 事件被挂载到当前元素
      // 和 DOM 事件一样
    },
	increment2(val, event) {
		this.num += val;
		console.log('event :>> ', event.target);
	}
  },
};
</script>

image.png

  • 事件修饰符

image.png

  • 按键修饰符

image.png

表单 Form

  • v-model
  • 常见表单项 textarea select checkbox radio
  • 修饰符 lazy number trim
<template>
  <div>
    <p>输入框: {{ name }}</p>
    <input type="text" v-model.trim="name" />
    <input type="text" v-model.lazy="name" />
    <input type="text" v-model.number="age" />

    <p>多行文本: {{ desc }}</p>
    <textarea v-model="desc"></textarea>
    <!-- 注意,<textarea>{{desc}}</textarea> 是不允许的!!! -->

    <p>复选框 {{ checked }}</p>
    <input type="checkbox" v-model="checked" />

    <p>多个复选框 {{ checkedNames }}</p>
    <input type="checkbox" id="jack" value="Jack" v-model="checkedNames" />
    <label for="jack">Jack</label>
    <input type="checkbox" id="john" value="John" v-model="checkedNames" />
    <label for="john">John</label>
    <input type="checkbox" id="mike" value="Mike" v-model="checkedNames" />
    <label for="mike">Mike</label>

    <p>单选 {{ gender }}</p>
    <input type="radio" id="male" value="male" v-model="gender" />
    <label for="male"></label>
    <input type="radio" id="female" value="female" v-model="gender" />
    <label for="female"></label>

    <p>下拉列表选择 {{ selected }}</p>
    <select v-model="selected">
      <option disabled value="">请选择</option>
      <option>A</option>
      <option>B</option>
      <option>C</option>
    </select>

    <p>下拉列表选择(多选) {{ selectedList }}</p>
    <select v-model="selectedList" multiple>
      <option disabled value="">请选择</option>
      <option>A</option>
      <option>B</option>
      <option>C</option>
    </select>

    <br />
    <button @click="consoleData">consoledata</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      name: "aki",
      age: 10,
      desc: "自我介绍",

      checked: true,
      checkedNames: [],

      gender: "male",

      selected: "",
      selectedList: [],
    };
  },
  methods: {
    consoleData() {
      console.log(this.$data);
    },
  },
};
</script>

Vue父子组件如何通讯

  • Vue组件使用

  • 父子组件通讯

    • props 父组件 -> 子组件
    • $emit 子组件 触发 父组件 事件
  • 组件之间如何通讯

    通过自定义事件

    event.js

    import Vue from 'vue'
    
    export default new Vue()
    
    

    组件A

    import event from "./event";
    ...
    mounted() {
      event.$on("onAddTitle", this.onAddTitle);
    },
    beforeUnmount() {
      event.$off("onAddTitle");
    },
    

    组件B

    import event from "./event";
    ...
    event.$emit("onAddTitle", this.title);
    

如何用自定义事件进行vue组件通讯

  • new Vue实例进行通信
  • event.$emit() 触发
  • event.$on() 监听
  • event.$off() 解绑(防止内存泄露) 事件绑定时定义了名字的函数进行绑定,方便后续解绑 销毁,防止内存泄露
export default {
    mounted() {
        event.$on('onAdd', this.add)
    },
    beforeDestroy() {
        event.$off('onAdd')
    }
}

生命周期调用顺序

生命周期图示

vue2.x

  • beforeCreate created
  • beforeMount mounted
  • beforeUpdate updated
  • activated deactivated
  • beforeDestroy destroyed
  • errorCaptured

image.png

vue3.x image.png

  • beforeDestroy可能要做什么?
  • created和mounted有什么区别?

生命周期(单个组件)

  • 挂载阶段
    • beforeCreate created
    • beforeMount mounted
  • 更新阶段
    • beforeUpdate updated
  • 销毁阶段
    • beforeDestroy destroyed

生命周期(父子组件)

调用顺序

  • 创建(从外到内)、渲染(从内到外)
    • beforeCreate(父)
    • created(父)
    • beforeMount(父)
    • beforeCreate(子)
    • created(子)
    • beforeMount(子)
    • mounted(子)
    • mounte(父)
  • 更新
    • 父 beforeUpdate
    • 子 beforeUpdate
    • 子 updated
    • 父 updated
  • 销毁
    • 父 beforeDestroy
    • 子 beforeDestroy
    • 子 destroyed
    • 父 destroyed

高级特性

自定义v-model

image.png

$nextTick

Vue.nextTick( [callback, context] )

Vue是异步渲染,data改变后,DOM不会立刻渲染,$nextTick会在DOM渲染之后被触发,以获取最新DOM节点

// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
  // DOM 更新了
})

// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
Vue.nextTick()
  .then(function () {
    // DOM 更新了
  })

refs

slot

  • 是什么:作用域插槽、具名插槽
  • 作用:让父组件可以往子组件中插入一段内容(不一定是字符串,可以是其他的组件,只要是符合Vue标准的组件或者标签都可以)
具名插槽

image.png

作用域插槽

让父组件可以访问到子组件的数据

image.png

动态、异步组件

  • 动态组件
    • 例子:新闻页面,图文视频组件的不同排列组合 image.png
    • : is="component-name"用法
    • 需要根据数据,动态渲染的场景。即组件类型不确定。 image.png
  • 异步组件(常用)
    • import 函数
      • import CustomComponent from './index': 同步加载,打包时也是打一个包出来
    • 按需加载,异步加载大组件
    image.png

Vue如何缓存组件:keep-alive

  • keep-alive作用
    • 缓存组件
    • 频繁切换 不需要重复渲染
    • 性能优化
  • 使用场景:常见的tab切换使用keep-alive
  • 用法
    <keep-alive> <!-- tab 切换 -->
            <KeepAliveStageA v-if="state === 'A'"/> <!-- v-show -->
            <KeepAliveStageB v-if="state === 'B'"/>
            <KeepAliveStageC v-if="state === 'C'"/>
    </keep-alive>
  • keep-alive 和 v-show的区别
    • v-show是通过css样式属性display控制 显示/隐藏 DOM
    • keep-alive是vue框架层级进行的js对象的渲染(一个组件就是一个js对象)
    • 使用原则
      • 带有层级的复杂组件,用keep-alive去包裹起来进行缓存,切换组件直接从缓存读取,大大提高性能。因为这样可以避免组件的频繁渲染销毁
      • 简单组件 用v-show

Vue组件如何抽离公共逻辑?(mixin) Vue3中的Component API是什么?

多个组件有相同逻辑

  • 实现

image.png

  • mixin问题
    • 变量来源不明确,不利于阅读(mixin中的变量或方法再当前组件是查不到的)
    • 多mixin可能会造成命名冲突
    • mixin和组件可能会出现多对多的关系,复杂度较高(一个组件引入多个mixin,多个组件引用一个mixin)
  • Vue3 Component API解决逻辑复用方案

Vuex 状态管理模式

  • 基本概念
    • State
    • Getters
    • Actions
    • Mutations
    • Modules
  • 用于Vue组件
    • Dispatch
    • Commit
    • mapState
    • mapGetters
    • mapActions
    • mapMutations
  • 数据流(重点) Actions里才能做异步操作,常用于Ajax请求

Mutations是同步操作,力求原子最小化

进行异步操作(后端API接口数据)必须在Actions中进行,同时Actions整合多个Mutations的commit操作,Mutations是原子操作

image.png

vue-router

路由模式(hash、H5 history)

配置

const router = new Vue({
    mode: 'history',
    routes: [...]
})

路由配置(动态路由、懒加载)

  • 动态路由
const User = {
    // 获取参数 如: 10 20
    template: <div>User {{ $route.params.id}} </div>
}

const router = new VueRouter({
    routes: [
        // 动态路径参数 以冒号开头,能命中 `/user/10` `/user/20` 等格式的路由
        { path: '/user/:id', component: User }
    ]
})
  • 懒加载
export default new VueRouter({
    routes: [
        {
            path: '/',
            component: () => import('../components/A')
        },
        {
            path: '/B',
            component: () => import('../components/B')
        },
        
    ]
})

Vue 原理

组件化?

如何理解MVVM

过去是通过操作DOM来实现网页的更新显示,现在是通过监听数据变化,驱动修改DOM

React和Vue都是这个模型

  • M:Model 数据,组件中的data,或者是Vuex里的数据
  • V:View 看到的视图
  • VM: ViewModel 沟通 model 和 view 的桥梁,监听事件,监听指令,
    • View 点击事件 各种DOM事件 ViewModel监听到 触发修改Model
    • Model数据修改后,ViewModel Directives 重新渲染

image.png

监听数据变化的核心API(实现响应式)

Object.defineProperty

  • 通过该API来监听数据,如果有变化,立刻触发视图渲染
  • 缺点
    • 深度监听,递归,一次性计算量大
    • 无法监听新增、删除属性(Vue.set Vue.delete)
    • 无法监听数组,需要特殊处理

基本用法

const object1 = {};
Object.defineProperty(object1, "name", {
	get() {
		return name;
	},
	set(newVal) {
		name = newVal;
		console.log("监听到name修改 set new value: ", name);
		return name;
	}
});

object1.name = "haha";

console.log(object1.name);
console.log(object1);
// https://codepen.io/huangzonggui/pen/ExvMpYO
监听对象(深度监听data变化)
监听数组变化
  1. 通过Object.create(Array.prototype)重新定义数组原型
    • Ojbect.create创建的对象 原型指向Array.prototype,扩展该对象方法,不会污染原数组 image.png
  1. 判断监听的数据类型如果是数组,将监听对象的隐式原型修改为重新定义的数组原型

image.png

Proxy

  • Vue3采用Proxy 来监听数据变化

  • 缺点

    • Proxy浏览器兼容性不好,且不可以polyfill

vdomdiff

vdom(virtual DOM)

  • 操作DOM耗费性能
  • jQuery是通过控制操作DOM的时机,手动调整
  • vue 和 react 是通过数据驱动视图,修改数据,计算出最小改变的DOM,再一次性修改,减少计算

如何计算最小变化

  • 因为JS计算快,所以可以通过将操作DOM的频繁变更转移到计算JS中
  • 用JS模拟DOM,构建出一棵Virtual DOM树,通过js来计算最小变更
  • 最后一次性修改操作DOM

JS 模拟DOM的结构(vnode)

  • tag
  • props (className style id 事件等)
  • childrens(子节点 数组 字符串)

image.png

模拟上图的DOM结构

{
    tag: 'div',
    props: {
        id: 'div1',
        className: ['container']
    },
    children: [
        {
            tag: 'p',
            children: 'vdom'
        },
        {
            tag: 'ul',
            props: {
                style: 'font-size: 20px'
            },
            children: [
                {
                    tag: 'li',
                    children: 'a'
                }
            ]
        }
    ]
}
通过 snabbdom 学习虚拟DOM

A virtual DOM library with focus on simplicity, modularity, powerful features and performance.

  • 简洁强大的vdom
  • Vue 参考它实现vdom和diff
  • snabbdom代码的解析
  • 重点
    • h函数
    • vnode(virtual node 虚拟节点)的数据结构
    • patch函数

snabbdom源码上vnode的数据结构

export interface VNode {
  sel: string | undefined;
  data: VNodeData | undefined;
  children: Array<VNode | string> | undefined;
  elm: Node | undefined;
  text: string | undefined;
  key: Key | undefined;
}

export interface VNodeData {
  props?: Props;
  attrs?: Attrs;
  class?: Classes;
  style?: VNodeStyle;
  dataset?: Dataset;
  on?: On;
  hero?: Hero;
  attachData?: AttachData;
  hook?: Hooks;
  key?: Key;
  ns?: string; // for SVGs
  fn?: () => VNode; // for thunks
  args?: any[]; // for thunks
  [key: string]: any; // for any other 3rd party module
}

diff算法

  • diff 算法是vdom的核心、最关键部分
  • diff 算法能在日常使用Vue React中体现出来(for 循环体中的 key 重要性)
  • diff即对比差异 是一个广泛的概念,如linux diff命令,git diff等

对比两颗树的差异

  • 一般做法
    • 遍历tree1
    • 遍历tree2
    • 排序
      • 时间复杂度O(n^3),1000个节点,计算1亿次,算法不可用
  • 优化时间复杂度为O(n)
    • 只比较同一等级
    • 如果tag不同,则直接删除重建,不做深度比较
    • 如果tagkey都相同,则认为是相同节点,不做深度比较

image.png

image.png

源码

看源码,主要找到核心函数h() patch()等,看它的参数返回值主要逻辑,不需要过于抠细枝末节

  • h函数:helper function for creating vnodes

  • patch函数(调用patchVnode):

    The patch function returned by init takes two arguments. The first is a DOM element or a vnode representing the current view. The second is a vnode representing the new, updated view.

    patch(oldVnode, newVnode);
    

    image.png

    • patch函数逻辑
      • 第一个参数不是 vnode
        • 第一个参数是DOM Element(DOM元素), 创建一个空的 vnode 关联到这个 DOM 元素上
      • 第一个参数是vnode,调用sameNode判断vnode是否相同
            function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
              // key 和 sel 都相等
              // undefined === undefined // true
              return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
            }
        
        • 相同的 vnode(keysel都相等):vnode 对比(patchVnode
        • 不同的 vnode,直接删除重建
  • patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue)函数(调用updateChildren):对比两个节点,如果不同,

    • 节点相同,返回
    • 节点不同(主要看textchildren,二选一,也就是有子节点,或者有字符串)
      • text没有值(一般children有值)
        • 新旧节点都有children,调用 updateChildren
        • 新节点有children,旧节点无children,添加(调用 addVnodes
        • 旧节点有children,新节点无children,删除children (调用removeVnodes
      • text有值(一般children无值)
        • 将新的text替换旧的text
      function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
        // 执行 prepatch hook
        const hook = vnode.data?.hook;
        hook?.prepatch?.(oldVnode, vnode);
    
        // 设置 vnode.elem
        const elm = vnode.elm = oldVnode.elm!;
    
        // 旧 children
        let oldCh = oldVnode.children as VNode[];
        // 新 children
        let ch = vnode.children as VNode[];
    
        if (oldVnode === vnode) return;
    
        // hook 相关
        if (vnode.data !== undefined) {
        // cbs: callbacks
          for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
          vnode.data.hook?.update?.(oldVnode, vnode);
        }
    
        // vnode.text === undefined (vnode.children 一般有值)
        if (isUndef(vnode.text)) {
          // 新旧都有 children
          if (isDef(oldCh) && isDef(ch)) {
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
          // 新 children 有,旧 children 无 (旧 text 有)
          } else if (isDef(ch)) {
            // 清空 text
            if (isDef(oldVnode.text)) api.setTextContent(elm, '');
            // 添加 children
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
          // 旧 child 有,新 child 无
          } else if (isDef(oldCh)) {
            // 移除 children
            removeVnodes(elm, oldCh, 0, oldCh.length - 1);
          // 旧 text 有
          } else if (isDef(oldVnode.text)) {
            api.setTextContent(elm, '');
          }
    
        // else : vnode.text !== undefined (vnode.children 无值)
        } else if (oldVnode.text !== vnode.text) {
          // 移除旧 children
          if (isDef(oldCh)) {
            removeVnodes(elm, oldCh, 0, oldCh.length - 1);
          }
          // 设置新 text 
          api.setTextContent(elm, vnode.text!);
        }
        hook?.postpatch?.(oldVnode, vnode);
      }
    
  • addVnodesremoveVnodes

  • updateChildren函数:四个首尾部指针对比,指针慢慢往中间移动,直到指针相遇,循环结束

    • 当遇到相同的节点,就会移动指针,这样做,循环体中的节点,如果没有变化,不需要重新渲染
    • 当没有遇到相同的节点
      • 判断key是否相同
        • 不同,则是新的节点,插入
        • 相同,则判断sel(内容)是否相等
          • 相等则是旧的节点,直接移动,不用生成新的节点
          • 不相等,生成新的节点 image.png
    • key的重要性
      • 当不传key时,会全部删除重建
      • 当传index作为key时,会有排序的变化,例如,之前是0的元素排到第一位,也会出现问题
      • 传入业务id作为key,直接移动 image.png

    image.png

模板编译

  • with语法
  • 模板编译成render函数
  • 执行render函数生成vnode

渲染过程

  • 初次渲染过程
  • 更新过程
  • 异步渲染

前端路由原理

路由模式

  • H5 history
  • hash

选择

  • toB系统,简单易用,推荐使用hash路由,对url规范不敏感
  • toC系统,考虑选择H5 history,需要服务端支持
  • 能选择简单的就不要选择复杂的,考虑成本和收益

hash

window.onhashchange

当 一个窗口的 hash (URL 中 # 后面的部分)改变时就会触发 hashchange 事件

url#号后面的部分

  • hash变化,会触发路由的跳转,前进后退
  • hash不会触发页面的刷新,这个是single page app(SPA)特点
  • hash跟服务器无关,不会提交到后台

通过hash变化触发路由的跳转,后退,触发视图的渲染

修改hash方式

  • 通过JS location.href=#user 来修改hash值
  • 通过手动修改浏览器地址栏的hash值
  • 通过浏览器的前进、后退

H5 history

用url规范的路由,但跳转不刷新页面

常规路由

H5 history路由

实现的api

  • history.pushState()  方法向当前浏览器会话的历史堆栈中添加一个状态
    history.pushState(state, title[, url])
    
  • window.onpopstatepopstate事件在window对象上的事件处理程序.

H5 history 需要后端配合

  • 无论你访问什么页面,都返回index.html这个路由
  • 然后再由前端通过history.pushState()的方式除触发路由的切换