透过vue源码分析响应式原理、

用vue已经一段时间了,对于轮子虽然会用可是也要知道轮子的构造,知其然并知其因此然。今天咱们对照着vue的源码一步一步进行分析,对vue的实现原理有一个初步的了解。
ps:第一次写文章各位请多包含,若是有错误请指出谢谢vue

了解模式和思路

vue的数据驱动是在数据劫持+订阅者-发布者模式下创建的,主要的依赖Observer Dep Watcher Compilernode

简单了解下这几个东西的做用

Observer:劫持全部数据添加getter,setter;
Dep:发布者,收集全部的依赖,当数据变化时通知依赖;
Watcher:订阅者,同时链接数据变化时候调用的更新函数;
Compiler:模板解析,转换成render生成vnode,对比新老vnode进行更新页面;react

了解了大概,接下来咱们根据源码分别分析这几项内部是如何实现的同时他们的关系是如何进行关联的;git

Observer

上面说道Observer是数据劫持和添加getter,setter,这里咱们讲解Observer是如何进行数据劫持,添加的getter和setter的做用是什么。 当咱们进行new Vue()以后,这段代码初始化Data (源码连接)github

export function initState (vm: Component) {
 const opts = vm.$options
  省略无用的代码----
  opts.data取得就是data return的对象,若是存在执行initData(vm)
  if (opts.data) {
    initData(vm)---->看这里调用了initData()
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
    省略无用的代码----
}
复制代码

initData执行了observe对数据加工,添加getter和setter。express

function initData (vm: Component) {
  let data = vm.$options.data
   省略无用的代码----
  // observe data
  observe(data, true /* asRootData */)---->这里执行observe,接下来咱们去observe去看一下作了什么
}
复制代码

observer源码连接编程

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data
  constructor (value: any) {
    /Observer里对传过来的数据进行判断,数组和对象的处理方法不是同样的/
    if (Array.isArray(value)) {
      删除无用的代码----
      this.observeArray(value)
    } else {
    //这里是针对对象进行处理的执行了walk
      this.walk(value)
    }
   }
   walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
            walk遍历了data的keys对每一个对象的key执行了下面的方法
            若是我再页面声明的是
            data:{
                name:"张三",
                age:"23"
            }
            那么obj就是 data,
            keys[i]就是每一个key(name或者age等)
      defineReactive(obj, keys[i])
    }
  }
  }
复制代码

这里咱们总结一下上面的代码逻辑,当初始化vue对象的时候vue会对data进行处理,循环data的key执行了walk中defineReactive方法传了两个参数,数据data,和每一个key。数组

重点是defineReactive,进入在下面代码里咱们能够看到vue就是利用Object.defineProperty() 对数据进行劫持对每一个key添加了get和set方法,同时咱们发现代码中有new Dep()这里面针对对象中的每一个key都添加了dep,咱们以前说过dep是收集订阅者的,当有数据更新的时候通知更新。(关于代码中dep咱们下面讲,先了解)bash

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
//这里针对data中每个key都new了一个发布者,收集全部订阅者。
  const dep = new Dep()
  const getter = property && property.get
  const setter = property && property.set
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      //关于dep咱们下面讲,先了解概念:Dep.target目标订阅者;dep.depend()收集订阅者
      //当咱们调用this.name的时候就会调用get方法当Dep.target存在的时候调用了dep.depend()
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      //这里对值进行比较,若是修改的是相同的值就return不进行下面的执行
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
      //更新value
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      //dep.notify()通知的方法
      //当进行数据更新的时候进行了dep.notify()通知更新
      dep.notify()
    }
  })
}
复制代码

小结

通过上面的分析咱们了解到vue进行数据劫持,经过Object.defineProperty() 添加get,set方法监控数据的调用和更新,当数据调用的时候触发get方法同时调用dep.depend()收集依赖,当数据更新的时候触发set方法,而后进行数据对比是不是相同值若是相同则return,若是是更新则更新val同时调用dep.notify()通知订阅者更新。闭包

咱们这里了解了observe是如何进行数据劫持和如何添加getter和setter的而且知道getter和setter的做用 到这里有几个疑问new Dep(),Dep.target,dep.depend(),dep.notify()这究竟是什么东西怎么运行的?

Dep

刚刚咱们留了几个问题
Dep.target是目标订阅者;
dep.depend()收集订阅者的;
dep.notify()通知更新的;

接下来咱们看源码解释一下他是如何肯定订阅者的,如何收集的,如何通知的。 Dep源码连接
如下删除了无用代码以后的代码,看下代码注释;

