中级-框架基础(上)

289 阅读15分钟

一、导学

学习前提

了解javascript和ES6基本语法

用过Vue React和Webpack

前端常见面试流程

一面(基础知识):JS基础知识、框架基本使用

二面(高级特性+原理):框架高级特性、框架原理

三面(设计+经验):项目设计能力、工作经验和环境

知识点介绍

Vue框架部分

Vue基本使用

Vue高级特性

Vue原理

React框架部分

React基本使用

React高级特性

React原理

工具部分

Webpack配置

性能优化

babel

项目设计

状态设计

组件设计

组件通讯

思维导图.png

二、课程介绍

先看几个面试题

先看几个面试题,先不解答,自己思考

思考如何应对这些,以及所有的面试

Vue面试题

v-show和v-if的区别

为何v-for中要用key

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

Vue组件如何通讯

描述组件渲染和更新的过程(非常重要)

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

React面试题

React组件如何通讯

JSX本质是什么

context是什么,有何用途

shouldCompoentUpdate的用途

描述redux单向数据流

image.png

setState是同步还是异步(场景图,见下文)

框架综合应用

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

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

webpack面试题

前端代码为何要进行构建和打包

module chunk bundle分别什么意思,有何区别?

loader和plugin的区别

webpack如何实现懒加载

webpack常用性能优化

babel-runtime和babel-polyfill的区别

如何应对上述面试题

框架的使用(基本使用,高级特性,周边插件)

框架的原理(基本原理的了解,热门技术的深度,全面性)

框架的实际应用,即设计能力(组件结构,数据结构)

面试官为何要这样考察

保证候选人能正常工作—考察使用

多个候选人竞争时,选择有技术追求—考察原理

看候选人是否能独立承担项目—考察设计能力

接下来

逐步讲解Vue React webpack的使用和原理

讲解Vue React如何做项目设计

三、Vue使用

1.概述

基本使用,组件使用—常用,必须会
高级特性—不常用,但体现深度
Vuex和Vue-router的使用

2.面试题

v-show和v-if的区别
为何v-for中要用key
描述Vue组件生命周期(有父子组件的情况)
Vue组件如何通讯
描述组件渲染和更新的过程
双向数据绑定v-model的实现原理

3.Vue基本使用

日常使用,必须掌握,面试必考(不一定会考)
梳理知识点,从冗长的文档中摘出考点和重点
考察形式不限(参考后面的面试真题),但都在范围之内

模板(指令、插值)

插值、表达式
指令、动态属性
v-html:会有XSS风险,会覆盖子组件

<template>
    <div>
        <p>文本插值 {{message}}</p>
        <p>JS 表达式 {{ flag ? 'yes' : 'no' }} (只能是表达式,不能是 js 语句)</p>

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

        <hr/>
        <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> <i>斜体</i>',
            dynamicId: `id-${Date.now()}`
        }
    }
}
</script>

computed和watch

computed有缓存,data不变则不会重新计算
watch如何深度监听
watch监听引用类型,拿不到oldVal

<template>
    <div>
        <p>num {{num}}</p>
        <p>double1 {{double1}}</p>
        <input v-model="double2"/>
    </div>
</template>

<script>
export default {
    data() {
        return {
            num: 20
        }
    },
    computed: {
        double1() {
            return this.num * 2
        },
        double2: {
            get() {
                return this.num * 2
            },
            set(val) {
                this.num = val/2
            }
        }
    }
}
</script>
<template>
    <div>
        <input v-model="name"/>
        <input v-model="info.city"/>
    </div>
</template>

<script>
export default {
    data() {
        return {
            name: '双越',
            info: {
                city: '北京'
            }
        }
    },
    watch: {
        name(oldVal, val) {
            // eslint-disable-next-line
            console.log('watch name', oldVal, val) // 值类型,可正常拿到 oldVal 和 val
        },
        info: {
            handler(oldVal, val) {
                // eslint-disable-next-line
                console.log('watch info', oldVal, val) // 引用类型,拿不到 oldVal 。因为指针相同,此时已经指向了新的 val
            },
            deep: true // 深度监听
        }
    }
}
</script>

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: true,

            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-else的用法,可使用变量,也可以使用===表达式
v-if和v-show的区别
v-if和v-show的使用场景:切换频繁使用v-show

v-show和v-if都能控制元素的显示和隐藏。

v-show本质就是通过设置css中的display设置为none,控制隐藏,无论true或者false初始都会进行渲染,因此切换开销比较小,初始开销较大

v-if是动态的向DOM树内添加或者删除DOM元素,因为懒加载,初始为false时,不会渲染,因此初始渲染开销较小,切换开销比较大,要不停的销毁和创建)

切换频繁使用v-show

<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'">A by v-show</p>
        <p v-show="type === 'b'">B by v-show</p>
    </div>
</template>

<script>
export default {
    data() {
        return {
            type: 'a'
        }
    }
}
</script>

循环(列表)渲染

如何遍历对象?—也可以用v-for
key的重要性。key不能乱写(如random或者index)
v-for和v-if不能一起使用

因为v-for的优先级比v-if高,意味着v-if 将分别重复运行于每个 v-for 循环中,造成不必要的计算,影响性能
解决方法:将 v-if 置于外层元素 或在计算属性中进行条件过滤

<template>
    <div>
        <p>遍历数组</p>
        <ul>
            <li v-for="(item, index) in listArr" :key="item.id">
                {{index}} - {{item.id}} - {{item.title}}
            </li>
        </ul>

        <p>遍历对象</p>
        <ul >
            <li v-for="(val, key, index) in listObj" :key="key">
                {{index}} - {{key}} -  {{val.title}}
            </li>
        </ul>
    </div>
</template>

<script>
export default {
    data() {
        return {
            flag: false,
            listArr: [
                { id: 'a', title: '标题1' }, // 数据结构中,最好有 id ,方便使用 key
                { id: 'b', title: '标题2' },
                { id: 'c', title: '标题3' }
            ],
            listObj: {
                a: { title: '标题1' },
                b: { title: '标题2' },
                c: { title: '标题3' },
            }
        }
    }
}
</script>

