vue2 双向绑定实现原理-续

上一篇大概解释了双向绑定实现原理,可是没有例子,因此可能看起来比较很差理解,vue

这一篇作为补充,会用一个例子来说解实现过程,首先上例子express

<template>
  <div id="app" class="app">
  
      <label>姓名:</label>
    <input v-model="name"> <!--<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, } }, computed:{ infoName(){ return "您的姓名是:" + this.name; }, infoSex(){ return "您的性别是:" + (this.sex == 1 ? "男":"女")); } }, watch:{ sex:function(newSex){ alert("你的性别已经改成"+ this.sex == 1 ? "男":"女"); }, name:function(newName){ alert("你的名字已经改成"+ newName); } }, } </script>

而后下面再上一个上面编译后的对应的render函数代码:数组

module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var 

_c=_vm._self._c||_h;
  return _c('div', {
    staticClass: "app",
    attrs: {
      "id": "app"
    }
  }, [_c('label', [_vm._v("姓名:")]), _vm._v(" "), _c('input', {
    domProps: {
      "value": _vm.name
    },
    on: {
      "input": function($event) {
        _vm.name = $event.target.value
      }
    }
  }), _vm._v(" "), _c('select', {
    directives: [{
      name: "model",
      rawName: "v-model",
      value: (_vm.sex),
      expression: "sex"
    }],
    on: {
      "change": function($event) {
        var $$selectedVal = Array.prototype.filter.call($event.target.options, function(o) 

{
          return o.selected
        }).map(function(o) {
          var val = "_value" in o ? o._value : o.value;
          return val
        });
        _vm.sex = $event.target.multiple ? $$selectedVal : $$selectedVal[0]
      }
    }
  }, [_c('option', {
    attrs: {
      "value": "1"
    }
  }, [_vm._v("男")]), _vm._v(" "), _c('option', {
    attrs: {
      "value": "0"
    }
  }, [_vm._v("女")])]), _vm._v(" "), _c('p', [_vm._v(_vm._s(_vm.infoName))]), _vm._v(" "), 

_c('p', [_vm._v(_vm._s(_vm.infoSex))])])
},staticRenderFns: []}

 

当上面组件在建立以后,调用$mount方法进行挂载的时候,会首先建立vm对应的watcher,缓存

在watcher的构造函数实现中,最后会调用一次get方法去初始化value值,而get方法对应的方法为render(简化以后),代码为:app

var  updateComponent = function () {
          vm._update(vm._render(), hydrating);
       };
 
    vm._watcher = new Watcher(vm, updateComponent, noop);

注:1.update方法是更新组件的方法,这个和vDom(虚拟dom)相关,这里忽略它,就认为render就是更新组件的方法dom

  2.其实render只是返回一个vNode,并非真正的更新渲染实现函数

 

而render方法 就是上面编译后的实现,每一个文件最后都会把template里面的内容编译成一个render方法oop

当运行这个render方法的时候,你们能够看到会去调用_vm.name,_vm.sex,_vm.infoName,_vm.infoSex得到值,性能

而这些属性值,在vm这个对象里面,其实只是一个代理,在调用initSate的时候初始化data方法里面返回的对象,并设置代理到vm,优化

而computed里面的属性值,是在建立这个vm对应的构造函数的时候,进行初始化并设置代理到vm的,其方法是 Vue.extend

_vm.name 实际上是多级代理:首先返回this[sourceKey][key], 这里sourceKey为_data,key为name因此实际上是返回_vm._data.name,

而_vm._data.name 会调用上一篇说到的Object.defineProperty定义的get方法,

这个时候,由于Dep.target不为空(由于这个时候是运行watcher的get方法,因此Dep.target为当前watcher),那么name 属性对应的dep就和这个watcher

创建的关联了,sex同理,

这里 说说 vm和infoName设置代理的过程,先上代码:

Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    const Sub = function VueComponent (options) {
      this._init(options)
    }
    
    Sub['super'] = Super

    
    if (Sub.options.computed) {
      initComputed(Sub)
    }

    // cache constructor
    cachedCtors[SuperId] = Sub
    return Sub
  }
}

function initComputed (Comp) {
  const computed = Comp.options.computed
  for (const key in computed) {
    defineComputed(Comp.prototype, key, computed[key])
  }
}

export function defineComputed (target: any, key: string, userDef: Object | Function) {
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = createComputedGetter(key)
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.set
      ? userDef.set
      : noop
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

function createComputedGetter (key) {
  return function computedGetter () {
    var watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate();
      }
      if (Dep.target) {
        watcher.depend();
      }
      return watcher.value
    }
  }
}

