关于vue.js:阿里前端高频vue面试题边面边更

42次阅读

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

Vue 中 computed 和 watch 有什么区别?

计算属性 computed

(1)** 反对缓存 **,只有依赖数据发生变化时,才会从新进行计算函数;(2)计算属性内 ** 不反对异步操作 **;(3)计算属性的函数中 ** 都有一个 get**(默认具备,获取计算属性)** 和 set**(手动增加,设置计算属性)办法;(4)计算属性是主动监听依赖值的变动,从而动静返回内容。

侦听属性 watch

(1)** 不反对缓存 **,只有数据发生变化,就会执行侦听函数;(2)侦听属性内 ** 反对异步操作 **;(3)侦听属性的值 ** 能够是一个对象,接管 handler 回调,deep,immediate 三个属性 **;(3)监听是一个过程,在监听的值变动时,能够触发一个回调,并 ** 做一些其余事件 **。

vue 是如何实现响应式数据的呢?(响应式数据原理)

Vue2: Object.defineProperty 从新定义 data 中所有的属性, Object.defineProperty 能够使数据的获取与设置减少一个拦挡的性能,拦挡属性的获取,进行依赖收集。拦挡属性的更新操作,进行告诉。

具体的过程:首先 Vue 应用 initData 初始化用户传入的参数,而后应用 new Observer 对数据进行观测,如果数据是一个对象类型就会调用 this.walk(value) 对对象进行解决,外部应用 defineeReactive 循环对象属性定义响应式变动,外围就是应用 Object.defineProperty 从新定义数据。

写过自定义指令吗 原理是什么

指令实质上是装璜器,是 vue 对 HTML 元素的扩大,给 HTML 元素减少自定义性能。vue 编译 DOM 时,会找到指令对象,执行指令的相干办法。

自定义指令有五个生命周期(也叫钩子函数),别离是 bind、inserted、update、componentUpdated、unbind

1. bind:只调用一次,指令第一次绑定到元素时调用。在这里能够进行一次性的初始化设置。2. inserted:被绑定元素插入父节点时调用 (仅保障父节点存在,但不肯定已被插入文档中)。3. update:被绑定于元素所在的模板更新时调用,而无论绑定值是否变动。通过比拟更新前后的绑定值,能够疏忽不必要的模板更新。4. componentUpdated:被绑定元素所在模板实现一次更新周期时调用。5. unbind:只调用一次,指令与元素解绑时调用。

原理

1. 在生成 ast 语法树时,遇到指令会给以后元素增加 directives 属性

2. 通过 genDirectives 生成指令代码

3. 在 patch 前将指令的钩子提取到 cbs 中, 在 patch 过程中调用对应的钩子

4. 当执行指令对应钩子函数时,调用对应指令定义的办法

如果让你从零开始写一个 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: {}})

Vue 组件间通信有哪几种形式?

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

(1)props / $emit 实用 父子组件通信

这种办法是 Vue 组件的根底,置信大部分同学耳闻能详,所以此处就不举例开展介绍。

(2)ref$parent / $children 实用 父子组件通信

  • ref:如果在一般的 DOM 元素上应用,援用指向的就是 DOM 元素;如果用在子组件上,援用就指向组件实例
  • $parent / $children:拜访父 / 子实例

(3)EventBus($emit / $on) 实用于 父子、隔代、兄弟组件通信

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

(4)$attrs/$listeners 实用于 隔代组件通信

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

(5)provide / inject 实用于 隔代组件通信

先人组件中通过 provider 来提供变量,而后在子孙组件中通过 inject 来注入变量。provide / inject API 次要解决了跨级组件间的通信问题,不过它的应用场景,次要是子组件获取下级组件的状态,跨级组件间建设了一种被动提供与依赖注入的关系。

(6)Vuex 实用于 父子、隔代、兄弟组件通信