事件

event参数,自定义参数
事件修饰符,按键修饰符
【观察】事件被绑定到哪里

<template>
    <div>
        <p>{{num}}</p>
        <button @click="increment1">+1</button>
        <button @click="increment2(2, $event)">+2</button>
    </div>
</template>

<script>
export default {
    data() {
        return {
            num: 0
        }
    },
    methods: {
        increment1(event) {
            // eslint-disable-next-line
            console.log('event', event, event.__proto__.constructor) // 是原生的 event 对象
            // eslint-disable-next-line
            console.log(event.target)
            // eslint-disable-next-line
            console.log(event.currentTarget) // 注意,事件是被注册到当前元素的,和 React 不一样
            this.num++

            // 1. event 是原生的
            // 2. 事件被挂载到当前元素
            // 和 DOM 事件一样
        },
        increment2(val, event) {
            // eslint-disable-next-line
            console.log(event.target)
            this.num = this.num + val
        },
        loadHandler() {
            // do some thing
        }
    },
    mounted() {
        window.addEventListener('load', this.loadHandler)
    },
    beforeDestroy() {
        //【注意】用 vue 绑定的事件,组建销毁时会自动被解绑
        // 自己绑定的事件,需要自己销毁!!!
        window.removeEventListener('load', this.loadHandler)
    }
}
</script>

image.png

image.png

表单

v-model
常见表单项textarea checkbox radio select
修饰符lazy number trim

当添加.lazy修饰符之后,相当于 双向数据绑定不起作用了,主要用于控制数据同步的时机,改变input框中的内容并不会使数据发生变化,当输入框失去焦点后触发change事件,数据才更新

<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>
    </div>
</template>

<script>
export default {
    data() {
        return {
            name: '双越',
            age: 18,
            desc: '自我介绍',

            checked: true,
            checkedNames: [],

            gender: 'male',

            selected: '',
            selectedList: []
        }
    }
}
</script>

4.组件

props和$emit

组件间通讯-自定义事件

//index.vue
<template>
    <div>
        <Input @add="addHandler"/>
        <List :list="list" @delete="deleteHandler"/>
    </div>
</template>

<script>
import Input from './Input'
import List from './List'

export default {
    components: {
        Input,
        List
    },
    data() {
        return {
            list: [
                {
                    id: 'id-1',
                    title: '标题1'
                },
                {
                    id: 'id-2',
                    title: '标题2'
                }
            ]
        }
    },
    methods: {
        addHandler(title) {
            this.list.push({
                id: `id-${Date.now()}`,
                title
            })
        },
        deleteHandler(id) {
            this.list = this.list.filter(item => item.id !== id)
        }
    },
    created() {
        // eslint-disable-next-line
        console.log('index created')
    },
    mounted() {
        // eslint-disable-next-line
        console.log('index mounted')
    },
    beforeUpdate() {
        // eslint-disable-next-line
        console.log('index before update')
    },
    updated() {
        // eslint-disable-next-line
        console.log('index updated')
    },
}
</script>
//Input.vue
<template>
    <div>
        <input type="text" v-model="title"/>
        <button @click="addTitle">add</button>
    </div>
</template>

<script>
import event from './event'

export default {
    data() {
        return {
            title: ''
        }
    },
    methods: {
        addTitle() {
            // 调用父组件的事件
            this.$emit('add', this.title)

            // 调用自定义事件
            event.$emit('onAddTitle', this.title)

            this.title = ''
        }
    }
}
</script>
//event.js 
import Vue from 'vue' 
export default new Vue()
//List.vue
<template>
    <div>
        <ul>
            <li v-for="item in list" :key="item.id">
                {{item.title}}

                <button @click="deleteItem(item.id)">删除</button>
            </li>
        </ul>
    </div>
</template>

<script>
import event from './event'

export default {
    // props: ['list']
    props: {
        // prop 类型和默认值
        list: {
            type: Array,
            default() {
                return []
            }
        }
    },
    data() {
        return {

        }
    },
    methods: {
        deleteItem(id) {
            this.$emit('delete', id)
        },
        addTitleHandler(title) {
            // eslint-disable-next-line
            console.log('on add title', title)
        }
    },
    created() {
        // eslint-disable-next-line
        console.log('list created')
    },
    mounted() {
        // eslint-disable-next-line
        console.log('list mounted')

        // 绑定自定义事件
        event.$on('onAddTitle', this.addTitleHandler)
    },
    beforeUpdate() {
        // eslint-disable-next-line
        console.log('list before update')
    },
    updated() {
        // eslint-disable-next-line
        console.log('list updated')
    },
    beforeDestroy() {
        // 及时销毁,否则可能造成内存泄露
        event.$off('onAddTitle', this.addTitleHandler)
    }
}
</script>

生命周期

单个组件

  • 挂载阶段(beforeCreate、created、beforeMount、mounted)
  • 更新阶段(beforeUpdate、updated)
  • 销毁阶段(beforeDestory、destroyed )

image.png

created和mounted区别
created把vue示例给初始化,只是存在js内存模型的内存变量而已,并没有开始渲染;
mounted组件真正在网页上汇聚完成了,页面渲染完成了

beforeDestory上可以做什么
解除绑定,销毁子组件以及事件监听器

父子组件

image.png

image.png

创建初始化vue实例是从外到内,只有父组件初始化完才能初始化子组件
渲染是从内到外,只有把子组件渲染完父组件才能渲染完
beforeUpdate:更新的时候父组件的data首先被修改,首先执行beforeUpdate
updated:只有子组件更新完了父组件才能更新完

渲染过程:父组件挂载完成一定是等子组件都挂载完成后,才算是父组件挂载完,所以父组件的mounted在子组件mouted之后。父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted

子组件更新过程:
影响到父组件: 父beforeUpdate -> 子beforeUpdate->子updated -> 父updated
不影响父组件: 子beforeUpdate -> 子updated

父组件更新过程:
影响到子组件: 父beforeUpdate -> 子beforeUpdate->子updated -> 父updated
不影响子组件: 父beforeUpdate -> 父updated

销毁过程:父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed

不管是哪种情况,都一定是父组件等待子组件完成后,才会执行自己对应完成的钩子

5.高级特性

//index.vue
<template>
    <div>
        <p>vue 高级特性</p>
        <hr>

        <!-- 自定义 v-model -->
        <!-- <p>{{name}}</p>
        <CustomVModel v-model="name"/> -->

        <!-- nextTick -->
        <!-- <NextTick/> -->

        <!-- slot -->
        <!-- <SlotDemo :url="website.url">
            {{website.title}}
        </SlotDemo> -->
        <!-- <ScopedSlotDemo :url="website.url">
            <template v-slot="slotProps">
                {{slotProps.slotData.title}}
            </template>
        </ScopedSlotDemo> -->

        <!-- 动态组件 -->
        <!-- <component :is="NextTickName"/> -->
        
        <!-- 异步组件 -->
        <!-- <FormDemo v-if="showFormDemo"/>
        <button @click="showFormDemo = true">show form demo</button> -->

        <!-- keep-alive -->
        <!-- <KeepAlive/> -->

        <!-- mixin -->
        <MixinDemo/>
    </div>
</template>

<script>
// import CustomVModel from './CustomVModel'
// import NextTick from './NextTick'
// import SlotDemo from './SlotDemo'
// import ScopedSlotDemo from './ScopedSlotDemo'
// import KeepAlive from './KeepAlive'
import MixinDemo from './MixinDemo'

export default {
    components: {
        // CustomVModel
        // NextTick
        // SlotDemo,
        // ScopedSlotDemo,
        // FormDemo: () => import('../BaseUse/FormDemo'),
        // KeepAlive
        MixinDemo
    },
    data() {
        return {
            name: '双越',
            website: {
                url: 'http://imooc.com/',
                title: 'imooc',
                subTitle: '程序员的梦工厂'
            },
            // NextTickName: "NextTick",
            showFormDemo: false
        }
    }
}
</script>

自定义v-model

//index.vue
<template>
    <div>
        <!-- 自定义 v-model -->
        <p>{{name}}</p>
        <CustomVModel v-model="name"/>
    </div>
</template>

<script>
import CustomVModel from './CustomVModel'

export default {
    components: {
        CustomVModel
    },
    data() {
        return {
            name: '双越',
        }
    }
}
</script>
//CustomVModel.vue
<template>
    <!-- 例如:vue 颜色选择 -->
    <input type="text"
        :value="text1"
        @input="$emit('change1', $event.target.value)"
    >
    <!--
        1. 上面的 input 使用了 :value 而不是 v-model
        2. 上面的 change1 和 model.event 要对应起来
        3. text1 属性对应起来
    -->
</template>

<script>
export default {
    model: {
        prop: 'text1', // 对应 props text1
        event: 'change1'
    },
    props: {
        text1: String,
        default() {
            return ''
        }
    }
}
</script>

refs与$nextTick

Vue是异步渲染(原理部分会详细讲解)
data改变之后,DOM不会立刻渲染
$nextTick会在DOM渲染之后被处罚,以获取最新DOM节点

//index.vue
<template>
    <div>
        <!-- nextTick -->
        <NextTick/>
    </div>
</template>

<script>
import NextTick from './NextTick'

export default {
    components: {
        NextTick
    },
    data() {
        return {
        }
    }
}
</script>
//NextTick.vue
<template>
  <div id="app">
    <ul ref="ul1">
        <li v-for="(item, index) in list" :key="index">
            {{item}}
        </li>
    </ul>
    <button @click="addItem">添加一项</button>
  </div>
</template>

<script>
export default {
  name: 'app',
  data() {
      return {
        list: ['a', 'b', 'c']
      }
  },
  methods: {
    addItem() {
        this.list.push(`${Date.now()}`)
        this.list.push(`${Date.now()}`)
        this.list.push(`${Date.now()}`)

        // 1. 异步渲染,$nextTick 待 DOM 渲染完再回调
        // 3. 页面渲染时会将 data 的修改做整合,多次 data 修改只会渲染一次
        this.$nextTick(() => {
          // 获取 DOM 元素
          const ulElem = this.$refs.ul1
          // eslint-disable-next-line
          console.log( ulElem.childNodes.length ) //没有$nextTick返回3,6,有返回6,9
        })
    }
  }
}
</script>

slot

基本使用

//index.vue
<template>
    <div>
 		<!-- slot -->
        <SlotDemo :url="website.url">
            {{website.title}}
        </SlotDemo>
    </div>
</template>

<script>
import SlotDemo from './SlotDemo'

export default {
    components: {
        SlotDemo
    },
    data() {
        return {
             website: {
                url: 'http://imooc.com/',
                title: 'imooc',
                subTitle: '程序员的梦工厂'
            },
        }
    }
}
</script>
//SlotDemo.vue
<template>
    <a :href="url">
        <slot>
            默认内容,即父组件没设置内容时,这里显示
        </slot>
    </a>
</template>

<script>
export default {
    props: ['url'],
    data() {
        return {}
    }
}
</script>

作用域插槽

//index.vue
<template>
    <div>
    	<ScopedSlotDemo :url="website.url">
            <template v-slot="slotProps">
                {{slotProps.slotData.title}}
            </template>
        </ScopedSlotDemo>
    </div>
</template>

<script>
import ScopedSlotDemo from './ScopedSlotDemo'

export default {
    components: {
        ScopedSlotDemo
    },
    data() {
        return {
             website: {
                url: 'http://imooc.com/',
                title: 'imooc',
                subTitle: '程序员的梦工厂'
            },
        }
    }
}
</script>
//ScopedSlotDemo.vue
<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>

具名插槽

image.png

动态组件

:is="component-name"用法
需要根据数据,动态渲染的场景,即组件类型不确定

image.png 比如一个新闻详情页,可能包含text、image、video组件,而且排序展示可能不一致

//index.vue
<template>
    <div>
   <!-- 动态组件 -->
       <!-- <component :is="NextTickName"/> -->
       <div v-for="(val,key) in newsData" :key="key">
			<component :is="val.type"/>
	   </div>
    </div>
</template>

<script>
import ScopedSlotDemo from './ScopedSlotDemo'

export default {
    components: {
        ScopedSlotDemo
    },
    data() {
        return {
            //NextTickName: "NextTick",
            newsData:[
			1:{type:'text},
			2:{type:'text},
			3:{type:'image},
		]
        }
    }
}
</script>

异步组件

import()函数
按需加载,异步加载大组件

//index.vue
<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>

keep-alive

缓存组件
频繁切换,不需要重复渲染
Vue常见性能优化

//index.vue
<template>
    <div>
  		<!-- keep-alive -->
        <KeepAlive/>
    </div>
</template>

<script>
import KeepAlive from './KeepAlive'
export default {
    components: {
       KeepAlive
    },
    data() {
        return {
             showFormDemo: false
        }
    }
}
</script>
//KeepAlive.vue
<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>
//KeepAliveStateA.vue
<template>
    <p>state A</p>
</template>

<script>
export default {
    mounted() {
        // eslint-disable-next-line
        console.log('A mounted')
    },
    destroyed() {
        // eslint-disable-next-line
        console.log('A destroyed')
    }
}
</script>
//KeepAliveStateB.vue
<template>
    <p>state B</p>
</template>

<script>
export default {
    mounted() {
        // eslint-disable-next-line
        console.log('B mounted')
    },
    destroyed() {
        // eslint-disable-next-line
        console.log('B destroyed')
    }
}
</script>
//KeepAliveStateC.vue
<template>
    <p>state C</p>
</template>

<script>
export default {
    mounted() {
        // eslint-disable-next-line
        console.log('C mounted')
    },
    destroyed() {
        // eslint-disable-next-line
        console.log('C destroyed')
    }
}
</script>

点击A-B-C-A-B-C
未加keep-alive前 image.png

加keep-alive后,离开不会destroyed,再次加载不会mounted
image.png

mixin

多个组件有相同的逻辑,抽离出来
mixin并不是完美的解决方案,会有一些问题
Vue 3 提出的Composition API旨在解决这些问题

//MixinDemo.vue 
<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>
//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)
    }
}

mixin问题

变量来源不明确,不利于阅读
多mixin可能会造成命名冲突
mixin和组件可能出现多对多的关系,复杂度较高

  1. 组件 的 data, methods 优先级高于mixin data, methods 优先级
  2. 生命周期函数,先执行 mixin 里面的,再执行组件里面的
  3. 组件的 自定义属性 优先级高于 mixin 自定义属性 优先级

6.Vuex使用

面试考点并不多(因为熟悉Vue之后,vuex没有难度)
但基本概念,基本使用和API必须掌握
可能会考察state的数据结构设计(后面会讲)

Vuex基本概念

state
getters
action
mutation
异步操作在Actions里进行

用于Vue组件

dispatch
commit
mapState
mapGetters
mapActions
mapMutations

Vuex 是专门为 Vue.js 设计的状态管理库,简单地说就是采用全局单例模式,将组件的共享状态抽离出来管理,使组件树中的每一个位置都可以获取共享的状态(变量)或者触发行为。实现响应式的全局变量

state–状态,在store实例中注册state;在组件中使用store.state访问

getters–类似计算属性,对store中某个属性相同的处理操作抽出出来,做了一个公共的处理,组件中通过store.getters调用

action–异步更改状态,参数有context和payload,context.state获取store变量,context.commit触发mutation,提交mutation去修改state,组件中使用this.store.dispatch调用

mutation–更改store中状态的唯一方法,参数有state和payload,不能包含异步操作,组件中使用this.$store.commit调用

辅助函数:mapState、mapMutations、mapGetters、mapActions,它们的使用我们可以配合ES6的展开运算符将其与局部计算属性或方法混合使用

 import {mapState、mapMutations、mapGetters、mapActions} from ‘vuex’
compoted:{
...mapState({})
}
compoted:{
...mapGetters({})
}
methods:{
...mapActions({})
}
methods:{
...mapMutations({})
}

image.png

7.Vue-router使用

面试考点并不多(前提是熟悉Vue)
路由模式(hash、H5 history)
路由配置(动态路由、懒加载)

Vue-router路由模式

hash模式(默认),如abc.com/#/user/10
H5 history模式,如abc.com/user/20
后者需要server端支持,因此无特殊需求可选择前者

image.png H5 history模式404不会跳转,需要配置404情况

image.png

image.png

Vue-router路由配置

image.png

image.png

四、Vue原理

Vue原理(大厂必考)

面试为何会考察原理

面试中如何考察?以何种方式

考察重点,而不是考察细节,掌握好2/8原则

和使用相关联的原理,例如vdom、模板渲染

整体流程是否全面?热门技术是否有深度?

Vue原理包括哪些?

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

如何理解MVVM

组件化基础

“很久以前”就有组件化

asp jsp php已经有组件化了

nodejs中也有类似的组件化 image.png

image.png

数据驱动视图(MVVM,setState)

传统组件,只是静态渲染,更新还要依赖于操作DOM
数据驱动视图–Vue MVVM
数据驱动视图–React setState(暂时先不看)

Vue MVVM

image.png image.png

总结

组件化
数据驱动视图
MVVM

监听data变化的核心API是什么

Vue响应式

组件data的数据一旦变化,立刻触发视图的更新
实现数据驱动视图的第一步
考察Vue原理的第一题(vue响应式的原理是什么?)
核心API-Object.defineProperty
如何实现响应式,代码演示
Object.defineProperty的一些缺点(Vue3.0启用Proxy)

1.png

Proxy有兼容性问题

Proxy兼容性不好,且无法polyfill
Vue2.x还会存在一段时间,所以都得学
Vue3.0相关知识,下一章讲,这里只是先提一下

Object.defineProperty基本用法

image.png

Object.defineProperty实现响应式

监听对象,监听数组
复杂对象,深度监听
几个缺点

如何深度监听data变化、数组变化

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) // 监听数组

总结

基础API-Object.defineProperty
如何监听对象(深度监听),监听数组
Object.defineProperty的缺点

