vue高级特性及部分原理

vue常用高级特性、自定义v-model、slot插槽、mixin抽离公共代码、keep-alive缓存组件、$nextTick、动态组件、异步组件、vuex、vue-router等。vue中的响应式原理、组件渲染过程、模板渲染、虚拟DOMvdom、diff算法等。

Vue 高级特性

  1. 自定义v-model :value与@input 结合实现

    1
    2
    3
    4
    5
    6
    7
    <!-- input-price -->
    <input type="text" :value="price"
      @input="price=$event.target.value">
    <!-- app.vue -->
    <div id="app">
    <input-price v-model="price"></input-price>
    </div>
  2. slot 插槽 作用域插槽 具名插槽

  3. mixin 抽离多个组件中相同的逻辑
    mixin存在问题:1. 变量来源不明确,不利于阅读多 2. mixin可能造成命名冲突 3. mixin组件可能出现多对多关系,复杂度较高. vue3 composition API 解决这些问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    <!-- mixin.js -->
    export default {
    data() {
    return { city: '北京' }
    },
    methods: {
    showName() { console.log(this.name) }
    },
    mounted() {
    console.log('mixin mounted', this.name)
    }
    }
    <!-- mixin demo -->
    <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: 'tew',
    major: 'web 前端'
    }
    },
    methods: {},
    mounted() {
    console.log('component mounted', this.name)
    }
    }
    </script>
    <!--
    当组件和混入对象含有同名选项时,选项会以恰当的方式进行合并
    1. data中的数据对象在内部会进行递归合并, 并在冲突时优先使用组件中data定义的数据
    2. 同名钩子函数(生命周期函数)将合并为一个数组, 都会被调用, 混入对象的钩子函数(生命周期函数)会在组件自身钩子之前调用
    3. 值为对象的选项,如methdos, components和drictives,将被合并为同一个对象,键名冲突时,取组件自身对象的键值对
    -->
    <script>
    var mixin = {
    data () {
    return { msg: 'hello' }
    },
    mounted () {
    console.log('from mixin')
    },
    methods: {
    test () { console.log('methods test from mixin') }
    }
    }
    export default {
    mixins: [mixin],
    data () {
    return { msg: 'world' }
    },
    mounted () {
    console.log(this.msg, 'self') // world
    this.test() // methods test from self
    },
    methods: {
    test () { console.log('methods test from self') }
    }
    }
    // 1. 'from mixin'; // 同名钩子函数 都将被调用 mixin钩子函数先执行
    // 2. world self // 数据冲突时 优先使用组件自身数据
    // 3. methods test from self // methods 对象键名冲突时 优先使用组件自身键值对
    </script>
  4. keep-alive 缓存 频繁切换不需要重复渲染组件

    1
    2
    3
    4
    5
    <keep-alive> <!-- tab 切换 -->
    <KeepAliveStageA v-if="state === 'A'"/> <!-- v-show -->
    <KeepAliveStageB v-if="state === 'B'"/>
    <KeepAliveStageC v-if="state === 'C'"/>
    </keep-alive>
  5. $nextTick与异步更新 由于异步渲染 会在DOM渲染完之后被触发,以获取最新DOM节点

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

  1. refs 元素设置 ref="ul" 属性 获取DOM元素 this.$refs.ul

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /* <ul ref="ul">
    <li v-for="(item, index) in list" :key="index">
    {{item}}
    </li>
    </ul>
    <button @click="addItem">添加一项</button> */
    addItem() {
    this.list.push(`${Date.now()}`)
    this.list.push(`${Date.now()}`)
    // 1. 异步渲染,$nextTick 待 DOM 渲染完再回调
    // 2. 页面渲染时会将 data 的修改做整合,多次 data 修改只会渲染一次
    this.$nextTick(() => {
    // 获取 DOM 元素
    const ulElem = this.$refs.ul
    console.log( ulElem.childNodes.length )
    })
    }
  2. 动态组件 <component :is="componentname" />

  3. 异步组件 按需加载 components: { DescPopup: () => import('../components/descPopup')}
  4. vue 生命周期
  5. vuex
    state mutation getters action
    dispatch commit mapState mapMutations mapGetters mapActions
  6. vue-router
    • 路由模式(hash(默认) H5 history(需要server端支持))
    • 路由配置(动态路由(动态参数) 懒加载)
    • 路由守卫
  7. event事件是 原生的event事件 事件被挂载到当前元素

Vue原理

