深入了解 Vue.js 是如何进行「依赖收集]
栏目: JavaScript · 发布时间: 5年前
内容简介:在上一章节我们已经粗略的分析了整个的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学习笔记七【可选的依赖项和依赖项排除】
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
代码阅读方法与实践
斯平内利斯 / 赵学良 / 清华大学出版社 / 2004-03-01 / 45.00元
代码阅读有自身的一套技能,重要的是能够确定什么时候使用哪项技术。本书中,作者使用600多个现实的例子,向读者展示如何区分好的(和坏的)代码,如何阅读,应该注意什么,以及如何使用这些知识改进自己的代码。养成阅读高品质代码的习惯,可以提高编写代码的能力。 阅读代码是程序员的基本技能,同时也是软件开发、维护、演进、审查和重用过程中不可或缺的组成部分。本书首次将阅读代码作为一项独立课题......一起来看看 《代码阅读方法与实践》 这本书的介绍吧!