time: 2019.11.21
author: heyunjiang
目录
1 现象分析
2 initState
3 observe 与 Observer
4 defineReactive
5 Dep
6 watcher
7 dom diff
补充一点:双向数据绑定 v-model 使用的是 value prop + change 事件来实现的,属于语法糖,简化编码步骤,统一不同类型表单的数据绑定。后续更新 dom 还是响应式系统来实现的
先分析现象:通过修改 state 数据,会触发组件重新渲染,this.hello = 'world'
看源码前分析:
源码分析:initState
// initData
function initData (vm: Component) {
let data = vm.$options.data
// 设置 vm._data 属性
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// proxy data on instance
const keys = Object.keys(data)
let i = keys.length
while (i--) {
const key = keys[i]
proxy(vm, `_data`, key)
}
// observe data
observe(data, true /* asRootData */)
}
// getData 内部执行来 data 方法
export function getData (data: Function, vm: Component): any {
// pushTarget 和 popTarget 是定义在 dep.js 内部方法,此处作用是 disable dep collection when invoking data getters
pushTarget()
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, `data()`)
return {}
} finally {
popTarget()
}
}
// proxy
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
从上面 initState 可以看到
接下来分析 observe() 方法
// observe 定义在 '/observer/index'
export function observe (value: any, asRootData: ?boolean): Observer | void {
let ob = new Observer(value)
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
可以看到
// Observer
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
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
this.walk(value)
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
从上面的 Observer 方法我们可以看到,Observer 有如下作用
每个组件一个 data 对象,一个根 Observer 对象,每个子 plain object 对应一个 Observer 对象,一个根 Dep 对象,多个子 Dep 对象,一个 watcher 对象
// 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
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
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
从上面的 defineReactive 方法可以看到
!shallow && observe(val)
,并且为该对象的每个属性再次调用一次 defineReactive 方法, 形成一个递归问题1 如何监听数据变化?
答: 为对象的每个属性通过 Object.defineProperty 修改 get、set 描述属性,并且通过实例化一个 Dep 对象来收集及通知变更
问题2 监听哪些数据变化?
答:1. data 的顶层数据 2. 类型为 object 的数据也会递归监听 3. 数组项为 object 类型的
注意一下情况不会被监听到:1. 数组项为非 object 的,如果通过下标更新,或者修改数组长度 2. 为 object 新增或者删除属性
前面看到每个属性对应一个 dep 实例,从 new Dep 到 dep.depend, dep.notify,也没有看到与当前属性哪里挂勾了,那么这个 dep 是干啥的呢?
time: 2019.12.10
在分析 Dep 之前,有几个问题
import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'
let uid = 0
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
从 dep 源码可以看到
问题
vue 为每个组件实例化一个 watcher;也会为每一个 computed 和 watch 属性都实例化一个 watcher
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true)
在实例化 watcher 的时候,会调用 this.get 方法,内部包含了 pushTarget(this)
,会将当前 watcher 压入 Dep 类的 watcher 实例栈中。
在 dep.depend 的时候,会执行 Dep.target.addDep(this)
,以及在 dep.notify 的时候,会执行 subs[i].update()
,这2个方法做了什么操作呢?也就是说,当数据变化的时候,会通过 dep.notify 调用 watcher 的 update 方法,实现更新缓存和更新
在 watcher 内部,这2个方法的实现如下
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)
}
}
}
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
可以看出
问题3 数据变化如何缓存?queueWatcher
答:通过 queueWatcher 将其缓存在一个数组队列中
问题4 什么时机触发更新?
答:上个 nextTick 结束了,然后马上开启下一个 nextTick 来 flush 队列
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
}
}
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
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
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
可以看出
updateComponent = () => {vm._update(vm._render(), hydrating)}
方法。this.hello = 'world'
更新当前组件的数据,也会在最后通过 vm._render 再次更新 vnode ,从而渲染更新真实 dom。value = this.getter.call(vm, vm)
,也就是执行了 vm._render,在 render 过程中会读取 vm.$data 上的数据,会触发属性的 getter,此刻 Dep.target 正式当前 watcher ,而此刻 getter 就会执行 dep.depend 将当前 watcher 压入当前 dep 实例的 watcher 队列中,就实现了一次组件的响应式系统现在的问题
这里简单总结一下 dom diff 的算法
在组件更新的时候,是通过对比前后虚拟 dom 来派发更新。预想一些问题
通过 vm._render()
调用 createComponent 生成新的 vnode,如果已经创建过了,则会在 render 时继续使用上次的 key
const { render, _parentVnode } = vm.$options
vnode = render.call(vm._renderProxy, vm.$createElement)
然后会通过 vm._update()
来更新 vnode,vnode 是挂载在 vm 实例上的
// vnode 保存在 vm 对象上
const vm: Component = this
const prevVnode = vm._vnode
vm._vnode = vnode
vm.$el = vm.__patch__(prevVnode, vnode)
// patch.js 通过 sameVnode 比较是否是同一个 vnode
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}
// sameVnode 通过比较前后 key 是否相等
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// 1 如果新旧 vnode 节点不变,则不更新
if (oldVnode === vnode) {
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}
const elm = vnode.elm = oldVnode.elm
let i
const data = vnode.data
// 2. 调用 prepatch 钩子
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const ch = vnode.children
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
从上面可以看到
在 Vnode 的 constructor 中,有 key 的声明 this.key = data && data.key
,这个是用户主动传入的,有如下作用
c.key = `__vlist${nestedIndex}_${i}__`