vue2响应式相关笔记

102 阅读12分钟

vue响应式原理

Object.defineProperty(obj, prop, descriptor) obj:即定义属性的对象 prop:要定义或者修改的属性名称 descriptor:是将被定义或者修改的属性描述符。列举几项比较重要的描述符 get:当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值 set:当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象 writable: 当且仅当该属性的 writable 键值为 true 时, 属性才能被赋值 enumerable: 当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中 configurable: 当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除

Vue源码响应式       一、initState       src/core/instance/state.js 主要作用包括初始化props、methods、data、computed、watch,以下主要讲解初始化data数据

      二、initData        src/core/instance/state.js 主要作用包括检测是否与props存在命名冲突,以及proxy方法

正常data数据将保存至vue实例的_data属性中,之所以可以通过this.xxx获取,都是通过proxy关联映射

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) }

      三、observe       src/core/observer/index.js 创建Dep对象。即每个被observe实例内部都存在dep对象,主要功能是收集依赖,后续有详解 判断是Object或者是Array,使用不同的响应式监听方案 目标是object时,直接调用defineReactive遍历key值,以下为defineReactive方法 export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { // 创建dep对象,主要是该对象的key值的。 const dep = new Dep()

// 获取该对象的key值自有属性对应的属性描述符。 主要通过闭包的方式将原有的get 和 set保留下来,防止因响应式重置导致原有get set失效 const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } const getter = property && property.get const setter = property && property.set

// 递归的调用检测当前val是否需要注册响应式 let childOb = !shallow && observe(val)

// 开始监听该对象key值的get 与 set方法 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { // 调用原有的get方法获取val值 const value = getter ? getter.call(obj) : val // Dep.target 是绑定在Dep class中的唯一属性。主要是记录当前活跃的watcher对象。其中包括 computed Watcher、watch Watcher、渲染Watcher // 由于js是单线程,所以任何时刻活跃的Watcher只有唯一一个 if (Dep.target) { // 同样使用Dep.target获取当前Watcher后,将当前Dep实例添加到Watcher实例的newDeps数组中,其中 将dep对象的subs中记录当前Watcher // 即通过调用该方法 当前Watcher与Dep实例分别将记录到自己维护的数组中 dep.depend() // childOb 存在时,说明子属性同样是响应式对象或者数组 if (childOb) { // child 的Observe对象中的Dep实例也需要收集当前依赖。 这就是为啥我们能够通过this.set(target,key) 的方式新增响应式。当调用set方法时,会将Observe所有dep中收集的Watcher进行更新,更新就是重新收集依赖的过程,也是重新渲染的过程 childOb.dep.depend() // 如果当前value是数组需要特殊处理 递归式的检测每个对象是不是存在__ob__属性 即是响应式对象,如果是则也需要收集当前依赖 // 主要原因就是Object.defineProperty 对于数组监听存在天然弱势,Vue3通过Proxy已解决,后续会讲 if (Array.isArray(value)) { dependArray(value) } } } return value }, // 主要功能时监听赋值操作 set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val // 优化处理,如果赋值与原有数据一致 则直接return,无需操作 if (newVal === value || (newVal !== newVal && value !== value)) { return }

  if (process.env.NODE_ENV !== 'production' && customSetter) {
    customSetter()
  }
  // 使用原有setter进行赋值操作
  if (setter) {
    setter.call(obj, newVal)
  } else {
    val = newVal
  }
  // 重新判断对赋值后的新数据进行监听
  childOb = !shallow && observe(newVal)
  // 通知已通过getter收集的依赖进行重新计算
  dep.notify()
}

}) }

      四、Dep对象,即依赖收集的核心  src/core/observer/dep.js Dep对象内部主要属性包括id(Dep实例的唯一标识)subs (记录当前Dep实例收集的各类Watcher) Dep 类存在静态属性target,记录当前被计算的Watcher对象,且同一时间,只有一个Watcher对象在被计算 export default class Dep { static target: ?Watcher; id: number; subs: Array; // 只有两个属性 唯一标识和记录Watcher。 特别需注意的是 创建dep对象的过程是由父到子 constructor () { this.id = uid++ this.subs = [] } // 增加当前收集的依赖 addSub (sub: Watcher) { this.subs.push(sub) } // 删除收集的依赖 removeSub (sub: Watcher) { remove(this.subs, sub) } // 对象属性getter时调用的方法,主要是检测当前是否存在计算的Watcher,存在即可开始收集依赖 depend () { if (Dep.target) { // Watcher实例的addDep方法会将当前dep收集到Watcher实例的newDeps数组,然后该dep再调用addSub方法,即将当前dep实例的的subs记录当前Watcher实例 Dep.target.addDep(this) } }

// 对象属性的setter时调用的方法,即遍历整个subs,进行分发通知更新操作。即调用Watcher实例的update方法,后续详解 notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }

      五、Watcher    src/core/observer/watcher.js Watcher实例中存在于Dep相关的属性包括 deps、newDeps (都是数组,记录上次收集依赖时的dep数组和新一轮收集时的dep数组) depsIds newDepIds(都是Set对象,分别用来记录新旧deps数组中的id) 针对不同类的Watcher,存在特殊属性。watch Watcher时user属性为true,且存在cb属性,即回调函数。computed Watcher存在lazy与dirty属性,lazy标识当前是computed,dirty用来标识当前Watcher的依赖已经更新,结果需要重新计算,由此computed是存在缓存的,防止重复计算。渲染Watcher是最普通的Watcher export default class Watcher { vm: Component; expression: string; cb: Function; id: number; deep: boolean; user: boolean; lazy: boolean; sync: boolean; dirty: boolean; active: boolean; deps: Array; newDeps: Array; depIds: ISet; newDepIds: ISet; getter: Function; value: any;

constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: Object ) { this.vm = vm vm._watchers.push(this) // options if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync } else { this.deep = this.user = this.lazy = this.sync = false } // watch类型监听创建过程中传入的回调函数 this.cb = cb // Watcher实例的唯一标识。需要特别注意一点的是,Watcher的创建过程是由父到子,同一vue实例中是由computed 到 watch 到 渲染。将来dep通知Watcher更新过程中也是按照id有小到大排序,再计算队列中的Watcher this.id = ++uid this.active = true // computed的Watcher特殊传入的属性。lazy标志是存在缓存模式 dirty说明是该Watcher收集的依赖dep更新时 才能触发当前Watcher重新计算结果 this.dirty = this.lazy // 上次依赖收集过程中的deps this.deps = [] // 此次依赖收集过程中的deps this.newDeps = [] // Set中只保存deps中的id,主要是用来添加过程中判断是否已经存在 this.depIds = new Set() this.newDepIds = new Set() this.expression = '' // 当前Watcher需要执行重新计算时的方法。 if (typeof expOrFn === 'function') { this.getter = expOrFn } else { // watch Watcher创建过程中expOrFn是String,我们写的是函数是存在于cb中。parsePath主要是是将watch 如果key值时 'obj.a.c'的写法做兼容 this.getter = parsePath(expOrFn) } // 如果不是lazy模式的话, 直接开始计算过程 this.value = this.lazy ? undefined : this.get() }

/**

  • 开始进行Watcher的计算过程。get主要操作就是将创建过程中传入的function进行重新计算 */ get () { // 至关重要!!!! 主要功能是目前开始计算当前的Watcher,故将当前Watcher实例赋值到Dep.target pushTarget(this) let value const vm = this.vm try { // 执行当前的getter方法,并且绑定this为当前Vue实例 value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, getter for watcher "${this.expression}") } else { throw e } } finally { // 一般watch Watcher会配置deep为true, 如果是deep时,则进行递归式的对子属性进行处理 if (this.deep) { traverse(value) } // getter执行完毕后,说明此次计算已经执行完毕,后续收集的依赖将不再添加至当前Watcher实例中。将Watcher堆栈中最顶层的Watcher实例 弹出。Dep.target 变更为上一个Watcher popTarget() // 将newDeps中的数组覆盖至deps数组中。下次更新时 依赖再次收集至newDeps数组中 this.cleanupDeps() } return value }

/**

  • 只添加未收集的Dep实例,最后再调用dep.addSub将当前Watcher实例添加到dep中 */ 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) } } }

/**

  • 目前依赖收集已经完成,将newDeps 赋值到deps中。 */ cleanupDeps () { // !!!很重要的操作 通过对旧deps遍历 如果发现新收集的依赖不存在该dep 则调用 dep.removeSub将dep实例中的Watcher进行去除。防止已经不再依赖的数据导致无效重新计算 let i = this.deps.length while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } let tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0 }

/**

  • 在对象的setter执行的过程中,将处罚dep所关联的所有Watcher进行update更新操作 */ update () { // 如果当前是Computed Watcher 则将dirty置为true 说明其依赖的计算数据已经更新,其他地方获取computed数据时 会让其重新计算结果 if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { // 将当前的Watcher推入执行队列中 queueWatcher(this) } }

/**

  • Scheduler执行的过程中会调用的方法.即通过queueWatcher将watcher放置进队列后,在队列执行过程中每个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 // watch Watcher的user为true 故需要执行回调方法。同时需要进行try catch。整体来看 就是人家不知道你在Watch瞎写了什么鬼东西 ,别你的报错导致程序不能执行。遇到报错抛出错误,不阻断 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) } } } }

/**

  • Computded Watcher在执行get方法时 会先判断dirty,如果为true 则会调用该方法进行重新计算。计算完毕后讲dirty重置为false */ evaluate () { this.value = this.get() this.dirty = false }

/**

  • 目前从源码来看 主要是在computed Watcher的getter方法在执行时,即可能场景是渲染Watcher计算时使用computed属性,此刻通过将computed Watcher执行depend方法,将原本收集于computed Watcher的dep 同样保存至渲染Watcher中
  • 主要效果是 如果某个属性在渲染Watcher中没有使用 仅在computed Watcher使用时,属性变更同样会使页面重新渲染 */ depend () { let i = this.deps.length while (i--) { this.deps[i].depend() } }

/**

  • 目前来看 在vue实例执行destroy时会执行清空的操作 */ teardown () { if (this.active) { // remove self from vm's watcher list // this is a somewhat expensive operation so we skip it // if the vm is being destroyed. if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this) } let i = this.deps.length while (i--) { this.deps[i].removeSub(this) } this.active = false } } }

      六、scheduler      src/core/observer/scheduler.js queueWatcher 主要功能是当当前Watcher推入到队列中。如果目前已经处于flushing状态,则根据id大小插入 flushSchedulerQueue  主要功能Flush both queues and run the watchers function flushSchedulerQueue () { flushing = true let watcher, id // 将需要渲染的Watcher进行排序 queue.sort((a, b) => a.id - b.id)

// 逐个执行Watcher的run 方法 for (index = 0; index < queue.length; index++) { watcher = queue[index] id = watcher.id has[id] = null watcher.run() }

// keep copies of post queues before resetting state const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice()

// 全部执行完后 进行重置 resetSchedulerState()

// 执行钩子函数 callActivatedHooks(activatedQueue) callUpdatedHooks(updatedQueue) }

Vue的pach流程、diff算法       一、pach过程    src/core/vdom/patch.js // 参数分别的含义:oldVnode:上次渲染时旧的Vnode数据,也可能会是一个Dom对象,vnode:此次重新render后返回的vnode // hydrating:表示是否是服务器渲染,removeOnly:transition-group使用的 // 后面两个暂时不知道是什么新加的鬼东西 return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) { // 如果最新的计算是没有Vnode的话,需要将原有的Vnode数据进行销毁 if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return }

let isInitialPatch = false
const insertedVnodeQueue = []
// 如果原有数据为空,说明是第一次渲染,则直接创建新的Dom树
if (isUndef(oldVnode)) {
  // empty mount (likely as component), create new root element
  isInitialPatch = true
  createElm(vnode, insertedVnodeQueue, parentElm, refElm)
// 存在已经渲染过的数据时,
} else {
  // 普通的Dom节点存在nodeType属性,即判定oldVnode是否是dom或者是Vnode
  const isRealElement = isDef(oldVnode.nodeType)
  // 如果是前后两次是相同的Vnode 判断标准主要是是判断 tag  是否是组件  data一样 后续会用到diff算法
  if (!isRealElement && sameVnode(oldVnode, vnode)) {
    // 直接patch到已经存在的已经存在的root中
    patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
	// 即新老Vnode是不相同的。 那主要的操作步骤就氛围  ①创建新节点②更新父的占位符节点③删除旧节点
  } else {
	// 如果oldVnode是一个原生Dom 会创建一个Vnode进行包裹
    if (isRealElement) {
	  // 开始挂载到真正的Element中

	  // 检查是否是服务端SSR渲染的内容,检查是否能够进行hydration
      if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
        oldVnode.removeAttribute(SSR_ATTR)
        hydrating = true
      }
	  // 检查hydrating是否为true  SSR逻辑不太懂 各位感兴趣自己看哈
      if (isTrue(hydrating)) {
        if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
          invokeInsertHook(vnode, insertedVnodeQueue, true)
          return oldVnode
        }
      }
	  // 创建空的node包裹一波去替换oldVnode
      oldVnode = emptyNodeAt(oldVnode)
    }
	// 替换旧的oldVnode中指向实际Element的指针
    const oldElm = oldVnode.elm
	// 获取旧的Element父类Dom,主要作用就是将来把心创建的Dom挂载在原有的父级上
    const parentElm = nodeOps.parentNode(oldElm)
	//创建真的Element !!!!!!创建新节点
    createElm(
      vnode,
      insertedVnodeQueue,
      oldElm._leaveCb ? null : parentElm,
      nodeOps.nextSibling(oldElm)
    )
	// 更新父的占位符节点
    if (isDef(vnode.parent)) {
      // 替换组件的根元素.
	  // 递归的替换父类的占位Vnode
      let ancestor = vnode.parent
      const patchable = isPatchable(vnode)
      while (ancestor) {
        for (let i = 0; i < cbs.destroy.length; ++i) {
          cbs.destroy[i](ancestor)
        }
        ancestor.elm = vnode.elm
        if (patchable) {
          for (let i = 0; i < cbs.create.length; ++i) {
            cbs.create[i](emptyNode, ancestor)
          }
          const insert = ancestor.data.hook.insert
          if (insert.merged) {
            // start at index 1 to avoid re-invoking component mounted hook
            for (let i = 1; i < insert.fns.length; i++) {
              insert.fns[i]()
            }
          }
        } else {
          registerRef(ancestor)
        }
        ancestor = ancestor.parent
      }
    }
	// 删除旧的节点
    if (isDef(parentElm)) {
      removeVnodes(parentElm, [oldVnode], 0, 0)
    } else if (isDef(oldVnode.tag)) {
	  // 递归的调用的destory钩子
      invokeDestroyHook(oldVnode)
    }
  }
}

invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm

}

// 主要功能是通过虚拟Vnode创建真实的Dom,并挂载在父节点中 function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) { vnode.isRootInsert = !nested // 检测是不是组件类型 如果是组件的话 开始重走vue子类创建过程 if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return }

const data = vnode.data
const children = vnode.children
const tag = vnode.tag
// 如果Vnode不存在tag,则表示可能是注释或者文本节点,使用不同方法直接插入到父节点
if (isDef(tag)) {
  // 调用平台的创建Dom的方法
  vnode.elm = vnode.ns
    ? nodeOps.createElementNS(vnode.ns, tag)
    : nodeOps.createElement(tag, vnode)
  setScope(vnode)

  if (__WEEX__) {
  // weex代码先删掉了  感兴趣可以自己看
  } else {
	// 创建子元素 主要是通过递归调用将children 作为入参调用当前createElm。需要特殊注意的是 目前还没有将当前DOM挂载到其父节点,调用此方法其实就是深度优先式的先创建子Dom
    createChildren(vnode, children, insertedVnodeQueue)
    if (isDef(data)) {
      invokeCreateHooks(vnode, insertedVnodeQueue)
    }
	// 即现在整个Dom子类元素已经创建完成 并挂载到到相应父节点。然后现在要做的就是把当前Vnode节点的对应的Dom挂载到其父节点上
    insert(parentElm, vnode.elm, refElm)
  }
// 是注释节点
} else if (isTrue(vnode.isComment)) {
  vnode.elm = nodeOps.createComment(vnode.text)
  insert(parentElm, vnode.elm, refElm)
// 是单独的文本内容
} else {
  vnode.elm = nodeOps.createTextNode(vnode.text)
  insert(parentElm, vnode.elm, refElm)
}

}

      二、diff     src/core/vdom/patch.js   updateChildren 算法主要是通过循环遍历寻找可重用的dom。定义了四个指针 oldStartIdx newStartIdx oldEndIdx newEndIdx

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

// 开始循环判断  无论老旧index都必须在范围内的合法数值
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  // 检查老的child Vnode是否存在数据,没有则指针向前或者向后
  if (isUndef(oldStartVnode)) {
    oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
  } else if (isUndef(oldEndVnode)) {
    oldEndVnode = oldCh[--oldEndIdx]
  // 检查old 第一个和new 第一个是否相同
  } else if (sameVnode(oldStartVnode, newStartVnode)) {
    patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
    oldStartVnode = oldCh[++oldStartIdx]
    newStartVnode = newCh[++newStartIdx]
  // 检查old最后一个和new最后一个是否相同
  } else if (sameVnode(oldEndVnode, newEndVnode)) {
    patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
    oldEndVnode = oldCh[--oldEndIdx]
    newEndVnode = newCh[--newEndIdx]
  // 检查old第一个和new最后一个是否相同
  } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
    patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
    canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
    oldStartVnode = oldCh[++oldStartIdx]
    newEndVnode = newCh[--newEndIdx]
  // 检查old最后一个和new第一个是否相同
  } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
    patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
    canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
    oldEndVnode = oldCh[--oldEndIdx]
    newStartVnode = newCh[++newStartIdx]
  } else {
	// 如果都不相同  先遍历剩余的old  返回一个old Vnode的key值和index的映射
    if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
	// 检查新的Vnode是否存在key值 存在的话直接从之前的映射获取。如果发现当前Vnode没有key值 则根据当前Vnode 在所有剩余的old中去检查是否相同
    idxInOld = isDef(newStartVnode.key)
      ? oldKeyToIdx[newStartVnode.key]
      : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
	// 说明old没有当前Vnode 即直接创建新的
    if (isUndef(idxInOld)) {
      createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
    } else {
      vnodeToMove = oldCh[idxInOld]
	  // 即使是相同的key值 仍然可能是不同的Vnode  还是会先校验是否相同 相同时直接复用 进行再次patchVnode
      if (sameVnode(vnodeToMove, newStartVnode)) {
        patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
        oldCh[idxInOld] = undefined
        canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
      } else {
        // 可能存在的情况是key值相同 但是不是一个Vnode 则创建新的
        createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
      }
    }
	// 无论如何 新的Vnode已经patch完成 index+1
    newStartVnode = newCh[++newStartIdx]
  }
}

React更新流程图