# 面试题

# Vue父子组件通讯

props $emit

# 自定义事件

// 绑定自定义事件 
mouunted() {
    event.$on('onAddTitle',this.handleEvent)
}

// 解绑事件
beforeDestroy() {
    // 及时销毁,否则会造成内存泄漏
    event.$off('onAddTitle',this.handleEvent)
}
 
// 调用自定义事件
event.$emit('onAddTitle',val)

# 生命周期(父子组件)

  1. 加载渲染过程

父 beforeCreate => 父 created => 父 beforeMount => 子 beforeCreate => 子 created => 子 beforeMount => 子 mounted => 父 mounted

  1. 子组件更新过程

父 beforeUpdate => 子 beforeUpdate => 子 updated => 父 updated

  1. 父组件更新过程

父 beforeUpdate => 父 updated

  1. 销毁过程

父 beforeDestroy => 子 beforeDestroy => 子 destroyed => 父 destroyed

# 自定义v-model

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

<script>
import CustomVModel from './CustomVModel'
export default {
    components: {
        CustomVModel,
    },
    data() {
        return {
            name: "hanzo"
        }
    }
}
</script>
<template>
  <div>
    <input type="text" 
        :value="textV"
        @input="$emit('changeV',$event.target.value)" 
    />
    <!-- 
        1. 上面的 input 使用了 :value 而不是 v-model
        2. 上面的 changeV 和 model.event 要对应起来
        3. textV 属性对应起来
     -->
  </div>
</template>

<script>
export default {
  model: {
    prop: "textV", // 对应 props text
    event: 'changeV'
  },
  props: {
    textV: {
      type: String,
      default: ""
    }
  }
};
</script>

# $nextTick

  • Vue是异步渲染
  • data改变后,DOM不会立刻渲染
  • $nextTick会在DOM渲染之后被触发,以获取最新的DOM节点
<template>
    <div>
        <ul ref="ul">
            <li v-for="(val,index) in list" :key="index">
                {{val}}
            </li>
        </ul>
        <button @click="addVal">添加一个</button>
    </div>
</template>

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

            // 获取 DOM 元素
            // const ulElem = this.$refs.ul
            // console.log(ulElem.children.length)
            // console.log(ulElem.childNodes.length)

            // 1. 异步渲染,$nextTick 待 DOM 渲染完了 再回调
            // 2. 页面渲染时会将 data 的修改做整合, 多次 data 修改只会渲染一次
            this.$nextTick(() => {
                const ulElem = this.$refs.ul
                console.log(ulElem.children.length)
                console.log(ulElem.childNodes.length)
            })


        }
    }
}
</script>

# slot

  • 基本使用
// 父组件
<template>
  <div id="app">
    <SlotDemo url="www.baidu.com">
      <span>百度</span>
    </SlotDemo>
  </div>
</template>

<script>
import SlotDemo from './components/Slot'
export default {
  name: 'app',
  components: {
    SlotDemo
  },
}
</script>
// 子组件
<template>
    <div>
        <a :href="url">
            <slot>
                默认没有内容就展示
            </slot>
        </a>
    </div>
</template>

<script>
export default {
    props: {
        url: {
            type: String,
            default: ''
        }
    }
}
</script>
  • 作用域插槽

    <template>
      <div id="app">
        <SlotDemo :url="website.url">
          <template v-slot="slotProps">
            {{slotProps.slotData.title}}
          </template>
        </SlotDemo>
      </div>
    </template>
    <script>
    import SlotDemo from './components/Slot'
    export default {
      name: 'app',
      components: {
        SlotDemo,
      },
    
      data() {
        return {
          website: {
            url: "http://www.baidu.com",
            title: "hanzo",
            subTitle:"花村老大"
          },
        }
      },
    }
    </script>
    
    
    <template>
      <div>
        <a :href="url">
          <slot :slotData="website">{{website.subTitle}}</slot>
        </a>
      </div>
    </template>
    
    <script>
    export default {
      props: {
        url: {
          type: String,
          default: ""
        }
      },
      data() {
        return {
          website: {
            url: "http://www.baidu.com",
            title: "genji",
            subTitle: "花村弟弟"
          }
        };
      }
    };
    </script>
    
      > 总结:
      >
      >1、在子组件中定义 :slotData将子组件data数据传向组组件
      >
      >2、在父组件使用 <template v-slot="slotProps"></template> 接手
      >
      >3、slotProps 就是 子组件 向 父组件传递过来的值  {{slotProps.slotData.title}} 就能拿到子组件的title
    
  • 具名插槽

