乐趣区

关于vue.js:校招前端二面高频vue面试题边面边更

Vue 中封装的数组办法有哪些,其如何实现页面更新

在 Vue 中,对响应式解决利用的是 Object.defineProperty 对数据进行拦挡,而这个办法并不能监听到数组外部变动,数组长度变动,数组的截取变动等,所以须要对这些操作进行 hack,让 Vue 能监听到其中的变动。那 Vue 是如何实现让这些数组办法实现元素的实时更新的呢,上面是 Vue 中对这些办法的封装:

// 缓存数组原型
const arrayProto = Array.prototype;
// 实现 arrayMethods.__proto__ === Array.prototype
export const arrayMethods = Object.create(arrayProto);
// 须要进行性能拓展的办法
const methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse"
];

/** * Intercept mutating methods and emit events */
methodsToPatch.forEach(function(method) {
  // 缓存原生数组办法
  const original = arrayProto[method];
  def(arrayMethods, method, function mutator(...args) {
    // 执行并缓存原生数组性能
    const result = original.apply(this, args);
    // 响应式解决
    const ob = this.__ob__;
    let inserted;
    switch (method) {
    // push、unshift 会新增索引,所以要手动 observer
      case "push":
      case "unshift":
        inserted = args;
        break;
      // splice 办法,如果传入了第三个参数,也会有索引退出,也要手动 observer。case "splice":
        inserted = args.slice(2);
        break;
    }
    // 
    if (inserted) ob.observeArray(inserted);// 获取插入的值,并设置响应式监听
    // notify change
    ob.dep.notify();// 告诉依赖更新
    // 返回原生数组办法的执行后果
    return result;
  });
});

简略来说就是,重写了数组中的那些原生办法,首先获取到这个数组的__ob__,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 持续对新的值察看变动(也就是通过 target__proto__ == arrayMethods 来扭转了数组实例的型),而后手动调用 notify,告诉渲染 watcher,执行 update。

Vue.js 的 template 编译

简而言之,就是先转化成 AST 树,再失去的 render 函数返回 VNode(Vue 的虚构 DOM 节点),具体步骤如下:

首先,通过 compile 编译器把 template 编译成 AST 语法树(abstract syntax tree 即 源代码的形象语法结构的树状表现形式),compile 是 createCompiler 的返回值,createCompiler 是用以创立编译器的。另外 compile 还负责合并 option。

而后,AST 会通过 generate(将 AST 语法树转化成 render funtion 字符串的过程)失去 render 函数,render 的返回值是 VNode,VNode 是 Vue 的虚构 DOM 节点,外面有(标签名、子节点、文本等等)

怎么了解 Vue 的单向数据流

数据总是从父组件传到子组件,子组件没有权力批改父组件传过来的数据,只能申请父组件对原始数据进行批改。这样会 避免从子组件意外扭转父级组件的状态,从而导致你的利用的数据流向难以了解

留神:在子组件间接用 v-model 绑定父组件传过来的 prop 这样是不标准的写法 开发环境会报正告

如果切实要扭转父组件的 prop 值,能够在 data 外面定义一个变量 并用 prop 的值初始化它 之后用$emit 告诉父组件去批改

有两种常见的试图扭转一个 prop 的情景 :

  1. 这个 prop 用来传递一个初始值;这个子组件接下来心愿将其作为一个本地的 prop 数据来应用。在这种状况下,最好定义一个本地的 data 属性并将这个 prop用作其初始值
props: ['initialCounter'],
data: function () {
  return {counter: this.initialCounter}
}
  1. 这个 prop 以一种原始的值传入且须要进行转换。在这种状况下,最好应用这个 prop 的值来定义一个计算属性
props: ['size'],
computed: {normalizedSize: function () {return this.size.trim().toLowerCase()}
}

虚构 DOM 真的比实在 DOM 性能好吗

  • 首次渲染大量 DOM 时,因为多了一层虚构 DOM 的计算,会比 innerHTML 插入慢。
  • 正如它能保障性能上限,在实在 DOM 操作的时候进行针对性的优化时,还是更快的。

Vue 是如何实现数据双向绑定的

Vue 数据双向绑定次要是指:数据变动更新视图,视图变动更新数据,如下图所示:

  • 输入框内容变动时,Data 中的数据同步变动。即 View => Data 的变动。
  • Data 中的数据变动时,文本节点的内容同步变动。即 Data => View 的变动

Vue 次要通过以下 4 个步骤来实现数据双向绑定的

  • 实现一个监听器 Observer:对数据对象进行遍历,包含子属性对象的属性,利用 Object.defineProperty() 对属性都加上 settergetter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变动
  • 实现一个解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,而后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,增加监听数据的订阅者,一旦数据有变动,收到告诉,调用更新函数进行数据更新
  • 实现一个订阅者 WatcherWatcher 订阅者是 ObserverCompile 之间通信的桥梁,次要的工作是订阅 Observer 中的属性值变动的音讯,当收到属性值变动的音讯时,触发解析器 Compile 中对应的更新函数
  • 实现一个订阅器 Dep:订阅器采纳 公布 - 订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行对立治理

Vue 数据双向绑定原理图

子组件能够间接扭转父组件的数据么,阐明起因

这是一个实际知识点,组件化开发过程中有个 单项数据流准则,不在子组件中批改父组件是个常识问题

思路

  • 讲讲单项数据流准则,表明为何不能这么做
  • 举几个常见场景的例子说说解决方案
  • 联合实际讲讲如果须要批改父组件状态应该如何做

