Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue2与Vue next响应式原理 #15

Open
luoway opened this issue Oct 16, 2019 · 0 comments
Open

Vue2与Vue next响应式原理 #15

luoway opened this issue Oct 16, 2019 · 0 comments
Labels

Comments

@luoway
Copy link
Owner

luoway commented Oct 16, 2019

什么是“响应式”

一个数据发生变化,与此数据相关的事件随即被触发。(观察者模式)

Vue 2 响应式原理

defineProperty:用于设置对象属性的getter/setter

const obj = {
  val: 1
}
let val = obj.val
Object.defineProperty(obj, 'val', {
  get(){
    //obj.val的值将被查询
    return val
  },
  set(newVal){
    //obj.val的值将被修改
    val = newVal
  }
})

转为响应式:

const obj = {
  val: 1
}
//以下内容又可作为函数defineReactive()
let val = obj.val
Object.defineProperty(obj, 'val', {
  get(){
    //obj.val的值将被查询
+   depend()	//obj.val被查询了,说明有事件依赖了obj.val
    return val
  },
  set(newVal){
    //obj.val的值将被修改
    val = newVal
+   notify()	//obj.val被修改了,通知依赖该属性的事件更新
  }
})

将对象转为响应式:

const obj = {
  val: 1
}
function observe(obj){
  Object.keys(obj).forEach(key => defineReactive(obj, key))
}

实现depend()/notify()

一个对象属性可以有多个观察者,这些观察者应当被集中(如放入数组中)方便通知

因此增加数组dep存放观察者:

function defineReactive (obj, key){
+ const dep = []
  
  let val = obj.val
  Object.defineProperty(obj, 'val', {
    get(){
      //obj.val的值将被查询
-     //depend()
+     if( window.currentWatcher ) dep.push( window.currentWatcher )
      return val
    },
    set(newVal){
      //obj.val的值将被修改
      val = newVal
-     //notify()
+     dep.forEach( watcher => watcher.update() )
    }
  })
} 

以上代码新增了一个概念:Watcher,即观察者。
对Watcher有两点要求:

  1. 响应属性getter触发时,能够被dep收集到
  2. 实现update()方法响应变化

实现Watcher

class Watcher {
  constructor(obj, key, cb){
    this.vm = obj
    this.getter = obj => obj[key]
    this.cb = cb
    this.get()
  }
  get(){
    window.currentWatcher = this
    this.getter(this.vm)	//触发getter
    window.currentWatcher = null
  }
  update(){
    this.cb()
  }
}

这个Watcher怎么用:

const obj = { val: 1 }
//转换为响应式:设置getter/setter
defineReactive(obj, 'val')
//进行观察
new Watcher(obj, 'val', ()=>{
  console.log('watcher 1:', obj.val)
})
new Watcher(obj, 'val', ()=>{
  console.log('watcher 2:', obj.val)
})

实现computed

computed和watch主要有两点不同:

  • 是一个响应属性,允许getter
  • 延迟计算,在getter时才触发更新

其使用方式可以设计为:

const obj = { val: 1 }
defineReactive(obj, 'val')
compute(obj, 'computedKey', ()=>{
  return obj.val + 1
})
console.log( obj.computedKey ) //2

compute实现方式可以是:

class Watcher {
+ constructor(obj, keyOrFn, cb, lazy){
    this.vm = obj
-   //this.getter = obj => obj[key]
+   this.getter = typeof keyOrFn === "function" ? keyOrFn : obj => obj[keyOrFn]
    this.cb = cb
-   //this.get()
+   this.dirty = this.lazy = lazy
+   this.value = this.lazy ? undefined : this.get()
  }
  get(){
    window.currentWatcher = this
-   //this.getter(this.vm)
+   let value = this.getter(this.vm)	//触发getter
    window.currentWatcher = null
+   return value
  }
  update(){
+   this.lazy ?
+     (this.dirty = true) : //update触发后,需要进行计算
      this.cb()
  }
+ evaluate(){
+   this.value = this.get()
+   this.dirty = false
+ }
}

function compute(obj, key, fn){
  const watcher = new Watcher(obj, fn, ()=>{}, true)
  Object.defineProperty(obj, key, {
    get(){
      if(watcher){
        if(watcher.dirty){
          watcher.evaluate()
        }
        return watcher.value
      }
    }
  })
}

以上就是Vue 2的响应式原理,一句话总结:

使用Object.defineProperty()拦截对象属性读写,建立观察者模式,实现监听对象属性变化。

缺点

  • Vue不能监听到对象新增属性及变化

    Object.defineProperty()只能为已存在属性设置getter/setter以监听变化,无法监听对象属性的新增

Vue next 响应式原理

鉴于Vue 2使用的Object.defineProperty()的先天不足,Vue next使用ES6新特性Proxy解决了上面问题。

Proxy

const obj = {}
const proxyObj = new Proxy(obj, {
  get(target, key, receiver){
    //对象将被查询
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver){
    //对象将被修改
    return Reflect.set(target, key, value, receiver)
  }
})

Proxy相比Object.defineProperty()的优势在于,Proxy实例能够监听本身的变化,因此可以监听到对象属性的增删改查。

API升级了,原理仍是相似的。

转为响应式:

function reactive (target){
  return new Proxy(target, {
    get(target, key, receiver){
      const res = Reflect.get(target, key, receiver)
      track(target, key)  //depend()
      return (res !== null && typeof res === 'object') ? reactive(res) : res
    },
    set(target, key, value, receiver){
      const result = Reflect.set(target, key, value, receiver)
      trigger(target, key)
      return result
    }
  })
}

const obj = {}
const proxyObj = reactive(obj)

实现track()/trigger()

在Vue 2,为了存放多个观察者,创建了闭包dep数组来存放观察者。

使用ES6,有更合适的结构Set,来存储观察者。

另一方面,闭包dep一旦被外部引用,就需要手动清除引用以避免内存泄漏 ,因此不便访问及管理。

使用ES6,可以存放到全局WeakMap中进行管理。

const targetMap = new WeakMap()

//功能等同于
//if( window.currentWatcher ) dep.push( window.currentWatcher )
function track(target, key){
  const effect = window.currentEffect  //currentWatcher
  if(!effect) return
  
  let depsMap = targetMap.get(target)  //对象target所有属性key的dep集合
  if (depsMap === void 0) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  
  let dep = depsMap.get(key)	//属性key对应的dep,用于存放观察者
  if (dep === void 0) {
    dep = new Set()
    depsMap.set(key, dep)
  }
  
  if(!dep.has(effect)){
    dep.add(effect)
  }
}

//功能等同于
//dep.forEach( watcher => watcher.update() )
function trigger(target, key){
  const depsMap = targetMap.get(target)
  if (depsMap === void 0) return false
  
  const effects = new Set()
  const computedRunners = new Set()
  //区分computed、effect
  depsMap.get(key).forEach(effect => {
    effect.computed ?
      computedRunners.add(effect) :
      effects.add(effect)
  })
  
  const run = effect => {
    effect.scheduler === void 0 ? effect() : effect.scheduler(effect)
  }
  computedRunners.forEach(run) //computed先于effect执行
  effects.forEach(run)
}

以上代码新增了一个概念:effect,类似Vue 2中的Watcher。
对effect有两点要求:

  1. 响应属性getter触发时,能够被dep收集到
  2. 本身是回调函数,或有computed属性为真且实现scheduler()方法响应变化

实现effect()

function effect(fn, options = {}){
  const effect = function effect(...args) {
    window.currentEffect = effect
    try{
      return fn(...args)
    }finally{
      window.currentEffect = null
    }
  }
  effect.scheduler = options.scheduler
  effect.computed = options.computed
  
  if(!options.lazy) effect()
  return effect
}

这个effect()怎么用:

const obj = reactive({ val: 1 })  //转换为响应式:设置getter/setter
//进行观察
effect(()=> {
  console.log('watcher 1:', obj.val)
})
effect(()=> {
  console.log('watcher 2:', obj.val)
})

实现computed()

function computed(getter){
  let dirty = true
  let value
  
  const runner = effect(getter, {
    lazy: true,
    computed: true,
    scheduler(){
      dirty = true
    }
  })
  
  return {
    effect: runner,
    get value(){
      if(dirty) {
        value = runner()
        dirty = false
      }
      return value
    }
  }
}

这个computed()怎么用:

const obj = reactive({ val: 1 }) 
const cObj = computed(()=>{
  console.log('computed', obj.val)
  return obj.val
})
console.log(cObj.value)

小结

  • Vue next 与 Vue 2 响应式原理基本一致
  • 使用Proxy完善Vue 2使用的Object.defineProperty()的不足
  • 使用WeakMap对创建的闭包进行弱引用,便于管理
  • Vue next 完全使用 Javascript 函数实现,源码中没有用到类
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant