vue2 双向绑定实现原理


都知道vue中实现了数据和视图的双向绑定,但具体是如何实现的呢?

今天就说说 我阅读的vue2中的代码实现的个人所得,如果说错了,欢迎指正。

注:我阅读的vue2代码的版本是v2.2.6,且都是以单文件的方式展示列子, 可以结合下一篇续给出的实际例子结合起来看,更容易理解

 

话不多说,首先上结论,然后再通过图以及代码进行详细解释

结论:m->v: 利用观察者模式,当数据改变的时候通知其观察者进行更新,

     v->m: 利用事件绑定对数据进行设值

 

实现过程:通过Object.defineProperty的方法定义 get和set方法对数据的操作拥有代理权,并且每个属性都会有一个对应的依赖,

每个vm组件都会有一个或多个watcher,当获取vm或者data的数据的时候就会调用get方法,如果这个时候有watcher正在运行观察,

那么就把这个依赖和此watcher都加入到各自的对象下的相关队列中去

1)model到view:当设置vm或者data对象的数据的时候,就会调用set方法,

如果这次结果 和上次的value不同,就让此依赖中的所有watcher加入到scheduler(调度者)的更新队列中去,

等待nextTick更新scheduler的队列,调用watcher的update方法,然后运行getter方法比较value值和之前保存的value值是否相同,

如果不同,则调用cb(回调函数),对应vm来说getter是vm.update(vm.render()),当调用getter的时候,整个过程就完成了model到view的更新了

2:view到model:一般用v-model方法进行实现,默认的是绑定dom组件的input事件(这个看源码的实现,如果你设置option.model.event的值的话,

事件就会变成你设置的值),当触发此事件的时候,会调用你绑定的值进行设置,就又回到1)过程了,当然也可以自己绑定自己关心的事件实现设值的效果

上图:

 

可以从结论中看到,其中有3个比较重要的部分,data对应的dep和vm对应的watcher以及把它们进行关联的Object.defineProperty定义的get和set方法,

而实现这一切的基础就是调用Object.defineProperty对属性设置get和set代理方法,所以vue2只能支持ie8以上,因为ie8及一下没有这个方法,也没法用shim的方式来实现

 

详细解释

一. watcher

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: Set;
  newDepIds: Set;
  getter: Function;
  value: any;

  constructor (vm: Component, expOrFn: string | Function, cb: Function, options?: Object) {}

    run(){}
    get(){}
    update(){}
    addDep(){}
    cleanupDeps{}
    evaluate(){}
    teardown(){}
          
}    

注:为了避免太长不方便看,方法的具体实现就没有帖出来了,如果有兴趣的可以去源码看看

 

简称观察者,通常意义的解释,就是对某些事物感兴趣而去观察他的人,用在vue2里面就是对一些表达式结果感兴趣而进行观察的对象,

用来统计更新vm的updateComponent方法,computed属性或者watch属性中的expOrFn(表达式或者方法)

 

watcher对象有3个比较重要的属性:

  getter:关心的表达式或者方法

  value: 运行getter的结果,在new watcher的时候如果lazy为false就会调用getter一次来初始化这个值

  cb:当运行getter方法得到的value值和之前的value值不同的时候,会调用cb,对于vm组件来说就是更新组件

还有一些其他属性:

  id: watcher的唯一标示,当对一个周期内的所有watcher执行更新的时候,会先根据这个id 从小到大进行排序,而且父级的vm的对应的id总是小于子集

  lazy:是否懒更新, 也就是当它关注的表达式中有相关数据改变的时候,它才更新,不然就一直用value值,像computed中的watcher的lazy就是true

  dirty:是否已经脏了,如果脏了,当获取这个值的时候就重新调用getter获得新值,否则直接返回value的值,和lazy有关系

  user: cb回调函数是否是用户传进来的,如果是的话执行这个回调就会用try catch的方式,以避免出错

  active: 这个watcher是否是活动的,如果是活动才会去运行getter

  deps:对应的所有依赖,是一个数组

  newDeps:这次更新后,收集到的下一轮更新所相关的依赖

  depIds:所有依赖的id,是一个set对象

  newDepIds: 下一轮的依赖id所对应的set对象

 

注:这里面的deps的作用个人觉得是:当vm销毁的时候或者运行了一此get方法的时候,从相关的依赖中删除自己,当某些需要的时候,重新和依赖关系,

  目前只看到computed的watcher 在获取值的时候,如果watcher的dirty为true,就会重新和deps里面的依赖重新建立关系,其他地方就没看到有使用了- -!

 

先上一个例子,下面会用到:

<template>
  <div id="app" class="app">
 
      <label>姓名:</label>
      <input :value="name" @input="name = $event.target.value">
      
      <select v-model="sex">
        <option value="1">男</option>
        <option value="0">女</option>
      </select>
    
    <p>{{infoName}}</p>
    <p>{{infoSex}}</p>
  </div>
    
</template>

<script>

 export default {

    data(){
      return {
        name:"",
        sex:1,
        resultObj:{
          allInfo:""
        },
      }
    },
    computed:{
      infoName(){
        return "您的姓名是:" + this.name;
      },
      infoSex(){
        return "您的性别是:" + (this.sex == 1 ? "男":"女");
      }
    },
    watch:{
      sex:function(newSex){
        this.resultObj.allInfo = this.name + "|" + this.sex;
        alert("你的性别已经改为"+ (this.sex == 1 ? "男":"女"));
      },
      name:function(newName){
        this.resultObj.allInfo = this.name + "|" + this.sex;
         alert("你的名字已经改为"+ newName);
      },
      "resultObj.allInfo":function(newInfo){
        alert("你新的的所有信息为"+ newInfo);
      }
    },
 }

</script>

注: 上述代码是为了展示 watcher而强行拼凑的代码,所以有重复的地方,不要在意这些细节

 

有3个地方会有对应的watcher

1.一个vm组件会对应一个watcher,这个watcher关注的是vm的_render方法,每个vue文件的template里面的内容都会被编译成一个_render方法,

  并且里面用到的属性或者computed都会转化成_vm.name 或者_vm.infoName 的代理形式,具体的转化过程在dep里面说到

2.computed对象里面的每个属性,都有一个对应的watcher,如上例:infoName,infoSex都会有一个对应的watcher,并且生成的watcher的lazy为true,

 即它们都是懒更新的,只有里面用到的相关数据出现变化的时候,这个watcher才会执行getter方法

3.watch对象里面的每个属性,都有一个对应的watcher,如上例:sex,name,"resultObj.allInfo"都会有一个对应的watcher

  "resultObj.allInfo" 这个对应的watcher需要特殊说明下它的getter,先看代码

if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    this.getter = parsePath(expOrFn);
    if (!this.getter) {
      this.getter = function () {};
}
////////////////////////////////////////
var bailRE = /[^\w.$]/;
function parsePath (path) {
  if (bailRE.test(path)) {
    return
  }
  var segments = path.split('.');
  return function (obj) {
    for (var i = 0; i < segments.length; i++) {
      if (!obj) { return }
      obj = obj[segments[i]];
    }
    return obj
  }
}
///////////////////////////////////////////////
function get(){
  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);  }
}

如果初始化watcher的时候传的expOrFn 是function,那么这个getter就直接是这个function,否则就就行转化,运行parsePath方法得到一个function

当然这个function有一个判断,是路径形式的才行,当运行getter方法的时候,传入vm作为obj,那么其实resultObj.allInfo 得到的是 vm.resultObj.allInfo的值,

当然 不可能直接vm["resultObj.allInfo"]或者vm."resultObj.allInfo"这样得到,所以用了一个循环来实现

 

这3种对应的watcher创建的时候分别为:

1) 使用$mount方法的时候

Vue$3.prototype.$mount = function (el, hydrating) {
    el = el && inBrowser ? query(el) : undefined;
    return mountComponent(this, el, hydrating)
};

function mountComponent (vm,el,hydrating) {

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

    vm._watcher = new Watcher(vm, updateComponent, noop);
}