答复范例

  1. 所有的 prop 都使得其父子之间造成了一个单向上行绑定:父级 prop 的更新会向下流动到子组件中,然而反过来则不行。这样会避免从子组件意外变更父级组件的状态,从而导致你的利用的数据流向难以了解。另外,每次父级组件产生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件外部扭转 prop。如果你这样做了,Vue 会在浏览器控制台中收回正告
const props = defineProps(['foo'])
// ❌ 上面行为会被正告, props 是只读的!
props.foo = 'bar'
  1. 理论开发过程中有两个场景会想要批改一个属性:

这个 prop 用来传递一个初始值;这个子组件接下来心愿将其作为一个本地的 prop 数据来应用。 在这种状况下,最好定义一个本地的 data,并将这个 prop 用作其初始值:

const props = defineProps(['initialCounter'])
const counter = ref(props.initialCounter)

这个 prop 以一种原始的值传入且须要进行转换。 在这种状况下,最好应用这个 prop 的值来定义一个计算属性:

const props = defineProps(['size'])
// prop 变动,计算属性自动更新
const normalizedSize = computed(() => props.size.trim().toLowerCase())
  1. 实际中如果的确想要扭转父组件属性应该 emit 一个事件让父组件去做这个变更。留神尽管咱们不能间接批改一个传入的对象或者数组类型的prop,然而咱们还是可能间接改内嵌的对象或属性

参考 前端进阶面试题具体解答

怎么实现路由懒加载呢

这是一道应用题。当打包利用时,JavaScript 包会变得十分大,影响页面加载。如果咱们能把不同路由对应的组件宰割成不同的代码块,而后当路由被拜访时才加载对应组件,这样就会更加高效

// 将
// import UserDetails from './views/UserDetails'
// 替换为
const UserDetails = () => import('./views/UserDetails')
​
const router = createRouter({
  // ...
  routes: [{path: '/users/:id', component: UserDetails}],
})

答复范例

  1. 当打包构建利用时,JavaScript 包会变得十分大,影响页面加载。利用路由懒加载咱们能把不同路由对应的组件宰割成不同的代码块,而后当路由被拜访的时候才加载对应组件,这样会更加高效,是一种优化伎俩
  2. 一般来说,对所有的 路由都应用动静导入 是个好主见
  3. component 选项配置一个返回 Promise 组件的函数就能够定义懒加载路由。例如:{path: '/users/:id', component: () => import('./views/UserDetails') }
  4. 联合正文 () => import(/* webpackChunkName: "group-user" */ './UserDetails.vue') 能够做 webpack 代码分块

Vue.set 的实现原理

  • 给对应和数组自身都减少了 dep 属性
  • 当给对象新增不存在的属性则触发对象依赖的 watcher 去更新
  • 当批改数组索引时,咱们调用数组自身的 splice 去更新数组(数组的响应式原理就是从新了 splice 等办法,调用 splice 就会触发视图更新)

根本应用

以下办法调用会扭转原始数组:push(), pop(), shift(), unshift(), splice(), sort(), reverse(),Vue.set(target, key, value)

  • 调用办法:Vue.set(target, key, value)

    • target:要更改的数据源(能够是对象或者数组)
    • key:要更改的具体数据
    • value:从新赋的值
<div id="app">{{user.name}} {{user.age}}</div>
<div id="app"></div>
<script>
    // 1. 依赖收集的特点:给每个属性都减少一个 dep 属性,dep 属性会进行收集,收集的是 watcher
    // 2. vue 会给每个对象也减少一个 dep 属性
    const vm = new Vue({
        el: '#app',
        data: { // vm._data  
            user: {name:'poetry'}
        }
    });
    // 对象的话:调用 defineReactive 在 user 对象上定义一个 age 属性,减少到响应式数据中,触发对象自身的 watcher,ob.dep.notify()更新 
    // 如果是数组 通过调用 splice 办法,触发视图更新
    vm.$set(vm.user, 'age', 20); // 不能给根属性增加,因为给根增加属性 性能耗费太大,须要做很多解决

    // 批改必定是同步的 -> 更新都是一步的  queuewatcher
</script>

相干源码

// src/core/observer/index.js 44
export class Observer {// new Observer(value)
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep() // 给所有对象类型减少 dep 属性}
}
// src/core/observer/index.js 201
export function set (target: Array<any> | Object, key: any, val: any): any {
  // 1. 是开发环境 target 没定义或者是根底类型则报错
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // 2. 如果是数组 Vue.set(array,1,100); 调用咱们重写的 splice 办法 (这样能够更新视图)
  if (Array.isArray(target) && isValidArrayIndex(key)) {target.length = Math.max(target.length, key)
    // 利用数组的 splice 变异办法触发响应式  
    target.splice(key, 1, val)
    return val
  }
  // 3. 如果是对象自身的属性,则间接增加即可
  if (key in target && !(key in Object.prototype)) {target[key] = val // 间接批改属性值  
    return val
  }
  // 4. 如果是 Vue 实例 或 根数据 data 时 报错,(更新_data 无意义)const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  // 5. 如果不是响应式的也不须要将其定义成响应式属性
  if (!ob) {target[key] = val
    return val
  }
  // 6. 将属性定义成响应式的
  defineReactive(ob.value, key, val)
  // 告诉视图更新
  ob.dep.notify()
  return val
}

咱们浏览以上源码可知,vm.$set 的实现原理是:

  • 如果指标是数组,间接应用数组的 splice 办法触发相应式;
  • 如果指标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式解决,则是通过调用 defineReactive 办法进行响应式解决(defineReactive 办法就是 Vue 在初始化对象时,给对象属性采纳 Object.defineProperty 动静增加 gettersetter 的性能所调用的办法)

你感觉 vuex 有什么毛病

剖析

相较于 reduxvuex 曾经相当简便好用了。但模块的应用比拟繁琐,对 ts 反对也不好。

体验

应用模块:用起来比拟繁琐,应用模式也不对立,基本上得不到类型零碎的任何反对

const store = createStore({
  modules: {a: moduleA}
})
store.state.a // -> 要带上 moduleA 的 key,内嵌模块的话会很长,不得不配合 mapState 应用
store.getters.c // -> moduleA 里的 getters,没有 namespaced 时又变成了全局的
store.getters['a/c'] // -> 有 namespaced 时要加 path,应用模式又和 state 不一样
store.commit('d') // -> 没有 namespaced 时变成了全局的,能同时触发多个子模块中同名 mutation
store.commit('a/d') // -> 有 namespaced 时要加 path,配合 mapMutations 应用感觉也没简化

答复范例

  1. vuex利用响应式,应用起来曾经相当方便快捷了。然而在应用过程中感觉模块化这一块做的过于简单,用的时候容易出错,还要常常查看文档
  2. 比方:拜访 state 时要带上模块 key,内嵌模块的话会很长,不得不配合mapState 应用,加不加 namespaced 区别也很大,gettersmutationsactions这些默认是全局,加上之后必须用字符串类型的 path 来匹配,应用模式不对立,容易出错;对 ts 的反对也不敌对,在应用模块时没有代码提醒。
  3. 之前 Vue2 我的项目中用过 vuex-module-decorators 的解决方案,尽管类型反对上有所改善,但又要学一套新货色,减少了学习老本。pinia呈现之后应用体验好了很多,Vue3 + pinia会是更好的组合

原理

上面咱们来看看 vuexstore.state.x.y这种嵌套的门路是怎么搞进去的

首先是子模块装置过程:父模块状态 parentState 下面设置了子模块名称 moduleName,值为以后模块state 对象。放在下面的例子中相当于:store.state['x'] = moduleX.state。此过程是递归的,那么 store.state.x.y 装置时就是:store.state['x']['y'] = moduleY.state

// 源码地位 https://github1s.com/vuejs/vuex/blob/HEAD/src/store-util.js#L102-L115
if (!isRoot && !hot) {
    // 获取父模块 state
    const parentState = getNestedState(rootState, path.slice(0, -1))
    // 获取子模块名称
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
        // 把子模块 state 设置到父模块上
        parentState[moduleName] = module.state
    })
}

Vue-router 除了 router-link 怎么实现跳转

申明式导航

<router-link to="/about">Go to About</router-link>

编程式导航

// literal string path
router.push('/users/1')
​
// object with path
router.push({path: '/users/1'})
​
// named route with params to let the router build the url
router.push({name: 'user', params: { username: 'test'} })

答复范例

  • vue-router导航有两种形式:申明式导航和编程形式导航
  • 申明式导航形式应用 router-link 组件,增加 to 属性导航;编程形式导航更加灵便,可传递调用 router.push(),并传递path 字符串或者 RouteLocationRaw 对象,指定 pathnameparams 等信息
  • 如果页面中简略示意跳转链接,应用 router-link 最快捷,会渲染一个 a 标签;如果页面是个简单的内容,比方商品信息,能够增加点击事件,应用编程式导航
  • 实际上外部两者调用的导航函数是一样的

vue-router 有哪几种导航守卫

  • 全局守卫
  • 路由独享守卫
  • 路由组件内的守卫

全局守卫

vue-router 全局有三个守卫

  • router.beforeEach 全局前置守卫 进入路由之前
  • router.beforeResolve 全局解析守卫 (2.5.0+) 在beforeRouteEnter 调用之后调用
  • router.afterEach 全局后置钩子 进入路由之后
// main.js 入口文件
import router from './router'; // 引入路由
router.beforeEach((to, from, next) => {next();
});
router.beforeResolve((to, from, next) => {next();
});
router.afterEach((to, from) => {console.log('afterEach 全局后置钩子');
});

路由独享守卫

如果你不想全局配置守卫的话,你能够为某些路由独自配置守卫

const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => { 
        // 参数用法什么的都一样, 调用程序在全局前置守卫前面,所以不会被全局守卫笼罩
        // ...
      }
    }
  ]
})

路由组件内的守卫

  • beforeRouteEnter 进入路由前, 在路由独享守卫后调用 不能 获取组件实例 this,组件实例还没被创立
  • beforeRouteUpdate (2.2) 路由复用同一个组件时, 在以后路由扭转,然而该组件被复用时调用 能够拜访组件实例 this
  • beforeRouteLeave 来到以后路由时, 导航来到该组件的对应路由时调用,能够拜访组件实例 this

Vue 组件之间通信形式有哪些

Vue 组件间通信是面试常考的知识点之一,这题有点相似于凋谢题,你答复出越多办法当然越加分,表明你对 Vue 把握的越纯熟。Vue 组件间通信只有指以下 3 类通信 父子组件通信 隔代组件通信 兄弟组件通信,上面咱们别离介绍每种通信形式且会阐明此种办法可实用于哪类组件间通信

组件传参的各种形式

组件通信罕用形式有以下几种

  • props / $emit 实用 父子组件通信

    • 父组件向子组件传递数据是通过 prop 传递的,子组件传递数据给父组件是通过$emit 触发事件来做到的
  • ref$parent / $children(vue3 废除) 实用 父子组件通信

    • ref:如果在一般的 DOM 元素上应用,援用指向的就是 DOM 元素;如果用在子组件上,援用就指向组件实例
    • $parent / $children:拜访拜访父组件的属性或办法 / 拜访子组件的属性或办法
  • EventBus($emit / $on) 实用于 父子、隔代、兄弟组件通信

    • 这种办法通过一个空的 Vue 实例作为地方事件总线(事件核心),用它来触发事件和监听事件,从而实现任何组件间的通信,包含父子、隔代、兄弟组件
  • $attrs / $listeners(vue3 废除) 实用于 隔代组件通信

    • $attrs:蕴含了父作用域中不被 prop 所辨认 (且获取) 的个性绑定 (classstyle 除外 )。当一个组件没有申明任何 prop时,这里会蕴含所有父作用域的绑定 (classstyle 除外 ),并且能够通过 v-bind="$attrs" 传入外部组件。通常配合 inheritAttrs 选项一起应用
    • $listeners:蕴含了父作用域中的 (不含 .native 润饰器的) v-on 事件监听器。它能够通过 v-on="$listeners" 传入外部组件
  • provide / inject 实用于 隔代组件通信

    • 先人组件中通过 provider 来提供变量,而后在子孙组件中通过 inject 来注入变量。provide / inject API 次要解决了跨级组件间的通信问题,不过它的应用场景,次要是子组件获取下级组件的状态,跨级组件间建设了一种被动提供与依赖注入的关系
  • $root 实用于 隔代组件通信 拜访根组件中的属性或办法,是根组件,不是父组件。$root 只对根组件有用
  • Vuex 实用于 父子、隔代、兄弟组件通信

    • Vuex 是一个专为 Vue.js 利用程序开发的状态管理模式。每一个 Vuex 利用的外围就是 store(仓库)。“store”基本上就是一个容器,它蕴含着你的利用中大部分的状态 (state )
    • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地失去高效更新。
    • 扭转 store 中的状态的惟一路径就是显式地提交 (commit) mutation。这样使得咱们能够不便地跟踪每一个状态的变动。

依据组件之间关系探讨组件通信最为清晰无效

  • 父子组件:props/$emit/$parent/ref
  • 兄弟组件:$parent/eventbus/vuex
  • 跨层级关系:eventbus/vuex/provide+inject/$attrs + $listeners/$root

上面演示组件之间通信三种状况: 父传子、子传父、兄弟组件之间的通信

1. 父子组件通信

应用 props,父组件能够应用props 向子组件传递数据。

父组件 vue 模板father.vue:

<template>
  <child :msg="message"></child>
</template>

<script>
import child from './child.vue';
export default {
  components: {child},
  data () {
    return {message: 'father message';}
  }
}
</script>

子组件 vue 模板child.vue:

<template>
    <div>{{msg}}</div>
</template>

<script>
export default {
  props: {
    msg: {
      type: String,
      required: true
    }
  }
}
</script>

回调函数(callBack)

父传子:将父组件里定义的 method 作为 props 传入子组件

// 父组件 Parent.vue:<Child :changeMsgFn="changeMessage">
methods: {changeMessage(){this.message = 'test'}
}
// 子组件 Child.vue:<button @click="changeMsgFn">
props:['changeMsgFn']

子组件向父组件通信

父组件向子组件传递事件办法,子组件通过 $emit 触发事件,回调给父组件

父组件 vue 模板father.vue:

<template>
    <child @msgFunc="func"></child>
</template>

<script>
import child from './child.vue';
export default {
    components: {child},
    methods: {func (msg) {console.log(msg);
        }
    }
}
</script>

子组件 vue 模板child.vue:

<template>
    <button @click="handleClick"> 点我 </button>
</template>

<script>
export default {
    props: {
        msg: {
            type: String,
            required: true
        }
    },
    methods () {handleClick () {
          //........
          this.$emit('msgFunc');
        }
    }
}
</script>

2. provide / inject 跨级拜访先人组件的数据

父组件通过应用 provide(){return{}} 提供须要传递的数据

export default {data() {
    return {
      title: '我是父组件',
      name: 'poetry'
    }
  },
  methods: {say() {alert(1)
    }
  },
  // provide 属性 可能为前面的后辈组件 / 嵌套的组件提供所须要的变量和办法
  provide() {
    return {
      message: '我是先人组件提供的数据',
      name: this.name, // 传递属性
      say: this.say
    }
  }
}

子组件通过应用 inject:[“参数 1”,”参数 2”,…] 接管父组件传递的参数

<template>
  <p> 曾孙组件 </p>
  <p>{{message}}</p>
</template>
<script>
export default {
  // inject 注入 / 接管先人组件传递的所须要的数据即可 
  // 接管到的数据 变量 跟 data 外面的变量一样 能够间接绑定到页面 {{}}
  inject: ["message","say"],
  mounted() {this.say();
  },
};
</script>

3. $parent + $children 获取父组件实例和子组件实例的汇合

  • this.$parent 能够间接拜访该组件的父实例或组件
  • 父组件也能够通过 this.$children 拜访它所有的子组件;须要留神 $children 并不保障程序,也不是响应式的
