Explain the responsive principle of Vue based on "dependency collection" (transfer)

add by zhj: The article is very easy to understand and understand the usage of Object.defineProperty

Original: https://zhuanlan.zhihu.com/p/29318017

 

Whenever asked about the responsive principle of VueJS, everyone may blurt out "Vue converts all properties of the data object into getters/setters through the Object.defineProperty method, and notifies changes when properties are accessed or modified." However, many people may not fully understand its deep responsive principle, and the quality of articles about its responsive principle on the Internet is also uneven, most of which are posted with a code and a comment. This article will start from a very simple example and analyze the specific implementation ideas of the reactive principle step by step.

1. Make data objects "observable"

First, we define a data object, taking one of the heroes in the glory of the king as an example:

const hero = {
  health: 3000,
  IQ: 150
}

We define this hero to have 3000 health and 150 IQ. But I don't know who he is yet, but it doesn't matter, just need to know that this hero will run through our entire article, and our purpose is to know who this hero is through the attributes of this hero.

Now we can directly read and write the attribute value corresponding to this hero through hero.health and hero.IQ. However, we don't know when this hero's stats are being read or modified. So what should we do to make the hero take the initiative to tell us that his attributes have been modified? At this time, you need to use the power of Object.defineProperty.

Regarding the introduction of Object.defineProperty, MDN says this:

The Object.defineProperty() method defines a new property directly on an object, or modifies an existing property of an object, and returns the object.

In this article, we only use this method to make objects "observable". For more details about this method, please refer to https://developer.mozilla.org... , so I won't go into details.

So how to make this hero actively notify us of the read and write status of its attributes? First rewrite the above example:

let hero = {}
let val = 3000
Object.defineProperty(hero, 'health', {
  get () {
    console.log('我的health属性被读取了!')
    return val
  },
  set (newVal) {
    console.log('我的health属性被修改了!')
    val = newVal
  }
})

We define a health property for hero through the Object.defineProperty method, which triggers a console.log when it is read or written. Try it now:

console.log(hero.health)

// -> 3000
// -> 我的health属性被读取了!

hero.health = 5000
// -> 我的health属性被修改了

It can be seen that the hero can already actively tell us the reading and writing of its attributes, which also means that the data object of this hero is already "observable". In order to make all the attributes of the hero observable, we can think of a way:

/**
 * 使一个对象转化成可观测对象
 * @param { Object } obj 对象
 * @param { String } key 对象的key
 * @param { Any } val 对象的某个key的值
 */
function defineReactive (obj, key, val) {
  Object.defineProperty(obj, key, {
    get () {
      // 触发getter
      console.log(`我的${key}属性被读取了!`)
      return val
    },
    set (newVal) {
      // 触发setter
      console.log(`我的${key}属性被修改了!`)
      val = newVal
    }
  })
}

/**
 * 把一个对象的每一项都转化成可观测对象
 * @param { Object } obj 对象
 */
function observable (obj) {
  const keys = Object.keys(obj)
  keys.forEach((key) => {
    defineReactive(obj, key, obj[key])
  })
  return obj
}

Now we can define a hero as:

const hero = observable({
  health: 3000,
  IQ: 150
})

Readers can try reading and writing the hero's attributes on the console to see if it has become observable.

2. Calculated properties

Now that the hero has become observable, he will tell us about any read and write operations, but that's all, we still don't know who he is. If we want to modify the hero's health and IQ, he can actively tell him other information, how should this be done? Suppose this is possible:

watcher(hero, 'type', () => {
  return hero.health > 4000 ? '坦克' : '脆皮'
})

We define a watcher as a "listener" that listens to the hero's type property. The value of this type attribute depends on hero.health. In other words, when hero.health changes, hero.type should also change. The former is a dependency of the latter. We can call this hero.type a "computed property".

So, how should we construct this listener correctly? It can be seen that, in the assumption, the listener receives three parameters, namely the monitored object, the monitored property and the callback function, and the callback function returns a value of the monitored property. Following this line of thought, let's try to write a piece of code:

/**
 * 当计算属性的值被更新时调用
 * @param { Any } val 计算属性的值
 */
function onComputedUpdate (val) {
  console.log(`我的类型是:${val}`);
}

/**
 * 观测者
 * @param { Object } obj 被观测对象
 * @param { String } key 被观测对象的key
 * @param { Function } cb 回调函数,返回“计算属性”的值
 */
