深入理解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 相关文章推荐
jQuery表格插件ParamQuery简单使用方法示例
Dec 05 Javascript
Jquery遍历节点的方法小集
Jan 22 Javascript
jqGrid中文文档之选项设置
Dec 02 Javascript
移动端点击图片放大特效PhotoSwipe.js插件实现
Aug 25 Javascript
浅谈jQuery双事件多重加载的问题
Oct 05 Javascript
jQuery EasyUI右键菜单实现关闭标签/选项卡
Oct 10 Javascript
微信小程序 wxapp地图 map详解
Oct 31 Javascript
关于vue.js过渡css类名的理解(推荐)
Apr 10 Javascript
Node使用Sequlize连接Mysql报错:Access denied for user ‘xxx’@‘localhost’
Jan 03 Javascript
详解vue中axios的封装
Jul 18 Javascript
javascript设计模式 ? 状态模式原理与用法实例分析
Apr 22 Javascript
PHP 502bad gateway原因及解决方案
Nov 13 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实现Ftp用户的在线管理的代码
2007/03/06 PHP
让Nginx支持ThinkPHP的URL重写和PATHINFO的方法分享
2011/08/08 PHP
php模拟js函数unescape的函数代码
2012/10/20 PHP
PHP仿tp实现mvc框架基本设计思路与实现方法分析
2018/05/23 PHP
js 父窗口控制子窗口的行为-打开,关闭,重定位,回复
2010/04/20 Javascript
js中的布尔运算符使用介绍
2013/11/20 Javascript
用unescape反编码得出汉字示例
2014/04/24 Javascript
深入理解JavaScript系列(22):S.O.L.I.D五大原则之依赖倒置原则DIP详解
2015/03/05 Javascript
JS定义网页表单提交(submit)的方法
2015/03/20 Javascript
jQuery团购倒计时特效实现方法
2015/05/07 Javascript
原生JS实现拖拽图片效果
2020/08/27 Javascript
关于在Servelet中如何获取当前时间的操作方法
2016/06/28 Javascript
JS读写CSS样式的方法汇总
2016/08/16 Javascript
js实现常见的工具条效果
2017/03/02 Javascript
JS实现的简单标签点击切换功能示例
2017/09/21 Javascript
vue cli升级webapck4总结
2018/04/04 Javascript
基于JS实现带动画效果的流程进度条
2018/06/01 Javascript
vue.js删除列表中的一行
2018/06/30 Javascript
vue实现word,pdf文件的导出功能
2018/07/31 Javascript
爬虫利器Puppeteer实战
2019/01/09 Javascript
关于NodeJS中的循环引用详解
2019/07/23 NodeJs
JS+CSS+HTML实现“代码雨”类似黑客帝国文字下落效果
2020/03/17 Javascript
Vue-cli 移动端布局和动画使用详解
2020/08/10 Javascript
JavaScript中arguments的使用方法详解
2020/12/20 Javascript
[48:11]完美世界DOTA2联赛 Magma vs GXR 第二场 11.07
2020/11/10 DOTA
python动态监控日志内容的示例
2014/02/16 Python
Python中的并发编程实例
2014/07/07 Python
windows 10下安装搭建django1.10.3和Apache2.4的方法
2017/04/05 Python
人事主管岗位职责范本
2013/12/04 职场文书
室内拓展活动方案
2014/02/13 职场文书
社区矫正工作方案
2014/06/04 职场文书
贯彻落实“八项规定”思想汇报
2014/09/13 职场文书
领导干部遵守党的政治纪律情况思想汇报
2014/09/14 职场文书
教师批评与自我批评(群众路线)
2014/10/15 职场文书
幼儿园教师教育随笔
2015/08/14 职场文书
放假通知怎么写
2015/08/18 职场文书