vue2 双向绑定实现原理
都知道vue中实现了数据和视图的双向绑定,但具体是如何实现的呢?
今天就说说 我阅读的vue2中的代码实现的个人所得,如果说错了,欢迎指正。
注:我阅读的vue2代码的版本是v2.2.6,且都是以单文件的方式展示列子, 可以结合下一篇续给出的实际例子结合起来看,更容易理解
话不多说,首先上结论,然后再通过图以及代码进行详细解释
结论:m->v: 利用观察者模式,当数据改变的时候通知其观察者进行更新,
v->m: 利用事件绑定对数据进行设值
实现过程:通过Object.defineProperty的方法定义 get和set方法对数据的操作拥有代理权,并且每个属性都会有一个对应的依赖,
每个vm组件都会有一个或多个watcher,当获取vm或者data的数据的时候就会调用get方法,如果这个时候有watcher正在运行观察,
那么就把这个依赖和此watcher都加入到各自的对象下的相关队列中去
1)model到view:当设置vm或者data对象的数据的时候,就会调用set方法,
如果这次结果 和上次的value不同,就让此依赖中的所有watcher加入到scheduler(调度者)的更新队列中去,
等待nextTick更新scheduler的队列,调用watcher的update方法,然后运行getter方法比较value值和之前保存的value值是否相同,
如果不同,则调用cb(回调函数),对应vm来说getter是vm.update(vm.render()),当调用getter的时候,整个过程就完成了model到view的更新了
2:view到model:一般用v-model方法进行实现,默认的是绑定dom组件的input事件(这个看源码的实现,如果你设置option.model.event的值的话,
事件就会变成你设置的值),当触发此事件的时候,会调用你绑定的值进行设置,就又回到1)过程了,当然也可以自己绑定自己关心的事件实现设值的效果
上图:

可以从结论中看到,其中有3个比较重要的部分,data对应的dep和vm对应的watcher以及把它们进行关联的Object.defineProperty定义的get和set方法,
而实现这一切的基础就是调用Object.defineProperty对属性设置get和set代理方法,所以vue2只能支持ie8以上,因为ie8及一下没有这个方法,也没法用shim的方式来实现
详细解释
一. watcher
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: Set;
newDepIds: Set;
getter: Function;
value: any;
constructor (vm: Component, expOrFn: string | Function, cb: Function, options?: Object) {}
run(){}
get(){}
update(){}
addDep(){}
cleanupDeps{}
evaluate(){}
teardown(){}
}
注:为了避免太长不方便看,方法的具体实现就没有帖出来了,如果有兴趣的可以去源码看看
简称观察者,通常意义的解释,就是对某些事物感兴趣而去观察他的人,用在vue2里面就是对一些表达式结果感兴趣而进行观察的对象,
用来统计更新vm的updateComponent方法,computed属性或者watch属性中的expOrFn(表达式或者方法)
watcher对象有3个比较重要的属性:
getter:关心的表达式或者方法
value: 运行getter的结果,在new watcher的时候如果lazy为false就会调用getter一次来初始化这个值
cb:当运行getter方法得到的value值和之前的value值不同的时候,会调用cb,对于vm组件来说就是更新组件
还有一些其他属性:
id: watcher的唯一标示,当对一个周期内的所有watcher执行更新的时候,会先根据这个id 从小到大进行排序,而且父级的vm的对应的id总是小于子集
lazy:是否懒更新, 也就是当它关注的表达式中有相关数据改变的时候,它才更新,不然就一直用value值,像computed中的watcher的lazy就是true
dirty:是否已经脏了,如果脏了,当获取这个值的时候就重新调用getter获得新值,否则直接返回value的值,和lazy有关系
user: cb回调函数是否是用户传进来的,如果是的话执行这个回调就会用try catch的方式,以避免出错
active: 这个watcher是否是活动的,如果是活动才会去运行getter
deps:对应的所有依赖,是一个数组
newDeps:这次更新后,收集到的下一轮更新所相关的依赖
depIds:所有依赖的id,是一个set对象
newDepIds: 下一轮的依赖id所对应的set对象
注:这里面的deps的作用个人觉得是:当vm销毁的时候或者运行了一此get方法的时候,从相关的依赖中删除自己,当某些需要的时候,重新和依赖关系,
目前只看到computed的watcher 在获取值的时候,如果watcher的dirty为true,就会重新和deps里面的依赖重新建立关系,其他地方就没看到有使用了- -!
先上一个例子,下面会用到:
<template>
<div id="app" class="app">
<label>姓名:</label>
<input :value="name" @input="name = $event.target.value">
<select v-model="sex">
<option value="1">男</option>
<option value="0">女</option>
</select>
<p>{{infoName}}</p>
<p>{{infoSex}}</p>
</div>
</template>
<script>
export default {
data(){
return {
name:"",
sex:1,
resultObj:{
allInfo:""
},
}
},
computed:{
infoName(){
return "您的姓名是:" + this.name;
},
infoSex(){
return "您的性别是:" + (this.sex == 1 ? "男":"女");
}
},
watch:{
sex:function(newSex){
this.resultObj.allInfo = this.name + "|" + this.sex;
alert("你的性别已经改为"+ (this.sex == 1 ? "男":"女"));
},
name:function(newName){
this.resultObj.allInfo = this.name + "|" + this.sex;
alert("你的名字已经改为"+ newName);
},
"resultObj.allInfo":function(newInfo){
alert("你新的的所有信息为"+ newInfo);
}
},
}
</script>
注: 上述代码是为了展示 watcher而强行拼凑的代码,所以有重复的地方,不要在意这些细节
有3个地方会有对应的watcher
1.一个vm组件会对应一个watcher,这个watcher关注的是vm的_render方法,每个vue文件的template里面的内容都会被编译成一个_render方法,
并且里面用到的属性或者computed都会转化成_vm.name 或者_vm.infoName 的代理形式,具体的转化过程在dep里面说到
2.computed对象里面的每个属性,都有一个对应的watcher,如上例:infoName,infoSex都会有一个对应的watcher,并且生成的watcher的lazy为true,
即它们都是懒更新的,只有里面用到的相关数据出现变化的时候,这个watcher才会执行getter方法
3.watch对象里面的每个属性,都有一个对应的watcher,如上例:sex,name,"resultObj.allInfo"都会有一个对应的watcher
"resultObj.allInfo" 这个对应的watcher需要特殊说明下它的getter,先看代码
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = function () {};
}
////////////////////////////////////////
var bailRE = /[^\w.$]/;
function parsePath (path) {
if (bailRE.test(path)) {
return
}
var segments = path.split('.');
return function (obj) {
for (var i = 0; i < segments.length; i++) {
if (!obj) { return }
obj = obj[segments[i]];
}
return obj
}
}
///////////////////////////////////////////////
function get(){
if (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);
}
}
如果初始化watcher的时候传的expOrFn 是function,那么这个getter就直接是这个function,否则就就行转化,运行parsePath方法得到一个function
当然这个function有一个判断,是路径形式的才行,当运行getter方法的时候,传入vm作为obj,那么其实resultObj.allInfo 得到的是 vm.resultObj.allInfo的值,
当然 不可能直接vm["resultObj.allInfo"]或者vm."resultObj.allInfo"这样得到,所以用了一个循环来实现
这3种对应的watcher创建的时候分别为:
1) 使用$mount方法的时候
Vue$3.prototype.$mount = function (el, hydrating) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating)
};
function mountComponent (vm,el,hydrating) {
var updateComponent = function () {
vm._update(vm._render(), hydrating);
};
vm._watcher = new Watcher(vm, updateComponent, noop);
}
注:1.有些地方我是从分模块实现的源码中拷贝过来的,有些是直接是为了方便查找,就直接在vue.common.js中拷贝过来,所以可能代码风格有些不一样,
而且为了更方便理解,会对某些地方做小小的改动,会删除一些和本话题无关的代码实现过程
2.hydrating 这个属性和vdom(虚拟dom)实现有关系,在这里暂时不去解释它的作用, 单词意思为:保湿
通过实现可以看出来,watcher的getter 就是vm的render方法(update是更新组件,和数据没有关系,所以把getter转化为render),每个vm都有自己独立对应的一个watcher,
这个watcher关心的是整个组件的渲染
上面说的是 在使用$mount方法的时候 会创建这个watcher,但是有时候,我们在创建组件的时候,没有使用$mount ,比如
const app = new Vue({
el:"#app"
router: router,
store:store,
render: h => h(App)
});
这个时候就没有用$mount,那么它就不会有watcher了吗?
先上代码:
function Vue (options) {
this._init(options)
}
Vue.prototype._init = function (options?: Object) {
//...其他初始化实现
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
注: 我吧_init方法里面不相关的代码都删除了,只留下了我们关心的地方
在new vue的时候,我们传入的对象有个el属性,关键就在这里,而在vm初始化的时候会判断传进来的对象有没有这个属性,如果有就会自动去调用$mount方法,而不需要手动
当然如果又没有el,又没有手动调用$mount方法,那么这个组件就不会出现在view上,也不会创建对应的watcher
2.解析computed属性的时候
Vue.prototype._init = function (options?: Object) {
initState(vm)
}
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(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch) initWatch(vm, opts.watch)
}
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
// create internal watcher for the computed property.
watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)
}
}
注:1.把有些和此话题不相关的代码都删除了,所以帖出来的代码不是完整的
2.在逛vue论坛的时候,遇到有人提问:在prop属性设置default为methods里面的方法为什么报空,大家看看initState的初始化顺序就明白了
3.data,computed,watch等都会存储在vm.$option中
在解析computed属性的时候,会对里面的每个属性都会有创建一个对应的watcher,并且会用用这个属性的key作为_computedWatchers 对象的key来保存这个watcher
当你默认传一个function的时候,会把这个这个function当做watcher的getter,否则就会认为你传了一个包含了get属性的对象,并且是一个方法,
这个时候就会用这个get方法当做watcher的getter
3.使用$watch方法的时候
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
function createWatcher (vm: Component, key: string, handler: any) {
let options
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
vm.$watch(key, handler, options)
}
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: Function,
options?: Object
): Function {
const vm: Component = this
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
cb.call(vm, watcher.value)
}
return function unwatchFn () {
watcher.teardown()
}
}
和使用$mount方法一样,无论是你初始化的对象包含了watch属性或者使用$watch 都会创建相对于的watcher
使用$watch会返回一个方法,当你对观察的expOrFn不在感兴趣的时候,可以用来删除这个观察者
二.dep
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
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 < l; i++) {
subs[i].update()
}
}
}
简称依赖,对vm中data返回的每个属性都会初始化一个依赖,用来收集对这个属性感兴趣的watcher,也就是watcher中运行getter方法会用到的相关属性,
那么这个属性对应的依赖就会把watcher加入到dep下的subs队列中去,也会把本身dep加入到watcher中的deps队列中去,
当这个属性的值改变的时候,通知watcher进行更新操作,也就是执行getter方法
属性解释:
subs:对某个属性感兴趣的watcher队列
dep一般对应vm的中data方法返回的对象以及它的属性
export default {
data(){
return {
name:"",
sex:1,
items:[]
}
}
}
比如上面的 data会有一个dep,name和sex也分别有一个dep
dep的创建过程
function initState (vm) {
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */);
}
}
function initData (vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
if (!isPlainObject(data)) {
data = {};
}
var keys = Object.keys(data);
var props = vm.$options.props;
var i = keys.length;
while (i--) {
if (props && hasOwn(props, keys[i])) {
///warn
} else if (!isReserved(keys[i])) {
proxy(vm, "_data", keys[i]);
}
}
// observe data
observe(data, true /* asRootData */);
}
function observe (value, asRootData) {
if (!isObject(value)) {
return
}
var ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
observerState.shouldConvert &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value);
}
return ob
}
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, '__ob__', this);
if (Array.isArray(value)) {
var augment = hasProto
? protoAugment
: copyAugment;
augment(value, arrayMethods, arrayKeys);
this.observeArray(value);
} else {
this.walk(value);
}
};
Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive$$1(obj, keys[i], obj[keys[i]]);
}
};
function defineReactive$$1 (
obj,
key,
val,
customSetter
) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
var childOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var 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) {
var 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 = observe(newVal);
dep.notify();
}
});
}
Observer.prototype.observeArray = function observeArray (items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};
注:vm初始化会调用initState方法,所以省略了init的过程
整个过程简而言之就是

