Vue.js源码学习之依赖收集
2020/4/16 11:08:47
本文主要是介绍Vue.js源码学习之依赖收集,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
Vue.js一个核心特性就是响应式数据,Vue会把普通对象变换成响应式对象,其中核心逻辑就是依赖收集。
defineReactive
Vue.js中将普通对象转换为响应式对象的核心代码如下:
function defineReactive$$1 ( obj, key, val, customSetter, shallow ) { 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; if ((!getter || setter) && arguments.length === 2) { val = obj[key]; } var childOb = !shallow && 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 (customSetter) { customSetter(); } // #7981: for accessor properties without setter if (getter && !setter) { return } if (setter) { setter.call(obj, newVal); } else { val = newVal; } childOb = !shallow && observe(newVal); dep.notify(); } }); } 复制代码
这段代码需要关注的有如下几点:
1. Object.defineProperty(obj, prop, descriptor) 复制代码
该方法能够直接修改一个对象的属性或者添加一个属性,并返回这个对象。其中需要关注的是get和set方法,如果我们修改某个对象的值就会触发setter方法,如果访问某个对象的值就会触发getter方法。
2. var dep = new Dep(); 复制代码
Dep构造函数是整个vue依赖收集的核心,定义一个响应式数据的时候就会创建一个Dep实例,访问该对象属性的时候会触发dep.depend()
,修改该对象属性的时候会触发dep.notify()
,这个就像订阅模式,当被订阅者属性发生变化的时候就会进行广播,告诉订阅者数据发生了变化
vm.set(target, key, val)
说到这,我们知道在vue.js中动态添加对象属性不是响应式的,因为动态添加的属性不在new Vue()的初始化中,需要通过vm.$set(target,property,valye)
来动态添加。我们来看下vm.$set
是如何实现的。
function set (target, key, val) { if (isUndef(target) || isPrimitive(target) ) { warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target)))); } if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key); target.splice(key, 1, val); return val } if (key in target && !(key in Object.prototype)) { target[key] = val; return val } var ob = (target).__ob__; if (target._isVue || (ob && ob.vmCount)) { warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' ); return val } if (!ob) { target[key] = val; return val } defineReactive$$1(ob.value, key, val); ob.dep.notify(); return val } 复制代码
其中判断了是否为数组,如果是数组就通过splice
方法更新。set()
方法的核心在下面两句:
defineReactive$$1(ob.value, key, val); ob.dep.notify(); 复制代码
本质上还是调用defineReactive$$1
方法将当前属性设置为响应式。
Dep
Dep 是整个 getter 依赖收集的核心。
var uid = 0; /** * A dep is an observable that can have multiple * directives subscribing to it. */ var Dep = function Dep () { this.id = uid++; this.subs = []; }; Dep.prototype.addSub = function addSub (sub) { this.subs.push(sub); }; Dep.prototype.removeSub = function removeSub (sub) { remove(this.subs, sub); }; Dep.prototype.depend = function depend () { if (Dep.target) { Dep.target.addDep(this); } }; Dep.prototype.notify = function notify () { // stabilize the subscriber list first var subs = this.subs.slice(); if (!config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort(function (a, b) { return a.id - b.id; }); } for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } }; // The current target watcher being evaluated. // This is globally unique because only one watcher // can be evaluated at a time. Dep.target = null; var targetStack = []; function pushTarget (target) { targetStack.push(target); Dep.target = target; } function popTarget () { targetStack.pop(); Dep.target = targetStack[targetStack.length - 1]; } 复制代码
Dep是一个构造函数,在其原型上分别定义了addSub
,removeSub
,depend
,notify
方法。需要特别注意的是它的一个属性target。pushTarget
方法可以设置target值。通过之前代码分析我们知道将普通对象转为响应式对象,有段非常重要的代码是:
var dep=new Dep(); ... dep.notify(); ... dep.depend(); 复制代码
我们看它们的实现:
Dep.prototype.depend = function depend () { if (Dep.target) { Dep.target.addDep(this); } }; Dep.prototype.notify = function notify () { // stabilize the subscriber list first var subs = this.subs.slice(); if (!config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort(function (a, b) { return a.id - b.id; }); } for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } }; 复制代码
depend()
方法实际上就是Dep的target属性添加当前对象。而notify()
方法是执行subs
数组中的方法的update
。那么这个target
到底是谁?我们看pushTarget()
方法在哪调用的 。
Watcher.prototype.get = function get () { pushTarget(this);//看这里 ... }; 复制代码
也就是说,构造函数Dep的target是指向Watcher构造函数的。
Watcher
var Watcher = function Watcher ( vm, expOrFn, cb, options, isRenderWatcher ) { 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; this.before = options.before; } else { this.deep = this.user = this.lazy = this.sync = false; } this.cb = cb; this.id = ++uid$2; // 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 = expOrFn.toString(); // parse expression for getter if (typeof expOrFn === 'function') { this.getter = expOrFn; } else { this.getter = parsePath(expOrFn); if (!this.getter) { this.getter = noop; 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. */ Watcher.prototype.get = function get () { pushTarget(this); var value; var 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. */ Watcher.prototype.addDep = function addDep (dep) { var 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. */ Watcher.prototype.cleanupDeps = function cleanupDeps () { var i = this.deps.length; while (i--) { var dep = this.deps[i]; if (!this.newDepIds.has(dep.id)) { dep.removeSub(this); } } var 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. */ Watcher.prototype.update = function 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. */ Watcher.prototype.run = function run () { if (this.active) { var 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 var 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. */ Watcher.prototype.evaluate = function evaluate () { this.value = this.get(); this.dirty = false; }; /** * Depend on all deps collected by this watcher. */ Watcher.prototype.depend = function depend () { var i = this.deps.length; while (i--) { this.deps[i].depend(); } }; /** * Remove self from all dependencies' subscriber list. */ Watcher.prototype.teardown = function 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); } var i = this.deps.length; while (i--) { this.deps[i].removeSub(this); } this.active = false; } }; 复制代码
从文章最开始的那张图可以看出,Watcher是连接Data和Component的桥梁,它就像一双眼睛监视数据的变化,一旦数据发生变化,就会通知Component的更新,而连接Data和Watcher之间的桥梁就是上面提到的Dep。
什么时候开始watch
Watcher的实例化有三处地方:initComputed(),stateMixin(),mountComponent(),而这三处地方分别是初始化computed数据
,Mixin
和挂载component
Watcher做了什么
1. Watcher.prototype.get(),将当前的的watcher赋值给Dep.target,执行this.getter.call(vm, vm)
,每次触发的时候都会清除掉所有依赖收集,避免重复订阅。
Watcher.prototype.get = function get () { pushTarget(this); var value; var 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 }; 复制代码
2. Watcher.prototype.addDep(),添加依赖。访问响应式对象都会执行defineProperty中的get()方法,而get()方法核心就是向Dep中添加依赖。
Watcher.prototype.addDep = function addDep (dep) { var id = dep.id; if (!this.newDepIds.has(id)) { this.newDepIds.add(id); this.newDeps.push(dep); if (!this.depIds.has(id)) { dep.addSub(this); } } }; 复制代码
3. Watcher.prototype.cleanupDeps(),清除依赖
Watcher.prototype.cleanupDeps = function cleanupDeps () { var i = this.deps.length; while (i--) { var dep = this.deps[i]; if (!this.newDepIds.has(dep.id)) { dep.removeSub(this); } } var 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; }; 复制代码
4. Watcher.prototype.update(),更新数据
Watcher.prototype.update = function update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true; } else if (this.sync) { this.run(); } else { queueWatcher(this); } }; 复制代码
我们有时候需要强制更新数据,会用到$forceUpdate()方法,而这个方法实际上就调用的watcher.update();
Vue.prototype.$forceUpdate = function () { var vm = this; if (vm._watcher) { vm._watcher.update(); } }; 复制代码
细心点的会发现Watcher.prototype.update()
最后一行代码是queueWatcher(this);
,也就是Watcher的更新不是同步的,而是一个队列。这也是我们为什么有时候改数据,视图不会立马更新或者不能及时拿到最新数据的原因。
小结:
-
- 普通对象转为响应式对象核心方法是defineProperty,获取属性值的时候会触发
dep.depend();
收集依赖,修改值的时候会触发 dep.notify(),通知变化。
- 普通对象转为响应式对象核心方法是defineProperty,获取属性值的时候会触发
-
- 收集依赖的核心是Dep构造函数,实际上利用的是订阅模式,添加订阅者,数据发生变化通知订阅者。
-
- Watcher的作用是访问数据向Dep中添加依赖,修改数据时通知变化,同时通过回调通知更新视图。
-
- Watcher更新的时候是更新watcher队列,所以数据及视图的更新是异步的。
这篇关于Vue.js源码学习之依赖收集的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-10-04package.json 文件位置在哪?-icode9专业技术文章分享
- 2024-10-01Craco.js学习:从入门到实践指南
- 2024-10-01Create-React-App学习:入门与实践指南
- 2024-10-01CSS-in-JS学习:从入门到实践指南
- 2024-09-30JSX语法学习:从入门到初步掌握
- 2024-09-30Mock.js学习:入门教程与实战演练
- 2024-09-30React Hooks学习:从入门到实践
- 2024-09-30受控组件学习:React中的基础入门教程
- 2024-09-29JS定时器教程:初学者必看指南
- 2024-09-29JS对象教程:初学者的全面指南