vue 2+ 源码分析(2)

332 阅读3分钟

1 nextTick原理

1 特点

异步更新渲染试图,内部实现兼容了各个浏览器的微任务和宏任务方法。可单独调用,也存在于queueWatcher方法中。

2 结合源码,详细分析

  1. nextTick方法的实现,将所有方法都放在callbacks数组中,根据pending = false执行timefunc,timeFunc中异步执行pending=false

    • pending初始值是false,当异步执行flushCallbacks后又会重新赋值为false,阻止了同步一直调用刷新视图
    // ulit/next-tick.js
    function nextTick (cb?: Function, ctx?: Object) {
      let _resolve
      // 把所有需要更新的方法都存放于callbacks数组中
      callbacks.push(() => {
        if (cb) {
          try {
            cb.call(ctx)
          } catch (e) {
            handleError(e, ctx, 'nextTick')
          }
        } else if (_resolve) {
          _resolve(ctx)
        }
      })
      // pending初始值是false,当异步执行flushCallbacks后又会重新赋值为false,阻止了同步一直调用刷新视图
      if (!pending) {
        pending = true
        timerFunc()
      }
      // $flow-disable-line
      if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
          _resolve = resolve
        })
      }
    }
    
    
  2. timeFunc中兼容了各个浏览器的异步任务,包括promisemutatitionObservesetImmediatesetTimeout,来异步调用flashCallback,将pending=false,遍历执行所有的任务

    function flushCallbacks () {
      pending = false
      const copies = callbacks.slice(0)
      callbacks.length = 0
      for (let i = 0; i < copies.length; i++) {
        copies[i]()
      }
    }
    
    // Here we have async deferring wrappers using microtasks.
    // In 2.5 we used (macro) tasks (in combination with microtasks).
    // However, it has subtle problems when state is changed right before repaint
    // (e.g. #6813, out-in transitions).
    // Also, using (macro) tasks in event handler would cause some weird behaviors
    // that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
    // So we now use microtasks everywhere, again.
    // A major drawback of this tradeoff is that there are some scenarios
    // where microtasks have too high a priority and fire in between supposedly
    // sequential events (e.g. #4521, #6690, which have workarounds)
    // or even between bubbling of the same event (#6566).
    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 */
    // 如果promise支持的话。则直接用promise,微任务
    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' && ( // MutationObserver是H5自带的属性,IE11才兼容
      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)) { // setImmediate IE自带,chorme已支持
      // Fallback to setImmediate.
      // Technically 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)
      }
    }
    
    export function nextTick (cb?: Function, ctx?: Object) {
      let _resolve
      // 把所有需要更新的方法都存放于callbacks数组中
      callbacks.push(() => {
        if (cb) {
          try {
            cb.call(ctx)
          } catch (e) {
            handleError(e, ctx, 'nextTick')
          }
        } else if (_resolve) {
          _resolve(ctx)
        }
      })
      // pending初始值是false,当异步执行flushCallbacks后又会重新赋值为false,阻止了同步一直调用刷新视图
      if (!pending) {
        pending = true
        timerFunc()
      }
      // $flow-disable-line
      if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
          _resolve = resolve
        })
      }
    }
    

3 总结

2 computed原理

1 特点

computed:计算属性,同步执行,具有缓存作用,只有绑定的依赖发生变化时 ,才会更新,重新求值。

2 结合源码,详细分析

  1. 初始化initComputed方法中,获取computed中方法函数getter,实例化new Watcher后将参数带入 new Watcher({vm, getter,noop,{lazy: true}}),

    // state.js
    const computedWatcherOptions = { lazy: true }
    // 在initState中初始化initComputed
    function initComputed (vm: Component, computed: Object) {
      // $flow-disable-line
      const watchers = vm._computedWatchers = Object.create(null)
      // computed properties are just getters during SSR
      const isSSR = isServerRendering()
    
      for (const key in computed) {
        const userDef = computed[key]
        // getter是一个函数,如果computed中的key, computed: {getName(){return xx}} 不是一个函数,则创建返回函数
        const getter = typeof userDef === 'function' ? userDef : userDef.get
        if (process.env.NODE_ENV !== 'production' && getter == null) {
          warn(
            `Getter is missing for computed property "${key}".`,
            vm
          )
        }
    
        if (!isSSR) {
          // create internal watcher for the computed property.
          // 实例化Watcher,这个是计算属性的watcher, 主要还是getter函数
          watchers[key] = new Watcher(
            vm, // vue实例
            getter || noop, // 传入getter
            noop, // 空函数
            computedWatcherOptions // 默认{lazy: true} 
          )
        }
    
  2. Watcher类中,将判断传入options中的lazy,赋值给dirty,dirty主要用于后续判断是否立即执行计算函数,即存在缓存的关键。其中还有lazy=true是不执行操作,即返回undefined,跟watch有区别,因为是computed是根据defineProtoity去定义的,即当模板调用时才会去计算属性。

    class Watcher {
      constructor (
        vm: Component,
        expOrFn: string | Function, 
        cb: Function, // 没有传则用noop代替空函数
        options?: ?Object,
        isRenderWatcher?: boolean
      ) {
        this.vm = vm
        if (isRenderWatcher) {
          vm._watcher = this
        }
        vm._watchers.push(this)
        // options
        if (options) {
          this.deep = !!options.deep
          this.user = !!options.user
          this.lazy = !!options.lazy
          this.sync = !!options.sync
          this.before = options.before
        } else {
          this.deep = this.user = this.lazy = this.sync = false
        }
        this.cb = cb
        this.id = ++uid // uid for batching
        this.active = true
        this.dirty = this.lazy // for lazy watchers // dirty从this.lazy中取值
        this.deps = []
        this.newDeps = []
        this.depIds = new Set()
        this.newDepIds = new Set()
        this.expression = process.env.NODE_ENV !== 'production'
          ? expOrFn.toString()
          : ''
        // parse expression for getter
        if (typeof expOrFn === 'function') {
          // expOrFn 可以看作是从computed传过来的computed中函数
          this.getter = expOrFn
        } else {
          this.getter = parsePath(expOrFn)
          if (!this.getter) {
            this.getter = noop
            process.env.NODE_ENV !== 'production' && warn(
              `Failed watching path: "${expOrFn}" ` +
              'Watcher only accepts simple dot-delimited paths. ' +
              'For full control, use a function instead.',
              vm
            )
          }
        }
        // computed属性,lazy默认为true,则不处理
        this.value = this.lazy
          ? undefined
          : this.get()
      }
    
  3. 重新定义了defineComputed,如果是在key不是在vm上。

    // 重新定义了defineComputed,key不是在vm上的
        if (!(key in vm)) {
          defineComputed(vm, key, userDef)
        } else if (process.env.NODE_ENV !== 'production') {
          if (key in vm.$data) {
            warn(`The computed property "${key}" is already defined in data.`, vm)
          } else if (vm.$options.props && key in vm.$options.props) {
            warn(`The computed property "${key}" is already defined as a prop.`, vm)
          }
        }
      }
    
  4. defineComputed函数中,执行了createComputedGetter,返回value,再通过defineProperty去创建监听数据,也就是说可以通过{{name}}直接在模板中使用。

    createComputeGetter中根据dirty=true是直接执行watcher.evaluate,将dirty设置为false,然后返回value,当dirty=flase是直接返回return watcher.value,起到缓存的作用。

    当数据重新变化的时候,update函数中lazy:true会重新将dirty赋值为true

    function defineComputed (
      target: any,
      key: string,
      userDef: Object | Function
    ) {
      const shouldCache = !isServerRendering()
      // const sharedPropertyDefinition = {
    //   enumerable: true,
    //   configurable: true,
    //   get: noop,
    //   set: noop
    // }
      if (typeof userDef === 'function') {
        sharedPropertyDefinition.get = shouldCache
          ? createComputedGetter(key)
          : createGetterInvoker(userDef)
        sharedPropertyDefinition.set = noop
      } else {
        // ev
        sharedPropertyDefinition.get = userDef.get
          ? shouldCache && userDef.cache !== false
            ? createComputedGetter(key)
            : createGetterInvoker(userDef.get)
          : noop
        sharedPropertyDefinition.set = userDef.set || noop
      }
      if (process.env.NODE_ENV !== 'production' &&
          sharedPropertyDefinition.set === noop) {
        sharedPropertyDefinition.set = function () {
          warn(
            `Computed property "${key}" was assigned to but it has no setter.`,
            this
          )
        }
      }
      Object.defineProperty(target, key, sharedPropertyDefinition)
    }
    
    // createComputeGetter
    
    function createComputedGetter (key) {
      return function computedGetter () {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
          if (watcher.dirty) {
            // 将dirty设置为false,返回value,核心就是起到缓存的作用
            watcher.evaluate()
          }
          if (Dep.target) {
            watcher.depend()
          }
          return watcher.value
        }
      }
    }
    

3 watch原理

1 特点

这里的watch指的是用户自定义的watch

watch: {
    name: function(newval,oldval) {
        console.log(newval)
    },
    deep: true
}

name发生改变时,执行该函数,存在options, 常见的有deep: true深度遍历,immediate: true立即执行,默认是当数据改变时再执行。

2 结合源码,详细分析

  1. creteWatch中执行 vm.$watch(key, func, options)

    function createWatcher (
      vm: Component,
      expOrFn: string | Function,
      handler: any,
      options?: Object
    ) {
      if (isPlainObject(handler)) {
        options = handler
        handler = handler.handler
      }
      if (typeof handler === 'string') {
        handler = vm[handler]
      }
      // watch: {
      //   name: function(newval, oldval) {
      //     console.log(newval)
      //   }
      // }
      // 这里就是用户定义的watch方法, expOrFn: 'name'; handler: function
      return vm.$watch(expOrFn, handler, options)
    }
    
  2. $watch中,创建一个watcher,如果options.immediate=true,则直接执行watch中的方法

    关键:创建一个watcher,expOrFn: string, cb: function

    Vue.prototype.$watch = function (
        expOrFn: string | Function,
        cb: any,
        options?: Object
      ): Function {
        const vm: Component = this
        if (isPlainObject(cb)) {
          return createWatcher(vm, expOrFn, cb, options)
        }
        options = options || {}
        options.user = true
        // 关键,创建一个watcher,expOrFn: string, cb: function
        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()
        }
      }
    }
    
  3. Watcher中,因为 expOrFn是字符串,parsePath(expOrFn)转成函数,lazy=flase,则执行this.get()this.deep=true,则深度遍历。

    export default class Watcher {
      constructor (
        vm: Component,
        expOrFn: string | Function, // computed是一个fn,watch是一个str
        cb: Function, // 没有传则用noop代替空函数
        options?: ?Object,
        isRenderWatcher?: boolean
      ) {
        this.vm = vm
        if (isRenderWatcher) {
          vm._watcher = this
        }
        vm._watchers.push(this)
        // options
        if (options) {
          this.deep = !!options.deep
          this.user = !!options.user
          this.lazy = !!options.lazy
          this.sync = !!options.sync
          this.before = options.before
        } else {
          // watch中如果没有options:{deep: true},都是false
          this.deep = this.user = this.lazy = this.sync = false
        }
        this.cb = cb
        this.id = ++uid // uid for batching
        this.active = true
        this.dirty = this.lazy // for lazy watchers
        this.deps = []
        this.newDeps = []
        this.depIds = new Set()
        this.newDepIds = new Set()
        this.expression = process.env.NODE_ENV !== 'production'
          ? expOrFn.toString()
          : ''
        // parse expression for getter
        if (typeof expOrFn === 'function') {
          // expOrFn 可以看作是从computed传过来的computed中函数
          this.getter = expOrFn
        } else {
          this.getter = parsePath(expOrFn) // 字符串转成函数 msg -> function(){return msg}
          if (!this.getter) {
            this.getter = noop
            process.env.NODE_ENV !== 'production' && warn(
              `Failed watching path: "${expOrFn}" ` +
              'Watcher only accepts simple dot-delimited paths. ' +
              'For full control, use a function instead.',
              vm
            )
          }
        }
        // computed属性,lazy默认为true,则不处理
        this.value = this.lazy
          ? undefined
          : this.get()
      }
    
      /**
       * Evaluate the getter, and re-collect dependencies.
       */
      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
      }