思考
带着问题看源码:
- vue 是如何实现响应式的?
- vue 是在哪个环节进行依赖收集的?
- vue 是在哪个环节完成 vnode => dom 的?
- data 改变之后是如何更新试图的?
核心概念
掏一张官方图片:
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
这里主要搞清几个概念:
- observe:通过
new Observe(obj)
把一个对象变成响应式(主要使用defineReactive
方法处理对象的属性) - Dep:每个响应式数据都会对应一个
dep
(Dep实例),用来存放依赖该值的订阅者(Watcher) - Watcher:通过调用
Watcher.update
方法触发更新
这里有个关于 Dep.target 的点弄了好久才明白,其实 target 是一个 Dep 类的静态变量,每一个 dep 实例都能访问到,代表的是当前进行依赖收集的 Watcher
简单梳理一下整个过程:
new Vue
之后拿到option 里面的 data
,执行initData
方法- 使用
observe
把data
变成响应式,通过walk
方法对data
的每一个属性使用defineReactive
进行处理 defineReactive
的过程中,对每一个属性闭包生成一个Dep
实例,使用defineProperty
劫持get
和set
- 在
get
的时候加入依赖收集操作dep.depend()
- 在
set
的时候执行依赖分发操作dep.notify()
- 在
- 执行
vm.$mount
(核心是执行mountComponent
)方法对组件进行挂载 - 生成一个
renderWatcher
实例 - 执行
vm._render()
方法得到一个Vnode
,在这一步由于触发了响应式数据的get
,因此会进行依赖收集 - 将得到的
Vnode
作为参数,调用Vm._update
方法 - 进行
patch
操作,渲染页面 - 页面对响应式数据做出修改,触发
set
内部dep.notify
进行更新
相关源码
跟着源码来理解上面的每个过程大致实现,只保留核心代码
首先看一下 _init
的整个流程
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
/*一个防止vm实例自身被观察的标志位*/
vm._isVue = true
// merge options
if (options && options._isComponent) {
// ...
} else {
/* 合并 options */
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
vm._self = vm
/*初始化生命周期*/
initLifecycle(vm)
/*初始化事件*/
initEvents(vm)
/*初始化render*/
initRender(vm)
/*触发beforeCreate钩子事件*/
callHook(vm, 'beforeCreate')
initInjections(vm)
/*初始化props、methods、data、computed与watch*/
initState(vm)
initProvide(vm)
/*触发created钩子事件*/
callHook(vm, 'created')
/* 初始化数据完成之后挂载 vm */
if (vm.$options.el) {
/*挂载组件*/
vm.$mount(vm.$options.el)
}
}
主要是对数据进行一系列初始化处理, 同时触发 vue 生命周期内的钩子函数
由于
beforeCreate
之前还没有对data
进行初始化, 这就是为什么beforeCreate
函数内无法访问data
的原因
initData (对数据进行响应式处理)
执行initData
的时机:
new Vue
=> _init
=> initState
=> initData
=> walk
=> defineReactive(核心)
core/instance/state.js
function initData (vm: Component) {
/*得到data数据, 如果是一个函数,就执行获得返回值*/
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
/* 核心就是执行observe方法*/
observe(data, true /* asRootData */)
}
observe
方法通过new Observe
将data
变成响应式,核心是使用defineReactive
,直接看Observe
源码:
export class Observer {
value: any
dep: Dep
vmCount: number
constructor(value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
/* 将Ob server实例绑定到data的__ob__属性上面去,observe的时候会先检测是否已经有__ob__对象存放Observer实例 */
def(value, "__ob__", this)
if (Array.isArray(value)) {
/*
如果是数组,将修改后可以截获响应的数组方法替换掉该数组的原型中的原生方法,达到监听数组数据变化响应的效果。
如果浏览器支持__proto__属性,则直接覆盖当前数组对象原型上的原生数组方法,否则直接覆盖数组对象的原型。
*/
const augment = hasProto
? protoAugment /*直接覆盖原型的方法来修改目标对象*/
: copyAugment /*定义(覆盖)目标对象或数组的某一个方法*/
augment(value, arrayMethods, arrayKeys)
/*遍历数组的每一个成员进行observe*/
this.observeArray(value)
} else {
/*如果是对象则直接walk进行绑定*/
this.walk(value)
}
}
/* 遍历每对象并且在它们上面绑定getter与setter。这个方法只有在value的类型是对象的时候才能被调用 */
walk(obj: Object) {
const keys = Object.keys(obj)
/*walk方法会遍历对象的每一个属性进行defineReactive绑定*/
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
/*对数组的每一个成员进行observe*/
observeArray(items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
/*数组需要遍历每一个成员进行observe*/
observe(items[i])
}
}
}
defineReactive
方法中,通过闭包生成一个Dep
实例,在defineProperty
重写的get
和set
方法中使用,进行依赖收集以及分发
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: Function
) {
/*在闭包中定义一个dep对象*/
const dep = new Dep()
/*如果之前该对象已经预设了getter以及setter将其执行,保证不会覆盖之前已经定义的getter/setter。*/
const getter = property && property.get
const setter = property && property.set
/*对象的子对象递归进行observe并返回子节点的Observer对象*/
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
/*如果原本对象拥有getter方法则执行*/
const value = getter ? getter.call(obj) : val
/* Dep.target 就是当前进行依赖收集的Watcher */
if (Dep.target) {
/*进行依赖收集*/
dep.depend()
if (childOb) {
/*子对象进行依赖收集,其实是同一个watcher*/
childOb.dep.depend()
}
if (Array.isArray(value)) {
/*是数组则需要对每一个成员都进行依赖收集,如果数组的成员还是数组,则递归。*/
dependArray(value)
}
}
return value
},
set: function reactiveSetter (newVal) {
/* 如果值没有发生变化不进行操作*/
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (setter) {
/*如果原本对象拥有setter方法则执行setter*/
setter.call(obj, newVal)
} else {
val = newVal
}
/*新的值需要重新进行observe,保证数据响应式*/
childOb = observe(newVal)
/*dep对象通知所有的观察者*/
dep.notify()
}
})
}
到这一步完成了数据的响应式, 也得到了我们第一个问题的答案 核心就是使用 defineProperty 劫持 data 每个属性的 get 和 set, 在 get 中进行依赖收集, 在 set 中进行依赖分发
Vm.$mount
可以看到 _init
方法内部,当对数据进行初始化完毕之后, 会调用vm.$mount(vm.$option.el)
因为 Vue
可以跨平台使用, $mount
会根据不同的平台进行不一样的处理, 这里只考虑 web 平台下
/platforms/web/runtime/index.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
/*获取DOM实例对象*/
el = el && inBrowser ? query(el) : undefined
/*挂载组件*/
return mountComponent(this, el, hydrating)
}
核心是使用 mountComponent
方法
这一步主要做了两件事, 收集依赖以及 渲染真实 dom
- 触发
beforeMount
钩子 vm._render()
获取vnode
, 同时会执行一个匿名函数, 在这一步会触发响应式数据的 get, 完成依赖收集vm._update
将获取的 vnode 进行__patch__
, 渲染真实 dom- 触发
mounted
钩子
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
/*render函数不存在的时候创建一个空的VNode节点*/
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
// ...
}
}
/*触发beforeMount钩子*/
callHook(vm, 'beforeMount')
/*updateComponent作为Watcher对象的getter函数,用来依赖收集*/
let updateComponent
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
// ...
} else {
/* 更新以及初次渲染都会通过该方法 */
updateComponent = () => {
// 更新view视图, 内部会调用__patch__ 进行 diff 算法,将 vnode 渲染为真实 DOM
vm._update(vm._render(), hydrating)
}
}
/*
这里对该vm注册一个rednerWatcher,Watcher 用 expOrFn 接受传递过来的函数 updateComponent, Watcher 内部会将 expOrFn 解析成 getter, 因此在 get 的时候会触发用于触发传递过来的函数 updateComponent
updateComponent 会触发 vm._render(),这一步会对模板进行解析,并执行一个匿名函数
*/
vm._watcher = new Watcher(vm, updateComponent, noop, null, true)
hydrating = false
if (vm.$vnode == null) {
/*标志位,代表该组件已经挂载*/
vm._isMounted = true
/*触发mounted钩子*/
callHook(vm, 'mounted')
}
return vm
}
Watcher
内部核心代码:
Watcher
用expOrFn
接受传递过来的函数 updateComponent, 并将 expOrFn 解析成 getter- Watcher 被 get 的时候会执行 updateComponent
/*把表达式expOrFn解析成getter*/
if (typeof expOrFn === 'function') {
this.getter = expOrFn
}
get () {
/*将自身watcher观察者实例设置给Dep.target,用以依赖收集。*/
pushTarget(this)
let value
const vm = this.vm
if (this.user) {
// ...
} else {
/* 主要是这一步, getter 被 */
value = this.getter.call(vm, vm)
}
/*将观察者实例从target栈中取出并设置给Dep.target*/
popTarget()
this.cleanupDeps()
return value
}
updateComponent
是依赖收集以及刷新页面关键的一步:
- 给当前
vm
注册一个renderWatcher
, 并传入updateComponent
函数 updateComponent
会在Watcher
的get
中调用, 即该watcher
被访问的时候vm._render()
内部会实现模板的解析, 执行一个匿名函数, 该方法返回一个vnode
. 在这一步因为对data
内部属性进行了访问, 因此会触发之前定义在defineProperty
中get
的方法:dep.depend
, 进行依赖收集
vm._render
的匿名函数
(function anonymous() {
with (this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_v("\n\t\t\t我是一个" + _s(data.msg) + "\n\t\t\t"), _c('button', {
on: {
"click": handleChange
}
}, [_v("click")])])
}
})
其中 _c
是createElement
的缩写, 用来创建vnode
, _s
用来将数据转为字符串
依赖收集逻辑如下:
1. new Watcher
的同时, 触发内部 get
方法, 调用 pushTarget
将自身设置给 Dep
内静态变量 target
, 用于依赖收集
2. 模板解析过程中, 会访问 data
内部的数据, 触发 get
内部 dep.depend
方法, 进行依赖收集, 此时 Dep.target
为当前 Watcher
实例
3. vm._update
用来进行视图更新, 内部会调用__patch__
方法将 vnode 转换为真实 dom
执行到这里已经完成了数据的响应式处理, 依赖收集, 以及页面渲染, 生命周期也到了 mount
这一步, 接下来是数据修改之后如何更新页面的逻辑
Watcher.update(更新视图)
当对响应式数据进行修改之后, 会触发 set 内部的依赖分发功能 dep.notify
, 该方法内部其实就是遍历整个依赖列表subs
, 里面保存的是依赖当前数据的Watcher
, 触发他们的 update
方法
/*通知所有订阅者*/
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
Watcher.update
是Watch
内部定义的方法, 这里就长话短说(不考虑异步更新优化策略), 其实就是调用 Watcher.run()
方法, 然后触发 Watcher
的 getter
, 上面说过 Watcher
被 get
的时候会触发传递过来的函数 updateComponent
, 因此又回到了之前的逻辑
run () {
if (this.active) {
/* get操作在获取value本身也会执行getter从而调用update更新视图 */
const value = this.get()
}
dep.notify
=> Watcher.update
=> Watcher.run
=> watcher.getter
=> updateComponent
=> vm._render
(得到vnode
并拿到修改后的数据) => vm._update
进行 patch
并更新视图
由于对已经进行过收集的依赖会做缓存, 因此触发响应式数据的 get 的时候并不会重新对依赖进行收集
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)
}
}
}
总结
至此已经得到了所有问题的答案
- vue 是如何实现响应式的
- 核心是通过
defineProperty
劫持对象的get
以及set
- 在
get
中收集依赖, 在set
中分发依赖
- 核心是通过
- vue 是在哪个环节进行依赖收集的
- 执行
vm.$mount
的时候, 会执行 updateComponent 方法 - 创建一个
renderWatcher
, 并将this.getter
设为updateComponent
vm._render
实现模板的解析, 并得到vnode
, 在这一步触发响应式数据的get
, 触发依赖收集
- 执行
- vue 是在哪个环节完成 vnode => dom 的
vm.update
方法会进行patch
, 将vnode
渲染为真实 dom
- data 改变之后是如何更新试图的
- 当触发
set
之后, 执行dep.notify
,接着会触发watcher.run
watcher.run
会触发watcher
内部的get
方法从而执行this.getter.call(vm, vm)
this.getter
其实就是创建实例时传递过来的updateComponent方法, 因此会重新触发之前的渲染逻辑,从而更新试图
- 当触发