Vue原理主要三部分: 组件化、响应式、vdom和diff

  1. 组件化和MVVM 数据驱动视图 M model V view VM 模型(指令 事件等将view和model链接起来)
  2. 响应式原理 核心API Object.defineProperty

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    // Object.defineProperty的缺点
    // 1. 复杂对象需要深度监听 需要递归调用, 一次性计算量大
    // 2. 无法监听新增属性/删除属性(Vue.set/Vue.delete)
    // 3. 无法监听原生数组 需要特殊处理 重写了数组的7个方法

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

    // 重新定义属性,监听起来
    function defineReactive(target, key, value) {
    observer(value) // 深度监听
    Object.defineProperty(target, key, { // 核心 API
    get() { return value },
    set(newValue) {
    if (newValue !== value) {
    observer(newValue) // 深度监听
    // 设置新值
    // 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
    value = newValue
    }
    }
    })
    }

    // 监听对象属性
    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])
    }
    }
  3. vdom(Virtual DOM)是实现vuereact的重要基石 snabbdom vdom库

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // vdom 数据驱动视图 控制DOM操作
    // vdom 用JS模拟DOM结构 计算出最小变更, 操作DOM
    /* 真实DOM
    <div id="div1" class="wrap">
    <p>vdom</p>
    <ul style="font-size: 20px">
    <li>a</li>
    </ul>
    </div>
    */
    /* JS 模拟DOM
    {
    tag: 'div',
    props:{ className: 'wrap', id: 'div1' },
    children: [
    { tag: 'p', children: 'vdom' },
    {
    tag: 'ul',
    props: { style:'font-size: 20px'},
    children: [
    { tag: 'li', children: 'a' }
    ]
    }
    ]
    }
    */
  4. diff算法是vdom中最核心、最关键的部分 vdom树的diff对比

    1
    2
    3
    4
    // 只比较同一层级 不跨级比较
    // tag不相同 则直接删除重建,不再深度比较
    // tag和key 两者都相同 则认为是相同节点,不再深度比较
    // vdom核心概念: h, vnode, ptach, diff, key等
  5. 模板编译

    • with语法

      1
      2
      3
      4
      5
      6
      7
      8
      const obj = { a: 100, b: 200 }
      // with 能改变 {} 内自由变量的查找方式 将 {} 内自由变量当做 obj 的属性来查找
      with(obj){
      console.log(a) // 100
      console.log(b) // 200
      console.log(c) // 报错
      }
      // with 慎用 他打破了作用域规则 易读性变差
    • vue-template-complier将模板编译为render函数

      1
      2
      3
      4
      // 模板编译是将模板转化为某种JS代码
      // 模板编译为render函数 执行render函数返回vnode
      // 基于vnode在执行patch和diff
      // webpack vue-loader 会在开发环境编译模板
    • 执行render函数生成vnode render函数代替template

      1
      2
      3
      4
      5
      6
      7
      8
      Vue.component('title', {
      // template: '<div class="test"><a href="xxx">this is a tag</a></div>'
      render: function(createElement){
      return createElement('div',{ className: 'test'} , [
      createElement(a, { attrs: { href: 'xxx' }}, 'this is a atg')
      ])
      }
      })
  6. 组件渲染/更新过程
    初次渲染

  • 解析模板为render函数
  • 触发响应式, 监听data属性getter setter
  • 执行render函数(会触发getter), 生成vnode, patch(elem, vnode)
    更新过程
  • 修改data, 触发setter
  • 重新执行render函数, 生成newVnode
  • patch(vnode, newVnode)
  1. 前段路由
  • hash
    hash变化不会刷新页面, 会触发网页跳转,即浏览器的前进、后退

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <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>
  • H5 history
    主要使用 history.pushState和window.onpopstate 实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <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
    </script>
    </body>

Vue面试题

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

    1
    2
    3
    // v-show 通过css display 控制显示和隐藏
    // v-if 组件真正的渲染和销毁, 而不是显示和隐藏
    // 频繁切换显示状态用 v-show, 否则用 v-if
  2. 为何v-for中要用key
    key可以提升vue的编译效率 提高复用
    key不能是index和random, 因为diff算法中通过tag和key来判断, 是否是相同节点sameNode
    当使用index索引做key时, 删除列表元素会有bug(bug产生原因就是vue就地复用策略导致的)

  3. 生命周期(父子组件生命周期)
    加载渲染过程 父beforeCreate->父created->父beforeMounted->子beforeCreate->子created->子beforeMount->子mounted->父mounted
    子组件更新过程 父beforeUpdate->子beforeUpdate->子updated->父updated
    父组件更新过程 父beforeUpdate->父updated
    销毁过程 父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
  4. Vue组件如何通讯
  • 父子组件 props和$emit
  • 自定义事件 event.$on event.$emit event.$off
  • vuex
  • provide和inject
  • $parent和$children/$refs
  • eventbus
  • $attrs/$listeners
  • dispatch/boardcast
  1. 描述组件渲染和和更新的过程
    初次渲染
  • 解析模板为render函数
  • 触发响应式, 监听data属性getter setter
  • 执行render函数(会触发getter), 生成vnode, patch(elem, vnode)
    更新过程
  • 修改data, 触发setter
  • 重新执行render函数, 生成newVnode
  • patch(vnode, newVnode)
  1. 双向数据绑定v-model的实现原理 :value与@input 结合实现

    1
    2
    3
    4
    5
    6
    7
    <!-- input-price -->
    <input type="text" :value="price"
      @input="price=$event.target.value">
    <!-- app.vue -->
    <div id="app">
    <input-price v-model="price"></input-price>
    </div>
  2. computed有何特点
    computed 会缓存, data不变不会重新计算 提高性能

  3. 如何将组件所有props传递给子组件
    <com v-bind="$props">

  4. 何时使用异步组件
    加载大组件 路由异步加载

  5. 何时需要使用beforeDestroy

  • 解绑自定义事件 event.$off
  • 清楚定时器
  • 解除自定义的DOM事件, 如scroll等
  1. vuex中action和mutation有何区别
  • action中处理异步, mutation不可以处理异步只处理同步
  • action可以整合多个mutation
  1. Vue常见性能优化方式
  • 自定义事件 dom事件及时销毁
  • 合理使用异步组件
  • 合理使用keep-alive
  • data层级不要太深
-------------本文结束感谢您的阅读-------------
0%