【02】Vue进阶

1-导学

1-1-技术面试流程

基础------>高级特性+原理(框架)------->设计+工作经验

1-2-知识点介绍

  1. vue和react:基本使用、高级特性、原理
  2. 工具: webpack的配置、性能优化、babel
  3. 项目设计: 状态设计、组件设计、组件通信

1-3-浅看面试题

  1. Vue

    • v-show和v-if的区别

    • 为什么v-for会用key

    • 描述vue组件生命周期(有父子组件的情况下)

    • Vue组件如何通信

    • 描述组件渲染和更新的过程

    • 双向数据绑定和v-model的实现原理

  2. React

    • React组件如何通信
    • JSX本质是什么
    • context是什么,有什么用途
    • shouldComponentUpdate的用途
    • 描述redux单项数据流
    • setState是同步还是异步
  3. 框架综合应用

    • 基于React设计一个todolist(组件结果,redux state数据结构)

    • 基于Vue设计一个购物车(组件结果,vuex State数据结构)

      1. webpack面试题
      • 前端代码为何要进行构建和打包
      • module chunck bundle分别是什么意思,有什么区别?
      • loader和plugin的区别?
      • webpack如何实现懒加载
      • webpack常见的性能优化
      • babel-runtime和babel-polyfill的区别

1-4-如何应对上述面试题

  • 框架的使用(基本使用、高级特性、周边插件)
  • 框架的原理(基本原理的了解、热门技术的深度、全面性)
  • 框架的实际运用,即设计能力(组件结构,数据结构)

2-Vue2.0

2-1-导学

  1. 先学vue2再学vue3

    • vue3不是从0做出来的,而是从vue进化的
    • vue2还会被继续使用,面试会继续考察
    • vue2的语法,绝大部分是被vue3支持的
  2. vue和react越来越接近

    • vue3的Options API对应React class Component
    • vue3的Composition API 对应 React Hooks

2-2-Vue的使用

2-2-1-基本使用,组件使用

01-基本使用
  • 差值{{message}}、表达式{{flage?'yes':'no'}}(只能是表达式,不能是js语句)
  • 指令、动态属性:id="myid"
  • v-html: 会有xss风险,会覆盖子组件
  • computed和watch
    • computed有缓存,data不变则不会重新计算
    • watch如何深度监听?
    • watch监听引用类型,拿不到oldValue
  • class和style
    • 使用动态属性、使用驼峰式写法

      <p :class="{ black: isBlack, yellow: isYellow }">使用 class</p>
      <p :class="[black, yellow]">使用 class (数组)</p>
      <p :style="styleData">使用 style</p>
                                                
       data() {
           return {
               isBlack: true,
               isYellow: true,
               black: 'black',
               yellow: 'yellow',
               styleData: {
                   fontSize: '40px', // 转换为驼峰式
                   color: 'red',
                   backgroundColor: '#ccc' // 转换为驼峰式
               }
           }
       }
      
  • 条件渲染
    • v-if v-else的用法,可以使用变量,也可以使用 === 表达式
    • v-if 和 v-for 的区别?
    • v-if 和 v-for 的使用场景?更新不频繁就 if, 频繁就show。
  • 循环渲染
    • 如何遍历对象?-----也可以用 v-for
    • key 的重要性。key不能乱写(如random或者index)
    • v-for 和 v-if 不能一起使用!for 比 if 计算优先级高一些,先用 for 进行循环,然后用 if 进行判断。如有三项数据,进行三项次循环,每一次循环进行 if 判断,做了三次重复的判断。
  • 事件
    • event 参数,自定义参数。1、event是原生对象2、事件被挂载到当前元素

      <button @click="increment1">+1</button>
      <button @click="increment2(2, $event)">+2</button>
      
    • 事件修饰符,按键修饰符

      <!-- 阻止事件继续传播 -->
      <a v-on:click.stop=""></a>
      <!-- 提交事件不再重载页面 -->
      <from v-on:submit.prevent=""></from>
      <!-- 修饰符可以串联 -->
      <a v-on:click.stop.prevent=""></a>
      <!-- 只有修饰符 -->
      <from v-on:click.submit.prevent></from>
      <!-- 添加事件监听器时使用事件捕获模式 -->
      <!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
      <div v-on:click.capture=""></div>
      <!-- 只当在event.target 是当前元素自身时触发处理函数 -->
      <!-- 即事件不是从内部元素触发的 -->
      <div v-on:click.self=""></div>
      
      <!-- 即使 Alt 或 Shift 被一同按下,也会触发 -->
      <button @click.ctrl=""></button>
      <!-- 有且只有Ctrl被按下时,才会触发 -->
      <button @click.ctrl.exact=""></button>
      <!-- 没有任何系统修饰符被按下,才会触发 -->
      <button @click.exact=""></button>
      
    • 【观察】事件被绑定到哪里?

  • 表单
    • 修饰符 lazy number trim

      <input type="text" v-model.trim="name"/>
      <input type="text" v-model.lazy="name"/> <!-- 输入完成后才会变化 -->
      <input type="text" v-model.number="age"/>
      
    • 常见表单项 textarea checkbox radio select

      <!-- checkedNames为数组 -->
      <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>
      <!-- gender为具体值的字符串-->
       <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>
      
02-组件使用
  • props 和 $emit

    父到子,通过props传属性。子到父,通过$emit触发父的方法。

    <List :list="list" @delete="deleteHandler"/>
    
    props: {
        // prop 类型和默认值
        list: {
            type: Array,
                default() {
                    return []
                }
        }
    },
     methods: {
         deleteItem(id) {
             this.$emit('delete', id)
         }
     },
    
  • 组件间通讯 - 自定义事件
    // 绑定自定义事件
    event.$on('onAddTitle', this.addTitleHandler)
    // 调用自定义事件
    event.$emit('onAddTitle', this.title)
    // 及时销毁,否则可能造成内存泄露
    event.$off('onAddTitle', this.addTitleHandler)
    
  • 组件生命周期
    • 挂载阶段
    • 更新阶段
    • 销毁阶段

2-2-2-高级特性

01-自定义v-model

父组件:

<template>
    <div>
        <p>{{name}}</p>
        <CustomVModel v-model="name"/>
    </div>
</template>
<script>
import CustomVModel from './CustomVModel'
export default {
    components: {
        CustomVModel
    },
    data() {
        return {
            name: '双越'
        }
    }
}
</script>

子组件:

<template>
    <!-- 例如:vue 颜色选择 -->
    <input type="text"
        :value="text1"
        @input="$emit('change1', $event.target.value)"
    >
    <!--
        1. 上面的 input 使用了 :value 而不是 v-model
        2. 上面的 change1 和 model.event1 要对应起来
        3. text1 属性对应起来
    -->
</template>
<script>
export default {
    model: {
        prop: 'text1', // 对应 props text1
        event: 'change1'
    },
    props: {
        text1: String,
        default() {
            return ''
        }
    }
}
</script>
02-$nextTick和refs
  • Vue是异步渲染
  • data改变之后,DOM不会立刻渲染
  • $nextTick 会在DOM渲染之后被触发,以获得最新的DOM节点
  • 页面渲染时会将 data 的修改做整合,多次 data 修改只会渲染一次
03-slot
  • 基本使用

    <!-- 父组件 -->
    <SlotDemo :url="website.url">
        {{website.title}}
    </SlotDemo>
    
    <template>
        <a :href="url">
            <slot>
                默认内容,即父组件没设置内容时,这里显示
            </slot>
        </a>
    </template>
    <script>
    export default {
        props: ['url']
    }
    </script>
    
  • 作用域插槽

    <template>
        <a :href="url">
            <slot :slotData="website">
                {{website.subTitle}} <!-- 默认值显示 subTitle ,即父组件不传内容时 -->
            </slot>
        </a>
    </template>
    <script>
    export default {
        props: ['url'],
        data() {
            return {
                website: {
                    url: 'http://wangEditor.com/',
                    title: 'wangEditor',
                    subTitle: '轻量级富文本编辑器'
                }
            }
        }
    }
    </script>
    
    <!-- 父组件 -->
    <ScopedSlotDemo :url="url">
        <template v-slot="slotProps">
    		{{slotProps.slotData.title}}
        </template>
    <script>
    
  • 具名插槽

    <div class="container">
      <header>
          <slot name="header"></slot>
      </header>
      <main>
        <slot></slot>
      </main>
      <footer>
        <slot name="footer"></slot>
      </footer>
    </div>
    
    <!-- 父组件 -->
    <base-layout>
      <template v-slot:header>
        <h1>插入到header 的slot中</h1>
      </template>
    
      <p>插入到 main slot 中, 即未命名的slot</p>
    
      <template v-slot:footer>
        <p>插入到 footer 的slot中</p>
      </template>
    </base-layout>
    
04-动态、异步组件
01-动态属性
  • :is=“component-name” 用法

  • 需要根据数据,动态渲染的场景。即组件类型不确定。

    <template>
        <div>
            <!-- 动态组件 -->
            <component :is="NextTickName"/> <!-- 必须得是动态的属性 -->
        </div>
    </template>
    <script>
    import NextTick from './NextTick'
    export default {
        components: {
           NextTick
        },
        data() {
            return {
                NextTickName: "NextTick",
            }
        }
    }
    </script>
    
02-异步组件
<template>
    <div>
        <FormDemo v-if="showFormDemo"/>
        <button @click="showFormDemo = true">show form demo</button>
    </div>
</template>
<script>
export default {
    components: {
        FormDemo: () => import('../BaseUse/FormDemo'), // 异步渲染
    },
    data() {
        return {
            showFormDemo: false
        }
    }
}
</script>
05-keep-alive
  • 缓存组件

  • 频繁切换,不需要重复渲染

  • Vue常见性能优化

    带有层级的,或者tab,用v-show不方便

    <template>
        <div>
            <button @click="changeState('A')">A</button>
            <button @click="changeState('B')">B</button>
            <button @click="changeState('C')">C</button>
    
            <keep-alive> <!-- tab 切换 -->
                <KeepAliveStageA v-if="state === 'A'"/> <!-- v-show -->
                <KeepAliveStageB v-if="state === 'B'"/>
                <KeepAliveStageC v-if="state === 'C'"/>
            </keep-alive>
        </div>
    </template>
    <script>
    import KeepAliveStageA from './KeepAliveStateA'
    import KeepAliveStageB from './KeepAliveStateB'
    import KeepAliveStageC from './KeepAliveStateC'
    export default {
        components: {
            KeepAliveStageA,
            KeepAliveStageB,
            KeepAliveStageC
        },
        data() {
            return {
                state: 'A'
            }
        },
        methods: {
            changeState(state) {
                this.state = state
            }
        }
    }
    </script>
    
06-mixin
  • 多个组件都有相同的逻辑,抽离出来
  • mixin并不是完美的解决方案,会有一些问题
  • Vue3提出的Composition API 在解决这些问题
  • mixin 的问题
    • 变量来源不明确,不利于阅读
    • 多mixin可能引起命名冲突
    • mixin 和组件可能出现多对多的关系,复杂度较高

mixin.js:

export default {
    data() {
        return {
            city: '北京'
        }
    },
    methods: {
        showName() {
            // eslint-disable-next-line
            console.log(this.name)
        }
    },
    mounted() {
        // eslint-disable-next-line
        console.log('mixin mounted', this.name)
    }
}

组件:

<template>
    <div>
        <p>{{name}} {{major}} {{city}}</p>
        <button @click="showName">显示姓名</button>
    </div>
</template>

<script>
import myMixin from './mixin'

export default {
    mixins: [myMixin], // 可以添加多个,会自动合并起来
    data() {
        return {
            name: '双越',
            major: 'web 前端'
        }
    },
    methods: {},
    mounted() {
        // eslint-disable-next-line
        console.log('component mounted', this.name)
    }
}
</script>

2-2-3-VueX和Vue-router 使用

01-VueX
  • VueX的基本概念
    • state
    • getter
    • action
    • mutation
  • 用于Vue组件
    • dispatch
    • commit
    • mapState
    • mapGetter
    • mapAction
    • mapMutation

在这里插入图片描述

所有的异步操作都在Actions里完成

02-Vue-Router
  • 路由模式(hash、H5 history)
  • 路由配置(动态路由,懒加载)
01-路由模式
  • hash模式(默认),如http://abc.com/#/user/10

  • H5 hostorty模式,如http://abc/user/20

  • 后者需要server端支持,因此无特殊需求可选择前者

02-路由配置
  • 动态路由
  • 懒加载 component:()=>import("./a")

总结:vueX和vue-router懂怎么配置就行,重点在vue

2-3-回顾题目

2-3-1-v-show和v-if的区别

v-if会频繁的操作dom,v-show只会频繁控制样式的显示隐藏,如果组件用于频繁切换的场景那么就用v-show。

2-3-2-为何v-for中要用key

key不能用index,只能用业务中不能重复的值。

2-3-3-描述Vue组件生命周期(有父子组件的情况下)

1,单个组件:挂载(beforeCreate,created,beforeMount,mounted),更新(beforeUpdate,updated),销毁(beforeDestroy,destroyed)。

2,父子组件:

  • 父created -> 子created -> 子mounted -> 父mounted
  • 父beforeUpdated -> 子beforeUpdated -> 子updated -> 父updated

创建是从外到内的,渲染是从内到外的。

2-3-4-vue组件如何通讯

  • 属性和触发事件的方式(props和this.$emit)
  • 自定义事件的方式(两个组件没有关系或者关系深)
  • vueX的通讯

2-3-5-描述组件渲染和更新的过程

暂时不会,接下来可以会

2-3-6-双向数据绑定v-model实现原理

暂时不会,接下来可以会

2-4-Vue的原理(大厂必考)

  • 面试为何会考察原理?

    • 知其然知其所以然。
    • 了解原理才能更好运用,竞争激烈。
    • 大厂造轮子(有钱有资源,业务定制,技术KPI)
  • 面试中如何考察?以何种方式考察?

    • 考察重点,非细节,掌握好2/8原则。
    • 和使用相关的原理,例如 vdom、模板渲染。
    • 整体流程是否全面?热门技术是否有深度?
  • vue原理包括那些?

    • 组件化
    • 响应式
    • vdom,diff
    • 模板编译
    • 渲染过程
    • 前端路由

2-4-1-组件化的基础

  • “很久以前”就有组件化

    • asp jsp php已经有组件化了
    • nodejs中也有类似的组件化
  • 数据驱动视图(MVVM,setState)

    • 传统组件,只是静态渲染,更新还要依赖操作DOM

    • 数据驱动视图 - vue MVVM(model-view-viewModel)

    • 数据驱动视图 - React setState

在这里插入图片描述

2-4-2-Vue的响应式

01-导学
  • 组件data的变化,立刻触发视图的更新。
  • 实现数据驱动视图的第一步。
  • 考察vue原理的第一题。
  • 核心API - Object.defineProperty
  • 如何实现响应式,代码演示。
  • Object.defineProperty的一些缺点(Vue3.0启用proxy)
  • Proxy有兼容性的问题
    • Proxy兼容性不太好,且无法用polyfill。
    • Vue2.X还会存在一段时间,所以都得学。
    • Vue3.0下章讲。
02-Object.defineProperty的基本用法
const data = {};
const name = "zhangsan";
Object.defineProperty(data,"name",{
    get: function() {
        console.log("get")
        return name;
    },
    set: function(newVal) {
        console.log("set");
        name = newVal;
    }
})

console.log(data.name)
// get
// zhangsan

data.name = "lisi";
//set
03-Object.defineProperty 实现响应式
  • 监听对象,监听数组
  • 复杂对象,深度监听
  • 几个缺点
    • 深度监听,需要递归到底,一次性计算量大。
    • 无法监听新增/删除属性(Vue.set Vue.delete)。
    • 无法监听原生数组,需要特殊处理。
// 触发更新视图
function updateView() {
    console.log('视图更新')
}

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
    arrProto[methodName] = function () {
        updateView() // 触发视图更新
        oldArrayProperty[methodName].call(this, ...arguments)
        // Array.prototype.push.call(this, ...arguments)
    }
})

// 重新定义属性,监听起来
function defineReactive(target, key, value) {
    // 深度监听
    observer(value)

    // 核心 API
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (newValue !== value) {
                // 深度监听
                observer(newValue)

                // 设置新值
                // 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
                value = newValue

                // 触发更新视图
                updateView()
            }
        }
    })
}

// 监听对象属性
function observer(target) {
    if (typeof target !== 'object' || target === null) {
        // 不是对象或数组
        return target
    }

    // 污染全局的 Array 原型
    // Array.prototype.push = function () {
    //     updateView()
    //     ...
    // }

    if (Array.isArray(target)) {
        target.__proto__ = arrProto
    }

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

// 准备数据
const data = {
    name: 'zhangsan',
    age: 20,
    info: {
        address: '北京' // 需要深度监听
    },
    nums: [10, 20, 30]
}

// 监听数据
observer(data)

// 测试
// data.name = 'lisi'
// data.age = 21
// // console.log('age', data.age)
data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
// data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组

2-4-3-虚拟DOM(Virtual DOM)和diff

01-导学

1、基础

  • vdom是实现vue和react的基石。
  • diff算法是vdom中最核心、最关键的部分。
  • vdom是一个热门话题,也是面试中的热门问题。

2、背景

  • DOM操作操作非常耗费性能
  • 以前JQuery,可以自行控制DOM操作时机,手动调整。
  • vue和react是数据驱动视图,如何有效控制DOM操作?

3、解决方案 - vdom

  • 有了一定复杂度,想要减少计算次数比较困难

  • 能不能把计算放到js中计算?因为JS执行速度很快

  • vdom - 用js模拟DOM结构,计算出最小变更,操作DOM

    <div id="div1" class="container">
      <p>vdom </p>
      <ul style="font-size: 20px">
        <li>a</li>
      </ul>
    </div>
    
    {
      tag: "div",
      props: {
        className: "container",
        id: "div1"
      },
      children: [
        {
          tag: "p",
          children: []
        },
        {
          tag: "ul",
          props: {style: "font-size:20px;"},
          children: [
            {
              tag: "li",
              children: "a"
            }
          ]
        }
      ]
    }
    

    4、通过snabbdom学习vdom

    • 简介强大的dom库,易学易用

    • Vue参考它实现的vdom和diff

    • https://github.com/snabbdom/snabbdom

    • Vue3.0重写了vdom的代码,优化了性能

    • 但vdom的基本理念不变,面试考点也不变

    • React和vdom的具体实现也不相同,但不妨统一学习

    5、snabbdom重点总结

    • h函数
    • vnode结构
    • patch函数

    6、vdom总结

    • 用js模拟Dom结构(vnode)
    • 新旧vnode对比,得出最小的更新范围,最后更新DOM
    • 数据驱动视图模式下,有效控制DOM操作
02-diff算法
  • diff算法是vdom中最核心,最关键的部分
  • diff算法能在日常中使用Vue React 中体现出来(如key)
  • diff算法是前端热门话题,面试宠儿
01-diff算法的概述

在这里插入图片描述

  • diff即对比,是一个广泛的概念,如linux diff命令、git diff等
  • 两个js对象也可以做diff,如https://github.com/flitbit/diff
  • 两棵树做diff,如这里的vdom diff

1、树的diff时间复杂度O(n^3)

  • 第一,遍历tree1;
  • 第二,遍历tree2;
  • 第三,排序
  • 1000个节点,要计算1亿次,算法不可用

2、优化时间复杂度到O(n)

  • 只比较同一层级,不跨级比较

在这里插入图片描述

  • tab不相同,则直接删掉重建,不在深度比较

在这里插入图片描述

  • tag和key,两者相同,则认为是相同节点,不再深度比较
02-snabbdom - 源码解读

1、示例:

import { init,  classModule, propsModule, styleModule, eventListenersModule, h,} from "snabbdom";

const patch = init([
  // 通过传入模块初始化 patch 函数
  classModule, // 开启 classes 功能
  propsModule, // 支持传入 props
  styleModule, // 支持内联样式同时支持动画
  eventListenersModule, // 添加事件监听
]);

const container = document.getElementById("container");

const vnode = h("div#container.two.classes", { on: { click: someFn } }, [
  h("span", { style: { fontWeight: "bold" } }, "This is bold"),
  " and this is just normal text",
  h("a", { props: { href: "/foo" } }, "I'll take you places!"),
]);
// 传入一个空的元素节点 - 将产生副作用(修改该节点)
patch(container, vnode);

const newVnode = h(
  "div#container.two.classes",
  { on: { click: anotherEventHandler } },
  [
    h(
      "span",
      { style: { fontWeight: "normal", fontStyle: "italic" } },
      "This is now italic type"
    ),
    " and this is still just normal text",
    h("a", { props: { href: "/bar" } }, "I'll take you places!"),
  ]
);
// 再次调用 `patch`
patch(vnode, newVnode); // 将旧节点更新为新节点

2、要素:

  • vnode = h(标签和选择器,本身的属性,孩子);
  • patch(dom/vnode, vnode);

3、源码:

  • h函数

    • 返回的还是一个函数,return vnode(sel, data, children, text, undefind)
  • vnode 导出一个函数

    • 这个函数内返回一个对象,return (sel, data, children, text, elm, key)
  • patch函数

    • 来源:var patch = snabbdom.init([相关参数])

    • init函数返回了一个patch函数。

    • patch函数参数

      function patch(oldVnode: VNode|Element, vnode: VNode):VNode {
        
        /** 略 */
        
        // cbs就是callbacks,执行pre hooks。pre属于生命周期第一个周期
        for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
        
        // 第一个参数不是vnode
        if(!isVnode(oldVnode)){
          // 创建一个空的vnode,关联到这个DOM元素
          oldVnode = emptyNodeAt(oldVnode);
        }
        
        // 相同的 vnode,sameVnode返回的是:vnode1.key===vnode2.key && vnode1.sel===vnode2.sel,key和sel都相同,如果没有传key,则都是undefind,undefind===undefind是true。
        if(sameVnode(oldVnode, vnode)) {
          // 进行vnode对比
          patchVnode(oldVnode, vnode, insertedVnodeQueue)
        }else{
          // 不相同的 vnode,直接删掉重建
          
          /** 略 */
          
          // 重建
          createdElm(vnode, insertedVnodeQueue)
          
        }
      }
      
    • patchVnode

      function patchVnode(oldValue: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
        // 执行prepatch hook
        const hook = vnode.data?.hook;
        hook?.prepatch?.(oldVnode, vnode);
        
        // 设置vnode.elem, 把新的vnode上挂上dom
        const elm = (vnode.elm = oldVnode.elm)!;
        
        // 旧的children
        const oldCh = oldVnode.children as VNode[];
        // 新的children
        const ch = vnode.children as VNode[];
        
        if(oldVnode === vnode) return;
        
        /** hook相关,略 */
        
        // vnode.text === undefind (vnode.children一般有值)
        if(isUndef(vnode.text)){
          if (isDef(oldCh) && isDef(ch)) { // 新旧都有 children
              if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
            } else if (isDef(ch)) { // 新children有
              if (isDef(oldVnode.text)) api.setTextContent(elm, "");
              addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
            } else if (isDef(oldCh)) { // 没有新的,有旧的
              removeVnodes(elm, oldCh, 0, oldCh.length - 1);
            } else if (isDef(oldVnode.text)) {
              api.setTextContent(elm, "");
            }
        } else if(oldValue.text !== vnode.text) {// vnode.text !== undefind (vnode.children无值)
          // 移除旧children
        		if(isdef(oldCh)) {
        		  // 移除旧children
                removeVnodes(elm, oldCh, 0, oldCh.length-1);
        		}
        		// 设置新 text
        		api.setTextContent(elm, vnode.text);
        }
      }
      
  • updateChildren

    这个只是snabbdom的实现方式,vue和react可能对比方式有所不同。

在这里插入图片描述
在这里插入图片描述

function updateChildren (parentElm: Node, oldCh: VNode[], newCh: VNode[], insertedVnodeQueue: VNodeQueue) {
    let oldStartIdx = 0;
    let newStartIdx = 0;
    let oldEndIdx = oldCh.length - 1;
    let oldStartVnode = oldCh[0];
    let oldEndVnode = oldCh[oldEndIdx];
    let newEndIdx = newCh.length - 1;
    let newStartVnode = newCh[0];
    let newEndVnode = newCh[newEndIdx];
    let oldKeyToIdx: KeyToIndexMap | undefined;
    let idxInOld: number;
    let elmToMove: VNode;
    let before: any;

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (oldStartVnode == null) {
        oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
      } else if (oldEndVnode == null) {
        oldEndVnode = oldCh[--oldEndIdx];
      } else if (newStartVnode == null) {
        newStartVnode = newCh[++newStartIdx];
      } else if (newEndVnode == null) {
        newEndVnode = newCh[--newEndIdx];
        
      // 开始和开始对比
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];
        
      // 结束和结束对比
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
        oldEndVnode = oldCh[--oldEndIdx];
        newEndVnode = newCh[--newEndIdx];
        
      // 开始和结束对比
      } else if (sameVnode(oldStartVnode, newEndVnode)) {
        // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
        api.insertBefore(
          parentElm,
          oldStartVnode.elm!,
          api.nextSibling(oldEndVnode.elm!)
        );
        oldStartVnode = oldCh[++oldStartIdx];
        newEndVnode = newCh[--newEndIdx];
        
      // 结束和开始对比
      } else if (sameVnode(oldEndVnode, newStartVnode)) {
        // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
        oldEndVnode = oldCh[--oldEndIdx];
        newStartVnode = newCh[++newStartIdx];
        
      // 没对比上
      } else {
        if (oldKeyToIdx === undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
        }
        // 拿新节点的key,能否对应上 oldCh 中某个节点的key
        idxInOld = oldKeyToIdx[newStartVnode.key as string];
        
        // 如果没有对应上
        if (isUndef(idxInOld)) {
          // New element
          // 新的,没有对应,直接插入
          api.insertBefore(
            parentElm,
            createElm(newStartVnode, insertedVnodeQueue),
            oldStartVnode.elm!
          );
            
        // 对应上了
        } else {
          elmToMove = oldCh[idxInOld];
            
          // sel 是否相等(sameVnode的条件)
          if (elmToMove.sel !== newStartVnode.sel) {
            // New element
          	// 新的,没有对应,直接插入
            api.insertBefore(
              parentElm,
              createElm(newStartVnode, insertedVnodeQueue),
              oldStartVnode.elm!
            );
              
          // select 相等,key相等
          } else {
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
            oldCh[idxInOld] = undefined as any;
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
          }
        }
        newStartVnode = newCh[++newStartIdx];
      }
    }

    if (newStartIdx <= newEndIdx) {
      before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
      addVnodes(
        parentElm,
        before,
        newCh,
        newStartIdx,
        newEndIdx,
        insertedVnodeQueue
      );
    }
    if (oldStartIdx <= oldEndIdx) {
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
    }
}

在这里插入图片描述

  • 不用key的话,老版本ABCD全部删掉重渲染,如果有key的话就可以直接移动过来。
  • 如果key使用随机数也不行,随机数都是新的,对应不上。
  • 如果key是index也不行,如果abcd有排序的变化也会出问题。
03-vdom和diff的总结
  • 细节不重要,updateChildren的过程也不重要,不要深究。
  • vdom核心概念很重要:h, vnode, patch, dff, key等。
  • vdom存在价值更重要:数据驱动视图,控制DOM操作。

2-4-4-模板编译

  • 模板是vue开发常用的部分,即与使用相关联的原理。
  • 它不是html,有指令、差值、js表达式,到底是什么?
  • 面试不会直接问,但是会通过“组件渲染和更新的过程”考察
01-前置知识:JS的with语法
const obj = {a:100, b:200}

console.log(obj.a)
console.log(obj.b)
console.log(obj.c) // undefined

// 使用with,能改变{}内自由变量的查找方式
// 将{}内的自由变量,当做obj的属性来找
with(obj) {
  	console.log(a)
	console.log(b)
	console.log(c) // 会报错!!
}
  • 改变了{}内自由变量的查找规则,当做obj的属性来查找
  • 如果找不到匹配的obj属性,就会报错
  • with要慎用,它打破了作用于规则,易读性变差
02-vue template complier 将模板编译为render函数
  • 模板不是html,有指令、差值、JS表达式,能实现判断、循环。
  • html是标签语言,只有JS才能实现判断、循环(图灵完备的)。
  • 因此,模板一定是转换成某种js代码,即编译模板。
const compiler = require('vue-template-compiler')

// 插值
// const template = `<p>{{message}}</p>`
// with(this){return createElement('p',[createTextVNode(toString(message))])}
// h -> vnode
// createElement -> vnode

// // 表达式
// const template = `<p>{{flag ? message : 'no message found'}}</p>`
// // with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}

// // 属性和动态属性
// const template = `
//     <div id="div1" class="container">
//         <img :src="imgUrl"/>
//     </div>
// `
// with(this){return _c('div',
//      {staticClass:"container",attrs:{"id":"div1"}},
//      [
//          _c('img',{attrs:{"src":imgUrl}})])}

// // 条件
// const template = `
//     <div>
//         <p v-if="flag === 'a'">A</p>
//         <p v-else>B</p>
//     </div>
// `
// with(this){return _c('div',[(flag === 'a')?_c('p',[_v("A")]):_c('p',[_v("B")])])}

// 循环
// const template = `
//     <ul>
//         <li v-for="item in list" :key="item.id">{{item.title}}</li>
//     </ul>
// `
// with(this){return _c('ul',_l((list),function(item){return _c('li',{key:item.id},[_v(_s(item.title))])}),0)}

// 事件
// const template = `
//     <button @click="clickHandler">submit</button>
// `
// with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}

// v-model
const template = `<input type="text" v-model="name">`
// 主要看 input 事件
// with(this){return _c('input',{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],attrs:{"type":"text"},domProps:{"value":(name)},on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})}

// render 函数
// 返回 vnode
// patch

// 编译
const res = compiler.compile(template)
console.log(res.render)

// ---------------分割线--------------

// // 从 vue 源码中找到缩写函数的含义
// function installRenderHelpers (target) {
//     target._o = markOnce;
//     target._n = toNumber;
//     target._s = toString;
//     target._l = renderList;
//     target._t = renderSlot;
//     target._q = looseEqual;
//     target._i = looseIndexOf;
//     target._m = renderStatic;
//     target._f = resolveFilter;
//     target._k = checkKeyCodes;
//     target._b = bindObjectProps;
//     target._v = createTextVNode;
//     target._e = createEmptyVNode;
//     target._u = resolveScopedSlots;
//     target._g = bindObjectListeners;
//     target._d = bindDynamicKeys;
//     target._p = prependModifier;
// }

  • 编译模板编译为render函数,执行render函数返回vnode
  • 基于vnode再执行patch和diff
  • 使用webpack vue-loader,会在开发环境编译模板(重要),会比引入js自己写要快。
03-vue组件中使用render代替template
Vue.component("heading", {
  // template: "xxx",
  render: function (createElement) {
    return createElement('h' + this.level,[ // h1,h2
      createElement('a',{
        attr: {
          name: "headerId",
          href: "#" + "headerId"
        }
      }, "this is a tag")
    ])
  }
})

2-4-5-组件渲染更新过程总结

  • 响应式: 监听data属性,setter和getter。
  • 模板编译:模板到render函数,再到vnode。
  • vdom: patch(elem, vnode)和 patch(vnode, newVnode)。
01-初次渲染过程
  • 解析模板为render函数(或在开发环境已完成, vue-loader)。
  • 触发响应式,监听data属性getter、setter。
  • 执行render函数,生成vnode, patch(elem, vnode)。
02-更新过程
  • 修改data,触发setter(此前在getter中已经被监听)。
  • 重新执行render函数,生成newVnode。
  • patch(vnode, newVnode)。

在这里插入图片描述

03-异步渲染
  • 回顾$nextTick
  • 汇总data的修改,一次性更新视图
  • 减少dom操作次数,提高性能

2-5-前端路由的原理

2-5-1-网页url组成部分

// http:127.0.0.1:8881/01-hash.html?a=100&b=20#/aaa/bbb
location.protocol // http
location.hostname // 127.0.0.1
location.host // 127.0.0.1:8881
location.pathname // /01-hash.html
location.search // ?a=100&b=20
location.hash // #/aaa/bbb

2-5-2-hash的特点

  • hash变化会触发网页跳转,即浏览器的前进后退(但不会刷新)。
  • hash不会刷新页面,SPA必需的特点。
  • hash永远不会提交到server端(前端自生自灭)。
// hash 变化,包括:
// a. JS 修改 url
// b. 手动修改 url 的 hash
// c. 浏览器前进、后退
window.onhashchange = (event) => {
  console.log('old url', event.oldURL)
  console.log('new url', event.newURL)

  console.log('hash:', location.hash)
}

// 页面初次加载,获取 hash
document.addEventListener('DOMContentLoaded', () => {
  console.log('hash:', location.hash)
})

// JS 修改 url
document.getElementById('btn1').addEventListener('click', () => {
  location.href = '#/user'
})

2-5-3-H5 history

  • url规范的路由,但跳转时不刷新页面
  • history.pushState
  • window.onpopState
  • 正常页面浏览
    • https://github.com/XXX 刷新页面
    • https://github.com/XXX/yyy 刷新页面
    • https://github.com/XXX/yyy/zzz 刷新页面
  • 改造为H5 history模式
    • https://github.com/XXX 刷新页面
    • https://github.com/XXX/yyy 前端跳转,不刷新页面
    • https://github.com/XXX/yyy/zzz 前端跳转,不刷新页面
// 页面初次加载,获取 path
document.addEventListener('DOMContentLoaded', () => {
  console.log('load', location.pathname)
})

// 打开一个新的路由
// 【注意】用 pushState 方式,浏览器不会刷新页面
document.getElementById('btn1').addEventListener('click', () => {
  const state = { name: 'page1' }
  console.log('切换路由到', 'page1')
  history.pushState(state, '', 'page1') // 重要!!
})

// 监听浏览器前进、后退
window.onpopstate = (event) => { // 重要!!
  console.log('onpopstate', event.state, location.pathname)
}

// 需要 server 端配合,可参考
// https://router.vuejs.org/zh/guide/essentials/history-mode.html#%E5%90%8E%E7%AB%AF%E9%85%8D%E7%BD%AE%E4%BE%8B%E5%AD%90

2-5-4-总结

  • 小结
    • hash - window.onhashchange
    • H5 history - history.pushState 和 window.onpopState
    • H5 history 需要后端支持
  • 两者选择
    • to B 的系统推荐使用hash,简单易用,对url规范不敏感
    • to C的系统,可以考虑选择H5 history,但需要服务器支持
    • 能选简单的就选简单的,要考虑成本和收益

2-6-Vue原理总结

  • 组件化
    • 组件化的历史
    • 数据驱动视图
    • MVVM
  • 响应式
    • Object.defineProperty
    • 监听对象(深度),监听数组
    • Object.defineProperty的缺点(Vue3 用proxy,后面会讲)
  • vdom和diff
    • 应用背景
    • vnode结构
    • snabbdom的使用:vnode h patch
  • 模板编译
    • with语法
    • 模板编译为render函数
    • 执行render函数生成vnode
  • 渲染过程
    • 初次渲染过程
    • 更新过程
    • 异步渲染
  • 前端路由
    • hash
    • H5 history
    • 两者的对比

3-面试真题演练

  • 是自己个人觉得的面试重点
  • 网上收集的面试题
  • 热门技术和知识点
  • 以下真题将分为几组,每组以最重要的题为组名

3-1-v-for为何使用key

3-1-1-v-show和v-if的区别?

  • v-show是通过css display控制显示和隐藏
  • v-if是组件真正的渲染和销毁,而不是显示和隐藏
  • 频繁切换显示状态用v-show,否则用v-if

3-1-2-为何在v-for中用key?

  • 必须使用key,且不能是index和random
  • diff算法中通过tag和key来判断,是否是sameNode
  • 减少渲染次数,提升渲染性能

3-1-3-描述Vue组件的生命周期(父子组件)

  • 单组件生命周期图(官网)

  • 父子组件生命周期的关系

  • 1,单个组件:挂载(beforeCreate,created,beforeMount,mounted),更新(beforeUpdate,updated),销毁(beforeDestroy,destroyed)。

    2,父子组件:

    • 父created -> 子created -> 子mounted -> 父mounted
    • 父beforeUpdated -> 子beforeUpdated -> 子updated -> 父updated

    创建是从外到内的,渲染是从内到外的。

3-1-4-Vue组件如何通讯(常见)

  • 父子组件propsthis.$emit
  • 自定义事件event.$onevent.$offevent.$emit
  • vuex

3-1-5-描述组件渲染和更新的过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-upWPvX1k-1681782975724)(image/20221122.PNG)]

3-2-组件data为何是函数

3-2-1-双向数据绑定v-model的实现原理

  • input元素的value = this.name
  • 绑定input事件this.name = $event.target.value
  • data更新触发re-render

3-2-2-对MVVM的理解

在这里插入图片描述

3-2-3-computed有何特点?

  • 缓存,data不变不会重新计算
  • 提高技能

3-2-4-为何组件data必须是一个函数?

export default {
  name: "app"
  data() {
    return {
      name: "vue"
    }
  }
}
  • export default 导出的对象实际上被编译后是一个class类,对组件的使用相当于实例化了一个类。
  • 所以实例化的时候执行data,如果data不是函数的话,在一个地方修改了name,其他name的值也会变。
  • 如果是函数,两个都在闭包之中,不会相互影响

3-2-5-ajax请求应该放在哪个生命周期?

  • mounted
  • js是单线程的,ajax异步获取数据
  • 反正mounted之前没有用,只会让逻辑更加混乱。

3-3-何时使用keep-alive

3-3-1-如何将组件所有props传递给子组件?

  • $props
  • <User v-bind="$props">
  • 细节知识点,优先级不高

3-3-2-如何自己实现v-model

  • 父组件绑定值:<Children v-model=“name”></Children>

  • 子组件接收:

    • <input type="text" :value="name" @input="$emit('change',$event.target.value)" />

    • props接收: props:{ name: String }

    • model里绑定: model:{ prop: "name", event: "change" }

  • 补充:其实主要是通过$emit触发去更新父组件的值,而model里绑定的event值就是$emit 的事件名称。

  • 其他:

    • 关于学习的时候发现双向更新数据的好用方法:.sync 修饰符。官网:https://v2.cn.vuejs.org/v2/guide/components-custom-events.html#sync-%E4%BF%AE%E9%A5%B0%E7%AC%A6

    • 父组件绑定:

      <template>
        	// 方法一:统一绑定
          <Children v-bind.sync="appType"></Children>
        	// 方法二:分开绑定
        	<Children :path.sync="appType.path" :idList.sync="appType.idList"></Children>
      </template>
      <script>
      export default {
          data() {
              return {
                  appType: {
                    path: [],
                    idList: []
                  }
              }
          }
      }
      </script>
      
    • 子组件的接收:

      <template>
        	{{ path }}
        	{{ idList }}
      </template>
      <script>
      export default {
          props: {
            path: Array,
            idList: idList
          }
        	methods: {
            update: {
        		// 请注意!事件名称必须是 update: 加上你要更新的data
        		this.$emit("update:path", [1,2,3]);
        		this.$emit("update:idList", [3,2,1])
            }
        	}
      }
      </script>
      

3-3-3-多个组件有相同的逻辑,如何抽离?

  • mixin
  • mixin的一些缺点

3-3-4-何时要使用异步组件?

  • 加载大组件
  • 路由异步加载

3-3-5-何时需要使用keep-alive?

  • 缓存组价,不需要重复渲染。
  • 如多个静态tab页的切换。
  • 优化性能。

3-4-何时需要使用beforeDestory

3-4-1-何时需要使用beforeDestory

  • 解绑自定义事件event.$off
  • 清除定时器
  • 解绑自定义的dom事件,如 window srcoll 等

3-4-2-什么是 作用域插槽

// 父组件
<template>
    <a :href="url">
        <slot :slotData="website">
            {{website.subTitle}} <!-- 默认值显示 subTitle ,即父组件不传内容时 -->
        </slot>
    </a>
</template>

<script>
export default {
    props: ['url'],
    data() {
        return {
            website: {
                url: 'http://wangEditor.com/',
                title: 'wangEditor',
                subTitle: '轻量级富文本编辑器'
            }
        }
    }
}
</script>

// 子组件
<scopedSlotDemo>
  <template v-slot="slotProps">
    {{slotProps.website.title}}
  </template>
</scopedSlotDemo>

3-4-3-Vuex中的action和mutation有何区别?

  • action中可以处理异步,mutation不可以。
  • mutation做原子操作(每次只做一个操作)。
  • action可以整合多个mutation.

3-4-4-Vue-router 常用的路由模式

  • hash模式
  • H5 history(需要服务端支持)
  • 两者比较

3-4-5-如何配置Vue-router 异步加载

export default new VueRouter{
  routes: [
    path: "/",
    component: () => import(
    	'./../components/Navigater'
    )
  ]
}

3-5-diff算法时间复杂度

3-5-1-请用vnode描述一个DOM结构

<div id="div1" class="container">
  <p>vdom</p>
  <ul style="font-size: 20px;">
      <li>a</li>
  </ul>
</div>
{
  tag: "div",
  props: {
    id: "div1",
    className: "container"
  }
  children: [
    {
      tag: "p",
      children: "vdom"
    },
    {
      tag: "ul",
      props: {style: "font-size: 20px;"},
      children: [
        {
          tag: "li",
          children: "a"
        }
      ]
    }
  ]
}

3-5-2-监听data变化的核心API是什么?

  • Object.defineProperty
  • 以及深度监听、监听数组
  • 有何缺点

3-5-3-Vue如何监听数组变化

  • Object.defineProperty不能监听数组变化
  • 重新定义原型,重写push pop等方法,实现监听
  • Proxy 可以支持监听数组变化

3-5-4-请描述响应式原理

  • 监听数组变化
  • 组件渲染和更新的流程

3-5-5-diff算法的时间复杂度

  • O(n)
  • 在O(n^3)基础上做了一些调整

3-6-Vue常见性能优化

3-6-1-简述diff算法过程

  • patch(elem, vnode) 和 patch(vnode, newVnode)
  • patchVnode 和 addVnodes 和 removeVnodes
  • updateChildren(key的重要性)

3-6-2-Vue为何是异步渲染,$nextTick何用?

  • 异步渲染(以及合并data修改),以提高渲染性能
  • $nextTick在DOM更新完之后,触发回调

3-6-3-Vue常见性能优化方式

  • 合理使用v-show和v-if
  • 合理使用computed
  • v-for时加key,以及避免和v-if同时使用
  • 自定义事件、DOM事件及时销毁
  • 合理使用异步组件
  • 合理使用keep-alive
  • data层级不要太深
  • 使用vue-loader在开发环境做模板编译(预编译)
  • webpack层面的优化
  • 前端通用的性能优化,如图片懒加载
  • 使用SSR

4-Vue3.0

对vue3进行全面讲解,包括vue3升级变动,Vue3核心知识点,compositionAPI如何使用,以及vue3的一些基础原理

  • 产出
    • vue3常考知识和面试题
  • 主要内容
    • vue3的基本使用
    • vue3相比于vue2的升级
    • ref相关知识点(比较难理解)
    • compositionAPI
    • vue3原理讲解
  • 关键字
    • vue3
    • ref
    • CompositionAPI
    • Proxy
  • 学习方法
    • Vue3课程示例,一定要亲自动手练习
    • 学习原理要关注重点,核心的部分,关注流程,不要关注细节

4-1-考点概述

(思维导图)

4-2-面试题

  • Vue3比Vue2有什么优势?
  • 描述Vue3的生命周期
  • 如何看待CompositionAPI和OptionsAPI?
  • 如何理解red toRef 和 toRefs?
  • Vue3升级了哪些重要的功能?
  • CompositionAPI如何实现代码复用?
  • Vue3如何实现响应式
  • watch和watchEffect的区别是什么?
  • setup中如何获取组件实例?
  • vue3为何比vue2快?
  • Vite是什么?
  • CompositionAPI和ReactHooks的对比

4-3-vue3对vue2有什么优势?

  • 性能更好
  • 体积更小
  • 更好的ts支持
  • 更好的代码组织
  • 更好的逻辑抽离
  • 更多新功能

4-4-Vue3生命周期

4-4-1-Option API生命周期

  • beforeDestroy改为beforeUnmount
  • destoryed改为unmounted
  • 其他沿用Vue2的生命周期

4-4-2-Composition API生命周期

<template>
    <p>生命周期 {{msg}}</p>
</template>

<script>
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'

