Vue 依赖收集原理分析
此文已由作者吴维伟授权网易云社区发布。
欢迎访问网易云社区,了解更多网易技术产品运营经验。
Vue实例在初始化时,可以接受以下几类数据:
模板
初始化数据
传递给组件的属性值
computed
watch
methods
Vue 根据实例化时接受的数据,在将数据和模板转化成DOM节点的同时,分析其依赖的数据。在特定数据改变时,自动在下一个周期重新渲染DOM节点
本文主要分析Vue是如何进行依赖收集的。
Vue中,与依赖收集相关的类有:
Dep : 一个订阅者的列表类,可以增加或删除订阅者,可以向订阅者发送消息
Watcher : 订阅者类。它在初始化时可以接受getter, callback两个函数作为参数。getter用来计算Watcher对象的值。当Watcher被触发时,会重新通过getter计算当前Watcher的值,如果值改变,则会执行callback.
对初始化数据的处理
对于一个Vue组件,需要一个初始化数据的生成函数。如下:
export default {
data () {
return {
text: 'some texts',
arr: [],
obj: {}
}
}
}
Vue为数据中的每一个key维护一个订阅者列表。对于生成的数据,通过Object.defineProperty对其中的每一个key进行处理,主要是为每一个key设置get, set方法,以此来为对应的key收集订阅者,并在值改变时通知对应的订阅者。部分代码如下:
const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return
} // cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) {
dep.depend() if (childOb) {
childOb.dep.depend()
} if (Array.isArray(value)) {
dependArray(value)
}
} return value
}, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) { return
} /* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
} if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = observe(newVal)
dep.notify()
}
})
每一key都有一个订阅者列表 const dep = new Dep()
在为key进行赋值时,如果值发生了改变,则会通知所有的订阅者 dep.notify()
在对key进行取值时,如果Dep.target有值,除正常的取值操作外会进行一些额外的操作来添加订阅者。大多数时间里,Dep.target的值都为null,只有订阅者在进行订阅操作时,Dep.target才有值,为正在进行订阅的订阅者。此时进行取值操作,会将订阅者加入到对应的订阅者列表中。
订阅者在进行订阅操作时,主要包含以下3个步骤:
将自己放在Dep.target上
对自己依赖的key进行取值
将自己从Dep.target移除
在执行订阅操作后,订阅者会被加入到相关key的订阅者列表中。
针对对象和数组的处理
如果为key赋的值为对象:
会递归地对这个对象中的每一key进行处理
如果为key赋的值为数组:
递归地对这个数组中的每一个对象进行处理
重新定义数组的push,pop,shift,unshift,splice,sort,reverse方法,调用以上方法时key的订阅者列表会通知订阅者们“值已改变”。如果调用的是push,unshift,splice方法,递归处理新增加的项
对模板的处理
Vue将模板处理成一个render函数。需要重新渲染DOM时,render函数结合Vue实例中的数据生成一个虚拟节点。新的虚拟节点和原虚拟节点进行对比,对需要修改的DOM节点进行修改。
订阅者
订阅者在初始化时主要接受2个参数getter, callback。getter用来计算订阅者的值,所以其在执行时会对订阅者所有需要订阅的key进行取值。订阅者的订阅操作主要是通过getter来实现。
部分代码如下:
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this) let value
const vm = this.vm if (this.user) { try { value = this.getter.call(vm, vm)
} catch (e) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
}
} else { value = this.getter.call(vm, vm)
} // "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget() this.cleanupDeps() return value
}
主要步骤:
将自己放在Dep.target上(pushTarget(this))
执行getter(this.getter.call(vm, vm))
将自己从Dep.target移除(popTarget())
清理之前的订阅(this.cleanupDeps())
此后,订阅者在依赖的key的值发生变化会得到通知。获得通知的订阅者并不会立即被触发,而是会被加入到一个待触发的数组中,在下一个周期统一被触发。
订阅者在被触发时,会执行getter来计算订阅者的值,如果值改变,则会执行callback.
负责渲染DOM的订阅者
Vue实例化后都会生成一个用于渲染DOM的订阅者。此订阅者在实例化时传入的getter方法为渲染DOM的方法。
部分代码如下:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
vm._watcher = new Watcher(vm, updateComponent, noop)
vm._render()结合模板和数据,计算出虚拟DOM vm._update()根据虚拟DOM渲染真实的DOM节点
此订阅者在初始化时就会进行订阅操作。实例化时传入的getter为updateComponent。其中的vm._render()在执行时一定会对所有依赖的key进行取值,能完成对依赖的key的订阅。同时vm._update()完成了第一次DOM渲染。当前依赖的key的值发生变化,订阅者被触发时,作为getter的updateComponent会重新执行,重新渲染DOM。因为getter返回的值一直为undefined,所以此订阅者中的callback并没有被用到,于是传入了一个空函数noop作为callback
对computed的处理
通过computed可以定义一组计算属性,通过计算属性可以将一些复杂的计算过程抽离出来,保持模板的简单和清晰。
代码示例:
export default {
data () { return { text: 'some texts', arr: [], obj: {}
}
}, computed: { key1: function () { return this.text + this.arr.length
}
}
}
在定义一个计算属性时,需要定义一个key和一个计算方法。
Vue在对computed进行处理时,会为每一个计算属性生成一个lazy状态的订阅者。普通的订阅者在实例化和触发时会执行getter来计算自身的值和进行订阅操作。而lazy状态的订阅者在上述情况下只会将自身置为dirty状态,不进行其它操作。在订阅者执行自身的evaluate方法时,会清除自身的dirty状态并执行getter来计算自身的值和进行订阅。
Vue在为计算属性生成订阅者时的示例代码如下:
const computedWatcherOptions = { lazy: true }// create internal watcher for the computed property.watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)
传入的getter为自定义的计算方法,callback为空函数。(lazy状态的订阅者永远都没有机会执行callback)
Vue 在自身实例上为指定key定义get方法,使可以通过Vue实例获取计算属性的值。
部分代码如下:
function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { if (watcher.dirty) {
watcher.evaluate()
} if (Dep.target) {
watcher.depend()
} return watcher.value
}
}
}
在对计算属性定义的key进行取值时,会首先获取之前生成好的订阅者。只有订阅者处于dirty状态时,才会执行evaluate计算订阅者的值。所以为计算属性定义的计算方法只有在对计算属性的key进行取值并且计算属性依赖的key曾经改变时才会执行。
假如对上文定义的计算属性key1进行取值
vm.key1; //第一次取值,自定义计算方法执行vm.key1; //第二次取值,依赖的key的值没有变化,自定义计算方法不会执行vm.text = '' //改变计算属性依赖的key的值,计算属性对应的订阅者会进入dirty状态,自定义计算方法不会执行vm.key1; //第三次取值,计算属性依赖的key的值发生了变化并且对计算属性进行取值,自定义的计算方法执行
订阅计算属性值的变化
计算属性的key不会维护一个订阅者列表,也不能通过计算属性的set方法在触发所有订阅者。(计算属性不能被赋值)。一个订阅者执行订阅操作来订阅计算属性值的变化其实是订阅了计算属性依赖的key的值的变化。 在计算属性的get方法中
if (Dep.target) { watcher.depend()}
如果有订阅者来订阅计算属性的变化,计算属性会将自己的订阅复制到正在进行订阅的订阅者上。watcher.depend()的作用就是如此。
例如:
//初始化订阅者watcher, 依赖计算属性key1var watcher = new Watcher(function () { return vm.key1
}, noop)
vm.text = '' //计算属性key1依赖的text的值发生变化,watcher会被触发
对watch的处理
Vue实例化时可以传入watch对象,来监听某些值的变化。 例如:
export default {
watch: { 'a.b.c': function (val, oldVal) { console.log(val) console.log(oldVal)
}
}
}
Vue 会为watch中的每一项生成一个订阅者。订阅者的getter通过处理字符串得到。如'a.b.c'会被处理成
function (vm) { var a = vm.a var b = a.b var c = b.c return c
}
处理字符串的源码如下:
/**
* Parse simple path.
*/const bailRE = /[^\w.$]/export function parsePath (path: string): any { if (bailRE.test(path)) { return
} const segments = path.split('.') return function (obj) { for (let i = 0; i < segments.length; i++) { if (!obj) return
obj = obj[segments[i]]
} return obj
}
}
订阅者的callback为定义watch时传入的监听函数。当订阅者被触发时,如果订阅者的值发生变化,则会执行callback。callback执行时会传入变化后的值,变化前的值作为参数。
网易云免费体验馆,0成本体验20+款云产品!
更多网易技术、产品、运营经验分享请点击。
相关文章:
【推荐】 == vs === in Javascript
Vue 依赖收集原理分析的更多相关文章
- Spring依赖注入原理分析
在分析原理之前我们先回顾下依赖注入的概念: 我们常提起的依赖注入(Dependency Injection)和控制反转(Inversion of Control)是同一个概念.具体含义是:当某个角色( ...
- 三、vue依赖收集
Vue 会把普通对象变成响应式对象,响应式对象 getter 相关的逻辑就是做依赖收集,这一节我们来详细分析这个过程 Dep Dep 是整个 getter 依赖收集的核心,它的定义在 src/core ...
- Vue依赖收集引发的问题
问题背景 在我们的项目中有一个可视化配置的模块,是通过go.js生成canvas来实现的.但是,我们发现这个模块在浏览器中经常会引起该tab页崩溃.开启chrome的任务管理器一看,进入该页面内存和c ...
- vue双向绑定原理分析
当我们学习angular或者vue的时候,其双向绑定为我们开发带来了诸多便捷,今天我们就来分析一下vue双向绑定的原理. 简易vue源码地址:https://github.com/jiangzhenf ...
- Vue双向数据绑定原理分析(转)
add by zhj: 目前组里使用的是前端技术是jQuery + Bootstrap,后端使用的Django,Flask等,模板是在后端渲染的.前后端没有分离,这种做法有几个缺点 1. 模板一般是由 ...
- Spring学习笔记——Spring依赖注入原理分析
我们知道Spring的依赖注入有四种方式,各自是get/set方法注入.构造器注入.静态工厂方法注入.实例工厂方法注入 以下我们先分析下这几种注入方式 1.get/set方法注入 public cla ...
- Vue 双向数据绑定原理分析 以及 Object.defineproperty语法
第三方精简版实现 https://github.com/luobotang/simply-vue Object.defineProperty 学习,打开控制台分别输入以下内容调试结果 userInfo ...
- 网站统计中的数据收集原理及实现(share)
转载自:http://blog.codinglabs.org/articles/how-web-analytics-data-collection-system-work.html 网站数据统计分析工 ...
- Spark原理分析目录
1 Spark原理分析 -- RDD的Partitioner原理分析 2 Spark原理分析 -- RDD的shuffle简介 3 Spark原理分析 -- RDD的shuffle框架的实现概要分析 ...
随机推荐
- keras常见问题解答
https://keras.io/zh/getting-started/faq/ https://keras.io/zh/ https://github.com/keras-team/keras/tr ...
- cart树剪枝
当前子树的损失函数: $C_a(T) = C(T) + a|T|$, 其中$C(T)$为对训练数据的预测误差,$|T|$为树的叶子结点数目,反映模型的复杂度.对固定的$a$,一定存在使损失函数$C_a ...
- Spring -- Bean自己主动装配&Bean之间关系&Bean的作用域
对于学习spring有帮助的站点:http://jinnianshilongnian.iteye.com/blog/1482071 Bean的自己主动装配 Spring IOC 容器能够自己主动装配 ...
- 【JavaScript】数据类型
学习不论什么一种程序设计语言.数据类型都是不可缺少的一部分内容,非常基础,也非常重要.该用何种数据类型定义变量.这也是编程中最基础的一项. ECMAScript中有5种简单数据类型:Undefined ...
- 如何干掉那些.ipch 与 .sdf文件
参考资料: http://blog.163.com/yangjun1988422@126/blog/static/47412917201074446054/ vs2010中臃肿的ipch和sd ...
- hive:Access denied for user 'root'@'%'
配置hive全分布模式时候,在mysql里面创建用户:create user 'hive' identified by 'hive'; 然后给hive帐号分配全部权限: grant all privi ...
- [Elasticsearch] 部分匹配 (四) - 索引期间优化ngrams及索引期间的即时搜索
本章翻译自Elasticsearch官方指南的Partial Matching一章. 索引期间的优化(Index-time Optimizations) 眼下我们讨论的全部方案都是在查询期间的.它们不 ...
- IE8与vs2005冲突 添加MFC类向导错误解决方法—— internet explorer脚本错误
IE8 与 VS2005 冲突问题解决方法 问题表现为: MFC类向导添加类时,出现“当前页面的脚本发生错误”,进入MFC类向导后上方有一个小黄条“此网站的某个加载项运行失败.请检查"Int ...
- eclipse通过maven建立java se工程配置log4j,打包成zip,将jar包和配置文件分开,并以bat和sh文件启动java程序
一.新建maven的java工程 1.eclipse里file-new-other,选择maven Project 2.选中 Use default Workspace location,然后 nex ...
- Class.forName("java.lang.String")的作用?
返回字节码: 返回的方式有2种: 第一种是这个类的字节码已经加载到内存里面来了,现在想要取到它的字节码,我直接找到那份字节码把他返回: 第二种是我去得到这个类的字节码,结果在虚拟机里面还没有这个类的字 ...