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

参考

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

源码实现

双向绑定的实现流程以下html

双向绑定原理

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

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

源代码执行流程图: java

双向绑定

监听数据的实现

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

数据劫持源码流程图

初始化数据initData

initData初始化数据,监听数据的变化,使得数据的变化可以响应react

  • 获取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实例上。git

//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]进行修改,这是和直接复制引用不一样的地方github

observe

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

  • 判断当前数据是否为对象
  • 判断__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)

}
复制代码
相关文章
相关标签/搜索