function watcher (obj, key, cb) {
  Object.defineProperty(obj, key, {
    get () {
      const val = cb()
      onComputedUpdate(val)
      return val
    },
    set () {
      console.error('计算属性无法被赋值!')
    }
  })
}

Now we can put the hero inside the listener and try running the above code:

watcher(hero, 'type', () => {
  return hero.health > 4000 ? '坦克' : '脆皮'
})

hero.type

hero.health = 5000

hero.type

// -> 我的health属性被读取了!
// -> 我的类型是:脆皮
// -> 我的health属性被修改了!
// -> 我的health属性被读取了!
// -> 我的类型是:坦克

It looks fine now, everything is working fine, is that the end of it? Don't forget, we are now manually reading hero.type to get the type of this hero, not what he told us voluntarily. If we want the hero to be able to initiate a notification as soon as the health attribute is modified, what should we do? This involves the core knowledge point of this article - dependency collection.

3. Dependency collection

We know that when an observable's properties are read or written, its getter/setter methods are triggered. Another way of thinking, if we can execute the onComputedUpdate() method in the listener in the getter/setter of the observable object, can we achieve the function of letting the object actively send notifications?

Since the onComputedUpdate() method in the listener needs to receive the value of the callback function as a parameter, and there is no such callback function in the observable object, we need to use a third party to help us connect the listener and the observable object.

This third party does one thing - collects the value of the callback function in the listener and the onComputedUpdate() method.

Now let's name this third party "dependency collector", let's see how it should be written:

const Dep = {
  target: null
}

It's that simple. The target of the dependency collector is used to store the onComputedUpdate() method in the listener.

After defining the dependency collector, let's go back to the listener to see where the onComputedUpdate() method should be assigned to Dep.target:

function watcher (obj, key, cb) {
  // 定义一个被动触发函数,当这个“被观测对象”的依赖更新时调用
  const onDepUpdated = () => {
    const val = cb()
    onComputedUpdate(val)
  }

  Object.defineProperty(obj, key, {
    get () {
      Dep.target = onDepUpdated
      // 执行cb()的过程中会用到Dep.target,
      // 当cb()执行完了就重置Dep.target为null
      const val = cb()
      Dep.target = null
      return val
    },
    set () {
      console.error('计算属性无法被赋值!')
    }
  })
}

We define a new onDepUpdated() method inside the listener. This method is very simple. It packs the value of the listener callback function and onComputedUpdate() into one piece, and assigns it to Dep.target. This step is very critical. Through this operation, the dependency collector obtains the callback value of the listener and the onComputedUpdate() method. As a global variable, Dep.target can of course be used by getters/setters of observable objects.

Take a second look at our watcher instance:

watcher(hero, 'type', () => {
  return hero.health > 4000 ? '坦克' : '脆皮'
})

In its callback function, the hero's health property is called, that is, the corresponding getter function is triggered. It's important to understand this, because next we need to go back to the defineReactive() method that defines the observable and rewrite it:

function defineReactive (obj, key, val) {
  const deps = []
  Object.defineProperty(obj, key, {
    get () {
      if (Dep.target && deps.indexOf(Dep.target) === -1) {
        deps.push(Dep.target)
      }
      return val
    },
    set (newVal) {
      val = newVal
      deps.forEach((dep) => {
        dep()
      })
    }
  })
}

As you can see, in this method, we define an empty array deps, and when the getter is triggered, a Dep.target will be added to it. Going back to the key knowledge point Dep.target is equal to the onComputedUpdate() method of the listener. At this time, the observable object has been bound to the listener. Whenever the setter of the observable object is triggered, the Dep.target method stored in the array will be called, that is, the onComputedUpdate() method inside the listener is automatically triggered.

As for why deps here is an array instead of a variable, it is because the same property may be depended on by multiple computed properties, that is, there are multiple Dep.targets. Define deps as an array. If the setter of the current property is triggered, the onComputedUpdate() method of multiple computed properties can be called in batches.

After completing these steps, basically our entire responsive system has been built. The complete code is pasted below:

/**
 * 定义一个“依赖收集器”
 */
const Dep = {
  target: null
}

/**
 * 使一个对象转化成可观测对象
 * @param { Object } obj 对象
 * @param { String } key 对象的key
 * @param { Any } val 对象的某个key的值
 */
function defineReactive (obj, key, val) {
  const deps = []
  Object.defineProperty(obj, key, {
    get () {
      console.log(`我的${key}属性被读取了!`)
      if (Dep.target && deps.indexOf(Dep.target) === -1) {
        deps.push(Dep.target)
      }
      return val
    },
    set (newVal) {
      console.log(`我的${key}属性被修改了!`)
      val = newVal
      deps.forEach((dep) => {
        dep()
      })
    }
  })
}

/**
 * 把一个对象的每一项都转化成可观测对象
 * @param { Object } obj 对象
 */
function observable (obj) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i], obj[keys[i]])
  }
  return obj
}

/**
 * 当计算属性的值被更新时调用
 * @param { Any } val 计算属性的值
 */
function onComputedUpdate (val) {
  console.log(`我的类型是:${val}`)
}

/**
 * 观测者
 * @param { Object } obj 被观测对象
 * @param { String } key 被观测对象的key
 * @param { Function } cb 回调函数,返回“计算属性”的值
 */
function watcher (obj, key, cb) {
  // 定义一个被动触发函数,当这个“被观测对象”的依赖更新时调用
  const onDepUpdated = () => {
    const val = cb()
    onComputedUpdate(val)
  }

  Object.defineProperty(obj, key, {
    get () {
      Dep.target = onDepUpdated
      // 执行cb()的过程中会用到Dep.target,
      // 当cb()执行完了就重置Dep.target为null
      const val = cb()
      Dep.target = null
      return val
    },
    set () {
      console.error('计算属性无法被赋值!')
    }
  })
}

const hero = observable({
  health: 3000,
  IQ: 150
})

watcher(hero, 'type', () => {
  return hero.health > 4000 ? '坦克' : '脆皮'
})

console.log(`英雄初始类型:${hero.type}`)

hero.health = 5000

// -> 我的health属性被读取了!
// -> 英雄初始类型:脆皮
// -> 我的health属性被修改了!
// -> 我的health属性被读取了!
// -> 我的类型是:坦克

The above code can be executed directly on the code pen click preview or on the browser console.

Fourth, code optimization

In the above example, the dependency collector is just a simple object. In fact, the functions related to dependency collection, such as the deps array inside defineReactive(), should be integrated into the Dep instance, so we can rewrite the dependency collector:

class Dep {
  constructor () {
    this.deps = []
  }

  depend () {
    if (Dep.target && this.deps.indexOf(Dep.target) === -1) {
      this.deps.push(Dep.target)
    }
  }

  notify () {
    this.deps.forEach((dep) => {
      dep()
    })
  }
}

Dep.target = null

In the same way, we encapsulate and optimize both observable and watcher to make this responsive system modular:

class Observable {
  constructor (obj) {
    return this.walk(obj)
  }

  walk (obj) {
    const keys = Object.keys(obj)
    keys.forEach((key) => {
      this.defineReactive(obj, key, obj[key])
    })
    return obj
  }

  defineReactive (obj, key, val) {
    const dep = new Dep()
    Object.defineProperty(obj, key, {
      get () {
        dep.depend()
        return val
      },
      set (newVal) {
        val = newVal
        dep.notify()
      }
    })
  }
}
class Watcher {
  constructor (obj, key, cb, onComputedUpdate) {
    this.obj = obj
    this.key = key
    this.cb = cb
    this.onComputedUpdate = onComputedUpdate
    return this.defineComputed()
  }

  defineComputed () {
    const self = this
    const onDepUpdated = () => {
      const val = self.cb()
      this.onComputedUpdate(val)
    }

    Object.defineProperty(self.obj, self.key, {
      get () {
        Dep.target = onDepUpdated
        const val = self.cb()
        Dep.target = null
        return val
      },
      set () {
        console.error('计算属性无法被赋值!')
      }
    })
  }
}

Then let's run:

const hero = new Observable({
  health: 3000,
  IQ: 150
})

new Watcher(hero, 'type', () => {
  return hero.health > 4000 ? '坦克' : '脆皮'
}, (val) => {
  console.log(`我的类型是:${val}`)
})

console.log(`英雄初始类型:${hero.type}`)

hero.health = 5000

// -> 英雄初始类型:脆皮
// -> 我的类型是:坦克

The code has been placed on the code pen and clicked to preview, and the browser console can also be run~

Five, the end

Seeing the above code, do you find that it is very similar to the VueJS source code? In fact, the ideas and principles of VueJS are similar, but it does more things, but the core is still here.

When I was learning the source code of VueJS, I was once dizzy by the responsive principle, but I didn't understand it all at once. After continuous thinking and trying, and referring to the ideas of many other people, I finally fully grasped this knowledge point. I hope this article is helpful to you. If you find any mistakes or omissions, please point them out to me. Thank you~

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324973396&siteId=291194637