关于Vue.js
Vue.js是一款MVVM框架,通过响应式在修改数据的时候更新视图。Vue.js的响应式原理依赖于Object.defineProperty
,尤大大在Vue.js文档
中就已经提到过,这也是Vue.js不支持IE8 以及更低版本浏览器的原因。Vue通过设定对象属性的 setter/getter 方法来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图 ,下面来一探究竟。
4.1、Object.defineProperty
Object.defineProperty方法会直接在一个对象上定义一个新的属性,或者修改对象的现有属性并返回这个对象,它的语法如下:
Object.defineProperty(obj, prop, descriptor)
- obj是当前要操作的对象
- props是要定义或修改的属性名称
- descriptor是要被定义或修改的属性的描述符
⽐较核⼼的是 descriptor ,它有很多可选键值,具体的可以去参阅它的⽂档。这⾥我们最关⼼的是
get 和 set , get 是⼀个给属性提供的 getter ⽅法,当我们访问了该属性的时候会触发 getter ⽅
法; set 是⼀个给属性提供的 setter ⽅法,当我们对该属性做修改的时候会触发 setter ⽅法。
⼀旦对象拥有了 getter 和 setter,我们可以简单地把这个对象称为响应式对象。那么 Vue.js 把哪些对象
变成了响应式对象了呢,接下来我们从源码层⾯分析。
4.2、initState初始化数据
我们上面讲过,在 Vue 的初始化阶段, _init ⽅法执⾏的时候,会执⾏ initState(vm) ⽅法,这个⽅法主要是对 props 、 methods 、 data 、 computed 和 wathcer 等属性做了初始化操作。这⾥我们重点分析 props 和 data 。
4.2.1、 initProps
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
//初始化vm_props对象,最终可以通过vn._props访问props数据
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
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) {
keys.push(key)
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 {
//调用defineReactive,把每个prop对应的值变成响应式
defineReactive(props, key, value)
}
if (!(key in vm)) {
//通过proxy将vm._props.xxx代理到vm.xxx上
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
总结:props 的初始化主要过程,就是遍历定义的 props 配置。遍历的过程主要做两件事情:⼀个是调⽤ defineReactive ⽅法把每个 prop 对应的值变成响应式,可以通过 vm._props.xxx 访问到定
义 props 中对应的属性。对于 defineReactive ⽅法,我们稍后会介绍;另⼀个是通过 proxy
把 vm._props.xxx 的访问代理到 vm.xxx 上。
4.2.2、initData
function initData (vm: Component) {
//获取实例上的数据
let data = vm.$options.data
//先判断data是否是一个函数
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
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
//将data的数据和props,methods上的数据进行比较,不能出现重复的定义
//因为他们最终都会挂载到vm上
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]
if (process.env.NODE_ENV !== 'production') {
//如果methods上存在这个键值
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
//如果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)) {
//给数据进行代理
proxy(vm, `_data`, key)
}
}
// observe data
//对数据进行响应式的处理
observe(data, true /* asRootData */)
}
总结:data初始化做了三件事
- 遍历data,检查data中的数据是否和methods,props中定义的数据重复
- 遍历data,将每一个值vm._data.xxx都代理到vm.xxx上
- 调用observer方法观测整个data的变化,使data变成响应式数据
我们看到,无论是props还是data初始化,都是把他们变成响应式对象,在这个过程中我们使用了几个重要的函数函数,下面就是介绍这些函数。
4.2.3、proxy
首先介绍下代理,代理的作用就是吧props和data上的属性代理到vm实例上,这也是为什么我们定义了props可以直接通过this.xxx调用。
const sharedPropertyDefinition = {
enumerable: true,//是否是可枚举的
configurable: true,//是否是可配置的
get: noop,//空函数
set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
//初始化变量的get方法,把target[sourceKey][key]的读取变成了对target[key]的读取
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
//初始化变量的set方法,把target[sourceKey][key]的设置变成了对target[key]的设置
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
4.1.4、observer
observe ⽅法的作⽤就是给⾮ VNode 的对象类型数据添加⼀个 Observer ,如果已经添加过则直接返回,否则在满⾜⼀定条件下去实例化⼀个 Observer 对象实例。接下来我们来看⼀下 Observer
类。
export class Observer {
value: any;
//observer和watcher的纽带,当数据发生变化时会被observer观察到,然后由dep通知watcher去更新视图
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value//被观察到的数据对象
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)//增加一个标志,表示已经被observer观察
// 如果value是数组,就辨遍历数组,对数组中每一项进行observer观察
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)//遍历数组的函数
} else {
//如果是对象,就遍历对象的每一个key,对每个key调用defineReactive获取对key的set和get的控制权
this.walk(value)
}
}
Observer 是⼀个类,它的作⽤是给对象的属性添加 getter 和 setter,⽤于依赖收集和派发更新。在 Observer 的构造函数中,会对 value 做判断,对于数组会调⽤ observeArray ⽅法,否则对纯对象调⽤ walk ⽅法。可以看到 observeArray 是遍历数组再次调⽤ observe ⽅法,⽽walk ⽅法是遍历对象的 key 调⽤ defineReactive ⽅法。
-
observeArray
:遍历数组,对数组的每个元素调用observer -
walk
:遍历对象的每个Key,对对象上的每个key调用defineReactive -
defineReactive
:通过Object.defineProperty 设置对象的key属性,使得我们能够获得该属性的该属性的get/set使用权,一般是由Watcher的实例进行get操作,此时Watcher实例对象会被添加到Dep数组中,在外部操作触发set时,通过Dep通知Watcher进行更新。
4.1.5、defineReactive
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val,
//Dep.target全局变量指向当前正在解析指令的Compile生成的Watcher
if (Dep.target) {
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 */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && 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()//被更新了,通知所有watcher去更新
}
})
}
defineReactive 函数最开始初始化 Dep 对象的实例,接着拿到 obj 的属性描述符,然后对⼦对
象递归调⽤ observe ⽅法,这样就保证了⽆论 obj 的结构多复杂,它的所有⼦属性也能变成响应
式的对象,这样我们访问或修改 obj 中⼀个嵌套较深的属性,也能触发 getter 和 setter。最后利⽤
Object.defineProperty 去给 obj 的属性 key 添加 getter 和 setter
4.3、依赖收集
经过上面的分析我们知道,Vue会把普通对象变成响应式对象,响应式对象的getter就是用来收集依赖的,再看看getter的实现。
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val,
//Dep.target全局变量指向当前正在解析指令的Compile生成的Watcher
if (Dep.target) {
dep.depend()//被读取了,将这个以来收集起来
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
getter函数最重要的一步就是通过调用depend函数进行依赖的收集,depend函数是dep定义的一个函数,稍后会详细介绍。
4.3.1、Dep
Dep是observer和watcher的纽带,当数据发生变化时会被observer观察到,然后由dep通知watcher。
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++//每个dep都有唯一的id
this.subs = []//用于存放依赖
}
//向subs数组添加依赖
addSub (sub: Watcher) {
this.subs.push(sub)
}
//移除依赖
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
//设置Watcher的依赖,这里添加Deo.target目的是判断是不是Watcher的构造函数的调用
//也就是说判断他是Watcher的this.get调用的的而不是普通调用
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
//通知所有绑定的Watcher调用update()进行更新
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()
}
}
}
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
//这是全局唯一的,在任何时候只有一个watcher正在评估
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
// 将当前的watcher推入堆栈中,关于为什么要推入堆栈,主要是要处理模板或render函数中嵌套了多层组件,需要递归处理
targetStack.push(target)
// 设置当前watcher到全局的Dep.target,通过在此处设置,key使得在进行get的时候对当前的订阅者进行依赖收集
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
-
add
:接受参数为Watcher实例,并把Watcher实例记录依赖的数组中 -
depend
:Dep.target存放的是当前需要操作的Watcher实例,调用depend会调用该实例的addDep方法 -
notify
:通知数组中所有Watcher进行更新操作
4.3.2、Watcher
Watcher作为观察者,用来订阅数据变化并执行相应的操作,比如视图的更新。
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean//是否为渲染watcher
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
//当前Watcher添加到vue实例上
vm._watchers.push(this)
// options
//参数配置,默认是false
if (options) {
....
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = 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()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
//将watcher对象的getter设置成uptateComponent
this.getter = expOrFn
} else {
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
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
//在get函数中,主要是收集一些依赖,然后在初始化或者有更新时,调用this.getter(对应着updateComponent函数)
get () {
//将Dep的target添加到targetStack,同时Dep的target赋值为当前watcher
pushTarget(this)
let value
const vm = this.vm
try {
//调用updateComponent方法,之后在updateComponent中接着会调用_update方法更新dom
//这时挂载到vue原型上的方法,而_render方法重新渲染了VNode
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)
}
//update执行完成之后,又将dep.target从targetStack弹出
popTarget()
this.cleanupDeps()
}
return value
}
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
cleanupDeps () {
let i = this.deps.length
......
}
//通知watcher更新
update () {
.......
}
run () {
.......
}
.............
}
-
addDep
:接收参数dep,让当前Watcher订阅dep -
cleanupDeps
:将新的newDepIds(这里保存的是dep的id)和旧的deps去对比,找存在于出旧的deps但不存在于新的newDeplds中的dep,就从这些dep中移除当前的依赖,这样可以有效地避免没必要的updata,稍后在new Watcher()渲染过程分析中我会可以举个简单的例子说明。 -
updata
:立即执行watcher或者将watcher加入队列等待统一flush -
run
:运行watcher,调用this.get()求值,然后触发回调
4.3.3、new Watcher发生了什么
之前我们讲过,在$mount函数中我们主要是通过调用mountComponent()函数去实现我们的数据挂载,而在mountComponent函数中就使用了new Watcher(),在这个过程中我们调用了每个数据的getter函数,这样就实现了每个数据首次依赖的收集。
当我们去实例化⼀个渲染 watcher 的时候,⾸先进⼊ watcher 的构造函数逻辑,然后会执⾏它的
this.get() ⽅法,进⼊ get 函数,⾸先会执⾏
pushTarget(this)
//函数的实现
export function pushTarget (_target: Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
执行这个函数就是把 Dep.target 赋值为当前的渲染 watcher 并压栈(为了恢复⽤)。接着⼜执⾏了
value = this.getter.call(vm, vm)
//this.getter 对应就是 updateComponent 函数,这实际上就是在执⾏:
vm._update(vm._render(), hydrating)
它会先执⾏ vm._render() ⽅法,因为之前分析过这个⽅法会⽣成 渲染 VNode,并且在这个过程中
会对 vm 上的数据访问,这个时候就触发了数据对象的 getter。那么每个对象值的 getter 都持有⼀个 dep ,在触发 getter 的时候会调⽤ dep.depend() ⽅法,也就会执⾏
Dep.target.addDep(this)
刚才我们提到这个时候 Dep.target 已经被赋值为当前在操作的 watcher ,那么就执⾏到 addDep ⽅法:
addDep (dep: Dep) {
const id = dep.id//获取dep的id
//如果新的DepIds数组中没有当前dep,将入组
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
//如果dep没有当前正在操作的Watcher
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
这时候会做⼀些逻辑判断(保证同⼀数据不会被添加多次)后执⾏ dep.addSub(this) ,那么就会执
⾏ this.subs.push(sub) ,也就是说把当前的 watcher 订阅到这个数据持有的 dep 的 subs中。
所以在 vm._render() 过程中,会触发所有数据的 getter,这样实际上已经完成了⼀个依赖收集的过程。那么到这⾥就结束了么,其实并没有,再完成依赖收集后,还有⼏个逻辑要执⾏,⾸先是:
if (this.deep) {
traverse(value)
}
这个是要递归去访问 value ,触发它所有⼦项的 getter ,这个之后会详细讲。接下来执⾏:
popTarget()
//函数实现
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
实际上就是把 Dep.target 恢复成上⼀个状态,因为当前 vm 的数据依赖收集已经完成,那么对应的渲染 Dep.target 也需要改变。最后执⾏this.cleanupDeps()
这里着重讲下this.cleanupDeps()这个函数。
考虑到 Vue 是数据驱动的,所以每次数据变化都会重新render,那么 vm._render() ⽅法⼜会再次执⾏,并再次触发数据的 getters,我们知道每次执行一次render都是一次的性能消耗,那有什么办法能帮助我们去阻止没必要render,所以 Wathcer 在构造函数中会初始化 2 个 Dep 实例数组, newDeps 表⽰新添加的 Dep 实例数组,⽽ deps 表⽰上⼀次添加的 Dep 实例数组。在执⾏ cleanupDeps 函数的时候,会⾸先遍历 deps ,移除对 dep 的订阅,然后把 newDepIds和 depIds 交换, newDeps 和 deps 交换,并把 newDepIds 和 newDeps 清空。那么为什么需要做 deps 订阅的移除呢,在添加 deps 的订阅过程,已经能通过 id 去重避免重复订阅了。
考虑到⼀种场景,我们的模板会根据 v-if 去渲染不同⼦模板 a 和 b,当我们满⾜某种条件的时候渲染 a 的时候,会访问到 a 中的数据,这时候我们对 a 使⽤的数据添加了 getter,做了依赖收集,那么当我们去修改 a 的数据的时候,理应通知到这些订阅者。那么如果我们⼀旦改变了条件渲染了 b 模板,⼜会对 b 使⽤的数据添加了 getter,如果我们没有依赖移除的过程,那么这时候我去修改 a 模板的数据,会通知 a 数据的订阅的回调,这显然是有浪费的。因此 Vue 设计了在每次添加完新的订阅,会移除掉旧的订阅,这样就保证了在我们刚才的场景中,如果渲染 b 模板的时候去修改 a 模板的数据,a 数据订阅回调已经被移除了,所以不会有任何浪费,真的是⾮常赞叹 Vue 对⼀些细节上的处理。
4.4、异步更新DOM策略及nextTick
Vue实现响应式并不是数据变化后DOM立即变化,而是按照一定策略进行DOM更新,在Vue文档中指出Vue是异步执行DOM更新。那Vue为什么要使用异步更新又是怎么实现的?
我们先看看Watcher队列 ,当触发某个数据的setter时,Dep就会通过调用notify去通知所有的依赖watcher.
//通知watcher更新
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
/*同步则执行run直接渲染视图*/
this.run()
} else {
/*异步推送到观察者队列中,下一个tick时调用。*/
queueWatcher(this)
}
}
从源码中我们可以知道Vue.js默认是使用异步更新,当执行updata时,会调用queueWatcher函数。
//将一个观察者对象push进观察者队列,在队列中已经存在相同的id则该观察者对象将被跳过,除非是在队列被刷新时推送。
export function queueWatcher (watcher: Watcher) {
//获取watcher的id
const id = watcher.id
//检测id是否存在,已经存在直接跳过,不存在直接标志哈希表has
if (has[id] == null) {
has[id] = true
/*如果没有flush掉,直接push到队列中即可*/
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.
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) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
这⾥引⼊了⼀个队列的概念,这也是 Vue 在做派发更新的时候的⼀个优化的点,它并不会每次数据改
变都触发 watcher 的回调,⽽是把这些 watcher 先添加到⼀个队列⾥,然后在 nextTick 后执
⾏ flushSchedulerQueue 。这⾥有⼏个细节要注意⼀下,⾸先⽤ has 对象保证同⼀个 Watcher 只添加⼀次;接着对flushing 的判断,else 部分的逻辑稍后我会讲;最后通过 wating 保证对
nextTick(flushSchedulerQueue) 的调⽤逻辑只有⼀次。那什么是nextTick?
/* @flow */
/* globals MutationObserver */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
const callbacks = []
let pending = false
/*下一个tick时的回调*/
function flushCallbacks () {
pending = false
//将每次保存的cb函数取出并执行
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc
/*
一共有Promise、MutationObserver以及setTimeout三种尝试得到timerFunc的方法
优先使用Promise,在Promise不存在的情况下使用MutationObserver,这两个方法都会在microtask中执行,会比setTimeout更早执行,所以优先使用。如果上述两种方法都不支持的环境则会使用setTimeout,在task尾部推入这个函数,等待调用执行。
*/
if (typeof Promise !== 'undefined' && isNative(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)
/*新建一个textNode的DOM对象,用MutationObserver绑定该DOM并指定回调函数,在DOM变化的时候则会触发回调,该回调会进入主线程(比任务队列优先执行),即textNode.data = String(counter)时便会触发回调*/
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)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
/*使用setTimeout将回调推入任务队列尾部*/
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
export function nextTick (cb?: Function, ctx?: Object) {
//cb是一个函数遍历队列中的watcher,执行watcher.run();
let _resolve
/*cb存到callbacks中*/
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
这里解释一下,一共有Promise、MutationObserver以及setTimeout三种尝试得到timerFunc的方法。 优先使用Promise,在Promise不存在的情况下使用MutationObserver,这两个方法的回调函数都会在microtask中执行,它们会比setTimeout更早执行,所以优先使用。 如果上述两种方法都不支持的环境则会使用setTimeout,在task尾部推入这个函数,等待调用执行。 为什么在miscrotask执行会更早呢,JS 的 event loop 执行时会区分 task 和 microtask,引擎在每个 task 执行完毕,从队列中取下一个 task 来执行之前,会先执行完所有 microtask 队列中的 microtask。setTimeout 回调会被分配到一个新的 task 中执行,而 Promise 的 resolver、MutationObserver 的回调都会被安排到一个新的 microtask 中执行,会比 setTimeout 产生的 task 先执行。
综上,nextTick的目的就是产生一个回调函数加入task或者microtask中,当前栈执行完以后(可能中间还有别的排在前面的函数)调用该回调函数,起到了异步触发(即下一个tick时触发)的目的。
为什么要异步更新视图呢?
来看一下下面这一段代码
<template>
<div>
<div>{{test}}</div>
</div>
</template>
export default {
data () {
return {
test: 0
};
},
mounted () {
for(let i = 0; i < 1000; i++) {
this.test++;
}
}
}
现在有这样的一种情况,mounted的时候test的值会被++循环执行1000次。 每次++时,都会根据响应式触发setter->Dep->Watcher->update->patch。 如果这时候没有异步更新视图,那么每次++都会直接操作DOM更新视图,这是非常消耗性能的。 所以Vue.js实现了一个queue队列,在下一个tick的时候会统一执行queue中Watcher的run。同时,拥有相同id的Watcher不会被重复加入到该queue中去,所以不会执行1000次Watcher的run。最终更新视图只会直接将test对应的DOM的0变成1000。 保证更新视图操作DOM的动作是在当前栈执行完以后下一个tick的时候调用,大大优化了性能 。
4.5、computed和watch
- computed
computed实质上是一个computed watcher,当它所依赖的数据发生变化时会进行重新计算,然后对比新旧值,如果发生了变化就会触发渲染watcher重新渲染,所以对于计算属性Vue想确保不仅仅是计算属性依赖的值发生变化,而是想当计算属性最终计算的值发生变化才会触发渲染watcher重新渲染,这本质上是一种优化。
watch
⼀旦我们 watch 的数据发送变化,它最终会执⾏ watcher 的run ⽅法,执⾏回调函数 cb ,并且如果我们设置了 immediate 为 true,则直接会执⾏回调函数cb ,最后返回了⼀个 unwatchFn ⽅法,它会调⽤ teardown ⽅法去移除这个 watcher 。
拓展:watcher 总共有 4 种类型,这里介绍deep watcher
deep watcher
通常,如果我们想对⼀下对象做深度观测的时候,需要设置这个属性为 true,考虑到这种情况:
var vm = new Vue({ data() { a: { b: 1 } }, watch: { a: { handler(newVal) { console.log(newVal) } } } }) vm.a.b = 2
这个时候是不会 log 任何数据的,因为我们是 watch 了 a 对象,只触发了 a 的 getter,并没有触发a.b 的 getter,所以并没有订阅它的变化,导致我们对 vm.a.b = 2 赋值的时候,虽然触发了setter,但没有可通知的对象,所以也并不会触发 watch 的回调函数了。⽽我们只需要对代码做稍稍修改,就可以观测到这个变化了
watch: { a: { deep: true, handler(newVal) { console.log(newVal) } } }
这样就创建了⼀个 deep watcher 了。