热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Vue源码–解读vue响应式原理

Vue源码--解读vue响应式原理原文链接:https:geniuspeng.github.io20180105vue-reactivityVue的官方说明里有深入响应式原理这一节

原文链接:https://geniuspeng.github.io/2018/01/05/vue-reactivity/

Vue的官方说明里有深入响应式原理这一节。在此官方也提到过:

当你把一个普通的 Javascript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器的原因。

官网只是说了一个大概原理,浏览了一下vue的源码以及其他关于此处的解释,这一块其实可以总结为两点:利用Object.defineProperty进行数据劫持同时结合观察者模式(发布/订阅模式)来实现数据双向绑定,这也是vue响应式原理的核心。首先先把这两个东西简单介绍一下吧~

关于Object.defineProperty

关于Object.defineProperty我觉得还是MDN上说的更明白详细一些,我写在这里无非是一些ctrl C+V,总之我们要知道它是一个定义属性描述符的东西,可以劫持一个对象属性的getter和setter,而Vue正是通过在属性改变时劫持这些来通知watcher(后文会讲)。下面这个例子实现了一个极简的双向绑定:

它的效果就是,input框输出改变的时候,span的内容随之改变,同时如果在控制台直接改变obj.hello属性,input和span的内容同时会一起变化(验证这个得去codepen页面中的console控制台),其原理就是利用Object.defineProperty劫持了属性的set方法。

关于观察者模式

观察者模式(有时又被称为发布(publish )-订阅(Subscribe)模式)是软件设计模式的一种。在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实现事件处理系统。
观察者模式根据具体不同的场景可以实现出不同的版本,但其核心思想没有变化,下面是根据vue源码简化出来的一个简单模型








1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

function Dep() {

//主题对象

this.subs = []; //订阅者列表
}
Dep.prototype.notify = function() { //主题对象通知订阅者
this.subs.forEach(function(sub){ //遍历所有的订阅者,执行订阅者提供的更新方法
sub.update();
});
}
function Sub(x) { //订阅者
this.x = x;
}、
Sub.prototype.update = function() { //订阅者更新
this.x = this.x * this.x;
console.log(this.x);
}
var pub = { //发布者
publish: function() {


dep.notify();
}
};
var dep = new Dep(); //主题对象实例
Array.prototype.push.call(dep.subs, new Sub(1), new Sub(2), new Sub(3)); //新增 3 个订阅者
pub.publish(); //发布者发布更新
// 1
// 4
// 9

首先有3个主体:发布者,主题对象,订阅者,其中发布者和主题对象是一对一的(某些其他场景可能一对多?),而主题对象和订阅者也是一对多的关系。发布者通过发布某个主题对象,主题对象更新并通知其所有的订阅者,然后每个订阅者执行update进行更新。
对于Vue,我们这里可以认为Vue实例中的data中的每一项属性是一个Dep,而所有用到这个属性的地方都是这个Dep的一个订阅者(sub),当这个属性值变化时,观察者通过监听捕获变化,告诉这个dep去notify每一个订阅者。

Observer ,Dep和Watcher

Observer、Watcher、Dep 是响应式原理中涉及到的 3 个重要的对象,可以说分别上面的发布者,主体对象,订阅者相对应。其关系可以简化为下图所示:
https://img7.php1.cn/3cdc5/c7bc/4aa/9473f40dbdb3a971.png

Observer对象

这个东西,字面意思是一个观察者,我个人理解就是上面所说的Object.defineProperty + 发布者的结合体,它的主要功能是做数据劫持,在数据获得更新的时候(拦截set方法),执行主题对象(Dep)的notify方法,通知所有的订阅者(Watcher)。
Observer类定义在src/core/observer/index.js中,先来看一下Observer的构造函数:








1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

constructor (value: any) {


this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {


const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {


this.walk(value)
}
}

value是需要被观察的数据对象,在构造函数中,会给value增加ob属性,作为数据已经被Observer观察的标志。如果value是数组,就使用observeArray遍历value,对value中每一个元素调用observe分别进行观察。如果value是对象,则使用walk遍历value上每个key,对每个key调用defineReactive来获得该key的set/get控制权。
在src/core/observer/index.js中还有几个方法,大致解释如下


  • observeArray: 遍历数组,对数组的每个元素调用observe

  • observe: 检查对象上是否有ob属性,如果存在,则表明该对象已经处于Observer的观察中,如果不存在,则new Observer来观察对象(其实还有一些判断逻辑,为了便于理解就不赘述了)

  • walk: 遍历对象的每个key,对对象上每个key的数据调用defineReactive

  • defineReactive: 通过Object.defineProperty设置对象的key属性,使得能够捕获到该属性值的set/get动作。一般是由Watcher的实例对象进行get操作,此时Watcher的实例对象将被自动添加到Dep实例的依赖数组中,在外部操作触发了set时,将通过Dep实例的notify来通知所有依赖的watcher进行更新。(后面详细介绍)

defineReactive方法如下,这是observer类的一个核心方法:








1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {


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 = !shallow && 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 = !shallow && observe(newVal)
dep.notify()
}
})
}