1).initData
在初始化data的时候,是用一个三元运算符,判断data是否是一个function,如果是的话,就运行 getData方法
function getData (data, vm) {
try {
return data.call(vm)
} catch (e) {
handleError(e, vm, "data()");
return {}
}
}
其实getData 也就是运行一次data方法而已,只是为了安全考虑,加了try catch
如果不是的话,就返回 data || {} 给它,这里有个小技巧就是,如果data不为空就返回data,否则就返回一个{},
然后 在对data里面的属性设置代理,让我们能够用vm.key的方式来获取或者设置data里面的数据,而不用vm._data.key的方式
在方法的最后面 就是调用observe 来生成一个observer(可以依赖的对象)
2).observe
首先判断 是不是一个object,当然 这里的object 是一个泛型,是指 typeof 为 "object" 且 != null 的对象,也可以是Array,
function isObject (obj) {
return obj !== null && typeof obj === 'object'
}
如果不是一个object 那么就会中断
再判断 是否已经包含了一个 observer 对象,如果有 则返回这个observer对象
再判断
observerState.shouldConvert: 是可以转换的
!isServerRendering(): 不是服务器渲染
(Array.isArray(value) || isPlainObject(value)) 是 数组 或则 Object 类型
Object.isExtensible(value): 这个对象是可以添加属性的, 做这个判断是因为 会在Object上 新增一个__ob__属性来对应它的Observer对象
!value._isVue: 不能是 vm对象, vm组件 严格来说 也是Object对象,但是他不能被观察,所以有这个判断
注: 判断还挺多的。。。。
如果上述条件 任何一个不满足 就会中断,不过我们只需要关心的是。。我们传进来的数据满足是 数组 或者object对象就行了,其他的一般不会触及
好了 下一步就是肉戏来了,就是创建一个 observer 对象
3). new observer
保存value数据对象,然后最重要的相关核心实现就从这里开始了,
首先创建一个dep依赖(说了这么多终于说到它了),这个dep 就对应的这个value数据对象,
然后 会判断这个value是不是数组对象,
如果是的话, 先执行
augment(value, arrayMethods, arrayKeys)
而 augment 一般会对应这个方法
function protoAugment (target, src) {
/* eslint-disable no-proto */
target.__proto__ = src;
/* eslint-enable no-proto */
}
这一步比较关键,把这个数组对象默认的prototype对应的方法替换成arrayMethods 里面的方法,而arrayMethods 这个对象里面包含了哪些方法呢,请看代码
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
// cache original method
var original = arrayProto[method];
def(arrayMethods, method, function mutator () {
var arguments$1 = arguments;
// avoid leaking arguments:
// http://jsperf.com/closure-with-arguments
var i = arguments.length;
var args = new Array(i);
while (i--) {
args[i] = arguments$1[i];
}
var result = original.apply(this, args);
var ob = this.__ob__;
var inserted;
switch (method) {
case 'push':
inserted = args;
break
case 'unshift':
inserted = args;
break
case 'splice':
inserted = args.slice(2);
break
}
if (inserted) { ob.observeArray(inserted); }
// notify change
ob.dep.notify();
return result
});
});
从代码看:包含了 'push','pop','shift','unshift','splice','sort','reverse' 方法,并且arrayMethods的prototype是Array的prototype,
具体作用在下面回和例子结合说到
然后,再执行 observeArray,
Observer.prototype.observeArray = function observeArray (items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};
其实也就是对数据里面的每个元素再进行观察, 从observe的判断条件来看,普通的数据就不要想了,此元素 必须是一个object 或者array,才会生成一个observer对象(这点很重要)
好了,通过上面对数组的实现,可以解释一下 我们平时 在用数组稍不注意就会犯的一个错误, 就是改变一个数组的普通元素的值,但是 界面上却没有响应它的改动呢,
比如
<template>
<div id="app" class="app">
<p v-for="value of ary">{{value}}</p>
</div>
</template>
<script>
export default {
data(){
return {
ary:[1,2,3,4]
}
},
mounted(){
setTimeout(()=>{
this.ary[2] = 10;
}, 2000);
}
}
上述代码,会在两秒之后改变界面的显示吗?
不会,因为 this.ary[2] = 10, 这个只是普通的操作,vue2 没有对这个属性添加相关的dep(我觉得是为了性能考虑吧,不然的话 数组里面的数据大了,对每个数据都建立一个dep,那就太恐怖了)
这样的操作,是没有任何改变的,
而上面vue2 特意自己实现了array 的一些相关方法去实现这个效果,所以如果要改变普通元素的数据的时候我们可以这样,
mounted(){
setTimeout(()=>{
this.ary.splice(2,1,10);
}, 2000);
}
感兴趣的可以测试下,
起作用的原因在于arrayMethods这个对象,当我们操作splice的时候,不是操作的ary的原生的splice方法,而是操作的arrayMethods里面的splice方法,
而这个方法在执行了原生的对应的方法后,还会去调用ob.dep.notify()方法执行更新
以上是如果这个数据类型是数组的时候,当不是的时候,那么执行walk方法,
然后获取对象的keys 循环对key调用defineReactive方法,然后当这个数据 又是一个object或者array对象的时候,又再一次去观察它
可以说defineReactive 这个方法里面的实现,是实现双向绑定最最核心的一个方法了,其他的一系列都是在这基础上做扩展,或是为了健壮性或者为了解耦等等,
这个方法实现了对data的每个属性创建一个dep,并且对data的原生属性的操作利用Object.defineProperty方法定义一个set和get的代理,让data和dep产生关系,
而且 在set和get具体实现里,也实现了 让dep和watcher 产生关系,所以说这个是vue2实现双向绑定最最最核心的方法了,因为这么重要,说就单独在下一个部分来解释它
三 dep 和 watcher 是如何联系起来的
function defineReactive$$1 (
obj,
key,
val,
customSetter
) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
var childOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var 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) {
var 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 = observe(newVal);
dep.notify();
}
});
}
下面就来看看 这个方法的具体实现:
首先定义了一个dep,然后在获取这个属性的描述对象,用这个对象来判断这个属性是不是可以配置的,如果为否,则退出,因为使用Object.defineProperty
这个方法是需要这个属性是可以配置的
然后获取这个属性的 get和set方法并缓存,因为Object.defineProperty会重定义这两个方法,会覆盖它们,缓存起来以便于调用
再然后 let childOb = observe(val) 这句话,就实现生成子对象的observer,这样就实现了循环调用,对所有子对象都能够生成observer以及子对象的属性值的dep
当然,这个子对象得是 Object 或者 array
请注意:核心来了,使用Object.defineProperty方法 定义set 和get方法
我们看set方法实现:
首先得到value值,这里用三元运算符来判断之前缓存的get方法是否存在,如果存在就调用这个get方法否则直接返回val值
然后再 判断 Dep.target 是否存在,我们看看源码里面的注释对这个属性的解释
// the current target watcher being evaluated. // this is globally unique because there could be only one // watcher being evaluated at any time. Dep.target = null
这个属性表示当前正在计算的watcher, 并且在用一个时间内整个全局内只会同时存在一个
就是说:正在运行 get方法的watcher,而get是一个方法,而js是单线程,不会同一时间运行多个方法,那当运行这个get方法过程中又可能存在调用其他watcher的get方法的情况,
比如一个vm组件的render方法里面包含computed的属性的时候,这又是如何处理的呢,请看代码
const targetStack = []
export function pushTarget (_target: Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
Dep.target = targetStack.pop()
}
首先定义了一个target栈,是通过一个数组实现的,
运行watcher的get方法的时候 会先调用pushTarget方法,方法里面会 判断Dep.target,如果存在则把它压进栈里面,然后把新的taget赋值给Dep.target
watcher的个体方法后面会运行popTarget,把target栈的最后一个元素赋值给Dep.target,当然如果不存在,那么Dep.target为空
通过代码可以看出来,Dep永远是对应当前正在计算的watcher,如果还不懂的话也没关系,最后会通过一个例子把整个相关的流程都解释一遍,
我们回到set方法实现:
判断Dep.target是否存在,如果存在那么表示,我获取这个属性的值的是在运行watcher的get方法过程中(因为只有运行watcher的get方法,才会pushTarget,
而且在get方法后面还会运行popTarget把Dep.target重置),那么代表这个属性值是和这个watcher是有关联的,
好,既然是有关联的,那么dep 就和watcher要进行关联操作了,即执行dep.depend方法,这个方法实现了,把dep放进watcher的newDeps里面,为什么是newDeps
而不是deps呢,因为deps代表当前的依赖,而newDeps是表示新的依赖,即运行完这次更新之后,这个watcher的相关依赖,而运行get方法的时候会清楚当前的deps里面的依赖,
把newDeps里面的依赖复制到deps里面去
如果有childOb的话,也会把childOb同watcher关联,因为如果有childOb,那么这个value肯定是一个对象,操作这个对象有两个方式,要么获取value对象的子属性,要么设置
这个对象为新对象,这和childOb都有关系,所以需要把childOb同watcher关联
再判断value值是否为array,如果是的话,就去关联array下面的对象
以上就是整个dep 和watcher 关联的部分了
而set实现就是实现通知关联的watcher进行更新,因为只有数据改变的时候,才会需要更新,所以把通知更新实现放在set方法里面
set具体实现:
当改变一个属性对应的值的时候, 先获取现在的值并做比较,如果相同就退出
然后不同的话,那么就进行复制操作
然后再执行更新操作,而在watcher部分说过,watcher的getter有3个类型:1:vm的render方法,2:computed的属性,3:watch的属性
这个时候,大多数情况是执行vm。render进行更新,当然这个render只是返回一个vNode(虚拟节点)然后调用vm的update进行真正的更新,
这里为了简化理解,所以这样说
上面就是整个关联的部分,在总结一下过程就是:在创建watcher进行的时候,1和3方式的会立即去运行get方法,然后和相关运算用到的属性对应的dep建立关联,
而2对应的computed的属性因为是懒更新的,所以在创建watcher的时候并不会立即执行关联,而是在调用的时候才会用到,而一般是在vm.render方法里面会有调用
双向绑定到这里就解释完了,本来还想继续在这里用一个例子来说明整个过程,但是发现貌似这篇文字已经很多了,那就放在下一篇《双向绑定续》里面来解释
注:因为是第一次写博客,所以可能一些东西没考虑完善,或者错误的地方,请指正,谢谢

浙公网安备 33010602011771号