<!-- parent.vue -->
<template>
<div>
  <child1></child1>   
  <child2></child2> 
  <button @click="clickChild">$children 形式获取子组件值 </button>
</div>
</template>
<script>
import child1 from './child1'
import child2 from './child2'
export default {data(){
    return {total: 108}
  },
  components: {
    child1,
    child2  
  },
  methods: {funa(e){console.log("index",e)
    },
    clickChild(){console.log(this.$children[0].msg);
      console.log(this.$children[1].msg);
    }
  }
}
</script>
<!-- child1.vue -->
<template>
  <div>
    <button @click="parentClick"> 点击拜访父组件 </button>
  </div>
</template>
<script>
export default {data(){
    return {msg:"child1"}
  },
  methods: {
    // 拜访父组件数据
    parentClick(){this.$parent.funa("xx")
      console.log(this.$parent.total);
    }
  }
}
</script>
<!-- child2.vue -->
<template>
  <div>
    child2
  </div>
</template>
<script>
export default {data(){
    return {msg: 'child2'}
  }
}
</script>

4. $attrs + $listeners 多级组件通信

$attrs 蕴含了从父组件传过来的所有 props 属性

// 父组件 Parent.vue:<Child :name="name" :age="age"/>

// 子组件 Child.vue:<GrandChild v-bind="$attrs" />

// 孙子组件 GrandChild
<p> 姓名:{{$attrs.name}}</p>
<p> 年龄:{{$attrs.age}}</p>

$listeners蕴含了父组件监听的所有事件

// 父组件 Parent.vue:<Child :name="name" :age="age" @changeNameFn="changeName"/>

// 子组件 Child.vue:<button @click="$listeners.changeNameFn"></button>

5. ref 父子组件通信

// 父组件 Parent.vue:<Child ref="childComp"/>
<button @click="changeName"></button>
changeName(){console.log(this.$refs.childComp.age);
    this.$refs.childComp.changeAge()}

// 子组件 Child.vue:data(){
    return{age:20}
},
methods(){changeAge(){this.age=15}
}

6. 非父子, 兄弟组件之间通信

vue2中废除了 broadcast 播送和散发事件的办法。父子组件中能够用 props$emit()。如何实现非父子组件间的通信,能够通过实例一个 vue 实例 Bus 作为媒介,要互相通信的兄弟组件之中,都引入 Bus,而后通过别离调用 Bus 事件触发和监听来实现通信和参数传递。Bus.js 能够是这样:

// Bus.js

// 创立一个地方工夫总线类  
class Bus {constructor() {this.callbacks = {};   // 寄存事件的名字  
  }  
  $on(name, fn) {this.callbacks[name] = this.callbacks[name] || [];  
    this.callbacks[name].push(fn);  
  }  
  $emit(name, args) {if (this.callbacks[name]) {this.callbacks[name].forEach((cb) => cb(args));  
    }  
  }  
}  

// main.js  
Vue.prototype.$bus = new Bus() // 将 $bus 挂载到 vue 实例的原型上  
// 另一种形式  
Vue.prototype.$bus = new Vue() // Vue 曾经实现了 Bus 的性能  
<template>
    <button @click="toBus"> 子组件传给兄弟组件 </button>
</template>

<script>
export default{
    methods: {toBus () {this.$bus.$emit('foo', '来自兄弟组件')
    }
  }
}
</script>

另一个组件也在钩子函数中监听 on 事件

export default {data() {
    return {message: ''}
  },
  mounted() {this.$bus.$on('foo', (msg) => {this.message = msg})
  }
}

7. $root 拜访根组件中的属性或办法

  • 作用:拜访根组件中的属性或办法
  • 留神:是根组件,不是父组件。$root只对根组件有用
var vm = new Vue({
  el: "#app",
  data() {
    return {rootInfo:"我是根元素的属性"}
  },
  methods: {alerts() {alert(111)
    }
  },
  components: {
    com1: {data() {
        return {info: "组件 1"}
      },
      template: "<p>{{info}} <com2></com2></p>",
      components: {
        com2: {
          template: "<p> 我是组件 1 的子组件 </p>",
          created() {this.$root.alerts()// 根组件办法
            console.log(this.$root.rootInfo)// 我是根元素的属性
          }
        }
      }
    }
  }
});

8. vuex

  • 实用场景: 简单关系的组件数据传递
  • Vuex 作用相当于一个用来存储共享变量的容器
  • state用来寄存共享变量的中央
  • getter,能够减少一个 getter 派生状态,(相当于 store 中的计算属性),用来取得共享变量的值
  • mutations用来寄存批改 state 的办法。
  • actions也是用来寄存批改 state 的办法,不过 action 是在 mutations 的根底上进行。罕用来做一些异步操作

小结

  • 父子关系的组件数据传递抉择 props$emit进行传递,也可抉择ref
  • 兄弟关系的组件数据传递可抉择 $bus,其次能够抉择$parent 进行传递
  • 先人与后辈组件数据传递可抉择 attrslisteners或者 ProvideInject
  • 简单关系的组件数据传递能够通过 vuex 寄存共享的变量

实现双向绑定

咱们还是以 Vue 为例,先来看看 Vue 中的双向绑定流程是什么的

  1. new Vue()首先执行初始化,对 data 执行响应化解决,这个过程产生 Observe
  2. 同时对模板执行编译,找到其中动静绑定的数据,从 data 中获取并初始化视图,这个过程产生在 Compile
  3. 同时定义⼀个更新函数和 Watcher,未来对应数据变动时Watcher 会调用更新函数
  4. 因为 data 的某个 key 在⼀个视图中可能呈现屡次,所以每个 key 都须要⼀个管家 Dep 来治理多个Watcher
  5. 未来 data 中数据⼀旦发生变化,会首先找到对应的 Dep,告诉所有Watcher 执行更新函数