虚拟DOM-面试里的网红

虚拟DOM(Virtual DOM)和diff

vdom是实现vue和React的重要基石
diff算法是vdom中最核心、最关键的部分
vdom是一个热门话题,也是面试中的热门话题
DOM操作非常耗费性能
以前用jQuery,可以自行控制DOM操作的时机,手动调整
Vue和React是数据驱动视图,如何有效控制DOM操作?

解决方案-vdom

有了一定复杂度,想减少计算次数比较难
能不能把计算,更多的转移为JS计算?因为JS执行速度很快
vdom-用JS模拟DOM结构,计算出最小的变更,操作DOM

image.png

通过snabbdom学习vdom

简洁强大的vdom库,易学易用
Vue参考它实现的vdom和diff
github.com/snabbdom/sn…
Vue3.0重写了vdom的代码,优化了性能
但vdom的基本理念不变,面试考点也不变
React vdom具体实现和Vue也不同,但不妨碍统一学习

用过虚拟DOM吗?

snabbdom重点总结

h函数
vnode数据结构
patch函数

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="container"></div>
    <button id="btn-change">change</button>

    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
    <script src="./demo1.js"></script>
</body>
</html>

const snabbdom = window.snabbdom

// 定义 patch
const patch = snabbdom.init([
    snabbdom_class,
    snabbdom_props,
    snabbdom_style,
    snabbdom_eventlisteners
])

// 定义 h
const h = snabbdom.h

const container = document.getElementById('container')

// 生成 vnode
const vnode = h('ul#list', {}, [
    h('li.item', {}, 'Item 1'),
    h('li.item', {}, 'Item 2')
])
patch(container, vnode)

document.getElementById('btn-change').addEventListener('click', () => {
    // 生成 newVnode
    const newVnode = h('ul#list', {}, [
        h('li.item', {}, 'Item 1'),
        h('li.item', {}, 'Item B'),
        h('li.item', {}, 'Item 3')
    ])
    patch(vnode, newVnode)
})
//table-without-vdom.html 
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="container"></div>
    <button id="btn-change">change</button>

    <script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script>
    <script type="text/javascript">
        const data = [
            {
                name: '张三',
                age: '20',
                address: '北京'
            },
            {
                name: '李四',
                age: '21',
                address: '上海'
            },
            {
                name: '王五',
                age: '22',
                address: '广州'
            }
        ]

        // 渲染函数
        function render(data) {
            const $container = $('#container')

            // 清空容器,重要!!!
            $container.html('')

            // 拼接 table
            const $table = $('<table>')

            $table.append($('<tr><td>name</td><td>age</td><td>address</td>/tr>'))
            data.forEach(item => {
                $table.append($('<tr><td>' + item.name + '</td><td>' + item.age + '</td><td>' + item.address + '</td>/tr>'))
            })

            // 渲染到页面
            $container.append($table)
        }

        $('#btn-change').click(() => {
            data[1].age = 30
            data[2].address = '深圳'
            // re-render  再次渲染
            render(data)
        })

        // 页面加载完立刻执行(初次渲染)
        render(data)

    </script>
</body>
</html>
//table-with-vdom.html 
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="container"></div>
    <button id="btn-change">change</button>

    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
    <script type="text/javascript">
        const snabbdom = window.snabbdom
        // 定义关键函数 patch
        const patch = snabbdom.init([
            snabbdom_class,
            snabbdom_props,
            snabbdom_style,
            snabbdom_eventlisteners
        ])

        // 定义关键函数 h
        const h = snabbdom.h

        // 原始数据
        const data = [
            {
                name: '张三',
                age: '20',
                address: '北京'
            },
            {
                name: '李四',
                age: '21',
                address: '上海'
            },
            {
                name: '王五',
                age: '22',
                address: '广州'
            }
        ]
        // 把表头也放在 data 中
        data.unshift({
            name: '姓名',
            age: '年龄',
            address: '地址'
        })

        const container = document.getElementById('container')

        // 渲染函数
        let vnode
        function render(data) {
            const newVnode = h('table', {}, data.map(item => {
                const tds = []
                for (let i in item) {
                    if (item.hasOwnProperty(i)) {
                        tds.push(h('td', {}, item[i] + ''))
                    }
                }
                return h('tr', {}, tds)
            }))

            if (vnode) {
                // re-render
                patch(vnode, newVnode)
            } else {
                // 初次渲染
                patch(container, newVnode)
            }

            // 存储当前的 vnode 结果
            vnode = newVnode
        }

        // 初次渲染
        render(data)


        const btnChange = document.getElementById('btn-change')
        btnChange.addEventListener('click', () => {
            data[1].age = 30
            data[2].address = '深圳'
            // re-render
            render(data)
        })

    </script>
</body>
</html>

vdom总结

用JS模拟DOM结构(vnode)
新旧vnode对比,得出最小的更新范围,最后更新DOM
数据驱动视图的模式下,有效控制DOM操作

虚拟DOM-diff算法概述

diff算法是vdom中最核心、最关键的部分
diff算法能在日常使用vue React中体现出来(如key)
diff算法是前端热门话题,面试“宠儿”
diff即对比,是一个广泛的感念,如linux diff命令、git diff等
两个js对象也可以做diff,如github.com/cujojs/jiff
两棵树做diff,如这里的vdom diff

image.png

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

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

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

只比较同一层级,不跨级比较
tag不相同,则直接删掉重建,不再深度比较
tag和key,两者都相同,则认为是相同节点,不再深度比较

image.png

image.png

深入diff算法源码

生成vnode

h函数

patch函数

执行pre hook
第一个参数不是vnode-创建一个空的vnode(emptyNodeAt),关联到这个DOM元素
判断vnode是否相同(sameVnode)-key和sel都相等
相同执行patchVnode
不相同,直接删掉重建

patchVnode函数(vnode对比)

  • 执行prepatch hook(生命周期的钩子)
  • 设置vnode.elem
  • vnode.text===undefined(vnode.children一般有值)
 新旧都有children,updateChildren;
 新children有,旧children无(旧text有),清空text,添加children(addVnodes);
 旧children有,新children无,移除children(removeVnodes);
 旧text有,清空