import type Watcher from './watcher'

//这里有个uid,new的每一个Dep()的时候公用一个uid
let uid = 0
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
//这里进行了uid++因此保证了每一个data new Dep的uid都是不一样的
  constructor () {
    this.id = uid++
    this.subs = []
  }
  //这里出现了Watcher对象,订阅者
addSub (sub: Watcher) {
    this.subs.push(sub)
  }
//这个就是上面调用dep.depend()收集的方法,添加了起来
  depend () {
    if (Dep.target) {
    //这里调用的addDep()把这个依赖收集了起来
      Dep.target.addDep(this)
    }
  }
//dep.notify()这里是通知的方法,通知了全部订阅者调用了update()方法;
//这里咱们先记着notify()通知了全部的订阅者(Watcher)调用了Watcher的update();
  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

//这就是目标订阅者,默认是没有订阅者null;
//Dep.target同时只能存在一个保证惟一性
Dep.target = null

//这里暴露了一个方法用来添加目标订阅者;
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}
复制代码

小结

看完了Dep咱们知道这个源码有两个做用一是收集订阅者,二是通知订阅者进行更新,是数据和更新方法的一个纽带;这里再回忆一下,数据更新的时候调用了数据的get方法同时调用dep.depend()收集依赖;数据更新的时候调用set方法而且调用 dep.notify()通知更新,贴上代码以下:

get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      //当咱们调用this.name的时候就会调用get方法当Dep.target存在的时候调用了dep.depend()
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      //这里对值进行比较,若是修改的是相同的值就return不进行下面的执行
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
      //更新value
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      //当进行数据更新的时候进行了dep.notify()通知更新
      dep.notify()
    }
复制代码

Watcher

说完了Dep发布者,他是收集订阅者,通知订阅者更新的,有几个疑问什么那是何时才有订阅者的,订阅者究竟是谁。刚刚说Dep.target是目标订阅者那么他是何时添加的呢,接下来咱们分析这几个问题,订阅者是谁,何时添加的。Dep.target是何时存在的; 咱们翻开vue Watcher的源码

//咱们分析一下源码new Watcher()构造函数里接收了几个参数
export default class Watcher {
 constructor (
    vm: Component, //vue对象
    expOrFn: string | Function,//传入进来的key值或者函数
    cb: Function,//回调函数(多是更新函数,或者是其余回调)
    options?: ?Object,//一些配置computed的参数这里咱们先不考虑
    isRenderWatcher?: boolean//不考虑
  ) {
    this.vm = vm
    
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
    //这里默认参数都是false
      this.deep = this.user = this.lazy = this.sync = false
    }
    //把传入的回调给了this.cb;
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    //这里对 传入的 参数判断是否是函数不是函数执行parsePaht();
    //parsePaht这里也是返回了一个新函数
    源码地址:(https://github.com/vuejs/vue/blob/dev/src/core/util/lang.js)
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    //注意这里当new Watcher(传入相应的参数)执行这,this.lazy是false,调用this.get();看下面的get方法
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  //执行get()
  get () {
     //还记的dep中暴露的pushTarget方法吗
    pushTarget(this)
   -------------------------dep中的代码-------------------------------
   //这里pushTarget添加了目标订阅者
   //此时Dep.target不是null了
            export function pushTarget (target: ?Watcher) {
              targetStack.push(target)
              Dep.target = target
        }
        继续往下看
-----------------------------------------------------------------
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)//这里用了.call传入了vm vue对象
    这个getter就是new Watcher传进来的函数或者是parsePath(key)值包装后返回的函数
    一般调用的是parsePath(key)返回的函数
    --------------------这里贴上parsePath(key)-----------------------------
    源码连接:https://github.com/vuejs/vue/blob/dev/src/core/util/lang.js
            export function parsePath (path: string): any {
            这个path就是传入进来的data中数据key值如‘name,age’或者是其余的多个值key
          if (bailRE.test(path)) {
            return
          }
          //进行拆分红数组
          const segments = path.split('.')
          //能够看到这里用了个闭包这个obj就是上面的vm
          return function (obj) {
            for (let i = 0; i < segments.length; i++) {
              if (!obj) return
              //这里至关于调用this.name那么就调用了get方法
              //到这Dep.target有了目标订阅者,也调用了get方法get方法中就调用了dep.depend()收集了订阅者
              obj = obj[segments[i]]
            }
            //返回了这个值
            return obj
          }
        }
    -----------------------------------------------------------------------
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    }
    //返回了value
    return value
  }
 }
}
复制代码

