Vue 源码解析(一):依赖收集(Observer,Dep与Watcher对象)
文章目录
- Vue 实例新建流程
- initData初始化数据
- initComputed初始化计算属性
- defineComputed 定义计算属性
- Vue 数据属性依赖收集流程总结
- 数组直接赋值不能更新原因
Vue 实例新建流程
Vue源码目录结构说明
- src
- compiler
解析模板生成AST和render函数
- core
- components
目前只有keep-alive组件
- global-api
向Vue对象注入全局方法:Vue.use(),Vue.extend()等
- instance
向Vue实例对象注入方法:this.$emit(),this.$forceUpdate()等
- observer
实现data与Watch对象的依赖收集与更新
- util
工具类
- vdom
Vdom有关方法
- components
- entries
Vue 不同类型源码入口
- platforms
- server
- sfc
- shared
- compiler
这次主要我们用到的目录只有:global-api,instance,observer三个。主要介绍Vue实例化时初始数据和计算属性时的源码具体内容。
initData初始化数据
Vue.prototype._init = function (options?: Object) {const vm: Component = this// a uidvm._uid = uid++...// a flag to avoid this being observedvm._isVue = true...// expose real selfvm._self = vminitLifecycle(vm)initEvents(vm)initRender(vm)callHook(vm, 'beforeCreate')initInjections(vm) // resolve injections before data/propsinitState(vm)initProvide(vm) // resolve provide after data/propscallHook(vm, 'created')...if (vm.$options.el) {vm.$mount(vm.$options.el)}}
}
当我们在 new Vue()
时首先调用得就是 _init()
方法。它主要初始化生命周期,事件,渲染相关数据,调用 beforeCreate
钩子,初始化provide/injections相关数据,初始化data相关数据,调用 created
钩子。最后调用 $mount()
方法挂载到对应的DOM元素上。
今天我们主要看一下初始化数据相关的部分。也就是 _init()
方法里面的 initState()
函数。
// 初始化数据
export function initState (vm: Component) {vm._watchers = []const opts = vm.$optionsif (opts.props) initProps(vm, opts.props) // 1. 初始化参数if (opts.methods) initMethods(vm, opts.methods) // 2、初始化方法if (opts.data) { // 3、初始化数据initData(vm)} else {observe(vm._data = {}, true /* asRootData */)}if (opts.computed) initComputed(vm, opts.computed) // 4、初始化计算属性if (opts.watch) initWatch(vm, opts.watch) // 5、初始化监听属性
}
这个 initState()
代码很短,要做的事情也很清楚,就是初始化了 props
参数,methods
方法,data
数据,computed
计算属性,watch
监听属性。这次我们主要关注 initData()
方法和 initComputed()
方法。了解Vue依赖收集与双向绑定的完整流程。
所以,首先我们先看一下 initData()
方法。
function initData (vm: Component) {let data = vm.$options.datadata = vm._data = typeof data === 'function'? getData(data, vm): data || {}...// proxy data on instanceconst keys = Object.keys(data)const props = vm.$options.propslet i = keys.lengthwhile (i--) {if (props && hasOwn(props, keys[i])) { // 判断props和data里的属性是否有重复process.env.NODE_ENV !== 'production' && warn(`The data property "${keys[i]}" is already declared as a prop. ` +`Use prop default value instead.`,vm)} else if (!isReserved(keys[i])) { // 判断data属性不以$或_开头proxy(vm, `_data`, keys[i]) // 将this.XXX代理到this._data.XXX}}// observe dataobserve(data, true /* asRootData */) //
}function getData (data: Function, vm: Component): any {try {return data.call(vm)} catch (e) {handleError(e, vm, `data()`)return {}}
}export function proxy (target: Object, sourceKey: string, key: string) {sharedPropertyDefinition.get = function proxyGetter () {return this[sourceKey][key]}sharedPropertyDefinition.set = function proxySetter (val) {this[sourceKey][key] = val}Object.defineProperty(target, key, sharedPropertyDefinition)
}
initData()
方法主要做了三件事:
第一件事:一般定义 data
属性都是一个函数,返回的对象才是具体的数据。所以首先判断 data
是不是函数,如果是就通过 getData()
方法运行 data
方法得到返回的数据,否则就返回 data || {}
。
并且将 data
函数返回的结果赋值给 vm._data
和自己。vm._data
就是保存Vue对象运行数据的属性。以后如果数据发生变化,修改得也是这里的数据。vm.$option.data
是参数里面的原始数据,不能修改。
第二件事:一般我们访问Vue对象里面的数据是使用 this.XXX
这种形式。而现在数据保存在 this._data
里面的,所以使用 proxy()
函数,将 this.XXX
代理到 this._data.XXX
上去。
不仅如此,在代理之后,还检查了 data
里面的属性是否与 props
里面的属性重名。如果重名,则认为些属性是参数,data
里面的重名属性将不会被代理。
第三件事,就是运行 observer()
方法,下面会讲解这个方法具体做了什么。
export function observe (value: any, asRootData: ?boolean): Observer | void {if (!isObject(value)) {return}let ob: Observer | voidif (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { // 是否已经存在__ob__观察者对象ob = value.__ob__} else if (observerState.shouldConvert && // ???!isServerRendering() && // 不是服务器渲染(Array.isArray(value) || isPlainObject(value)) && // data必须是数组或者简单对象Object.isExtensible(value) && // data对象必须是可扩展的(可以添加额外属性)!value._isVue // ) {ob = new Observer(value) // 初始化data.__ob__属性}if (asRootData && ob) {ob.vmCount++ // 根对象}return ob
}
这个方法的关键在 ob = new Observer(value)
这一行。为数据对象添加一个__ob__属性,而这个属性就是一个Observer对象的实例。下面主要是看一下Observer类的构造函数。
export class Observer {value: any;dep: Dep;vmCount: number; // number of vms that has this object as root $dataconstructor (value: any) {this.value = valuethis.dep = new Dep()this.vmCount = 0def(value, '__ob__', this) // 等同于value.__ob__ = thisif (Array.isArray(value)) {const augment = hasProto? protoAugment: copyAugmentaugment(value, arrayMethods, arrayKeys)this.observeArray(value)} else {this.walk(value)}}walk (obj: Object) {const keys = Object.keys(obj)for (let i = 0; i < keys.length; i++) {defineReactive(obj, keys[i], obj[keys[i]])}}
}
实例化Observer对象的时候,首先初始化了一些变量value(要观察的对象),dep和vmCount。然后将自己赋值给 value.__ob__
属性。最后,判断value的类型,如果是数组就走 observeArray()
方法,如果是其它,就走 walk()
方法。
我们先看一下其它类型,主要是对象走的 walk()
方法,很简单,就是遍历对象所有属性,然后每个属性调用 defineReactive()
方法。
export function defineReactive (obj: Object,key: string,val: any,customSetter?: Function
) {const dep = new Dep()const property = Object.getOwnPropertyDescriptor(obj, key)if (property && property.configurable === false) {return}// cater for pre-defined getter/settersconst getter = property && property.get // 暂存属性之前的get和set修饰器方法const setter = property && property.setlet 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...if (setter) {setter.call(obj, newVal)} else {val = newVal}childOb = observe(newVal) // 触发依赖的重新收集dep.notify() // 触发所有依赖监听器的更新}})
}
但是,这没有Watch对象的新建流程,所以,我们现在了解的依赖收集是不完整的。接下来,我们以计算属性的Watch对象新建为例子,让Observer,Dep和Watch对象之间的关系变得清楚明白。
initComputed初始化计算属性
// 初始化数据
export function initState (vm: Component) {vm._watchers = []const opts = vm.$optionsif (opts.props) initProps(vm, opts.props) // 1. 初始化参数if (opts.methods) initMethods(vm, opts.methods) // 2、初始化方法if (opts.data) { // 3、初始化数据initData(vm)} else {observe(vm._data = {}, true /* asRootData */)}
> if (opts.computed) initComputed(vm, opts.computed) // <-- 4、初始化计算属性if (opts.watch) initWatch(vm, opts.watch) // 5、初始化监听属性
}
还是再看 initState()
这个方法,我们可以看到初始化计算属性紧跟在初始化数据之后。
function initComputed (vm: Component, computed: Object) {const watchers = vm._computedWatchers = Object.create(null)for (const key in computed) {const userDef = computed[key]let getter = typeof userDef === 'function' ? userDef : userDef.get // 计算属性是函数或者拥有get/set属性的对象if (process.env.NODE_ENV !== 'production') {if (getter === undefined) {warn(`No getter function has been defined for computed property "${key}".`,vm)getter = noop}}// create internal watcher for the computed property.watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions) // 一个计算属性对应一个Watcher对象// component-defined computed properties are already defined on the// component prototype. We only need to define computed properties defined// at instantiation here.if (!(key in vm)) {defineComputed(vm, key, userDef)}}
}
而在 initComputed()
方法 ,首先创建一个存放所有计算属性Watch对象的 vm._computedWatchers
属性。然后在循环中将每个计算属性新建一个Watcher对象。
export default class Watcher {constructor (vm: Component,expOrFn: string | Function,cb: Function,options?: Object) {this.vm = vmvm._watchers.push(this)...// parse expression for getterif (typeof expOrFn === 'function') {this.getter = expOrFn} else {this.getter = parsePath(expOrFn)if (!this.getter) {this.getter = function () {}process.env.NODE_ENV !== 'production' && warn(`Failed watching path: "${expOrFn}" ` +'Watcher only accepts simple dot-delimited paths. ' +'For full control, use a function instead.',vm)}}this.value = this.lazy? undefined: this.get()}
}
在新建Watch对象的构造函数里面,首先把自己保存到了 vm._watchers
数组里面。这个数组保存了Vue实例所有的Watcher对象。然后将 this.getter = expOrFn
这个expOrFn函数就是 initComputed
方法里面的userDef或者userDef.get,就是用户自己定义计算属性时手写的方法,或者手写计算对象里面的get函数。最后对this.value进行赋值,这个this.value就是Watcher的返回值。也就是计算属性的返回值。
接下来我们就看看调用的this.get()方法如何得到计算属性的结果。
get () {pushTarget(this)let valueconst vm = this.vmif (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 watchingif (this.deep) {traverse(value)}popTarget()this.cleanupDeps()return value
}
// dep.js里面导出的方法
export function pushTarget (_target: Watcher) {if (Dep.target) targetStack.push(Dep.target)Dep.target = _target
}
在get方法里面首先调用了pushTarget方法,这个方法是在dep.js文件里面,主要作用就是把当前正在实例化的Watcher对象赋值到Dep.target这个全局变量里面。
之后进行了一个判断,但是无论是真还是假,都会运行语句 value = this.getter.call(vm, vm)
运行this.getter方法,就是直接运行程序员写的计算属性方法,得到方法的返回值。而在运行这个方法时就会触发data对象里面的Observer的get方法。从而触发Watcher对象的依赖收集。
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) {...}
}
在get方法里面会先判断当时Dep.target这个全局变量有没有值,很明显,在我们实例化Watcher对象的构造函数里面正好为Dep.target赋值了,所以最后运行 dep.depend()
方法。这个方法也是依赖收集的重点。
export default class Dep {static target: ?Watcher;id: number;subs: Array<Watcher>;...depend () {if (Dep.target) {Dep.target.addDep(this) // <-- 1、watcher与dep产生联系}}addSub (sub: Watcher) {this.subs.push(sub) // <-- 4、将watcher添加进subs数组}
}
export default class Watcher {...addDep (dep: Dep) {const id = dep.idif (!this.newDepIds.has(id)) {this.newDepIds.add(id)this.newDeps.push(dep) // <-- 2、将dep添加进newDeps数组if (!this.depIds.has(id)) {dep.addSub(this) // <-- 3、调用dep的添加方法}}}
}
在dep.depend()这个方法主要就是运行一件事,就是Dep.target.addDep(this),Dep.target就是当前正在实例化的Watcher对象,所以这个语句也就是watcher.addDep(this),这个this就是计算依赖data当前属性的里面的Dep对象。Watcher对象与Dep对象终于产生了联系。
而在addDep方法里面首先将dep对象添加到了watcher对象里面的newDeps数组里面,然后调用了dep.addSub(this)方法,将watcher对象添回到dep对象里面的subs数组。
这里我们可以发现,dep对象和watcher对象是双向引用的,分别有一个数组进行保存。这样dep在属性set方法调用时,可以通知自己影响的数组里面所有的watcher更新。而watcher也知道自己依赖哪些dep对象。
而Observer对象主要是为属性代理get和set方法提供的载体。dep对象和watcher对象才是属性与视图建立的双向联系。
可能的依赖关系:
属性<–依赖–视图
属性<–依赖–计算属性<–依赖–视图
属性<–依赖–计算属性<–依赖–计算属性<–依赖–视图
视图也是由Watcher对象建立与data属性的依赖关系的。在Vue实例化后面,我们可以看到。
defineComputed 定义计算属性
计算属性也有可能被其它属性或者视图依赖,所以计算属性也应该与data里面的属性一样,也被代理才对。所以让我们继续把计算属性的逻辑看完。
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {defineComputed(vm, key, userDef) // <-- 计算属性后面的逻辑
}
新建Watcher对象之后,计算属性通过watcher建立与data属性的依赖关系。我们看一下之后调用 defineComputed()
方法。
const sharedPropertyDefinition = {enumerable: true,configurable: true,get: noop,set: noop
}
export function defineComputed (target: any, key: string, userDef: Object | Function) {if (typeof userDef === 'function') {sharedPropertyDefinition.get = createComputedGetter(key)sharedPropertyDefinition.set = noop} else {sharedPropertyDefinition.get = userDef.get? userDef.cache !== false? createComputedGetter(key): userDef.get: noopsharedPropertyDefinition.set = userDef.set? userDef.set: noop}Object.defineProperty(target, key, sharedPropertyDefinition)
}
这个defineComputed方法主要功能就是在vm代理出一个计算属性让用户可以通过this.XXX访问到计算属性。其中sharedPropertyDefinition定义的计算属性的修饰器,其中定义属性可枚举,可配置,set和get。我们可以看到当计算属性为函数时get是通过createComputedGetter方法定义的,而set为空函数。不是函数时则通过userDef.get和userDef.set定义。
下面我们重点说说这个createComputedGetter方法。
function createComputedGetter (key) {return function computedGetter () { // 返回计算属性get代理方法const watcher = this._computedWatchers && this._computedWatchers[key]if (watcher) {if (watcher.dirty) { // ???watcher.evaluate()}if (Dep.target) {watcher.depend() // <-- 计算属性也可以触发watcher依赖收集}return watcher.value}}
}
export default class Watcher {depend () {let i = this.deps.lengthwhile (i--) {this.deps[i].depend() // <-- watcher调用自己的dep依赖数组进行其它watcher的依赖收集}}
}
可以看到createComputedGetter方法返回的是代理get方法,返回watcher.value的值。一个计算属性就有一个watcher对象。甚至可以说watcher才是计算属性的本体。因为计算,更新的逻辑都在watcher里面。计算属性本身只是watcher的壳而已。
在这个方法里面watcher对象与data里面的dep起到了相同的作用,都可以进行依赖收集。不过我们看到watcher.depend()并不直接收集,因为他也依赖其它属性,所以它直接调用自己deps数组进行收集 this.deps[i].depend()
,让自己依赖的属性直接去收集。
也就是说watcher对象之间是没有直接依赖关系的,依赖被转移到了dep这一层。但是要注意,获取值的时候,计算属性还是通过自己依赖的计算获取新的值,并不能直接通过dep得到值。
dep可以通知依赖自己的watcher,watcher并不一定直接去得到dep属性的新值,也可能通过其它watcher得到新值。这样说的话,在set方法调用时,触发更新的watcher的先后顺序就成了重中之重。
因为如果依赖其它计算属性的watcher先更新,那它得到的计算属性此时还没有更新,得到的value值是缓存之前的值。这样watcher实际上是没有更新的。
这个问题我们在依赖更新的时候再讨论吧。下面是简单的计算属性和data依赖图。
data属性(dep)<--依赖--计算属性(先更新)^ ^| |依赖<<<<<<转移<<<<<<<依赖(假)| |
计算属性(后更新) -------
Vue 数据属性依赖收集流程总结
vm._data = {firstName: {__ob__: Observer {dep: Dep {...subs: Watcher [] // 依赖此项属性的所有监听器数组}},// 代理set方法set firstName: function {...dep.notify() // 触发所有监听器的更新},// 代理get方法get firstName: function {...if(Dep.target) { // 创建Watcher监听器此项有值dep.depend() // 进行依赖(监听器)收集...}return value}}
}
数组直接赋值不能更新原因
data() {return {a: [{name: 'hello'}]}
}
// 方法1 只修改数组对象里面的属性
this.a[0].name = 'world' // 修改一开始就存在的属性
this.$set(this.a[0], "age", '15') // 添加不存在的属性// 方法2 修改数组整个对象
this.a[0] = {name: 'world'} // <-- 直接修改不生效
this.$set(this.a, 0, {name: 'world'}) // 修改数组对象
this.a.splice(0, 1, {name: 'world'}) // 另一种修改数组对象
this.a.push({name: 'world'}) // 添加数组对象
this.a.splice(0, 0, {name: 'world'}) // 另一种添加数组对象// 方法3
this.$forceUpdate() // 直接手动刷新
Vue 源码解析(一):依赖收集(Observer,Dep与Watcher对象)相关推荐
- Vue源码解析(一)
前言:接触vue已经有一段时间了,前面也写了几篇关于vue全家桶的内容,感兴趣的小伙伴可以去看看,刚接触的时候就想去膜拜一下源码~可每次鼓起勇气去看vue源码的时候,当看到几万行代码的时候就直接望而却 ...
- 【vuejs深入三】vue源码解析之二 htmlParse解析器的实现
写在前面 一个好的架构需要经过血与火的历练,一个好的工程师需要经过无数项目的摧残. 昨天博主分析了一下在vue中,最为基础核心的api,parse函数,它的作用是将vue的模板字符串转换成ast,从而 ...
- [Vue源码解析] patching算法
[Vue源码解析] patching算法 pathching算法:通过对比新旧VNode的不同,然后找出需要更新的节点进行更新 操作:1.创建新增节点 2.删除废弃节点 3.修改需要更新的节点 创建节 ...
- 游戏陪玩平台源码开发,依赖收集和触发的实现
概述 在游戏陪玩平台源码开发中,依赖收集和触发比较复杂,不容易理解.今天咱们将它最简化的讲出来. 前期准备 1.声明一个Map做储蓄: 2.current用来传递数据: 3.data数据备用: con ...
- Vue源码解析(尚硅谷)
视频地址:Vue源码解析系列课程 一.Vue源码解析之mustache模板引擎 1. 什么是模板引擎 模板引擎是将数据要变为视图最优雅的解决方案 历史上曾经出现的数据变为视图的方法 2. mustac ...
- Vue源码解析之数组变异
力有不逮的对象 众所周知,在 Vue 中,直接修改对象属性的值无法触发响应式.当你直接修改了对象属性的值,你会发现,只有数据改了,但是页面内容并没有改变. 这是什么原因? 原因在于: Vue 的响应式 ...
- Vue源码解析:虚拟dom比较原理
通过对 Vue2.0 源码阅读,想写一写自己的理解,能力有限故从尤大佬2016.4.11第一次提交开始读,准备陆续写: 模版字符串转AST语法树 AST语法树转render函数 Vue双向绑定原理 V ...
- Vue源码解析之Template转化为AST的实现方法
什么是AST 在Vue的mount过程中,template会被编译成AST语法树,AST是指抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree) ...
- Vue源码解析之函数入口
从入口开始看起 写博客就是记录自己撸码的过程和问题,好了~废话就不多说了,直接源码撸起,通过上一篇博客咱们大致知道了Vue源码目录设计,下面我们要一步步找到vue的入口 通过查看package.jso ...
- vue源码解析之observe
一. vue文档中有"由于 JavaScript 的限制,Vue 不能检测以下数组的变动",是否真是由于JavaScript的限制,还是出于其他原因考虑 当你利用索引直接设置一个数 ...
最新文章
- ContentType大全
- TCP连接中TIME_WAIT连接过多
- 事件冒泡和捕获的执行顺序
- db2表结构导出导入,数据库备份
- python图片二进制流转换成图片_python将图片二进制数据转换成Django file对象
- java编写斐波那契数列,实战案例
- (转)UML类图与类的关系详解
- 阶段1 语言基础+高级_1-3-Java语言高级_06-File类与IO流_02 递归_3_练习_使用递归计算阶乘...
- 黑莓9530完美刷机教程
- AxureRP9(team版)安装+汉化+秘钥
- 基于JavaMail的Java邮件发送:简单邮件发送
- Resource compilation failed. Check logs for details.
- XDOJ32角谷定理
- linux下删除oracle数据库实例
- handlebars使用
- MAC地址的介绍(单播、广播、组播、数据收发)
- 我见过最通俗易懂的快速排序过程讲解,转自《坐在马桶上看算法:快速排序》
- java小小工具 对象信息管理
- android tensorflow文字识别身份证识别ocr文字识别商用源码
- java 最长不重复子串,最长无重复字符子串
热门文章
- Cannot run program /home/xtt/Work/IDE/android-studio/sdk/build-tools/android-4.4.2/aapt: error=2
- Linux Ansys
- Robocup 仿真2D 学习笔记(一) ubuntu16.04 搭建 robocup 仿真2D环境
- http://hi.baidu.com/%BE%C5%CC%EC%C4%A7%CA%DE/blog/item/9b3263626a75ff49ebf8f808.html
- java 圆的类_java:设计实现圆形类、正方形类、长方形类
- Jmeter IP欺骗
- Math 数学方法、随机数公式、随机数公式推理
- 第六章-博弈论之Stackelberg博弈
- wilcoxon秩和检验--学习笔记
- 记一次windows下安装部署运维监控系统WGCOUD的步骤