vnode.text!==undefined(vnode.children一般无值)

新旧text不一样,移除旧children,设置新text\

updateChildren函数

image.png

  • 开始和开始对比
    patchVnode()
    累加累减

  • 结束和结束对比
    patchVnode()
    累加累减

  • 开始和结束对比
    patchVnode()
    累加累减

  • 结束和开始对比
    patchVnode()
    累加累减

  • 以上四个都未命中
    拿新节点key,能否对应上oldCh中的某个节点的key
    没对应上,New element
    对应上,拿到对应上key的节点,判断sel是否相等,不相等New element,相等patchVnode()

image.png

image.png 不使用key全部删掉然后插入,使用key直接移动过来,不用做销毁然后重新渲染的过程

虚拟DOM-考点总结和复习

diff算法总结

patchVnode
addVnodes removeVnodes
updateChildren(key的重要性)

vdom和diff-总结

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

模板编译前置知识点-with语法

模板编译

模板是vue开发中最常用的部分,即与使用相关联的原理
它不是html,有指令、插值、JS表达式,到底是什么
面试不会直接问,但会通过“组件渲染和更新过程”考察
前置知识:JS的with语法
vue template complier将模板编译为render函数
执行render函数生成vnode

with语法

改变{}内自由变量的查找规则,当做obj属性来查找
如果找不到匹配的obj属性,就会报错
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) //会报错!!!
}

vue模板被编译成什么?

模板不是html,有指令、插值JS表达式,能实现判断、循环
html是标签语言,只有JS才能实现判断、循环(图灵完备的:能实现顺序执行、判断、循环)
因此,模板一定是转换为某种JS代码,即编译模板

const compiler = require('vue-template-compiler')

// 插值
// const template = `<p>{{message}}</p>`
// with(this){return _c('p',[_v(_s(message))])}
// with(this){return createElement('p',[createTextVNode(toString(message))])}
//this->new Vue({....})
// 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,会在开发环境下编译模板(重要)

vue组件中使用render代替template

image.png

讲完模板编译,再讲这个render,就比较好理解了
在有些复杂情况中,不能用template,可以考虑用render
React一直都用render(没有模板),和这里一样

总结

with语法
模板到render函数,再到vnode,再到渲染和更新
vue组件可以用render代替template

回顾和复习已学的知识点

组件渲染/更新过程

一个组件渲染到页面,修改data触发更新(数据驱动视图)
其背后原理是什么,需要掌握哪些要点
考察对流程了解的全面程度

回顾学过的知识

响应式:监听data属性getter setter(包括数组)
模板编译:模板到render函数,再到vnode
vdom:patch(elem,vnode)和patch(vnode,newVnode)

组件渲染/更新过程

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

vue组件是如何渲染和更新的

初次渲染过程

  • Step1:解析模板为 render 函数(这步操作或在开发中通过 vue-loader 已完成)
  • Step2:触发响应式,监听 data 属性 getter 和 setter(下一步执行 render 函数可能会用到 getter)
  • Step3:执行 render 函数,生成 vnode,渲染节点 patch(elem, vnode)

执行render函数会触发getter

<p>{{message}}</p>
<script>
export default {
	data(){
		return {
			message:'hello',//会触发get
			city:'北京'//不会触发get,因为模板没用到,即和视图没关系
		}
	}
}
</script>

更新过程

  • Step1:修改 data,触发 setter(前提是该 data 此前在 getter 中已被监听,即模板中被引用的 data)
  • Step2:重新执行 render 函数,生成 newVnode
  • Step3:更新节点 patch(vnode, newVnode)

其中 vnode 和 newVnode 的最小差异由 patch 的 diff 算法计算。

完整流程图

组件渲染与更新的完整流程图如下所示:

  • 黄色方框为 render 函数(此时模板已经编译完),它会生成 vnode(绿色 Virtual DOM Tree)。
  • 黄色方框在执行 render 时,会触发(Touch)紫色圆圈(Data)里面的 getter。
  • 紫色圆圈(Data)里的 getter 触发时,会收集依赖,模板里哪个变量的 getter 被触发了,就会将相应变量观察起来(蓝色圆圈 Watcher)
  • 一旦修改了 Data,就会通知 Watcher,如果修改的 data 是之前作为依赖被观察的,则重新触发渲染(re-render)。

image.png

异步渲染

回顾$nextTick
汇总data的修改,一次性更新视图
减少DOM操作次数,提高性能

// 异步渲染
 methods: {
    addItem() {
        this.list.push(`${Date.now()}`)
        this.list.push(`${Date.now()}`)
        this.list.push(`${Date.now()}`)

        // 1. 异步渲染,$nextTick 待 DOM 渲染完再回调
        // 3. 页面渲染时会将 data 的修改做整合,多次 data 修改只会渲染一次
        this.$nextTick(() => {
          // 获取 DOM 元素
          const ulElem = this.$refs.ul1
          // eslint-disable-next-line
          console.log( ulElem.childNodes.length )
        })
    }
  }

总结

渲染和响应式的关系
渲染和模板编译的关系
渲染和vdom的关系

如何用JS实现hash路由

前端路由原理

稍微复杂一点的SPA,都需要路由
vue-router也是vue全家桶的标配之一
属于“和日常使用相关联的原理”,面试常考

回顾vue-router的路由模式
hash
H5 history image.png

hash的特点

hash变化会触发网页跳转,即浏览器的前进、后退
hash变化不会刷新页面,SPA必需的特点
hash永远不会提交到server端(前端自生自灭)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>hash test</title>
</head>
<body>
    <p>hash test</p>
    <button id="btn1">修改 hash</button>

    <script>
        // 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'
        })
    </script>
</body>
</html>

如何用JS实现H5 history路由

H5 history

用url规范的路由,但跳转不刷新页面
history.pushState
window.onpopstate

正常页面浏览

github.com/xxx 刷新页面
github.com/xxx/yyy 刷新页面
github.com/xxx/yyy/zzz 刷新页面

改造成H5 history模式

github.com/xxx 刷新页面
github.com/xxx/yyy 前端跳转,不刷新页面
github.com/xxx/yyy/zzz 前端跳转,不刷新页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>history API test</title>
</head>
<body>
    <p>history API test</p>
    <button id="btn1">修改 url</button>

    <script>
        // 页面初次加载,获取 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
    </script>
</body>
</html>

总结

hash–window.onhashchange
H5 history–history.pushState和window.onpopstate
H5 history需要后端支持

两者选择

to B的系统推荐用hash,简单易用,对url规范不敏感
to C的系统,可以考虑选择H5 history,但需要服务端支持
能选择简单的,就别用复杂的,要考虑成本和收益

vue原理-考点总结和复习

组件化

组件化的历史
数据驱动视图
MVVM

响应式

Object.defineProperty
监听对象(深度),监听数组
Object.defineProperty的缺点(3个缺点)(Vue3用Proxy,后面会讲)

vdom和diff

应用背景
vnode结构
snabbdom使用:vnode h patch

模板编译

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

渲染过程

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

前端路由

hash
H5 history
两者对比(如何选择)

五、Vue 面试真题演练

1.v-show和v-if的区别

  • v-if 在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点。
  • v-show 会被编译成指令,条件不满足时控制样式将对应节点隐藏 (display:none)

使用场景

  • v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景
  • v-show 适用于需要非常频繁切换条件的场景

2.为何在v-for中用key

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


如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速
更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。
更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快 相关代码如下

// 判断两个vnode的标签和key是否相同 如果相同 就可以认为是同一节点就地复用
function isSameVnode(oldVnode, newVnode) {
  return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key;
}

// 根据key来创建老的儿子的index映射表  类似 {'a':0,'b':1} 代表key为'a'的节点在第一个位置 key为'b'的节点在第二个位置
function makeIndexByKey(children) {
  let map = {};
  children.forEach((item, index) => {
    map[item.key] = index;
  });
  return map;
}
// 生成的映射表
let map = makeIndexByKey(oldCh);

3.描述vue组件生命周期(父子组件)

单组件:

  • beforeCreate 在实例初始化之后,数据观测observer 和event、watcher事件配置之前被调用
  • created 实例已经创建完成,在这一步,以下配置被完成
    • 数据观测
    • 属性和方法的运算
    • watch/event时间回调
    • $el尚未生成
  • beforeMount 在挂载之前被调用,render尚未被调用
  • mounted el被新创建的vm.$el替换,并挂载到实例上去之后调用
  • beforeUpdate 数据更新时,被调用,发生在虚拟Dom重新渲染和打补丁之前
  • update 由于数据更改导致的虚拟Dom重新渲染和打补丁,在这之后调用
  • beforeDestroy 实例销毁之前调用
  • destroyed 实例销毁之后调用,调用后Vue实例的所有东西都会被解绑,所有的事件监听会被移除,子实例被销毁,该钩子在服务端渲染期间不被调用
  • keep-alive(activated & deactivated)

vue 的父组件和子组件生命周期钩子函数执行顺序?

  • 加载渲染过程
    父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
  • 子组件更新过程
    父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
  • 父组件更新过程
    父 beforeUpdate -> 父 updated
  • 销毁过程
    父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

4.vue组件如何通讯(常见)

  • props
  • $emit
  • $attr
  • $listener
  • provide inject (隔代通信)
  • parentparent children
  • vuex

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

组件渲染与更新

Vue 原理的三大模块分别为响应式、vdom 和模板编译,前面已经分别学习过,现在通过总结渲染过程来将它们串起来回顾。

初次渲染过程

  • Step1:解析模板为 render 函数(这步操作或在开发中通过 vue-loader 已完成)
  • Step2:触发响应式,监听 data 属性 getter 和 setter(下一步执行 render 函数可能会用到 getter)
  • Step3:执行 render 函数,生成 vnode,渲染节点 patch(elem, vnode)

更新过程

  • Step1:修改 data,触发 setter(前提是该 data 此前在 getter 中已被监听,即模板中被引用的 data)
  • Step2:重新执行 render 函数,生成 newVnode
  • Step3:更新节点 patch(vnode, newVnode)

其中 vnode 和 newVnode 的最小差异由 patch 的 diff 算法计算。

完整流程图

组件渲染与更新的完整流程图如下所示:

  • 黄色方框为 render 函数(此时模板已经编译完),它会生成 vnode(绿色 Virtual DOM Tree)。
  • 黄色方框在执行 render 时,会触发(Touch)紫色圆圈(Data)里面的 getter。
  • 紫色圆圈(Data)里的 getter 触发时,会收集依赖,模板里哪个变量的 getter 被触发了,就会将相应变量观察起来(蓝色圆圈 Watcher)
  • 一旦修改了 Data,就会通知 Watcher,如果修改的 data 是之前作为依赖被观察的,则重新触发渲染(re-render)。

image.png

6.双向数据绑定v-model的实现原理

简单的说,就是 :value 和 @input 的结合使用,v-model就是他们两个的语法糖。
input元素的value - this.name
绑定input事件 this.name = $event.target.value
data更新触发re-render


v-model 只是语法糖而已
v-model 在内部为不同的输入元素使用不同的 property 并抛出不同的事件:

  • text 和 textarea 元素使用 value property 和 input 事件;
  • checkbox 和 radio 使用 checked property 和 change 事件;
  • select 字段将 value 作为 prop 并将 change 作为事件 在普通标签上
    <input v-model="sth" />  //这一行等于下一行
    <input v-bind:value="sth" v-on:input="sth = $event.target.value" />

在组件上

<currency-input v-model="price"></currentcy-input>
<!--上行代码是下行的语法糖
 <currency-input :value="price" @input="price = arguments[0]"></currency-input>
-->

<!-- 子组件定义 -->
Vue.component('currency-input', {
 template: `
  <span>
   <input
    ref="input"
    :value="value"
    @input="$emit('input', $event.target.value)"
   >
  </span>
 `,
 props: ['value'],
})

7.对MVVM的理解

image.png

8.computed 和 watch 的区别和运用的场景

computed 是计算属性,依赖其他属性计算值,并且 computed 的值有缓存,只有当计算值变化才会返回内容,它可以设置 getter 和 setter。

watch 监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作。

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

答案引用:juejin.cn/post/696122…

9.为何组件data必须是一个函数

因为js本身的特性带来的,如果 data 是一个对象,那么由于对象本身属于引用类型,当我们修改其中的一个属性时,会影响到所有Vue实例的数据。如果将 data 作为一个函数返回一个对象,那么每一个实例的 data 属性都是独立的,不会相互影响了

10.ajax请求应该放在哪个生命周期

可以在钩子函数 created、beforeMount、mounted 中进行异步请求,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。

如果异步请求不需要依赖 Dom 推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面 loading 时间;
  • ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;

作者:Big shark@LX
链接:juejin.cn/post/696122… 来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

11.如何将组件所有props传递给子组件

 $props:<User v−bind="$props" />

12.如何自己实现v-model

image.png

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

mixin
以及mixin的一些缺点

mixin的缺点:
变量来源不明确,不利于阅读
多mixin可能会造成命名冲突
mixin和组件可能出现多对多的关系,复杂度较高

mixin和组件的顺序?

  1. 组件 的 data, methods 优先级高于mixin data, methods 优先级
  2. 生命周期函数,先执行 mixin 里面的,再执行组件里面的
  3. 组件的 自定义属性 优先级高于 mixin 自定义属性 优先级

14.何时要使用异步组件

加载大组件
路由异步加载

15.何时需要使用keep-alive

缓存组件,不需要重复渲染
如多个静态tab页的切换
优化性能
juejin.cn/post/704307…

16.何时需要使用beforeDestory

解绑自定义事件 event.$off
清除定时器
解绑自定义的DOM时间,如window scroll等

17.什么是作用域插槽

image.png

18.Vuex中action和mutation有何区别

juejin.cn/post/696122…
action中处理异步,mutation不可以
mutation做原子操作
action可以整合多个mutation

19.Vue-router常用的路由模式

hash默认
H5 history(需要服务端支持)

vuerouter的两种模式的区别

  • vue-router中有三种模式,分别是hash、history、abstract
  • abstract在不支持浏览器的API换景使用
  • hash模式兼容性好,但是不美观,不利于SEO
  • history美观,historyAPI+popState,但是刷新会出现404
    作者:海明月
    链接:juejin.cn/post/704307…

vue-router 中常用的路由模式实现原理

hash 模式

  1. location.hash 的值实际就是 URL 中#后面的东西 它的特点在于:hash 虽然出现 URL 中,但不会被包含在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。
  2. 可以为 hash 的改变添加监听事件

window.addEventListener("hashchange", funcRef, false);

history 模式

利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。

这两个方法应用于浏览器的历史记录站,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。

特点:虽然美观,但是刷新会出现 404 需要后端进行配置

作者:Big shark@LX
链接:juejin.cn/post/696122…

20.如何配置Vue-router异步加载

image.png

21.请用vnode描述一个DOM结构

image.png

22.监听data变化的核心API是什么

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

23.Vue如何监听数组变化

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

24.请描述响应式原理

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

25.diff算法的时间复杂度

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

26.简述diff算法过程

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

27.Vue为何是异步渲染,$nextTick何用

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

nextTick 中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法

28.Vue常见性能优化方式

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


  • 对象层级不要过深,否则性能就会差

  • 不需要响应式的数据不要放到 data 中(可以用 Object.freeze() 冻结数据)

  • v-if 和 v-show 区分使用场景

  • computed 和 watch 区分使用场景

  • v-for 遍历必须加 key,key 最好是 id 值,且避免同时使用 v-if

  • 大数据列表和表格性能优化-虚拟列表/虚拟表格

  • 防止内部泄漏,组件销毁后把全局变量和事件销毁

  • 图片懒加载

  • 路由懒加载

  • 第三方插件的按需引入

  • 适当采用 keep-alive 缓存组件

  • 防抖、节流运用

  • 服务端渲染 SSR or 预渲染

六、vue3预学习

Vue3要来了Vue2就要过时了吗

Vue3

Vue3尚未发布,还在开发中
面试会考察候选人对新技术的关注程度(Vue太热门)
新版本发布之后,再做补充

Vue2.x马上就要过时了吗

Vue3从正式发布到推广开发,还需要一段时间
Vue2.x应用范围非常广,有大量项目需要维护、升级
Proxy存在浏览器兼容性问题,且不能polyfill

Vue3升级内容

全部用ts重写(响应式、vdom、模板编译等)
性能提升,代码量减少
会调整部分API

Proxy实现响应式

回顾Object.defineProperty
Proxy实现响应式
两者对比

Object.defineProperty的缺点

深度监听需要一次性递归
无法监听新增属性/删除属性(Vue.set Vue.delete)
无法原生监听数组,需要特殊处理

Proxy实现响应式

基本使用
Reflect
实现响应式

Proxy基本使用

image.png

// 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 // 是否删除成功
    }
})

image.png

image.png

Reflect作用

和Proxy能力一一对应
规范化、标准化、函数式
替代掉Object上的工具函数 image.png

image.png

Vue3用Proxy实现响应式

Proxy实现响应式

深度监听,性能更好
可监听新增/删除属性
可监听数组变化

// 创建响应式
function reactive(target = {}) {
    if (typeof target !== 'object' || target == null) {
        // 不是对象或数组,则返回
        return target
    }

    // 代理配置
    const proxyConf = {
        get(target, key, receiver) {
            // 只处理本身(非原型的)属性
            const ownKeys = Reflect.ownKeys(target)
            if (ownKeys.includes(key)) {
                console.log('get', key) // 监听
            }
    
            const result = Reflect.get(target, key, receiver)
        
            // 深度监听
            // 性能如何提升的?
            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)

性能如何提升的?
只有get时递归,而且不是一次性递归,获取proxyData.info时只会到city和a这一层,不会到b、c、d、e这一层

总结

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