Vue响应式原理解读
Vue响应式原理解读
什么是响应式?
响应式就是当数据发生改变时、视图也会跟着更新
Vue响应式原理是数据劫持
和发布订阅模式
- Vue2.0时使用的ES5中的
Object.defineProperty
设置对象属性的set/get
方法来监听数据的变化 - Vue3.0的时使用的ES6中的
Proxy
实现数据劫持 - 观察者模式
下面我们就通过最直观的方法来了解Vue响应式的原理吧
Object.defineProperty
基本语法
1 | Object.defineProperty(obj, prop, descriptor) |
参数
obj
要定义属性的对象。prop
要定义或修改的属性的名称或Symbol
。descriptor
要定义或修改的属性描述符。
descriptor
属性修饰符:
修饰符 | 说明 |
---|---|
configurable | 为 true 时,该属性也能从对应的对象上被删除(delete)。 默认为 false 。 |
enumerable | 为true 可以被遍历。默认为 false |
数据描述符还具有以下可选键值: | |
value | 该属性对应的值。默认为 undefined 。 |
writable | 为 true 时,属性值value 才能被赋值运算符 改变。 默认为 false 。存在改属性是不能有get、set |
存取描述符还具有以下可选键值: | |
get | 属性的 getter 函数,如果没有 getter,则为 undefined 。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this 并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为 undefined 。 |
set | 属性的 setter 函数,如果没有 setter,则为 undefined 。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。 默认为 undefined 。 |
简单的例子:
1 | var obj = {}; |
最终打印:
1 | 触发set方法 |
在Vue2.0中我们使用Object.defineProperty
给data中的属性加上get、set
方法实现数据劫持
观察者模式
观察者模式
又称发布订阅模式(只是观察者模式的一个别称。)
,它定义了对象间的一种一对多
的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。
优点:更加解耦,支持广播通信
缺点:大量观察者,广播有性能问题
这里就简单理解一下 参考: https://blog.csdn.net/qq_25800235/article/details/86659422
1 | class Observer { |
代码实现
我将从以下几个方面来实现:
初始化Vue
编译模板:将指令转换为数据
数据劫持:通过
Object.defineProperty
对每个数据添加get/set方法数据代理:可以直接通过
vue
实例访问data
中的数据、例:vue.people.name
与vue.$data.people.name
发布订阅模式: 通知观察者更新数据
初始化Vue
html
1 | <div id="app"> |
首先我们需要有一个vue实例
1 | const vue = new Vue({ |
那我就创建一个Vue
类、模仿Vue默认传入options
对象、并且对el
和data
1 | class Vue { |
编译模板
我们需要在el
存在时对模板进行编译、这时我们准备一个Compile
进行编译模板、构造函数中需要传入el
和当前对象(this)
首先是对传入的
el
(可能传入:#app
或者document.querySelector("#app")
)进行判断、封装一个函数来判断是否为元素节点(nodeType = 1)
1
2
3
4
5// 判断是否为元素节点
isElementNode(node) {
// nodeType 为 1 是元素节点
return node.nodeType === 1
}因为我们需要对所有节点进行分析、频繁操作会影响性能、可以创建一个
文档碎片 document.createDocumentFragment()
、遍历每个节点并且添加appendChild
到文档碎片中、每次获取第一个节点firstChild
并去除、可以依次放入文档碎片中、对每个元素替换完之后再放回appendChild
Dom中1
2
3
4
5
6
7
8
9
10
11
12//将el中的元素放入文档碎片(内存)中
node2fragment(node) {
// 创建文档碎片
let fragment = document.createDocumentFragment();
let firstChild;
// 通过每次获取第一个元素来将所有元素添加到文档碎片中
while (firstChild = node.firstChild) {
// appendChild 具有移动性 可以将页面中的元素移动到文档碎片中
fragment.appendChild(firstChild)
}
return fragment
}把内容放入文档碎片后、对文档中的数据进行编译:双大括号里的内容转换成真正的数据、
compile方法
编译、获取Fragment
中的中的所有子节点childNodes
(返回一个伪数组)、通过扩展运算符(…) 转换成为数组再进行遍历得到每一项(child) 进行判断是否为元素节点(isElementNode
)或者是文本节点- 如果是
元素节点
:我们首先遍历的是第一层元素(并且编译元素节点compileElementNode
) - 如果还有子元素、我们需要再进行递归
子元素
、如果是文本节点
: 调用compileText
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//编译模板
compile(node) {
// 获取子节点 返回的是一个伪数组
let childNodes = node.childNodes; // NodeList(7) [text, input, text, div, text, ul, text]
// 通过...转成 数组
[...childNodes].forEach((child) => {
//判断是为元素节点 如 <input type="text" v-model="people.name">
if (this.isElementNode(child)) {
// 编译元素节点
this.compileElementNode(child);
// 如果是元素 需要判断自己的子节点 (递归)
this.compile(child)
} else {
// 编译文本节点
this.compileText(child);
}
})
}- 如果是
再来实现一下这两个方法
compileElementNode
- 到这里了我们可以确定获取到
<input type="text" v-model="people.name">
、开始获取每个属性值(attributes)
- 同样进行
遍历
得到每个属性(是以对象形式的、name为key、value是value)、进行解构赋值(把value
重新命名为expr
) - 判断是否以
v-
开头的(isDirective
)、获取到v-model
在进行(-
)分割得到对应的指令directive:事件
、当然如果是v-on:click
需要再进行分隔得到directiveName
和事件名eventName
、需要一个工具类CompileUtil
将表达式expr(people.name)
替换成对应的数据、expr
去除两边空格
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 编译元素
compileElementNode(node) {
// 返回的是一个伪数组
let attributes = node.attributes;
[...attributes].forEach((attr) => {
// 通过解构赋值获取 name(type) 和 value(text)
let { name, value: expr } = attr;
if (this.isDirective(name)) {
//获取指令、 如 v-model 获取 model v-on:click
let [, directive] = name.split("-");
let [directiveName, eventName] = directive.split(":");
CompileUtil[directiveName](node, expr.trim(), this.vm, eventName)
}
})
}
//判断是否为指令
isDirective(attrName) {
//ES6 语法 判断是否以 v- 开头
return attrName.startsWith("v-")
}compileText
文本节点
通过textContent
获取文本内容、得到的内容我们需要判断是否含有 {{ people.name }} 、通过正则表达式/\{\{(.+?)\}\}/
判断、再通过工具类CompileUtil
将表达式expr(people.name)
替换成对应的数据1
2
3
4
5
6
7
8
9// 编译文本
compileText(node) {
let content = node.textContent; // 获取文本内容
//匹配 {{ xxx }} .+? 匹配(任意字符重复一次或更多次)再重复0次或一次避免匹配 {{ aaa }} {{ bbb }}
let reg = /\{\{(.+?)\}\}/;
if (reg.test(content)) {
CompileUtil['text'](node, content, this.vm)
}
}- 到这里了我们可以确定获取到
工具类
CompileUtil
通过不同指令执行不同的方法:
首先对
CompileUtil[directiveName](node, expr.trim(), this.vm, eventName)
进行分析:为了确保能够正确的设置值和获取值我们需要准备
getVal(vm, expr)
和setVal(vm, expr, value)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18getVal(vm, expr) { // 我们以 people.name 进行分析
return expr.split(".").reduce((data, curnett) => {
/* data 前一项数据 curnett 当前数据
第一次 从 vm.$data[people] 返回 people: { name: 18 }
第二次 people[name] 得到 18
*/
return data[curnett];
}, vm.$data/*这里为初始的值*/)
},
setVal(vm, expr, value) { // 将 people.name 设置为新值
expr.split(".").reduce((data, curnett, index, arr) => {
if (index == arr.length - 1) {
// 只有当前索引等于数组的最后一项的索引 将修改数据 people[name] = value
return data[curnett] = value
}
return data[curnett]
}, vm.$data)
},以及修改值的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//工具类替换模板内容
CompileUtil = {
//...
//更新数据
updater: {
// v-mdoel 数据修改
modelUpdater(node, value) {
node.value = value;
},
// 文本数据修改
textUpdater(node, value) {
node.textContent = value
},
// v-html 数据修改
htmlUpdater(node,value){
node.innerHTML = value
}
}
}v-model
指令model方法
1
2
3
4
5
6
7
8
9
10
11
12// v-model 指令 node为元素节点、expr为表达式(people.name)、vm当前实例
model(node, expr, vm) {
let fn = this.updater['modelUpdater']
// 给input 赋予 value 属性 node.value = xxx
let val = this.getVal(vm, expr)
//监听事件
node.addEventListener('input', (event) => {
//值修改时修改数据
this.setVal(vm, expr, event.target.value)
})
fn(node, val)
},{{ }} 模板
text
方法1
2
3
4
5
6
7
8
9text(node, expr, vm) {
let fn = this.updater['textUpdater']
// 通过 replace 将 {{ people.name }} 替换为对应的值
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
// args[1]: args 是一个数组、第二个参数是匹配到的内容
return this.getVal(vm, args[1].trim())
})
fn(node, content)
},v-on:click
指令on方法
1
2
3
4
5
6on(node, expr, vm, eventName) {
node.addEventListener(eventName, (event) => {
// 相当于 vue[change]()
vm[expr].call(vm, event)
})
},v-html
指令html
方法1
2
3
4
5
6html(node,expr,vm){
let fn = this.updater['htmlUpdater']
// 给input 赋予 value 属性 node.value = xxx
let val = this.getVal(vm, expr)
fn(node, val)
},完整代码
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
56
57
58//工具类替换模板内容
CompileUtil = {
getVal(vm, expr) {
return expr.split(".").reduce((data, curnett) => {
return data[curnett];
}, vm.$data)
},
setVal(vm, expr, value) {
expr.split(".").reduce((data, curnett, index, arr) => {
if (index == arr.length - 1) {
return data[curnett] = value
}
return data[curnett]
}, vm.$data)
},
// v-model 指令 node为元素节点、expr为表达式(people.name)、vm当前实例
model(node, expr, vm) {
let fn = this.updater['modelUpdater']
// 给input 赋予 value 属性 node.value = xxx
let val = this.getVal(vm, expr)
node.addEventListener('input', (event) => {
this.setVal(vm, expr, event.target.value)
})
fn(node, val)
},
on(node, expr, vm, eventName) {
node.addEventListener(eventName, (event) => {
vm[expr].call(vm, event)
})
},
html(node,expr,vm){
let fn = this.updater['htmlUpdater']
// 给input 赋予 value 属性 node.value = xxx
let val = this.getVal(vm, expr)
fn(node, val)
},
text(node, expr, vm) {
let fn = this.updater['textUpdater']
// 通过 replace 将 {{ people.name }} 替换为对应的值
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
// args[1]: args 是一个数组、第二个参数是匹配到的内容
return this.getVal(vm, args[1].trim())
})
fn(node, content)
},
//更新数据
updater: {
modelUpdater(node, value) {
node.value = value;
},
textUpdater(node, value) {
node.textContent = value
},
htmlUpdater(node,value){
node.innerHTML = value
}
}
}
数据劫持
在我们判断数据是否存在时、首先需要给所有数据添加set/get
、通过Observer类
observer(data)
方法 首先判断data是否存在并且是否为对象类型、是就进行for in
遍历、Recative
化- 通过
Object.defineProperty
添加get和set方法
1 | // 改类实现数据劫持、添加set/get方法 |
数据代理
使用过vue我们可以知道、上面的示例中
、可以通过vue.people.name
、访问到数据、这样就进行了代理
1 | //代理 使得 通过 vue.people.name 访问和修改数据 |
发布订阅模式
Watcher
类似vue中的watch
、可以监听数据的变化
1 | watch:{ |
此时需要Watcher
来监听数据的变化来做出相应的改变
1 | // (观察者) 发布订阅模式 |
Dep 用于存放 Watcher 来通知 数据更新
1 | // 用于通知观察者更新数据 |
问题何时 创建 Dep 和 Watcher
创建Dep
需要在数据劫持时
1 | defineRecative(key, data, value) { |
创建Watcher
1 | // v-model 指令 node为元素节点、expr为表达式(people.name)、vm当前实例 |
最终效果:
每个Dep中存放对应的值依赖
当数据改变时、会调用dep.notify
通知 Dep 对象中的所有Wachter数据更新、这时Watcher的第三个参数: 回调函数就会修改数据了
最终代码
1 | class Vue { |
源码分析
代码: https://github.com/vuejs/vue/tree/v2.6.11
我们从 new Vue
的时候开始分析(执行 new Vue 时会依次执行以下方法):
Vue.prototype._init(option)
initState(vm)
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
56
57
58
59
60
61// 代码位置 https://github.com/vuejs/vue/blob/v2.6.11/src/core/instance/state.js
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
// 有数据就会执行 initData方法
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
// 1. data属性代理
proxy(vm, `_data`, key)
}
}
// observe data
// 2.对data调用observe
observe(data, true /* asRootData */)
}1.中
通过 while 循环内调用proxy
函数把data的属性代理到vue实例上。之后可以通过vue.key
访问到data.key
2.中
之后对data调用observe
方法、数据将会变成响应式
observe(vm._data)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// https://github.com/vuejs/vue/blob/v2.6.11/src/core/observer/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
// 3.检测当前的数据是否是对象或者数组,如果是,则生成对应的Observer
shouldObserve &&
!isServernetdering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 3.
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}3.中
对传入的数据对象进行了判断、只对对象和数组类型
生成Observernew Observer(data)
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117// 代码位置: https://github.com/vuejs/vue/blob/v2.6.11/src/core/observer/observer.js
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
// 生成了一个消息订阅器dep实例 关于dep的结构稍后详细介绍
this.dep = new Dep()
this.vmCount = 0
// def函数给当前数据添加不可枚举的__ob__属性,表示该数据已经被observe过
def(value, '__ob__', this)
// 4.对数组类型的数据 调用observeArray方法;对对象类型的数据,调用walk方法
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
/* observe数组类型数据的每个值, */
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
/* defineReactive的核心思想改写数据的getter和setter */
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 生成一个dep实例,注意此处的dep和前文Observer类里直接添加的dep的区别
const dep = new Dep()
// 检验该属性是否允许重新定义setter和getter
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
// 获取原有的 getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 此处对val进行了observe
let childOb = !shallow && observe(val)
// 下面的代码利用Object.defineProperty函数把数据转化成getter和setter,并且在getter和setter时,进行了一些操作
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
// dep.depend()其实就是dep和watcher进行了互相绑定,而Dep.target表示需要绑定的那个watcher,任何时刻都最多只有一个,后面还会解释
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()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 观察新的val并通知订阅者们属性有更新
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}在Observer类代码中,首先给当前数据添加了一个dep实例,存放于对象或者数组类型数据的,然后把
_ob_
挂在该数据上它是该数据项被observe的标志、可以看的每个data上都有
_ob_
(observe只对对象和数组有效)随后,对于数组和对象类型的数据做不同处理:
- 对于数组类型的数: 调用
observeArray
方法 - 对于对象,我们执行
walk()
方法,而就是对于当前数据对象的每个key,执行defineReactive()
方法、这个方法就是给data添加get/set方法
- 对于数组类型的数: 调用
接下来看一下dep
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// 代码位置: https://github.com/vuejs/vue/blob/v2.6.11/src/core/observer/observer.js
let uid = 0
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
// 添加一个watcher
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 移除一个watcher
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 让当前watcher收集依赖 同时Dep.target.addDep也会触发当前dep收集watcher
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知watcher们对应的数据有更新
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}这个类有两个属性:
- 第一个是id,在每个vue实例中都从0开始计数
- 另一个是
subs
数组,用于存放wacther
前面我们知道: 一个数据对应一个Dep,所以
subs
里存放的也就是依赖该数据需要绑定的wactherDep.target
属性是全局共享的,表示当前在收集依赖的那个Watcher,在每个时刻最多只会有一个接下来看一下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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219// 代码位置: https://github.com/vuejs/vue/blob/v2.6.11/src/core/observer/watcher.js
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isnetderWatcher?: boolean
) {
this.vm = vm
if (isnetderWatcher) {
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
this.before = options.before
} 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 = noop
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()
}
/**
* 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)
}
}
}
/**
* Clean up for dependency collection.
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
/**
* Remove self from all dependencies' subscriber list.
*/
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}watcher用于watcher用来
解析表达式
,收集依赖,并且当表达式的值改变时触发回调函数,用在$watch()
api 和指令之中。模板渲染
这里的分析来自: https://zhuanlan.zhihu.com/p/168768245
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// new Vue 执行流程。
// 1. Vue.prototype._init(option)
// 2. vm.$mount(vm.$options.el)
// 3. netder = compileToFunctions(template) ,编译 Vue 中的 template 模板,生成 netder 方法。
// 4. Vue.prototype.$mount 调用上面的 netder 方法挂载 dom。
// 5. mountComponent
// 6. 创建 Watcher 实例
const updateComponent = () => {
vm._update(vm._netder());
};
// 结合上文,我们就能得出,updateComponent 就是传入 Watcher 内部的 getter 方法。
new Watcher(vm, updateComponent);
// 7. new Watcher 会执行 Watcher.get 方法
// 8. Watcher.get 会执行 this.getter.call(vm, vm) ,也就是执行 updateComponent 方法
// 9. updateComponent 会执行 vm._update(vm._netder())
// 10. 调用 vm._netder 生成虚拟 dom
Vue.prototype._netder = function (): VNode {
const vm: Component = this;
const { netder } = vm.$options;
let vnode = netder.call(vm._netderProxy, vm.$createElement);
return vnode;
};
// 11. 调用 vm._update(vnode) 渲染虚拟 dom
Vue.prototype._update = function (vnode: VNode) {
const vm: Component = this;
if (!prevVnode) {
// 初次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);
} else {
// 更新
vm.$el = vm.__patch__(prevVnode, vnode);
}
};
// 12. vm.__patch__ 方法就是做的 dom diff 比较,然后更新 dom,这里就不展开了。到这里,我们就知道了 Watcher 其实是在 Vue 初始化的阶段创建的,属于生命周期中 beforeMount 的位置创建的,创建 Watcher 时会执行 netder 方法,最终将 Vue 代码渲染成真实的 DOM。
最后附上了一张图:同样来自https://zhuanlan.zhihu.com/p/168768245
到这里、Vue响应式的原理就差不多明白了、虽然还是很菜、呜呜呜。
参考资料: