关于前端:美团前端vue面试题

35次阅读

共计 29630 个字符,预计需要花费 75 分钟才能阅读完成。

Composition API 与 Options API 有什么不同

剖析

Vue3最重要更新之一就是 Composition API,它具备一些列长处,其中不少是针对Options API 裸露的一些问题量身打造。是 Vue3 举荐的写法,因而把握好 Composition API 利用对把握好 Vue3 至关重要

What is Composition API?(opens new window)

  • Composition API呈现就是为了解决 Options API 导致雷同性能代码扩散的景象

体验

Composition API能更好的组织代码,上面用 composition api 能够提取为useCount(),用于组合、复用

compositon api 提供了以下几个函数:

  • setup
  • ref
  • reactive
  • watchEffect
  • watch
  • computed
  • toRefs
  • 生命周期的hooks

答复范例

  1. Composition API是一组 API,包含:Reactivity API 生命周期钩子 依赖注入 ,使用户能够通过导入函数形式编写vue 组件。而 Options API 则通过申明组件选项的对象模式编写组件
  2. Composition API最次要作用是可能简洁、高效复用逻辑。解决了过来 Options APImixins的各种毛病;另外 Composition API 具备更加麻利的代码组织能力,很多用户喜爱 Options API,认为所有货色都有固定地位的选项搁置代码,然而单个组件增长过大之后这反而成为限度,一个逻辑关注点扩散在组件各处,造成代码碎片,保护时须要重复横跳,Composition API 则能够将它们无效组织在一起。最初 Composition API 领有更好的类型推断,对 ts 反对更敌对,Options API在设计之初并未思考类型推断因素,尽管官网为此做了很多简单的类型体操,确保用户能够在应用 Options API 时取得类型推断,然而还是没方法用在 mixinsprovide/inject
  3. Vue3首推 Composition API,然而这会让咱们在代码组织上多花点心理,因而在抉择上,如果咱们我的项目属于中低复杂度的场景,Options API 仍是一个好抉择。对于那些大型,高扩大,强保护的我的项目上,Composition API会取得更大收益

可能的诘问

  1. Composition API是否和 Options API 一起应用?

能够在同一个组件中应用两个 script 标签,一个应用 vue3,一个应用 vue2 写法,一起应用没有问题

<!-- vue3 -->
<script setup>
  // vue3 写法
</script>

<!-- 降级 vue2 -->
<script>
  export default {data() {},
    methods: {}}
</script>

v-if 和 v -show 区别

  • v-show暗藏则是为该元素增加 css--display:nonedom 元素仍旧还在。v-if显示暗藏是将 dom 元素整个增加或删除
  • 编译过程:v-if切换有一个部分编译 / 卸载的过程,切换过程中适合地销毁和重建外部的事件监听和子组件;v-show只是简略的基于 css 切换
  • 编译条件:v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染
  • v-showfalse 变为 true 的时候不会触发组件的生命周期
  • v-iffalse 变为 true 的时候,触发组件的 beforeCreatecreatebeforeMountmounted 钩子,由 true 变为 false 的时候触发组件的 beforeDestorydestoryed 办法
  • 性能耗费:v-if有更高的切换耗费;v-show有更高的初始渲染耗费

v-show 与 v -if 的应用场景

  • v-ifv-show 都能管制 dom 元素在页面的显示
  • v-if 相比 v-show 开销更大的(间接操作 dom 节 点减少与删除)
  • 如果须要十分频繁地切换,则应用 v-show 较好
  • 如果在运行时条件很少扭转,则应用 v-if 较好

v-show 与 v -if 原理剖析

  1. v-show原理

不论初始条件是什么,元素总是会被渲染

咱们看一下在 vue 中是如何实现的

代码很好了解,有 transition 就执行 transition,没有就间接设置display 属性

