We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
本文将把vue 2.x的响应式原理及其依赖收集进行分析讲解,并简单用代码模拟一遍vue这一部分的源码。
点击查看模式实现的代码
(此处及之后的vue泛指2.x版本的vue)
2.x
绝大部分人都知道vue2.x的响应式依赖一个APIObject.defineProperty,通过该API可以劫持属性的获取(get)/赋值(set),在回调中进行更新视图,从而达到由数据驱动视图去更新。
Object.defineProperty
但要实现响应式,细节上还需要更多,譬如:
属性被set之后,具备什么样的条件才能更新视图。
上述第一点满足后,框架如何知道具体要更新哪一个视图组件。
如何解决多个属性触发同一个组件更新的情况。
...
抛出上述问题之后,vue的做法是这样的:
data()
key
const dep = new Dep()
get
set
getter
dep
setter
watcher
render watcher
引用vue官方的一张图展示这一个过程:
首先组件render的时候,渲染在视图中的状态都会触发其getter,然后组件对应的Watcher在getter回调中将其作为依赖进行收集。当状态发生变化后,通知notify收集其依赖的Watcher,然后Watcher进行更新,触发组件的重新render。
render
Watcher
notify
接下来由代码来讲述这整个流程,基本就是vue源代码的简化版,省略了大部分的变量校验和与本文主题无关的代码。想仔细研究完整版本请自行查阅源码。
首先我们要拿到组件中声明的状态,默认规定是一个工厂函数,返回一个object,这样就可以保证每次根据配置实例化的状态,不会指向同一份状态。拿到状态data后,我们就进行观察劫持。
object
data
// 获取data 并保留一份指引在实例上,即this._data const data = vm._data = vm.$option.data.call(vm) // 将状态代理到实例上,就可以通过this.xxx获取 // 源码中是将 this.xxx 代理到 this._data.xxx const keys = Object.keys(data) let i = keys.length while (i--) { const key = keys[i] proxy(this, '_data', key) } // 然后进行递归的observe observe(data)
下面则是进行代理的proxy方法实现
proxy
// 将状态代理到目标上 function proxy (source, sourceKey, k) { Object.defineProperty(source, k, { enumerable: true, configurable: true, get: function () { return this[sourceKey][k] }, set: function (val) { this[sourceKey][k] = val } }) }
接着讲解的是observe(data),这里要完成的就是递归进行劫持。源码中整个流程:observe(data)->new Observer(data)->walk(data)->defineReative(data, key)。当中涉及到数组和普通对象的处理,以及是否需要劫持的判断,故在此不作展开。简化版如下,并不影响我们的逻辑分析:
observe(data)
new Observer(data)
walk(data)
defineReative(data, key)
function observe (obj) { const keys = Object.keys(obj) for (const key of keys) { const dep = new Dep() // 保存对象-Key的取值 let val = obj[key] // 递归劫持 if (Object.prototype.toString.call(val) === 'object') { observe(val) } Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function () { // 进行依赖收集 if (Dep.target) { dep.depend() } return val }, set: function (newVal) { // vue此处做了优化,如果值没变化,则不会通知watcher if (newVal === val) return // 变化之后需要再次赋值 val = newVal // 由依赖进行通知 dep.notify() } }) } }
这是递归遍历data时,为每一个key值实例化的类,一个key对应一个dep。首先看下Dep类的简单实现:
Dep
let depId = 0 class Dep { // 静态属性,类型是Watcher static target; constructor () { this.id = depId++ this.subs = [] } // 添加订阅者,当属性发生变化,可以透过属性去查找其对应的watcher addSub (sub) { this.subs.push(sub) } // 移除订阅 removeSub (sub) { const index = this.subs.findIndex(sub) if (index !== -1) { this.subs.splice(index, 1) } } // 依赖收集 depend () { if (Dep.target) { Dep.target.addDep(this) } } // 广播更新 notify () { this.subs.forEach(sub => { sub.update() }) } } Dep.target = null
有两个设计点:
user watcher
watch
target
Dep.target
vue使用了一个栈来维护当前的Dep.target,因为考虑到当前watcher更新时,可能会触发另一个watcher的更新渲染,需要对上一个watcher进行保留。
const targetStack = [] function pushTarget (target) { targetStack.push(target) Dep.target = target } function popTarget () { targetStack.pop() Dep.target = targetStack[targetStack.length - 1] }
而什么时候需要赋值当前的Dep.target,就在于Watcher的设计了。
vue里面的Watcher,负责了三个功能:computed、用户自定义watcher、data状态的依赖收集,而前两者都有依赖于第三个依赖收集的机制。源码里的Watcher考虑到了更多的情景,这里只针对依赖收集的那一部分代码进行简单实现:
computed
let watchId = 0 class Watcher { constructor (vm, expOrFn) { this.id = watchId++ this.vm = vm // 这里是获取值的回调,也可以穿入render/update方法 this.getter = expOrFn // 用于处理依赖 this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() // 实例化时,会执行一遍“获取”的get函数 this.value = this.get() } // 收集依赖 addDep (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) } } } // 清理依赖 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渲染的值,如果是render watcher,其返回的值不必用到,只会执行逻辑上的渲染 get () { pushTarget(this) const value = this.getter.call(this.vm) popTarget() return value } // 这里源码里是会启动一个异步队列,进行更新 update () { Promise.resolve().then(() => { this.get() }) } }
watcher的实例化时机就在所有状态、事件、注入都初始化完了之后,DOM进行mount之前。那一刻data中的状态均已完成了响应式劫持的声明。
mount
// 源码中大概的调用: updateComponent = () => { vm._update(vm._render(), hydrating) } new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */)
对照回往上watcher的声明,可以发现其实render watcher里,expOrFn函数就会被传递成一个render的函数。代表着,实例化watcher代码的最后执行this.get(),相当于都会执行一次传递过来的render。
expOrFn
this.get()
紧接着我们来看get函数
get () { pushTarget(this) const value = this.getter.call(this.vm) popTarget() return value }
在这里,就把当前正在渲染watcher变成Dep.target,然后执行参数中传递过来的render,在render过程中就会触发data状态的get属性回调,并执行依赖收集。
addDep (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) } } }
依赖收集这里用了两对数组,一对是new开头的,分别存放id和实例的。另一对则不带new,意味着是原来的。
new
id
这样设计的原因是**每一次的render,模板中用到的状态可能会不一样。**e.g:v-if的状态由true改变成了false,并且v-if下的代码块中包含了声明的状态date。那么对比前后,第一次渲染的时候watcher收集到了date的依赖,但是状态改变之后,date的状态被v-if="false"包裹了,对于视图来说我们不需要收集这个依赖去更新了。
v-if
true
false
date
v-if="false"
所以每一次更新我们都要重新清除cleanupDeps上一次收集过的依赖,赋值给新的依赖。就可以避免改变视图中没用到的状态,也会触发更新这个场景,可以说是一种优化。
cleanupDeps
结合了上述编写的简单Dep和Watcher,简单写一个Vue类试验一下。
Vue
class Vue { constructor (option) { this._option = option this._el = document.querySelector(option.el) this._template = this._el.innerHTML this._initState(this) new Watcher(this, function update () { this._mount() }) } // 递归遍历data 初始化响应式 _initState () { const data = this._data = this._option.data ? this._option.data() : {} const keys = Object.keys(data) let i = keys.length while (i--) { const key = keys[i] proxy(this, '_data', key) } observe(data) } _mount () { const _this = this let template = _this._template // 替换差值表达式 let matchText while ((matchText = /\{\{((\w)+?)\}\}/.exec(template))) { template = template.replace(matchText[0], _this._data[matchText[1]]) } _this._el.innerHTML = template } }
这里没有模拟virtual-dom,只模拟vue中响应式和依赖收集的场景。然后html中编写以下代码:
virtual-dom
<body> <div id="app"> <div>计数器:{{counter}}</div> <div>当前时间戳:{{currentDate}}</div> </div> <button id="counter">增加</button> <button id="timer">打印时间</button> </body> <script> const app = new Vue({ el: '#app', data () { return { counter: 1, currentDate: Date.now() } } }) const $ = sel => document.querySelector(sel) $('#counter').onclick = () => { app.counter++ } $('#timer').onclick = () => { app.currentDate = Date.now() } </script>
试验也能成功
The text was updated successfully, but these errors were encountered:
No branches or pull requests
本文将把vue 2.x的响应式原理及其依赖收集进行分析讲解,并简单用代码模拟一遍vue这一部分的源码。
点击查看模式实现的代码
(此处及之后的vue泛指
2.x
版本的vue)绝大部分人都知道vue2.x的响应式依赖一个API
Object.defineProperty
,通过该API可以劫持属性的获取(get)/赋值(set),在回调中进行更新视图,从而达到由数据驱动视图去更新。但要实现响应式,细节上还需要更多,譬如:
属性被set之后,具备什么样的条件才能更新视图。
上述第一点满足后,框架如何知道具体要更新哪一个视图组件。
如何解决多个属性触发同一个组件更新的情况。
...
抛出上述问题之后,vue的做法是这样的:
data()
之后,每个属性key
作为一个依赖,实例化一个名为Dep的依赖对象const dep = new Dep()
,并用Object.defineProperty
劫持get
/set
,getter
,在getter
的回调中收集其对应的依赖dep
set
属性时,在属性的setter
回调中,其对应dep
通知所有收集到它的对象。watcher
对应到每一个组件,Vue把其称为render watcher
。属性被set
之后,依赖对象dep
就会通知将它收集的watcher
,由watcher
进行更新视图。引用vue官方的一张图展示这一个过程:
首先组件
render
的时候,渲染在视图中的状态都会触发其getter
,然后组件对应的Watcher
在getter
回调中将其作为依赖进行收集。当状态发生变化后,通知notify
收集其依赖的Watcher
,然后Watcher
进行更新,触发组件的重新render。状态属性的getter/setter
首先我们要拿到组件中声明的状态,默认规定是一个工厂函数,返回一个
object
,这样就可以保证每次根据配置实例化的状态,不会指向同一份状态。拿到状态data
后,我们就进行观察劫持。下面则是进行代理的
proxy
方法实现接着讲解的是
observe(data)
,这里要完成的就是递归进行劫持。源码中整个流程:observe(data)
->new Observer(data)
->walk(data)
->defineReative(data, key)
。当中涉及到数组和普通对象的处理,以及是否需要劫持的判断,故在此不作展开。简化版如下,并不影响我们的逻辑分析:依赖 Dep
这是递归遍历
data
时,为每一个key
值实例化的类,一个key
对应一个dep
。首先看下Dep
类的简单实现:有两个设计点:
render watcher
收集,也可以被user watcher
收集,即用户自己编写的watch
选项,等等。target
,存放当前进行render
的watcher
。虽然说vue的更新是异步的,但是这个异步只是相对于改变状态的操作而言,对于模版/render方法渲染单个组件的过程依然是js同步进行的。所以全局同一时候只会有一个watcher
进行更新,更新完当前的watcher
,再将新的watcher
重新赋值到Dep.target
。vue使用了一个栈来维护当前的
Dep.target
,因为考虑到当前watcher
更新时,可能会触发另一个watcher
的更新渲染,需要对上一个watcher
进行保留。而什么时候需要赋值当前的
Dep.target
,就在于Watcher
的设计了。Watcher
vue里面的
Watcher
,负责了三个功能:computed
、用户自定义watcher
、data
状态的依赖收集,而前两者都有依赖于第三个依赖收集的机制。源码里的Watcher
考虑到了更多的情景,这里只针对依赖收集的那一部分代码进行简单实现:watcher
的实例化时机就在所有状态、事件、注入都初始化完了之后,DOM进行mount
之前。那一刻data
中的状态均已完成了响应式劫持的声明。对照回往上
watcher
的声明,可以发现其实render watcher
里,expOrFn
函数就会被传递成一个render
的函数。代表着,实例化watcher
代码的最后执行this.get()
,相当于都会执行一次传递过来的render
。紧接着我们来看
get
函数在这里,就把当前正在渲染
watcher
变成Dep.target
,然后执行参数中传递过来的render
,在render
过程中就会触发data
状态的get
属性回调,并执行依赖收集。依赖收集这里用了两对数组,一对是
new
开头的,分别存放id
和实例的。另一对则不带new
,意味着是原来的。这样设计的原因是**每一次的
render
,模板中用到的状态可能会不一样。**e.g:v-if
的状态由true
改变成了false
,并且v-if
下的代码块中包含了声明的状态date
。那么对比前后,第一次渲染的时候watcher
收集到了date
的依赖,但是状态改变之后,date
的状态被v-if="false"
包裹了,对于视图来说我们不需要收集这个依赖去更新了。所以每一次更新我们都要重新清除
cleanupDeps
上一次收集过的依赖,赋值给新的依赖。就可以避免改变视图中没用到的状态,也会触发更新这个场景,可以说是一种优化。实践 && 测试
结合了上述编写的简单
Dep
和Watcher
,简单写一个Vue
类试验一下。这里没有模拟
virtual-dom
,只模拟vue中响应式和依赖收集的场景。然后html中编写以下代码:试验也能成功
The text was updated successfully, but these errors were encountered: