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

[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')
    

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

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容