乐趣区

关于前端:深入浅出-Vue-响应式原理源码剖析

先看张图,理解一下大体流程和要做的事

初始化

在 new Vue 初始化的时候,会对咱们组件的数据 props 和 data 进行初始化,因为本文次要就是介绍响应式,所以其余的不做过多阐明来,看一下源码

源码地址:src/core/instance/init.js - 15 行

export function initMixin (Vue: Class<Component>) {
  // 在原型上增加 _init 办法
  Vue.prototype._init = function (options?: Object) {
    ...
    vm._self = vm
    initLifecycle(vm) // 初始化实例的属性、数据:$parent, $children, $refs, $root, _watcher... 等
    initEvents(vm) // 初始化事件:$on, $off, $emit, $once
    initRender(vm) // 初始化渲染:render, mixin
    callHook(vm, 'beforeCreate') // 调用生命周期钩子函数
    initInjections(vm) // 初始化 inject
    initState(vm) // 初始化组件数据:props, data, methods, watch, computed
    initProvide(vm) // 初始化 provide
    callHook(vm, 'created') // 调用生命周期钩子函数
    ...
  }
}

初始化这里调用了很多办法,每个办法都做着不同的事,而对于响应式次要就是组件内的数据 propsdata。这一块的内容就是在 initState() 这个办法里,所以咱们进入这个办法源码看一下

initState()

源码地址:src/core/instance/state.js - 49 行

export function initState (vm: Component) {vm._watchers = []
  const opts = vm.$options
  // 初始化 props
  if (opts.props) initProps(vm, opts.props)
  // 初始化 methods
  if (opts.methods) initMethods(vm, opts.methods)
  // 初始化 data 
  if (opts.data) {initData(vm)
  } else {
    // 没有 data 的话就默认赋值为空对象,并监听
    observe(vm._data = {}, true /* asRootData */)
  }
  // 初始化 computed
  if (opts.computed) initComputed(vm, opts.computed)
  // 初始化 watch
  if (opts.watch && opts.watch !== nativeWatch) {initWatch(vm, opts.watch)
  }
}

又是调用一堆初始化的办法,咱们还是直奔主题,取咱们响应式数据相干的,也就是 initProps()initData()observe()

一个一个持续扒,非得整明确响应式的全副过程

initProps()

源码地址:src/core/instance/state.js - 65 行

这里次要做的是:

  • 遍历父组件传进来的 props 列表
  • 校验每个属性的命名、类型、default 属性等,都没有问题就调用 defineReactive 设置成响应式
  • 而后用 proxy() 把属性代理到以后实例上,如把 vm._props.xx 变成 vm.xx,就能够拜访
function initProps (vm: Component, propsOptions: Object) {
  // 父组件传入子组件的 props
  const propsData = vm.$options.propsData || {}
  // 通过转换后最终的 props
  const props = vm._props = {}
  // 寄存 props 的 key,就算 props 值空了,key 也会在外面
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // 转换非根实例的 props
  if (!isRoot) {toggleObserving(false)
  }
  for (const key in propsOptions) {keys.push(key)
    // 校验 props 类型、default 属性等
    const value = validateProp(key, propsOptions, propsData, vm)
    // 在非生产环境中
    if (process.env.NODE_ENV !== 'production') {const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {warn(`hyphenatedKey 是保留属性,不能用作组件 prop`)
      }
      // 把 props 设置成响应式的
      defineReactive(props, key, value, () => {
        // 如果用户批改 props 收回正告
        if (!isRoot && !isUpdatingChildComponent) {warn(` 防止间接扭转 prop`)
        }
      })
    } else {
      // 把 props 设置为响应式
      defineReactive(props, key, value)
    }
    // 把不在默认 vm 上的属性,代理到实例上
    // 能够让 vm._props.xx 通过 vm.xx 拜访
    if (!(key in vm)) {proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

initData()

源码地址:src/core/instance/state.js - 113 行

这里次要做的是:

  • 初始化一个 data,并拿到 keys 汇合
  • 遍历 keys 汇合,来判断有没有和 props 里的属性名或者 methods 里的办法名重名的
  • 没有问题就通过 proxy() 把 data 里的每一个属性都代理到以后实例上,就能够通过 this.xx 拜访了
  • 最初再调用 observe 监听整个 data
function initData (vm: Component) {
  // 获取以后实例的 data 
  let data = vm.$options.data
  // 判断 data 的类型
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {data = {}
    process.env.NODE_ENV !== 'production' && warn(` 数据函数应该返回一个对象 `)
  }
  // 获取以后实例的 data 属性名汇合
  const keys = Object.keys(data)
  // 获取以后实例的 props 
  const props = vm.$options.props
  // 获取以后实例的 methods 对象
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {const key = keys[i]
    // 非生产环境下判断 methods 里的办法是否存在于 props 中
    if (process.env.NODE_ENV !== 'production') {if (methods && hasOwn(methods, key)) {warn(`Method 办法不能反复申明 `)
      }
    }
    // 非生产环境下判断 data 里的属性是否存在于 props 中
    if (props && hasOwn(props, key)) {process.env.NODE_ENV !== 'production' && warn(` 属性不能反复申明 `)
    } else if (!isReserved(key)) {
      // 都不重名的状况下,代理到 vm 上
      // 能够让 vm._data.xx 通过 vm.xx 拜访
      proxy(vm, `_data`, key)
    }
  }
  // 监听 data
  observe(data, true /* asRootData */)
}

observe()

源码地址:src/core/observer/index.js - 110 行

这个办法次要就是用来给数据加上监听器的

这里次要做的是:

  • 如果是 vnode 的对象类型或者不是援用类型,就间接跳出
  • 否则就给没有增加 Observer 的数据增加一个 Observer,也就是监听者
export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 如果不是 'object' 类型 或者是 vnode 的对象类型就间接返回
  if (!isObject(value) || value instanceof VNode) {return}
  let ob: Observer | void
  // 应用缓存的对象
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {ob = value.__ob__} else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 创立监听者
    ob = new Observer(value)
  }
  if (asRootData && ob) {ob.vmCount++}
  return ob
}

Observer

源码地址:src/core/observer/index.js - 37 行

这是一个类,作用是把一个失常的数据成可观测的数据

这里次要做的是:

  • 给以后 value 打上曾经是响应式属性的标记,防止反复操作
  • 而后判断数据类型

    • 如果是对象,就遍历对象,调用 defineReactive()创立响应式对象
    • 如果是数组,就遍历数组,调用 observe()对每一个元素进行监听
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // 根对象上的 vm 数量
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    // 给 value 增加 __ob__ 属性,值为 value 的 Observe 实例
    // 示意曾经变成响应式了,目标是对象遍历时就间接跳过,防止反复操作
    def(value, '__ob__', this)
    // 类型判断
    if (Array.isArray(value)) {
      // 判断数组是否有__proty__
      if (hasProto) {
        // 如果有就重写数组的办法
        protoAugment(value, arrayMethods)
      } else {
        // 没有就通过 def,也就是 Object.defineProperty 去定义属性值
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {this.walk(value)
    }
  }
  // 如果是对象类型
  walk (obj: Object) {const keys = Object.keys(obj)
    // 遍历对象所有属性,转为响应式对象,也是动静增加 getter 和 setter,实现双向绑定
    for (let i = 0; i < keys.length; i++) {defineReactive(obj, keys[i])
    }
  }
  // 监听数组
  observeArray (items: Array<any>) {
    // 遍历数组,对每一个元素进行监听
    for (let i = 0, l = items.length; i < l; i++) {observe(items[i])
    }
  }
}

defineReactive()

源码地址:src/core/observer/index.js - 135 行

这个办法的作用是定义响应式对象

这里次要做的是:

  • 先初始化一个 dep 实例
  • 如果是对象就调用 observe,递归监听,以保障不论构造嵌套多深,都能变成响应式对象
  • 而后调用 Object.defineProperty() 劫持对象属性的 getter 和 getter
  • 如果获取时,触发 getter 会调用 dep.depend() 把观察者 push 到依赖的数组 subs 里去,也就是依赖收集
  • 如果更新时,触发 setter 会做以下操作

    • 新值没有变动或者没有 setter 属性的间接跳出
    • 如果新值是对象就调用 observe() 递归监听
    • 而后调用 dep.notify() 派发更新
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {

  // 创立 dep 实例
  const dep = new Dep()
  // 拿到对象的属性描述符
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {return}
  // 获取自定义的 getter 和 setter
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {val = obj[key]
  }
  // 如果 val 是对象的话就递归监听
  // 递归调用 observe 就能够保障不论对象构造嵌套有多深,都能变成响应式对象
  let childOb = !shallow && observe(val)
  // 截持对象属性的 getter 和 setter
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // 拦挡 getter,当取值时会触发该函数
    get: function reactiveGetter () {const value = getter ? getter.call(obj) : val
      // 进行依赖收集
      // 初始化渲染 watcher 时拜访到须要双向绑定的对象,从而触发 get 函数
      if (Dep.target) {dep.depend()
        if (childOb) {childOb.dep.depend()
          if (Array.isArray(value)) {dependArray(value)
          }
        }
      }
      return value
    },
    // 拦挡 setter,当值扭转时会触发该函数
    set: function reactiveSetter (newVal) {const value = getter ? getter.call(obj) : val
      // 判断是否发生变化
      if (newVal === value || (newVal !== newVal && value !== value)) {return}
      if (process.env.NODE_ENV !== 'production' && customSetter) {customSetter()
      }
      // 没有 setter 的拜访器属性
      if (getter && !setter) return
      if (setter) {setter.call(obj, newVal)
      } else {val = newVal}
      // 如果新值是对象的话递归监听
      childOb = !shallow && observe(newVal)
      // 派发更新
      dep.notify()}
  })
}

下面说了通过 dep.depend 来做依赖收集,能够说 Dep 就是整个 getter 依赖收集的外围了

依赖收集

依赖收集的外围是 Dep,而且它与 Watcher 也是密不可分的,咱们来看一下

Dep

源码地址:src/core/observer/dep.js

这是一个类,它实际上就是对 Watcher 的一种治理

这里首先初始化一个 subs 数组,用来寄存依赖,也就是观察者,谁依赖这个数据,谁就在这个数组里,而后定义几个办法来对依赖增加、删除、告诉更新等

另外它有一个动态属性 target,这是一个全局的 Watcher,也示意同一时间只能存在一个全局的 Watcher

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) {
      // 调用 Watcher 的 addDep 函数
      Dep.target.addDep(this)
    }
  }
  // 派发更新(下一章节介绍)
  notify () {...}
}
// 同一时间只有一个观察者应用,赋值观察者
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]
}

Watcher

源码地址:src/core/observer/watcher.js

Watcher 也是一个类,也叫观察者(订阅者),这里干的活还挺简单的,而且还串连了渲染和编译

先看源码吧,再来捋一下整个依赖收集的过程

let uid = 0
export default class Watcher {
  ...
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {vm._watcher = this}
    vm._watchers.push(this)
    // Watcher 实例持有的 Dep 实例的数组
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.value = this.lazy
      ? undefined
      : this.get()
    if (typeof expOrFn === 'function') {this.getter = expOrFn} else {this.getter = parsePath(expOrFn)
    }
  }
  get () 
    // 该函数用于缓存 Watcher
    // 因为在组件含有嵌套组件的状况下,须要复原父组件的 Watcher
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 调用回调函数,也就是 upcateComponent,对须要双向绑定的对象求值,从而触发依赖收集
      value = this.getter.call(vm, vm)
    } catch (e) {...} finally {
      // 深度监听
      if (this.deep) {traverse(value)
      }
      // 复原 Watcher
      popTarget()
      // 清理不须要了的依赖
      this.cleanupDeps()}
    return value
  }
  // 依赖收集时调用
  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)) {
        // 把以后 Watcher push 进数组
        dep.addSub(this)
      }
    }
  }
  // 清理不须要的依赖(上面有)
  cleanupDeps () {...}
  // 派发更新时调用(上面有)
  update () {...}
  // 执行 watcher 的回调
  run () {...}
  depend () {
    let i = this.deps.length
    while (i--) {this.deps[i].depend()}
  }
}

补充:

  1. 咱们本人组件里写的 watch,为什么主动就能拿到新值和老值两个参数?
    就是在 watcher.run() 外面会执行回调,并且把新值和老值传过来
  2. 为什么要初始化两个 Dep 实例数组
    因为 Vue 是数据驱动的,每次数据变动都会从新 render,也就是说 vm.render() 办法就又会从新执行,再次触发 getter,所以用两个数组示意,新增加的 Dep 实例数组 newDeps 和上一次增加的实例数组 deps

依赖收集过程

在首次渲染挂载的时候,还会有这样一段逻辑

mountComponent 源码地址:src/core/instance/lifecycle.js - 141 行

