06-拓展$set、$delete、$watch方法以及vue异步更新是怎么实现的

1,014 阅读3分钟

Vue 实例对象的 $set 方法对应的是 set 方法, $delete 对应的是 del

位置: src/core/instance/state.js

Vue.prototype.$set = set
Vue.prototype.$delete = del

$set

位置:src/core/observer/index.js

/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
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)
    // 做替换操作
    target.splice(key, 1, val)
    return val
  }
  // 对象——已经存在,说明是响应式的,直接更新即可
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  // 对象——之前不存在此属性的情况
  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
  }
  // ob不存在,target不是响应式的
  if (!ob) {
    target[key] = val
    return val
  }
  // 对新添加的key或者val做响应式
  defineReactive(ob.value, key, val)
  // 通知更新
  ob.dep.notify()
  return val
}

$delete

位置:src/core/observer/index.js

/**
 * Delete a property and trigger change if necessary.
 */
export function del (target: Array<any> | Object, key: any) {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
    return
  }
  if (!hasOwn(target, key)) {
    return
  }
  delete target[key]
  if (!ob) {
    return
  }
  // 通知更新
  ob.dep.notify()
}

$watch

位置: src/core/instance/state.js

// 使用
watch:{
    list:{
      handler(newVal,oldVal){},
      immediate:true,
      deep:true
    }
}
this.$watch('exp',function(newVal,oldVal){
    
})
// 源码
Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    // 如果回调cb是对象
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true // 表明这是一个用户watcher,而不是组件的render watcher
    const watcher = new Watcher(vm, expOrFn, cb, options)
    // 设置immediate理解执行一次cb
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    // 返回取消监听的函数
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}

异步更新队列

1. 两个核心点

  • vue更新dom是异步的
  • 是批量更新的

同时对同一个数据进行多次操作的时候,dep通知watcher更新的时候,watcher做了去重

2. 测试代码

<body>
    <div id="demo">
        <h1>异步更新</h1>
        <p id="p1">{{foo}}</p>
    </div>
    <script>
        // 创建实例
        const app = new Vue({
            el: '#demo',
            data: { foo: '' },
            mounted() {
                setInterval(() => {                    
                    this.foo = 1 // 执行入队
                    this.foo = 2
                    this.foo = 3 // 起作用的值是最后一行
                    console.log(p1.innerHTML)
                    this.$nextTick(() => {
                        console.log(p1.innerHTML)
                    })
                }, 1000);
            }
        });
    </script>
</body>

上面的代码给foo进行了3次赋值,这时候触发了Object.defineProperty中的3次set,那么dep通知了watcher3次去更新

3. 数据劫持-数据变更时通知watcher更新

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
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        // 将新值赋到旧值上
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      // 通知更新
      dep.notify()
    }
})

注意:上面最重要的两点
// 将新值赋到旧值上
val = newVal
// 通知更新
dep.notify()

4. dep.js

通知watcher去更新

notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()// 更新
    }
}

5. watcher.js-入队操作

更新真正执行的是queueWatcher方法,也就是watcher入队操作,这时候需要思考一个问题,上面操作了3次foo,那么会入队3次吗?

/**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
    // watcher入队操作
      queueWatcher(this)
    }
}

6. queueWatcher方法

这里做了去重,所以上面3次对foo重新赋值,只有第一次入队了,入队操作后悔执行nextTick方法,那么这时候大家就会发出疑问了,如果是这样的话,foo最终的值是不是1呢?接着往下看nextTick

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // 去重
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
    // 不存在队列中才入队
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      // 异步刷新队列,下一帧,下一个时刻,下一个更新周期
      nextTick(flushSchedulerQueue)
    }
  }
}

7. nextTick怎么实现异步刷新队列?

let timerFunc

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Techinically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

整个vue在初始化的时候会判断一下当前的运行环境,来决定我们用什么做异步,

  • 首选Promise

promise.then是微任务,微任务会在浏览器更新前执行,也就是说微任务执行完,浏览器才更新

  • MutationObserver

  • setImmediate

  • setTimeout

8.执行队列中的方法

/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }

watcher.run()

综上所述:foo的数据是实实在在的修改了3次,但是更新到页面仅仅更新了1次,而且因为更新操作是异步的,所以尽管watcher只有第一次入队成功了,但是最后执行队列中的任务时,foo的值已经第3次修改完了,所以更新到页面上的时候,取了第3次的值

最后

vue的异步更新,涉及到了微任务与宏任务,单独出个专题介绍吧~

参考

vue源码 标题