深入了解 Vue.js 是如何进行「依赖收集]

前言
在上一章节我们已经粗略的分析了整个的Vue 的源码(还在草稿箱,需要梳理清楚才放出来),但是还有很多东西没有深入的去进行分析,我会通过如下几个重要点,进行进一步深入分析。
- 深入了解 Vue 响应式原理(数据拦截)
- 深入了解 Vue.js 是如何进行「依赖收集」,准确地追踪所有修改
- 深入了解 Virtual DOM
- 深入了解 Vue.js 的批量异步更新策略
- 深入了解 Vue.js 内部运行机制,理解调用各个 API 背后的原理
这一章节我们针对2. 深入了解 Vue.js 是如何进行「依赖收集」,准确地追踪所有修改 来进行分析。
初始化Vue
我们简单实例化一个Vue的实例, 下面的我们针对这个简单的实例进行深入的去思考:
// app Vue instancevar app = new Vue({data: {newTodo: '',},// watch todos change for localStorage persistencewatch: {newTodo: {handler: function (newTodo) {console.log(newTodo);},sync: false,before: function () {}}}})// mountapp.$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 getterif (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 setterif (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 setterif (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 firstvar 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// ordersubs.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 flushif (!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-lineif (!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 macromicroTimerFunc = 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 statevar activatedQueue = activatedChildren.slice();var updatedQueue = queue.slice();resetSchedulerState();// call component updated and activated hookscallActivatedHooks(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的工作方式。
转载:https://juejin.im/post/5c75f336e51d45708f2a26da
以上是 深入了解 Vue.js 是如何进行「依赖收集] 的全部内容, 来源链接: utcz.com/z/377785.html
