keil debug如何在watch直接修改变量值_如何鹤立鸡群?试试vue高级语法(附业务场景以及源码解析)...

bbf6df5ad0809afc1b329560d76bafdf.png

1.watch一个融合多个数据源的computed以及发散

业务场景:多个数据触发同一个操作

熟悉中后台开发的同学应该不会对列表过滤翻页这样的场景感到陌生

这样的页面有一个特性,就是多种操作会出发同一个后端接口(获取列表数据)

正常的思路,mounted,handlePageChange,handleFilterChange等处分别调用一次封装好的异步逻辑函数

然而如果页面的功能日趋复杂化,维护的时候就会出现逻辑分散,难以理解的问题

所以,如何将多个功能点触发同一段异步的逻辑“集中起来”,就是我们开发中的痛点

其实,我们完全可以使用一个computed监听多个数据的变化,然后再利用一个watch对该computed进行监听,在watch内部编写我们的公共异步逻辑

c248c834f23956195e2ee8fc96479eb8.png

这里涉及到的知识点.主要是watch的对象用法,以及watch监听computed

当然,既然我们这篇文章叫做高级用法,对标高级工程师,哪能就这么讲下用法就过去了?

我相信挺多同学看到这里会有一个疑问: watch中的immediate和mounted功能非常相似,他们有什么区别呢?

首先我们来看下,他们执行顺序之间的差异

a52339be9ad5eae90301bb65a8d1b773.png

问题一下子就来了,为啥会是这个顺序?

我们直接来看下vue的位于instance/index.js源码

1b2a03a7e6fb429c9c5f304410082ab2.png

我们可以看到这段代码中"混入"了一些初始化的内容

点进去继续探索一下

c5019f9aaa01b7f9ab6d9816a4c916f5.png
initLifecycle

是不是非常清晰了?

那么我们再来看一下我们特别关注的initState中的内容

73319d9e9eb5aba2791a29e3ff8a37cb.png

因为computed挂载顺序先于watch,所以我们上面用watch来监听computed是非常合理的

无非也就都是个watcher而已

同时这里提出一个问题供大家思考: 为什么vue的设计者要使用这样的顺序呢?

而initWatch会创建一个watcher并执行$watch,而$watcher中如果发现watch携带了immediate标识会立即执行

d1afcc43001e24c94dc15433434f14d0.png

而这时我们会发现$watch竟然还返回了一个叫做unwatchFn的东西

这玩意就是我们平时用于卸载watch的函数,他可以清空该watch的执行队列

例如我们有一个需求,当某个数据变化三次以后便取消对该数据的监听,不在触发watch中的handler逻辑,我们就可以

const 

那么这里老师想要提出一个问题,这段逻辑可以写在beforeCreated之中吗?

带着这个疑问我们继续后面的学习吧~

(deep, 监听某个子属性,触发多个handler后面再写吧,主要是我觉得这几个点太基础了,说起来没啥意思)


2. 使用Object.freeze()冻结项目所使用的常量

日常开发中我们常常会碰到许多常量

例如常规的一些表格columns,或者是大量表单元素的label

他们的统一的特点是一旦编写就几乎不会发生任何变化,且通常会被我们抽取到公共的contants.js中

e45e7bd5f3cc3f32fefe7d4a86a4bc2c.png
一个非常规矩的constants

但是我们在使用的时候,必须将其引入后放置到data中

然而,对vue源码比较熟悉的同学一定知道,只要数据进入的data,vue就会对其监听

以图在数据变化时更新视图

显然,这对于永远不会变化的常量来说造成了大量的性能浪费,拖慢了代码的计算速度

有没有好的办法来解决这个问题呢?

当然是有的,而且vue的官方文档也曾隐晦的暗示过我们

d1d361a50500a0a89ef841593cb2f972.png

我们只需要把contants.js export default出来的大对象用Object.freeze()处理一下

则contants下辖的所有字段都不会再触发vue的监听了

b94260d2602ffc356b728fafa16be50b.png
无法对readonly赋值

这里非常值得注意的一点是freeze处理后的对象的第一层键值对是无法修改的,但是如果深层含有数组的话,其实还是"允许"开发者修改的

326cac066e6da794607fa25a72ee9fa6.png
试图混入老婆群的基佬

只不过这种修改已经无法触发vue的rerender了

