vue系列--- vue响应式原理

vue响应式原理

要说vue响应式原理首先要说的是Object.defindProperty(),这个是响应式原理的基础。mdn

下面我举个栗子

    let obj = {
        name: "zs",
        age:10
    }
    Object.keys(obj).forEach(item=>{
      let val = obj[item]
      Object.defineProperty(obj,item,{
        get() { 
          console.log(`访问了${item}属性`);
          return val; 
        },
        set(newValue) { 
          console.log(`新值是${newValue}`);
          val = newValue; 
        },
        enumerable : true,
        configurable : true
      })
    })
    console.log(obj.age);
    console.log(obj.name);
    obj.age = 20
    obj.name = "ls"

操作结果

1.png

从上面的例子可以看出如果没使用Object.defindProperty()直接访问obj.age也可以得到10的值,如果我想在访问obj.age属性的时候做一些其他的事,例如年龄不能超过500,如果超过500直接返回500,这个需求对于直接访问obj的对应属性是没办法办到了,这个时候需要我们访问obj对应的key的时候触发一个方法,或者说我们可以代理访问obj的key,这个时候Object.defindProperty()这个api完美的解决了这个问题。在访问和设置对应的属性的时候可以劫持这个属性的访问,这也是vue响应式原理的基础。

vue响应式原理

...
<div id="app">
  {{obj.name}}
  <button @click="modify">修改</button>
</div>
<script>
new Vue({
  el: '#app',
  data: {
    obj:{name:"zs"}
  },
  methods:{
    modify(){
        this.obj.name = "lisi"
    }
  }
})
...

当我们点击添加按钮后,修改obj.name这个属性,页面自动更改了,这个就是vue响应式所做的工作

到底如何实现的当数据发生变化后,页面自动更新的?

下面我们来从new Vue开始分析

//1.new Vue的时候会执行_init方法
function Vue (options) {
  this._init(options)
}
//2. init方法调用initState
Vue.prototype._init = function (options?: Object) {
   ...
    initState(vm)
    // 调用钩子函数
    callHook(vm, 'created')
    // 当传入挂载点时,渲染页面
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
    ...
  }
  //3.initState方法调用initData
  function initState (vm: Component) {
      const opts = vm.$options
      // 获取配置对象中的data属性进行初始化
      if (opts.data) {
        initData(vm)
      }
  }
  //4.initData方法调用observe
  initData (vm: Component) {
      let data = vm.$options.data
      // 使传入的数据变为响应式的,也就是劫持对象的set和get,方便依赖收集
      observe(data, true /* asRootData */)
  }
  //5. 
  function observe (value: any, asRootData: ?boolean): Observer | void {
      let ob: Observer | void
      ob = new Observer(value)
      return ob
  }
  //6. 真正对数据进行递归劫持的Observer类
  export class Observer {
      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()
        // 判断是否为数组,
        if (Array.isArray(value)) {
          const augment = hasProto
            ? protoAugment
            : copyAugment
          augment(value, arrayMethods, arrayKeys)
          this.observeArray(value)
        } else {
          循环劫持对像的属性
          this.walk(value)
        }
      }
      walk (obj: Object) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
          defineReactive(obj, keys[i])
        }
      }
      observeArray (items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
          observe(items[i])
        }
      }
  }
  //7.
  export function defineReactive (
      obj: Object,
      key: string,
      val: any,
      customSetter?: ?Function,
      shallow?: boolean
    ) {
      // 初始化依赖收集器(可以理解为微信公众号)
      const dep = new Dep()
      val = obj[key]
      // 递归劫持
      let childOb = observe(val)
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
          const value = getter ? getter.call(obj) : val
          // 当Dep.target为真时依赖收集(什么时候为真看下方分析)
          if (Dep.target) {
            dep.depend()
          }
          return value
        },
        set: function reactiveSetter (newVal) {
          if (setter) {
            setter.call(obj, newVal)
          } else {
            val = newVal
          }
          // 将传入的新值变为响应式的
          childOb =  observe(newVal)
          // 当对应的值改变的时候派发更新(也就是使用到这个值的每一个地方都替换为新值)
          dep.notify()
        }
      })
  }