export default {
    name: 'LifeCycles',

    props: {
        msg: String
    },

    // 等于 beforeCreate 和 created
    setup() {
        console.log('setup')

        onBeforeMount(() => {
            console.log('onBeforeMount')
        })
        onMounted(() => {
            console.log('onMounted')
        })
        onBeforeUpdate(() => {
            console.log('onBeforeUpdate')
        })
        onUpdated(() => {
            console.log('onUpdated')
        })
        onBeforeUnmount(() => {
            console.log('onBeforeUnmount')
        })
        onUnmounted(() => {
            console.log('onUnmounted')
        })
    },

    // beforeCreate() {
    //     console.log('beforeCreate')
    // },
    // created() {
    //     console.log('created')
    // },
    // beforeMount() {
    //     console.log('beforeMount')
    // },
    // mounted() {
    //     console.log('mounted')
    // },
    // beforeUpdate() {
    //     console.log('beforeUpdate')
    // },
    // updated() {
    //     console.log('updated')
    // },
    // // beforeDestroy 改名
    // beforeUnmount() {
    //     console.log('beforeUnmount')
    // },
    // // destroyed 改名
    // unmounted() {
    //     console.log('unmounted')
    // }
}
</script>

4-5-如何理解CompositionAPI和OptionsAPI

  • compositionAPI带来了什么?
  • compositionAPI和optionAPI如何选择?
  • 别误解compositionAPI

4-5-1-CompositionAPI带来了什么?

  • 更好的代码组织

在这里插入图片描述

  • 每种颜色是一种逻辑,compositionAPI在代码多、应用复杂的时候更好用

  • 更好的逻辑复用(有一道专门的面试题)

  • 更好的类型推导

    {
        data() {
            return {
              a:10
            }
        },
    	methods: {
      		fn1() {
              const a = this.a
      		}
    	},
    	mounted() {
          	this.fn1();
    	}
    }
    
    • 如果是通用的js语法应该是this.methods.fn1(),这种语法糖不利于一些工具的使用和类型推导

4-5-2-compositionAPI和optionAPI如何选择?

  • 不建议共用,会引起混乱
  • 小型项目,业务逻辑简单,用OptionAPI
  • 中大型项目,逻辑复杂,用CompositionAPI

4-5-3-别误解CompositionAPI

  • CompositionAPI是高阶技巧,不是基础必会
  • CompositionAPI是为了解决复杂业务逻辑而设计
  • CompositionAPI就行Hooks在React中的地位

4-5-3-小结

  • compositionAPI带来了什么?
  • compositionAPI和optionAPI如何选择?
  • 别误解compositionAPI

4-6-如何理解ref toRef和toRefs

  • 是什么
  • 最佳使用方式
  • 进阶和深入理解

4-6-1-是什么?

  • ref

    • 生成值类型的响应式数据

    • 可用于模板和reactive

    • 通过.value修改值

    • 代码示例

      <template>
          <p>ref demo {{ageRef}} {{state.name}}</p>
      </template>
      
      <script>
      import { ref, reactive } from 'vue'
      
      export default {
          name: 'Ref',
          setup() {
              const ageRef = ref(20) // 值类型 响应式
              const nameRef = ref('双越')
      
              const state = reactive({
                  name: nameRef
              })
      
              setTimeout(() => {
                  console.log('ageRef', ageRef.value)
      
                  ageRef.value = 25 // .value 修改值
                  nameRef.value = '双越A'
              }, 1500);
      
              return {
                  ageRef,
                  state
              }
          }
      }
      </script>
      
      <template>
          <p ref="elemRef">我是一行文字</p>
      </template>
      
      <script>
      import { ref, onMounted } from 'vue'
      
      export default {
          name: 'RefTemplate',
          setup() {
              const elemRef = ref(null)
      
              onMounted(() => {
                  console.log('ref template', elemRef.value.innerHTML, elemRef.value)
              })
      
              return {
                  elemRef
              }
          }
      }
      </script>
      
  • toRef

    • 针对一个响应式对象(reactive封装)的prop

    • 创建一个ref,具有响应式

    • 两者保持引用关系

    • 代码演示

      <template>
          <p>toRef demo - {{ageRef}} - {{state.name}} {{state.age}}</p>
      </template>
      
      <script>
      import { ref, toRef, reactive } from 'vue'
      
      export default {
          name: 'ToRef',
          setup() {
              const state = reactive({
                  age: 20,
                  name: '双越'
              })
      
              const age1 = computed(() => {
                  return state.age + 1
              })
      
              // // toRef 如果用于普通对象(非响应式对象),产出的结果不具备响应式
              // const state = {
              //     age: 20,
              //     name: '双越'
              // }
      
              const ageRef = toRef(state, 'age')
      
              setTimeout(() => {
                  state.age = 25
              }, 1500)
      
              setTimeout(() => {
                  ageRef.value = 30 // .value 修改值
              }, 3000)
      
              return {
                  state,
                  ageRef
              }
          }
      }
      </script>
      
    • 普通对象实现响应式用reactive;一个响应式对象里一个属性要单独实现响应式用toRef

  • toRefs

    • 将响应式对象(reactive封装)转化为普通对象

    • 对象的每个prop都是对应的ref

    • 两者保持引用关系

    • 代码演示

      <template>
          <p>toRefs demo {{age}} {{name}}</p>
      </template>
      
      <script>
      import { ref, toRef, toRefs, reactive } from 'vue'
      
      export default {
          name: 'ToRefs',
          setup() {
              const state = reactive({
                  age: 20,
                  name: '双越'
              })
      
              const stateAsRefs = toRefs(state) // 将响应式对象,变成普通对象
      
              // const { age: ageRef, name: nameRef } = stateAsRefs // 每个属性,都是 ref 对象
              // return {
              //     ageRef,
              //     nameRef
              // }
      
              setTimeout(() => {
                  state.age = 25
              }, 1500)
      
              return stateAsRefs
              // return state; // 如果这样的话<p>toRefs demo {{state.age}} {{state.name}}</p>
              // 如果直接解构,属性更改不会触发相应 return {...state}
      
          }
      }
      </script>
      

4-6-2-ref toRef和toRefs的最佳使用方式

  1. 合成函数返回响应式对象

    function useFeatureX() {
      const state = reactive({
        x: 1,
        y:2
      })
      // 逻辑运行忽略N行
      
      // 返回时转化为ref
      return toRefs(state)
    }
    
    export default {
      setup() {
        // 可以在不失去响应性的情况下破坏结构
    	const {x,y} = useFeatureX()
        return {
          x,
          y
        }
      }
    }
    
  2. 最佳使用方式

    • 用reactive做对象的响应式,用ref做值类型响应式
    • setup中返回toRefs(state),或者toRef(state, “xxx”)
    • ref的变量命名都用xxxRef
    • 合成函数返回响应式对象时,使用toRefs

4-6-3-进阶,深入理解

  • 为何需要ref?
  • 为何需要.value?
  • 为何需要toRef toRefs
  1. 为何需要ref?

    • 返回值类型,会丢失响应式

    • 如在setup、computed、合成函数,都有可能返回值类型

    • 如果vue不定义ref,用户将自造ref,反而混乱

      <template>
          <p>why ref demo {{state.age}} - {{age1}}</p>
      </template>
      
      <script>
      import { ref, toRef, toRefs, reactive, computed } from 'vue'
      
      function useFeatureX() {
          const state = reactive({
              x: 1,
              y: 2
          })
      
          return toRefs(state)
      }
      
      export default {
          name: 'WhyRef',
          setup() {
              const { x, y } = useFeatureX()
      
              const state = reactive({
                  age: 20,
                  name: '双越'
              })
      
              // computed 返回的是一个类似于 ref 的对象,也有 .value
              const age1 = computed(() => {
                  return state.age + 1
              })
      
              setTimeout(() => {
                  state.age = 25
              }, 1500)
      
              return {
                  state,
                  age1,
                  x,
                  y
              }
          }
      }
      </script>
      
  2. 为何需要.value?

    • ref是一个对象(不丢失响应式),value存储值

    • 通过.value属性的get和set 实现响应式

    • 用于模板,reactive时,不需要.value,其他情况都需要

      // computed 返回的是一个类似于 ref 的对象,也有 .value
      const age1 = computed(() => {
        return state.age + 1
      })
      
      // 错误
      function computed(getter) {
        let value
        watchEffect(()=>{ // 测试的时候可以把watchEffect代替到setTimeout
          value = getter()
        })
        return value
      }
      // let a = 100
      // b = a;
      // a = 200; // 等价于 a = computed(()=>100)
      // b // 100
      
      // 正确
      function computed(getter) {
        let ref = {
          value: null
        }
        watchEffect(()=>{ // 测试的时候可以把watchEffect代替到setTimeout
          ref.value = getter()
        })
        return ref
      }
      // const obj1 = {x:100}
      // const obj2 = obj1
      // obj1.x = 200; // 等价于  obj1.x = computed(()=>200)
      // boj2.x // 200
      
  3. 为何需要toRef toRefs

    • 初衷:不丢失响应式的情况下,吧对象数据分散/扩散
    • 前提:针对的是响应式对象(reactive封装的)非普通对象
    • 注意: 不创造响应式,而是延续响应式

4-7-vue3升级了哪些重要功能

  • createApp
  • emits属性
  • 生命周期
  • 多事件
  • Fragment
  • 移除.sync
  • 异步组件的写法
  • 移除filter
  • teleport
  • Supense
  • Composition API

4-7-1-createApp

// vue2.x
const app = new Vue({/* 选项 */})

// vue3.x
const app = Vue.createApp({/* 选项 */})
// vue2.x
Vue.use(/*...*/)
Vue.mixin(/*...*/)
Vue.component(/*...*/)
Vue.directive(/*...*/)

// vue3.x
app.use(/*...*/)
app.mixin(/*...*/)
app.component(/*...*/)
app.directive(/*...*/)

4-7-2-emits属性

<!-- 父组件 -->
<HelloWorld :msg="msg" @onSayHello="onSayHello" />
export default {
  name: "helloWorld",
  props: {
    msg: String
  },
  emits: ["onSayHello"], // 把sayHello改成onSayHello
  setup(props,{emit}) {
    emit("onSayHello", "vue3")
  }
}

4-7-3-多事件处理

<!-- 在methods里定义one,two两个函数 -->
<button @click="one($event), two($event)">
  Submit
</button>

4-7-4-Fragment

<!-- Vue2.x 组件模板 -->
<template>
  <div class="blog-post">
    <h3>{{ title }}</h3>
    <div v-html="content"></div>
  </div>
</template>

<!-- Vue3.x 组件模板 -->
<template>
  <h3>{{ title }}</h3>
  <div v-html="content"></div>
</template>

4-7-5-移除.sync

<!-- vue2.x -->
<MyComponent v-bind:title.sync="title"></MyComponent>

<!-- vue3.x -->
<MyComponent v-model:title="title"></MyComponent>

4-7-6-异步组件

// vue2.x
new Vue({
  components: {
    "mycomponent": ()=>import("./my-async-component.vue")
  }
})
// vue3.x
import {createApp, defineAsyncComponent} from 'vue'

createApp({
  components: {
    AsyncComponent: defineAsyncComponent(()=>import("./my-async-component.vue"))
  }
})

4-7-7-移除filter

<!-- 以下filter在vue3中不可用了! -->

<!-- 在双花括号中 -->
{{ message | capitalize}}

<!-- 在 v-blind z中 -->
<div v-blind:id="rawId | formatId"></div>

4-7-8-Teleport

<!-- data中设置modalOpen: false -->

<button @click="modalOpen=true">
  Open full screen modal!(With teleport)
</button>

<teleport to="body"> <!-- vue3可以直接放在body上去 -->
  <div v-if="modalOpen" class="modal">
    <div>
      teleport 弹窗 (父元素是 body)
      <button @click="modalOpen=false">close</button>
    </div>
  </div>
</teleport>
  

4-7-9-Suspense

<Suspense>
  <template>
    <Test1 /> <!-- 是一个异步组件 -->
  </template>
  <!-- #fallback 就是一个具名插槽 
	即 Suspens组件内部,有两个slot,
	其中一个具名为 fallback -->
  <template #fallback>
    Loading...
  </template>
</Suspense>

4-7-10-CompositionAPI

  • reactive
  • ref相关
  • readonly
  • watch和watchEffect
  • setup
  • 生命周期钩子函数

4-8-CompositionAPI如何实现逻辑复用

  • 抽离逻辑代码到一个函数

  • 函数命名约定为 useXxxx格式(React Hook也是)

  • 在setup中引用useXxxx函数

    <template>
        <p>mouse position {{x}} {{y}}</p>
    </template>
    
    <script>
    import { reactive } from 'vue'
    import useMousePosition from './useMousePosition'
    // import useMousePosition2 from './useMousePosition'
    
    export default {
        name: 'MousePosition',
        setup() {
            const { x, y } = useMousePosition()
            return {
                x,
                y
            }
    
            // const state = useMousePosition2()
            // return {
            //     state
            // }
        }
    }
    </script>
    
    import { reactive, ref, onMounted, onUnmounted } from 'vue'
    
    function useMousePosition() {
        const x = ref(0)
        const y = ref(0)
    
        function update(e) {
            x.value = e.pageX
            y.value = e.pageY
        }
    
        onMounted(() => {
            console.log('useMousePosition mounted')
            window.addEventListener('mousemove', update)
        })
    
        onUnmounted(() => {
            console.log('useMousePosition unMounted')
            window.removeEventListener('mousemove', update)
        })
    
        return {
            x,
            y
        }
    }
    
    // function useMousePosition2() {
    //     const state = reactive({
    //         x: 0,
    //         y: 0
    //     })
    
    //     function update(e) {
    //         state.x = e.pageX
    //         state.y = e.pageY
    //     }
    
    //     onMounted(() => {
    //         console.log('useMousePosition mounted')
    //         window.addEventListener('mousemove', update)
    //     })
    
    //     onUnmounted(() => {
    //         console.log('useMousePosition unMounted')
    //         window.removeEventListener('mousemove', update)
    //     })
    
    //     return state
    // }
    
    export default useMousePosition
    // export default useMousePosition2
    

4-9-Vue3如何实现响应式

  • 回顾Vue2.0的Object.defineProperty
  • 学习Proxy语法
  • Vue3如何用Proxy实现响应式
  • 两者对比

4-9-1-回顾Vue2.0的Object.defineProperty

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

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
    arrProto[methodName] = function () {
        updateView() // 触发视图更新
        oldArrayProperty[methodName].call(this, ...arguments)
        // Array.prototype.push.call(this, ...arguments)
    }
})

// 重新定义属性,监听起来
function defineReactive(target, key, value) {
    // 深度监听
    observer(value)

    // 核心 API
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (newValue !== value) {
                // 深度监听
                observer(newValue)

                // 设置新值
                // 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
                value = newValue

                // 触发更新视图
                updateView()
            }
        }
    })
}

// 监听对象属性
function observer(target) {
    if (typeof target !== 'object' || target === null) {
        // 不是对象或数组
        return target
    }

    // 污染全局的 Array 原型
    // Array.prototype.push = function () {
    //     updateView()
    //     ...
    // }

    if (Array.isArray(target)) {
        target.__proto__ = arrProto
    }

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

// 准备数据
const data = {
    name: 'zhangsan',
    age: 20,
    info: {
        address: '北京' // 需要深度监听
    },
    nums: [10, 20, 30]
}

// 监听数据
observer(data)

// 测试
// data.name = 'lisi'
// data.age = 21
// // console.log('age', data.age)
data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
// data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组
  • 缺点
    • 深度监听需要一次性递归
    • 无法监听新增和删除属性(Vue.set Vue.delete)
    • 无法监听原生数组,需要特殊处理

4-9-2-实现响应式

  1. 基本使用

    const data = {
      name: "zhangsan",
      age: "20"
    }
    
    //const data = ['a', 'b', 'c'];
    //data.push("d"); 
    // 依次打印:"get push" "get length" "set 3 d" "result true" "set length 4" "result true"
    
const ProxyData = new Proxy(data, {
  get(target, key, receiver) {
     const result = Reflect.get(target, key, receiver)
     console.log("get", key)
     return result; // 返回结果
   },
  set(target, key, val, receiver) {
     const result = Reflect.get(target, key, val, receiver)
     console.log("set", key, val)
     console.log("result", result)
     return result; // 是否设置成功
   },
   deleteProperty(target, key) {
     const result = Reflect.get(target, key)
     console.log("delete Property", key)
     console.log("result", result)
     return result; // 是否删除成功
   },
})

// const data = {
//     name: 'zhangsan',
//     age: 20,
// }
const data = ['a', 'b', 'c']

const proxyData = new Proxy(data, {
    get(target, key, receiver) {
        // 只处理本身(非原型的)属性
        const ownKeys = Reflect.ownKeys(target)
        if (ownKeys.includes(key)) {
            console.log('get', key) // 监听
        }

        const result = Reflect.get(target, key, receiver)
        return result // 返回结果
    },
    set(target, key, val, receiver) {
        // 重复的数据,不处理
        if (val === target[key]) {
            return true
        }

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

    • 和Proxy能力一一对应

    • 规范化、标准化、函数式

      const obj = {a:100, b:200}
      
      "a" in obj
      Reflect.has(obj, "a")
      
      delete obj.a
      Reflect.deleteProperty(obj, "a")
      
    • 代替掉Object上的工具函数

      const obj = {a:100, b:200}
      
      Obejct.getOwnPropertyNames(obj) // [a, b]
      Reflect.ownKeys(obj)
      
  2. 实现响应式

// 创建响应式
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)
        
            // 深度监听
            // 性能如何提升的?
            return reactive(result)
        },
        set(target, key, val, receiver) {
            // 重复的数据,不处理
            if (val === target[key]) {
                return true
            }
    
            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('result', result) // true
            return result // 是否设置成功
        },
        deleteProperty(target, key) {
            const result = Reflect.deleteProperty(target, key)
            console.log('delete property', key)
            // console.log('result', result) // true
            return result // 是否删除成功
        }
    }

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

// 测试数据
const data = {
    name: 'zhangsan',
    age: 20,
    info: {
        city: 'beijing',
        a: {
            b: {
                c: {
                    d: {
                        e: 100
                    }
                }
            }
        }
    }
}

const proxyData = reactive(data)
  • 深度监听,性能更好
  • 可监听 新增/删除 属性
  • 可监听数组变化

4-9-3-总结

  • Proxy能规避Object.defineProperty的问题
  • Proxy无法兼容所有浏览器,无法plofill

4-10-v-model参数的用法

<template>
    <p>{{name}} {{age}}</p>

    <user-info
        v-model:name="name"
        v-model:age="age"
    ></user-info>
</template>

<script>
import { reactive, toRefs } from 'vue'
import UserInfo from './UserInfo.vue'

export default {
    name: 'VModel',
    components: { UserInfo },
    setup() {
        const state = reactive({
            name: '双越',
            age: '20'
        })

        return toRefs(state)
    }
}
</script>
<template>
    <input :value="name" @input="$emit('update:name', $event.target.value)"/>
    <input :value="age" @input="$emit('update:age', $event.target.value)"/>
</template>

<script>
export default {
    name: 'UserInfo',
    props: {
        name: String,
        age: String
    }
}
</script>

4-11-watch和watchEffect的区别

  • 两者都可监听data属性变化
  • watch需要明确监听哪个属性
  • watchEffec会根据其中的属性,自动监听其变化
<template>
    <p>watch vs watchEffect</p>
    <p>{{numberRef}}</p>
    <p>{{name}} {{age}}</p>
</template>

<script>
import { reactive, ref, toRefs, watch, watchEffect } from 'vue'

export default {
    name: 'Watch',
    setup() {
        const numberRef = ref(100)
        const state = reactive({
            name: '双越',
            age: 20
        })

        watchEffect(() => {
            // 初始化时,一定会执行一次(收集要监听的数据)
            console.log('hello watchEffect')
        })
        watchEffect(() => {
            console.log('state.name', state.name)
        })
        watchEffect(() => {
            console.log('state.age', state.age)
        })
        watchEffect(() => {
            console.log('state.age', state.age)
            console.log('state.name', state.name)
        })
        setTimeout(() => {
            state.age = 25
        }, 1500)
        setTimeout(() => {
            state.name = '双越A'
        }, 3000)
        

        // watch(numberRef, (newNumber, oldNumber) => {
        //     console.log('ref watch', newNumber, oldNumber)
        // }
        // // , {
        // //     immediate: true // 初始化之前就监听,可选
        // // }
        // )

        // setTimeout(() => {
        //     numberRef.value = 200
        // }, 1500)

        // watch(
        //     // 第一个参数,确定要监听哪个属性
        //     () => state.age,

        //     // 第二个参数,回调函数
        //     (newAge, oldAge) => {
        //         console.log('state watch', newAge, oldAge)
        //     },

        //     // 第三个参数,配置项
        //     {
        //         immediate: true, // 初始化之前就监听,可选
        //         // deep: true // 深度监听
        //     }
        // )

        // setTimeout(() => {
        //     state.age = 25
        // }, 1500)
        // setTimeout(() => {
        //     state.name = '双越A'
        // }, 3000)

        return {
            numberRef,
            ...toRefs(state)
        }
    }
}
</script>

4-12-setup中如何获取组件的实例

  • 在setup和其他compositionAPI中没有this
  • 可通过getCurrentInstance获取当前实例
  • 若使用OptionAPI可照常使用this
<template>
    <p>get instance</p>
</template>

<script>
import { onMounted, getCurrentInstance } from 'vue'

export default {
    name: 'GetInstance',
    data() {
        return {
            x: 1,
            y: 2
        }
    },
    setup() {
        console.log('this1', this)

        onMounted(() => {
            console.log('this in onMounted', this)
            console.log('x', instance.data.x)
        })

        const instance = getCurrentInstance()
        console.log('instance', instance)
    },
    mounted() {
        console.log('this2', this)
        console.log('y', this.y)
    }
}
</script>

4-13-Vue3为何比Vue2快

  • Proxy响应式
  • PatchFlag
  • hoistStatic
  • catchHandler
  • SRR优化
  • tree-shaking

4-13-1-PatchFlag

  1. 编译模板是,动态节点做标记

  2. 标记,分为不同的类型,如TEXT PROPS

    代码演示:https://vue-next-template-explorer.netlify.app/#eyJzcmMiOiI8ZGl2PlxyXG4gIDxzcGFuPkhlbGxvIFdvcmxkPC9zcGFuPlxyXG4gIDxzcGFuPnt7IG1zZyB9fTwvc3Bhbj5cclxuICA8c3BhbiA6Y2xhc3M9XCJuYW1lXCI+5ZCN56ewPC9zcGFuPlxyXG4gIDxzcGFuIDppZD1cIm5hbWVcIj7lkI3np7A8L3NwYW4+XHJcbiAgPHNwYW4gOmlkPVwibmFtZVwiPnt7IG1zZyB9fTwvc3Bhbj5cclxuICA8c3BhbiA6aWQ9XCJuYW1lXCIgOm1zZz1cIm1zZ1wiPuWQjeensDwvc3Bhbj5cclxuPC9kaXY+Iiwib3B0aW9ucyI6e319

    <div>
      <span>Hello World</span>
      <span>{{ msg }}</span>
      <span :class="name">名称</span>
      <span :id="name">名称</span>
      <span :id="name">{{ msg }}</span>
      <span :id="name" :msg="msg">名称</span>
    </div>
    

    编译后:

    import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, normalizeClass as _normalizeClass, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
    
    export function render(_ctx, _cache, $props, $setup, $data, $options) {
      return (_openBlock(), _createElementBlock("div", null, [
        _createElementVNode("span", null, "Hello World"),
        _createElementVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */),
        _createElementVNode("span", {
          class: _normalizeClass(_ctx.name)
        }, "名称", 2 /* CLASS */),
        _createElementVNode("span", { id: _ctx.name }, "名称", 8 /* PROPS */, ["id"]),
        _createElementVNode("span", { id: _ctx.name }, _toDisplayString(_ctx.msg), 9 /* TEXT, PROPS */, ["id"]),
        _createElementVNode("span", {
          id: _ctx.name,
          msg: _ctx.msg
        }, "名称", 8 /* PROPS */, ["id", "msg"])
      ]))
    }
    
    // Check the console for the AST
    
  3. diff算法时,可以区分静态节点,以及不同的类型的动态节点