注:1.有些地方我是从分模块实现的源码中拷贝过来的,有些是直接是为了方便查找,就直接在vue.common.js中拷贝过来,所以可能代码风格有些不一样,

  而且为了更方便理解,会对某些地方做小小的改动,会删除一些和本话题无关的代码实现过程

      2.hydrating 这个属性和vdom(虚拟dom)实现有关系,在这里暂时不去解释它的作用, 单词意思为:保湿

 

通过实现可以看出来,watcher的getter 就是vm的render方法(update是更新组件,和数据没有关系,所以把getter转化为render),每个vm都有自己独立对应的一个watcher,

这个watcher关心的是整个组件的渲染

上面说的是 在使用$mount方法的时候 会创建这个watcher,但是有时候,我们在创建组件的时候,没有使用$mount ,比如

const app = new Vue({
  el:"#app"
  router: router,
  store:store,
  render: h => h(App)
});

这个时候就没有用$mount,那么它就不会有watcher了吗?

先上代码: 

function Vue (options) {

  this._init(options)
}


Vue.prototype._init = function (options?: Object) {

//...其他初始化实现
if (vm.$options.el) { vm.$mount(vm.$options.el) } }

注: 我吧_init方法里面不相关的代码都删除了,只留下了我们关心的地方

在new vue的时候,我们传入的对象有个el属性,关键就在这里,而在vm初始化的时候会判断传进来的对象有没有这个属性,如果有就会自动去调用$mount方法,而不需要手动

当然如果又没有el,又没有手动调用$mount方法,那么这个组件就不会出现在view上,也不会创建对应的watcher

 

2.解析computed属性的时候

Vue.prototype._init = function (options?: Object) {
     initState(vm)
}

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch) initWatch(vm, opts.watch)
}

function initComputed (vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null)

  for (const key in computed) {
    const userDef = computed[key]
    let getter = typeof userDef === 'function' ? userDef : userDef.get
    
    // create internal watcher for the computed property.
    watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)
  }
}

注:1.把有些和此话题不相关的代码都删除了,所以帖出来的代码不是完整的

  2.在逛vue论坛的时候,遇到有人提问:在prop属性设置default为methods里面的方法为什么报空,大家看看initState的初始化顺序就明白了

  3.data,computed,watch等都会存储在vm.$option中

 

在解析computed属性的时候,会对里面的每个属性都会有创建一个对应的watcher,并且会用用这个属性的key作为_computedWatchers 对象的key来保存这个watcher

当你默认传一个function的时候,会把这个这个function当做watcher的getter,否则就会认为你传了一个包含了get属性的对象,并且是一个方法,

这个时候就会用这个get方法当做watcher的getter

 

3.使用$watch方法的时候

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

function createWatcher (vm: Component, key: string, handler: any) {
  let options
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  vm.$watch(key, handler, options)
}

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ): Function {
    const vm: Component = this
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

和使用$mount方法一样,无论是你初始化的对象包含了watch属性或者使用$watch 都会创建相对于的watcher

使用$watch会返回一个方法,当你对观察的expOrFn不在感兴趣的时候,可以用来删除这个观察者

 

二.dep

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

简称依赖,对vm中data返回的每个属性都会初始化一个依赖,用来收集对这个属性感兴趣的watcher,也就是watcher中运行getter方法会用到的相关属性,

那么这个属性对应的依赖就会把watcher加入到dep下的subs队列中去,也会把本身dep加入到watcher中的deps队列中去,

当这个属性的值改变的时候,通知watcher进行更新操作,也就是执行getter方法

 

属性解释:

  subs:对某个属性感兴趣的watcher队列

 

 

dep一般对应vm的中data方法返回的对象以及它的属性  

export default {
  data(){
    return {
      name:"",
      sex:1,
      items:[]
    }   } }

比如上面的 data会有一个dep,name和sex也分别有一个dep

 

dep的创建过程

function initState (vm) {
  if (opts.data) {
    initData(vm);
  } else {
    observe(vm._data = {}, true /* asRootData */);
  }
}

function initData (vm) {
  var data = vm.$options.data;
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {};
  if (!isPlainObject(data)) {
    data = {};
  }
  
  var keys = Object.keys(data);
  var props = vm.$options.props;
  var i = keys.length;
  while (i--) {
    if (props && hasOwn(props, keys[i])) {
     ///warn
    } else if (!isReserved(keys[i])) {
      proxy(vm, "_data", keys[i]);
    }
  }
  // observe data
  observe(data, true /* asRootData */);
}

function observe (value, asRootData) {
  if (!isObject(value)) {
    return
  }
  var ob;
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    observerState.shouldConvert &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value);
  }

  return ob
}

var Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  def(value, '__ob__', this);
  if (Array.isArray(value)) {
    var augment = hasProto
      ? protoAugment
      : copyAugment;
    augment(value, arrayMethods, arrayKeys);
    this.observeArray(value);
  } else {
    this.walk(value);
  }
};

Observer.prototype.walk = function walk (obj) {
  var keys = Object.keys(obj);
  for (var i = 0; i < keys.length; i++) {
    defineReactive$$1(obj, keys[i], obj[keys[i]]);
  }
};

function defineReactive$$1 (
  obj,
  key,
  val,
  customSetter
) {
  var dep = new Dep();

  var property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  var getter = property && property.get;
  var setter = property && property.set;

  var childOb = observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
        }
        if (Array.isArray(value)) {
          dependArray(value);
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      var 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.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = observe(newVal);
      dep.notify();
    }
  });
}

Observer.prototype.observeArray = function observeArray (items) {
  for (var i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
};

注:vm初始化会调用initState方法,所以省略了init的过程

 

整个过程简而言之就是 

 

1).initData

  在初始化data的时候,是用一个三元运算符,判断data是否是一个function,如果是的话,就运行 getData方法

function getData (data, vm) {
  try {
    return data.call(vm)
  } catch (e) {
    handleError(e, vm, "data()");
    return {}
  }
}

其实getData 也就是运行一次data方法而已,只是为了安全考虑,加了try catch

如果不是的话,就返回 data || {} 给它,这里有个小技巧就是,如果data不为空就返回data,否则就返回一个{},

然后 在对data里面的属性设置代理,让我们能够用vm.key的方式来获取或者设置data里面的数据,而不用vm._data.key的方式

在方法的最后面 就是调用observe 来生成一个observer(可以依赖的对象)

 

2).observe

 首先判断 是不是一个object,当然 这里的object 是一个泛型,是指 typeof 为 "object" 且 != null 的对象,也可以是Array,

function isObject (obj) {
  return obj !== null && typeof obj === 'object'
}

如果不是一个object 那么就会中断

再判断 是否已经包含了一个 observer 对象,如果有 则返回这个observer对象

再判断 

observerState.shouldConvert: 是可以转换的
!isServerRendering(): 不是服务器渲染
(Array.isArray(value) || isPlainObject(value)) 是 数组 或则 Object 类型
Object.isExtensible(value): 这个对象是可以添加属性的, 做这个判断是因为 会在Object上 新增一个__ob__属性来对应它的Observer对象

!value._isVue: 不能是 vm对象, vm组件 严格来说 也是Object对象,但是他不能被观察,所以有这个判断

注: 判断还挺多的。。。。

 

如果上述条件 任何一个不满足 就会中断,不过我们只需要关心的是。。我们传进来的数据满足是 数组 或者object对象就行了,其他的一般不会触及

好了 下一步就是肉戏来了,就是创建一个 observer 对象

3). new observer

保存value数据对象,然后最重要的相关核心实现就从这里开始了,

首先创建一个dep依赖(说了这么多终于说到它了),这个dep 就对应的这个value数据对象,

然后 会判断这个value是不是数组对象,

如果是的话, 先执行

augment(value, arrayMethods, arrayKeys)
而 augment 一般会对应这个方法
function protoAugment (target, src) {
/* eslint-disable no-proto */
target.__proto__ = src;
/* eslint-enable no-proto */
}

这一步比较关键,把这个数组对象默认的prototype对应的方法替换成arrayMethods 里面的方法,而arrayMethods 这个对象里面包含了哪些方法呢,请看代码