上面是new Vue 到数据变为响应式的整个过程,然而上面的流程只能让数据变为响应式的,当数据发生变化后页面如何更新,依赖是如何收集的,什么时候会触发依赖收集,

当我们访问obj.name 的时候会触发 Object.defineProperty()的get访问器,在get访问器中有一个Dep.target,其中Dep是依赖收集器,Dep.target是Dep类上面的一个静态属性 类型为Watcher,下面讲一下Dep和Watcher的关系

Dep相当于我们的微信公众号,Watcher 相当于关注者,本质上他们是多对多的关系 ,当一个人想收到某个公众号推送的消息,那么他必须关注这个公众号,也就是订阅这个公众号,这个订阅也就是“收集依赖”的一个过程,当公众号有新的文章之后,可以推送给关注他的人,这个关注者就能收到这个变化,也就是代码中当获取obj的属性的时候也就是触发get方法中进行依赖收集(dep.depend())也就是记录一下那几个地方使用了该属性,在改变值的时候派发更新(dep.notify())也就是使用到该属性的地方地需要改变为新值。

什么时候进行依赖收集?

看上面的get方法当Dep.target为真的时候才进行依赖收集,那么什么时候Dep.target为真呢?看下面源码的解析

export default class Watcher {
  ...
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    this.cb = cb
    this.id = ++uid // uid for batching
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.getter = expOrFn
    this.value = this.get()
  }
  // 真正进行依赖收集的方法
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    // 执行传入的方法也就是vm._update(vm._render(), hydrating)进行渲染,vm._render()最后的返回值是一个虚拟节点,vm._update方法真正进行diff算法更改dom
    value = this.getter.call(vm, vm)
    return value
  }
  // 使watcher记录下每一个dep(订阅器)(微信公众号)
  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)
      }
    }
  }
  // 清空dep
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      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
  }
  // 派发更新真正要执行的
  update () {
     queueWatcher(this)
  }
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}

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

可以看到当调用pushTarget方法后 Dep.target变为_target,也就是在Watcher类里面的get方法被调用的时候Dep.target变为_target,那么什么时候get被调用呢,就是在watcher的构造函数中执行,也就是new Watcher 的时候执行,那么什么时候会new一个Watcher对象呢

// 1. new Vue()
// 2. Vue.prototype._init(option)
// 3. vm.$mount(vm.$options.el)
// 4. render = compileToFunctions(template) 将vue中的template模板编译为render函数
// 5. Vue.prototype.$mount 在init方法里面有这么一句话 if (vm.$options.el) {vm.$mount(vm.$options.el)}当有挂载点的时候会执行$mount方法
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
// 6. mountComponent
function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // 执行beforeMount钩子函数
  callHook(vm, 'beforeMount')
  let updateComponent
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
  // 这个时候会new 一个 渲染watcher 当创建对象的时候会执行updateComponent方法进而执行 vm._update(vm._render(), hydrating)进行渲染
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
    //vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例。
  if (vm.$vnode == null) {
    //表示这个实例已经挂载了,
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

从上面我们可以看到当用户new Vue的时候传了el 也就是挂载点的时候 会执行vm.mount(vm.$options.el),进而调用mountComponent方法,在这个方法里面new Watcher的时候执行 watcher构造函数里的get方法使的Dep.target为真并调用传入的getter方法也就是 vm._update(vm._render(), hydrating)进行渲染,渲染模板的时候会获取模板中的数据,从而触发对象的属性访问器get,而这个时候Dep.target恰好为真,利用了闭包,调用dep.depend()将依赖收集起来(也就是点击了订阅),在改变值的时候派发更新

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容