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
需要剖析- 如何实现这些需要
答复范例
- 官网说
vuex
是一个状态管理模式和库,并确保这些状态以可预期的形式变更。可见要实现一个vuex
- 要实现一个
Store
存储全局状态 - 要提供批改状态所需API:
commit(type, payload), dispatch(type, payload)
- 实现
Store
时,能够定义Store
类,构造函数接管选项options
,设置属性state
对外裸露状态,提供commit
和dispatch
批改属性state
。这里须要设置state
为响应式对象,同时将Store
定义为一个Vue
插件 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.$storeVue.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 0count.value++// -> logs 1
watch
侦测一个或多个响应式数据源并在数据源变动时调用一个回调函数
const state = reactive({ count: 0 })watch( () => state.count, (count, prevCount) => { /* ... */ })
答复范例
watchEffect
立刻运行一个函数,而后被动地追踪它的依赖,当这些依赖扭转时从新执行该函数。watch
侦测一个或多个响应式数据源并在数据源变动时调用一个回调函数watchEffect(effect)
是一种非凡watch
,传入的函数既是依赖收集的数据源,也是回调函数。如果咱们不关怀响应式数据变动前后的值,只是想拿这些数据做些事件,那么watchEffect
就是咱们须要的。watch
更底层,能够接管多种数据源,包含用于依赖收集的getter
函数,因而它齐全能够实现watchEffect
的性能,同时因为能够指定getter
函数,依赖能够管制的更准确,还能获取数据变动前后的值,因而如果须要这些时咱们会应用watch
watchEffect
在应用时,传入的函数会立即执行一次。watch
默认状况下并不会执行回调函数,除非咱们手动设置immediate
选项- 从实现上来说,
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
种拦挡方:不限于apply
、ownKeys
、deleteProperty
、has
等等是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