在这里插入图片描述

4-13-2-HoistStatic

  1. 将静态节点的定义,提升到父作用域,缓存起来(典型的拿空间换时间的优化策略)

    代码演示:https://vue-next-template-explorer.netlify.app/#eyJzcmMiOiI8ZGl2PlxyXG4gIDxzcGFuPkhlbGxvIFZ1ZTM8L3NwYW4+XHJcbiAgPHNwYW4+SGVsbG8gVnVlMzwvc3Bhbj5cclxuICA8c3Bhbj5IZWxsbyBWdWUzPC9zcGFuPlxyXG4gIDxzcGFuPnt7IG1zZyB9fTwvc3Bhbj5cclxuPC9kaXY+Iiwib3B0aW9ucyI6eyJob2lzdFN0YXRpYyI6dHJ1ZX19

    <div>
      <span>Hello Vue3</span>
      <span>Hello Vue3</span>
      <span>Hello Vue3</span>
      <span>{{ msg }}</span>
    </div>
    
    import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
    
    const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", null, "Hello Vue3", -1 /* HOISTED */)
    const _hoisted_2 = /*#__PURE__*/_createElementVNode("span", null, "Hello Vue3", -1 /* HOISTED */)
    const _hoisted_3 = /*#__PURE__*/_createElementVNode("span", null, "Hello Vue3", -1 /* HOISTED */)
    
    export function render(_ctx, _cache, $props, $setup, $data, $options) {
      return (_openBlock(), _createElementBlock("div", null, [
        _hoisted_1,
        _hoisted_2,
        _hoisted_3,
        _createElementVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
      ]))
    }
    
    // Check the console for the AST
    
  2. 多个相邻静态节点,会被合并起来

    代码演示:https://vue-next-template-explorer.netlify.app/#eyJzcmMiOiI8ZGl2PlxyXG4gIDxzcGFuPkhlbGxvIFZ1ZTM8L3NwYW4+XHJcbiAgPHNwYW4+SGVsbG8gVnVlMzwvc3Bhbj5cclxuICA8c3Bhbj5IZWxsbyBWdWUzPC9zcGFuPlxyXG4gIDxzcGFuPkhlbGxvIFZ1ZTM8L3NwYW4+XHJcbiAgPHNwYW4+SGVsbG8gVnVlMzwvc3Bhbj5cclxuICA8c3Bhbj5IZWxsbyBWdWUzPC9zcGFuPlxyXG4gIDxzcGFuPkhlbGxvIFZ1ZTM8L3NwYW4+XHJcbiAgPHNwYW4+SGVsbG8gVnVlMzwvc3Bhbj5cclxuICA8c3Bhbj5IZWxsbyBWdWUzPC9zcGFuPlxyXG4gIDxzcGFuPkhlbGxvIFZ1ZTM8L3NwYW4+XHJcbiAgPHNwYW4+e3sgbXNnIH19PC9zcGFuPlxyXG48L2Rpdj4iLCJvcHRpb25zIjp7ImhvaXN0U3RhdGljIjp0cnVlfX0=

    <div>
      <span>Hello Vue3</span>
      <span>Hello Vue3</span>
      <span>Hello Vue3</span>
      <span>Hello Vue3</span>
      <span>Hello Vue3</span>
      <span>Hello Vue3</span>
      <span>Hello Vue3</span>
      <span>Hello Vue3</span>
      <span>Hello Vue3</span>
      <span>Hello Vue3</span>
      <span>{{ msg }}</span>
    </div>
    
    import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, createStaticVNode as _createStaticVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
    
    const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<span>Hello Vue3</span><span>Hello Vue3</span><span>Hello Vue3</span><span>Hello Vue3</span><span>Hello Vue3</span><span>Hello Vue3</span><span>Hello Vue3</span><span>Hello Vue3</span><span>Hello Vue3</span><span>Hello Vue3</span>", 10)
    
    export function render(_ctx, _cache, $props, $setup, $data, $options) {
      return (_openBlock(), _createElementBlock("div", null, [
        _hoisted_1,
        _createElementVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
      ]))
    }
    
    // Check the console for the AST
    

4-13-3-cacheHandler

  1. 缓存事件

    代码演示:https://vue-next-template-explorer.netlify.app/#eyJzcmMiOiI8ZGl2PlxyXG4gIDxzcGFuIEBjbGljaz1cImNsaWNrSGFuZGxlclwiPkhlbGxvIFZ1ZTM8L3NwYW4+XHJcbjwvZGl2PiIsIm9wdGlvbnMiOnsiY2FjaGVIYW5kbGVycyI6dHJ1ZX19

    <div>
      <span @click="clickHandler">Hello Vue3</span>
    </div>
    
    import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
    
    // export function render(_ctx, _cache, $props, $setup, $data, $options) {
    //  return (_openBlock(), _createElementBlock("div", null, [
    //    _createElementVNode("span", { onClick: _ctx.clickHandler }, "Hello Vue3", 8 /* PROPS */, ["onClick"])
    //  ]))
    // }
    
    export function render(_ctx, _cache, $props, $setup, $data, $options) {
      return (_openBlock(), _createElementBlock("div", null, [
        _createElementVNode("span", {
          onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.clickHandler && _ctx.clickHandler(...args)))
        }, "Hello Vue3")
      ]))
    }
    
    // Check the console for the AST
    

4-13-4-SSR优化

  1. 静态节点直接输出,绕过了vdom
  2. 动态节点,还是需要渲染

代码演示: https://vue-next-template-explorer.netlify.app/#eyJzcmMiOiI8ZGl2PlxyXG4gIDxzcGFuPkhlbGxvIFZ1ZTM8L3NwYW4+XHJcbiAgPHNwYW4+SGVsbG8gVnVlMzwvc3Bhbj5cclxuICA8c3Bhbj5IZWxsbyBWdWUzPC9zcGFuPlxyXG4gIDxzcGFuPnt7IG1zZyB9fTwvc3Bhbj5cclxuPC9kaXY+Iiwic3NyIjp0cnVlLCJvcHRpb25zIjp7fX0=

<div>
  <span>Hello Vue3</span>
  <span>Hello Vue3</span>
  <span>Hello Vue3</span>
  <span>{{ msg }}</span>
</div>
import { mergeProps as _mergeProps } from "vue"
import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "vue/server-renderer"

export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
  const _cssVars = { style: { color: _ctx.color }}
  _push(`<div${
    _ssrRenderAttrs(_mergeProps(_attrs, _cssVars))
  }><span>Hello Vue3</span><span>Hello Vue3</span><span>Hello Vue3</span><span>${
    _ssrInterpolate(_ctx.msg)
  }</span></div>`)
}

// Check the console for the AST

4-13-5-tree shaking

  1. 编译时,根据不同的情况,引入不同的API

    代码演示:https://vue-next-template-explorer.netlify.app/#eyJzcmMiOiI8ZGl2PlxyXG4gIDxzcGFuIHYtaWY9XCJtc2dcIj5IZWxsbyBWdWUzPC9zcGFuPlxyXG4gIDxpbnB1dCB2LW1vZGVsPVwibXNnXCIgLz5cclxuPC9kaXY+Iiwic3NyIjpmYWxzZSwib3B0aW9ucyI6e319

    <div>
      <span v-if="msg">Hello Vue3</span>
      <input v-model="msg" />
    </div>
    <script>
    import { openBlock as _openBlock, createElementBlock as _createElementBlock, createCommentVNode as _createCommentVNode, vModelText as _vModelText, createElementVNode as _createElementVNode, withDirectives as _withDirectives } from "vue"
    
    export function render(_ctx, _cache, $props, $setup, $data, $options) {
      return (_openBlock(), _createElementBlock("div", null, [
        (_ctx.msg)
          ? (_openBlock(), _createElementBlock("span", { key: 0 }, "Hello Vue3"))
          : _createCommentVNode("v-if", true),
        _withDirectives(_createElementVNode("input", {
          "onUpdate:modelValue": $event => ((_ctx.msg) = $event)
        }, null, 8 /* PROPS */, ["onUpdate:modelValue"]), [
          [_vModelText, _ctx.msg]
        ])
      ]))
    }
    </script>
    
    <div>
      <span>Hello Vue3</span>
    </div>
    <script>
    import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
    
    export function render(_ctx, _cache, $props, $setup, $data, $options) {
      return (_openBlock(), _createElementBlock("div", null, [
        _createElementVNode("span", null, "Hello Vue3")
      ]))
    }
    </script>
    

4-14-Vite为什么启动非常快

vite是什么?

  • 一个前端打包工具,Vue作者发起的项目。
  • 借助Vue的影响力,发展较快,和webpack竞争。
  • 优势:开发环境下无需打包,启动快。

vite为何启动快?

  • 开发环境使用ES6 Module,无需打包----非常快。
  • 生产环境使用rollup,并不会快很多。

ES6 Module

基础代码演示:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ES Module demo</title>
</head>
<body>
    <p>基本演示</p>

    <script type="module">
        import add from './src/add.js'

        const res = add(1, 2)
        console.log('add res', res)
    </script>

    <script type="module">
        import { add, multi } from './src/math.js'
        console.log('add res', add(10, 20))
        console.log('multi res', multi(10, 20))
    </script>
</body>
</html>
// add.js
import print from './print.js'
export default function add(a, b) {
    print('print', 'add')
    return a + b
}

// print.js
export default function (a, b) {
    console.log(a, b)
}
// math.js
export function add(a, b) {
    return a + b
}

export function multi(a, b) {
    return a * b
}

外链演示:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ES Module demo</title>
</head>
<body>
    <p>外链</p>

    <script type="module" src="./index.js"></script>
</body>
</html>
// index.js
import { add, multi } from './math.js'
console.log('add res', add(10, 20))
console.log('multi res', multi(10, 20))

远程引用:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ES Module demo</title>
</head>
<body>
    <p>远程引用</p>

    <script type="module">
        import { createStore } from 'https://unpkg.com/redux@latest/es/redux.mjs' // mjs: module js
        console.log('createStore', createStore)
    </script>
</body>
</html>

