深入了解 Vue.js 是如何进行「依赖收集]
栏目: JavaScript · 发布时间: 6年前
内容简介:在上一章节我们已经粗略的分析了整个的Vue 的源码(还在草稿箱,需要梳理清楚才放出来),但是还有很多东西没有深入的去进行分析,我会通过如下几个重要点,进行进一步深入分析。这一章节我们针对2. 深入了解 Vue.js 是如何进行「依赖收集」,准确地追踪所有修改来进行分析。我们简单实例化一个Vue的实例, 下面的我们针对这个简单的实例进行深入的去思考:
在上一章节我们已经粗略的分析了整个的Vue 的源码(还在草稿箱,需要梳理清楚才放出来),但是还有很多东西没有深入的去进行分析,我会通过如下几个重要点,进行进一步深入分析。
- 深入了解 Vue 响应式原理(数据拦截)
- 深入了解 Vue.js 是如何进行「依赖收集」,准确地追踪所有修改
- 深入了解 Virtual DOM
- 深入了解 Vue.js 的批量异步更新策略
- 深入了解 Vue.js 内部运行机制,理解调用各个 API 背后的原理
这一章节我们针对2. 深入了解 Vue.js 是如何进行「依赖收集」,准确地追踪所有修改来进行分析。
初始化Vue
我们简单实例化一个Vue的实例, 下面的我们针对这个简单的实例进行深入的去思考:
// app Vue instance
var app = new Vue({
data: {
newTodo: '',
},
// watch todos change for localStorage persistence
watch: {
newTodo: {
handler: function (newTodo) {
console.log(newTodo);
},
sync: false,
before: function () {
}
}
}
})
// mount
app.$mount('.todoapp')
复制代码
initState
在上面我们有添加一个 watch 的属性配置:
从上面的代码我们可知,我们配置了一个key为 newTodo 的配置项, 我们从上面的代码可以理解为:
当 newTodo 的值发生变化了,我们需要执行 hander 方法,所以我们来分析下具体是怎么实现的。
我们还是先从 initState 方法查看入手:
function initState (vm) {
vm._watchers = [];
var 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 && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
复制代码
我们来具体分析下 initWatch 方法:
function initWatch (vm, watch) {
for (var key in watch) {
var handler = watch[key];
if (Array.isArray(handler)) {
for (var i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
createWatcher(vm, key, handler);
}
}
}
复制代码
从上面的代码分析,我们可以发现 watch 可以有多个 hander ,写法如下:
watch: {
todos:
[
{
handler: function (todos) {
todoStorage.save(todos)
},
deep: true
},
{
handler: function (todos) {
console.log(todos)
},
deep: true
}
]
},
复制代码
我们接下来分析 createWatcher 方法:
function createWatcher (
vm,
expOrFn,
handler,
options
) {
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
if (typeof handler === 'string') {
handler = vm[handler];
}
return vm.$watch(expOrFn, handler, options)
}
复制代码
总结:
- 从这个方法可知,其实我们的
hanlder还可以是一个string - 并且这个
hander是vm对象上的一个方法,我们之前已经分析methods里面的方法都最终挂载在vm实例对象上,可以直接通过vm["method"]访问,所以我们又发现watch的另外一种写法, 直接给watch的key直接赋值一个字符串名称, 这个名称可以是methods里面定一个的一个方法:
watch: {
todos: 'newTodo'
},
复制代码
methods: {
handlerTodos: function (todos) {
todoStorage.save(todos)
}
}
复制代码
接下来调用 $watch 方法
Vue.prototype.$watch = function (
expOrFn,
cb,
options
) {
var vm = this;
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {};
options.user = true;
var watcher = new Watcher(vm, expOrFn, cb, options);
if (options.immediate) {
try {
cb.call(vm, watcher.value);
} catch (error) {
handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));
}
}
return function unwatchFn () {
watcher.teardown();
}
};
复制代码
在这个方法,我们看到有一个 immediate 的属性,中文意思就是 立即 , 如果我们配置了这个属性为 true , 就会立即执行 watch 的 hander ,也就是 同步 执行, 如果没有设置, 则会这个 watcher 是 异步 执行,下面会具体分析怎么去异步执行的。 所以这个属性可能在某些业务场景应该用的着。
在这个方法中 new 了一个 Watcher 对象, 这个对象是一个重头戏,我们下面需要好好的分析下这个对象。 其代码如下(删除只保留了核心的代码):
var Watcher = function Watcher (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
this.vm = vm;
vm._watchers.push(this);
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
}
}
this.value = this.lazy
? undefined
: this.get();
};
复制代码
主要做了如下几件事:
- 将
watcher对象保存在vm._watchers中 - 获取
getter,this.getter = parsePath(expOrFn); - 执行
this.get()去获取value
其中 parsePath 方法代码如下,返回的是一个函数:
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
}
}
复制代码
在调用 this.get() 方法中去调用 value = this.getter.call(vm, vm);
然后会调用上面通过 obj = obj[segments[i]]; 去取值,如 vm.newTodo , 我们从 深入了解 Vue 响应式原理(数据拦截) ,已经知道,Vue 会将 data 里面的所有的数据进行拦截,如下:
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();
}
});
复制代码
所以我们在调用 vm.newTodo 时,会触发 getter ,所以我们来深入的分析下 getter 的方法
getter
getter 的代码如下:
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
}
复制代码
- 首先取到值
var value = getter ? getter.call(obj) : val; - 调用
Dep对象的depend方法, 将dep对象保存在target属性中Dep.target.addDep(this);而target是一个Watcher对象 其代码如下:
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);
}
}
};
复制代码
生成的 Dep 对象如下图:
3. 判断是否有自属性,如果有自属性,递归调用。
现在我们已经完成了依赖收集, 下面我们来分析当数据改变是,怎么去准确地追踪所有修改。
准确地追踪所有修改
我们可以尝试去修改 data 里面的一个属性值,如 newTodo , 首先会进入 set 方法,其代码如下:
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();
}
复制代码
下面我来分析这个方法。
- 首先判断新的value 和旧的value ,如果相等,则就直接return
- 调用
dep.notify();去通知所有的subs,subs是一个类型是Watcher对象的数组 而subs里面的数据,是我们上面分析的getter逻辑维护的watcher对象.
而 notify 方法,就是去遍历整个 subs 数组里面的对象,然后去执行 update()
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();
}
};
复制代码
上面有一个判断 config.async ,是否是异步,如果是异步,需要排序,先进先出, 然后去遍历执行 update() 方法,下面我们来看下 update() 方法。
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
复制代码
上面的方法,分成三种情况:
- 如果
watch配置了lazy(懒惰的),不会立即执行(后面会分析会什么时候执行) - 如果配置了
sync(同步)为true则会立即执行hander方法 - 第三种情况就是会将其添加到
watcher队列(queue)中
我们会重点分析下第三种情况, 下面是 queueWatcher 源码
function queueWatcher (watcher) {
var id = watcher.id;
if (has[id] == null) {
has[id] = true;
if (!flushing) {
queue.push(watcher);
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
// queue the flush
if (!waiting) {
waiting = true;
if (!config.async) {
flushSchedulerQueue();
return
}
nextTick(flushSchedulerQueue);
}
}
}
复制代码
- 首先
flushing默认是false, 所以将watcher保存在queue的数组中。 - 然后
waiting默认是false, 所以会走if(waiting)分支 -
config是Vue的全局配置, 其async(异步)值默认是true, 所以会执行nextTick函数。
下面我们来分析下 nextTick 函数
nextTick
nextTick 代码如下:
function nextTick (cb, ctx) {
var _resolve;
callbacks.push(function () {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
if (useMacroTask) {
macroTimerFunc();
} else {
microTimerFunc();
}
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
复制代码
nextTick 主要做如下事情:
- 将传递的参数
cb的执行放在一个匿名函数中,然后保存在一个callbacks的数组中 -
pending和useMacroTask的默认值都是false, 所以会执行microTimerFunc()(微Task)microTimerFunc()的定义如下:
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
} else {
// fallback to macro
microTimerFunc = macroTimerFunc
}
复制代码
其实就是用 Promise 函数(只分析 Promise 兼容的情况), 而 Promise 是一个i额 微Task 必须等所有的 宏Task 执行完成后才会执行, 也就是主线程空闲的时候才会去执行 微Task ;
现在我们查看下 flushCallbacks 函数:
function flushCallbacks () {
pending = false;
var copies = callbacks.slice(0);
callbacks.length = 0;
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
复制代码
这个方法很简单,
- 第一个是变更
pending的状态为false - 遍历执行
callbacks数组里面的函数,我们还记得在nextTick函数中,将cb保存在callbacks中。
我们下面来看下 cb 的定义,我们调用 nextTick(flushSchedulerQueue); , 所以 cb 指的就是 flushSchedulerQueue 函数, 其代码如下:
function flushSchedulerQueue () {
flushing = true;
var watcher, id;
queue.sort(function (a, b) { return a.id - b.id; });
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null;
watcher.run();
// in dev build, check and stop circular updates.
if (has[id] != null) {
circular[id] = (circular[id] || 0) + 1;
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? ("in watcher with expression \"" + (watcher.expression) + "\"")
: "in a component render function."
),
watcher.vm
);
break
}
}
}
// keep copies of post queues before resetting state
var activatedQueue = activatedChildren.slice();
var updatedQueue = queue.slice();
resetSchedulerState();
// call component updated and activated hooks
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush');
}
}
复制代码
- 首先将
flushing状态开关变成true - 将
queue进行按照ID升序排序,queue是在queueWatcher方法中,将对应的Watcher保存在其中的。 - 遍历
queue去执行对应的watcher的run方法。 - 执行
resetSchedulerState()是去重置状态值,如waiting = flushing = false - 执行
callActivatedHooks(activatedQueue);更新组件ToDO: - 执行
callUpdatedHooks(updatedQueue);调用生命周期函数updated - 执行
devtools.emit('flush');刷新调试工具。
我们在3. 遍历queue去执行对应的watcher的run 方法。, 发现 queue 中有两个 watcher , 但是我们在我们的 app.js 中初始化 Vue 的 时候 watch 的代码如下:
watch: {
newTodo: {
handler: function (newTodo) {
console.log(newTodo);
},
sync: false
}
}
复制代码
从上面的代码上,我们只 Watch 了一个 newTodo 属性,按照上面的分析,我们应该只生成了一个 watcher , 但是我们却生成了两个 watcher 了, 另外一个 watcher 到底是怎么来的呢?
总结:
- 在我们配置的
watch属性中,生成的Watcher对象,只负责调用hanlder方法。不会负责UI的渲染 - 另外一个
watch其实算是Vue内置的一个Watch(个人理解),而是在我们调用Vue的$mount方法时生成的, 如我们在我们的app.js中直接调用了这个方法:app.$mount('.todoapp'). 另外一种方法不直接调用这个方法,而是在初始化Vue的配置中,添加了一个el: '.todoapp'属性就可以。这个Watcher负责了UI的最终渲染,很重要,我们后面会深入分析这个Watcher -
$mount方法是最后执行的一个方法,所以他生成的Watcher对象的Id是最大的,所以我们在遍历queue之前,我们会进行一个升序排序, 限制性所有的Watch配置中生成的Watcher对象,最后才执行$mount中生成的Watcher对象,去进行UI渲染。
$mount
我们现在来分析 $mount 方法中是怎么生成 Watcher 对象的,以及他的 cb 是什么。其代码如下:
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
复制代码
- 从上面的代码,我们可以看到最后一个参数
isRenderWatcher设置的值是true, 表示是一个Render Watcher, 在watch中配置的,生成的Watcher这个值都是false, 我们在Watcher的构造函数中可以看到:
if (isRenderWatcher) {
vm._watcher = this;
}
复制代码
如果 isRenderWatcher 是 true 直接将这个特殊的 Watcher 挂载在 Vue 实例的 _watcher 属性上, 所以我们在 flushSchedulerQueue 方法中调用 callUpdatedHooks 函数中,只有这个 watcher 才会执行生命周期函数 updated
function callUpdatedHooks (queue) {
var i = queue.length;
while (i--) {
var watcher = queue[i];
var vm = watcher.vm;
if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'updated');
}
}
}
复制代码
- 第二个参数
expOrFn, 也就是Watcher的getter, 会在实例化Watcher的时候调用get方法,然后执行value = this.getter.call(vm, vm);, 在这里就是会执行updateComponent方法,这个方法是UI 渲染的一个关键方法,我们在这里暂时不深入分析。 - 第三个参数是
cb, 传入的是一个空的方法 - 第四个参数传递的是一个
options对象,在这里传入一个before的function, 也就是,在UI重新渲染前会执行的一个生命中期函数beforeUpdate
TODO:继续分析 computed 的工作方式。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 深入理解控制反转(IoC)和依赖注入(DI)
- 深入浅出Spring Boot 起步依赖和自动配置
- 浅析依赖倒转、控制反转、IoC 容器、依赖注入。
- Angular 4 依赖注入教程之五 FactoryProvider配置依赖对象
- Gradle构建SpringBoot程序依赖管理之依赖版本自动控制
- Maven学习笔记七【可选的依赖项和依赖项排除】
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Developer's Guide to Social Programming
Mark D. Hawker / Addison-Wesley Professional / 2010-8-25 / USD 39.99
In The Developer's Guide to Social Programming, Mark Hawker shows developers how to build applications that integrate with the major social networking sites. Unlike competitive books that focus on a s......一起来看看 《Developer's Guide to Social Programming》 这本书的介绍吧!