data的值 如何初始化vue_Vue原理解析(九):搞懂computed和watch原理,减少使用场景思考时间...

之前的章节,我们按照流程介绍了vue的初始化、虚拟Dom生成、虚拟Dom转为真实Dom、深入理解响应式以及diff算法等这些核心概念,对它内部的实现做了分析,这些都是偏底层的原理。接下来我们将介绍日常开发中经常使用的API的原理,进一步丰富对vue的认识,它们主要包括以下:

响应式相关APIthis.$watchthis.$setthis.$delete
事件相关APIthis.$onthis.$offthis.$oncethis.$emit
生命周期相关APIthis.$mountthis.$forceUpdatethis.$destroy
全局APIVue.extendVue.nextTickVue.setVue.deleteVue.componentVue.useVue.mixinVue.compileVue.versionVue.directiveVue.filter

这一章节主要分析computedwatch属性,对于接触vue不久的朋友可能会对computedwatch有疑惑,什么时候使用哪个属性留有存疑,接下来我们将从内部实现的角度出发,彻底搞懂它们分别适用的场景。 * ### this.$watch

这个API是我们之前介绍响应式时的Watcher类的一种封装,也就是三种watcher中的user-watcher,监听属性经常会被这样使用到:

export default {watch: {name(newName) {...}}
}

其实它只是this.$watch这个API的一种封装:

export default {created() {this.$watch('name', newName => {...})}
}
监听属性初始化

为什么这么说,我们首先来看下初始化时watch属性都做了什么:

