美文网首页
Vue 源码解析 - 数据驱动与响应式原理

Vue 源码解析 - 数据驱动与响应式原理

作者: Whyn | 来源:发表于2020-04-12 11:12 被阅读0次

    [TOC]

    数据驱动与响应式原理

    Vue 的一个核心思想就是 数据驱动,即数据会驱动界面,如果想修改界面,直接对相应的数据进行修改即可, DOM 元素会自动更新。

    数据驱动 的思想,其实质是将界面 DOM 元素映射到数据上,解耦了业务与视图,这样可以让我们只专注于对数据的操作,而无须与视图进行交互,简化了业务逻辑,代码更加清晰。

    :前端开发中,视图的展示本身就是由数据进行驱动的。传统前端开发过程中,一般都是先从后端获取数据,然后手动将数据渲染到前端 DOM 元素上,由于一个页面上可能存在很多 DOM 元素需要进行渲染,这样我们的业务代码中就充斥着许多与业务无关的 DOM 操作,代码会显得臃肿和混乱。而 数据驱动 思想可以自动完成渲染到 DOM 元素这个操作,对于我们的代码来说,只需进行数据获取即可,这才是更加纯粹的数据驱动模型。

    举个栗子:一个最简单的数据驱动例子如下所示:

    <div id="app">
        <h1>{{message}}</h1>
    </div>
    
    <script>
        const vm = new Vue({
            el: '#app',
            data: {
                message: 'Hello Vue!'
            }
        });
    </script>
    

    h1元素的内容由Vue实例的数据data.message进行驱动,并且,当我们手动更改message的值时,可以看到,视图同时也随之更新了。

    由上,我们可以知道,数据驱动 其实包含两部分内容:组件挂载响应式

    • 组件挂载:是将初始数据渲染到真实 DOM 上的过程
    • 响应式:是数据的更新驱动视图的过程

    组件挂载 内容请参考:Vue 源码解析 - 组件挂载

    以下我们主要对 Vue响应式原理 进行解析。

    Vue 源码解析 - 主线流程 中,我们知道,当new Vue(Options)的时候,实际上调用的是_init函数:

    // src/core/instance/index.js
    function Vue(options) {
        ...
        this._init(options)
    }
    

    _init函数中, 会执行一系列的初始化,其中就包含有initState(vm)

    // src/core/instance/init.js
    export function initMixin(Vue: Class<Component>) {
        Vue.prototype._init = function (options?: Object) {
            ...
            // 初始化 props、methods、data、computed 与 watch
            initState(vm)
            ...
        }
    }
    

    initState函数主要对Vue组件的props,methods,data,computedwatch等状态进行初始化:

    // src/core/instance/state.js
    export function initState(vm: Component) {
        vm._watchers = []
        const opts = vm.$options
        //   初始化 options.props
        if (opts.props) initProps(vm, opts.props)
        //   初始化 options.methods
        if (opts.methods) initMethods(vm, opts.methods)
        if (opts.data) {
            // 初始化 options.data
            initData(vm)
        } else {
            // 没有 options.data 时,绑定为一个空对象
            observe(vm._data = {}, true /* asRootData */)
        }
        //   初始化 options.computed
        if (opts.computed) initComputed(vm, opts.computed)
        if (opts.watch && opts.watch !== nativeWatch) {
            // 初始化 options.watcher
            initWatch(vm, opts.watch)
        }
    }
    

    我们主要来看下initState函数中的initData(vm)操作:

    // core/instance/state.js
    function initData (vm: Component) {
        let data = vm.$options.data
        data = vm._data = typeof data === 'function'
            ? getData(data, vm)     // getData:解析出原本的 options.data
            : data || {}            // data 不是函数,直接使用
        if (!isPlainObject(data)) { // data 不是对象,开发环境会给出警告
            data = {}
            ...
        }
        // proxy data on instance
        const keys = Object.keys(data)
        ...
        let i = keys.length
        while (i--) {
            const key = keys[i]
            ...
            else if (!isReserved(key)) {
                // 为 vm 组件对象设置与 key 同名的访问器属性,作为 key 的代理,真实值存储于 vm._data 对象中
                // 这步操作过后,vm 就具备了与 options.data 所有的同名 key 的访问器属性,因此,使用 this.xxx 操作
                // data 中的数据就是操作组件对象 vm 的访问器属性,相当于 options.data 的数据被组件对象拦截了。 
                proxy(vm, `_data`, key)
            }
        }
        // observe data
        observe(data, true /* asRootData */)
    }
    export function getData (data: Function, vm: Component): any {
        ...
        return data.call(vm, vm)
        ...
    }
    
    // src/shared/util.js
    /**
     * Strict object type check. Only returns true
     * for plain JavaScript objects.
     */
    export function isPlainObject (obj: any): boolean {
        return _toString.call(obj) === '[object Object]'
    }
    
    // src/core/util/lang.js
    /**
     * Check if a string starts with $ or _
     */
    export function isReserved (str: string): boolean {
        const c = (str + '').charCodeAt(0)
        return c === 0x24 || c === 0x5F
    }
    

    initData函数会获取我们在new Vue(Options)时传递进去的Options.data数据,然后进行遍历,对其进行proxy操作,最后会为Options.data进行observe操作。

    下面我们先对proxy进行分析,其源码如下:

    // core/instance/state.js
    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
        }
        // 为 target 添加访问器属性 key
        Object.defineProperty(target, key, sharedPropertyDefinition)
    }
    
    const sharedPropertyDefinition = {
        enumerable: true,
        configurable: true,
        get: noop,
        set: noop
    }
    

    proxy函数的功能就是通过Object.definePropertytarget对象添加字段为key的访问器属性,并设置其get/set的具体操作。

    当我们调用proxy(vm, '_data', key)时,就是为vm添加与Options.data相同键值的访问器属性,这些属性的get/set方法内部实现为:this['_data'],也即:当调用this.xxxthisVue实例,xxxOptions.data中的某个key)时,会被代理到sharedPropertyDefinition.get / sharedPropertyDefinition.set,其结果为:this._data.xxx

    简而言之,proxy函数的作用就是让 Vue 实例创建与Options.data相同键值的访问器属性,使得在源码中可以使用this(即Vue实例)访问Options.data中的同名key的值,相当于代理了Options.data

    initData的最后,还为Options.data做了observe操作,我们接下来查看下observe函数源码:

    // src/core/observer/index.js
    /**
     * Attempt to create an observer instance for a value,
     * returns the new observer if successfully observed,
     * or the existing observer if the value already has one.
     */
    export function observe(value: any, asRootData: ?boolean): Observer | void {
        // value 必须为对象(且非 VNode),否则就无须观察
        if (!isObject(value) || value instanceof VNode) {
            return
        }
        let ob: Observer | void
        // 有 __ob__ 属性表示 value 已经被 Observer 了
        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
    }
    

    observe函数的注释可以知道,observe函数会为要被观察的数据对象(即Options.data等)创建一个观察者实例Observer,该函数主要做了如下几件事:

    • 判断数据是否需要被观察:如果传递进来的value不是对象,或者是VNode对象,则无须进行观察。

    这里进行判断主要是因为Observer会对被观察对象的每一个key都进行监控,代码编写上涉及一个递归过程(见后文),停止的条件就是非对象类型或是VNode类型。

    • 判断数据是否有__ob__属性:如果数据对象拥有__ob__属性,且该属性是一个Observer,说明这个数据对象已经被监控了,故无须再次进行监控。
    • 监控数据对象:如果数据未被监控,则创建一个Observer实例监控该数据。

    我们主要来看下Observer实例的创建过程,即:new Observer(value)

    // src/core/observer/index.js
    /**
     * Observer class that is attached to each observed
     * object. Once attached, the observer converts the target
     * object's property keys into getter/setters that
     * collect dependencies and dispatch updates.
     */
    export class Observer {
        value: any;
        dep: Dep;
        vmCount: number; // number of vms that have this object as root $data
    
        constructor(value: any) {
            this.value = value
            this.dep = new Dep()
            this.vmCount = 0
            // 为 value 对象添加 __ob__ 属性,其值指向当前 Observer
            def(value, '__ob__', this)
            if (Array.isArray(value)) {
                ...
                this.observeArray(value)
            } else {
                this.walk(value)
            }
        }
    
        /**
         * Walk through all properties and convert them into
         * getter/setters. This method should only be called when
         * value type is Object.
         */
        walk(obj: Object) {
            const keys = Object.keys(obj)
            for (let i = 0; i < keys.length; i++) {
                defineReactive(obj, keys[i])
            }
        }
    
        /**
         * Observe a list of Array items.
         */
        observeArray(items: Array<any>) {
            for (let i = 0, l = items.length; i < l; i++) {
                observe(items[i])
            }
        }
    }
    
    // src/core/util/lang.js
    /**
    * Define a property.
    */
    export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
     Object.defineProperty(obj, key, {
       value: val,
       enumerable: !!enumerable,
       writable: true,
       configurable: true
     })
    }
    

    Observer的注释可以看到,每个需要被观察的数据对象都会被Observer实例监控,Observer实例会把被观察数据对象的所有键值属性转换为getter / setter函数(即将被观察数据对象的所有键值属性转换为访问器属性),从而可以进行 依赖收集派发更新 操作。其具体的操作细节如下:

    • 创建Dep对象:每一个Observer对象内部维护一个Dep实例,Dep的源码内容如下:

      // src/core/observer/dep.js
      let uid = 0
      
      /**
       * A dep is an observable that can have multiple
       * directives subscribing to it.
       */
      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) {
                  Dep.target.addDep(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()
              }
          }
      }
      

      可以看到,Dep内部维护了一个Watcher数组subs,具备添加Watcher和删除Watcher的能力,且其具备通知功能notify,因此,当需要更新数据时,Dep可以通知到相应的Watcher,让它们重新进行更新过程。

    • 为数据对象添加__ob__属性:此处相当于一个标记作用,表明该数据对象已经被进行观察。

      def(value, '__ob__', this)
      
    • 进行响应式设置:其代码如下:

      if (Array.isArray(value)) {
          ...
          this.observeArray(value)
      } else {
          this.walk(value)
      }
      

      响应式设置对于数据对象的类型做了区分,共有两种形式:

      • 数组类型:如果数据对象是数组,那么就调用observeArray方法,查看下其源码:
      // src/core/observer/index.js
      /**
       * Observe a list of Array items.
       */
      observeArray(items: Array<any>) {
          for(let i = 0, l = items.length; i<l; i++) {
          observe(items[i])
      }
        }
      

      其实逻辑很简单,如果是数组,那么遍历每一个数据元素,依次进行observe设置。因此,数组类型的数据对象每个元素都会有各自一个Observer对其进行监控。

      :在 Vue 中,data必须为一个对象,因此是不会进行observeArray这个流程的,但是如果数据对象的某个key的值为对象/数组,Vue 则会对该值进行observe操作,因此该值是数组的话,则会进入observeArray流程,从而具备响应式,比如下面的例子:

      const vm = new Vue({
          el: "#app",
          data: {
              message: [{msg: 'Hello Vue'}]
          },
      });
      

      data.message为数组,则其会进入observeArray流程,从而让data.message[0].msg也具备响应式功能。

    • 对象类型:如果数据对象不是数组,直接调用walk函数,其源码如下:

      // src/core/observer/index.js
      /**
       * Walk through all properties and convert them into
       * getter/setters. This method should only be called when
       * value type is Object.
       */
      walk(obj: Object) {
          const keys = Object.keys(obj)
          for (let i = 0; i < keys.length; i++) {
              defineReactive(obj, keys[i])
          }
      }
      

      walk函数主要就是遍历数据对象的所有key,然后对每个key都进行响应式设置,具体设置如下:

      // src/core/observer/index.js
      /**
       * Define a reactive property on an Object.
       */
      export function defineReactive(
          obj: Object,
          key: string,
          val: any,
          customSetter?: ?Function,
          shallow?: boolean
      ) {
          // 每个 Dep 对应一个 key
          const dep = new Dep()
      
          // 获取对象 obj 的 key 的属性描述符
          const property = Object.getOwnPropertyDescriptor(obj, key)
          if (property && property.configurable === false) {
              return
          }
      
          // cater for pre-defined getter/setters
          const getter = property && property.get // 如果是访问器属性,直接获取 getter,否则为空
          const setter = property && property.set // 如果是访问器属性,直接获取 setter,否则为空
          // 如果不是访问器属性,并且当前方法传入的参数个数为 2,则直接获取当前 key 的值
          if ((!getter || setter) && arguments.length === 2) {
              val = obj[key]
          }
      
          // 如果当前 key 的值为对象,则递归进行观察
          let childOb = !shallow && observe(val)
          // 将 obj 的 key 设置为访问器属性,从而具备拦截功能
          Object.defineProperty(obj, key, {
              enumerable: true,
              configurable: true,
              get: function reactiveGetter() {
                  ...
              },
              set: function reactiveSetter(newVal) {
                  ...
              }
          })
      }
      

      defineReactive函数源码可以看到,其为数据对象的每个key又创建了一个Dep实例,前面说过,Dep内部维护了一系列Watcher实例,其具备通知功能,而在这里,可以看出,Dep也具备依赖收集功能。

      当创建完成一个Dep实例后,会判断一下数据对象当前key是否是一个访问器属性(即带有gettter / setter),如果是一个普通属性且defineReactive参数为 2 的话,那就先获取其值。

      如果当前key的值是一个对象的话,那么会通过observe函数对其值进行观测。这里可以看出,如果数据对象某个key的值是一个对象,那么无论对象的嵌套多深,Vue 都能进行监控(即对key的值进行响应式设置)。

      最后通过Object.defineProperty将数据对象的当前key设置为访问器属性,从而具备动态拦截功能,其具体设置如下:

      • 首先看下访问器属性的getter函数:

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

        getter函数首先会获取当前key的值,然后会进行依赖收集dep.depend(),但是在进行依赖收集之前,会先判断下Dep.target,看下源码:

        // src/core/observer/dep.js
        export default class Dep {
            static target: ?Watcher;
            ...
        }
        

        Dep.target是一个Watcher实例,这个Watcher实例的创建过程我们在 Vue 源码解析 - 组件挂载 已经讲过,大致过程为:

        在进行组件挂载时,mount函数内部通过mountComponent进行挂载,而mountComponent内部有如下一段代码:

        // src/core/instance/lifecycle.js
        export function mountComponent(...): Component {
            ...
            updateComponent = () => {
                vm._update(vm._render(), hydrating);
            };
        
            new Watcher(
                vm,
                updateComponent,
                noop,
                {
                    before() {
                        if (vm._isMounted && !vm._isDestroyed) {
                            callHook(vm, "beforeUpdate");
                        }
                    },
                },
                true /* isRenderWatcher */
            );
            return vm;
        }
        

        所以,在组件进行挂载的时候,就会创建一个Watcher实例,查看下Watcher源码:

        // src/core/observer/watcher.js
        export default class Watcher {
            ...
            constructor(...) {
                ...
                this.value = this.lazy
                    ? undefined
                    : this.get()
            }
        
            /**
             * Evaluate the getter, and re-collect dependencies.
             */
            get() {
                pushTarget(this)
                ...
                value = this.getter.call(vm, vm)
                ...
                return value
            }
            ...
        }
        

        new Watcher的时候,其构造函数内会调用this.get(),而this.get()内第一个操作就是pushTarget,其源码如下:

        // src/core/observer/dep.js
        export function pushTarget(target: ?Watcher) {
            ...
            Dep.target = target
        }
        

        这里就对Dep.target进行了设置。

        然后我们再回到Watcher构造函数,在pushTarget后,会调用this.getter.call函数,其实就是调用updateComponent函数:

        updateComponent = () => {
            vm._update(vm._render(), hydrating);
        };
        

        这个过程就会触发vm._render函数,我们知道,这个函数最终会渲染出一个VNode,生成VNode的过程会涉及到对数据对象(比如vm.data)的获取,此时就会触发数据对象相应keygetter函数。

        到这里,我们就理清了Dep.target的设置位置以及数据对象获取拦截的过程。

        下面就继续回到getter函数,接着看下具体的依赖收集步骤:

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

        首先调用dep.depend方法,查看下其源码:

        // src/core/observer/dep.js
        depend() {
            if (Dep.target) {
                Dep.target.addDep(this)
            }
        }
        addSub(sub: Watcher) {
            this.subs.push(sub)
        }
        
        // src/core/observer/watcher.js
        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)
                }
            }
        }
        

        这里最终调用的是dep.addSub(this),于是这里就成功地将关注某个keyWatcher实例添加到Dep实例中了。

        收集完成当前key的依赖后,还对childOb进行了判断:

        // src/core/observer/index.js
        export function defineReactive(...) {
            // 如果当前 key 的值为对象,则递归进行观察
            let childOb = !shallow && observe(val)
            // 将 obj 的 key 设置为访问器属性,从而具备拦截功能
            Object.defineProperty(obj, key, {
                ...
                get: function reactiveGetter() {
                    ...
                        if (childOb) {
                            childOb.dep.depend()
                            if (Array.isArray(value)) {
                                dependArray(value)
                            }
                        }
                    }
                    return value
                },
                ...
            })
        }
        
        function dependArray(value: Array<any>) {
            for (let e, i = 0, l = value.length; i < l; i++) {
                e = value[i]
                e && e.__ob__ && e.__ob__.dep.depend()
                if (Array.isArray(e)) {
                    dependArray(e)
                }
            }
        }
        

        childOb是对数据对象当前key的值观测的对象Observer,如果当前key的值为对象,那么也同样会进行依赖收集childOb.dep.depend(),依赖收集的Watcher放置到该值数据对象的Observer.dep上。
        如果当前key的值为数组,那么还会对数组的每个元素进行依赖收集dependArray(value)

        getter函数中,dep.depend()主要是对当前key进行依赖收集,监控的是当前key的更改;而childOb.dep.depend()是对当前key的值进行依赖收集,这样当key的值的某个数据更改时,才能监控得到。

        简而言之,每个数据对象都对应一个Observer,数据对象的每个key都对应一个DepDep负责依赖收集和派发更新),Dep对应一系列对该key感兴趣的Watcher

        如果数据对象某个key的值为对象,则该值对象也对应一个Observer,由该Observer负责对该值对象进行响应式设置,此时依赖收集和派发更新由该值对象的Observer.dep负责。

        如果数据对象某个key的值为数组对象,则会为该数组对象创建一个Observer,同时也会为每个数组元素各自创建一个Observer,让每个数组元素具备响应式功能。此时的依赖收集和派发更新交由该数组对象的Observer.dep负责。

        数据对象与ObserverDep大致关系如下图所示:

        data - Observer - Dep

        综上所述,整个响应式数据拦截与依赖收集可以简单理解为:
        每个数据对象都对应一个ObserverObserver会遍历数据对象的每个key,将其设置为访问器属性,使该key具备动态拦截功能。同时,Observer还会为每个key设置一个Dep,用于 依赖收集派发更新

      • 下面来看下setter函数:

      // src/core/observer/index.js
      set: function reactiveSetter(newVal) {
          // 获取旧值
          const value = getter ? getter.call(obj) : val;
          /* eslint-disable no-self-compare */
          if (newVal === value || (newVal !== newVal && value !== value)) {
              return;
          }
          ...
          // 设置新值
          if (setter) {
              setter.call(obj, newVal);
          } else {
              val = newVal;
          }
          // 对新值进行响应式设置
          childOb = !shallow && observe(newVal);
          // 派发更新
          dep.notify();
      }
      

      setter主要就做了两件事:

      1. 响应式设置:如果设置的新值是object类型,那么就进行响应式设置。
      2. 派发更新:其源码如下:
      // src/core/observer/dep.js
      notify() {
          // stabilize the subscriber list first
          const subs = this.subs.slice()
          ...
          for (let i = 0, l = subs.length; i < l; i++) {
              subs[i].update()
          }
      }
      

      其实就是遍历Dep实例的subs数组,依次调用Watcher.update方法,通知其数据更新。

      我们来看下Watcherupdate方法源码:

      // src/core/observer/watcher.js
      export default class Watcher {
          ...
          update() {
              /* istanbul ignore else */
              if (this.lazy) {
                  this.dirty = true
              } else if (this.sync) {
                  this.run()
              } else {
                  queueWatcher(this)
              }
          }
          ...
      }
      

      update会根据自身携带的一些标识进行不同的处理,对于一般的数据更新场景,比如我们最上面的例子,当我们调用vm.message='others'时,这里会进入queueWatcher流程,我们来看下该函数源码:

      // src/core/observer/scheduler.js
      const queue: Array<Watcher> = []
      let has: {[key: number]: ?true} = {}
      let waiting = false
      let flushing = false
      /**
       * Push a watcher into the watcher queue.
       * Jobs with duplicate IDs will be skipped unless it's
       * pushed when the queue is being flushed.
       */
      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
                  ...
                  nextTick(flushSchedulerQueue)
              }
          }
      }
      

      queueWatcher内部首先对watcher.id进行获取,判断一些缓存中是否存在该idhas[id],确保队列queue不重复添加相同的Watcher

      如果Watcher是首次添加,那么根据当前是否处于flushing状态,分别进行不同的处理:

      • 如果不处于flushing,则直接将Watcher入对列
      • 如果处于flushing状态,则将当前Watcherid将其添加进队列queue的相应位置。

      最后在nextTick触发flushSchedulerQueue函数。

      Vue 中,nextTick相当于开启了一个异步任务,可以确保视图更新完成后再执行相应任务,因此,nextTick(flushSchedulerQueue)会在视图更新完成后,执行flushSchedulerQueue,我们来看下该函数源码:

      // src/core/observer/scheduler.js
      /**
      * Flush both queues and run the watchers.
      */
      function flushSchedulerQueue() {
          currentFlushTimestamp = getNow()
          // 表示正处于 flushing 状态
          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
              watcher.run()
              ...
          }
      
          // keep copies of post queues before resetting state
          const activatedQueue = activatedChildren.slice()
          const updatedQueue = queue.slice()
      
          // 重置状态
          resetSchedulerState()
      
          // call component updated and activated hooks
          callActivatedHooks(activatedQueue) // callHook(vm, "activated");
          callUpdatedHooks(updatedQueue) // callHook(vm, 'updated')
          // devtool hook
          /* istanbul ignore if */
          if (devtools && config.devtools) {
              devtools.emit('flush')
          }
      }
      

      flushSchedulerQueue函数内部做了以下几件事:

      1. 设置当前为flushing状态:flushing = true

      2. 对队列queue依据watcher.id由小到大进行排序:queue.sort((a, b) => a.id - b.id),这是为了确保以下问题:

        • 组件更新是由父到子(因为父组件会比子组件先创建)
        • 用户Watcher会比渲染Watcher先执行(因为用户Watcher会比渲染Watcher先创建)
        • 如果父组件的Watcher在执行过程中,其中一个子组件被销毁了,那么可以跳过该子组件的Watcher运行。
      3. 遍历queue,依次执行每个Watcherrun方法。

        :在遍历queue的时候,每次都要重新计算queue的大小,原因是每次在执行Watcher.run的时候,可能还会创建新的Watcher,此时这些新的Watcher就会走到我们前面刚分析过的queueWatcher

      // src/core/observer/scheduler.js
      export function queueWatcher(watcher: Watcher) {
          ...
          if (!flushing) {
                  ...
          } 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)
          }
              ...
      
      }
      

      flushingtrue时,这时就会走else分支,从而会动态添加新的Watcher到队列queue中。

      我们主要来看下watcher.run方法:

      // src/core/observer/watcher.js
      export default class Watcher {
          ...
          /**
           * Scheduler job interface.
           * Will be called by the scheduler.
           */
          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
                      // 用户 Watcher
                      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)
                      }
                  }
              }
          }
          ...
      }
      

      Watcher.run方法内部首先会通过this.get()获取新值,this.get()前面我们分析过,这里再次看下其源码:

      // src/core/observer/watcher.js
      export default class Watcher {
          ...
          /**
           * 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) {
                  ...
              } finally {
                  // "touch" every property so they are all tracked as
                  // dependencies for deep watching
                  if (this.deep) {
                      traverse(value)
                  }
                  popTarget()
                  this.cleanupDeps()
              }
              return value
          }
          ...
      }
      

      this.get()内部主要通过this.getter.call(vm,vm)获取新值,Vue 中存在两种Watcher

      • 用户Watcher:比如我们自定义的watch函数就是一个用户Watcher,此时,this.getter.call(vm,vm)获取得到的是用户Watcher的值,那么对于Watcher.run方法来说,获取到新值后,就会对新值进行判断,然后将新值与旧值传递给回调函数this.cb,这样,我们自定义的watch函数就能在回调中获取新值与旧值:
      // src/core/observer/watcher.js
      export default class Watcher {
              ...
          run() {
              if (this.active) {
                  const value = this.get()
                  if (
                      value !== this.value ||
                      isObject(value) ||
                      this.deep
                  ) {
                      ...
                      this.cb.call(this.vm, value, oldValue)
                      ...
                  }
              }
              ...
      
          }
      
      • 渲染Watcher:对于渲染Watcher来说,this.getter函数是updateComponent函数:
      // src/core/instance/lifecycle.js
      updateComponent = () => {
          vm._update(vm._render(), hydrating)
      }
      

      所以this.get()的用途不是获取返回值,而是间接触发重新渲染出一个VNode,然后在update内部最终执行patch流程。

      我们在 Vue 源码解析 - 组件挂载 分析过,Vue 中总共存在两种数据渲染:

      • 首次渲染:首次将虚拟节点渲染到一个真实的 DOM 中。
      • 数据更新:对虚拟节点绑定的真实 DOM 节点上的数据进行更新。
      // src/core/instance/lifecycle.js
      export function lifecycleMixin(Vue: Class<Component>) {
          Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
              ...
              if (!prevVnode) {
                  // initial render
                  vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
              } else {
                  // updates
                  vm.$el = vm.__patch__(prevVnode, vnode)
              }
              ...
          }
          ...
      }
      

      此处对应的就是 数据更新 部分内容:vm.__patch__(prevVnode, vnode),这部分最终还是会走到patch函数:

      // src/core/vdom/patch.js
        return function patch (oldVnode, vnode, hydrating, removeOnly) {
            ...
            const isRealElement = isDef(oldVnode.nodeType)
            if (!isRealElement && sameVnode(oldVnode, vnode)) {
              // patch existing root node
              patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
            } else {
              if (isRealElement) {
                  ...
              }
      
              // replacing existing element
              const oldElm = oldVnode.elm
              // 获取挂载 DOM 的父节点
              const parentElm = nodeOps.parentNode(oldElm)
      
              // create new node
              createElm(
                vnode,
                insertedVnodeQueue,
                // extremely rare edge case: do not insert if old element is in a
                // leaving transition. Only happens when combining transition +
                // keep-alive + HOCs. (#4590)
                oldElm._leaveCb ? null : parentElm,
                nodeOps.nextSibling(oldElm)
              )
      
              // update parent placeholder node element, recursively
              if (isDef(vnode.parent)) {
                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)
                    }
                    // #6513
                    // invoke insert hooks that may have been merged by create hooks.
                    // e.g. for directives that uses the "inserted" hook.
                    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
                }
              }
      
              // destroy old node
              if (isDef(parentElm)) {
                removeVnodes([oldVnode], 0, 0)
              } else if (isDef(oldVnode.tag)) {
                invokeDestroyHook(oldVnode)
              }
            }
          }
      
          invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
          return vnode.elm
        }
      

      这里通过判断oldVnodevnode是否是相同虚拟节点,会进入不同的分支处理,判断相同节点的依据如下:

      // src/core/vdom/patch.js
      function sameVnode(a, b) {
          return (
              a.key === b.key && (
                  (
                      a.tag === b.tag &&
                      a.isComment === b.isComment &&
                      isDef(a.data) === isDef(b.data) &&
                      sameInputType(a, b)
                  ) || (
                      isTrue(a.isAsyncPlaceholder) &&
                      a.asyncFactory === b.asyncFactory &&
                      isUndef(b.asyncFactory.error)
                  )
              )
          )
      }
      

      判断相同VNode的主要依据是key要相同,同时以下条件满足其一即可:

      • 同步组件:需满足tagisComment相同,都定义了data,都拥有相同的input类型
      • 异步组件:需满足asyncFactory相同

      patch函数对新旧VNode的比较,其实就是VNode之间的 diff 算法,这部分内容网上已经有很详细的讲解,这里直接引用网上的文章:一起搞明白令人头疼的diff算法

      1. 当队列queue遍历完成后,就会重新状态resetSchedulerState
      // src/core/observer/scheduler.js
      /**
       * Reset the scheduler's state.
       */
      function resetSchedulerState() {
          index = queue.length = activatedChildren.length = 0
          has = {}
          ...
          waiting = flushing = false
      }
      
      1. 最后触发生命周期钩子函数:
      // call component updated and activated hooks
      callActivatedHooks(activatedQueue) // callHook(vm, "activated");
      callUpdatedHooks(updatedQueue)     // callHook(vm, 'updated')
      

    到这里,数据驱动与响应式原理的分析过程就大致结束了。

    参考

    相关文章

      网友评论

          本文标题:Vue 源码解析 - 数据驱动与响应式原理

          本文链接:https://www.haomeiwen.com/subject/dudvmhtx.html