05b22ec5c1e286f0c004a1b61e40bcf7.png
基佬成功上位,然而并没有触发位于updated,vue视图并没有因为数据的改变而更新

至于为什么freeze可以达成这样的效果呢?

我们可以看下位于core/instance/observer/index里面的defineReactive方法

8f5e283abc3c58244d72ccf82c14108e.png

我们可以看到,当获取到需要监听的对象时,会先判断是否可以获取property的描述

如果获取不到描述,直接return,不会对对象进行进一步监听

到这里,我们可以扩展一下我们的知识面

了解下例如Object.preventExtensions(), Object.seal() 以及最最核心的defineProperty中的configurable, writeable以及enumerable


3. 通过$options在非template中使用过滤器方法

49ee14cde2558f0c155b84d9d61b4dd7.png

filter在我们日常开发中非常常见,可以实现的功能也非常多

例如: 为列表加上一个升序的key, 或者使用moment处理时间

829c6ca6a9faff5dfb3b58809e3498f5.png

2ef0598fe98b2d88bb7a0fb3c5bd5f39.png

可是目前这种写法仅限于在胡子语法或者v-bind中才可以使用

那么如果我们想要在类似methods中使用我们已经注册到全局或者单个组件内部的filter时该怎么做呢?

其实也非常简单,vue为我们提供了一个强大的内置api----$options

首先我们来打印一下this.$options看看他里面都有什么吧

4e84bf9a5c9d782787353bb119a96fbf.png

乍一看好像也没啥有用的东西

那我们直接打印一下filters吧

6252f5e71c6ece1f35fac1f56204732d.png

06fe6171d405c6dece8c8847e3779270.png

惊不惊喜?意不意外?

为什么会出现这种状况呢?

其实原理也很简单,对于vue的学习,越是探索到源码级别,其实就越会有一种体会

学习vue本质上其实是在学习js基础,设计模式以及算法

对于vue来说,无论是挂载在某一个组件上的filters还是注册在全局的公共组件,其实本质上都是挂载在vue实例的某一层原型链上

而原型链有什么特性呢?

那肯定就是如果在某一层级找不到该属性,就会递归式的向上寻找上级原型链上是否存在该属性


4. 通过hook:hookName来把控生命周期逻辑

日常开发中,常常会碰到一个场景

进入一个列表页面,我们需要使用定时器轮训列表接口来获取最新的数据

而且我们还需要在组件销毁的时候同步销毁定时器

这本来是一段非常正常的逻辑,涉及到的时mounted和beforeDestroy两个狗子

但是,如果我们的页面逻辑日趋庞大,创建定时器和销毁定时器的逻辑分离过远,难以维护和理解

而且我们需要单独配置一个data字段来保存timer

那么我们有没有一种办法可以将两处逻辑放在一起呢?

d602529dca557581ac17fb0ee804480e.png

这里我们直接在mounted里面监听一次beforeDestory这个钩子,并清除定时器的触发

眼尖的同学可能会发现我在这段逻辑的上部还监听了updated这个钩子

那么我们这里就会有一个疑问:

通过$on这类逻辑监听的钩子中编写的逻辑和正常写在vue语法中的生命周期是否能够合并?

如果能够合并,他们的先后顺序又是怎么样的?

e2185d983467064a20ed1e2729a29373.png

e762793085e3935dff840b61741403fb.png

大橘已定!bingo!

首先,两段逻辑都会执行,生命周期已经被合并

其次,我们的在updated中编写的逻辑会先执行,随后,才会执行其他位置通过$on这类方法新增的逻辑

d2b2391149d4d46055df65e4072e50b2.png
当然肯定有好事的想要怎么写(手动滑稽)

这时候肯定有抖抖嗖嗖的同学要问了,老师啊,这玩意还有别的用处吗

当然有,我们可以把思维发散一下

既然我们可以通过$on以及$once监听hook,那么我们为啥不可以直接使用v-on来直接监听呢?

如果顺带在联系一下组件化的思想,是不是我们可以通过这种方法,直接鉴定子组件的生命周期呢?

faf14cc49de2f04c54f656c7d9fd7dc5.png
挂载一个事件

8b2167f10b99bc0957f94c90656a614a.png
父组件中的方法

f7ef269f6b210ed535e106484f3ad5a2.png
子组件中的mounted钩子

运行一下,我们来看一下结果和执行顺序

703d98dd8d8c8ac0071d9c6fb2ce99c5.png

