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框架的实现概要分析 ...
随机推荐
- Android 使用ListView的A-Z字母排序功能实现联系人模块
在上一篇文章其中,主要学习了ListView的A-Z字母排序功能以及依据输入框的输入值改变来过滤搜索结果,假设输入框里面的值为空.更新为原来的列表,否则为过滤数据列表,包含汉字转成拼音的功能.假设你还 ...
- 使用word2010写文章发布到blog
参考文档: http://www.cnblogs.com/liuxianan/archive/2013/04/13/3018732.html 使用Windows Live Writer 2012和Of ...
- Expression Tree 学习笔记(一)
大家可能都知道Expression Tree是.NET 3.5引入的新增功能.不少朋友们已经听说过这一特性,但还没来得及了解.看看博客园里的老赵等诸多牛人,将Expression Tree玩得眼花缭乱 ...
- mybatis 一对一映射
xml <mapper namespace="com.oracle.dao.one2oneDao"> <sql id="personColum" ...
- C++再论单例模式
#include <iostream> #include <windows.h> #include <mutex> std::mutex gmutex; using ...
- Ubuntu E: 无法获得锁 /var/lib/dpkg/lock - open (11: 资源暂时不可用)
E: 无法获得锁 /var/lib/dpkg/lock - open (11: 资源暂时不可用)E: 无法锁定管理目录(/var/lib/dpkg/),是否有其他进程正占用它? 不要删掉这些目录!! ...
- Ubuntu 16.04 LTS 配置 Jupyter notebook 为服务器
原材料: Ubuntu 16.04 LTS 64bit 已经配置好 IPython 和 Jupyter (安装步骤可以参照:http://www.cnblogs.com/McKean/p/619497 ...
- C++ string 实现大整数相加减
随意两个大整数的加减算法.可自己主动推断正负号.代码例如以下: #include <iostream> #include <vector> #include <cstri ...
- linux进程间通信消息队列:msgsnd: Invalid argument
今天写了个消息队列的小测试程序结果send端程序总是出现:msgsnd: Invalid argument,搞了半个小时也没搞明白,后来查资料发现我将(st_msg_buf.msg_type = 0; ...
- Codeforces Round #422 (Div. 2) D. My pretty girl Noora 数学
D. My pretty girl Noora In Pavlopolis University where Noora studies it was decided to hold beau ...