var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  // cache original method
  var original = arrayProto[method];
  def(arrayMethods, method, function mutator () {
    var arguments$1 = arguments;

    // avoid leaking arguments:
    // http://jsperf.com/closure-with-arguments
    var i = arguments.length;
    var args = new Array(i);
    while (i--) {
      args[i] = arguments$1[i];
    }
    var result = original.apply(this, args);
    var ob = this.__ob__;
    var inserted;
    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
    ob.dep.notify();
    return result
  });
});

  

从代码看:包含了 'push','pop','shift','unshift','splice','sort','reverse' 方法,并且arrayMethods的prototype是Array的prototype,

具体作用在下面回和例子结合说到

然后,再执行 observeArray,

Observer.prototype.observeArray = function observeArray (items) {
  for (var i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
};

其实也就是对数据里面的每个元素再进行观察, 从observe的判断条件来看,普通的数据就不要想了,此元素 必须是一个object 或者array,才会生成一个observer对象(这点很重要)

好了,通过上面对数组的实现,可以解释一下 我们平时 在用数组稍不注意就会犯的一个错误, 就是改变一个数组的普通元素的值,但是 界面上却没有响应它的改动呢,

比如

<template>
  <div id="app" class="app">
      <p v-for="value of ary">{{value}}</p>
  </div>
    
</template>

<script>

 export default {

    data(){
      return {
        ary:[1,2,3,4]
      }
    },
    mounted(){
      setTimeout(()=>{
        this.ary[2] = 10;
      }, 2000);
    }
 }

  

上述代码,会在两秒之后改变界面的显示吗? 

不会,因为 this.ary[2] = 10, 这个只是普通的操作,vue2 没有对这个属性添加相关的dep(我觉得是为了性能考虑吧,不然的话 数组里面的数据大了,对每个数据都建立一个dep,那就太恐怖了)

这样的操作,是没有任何改变的,

而上面vue2 特意自己实现了array 的一些相关方法去实现这个效果,所以如果要改变普通元素的数据的时候我们可以这样,

mounted(){
      setTimeout(()=>{
        this.ary.splice(2,1,10);
      }, 2000);
    }

感兴趣的可以测试下,

起作用的原因在于arrayMethods这个对象,当我们操作splice的时候,不是操作的ary的原生的splice方法,而是操作的arrayMethods里面的splice方法,

而这个方法在执行了原生的对应的方法后,还会去调用ob.dep.notify()方法执行更新

 

以上是如果这个数据类型是数组的时候,当不是的时候,那么执行walk方法,

然后获取对象的keys 循环对key调用defineReactive方法,然后当这个数据 又是一个object或者array对象的时候,又再一次去观察它

可以说defineReactive 这个方法里面的实现,是实现双向绑定最最核心的一个方法了,其他的一系列都是在这基础上做扩展,或是为了健壮性或者为了解耦等等,

这个方法实现了对data的每个属性创建一个dep,并且对data的原生属性的操作利用Object.defineProperty方法定义一个set和get的代理,让data和dep产生关系,

而且 在set和get具体实现里,也实现了 让dep和watcher 产生关系,所以说这个是vue2实现双向绑定最最最核心的方法了,因为这么重要,说就单独在下一个部分来解释它

 

三 dep 和 watcher 是如何联系起来的

function defineReactive$$1 (
  obj,
  key,
  val,
  customSetter
) {
  var dep = new Dep();

  var property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  var getter = property && property.get;
  var setter = property && property.set;

  var childOb = observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
        }
        if (Array.isArray(value)) {
          dependArray(value);
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      var 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.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = observe(newVal);
      dep.notify();
    }
  });
}

 

下面就来看看 这个方法的具体实现:

 

首先定义了一个dep,然后在获取这个属性的描述对象,用这个对象来判断这个属性是不是可以配置的,如果为否,则退出,因为使用Object.defineProperty

这个方法是需要这个属性是可以配置的

 

然后获取这个属性的 get和set方法并缓存,因为Object.defineProperty会重定义这两个方法,会覆盖它们,缓存起来以便于调用

 