动态引用:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ES Module demo</title>
</head>
<body>
    <p>动态引入</p>
    <button id="btn1">load1</button>
    <button id="btn2">load2</button>

    <script type="module">
        document.getElementById('btn1').addEventListener('click', async () => {
            const add = await import('./src/add.js')
            const res = add.default(1, 2)
            console.log('add res', res)
        })
        document.getElementById('btn2').addEventListener('click', async () => {
            const { add, multi } = await import('./src/math.js')
            console.log('add res', add(10, 20))
            console.log('multi res', multi(10, 20))
        })
    </script>
</body>
</html>

4-15-CompositionAPI和ReactHooks对比

  • 前者setup只会调用一次,而后者函数会被多次调用
  • 前者无需useMemo useCallback(就是不用缓存数据),因为setup只调用一次
  • 前者无需顾虑调用顺序,而后者需要保证hooks的顺序一致
  • 前者reactive + ref 比后者useState要难理解

5-Vue3和JSX

  • Vue3中的JSX的基本应用
  • JSX和template的区别
  • JSX和slot(体会JSX的优越性)
  • 注意
    • JSX最早是React提出的概念(现在已经发展壮大)
    • 如果不了解JSX语法,可以先学习React

5-1-Vue3中JSX的基本使用

  • 基本使用
  • 使用.jsx格式文件和defineComponent
  • 引用自定义组件,传递属性

代码演示:

demo.vue

<template>
    <p @click="changeFlag">Demo {{flagRef}}</p>
    <child a="abc" v-if="flagRef"></child>
    <ul>
        <li v-for="item in state.list" :key="item">{{item}}</li>
    </ul>
</template>

<script>
import { ref, reactive } from 'vue'
import Child from './Child'

export default {
    name: 'Demo',
    components: { Child },
    setup() {
        const flagRef = ref(true)

        function changeFlag() {
            flagRef.value = !flagRef.value
        }

        const state = reactive({
            list: ['a', 'b', 'c']
        })

        return {
            flagRef,
            changeFlag,
            state
        }
    }
}
</script>

demo1.vue

<script>
import { ref } from 'vue'
import Child from './Child'

export default {
    components: { Child },
    setup() {
        const countRef = ref(200)

        const render = () => {
            return <p>demo1 {countRef.value}</p> // jsx
        }
        return render
    }
}
</script>

demo.jsx

import { defineComponent, ref, reactive } from 'vue'
import Child from './Child'

export default defineComponent(() => {
    const flagRef = ref(true)

    function changeFlag() {
        flagRef.value = !flagRef.value
    }

    const state = reactive({
        list: ['a1', 'b1', 'c1']
    })

    const render = () => {
        return <>
            <p onClick={changeFlag}>demo1 {flagRef.value.toString()}</p>
            {flagRef.value && <Child a={flagRef.value}></Child>}

            <ul>
            {state.list.map(item => <li>{item}</li>)}
            </ul>
        </>
    }
    return render
})

// defineComponent(props, context)=>{}
// 1. setup 函数: defineComponent(()={setup函数内容})
// 2. 组件的配置:  defineComponent({配置的map})

// 可以用.jsx
//可以用.tsx   typescript的形式

child.jsx

import { defineComponent } from 'vue'

export default defineComponent({
    props: ['a'],
    setup(props) {
        const render = () => {
            return <p>Child {props.a}</p>
        }
        return render
    }
})

5-2-JSX和template的区别

  • 语法上有很大的区别
    • JSX本质就是js代码,可以使用js的任何能力
    • template只能嵌入简单的js表达式,其他需要指令,如v-if
    • JSX已经成为ES规范,template还是Vue自家规范
  • 本质上是相同的,都会编译为js代码(render函数)
  • 具体示例:插槽,自定义组件,属性和事件,条件和循环(在上面的代码演示里)

5-3-JSX和slot(插槽)

  • slot是vue发明的概念,为了完善template的能力
  • slot一直是Vue初学者的噩梦,特别是作用域slot
  • 但是使用JSX将很容易理解,因为JSX的本质就是js

5-3-1-Template与slot代码演示

demo.vue

<template>
    <tabs default-active-key="1" @change="onTabsChange">
        <tab-panel key="1" title="title1">
            <div>tab panel content 1</div>
        </tab-panel>
        <tab-panel key="2" title="title2">
            <div>tab panel content 2</div>
        </tab-panel>
        <tab-panel key="3" title="title3">
            <div>tab panel content 3</div>
        </tab-panel>
    </tabs>
</template>

<script>
import Tabs from './Tabs'
import TabPanel from './TabPanel'

export default {
    components: { Tabs, TabPanel },
    methods: {
        onTabsChange(key) {
            console.log('tab changed', key)
        }
    },
}
</script>

TablePanel.vue

<template>
    <slot></slot>
</template>

<script>
export default {
    name: 'TabPanel',
    props: ['key', 'title'],
}
</script>

Tabs.vue

<template>
    <div>
        <!-- tabs 头,按钮 -->
        <button
            v-for="titleInfo in titles"
            :key="titleInfo.key"
            :style="{ color: titleInfo.key === actKey ? 'blue' : '#333' }"
            @click="changeActKey(titleInfo.key)"
        >
            {{titleInfo.title}}
        </button>
    </div>

    <slot></slot> <!-- 如何控制slot里面东西的显示?用slot将会笔记复杂 -->
</template>

<script>
import { ref } from 'vue'

export default {
    name: 'Tabs',
    props: ['defaultActiveKey'],
    emits: ['change'],
    setup(props, context) {
        const children = context.slots.default()
        const titles = children.map(panel => {
            const { key, title } = panel.props || {}
            return {
                key,
                title
            }
        })

        // 当前 actKey
        const actKey = ref(props.defaultActiveKey)
        function changeActKey(key) {
            actKey.value = key
            context.emit('change', key)
        }

        return {
            titles,
            actKey,
            changeActKey
        }
    }
}
</script>

5-3-2-JSX与slot代码演示

demo.vue

<template>
    <tabs default-active-key="1" @change="onTabsChange">
        <tab-panel key="1" title="title1">
            <div>tab panel content 1</div>
        </tab-panel>
        <tab-panel key="2" title="title2">
            <div>tab panel content 2</div>
        </tab-panel>
        <tab-panel key="3" title="title3">
            <div>tab panel content 3</div>
        </tab-panel>
    </tabs>
</template>

<script>
import Tabs from './Tabs.jsx'
import TabPanel from './TabPanel'

export default {
    components: { Tabs, TabPanel },
    methods: {
        onTabsChange(key) {
            console.log('tab changed', key)
        }
    },
}
</script>

TabPanel.vue

<template>
    <slot></slot>
</template>

<script>
export default {
    name: 'TabPanel',
    props: ['key', 'title'],
}
</script>

Tabs.jsx

import { defineComponent, ref } from 'vue'

export default defineComponent({
    name: 'Tabs',
    props: ['defaultActiveKey'],
    emits: ['change'],
    setup(props, context) {
        const children = context.slots.default()
        const titles = children.map(panel => {
            const { key, title } = panel.props || {}
            return {
                key,
                title
            }
        })

        // 当前 actKey
        const actKey = ref(props.defaultActiveKey)
        function changeActKey(key) {
            actKey.value = key
            context.emit('change', key)
        }

        // jsx
        const render = () => <>
            <div>
                {/* 渲染 buttons */}
                {titles.map(titleInfo => {
                    const { key, title } = titleInfo
                    return <button
                        key={key}
                        style={{ color: actKey.value === key ? 'blue' : '#333' }}
                        onClick={() => changeActKey(key)}
                    >{title}</button>
                })}
            </div>

            <div>
                {children.filter(panel => {
                    const { key } = panel.props || {}
                    if (actKey.value === key) return true // 匹配上 key ,则显示
                    return false // 否则,隐藏
                })}
            </div>
        </>
        return render
    }
})

5-3-3-scoped-slot-template代码演示

demo.vue

<template>
    <child>
        <!-- <p>scoped slot</p> -->

        <template v-slot:default="slotProps">
            <p>msg: {{slotProps.msg}} 123123</p>
        </template>
    </child>
</template>

<script>
import Child from './Child'

export default {
    components: { Child }
}
</script>

Child.vue

<template>
    <p>child</p>
    <slot :msg="msg"></slot>
</template>

<script>
export default {
    data() {
        return {
            msg: '作用域插槽 Child'
        }
    }
}
</script>

5-3-4-scoped-slot-jsx代码演示

demo.jsx

import { defineComponent } from 'vue'
import Child from './Child'

export default defineComponent(() => {
    function render(msg) {
        return <p>msg: {msg} 123123</p>
    }

    return () => {
        return <>
            <p>Demo - JSX</p>
            <Child render={render}></Child>
        </>
    }
})

child.jsx

import { defineComponent, ref } from 'vue'

export default defineComponent({
    props: ['render'],
    setup(props) {
        const msgRef = ref('作用域插槽 Child - JSX')

        return () => {
            return <p>{props.render(msgRef.value)}</p>
        }
    }
})

5-4-Vue3 script setup(vue3.2更新

  • Vue3引入了compositionAPI
  • compositionAPI最重要的是setup函数
  • script只有一个setup函数太孤单,如何简化一下?

5-4-1-基本使用

  • 顶级变量,自定义组件,可以直接用于模板
  • 可以正常使用ref reactive computed等功能
  • 和其他<script>同时使用

5-4-2-属性和事件

  • defineProps
  • defineEmits

5-4-3-defineExpose

  • 暴露数据给父组件

5-4-4-代码演示

demo.vue

<script>
function add(a, b) { return a + b }
</script>

<script setup> // 标签setup
import { ref, reactive, toRefs, onMounted } from 'vue'
import Child1 from './Child1'
import Child2 from './Child2'
import Child3 from './Child3'

// 顶级变量 可以直接用于template
const countRef = ref(100)

function addCount() {
    countRef.value++
}

const state = reactive({
    name: '双越'
})
const { name } = toRefs(state)

console.log( add(10, 20) )

function onChange(info) {
    console.log('on change', info)
}
function onDelete(info) {
    console.log('on delete', info)
}

const child3Ref = ref(null)  // ref="child3Ref"
onMounted(() => {
    // 拿到 Child3 组件的一些数据
    console.log(child3Ref.value)
    console.log(child3Ref.value.a)
    console.log(child3Ref.value.b)
})

</script>

<template>
    <p @click="addCount">{{countRef}}</p>
    <p>{{name}}</p>
    <child-1></child-1>
    <hr>

    <child-2 :name="name" :age="countRef" @change="onChange" @delete="onDelete"></child-2>
    <hr>

    <child-3 ref="child3Ref"></child-3>
</template>

Child1.vue

<script setup>
</script>

<template>
    <p>Child1</p>
</template>

Child2.vue

<script setup>
import { defineProps, defineEmits } from 'vue'

// 定义属性
const props = defineProps({
    name: String,
    age: Number
})

// 定义事件
const emit = defineEmits(['change', 'delete'])
function deleteHandler() {
    emit('delete', 'aaa')
}

</script>

<template>
    <p>Child2 - name: {{props.name}}, age: {{props.age}}</p>
    <button @click="$emit('change', 'bbb')">change</button>
    <button @click="deleteHandler">delete</button>
</template>

Child3.vue

<script setup>
import { ref, defineExpose } from 'vue'

const a = ref(101)
const b = 201

defineExpose({
    a,
    b
})

</script>

<template>
    <p>Child3</p>
</template>

总结:

  • 基本使用,<script>写在<template>前面,看起来更直观
  • 定义属性defineProps,定义事件defineEmits
  • defineExpose暴露数据给父组件

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值