注: 1.extend方法里面的实现删除了不少,这个方法是用来继承基本vue组件类并返回一个构造函数,而且会缓存起来,当下次调用的时候就直接返回而不用再初始化

       2. 咱们须要关注的执行顺序是 extend->initComputed->defineComputed->createComoputederGetter

 

经过实现能够看出,当这个组件在第一次建立的时候,会利用Vue.extend建立其对应的构造函数,而extend方法会初始化他对应的computed,而后对computed对象里面的每一个属性设置一个代理,

当调用vm.infoName的时候,其实的获取 watcher对应的value值,

 

咱们在看看上面的执行过程,建立vm -> 建立对应的watcher -> 执行render -> 调用_vm.infoName -> 调用computedGetter方法,

而根据 上一篇 computed属性建立对应的watcher过程,每一个属性都有一个对应的watcher,而且会用key作对应,并且这个watcher的dirty为true,在初始化的时候也没有

调用对应的get方法,这个时候由于dirt为true,因此为调用watcher的evaluate强行计算一次,而后dirty为false,之后若是对应的dep的对应的data值没有改变,那么就会一直用这个value值

而再也不从新计算,当涉及到的data值,好比在这个里面就是name值改变的时候,会通知watcher进行更新,而watcher会把dirty再次设置为true,这个时候再调用infoName的时候就会从新计算

 

上面就是初始化的过程当中创建关联的过程,

 

而当咱们在name对应的input的进行输入的时候,这个时候触发绑定的input事件,会执行 name = $event.target.value 的操做

这个时候最终会执行Object.defineProperty定义的get方法,这个时候确定不会和以前的value值相同,由于初始化的value值为"",而新的值确定不会为"",

会调用dep.notify通知watcher方法更新,执行过程代码为:

dep.notify()

notify () {
  
  const subs = this.subs.slice()
  for (let i = 0, l = subs.length; i < l; i++) {
     subs[i].update()
  }
}

update () {
    /* istanbul ignore else */
	if (this.lazy) {
	  this.dirty = true
	} else if (this.sync) {
	  this.run()
	} else {
	  queueWatcher(this)
	}
}

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 >= 0 && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(Math.max(i, index) + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

function flushSchedulerQueue () {
  flushing = true
  let watcher, id, vm

  // 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]
    id = watcher.id
    has[id] = null
    watcher.run()
  }
}

  

咱们须要关注的执行流程为: dep.notify -> watcher.update -> queueWatcher -> nextTick -> flushSchedulerQueue

 

在watcher.update 方法里面 能够看到 有3个分支,第一: 若是lazy 那么就是 只设置为dirty为true,等待获取value值的时候再去从新计算

第二:若是 sync为true 那么就当即执行run,

第三:把这个watcher加入到scheduler(调度者)的更新队列中去,若是当前没有执行更新,那么加入数组,等待nextTick(下一个时间片)去更新,

若是当前正在执行更新,那么就会根据id作一个从小到大的排序,更小的id老是更先更新,由于从id的产生来讲,父节点的id老是比子节点的id小,

 

nextTick的源码实现由于有点多,并且和此话题关系不是很重,我大概讲一下nextTick的实现就好了

nextTick方法的主要做用 加入一个function,这个function 会在下一个时间片执行,具体更具环境不一样而利用不一样实现,

优先使用Promise,其次使用MutationObserver,若是上述两种方式都不能使用,那么就使用setTimeout(functionName,0)的方式来实现

这个方法实现了,讲多个watcher的更新集中到一块儿实现一次更新,而避免每一个watcher更新都直接当即更新,由于watcher的关联性很大,并且有不少dep对应的watcher多是

同一个watcher,这样能够避免避免重复更新,从而大大的优化了性能,好比上面的列子,当我同时改变name 和sex的时候,其实他们的watcher都是同样,都是vm对应的那个watcher,

那么若是name改变的时候实行一次render,sex改变的时候又执行一次render,那就有点可怕了,固然这又会照成一个影响,就是我改变了数据以后,dom没有当即改变,这个时候咱们能够

利用vue2 提供的nextTick方法来实现咱们的需求

 

在下一个时间片,会调用flushSchedulerQueue方法,首先对队列里面的watcher按照id进行升序排序,而后循环队列,调用watcher.run,而后再运行get方法进行更新

 

上面就是整个通知更新执行过程了

 

总的归纳过程就是, 当调用watcher的get方法进行计算的时候, 获取data的数据即调用get方法,这个时候会进行进行关联,

当数据改变的时候执行set方法,会通知dep相关了的watcher进行更新

相关文章
相关标签/搜索