上面watcher主动调用getter就是调用了数据的get.

get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      //此时Dep.target不是null
      if (Dep.target) {
      //dep收集了依赖调用了Dep.target.addDep(this)
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
复制代码

小结

通过了这么一大长串的代码分析咱们了解到了当new Watcher()的时候会调用数据自己的get同时添加了new出来的订阅者。那么订阅者是谁呢何时才能new Watcher()?最显而易见的就是template模板当数据变化的时候模板会更新,那么这个模板就是订阅者。咱们能够看一下源码页面初始化的时候进行初始化Watcher。

Compiler模板解析,页面挂载,出现订阅者

页面挂载源码

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
 //删除一些无关的------
 //beforeMount的生命周期的回调
  callHook(vm, 'beforeMount')
  
//这里声明了一个更新组件的变量;
  let updateComponent
  //判断当前开发和生产环境给updateComponent赋值成一个函数
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    //这里updateComponent返回了vm._update()里面传入了render方法,其实就是页面的渲染方法;
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }
//订阅者在这生成了,传入了参数
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  ----------------Watcher中的代码片断-------
   constructor (
    vm: Component, //vue对象
    expOrFn: string | Function, //updateComponent
    cb: Function,//回调函数 noop
    options?: ?Object,  //before对象
    isRenderWatcher?: boolean //ture
  ) {
     //把noop赋值给了cb
      this.cb = cb;
   //传进来的expOrFn是函数因此执行了这段
    if (typeof expOrFn === 'function') {
      这个getter就是updateComponent 
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    //这里执行了get把返回值赋值给了this.value
    this.value = this.lazy
      ? undefined
      : this.get()
      
  }
  get () {
   //添加了订阅者
    pushTarget(this)
    let value
    const vm = this.vm
    try {
    value = this.getter.call(vm, vm)
    //执行了getter,就是执行了
      <!--updateComponent = () => {
         vm._update(vm._render(), hydrating)
       }-->
      //那么这个value就是vm._update(vm._render(), hydrating)渲染函数;
      //render函数中会解析模板中{{name}}语法,就是调用了data中的数据,每一个对象new了个dep,收集了这个依赖
      //那么当name数据更新的时候就会触发更新方法dep通知刚刚添加的订阅者更新;
      
    } 
    return value
  }
  --------------------------------------------
  return vm
}
复制代码

小结

通过以上的分析咱们知道了在页面挂载的时候会模板解析同时new了一个Watcher(订阅者),当模板中用到data的数据时每一个数据生成了本身的dep,dep收集订阅者(能够理解成模板),当data的数据发生变化的时候,这个数据的dep就会通知订阅者更新,调用更新函数从新渲染页面。

页面更新

到这里咱们知道了页面初始化的时候就是一个订阅者,这个订阅者用到了data中的数据,data中的数据收集了他,当数据变化的时候通知订阅者更新,那么是如何更新的,咱们回头看一下Object.defineProperty 这里的set就是当数据更新的时候调用的

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      //这里对值进行比较,若是修改的是相同的值就return不进行下面的执行
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
      //更新value
        val = newVal
      }
      //数据更新的时候就会调用dep.notify()通知订阅者更新
      dep.notify()
    }
  })
  
  -------------dep的notify()--------------------------
  notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    //这里调用订阅者的update()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
-------------------------------------------------
-----------------watcher的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) {
 //这里取了订阅者的id判断这个id是否存在若是不存在则加入队列中;
 //若是存在则替换掉前一个订阅者,这就是当咱们在代码中对一个数据进行屡次修改的时候vue只取最后一次的更新;
  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
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      //把更新方法放到下一个事件循环中执行;
      nextTick(flushSchedulerQueue)
         }
       }
     }
复制代码

总结

到这里vue数据响应式的原理就结束了,咱们最后总结一下:
vue进行数据劫持经过Object.defineProperty 给每一个数据添加get,set方法而且添加了dep(发布者),当数据被使用的时候触发get-->dep.depend()添加订阅者,当数据改变的时候触发set-->dep.notify()通知订阅者更新。 当咱们重复对一个数据进行修改的时候如

this.age=12, this.age=13, this.age=14 queueWatcher()会生成更新队列一个数据的屡次修改会先到队列中检查是否存在,若是存在则会替换掉以前的watcher,若是没有则添加到队列中。因此对一个值的屡次修改只取最后一次进行更新。

相关文章
相关标签/搜索