流程图如下:

先来一个构造函数:执行初始化,对 data 执行响应化解决

class Vue {constructor(options) {  
    this.$options = options;  
    this.$data = options.data;  

    // 对 data 选项做响应式解决  
    observe(this.$data);  

    // 代理 data 到 vm 上  
    proxy(this);  

    // 执行编译  
    new Compile(options.el, this);  
  }  
}  

data 选项执行响应化具体操作

function observe(obj) {if (typeof obj !== "object" || obj == null) {return;}  
  new Observer(obj);  
}  

class Observer {constructor(value) {  
    this.value = value;  
    this.walk(value);  
  }  
  walk(obj) {Object.keys(obj).forEach((key) => {defineReactive(obj, key, obj[key]);  
    });  
  }  
}  

编译Compile

对每个元素节点的指令进行扫描跟解析, 依据指令模板替换数据, 以及绑定相应的更新函数

class Compile {constructor(el, vm) {  
    this.$vm = vm;  
    this.$el = document.querySelector(el);  // 获取 dom  
    if (this.$el) {this.compile(this.$el);  
    }  
  }  
  compile(el) {  
    const childNodes = el.childNodes;   
    Array.from(childNodes).forEach((node) => { // 遍历子元素  
      if (this.isElement(node)) {   // 判断是否为节点  
        console.log("编译元素" + node.nodeName);  
      } else if (this.isInterpolation(node)) {console.log("编译插值⽂本" + node.textContent);  // 判断是否为插值文本 {{}}  
      }  
      if (node.childNodes && node.childNodes.length > 0) {  // 判断是否有子元素  
        this.compile(node);  // 对子元素进行递归遍历  
      }  
    });  
  }  
  isElement(node) {return node.nodeType == 1;}  
  isInterpolation(node) {return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);  
  }  
}  

依赖收集

视图中会用到 data 中某 key,这称为依赖。同⼀个key 可能呈现屡次,每次都须要收集进去用⼀个 Watcher 来保护它们,此过程称为依赖收集多个 Watcher 须要⼀个 Dep 来治理,须要更新时由 Dep 统⼀告诉

实现思路

  1. defineReactive时为每⼀个 key 创立⼀个 Dep 实例
  2. 初始化视图时读取某个key,例如name1,创立⼀个watcher1
  3. 因为触发 name1getter办法,便将 watcher1 增加到 name1 对应的 Dep
  4. name1 更新,setter触发时,便可通过对应 Dep 告诉其治理所有 Watcher 更新
// 负责更新视图  
class Watcher {constructor(vm, key, updater) {  
    this.vm = vm  
    this.key = key  
    this.updaterFn = updater  

    // 创立实例时,把以后实例指定到 Dep.target 动态属性上  
    Dep.target = this  
    // 读一下 key,触发 get  
    vm[key]  
    // 置空  
    Dep.target = null  
  }  

  // 将来执行 dom 更新函数,由 dep 调用的  
  update() {this.updaterFn.call(this.vm, this.vm[this.key])  
  }  
}  

申明Dep

class Dep {constructor() {this.deps = [];  // 依赖治理  
  }  
  addDep(dep) {this.deps.push(dep);  
  }  
  notify() {this.deps.forEach((dep) => dep.update());  
  }  
} 

创立 watcher 时触发getter

class Watcher {constructor(vm, key, updateFn) {  
    Dep.target = this;  
    this.vm[this.key];  
    Dep.target = null;  
  }  
}  

依赖收集,创立 Dep 实例

