Vue收集依赖与触发依赖源码刨析
作者:Young soul2
vue对依赖的管理使用的是发布订阅者模式,其中watcher扮演订阅者,Dep扮演发布者。所以dep中会有多个watcher,一个订阅者也可以有多个发布者(依赖)。总共三个过程:定义依赖、收集依赖、触发依赖。下面开始详细讲解三个过程
定义依赖
定义依赖是什么时候开始的呢?通过源码可以发现在执行_init函数的时候会执行initState(vm)方法:
function initState(vm) { ... if (opts.data) { initData(vm); } else { var ob = observe((vm._data = {})); ob && ob.vmCount++; } ... }
先触发initData方法:
function initData(vm) { var data = vm.$options.data; data = vm._data = isFunction(data) ? getData(data, vm) : data || {}; ... var keys = Object.keys(data); ... var i = keys.length; while (i--) { var key = keys[i]; { if (methods && hasOwn(methods, key)) { warn$2("Method \"".concat(key, "\" has already been defined as a data property."), vm); } } if (props && hasOwn(props, key)) { warn$2("The data property \"".concat(key, "\" is already declared as a prop. ") + "Use prop default value instead.", vm); } else if (!isReserved(key)) { proxy(vm, "_data", key); } } // observe data var ob = observe(data); ob && ob.vmCount++; }
首先会获取data数据,然后执行proxy(vm, “_data”, key):
var sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop }; function proxy(target, sourceKey, key) { sharedPropertyDefinition.get = function proxyGetter() { return this[sourceKey][key]; }; sharedPropertyDefinition.set = function proxySetter(val) { this[sourceKey][key] = val; }; Object.defineProperty(target, key, sharedPropertyDefinition); }
在vm实例中添加了_data对象并将data的数据给了_data。随后执行observe(data):
function observe(value, shallow, ssrMockReactivity) { ... else if (shouldObserve && (ssrMockReactivity || !isServerRendering()) && (isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value.__v_skip /* ReactiveFlags.SKIP */) { ob = new Observer(value, shallow, ssrMockReactivity); } return ob; }
主要执行 new Observer(value, shallow, ssrMockReactivity)方法:
function Observer(value, shallow, mock) { if (shallow === void 0) { shallow = false; } if (mock === void 0) { mock = false; } this.value = value; this.shallow = shallow; this.mock = mock; // this.value = value this.dep = mock ? mockDep : new Dep(); this.vmCount = 0; def(value, '__ob__', this); if (isArray(value)) { ... } else { /** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. */ var keys = Object.keys(value); for (var i = 0; i < keys.length; i++) { var key = keys[i]; defineReactive(value, key, NO_INIITIAL_VALUE, undefined, shallow, mock); } } }
主要执行defineReactive:
function defineReactive(obj, key, val, customSetter, shallow, mock) { 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) && (val === NO_INIITIAL_VALUE || arguments.length === 2)) { val = obj[key]; } var childOb = !shallow && observe(val, false, mock); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { var value = getter ? getter.call(obj) : val; if (Dep.target) { { dep.depend({ target: obj, type: "get" /* TrackOpTypes.GET */, key: key }); } if (childOb) { childOb.dep.depend(); if (isArray(value)) { dependArray(value); } } } return isRef(value) && !shallow ? value.value : value; }, set: function reactiveSetter(newVal) { var value = getter ? getter.call(obj) : val; if (!hasChanged(value, newVal)) { return; } if (customSetter) { customSetter(); } if (setter) { setter.call(obj, newVal); } else if (getter) { // #7981: for accessor properties without setter return; } else if (!shallow && isRef(value) && !isRef(newVal)) { value.value = newVal; return; } else { val = newVal; } childOb = !shallow && observe(newVal, false, mock); { dep.notify({ type: "set" /* TriggerOpTypes.SET */, target: obj, key: key, newValue: newVal, oldValue: value }); } } }); return dep; }
可以看出新增了一个依赖对象Dep,表示是该数据被哪些组件所依赖,并定义了data下数据的get和set方法。
收集依赖
vue是怎么收集依赖的呢?当组件渲染的时候会执行下面的渲染函数:
var render = function render() { var _vm = this, _c = _vm._self._c return _c("div", [ _vm._v("\n " + _vm._s(_vm.num) + "\n " + _vm._s(_vm.a) + "\n "), _c("button", { on: { click: _vm.addModule } }, [_vm._v("新增")]), ]) }
原内容如下:
<template> <div> {{num}} {{a}} <button @click="addModule">新增</button> </div> </template> <script> export default { name: "TestWebpackTest", mounted() { console.log(this); }, data() { return { num: 1, a:2 }; }, computed:{ getNum(){ return this.num+Math.random() }, getA(){ return this.a+Math.random() } }, methods: { addModule() { this.num++; } } }; </script> <style lang="scss"> div { .test { width: 10px; height: 15px; background-color: blue; } } </style>
在渲染组件模版的时候会取获取数据,此时会触发data中定义数据的getter方法,此时为当前挂载组件实例化watcher的时候会设置Dep.target。
Dep.prototype.depend = function (info) { if (Dep.target) { Dep.target.addDep(this); if (info && Dep.target.onTrack) { Dep.target.onTrack(__assign({ effect: Dep.target }, info)); } } };
通知当前依赖的组件去添加依赖,当前依赖的组件会将该依赖添加进入newDeps。相反依赖也可能被多个组件使用,所以在该依赖也有多个组件。
Watcher.prototype.addDep = function (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); } } };
其实本质就是建立组件和变量之间的依赖关系,一个组件可以有多个依赖,一个依赖可以被多个组件使用。
触发依赖
当数据发生变化时会触发数据的gettter方法:
dep.notify({ type: "set" /* TriggerOpTypes.SET */, target: obj, key: key, newValue: newVal, oldValue: value });
调用当前依赖的notify方法去通知组件更新:
Dep.prototype.notify = function (info) { // stabilize the subscriber list first var subs = this.subs.slice(); ... for (var i = 0, l = subs.length; i < l; i++) { if (info) { var sub = subs[i]; sub.onTrigger && sub.onTrigger(__assign({ effect: subs[i] }, info)); } subs[i].update(); } };
该方法就是获取当前依赖下的组件并调用该组件的update方法:
Watcher.prototype.update = function () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true; } else if (this.sync) { this.run(); } else { queueWatcher(this); } };
下面的内容我在另外一篇文章中讲过:vue2.7.10在数据发生变化后是如何更新页面的。
总结
- 定义依赖是在实例化组件的时候执行的此时的Dep.target指向当前vm实例,在initData方法中会遍历data数据并设置get和set方法,每个数据都有一个dep(依赖),表示每个数据都会被多个组件所依赖。
- 收集依赖是在执行render方法的时候。该方法触发数据的get方法,建立数据的dep(依赖)和watcher之间的联系。
- 触发依赖是在数据发生改变的时候执行。此时会触发数据的set方法,取出当前数据的依赖者(watcher)并循环调用依赖者的update方法更新视图。
到此这篇关于Vue收集依赖与触发依赖源码刨析的文章就介绍到这了,更多相关Vue收集依赖内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!