export function mountComponent (...): Component {
  // 调用生命周期钩子函数
  callHook(vm, 'beforeMount')
  let updateComponent
  updateComponent = () => {// 调用 _update 对 render 返回的虚构 DOM 进行 patch(也就是 Diff)到实在 DOM,这里是首次渲染
    vm._update(vm._render(), hydrating)
  }
  // 为以后组件实例设置观察者,监控 updateComponent 函数失去的数据,上面有介绍
  new Watcher(vm, updateComponent, noop, {
    // 当触发更新的时候,会在更新之前调用
    before () {
      // 判断 DOM 是否是挂载状态,就是说首次渲染和卸载的时候不会执行
      if (vm._isMounted && !vm._isDestroyed) {
        // 调用生命周期钩子函数
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  // 没有老的 vnode,阐明是首次渲染
  if (vm.$vnode == null) {
    vm._isMounted = true
    // 调用生命周期钩子函数
    callHook(vm, 'mounted')
  }
  return vm
}

依赖收集:

  • 挂载之前会实例化一个渲染 watcher,进入 watcher 构造函数里就会执行 this.get() 办法
  • 而后就会执行 pushTarget(this),就是把 Dep.target 赋值为以后渲染 watcher 并压入栈(为了复原用)
  • 而后执行 this.getter.call(vm, vm),也就是下面的 updateComponent() 函数,外面就执行了 vm._update(vm._render(), hydrating)
  • 接着执行 vm._render() 就会生成渲染 vnode,这个过程中会拜访 vm 上的数据,就触发了数据对象的 getter
  • 每一个对象值的 getter 都有一个 dep,在触发 getter 的时候就会调用 dep.depend() 办法,也就会执行 Dep.target.addDep(this)
  • 而后这里会做一些判断,以确保同一数据不会被屡次增加,接着把符合条件的数据 push 到 subs 里,到这就曾经 实现了依赖的收集,不过到这里还没执行完,如果是对象还会递归对象触发所有子项的 getter,还要复原 Dep.target 状态

移除订阅

移除订阅就是调用 cleanupDeps() 办法。比方在模板中有 v-if 咱们收集了符合条件的模板 a 里的依赖。当条件扭转时,模板 b 显示进去,模板 a 暗藏。这时就须要移除 a 的依赖

这里次要做的是:

  • 先遍历上一次增加的实例数组 deps,移除 dep.subs 数组中的 Watcher 的订阅
  • 而后把 newDepIds 和 depIds 替换,newDeps 和 deps 替换
  • 再把 newDepIds 和 newDeps 清空
// 清理不须要的依赖
  cleanupDeps () {
    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
  }

派发更新

notify()

触发 setter 的时候会调用 dep.notify() 告诉所有订阅者进行派发更新

notify () {const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // 如果不是异步,须要排序以确保正确触发
      subs.sort((a, b) => a.id - b.id)
    }
    // 遍历所有 watcher 实例数组
    for (let i = 0, l = subs.length; i < l; i++) {
      // 触发更新
      subs[i].update()}
  }

update()

触发更新时调用

  update () {if (this.lazy) {this.dirty = true} else if (this.sync) {this.run()
    } else {
      // 组件数据更新会走这里
      queueWatcher(this)
    }
  }

queueWatcher()

源码地址:src/core/observer/scheduler.js - 164 行

这是一个队列,也是 Vue 在做派发更新时的一个优化点。就是说在每次数据扭转的时候不会都触发 watcher 回调,而是把这些 watcher 都增加到一个队列里,而后在 nextTick 后才执行

这里和下一大节 flushSchedulerQueue() 的逻辑有穿插的中央,所以要联结起来了解

次要做的是:

  • 先用 has 对象查找 id,保障同一个 watcher 只会 push 一次
  • else 如果在执行 watcher 期间又有新的 watcher 插入进来就会到这里,而后从后往前找,找到第一个待插入的 id 比以后队列中的 id 大的地位,插入到队列中,这样队列的长度就产生了变动
  • 最初通过 waiting 保障 nextTick 只会调用一次
export function queueWatcher (watcher: Watcher) {
  // 取得 watcher 的 id
  const id = watcher.id
  // 判断以后 id 的 watcher 有没有被 push 过
  if (has[id] == null) {has[id] = true
    if (!flushing) {
      // 最开始会进入这里
      queue.push(watcher)
    } else {
      // 在执行上面 flushSchedulerQueue 的时候,如果有新派发的更新会进入这里,插入新的 watcher,上面有介绍
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {i--}
      queue.splice(i + 1, 0, watcher)
    }
    // 最开始会进入这里
    if (!waiting) {
      waiting = true
      if (process.env.NODE_ENV !== 'production' && !config.async) {flushSchedulerQueue()
        return
      }
      // 因为每次派发更新都会引起渲染,所以把所有 watcher 都放到 nextTick 里调用
      nextTick(flushSchedulerQueue)
    }
  }
}

flushSchedulerQueue()

源码地址:src/core/observer/scheduler.js - 71 行

这里次要做的是:

  • 先排序队列,排序条件有三点,看正文
  • 而后遍历队列,执行对应 watcher.run()。须要留神的是,遍历的时候每次都会对队列长度进行求值,因为在 run 之后,很可能又会有新的 watcher 增加进来,这时就会再次执行到下面的 queueWatcher
function flushSchedulerQueue () {currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  // 依据 id 排序,有如下条件
  // 1. 组件更新须要按从父到子的程序,因为创立过程中也是先父后子
  // 2. 组件内咱们本人写的 watcher 优先于渲染 watcher
  // 3. 如果某组件在父组件的 watcher 运行期间销毁了,就跳过这个 watcher
  queue.sort((a, b) => a.id - b.id)

  // 不要缓存队列长度,因为遍历过程中可能队列的长度发生变化
  for (index = 0; index < queue.length; index++) {watcher = queue[index]
    if (watcher.before) {
      // 执行 beforeUpdate 生命周期钩子函数
      watcher.before()}
    id = watcher.id
    has[id] = null
    // 执行组件内咱们本人写的 watch 的回调函数并渲染组件
    watcher.run()
    // 查看并进行循环更新,比方在 watcher 的过程中又从新给对象赋值了,就会进入有限循环
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {warn(` 有限循环了 `)
        break
      }
    }
  }
  // 重置状态之前,先保留一份队列备份
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()
  resetSchedulerState()
  // 调用组件激活的钩子  activated
  callActivatedHooks(activatedQueue)
  // 调用组件更新的钩子  updated
  callUpdatedHooks(updatedQueue)
}

updated()

终于能够更新了,updated 大家都相熟了,就是生命周期钩子函数

下面调用 callUpdatedHooks() 的时候就会进入这里,执行 updated

function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {callHook(vm, 'updated')
    }
  }
}

至此 Vue2 的响应式原理流程的源码根本就剖析结束了,接下来就介绍一下下面流程中的不足之处

defineProperty 缺点及解决

应用 Object.defineProperty 实现响应式对象,还是有一些问题的

  • 比方给对象中增加新属性时,是无奈触发 setter 的
  • 比方不能检测到数组元素的变动

而这些问题,Vue2 里也有相应的解决文案

Vue.set()

给对象增加新的响应式属性时,能够应用一个全局的 API,就是 Vue.set() 办法

源码地址:src/core/observer/index.js - 201 行

set 办法接管三个参数:

  • target:数组或一般对象
  • key:示意数组下标或对象的 key 名
  • val:示意要替换的新值

这里次要做的是:

  • 先判断如果是数组,并且下标非法,就间接应用重写过的 splice 替换
  • 如果是对象,并且 key 存在于 target 里,就替换值
  • 如果没有 __ob__,阐明不是一个响应式对象,间接赋值返回
  • 最初再把新属性变成响应式,并派发更新
export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // 如果是数组 而且 是非法的下标
  if (Array.isArray(target) && isValidArrayIndex(key)) {target.length = Math.max(target.length, key)
    // 间接应用 splice 就替换,留神这里的 splice 不是原生的,所以才能够监测到,具体看上面
    target.splice(key, 1, val)
    return val
  }
  // 到这阐明是对象
  // 如果 key 存在于 target 里,就间接赋值,也是能够监测到的
  if (key in target && !(key in Object.prototype)) {target[key] = val
    return val
  }
  // 获取 target.__ob__
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  // 在 Observer 里介绍过,如果没有这个属性,就阐明不是一个响应式对象
  if (!ob) {target[key] = val
    return val
  }
  // 而后把新增加的属性变成响应式
  defineReactive(ob.value, key, val)
  // 手动派发更新
  ob.dep.notify()
  return val
}

重写数组办法

源码地址:src/core/observer/array.js

这里做的次要是:

  • 保留会扭转数组的办法列表
  • 当执行列表里有的办法的时候,比方 push,先把本来的 push 保存起来,再做响应式解决,再执行这个办法
// 获取数组的原型
const arrayProto = Array.prototype
// 创立继承了数组原型的对象
export const arrayMethods = Object.create(arrayProto)
// 会扭转原数组的办法列表
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
// 重写数组事件
methodsToPatch.forEach(function (method) {
  // 保留本来的事件
  const original = arrayProto[method]
  // 创立响应式对象
  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)
    // 派发更新
    ob.dep.notify()
    // 做完咱们须要的解决后,再执行本来的事件
    return result
  })
})

往期精彩

  • render 函数是怎么来的?深入浅出 Vue 中的模板编译
  • 深入浅出虚构 DOM 和 Diff 算法,及 Vue2 与 Vue3 中的区别
  • Vue3 的 7 种和 Vue2 的 12 种组件通信,值得珍藏
  • 最新的 Vue3.2 都更新了些什么理解一下
  • JavaScript 进阶知识点
  • 前端异样监控和容灾
  • 20 分钟助你拿下 HTTP 和 HTTPS,坚固你的 HTTP 常识体系

结语

如果本文对你有一丁点帮忙,点个赞反对一下吧,感激感激

退出移动版