Vuex 是一个专为 Vue.js 利用程序开发的状态管理模式。每一个 Vuex 利用的外围就是 store(仓库)。“store”基本上就是一个容器,它蕴含着你的利用中大部分的状态 (state)。

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地失去高效更新。
  • 扭转 store 中的状态的惟一路径就是显式地提交 (commit) mutation。这样使得咱们能够不便地跟踪每一个状态的变动。

Vue 中组件生命周期调用程序说一下

组件的调用程序都是 先父后子 , 渲染实现的程序是 先子后父

组件的销毁操作是 先父后子 ,销毁实现的程序是 先子后父

加载渲染过程

父 beforeCreate-> 父 created-> 父 beforeMount-> 子 beforeCreate-> 子 created-> 子 beforeMount- > 子 mounted-> 父 mounted

子组件更新过程

父 beforeUpdate-> 子 beforeUpdate-> 子 updated-> 父 updated

父组件更新过程

父 beforeUpdate -> 父 updated

销毁过程

父 beforeDestroy-> 子 beforeDestroy-> 子 destroyed-> 父 destroyed

diff 算法

<details open=””><summary>答案 </summary>
<p>
</p><p> 工夫复杂度: 个树的齐全 diff 算法是一个工夫复杂度为 O(n*3),vue 进行优化转化成 O(n)。</p>
<p> 了解:</p>
<ul>
<li>
<p> 最小量更新, key 很重要。这个能够是这个节点的惟一标识,通知 diff 算法,在更改前后它们是同一个 DOM 节点 </p>
<ul>
<li> 扩大 v-for 为什么要有 key,没有 key 会暴力复用,举例子的话轻易说一个比方挪动节点或者减少节点(批改 DOM),加 key 只会挪动缩小操作 DOM。</li>
</ul>
</li>
<li>
<p> 只有是同一个虚构节点才会进行精细化比拟,否则就是暴力删除旧的,插入新的。</p>
</li>
<li>
<p> 只进行同层比拟,不会进行跨层比拟。</p>
</li>
</ul>
<p>diff 算法的优化策略:四种命中查找,四个指针 </p>
<ol>
<li>
<p> 旧前与新前(先比结尾,后插入和删除节点的这种状况)</p>
</li>
<li>
<p> 旧后与新后(比结尾,前插入或删除的状况)</p>
</li>
<li>
<p> 旧前与新后(头与尾比,此种产生了,波及挪动节点,那么新前指向的节点,挪动到旧后之后)</p>
</li>
<li>
<p> 旧后与新前(尾与头比,此种产生了,波及挪动节点,那么新前指向的节点,挪动到旧前之前)</p>
</li>
</ol>
<p></p>
</details>

— 问完下面这些如果都能很分明的话,根本 O 了 —

以下的这些简略的概念,你必定也是没有问题的啦😉

Vue 模版编译原理晓得吗,能简略说一下吗?

简略说,Vue 的编译过程就是将 template 转化为 render 函数的过程。会经验以下阶段:

  • 生成 AST 树
  • 优化
  • codegen

首先解析模版,生成AST 语法树(一种用 JavaScript 对象的模式来形容整个模板)。应用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相干解决。

Vue 的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变动,对应的 DOM 也不会变动。那么优化过程就是深度遍历 AST 树,依照相干条件对树节点进行标记。这些被标记的节点 (动态节点) 咱们就能够 跳过对它们的比对,对运行时的模板起到很大的优化作用。

编译的最初一步是 将优化后的 AST 树转换为可执行的代码

虚构 DOM 的优劣如何?

长处:

  • 保障性能上限: 虚构 DOM 能够通过 diff 找出最小差别, 而后批量进行 patch, 这种操作尽管比不上手动优化, 然而比起粗犷的 DOM 操作性能要好很多, 因而虚构 DOM 能够保障性能上限
  • 无需手动操作 DOM: 虚构 DOM 的 diff 和 patch 都是在一次更新中主动进行的, 咱们无需手动操作 DOM, 极大进步开发效率
  • 跨平台: 虚构 DOM 实质上是 JavaScript 对象, 而 DOM 与平台强相干, 相比之下虚构 DOM 能够进行更不便地跨平台操作, 例如服务器渲染、挪动端开发等等