defineReactive 是对 Object.defineProperty 方法的包装,结合 observe 方法对数据项进行深入遍历,最终将所有的属性就转化为 getter 和 setter。至此,所有的数据都已经转换为 Observer 对象。即数据的读操作都会触发 getter 函数,写操作都会触发 setter 函数。
那走到这里,会有很多问题,比如Dep在哪?如何往Dep中添加订阅者?

Dep对象

Dep类定义在src/core/observer/dep.js中。
Dep是Observer与Watcher之间的纽带,也可以认为Dep是服务于Observer的订阅系统。Watcher订阅某个Observer的Dep,当Observer观察的数据发生变化时,通过Dep通知各个已经订阅的Watcher。其构造函数和主要方法如下。其中sub就是订阅者Watcher,在后文介绍:








1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

class Dep {


static target: ?Watcher;
id: number;
subs: Array;

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 () {


// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i

subs[i].update()
}
}
}
Dep.target = null

Dep提供了如下几个方法:


  • addSub: 接收的参数为Watcher实例,并把Watcher实例存入记录依赖的数组中

  • removeSub: 与addSub对应,作用是将Watcher实例从记录依赖的数组中移除

  • depend: Dep.target上存放这当前需要操作的Watcher实例,调用depend会调用该Watcher实例的addDep方法,addDep的功能可以看下面对Watcher的介绍

  • notify: 通知依赖数组中所有的watcher进行更新操作


Watcher对象

Watcher类定义在src/core/observer/watcher.js。其构造函数如下:








1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {


this.vm = vm
if (isRenderWatcher) {


vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {


this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {


this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (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()
}

watcher实例有这些方法:


  • get: 将Dep.target设置为当前watcher实例,在内部调用this.getter,如果此时某个被Observer观察的数据对象被取值了,那么当前watcher实例将会自动订阅数据对象的Dep实例

  • addDep: 接收参数dep(Dep实例),让当前watcher订阅dep

  • cleanupDeps: 清除newDepIds和newDep上记录的对dep的订阅信息

  • update: 立刻运行watcher或者将watcher加入队列中等待统一flush

  • run: 运行watcher,调用this.get()求值,然后触发回调

  • evaluate: 调用this.get()求值

  • depend: 遍历this.deps,让当前watcher实例订阅所有dep

  • teardown: 去除当前watcher实例所有的订阅

Observer,Dep,Watcher所有的代码都过一遍之后,再来回头看看究竟是如何添加订阅者的。说白了就是Dep中的this.subs = []方法如何添加相应的订阅者watcher的。在这里还应该列出两个重要的方法,watcher的get()方法:








1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {


pushTarget(this)
let value
const vm = this.vm
try {


value = this.getter.call(vm, vm)
} catch (e) {


if (this.user) {


handleError(e, vm, `getter for watcher "${
this.expression}
"`
)

} else {


throw e
}
} finally {


// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {


traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}

/**
* Add a dependency to this directive.
*/
addDep (dep: Dep) {


const id = dep.id
if (!this.newDepIds.has(id)) {


this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {


dep.addSub(this)
}
}
}

我们在Dep中可以看到Dep在一开始定义了一个全局属性Dep.target,在新建watcher是,这个属性为null,而在watcher的构造函数中最后会执行自己的get()方法,进而执行pushTarget(this)方法,可以看到get()方法中做了一件事情:value = this.getter.call(vm, vm),然后popTarget()。Dep.target只是一个标记,存储当前的watcher实例,而执行这句话的意义在于触发Object.defineProperty中的get拦截,而在bject.defineProperty中的get那里,我们可以看到dep.depend(),正是在这里将当前的订阅者watcher绑定当Dep上。
也就是说,每个watcher第一次实例化的时候,都会作为订阅者订阅其相应的Dep。
而关于watcher何时进行实例化的问题,模板渲染初始化相关指令调用data中的属性,或者正常进行数据绑定时都会创建watcher实例。
PS: 在模板渲染过程中,vue1.x与vue2.x完全不同,1.x是利用documentFragment来实现,而2.x向react靠拢,加入了virtual dom,同时通过自己生产的render方法进行渲染,不过无论是那种方法,都会在初始时对所关联的data属性进行watcher实例化并且绑定watcher变化时的更新callback,确保data更新时会重新对相应的地方进行视图更新。

总结一下:


  1. 模板编译过程中的指令和数据绑定都会生成 Watcher 实例,watch 函数中的对象也会生成 Watcher 实例,在实例化的过程中,会调用 watcher.js 中的 get 函数 touch 这个 Watcher 的表达式或函数涉及的所有属性;

  2. touch 开始之前,Watcher 会设置 Dep 的静态属性 Dep.target 指向其自身,目的在于标记此watcher实例是第一次创建,需要添加到一个Dep中;

  3. touch 属性的过程中,属性的 getter 函数会被访问;

  4. 属性 getter 函数中会判断 Dep.target(target 中保存的是第 2 步中设置的 Watcher 实例)是否存在,若存在则将 getter 函数所在的 Observer 实例的 Dep 实例保存到 Watcher 的列表中,并在此 Dep 实例中添加 Watcher 为订阅者;

  5. 重复上述过程直至 Watcher 的表达式或函数涉及的所有属性均 touch 结束(即表达式或函数中所有的数据的 getter 函数都已被触发),Dep.target 被置为 null,此时已经将该watcher作为订阅者绑定到Dep中;


文末福利:

福利一:前端,Java,产品经理,微信小程序,Python等10G资源合集大放送:jianshu.com/p/e8197d4d9

福利二:微信小程序入门与实战全套详细视频教程。

【领取方法】

关注 【编程微刊】微信公众号:

回复【小程序demo】一键领取130个微信小程序源码demo资源。

回复【领取资源】一键领取前端,Java,产品经理,微信小程序,Python等资源合集10G资源大放送。


推荐阅读
  • C++ 异步编程中获取线程执行结果的方法与技巧及其在前端开发中的应用探讨
    本文探讨了C++异步编程中获取线程执行结果的方法与技巧,并深入分析了这些技术在前端开发中的应用。通过对比不同的异步编程模型,本文详细介绍了如何高效地处理多线程任务,确保程序的稳定性和性能。同时,文章还结合实际案例,展示了这些方法在前端异步编程中的具体实现和优化策略。 ... [详细]
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • 本文将继续探讨 JavaScript 函数式编程的高级技巧及其实际应用。通过一个具体的寻路算法示例,我们将深入分析如何利用函数式编程的思想解决复杂问题。示例中,节点之间的连线代表路径,连线上的数字表示两点间的距离。我们将详细讲解如何通过递归和高阶函数等技术实现高效的寻路算法。 ... [详细]
  • C++ 开发实战:实用技巧与经验分享
    C++ 开发实战:实用技巧与经验分享 ... [详细]
  • 在处理木偶评估函数时,我发现可以顺利传递本机对象(如字符串、列表和数字),但每当尝试将JSHandle或ElementHandle作为参数传递时,函数会拒绝接受这些对象。这可能是由于这些句柄对象的特殊性质导致的,建议在使用时进行适当的转换或封装,以确保函数能够正确处理。 ... [详细]
  • 重要知识点有:函数参数默许值、盈余参数、扩大运算符、new.target属性、块级函数、箭头函数以及尾挪用优化《深切明白ES6》笔记目次函数的默许参数在ES5中,我们给函数传参数, ... [详细]
  • 深入解析 Lifecycle 的实现原理
    本文将详细介绍 Android Jetpack 中 Lifecycle 组件的实现原理,帮助开发者更好地理解和使用 Lifecycle,避免常见的内存泄漏问题。 ... [详细]
  • 解决Bootstrap DataTable Ajax请求重复问题
    在最近的一个项目中,我们使用了JQuery DataTable进行数据展示,虽然使用起来非常方便,但在测试过程中发现了一个问题:当查询条件改变时,有时查询结果的数据不正确。通过FireBug调试发现,点击搜索按钮时,会发送两次Ajax请求,一次是原条件的请求,一次是新条件的请求。 ... [详细]
  • 第二十五天接口、多态
    1.java是面向对象的语言。设计模式:接口接口类是从java里衍生出来的,不是python原生支持的主要用于继承里多继承抽象类是python原生支持的主要用于继承里的单继承但是接 ... [详细]
  • 在处理大规模数据数组时,优化分页组件对于提高页面加载速度和用户体验至关重要。本文探讨了如何通过高效的分页策略,减少数据渲染的负担,提升应用性能。具体方法包括懒加载、虚拟滚动和数据预取等技术,这些技术能够显著降低内存占用和提升响应速度。通过实际案例分析,展示了这些优化措施的有效性和可行性。 ... [详细]
  • 全面解析JavaScript代码注释技巧与标准规范
    在Web前端开发中,JavaScript代码的可读性和维护性至关重要。本文将详细介绍如何有效地使用注释来提高代码的可读性,并探讨JavaScript代码注释的最佳实践和标准规范。通过合理的注释,开发者可以更好地理解和维护复杂的代码逻辑,提升团队协作效率。 ... [详细]
  • V8不仅是一款著名的八缸发动机,广泛应用于道奇Charger、宾利Continental GT和BossHoss摩托车中。自2008年以来,作为Chromium项目的一部分,V8 JavaScript引擎在性能优化和技术创新方面取得了显著进展。该引擎通过先进的编译技术和高效的垃圾回收机制,显著提升了JavaScript的执行效率,为现代Web应用提供了强大的支持。持续的优化和创新使得V8在处理复杂计算和大规模数据时表现更加出色,成为众多开发者和企业的首选。 ... [详细]
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • 在 Vue 应用开发中,页面状态管理和跨页面数据传递是常见需求。本文将详细介绍 Vue Router 提供的两种有效方式,帮助开发者高效地实现页面间的数据交互与状态同步,同时分享一些最佳实践和注意事项。 ... [详细]
  • Codeforces 605C:Freelancer's Dreams —— 凸包算法解析与题解分析 ... [详细]
author-avatar
HuPangpang_
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有