<template>
  <div class="nav-bar" :class="{'is_fixed':isFixed}">
    <div class="container">
        <slot name="buy"></slot>
      </div>
    </div>
  </div>
</template>
<template>
  <div class="product">
    <product-param :name="product.name">
      <template v-slot:buy>
        <button class="btn" @click="buy">立即购买</button>
      </template>
    </product-param>
  </div>
</template

# Vue动态组件

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

# 简单使用

<template>
  <div id="app">
    <!-- 动态组件 -->
    <component :is="NextTickName"></component>
  </div>
</template>

<script>
import NextTick from './components/NextTick'
export default {
  name: 'app',
  components: {
    NextTick,
  },
  data() {
    return {
      NextTickName: "NextTick",
    }
  },
}
</script>

# 场景使用

<template>
  <div id="app">
    <div v-for="(val,index) in newsData" :key="index">
		<component :is="val.type" />    	  
    </div>
  </div>
</template>

<script>
import NextTick from './components/NextTick'
export default {
  name: 'app',
  components: {
    NextTick,
  },
  data() {
    return {
		newsData: [
            {id: 1,type: 'text'},
            {id: 2,type: 'text'},
            {id: 3,type: 'image'},
        ]	      
    }
  },
}
</script>

# Vue异步加载组件

  • import() 函数
  • 按需加载,异步加载大组件
<template>
  <div id="app">
    <!-- 异步组件 -->
    <Model v-if="show" />
    <button @click="show = true">click</button>
  </div>
</template>

<script>
export default {
  name: 'app',
  components: {
    Model: () => import('./components/Model')
  },
  data() {
    return {
      show: false,
    }
  },
}
</script>

# Keep-alive

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

# 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 {
    name: 'mixin',
    mixins:[myMixin], // 可以添加多个,会自动合并起来
    data() {
        return {
            name: "hanzo",
            major: "前端"
        }
    },
    mounted() {
        console.log('component mounted',this.name)
    }
}
</script>
  • mixin.js
export default {
    data() {
        return {
            city: "成都"
        }
    },
    methods: {
        showName() {
            console.log(this.name)
        }
    },
    mounted() {
        console.log('mixin mounted',this.name)
    },
}

# mixin的问题

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

# Vue-Router

# 路由模式

  • hash模式,例如 http://www.baidu.com/#/user/19
  • H5 history模式,例如 http://www.baidu.com/user/19
  • 后者是需要server端支持的,因此无特殊需求可以选择前者

# 动态路由

const User = {
    template: `<div>{{$router.params.id}}</div>`
}

const router = new VueRouter({
    routes: [
        // 动态路径参数 以冒号开头 能命中 `/user/10` `/user/20` 等格式的路由
        {path: '/user/:id', component: User}
    ]
})

# 懒加载

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

# 如何理解MVVM

# 组件化基础

  • 数据驱动视图(MVVM,setState)

# Vue的响应式

  • 组件data的数据一旦变化,立刻触发视图的更新
  1. 核心 API - Object.defineProperty
  2. Vue3.0 使用的是 Proxy
    1. Proxy兼容性不好,且无法polyfill

# Object.defineProperty实现响应式

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

# 代码演示

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

// 重新定义属性,监听起来

function defineReactive(target, key, value) {
    // 核心API
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (newValue !== value) {
                // 设置新值
                // 注意,value一直在闭包中,此处设置完之后,再get时也是
                value = newValue

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

// 监听对象

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

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

// 准备数据
const data = {
    name: "hanzo",
    age: 20,
    // info: {
    //     address: "成都"
    // },
    // nums: [1,2,3]
}

// 监听数据
observer(data)

// 测试
data.name = "genji"
data.age = 18

# Object.defineProperty缺点

  • 深度监听,需要递归到底,一次性计算量大
  • 无法监听新增属性/删除属性 (Vue.set / Vue.delete)
  • 无法原生监听数组,需要特殊处理
// 触发更新视图
function updateView() {
    console.log("视图更新")
}

// 3.
// 重新定义数组原型
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)
    }
})

// 重新定义属性,监听起来

function defineReactive(target, key, value) {

    // 1. 深度监听
    observer(value)

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

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

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

// 监听对象

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

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

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

// 准备数据
const data = {
    name: "hanzo",
    age: 20,
    info: {
        address: "成都" // 1. 需要深度监听
    },
    nums: [1, 2, 3]
}

// 监听数据
observer(data)

// 测试
data.name = "genji"
// data.age = 18
data.age = { num: 21 }
data.age.num = 22
data.info.address = "达州"
data.x = "100" // 新增属性,监听不到 --- 所有有了 Vue.set
delete data.name // 删除属性,监听不到 --- 所有有了 Vue.delete
data.nums.push(4)

# 虚拟DOM和diff

  • DOM操作非常消耗性能
  • Vue 和 React 是数据驱动视图

# 解决方案

  • Vdom - 用 JS 模拟 DOM 结构,计算出最小的变更,操作DOM1

示例:

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

# diff算法

  • diff算法是vdom中最核心、最关键的部分

# 模板编译

  • 模板是 vue 开发中最常用的部分,即与使用相关联的原理
  • 它不是html,有指令、插值、JS表达式
  • JS 的 with 语法
  • vue template complier 将模板编译为render函数
  • 执行render函数生产vnode

# with语法

const obj = { a: 100, b: 200 }
console.log(obj.a) // 100
console.log(obj.b) // 200
console.log(obj.c) // undefined

// --------------------------------

// 使用with,能改变 {} 内自由变量的查找方式
// 将 {} 内自由变量,当做 obj 的属性来查找
with(obj) {
    console.log(a) // 100
    console.log(b) // 200
    console.log(c) // c is not defined
}
  • 改变 {} 内自由变量的查找方式,当做 obj 属性来查找
  • 如果找不到匹配的 obj 属性,就会报错
  • with 要慎用,它打破了作用域规则,易读性变差
const compiler = require('vue-template-compiler')

// 插值

// const template = `<p>{{message}}</p>`
// with(this){return _c('p',[_v(_s(message))])}
// with(this){return crateElement('p',[createTextVNode(toString(message))])}


// 表达式
// 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")]):_e(),_v(" "),_v("\">B"),_c('p')])}


// 循环
// 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" />`
// 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

// 编译
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._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

Vue.component('demo',{
	// template: 'xxx'
	render: function(createElement) {
		return createElement(
			'h' + this.level,
			[
				createElement('a',{
					attrs: {
						name: "headerId",
						href: '#' + 'headerId'
					}
				},'this is a tag')
			]
		)
	}
})

# 组件 渲染/更新 过程

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

# 初次渲染过程

  • 解析模板为 render 函数(或在开发环境已完成,vue-loader)
  • 触发响应式,监听data属性 getter setter
  • 执行 render 函数,生出 vnode ,patch(elem,vnode)

# 更新过程

  • 修改 data,触发 setter (此前在 getter 中已被监听)
  • 重新执行 render 函数,生出 newVnode
  • patch(vnode,newVnode)

# 异步渲染

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

# 前端路由原理

# 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">
    <title>Document</title>
</head>
<body>
    <!-- <script src="./index.js"></script> -->
    <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)
        })

        document.getElementById("btn1").addEventListener('click', () => {
            location.hash = '#/user'
        })
    </script>
</body>
</html>

# H5 history

  • 用url规范的路由,但跳转时不刷新页面
  • history.pushState
  • window.onpopstate
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <p>histroy API test</p>
    <button id="btn1">修改 url</button>

    <script>
        // 初次加载页面,获取 hash
        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 = function (event) {
            console.log("onpopstate", event.state, location.pathname)
        }


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

# 总结

  • hash -- window.onhashchange
  • H5 history - history.pushState 和 window.onpopstate

# 两者选择

  • to B 的系统推荐用 hash,简单易用,对 url 规范不敏感

  • to C 的系统可以使用 H5 history,但需要服务端支持

# v-for为何要使用key

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

# 为何组件中data必须是个函数

# ajax请求应该放在哪个声明周期

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

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

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

# 何时需要使用beforeDestory

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

# Vuex中 action 和 mutation 有何区别

  • action可以处理异步,mutation不能
  • action可以整合多个mutation

# Vue常见性能优化

  • 合理使用 v-show 和 v-if
  • 合理使用computed
  • v-for 时要加key,避免和 v-if同时使用
  • 自定义事件,DOM事件及时销毁
  • 合理使用异步组件
  • 合理使用keep-alive
  • data层级不要太深

# Proxy实现响应式