毛病:

  • 无奈进行极致优化: 在一些性能要求极高的利用中虚构 DOM 无奈进行针对性的极致优化, 比方 VScode 采纳间接手动操作 DOM 的形式进行极其的性能优化

参考:前端 vue 面试题具体解答

computed 和 watch 的区别和使用的场景?

computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值产生扭转,下一次获取 computed 的值时才会从新计算 computed 的值;

watch: 更多的是「察看」的作用,相似于某些数据的监听回调,每当监听的数据变动时都会执行回调进行后续操作;

使用场景:

  • 当咱们须要进行数值计算,并且依赖于其它数据时,应该应用 computed,因为能够利用 computed 的缓存个性,防止每次获取值时,都要从新计算;
  • 当咱们须要在数据变动时执行异步或开销较大的操作时,应该应用 watch,应用 watch 选项容许咱们执行异步操作 (拜访一个 API),限度咱们执行该操作的频率,并在咱们失去最终后果前,设置中间状态。这些都是计算属性无奈做到的。

vue3 中 watch、watchEffect 区别

  • watch是惰性执行,也就是只有监听的值发生变化的时候才会执行,然而 watchEffect 不同,每次代码加载 watchEffect 都会执行(疏忽 watch 第三个参数的配置,如果批改配置项也能够实现立刻执行)
  • watch须要传递监听的对象,watchEffect不须要
  • watch只能监听响应式数据:ref定义的属性和 reactive 定义的对象,如果间接监听 reactive 定义对象中的属性是不容许的(会报正告),除非应用函数转换一下。其实就是官网上说的监听一个getter
  • watchEffect如果监听 reactive 定义的对象是不起作用的,只能监听对象中的属性

看一下 watchEffect 的代码

<template>
<div>
  请输出 firstName:<input type="text" v-model="firstName">
</div>
<div>
  请输出 lastName:<input type="text" v-model="lastName">
</div>
<div>
  请输出 obj.text:<input type="text" v-model="obj.text">
</div>
 <div>【obj.text】{{obj.text}}
 </div>
</template>

<script>
import {ref, reactive, watch, watchEffect} from 'vue'
export default {
  name: "HelloWorld",
  props: {msg: String,},
  setup(props,content){let firstName = ref('')
    let lastName = ref('')
    let obj= reactive({text:'hello'})
    watchEffect(()=>{console.log('触发了 watchEffect');
      console.log(` 组合后的名称为:${firstName.value}${lastName.value}`)
    })
    return{
      obj,
      firstName,
      lastName
    }
  }
};
</script>

革新一下代码

watchEffect(()=>{console.log('触发了 watchEffect');
  // 这里咱们不应用 firstName.value/lastName.value,相当于是监控整个 ref, 对应第四点下面的论断
  console.log(` 组合后的名称为:${firstName}${lastName}`)
})

watchEffect(()=>{console.log('触发了 watchEffect');
  console.log(obj);
})

略微革新一下

let obj = reactive({text:'hello'})
watchEffect(()=>{console.log('触发了 watchEffect');
  console.log(obj.text);
})

再看一下 watch 的代码,验证一下

let obj= reactive({text:'hello'})
// watch 是惰性执行,默认初始化之后不会执行,只有值有变动才会触发,可通过配置参数实现默认执行
watch(obj, (newValue, oldValue) => {
  // 回调函数
  console.log('触发监控更新了 new',  newValue);
  console.log('触发监控更新了 old',  oldValue);
},{
  // 配置 immediate 参数,立刻执行,以及深层次监听
  immediate: true,
  deep: true
})

  • 监控整个 reactive 对象,从下面的图能够看到 deep 理论默认是开启的,就算咱们设置为 false 也还是有效。而且旧值获取不到。
  • 要获取旧值则须要监控对象的属性,也就是监听一个getter,看下图