function initState(vm) {  // 初始化所有状态时vm._watchers = []  // 当前实例watcher集合const opts = vm.$options  // 合并后的属性... // 其他状态初始化if(opts.watch) {  // 如果有定义watch属性initWatch(vm, opts.watch)  // 执行初始化方法}
}---------------------------------------------------------function initWatch (vm, watch) {  // 初始化方法for (const key in watch) {  // 遍历watch内多个监听属性const handler = watch[key]  // 每一个监听属性的值if (Array.isArray(handler)) {  // 如果该项的值为数组for (let i = 0; i < handler.length; i++) {createWatcher(vm, key, handler[i])  // 将每一项使用watcher包装}} else {createWatcher(vm, key, handler) // 不是数组直接使用watcher}}
}---------------------------------------------------------function createWatcher (vm, expOrFn, handler, options) {if (isPlainObject(handler)) { // 如果是对象,参数移位options = handler  handler = handler.handler}if (typeof handler === 'string') {  // 如果是字符串,表示为方法名handler = vm[handler]  // 获取methods内的方法}return vm.$watch(expOrFn, handler, options)  // 封装
}

以上对监听属性的多种不同的使用方式,都做了处理。使用示例在官网上均可找到:watch示例,这里就不做过多的介绍了。可以看到最后是调用了vm.$watch方法。

监听属性实现原理

所以我们来看下$watch的内部实现:

Vue.prototype.$watch = function(expOrFn, cb, options = {}) {const vm = thisif (isPlainObject(cb)) {  // 如果cb是对象,当手动创建监听属性时return createWatcher(vm, expOrFn, cb, options)}options.user = true  // user-watcher的标志位,传入Watcher类中const watcher = new Watcher(vm, expOrFn, cb, options)  // 实例化user-watcherif (options.immediate) {  // 立即执行
cb.call(vm, watcher.value)  // 以当前值立即执行一次回调函数}  // watcher.value为实例化后返回的值return function unwatchFn () {  // 返回一个函数,执行取消监听watcher.teardown()}
}---------------------------------------------------------------export default {data() {return {name: 'cc'}  },created() {this.unwatch = this.$watch('name', newName => {...})this.unwatch()  // 取消监听}
}

虽然watch内部是使用this.$watch,但是我们也是可以手动调用this.$watch来创建监听属性的,所以第二个参数cb会出现是对象的情况。接下来设置一个标记位options.usertrue,表明这是一个user-watcher再给watch设置了immediate属性后,会将实例化后得到的值传入回调,并立即执行一次回调函数,这也是immediate的实现原理。最后的返回值是一个方法,执行后可以取消对该监听属性的监听。接下来我们看看user-watcher是如何定义的:

class Watcher {constructor(vm, expOrFn, cb, options) {this.vm = vmvm._watchers.push(this)  // 添加到当前实例的watchers内if(options) {this.deep = !!options.deep  // 是否深度监听this.user = !!options.user  // 是否是user-wathcerthis.sync = !!options.sync  // 是否同步更新}
this.active = true  // // 派发更新的标志位this.cb = cb  // 回调函数if (typeof expOrFn === 'function') {  // 如果expOrFn是函数this.getter = expOrFn} else {this.getter = parsePath(expOrFn)  // 如果是字符串对象路径形式,返回闭包函数}...}
}

当是user-watcher时,Watcher内部是以上方式实例化的,通常情况下我们是使用字符串的形式创建监听属性,所以首先来看下parsePath方法是干什么的:

const bailRE = /[^w.$]/  // 得是对象路径形式,如info.namefunction parsePath (path) {if (bailRE.test(path)) return // 不匹配对象路径形式,再见const segments = path.split('.')  // 按照点分割为数组return function (obj) {  // 闭包返回一个函数for (let i = 0; i < segments.length; i++) {if (!obj) returnobj = obj[segments[i]]  // 依次读取到实例下对象末端的值}return obj}
}

parsePath方法最终返回一个闭包方法,此时Watcher类中的this.getter就是一个函数了,再执行this.get()方法时会将this.vm传入到闭包内,补全Watcher其他的逻辑:

class Watcher {constructor(vm, expOrFn, cb, options) {...this.getter = parsePath(expOrFn)  // 返回的方法this.value = this.get()  // 执行get}get() {pushTarget(this)  // 将当前user-watcher实例赋值给Dep.target,读取时收集它let value = this.getter.call(this.vm, this.vm)  // 将vm实例传给闭包,进行读取操作if (this.deep) {  // 如果有定义deep属性traverse(value)  // 进行深度监听}popTarget()return value  // 返回闭包读取到的值,参数immediate使用的就是这里的值}...
}

因为之前初始化已经将状态已经全部都代理到了this下,所以读取this下的属性即可,比如:

export default {data() {  // data的初始化先与watchreturn {info: {name: 'cc'}}},created() {this.$watch('info.name', newName => {...})  // 何况手动创建}
}

首先读取this下的info属性,然后读取info下的name属性。大家注意,这里我们使用了读取这个动词,所以会执行之前包装data响应式数据的get方法进行依赖收集,将依赖收集到读取到的属性的dep里,不过收集的是user-watcherget方法最后返回闭包读取到的值。

之后就是当info.name属性被重新赋值时,走派发更新的流程,我们这里把和render-watcher不同之处做单独的说明,派发更新会执行Watcher内的update方法内:

class Watcher {constructor(vm, expOrFn, cb, options) {...}update() {  // 执行派发更新if(this.sync) {  // 如果有设置sync为true
This Run - this.run()  // 不走nextTick队列,直接执行} else {queueWatcher(this)  // 否则加入队列,异步执行run()}}run() {if (this.active) {this.getAndInvoke(this.cb)  // 传入回调函数}}getAndInvoke(cb) {const value = this.get()  // 重新求值if(value !== this.value || isObject(value) || this.deep) {const oldValue = this.value  // 缓存之前的值this.value = value  // 新值if(this.user) {  // 如果是user-watcher
cb.call(this.vm, value, oldValue)  // 在回调内传入新值和旧值}}}
}

其实这里的sync属性已经没在官网做说明了,不过我们看到源码中还是保留了相关代码。接下来我们看到为什么watch的回调内可以得到新值和旧值的原理,因为cb.call(this.vm, value, oldValue)这句代码的原因,内部将新值和旧值传给了回调函数。

watch监听属性示例:
<template>  <div>{{name}}</div>
</template>export default {  // App组件data() {return {name: 'cc'}},watch: {name(newName, oldName) {...}  // 派发新值和旧值给回调},mounted() {setTimeout(() => {  
this.name = 'ww'  // 触发name的set}, 1000)}
}

4922f8f9d4fcea75359705c5536c083d.png
监听属性的deep深度监听原理

之前的get方法内有说明,如果有deep属性,则执行traverse方法:

const seenObjects = new Set()  // 不重复添加function traverse (val) {_traverse(val, seenObjects)seenObjects.clear()
}function _traverse (val, seen) {let i, keysconst isA = Array.isArray(val)  // val是否是数组if ((!isA && !isObject(val))  // 如果不是array和object|| Object.isFrozen(val)  // 或者是已经冻结对象|| val instanceof VNode) {  // 或者是VNode实例return  // 再见}if (val.__ob__) {  // 只有object和array才有__ob__属性const depId = val.__ob__.dep.id  // 手动依赖收集器的idif (seen.has(depId)) {  // 已经有收集过return  // 再见}seen.add(depId)  // 没有被收集,添加}if (isA) {  // 是arrayi = val.lengthwhile (i--) {_traverse(val[i], seen)  // 递归触发每一项的get进行依赖收集}} else {  // 是objectkeys = Object.keys(val)i = keys.lengthwhile (i--) {_traverse(val[keys[i]], seen)  // 递归触发子属性的get进行依赖收集}}
}

看着还挺复杂,简单来说deep的实现原理就是递归的触发数组或对象的get进行依赖收集,因为只有数组和对象才有__ob__属性,也就是我们第七章说明的手动依赖管理器,将它们的依赖收集到Observer类里的dep内,完成deep深度监听。

watch总结:这里说明了为什么watchthis.$watch的实现是一致的,以及简单解释它的原理就是为需要观察的数据创建并收集user-watcher,当数据改变时通知到user-watcher将新值和旧值传递给用户自己定义的回调函数。最后分析了定义watch时会被使用到的三个参数:syncimmediatedeep它们的实现原理。简单说明它们的实现原理就是:sync是不将watcher加入到nextTick队列而同步的更新、immediate是立即以得到的值执行一次回调函数、deep是递归的对它的子值进行依赖收集。
  • this.$set
    这个API已经在第七章的最后做了具体分析,大家可以前往this.$set实现原理查阅。
  • this.$delete
    这个API也已经在第七章的最后做了具体分析,大家可以前往this.$delete实现原理查阅。
  • computed计算属性

计算属性不是API,但它是Watcher类的最后也是最复杂的一种实例化的使用,还是很有必要分析的。(vue版本2.6.10)其实主要就是分析计算属性为何可以做到当它的依赖项发生改变时才会进行重新的计算,否则当前数据是被缓存的。计算属性的值可以是对象,这个对象需要传入getset方法,这种并不常用,所以这里的分析还是介绍常用的函数形式,它们之间是大同小异的,不过可以减少认知负担,聚焦核心原理实现。

export default {computed: {newName: {  // 不分析这种了~get() {...},  // 内部会采用get属性为计算属性的值set() {...}}}
}
计算属性初始化
function initState(vm) {  // 初始化所有状态时vm._watchers = []  // 当前实例watcher集合const opts = vm.$options  // 合并后的属性... // 其他状态初始化if(opts.computed) {  // 如果有定义计算属性initComputed(vm, opts.computed)  // 进行初始化}...
}---------------------------------------------------------------------------function initComputed(vm, computed) {const watchers = vm._computedWatchers = Object.create(null) // 创建一个纯净对象for(const key in computed) {const getter = computed[key]  // computed每项对应的回调函数watchers[key] = new Watcher(vm, getter, noop, {lazy: true})  // 实例化computed-watcher...}
}
计算属性实现原理

这里还是按照惯例,将定义的computed属性的每一项使用Watcher类进行实例化,不过这里是按照computed-watcher的形式,来看下如何实例化的:

class Watcher{constructor(vm, expOrFn, cb, options) {this.vm = vmthis._watchers.push(this)if(options) {this.lazy = !!options.lazy  // 表示是computed}this.dirty = this.lazy  // dirty为标记位,表示是否对computed计算this.getter = expOrFn  // computed的回调函数this.value = undefined}
}

这里就点到为止,实例化已经结束了。并没有和之前render-watcher以及user-watcher那般,执行get方法,这是为什么?我们接着分析为何如此,补全之前初始化computed的方法:

function initComputed(vm, computed) {...for(const key in computed) {const getter = computed[key]  // // computed每项对应的回调函数...if (!(key in vm)) {defineComputed(vm, key, getter)}... key不能和data里的属性重名... key不能和props里的属性重名}
}

这里的App组件在执行extend创建子组件的构造函数时,已经将key挂载到vm的原型中了,不过之前也是执行的defineComputed方法,所以不妨碍我们看它做了什么:

function defineComputed(target, key) {...Object.defineProperty(target, key, {enumerable: true,configurable: true,get: createComputedGetter(key),set: noop})
}

这个方法的作用就是让computed成为一个响应式数据,并定义它的get属性,也就是说当页面执行渲染访问到computed时,才会触发get然后执行createComputedGetter方法,所以之前的点到为止再这里会续上,看下get方法是怎么定义的:

function createComputedGetter (key) { // 高阶函数return function () {  // 返回函数const watcher = this._computedWatchers && this._computedWatchers[key]// 原来this还可以这样用,得到key对应的computed-watcherif (watcher) {if (watcher.dirty) {  // 在实例化watcher时为true,表示需要计算watcher.evaluate()  // 进行计算属性的求值}if (Dep.target) {  // 当前的watcher,这里是页面渲染触发的这个方法,所以为render-watcherwatcher.depend()  // 收集当前watcher}return watcher.value  // 返回求到的值或之前缓存的值}}
}------------------------------------------------------------------------------------class Watcher {...evaluate () {this.value = this.get()  //  计算属性求值this.dirty = false  // 表示计算属性已经计算,不需要再计算}depend () {let i = this.deps.length  // deps内是计算属性内能访问到的响应式数据的dep的数组集合while (i--) {this.deps[i].depend()  // 让每个dep收集当前的render-watcher}}
}

这里的变量watcher就是之前computed对应的computed-watcher实例,接下来会执行Watcher类专门为计算属性定义的两个方法,在执行evaluate方法进行求值的过程中又会触发computed内可以访问到的响应式数据的get,它们会将当前的computed-watcher作为依赖收集到自己的dep里,计算完毕之后将dirty置为false,表示已经计算过了。

然后执行depend让计算属性内的响应式数据订阅当前的render-watcher,所以computed内的响应式数据会收集computed-watcherrender-watcher两个watcher,当computed内的状态发生变更触发set后,首先通知computed需要进行重新计算,然后通知到视图执行渲染,再渲染中会访问到computed计算后的值,最后渲染到页面。

Ps: 计算属性内的值须是响应式数据才能触发重新计算。

computed内的响应式数据变更后触发的通知:

class Watcher {...update() {  // 当computed内的响应式数据触发set后if(this.lazy) {this.diray = true  // 通知computed需要重新计算了}...}
}

最后还是以一个示例结合流程图来帮大家理清楚这里的逻辑:

export default {data() {return {manName: "cc",womanName: "ww"};},computed: {newName() {return this.manName + ":" + this.womanName;}},methods: {changeName() {this.manName = "ss";}}
};

1f8f8d0149b0fbdba5895eb31f440b73.gif
watch总结:为什么计算属性有缓存功能?因为当计算属性经过计算后,内部的标志位会表明已经计算过了,再次访问时会直接读取计算后的值;为什么计算属性内的响应式数据发生变更后,计算属性会重新计算?因为内部的响应式数据会收集computed-watcher,变更后通知计算属性要进行计算,也会通知页面重新渲染,渲染时会读取到重新计算后的值。

最后按照惯例我们还是以一道vue可能会被问到的面试题作为本章的结束~

面试官微笑而又不失礼貌的问道:
  • 请问computed属性和watch属性分别什么场景使用?

怼回去:

  • 当模板中的某个值需要通过一个或多个数据计算得到时,就可以使用计算属性,还有计算属性的函数不接受参数;监听属性主要是监听某个值发生变化后,对新值去进行逻辑处理。

顺手点个赞或关注呗,找起来也方便~

胡成:你可能会用的上的一个vue功能组件库,持续完善中...​zhuanlan.zhihu.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/513001.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

存储引擎 boltdb 的设计奥秘?

作者 | 奇伢来源 | 奇伢云存储etcd 的存储etcd v3 是使用的持久化存储来存储它的 kv 数据&#xff0c;etcd 存储的是非常核心的元数据信息&#xff0c;所以最重要的是稳定。使用的是 boltdb 。下面说道说道这个 boltdb 。boltdb 是什么&#xff1f;boltdb 是一个非常出名的存储…

ftl转PDF服务器上中文不显示,解决Linux中swftools转换中文pdf时出现乱码问题

前段时间安装了swftools可以将pdf转换为swf文件,然后配合一个FlexPaper,就可以做成和百度图文库类似的东东了.但是今天,我们开发的同事告诉我说pwd转换的这个会有乱码问题,我看了一下果然是乱码了.果断打开百度,看了一下主要是由于没有相应的字体文件,所以才造成乱码的而且在使…

提升你的职场竞争力——“低代码开发师”来了!

简介&#xff1a; 最近&#xff0c;钉钉发布了低代码开发师能力图谱&#xff0c;引发业界的广泛关注 。现在低代码开发师&#xff08;初级&#xff09;认证已经启动。 最近&#xff0c;钉钉发布了低代码开发师能力图谱&#xff0c;引发业界的广泛关注 。 所谓的低代码开发其实…

mapreduce复制连接的代码_我的 Hive 为什么跑不起来/跑得慢?看看是不是少了这几行代码?...

《饮食男女》开头说&#xff1a;“人生不能像做菜&#xff0c;把所有的料都准备好了才下锅。”但做大数据挖掘不一样&#xff0c;MapReduce 不同于人生&#xff0c;一定要把准备工作做好了&#xff0c;才能顺利运行后面的步骤。如果你的 HiveQL 代码没毛病&#xff0c;却一运行…

数字化转型的路上,手握一张地图,但路还得自己走

简介&#xff1a; 本文作者来自于中国人寿保险股份有限公司研发中心&#xff0c;对企业数字化转型、云原生实践有比较资深的经验。以下内容整理自作者对最新出版的《阿里云云原生架构实践》的读后感。 作者&#xff5c;肖晟 ​ 本文作者来自于中国人寿保险股份有限公司研发中…

tp 数据库查询排序_怎么进行数据库分库分表?

一&#xff0c;数据切分关系型数据库本身比较容易成为系统瓶颈&#xff0c;单机存储容量、连接数、处理能力都有限。当单表的数据量达到1000W或100G以后&#xff0c;由于查询维度较多&#xff0c;即使添加从库、优化索引&#xff0c;做很多操作时性能仍下降严重。此时就要考虑对…

服务器响应为4.7.0,454 4.7.0 临时身份验证失败 - Exchange | Microsoft Docs

454 4.7.0 Exchange Server2021/4/9适用于:Exchange Server 2019, Exchange Server 2016 Standard Edition, Exchange Server 2016 Enterprise Edition, Exchange Server 2013 Standard Edition, Exchange Server 2013 Enterprise, Exchange Server 2010 Standard, Exchange Se…

流利说统一可观察性平台实践

简介&#xff1a; 流利说利用日志服务SLS构建统一可观察性平台最佳实践 在线教育行业现状 随着 90 年代互联网的引入&#xff0c;在线教育产品也依托于互联网诞生。随着互联网技术的发展&#xff0c;在线教育产品也开 始了出现新的模式。在线教育从最初单纯的文字形式&#xf…

“CSDN 2021年度IT技术影响力之星评选”正式开启报名!

2021年&#xff0c;数字化转型正磅礴兴起&#xff0c;大批传统企业正在拥抱数字化&#xff0c;云计算、大数据、AI、5G应用能力正在变成企业的核心竞争力&#xff1b;核心技术正在崛起&#xff0c;在操作系统、数据库&#xff0c;依靠开源的力量&#xff0c;众多开发者背后的行…

java log4j logback jcl_Java 日志二三事

前言Java 拥有功能和性能都非常强大的日志库&#xff0c;但另一方面&#xff0c;Java 日志库依赖看起来丰富的让人眼花缭乱。相信大家或多或少都有这样的疑问&#xff0c;Log4j&#xff0c;SLF4J&#xff0c;Logback&#xff0c;Log4j2 这些日志框架我该如何选择&#xff1f;它…

一文了解EPaxos核心协议流程

简介&#xff1a; EPaxos&#xff08;Egalitarian Paxos&#xff09;作为工业界备受瞩目的下一代分布式一致性算法&#xff0c;具有广阔的应用前景。但纵观业内&#xff0c;至今仍未出现一个EPaxos的工程实现&#xff0c;甚至都没看到一篇能把EPaxos讲得通俗一点的文章。EPaxos…

xpspeak安装教程_查漏补缺:教你正确操作 XPS 分峰软件 XPSPEAK

一、引言X射线光电子能谱(XPS)&#xff1a;利用X射线辐射样品&#xff0c;使原子或分子的内层电子或价电子受激发射出来&#xff0c;光子激发出来的电子称为光电子&#xff0c;利用能量分析器分析光电子的能量&#xff0c;作出光电子能谱图&#xff0c;横坐标一般为结合能&…

低代码发展系列专访之五:低代码的最大价值点是“技术平民化”吗?

话题&#xff1a;低代码专访编辑 | LLBin前言&#xff1a;2019年开始&#xff0c;低代码爆火。有人认为它是第四代编程语言&#xff0c;有人认为它是开发模式的颠覆&#xff0c;也有人认为是企业管理模式的变革……有很多声音&#xff0c;社区讨论很热烈。CSDN随后展开低代码平…

云原生不仅颠覆了技术栈,背后的每个岗位也在悄然发生改变

简介&#xff1a; 随着云原生理念与云原生技术的不断完善和发展&#xff0c;越来越多的行业开始落地实践云原生技术&#xff0c;这对不同岗位的技术从业者产生了不同程度的影响。不管是对 IT 主管还是对一线开发人员和运维人员来说&#xff0c;从业务逻辑到技术选型&#xff0c…

梦幻跨服购买需要登录服务器未响应,梦幻西游8月4日定期维护公告:跨服购买限制放宽...

核心提示&#xff1a;法宝”系统新增“多套法宝切换”功能。亲爱的玩家朋友&#xff1a;为保证服务器的运行稳定和服务质量&#xff0c;《梦幻西游2》所有服务器将于2015年8月4日上午8:00停机&#xff0c;进行每周例行的维护工作。预计维护时间为上午8:00&#xff5e;9:45。如果…

element 方法返回的boolean被当成字符串了_JavaScript 原生对象、属性、方法、事件、事件参数...

/*** 事件参考 https://developer.mozilla.org/zh-CN/docs/Web/Events* Event <- UIEvent <- MouseEvent* //** Event https://developer.mozilla.org/zh-CN/docs/Web/API/Event#DOM_Event_interface* detail: 1* sourceCapabilities: InputDeviceCapabilities {fires…

深度技术揭秘 | 大促狂欢背后,如何有效评估并规划数据库计算资源?

简介&#xff1a; 经过“双11”、“618”这类互联网促销活动的验证&#xff0c;越来越多的互联网公司采用不定期营销活动来刺激消费&#xff0c;达到提升营收能力的目标。然而&#xff0c;在每一次业务狂欢的背后&#xff0c;如何科学地为促销活动准备相应的计算资源就变成了困…

学画画软件app推荐_今日推荐:拍照摄影APP之稀缺软件篇

你也许热衷拍摄或喜欢摄影&#xff0c;那么日常的拍摄主要的工具离不开手机&#xff0c;好的拍照摄影APP当然也必不可少。一个好的拍照软件更加重要&#xff0c;有时候市面上常用的拍照软件不能满足你特殊的拍摄手法&#xff0c;经常需要重新编辑或修改才能达到效果&#xff0c…

五大数据库理念,读懂亚马逊云科技的数据库布局

1970 年&#xff0c;关系型数据库之父 E.F.Codd 发表《用于大型共享数据库的关系数据模型》论文&#xff0c;正式拉开数据库技术发展序幕。以 Oracle、DB2、SQL Server 为代表的三大商业数据库产品独占鳌头&#xff0c;随后涌现出 MySQL、PostgreSQL 等为代表的开源数据库 &…

干货|一文读懂阿里云数据库Autoscaling是如何工作的

简介&#xff1a; 阿里云数据库实现了其特有的Autosaling能力&#xff0c;该能力由数据库内核、管控及DAS&#xff08;数据库自治服务&#xff09;团队共同构建&#xff0c;内核及管控团队提供了数据库Autoscaling的基础能力&#xff0c;DAS则负责性能数据的监测、Scaling决策算…