再然后 let childOb = observe(val)  这句话,就实现生成子对象的observer,这样就实现了循环调用,对所有子对象都能够生成observer以及子对象的属性值的dep

当然,这个子对象得是 Object 或者 array

 

请注意:核心来了,使用Object.defineProperty方法 定义set 和get方法

我们看set方法实现:

首先得到value值,这里用三元运算符来判断之前缓存的get方法是否存在,如果存在就调用这个get方法否则直接返回val值

然后再 判断 Dep.target 是否存在,我们看看源码里面的注释对这个属性的解释

// 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

这个属性表示当前正在计算的watcher, 并且在用一个时间内整个全局内只会同时存在一个

就是说:正在运行 get方法的watcher,而get是一个方法,而js是单线程,不会同一时间运行多个方法,那当运行这个get方法过程中又可能存在调用其他watcher的get方法的情况,

比如一个vm组件的render方法里面包含computed的属性的时候,这又是如何处理的呢,请看代码

const targetStack = []

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

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

 

首先定义了一个target栈,是通过一个数组实现的,

运行watcher的get方法的时候 会先调用pushTarget方法,方法里面会 判断Dep.target,如果存在则把它压进栈里面,然后把新的taget赋值给Dep.target

watcher的个体方法后面会运行popTarget,把target栈的最后一个元素赋值给Dep.target,当然如果不存在,那么Dep.target为空

通过代码可以看出来,Dep永远是对应当前正在计算的watcher,如果还不懂的话也没关系,最后会通过一个例子把整个相关的流程都解释一遍,

 

我们回到set方法实现:

判断Dep.target是否存在,如果存在那么表示,我获取这个属性的值的是在运行watcher的get方法过程中(因为只有运行watcher的get方法,才会pushTarget,

而且在get方法后面还会运行popTarget把Dep.target重置),那么代表这个属性值是和这个watcher是有关联的,

好,既然是有关联的,那么dep 就和watcher要进行关联操作了,即执行dep.depend方法,这个方法实现了,把dep放进watcher的newDeps里面,为什么是newDeps

而不是deps呢,因为deps代表当前的依赖,而newDeps是表示新的依赖,即运行完这次更新之后,这个watcher的相关依赖,而运行get方法的时候会清楚当前的deps里面的依赖,

把newDeps里面的依赖复制到deps里面去

 

如果有childOb的话,也会把childOb同watcher关联,因为如果有childOb,那么这个value肯定是一个对象,操作这个对象有两个方式,要么获取value对象的子属性,要么设置

这个对象为新对象,这和childOb都有关系,所以需要把childOb同watcher关联

 

再判断value值是否为array,如果是的话,就去关联array下面的对象

 

以上就是整个dep 和watcher 关联的部分了

 

而set实现就是实现通知关联的watcher进行更新,因为只有数据改变的时候,才会需要更新,所以把通知更新实现放在set方法里面

 

set具体实现:

当改变一个属性对应的值的时候, 先获取现在的值并做比较,如果相同就退出

然后不同的话,那么就进行复制操作

然后再执行更新操作,而在watcher部分说过,watcher的getter有3个类型:1:vm的render方法,2:computed的属性,3:watch的属性

这个时候,大多数情况是执行vm。render进行更新,当然这个render只是返回一个vNode(虚拟节点)然后调用vm的update进行真正的更新,

这里为了简化理解,所以这样说

 

上面就是整个关联的部分,在总结一下过程就是:在创建watcher进行的时候,1和3方式的会立即去运行get方法,然后和相关运算用到的属性对应的dep建立关联,

而2对应的computed的属性因为是懒更新的,所以在创建watcher的时候并不会立即执行关联,而是在调用的时候才会用到,而一般是在vm.render方法里面会有调用

 

双向绑定到这里就解释完了,本来还想继续在这里用一个例子来说明整个过程,但是发现貌似这篇文字已经很多了,那就放在下一篇《双向绑定续》里面来解释

注:因为是第一次写博客,所以可能一些东西没考虑完善,或者错误的地方,请指正,谢谢

 

 

 

 

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM