跳到主要内容

思考🤔

  • 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>

v-for

<template>
<div>
可以通过v-fo in 数字,来迭代
<div v-for="(val, index) in 10">val: {{ val }}, index: {{ index }}</div>
</div>
</template>

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: [],
newProp: "",
};
},
methods: {
consoleData() {
console.log(this.$data);
},
},
};
</script>

Vue 中动态添加具有响应式的 form 属性的方法如下:

使用 Vue.set 方法: 使用下标访问:

this.$set(this.form, 'newProp', '')

调用 Vue.set 方法或 this.$set 方法,来动态添加一个具有响应式的 newProp 属性。

请注意,在 Vue 2.x 版本中,不能直接为对象添加新的属性,因为新增的属性不会被监听。因此,必须使用上述方法来动态添加属性,从而实现具有响应式的效果。

踩过坑:动态赋值,select 时没有回显

Vue父子组件如何通讯

  • Vue组件使用

  • 父子组件通讯

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

    通过自定义事件

    event.js

    import Vue from 'vue'

    export default new Vue()

组件A
```js
import event from "./event";
...
mounted() {
event.$on("onAddTitle", this.onAddTitle);
},
beforeUnmount() {
event.$off("onAddTitle");
},
```

组件B
```js
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)

如何计算最小变化

  • 因为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等

参考: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

vdom 和 diff 总结

  • 细节不重要,updateChildren 的过程也不重要,不用深究
  • vdom 核心概念(重要):h、vnode、patch、diff、key 等
  • vdom 存在的价值(重要):数据驱动视图、控制 DOM 操作

模板编译

  • 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()的方式除触发路由的切换