总结

  • 如果定义了 reactive 的数据,想去应用 watch 监听数据扭转,则无奈正确获取旧值,并且 deep 属性配置有效,主动强制开启了深层次监听。
  • 如果应用 ref 初始化一个对象或者数组类型的数据,会被主动转成 reactive 的实现形式,生成 proxy 代理对象。也会变得无奈正确取旧值。
  • 用任何形式生成的数据,如果接管的变量是一个 proxy 代理对象,就都会导致 watch 这个对象时,watch回调里无奈正确获取旧值。
  • 所以当大家应用 watch 监听对象时,如果在不须要应用旧值的状况,能够失常监听对象没关系;然而如果当监听扭转函数外面须要用到旧值时,只能监听 对象.xxx` 属性 的形式才行

watch 和 watchEffect 异同总结

体验

watchEffect立刻运行一个函数,而后被动地追踪它的依赖,当这些依赖扭转时从新执行该函数

const count = ref(0)
​
watchEffect(() => console.log(count.value))
// -> logs 0
​
count.value++
// -> logs 1

watch侦测一个或多个响应式数据源并在数据源变动时调用一个回调函数

const state = reactive({count: 0})
watch(() => state.count,
  (count, prevCount) => {/* ... */}
)

答复范例

  1. watchEffect立刻运行一个函数,而后被动地追踪它的依赖,当这些依赖扭转时从新执行该函数。watch侦测一个或多个响应式数据源并在数据源变动时调用一个回调函数
  2. watchEffect(effect)是一种非凡 watch,传入的函数既是依赖收集的数据源,也是回调函数。如果咱们不关怀响应式数据变动前后的值,只是想拿这些数据做些事件,那么watchEffect 就是咱们须要的。watch更底层,能够接管多种数据源,包含用于依赖收集的 getter 函数,因而它齐全能够实现 watchEffect 的性能,同时因为能够指定 getter 函数,依赖能够管制的更准确,还能获取数据变动前后的值,因而如果须要这些时咱们会应用watch
  3. watchEffect在应用时,传入的函数会立即执行一次。watch默认状况下并不会执行回调函数,除非咱们手动设置 immediate 选项
  4. 从实现上来说,watchEffect(fn)相当于watch(fn,fn,{immediate:true})

watchEffect定义如下

export function watchEffect(
  effect: WatchEffect,
  options?: WatchOptionsBase
): WatchStopHandle {return doWatch(effect, null, options)
}

watch定义如下

export function watch<T = any, Immediate extends Readonly<boolean> = false>(
  source: T | WatchSource<T>,
  cb: any,
  options?: WatchOptions<Immediate>
): WatchStopHandle {return doWatch(source as any, cb, options)
}

很显著 watchEffect 就是一种非凡的 watch 实现。

v-model 的原理?

咱们在 vue 我的项目中次要应用 v-model 指令在表单 input、textarea、select 等元素上创立双向数据绑定,咱们晓得 v-model 实质上不过是语法糖,v-model 在外部为不同的输出元素应用不同的属性并抛出不同的事件:

  • text 和 textarea 元素应用 value 属性和 input 事件;
  • checkbox 和 radio 应用 checked 属性和 change 事件;
  • select 字段将 value 作为 prop 并将 change 作为事件。

以 input 表单元素为例:

<input v-model='something'>

相当于

<input v-bind:value="something" v-on:input="something = $event.target.value">

如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:

父组件:<ModelChild v-model="message"></ModelChild>

子组件:<div>{{value}}</div>

props:{value: String},
methods: {test1(){this.$emit('input', '小红')
  },
},

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 的生命周期办法有哪些 个别在哪一步发申请

beforeCreate 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。在以后阶段 data、methods、computed 以及 watch 上的数据和办法都不能被拜访

created 实例曾经创立实现之后被调用。在这一步,实例已实现以下的配置:数据观测(data observer),属性和办法的运算,watch/event 事件回调。这里没有 $el, 如果非要想与 Dom 进行交互,能够通过 vm.$nextTick 来拜访 Dom

beforeMount 在挂载开始之前被调用:相干的 render 函数首次被调用。

mounted 在挂载实现后产生,在以后阶段,实在的 Dom 挂载结束,数据实现双向绑定,能够拜访到 Dom 节点

beforeUpdate 数据更新时调用,产生在虚构 DOM 从新渲染和打补丁(patch)之前。能够在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程

updated 产生在更新实现之后,以后阶段组件 Dom 已实现更新。要留神的是防止在此期间更改数据,因为这可能会导致有限循环的更新,该钩子在服务器端渲染期间不被调用。

beforeDestroy 实例销毁之前调用。在这一步,实例依然齐全可用。咱们能够在这时进行善后收尾工作,比方革除计时器。

destroyed Vue 实例销毁后调用。调用后,Vue 实例批示的所有货色都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。

activated keep-alive 专属,组件被激活时调用

deactivated keep-alive 专属,组件被销毁时调用

异步申请在哪一步发动?

能够在钩子函数 created、beforeMount、mounted 中进行异步申请,因为在这三个钩子函数中,data 曾经创立,能够将服务端端返回的数据进行赋值。

如果异步申请不须要依赖 Dom 举荐在 created 钩子函数中调用异步申请,因为在 created 钩子函数中调用异步申请有以下长处:

  • 能更快获取到服务端数据,缩小页面 loading 工夫;
  • ssr 不反对 beforeMount、mounted 钩子函数,所以放在 created 中有助于一致性;

created 和 mounted 的区别

  • created: 在模板渲染成 html 前调用,即通常初始化某些属性值,而后再渲染成视图。
  • mounted: 在模板渲染成 html 后调用,通常是初始化页面实现后,再对 html 的 dom 节点进行一些须要的操作。

Vue 的基本原理

当一个 Vue 实例创立时,Vue 会遍历 data 中的属性,用 Object.defineProperty(vue3.0 应用 proxy)将它们转为 getter/setter,并且在外部追踪相干依赖,在属性被拜访和批改时告诉变动。每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会告诉 watcher 从新计算,从而以致它关联的组件得以更新。

Vue 生命周期钩子是如何实现的

  • vue的生命周期钩子就是回调函数而已,当创立组件实例的过程中会调用对应的钩子办法
  • 外部会对钩子函数进行解决,将钩子函数保护成数组的模式

Vue 的生命周期钩子外围实现是利用公布订阅模式先把用户传入的的生命周期钩子订阅好(外部采纳数组的形式存储)而后在创立组件实例的过程中会一次执行对应的钩子办法(公布)

<script>
    // Vue.options 中会寄存所有全局属性

    // 会用本身的 + Vue.options 中的属性进行合并
    // Vue.mixin({//     beforeCreate() {//         console.log('before 0')
    //     },
    // })
    debugger;
    const vm = new Vue({
        el: '#app',
        beforeCreate: [function() {console.log('before 1')
            },
            function() {console.log('before 2')
            }
        ]
    });
    console.log(vm);
</script>

相干代码如下

export function callHook(vm, hook) {
  // 顺次执行生命周期对应的办法
  const handlers = vm.$options[hook];
  if (handlers) {for (let i = 0; i < handlers.length; i++) {handlers[i].call(vm); // 生命周期外面的 this 指向以后实例
    }
  }
}

// 调用的时候
Vue.prototype._init = function (options) {
  const vm = this;
  vm.$options = mergeOptions(vm.constructor.options, options);
  callHook(vm, "beforeCreate"); // 初始化数据之前
  // 初始化状态
  initState(vm);
  callHook(vm, "created"); // 初始化数据之后
  if (vm.$options.el) {vm.$mount(vm.$options.el);
  }
};

// 销毁实例实现
Vue.prototype.$destory = function() {
     // 触发钩子
    callHook(vm, 'beforeDestory')
    // 本身及子节点
    remove() 
    // 删除依赖
    watcher.teardown() 
    // 删除监听
    vm.$off() 
    // 触发钩子
    callHook(vm, 'destoryed')
}

原理流程图

keep-alive 中的生命周期哪些

keep-alive 是 Vue 提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,避免反复渲染 DOM。

如果为一个组件包裹了 keep-alive,那么它会多出两个生命周期:deactivated、activated。同时,beforeDestroy 和 destroyed 就不会再被触发了,因为组件不会被真正销毁。

当组件被换掉时,会被缓存到内存中、触发 deactivated 生命周期;当组件被切回来时,再去缓存里找这个组件、触发 activated 钩子函数。

Vue 是如何收集依赖的?

在初始化 Vue 的每个组件时,会对组件的 data 进行初始化,就会将由一般对象变成响应式对象,在这个过程中便会进行依赖收集的相干逻辑,如下所示∶

function defieneReactive (obj, key, val){const dep = new Dep();
  ...
  Object.defineProperty(obj, key, {
    ...
    get: function reactiveGetter () {if(Dep.target){dep.depend();
        ...
      }
      return val
    }
    ...
  })
}

以上只保留了要害代码,次要就是 const dep = new Dep()实例化一个 Dep 的实例,而后在 get 函数中通过 dep.depend() 进行依赖收集。(1)Dep Dep 是整个依赖收集的外围,其要害代码如下:

class Dep {
  static target;
  subs;

  constructor () {
    ...
    this.subs = [];}
  addSub (sub) {this.subs.push(sub)
  }
  removeSub (sub) {remove(this.sub, sub)
  }
  depend () {if(Dep.target){Dep.target.addDep(this)
    }
  }
  notify () {const subs = this.subds.slice();
    for(let i = 0;i < subs.length; i++){subs[i].update()}
  }
}

Dep 是一个 class,其中有一个关 键的动态属性 static,它指向了一个全局惟一 Watcher,保障了同一时间全局只有一个 watcher 被计算,另一个属性 subs 则是一个 Watcher 的数组,所以 Dep 实际上就是对 Watcher 的治理,再看看 Watcher 的相干代码∶

(2)Watcher

class Watcher {
  getter;
  ...
  constructor (vm, expression){
    ...
    this.getter = expression;
    this.get();}
  get () {pushTarget(this);
    value = this.getter.call(vm, vm)
    ...
    return value
  }
  addDep (dep){
        ...
    dep.addSub(this)
  }
  ...
}
function pushTarget (_target) {Dep.target = _target}

Watcher 是一个 class,它定义了一些办法,其中和依赖收集相干的次要有 get、addDep 等。

(3)过程

在实例化 Vue 时,依赖收集的相干过程如下∶
初 始 化 状 态 initState,这 中 间 便 会 通 过 defineReactive 将数据变成响应式对象,其中的 getter 局部便是用来依赖收集的。
初始化最终会走 mount 过程,其中会实例化 Watcher,进入 Watcher 中,便会执行 this.get() 办法,

updateComponent = () => {vm._update(vm._render())
}
new Watcher(vm, updateComponent)

get 办法中的 pushTarget 实际上就是把 Dep.target 赋值为以后的 watcher。

this.getter.call(vm,vm),这里的 getter 会执行 vm._render() 办法,在这个过程中便会触发数据对象的 getter。那么每个对象值的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 办法,也就会执行 Dep.target.addDep(this)。方才 Dep.target 曾经被赋值为 watcher,于是便会执行 addDep 办法,而后走到 dep.addSub() 办法,便将以后的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目标是为后续数据变动时候能告诉到哪些 subs 做筹备。所以在 vm._render() 过程中,会触发所有数据的 getter,这样便曾经实现了一个依赖收集的过程。

说说你对 proxy 的了解,Proxy 相比于 defineProperty 的劣势

Object.defineProperty() 的问题次要有三个:

  • 不能监听数组的变动:无奈监控到数组下标的变动,导致通过数组下标增加元素,不能实时响应
  • 必须遍历对象的每个属性:只能劫持对象的属性,从而须要对每个对象,每个属性进行遍历,如果属性值是对象,还须要深度遍历。Proxy 能够劫持整个对象,并返回一个新的对象
  • 必须深层遍历嵌套的对象

Proxy 的劣势如下:

  • 针对对象:针对整个对象,而不是对象的某个属性,所以也就不须要对 keys 进行遍历
  • 反对数组:Proxy 不须要对数组的办法进行重载,省去了泛滥 hack,缩小代码量等于缩小了保护老本,而且规范的就是最好的
  • Proxy的第二个参数能够有 13 种拦挡方:不限于 applyownKeysdeletePropertyhas 等等是 Object.defineProperty 不具备的
  • Proxy返回的是一个新对象, 咱们能够只操作新的对象达到目标, 而 Object.defineProperty 只能遍历对象属性间接批改
  • Proxy作为新规范将受到浏览器厂商重点继续的性能优化,也就是传说中的新规范的性能红利

proxy 具体应用点击查看(opens new window)

Object.defineProperty 的劣势如下:

兼容性好,反对 IE9,而 Proxy 的存在浏览器兼容性问题, 而且无奈用 polyfill 磨平

defineProperty 的属性值有哪些

Object.defineProperty(obj, prop, descriptor)

// obj 要定义属性的对象
// prop 要定义或批改的属性的名称
// descriptor 要定义或批改的属性描述符

Object.defineProperty(obj,"name",{
  value:"poetry", // 初始值
  writable:true, // 该属性是否可写入
  enumerable:true, // 该属性是否可被遍历失去(for...in,Object.keys 等)configurable:true, // 定该属性是否可被删除,且除 writable 外的其余描述符是否可被批改
  get: function() {},
  set: function(newVal) {}})

相干代码如下

import {mutableHandlers} from "./baseHandlers"; // 代理相干逻辑
import {isObject} from "./util"; // 工具办法

export function reactive(target) {
  // 依据不同参数创立不同响应式对象
  return createReactiveObject(target, mutableHandlers);
}
function createReactiveObject(target, baseHandler) {if (!isObject(target)) {return target;}
  const observed = new Proxy(target, baseHandler);
  return observed;
}

const get = createGetter();
const set = createSetter();

function createGetter() {return function get(target, key, receiver) {
    // 对获取的值进行喷射
    const res = Reflect.get(target, key, receiver);
    console.log("属性获取", key);
    if (isObject(res)) {
      // 如果获取的值是对象类型,则返回以后对象的代理对象
      return reactive(res);
    }
    return res;
  };
}
function createSetter() {return function set(target, key, value, receiver) {const oldValue = target[key];
    const hadKey = hasOwn(target, key);
    const result = Reflect.set(target, key, value, receiver);
    if (!hadKey) {console.log("属性新增", key, value);
    } else if (hasChanged(value, oldValue)) {console.log("属性值被批改", key, value);
    }
    return result;
  };
}
export const mutableHandlers = {
  get, // 当获取属性时调用此办法
  set, // 当批改属性时调用此办法
};

Proxy只会代理对象的第一层,那么 Vue3 又是怎么解决这个问题的呢?

判断以后 Reflect.get 的 返回值是否为 Object,如果是则再通过reactive 办法做代理,这样就实现了深度观测。

监测数组的时候可能触发屡次 get/set,那么如何避免触发屡次呢?

咱们能够判断 key 是否为以后被代理对象 target 本身属性,也能够判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger

正文完
 0