前言
在上一章节我们已经粗略的分析了整个的 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 的工作方式.
来源: https://juejin.im/post/5c75f336e51d45708f2a26da