前言
首先借用一张图来看下vue响应式原理的大致流程:
理解响应式原理需要了解三个类: Observe、Dep、Watcher, Observe类为每个属性创建一个dep类,同时data中的属性进行劫持,属性get的时候往dep.sub数组里面添加watcher, set属性的时候执行dep函数中的notify通知所有的watcher触发更新。
上图说明了,组件在执行render函数,构建虚拟dom的过程中:构建虚拟dom过程中会触发属性的get方法,将watcher收集为依赖,然后将页面挂载到真实dom上,当属性发生改变时,触发set函数,set函数触发属性dep的notify函数通知watcher,生成新的dom树,然后与原有dom树进行对比,更新真实dom。
思考:怎么直接访问data里面的数据呢?
实例化过程执行_init()方法,往实例上添加属性、初始化data ,将data绑定到实例vm上,创建observer实例,全局监听data,为每个属性创建一个dep实例,Object.defineProperty 添加get 方法, set方法(研究响应式原理会重点分析)
上面是之前文章提到过的,实例化的时候会对data进行初始化,我们先看下做了那些操作:
function initData (vm) {
let data = vm.$options.data
data = vm._data = typeof data === 'function' // vm._data
? data()
: data || {}
const keys = Object.keys(data)
let i = keys.length
while (i--) {
proxy(vm, `_data`, keys[i])
}
// 正式监听全局 data 了
observe(data, true) // data 是引用的关系,引用的vm._data
}
function proxy (target, sourceKey, key) { // vm _data msg
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get: function () {
return this[sourceKey][key] // this为 target 即 vm._data.msg
},
set: function (val) {
this[sourceKey][key] = val
}
})
}
复制代码
对data进行遍历,将data上的属性直接绑到vue实例上,this.xxx 就是 this.data.xxx
接下来看下,这三个类是如何实现的?相互之间有何关系?也就是响应式原理
响应式原理
1. Observer类
render的时候收集依赖,修改数据的时候进行更新,这里显然用到的是观察者模式,修改data数据属性的时候都会触发页面更新,也就是说每个属性都会对应一个观察者模式的实例,因此我们用Observer类来管理data属性的所有的观察者模式,Dep类作为目标对象,watcher作为观察者。
Observer类循环对data中的属性,每个属性创建一个Dep实例,通过Object.defineProperty 进行劫持,get的时候收集观察者watcher, set的时候通知watcher更新
class Observer {
constructor(value) {
this.walk(value)
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
// data 的每个属性都执行这个函数
defineReactive(obj, keys[i])
}
}
}
function defineReactive (obj, key) {
// 每个属性都对应一个 dep 实例
const dep = new Dep()
let val = obj[key] // 属性值
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function () {
if (Dep.target) { // Dep.target 即为 watcher
// 将 watcher 加入数组中,
// dep.depend() 等价于 dep.addSub(Dep.target)
dep.depend()
}
// 返回属性值
return val
},
set: function (newVal) {
// data中所有的变量赋值都会走set,只不过render中没用到的,那么它的dep.subs是空的,不会触发watcher的update
if (newVal !== val) {
val = newVal // val是闭包,会一直存在
dep.notify() // 通知 watcher 更新
}
}
})
}
复制代码
我们看到了收集依赖的时候执行了dep.depend(),而不是直接执行dep.addSub(Dep.target),这是为什么呢?先留个疑问,下面分析
2. Dep 类
dep类
id: dep的唯一标识,每个属性对应一个dep
subs: [watcher],用于记录该属性的所有观察者
let depId = 0
class Dep {
constructor () {
this.id = ++depId
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // sub[i] 就是一个watcher,每个watch都有个update方法,相当于执行回调函数
}
}
// addSub 是'Dep主动'添加watcher
// depend 是让 watcher 主动‘被添加’到Dep中
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
}
Dep.target = null
复制代码
3. Watcher
Watcher类
deps: [dep] 维护一个depId的数组,所有依赖的数据,如果老的dep id 在新get收集后的ids中不存在,那么会删除老的dep中对应的id
getter: 表达式或者是函数
value: 每次求值得到的结果
watcher在源码中有三种,分别是全局watcher, 自定义$watcher,还有就是conputed watcher,这里先不考虑computed watcher
全局watcher
全局watcher是是什么时候定义的呢?
注意:mountComponent 函数中的 new Watcher(vm, updateComponent, noop, {})
在vue实例挂载阶段也就是执行$mount函数的时候,调用了mountComponent 函数, 执行了的new Watcher(vm, updateComponent, noop, {}),实例化watcher的时候constructor中执行了this.get方法,触发了回调updateComponent,也就是 vm._update(vm._render()) ,执行render方法,在render方法执行中访问data里面的属性,被Object.defineProperty的get方法劫持(初始化数据阶段做了数据劫持),触发依赖收集,将全局watcher加入到subs数组中
也就是说,vue只会对render里面用到的数据进行监听
自定义watcher
自定义watcher是怎么被属性加到自己的subs数组里的呢?
实例化获取value时触发get方法,进一步触发属性的get,从而被收集起来
// 定义 $mount 挂载方法
Vue.prototype.$mount = function (el) {
const vm = this
vm.$el = typeof el === 'string' ? document.querySelector(el) : document.body
const options = vm.$options;
if (!options.render) {
// 如果不存在 render ,那么就需要 compileToFunctions 编译 render了,这里我们刚好跳过了。。。
// vm.options.render = compileToFunctions(vm.options.template)
}
// 继续
mountComponent(vm) // 在 lifecycle.js 中定义的
}
// lifecycle.js
function mountComponent (vm) {
// callHook(vm, 'beforeMount') // 生命周期
let updateComponent = function () {
vm._update(vm._render()) // 执行 _render()获得 vnode,再执行 _update 渲染到 dom 中
}
// 定义全局Watcher
new Watcher(vm, updateComponent, noop, {})
// noop 是一个空函数,本来应该传cb回调函数,但是全局这个Watcher,run的时候执行了const val = this.get()
// 相当于执行了updateComponent(),即updateComponent就是作为回调函数了,因此cb传个空函数
}
class Watcher {
constructor(vm, expOrFn, cb, options) {
// expOrFn 有两种
// 1. vm.$watch('msg', cb) expOrFn === msg 为想要监听的属性
// 2. 全局 Watcher 中,expOrFn === updateComponent 函数
this.vm = vm
this.cb = cb
this.expOrFn = expOrFn
this.depIds = new Set() // 用于dep去重
this.val = this.get() // 触发依赖收集
}
// 让key对应的dep,收集自己
get () {
let value
const vm = this.vm
Dep.target = this // 变为自己
if (typeof this.expOrFn === 'function') { // 如果全局watcher 传进来的是 updateComponent 函数
this.getter = this.expOrFn
value = this.getter.call(vm) // 执行 updateComponent ,触发了 _render
} else { // 如果是 vm.$watcher('msg') 这种的
let key = this.expOrFn // key === msg
console.log(8989,key)
value = vm._data[key] // 这一步触发了 属性的 get ,defineReactive中,此时 Dep.target 是有值的哈
}
Dep.target = null // 清空 watcher
this.cleanupDeps() // 源码是要清理已经不在render中的属性了,因为可能v-if把一个元素给去掉了
return value
}
// 执行监听回调
update () {
this.run()
}
run () {
const vm = this.vm
const val = this.get() // 每次修改一个属性,都会再次执行this.get(),触发新的render收集依赖;;;新老依赖的dep会通过cleanupDeps做清理
if (val !== this.val) {
const oldVal = this.val
this.val = val
this.cb.call(vm, val, oldVal) // 新、老 val 传给回调函数并执行
}
}
addDep (dep) {
const id = dep.id
if (!this.depIds.has(id)) { // 去重,防止多次render时多次收集依赖
this.depIds.add(id)
dep.addSub(this) // dep 将 watcher 实例添加进去
console.log(12, this.depIds, dep)
}
}
// 我们这里没实现它,但是它很有用,如果老的dep id 在新get收集后的ids中不存在,那么会删除老的dep中对应的id
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
}
}
复制代码
以上只说了watcher是怎么被收集起来的,那怎么触发更新呢?
当修改属性值时,执行属性的set方法,通知watcher更新,但是属性需要通知的watcher不止一个呀,有全局watcher和自定义watcher,都执行了run函数,但是这里有区别:
全局watcher执行润函数时,this.value = get()会触发render函数,重新收集依赖,并将本次更新显示到页面上,全局watcher的value都是undefined,所以不会往下执行
自定义watcher,this.get()获取到最新值后会和之前的值作对比,不一致就会执行自己的回调函数
疑问❓
至此,三个类就结束了,当然现在是简化的,现在可以解释一下为什么在watcher类里面执行了dep.add(watcher)收集依赖,而不是属性get的时候直接往dep里面添加依赖。
我们以为的:
this.xxx -> get -> dep.addSub(watcher) dep.target为watcher
实际:
this.xxx -> get -> dep.depend -> dep.target.addDep(dep) -> dep.addSub(watcher)
每次触发get都收集依赖,怎么去重呢?
本来get的时候就可以收集watcher了,实际上get到dep里面转了一圈,进到watcher,在watcher里面去添加依赖,这是因为,属性可以再render中出现很多次,每次都触发get都收集依赖,那么得去重呀,怎么去重呢?
- 在dep里面维护watcherId数组,收集过的就不再收集了,看起来没毛病,但是没这么做
2. watcher里面维护一个depId数组,收集依赖的时候先去dep里面转一圈,拿到dep ,传递给watcher,这样每次收集watcher 都能获取到当前dep id , depId数组里面不存在当前dep id,说明该dep没收集过,dep.addSub(watcher),并将当前dep Id push到depId数组里面, 比较绕。。。
为什么选择方式二呢?为什么要在watcher里面去收集依赖呢?
为什么要在watcher里面去收集依赖呢?
// 想想v-if
// v-if 由 true -> false
// v-if 为 true 的时候, this.name才会出现在render函数里面
// v-if 为 false 的时候, this.name不会出现在render函数里面
<p v-if="false">{{ this.name }}</p>
复制代码
因为开始的时候为true,this.name属性中收集了watcher,当this.name变化的时候会触发更新,那false了呢?,不清理的话,是不是修改之后还会触发更新,这不就浪费性能了嘛。。。。。。
要清理dep里面的watcher,那得知道哪个dep由true变false了,那不就得维护一个depId数组(这里每个data里面的属性对应一个dep,每个dep有一个唯一的depId),对比一下, 上一次render的时候和本次render的时候,少了哪些dep,dep数组在哪里维护?当然是watcher啊,因为render中访问的属性都会收集watcher,而触发页面渲染的回调函数也是在watcher中执行的,所以在watcher中维护depId数组,一来是用于去重,二来是清理上一次render的时候和本次render的时候,少了的dep里面的watcher
想想在dep里面能不能做呢,给dep加个标识是显示还是没显示,是否清空watcher?
不能吧, this.name不会出现在render函数里面,那就不会触发get,更没法改this.name对应的dep的标识了呀~~
总结:
理解为,属性在render函数中有时存在有时不存在,为了避免不存在的时候,修改属性值会触发页面更新,影响性能,因此在watcher中维护depId数组,对比前后两次的depId数组,清空本次不存在的dep里面的watcher,顺便在watcher里面调用dep的addSub(this),把自己添加到dep的数组里,进一步想想,都维护了depId数组了,存在的depId,再次过来的时候就不向dep里面添加自己(watcher)了呗,当然就能对watcher去重了
简单代码
class Observer {
constructor(value) {
this.walk(value)
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
// data 的每个属性都执行这个函数
defineReactive(obj, keys[i])
}
}
}
function defineReactive (obj, key) {
// 每个属性都对应一个 dep 实例
const dep = new Dep()
let val = obj[key] // 属性值
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function () {
if (Dep.target) { // Dep.target 即为 watcher
// 将 watcher 加入数组中,
// dep.depend() 等价于 dep.addSub(Dep.target)
dep.depend()
}
// 返回属性值
return val
},
set: function (newVal) {
// data中所有的变量赋值都会走set,只不过render中没用到的,那么它的dep.subs是空的,不会触发watcher的update
if (newVal !== val) {
val = newVal // val是闭包,会一直存在
dep.notify() // 通知 watcher 更新
}
}
})
}
let depId = 0
class Dep {
constructor () {
this.id = ++depId
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // sub[i] 就是一个watcher,每个watch都有个update方法,相当于执行回调函数
}
}
// addSub 是'Dep主动'添加watcher
// depend 是让 watcher 主动‘被添加’到Dep中
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
}
Dep.target = null
// 定义 $mount 挂载方法
Vue.prototype.$mount = function (el) {
const vm = this
vm.$el = typeof el === 'string' ? document.querySelector(el) : document.body
const options = vm.$options;
if (!options.render) {
// 如果不存在 render ,那么就需要 compileToFunctions 编译 render了,这里我们刚好跳过了。。。
// vm.options.render = compileToFunctions(vm.options.template)
}
// 继续
mountComponent(vm) // 在 lifecycle.js 中定义的
}
// lifecycle.js
function mountComponent (vm) {
// callHook(vm, 'beforeMount') // 生命周期
let updateComponent = function () {
vm._update(vm._render()) // 执行 _render()获得 vnode,再执行 _update 渲染到 dom 中
}
// 定义全局Watcher
new Watcher(vm, updateComponent, noop, {})
// noop 是一个空函数,本来应该传cb回调函数,但是全局这个Watcher,run的时候执行了const val = this.get()
// 相当于执行了updateComponent(),即updateComponent就是作为回调函数了,因此cb传个空函数
}
class Watcher {
constructor(vm, expOrFn, cb, options) {
// expOrFn 有两种
// 1. vm.$watch('msg', cb) expOrFn === msg 为想要监听的属性
// 2. 全局 Watcher 中,expOrFn === updateComponent 函数
this.vm = vm
this.cb = cb
this.expOrFn = expOrFn
this.depIds = new Set() // 用于dep去重
this.val = this.get() // 触发依赖收集
}
// 让key对应的dep,收集自己
get () {
let value
const vm = this.vm
Dep.target = this // 变为自己
if (typeof this.expOrFn === 'function') { // 如果全局watcher 传进来的是 updateComponent 函数
this.getter = this.expOrFn
value = this.getter.call(vm) // 执行 updateComponent ,触发了 _render
} else { // 如果是 vm.$watcher('msg') 这种的
let key = this.expOrFn // key === msg
console.log(8989,key)
value = vm._data[key] // 这一步触发了 属性的 get ,defineReactive中,此时 Dep.target 是有值的哈
}
Dep.target = null // 清空 watcher
this.cleanupDeps() // 源码是要清理已经不在render中的属性了,因为可能v-if把一个元素给去掉了
return value
}
// 执行监听回调
update () {
this.run()
}
run () {
const vm = this.vm
const val = this.get() // 每次修改一个属性,都会再次执行this.get(),触发新的render收集依赖;;;新老依赖的dep会通过cleanupDeps做清理
if (val !== this.val) {
const oldVal = this.val
this.val = val
this.cb.call(vm, val, oldVal) // 新、老 val 传给回调函数并执行
}
}
addDep (dep) {
const id = dep.id
if (!this.depIds.has(id)) { // 去重,防止多次render时多次收集依赖
this.depIds.add(id)
dep.addSub(this) // dep 将 watcher 实例添加进去
console.log(12, this.depIds, dep)
}
}
// 我们这里没实现它,但是它很有用,如果老的dep id 在新get收集后的ids中不存在,那么会删除老的dep中对应的id
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
}
}
复制代码