前端面试知识点梳理——Vue

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

框架综合应用
基于Vue设计一个购物车(组件结构,vuex state数据结构)

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

如何应对面试题

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

Vue基础

Vue使用

基本使用,组件使用 ———— 常用,必须会

指令、插值

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

computed 和 watch

  • computed 有缓存,data不变则不会重新计算
  • watch 如何深度监听(默认浅监听)
data(){
    return {
        name:'wchao',
        info:{
            city:'北京'
        }
    }
}
watch:{
    name(oldVal,val){
        console.log('watch info' , oldVal, val)
    }
    info:{
        handler(oldVal,val){
            console.log('watch info' , oldVal, val) 
        },
        deep:true
    }
}
  • watch 监听引用类型,指针相同,拿不到oldVal

class 和 style

  • 使用动态属性
  • 使用驼峰式写法
<template>
    <div>
        <p :class="{black:isBlack,yellow:isYellow}"></p>
        <p :class="[black,yellow]"></p>
        <p :style="styleData"></p>
    </div>
</template>
<script>
    export default {
        data(){
            return{
                isBlack:true,
                isYellow:true,
                black:'black',
                yellow:'yellow',
                styleData:{
                    fontSize:'40px',
                    color:'red'
                }
            }
        }
    }
</script>

条件渲染

  • v-if v-else的用法,可使用变量,也可以使用 === 表达式
  • v-if 和 v-show的区别
    • v-if 不会提前渲染
    • v-show 全部都渲染,隐藏
    • 频繁切换 建议使用v-show
  • v-if 和 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-forv-if 的计算优先级高,这意味着 v-if 将分别重复运行于每个 v-for 循环中
    • v-if 一般放于容器上
<template>
    <div>
        <p>遍历数组</p>
        <ul>
            <li v-for="(item,index) in listArr" :key="item.id">
                {{item}} - {{item.id}} - {{item.title}}
            </li>
        </ul>
        <p>遍历对象</p>
        <ul>
            <li v-if="flag" 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:'b',title:'标题2'},
                    {id:'c',title:'标题2'}
                ],
                listObj:{
                    a:{ title:'标题1' },
                    b:{ title:'标题2' },
                    c:{ title:'标题3' },
                }
            }
        }
    }
</script>

事件

  • event参数,自定义参数
  • 事件修饰符
    <!-- 阻止单击事件继续传播 -->
    <a v-on:click.stop="doThis"></a>
    <!-- 提交事件不在重载页面 -->
    <form v-on:submit.prevent="onSubmit"></form>
    <!-- 修饰符可以串联 -->
    <a v-on:click.stop.prevent="doThis"></a>
    <!-- 只有修饰符 -->
    <form v-on:submit.prevent></form>
    <!-- 添加事件监听器时使用事件捕获模式 -->
    <!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
    <div v-on:click.capture="doThis">....</div>
    <!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
    <!-- 即事件不是从内部元素触发 -->
    <div v-on:click.self="doTh">...</div>
    
  • 按键修饰符
    <!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
    <button @click.ctrl="doThis">A</button>
    <!-- 有且只有 Ctrl 被按下的时候才触发 -->
    <button @click.ctrl.exact="doThis">A</button>
    <!-- 没有任何系统修饰符被按下的时候才触发 -->
    <button @click.exact="doThis">A</button>
    
  • 【观察】事件被绑定到哪里
<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){
                console.log('event',event,event.__proto__.constructor) 
                console.log(event.target)
                console.log(evetn.currentTarget)
                this.num++
                // 1.event是原生的,没有进行任何的装饰
                // 2.事件被挂载到当前元素
            }
            increment2(val, event){
                console.log(event.target)
                this.num = this.num + val
            }
        }
    }
    
</script>

表单

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

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

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

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

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

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

        <p>下拉列表选择(多选) {{selectedList}}</p>
        <select v-model="selectedList" multiple>
            <option disabled value="">请选择</option>
            <option>A</option>
            <option>B</option>
            <option>C</option>
        </select>
    </div>
</template>
<script>
    export default {
        data() {
            return {
                name: '王超',
                age: 18,
                desc: '自我介绍',

                checked: true,
                checkedNames: [],

                gender: 'male',

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

Vue组件使用

  • props 和 $emit
    • 父向子传递数据 props
    • 子向父触发事件 $emit
  • 组件间通讯 - 绑定自定义事件
    • event.$emit('onAddTitle',this.title)
    • event.$on('onAddTitle',this.addTitleHandle)
    • event.$off('onAddTitle',this.addTitleHandle) —— 及时销毁,否则可能造成内存泄露
  • 组件生命周期
    • 生命周期图 生命周期图
    • 单个组件
      • 挂载阶段
        • beforeCreate
        • created —— vue实例初始化完成
        • beforeMount
        • mounted —— 页面渲染完
      • 更新阶段
        • beforeUpdate
        • updated
      • 销毁阶段
        • beforeDestroy
        • destroyed
    • 父子组件
      • 挂载阶段
        • 父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created->子beforeMount -> 子mounted -> 父mounted
      • 更新阶段
        • 父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated
      • 销毁阶段
        • 父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed
<!-- 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() {
        console.log('index created')
    },
    mounted() {
        console.log('index mounted')
    },
    beforeUpdate() {
        console.log('index before update')
    },
    updated() {
        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>
<!-- 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) {
            console.log('on add title', title)
        }
    },
    created() {
        console.log('list created')
    },
    mounted() {
        console.log('list mounted')

        // 绑定自定义事件
        event.$on('onAddTitle', this.addTitleHandler)
    },
    beforeUpdate() {
        console.log('list before update')
    },
    updated() {
        console.log('list updated')
    },
    beforeDestroy() {
        // 及时销毁,否则可能造成内存泄露
        event.$off('onAddTitle', this.addTitleHandler)
    }
}
</script>
// event.js    
//
import Vue from 'vue'
export default new Vue()

高级特性 ———— 不常用,但体现深度

自定义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>

$nextTick

  • Vue 是异步渲染
  • data改变之后,DOM不会立刻渲染
  • $nextTick会在DOM渲染后被触发,以获取最新DOM节点
<!--index.vue -->
<template>
    <div>
        <p>vue 高级特性</p>
        <hr>

       
        <!-- nextTick -->
        <NextTick />
    </div>
</template>

<script>
import NextTick from './NextTick'

export default {
    components: {
       NextTick
    }
}
</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()}`)
        
        // const ulElem = this.$refs.ul1
        // console.log( ulElem.childNodes.length )  // 3 6 9
        
        // 1. 异步渲染,data改变之后,DOM不会立刻渲染
        // 2. $nextTick 待 DOM 渲染完再回调
        // 3. 页面渲染时会将 data 的修改做整合,多次 data 修改只会渲染一次
        this.$nextTick(() => {
          // 获取 DOM 元素
          const ulElem = this.$refs.ul1
          console.log( ulElem.childNodes.length )   // 6 9 12
        })
    }
  }
}
</script>

slot(插槽)

<!-- index.vue -->
<template>
    <div>
        <p>vue 高级特性</p>
        <hr>
        <!-- slot -->
        <!-- <SlotDemo :url="website.url">
            {{website.title}}
        </SlotDemo> -->
        <!-- <ScopedSlotDemo :url="website.url">
            <template v-slot="slotProps">
                {{slotProps.slotData.title}}
            </template>
        </ScopedSlotDemo> -->
    </div>
</template>
<script>
// import SlotDemo from './SlotDemo'
// import ScopedSlotDemo from './ScopedSlotDemo'
export default {
    components: {
        // SlotDemo,
        // ScopedSlotDemo,
    },
    data() {
        return {
            name: '王超',
            website: {
                url: 'http://www.baidu.com/',
                title: '百度',
                subTitle: '程序员的梦工厂'
            },
            showFormDemo: false
        }
    }
}
</script>
  • 基本使用
    <!-- 基本使用 SlotDemo.vue -->
    <template>
        <a :href="url">
            <slot>
                默认内容,即父组件没设置内容时,这里显示
            </slot>
        </a>
    </template>
    
    <script>
    export default {
        props: ['url']
    }
    </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>
  • 具名插槽
<!-- 具名插槽NamedSlot组件 -->
<template>
    <header>
        <slot name="header"></slot>
    </header>
</template>
<NamedSlot>
    <template v-slot:header>
        <h1>将插入header slot中</h1>
    </template>
</NamedSlot>

动态、异步组件

  • 动态组件
    • :is = "component-name" 用法
    • 需要根据数据,动态渲染的场景。即组件类型不确定。
    <!-- index.vue -->
    <template>
        <div>
            <!-- 动态组件 -->
            <component :is="NextTickName"/>
            <div v-for="(item,index) in newsData" :key = "item.id">
                <component :is="item.type"/>
            </div>
        </div>
    </template>
    <script>
    import NextTick from './NextTick'
    export default {
        components: {
            NextTick
        },
        data() {
            return {
                NextTickName: "NextTick",
                newsData:[
                   {
                       id:1
                       type:'text'
                   },
                   {
                       id:2
                       type:'text'
                   },
                   {
                       id:2
                       type:'img'
                   }
                ]
            }
        }
    }
    </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

  • 缓存组件
  • 频繁切换,不需要重复渲染
  • keep-alive 是在Vue框架层级进行js对象的渲染
  • Vue 常见性能优化
<!-- index.vue -->
<template>
    <div>
        <!-- keep-alive -->
        <KeepAlive/>
    </div>
</template>

<script>
import KeepAlive from './KeepAlive'
export default {
    components: {
        KeepAlive
    }
}
</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>
<!-- KeepAliveStageA.vue -->
<template>
    <p>state A</p>
</template>

<script>
export default {
    mounted() {
        console.log('A mounted')
    },
    destroyed() {
        console.log('A destroyed')
    }
}
</script>

<!-- KeepAliveStageB.vue -->
<template>
    <p>state B</p>
</template>

<script>
export default {
    mounted() {
        console.log('B mounted')
    },
    destroyed() {
        console.log('B destroyed')
    }
}
</script>

<!-- KeepAliveStageC.vue -->
<template>
    <p>state C</p>
</template>

<script>
export default {
    mounted() {
        console.log('C mounted')
    },
    destroyed() {
        console.log('C destroyed')
    }
}
</script>

mixin

  • 多个组件有相同的逻辑,抽离出来
  • mixin 并不是完美的解决方案,会有一些问题
    • 变量来源不明确,不利于阅读
    • 多mixin可能会造成命名冲突
    • mixin和组件可能出现多对多的关系,复杂度较高
  • vue3 提供了解决方案
<!-- index.vue -->
<template>
    <div>
        <!-- mixin -->
        <MixinDemo/>
    </div>
</template>

<script>
import MixinDemo from './MixinDemo'
export default {
    components: {
        MixinDemo
    },
    data() {
        return {
            name: '王超',
            website: {
                url: 'http://www.baidu.com/',
                title: '百度',
                subTitle: '程序员的梦工厂'
            },
            // NextTickName: "NextTick",
            showFormDemo: false
        }
    }
}
</script>
<!-- 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() {
        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)
    }
}

周边插件( Vuex 和 Vue-router )使用

Vuex

  • 基本概念
    • state
    • getters
    • actions —— (异步操作)
    • mutations
  • 用于Vue组件
    • dispatch
    • commit
    • mapState
    • mapGetters
    • mapActions
    • mapMutations
  • 图示 Vuex

Vue-router

  • 路由模式

    • hash模式(默认),如http://abc.com/#/user/10
    • H5 history模式,如http://abc.com/user/10
      const router = new VueRouter({
        mode: 'history',
        routes: [...]
      })
      
    • 后者需要server端支持,因此无特殊需求可选择前者
  • 配置路由

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

Vue原理

组件化

组件化基础

  • “很久以前” 就有组件化
    • asp jsp php 已经有组件化了(MVC)
    • nodejs 中也有类似的组件化
  • 数据驱动视图(MVVM,setState)
    • 传统组件,只是静态渲染,更新还要依赖于操作DOM
    • 数据驱动视图 —— Vue MVVM
    • 图示 数据驱动视图

vue响应式

组件data的数据一旦变化,立刻触发视图的更新

实现数据驱动视图的第一步

核心API - Object.defineProperty

  • 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
  • 如何实现响应式
    • 监听的对象,监听数组
    • 复杂对象,深度监听
    // 触发更新视图
    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) // 监听数组
    
    
  • Object.defineProperty的一些缺点(Vue3.0启用Proxy(兼容性不好,且无法使用polyfill))
    • 深度监听,需要递归到底,一次性计算量大
    • 无法监听新增属性/删除属性(Vue.set Vue.delete)
    • 无法原生监听数组,需要特殊处理(创建新对象,把新对象的原型指向数组原型,重新定义扩展数组方法,增加渲染视图方法,并把数组原型this指向新对象,然后在监听方法里增加数组类型判断。)

vdom 和 diff算法

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

虚拟DOM(Virtual DOM)

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

      <-- html DOM片段 xml语言 -->
      <div id="div1" class="container">
          <p>vdom</p>
          <ul style="font-size:20px">
              <li>a</li>
          </ul>
      </div>
      
      // js转换DOM
      {
          tag:"div",
          props:{
              className:'container'
              id:'div1'
          }
          children:[
              {
                  tag:"p",
                  children:"vdom"
              },
              {
                  tag:"ul",
                  props:{
                      style:'font-size:20px'
                  }
                  children:[
                      {
                          tag:"li",
                          children:"a"
                      }
                  ]
              },
          ]
      }
      

diff算法

  • diff算法是vdom中最核心、最关键的部分
  • diff算法能在日常使用vue React中体现出来(如key)
  • diff算法是前端热门话题,面试“宠儿”
  • diff算法概述
    • diff即对比,是一个广泛的概念,如linux diff命令、git diff等
    • 两个js对象也可以做diff,如 github.com/cujojs/jiff
    • 两颗树做diff,如这里的vdom diff
    • 树diff的时间复杂度O(n^3)
        1. 遍历tree1
        1. 遍历tree2
        1. 排序
      • 1000个节点,要计算1亿次,算法不可用
    • 优化时间复杂度到O(n)
      • 只比较同一层级,不跨级比较 优化时间复杂度
      • tag不相同,则直接删掉重建,不在深度比较 tag不相同,则直接删掉重建,不在深度比较
      • tag 和 key,两者都相同,则认为是相同节点,不再深度比较
  • 深入diff算法源码 —— 生成vnode(sel, data, children, text, elm, key)
    • 用h()函数生成vdom,返回vnode方法
    • vnode处理后,返回一个对象
  • 深入diff算法源码 —— patch(oldVnode, vnode, hydrating, removeOnly)
    • 执行 pre hook (生命周期pre)
    • 判断 第一个参数不是 vnode,则创建一个空的 vnode ,关联到这个 DOM 元素
    • 如果相同的 vnode(key 和 sel 都相等),执行patchVnode() ——> vnode对比
    • 如果不同的 vnode ,直接删掉重建
  • 深入diff算法源码 —— patchVnode(oldVnode, vnode, insertedVnodeQueue,ownerArray,index,ownerArray,removeOnly)
    • 新旧都有children ——> updateChildren() // 对比
    • 新children有 旧children没有(旧text有) ——> 添加children,清空text
    • 旧children有 新children没有 ——> 移除children
    • 旧 text 有 ——> 清空text
  • 深入diff算法源码 —— updateChildren(parentElm,oldCh,newCh,insertedVnodeQueue,removeOnly)
    • 第一步、指针处相互比较,旧开始——新开始、旧结束——新结束、旧开始——新结束、旧结束——新开始,如果没有相等继续,如图: 指针处相互比较
    • 第二步、然后拿新节点 key ,能否对应上 oldCh 中的某个节点的 key
      • 没有对应上,就直接创建新的Node插入到 oldStartIdx指针对应的Node(a)之前
      • 对应上了,判断移动到的节点是否与新开始节点相等
        • 相等,记录此时的oldKeyToIdx,将真实的DOM节点移动到oldStartIdx指针对应的节点之前,然后找将oldKeyToidx对应的值改为undefined。 相等
        • 不相等,创建一个新的node节点插入到oldStartIdx指针对应的node(a)之前。 不相等
    • 循环比对,指针移动,直到oldStartIdx>oldEndIdx(老节点遍历完成)或者newStartIdx>newEndIdx(新节点遍历完成),跳出循环,diff结束
  • diff算法总结(如何理解diff算法)
    • diff算法是虚拟DOM(vdom)技术的必然产物:通过新旧虚拟DOM做对比(即diff),将变化的地方更新在真实DOM上;另外,也需要diff高效的执行对比过程,从而降低时间复杂度为O(n)
    • vue2.x中为了降低Watcher粒度,每个组件只有一个Watcher与之对应,只有引入diff才能精确找到发生变化的地方
    • vue中diff执行的时刻是组件实例执行其更新函数时,他会比对上一次渲染结果oldVnode和新的渲染结果newVnode,此过程称为patch
    • diff过程整体遵循深度优先、同层比较的策略;两个节点之间比较会根据他们是否拥有子节点或者文本节点做不同的操作;比较两组子节点是算法的重点,首先假设头尾节点可用相同做4次比对尝试,如果没有找到相同节点才按照通用方式遍历查找,查找结束再按情况处理剩下的节点;借助key通常可以非常精确找到相同节点,因此整个patch过程非常高效。

模板编译

JS的 with 语法

  • 使用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)  // 会报错
}

何为模板编译

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

模板编译编译流程

  • 模板编译为rander函数,执行render函数返回vnode
  • 基于vnode 在执行 patch和diff
  • 使用 webpack vue-loader,会在开发环境下编译模板(重要)

vue组件中使用render代替template

Vue.component('heading',{
    render:function(createElment){
        return createElement(
            'h1',
            [
                createElement('a',{
                    attrs:{
                        name:'headerId',
                        href:'#'
                    }
                },'this is a tag')
            ]
        )
    }
})

渲染过程

组件渲染/更新 过程

组件渲染/更新 过程

  • 初次渲染过程
    • 解析模板为rander函数(或在开发环境已完成,vue-loader)
    • 触发响应式,监听data属性 getter setter
    • 执行render函数,触发getter,生成vnode,patch(elem,vnode)
  • 更新过程
    • 修改dta,触发seter(此前在getter中已被监听)
    • 重新执行render函数,生成newVnode
    • patch(vnode,newVnode)

异步渲染

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

总结

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

前端路由

网页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.port      // '8881'
location.pathname  // '/01-hash.html'
location.search    // '?a=100&b=20'
location.hash      // '#/aaa/bbb'

hash路由

  • hash 变化会触发网页跳转,即浏览器的前进、后退
  • hash 变化不会刷新页面,SPA必须的特点
  • hash 永远不会提交到 server 端(前端自生自灭)
<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>

H5 history

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

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

两者选择

  • to B 的系统推荐用hash,简单易用,对url规范不敏感
  • to c 的系统,可以考虑选择H5 history,但需要服务端支持

Vue面试题

v-if 和 v-for哪个优先级更高?如果两个同时出现,应该怎么优化得到更好的性能

  • v-for优先于v-if被解析(把你怎么知道的告诉面试官)
// 源码中找答案:compiler/codegen/index.js > genElement()
if(el.staticRoot && !e.staticProcessed){
    return ...
}else if(el.for && !el.forProcessed){
    return ...
}else if(el.if && !el.forProcessed){
    return ...
}
...
  • 如果同时出现,每次渲染都会先执行循环在判断条件,无论如果循环都不可避免,浪费了性能
  • 要避免出现这种情况,则在外层嵌套template,在这一层进行v-if判断,然后在内部进行v-for循环
  • 实在避免不了,可用computed计算属性提前过滤列表

Vue组件data为什么必须是个函数而Vue的根实例则没有此限制?

// 源码中找答案:src/core/instance/state.js > initData()
  • 组件需要是函数,根实例可以是对象
  • Vue组件可能存在多个实例,如果使用对象形式定义data,则会导致他们共用一个data对象,那么状态变更将会影响所有组件实例,这是不合理的;采用函数形式定义,在initData时会将其作为工厂函数返回全新data对象,有效规避多实例之间状态污染问题。而在Vue根实例创建过程中则不存在改限制,也是因为根实例只能有一个,不需要担心这种情况

你知道vue中key的作用和工作原理吗?说说你对它的理解

// 源码中找答案:src/core/vdom/patch.js > updateChildren()
  • key的作用主要是为了高效的更新虚拟DOM,其原理是vue在patch过程中通过key可以精准判断两个节点是否是同一个,从而避免频繁更新不同元素,是的整个patch过程更加高效,减少DOM操作量,提高性能。
  • 另外,若不设置key还可能在列表更新时引发一些隐蔽的bug
  • vue中在使用相同标签名元素的过渡切换时,也会使用到key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果。

谈一谈对vue组件化的理解

  • 组件是独立和可复用的代码组织单元。组件系统是Vue核心特性之一,它使开发者使用小型、独立和通常可复用的组件构建大型应用。
  • 组件化开发能大幅提高应用开发效率、测试性、复用性。
  • 组件使用按分类有:页面组件、业务组件、通用组件
  • vue的组件是基于配置的,我们通常编写的组件是组件配置而非组件,框架后续会生成其构造函数,他们基于VueComponent,扩展于Vue
  • vue中常见组件化技术有:属性prop,自定义事件,插槽等,他们主要用于组件通讯、扩展等
  • 合理的划分组件,有助于提升应用性能
  • 组件应该是高内聚、低耦合的
  • 遵循单向数据流的原则

谈一谈对vue设计原则的理解

  • 渐进式javacript框架
  • 易用性
  • 灵活性
  • 高效性

谈谈你对MVC、MVP、MVVM的理解

  • 这三者都是框架模式,它们设计的目标都是为了解决Model层和View层解耦问题。
  • MVC模式(model view Controller)出现较早主要应用在后端,如php,在前端领域的早期也有应用,如Backbone.js。他的优点是分层清晰,缺点是数据流混乱,灵活性带来的维护性问题
  • MVP模式(model view Presenter)是mvc的进化形式,Presenter作为中间层负责MV通信,解决了两者耦合问题,但是P层过去臃肿会导致维护问题。
  • MVVM模式(model view viewmodel)在前端领域有广泛应用,它不仅解决了MV解耦问题,还同时解决了维护两者映射关系的大量繁杂代码和DOM操作代码,在提高开发效率、可读性同时还保持了优越的性能表现。

你了解哪些Vue性能优化方法

  • 路由懒加载
    const router = new VueRouter({
        routes:[
            {
                path:'/foo',
                component:() => import('./Foo.vue')
            }
        ]
    })
    
  • keep-alive缓存页面
<template>
    <div id="app">
        <keep-alive>
            <router-view />
        </keep-alive>
    </div>
</template>
  • 使用v-show复用DOM
<template>
    <div class="cell">
        <!-- 这种情况用v-show复用DOM,比v-if效果好 -->
        <div v-show="value" class="on">11111</div>
        <div v-show="!value" class="off">222222</div>
    </div>
</template>
  • v-for时加key,以及避免和v-if同时使用
<template>
   <ul>
       <li
         v-for = "user in activeUsers"
         :key = "user.id"
       >
       {{ user.name }}
       </li>
   </ul>
</template>
<script>
    export default {
        computed:{
            activeUsers: function(){
                return this.users.filter(function(user){
                    return user.isActive
                })
            }
        }
    }
</script>
  • 事件销毁
created(){
    this.timer = setInterval(this.refresh,2000)
},
beforeDestroy(){
    clearInterval(this.timer)
}
  • 图片懒加载(vue-lazyload)
<img v-lazy="/static/img/1.png">
  • 第三方插件按需引入
import Vue from 'vue'
import { Button,Select } from 'element-ui'
Vue.use(Button)
Vue.use(Select)
  • 无状态的组件标记为函数式组件
<template>
   <div class="cell">
       <div v-if="props.value" class="on"></div>
       <section v-else class="off"></section>
   </div>
</template>
<script>
    export default {
        props:['value']
    }
</script>
  • 子组件分割
<template>
   <div>
      <childcomp /> 
   </div>
</template>
<script>
    export default {
        components:{
            childcomp:{
                methods:{
                    heavy(){
                    //耗时任务
                    }
                    render(h){
                        return h('div',this.heavy())
                    }
                }
            }
        }
    }
</script>
  • 长性能列表的优化
    • 如果列表是纯粹的数据展示,不会有任何改变,就不需要做响应式
    export default {
        data: () => ({
            users:[]
        })
        async created(){
            const users = await axios.get('/api/users')
            this.users = object.freeze(users)
        }
    }
    
    • 如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域内容

描述Vue组件生命周期(父子组件)

  • 见上文梳理

Vue组件如何通讯(常见)

  • 父子组件props 和 this.$emit
  • 自定义事件 event.onevent.on event.off event.$emit
  • vuex

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

  • 执行render函数 触发 getter 收集依赖到 watcher
  • 更新的时候 触发 setter 通知 watcher 触发 re-render 渲染

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

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

对MVVM的理解

computed 有何特点

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

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

  • mounted (整个渲染完成,dom渲染完成)
  • js是单线程,ajax异步获取数据

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

  • $props
  • <User v-bind="$props">

何时需要使用beforeDestory

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

Vuex中action和mutation有何区别

  • action中处理异步,mutation不可以
  • mutation做原子操作
  • action 可以整合多个mutation

请用vnode描述一个DOM结构

{
    tag:'xx',
    props:{
        id:'xx'
    },
    children:[
        tag:'xx',
        children:"xxx"
    ]
}

简述diff算法过程

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

你对Vue3.0的新特性有没有了解

  • 更快
    • 虚拟DOM重写 期待更多的编译时提示来减少运行时开销,使用更有效的代码来创建虚拟节点
      组件快速路径 + 单个调用 + 子节点类型检测
      • 跳过不必要的条件分支
      • JS引擎更容易优化
    虚拟DOM重写
    • 优化slots的生成 vue3中可以单独重新渲染父级和子级
      • 确保实例正确的跟踪依赖关系
      • 避免不必要的父子组件重新渲染
    优化slots的生成
    • 静态树提升 使用静态树提升,这意味着Vue3 的编译器将能够检测到什么是静态的,然后将其提升,从而降低了渲染成本。
      • 跳过修补整棵树,从而降低渲染成本
      • 即使多次出现也能正常工作 静态树提升
    • 静态属性提升 使用静态属性提升,Vue 3 打补丁时将跳过这些属性不会改变的节点,但是孩子还要继续patch 静态属性提升
    • 基于Proxy的响应式系统 Vue 2的响应式系统使用 Object.defineProperty的getter和setter。Vue 3将使用ES2015 Proxy作为其观察机制,这将会带来如下变化:
      • 组件实例初始化的速度提高100%
      • 使用Proxy节省以前一半的内存开销,加快速度,但是存在低浏览器版本的不兼容
      • 为了继续支持IE11,Vue 3将发布一个支持旧观察机制的和新Proxy版本的构建
        企业微信截图_1e58bb75-be05-4fe7-bf46-4533d18606e1.png
  • 更小
    • 通过摇树优化核心库体积
  • 更容易维护
    • TypeScript + 模块化 Vue 3将带来更可维护的源代码。他不仅会使用TypeScript,而且许多包被解耦,更加模块化 企业微信截图_2f356e07-2bb1-47bb-9ad1-5a8caef0048c.png
  • 更加友好
    • 跨平台:编译器核心和运行时核心与平台无关,使得Vue更容易与任何平台(web、Android、IOS)一起使用
  • 更容易使用
    • 改进的TypeScript支持,编辑器能提供强有力的类型检查和错误及警告
    • 更好的调试支持
    • 独立的响应化模块
    • Composition API

Vue项目设计

Vue实现购物车

设想原型图

设想原型图

data数据结构设计

  • 用数据描述所有的内容
  • 数据要结构化,易于程序操作(遍历、查找)
  • 数据要可扩展,以便增加新的功能

组件设计和组件通讯

  • 从功能上拆分层次
  • 尽量让组件原子化
  • 容器组件(只管理数据)& UI组件(只显示视图)

代码演示

<!-- index.vue 容器组件 -->
<template>
    <div>
        <ProductionList :list="productionList"/>
        <hr>
        <CartList
            :productionList="productionList"
            :cartList="cartList"
        />
    </div>
</template>

<script>
import ProductionList from './ProductionList/index'
import CartList from './CartList/index'
import event from './event'
export default {
    components: {
        ProductionList,
        CartList
    },
    data() {
        return {
            productionList: [
                {
                    id: 1,
                    title: '商品A',
                    price: 10
                },
                {
                    id: 2,
                    title: '商品B',
                    price: 15
                },
                {
                    id: 3,
                    title: '商品C',
                    price: 20
                }
            ],
            cartList: [
                {
                    id: 1,
                    quantity: 1 // 购物数量
                }
            ]
        }
    },
    methods: {
        // 加入购物车
        addToCart(id) {
            // 先看购物车中是否有该商品
            const prd = this.cartList.find(item => item.id === id)
            if (prd) {
                // 数量加一
                prd.quantity++
                return
            }
            // 购物车没有该商品
            this.cartList.push({
                id,
                quantity: 1 // 默认购物数量 1
            })
        },
        // 从购物车删除一个(即购物数量减一)
        delFromCart(id) {
            // 从购物车中找出该商品
            const prd = this.cartList.find(item => item.id === id)
            if (prd == null) {
                return
            }

            // 数量减一
            prd.quantity--

            // 如果数量减少到了 0
            if (prd.quantity <= 0) {
                this.cartList = this.cartList.filter(
                    item => item.id !== id
                )
            }
        }
    },
    mounted() {
        event.$on('addToCart', this.addToCart)
        event.$on('delFromCart', this.delFromCart)
    }
}
</script>
<!-- ProductionList.vue 组件 -->
<template>
    <div>
        <ProductionItem
            v-for="item in list"
            :key="item.id"
            :item="item"
        />
    </div>
</template>

<script>
import ProductionItem from './ProductionItem'

export default {
    components: {
        ProductionItem,
    },
    props: {
        list: {
            type: Array,
            default() {
                return [
                    // {
                    //     id: 1,
                    //     title: '商品A',
                    //     price: 10
                    // }
                ]
            }
        }
    }
}
</script>
<!-- ProductionItem.vue 组件 -->
<template>
    <div>
        <span>{{item.title}}</span>
        &nbsp;
        <span>{{item.price}}元</span>
        &nbsp;
        <a href="#" @click="clickHandler(item.id, $event)">加入购物车</a>
    </div>
</template>

<script>
import event from '../event'

export default {
    props: {
        item: {
            type: Object,
            default() {
                return {
                    // id: 1,
                    // title: '商品A',
                    // price: 10
                }
            }
        }
    },
    methods: {
        clickHandler(id, e) {
            e.preventDefault()
            event.$emit('addToCart', id)
        }
    },
}
</script>
<!-- CartList.vue 组件 -->
<template>
    <div>
        <CartItem
            v-for="item in list"
            :key="item.id"
            :item="item"
        />
        <p>总价 {{totalPrice}}</p>
    </div>
</template>

<script>
import CartItem from './CartItem'

export default {
    components: {
        CartItem,
    },
    props: {
        productionList: {
            type: Array,
            default() {
                return [
                    // {
                    //     id: 1,
                    //     title: '商品A',
                    //     price: 10
                    // }
                ]
            }
        },
        cartList: {
            type: Array,
            default() {
                return [
                    // {
                    //     id: 1,
                    //     quantity: 1
                    // }
                ]
            }
        }
    },
    computed: {
        // 购物车商品列表
        list() {
            return this.cartList.map(cartListItem => {
                // 找到对应的 productionItem
                const productionItem = this.productionList.find(
                    prdItem => prdItem.id === cartListItem.id
                )

                // 返回商品信息,外加购物数量
                return {
                    ...productionItem,
                    quantity: cartListItem.quantity
                }
                // 如:
                // {
                //     id: 1,
                //     title: '商品A',
                //     price: 10,
                //     quantity: 1 // 购物数量
                // }
            })
        },
        // 总价
        totalPrice() {
            return this.list.reduce(
                (total, curItem) => total + (curItem.quantity * curItem.price),
                0
            )
        }
    }
}
</script>
<!-- CartItem.vue 组件 -->
<template>
    <div>
        <span>{{item.title}}</span>
        &nbsp;
        <span>(数量 {{item.quantity}})</span>
        &nbsp;
        <a href="#" @click="addClickHandler(item.id, $event)">增加</a>
        &nbsp;
        <a href="#" @click="delClickHandler(item.id, $event)">减少</a>
    </div>
</template>

<script>
import event from '../event'

export default {
    props: {
        item: {
            type: Object,
            default() {
                return {
                    // id: 1,
                    // title: '商品A',
                    // price: 10,
                    // quantity: 1 // 购物数量
                }
            }
        }
    },
    methods: {
        addClickHandler(id, e) {
            e.preventDefault()
            event.$emit('addToCart', id)
        },
        delClickHandler(id, e) {
            e.preventDefault()
            event.$emit('delFromCart', id)
        }
    }
}
</script>
// event.js 用于定义方法
import Vue from 'vue'
export default new Vue()