Talk is cheap,show me the code。
先抄一把官方文档的简单介绍吧,然后再贴个总览图,再细细说来。逐行源码分析,并不是单纯的理论。
当你把一个普通的 JavaScript 对象传入vue实例作为 data 选项,vue将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter 。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是vue不支持 IE8 以及更低版本浏览器的原因。
这些 gettersetter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。这里需要注意的是不同浏览器在控制台打印数据对象时对 gettersetter 的格式化并不同,所以建议安装 vue-devtools来获取对检查数据更加友好的用户界面。
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
这章节主要对上述Data(紫色那块)来个源码分析吧,基于2.16.14版本。
Data中的数据对象从配置属性用Observer类变成了访问器属性,访问器中的getter属性通过Dep类收集依赖(Watcher类),访问器中的setter属性通知依赖更新。
从observe方法开始,observe方法作用是筛查value,源码(有删减)如下:
function observe (value) {
// 初步过滤一些不合适的value
// 在对象的基础上,过滤虚拟节点对象,为什么要过滤掉虚拟节点?
// Vnode是内部的一个类,什么全局方法会暴露出这个类呢?
// 然而查找文档并没有,倒是存在一个Vnode接口暴露出来,也就解释了为什么要过滤掉VNode
if (!isObject(value) || value instanceof VNode) { return }
// 函数的最后会将这个观测的对象返回,这样做的目的为了递归侦测
var ob;
// 进一步过滤一些已经被observe的value
// 是自身的属性而不是委托而来的__ob__属性,判定__ob__对象是否是Observer的实例
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
/* 最后把控综合作用下的value
shouldObserve用来代码内部来判定是否可被侦测的时机
isServerRendering()是否为服务端渲染,暂不考虑服务端渲染
Array.isArray判断value是一个数组或者isPlainObject判断是一个普通的对象
Object.isExtensible确保是一个可拓展的对象,不然新属性的增加会被忽略
value._isVue确保不是一个Vue实例
*/
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 层层筛查下调用Observer类,生成ob实例
ob = new Observer(value);
}
return ob
}
Observer类生成ob实例,且是数组和对象不同响应式策略的分水岭。源码如下:
var Observer = function Observer (value) {
// this.value = value; ob对包含对侦测value的引用
// this.dep = new Dep(); ob中存一个dep对依赖的管理
this.value = value;
this.dep = new Dep();
// 在value上打上__ob__标签,表明已经被ob过了
// 通过Object.defineProperty添加引用ob,且不可枚举
def(value, '__ob__', this);
/*
数组和对象分别处理的分水岭,这里为什么会有分水岭?
defineProperty仅作用于对象属性的更改,不包括增删。
而没有监听数组的方式,怎么办呢?数组存在七个方法可以改变自身的,
那么可以通过代理这七个方法来监听数组的变化。
因此存在一些问题:
数组某个值的改变是无法监听到的,数组中的长度增减是无法监听到的,
通过索引的方式改变某个值无法监听
针对对象,对象属性值的新增和删除是无法监听到的
针对监听的不足,Vue通过两个全局的Api set和del来弥补
*/
if (Array.isArray(value)) {
/*
数组方法拦截判断是否存在隐式原型即__prototype__,
如果存在的话,直接将value的__prototype__指向arrayMethods
function protoAugment (target, src) { target.__proto__ = src;}
如果不存在,就在value上新增这七个代理方法并使之无法被枚举出来
function copyAugment (target, src, keys) {
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i];
def(target, key, src[key]);
}
}
*/
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
/*
用observeArray对每一个数组的元素进行侦听
function observeArray (items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};
*/
this.observeArray(value);
} else {
/*
对象的话,用walk方法对每一个属性进行侦听,
function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
};
*/
this.walk(value);
}
};
数组方法如何拦截的细节是我们现在关注的重点,源代码如下:
/*
var arrayMethods = Object.create(arrayProto);
派生一个对象,隐式原型链委托到Aarry.prototype
也就是arrayMethods.push()方法会调用Aarry.prototype.push()
*/
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
// 数组原生api中能改变自身的七种方法
var methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse'];
var arrayKeys = Object.getOwnPropertyNames(arrayMethods);
methodsToPatch.forEach(function (method) {
// 用来缓存原始的数组方法,即Aarry.prototype上的方法
var original = arrayProto[method];
/*
然后arrayMethods覆盖原先的七种方法,并且重写成mutator,
来执行额外操作ob.dep.notify();通知依赖更新
*/
def(arrayMethods, method, function mutator () {
var args = [], len = arguments.length;
// 浅复制,参数不会隔离,目的单纯的就是将一个类数组对象进行数组化
while ( len-- ) args[ len ] = arguments[ len ];
// 先执行原生操作
var result = original.apply(this, args);
var ob = this.__ob__;
var inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break
case 'splice':
inserted = args.slice(2);
break
}
// 对于新增的元素,继续ob
if (inserted) { ob.observeArray(inserted); }
ob.dep.notify();
// 返回执行原生的结果,拦截完成
return result
});
});
Set和Del是两个全局api,存在的目的是弥补defineProperty响应式的不足,删减的源码如下:
function set (target, key, val) {
// 如果是数组,则用已经被拦截的splice的方式来增加属性
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val
}
// 检测自有属性,且属性已存在的情况直接返回原值
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val
}
// target可能是一个表达式,确保其优先级
var ob = (target).__ob__;
// 排除非响应式系统中的数据
if (!ob) {
target[key] = val;
return val
}
// 最后遴选出已经被ob的对象新增属性,加入到响应式中,并且通知依赖更新
defineReactive(ob.value, key, val);
ob.dep.notify();
return val
}
function del (target, key) {
// 依旧是splice方法
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1);
return
}
var ob = (target).__ob__;
// 检测非自有属性
if (!hasOwn(target, key)) { return }
delete target[key];
// 检测非ob的对象
if (!ob) { return }
ob.dep.notify();
}
最后看一下核心的方法defineReactive,也是整个响应式系统侦听数据的核心,源码如下:
// 参数说明:响应式的对象,key和值
function defineReactive$$1 (obj, key, val) {
/*
为每一个属性实例化一个dep用来管理依赖
和Observer对象里new Dep()不同的是那是对于对象本身而不是其属性,例如:
data: {
a: {
b:'c'
}
}
a赋值成'd',是由Observer中的dep管理,而a.b赋值成'd'是这里的dep管理依赖
说白了就是一个深度依赖,我们在外部是看不到属性值是原始值的dep,
它们被困在函数的闭包里,也就是下面的setter/getter
*/
var dep = new Dep();
// 检查对象中某个属性是否可配置
// 不可配置就是没有get和set方法直接return
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) { return }
/*
对于get和set的检查,目的是正确的取属性值,有的对象属性可能已经被configurable过,
存在setter或者getter方法或者两个都存在,那么就会对val进行一个判定获取的规则
如果没有设置getter方法,又没有传入val那么只能从obj[key]获取,
问题是没有get方法这样能获取到吗?
没有设置get方法,默认返回undefined,
也就是configurable过后没有设置get方法就是undefined,就是undefined
如果设置了get方法,没有设置set方法,那么val的值暂时不能获取到,特殊情况是:
插入以下代码到此处
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get:()=>({a:'b'}),
})
此时val为undefined,childOb为undefined,
这就存在疑惑,这样不能进行深度依赖!
Why?为什么这样?
没有设置set方法,说明这个属性只能读,对于只能读的属性,主观上只读,
Vue尊重主观意愿,因此加入响应式没有意义!
需要注意的是:这会导致属性原有的 set 和 get 方法被覆盖,
所以要将属性原有的 setter/getter 缓存
*/
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
/*
如果没有get方法,又没有传入val那么只能从obj[key]获取,
如果值是一个对象那么进行递归处理
注意这里的赋值,是赋给子对象的ob对象,ob对象中存在:
this.value = value;
this.dep = new Dep();
这里已经形成了层层被侦听,以及每一层都存在ob.dep存依赖,
有点抽象举个简单的例子:
data: {
a:{
e:234
b:{
c:{
d:123
}
}
}
}
data的整个对象的ob中的dep的id为0,a属性ob中的dep的id为1,
a属性对象(a属性的值为对象或者数组)ob中的dep的id为2,依次
e属性3,b属性4,b对象5,c属性6,c对象7,d属性8
a属性和a属性对象有说明区别吗?
this.a = {},这时候对a属性重新赋值,可以检测到这样的更新
但是当this.a.e = {},显然是无法检测到重新赋值的更新,
因此还需要一个a对象:
{
e:234,
b:{
...
}
__ob__: {...}
}
*/
var childOb = observe(val);
/*
将对象obj中的每一个属性重新配置成get/set
dep.depend();在get中收集依赖
dep.notify();在set中通知依赖更新
*/
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 取值来说如果已经存在get,因为存在已经被配置的情况,
// 那么直接调用get函数来获取属性值
var value = getter ? getter.call(obj) : val;
/*
Dep紧接下文会说到
Dep.target是一个依赖,也就是watcher
dep.depend();是收集这个当前的依赖,即Dep.target的指向
如果存在属性对象的ob对象
childOb.dep.depend();那么继续收集依赖到属性对象的ob.dep上
如果属性值对象是一个数组,
那么递归的逐个收集数组中每个元素的依赖
function dependArray (value) {
for (var e = (void 0),
i = 0, l = value.length; i < l; i++) {
e = value[i];
e && e.__ob__ && e.__ob__.dep.depend();
if (Array.isArray(e)) {
dependArray(e);
}
}
}
*/
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;
// 比较新旧值是否相等, 后者是考虑NaN情况
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
// 只读属性重新定义set不会加入响应式
if (getter && !setter) { return }
// 没有get,仅仅只有set和没有get和没有set两种情况都是返回newVal
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
// 给对象值添加侦听
childOb = observe(newVal);
dep.notify();
}
});
}
Watcher对应总览图的紫色的部分,也叫依赖,它是Watcher实现的,什么是依赖?用到上述响应式数据的地方就叫依赖,比如,视图用到了数据,我们称之为视图依赖,也叫渲染依赖,computed中用到了数据,我们称之为计算依赖,也叫惰性依赖,watch中用到的数据,称之为侦听器依赖。惰性依赖的源码如下:
// computed计算属性是一个依赖
// 在实例上用_computedWatchers和_watcher进行区分
function initComputed (vm, computed) {
var watchers = vm._computedWatchers = Object.create(null);
for (var key in computed) {
var userDef = computed[key];
var getter = typeof userDef === 'function' ? userDef : userDef.get;
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
{ lazy: true }
);
}
}
侦听器依赖的源码如下:
// 可以根据不同的配置和形式来生成侦听器依赖,用法可以看官方文档api
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);
}
}
}
function createWatcher (
vm,
expOrFn,
handler,
options
) { return vm.$watch(expOrFn, handler, options) }
Vue.prototype.$watch = function (
expOrFn,
cb,
options
) {
var vm = this;
if (isPlainObject(cb)) {
// 递归生成侦听器依赖
return createWatcher(vm, expOrFn, cb, options)
}
var watcher = new Watcher(vm, expOrFn, cb, options);
return function unwatchFn () {
watcher.teardown();
}
};
最后是视图依赖,源码如下:
// 在beforeMount之后,就是生成初始化实例即将进行挂载
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
它们都是来自于Watcher类:
var Watcher = function Watcher (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
this.vm = vm;
// 视图依赖的标示
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
// 传入依赖的配置
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.lazy = !!options.lazy;
this.sync = !!options.sync;
this.before = options.before;
} else {
this.deep = this.user = this.lazy = this.sync = false;
}
this.cb = cb;
this.id = ++uid$2;
this.active = true;
this.dirty = this.lazy; // 惰性依赖
// 防止依赖收集的细节,暂时不需要关注
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = expOrFn.toString();
this.getter = expOrFn;
// 当是惰性依赖的时候,并不执行get()方法
this.value = this.lazy
? undefined
: this.get();
};
再来看看get方法,重点关注是不同的依赖如何收集的,源码如下:
Watcher.prototype.get = function get () {
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value
};
/*
先是计算依赖,并不会执行这个方法,此时watcher.value为undefined,也不会进行依赖收集。
然后是侦听器依赖,会执行this.getter方法,watch内声明的方法会先执行,这里会进行依赖收集,
即内部的数据会首先收集到当前的侦听器依赖。
最后是视图依赖,会调用render函数,render会调用响应式数据中的getter,
其中包括获取计算依赖的值,进行依赖收集。
当某一个响应式系统中的数据变化了,会调用setter方法,通知这个数据下的所有依赖更新
*/
Dep类是总览图中的Data到Watcher的联系,也就是Observe和Watcher的桥梁。在讨论Dep的作用时,先考虑下Dep.target的作用,如下源码所示:
// Dep.target是一个watcher,也就是所说的依赖,
// Dep.target 保持唯一性是因为同一时间只会有一个 watcher 被计算,
// Dep.target 就表示正在计算的 watcher
// targetStack是用来管理当前的依赖
/* 如果没记错的话 targetStack 是 Vue2 中才引入的机制,
而 Vue1 中则是仅靠 Dep.target 来进行依赖收集的。
根据我自己对 Vue1 和 Vue2 差异的理解,
引入 targetStack 的原因在于 Vue2 使用了新的视图更新方式。
具体来说,vue1 视图更新采用的是细粒度绑定的方式,而 vue2 采取的是 virtual DOM 的方式。
举个例子来说可能比较容易理解,对于下面的模版:
div>{{ a }} {{ c }}{{ b }}
Vue1 的处理方式可以简化理解为:watch(for a) -> directive(update {{ a }})watch(for b) -> directive(update {{ b }})watch(for c) -> directive(update {{ c }})
由于是数据到 DOM 操作操作指令的细粒度绑定,所以不论是指令还是 watcher 都是原子化的。
对于上面的模版,在处理完{{ a }}的视图绑定后,
创建新的 vue 实例 my 并且处理{{ b }}的视图绑定,随后继续处理{ c }}的绑定
而在 Vue2 中情况就完全不同,视图被抽象为一个 render 函数,
一个 render 函数只会生成一个 watcher,其处理机制可以简化理解为:
renderRoot () { renderMy ()...}
可以看到在 Vue2 中组件数的结构在视图渲染时就映射为 render 函数的嵌套调用,
有嵌套调用就会有调用栈。当 evaluate root 时,调用到 my 的 render 函数,
此时就需要中断 root 而进行 my 的 evaluate,
当 my 的 evaluate 结束后 root 将会继续进行,这就是 targetStack 的意义。
*/
Dep.target = null;
var targetStack = [];
function pushTarget (target) {
targetStack.push(target);
Dep.target = target;
}
function popTarget () {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
看看Dep类的实现:
// 某一个数据对应一个dep,subs中存着这个数据对应的依赖
var Dep = function Dep () {
this.id = uid++;
this.subs = [];
};
// 加依赖
Dep.prototype.addSub = function addSub (sub) {
this.subs.push(sub);
};
// 移除依赖
Dep.prototype.removeSub = function removeSub (sub) {
remove(this.subs, sub);
};
// 依赖收集,依赖收集数据,数据收集依赖,双向的
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
// 通知依赖更新
Dep.prototype.notify = function notify () {
// 稳定sub,在update时确保不会变动
var subs = this.subs.slice();
// 暂时不考虑异步
if (!config.async) {
/*
排序是给依赖进行排序,先侦听器依赖,然后按照视图中的顺序进行依赖
这样保证数据的按顺序合理显示
*/
subs.sort(function (a, b) { return a.id - b.id; });
}
// 依赖的逐个更新
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
ComputedWatcher的缓存原理
// 初始化计算属性 _init() => initState() => initComputed() => createComputedGetter
// return是一个函数,是记忆函数常用的操作
function createComputedGetter (key) {
return function computedGetter () {
// 判断实例上存在计算属性依赖
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
/**
依赖上的dirty是懒加载的flag
evaluate只会在lazy watcher执行
Watcher.prototype.evaluate = function evaluate () {
this.value = this.get();
this.dirty = false;
};
该函数调用get(),然后将该flag设为false,
下一次访问的时候就不会执行watcher.evaluate();
而是直接返回watcher.value
看这个Watcher.prototype.get方法
在get中调用的this现在并不是渲染依赖而是计算属性依赖
this.getter.call(vm, vm);就是调用计算属性值函数
*/
if (watcher.dirty) {
watcher.evaluate();
}
// 上面计算属性的target退栈了,这里是视图依赖
// this.cleanupDeps()上面清空,这里需要重新收集
if (Dep.target) {
watcher.depend();
}
return watcher.value
}
}
}
// 没有缓存直接调用,和方法的执行没什么两样了
function createGetterInvoker(fn) {
return function computedGetter () {
return fn.call(this, this)
}
}
走一下流程(完全理解Vue的渲染watcher、computed和user watcher_前端开发博客-CSDN博客):
-
1、首先在render
函数里面会读取this.info
,这个会触发createComputedGetter(key)
中的computedGetter(key)
;
-
2、然后会判断watcher.dirty
,执行watcher.evaluate()
;
-
3、进到watcher.evaluate()
,才真想执行this.get
方法,这时候会执行pushTarget(this)
把当前的computed watcher
push到stack里面去,并且把Dep.target 设置成当前的
computed watcher`;
-
4、然后运行this.getter.call(vm, vm)
相当于运行computed
的info: function() { return this.name + this.age }
,这个方法;
-
5、info
函数里面会读取到this.name
,这时候就会触发数据响应式Object.defineProperty.get
的方法,这里name
会进行依赖收集,把watcer
收集到对应的dep
上面;并且返回name = '张三'
的值,age
收集同理;
-
6、依赖收集完毕之后执行popTarget()
,把当前的computed watcher
从栈清除,返回计算后的值('张三+10'),并且this.dirty = false
;
-
7、watcher.evaluate()
执行完毕之后,就会判断Dep.target
是不是true
,如果有就代表还有渲染watcher
,就执行watcher.depend()
,然后让watcher
里面的deps
都收集渲染watcher
,这就是双向保存的优势。
-
8、此时name
都收集了computed watcher
和 渲染watcher
。那么设置name
的时候都会去更新执行watcher.update()
-
9、如果是computed watcher
的话不会重新执行一遍只会把this.dirty
设置成 true
,如果数据变化的时候再执行watcher.evaluate()
进行info
更新,没有变化的的话this.dirty
就是false
,不会执行info
方法。这就是computed缓存机制。
异步更新策略
这章节是图中Watcher到黄色的过程分析,主要是nextTick的源码分析:
// 回顾下nextTick的官方介绍,主要是要给异步更新策略
/*
可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,
并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,
只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。
然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,
如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
例如,当你设置 vm.someData = 'new value',
该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。
多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,
这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,
避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,
可以在数据变化之后立即使用 Vue.nextTick(callback)。
这样回调函数将在 DOM 更新完成后被调用。例如:
{{message}}
var vm = new Vue({
el: '#example',
data: {
message: '123'
}
})
vm.message = 'new message' // 更改数据
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
vm.$el.textContent === 'new message' // true
})
*/
// 如果是同步的,那么直接run去更新视图
// 否则将依赖推入一个依赖队列queueWatcher
Watcher.prototype.update = function update () {
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
// queueWatcher依赖队列,需要整体理解的一个过程
// 为了弄懂每一个标志的作用,需要整体的看一下
var queue = [];
var activatedChildren = [];
var has = {};
var circular = {}; // 阻止循环更新依赖
var waiting = false;
var flushing = false;
var index = 0;
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);
}
}
}
/*
当进来了第一个依赖,
has[id] = true,queue里push了这个依赖,waiting为true
如果是同步的那么直接执行flushSchedulerQueue
否则进入了nextTick
首先看下同步更新策略的情况,即执行flushSchedulerQueue
记录当前刷新队列的时间戳
flushing标志位为true,说明队列正在刷新
当第二个依赖进来,
因为是同步的原因已经执行完了resetSchedulerState()重置掉了flushing为false
一个接着一个
看下异步更新策略也就是nextTick,只有等下一次事件循环才会调用flushSchedulerQueue
当第二个依赖进来,入队列
当第三个依赖进来,依旧入队列
等到下一次事件循环才会执行flushSchedulerQueue
所以很好理解has,flushing,waiting的作用
has是用来防止重复的依赖入队列
flushing是是否正在调用flushSchedulerQueue,因为flushSchedulerQueue可能是一个异步
waiting是等待下一个nextTick
flushSchedulerQueue是一个异步执行的话,继续可以执行queueWatcher()
动态的将queue排序,确保按顺序更新
这里的splice是神来之笔,处理一些边界情况,如依赖的id小与当前更新的依赖id那么需要立即插入
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
*/
function flushSchedulerQueue () {
currentFlushTimestamp = getNow();
flushing = true;
//...
resetSchedulerState();
}
// nextTick
var callbacks = [];
var pending = false;
function nextTick (cb, ctx) {
var _resolve;
// 将cb推出队列
callbacks.push(function () {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
// 和waiting的作用一样
if (!pending) {
pending = true;
timerFunc();
}
// 没cb的边界情况
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
/*
这里把注释贴出来看看,第一段注释主要讲述使用宏任务还是微任务的抉择
这段代码的主要作用其实是兼容性的尝试,
大多数情况下优选依旧是Promise
因此核心是:
p.then(flushCallbacks);
*/
// 这里我们有使用微任务的异步延迟包装器。
// 在 2.5 中,我们使用了(宏)任务(结合微任务)。
// 但是,在重绘之前更改状态时会出现一些微妙的问题
//(例如#6813,出入转换)。
// 此外,在事件处理程序中使用(宏)任务会导致一些奇怪的行为
// 无法绕过的(例如#7109、#7153、#7546、#7834、#8109)。
// 所以我们现在再次使用微任务。
// 这种权衡的一个主要缺点是有一些场景
// 微任务的优先级太高,并且应该在两者之间触发
// 顺序事件(例如#4521、#6690,有变通方法)
// 甚至在同一事件的冒泡之间(#6566)。
var timerFunc;
// nextTick 行为利用了可以访问的微任务队列
// 通过原生 Promise.then 或 MutationObserver。
// MutationObserver 有更广泛的支持,但它被严重窃听
// 当在触摸事件处理程序中触发时,iOS 中的 UIWebView >= 9.3.3。 它
// 触发几次后完全停止工作......所以,如果是原生的
// Promise 可用,我们将使用它:
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
timerFunc = function () {
p.then(flushCallbacks);
if (isIOS) { setTimeout(noop); }
};
isUsingMicroTask = true;
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
var counter = 1;
var observer = new MutationObserver(flushCallbacks);
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true
});
timerFunc = function () {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
isUsingMicroTask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = function () {
setImmediate(flushCallbacks);
};
} else {
timerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}
// flushCallbacks
function flushCallbacks () {
// 更换pending的状态
pending = false;
var copies = callbacks.slice(0);
// 浅复制后清空callbacks
callbacks.length = 0;
// 还是异步调用了flushSchedulerQueue
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
// flushSchedulerQueue
// watcher.run();没有给watcher进行run一下
function flushSchedulerQueue () {
currentFlushTimestamp = getNow();
flushing = true;
var watcher, id;
// 在刷新前对队列进行排序。
// 这确保:
// 1. 组件从父级更新为子级。 (因为父母总是
// 在孩子之前创建)
// 2. 组件的用户观察者在其渲染观察者之前运行(因为
// 在渲染观察者之前创建用户观察者)
// 3. 如果一个组件在父组件的观察者运行期间被销毁,
// 可以跳过它的观察者。
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();
// 在开发构建中,检查并停止循环更新。
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
}
}
}
resetSchedulerState();
}
// 重置状态
function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0;
has = {};
{
circular = {};
}
waiting = flushing = false;
}
后续编译和渲染
后续的编译和渲染实在懒得写,意义也不大,也脱离了本文的主题,直接简述下吧,加深下整体的认识。
编译
平时使用模板时,可以在模板中使用变量、表达式或者指令等,这些语法在html中是不存在的,那vue中为什么可以实现?这就归功于模板编译功能。
模板编译的作用是生成渲染函数,通过执行渲染函数生成最新的vnode,最后根据vnode进行渲染。那么,如何将模板编译成渲染函数?此过程可以分成两个步骤:先将模板解析成AST(abstract syntax tree,抽象语法树),然后使用AST生成渲染函数。
由于静态节点不需要总是重新渲染,所以生成AST之后,生成渲染函数之前这个阶段,需要做一个优化操作:遍历一遍AST,给所有静态节点做一个标记,这样在虚拟DOM中更新节点时,如果发现这个节点有这个标记,就不会重新渲染它。所以,在大体逻辑上,模板编译分三部分内容:
- 将模板解析成AST
- 遍历AST标记静态节点
- 使用AST生成渲染函数
这三部分内容在模板编译中分别抽象出三个模块实现各自的功能:解析器、优化器和代码生成器。
渲染
还是通过一个例子来看渲染的过程。假设给定如下模板:
{{count}}
通过上述的编译过程,得到渲染函数:
function render() {
with(this){
return _c('div', {attrs:{"id":"app"}, on:{"click":add}}, [_v(_s(count))])
}
}
_c、_v、_s是生成不同的虚拟节点vnode,vnode 通过 parent 和 children 连接父节点和子节点,组成vnode树。有了vnode后,vue还需要根据vnode来创建DOM节点。如果是首次渲染,那么vue会走创建的逻辑。如果是数据的更新导致的重新渲染,那么vue会走更新的逻辑。
如果不是首次渲染,而是由数据变化所触发的重新渲染,那么vue会最大限度地复用已创建的DOM元素。而复用的前提就是通过比较新老vnode,找出需要更新的内容,然后最小限度地进行替换。这也是vue设计vnode的核心用途。
大量的DOM操作会极损耗浏览器性能。vue在每次数据发生变化后,都会重新生成vnode节点。通过比较新老vnode节点,找出需要进行操作的最小DOM元素子集。根据变化点,进行DOM元素属性、DOM子节点的更新。这种设计方式大大减少了DOM操作的次数。