您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
探索Vue computed 之谜
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
探索Vue computed 之谜
京东ZERO团队
2021-01-07
IP归属:未知
82000浏览
Vue
## 一、背景 在我们认识的大前端众多框架中,Vue是一套用于构建用户界面的渐进式框架,想就Vue中的computed与大家探讨一下。说到computed,对它的存在就会很感兴趣,那它存在的原因是什么呢?自然会引发大家的思考,可以想象当在模板中放入太多的逻辑,模板肯定会更加冗余,略显笨重导致难以维护。所以亟需将复杂的逻辑简单化,如此一来,computed也就应运而生了。 ## 二、案例说明 ```html <div id="computed"> <p>testMessage的值</p> </div> ``` ```js var vm = new Vue({ el: '# computed ', data: { message: 'HelloWorld' }, computed: {//代码1 testMessage: function () { return this.message.reverse().split('').join('') // dlroWolleH } } }) ``` 在vue中,computed属性类似于data,我们可以对其进行读取值和设置值的操作。在computed中分别对应着getter和setter,默认情况下,computed只有getter,没有setter,这也就意味着只能读取,不能改变设值。即computed默认格式: ```js var vm = new Vue({ el: '# computed ', data: { message: 'HelloWorld' }, computed: {//代码2 testMessage: { // 计算属性的 getter get: function() { return this.message.reverse().split('').join('') // dlroWolleH }, // 计算属性的 setter // set: function(newValue) { // this.message = newValue // } } }, }) ``` 如果在模板组件中需要修改计算属性自身值时,就会通过调用computed属性的setter函数实现,而且setter函数和getter函数由于各自是相互独立的。所以,只有在setter函数中触发message值的更改,当message改变了,getter函数中值才会改变。 ## 三、原理分析 由于computed也是响应式的,所以为了更深入了解其内在机制,在分析computed之前需要对响应式系统有个初步了解,Vue的响应式原理主要是通过Object.defineProperty实现的。被Object.defineProperty绑定过的对象,会具有响应式化的功能。也就是改变这个对象的时候会触发get和set事件,从而就会触发一些视图更新。 ![image.png](https://img13.360buyimg.com/imagetools/jfs/t1/134907/30/9807/311786/5f5b632bE1ca1980b/0aa7efa98dee559b.png) 如上图所示,Object.defineProperty会把Data中属性都转成getter/setter,进行追踪依赖和数据劫持,在属性被修改或者被访问时通知变化,每个组件实例有对应的watcher,在组件被渲染时把属性当成依赖,每个属性都有消息订阅器dep用于存放订阅的watcher对象,根据dep监听依赖项setter的调用通知watcher重新计算,从而使它关联的组件得到更新。 对响应式有了基本了解后,我们可以进入computed的分析了。 ## 四、源码解析 Computed的过程主要经历了这几个阶段: initComputed,definedComputed,createComputedGetter,watcher.evaluate,下面将从这几个方面进行源码解析。 #### 4.1 initComputed 计算属性的初始化主要是在src/core/instance/state.js文件中的initeState函数中去做的。 ```js /** * 初始化的开始之路 */ export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options if (opts.props) initProps(vm, opts.props) if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { initData(vm) } else { observe(vm._data = {}, true /* asRootData */) } /** * computed的初始化 */ if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } } ``` Vue在初始化组件的时候会调用initState()函数,此函数会对props、methods、data、computed以及watch等进行初始化操作。当对computed初始化时会调用initComputed(),并对其传入二个参数分别是vm和开发者定义的opts.computed选项。 ```js /** * initComputed() */ function initComputed (vm, computed) { var watchers = vm._computedWatchers = Object.create(null); // computed properties are just getters during SSR var isSSR = isServerRendering(); for (var key in computed) { var userDef = computed[key]; var getter = typeof userDef === 'function' ? userDef : userDef.get; if (!isSSR) { watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions //初始化定义为{ lazy: true } ); } if (!(key in vm)) { defineComputed(vm, key, userDef); } else { //错误提示 if (key in vm.$data) { warn(("The computed property \"" + key + "\" is already defined in data."), vm); } else if (vm.$options.props && key in vm.$options.props) { warn(("The computed property \"" + key + "\" is already defined as a prop."), vm); } } } } ``` 首先,创建一个空对象(watchers对象)并赋值给_computedWatchers并挂载到vm对象下,watchers主要用于存储computed中的方法并以键值对的方式存储。我们判断是否为服务器端的渲染,如果是服务器端渲染,就不会给computed添加watcher,因为在服务器端渲染的情况下,computed只有getter方法。遍历用户定义的所有计算属性,创建某个变量userDef存储该计算属性的get方法。如果用案例中代码1位置的方式定义计算属性,Vue就会将该计算属性对应的function作为它getter。如果用案例中代码2位置的方式定义计算属性,userDef存储的就是用户定义的get方法。当判断不是服务器端渲染时,会为每一个计算属性创建一个观察者watcher,并传入vue实例和{lazy:true},然后判断计算属性是否在data和prop中存在,不存在就执行definedComputed操作,否则就会报错。 #### 4.2 definedComputed ```js /** * definedComputed() */ const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop } export function defineComputed ( target: any,//vue实例 key: string,//计算属性 userDef: Object | Function//计算属性的定义 ) { const shouldCache = !isServerRendering() if (typeof userDef === 'function') { sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : userDef sharedPropertyDefinition.set = noop } else { sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? createComputedGetter(key) : userDef.get : noop sharedPropertyDefinition.set = userDef.set ? userDef.set : noop } if (process.env.NODE_ENV !== 'production' && sharedPropertyDefinition.set === noop) { sharedPropertyDefinition.set = function () { warn( `Computed property "${key}" was assigned to but it has no setter.`, this ) } } //将计算属性绑定在vue实例上 Object.defineProperty(target, key, sharedPropertyDefinition) } ``` defineComputed函数首先接收三个参数target、key、userDef,分别是该组件、计算属性以及计算属性对应的方法,defineComputed方法中创建了一个完整的属性描述符sharedPropertyDefinition,通过判断是否给该组件的computed设置了非自定义getter方法,同时判断是否非服务端渲染,然后在满足各种的情况下,重写了get和set方法,将会执行 createComputedGetter(key),最后用新的sharedPropertyDefinition把computed的key挂载到vm上,当我们访问这个属性时,就执行sharedPropertyDefinition中的get方法。这个get方法走的其实就是函数createComputedGetter(key)。这段代码主要是根据组件的不同状态,将计算属性绑定到vm组件上。 #### 4.3 createComputedGetter ```js /** * createComputedGetter() */ function createComputedGetter(key) { return function computedGetter() { var watcher = this._computedWatchers && this._computedWatchers[key]; if (watcher) { if (watcher.dirty) { watcher.evaluate(); } //获取依赖 if (Dep.target) { watcher.depend(); } return watcher.value } } } ``` 通过计算属性key的值找到对应的watcher,观察者就会执行观察的操作。通过watcher.depend()进行依赖收集绑定,正如案例中就是将message和testMessag进行绑定。如上代码中的watcher.dirty值决定着计算属性值是否需要进行重新计算,初始化的计算属性的watcher.dirty属性默认值为true,即第一次时会调用一次,因为初始化开始时{lazy:true},如下代码中可以看到在watcher的构造方法中,dirty = lazy,每次执行之后watcher.dirty会设置为false,只要依赖的data值改变时才会触发watcher属性的dirty为true,从而获取值时会重新计算,执行watcher.evaluate()。 #### 4.4 watcher.evaluate ```js /** * watcher.evaluate() */ class Watcher{ constructor(vm,expOrFn,cb,options){ this.vm = vm if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy }else{ this.deep = this.user = this.lazy = false } this.dirty = this.lazy this.cb = cb this.id = ++uid this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() if (typeof expOrFn === 'function') { //data依赖收集 this.getter = expOrFn } else { //watch依赖 this.getter = this.parsePath(expOrFn) } //设置Dep.target的值,依赖收集时的watcher对象 this.value = this.lazy ? undefined : this.get() } get(){ //设置Dep.target值,用以依赖收集 pushTarget(this) const vm = this.vm //此处会进行依赖收集 会调用data数据的get方法 let value = this.getter.call(vm, vm) popTarget() return value } //添加依赖 addDep (dep) { //去重 const id = dep.id if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { //收集watcher,每次data数据set时会遍历收集的watcher依赖进行相应视图更新或执行watch dep.addSub(this) } } } //更新 update () { if(this.lazy) { this.dirty = true }else{ this.run() } } //更新视图 run(){ const value = this.get() const oldValue = this.value this.value = value if (this.user) { //watch 监听走此处 this.cb.call(this.vm, value, oldValue) }else{ //data 监听走此处,这里会去执行Vue的diff相关方法,从而更新数据视图 this.cb.call(this.vm, value, oldValue) } } //如果计算属性依赖的data值发生变化时会调用,也即案例中当message值发生变化时会执行此方法 evaluate () { this.value = this.get() this.dirty = false } //收集依赖 depend () { let i = this.deps.length while (i--) { this.deps[i].depend() } } } } ``` watcher.evaluate()这块就会去执行this.get()并进行求值操作。在我们执行this.get的时候,会为Dep.target赋值,所以就会执行watcher.depend(),这里会对计算属性进行依赖收集,订阅该计算属性中的所有依赖值,当他们发生变化的时候就会执行dep.notify()操作,对该计算属性重新计算。当所依赖的值一旦改变,会通知计算属性,执行update方法,这时才会把this.dirty = true,之后会触发watcher的getter方法获取计算属性改变后关联属性的值,并将新的值更改到watcher对象下,从而进行数据更新。如果依赖值没有改变,那么多次访问该计算属性,也不会重新计算,返回的是上一次的计算值,这也就体现了computed数据的缓存。 ## 五、总结 当组件初始化的时候,computed和data一样都会有自身的响应系统。Observer会遍历data中的各个属性并执行get和set的数据劫持,通过watcher的内实例化的消息订阅器dep进行数据data收集依赖,初始化computed调用initComputed函数,当某个属性发生变化时就会触发set函数调用自身的dep.notify(),遍历当前dep中保存的所有的订阅者watcher值,并调用其update方法进行数据响应,从而更新数据。
原创文章,需联系作者,授权转载
上一篇:Web 字体 font-family 浅谈
下一篇:揭开烛龙平台卡顿监听的神秘面纱暨Android卡顿监听原理
相关文章
【技术干货】企业级扫描平台EOS关于JS扫描落地与实践!
京东mPaaS平台之Android组件化系统私有化部署改造实践!
【技术干货】企业级扫描平台EOS-Jenkins集群进阶之路
京东ZERO团队
文章数
39
阅读量
91754
作者其他文章
01
webpack打包组件配置(React版本)
这篇文章是以打包react插件的形式,介绍webpack的一些配置信息。如果写简单插件的话还是推荐使用rollup,但是可以用写插件的形式去学习一下webpack的一些东西。(适用于初中级webpack学者)
01
webpack核心概念与基本实现
webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。**
01
Typescript合成Webpack中
TypeScript是JavaScript类型的超集,它可以编译成纯JavaScript,简称ts。相对于ES6,TypeScript最大的改善是增加了类型系统,国内外很多大型工程都用它,如AngularJs,白鹭引擎、Antd。
01
小程序加载svg图片
小程序的[组件](https://developers.weixin.qq.com/miniprogram/dev/component/)中是没有支持`SVG`标签的。 但是在前端小伙伴的实际开发中,UED经常提供SVG图片过来,如果不想用引入`iconfont`的话,那么妹子我将介绍个很好用的方法。
最新回复
丨
点赞排行
共0条评论
京东ZERO团队
文章数
39
阅读量
91754
作者其他文章
01
webpack打包组件配置(React版本)
01
webpack核心概念与基本实现
01
Typescript合成Webpack中
01
小程序加载svg图片
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号