我的开源库:
- fly-barrage 前端弹幕库,项目官网:https://fly-barrage.netlify.app/,可实现类似于 B 站的弹幕效果,并提供了完整的 DEMO,Gitee 推荐项目;
- fly-gesture-unlock 手势解锁库,项目官网:https://fly-gesture-unlock.netlify.app/,在线体验:https://fly-gesture-unlock-online.netlify.app/,可高度自定义锚点的数量、样式以及尺寸;
通过上一篇文章 Vue源码阅读(7):将数据转换成响应式的 我们已经了解了数据是怎么转换成响应式的了,接下来我们以组件的渲染为例看 Vue 在运行时是如何进行依赖收集和变化侦测的。
1,从 $mount 开始看
1-1,_init 方法(core/instance/init.js)
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
// 初始化 state,包括 props、methods、data、computed、watch
initState(vm)
// 如果配置中有 el 的话,则自动执行挂载操作
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
_init 方法中的 initState() 方法是上一篇博客的内容,作用是将数据转换成响应式的,响应式的数据是进行依赖收集和变化侦测的前期准备。接下来,我们以组件的渲染为例,看看依赖收集和变化侦测的具体流程。
1-2,$mount 方法(src/platforms/web/runtime/index.js)
// 运行时版本代码使用的 $mount 函数。调用这个 $mount 函数,模板字符串必须已经编译成 render 函数
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
运行时 $mount 方法内部调用 mountComponent() 方法进行组件的挂载。
1-3,mountComponent(core/instance/lifecycle.js)
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// 将 el 设值到 vm 中的 $el
vm.$el = el
// 触发执行 beforeMount 生命周期函数(挂载之前)
callHook(vm, 'beforeMount')
// 一个更新渲染组件的方法
let updateComponent = () => {
// vm._render() 函数的执行结果是一个 VNode
// vm._update() 函数执行虚拟 DOM 的 patch 方法来执行节点的比对与渲染操作
vm._update(vm._render(), hydrating)
}
// 这里的 Watcher 实例是一个渲染 Watcher,组件级别的
vm._watcher = new Watcher(vm, updateComponent, noop)
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
mountComponent() 方法内部执行了一些生命周期函数, 并且创建了一个 Watcher 实例,传递的第二个参数是一个函数(updateComponent),该函数的作用是重新渲染页面。接下来我们看看 Watcher 类的实现。
2,Watcher 类的实现与依赖收集
Watcher 类的简要代码如下:
export default class Watcher {
vm: Component;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
) {
this.vm = vm
this.getter = expOrFn
this.value = this.get()
}
get () {
// 将自身实例赋值到 Dep.target 这个静态属性上(保证全局都能拿到这个 watcher 实例),
// 使得 getter 函数使用数据的 Dep 实例能够拿到这个 Watcher 实例,进行依赖的收集。
// pushTarget 操作很重要
pushTarget(this)
let value
const vm = this.vm
try {
// 执行 getter 函数,该函数执行时,会对响应式的数据进行读取操作,这个读取操作能够触发数据的 getter,
// 在 getter 中会将 Dep.target 这个 Watcher 实例存储到该数据的 Dep 实例中,以此就完成了依赖的收集
// 依赖收集需要执行 addDep() 方法完成
value = this.getter.call(vm, vm)
} catch (e) {
......
}
return value
}
}
我们从 Watcher 类的构造函数开始看,expOrFn 属性就是上面的 updateComponent 函数,在构造函数中将其设置到了 this.getter 属性上,然后调用 Watcher 类中的 get 方法。
接下来就是重点了,在 get 函数中,我们执行了一个 pushTarget 方法,方法的参数是当前的 Watcher 实例,pushTarget 方法的作用是:将当前的这个 Watcher 实例赋值到 Dep.target 属性上,这是一个静态属性,所以在代码的其他地方也能够访问到该属性,我们看一下 pushTarget 方法的代码实现:
2-1,src/core/observer/dep.js ==> pushTarget()
export function pushTarget (_target: Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export default class Dep {
static target: ?Watcher;
}
pushTarget 方法的实现很简单,就是简单地将这个 Watcher 实例设值到 Dep.target 静态属性上。
接下来执行 this.getter.call(vm, vm)。
2-2,this.getter.call(vm, vm)
this.getter 就是我们传递到 Watcher 构造函数中的 updateComponent 函数,在这里执行它。
// 一个更新渲染组件的方法
let updateComponent = () => {
// vm._render() 函数的执行结果是一个 VNode
// vm._update() 函数执行虚拟 DOM 的 patch 方法来执行节点的比对与渲染操作
vm._update(vm._render(), hydrating)
}
我们知道,在渲染组件的过程中,肯定会获取该组件使用到的数据,进行页面内容的填充。当代码获取这些数据的时候,就会触发这些数据的 getter 函数,我们看一下 getter 函数的相关内容。
export function defineReactive (
// 对象
obj: Object,
// key
key: string,
// 值
val: any,
customSetter?: ?Function,
// 浅的
shallow?: boolean
) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 触发执行上面拿到的 getter
const value = getter ? getter.call(obj) : val
/ 下面是依赖收集的操作 /
// 如果 Dep 上的静态属性 target 存在的话
if (Dep.target) {
// 向 dep 中添加依赖,依赖是 Watcher 的实例
dep.depend()
}
// getter 返回值
return value
},
set: function reactiveSetter (newVal) {
......
}
})
}
我们可以看到,在 getter 函数中,判断 Dep.target 上有没有值,如果有值的话,就执行 dep.depend() 方法进行依赖的收集,这个 dep 实例是和当前的数据相对应的,也就是说上面我们 new 的 Watcher 实例是依赖于该数据的,所以应该把这个 Watcher 实例存储到该数据对应的 dep 中。我们看下,depend() 方法的具体内容。
export default class Dep {
// 依赖函数
// 执行该函数可以将 Dep.target 依赖 push 进 subs 数组中
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
}
depend 方法的内部执行了 Watcher 实例的 addDep 方法,参数是其本身。
export default class Watcher {
addDep (dep: Dep) {
const id = dep.id
// 通过 if (!this.newDepIds.has(id)) 防止同一 dep 重复进入里面的逻辑
// 进入 if 代码块中的,每一次都是不同的 dep
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
}
在 addDep 方法中,如果这个 dep 是新的,不是和以前的 dep 重复的话,就进入最里面的逻辑,执行 dep 实例的 addSub 方法,参数是当前的 Watcher 实例。
export default class Dep {
// 用于收集依赖的数组
subs: Array<Watcher>;
// 向 subs 数组添加依赖的函数
addSub (sub: Watcher) {
this.subs.push(sub)
}
}
addSub 方法的内部,将 Watcher 实例 push 到了 subs 数组中,至此我们就完成了一次完整的依赖收集。
接下来说说变化侦测。
3,变化侦测的流程
在 Vue 应用中,一旦我们改变模板中使用的某个数据,这个组件就会重新进行渲染。改变数据的这个动作,我们可以通过 Object.defineProperty 中的 setter 检测到,如果是通过数组的原型函数改变数据的话,我们也能够在重写的原型函数中捕获到。
Object.defineProperty 中的 setter:
// 在此进行派发更新
set: function reactiveSetter (newVal) {
// 拿到旧的 value
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
// 如果存在用户自定义的 setter 的话,用这个用户自定义的 setter 赋值这个 value
if (setter) {
setter.call(obj, newVal)
} else {
// 否则就直接将 newVal 赋值给 val
val = newVal
}
// 将新设置值中的 keys 也转换成响应式的
childOb = !shallow && observe(newVal)
// 触发依赖的更新
dep.notify()
}
重写的原型函数:
[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
// 进行遍历处理
.forEach(function (method) {
// 缓存原生的相应方法
const original = arrayProto[method]
// 定义该 method 对应的自定义方法
def(arrayMethods, method, function mutator (...args) {
// 执行原生方法拿到执行结果值,在最后将这个结果值返回
const result = original.apply(this, args)
// 这里的 this 是执行当前方法的数组的实例。在 Vue 中,每个数据都会有 __ob__ 属性,这个属性
// 是 Observer 的实例,该实例有一个 dep 属性(Dep 的实例),该属性能够收集数组的依赖
const ob = this.__ob__
// 数组有三种新增数据的方法。分别是:'push','unshift','splice'
// 这些新增的数据也需要变成响应式的,在这里,使用 inserted 变量记录新增的数据
let inserted
switch (method) {
// 如果当前的方法是 push 或者 unshift 的话,新增的数据就是 args,将 args 设值给 inserted 即可
case 'push':
case 'unshift':
inserted = args
break
// 如果当前的方法是 splice 的话,那么插入的数据就是 args.slice(2)
case 'splice':
inserted = args.slice(2)
break
}
// 如果的确新增了数据的话,将 inserted 作为参数执行 observer.observeArray() 方法,把新增的每个元素都变成响应式的
if (inserted) ob.observeArray(inserted)
// 通知 ob.dep 中的依赖
ob.dep.notify()
// 在最后,返回 Array 方法执行的结果
return result
})
})
我们可以看到,这两种变化侦测的方法中,最后都会执行 dep.notify(),notify 方法的代码如下:
// 触发 subs 数组中依赖的更新操作
notify () {
// 数组的 slice 函数具有拷贝的作用
const subs = this.subs.slice()
// 遍历 subs 数组中的依赖项
for (let i = 0, l = subs.length; i < l; i++) {
// 执行依赖项的 update 函数,触发执行依赖
subs[i].update()
}
}
notify 方法的内容很简单,就是遍历执行依赖了对应数据 Watcher 实例的 update 方法,我们看下 Watcher 实例中的 update 方法。
export default class Watcher {
update () {
this.run()
}
run () {
if (this.active) {
const value = this.get()
}
}
get () {
pushTarget(this)
let value
const 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
}
}
// 将 expOrFn 对应的值返回出去
return value
}
}
我们可以看到,依次执行了:update() ==> run() ==> get() 方法,在 get 方法的内部会执行 this.getter,通过上面的内容我们知道,这个 this.getter 就是用于组件渲染的 updateComponent() 方法,执行这个方法,就会进行组件的重新渲染。好了,以上就是变化侦测到组件重新渲染的整个流程。
4,下集预告
以上这三篇博客就是与响应式原理有关的内容。接下来,先说说与响应式原理有关 API 的底层原理,然后再讲解模板编译原理相关的内容,敬请期待。