function defineReactive(obj, key, val) {this.observe(val);  
  const dep = new Dep();  
  Object.defineProperty(obj, key, {get() {Dep.target && dep.addDep(Dep.target);// Dep.target 也就是 Watcher 实例  
      return val;  
    },  
    set(newVal) {if (newVal === val) return;  
      dep.notify(); // 告诉 dep 执行更新办法},  
  });  
}  

Vue 组件为什么只能有一个根元素

vue3中没有问题

Vue.createApp({
  components: {
    comp: {
      template: `
        <div>root1</div>
        <div>root2</div>
      `
    }
  }
}).mount('#app')
  1. vue2中组件的确只能有一个根,但 vue3 中组件曾经能够多根节点了。
  2. 之所以须要这样是因为 vdom 是一颗单根树形构造,patch办法在遍历的时候从根节点开始遍历,它要求只有一个根节点。组件也会转换为一个vdom
  3. vue3中之所以能够写多个根节点,是因为引入了 Fragment 的概念,这是一个形象的节点,如果发现组件是多根的,就创立一个 Fragment 节点,把多个根节点作为它的 children。未来patch 的时候,如果发现是一个 Fragment 节点,则间接遍历 children 创立或更新

Vue3.2 setup 语法糖汇总

提醒:vue3.2 版本开始能力应用语法糖!

Vue3.0 中变量必须 return 进去,template 中能力应用;而在 Vue3.2 中只须要在 script 标签上加上 setup 属性,无需 returntemplate 便可间接应用,十分的香啊!

1. 如何应用 setup 语法糖

只需在 script 标签上写上 setup

<template>
</template>
<script setup>
</script>
<style scoped lang="less">
</style>

2. data 数据的应用

因为 setup 不需写 return,所以间接申明数据即可

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

const data = reactive({
  patternVisible: false,
  debugVisible: false,
  aboutExeVisible: false,
})

const content = ref('content')
// 应用 toRefs 解构
const {patternVisible, debugVisible, aboutExeVisible} = toRefs(data)
</script>

3. method 办法的应用

<template >
  <button @click="onClickHelp"> 帮忙 </button>
</template>
<script setup>
import {reactive} from 'vue'

const data = reactive({aboutExeVisible: false,})
// 点击帮忙
const onClickHelp = () => {console.log(` 帮忙 `)
  data.aboutExeVisible = true
}
</script>

4. watchEffect 的应用

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

let sum = ref(0)

watchEffect(()=>{
  const x1 = sum.value
  console.log('watchEffect 所指定的回调执行了')
})
</script>

5. watch 的应用

<script setup>
import {
  reactive,
  watch,
} from 'vue'
// 数据
let sum = ref(0)
let msg = ref('hello')
let person = reactive({
  name:'张三',
  age:18,
  job:{
    j1:{salary:20}
  }
})
// 两种监听格局
watch([sum,msg],(newValue,oldValue)=>{console.log('sum 或 msg 变了',newValue,oldValue)
  },
  {immediate:true}
)

watch(()=>person.job,(newValue,oldValue)=>{console.log('person 的 job 变动了',newValue,oldValue)
},{deep:true}) 

</script>

6. computed 计算属性的应用

computed 计算属性有两种写法(简写和思考读写的残缺写法)

<script setup>
import {
  reactive,
  computed,
} from 'vue'

// 数据
let person = reactive({
  firstName:'poetry',
  lastName:'x'
})

// 计算属性简写
person.fullName = computed(()=>{return person.firstName + '-' + person.lastName})

// 残缺写法
person.fullName = computed({get(){return person.firstName + '-' + person.lastName},
  set(value){const nameArr = value.split('-')
    person.firstName = nameArr[0]
    person.lastName = nameArr[1]
  }
})
</script>

7. props 父子传值的应用

父组件代码如下(示例):

<template>
  <child :name='name'/>  
</template>

<script setup>
  import {ref} from 'vue'
  // 引入子组件
  import child from './child.vue'
  let name= ref('poetry')
</script>

子组件代码如下(示例):

<template>
  <span>{{props.name}}</span>
</template>

<script setup>
import {defineProps} from 'vue'
// 申明 props
const props = defineProps({
  name: {
    type: String,
    default: 'poetries'
  }
})  
// 或者
//const props = defineProps(['name'])
</script>

8. emit 子父传值的应用

父组件代码如下(示例):

<template>
  <AdoutExe @aboutExeVisible="aboutExeHandleCancel" />
</template>
<script setup>
import {reactive} from 'vue'
// 导入子组件
import AdoutExe from '../components/AdoutExeCom'

const data = reactive({aboutExeVisible: false,})
// content 组件 ref

// 对于零碎暗藏
const aboutExeHandleCancel = () => {data.aboutExeVisible = false}
</script>

子组件代码如下(示例):

<template>
  <a-button @click="isOk">
    确定
  </a-button>
</template>
<script setup>
import {defineEmits} from 'vue';

// emit
const emit = defineEmits(['aboutExeVisible'])
/**
 * 办法
 */
// 点击确定按钮
const isOk = () => {emit('aboutExeVisible');
}
</script>

9. 获取子组件 ref 变量和 defineExpose 裸露

vue2 中的获取子组件的ref,间接在父组件中管制子组件办法和变量的办法

父组件代码如下(示例):

<template>
  <button @click="onClickSetUp"> 点击 </button>
  <Content ref="content" />
</template>

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

// content 组件 ref
const content = ref('content')
// 点击设置
const onClickSetUp = ({key}) => {content.value.modelVisible = true}
</script>
<style scoped lang="less">
</style>

子组件代码如下(示例):

<template>
   <p>{{data}}</p>
</template>

<script setup>
import {
  reactive,
  toRefs
} from 'vue'

/**
 * 数据局部
* */
const data = reactive({
  modelVisible: false,
  historyVisible: false, 
  reportVisible: false, 
})

defineExpose({...toRefs(data),
})
</script>

10. 路由 useRoute 和 useRouter 的应用

<script setup>
  import {useRoute, useRouter} from 'vue-router'

  // 申明
  const route = useRoute()
  const router = useRouter()

  // 获取 query
  console.log(route.query)
  // 获取 params
  console.log(route.params)

  // 路由跳转
  router.push({path: `/index`})
</script>

11. store 仓库的应用

<script setup>
  import {useStore} from 'vuex'
  import {num} from '../store/index'

  const store = useStore(num)

  // 获取 Vuex 的 state
  console.log(store.state.number)
  // 获取 Vuex 的 getters
  console.log(store.state.getNumber)

  // 提交 mutations
  store.commit('fnName')

  // 散发 actions 的办法
  store.dispatch('fnName')
</script>

12. await 的反对

setup语法糖中可间接应用 await,不须要写asyncsetup 会主动变成async setup

<script setup>
  import api from '../api/Api'
  const data = await Api.getData()
  console.log(data)
</script>

13. provide 和 inject 祖孙传值

父组件代码如下(示例):

<template>
  <AdoutExe />
</template>

<script setup>
  import {ref,provide} from 'vue'
  import AdoutExe from '@/components/AdoutExeCom'

  let name = ref('py')
  // 应用 provide
  provide('provideState', {
    name,
    changeName: () => {name.value = 'poetries'}
  })
</script>

子组件代码如下(示例):

<script setup>
  import {inject} from 'vue'
  const provideState = inject('provideState')

  provideState.changeName()
</script>

Vue 组件渲染和更新过程

渲染组件时,会通过 Vue.extend 办法构建子组件的构造函数,并进行实例化。最终手动调用 $mount() 进行挂载。更新组件时会进行 patchVnode 流程,外围就是diff 算法

怎么监听 vuex 数据的变动

剖析

  • vuex数据状态是响应式的,所以状态变视图跟着变,然而有时还是须要晓得数据状态变了从而做一些事件。
  • 既然状态都是响应式的,那天然能够 watch,另外vuex 也提供了订阅的 API:store.subscribe()

答复范例

  1. 我晓得几种办法:
  2. 能够通过 watch 选项或者 watch 办法监听状态
  3. 能够应用 vuex 提供的 API:store.subscribe()
  4. watch选项形式,能够以字符串模式监听 $store.state.xxsubscribe 形式,能够调用 store.subscribe(cb), 回调函数接管mutation 对象和 state 对象,这样能够进一步判断 mutation.type 是否是期待的那个,从而进一步做后续解决。
  5. watch形式简略好用,且能获取变动前后值,首选;subscribe办法会被所有 commit 行为触发,因而还须要判断 mutation.type,用起来略繁琐,个别用于vuex 插件中

实际

watch形式

const app = createApp({
    watch: {'$store.state.counter'() {console.log('counter change!');
      }
    }
})

subscribe形式:

store.subscribe((mutation, state) => {if (mutation.type === 'add') {console.log('counter change in subscribe()!');
    }
})

Vue 为什么没有相似于 React 中 shouldComponentUpdate 的生命周期

  • 考点: Vue的变动侦测原理
  • 前置常识: 依赖收集、虚构DOM、响应式零碎

根本原因是 VueReact的变动侦测形式有所不同

  • 当 React 晓得发生变化后,会应用 Virtual Dom Diff 进行差别检测,然而很多组件实际上是必定不会发生变化的,这个时候须要 shouldComponentUpdate 进行手动操作来缩小diff,从而进步程序整体的性能
  • Vue在一开始就晓得那个组件产生了变动,不须要手动管制 diff,而组件外部采纳的diff 形式实际上是能够引入相似于 shouldComponentUpdate 相干生命周期的,然而通常正当大小的组件不会有适量的 diff,手动优化的价值无限,因而目前 Vue 并没有思考引入 shouldComponentUpdate 这种手动优化的生命周期

Class 与 Style 如何动静绑定

Class 能够通过对象语法和数组语法进行动静绑定

对象语法:

<div v-bind:class="{active: isActive,'text-danger': hasError}"></div>

data: {
  isActive: true,
  hasError: false
}

数组语法:

<div v-bind:class="[isActive ? activeClass :'', errorClass]"></div>

data: {
  activeClass: 'active',
  errorClass: 'text-danger'
}

Style 也能够通过对象语法和数组语法进行动静绑定

对象语法:

<div v-bind:style="{color: activeColor, fontSize: fontSize +'px'}"></div>

data: {
  activeColor: 'red',
  fontSize: 30
}

数组语法:

<div v-bind:style="[styleColor, styleSize]"></div>

data: {
  styleColor: {color: 'red'},
  styleSize:{fontSize:'23px'}
}

v-once 的应用场景有哪些

剖析

v-onceVue 中内置指令,很有用的API,在优化方面常常会用到

体验

仅渲染元素和组件一次,并且跳过将来更新

<!-- single element -->
<span v-once>This will never change: {{msg}}</span>
<!-- the element have children -->
<div v-once>
  <h1>comment</h1>
  <p>{{msg}}</p>
</div>
<!-- component -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` directive -->
<ul>
  <li v-for="i in list" v-once>{{i}}</li>
</ul>

答复范例

  • v-oncevue 的内置指令,作用是仅渲染指定组件或元素一次,并跳过将来对其更新
  • 如果咱们有一些元素或者组件在初始化渲染之后不再须要变动,这种状况下适宜应用 v-once,这样哪怕这些数据变动,vue 也会跳过更新,是一种代码优化伎俩
  • 咱们只须要作用的组件或元素上加上 v-once 即可
  • vue3.2之后,又减少了 v-memo 指令,能够有条件缓存局部模板并管制它们的更新,能够说控制力更强了
  • 编译器发现元素下面有 v-once 时,会将首次计算结果存入缓存对象,组件再次渲染时就会从缓存获取,从而防止再次计算

原理

上面例子应用了v-once

<script setup>
import {ref} from 'vue'
​
const msg = ref('Hello World!')
</script>
​
<template>
  <h1 v-once>{{msg}}</h1>
  <input v-model="msg">
</template>

咱们发现 v-once 呈现后,编译器会缓存作用元素或组件,从而防止当前更新时从新计算这一部分:

// ...
return (_ctx, _cache) => {return (_openBlock(), _createElementBlock(_Fragment, null, [
    // 从缓存获取 vnode
    _cache[0] || (_setBlockTracking(-1),
      _cache[0] = _createElementVNode("h1", null, [_createTextVNode(_toDisplayString(msg.value), 1 /* TEXT */)
      ]),
      _setBlockTracking(1),
      _cache[0]
    ),
// ...
退出移动版