最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • vue源码分析(九)

    正文概述 掘金(cherish553)   2020-12-04   427

    三、派发更新

    当执行响应式对象的set的时候,会触发派发更新的逻辑,首先他会尝试拿到value,如果value和传入的newVal的值相同,那么他会结束执行。之后会执行到val = newVal进行赋值,然后会判断newVal是否是一个对象,如果是对象那么会执行observe把这个对象变成响应式。最后会触发 dep.notify()进行派发更新

       set: function reactiveSetter (newVal) {
          const value = getter ? getter.call(obj) : val
          /* eslint-disable no-self-compare */
          if (newVal === value || (newVal !== newVal && value !== value)) {
            return
          }
          ...
          // #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()
        }
    

    dep.notify函数首先拿到subs,然后会遍历subs,执行subs[i].update()实际上就是执行和subs绑定的watcher的update(),wathcer的update方法会执行到queueWatcher(this)

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

    queueWatcher方法会先拿到watcher的id,watcher的id 是执行this.id = ++uid得到的,这个id是一个数字的自增,所以每一个wathcher都有一个唯一的id。has在最开始的时候是一个空的对象,拿到watcher的id,他会把has中这个id的key设置为true,如果当前的has没有这个id,那么,才会执行下边的逻辑,这样做的目的是,假如你修改了同一个渲染wathcer中的数据,他会执行多次的queueWatcher,会往之后的执行队列中放入重复的watcher和更新,这样做避免了添加重复的watcher,即使修改了同一个渲染watcher的数据,那么在之后的flushSchedulerQueue当中,也只会执行一次重新渲染,相当于是多次数据操作合并为了一次更新。之后会执行queue.push(watcher)把当前的wathcer,push到queue数组当中,最后执行nextTick(flushSchedulerQueue)

    // src/core/observer/scheduler.js
    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)
        }
      }
    }
    

    nextTick(flushSchedulerQueue),其中的nextTick是把flushSchedulerQueue放到了下一个队列当中执行。flushSchedulerQueue首先调用queue.sort((a, b) => a.id - b.id)对queue队列根据id做一个排序,wathcer的id是先创建的id小,之后创建的id大,也就是父组件的watcher是要小于子组件的id的。为什么要做一个排序,1.组件的更新是由父到子的过程。2.组件的user watcher会在渲染watcher创建之前先创建。3.如果父组件执行了销毁,那么子组件也会被销毁,他所做的数据处理将会被跳过。排序之后会遍历queue,首先判断watcher是否有before这个方法,渲染watcher在创建的时候,会传入before这个函数, if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') }实际上是会去触发他的beforeUpdate生命周期。之后执行has[id] = null,把has中的watcher id的保留置空,然后会执行watcher.run()watcher.run()方法首先会调用const value = this.get()对新的value进行求值,在调用this.get()的过程当中,就会再次触发wathcer的getter,也就是之前传入的 updateComponent = () => { vm._update(vm._render(), hydrating) }函数,对页面进行重新渲染。执行完run之后会调用resetSchedulerState对has进行重置,执行到flushSchedulerQueue的最后会调用callUpdatedHooks(updatedQueue),实际上是对queue队列中的wathcer去调用他们的update生命周期,执行顺序是由后向前,也就是会先触发子组件的updated然后再去触发父组件的updated函数。

    // src/core/observer/scheduler.js
    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.
        ...
      }
    
      ...
    
      resetSchedulerState()
    
      // call component updated and activated hooks
      ...
      callUpdatedHooks(updatedQueue)
    
      ...
    }
    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')
        }
      }
    }
    // src/core/observer/watcher.js
     run () {
        if (this.active) {
          const value = this.get()
          if (
            value !== this.value ||
            // Deep watchers and watchers on Object/Arrays should fire even
            // when the value is the same, because the value may
            // have mutated.
            isObject(value) ||
            this.deep
          ) {
            // set new value
            const oldValue = this.value
            this.value = value
            ...
          }
        }
      }
    

    四、nextTick

    nextTick的是在执行flushSchedulerQueue的时候进行的一层处理,平时在开发中使用的this.$nextTick()和Vue.nextTick(),都是由nextTick函数实现。nextTick函数首先会定义_resolve,然后push我们传入的内容,之后会判断pending,然后把pending的值进行修改这样就保证了, 只会执行一次timerFunc

    // src/core/util/next-tick.js
    export function nextTick (cb?: Function, ctx?: Object) {
      let _resolve
        .push(() => {
        if (cb) {
          try {
            cb.call(ctx)
          } catch (e) {
            handleError(e, ctx, 'nextTick')
          }
        } else if (_resolve) {
          _resolve(ctx)
        }
      })
      if (!pending) {
        pending = true
        timerFunc()
      }
      // $flow-disable-line
      if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
          _resolve = resolve
        })
      }
    }
    

    timerFunc的目的是把当前放入的任务,放到下一个执行队列去执行,其中做了大量的降级判断和处理,首先他会判断if (typeof Promise !== 'undefined' && isNative(Promise)) 当前环境是否支持Promise,如果支持那么会通过promise.then(),来实现异步队列(promise.then是一个微任务),如果不支持promsie的话,他会判断当前环境是否支持MutationObserverMutationObserver函数提供了监视对DOM树所做更改的能力,他也是一个微任务,如果支持的话,他会首先创建一个变量let counter = 1,然后执行 const observer = new MutationObserver(flushCallbacks),把flushCallbacks作为监听的回调函数,之后创建一个文本节点,通过observe方法去监听这个文本节点,其中他设置了characterData: true这个属性是用于监听这个元素的文本是否被修改,默认是flase,在每一次触发timerFunc的时候,对文本节点的内容进行修改, counter = (counter + 1) % 2; textNode.data = String(counter),这样去模的操作,会保证这个文本节点的内容只会在1和0之间修改,不会造成最终这个文本节点的内容溢出。这样通过修改文本节点的内容,来触发MutationObserverobserve方法,再去触发对应的flushCallbacks回调函数。如果不支持MutationObserver的话他会采取setImmediate,如果不支持setImmediate函数,那么他会最终采用,setTimeout(flushCallbacks, 0)作为下一个任务队列,这两个方式都是下一个队列当中的宏任务。也就是说vue的渲染实际上是异步的,他会通过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.
      // 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)
      }
    }
    

    五、set

    在vue当中,我们给data中的数据进行修改,如果是一个对象,我们给他直接添加属性,或者我们在修改数组的时候,我们这样赋值arr[2]=xxx,这两种操作对于vue都是监听不到的,在页面中不会进行重新重新渲染,vue官方也提供了set这个api来帮助我们进行操作。set第第一个参数可以传递一个数组或者一个对象,第二个参数传入的是key,第三个参数传的是value。如果传入的值是一个数组,首先他会让target(当前数组)的length等于当前最大的index,然后会调用当前数组的splice方法对内容进行一个插入。如果是一个对象的话,那么他会首先判断这个key是否存在于当前的对象当中, 如果存在的话那么他会直接返回,不需要进行重新的渲染。如果当前的key不存在于当前的对象当中,也就是说他是一个新的key,那么他会尝试拿到当前对象的__ob__属性,然后他会判断当前的对象是否是根部的data,如果是的话,他会报一个警告。如果!ob也就是说不是一个响应式对象,也就是说给一个普通对象进行,set操作,那么他会往这个普通对象当中去进行一个赋值。如果该对象是一个响应式对象,那么他会首先执行defineReactive(ob.value, key, val),把新添加的key,变为一个响应式对象,最后手动调用ob.dep.notify(),调用ob.dep.notify()之后会触发页面的重新渲染,他会在渲染的时候执行响应式对象的时候let childOb = !shallow && observe(val),首先定义一个childOb,把其中的对象定义下来,然后通过childOb.dep.depend()进行对象的依赖收集,这样调用ob.dep.notify()就会通知到对应的渲染watcher进行重新渲染。

    // 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 (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
      }
      if (!ob) {
        target[key] = val
        return val
      }
      defineReactive(ob.value, key, val)
      ob.dep.notify()
      return val
    }
    

    如果是一个数组,他会调用数组的splice方法。在创建new Observer的时候,如果传入的是一个数组,他会调用protoAugmentcopyAugment方法。protoAugment方法他会传入当前的数组和arrayMethods,arrayMethods实际上是Object.create(Array.prototype),也就是拿到了Array的原型对象。protoAugment做的事是target.__proto__ = src也就是让当前数组的原型指向了src,也就是形成了,当前数组的原型指向一个空的对象,这个空的对象的原型指向了Array.prototype

     if (Array.isArray(value)) {
          if (hasProto) {
            protoAugment(value, arrayMethods)
          } else {
            copyAugment(value, arrayMethods, arrayKeys)
          }
          this.observeArray(value)
        }
        /**
     * Augment a target Object or Array by intercepting
     * the prototype chain using __proto__
     */
    function protoAugment (target, src: Object) {
      /* eslint-disable no-proto */
      target.__proto__ = src
      /* eslint-enable no-proto */
    }
    ...
    

    在初始化的时候,还会对arrayMethods进行一个补充,methodsToPatch中定义了一些数组的方法,之后对这些方法数组进行了遍历,首先定义const original = arrayProto[method],然后通过def函数,对数据进行劫持,其中的value就是mutatormutator函数首先通过original.apply(this, args)借带了Array.prototype上对应的方法,拿到result,在最后把result,return出去。返回result的过程中,会先定义inserted插入的值,如果有插入的值的话,会通过ob.observeArray绑定响应式,最后调用ob.dep.notify()进行重新渲染。无论是set还是调用了push,splice这些方法,最终都是由vue去触发了ob.dep.notify()进行重新的渲染。所以对data中的数据如果直接使用Array.prototype[methods]修改,也是不会动态的渲染到页面中。

    // src/core/observer/array.js
    const methodsToPatch = [
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse'
    ]
    methodsToPatch.forEach(function (method) {
      // cache original 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)
        // notify change
        ob.dep.notify()
        return result
      })
    })
    

    起源地下载网 » vue源码分析(九)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元