我相信这里肯定有同学要按捺不住内心的喜悦了

满脑子都是什么父组件操控子组件mouted的逻辑,还有什么公共钩子云云

其实这些靠监听子组件生命周期是无法实现的

主要原因是vue并没有为这块逻辑暴露子组件的vm实例

其实我们处于vue设计者的角度来思考一下,如果真的向父组件暴露的vm的话,肯定会造成逻辑管理的混乱,尤其是vue已经为我们提供了诸如mixin,$listeners这类成熟优雅的api的前提下

也可能会有同学读到这里觉得

我靠,这种用法为啥官网上都没写?你是从哪知道的?

答案其实也很简单,我是通过对于vue源码的阅读了解这种用法的

这两种用法的源码分别位于vuecore部分的instance(实例公共方法)以及lifecyles(vue生命周期的挂载混入公共方法)中

88012f30eb485e7611598dee7e1ea209.png

f59b85890d3eaaac3553e6cd6f6b815c.png

监听子组件生命周期触发,实际开发中用处比较多的是监听子组件是否更新,尤其是比较小众的第三方组件在没有提供api时,父组件可以根据钩子的触发来把控子组件的所处的钩子流程,完成相应的操作


5. 组件间通信一网打尽

其实这里我主要想提下provide和inject

很多开发同学对provide和inject关注甚少,甚至慢慢沦为了只在面试中考察知识宽度的一个知识点而已

为什么会这样呢?

原因其实也很简单,就是因为官方文档上的一句话

9955fee6d74d8e9c2924a26d0ab55d2d.png

然而,其实在我们日常开发中经常会封装符合自身业务场景的组件库,而这些组件库或者公共方法只要加个install方法不就是所谓的"高阶插件"了吗?

除此之外,provide/inject最被人诟病的莫过于官网中提到的

04c4fdd70c8f2833e2cd112c904a7dc8.png

大多数同学对着官网的demo敲了一遍,发现provide里面连this都获取不到,直接弃坑

然而事实真的是这样吗?

首先,我们上面的源码解析中有提到在initMixin中的initState方法中,属性的挂载顺序是

inject=>

state(props,methods,data,computed,watch)=>

provide=>

那么按照源码的思路,provide是一定可以使用state里面的数据的

我们只需要将一个返回对象的函数传给provide即可

dcf9800c151480c42069082b5aa488e0.png

相信很多同学在学习provide.inject的都是用的上图中pageProps这种方式

8872378254486805f6ad1a80ddf63cba.png

在子组件中承接一下,发现不是响应式的

换句话说,父组件的this.page变化,并没有触发子组件的updated和beforeUpdate

33459073e226ccd1f30b9db711b0c26c.png

然后很多同学们就微笑着关闭了vscode

其实,事实并不是这样的,只不过我们没有掌握provide/inject的真正用法

dcf9800c151480c42069082b5aa488e0.png

回到这块代码,你会发现,老师使用了parent这个属性向父组件下辖的所有层次的子组件下发了该父组件的this

而上面我们有提到过,this上面有个神奇的属性$options里面犹如一个舔狗狗场

8d5ef4ec6400017c5e28aedc18736f92.png

所以我们就猥琐的使用出奥义watch

1e0c2a18c1022431723a4d0b024c9607.png
因为只是监听属性,所以连$options都懒得用了

这不就是大家心心念念的响应式吗?

另外,provide/inject另一个强大的功能在于,可以通过传输this直接调用父组件的方法,来调控父组件的某些state

换个思路来理解,你会发现provide/inject就是一个远程打击版的props+emit

讲完了provide/inject,我们来聊聊2.6新推出的observable

7681c897fe03b7508a78141010ab2600.png

其实没啥好讲的,因为observable就是一个乞丐版的vuex

主要是用于例如内嵌app的h5页面开发,或者简单的spa,因为这类应用不需要vuex这种体量级的包

但是他仍然可以做为provide/inject的替代品使用,他的缺点和react的useContext一样明显,相信也不需要多说

不过既然上面两种方法都提到了子组件修改父组件属性的问题,也可以顺带提一嘴v-bind.sync修饰符

c9873f6d75a1c1c34f008b36b8cf57f1.png

直接使用update:[key]的方法直接修改父组件的data

fc0f4df21da5556b564776085455a224.png

0c9244a178dd769551bc5d31d1f71a4b.png

谁能想到我的儿子竟然会自己挑妈妈了呢?

5aa8f49a3d6f0a9ae4029ba75e2bc893.png

回归到vue的源码

ec1930de489cee404309d4e9f66bd0b0.png

其实vue关于v-bind的修饰符的源码思路也是很清晰的

解析出修饰符,将事件返回的$event直接赋值给vm对应的key就好了,而这个事件的名字是"update:""拼上[key],和我们的写法完全一致

至于其他的集中通讯方式,例如vuex,$parents,$root,$listeners,$attr,eventBus以及最常见的props传值就不再赘述了

不过老师还是想要讲解一下这集中通信方式的边界和使用场景是怎样的(排名分先后)

  • vuex
  • 优点:可以使用开发工具可视化所有数据,便于调试,写法优雅,性能较好
  • 缺点:包较大,如果module分布混乱或者命名不规范,维护非常困难,且spa公用一个状态树,造成性能浪费
  • 适用场景: 任何场景,甚至可以完全取代props传值
  • props/$emit
  • 优点:简单方便,而且提供有.sync修饰符,状态随组件卸载销毁,性能较好
  • 跨级组件通讯非常麻烦,难以维护(我对此深恶痛绝)
  • 使用场景:任何父子组件通讯场景
  • provide/inject && Vue.observable
  • 优点:跨组件通信方便,放在公共组件(无论是业务还是抽象包装类组件或者轮盘组件)中表现优异,点对点注入,性能优异
  • 缺点:对于小白来说维护困难,需要开发者对于逻辑有十分清晰的把控力,并且需要详细注释
  • 使用场景:公共组件或者插件开发中
  • ,$parents,$root,$listeners,$attr,eventBus
  • 使用场景:能少用就少用,没啥滋味

6. 侵入Vue实例,构建自定义生命周期函数

日常开发中,经常会碰到一些事件触发的公共逻辑

例如,多个页面,多次通过点击按钮的操作触发同一段异步逻辑

或者,需要频繁监听onsize等需要挂载在window上的事件

如果每个页面我们都在具体需要触发事件的页面或者功能区重复写同一段逻辑

未免也太麻烦了

此时,不如直接抽象触一个属于自己生命周期函数

使其能够像vue自身携带的mouted,created等钩子一样工作,岂不美哉?

首先,我们需要了解vue的生命周期函数是在core/instance/index中初始化Vue类的时候通过initMixins挂载上去的

e1e81ca6311a1143d8091d8c557f683f.png

这里的逻辑我们是无法修改的,原因在于这部分逻辑完全内置,且在实例化的时候,Vue已经从事件池中将相关的事件打捞并执行了

换句话说,实例化之前挂载我们的侵入性自定义钩子的方案是不可行的

所以,我们只能从已经实例化的vm上着手侵入构建自定义钩子

首先,在上文中其实我们有提到过

vue生命周期是有一定的合并策略的,而不同的钩子的合并策略略有不同

而且,这种合并策略其实vue是通过Vue.config暴露给用户的

所以,如果我们想要构建属于自己的生命周期,需要做的第一件事就是为我们的生命周期函数赋值一个合并策略

而合并的方式,我个人认为

一定要优雅

42f3abfea97062b168f740a960a7f98d.png

通过插件的形式,无疑使我们最好的选择

回到代码本身,其实这一步就是在Vue.config.optionMergeStrategies中添加一个名字叫做ShangHook的自定义钩子合并策略

而我们的策略原型,选择的是我们最常见mouted

策略一旦嵌入到Vue中,实例化出的vm的$children就会在$options携带有我们的自定义钩子

到这里,我们走完了第一步,就是实例化之前的初始化

随后,我们需要再走一步,也就是实例化之后,将我们的钩子推入事件池

e5e8a7b6354ad4c76b93059a646d00b2.png

此时,触发的事件已经建立,我们寻找到类名为shang的所有元素

随后为其挂载了一个点击事件,时间内执行了一个通知触发钩子的函数

保证事件和我们的钩子关联起来

de879f80d39fd6aa367f4c087b1145c0.png

这个函数涵盖了很多功能点,接下来我会逐个解析

首先,由于我们使用的时mounted的合并策略,那么原则上说,除了在script里面挂载的钩子逻辑以外,他还会在hook:shangHook以及mixin中触发

此时,我们的shangHook就必定是一个数组,因为他并非是"阉割版钩子",而是一个可以像Vue内置的其他钩子一样行使各种智能的"完全体钩子"

而且,我们甚至可以为shangHook加一些Vue钩子所不具备的特殊功能,例如传参和回调函数

7af568d5c53a543d2d55d7aa1a3146cd.png

c58ba4305904d81b2e0f79e21de70523.png

fbf46639bda41a30b94afc949398f04e.png

我这里只是为了展示直观,所以只加了两个字符串

而实际上,我们在上文的notifyClickChange方法中,我们在递归调用的时候传入了vm实例,那么vm实例中$options中的任何东西我们都可以随意使用

同时这边还可以加入一些默认逻辑,例如每次点击都全局message一条信息

同时,我们在组件中使用shangHook的时候,还可以使用类似React_useState的回调语法来控制异步逻辑的执行,甚至可以触发例如beforeUpdate或者beforeDestory这类钩子

从此,React和Vue语法上的鸿沟,又浅了一分~~~

让我们记住这历史性的一刻~~~

不过此时,相信有一些同学已经注意到老师在notify方法中传入了触发钩子元素的class

而且,我们还通过class中是否还有实例的uid来判断是否触发钩子

这是为什么呢?

首先,Vue内置的钩子是从原则上来说并不是一个组件触发钩子,其他无嵌套关系的组件也会触发

更多的是,谁触发了某个钩子,尽量不影响到其他无关组件的状态

而我们想要完成这种沙箱的效果,就需要给触发钩子的组件打上一个标记,那么uid就是最好的选择

我们只需要在需要出发钩子的元素上加一个包含当前组件uid的类名即可

1430fd306024938829d82c777d319db8.png

至此,我们的自定义钩子已经全部完成,这里老师提几个优化点供同学们思考

1.uid的挂载到类名当中,是否可以用webpack的loader来自动化添加?

2.notify中的函数使用递归方式过于耗费性能,可有更好的办法可以直接定位到触发钩子的组件?

好了,带着这些问题,我们开始后面的学习吧~


7. 自定义指令通过虚拟节点获取当前组件原发环境下的$options

日常开发中我们经常碰到点击一个操作按钮,然后弹窗提示用户是否确定操作?

如果用户点击确定,则调取一段异步逻辑,异步完成之时,通过全局message提示用户操作成功

这种场景,就非常适合我们使用自定义钩子来处理

6596a8c752935ade9750fea5359efac8.png

自定义钩子的挂载过程中,会为我们提供四个生命周期函数

bind,inserted unbind, update

这里我们只要使用到bind,也就是绑定元素的时候触发钩子

由于自定义全局指令的挂载必须要在实例化之前,而我们在指令的内置逻辑又需要用到ui框架提供的部分组件(这里使用的是我司常用的antd-vue)

所以我们先要保证ui插件先安装,随后在Vue的原型链上寻找我们需要用的组件方法,同时注册一个点击事件,顺便调节一下按钮的样式

这时候我们就碰到了一个问题---点击按钮所触发的异步必须由触发的组件提供,所以我们需要为自定义指令和挂载元素之间形成一个通讯链

幸运的是,vue已经为我们提供了binding参数,我们可以通过binding中的value拿到我们需要的方法

8cd90e306fcd638e6136393ee328ff85.png

随后,我们注意到binding后面还有一个vnode参数,而这个参数很多人会觉得没什么用处

原因也很简单,因为vnode就是我们常说的用于构建虚拟dom的虚拟节点

有很多难以理解的对象键值对,然而,vue却非常贴心的为我们在vnode中添加了context供我们使用

context提供的数据与$options非常类似,所以我们现在可以访问当前组件所在语法上下文中任意的属性和方法甚至生命周期

日常开发中,自定义指令由于虚拟节点的暴露,可以完成大量难以想象的公共功能,这种优越的用法,供各位同学参考使用


未完待续~最近上班有点忙,有空我就写点~呼~

绝对原创,希望大家多多支持~

4b8d8d2f8df4b0d6f014adcf966d52e2.png
网上盗的图,不过整体符合我的学习路线,聊以自勉

源码的分析我会随后更新到专栏中

我会从宏观到微观方法,层层递进,选择一条其他作者未曾尝试的顺序来解析vue的源码

目标在于提升对vue的把控力,另外还致力于提升面试官的出题思路和面试者的答题宽度

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值