// https://github.com/vuejs/vue-next/blob/3cd30c5245da0733f9eb6f29d220f39c46518162/packages/runtime-dom/src/directives/vShow.ts
export const vShow: ObjectDirective<VShowElement> = {beforeMount(el, { value}, {transition}) {
    el._vod = el.style.display === 'none' ? '' : el.style.display
    if (transition && value) {transition.beforeEnter(el)
    } else {setDisplay(el, value)
    }
  },
  mounted(el, { value}, {transition}) {if (transition && value) {transition.enter(el)
    }
  },
  updated(el, { value, oldValue}, {transition}) {// ...},
  beforeUnmount(el, { value}) {setDisplay(el, value)
  }
}
  1. v-if原理

v-if在实现上比 v-show 要简单的多,因为还有else else-if 等条件须要解决,这里咱们也只摘抄源码中解决 v-if 的一小部分

返回一个 node 节点,render函数通过表达式的值来决定是否生成DOM

// https://github.com/vuejs/vue-next/blob/cdc9f336fd/packages/compiler-core/src/transforms/vIf.ts
export const transformIf = createStructuralDirectiveTransform(/^(if|else|else-if)$/,
  (node, dir, context) => {return processIf(node, dir, context, (ifNode, branch, isRoot) => {
      // ...
      return () => {if (isRoot) {
          ifNode.codegenNode = createCodegenNodeForBranch(
            branch,
            key,
            context
          ) as IfConditionalExpression
        } else {
          // attach this branch's codegen node to the v-if root.
          const parentCondition = getParentCondition(ifNode.codegenNode!)
          parentCondition.alternate = createCodegenNodeForBranch(
            branch,
            key + ifNode.branches.length - 1,
            context
          )
        }
      }
    })
  }
)

v-if 和 v -for 哪个优先级更高

  • 实际中不应该把 v-forv-if放一起
  • vue2 中,v-for的优先级是高于 v-if,把它们放在一起,输入的渲染函数中能够看出会先执行循环再判断条件,哪怕咱们只渲染列表中一小部分元素,也得在每次重渲染的时候遍历整个列表,这会比拟节约;另外须要留神的是在vue3 中则齐全相同,v-if的优先级高于 v-for,所以v-if 执行时,它调用的变量还不存在,就会导致异样
  • 通常有两种状况下导致咱们这样做:

    • 为了过滤列表中的我的项目 (比方 v-for="user in users" v-if="user.isActive")。此时定义一个计算属性 (比方 activeUsers),让其返回过滤后的列表即可(比方users.filter(u=>u.isActive)
    • 为了防止渲染本应该被暗藏的列表 (比方 v-for="user in users" v-if="shouldShowUsers")。此时把 v-if 挪动至容器元素上 (比方 ulol)或者外面包一层 template 即可
  • 文档中明确指出永远不要把 v-ifv-for 同时用在同一个元素上,显然这是一个重要的注意事项
  • 源码外面对于代码生成的局部,可能清晰的看到是先解决 v-if 还是 v-for,程序上vue2vue3正好相同,因而产生了一些症状的不同,然而不管怎样都是不能把它们写在一起的

vue2.x 源码剖析

在 vue 模板编译的时候,会将指令系统转化成可执行的 render 函数

编写一个 p 标签,同时应用 v-ifv-for

<div id="app">
  <p v-if="isShow" v-for="item in items">
    {{item.title}}
  </p>
</div>

创立 vue 实例,寄存 isShowitems数据

const app = new Vue({
  el: "#app",
  data() {
    return {
      items: [{ title: "foo"},
        {title: "baz"}]
    }
  },
  computed: {isShow() {return this.items && this.items.length > 0}
  }
})

模板指令的代码都会生成在 render 函数中,通过 app.$options.render 就能失去渲染函数

ƒ anonymous() {with (this) { return 
    _c('div', { attrs: { "id": "app"} }, 
    _l((items), function (item) 
    {return (isShow) ? _c('p', [_v("\n" + _s(item.title) + "\n")]) : _e()}), 0) }
}
  • _lvue 的列表渲染函数,函数外部都会进行一次 if 判断
  • 初步失去论断:v-for优先级是比v-i f 高
  • 再将 v-forv-if置于不同标签
<div id="app">
  <template v-if="isShow">
    <p v-for="item in items">{{item.title}}</p>
  </template>
</div>

再输入下 render 函数

ƒ anonymous() {with(this){return 
    _c('div',{attrs:{"id":"app"}},
    [(isShow)?[_v("\n"),
    _l((items),function(item){return _c('p',[_v(_s(item.title))])})]:_e()],2)}
}

这时候咱们能够看到,v-forv-if 作用在不同标签时候,是先进行判断,再进行列表的渲染

咱们再在查看下 vue 源码

源码地位:\vue-dev\src\compiler\codegen\index.js

export function genElement (el: ASTElement, state: CodegenState): string {if (el.parent) {el.pre = el.pre || el.parent.pre}
  if (el.staticRoot && !el.staticProcessed) {return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {return genSlot(el, state)
  } else {
    // component or element
    ...
}

在进行 if 判断的时候,v-for是比 v-if 先进行判断

最终论断:v-for优先级比 v-if

watch 原理

watch 实质上是为每个监听属性 setter 创立了一个 watcher,当被监听的属性更新时,调用传入的回调函数。常见的配置选项有 deepimmediate,对应原理如下

  • deep:深度监听对象,为对象的每一个属性创立一个 watcher,从而确保对象的每一个属性更新时都会触发传入的回调函数。次要起因在于对象属于援用类型,单个属性的更新并不会触发对象 setter,因而引入 deep 可能很好地解决监听对象的问题。同时也会引入判断机制,确保在多个属性更新时回调函数仅触发一次,防止性能节约。
  • immediate:在初始化时间接调用回调函数,能够通过在 created 阶段手动调用回调函数实现雷同的成果

用过 pinia 吗?有什么长处?

1. pinia 是什么?

  • Vue3 中,能够应用传统的 Vuex 来实现状态治理,也能够应用最新的 pinia 来实现状态治理,咱们来看看官网如何解释 pinia 的:PiniaVue 的存储库,它容许您跨组件 / 页面共享状态。
  • 实际上,pinia就是 Vuex 的升级版,官网也说过,为了尊重原作者,所以取名 pinia,而没有取名Vuex,所以大家能够间接将pinia 比作为 Vue3Vuex

2. 为什么要应用 pinia?

  • Vue2Vue3 都反对,这让咱们同时应用 Vue2Vue3的小伙伴都能很快上手。
  • pinia中只有 stategetteraction,摈弃了Vuex 中的 MutationVuexmutation始终都不太受小伙伴们的待见,pinia间接摈弃它了,这无疑缩小了咱们工作量。
  • piniaaction 反对同步和异步,Vuex不反对
  • 良好的 Typescript 反对,毕竟咱们 Vue3 都举荐应用 TS 来编写,这个时候应用 pinia 就十分适合了
  • 无需再创立各个模块嵌套了,Vuex中如果数据过多,咱们通常分模块来进行治理,稍显麻烦,而 pinia 中每个 store 都是独立的,相互不影响。
  • 体积十分小,只有 1KB 左右。
  • pinia反对插件来扩大本身性能。
  • 反对服务端渲染

3. pinna 应用

pinna 文档(opens new window)

  1. 筹备工作

咱们这里搭建一个最新的 Vue3 + TS + Vite 我的项目

npm create vite@latest my-vite-app --template vue-ts
  1. pinia根底应用
yarn add pinia
// main.ts
import {createApp} from "vue";
import App from "./App.vue";
import {createPinia} from "pinia";
const pinia = createPinia();

const app = createApp(App);
app.use(pinia);
app.mount("#app");

2.1 创立store

//sbinsrc/store/user.ts
import {defineStore} from 'pinia'

// 第一个参数是应用程序中 store 的惟一 id
export const useUsersStore = defineStore('users', {// 其它配置项})

创立 store 很简略,调用 p inia中的 defineStore 函数即可,该函数接管两个参数:

  • name:一个字符串,必传项,该 store 的惟一id
  • options:一个对象,store的配置项,比方配置 store 内的数据,批改数据的办法等等。

咱们能够定义任意数量的 store,因为咱们其实一个store 就是一个函数,这也是 pinia 的益处之一,让咱们的代码扁平化了,这和 Vue3 的实现思维是一样的

2.2 应用store

<!-- src/App.vue -->
<script setup lang="ts">
import {useUsersStore} from "../src/store/user";
const store = useUsersStore();
console.log(store);
</script>

2.3 增加state

export const useUsersStore = defineStore("users", {state: () => {
    return {
      name: "test",
      age: 20,
      sex: "男",
    };
  },
});

2.4 读取 state 数据

<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <p> 姓名:{{name}}</p>
  <p> 年龄:{{age}}</p>
  <p> 性别:{{sex}}</p>
</template>
<script setup lang="ts">
import {ref} from "vue";
import {useUsersStore} from "../src/store/user";
const store = useUsersStore();
const name = ref<string>(store.name);
const age = ref<number>(store.age);
const sex = ref<string>(store.sex);
</script>

上段代码中咱们间接通过 store.age 等形式获取到了 store 存储的值,然而大家有没有发现,这样比拟繁琐,咱们其实能够用解构的形式来获取值,使得代码更简洁一点

import {useUsersStore, storeToRefs} from "../src/store/user";
const store = useUsersStore();
const {name, age, sex} = storeToRefs(store); // storeToRefs 获取的值是响应式的

2.5 批改 state 数据

<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <p> 姓名:{{name}}</p>
  <p> 年龄:{{age}}</p>
  <p> 性别:{{sex}}</p>
  <button @click="changeName"> 更改姓名 </button>
</template>
<script setup lang="ts">
import child from './child.vue';
import {useUsersStore, storeToRefs} from "../src/store/user";
const store = useUsersStore();
const {name, age, sex} = storeToRefs(store);
const changeName = () => {
  store.name = "张三";
  console.log(store);
};
</script>

2.6 重置state

  • 有时候咱们批改了 state 数据,想要将它还原,这个时候该怎么做呢?就比方用户填写了一部分表单,忽然想重置为最初始的状态。
  • 此时,咱们间接调用 store$reset()办法即可,持续应用咱们的例子,增加一个重置按钮
<button @click="reset"> 重置 store</button>
// 重置 store
const reset = () => {store.$reset();
};

当咱们点击重置按钮时,store中的数据会变为初始状态,页面也会更新

2.7 批量更改 state 数据

如果咱们一次性须要批改很多条数据的话,有更加简便的办法,应用 store$patch办法,批改 app.vue 代码,增加一个批量更改数据的办法

<button @click="patchStore"> 批量批改数据 </button>
// 批量批改数据
const patchStore = () => {
  store.$patch({
    name: "张三",
    age: 100,
    sex: "女",
  });
};
  • 有教训的小伙伴可能发现了,咱们采纳这种批量更改的形式仿佛代价有一点大,如果咱们 state 中有些字段无需更改,然而依照上段代码的写法,咱们必须要将 state 中的所有字段例举出了。
  • 为了解决该问题,pinia提供的 $patch 办法还能够接管一个回调函数,它的用法有点像咱们的数组循环回调函数了。
store.$patch((state) => {state.items.push({ name: 'shoes', quantity: 1})
  state.hasChanged = true
})

2.8 间接替换整个state

pinia提供了办法让咱们间接替换整个 state 对象,应用 store$state办法

store.$state = {counter: 666, name: '张三'}

上段代码会将咱们提前申明的 state 替换为新的对象,可能这种场景用得比拟少

  1. getters属性
  2. gettersdefineStore 参数配置项外面的另一个属性
  3. 能够把 getter 设想成 Vue 中的计算属性,它的作用就是返回一个新的后果,既然它和 Vue 中的计算属性相似,那么它必定也是会被缓存的,就和 computed 一样

3.1 增加getter

export const useUsersStore = defineStore("users", {state: () => {
    return {
      name: "test",
      age: 10,
      sex: "男",
    };
  },
  getters: {getAddAge: (state) => {return state.age + 100;},
  },
})

上段代码中咱们在配置项参数中增加了 getter 属性,该属性对象中定义了一个 getAddAge 办法,该办法会默认接管一个 state 参数,也就是 state 对象,而后该办法返回的是一个新的数据

3.2 应用getter

<template>
  <p> 新年龄:{{store.getAddAge}}</p>
  <button @click="patchStore"> 批量批改数据 </button>
</template>
<script setup lang="ts">
import {useUsersStore} from "../src/store/user";
const store = useUsersStore();
// 批量批改数据
const patchStore = () => {
  store.$patch({
    name: "张三",
    age: 100,
    sex: "女",
  });
};
</script>

上段代码中咱们间接在标签上应用了 store.gettAddAge 办法,这样能够保障响应式,其实咱们 state 中的 name 等属性也能够以此种形式间接在标签上应用,也能够放弃响应式

3.3 getter中调用其它getter

export const useUsersStore = defineStore("users", {state: () => {
    return {
      name: "test",
      age: 20,
      sex: "男",
    };
  },
  getters: {getAddAge: (state) => {return state.age + 100;},
    getNameAndAge(): string {return this.name + this.getAddAge; // 调用其它 getter},
  },
});

3.3 getter传参

export const useUsersStore = defineStore("users", {state: () => {
    return {
      name: "test",
      age: 20,
      sex: "男",
    };
  },
  getters: {getAddAge: (state) => {return (num: number) => state.age + num;
    },
    getNameAndAge(): string {return this.name + this.getAddAge; // 调用其它 getter},
  },
});
<p> 新年龄:{{store.getAddAge(1100) }}</p>
  1. actions属性
  2. 后面咱们提到的 stategetter s 属性都次要是数据层面的,并没有具体的业务逻辑代码,它们两个就和咱们组件代码中的 data 数据和 computed 计算属性一样。
  3. 那么,如果咱们有业务代码的话,最好就是卸载 actions 属性外面,该属性就和咱们组件代码中的 methods 类似,用来搁置一些解决业务逻辑的办法。
  4. actions属性值同样是一个对象,该对象外面也是存储的各种各样的办法,包含同步办法和异步办法

4.1 增加actions

export const useUsersStore = defineStore("users", {state: () => {
    return {
      name: "test",
      age: 20,
      sex: "男",
    };
  },
  getters: {getAddAge: (state) => {return (num: number) => state.age + num;
    },
    getNameAndAge(): string {return this.name + this.getAddAge; // 调用其它 getter},
  },
  actions: {
    // 在理论场景中,该办法能够是任何逻辑,比方发送申请、存储 token 等等。大家把 actions 办法当作一个一般的办法即可,非凡之处在于该办法外部的 this 指向的是以后 store
    saveName(name: string) {this.name = name;},
  },
});

4.2 应用actions

应用 actions 中的办法也非常简单,比方咱们在 App.vue 中想要调用该办法

const saveName = () => {store.saveName("poetries");
};

总结

pinia的知识点很少,如果你有 Vuex 根底,那么学起来更是大海捞针

pinia 无非就是以下 3 个大点:

  • state
  • getters
  • actions

Vue3.0 为什么要用 proxy?

在 Vue2 中,0bject.defineProperty 会扭转原始数据,而 Proxy 是创建对象的虚构示意,并提供 set、get 和 deleteProperty 等处理器,这些处理器可在拜访或批改原始对象上的属性时进行拦挡,有以下特点∶

  • 不需用应用 Vue.$setVue.$delete 触发响应式。
  • 全方位的数组变化检测,打消了 Vue2 有效的边界状况。
  • 反对 Map,Set,WeakMap 和 WeakSet。

Proxy 实现的响应式原理与 Vue2 的实现原理雷同,实现形式大同小异∶

  • get 收集依赖
  • Set、delete 等触发依赖
  • 对于汇合类型,就是对汇合对象的办法做一层包装:原办法执行后执行依赖相干的收集或触发逻辑。

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

$route$router 的区别

  • $route是“路由信息对象”,包含 pathparamshashqueryfullPathmatchedname 等路由信息参数。
  • $router 是“路由实例”对象包含了路由的跳转办法,钩子函数等

delete 和 Vue.delete 删除数组的区别?

  • delete只是被删除的元素变成了 empty/undefined 其余的元素的键值还是不变。
  • Vue.delete间接删除了数组 扭转了数组的键值。
var a=[1,2,3,4]
var b=[1,2,3,4]
delete a[0]
console.log(a)  //[empty,2,3,4]
this.$delete(b,0)
console.log(b)  //[2,3,4]

Vue 修饰符有哪些

vue 中修饰符分为以下五种

  • 表单修饰符
  • 事件修饰符
  • 鼠标按键修饰符
  • 键值修饰符
  • v-bind修饰符

1. 表单修饰符

在咱们填写表单的时候用得最多的是 input 标签,指令用得最多的是v-model

对于表单的修饰符有如下:

  • lazy

在咱们填完信息,光标来到标签的时候,才会将值赋予给 value,也就是在change 事件之后再进行信息同步

<input type="text" v-model.lazy="value">
<p>{{value}}</p>
  • trim

主动过滤用户输出的首空格字符,而两头的空格不会过滤

<input type="text" v-model.trim="value">
  • number

主动将用户的输出值转为数值类型,但如果这个值无奈被 parseFloat 解析,则会返回原来的值

<input v-model.number="age" type="number">

2. 事件修饰符

事件修饰符是对事件捕捉以及指标进行了解决,有如下修饰符

  • .stop 阻止了事件冒泡,相当于调用了 event.stopPropagation 办法
<div @click="shout(2)">
  <button @click.stop="shout(1)">ok</button>
</div>
// 只输入 1 
  • .prevent 阻止了事件的默认行为,相当于调用了 event.preventDefault 办法
<form v-on:submit.prevent="onSubmit"></form>
  • .capture 应用事件捕捉模式,使事件触发从蕴含这个元素的顶层开始往下触发
<div @click.capture="shout(1)">
    obj1
<div @click.capture="shout(2)">
    obj2
<div @click="shout(3)">
    obj3
<div @click="shout(4)">
    obj4
</div>
</div>
</div>
</div>
// 输入构造: 1 2 4 3 
  • .self 只当在 event.target 是以后元素本身时触发处理函数
<div v-on:click.self="doThat">...</div>

应用修饰符时,程序很重要;相应的代码会以同样的程序产生。因而,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素本身的点击

  • .once 绑定了事件当前只能触发一次,第二次就不会触发
<button @click.once="shout(1)">ok</button>
  • .passive 通知浏览器你不想阻止事件的默认行为

在挪动端,当咱们在监听元素滚动事件的时候,会始终触发 onscroll 事件会让咱们的网页变卡,因而咱们应用这个修饰符的时候,相当于给 onscroll 事件整了一个 .lazy 修饰符

<!-- 滚动事件的默认行为 (即滚动行为) 将会立刻触发 -->
<!-- 而不会期待 `onScroll` 实现  -->
<!-- 这其中蕴含 `event.preventDefault()` 的状况 -->
<div v-on:scroll.passive="onScroll">...</div>
  • 不要把 .passive.prevent 一起应用, 因为 .prevent 将会被疏忽,同时浏览器可能会向你展现一个正告。
  • passive 会通知浏览器你不想阻止事件的默认行为
  • native 让组件变成像 html 内置标签那样监听根元素的原生事件,否则组件上应用 v-on 只会监听自定义事件
<my-component v-on:click.native="doSomething"></my-component>

<!-- 应用.native 修饰符来操作一般 HTML 标签是会令事件生效的 -->

3. 鼠标按钮修饰符

鼠标按钮修饰符针对的就是左键、右键、中键点击,有如下:

  • .left 左键点击
  • .right 右键点击
  • .middle 中键点击
<button @click.left="shout(1)">ok</button>
<button @click.right="shout(1)">ok</button>
<button @click.middle="shout(1)">ok</button>

4. 键盘事件的修饰符

键盘修饰符是用来润饰键盘事件(onkeyuponkeydown)的,有如下:

keyCode存在很多,但 vue 为咱们提供了别名,分为以下两种:

  • 一般键entertabdeletespaceescupdownleftright…)
  • 零碎润饰键ctrlaltmetashift…)
<!-- 只有按键为 keyCode 的时候才触发 -->
<input type="text" @keyup.keyCode="shout()">

还能够通过以下形式自定义一些全局的键盘码别名

Vue.config.keyCodes.f2 = 113

5. v-bind 修饰符

v-bind修饰符次要是为属性进行操作,用来别离有如下:

  • async 能对 props 进行一个双向绑定
// 父组件
<comp :myMessage.sync="bar"></comp> 
// 子组件
this.$emit('update:myMessage',params);

以上这种办法相当于以下的简写

// 父亲组件
<comp :myMessage="bar" @update:myMessage="func"></comp>
func(e){this.bar = e;}

// 子组件 js
func2(){this.$emit('update:myMessage',params);
}

应用 async 须要留神以下两点:

  • 应用 sync 的时候,子组件传递的事件名格局必须为 update:value,其中value 必须与子组件中 props 中申明的名称完全一致
  • 留神带有 .sync 修饰符的 v-bind 不能和表达式一起应用
  • prop 设置自定义标签属性,防止裸露数据,避免净化 HTML 构造
<input id="uid" title="title1" value="1" :index.prop="index">
  • camel 将命名变为驼峰命名法,如将 view-Box 属性名转换为 viewBox
<svg :viewBox="viewBox"></svg>

利用场景

依据每一个修饰符的性能,咱们能够失去以下修饰符的利用场景:

  • .stop:阻止事件冒泡
  • .native:绑定原生事件
  • .once:事件只执行一次
  • .self:将事件绑定在本身身上,相当于阻止事件冒泡
  • .prevent:阻止默认事件
  • .caption:用于事件捕捉
  • .once:只触发一次
  • .keyCode:监听特定键盘按下
  • .right:右键

如果让你从零开始写一个 vuex,说说你的思路

思路剖析

这个题目很有难度,首先思考 vuex 解决的问题:存储用户全局状态并提供治理状态 API。

  • vuex需要剖析
  • 如何实现这些需要

答复范例

  1. 官网说 vuex 是一个状态管理模式和库,并确保这些状态以可预期的形式变更。可见要实现一个vuex
  2. 要实现一个 Store 存储全局状态
  3. 要提供批改状态所需 API:commit(type, payload), dispatch(type, payload)
  4. 实现 Store 时,能够定义 Store 类,构造函数接管选项 options,设置属性state 对外裸露状态,提供 commitdispatch批改属性 state。这里须要设置state 为响应式对象,同时将 Store 定义为一个 Vue 插件
  5. commit(type, payload)办法中能够获取用户传入 mutations 并执行它,这样能够按用户提供的办法批改状态。dispatch(type, payload)相似,但须要留神它可能是异步的,须要返回一个 Promise 给用户以解决异步后果

实际

Store的实现:

class Store {constructor(options) {this.state = reactive(options.state)
        this.options = options
    }
    commit(type, payload) {this.options.mutations[type].call(this, this.state, payload)
    }
}

vuex 简易版

/**
 * 1 实现插件,挂载 $store
 * 2 实现 store
 */

let Vue;

class Store {constructor(options) {
    // state 响应式解决
    // 内部拜访:this.$store.state.***
    // 第一种写法
    // this.state = new Vue({
    //   data: options.state
    // })

    // 第二种写法:避免外界间接接触外部 vue 实例,避免内部强行变更
    this._vm = new Vue({
      data: {$$state: options.state}
    })

    this._mutations = options.mutations
    this._actions = options.actions
    this.getters = {}
    options.getters && this.handleGetters(options.getters)

    this.commit = this.commit.bind(this)
    this.dispatch = this.dispatch.bind(this)
  }

  get state () {return this._vm._data.$$state}

  set state (val) {return new Error('Please use replaceState to reset state')
  }

  handleGetters (getters) {Object.keys(getters).map(key => {
      Object.defineProperty(this.getters, key, {get: () => getters[key](this.state)
      })
    })
  }

  commit (type, payload) {let entry = this._mutations[type]
    if (!entry) {return new Error(`${type} is not defined`)
    }

    entry(this.state, payload)
  }

  dispatch (type, payload) {let entry = this._actions[type]
    if (!entry) {return new Error(`${type} is not defined`)
    }

    entry(this, payload)
  }
}

const install = (_Vue) => {
  Vue = _Vue

  Vue.mixin({beforeCreate () {if (this.$options.store) {Vue.prototype.$store = this.$options.store}
    },
  })
}


export default {Store, install}

验证形式

import Vue from 'vue'
import Vuex from './vuex'
// this.$store
Vue.use(Vuex)

export default new Vuex.Store({
  state: {counter: 0},
  mutations: {
    // state 从哪里来的
    add (state) {state.counter++}
  },
  getters: {doubleCounter (state) {return state.counter * 2}
  },
  actions: {add ({ commit}) {setTimeout(() => {commit('add')
      }, 1000)
    }
  },
  modules: {}})

怎么缓存以后的组件?缓存后怎么更新

缓存组件应用 keep-alive 组件,这是一个十分常见且有用的优化伎俩,vue3keep-alive 有比拟大的更新,能说的点比拟多

思路

  • 缓存用keep-alive,它的作用与用法
  • 应用细节,例如缓存指定 / 排除、联合 routertransition
  • 组件缓存后更新能够利用 activated 或者beforeRouteEnter
  • 原理论述

答复范例

  1. 开发中缓存组件应用 keep-alive 组件,keep-alivevue 内置组件,keep-alive包裹动静组件 component 时,会缓存不流动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,避免反复渲染DOM
<keep-alive>
  <component :is="view"></component>
</keep-alive>
  1. 联合属性 includeexclude能够明确指定缓存哪些组件或排除缓存指定组件。vue3中联合 vue-router 时变动较大,之前是 keep-alive 包裹 router-view,当初须要反过来用router-view 包裹keep-alive
<router-view v-slot="{Component}">
  <keep-alive>
    <component :is="Component"></component>
  </keep-alive>
</router-view>
  1. 缓存后如果要获取数据,解决方案能够有以下两种
  2. beforeRouteEnter:在有 vue-router 的 我的项目,每次进入路由的时候,都会执行beforeRouteEnter
beforeRouteEnter(to, from, next){
  next(vm=>{console.log(vm)
    // 每次进入路由执行
    vm.getData()  // 获取数据})
},
  • actived:在 keep-alive 缓存的组件被激活的时候,都会执行 actived 钩子
activated(){this.getData() // 获取数据
},
  1. keep-alive是一个通用组件,它外部定义了一个 map,缓存创立过的组件实例,它返回的渲染函数外部会查找内嵌的component 组件对应组件的 vnode,如果该组件在map 中存在就间接返回它。因为 componentis属性是个响应式数据,因而只有它变动,keep-aliverender 函数就会从新执行

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

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

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

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

Vue 中的过滤器理解吗?过滤器的利用场景有哪些?

过滤器本质不扭转原始数据,只是对数据进行加工解决后返回过滤后的数据再进行调用解决,咱们也能够了解其为一个纯函数

Vue 容许你自定义过滤器,可被用于一些常见的文本格式化

ps: Vue3中已废除filter

如何用

vue 中的过滤器能够用在两个中央:双花括号插值和 v-bind 表达式,过滤器应该被增加在 JavaScript 表达式的尾部,由“管道”符号批示:

<!-- 在双花括号中 -->
{message | capitalize}

<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>

定义 filter

在组件的选项中定义本地的过滤器

filters: {capitalize: function (value) {if (!value) return ''
    value = value.toString()
    return value.charAt(0).toUpperCase() + value.slice(1)
  }
}

定义全局过滤器:

Vue.filter('capitalize', function (value) {if (!value) return ''
  value = value.toString()
  return value.charAt(0).toUpperCase() + value.slice(1)
})

new Vue({// ...})

留神:当全局过滤器和部分过滤器重名时,会采纳部分过滤器

过滤器函数总接管表达式的值 (之前的操作链的后果) 作为第一个参数。在上述例子中,capitalize 过滤器函数将会收到 message 的值作为第一个参数

过滤器能够串联:

{message | filterA | filterB}

在这个例子中,filterA 被定义为接管单个参数的过滤器函数,表达式 message 的值将作为参数传入到函数中。而后持续调用同样被定义为接管单个参数的过滤器函数 filterB,将 filterA 的后果传递到 filterB 中。

过滤器是 JavaScript函数,因而能够接管参数:

{{message | filterA('arg1', arg2) }}

这里,filterA 被定义为接管三个参数的过滤器函数。

其中 message 的值作为第一个参数,一般字符串 'arg1' 作为第二个参数,表达式 arg2 的值作为第三个参数

举个例子:

<div id="app">
  <p>{{msg | msgFormat('疯狂','--')}}</p>
</div>

<script>
    // 定义一个 Vue 全局的过滤器,名字叫做  msgFormat
    Vue.filter('msgFormat', function(msg, arg, arg2) {
        // 字符串的  replace 办法,第一个参数,除了可写一个 字符串之外,还能够定义一个正则
        return msg.replace(/ 单纯 /g, arg+arg2)
    })
</script>

小结:

  • 部过滤器优先于全局过滤器被调用
  • 一个表达式能够应用多个过滤器。过滤器之间须要用管道符“|”隔开。其执行程序从左往右

利用场景

平时开发中,须要用到过滤器的中央有很多,比方 单位转换 数字打点 文本格式化 工夫格式化 之类的等

比方咱们要实现将30000 => 30,000,这时候咱们就须要应用过滤器

Vue.filter('toThousandFilter', function (value) {if (!value) return ''
  value = value.toString()
  return .replace(str.indexOf('.') > -1 ? /(\d)(?=(\d{3})+\.)/g : /(\d)(?=(?:\d{3})+$)/g, '$1,')
})

原理剖析

应用过滤器

{{message | capitalize}}

在模板编译阶段过滤器表达式将会被编译为过滤器函数,次要是用过parseFilters,咱们放到最初讲

_s(_f('filterFormat')(message))

首先剖析一下_f

_f 函数全名是:resolveFilter,这个函数的作用是从 this.$options.filters 中找出注册的过滤器并返回

// 变为
this.$options.filters['filterFormat'](message) // message 为参数

对于resolveFilter

import {indentity,resolveAsset} from 'core/util/index' 

export function resolveFilter(id){return resolveAsset(this.$options,'filters',id,true) || identity
}

外部间接调用 resolveAsset,将option 对象,类型,过滤器id,以及一个触发正告的标记作为参数传递,如果找到,则返回过滤器;

resolveAsset的代码如下:

export function resolveAsset(options,type,id,warnMissing){ // 因为咱们找的是过滤器,所以在 resolveFilter 函数中调用时 type 的值间接给的 'filters', 理论这个函数还能够拿到其余很多货色
  if(typeof id !== 'string'){ // 判断传递的过滤器 id 是不是字符串,不是则间接返回
      return 
  }
  const assets = options[type]  // 将咱们注册的所有过滤器保留在变量中
  // 接下来的逻辑便是判断 id 是否在 assets 中存在,即进行匹配
  if(hasOwn(assets,id)) return assets[id] // 如找到,间接返回过滤器
  // 没有找到,代码继续执行
  const camelizedId  = camelize(id) // 万一你是驼峰的呢
  if(hasOwn(assets,camelizedId)) return assets[camelizedId]
  // 没找到,继续执行
  const PascalCaseId = capitalize(camelizedId) // 万一你是首字母大写的驼峰呢
  if(hasOwn(assets,PascalCaseId)) return assets[PascalCaseId]
  // 如果还是没找到,则查看原型链(即拜访属性)
  const result = assets[id] || assets[camelizedId] || assets[PascalCaseId]
  // 如果仍然没找到,则在非生产环境的控制台打印正告
  if(process.env.NODE_ENV !== 'production' && warnMissing && !result){warn('Failed to resolve' + type.slice(0,-1) + ':' + id, options)
  }
  // 无论是否找到,都返回查找后果
  return result
}

上面再来剖析一下_s

_s 函数的全称是 toString, 过滤器解决后的后果会当作参数传递给 toString函数,最终 toString函数执行后的后果会保留到 Vnode 中的 text 属性中,渲染到视图中

function toString(value){
  return value == null
  ? '': typeof value ==='object'
    ? JSON.stringify(value,null,2)// JSON.stringify()第三个参数可用来管制字符串外面的间距
    : String(value)
}

最初,在剖析下parseFilters,在模板编译阶段应用该函数阶段将模板过滤器解析为过滤器函数调用表达式

function parseFilters (filter) {let filters = filter.split('|')
    let expression = filters.shift().trim() // shift()删除数组第一个元素并将其返回,该办法会更改原数组
    let i
    if (filters) {for(i = 0;i < filters.length;i++){experssion = warpFilter(expression,filters[i].trim()) // 这里传进去的 expression 实际上是管道符号后面的字符串,即过滤器的第一个参数
        }
    }
    return expression
}
// warpFilter 函数实现
function warpFilter(exp,filter){
    // 首先判断过滤器是否有其余参数
    const i = filter.indexof('(')
    if(i<0){ // 不含其余参数,间接进行过滤器表达式字符串的拼接
        return `_f("${filter}")(${exp})`
    }else{const name = filter.slice(0,i) // 过滤器名称
        const args = filter.slice(i+1) // 参数,但还多了‘)’return `_f('${name}')(${exp},${args}` // 留神这一步少给了一个 ')'
    }
}

小结:

  • 在编译阶段通过 parseFilters 将过滤器编译成函数调用(串联过滤器则是一个嵌套的函数调用,前一个过滤器执行的后果是后一个过滤器函数的参数)
  • 编译后通过调用 resolveFilter 函数找到对应过滤器并返回后果
  • 执行后果作为参数传递给 toString 函数,而 toString 执行后,其后果会保留在 Vnodetext属性中,渲染到视图

Vue 中常见性能优化

编码优化

  1. 应用 v-show 复用DOM:防止反复创立组件
<template>
  <div class="cell">
    <!-- 这种状况用 v -show 复用 DOM,比 v -if 成果好 -->
    <div v-show="value" class="on">
      <Heavy :n="10000"/>
    </div>
    <section v-show="!value" class="off">
      <Heavy :n="10000"/>
    </section>
  </div>
</template>
  1. 正当应用路由懒加载、异步组件,无效拆分 App 尺寸,拜访时才异步加载
const router = createRouter({
  routes: [// 借助 webpack 的 import()实现异步组件
    {path: '/foo', component: () => import('./Foo.vue') }
  ]
})
  1. keep-alive缓存页面:防止反复创立组件实例,且能保留缓存组件状态
<router-view v-slot="{Component}">
    <keep-alive>
    <component :is="Component"></component>
  </keep-alive>
</router-view>
  1. v-oncev-memo:不再变动的数据应用v-once
<!-- 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-momo:上面这个列表只会更新选中状态变动项

<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
  <p>ID: {{item.id}} - selected: {{item.id === selected}}</p>
  <p>...more child nodes</p>
</div>
  1. 长列表性能优化:如果是大数据长列表,可采纳虚构滚动,只渲染少部分区域的内容
<recycle-scroller
  class="items"
  :items="items"
  :item-size="24"
>
  <template v-slot="{item}">
    <FetchItemView
      :item="item"
      @vote="voteItem(item)"
    />
  </template>
</recycle-scroller>
  1. 避免外部透露,组件销毁后把全局变量和事件销毁:Vue 组件销毁时,会主动解绑它的全副指令及事件监听器,然而仅限于组件自身的事件
export default {created() {this.timer = setInterval(this.refresh, 2000)
  },
  beforeUnmount() {clearInterval(this.timer)
  }
}
  1. 图片懒加载

对于图片过多的页面,为了减速页面加载速度,所以很多时候咱们须要将页面内未呈现在可视区域内的图片先不做加载,等到滚动到可视区域后再去加载

<!-- 参考 https://github.com/hilongjw/vue-lazyload -->
<img v-lazy="/static/img/1.png">
  1. 滚动到可视区域动静加载

https://tangbc.github.io/vue-virtual-scroll-list(opens new window)

  1. 第三方插件按需引入:(babel-plugin-component

element-plus 这样的第三方组件库能够按需引入防止体积太大

import {createApp} from 'vue';
import {Button, Select} from 'element-plus';
​
const app = createApp()
app.use(Button)
app.use(Select)
  1. 服务端渲染:SSR

如果 SPA 利用有首屏渲染慢的问题,能够思考SSR

以及上面的其余办法

  • 不要将所有的数据都放在 data 中,data中的数据都会减少 gettersetter,会收集对应的watcher
  • v-for 遍历为 item 增加 key
  • v-for 遍历防止同时应用 v-if
  • 辨别 computedwatch 的应用
  • 拆分组件(进步复用性、减少代码的可维护性, 缩小不必要的渲染)
  • 防抖、节流

用户体验

  • app-skeleton 骨架屏
  • pwa serviceworker

SEO 优化

  • 预渲染插件 prerender-spa-plugin
  • 服务端渲染 ssr

打包优化

  • Webpack 对图片进行压缩
  • 应用 cdn 的形式加载第三方模块
  • 多线程打包 happypack
  • splitChunks 抽离公共文件
  • 优化 SourceMap
  • 构建后果输入剖析,利用 webpack-bundle-analyzer 可视化剖析工具

根底的 Web 技术的优化

  • 服务端 gzip 压缩
  • 浏览器缓存
  • CDN 的应用
  • 应用 Chrome Performance 查找性能瓶颈

请说出 vue cli 我的项目中 src 目录每个文件夹和文件的用法

  • assets文件夹是放动态资源;
  • components是放组件;
  • router是定义路由相干的配置;
  • view视图;
  • app.vue是一个利用主组件;
  • main.js是入口文件

为什么 Vue 采纳异步渲染

Vue 是组件级更新,如果不采纳异步更新,那么每次更新数据都会对以后组件进行从新渲染,所以为了性能,Vue 会在本轮数据更新后,在异步更新视图。核心思想 nextTick

源码相干

dep.notify() 告诉 watcher进行更新,subs[i].update 顺次调用 watcherupdatequeueWatcherwatcher 去重放入队列,nextTickflushSchedulerQueue)在下一tick 中刷新 watcher 队列(异步)

update () { /* istanbul ignore else */ 
    if (this.lazy) {this.dirty = true} 
    else if (this.sync) {this.run() 
    } 
    else {queueWatcher(this); // 当数据发生变化时会将 watcher 放到一个队列中批量更新 
    }
}

export function queueWatcher (watcher: Watcher) { 
    const id = watcher.id // 会对雷同的 watcher 进行过滤 
    if (has[id] == null) {has[id] = true 
        if (!flushing) {queue.push(watcher) 
        } else { 
            let i = queue.length - 1 
            while (i > index && queue[i].id > watcher.id) {i--}
            queue.splice(i + 1, 0, watcher) 
        }
        // queue the flush 
        if (!waiting) { 
            waiting = true 
            if (process.env.NODE_ENV !== 'production' && !config.async) {flushSchedulerQueue() 
                return 
            }
            nextTick(flushSchedulerQueue) // 调用 nextTick 办法 批量的进行更新 
        } 
    } 
}

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 的性能所调用的办法)

构建的 vue-cli 工程都到了哪些技术,它们的作用别离是什么

  • vue.jsvue-cli工程的外围,次要特点是 双向数据绑定 和 组件零碎。
  • vue-routervue官网举荐应用的路由框架。
  • vuex:专为 Vue.js 利用我的项目开发的状态管理器,次要用于保护 vue 组件间共用的一些 变量 和 办法。
  • axios(或者 fetchajax):用于发动 GET、或 POSThttp申请,基于 Promise 设计。
  • vuex等:一个专为 vue 设计的挪动端 UI 组件库。
  • 创立一个 emit.js 文件,用于 vue 事件机制的治理。
  • webpack:模块加载和 vue-cli 工程打包器。

实现双向绑定

咱们还是以 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-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 标签;如果页面是个简单的内容,比方商品信息,能够增加点击事件,应用编程式导航
  • 实际上外部两者调用的导航函数是一样的

正文完
 0