vue双向绑定实现之源码解析

2,757 阅读8分钟

参考

本篇文章是基于参考且个人思考后以最简单的方式写出: 关于vue依赖收集部分:ustbhuangyi.github.io/vue-analysi… vue源码解析:github.com/answershuto…

源码实现

双向绑定的实现流程如下

双向绑定原理

根据上图(参考自:github.com/DMQ/mvvm) 双向绑定必须要实现以下几点:

  • 实现一个数据监听器Observer,能够对数据的所有属性进行监听
  • 实现一个订阅者的容器Dep,能够收集订阅者,并且数据变更时能够通知订阅者
  • 实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,绑定相应的更新函数,初始化视图
  • 实现一个观察者Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令对应的回调函数,更新视图

源代码执行流程图:

双向绑定

监听数据的实现

数据劫持监听的源码流程图如下:

数据劫持源码流程图

初始化数据initData

initData初始化数据,监听数据的变化,使得数据的变化能够响应

  • 获取data并判断
  • 获取Props,判断data中的属性是否在Props中被定义,被定义发出警告
  • 将data上的属性代理到vm实例上(代理不是取引用,为vm实例定义相同的属性,通过getter去获取data上的值,setter去修改data上的值)
  • observe数据,绑定数据data
function initData (vm: Component) {

  /*得到data数据*/
  //如果配置中的data是函数,则执行函数,如果是对象,则直接获取
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}

  /*判断是否是对象*/
  //如果data不是对象,且不是正式环境的情况下,发出警告
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }

  // proxy data on instance
  /*遍历data对象*/
  const keys = Object.keys(data)
  const props = vm.$options.props
  let i = keys.length

  //遍历data中的数据
  while (i--) {
    /*保证data中的key不与props中的key重复,props优先,如果有冲突会产生warning*/
    if (props && hasOwn(props, keys[i])) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${keys[i]}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(keys[i])) {
      /*判断是否是保留字段*/

      /*将data上面的属性代理到了vm实例上,即假设data有属性a,this.data.a可以通过this.a被访问*/
      proxy(vm, `_data`, keys[i])
    }
  }
  // observe data
  /*从这里开始我们要observe了,开始对数据进行绑定,这里有尤大大的注释asRootData,这步作为根数据,下面会进行递归observe进行对深层对象的绑定。*/
  observe(data, true /* asRootData */)
}

proxy(代理数据)

我们在访问vue中data、props、computed、methods的属性时,都是通过this.propertyName来访问 在初始化时,我们也会对这些数据进行区分。假设data中有属性a,我们如何通过this.a来访问this.data.a呢? proxy就是做这件事,帮助我们把数据代理到vm实例上。

//target为代理的实例,proxyObjectName为被代理的对象名,proxyKey为被代理对象的属性
function proxy(target,proxyObjectName,proxyKey){
 Object.defineProperty(target,proxyKey,{
  enumerable:true,
  configurable:true,
  get:function proxyGetter(){
   //注意这里的this在运行时指向target
   return this[proxyObjectName][proxyKey]
  },
  set:function proxySetter(newVal){
   this[proxyObjectName][proxyKey] = newVal
  }
 })
}

proxy之后,打印target对象看不到被代理对象的属性,但通过target[proxyKey]却能访问到,target[proxyKey]的修改也会对target[proxyObjectName][proxyKey]进行修改,这是和直接复制引用不同的地方

observe

observe函数尝试创建一个Observer实例(ob),如果成功创建Observer实例则返回新的Observer实例,如果已有Observer实例则返回现有的Observer实例。 Observer实例放在当前对象

  • 判断当前数据是否为对象
  • 判断__ob__属性是否存在,存在则直接引用,不存在则创建Observer实例
  • 如果是根数据则计数
/**
 * 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.
 */
 /*
 尝试创建一个Observer实例(__ob__),如果成功创建Observer实例则返回新的Observer实例,如果已有Observer实例则返回现有的Observer实例。
 */
export function observe (value: any, asRootData: ?boolean): Observer | void {
  /*判断是否是一个对象*/
  if (!isObject(value)) {
    return
  }
  let ob: Observer | void

  /*这里用__ob__这个属性来判断是否已经有Observer实例,如果没有Observer实例则会新建一个Observer实例并赋值给__ob__这个属性,如果已有Observer实例则直接返回该Observer实例*/
 
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (

    /*这里的判断是为了确保value是单纯的对象,而不是函数或者是Regexp等情况。*/
    observerState.shouldConvert &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {

    /*如果是根数据则计数,后面Observer中的observe的asRootData非true*/
    ob.vmCount++
  }
  return ob
}

Vue的响应式数据都会有一个__ob__的属性作为标记,里面存放了该属性的观察器,也就是Observer的实例,防止重复绑定。 所以判断数据是否可响应,看当前数据是否包含__ob__属性

Observer(数据监听器)

Observer实例存在于每个响应式数据的__ob__属性中,Observer的构造函数遍历对象的所有属性,对其进行双向绑定,使属性能够响应式。

Observer实例应该具有以下属性:

  • value:any 保存当前对象的值
  • dep:Dep 保存依赖收集
  • vmCount:number 保存vm实例将当前对象作为根数据root $data的次数

具有以下方法:

  • walk(obj:Object),对对象类型的数据进行绑定
  • observeArray(array : Array),对数组类型的成员进行绑定(对成员调用observe)

步骤如下:

  • 将Observer实例绑定到目标对象的__ob__属性
  • 判断当前目标对象是否为数组
  • 是数组则监听数组的方法,并且为数组的每个成员尝试构建一个Observer实例(即调用observe函数)
  • 是对象则响应式对象的属性
/**
 * Observer class that are attached to each observed
 * object. Once attached, the observer converts target
 * object's property keys into getter/setters that
 * collect dependencies and dispatches updates.
 */
 /*
 Observer类被赋予给每个响应式的对象,一旦拥有Observer实例,Obsever转化目标对象属性的  getter/setters,使得getter能够进行依赖收集,setter能够发布更新
 */
 
export class  {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0

    /*
    将Observer实例绑定到data的__ob__属性上面去,之前说过observe的时候会先检测是否已经有__ob__对象存放Observer实例了,def方法定义可以参考https://github.com/vuejs/vue/blob/dev/src/core/util/lang.js#L16
    */
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      //数组的响应体现在调用方法的时候,所以直接用下标修改数组的成员无法响应
      /*
          如果是数组,将修改后可以截获响应的数组方法替换掉该数组的原型中的原生方法,达到监听数组数据变化响应的效果。
          这里如果当前浏览器支持__proto__属性,则直接覆盖当前数组对象原型上的原生数组方法,如果不支持该属性,则直接覆盖数组对象的原型。
      */
      //判断是否支持__proto__属性
      //如果支持,则直接覆盖当前数组对象原型上的数组方法
      //如果不支持,则逐个覆盖目标数组的方法
      const augment = hasProto 
        ? protoAugment  /*直接覆盖原型的方法来修改目标对象*/
        : copyAugment   /*定义(覆盖)目标对象或数组的某一个方法*/
        
      augment(value, arrayMethods, arrayKeys)
      
      //对数组的每一个成员进行observe
      /*如果是数组则需要遍历数组的每一个成员进行observe*/
      this.observeArray(value)
      
    } else {

      /*如果是对象则直接walk进行绑定*/
      this.walk(value)
    }
  }

  /**
   * Walk through each property 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)

    /*walk方法会遍历对象的每一个属性进行defineReactive绑定*/
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {

    /*数组需要遍历每一个成员进行observe*/
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}


def函数实现

/**
 * 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
  })
}

数组的响应

如果修改数组的成员,并且该成员是个对象,那只需要递归对数组的成员进行双向绑定即可。 但如果我们进行pop、push等操作的时候,push进去的对象没有进行过双向绑定,那么我们如何监听数组的成员变化呢?VUE提供的方法是重写push、pop、shift、unshift、splice、sort、reverse这七个数组方法。 数组的类型也是object,可以理解为数组为具有特定实现方法的object,我们需要对这些方法进行监听并响应

重写的数组(对象)

根据Observer中对数组的响应式处理,如果浏览器支持__proto__属性,则直接修改__proto__为VUE重写的数组(对象),如果不支持,则需要覆盖当前数组的每一个方法为VUE重写的数组(对象)中的方法,逐个覆盖。

/**
 * Augment an 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 */
}

/**
 * Augment an target Object or Array by defining
 * hidden properties.
 */
/* istanbul ignore next */
/*定义(覆盖)目标对象或数组的某一个方法*/
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

重写的VUE数组(arrayMethods)实现:

  • 获取原生数组的原型,根据原生数组的原型创建新的数组对象arrayMethods,防止污染原生数组方法
  • 重写数组对象的push、pop、shift、unshift、splice、sort、reverse方法

重写方法的步骤:

  • 调用原生数组方法
  • 根据方法名来获取新增数据,例如如果是splice只取第三位参数开始的数据
  • 从__ob__属性中获取Observer实例,对新增的数组数据进行observe即调用observeArray方法,绑定新增的数组数据(由于目标(需要响应的)数组的方法最终会继承或者被重写数组(arrayMethods)的方法覆盖,所以在重写的方法内可以调用到目标数组的属性__ob__)
  • 调用Observer实例中订阅者容器Dep的发布数据更新的方法notify(),通知所有订阅当前数据的观察者
/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */
 //在这里没有采用类型检测,是因为flow这个框架在数组原型方法上表现不好
 //从这里可以了解到为什么vue3.0会采用typeScript开发

import { def } from '../util/index'

/*取得原生数组的原型*/
const arrayProto = Array.prototype
/*创建一个新的数组对象,修改该对象上的数组的七个方法,防止污染原生数组方法*/
export const arrayMethods = Object.create(arrayProto)

/**
 * Intercept mutating methods and emit events
 */
 /*这里重写了数组的这些方法,在保证不污染原生数组原型的情况下重写数组的这些方法,截获数组的成员发生的变化,执行原生数组操作的同时dep通知关联的所有观察者进行响应式处理*/
[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  // cache original method
  /*将数组的原生方法缓存起来,后面要调用*/
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator () {
    // avoid leaking arguments:
    // http://jsperf.com/closure-with-arguments
    let i = arguments.length
    const args = new Array(i)
    while (i--) {
      args[i] = arguments[i]
    }
    /*调用原生的数组方法*/
    const result = original.apply(this, args)

    /*数组新插入的元素需要重新进行observe才能响应式*/
    const ob = this.__ob__
    //记录新插入的元素
    let inserted
    //如果是splice(startIndex,removeNumber,...addItems),则下标为2开始的为新增元素
    switch (method) {
      case 'push':
        inserted = args
        break
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    //对新插入的元素进行绑定
    if (inserted) ob.observeArray(inserted)

    // notify change
    /*dep通知所有注册的观察者进行响应式处理*/
    ob.dep.notify()
    return result
  })
})

如果当前浏览器支持__proto__属性,则可以直接覆盖整个属性为VUE重写的数组对象,如果没有该属性,则必须通过def对当前数组对象的方法进行覆盖,效率较低,所以优先使用第一种。

从上述重写的数组对象可以看出,如果修改了通过数组下标或者设置length来修改数组,是无法监听的,所以无法为新增元素进行绑定,但是我们可以通过Vue.set或者splice方法

实现编译器

Compile

Compile的主要任务:

  • 解析模板指令,将模板中的变量替换成数据,生成抽象语法树
  • 初始化渲染视图
  • 为每个指令对应的Node节点绑定更新函数,添加Watcher为数据的订阅者
  • Watcher收到数据变动的通知,调用更新视图

因为遍历解析的过程有多次操作dom节点,为提高性能和效率,会先将vue实例根节点的el转换成文档碎片fragment进行解析编译操作,解析完成,再将fragment添加回原来的真实dom节点中

主要步骤:

  • 把DOM节点转换成fragment节点
  • 遍历编译所有节点,区分元素节点和文本节点编译
  • 如果是元素节点,遍历解析元素节点中的指令属性例如v-text、v-on
  • 根据指令类型,编译指令(普通指令如v-text、v-if等,事件指令如v-on:click)
  • 普通指令则调用指令处理合集(compileUtil)中的方法,获取对应指令的视图更新方法,生成观察者Wathcer(通过传入实例化Wathcher的回调函数中,闭包获取到需要更新的node,与node产生联系),绑定视图更新方法与Watcher的实例。
function Compile(el) {
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);
    if (this.$el) {
        //生成fragment
        this.$fragment = this.node2Fragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
    }
}

Compile.prototype = {
  node2Fragment: function(el) {
      var fragment = document.createDocumentFragment(), child;
      // 将原生节点拷贝到fragment
      while (child = el.firstChild) {
          fragment.appendChild(child);
      }
      return fragment;
  },
	init: function() { 
      this.compileElement(this.$fragment); 
  },
  compileElement: function(el) {
      //遍历编译当前结点及其所有子节点
      var childNodes = el.childNodes, me = this;
      [].slice.call(childNodes).forEach(function(node) {
          var text = node.textContent;
          var reg = /\{\{(.*)\}\}/;	// 表达式文本
          if (me.isElementNode(node)) {
              //按元素节点方式编译
              me.compile(node);
          } else if (me.isTextNode(node) && reg.test(text)) {
              //文本节点编译
              me.compileText(node, RegExp.$1);
          }
          //遍历编译子节点
          if (node.childNodes && node.childNodes.length) {
              me.compileElement(node);
          }
      });
   },
   compile: function(node) {
      //遍历当前结点的属性
      var nodeAttrs = node.attributes, me = this;
      [].slice.call(nodeAttrs).forEach(function(attr) {
          // 规定:指令以 v-xxx 命名
          // 如 <span v-text="content"></span> 中指令为 v-text
          var attrName = attr.name;	// v-text
          // 判断是否满足v-开头的属性
          if (me.isDirective(attrName)) {
              var exp = attr.value; // content
              var dir = attrName.substring(2);	// 取到指令text
              if (me.isEventDirective(dir)) {
                // 编译事件指令, 如 v-on:click
                  compileUtil.eventHandler(node, me.$vm, exp, dir);
              } else {
                // 编译普通指令,如v-text
                  compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
              }
          }
      });
    }    
};

compileUtil(指令处理集合)

var compileUtil = {
    text: function(node, vm, exp) {
        this.bind(node, vm, exp, 'text');
    },
    bind: function(node, vm, exp, dir) {
        //这段代码为核心,功能为:初始化视图,绑定视图更新函数到Watcher实例中
        var updaterFn = updater[dir + 'Updater'];
        // 第一次初始化视图
        updaterFn && updaterFn(node, vm[exp]);
        // 实例化观察者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher
        new Watcher(vm, exp, function(value, oldValue) {
          // 一旦属性值有变化,会收到通知执行此更新函数,更新视图
          // 闭包保存node,与当前Watcher产生联系
            updaterFn && updaterFn(node, value, oldValue);
        });
    },
    html: function(node, vm, exp) {
        this.bind(node, vm, exp, 'html');
    },

    model: function(node, vm, exp) {
        this.bind(node, vm, exp, 'model');

        var me = this,
            val = this._getVMVal(vm, exp);
	    
        node.addEventListener('input', function(e) {
            var newValue = e.target.value;
            if (val === newValue) {
                return;
            }

            me._setVMVal(vm, exp, newValue);
            val = newValue;
        });
    },

    class: function(node, vm, exp) {
        this.bind(node, vm, exp, 'class');
    },
    // 事件处理
    eventHandler: function(node, vm, exp, dir) {
        var eventType = dir.split(':')[1],
            fn = vm.$options.methods && vm.$options.methods[exp];

        if (eventType && fn) {
            node.addEventListener(eventType, fn.bind(vm), false);
        }
    },
     _getVMVal: function(vm, exp) {
        var val = vm;
        exp = exp.split('.');
        exp.forEach(function(k) {
            val = val[k];
        });
        return val;
    },
    _setVMVal: function(vm, exp, value) {
        var val = vm;
        exp = exp.split('.');
        exp.forEach(function(k, i) {
            // 非最后一个key,更新val的值
            if (i < exp.length - 1) {
                val = val[k];
            } else {
                val[k] = value;
            }
        });
    }
};

updater(指令对应的更新函数)

// 更新函数
var updater = {
    textUpdater: function(node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    },

    htmlUpdater: function(node, value) {
        node.innerHTML = typeof value == 'undefined' ? '' : value;
    },

    classUpdater: function(node, value, oldValue) {
        var className = node.className;
        className = className.replace(oldValue, '').replace(/\s$/, '');

        var space = className && String(value) ? ' ' : '';

        node.className = className + space + value;
    },

    modelUpdater: function(node, value, oldValue) {
        node.value = typeof value == 'undefined' ? '' : value;
    }
};

观察者Watcher

个人理解,总的来说Watcher就是个桥梁,作用是绑定视图更新函数与任意被监听的数据,当被监听的数据更新时,调用视图更新的回调

注意这两个Dep的差别:

  • 属性的Deps(存放在属性的__ob__中,是一个Observer实例)会在getter中收集Watcher实例,当数据更新时,Deps会通知所有订阅当前数据的Watcher实例进行视图更新
  • Watcher的Deps用于保存当前观察者依赖数据的订阅者中心(可能有多个,例如v-text="a&b",在实例化Watcher时会往a、b里的Dep都添加订阅,所以需要获取a、b的DepId防止重复添加订阅),防止重复添加watcher到依赖数据的订阅者中心中

值得注意的另一个点:

  • Watcher类不仅被应用到视图中指令绑定时,还被应用到watch(监听某个数据)、computed(依赖监听的数据返回值)当中

Watcher主要函数

  • get():获取当前表达式/函数的值,触发依赖收集(这里是调用Dep的depend方法,depend方法会调用当前Dep.target.addDep方法)
  • addDep():把当前的watcher订阅到这个数据持有的dep的subs中(这是真正的收集依赖)
  • update():调度者接口,给Dep调用,根据是否lazy来选择直接调用或者推入异步队列
  • run():调用get()获取最新数据,调用视图更新的回调接口
  • addDep():添加一个Dep到当前Dep容器中
  • cleanupDeps():清理依赖收集,调用当前Watcher所有依赖数据的Dep中的removeSub,清除当前Watcher实例
  • teardown():将自身从所有依赖数据的Dep中清除

Watcher类的源代码

export default class Watcher {
  vm: Component;      //存放vm实例
  expression: string;
  cb: Function;       //视图更新的回调函数
  id: number;       
  deep: boolean;      //是否采用深度监听(用于watch中的deep参数)
  user: boolean;      //是否是一个用户行为的监听(watch、computed),用于判断放入哪一个队列(有两条异步队列),和是否提示警告
  lazy: boolean;      //true 下次触发时获取expOrFn当前值;false 立即获取当前值
  sync: boolean;      //是否为同步执行回调
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: ISet;
  newDepIds: ISet;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ) {
    this.vm = vm
    /*_watchers存放订阅者实例*/
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
    } 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
    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
    /*把表达式expOrFn解析成getter*/
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
        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
        )
      }
    }
 
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
   /*获得getter的值并且重新进行依赖收集*/
  get () {
    /*将自身watcher观察者实例设置给Dep.target,用以依赖收集。*/
    pushTarget(this)
    let value
    const vm = this.vm

    /*
      执行了getter操作,看似执行了渲染操作,其实是执行了依赖收集。
      在将Dep.target设置为自身观察者实例以后,执行getter操作。
      譬如说现在的的data中可能有a、b、c三个数据,getter渲染需要依赖a跟c,
      那么在执行getter的时候就会触发a跟c两个数据的getter函数,
      在getter函数中即可判断Dep.target是否存在然后完成依赖收集,
      将该观察者对象放入闭包中的Dep的subs中去。
    */
    //如果是用户行为的监听,则发出警告
    //调用表达式,这里的getter指的是当前watcher对应的表达式,但表达式会触发依赖数据的getter
    if (this.user) {
      try {
        value = this.getter.call(vm, vm)
      } catch (e) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      }
    } else {
      value = this.getter.call(vm, vm)
    }
    // 这里用了touch来形容,意味着触发
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    /*如果存在deep,则触发每个深层对象的依赖,追踪其变化*/
    if (this.deep) {
      /*递归每一个对象或者数组,触发它们的getter,使得对象或数组的每一个成员都被依赖收集,形成一个“深(deep)”依赖关系*/
      traverse(value)
    }

    /*将观察者实例从target栈中取出并设置给Dep.target*/
    popTarget()
    this.cleanupDeps()
    return value
  }

  /**
   * Add a dependency to this directive.
   */
   /*添加一个依赖关系到Deps集合中*/
  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)
      }
    }
  }

  /**
   * Clean up for dependency collection.
   */
   /*清理依赖收集*/
  cleanupDeps () {
    /*移除所有观察者对象*/
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      //移除旧的Dep在新的Dep中不存在的与当前Watcher的绑定
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
   /*
      调度者接口,当依赖发生改变的时候进行回调。
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      /*同步则执行run直接渲染视图*/
      this.run()
    } else {
      /*异步推送到观察者队列中,由调度者调用。*/
      queueWatcher(this)
    }
  }

  /**
   * 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.
        /*
            即便值相同,拥有Deep属性的观察者以及在对象/数组上的观察者应该被触发更新,因为它们的值可能发生改变。
        */
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        /*设置新的值*/
        this.value = value

        /*触发回调渲染视图*/
        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)
        }
      }
    }
  }

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
   /*获取观察者的值,仅用于computed watchers*/
  evaluate () {
    if (this.dirty) {
      this.value = this.get()
      this.dirty = false
    }
    return this.value
  }

  /**
   * Depend on all deps collected by this watcher.
   */
   /*收集该watcher的所有deps依赖,仅用于Computed Watcher*/
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  /**
   * Remove self from all dependencies' subscriber list.
   */
   /*将自身从所有依赖收集订阅列表删除*/
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      /*从vm实例的观察者列表中将自身移除,由于该操作比较耗费资源,所以如果vm实例正在被销毁则跳过该步骤。*/
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}

pushTarget与popTarget

export function pushTarget (_target: Watcher) {
  //将上一个Watcher存放到栈中 
  if (Dep.target) targetStack.push(Dep.target)
  //将当前watcher设为target
  Dep.target = _target
}

export function popTarget (){
 //把 Dep.target 恢复成上一个状态
  Dep.target = targetStack.pop()
}

依赖收集过程

  • 通过构造函数调用get()收集依赖
  • get调用pushTarget将当前Watcher实例放入Dep.target,触发当前表达式(也就是触发表达式或函数内依赖数据的getter)
  • 被依赖数据的getter中通过调用dep.depend方法,dep.depend调用Dep.target.addDep(this)
  • Dep.target.addDep(this)也就是watcher.addDep(dep:Dep)把当前依赖数据的Dep放入Watcher实例中,并调用dep.addSub(this)
  • dep.addSub(this)把当前Watcher实例放入dep的subs数组中,也就是把Watcher当成订阅者收集起来
  • 如果是deep watch,完成深层遍历触发getter之后,popTarget,把Dep.target恢复成上一个状态,并调用cleanupDeps清空依赖

依赖清空

Watcher中 this.deps 和 this.newDeps 表示 Watcher 实例持有的 Dep 实例的数组;而 this.depIds 和 this.newDepIds 分别代表 this.deps 和 this.newDeps 的 id Set

Vue的mount过程是通过mountComponent函数,其中有一段比较重要的逻辑,大致如下:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

在这里实例化Watcher的时候回调用get()方法,也就是updateComponent方法,在其中会调用render()方法,这个方法会生成渲染VNode,并且在这个过程中会对 vm 上的数据访问,这个时候就触发了数据对象的getter。 考虑到 Vue 是数据驱动的,所以每次数据变化都会重新 render,那么 vm._render() 方法又会再次执行,并次触发数据的 getters,所以 Wathcer 在构造函数中会初始化 2 个 Dep 实例数组,newDeps 表示新添加的 Dep 实例数组,而 deps 表示上一次添加的 Dep 实例数组。

那么为什么需要做 deps 订阅的移除呢,在添加 deps 的订阅过程,已经能通过 id 去重避免重复订阅了。

考虑到一种场景,我们的模板会根据 v-if 去渲染不同子模板 a 和 b,当我们满足某种条件的时候渲染 a 的时候,会访问到 a 中的数据,这时候我们对 a 使用的数据添加了 getter,做了依赖收集,那么当我们去修改 a 的数据的时候,理应通知到这些订阅者。那么如果我们一旦改变了条件渲染了 b 模板,又会对 b 使用的数据添加了 getter,如果我们没有依赖移除的过程,那么这时候我去修改 a 模板的数据,会通知 a 数据的订阅的回调,这显然是有浪费的。

因此 Vue 设计了在每次添加完新的订阅,会移除掉旧的订阅,这样就保证了在我们刚才的场景中,如果渲染 b 模板的时候去修改 a 模板的数据,a 数据订阅回调已经被移除了,所以不会有任何浪费,真的是非常赞叹 Vue 对一些细节上的处理。

Dep

Dep是订阅者中心,数组成员是Watcher,在属性的getter中收集Watcher 需要注意的是,getter中调用Dep的depend方法,而不是直接调用addSub方法 depend方法调用Watcher实例中的addDep,addDep方法将dep放入watcher的dep数组中,再调用dep的addSub方法收集依赖

import type Watcher from './watcher'
import { remove } from '../util/index'

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()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null
const targetStack = []

export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  Dep.target = targetStack.pop()
}

defineReactive的实现

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: Function
) {
  /*在闭包中定义一个dep对象*/
  const dep = new Dep()

  //这里可以关注到,如果属性是不可配置的,将取消绑定
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  /*如果之前该对象已经预设了getter以及setter函数则将其取出来,新定义的getter/setter中会将其执行,保证不会覆盖之前已经定义的getter/setter。*/
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set

  /*对象的子对象递归进行observe并返回子节点的Observer对象*/
  let childOb = observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {

      /*如果原本对象拥有getter方法则执行*/
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {

        /*进行依赖收集*/
        dep.depend()
        if (childOb) {

          /*子对象进行依赖收集,其实就是将同一个watcher观察者实例放进了两个depend中,一个是正在本身闭包中的depend,另一个是子元素的depend*/
          childOb.dep.depend()
        }
        if (Array.isArray(value)) {

          /*是数组则需要对每一个成员都进行依赖收集,如果数组的成员还是数组,则递归。*/
          dependArray(value)
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {

      /*通过getter方法获取当前值,与新值进行比较,一致则不需要执行下面的操作*/
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {

        /*如果原本对象拥有setter方法则执行setter*/
        setter.call(obj, newVal)
      } else {
        val = newVal
      }

      /*新的值需要重新进行observe,保证数据响应式*/
      childOb = observe(newVal)

      /*dep对象通知所有的观察者*/
      dep.notify()
    }
  })
}


注意到重要的两点:

  • 如果属性是无法配置的,即configurable为false时,会取消双向绑定
  • 如果对象含有子对象,会对其进行深层次的双向绑定,并且会把watcher观察者实例递归放入子元素的dep中,这样子元素的更改都会引起依赖当前对象的视图更新

总结

现在再回来看这个执行流程,思路就能清晰了

双向绑定

从学习双向绑定源码的过程,能学习到以下几点

  • data和props不能重复属性,props的优先级更高
  • 查看__ob__来判断数据是否被监听:查看数据是否被监听,可以查看当前数据是否具有__ob__属性,因为__ob__属性存放着Obeserver实例,实例化Observer的过程就是数据监听的过程
  • 数组只能通过七种方法来为新增元素设置监听:通过数据监听的过程,可以知道对数组的监听只能通过push、pop、shift、unshift、reverse、sort、splice等方法对修改的元素进行监听,所以通过数组下表的形式去修改元素,是不会被监听到的。
  • 不需要双向绑定时,可以选择冻结数据:通过defineReactive使数据可响应的过程,可以知道当数据被设置了configurable为false时,是不会进行绑定的,因为Object.defineProperty此时无效,针对这一点,在我们确定不需要双向绑定的该数据的时候(由于双向绑定会实例化Observer,当数据量大时,最好取消双向绑定),可以设置数据的configurable为false,这一点可以通过Object.freeze实现.
  • 子属性的修改会也会引起视图更新:通过defineReactive可以得知,watcher会被绑定到当前依赖数据的Dep中,也会被绑定到依赖数据的子属性的Dep中,这意味着子属性被修改,也会引起视图更新
  • 在初始化阶段后新增子属性无法被监听:由于初始化阶段新增子属性不存在,所以无法被监听,只能等到下一次render()函数被执行后,才能被监听,这也是我们在设置v-model为一个新的对象属性时,会发现它的双向绑定失效,再反复的UI操作后,又能够生效,这是render()函数执行后再次绑定的结果。所以我们要使用官方提供的Vue.$set()为对象添加新的监听属性
  • Watcher的属性:Watcher作为绑定视图更新函数与依赖数据的桥梁,其属性决定了它的作用。
    • deep属性决定了该Watcher实例是个深监听的观察者,递归的触发子属性的getter,使得子属性被触发后Watcher会被子属性的Dep收集。
    • sync决定数据更新时,立即调用视图更新的回调函数还是异步推送到观察者队列queueWatcher()
    • user属性标志是否为一个用户行为的Watcher,如果是,在错误时进行警告提示,并且用于判断放入哪个一个异步队列

Object.freeze() 方法可以冻结一个对象(数组也是对象)。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。 要使对象不可变,需要递归冻结每个类型为对象的属性(深冻结):

//深冻结函数
function deepFreeze(obj){
   var propNames = Object.getOwnPropertyNames(obj)
   
   // 在冻结自身之前冻结属性
   propNames.forEach((name)=>{
   	let prop = obj[name]
	//如果prop是个对象,则冻结它
	if(typeof prop === 'object' && prop !== null){
	   deepFreeze(prop)
	}
   })
   //冻结当前对象
   return Object.freeze(obj)

}