Vue的重头戏:响应式原理
ES5:Object.defineProperty,不兼容IE8及以下浏览器。
Object.defineProperty(obj, prop, descriptor)
function initProps(vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
// 定义一个 keys,去缓存 props 中的每个 key 属性,为了性能优化
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
// 遍历 props 对象
for (const key in propsOptions) {
// 将每一个 key 添加到 keys 中缓存
keys.push(key)
// 获取每一个 prop 的默认值
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
// 主要就是把 props 变成响应式的
defineReactive(props, key, value)
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
// 对 props 做了 proxy 处理,这样一来,访问 this.xxx 时实际上就相当于访问了this._props.xxx
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
首先通过defineReactive把每个props变为响应式,然后用proxy代理到vm上(目的是可以通过this.xxx直接访问)。
// 设置代理,将 key 代理到 target 上
// 例如:对于 data 来讲,target 是 vm,sourceKey 是 data 本身 _data,key 就是 data 的每一个 key
// 这样做的好处就是访问 this.xxx 的时候可以直接访问到 this[data].xxx
export function proxy(target: Object, sourceKey: string, key: string) {
// target: vm sourceKey: _data key: key
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key] // vm['_data'].key
}
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val
}
// 这实际就是把 data 或者 props 等里面的 key 全部挂载到 vm 上
Object.defineProperty(target, key, sharedPropertyDefinition)
}
处理data:
function initData(vm: Component) {
let data = vm.$options.data
// 判断 data 是函数还是对象,data 在根实例上是对象,在组件实例上是function
// 是函数,调用 getData 将 data 转换为对象
// 并把 vm.$options.data 挂到 vm._data 上
data = vm._data = typeof data === 'function' ?
getData(data, vm) :
data || {}
// 处理过的 data 不是 object 类型,就报警告
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
// 循环
while (i--) {
const key = keys[i]
// 循环做一个对比,data 里面定义的属性名不能跟 props 与 method 中的一样
if (process.env.NODE_ENV !== 'production') {
// data 的 key 不能跟 method 中的一样
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
// data 的 key 不能跟 props 中的一样
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
// 对 vm 下的 key 逐个代理
// 对 data 做了 proxy 处理,这样一来,访问 this.xxx 时实际上就相当于访问了this._data.xxx
proxy(vm, `_data`, key)
}
}
// 响应式数据的处理
// observe data
observe(data, true /* asRootData */ )
}
data对象上的属性不能和props、methods对象上的属性相同,把data代理到vm上,通过observe每个属性转换为响应式。
// 为对象创建一个观察者实例
// 如果该对象已经被观察,那么返回已有的观察者实例,否则创建新的观察者实例
export function observe(value: any, asRootData: ? boolean): Observer | void {
// 必须是 object 类型,还有不能是 VNode
// 也就是说非对象、VNode类型都不做响应式处理
if (!isObject(value) || value instanceof VNode) {
return;
}
let ob: Observer | void;
if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
// 如果 value 对象存在观察者实例 __ob__ ,表示已经被观察,直接返回观察者实例 __ob__
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 通过 new Observer 创建观察者实例
// new Observer 的时候会执行 Observer 类的构造函数 constructor
// Observer 构造函数里面会执行 Observer.walk 调用 defineReactive 执行 Object.defineProperty 进行数据劫持
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob;
}
Obsever类用于给对象属性添加getter和setter,用于依赖收集和派发更新。
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor(value: any) {
this.value = value;
// 实例化一个 Dep
this.dep = new Dep();
this.vmCount = 0;
// 在 value 对象上设置 __ob__ 属性,本质是Object.defineProperty
// 代表当前 value 已经存在观察者实例,已经被观察
def(value, "__ob__", this);
if (Array.isArray(value)) {
// 如果是数组,对数组的处理
// 当支持 __proto__ 时,执行 protoAugment 会将当前数组的原型指向新的数组类 arrayMethods,
// 如果不支持 __proto__,则通过 copyAugment 代理设置,在访问数组方法时代理访问新数组 arrayMethods 中的数组方法
// 通过上面两步,接下来在实例内部调用 push, unshift 等数组的方法时,会执行 arrayMethods 类的方法
// 这也是数组进行依赖收集和派发更新的前提
if (hasProto) { // export const hasProto = '__proto__' in {}
// hasProto 用来判断当前环境下是否支持 __proto__ 属性
// protoAugment 是通过原型指向的方式,将数组指定的七个方法指向 arrayMethods
protoAugment(value, arrayMethods);
} else {
// copyAugment 通过数据代理的方式, 将数组指定的七个方法指向 arrayMethods
copyAugment(value, arrayMethods, arrayKeys);
}
// 调用 this.observeArray 遍历数组,为数组的每一项设置观察,处理数组元素为对象的情况
this.observeArray(value);
} else {
// 如果是对象
this.walk(value);
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
// 调用 defineReactive 实现 Object.defineProperty 对 value 进行劫持
walk(obj: Object) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
/**
* Observe a list of Array items.
*/
observeArray(items: Array < any > ) {
// 遍历数组,对里面的的每一个元素进行观察
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
}
}
在对数组的处理中,重写了push、pop、shift、unshift、splice、sort、reverse七种方法,重写方法在实现时除了将数组方法名对应的原始方法调用一遍并将执行结果返回外,还通过执行ob.dep.notify()将当前数组的变更通知给其订阅者。当支持 __proto__ 时,执行 protoAugment 会将当前数组的原型指向新的数组类 arrayMethods,如果不支持 __proto__,则通过 copyAugment 代理设置,在访问数组方法时代理访问新数组 arrayMethods 中的数组方法。
defineReactive的作用是定义响应式对象,给对象动态添加getter和setter。
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter ? : ? Function,
shallow ? : boolean
) {
// 创建一个 dep 实例
const dep = new Dep();
// obj[key] 的属性描述符,发现它是不可配置对象的话直接 return
// js 对象属性 configurable = false 表示不可通过 delete 删除
const property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return;
}
// 保存记录 getter 和 setter,获取值 val
const getter = property && property.get;
const setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
// 当 val 即 obj[key] 的值为对象的情况,递归调用 observe,保证对象中的所有 key 都被观察
let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// get 拦截 obj[key] 读取操作,做依赖收集
get: function reactiveGetter() {
// 先获取值,如果已经收集过依赖,那么就不需要再重复收集,在最后直接返回即可
const value = getter ? getter.call(obj) : val;
// Dep.target 是 Dep 的一个静态属性,值是 watcher,在 new Watcher 的时候设置
// 在 new Watcher 时会执行回调函数 updateComponent
// 回调函数中如果有 vm.key 的读取行为,会触发这里进行读取拦截,收集依赖
// 回调函数执行完以后又会将 Dep.target 设置为 null,避免这里重复收集依赖
// 也就是说,data 只有在首次渲染的时候才会去收集依赖 watcher
if (Dep.target) {
// 依赖收集,在 dep 中添加 watcher
// 一个组件一个 watcher,如果用户手动创建 watcher 比如 watch 选 this.$watch
dep.depend();
if (childOb) {
// 对象中嵌套对象的观察者对象,如果存在也对其进行依赖收集
childOb.dep.depend();
if (Array.isArray(value)) {
// 如果数组元素是数组或者对象,递归去为内部的元素收集相关的依赖
dependArray(value);
}
}
}
return value;
},
// 主要做派发更新
set: function reactiveSetter(newVal) {
// 先获取旧的值
const value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
// 如果新值和旧值一样时,return,不会触发响应式更新
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== "production" && customSetter) {
customSetter();
}
// setter 不存在说明该属性是一个只读属性,直接 return
if (getter && !setter) return;
// 设置新值
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
// 对新值进行观察,让新值也是响应式的
childOb = !shallow && observe(newVal);
// 依赖派发,通知更新
dep.notify();
},
});
}
用Dep管理Watcher:
// 依赖收集
export default class Dep {
// Dep.targte 就是一个 watcher
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
// subs 存储 watcher 的
this.subs = []
}
// 在 dep 中添加 watcher
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 将 dep 添加进 watcher
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 派发更新
// 通知 dep 中的所有 watcher,执行 watcher.update() 方法
// watcher.update 中实际上就是调用 updateComponent 对页面进行重新渲染
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !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((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
静态属性target是全局唯一的Watcher。
Watcher的实现:
//一个组件一个 watcher(渲染 watcher)或者一个表达式一个 watcher(用户watcher)
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
// options
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; // uid for batching
this.active = true;
// 创建 计算watcher 实例的时候,先把 this.dirty 置为 true
// 这个 dirty 就是 computed 缓存的关键,dirty=true,代表需要重新计算
this.dirty = this.lazy; // for lazy watchers
this.deps = [];
this.newDeps = [];
this.depIds = new Set();
this.newDepIds = new Set();
this.expression =
process.env.NODE_ENV !== "production" ? expOrFn.toString() : "";
// expOrFn: 主要看 new Watcher 的时候传进来什么,不同场景会有区别
// 1、如果是渲染 watcher(处理 data),就是 new Watcher 传进来的 updateComponent
// 2、如果是用户 watcher(处理 watch),就是 watch:{ msg: function() {} }】 的msg 函数
// 3、如果是计算 watcher(处理 computed),就是【computed:{ getName: function() {} }】中的 getName 函数
// 将 expOrFn 赋值给 this.getter
if (typeof expOrFn === "function") {
// 如果 expOrFn 是一个函数,比如 渲染watcher 的情况,是 updateComponent 函数
this.getter = expOrFn;
} else {
// 不是函数,比如 用户watcher 的情况,是 watch 的 key
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
process.env.NODE_ENV !== "production" &&
warn(
`Failed watching path: "${expOrFn}" ` +
"Watcher only accepts simple dot-delimited paths. " +
"For full control, use a function instead.",
vm
);
}
}
// 如果是 lazy 代表的是 computed
// 不是 computed,执行 this.get()
this.value = this.lazy ? undefined : this.get();
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get() {
// 将 watcher 添加到 Dep.target
pushTarget(this);
let value;
const vm = this.vm;
try {
// 执行 this.getter
// 上面已经分析过,this.getter 会根据不同的 watcher 会不一样
// 1、渲染 watcher:this.getter 是 updateComponent 函数
// 2、用户 watcher:就是 watch:{ msg: function() {} }】 的 msg 函数
// 3、如果是计算 watcher(处理 computed),就是【computed:{ getName: function() {} }】中的 getName 函数
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`);
} else {
throw e;
}
} finally {
// 递归访问value
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value;
}
/**
* Add a dependency to this directive.
*/
// 将 dep 放到自己(watcher)上
// 将自己(watcher)添加到 dep 的 subs 数组
addDep(dep: Dep) {
const id = dep.id;
if (!this.newDepIds.has(id)) {
// newDepIds是具有唯一成员是Set数据结构,newDeps是数组
// 他们用来记录当前 watcher 所拥有的数据,这一过程会进行逻辑判断,避免同一数据添加多次
this.newDepIds.add(id);
// 将 dep 添加进 watcher.newDeps 中
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
// 调用 dep.addSub 将 watcher 添加进 dep
dep.addSub(this);
}
}
}
/**
* Clean up for dependency collection.
*/
cleanupDeps() {
let i = this.deps.length;
while (i--) {
const dep = this.deps[i];
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this);
}
}
let tmp = this.depIds;
this.depIds = this.newDepIds;
this.newDepIds = tmp;
this.newDepIds.clear();
tmp = this.deps;
this.deps = this.newDeps;
this.newDeps = tmp;
this.newDeps.length = 0;
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
// 根据 watcher 配置项,决定接下来怎么走,一般是 queueWatcher
// 如果是 计算watcher,那么就是将 lazy 标记为 true,代表有脏数据,需要重新计算
update() {
/* istanbul ignore else */
// lazy 为 true 代表是 computed
if (this.lazy) {
// 如果是 计算watcher,则将 dirty 置为 true
// 当页面渲染对计算属性取值时,触发 computed 的读取拦截 getter
// 然后执行 watcher.evaluate 重新计算取值
this.dirty = true;
} else if (this.sync) {
// 是否是同步 watcher
// 同步执行,在使用 vm.$watch 或者 watch 选项时可以传一个 sync 选项
// sync 为 true 数据更新时该 watcher 就不走异步更新队列,直接执行 this.run 方法进行更新
this.run();
} else {
// 把需要更新的 watcher 往一个队列里面推
// 更新时一般都进到这里
queueWatcher(this);
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
/**
* watcher.run 被刷新队列函数 flushSchedulerQueue 调用,完成如下几件事:
* 1、执行实例化 watcher 传递的第二个参数,updateComponent 或者 获取 this.xx 的一个函数(parsePath 返回的函数)
* 2、更新旧值为新值
* 3、执行实例化 watcher 时传递的第三个参数,比如用户 watcher 的回调函数,或者渲染 watcher 的空函数
*/
run() {
if (this.active) {
// 首先就执行 watcher.get,watcher.get 中执行 this.getter 得到 value
const value = this.get();
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep // 深度监听
) {
// set new value
const oldValue = this.value;
this.value = value;
if (this.user) {
// 如果是用户 watcher
try {
// 执行 handler 回调
this.cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(
e,
this.vm,
`callback for watcher "${this.expression}"`
);
}
} else {
// 如果是渲染 watcher,第三个参数是一个空函数 this.cb = noop
this.cb.call(this.vm, value, oldValue);
}
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
// 主要是 computed 的
evaluate() {
this.value = this.get();
// computed 标记为已经执行过更新
this.dirty = false;
}
/**
* Depend on all deps collected by this watcher.
*/
// computed 中调用这个进行依赖收集
depend() {
let i = this.deps.length;
while (i--) {
this.deps[i].depend();
}
}
/**
* Remove self from all dependencies' subscriber list.
*/
teardown() {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this);
}
let i = this.deps.length;
while (i--) {
this.deps[i].removeSub(this);
}
this.active = false;
}
}
}
依赖收集
Vue在mount阶段会执行updateComponent函数,会去实例化一个渲染watcher,在构造函数中执行get函数,将Dep.target赋值为当前watcher并且加入栈中。
在数据触发getter时调用dep.depend方法,即执行Dep.target.addDep函数,把当前watcher订阅到数据劫持的dep的subs,数据变化时通知subs。
addDep(dep: Dep) {
const id = dep.id;
if (!this.newDepIds.has(id)) {
// newDepIds是具有唯一成员是Set数据结构,newDeps是数组
// 他们用来记录当前 watcher 所拥有的数据,这一过程会进行逻辑判断,避免同一数据添加多次
this.newDepIds.add(id);
// 将 dep 添加进 watcher.newDeps 中
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
// 调用 dep.addSub 将 watcher 添加进 dep
dep.addSub(this);
}
}
}
依赖收集完后把Dep.target恢复称上一个状态,然后cleanupDeps清除旧的订阅(避免浪费?)。
派发更新
setter会调用dep.notify方法,通知 dep 中的所有 watcher,执行 watcher.update() 方法,根据不同状态执行不同逻辑,一般是 queueWatcher 如果是 计算watcher lazy为true,那么就将 dirty 标记为 true,代表有脏数据,需要时重新计算。
// 将 watcher 放进 watcher 队列 queue 中
export function queueWatcher(watcher: Watcher) {
const id = watcher.id;
// 如果 watcher 已经存在,则会跳过,不会重复
// 避免一个数据改变多次重复操作 DOM 带来的性能问题
if (has[id] == null) {
// 缓存 watcher id,主要用来判断 watcher 有没有重复入队
has[id] = true;
if (!flushing) {
// 如果没有处于刷新队列状态,直接入队
queue.push(watcher);
} else {
// 已经在刷新队列了
// 从队列末尾开始倒序遍历,根据当前 watcher.id 找到它大于的 watcher.id 的位置,然后将自己插入到该位置之后的下一个位置
// 即将当前 watcher 放入已排序的队列中,且队列仍是有序的
let 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 (process.env.NODE_ENV !== "production" && !config.async) {
// 如果是同步执行,直接刷新调度队列
// Vue 默认是异步执行,一般是不会同步执行,如果改为同步执行,性能将会受到很大影响
flushSchedulerQueue();
return;
}
// nextTick 函数,vm.$nextTick、Vue.nextTick
// 1、接收一个回调函数 flushSchedulerQueue,并将 flushSchedulerQueue 放入 callbacks 数组
// 2、通过 pending 控制向浏览器任务队列中添加 flushCallbacks 函数
// 3、通过事件循环的微任务、宏任务实现异步更新
nextTick(flushSchedulerQueue);
}
}
}
flushSchedulerQueue函数的定义:
function flushSchedulerQueue() {
currentFlushTimestamp = getNow();
flushing = true; // 将 flushing 置为 true,代表正在刷新队列
let watcher, id;
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
// 刷新前先对队列进行排序,保证了:
// 1、组件的更新顺序为从父级到子级,因为父组件总是在子组件之前被创建
// 2、一个组件的用户 watcher 在其渲染 watcher 之前被执行,因为用户 watcher 先于渲染 watcher 创建
// 3、如果一个组件在其父组件的 watcher 执行期间被销毁,则它的 watcher 可以被跳过
queue.sort((a, b) => a.id - b.id);
// do not cache length because more watchers might be pushed
// as we run existing watchers
// 使用 queue.length,动态计算队列的长度,没有缓存长度
// 是因为在执行现有 watcher 期间队列中可能会被 push 进新的 watcher
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
// 如果 watcher 中存在 before,执行 before 钩子
// new Watcher(vm, updateComponent, noop, {
// before () {
// if (vm._isMounted && !vm._isDestroyed) {
// callHook(vm, 'beforeUpdate')
// }
// }
// }, true /* isRenderWatcher */)
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null;
// 执行 watcher 的 run 去执行相应的更新函数进行页面更新
// watcher.run 实际上也就是调用 updateComponent 进到页面挂载
watcher.run();
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== "production" && 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
const activatedQueue = activatedChildren.slice();
const updatedQueue = queue.slice();
// 重置,将 flushing 置为 false,把watcher队列清空
resetSchedulerState();
// call component updated and activated hooks
// 触发 activated
callActivatedHooks(activatedQueue);
// 触发 update 生命周期
callUpdatedHooks(updatedQueue);
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit("flush");
}
}
run方法通过get得到当前值,如果满足新旧值不等、新值是对象类型、deep 模式任何一个条件,则执行 watcher 的回调,并把新旧值作为参数传入。因为执行get方法时,会执行getter方法,触发updateComponent,因此会触发组件重新渲染,执行patch过程。
nextTick(flushSchedulerQueue)将flushSchedulerQueue函数传入nextTick,nextTick的执行逻辑。
// cb:回调函数 flushSchedulerQueue
// ctx:上下文
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 将回调函数 cb(flushSchedulerQueue)放进 callbacks 数组中
// 如果是直接通过 Vue.nextTick 或者 vm.$nextTick 调用,cb 就是调用时传的 callback
// this.$nextTick(() => {})
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// 如果 pending 为 false,代表浏览器任务队列为空(即没有 flushCallbacks)
// 如果 pending 为 true,代表浏览器任务队列存在任务
// 在执行 flushCallbacks 的时候会再次将 pending 标记为 false
// 也就是说,pending 在这里的作用就是:保证在同一时刻,浏览器的任务队列中只有一个 flushCallbacks 函数
if (!pending) {
pending = true
// 执行 timerFunc 函数
// timerFunc 函数的主要作用就是:通过微任务或者宏任务的方式往浏览器添加任务队列
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
timerFunc的使用顺序:
// 主要就是将 flushCallbacks 放进浏览器的异步任务队列里面。
// 中间通过降级的方式处理兼容问题,优先使用 Promise,其次是 MutationObserver,然后是 setImmediate,最后才是使用 setTimeout
// 也就是优先微任务处理,微任务不行逐步降级到宏任务处理
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// 如果支持 Promise 则优先使用 Promise
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
// 使用 MutationObserver
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 使用 setImmediate,其实 setImmediate 已经算是宏任务了,但是性能会比 setTimeout 稍微好点
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// setTimeout 是最后的选择
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
检测变化的特殊情况
对于响应式对象(数组和对象)的处理,有些情况不能触发响应式。
- 给对象新增一个属性
- 利用索引直接设置数组项
- 修改数组的长度
对于给对象新增属性和利用索引设置数组项,Vue2通过全局API Vue.set实现(删除由Vue.del实现)。
// 通过 Vue.set 或 this.$set 设置 target[key] = val
export function set(target: Array < any > | Object, key: any, val: any): any {
if (
process.env.NODE_ENV !== "production" &&
(isUndef(target) || isPrimitive(target))
) {
warn(
`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`
);
}
// 如果 target 是数组,利用数组的 splice 变异方法触发响应式
// Vue.set([1,2,3], 1, 5)
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 修改数组的长度, 避免数组索引 key 大于数组长度导致 splcie() 执行有误
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val;
}
// 如果 key 已经存在 target 中,更新 target[key] 的值为 val
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
// 读取一下 target.__ob__,这个主要用来判断 target 是否是响应式对象
const ob = (target: any).__ob__;
// 需要操作的目标对象不能是 Vue 实例或 Vue 实例的根数据对象
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== "production" &&
warn(
"Avoid adding reactive properties to a Vue instance or its root $data " +
"at runtime - declare it upfront in the data option."
);
return val;
}
// 当 target 不是响应式对象,并且对象本身不存在这个新属性 key
// 新属性会被设置,但是不会做响应式处理
if (!ob) {
target[key] = val;
return val;
}
// target 是响应式对象,并且对象本身不存在这个新属性 key
// 给对象定义新属性,通过 defineReactive 方法将新属性设置为响应式
// ob.dep.notify 通知更新
defineReactive(ob.value, key, val);
ob.dep.notify();
return val;
}
根据target是数组或对象分别处理,对象也分为响应式对象和普通对象,以及key值是否为原有key。
修改数组长度可以使用splice函数,涉及到Vue2的数据重写。 重写数组的具体实现:
if (Array.isArray(value)) {
// 如果是数组
// 当支持 __proto__ 时,执行 protoAugment 会将当前数组的原型指向新的数组类 arrayMethods,
// 如果不支持 __proto__,则通过 copyAugment 代理设置,在访问数组方法时代理访问新数组 arrayMethods 中的数组方法
// 通过上面两步,接下来在实例内部调用 push, unshift 等数组的方法时,会执行 arrayMethods 类的方法
// 这也是数组进行依赖收集和派发更新的前提
if (hasProto) { // export const hasProto = '__proto__' in {}
// hasProto 用来判断当前环境下是否支持 __proto__ 属性
// protoAugment 是通过原型指向的方式,将数组指定的七个方法指向 arrayMethods
protoAugment(value, arrayMethods);
} else {
// copyAugment 通过数据代理的方式, 将数组指定的七个方法指向 arrayMethods
copyAugment(value, arrayMethods, arrayKeys);
}
// 调用 this.observeArray 遍历数组,为数组的每一项设置观察,处理数组元素为对象的情况
this.observeArray(value);
}
// 通过更改原型指向的方式
function protoAugment(target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src;
/* eslint-enable no-proto */
}
/**
* Augment a target Object or Array by defining
* hidden properties.
*/
/* istanbul ignore next */
// 通过 Object.defineProperty 代理的方式
function copyAugment(target: Object, src: Object, keys: Array < string > ) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i];
def(target, key, src[key]);
}
}
arrayMethods的定义:
// 对数组的原型进行备份
const arrayProto = Array.prototype
// 通过继承的方式创建新的 arrayMethods
export const arrayMethods = Object.create(arrayProto)
// 当外部访问通过以下7种方法访问数组,会被处理
// 因为这7种方法会改变数组
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
// 对数组那七种方法进行拦截并执行 mutator 函数
methodsToPatch.forEach(function (method) {
// cache original method
// 缓冲原始数组的方法
const original = arrayProto[method]
// 利用 Object.defineProperty 对 arrayMethods 进行拦截
def(arrayMethods, method, function mutator(...args) {
// 先执行数组原生方法,保证了与原生数组方法的执行结果一致
// 例如 push.apply()
const result = original.apply(this, args)
const ob = this.__ob__
// 如果 method 是以下三个之一,说明是新插入了元素
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args // 比如:args 是 [{...}]
break
case 'splice':
inserted = args.slice(2)
break
}
// 对插入的元素进行响应式处理
if (inserted) ob.observeArray(inserted)
// 通过 dep.notify 通知更新
ob.dep.notify()
return result
})
})
总结
Vue2的响应式原理通过Object.defineProperty实现,主要包括依赖收集和派发更新两个过程。依赖收集是在数据getter时把watcher收集到dep的subs,派发更新的时候notify所有的watcher,watcher执行update方法。因为是批量更新,更新时会涉及到nextTick。nextTick根据浏览器兼容性,优先使用 Promise,其次是 MutationObserver,然后是 setImmediate,最后才是使用 setTimeout。
在响应式原理里,需要注意数组和对象的特殊情况,通过Vue.set实现响应式,数组则通过数组重写的方式,支持__proto__的情况下重写arrayMethods作为原型,不支持则遍历,把arrayMethods直接添加到target上。