文章目录

  • 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有关方法
    • entries Vue 不同类型源码入口
    • platforms
    • server
    • sfc
    • shared

这次主要我们用到的目录只有: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对象)相关推荐

  1. Vue源码解析(一)

    前言:接触vue已经有一段时间了,前面也写了几篇关于vue全家桶的内容,感兴趣的小伙伴可以去看看,刚接触的时候就想去膜拜一下源码~可每次鼓起勇气去看vue源码的时候,当看到几万行代码的时候就直接望而却 ...

  2. 【vuejs深入三】vue源码解析之二 htmlParse解析器的实现

    写在前面 一个好的架构需要经过血与火的历练,一个好的工程师需要经过无数项目的摧残. 昨天博主分析了一下在vue中,最为基础核心的api,parse函数,它的作用是将vue的模板字符串转换成ast,从而 ...

  3. [Vue源码解析] patching算法

    [Vue源码解析] patching算法 pathching算法:通过对比新旧VNode的不同,然后找出需要更新的节点进行更新 操作:1.创建新增节点 2.删除废弃节点 3.修改需要更新的节点 创建节 ...

  4. 游戏陪玩平台源码开发,依赖收集和触发的实现

    概述 在游戏陪玩平台源码开发中,依赖收集和触发比较复杂,不容易理解.今天咱们将它最简化的讲出来. 前期准备 1.声明一个Map做储蓄: 2.current用来传递数据: 3.data数据备用: con ...

  5. Vue源码解析(尚硅谷)

    视频地址:Vue源码解析系列课程 一.Vue源码解析之mustache模板引擎 1. 什么是模板引擎 模板引擎是将数据要变为视图最优雅的解决方案 历史上曾经出现的数据变为视图的方法 2. mustac ...

  6. Vue源码解析之数组变异

    力有不逮的对象 众所周知,在 Vue 中,直接修改对象属性的值无法触发响应式.当你直接修改了对象属性的值,你会发现,只有数据改了,但是页面内容并没有改变. 这是什么原因? 原因在于: Vue 的响应式 ...

  7. Vue源码解析:虚拟dom比较原理

    通过对 Vue2.0 源码阅读,想写一写自己的理解,能力有限故从尤大佬2016.4.11第一次提交开始读,准备陆续写: 模版字符串转AST语法树 AST语法树转render函数 Vue双向绑定原理 V ...

  8. Vue源码解析之Template转化为AST的实现方法

    什么是AST 在Vue的mount过程中,template会被编译成AST语法树,AST是指抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree) ...

  9. Vue源码解析之函数入口

    从入口开始看起 写博客就是记录自己撸码的过程和问题,好了~废话就不多说了,直接源码撸起,通过上一篇博客咱们大致知道了Vue源码目录设计,下面我们要一步步找到vue的入口 通过查看package.jso ...

  10. vue源码解析之observe

    一. vue文档中有"由于 JavaScript 的限制,Vue 不能检测以下数组的变动",是否真是由于JavaScript的限制,还是出于其他原因考虑 当你利用索引直接设置一个数 ...

最新文章

  1. ContentType大全
  2. TCP连接中TIME_WAIT连接过多
  3. 事件冒泡和捕获的执行顺序
  4. db2表结构导出导入,数据库备份
  5. python图片二进制流转换成图片_python将图片二进制数据转换成Django file对象
  6. java编写斐波那契数列,实战案例
  7. (转)UML类图与类的关系详解
  8. 阶段1 语言基础+高级_1-3-Java语言高级_06-File类与IO流_02 递归_3_练习_使用递归计算阶乘...
  9. 黑莓9530完美刷机教程
  10. AxureRP9(team版)安装+汉化+秘钥
  11. 基于JavaMail的Java邮件发送:简单邮件发送
  12. Resource compilation failed. Check logs for details.
  13. XDOJ32角谷定理
  14. linux下删除oracle数据库实例
  15. handlebars使用
  16. MAC地址的介绍(单播、广播、组播、数据收发)
  17. 我见过最通俗易懂的快速排序过程讲解,转自《坐在马桶上看算法:快速排序》
  18. java小小工具 对象信息管理
  19. android tensorflow文字识别身份证识别ocr文字识别商用源码
  20. java 最长不重复子串,最长无重复字符子串

热门文章

  1. Cannot run program /home/xtt/Work/IDE/android-studio/sdk/build-tools/android-4.4.2/aapt: error=2
  2. Linux Ansys
  3. Robocup 仿真2D 学习笔记(一) ubuntu16.04 搭建 robocup 仿真2D环境
  4. http://hi.baidu.com/%BE%C5%CC%EC%C4%A7%CA%DE/blog/item/9b3263626a75ff49ebf8f808.html
  5. java 圆的类_java:设计实现圆形类、正方形类、长方形类
  6. Jmeter IP欺骗
  7. Math 数学方法、随机数公式、随机数公式推理
  8. 第六章-博弈论之Stackelberg博弈
  9. wilcoxon秩和检验--学习笔记
  10. 记一次windows下安装部署运维监控系统WGCOUD的步骤