深入理解Vue Computed计算属性原理


Posted in Javascript onMay 29, 2018

Computed 计算属性是 Vue 中常用的一个功能,但你理解它是怎么工作的吗?

拿官网简单的例子来看一下:

<div id="example">
 <p>Original message: "{{ message }}"</p>
 <p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
var vm = new Vue({
 el: '#example',
 data: {
  message: 'Hello'
 },
 computed: {
  // a computed getter
  reversedMessage: function () {
   // `this` points to the vm instance
   return this.message.split('').reverse().join('')
  }
 }
})

Situation

Vue 里的 Computed 属性非常频繁的被使用到,但并不是很清楚它的实现原理。比如:计算属性如何与属性建立依赖关系?属性发生变化又如何通知到计算属性重新计算?

关于如何建立依赖关系,我的第一个想到的就是语法解析,但这样太浪费性能,因此排除,第二个想到的就是利用 JavaScript 单线程的原理和 Vue 的 Getter 设计,通过一个简单的发布订阅,就可以在一次计算属性求值的过程中收集到相关依赖。

因此接下来的任务就是从 Vue 源码一步步分析 Computed 的实现原理。

Task

分析依赖收集实现原理,分析动态计算实现原理。

Action

data 属性初始化 getter setter:

// src/observer/index.js

// 这里开始转换 data 的 getter setter,原始值已存入到 __ob__ 属性中
Object.defineProperty(obj, key, {
 enumerable: true,
 configurable: true,
 get: function reactiveGetter () {
  const value = getter ? getter.call(obj) : val
  // 判断是否处于依赖收集状态
  if (Dep.target) {
   // 建立依赖关系
   dep.depend()
   ...
  }
  return value
 },
 set: function reactiveSetter (newVal) {
  ...
  // 依赖发生变化,通知到计算属性重新计算
  dep.notify()
 }
})

computed 计算属性初始化

// src/core/instance/state.js

// 初始化计算属性
function initComputed (vm: Component, computed: Object) {
 ...
 // 遍历 computed 计算属性
 for (const key in computed) {
  ...
  // 创建 Watcher 实例
  // create internal watcher for the computed property.
  watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)

  // 创建属性 vm.reversedMessage,并将提供的函数将用作属性 vm.reversedMessage 的 getter,
  // 最终 computed 与 data 会一起混合到 vm 下,所以当 computed 与 data 存在重名属性时会抛出警告
  defineComputed(vm, key, userDef)
  ...
 }
}

export function defineComputed (target: any, key: string, userDef: Object | Function) {
 ...
 // 创建 get set 方法
 sharedPropertyDefinition.get = createComputedGetter(key)
 sharedPropertyDefinition.set = noop
 ...
 // 创建属性 vm.reversedMessage,并初始化 getter setter
 Object.defineProperty(target, key, sharedPropertyDefinition)
}

function createComputedGetter (key) {
 return function computedGetter () {
  const watcher = this._computedWatchers && this._computedWatchers[key]
  if (watcher) {
   if (watcher.dirty) {
    // watcher 暴露 evaluate 方法用于取值操作
    watcher.evaluate()
   }
   // 同第1步,判断是否处于依赖收集状态
   if (Dep.target) {
    watcher.depend()
   }
   return watcher.value
  }
 }
}

无论是属性还是计算属性,都会生成一个对应的 watcher 实例。

// src/core/observer/watcher.js

// 当通过 vm.reversedMessage 获取计算属性时,就会进到这个 getter 方法
get () {
 // this 指的是 watcher 实例
 // 将当前 watcher 实例暂存到 Dep.target,这就表示开启了依赖收集任务
 pushTarget(this)
 let value
 const vm = this.vm
 try {
  // 在执行 vm.reversedMessage 的函调函数时,会触发属性(步骤1)和计算属性(步骤2)的 getter
  // 在这个执行过程中,就可以收集到 vm.reversedMessage 的依赖了
  value = this.getter.call(vm, vm)
 } catch (e) {
  if (this.user) {
   handleError(e, vm, `getter for watcher "${this.expression}"`)
  } else {
   throw e
  }
 } finally {
  if (this.deep) {
   traverse(value)
  }
  // 结束依赖收集任务
  popTarget()
  this.cleanupDeps()
 }
 return value
}

上面多出提到了 dep.depend, dep.notify, Dep.target,那么 Dep 究竟是什么呢?

Dep 的代码短小精悍,但却承担着非常重要的依赖收集环节。

// src/core/observer/dep.js

export default class Dep {
 static target: ?Watcher;
 id: number;
 subs: Array<Watcher>;

 constructor () {
  this.id = uid++
  this.subs = []
 }

 addSub (sub: Watcher) {
  this.subs.push(sub)
 }

 removeSub (sub: Watcher) {
  remove(this.subs, sub)
 }

 depend () {
  if (Dep.target) {
   Dep.target.addDep(this)
  }
 }

 notify () {
  const subs = this.subs.slice()
  for (let i = 0, l = subs.length; i < l; i++) {
   // 更新 watcher 的值,与 watcher.evaluate() 类似,
   // 但 update 是给依赖变化时使用的,包含对 watch 的处理
   subs[i].update()
  }
 }
}

// 当首次计算 computed 属性的值时,Dep 将会在计算期间对依赖进行收集
Dep.target = null
const targetStack = []

export function pushTarget (_target: Watcher) {
 // 在一次依赖收集期间,如果有其他依赖收集任务开始(比如:当前 computed 计算属性嵌套其他 computed 计算属性),
 // 那么将会把当前 target 暂存到 targetStack,先进行其他 target 的依赖收集,
 if (Dep.target) targetStack.push(Dep.target)
 Dep.target = _target
}

export function popTarget () {
 // 当嵌套的依赖收集任务完成后,将 target 恢复为上一层的 Watcher,并继续做依赖收集
 Dep.target = targetStack.pop()
}

Result

总结一下依赖收集、动态计算的流程:

1. data 属性初始化 getter setter

2. computed 计算属性初始化,提供的函数将用作属性 vm.reversedMessage 的 getter

3. 当首次获取 reversedMessage 计算属性的值时,Dep 开始依赖收集

4. 在执行 message getter 方法时,如果 Dep 处于依赖收集状态,则判定 message 为 reversedMessage 的依赖,并建立依赖关系

5. 当 message 发生变化时,根据依赖关系,触发 reverseMessage 的重新计算
到此,整个 Computed 的工作流程就理清楚了。

Vue 是一个设计非常优美的框架,使用 Getter Setter 设计使依赖关系实现的非常顺其自然,使用计算与渲染分离的设计(优先使用 MutationObserver,降级使用 setTimeout)也非常贴合浏览器计算引擎与排版引擎分离的的设计原理。

如果你想成为一名架构师,不能只停留在框架的 API 使用层面。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
Ext第一周 史上最强学习笔记---GridPanel(基础篇)
Dec 29 Javascript
JavaScript高级程序设计 阅读笔记(四) ECMAScript中的类型转换
Feb 27 Javascript
jQuery实现图片与文字描述左右滑动自动切换的方法
Jul 27 Javascript
javascript实现随机生成DIV背景色
Jun 20 Javascript
多功能jQuery树插件zTree实现权限列表简单实例
Jul 12 Javascript
基于jQuery实现表格的排序
Dec 02 Javascript
写jQuery插件时的注意点
Feb 20 Javascript
vue学习笔记之指令v-text &amp;&amp; v-html &amp;&amp; v-bind详解
May 12 Javascript
vue-resource 拦截器(interceptor)的使用详解
Jul 04 Javascript
浅谈 vue 中的 watcher
Dec 04 Javascript
layui多图上传实现删除功能的例子
Sep 23 Javascript
微信小程序实现分享商品海报功能
Sep 30 Javascript
javascript、php关键字搜索函数的使用方法
May 29 #Javascript
Vue路由切换时的左滑和右滑效果示例
May 29 #Javascript
Vue 组件传值几种常用方法【总结】
May 28 #Javascript
讲解vue-router之命名路由和命名视图
May 28 #Javascript
微信小程序实现图片上传功能
May 28 #Javascript
微信小程序上传图片功能(附后端代码)
Jun 19 #Javascript
讲解vue-router之什么是编程式路由
May 28 #Javascript
You might like
PHP合并数组+与array_merge的区别分析
2010/08/01 PHP
PHP中的函数嵌套层数限制分析
2011/06/13 PHP
javascript模仿msgbox提示效果代码
2008/06/10 Javascript
一些常用的JS功能函数(2009-06-04更新)
2009/06/04 Javascript
JavaScript 学习笔记(九)call和apply方法
2010/01/11 Javascript
新手常遇到的一些jquery问题整理
2010/08/16 Javascript
javscript对象原型的一些看法
2010/09/19 Javascript
js里怎么取select标签里的值并修改
2012/12/10 Javascript
Jquery如何实现点击时高亮显示代码
2014/01/22 Javascript
jQuery CSS()方法改变现有的CSS样式表
2014/09/09 Javascript
AngularJS入门教程之学习环境搭建
2014/12/06 Javascript
JS如何判断是否为ie浏览器的方法(包括IE10、IE11在内)
2015/12/13 Javascript
jquery对象访问是什么及使用方法介绍
2016/05/03 Javascript
微信小程序 教程之注册程序
2016/10/17 Javascript
探索Vue.js component内容实现
2016/11/03 Javascript
详解JavaScript RegExp对象
2017/02/04 Javascript
详解NodeJS框架express的路径映射(路由)功能及控制
2017/03/24 NodeJs
详解webpack import()动态加载模块踩坑
2018/07/17 Javascript
手把手教你使用TypeScript开发Node.js应用
2019/05/06 Javascript
初学node.js中实现删除用户路由
2019/05/27 Javascript
js实现抽奖的两种方法
2020/03/19 Javascript
详解python如何调用C/C++底层库与互相传值
2016/08/10 Python
Python使用add_subplot与subplot画子图操作示例
2018/06/01 Python
对Python 除法负数取商的取整方式详解
2018/12/12 Python
Python GUI编程学习笔记之tkinter事件绑定操作详解
2020/03/30 Python
Django实现微信小程序支付的示例代码
2020/09/03 Python
html5中svg canvas和图片之间相互转化思路代码
2014/01/24 HTML / CSS
英国领先的男装设计师服装购物网站:Mainline Menswear
2018/02/04 全球购物
泰国在线书店:SE-ED
2020/06/21 全球购物
维德科技C#面试题笔试题
2015/12/09 面试题
十八届三中全会个人学习材料
2014/02/13 职场文书
化学系大学生自荐信范文
2014/03/01 职场文书
员工拾金不昧表扬稿
2015/05/05 职场文书
网吧管理制度范本
2015/08/05 职场文书
vite+vue3.0+ts+element-plus快速搭建项目的实现
2021/06/24 Vue.js
CSS实现渐变色边框(Gradient borders)的5种方法
2022/03/25 HTML / CSS