1、找到响应式处理入口
进入页面,Vue开始初始化,执行new Vue(),进入到Vue的构造函数中,在构造函数中执行了_init()方法。
在_init()这个方法中,调用了initState()方法 => 在initState()方法中,调用了initData()方法 => 在initData()方法中,调用了observe()方法。
observe()方法就是Vue进行响应式处理的入口。
2、找到数组响应式处理入口
在observe()方法中,创建了Observer实例,在创建Observer实例的过程中,对传入的数据进行了判断,如果是数组,单独对数组进行处理,如果不是数组,调用walk方法对数据进行响应式处理。
constructor (value: any) {
// 如果是数组,对数组进行特殊处理
if (Array.isArray(value)) {
// 判断浏览器是否支持原型 __proto__ 下面两个方法实质处理方式是一样的
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// 遍历数组的每一项,调用observe方法对数组的每一项以递归的方式进行响应式处理
this.observeArray(value)
} else {
this.walk(value)
}
}
3、对数组进行处理
这里假设浏览器支持__proto__原型,进入到protoAugment方法中,这个方法很简单, target.__proto__ = src 就是将数组的原型指向arrayMethods。我们主要关心的就是arrayMethods,这也是对数组处理的核心部分。
const arrayProto = Array.prototype
// arrayMethods 的原型就是数组的原型
export const arrayMethods = Object.create(arrayProto)
// 数组中会修改自身的7个方法
const methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse']
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
// 通过def方法将数组中会修改自身的方法进行重写
def(arrayMethods, method, function mutator (...args) {
// 实质还是调用数组原型上面的方法
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 如果数组中增加了数据,遍历新增的数据进行响应式处理
if (inserted) ob.observeArray(inserted)
// notify change 数组改变后调用 notify方法 通知 Watcher 去更新视图
ob.dep.notify()
return result
})
})
从源码上很容易可以看出,vue对数组的响应式处理,其实质还是调用了数组原型上的方法,在数组发生变化后调用了notify去派发更新。
4、几个需要注意的点
1> ES6新增的fill方法、copyWithin方法也会改变数组自身,不会触发视图更新
2> 使用数组索引的方式修改数据,不会触发视图更新
3> 修改数组的length属性,不会触发视图更新
vue依赖收集的具体时机
1、首先,了解下vue生命周期函数
vue生命周期分为beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destroyed
2、从源码层面走个流程找到依赖收集的具体时机
1> 进入页面,Vue开始初始化,执行new Vue(),进入到Vue的构造函数中,在构造函数中执行了_init()方法。
2> 在_init()这个方法里,首先初始化绑定事件和生命周期钩子,然后调用 beforeCreate 这个钩子函数,在这个钩子函数中还没有初始化数据,所以在这个钩子函数中一般不进行操作。
3> 紧接着进行props、data、methods、computed、watch等的初始化,这个过程中已经将数据转换为了响应式数据。紧接着调用了 created 这个钩子函数,在这个钩子函数中已经可以拿到数据,我们可以在这个钩子函数中向后端发起请求,异步获取到数据,这个时候修改数据不会调用update函数,也不会触发其他生命周期钩子。
// vm 的声明周期相关变量初始化
// $children/$parent/$root/$refs
initLifecycle(vm)
// vm 的事件监听初始化,父组件绑定在当前组件上的事件
initEvents(vm)
// $slots/$scopedSlots/_c/$createElement/$attress/$listeners
initRender(vm)
// 生命钩子的回调
callHook(vm, 'beforeCreate')
// 把 inject 的成员注入到 vm 上
initInjections(vm) // resolve injections before data/props
// 初始化 vm 的 _props/methods/_data/computed/watch
initState(vm)
// 初始化 provide
initProvide(vm) // resolve provide after data/props
// 生命钩子的回调
callHook(vm, 'created')
// 紧接着调用$mount函数
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
4> 紧接着调用mount函数中将 template/el 转化成 render 函数,准备渲染。然后调用了mountComponent,在mountComponent中,首先调用了beforeMount钩子,在这个钩子中,模板已经编译好了,还没有转为真实DOM挂载到页面,这个钩子函数中也可以请求数据,修改数据也不会触发updata函数以及其他生命周期钩子函数。
5> 紧接着定义了updateComponent,给updateComponent赋值。然后初始化Watcher实例,并将updateComponent作为参数传入Watcher,在Watcher构造器中,判断了expOrFn(即updateComponent参数)是否是函数,如果是函数,赋给this.getter,然后调用了this.get()方法
callHook(vm, 'beforeMount')
let updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
// updateComponent 作为回调函数cb
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
6> 重点来了,依赖收集就是发生在这个get方法中,在beforeMount钩子函数之后。在get方法中,首先调用pushTarget()方法将当前Watcher实例入栈,并设置Dep.target = Watcher实例(启用依赖收集)。然后调用this.getter(),即调用updateComponent这个回调函数,在这个回调函数中,首先调用_render()方法将虚拟DOM渲染为真实DOM,然后调用_render()方法将真实DOM挂载到页面上。在这个过程中,触发了c方法,v方法,s方法,会访问到页面显示所依赖的数据,触发getter,然后判断Dep.target是否存在,我们在pushTarget中已经启用了依赖收集,所以这个时候就会通过判断,执行depend方法,调用Watcher的addDep方法,在addDep方法中,首先获取dep的id,然后判断newDepIds数组中是否存在这个id,防止重复收集依赖。如果不存在,将dep.id存到newDepIds数组中,并将这个Watcher实例增加到dep的subs数组中,依赖收集完成。接着将真实DOM挂载到页面上,触发mounted钩子函数。
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
// options
if (options) {
this.lazy = !!options.lazy
}
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
}
this.value = this.lazy
? undefined
: this.get()
}
get () {
// 开启依赖收集
pushTarget(this)
const vm = this.vm
try {
// 调用 updateComponent 期间访问到依赖数据触发getter进行依赖收集
value = this.getter.call(vm, vm)
} catch (e) {
} finally {
// 当前组件依赖收集完毕,如果有父组件,继续收集父组件依赖
popTarget()
this.cleanupDeps()
}
return value
}
7> 当数据更新后,就会立即执行beforeUpdate钩子函数,然后 Vue 的虚拟 dom 机制会重新构建虚拟 dom 与上一次的虚拟 dom 树利用 diff 算法进行对比之后重新渲染,一般不做什么事 。
8> 当更新完成后,执行 updated 钩子函数,数据已经更改完成,dom 也重新 render 完成,可以操作更新后的虚拟 dom 。
9> 当经过某种途径调用$destroy 方法后,立即执行 beforeDestroy 钩子函数,一般在这里做一些善后工作,例如清除计时器、清除绑定的事件等等。组件的数据绑定、监听...去掉后只剩下 dom 空壳,这个时候,执行 destroyed 钩子函数,在这里做善后工作也可以 。
最后:
青训营即将结束,我在此祝愿青训营的小伙伴们都能实现自己的愿望,诸事如意。