The Vue Response Principle is Understood

  Front end, javascript, Programmer, vue.js

Preface

One of Vue’s most unique characteristics is its non-invasive responsive system. The data model is just an ordinary JavaScript object. When you modify them, the view will be updated. This makes state management very simple and straightforward, but it is equally important to understand how it works so that you can avoid some common problems. -Official Documents
This article will give a detailed introduction to the reactive principle and take you to implement a basic version of the reactive system. Please stamp the code of this articleGithub blog

What is responsive

Let’s look at an example first:

<div id="app">
    <div>Price :¥{{ price }}</div>
    <div>Total:¥{{ price * quantity }}</div>
    <div>Taxes: ¥{{ totalPriceWithTax }}</div>
    <button @click="changePrice">改变价格</button>
</div>
var app = new Vue({
  el: '#app',
  data() {
    return {
      price: 5.0,
      quantity: 2
    };
  },
  computed: {
    totalPriceWithTax() {
      return this.price * this.quantity * 1.03;
    }
  },
  methods: {
    changePrice() {
      this.price = 10;
    }
  }
})

响应式.gif

In the above example, when price changes, Vue knew that he needed to do three things:

  • Update the value of price on the page
  • Calculates the value of the expression price*quantity to update the page
  • Call totalPriceWithTax function to update the page

After the data changes, the page will be rendered again. This is Vue response. How does this happen?

To complete this process, we need to:

  • Detect changes in data
  • What data does the collection view rely on
  • When the data changes, the view part that needs to be updated is automatically “notified” and updated.

The corresponding professional proverbs are:

  • Data hijacking/data proxy
  • Dependency collection
  • Publish subscription mode

How to Detect Data Changes

First of all, there is a question, in Javascript, how to detect changes in an object?
In fact, there are two ways to detect changes: usingObject.definePropertyAnd ES6ProxyThis is data hijacking or data proxy. This part of code mainly refers to Everest architecture class.

Method 1.Object.defineProperty implementation

Vue monitors the changes of data by setting the setter/getter method of the object property, and collects dependencies through getter. Each setter method is an observer and notifies subscribers to update the view when the data changes.

function render () {
  console.log('模拟视图渲染')
}
let data = {
  name: '浪里行舟',
  location: { x: 100, y: 100 }
}
observe(data)
function observe (obj) { // 我们来用它使对象变成可观察的
  // 判断类型
  if (!obj || typeof obj !== 'object') {
    return
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
  function defineReactive (obj, key, value) {
    // 递归子属性
    observe(value)
    Object.defineProperty(obj, key, {
      enumerable: true, //可枚举(可以遍历)
      configurable: true, //可配置(比如可以删除)
      get: function reactiveGetter () {
        console.log('get', value) // 监听
        return value
      },
      set: function reactiveSetter (newVal) {
        observe(newVal) //如果赋值是一个对象,也要递归子属性
        if (newVal !== value) {
          console.log('set', newVal) // 监听
          render()
          value = newVal
        }
      }
    })
  }
}
data.location = {
  x: 1000,
  y: 1000
} //set {x: 1000,y: 1000} 模拟视图渲染
data.name // get 浪里行舟

The main function of the above code is to:observeThis function passes in aobj(Objects that need to be tracked for changes), each attribute of the object is passed through by traversing all attributesdefineReactiveProcessing, in order to achieve the detection of object changes. It is worth noting that,observeRecursive calls are made.
How do we detect VuedataThe data in is actually very simple:

class Vue {
    /* Vue构造类 */
    constructor(options) {
        this._data = options.data;
        observer(this._data);
    }
}

In this way, as long as we have new as a Vue object, we willdataTo track changes in the data.
However, there are several points to be added in this way:

  • The addition or deletion of object attributes cannot be detected(e.g.data.location.a=1)。

This is because Vue passedObject.definePropertyTo convert the object’s key togetter/setterTo track changes, butgetter/setterOnly one data can be tracked to see if it has been modified, and the newly added and deleted attributes cannot be tracked. If it is to delete attributes, we can usevm.$deleteImplementation, what should I do if I add a new attribute?
1) can be usedVue.set(location, a, 1)Method to add reactive attributes to nested objects;
2) You can also reassign this object, for exampledata.location = {...data.location,a:1}

  • Object.definePropertyYou cannot listen to changes in arrays, you need to rewrite array methods., the specific code is as follows:
function render() {
  console.log('模拟视图渲染')
}
let obj = [1, 2, 3]
let methods = ['pop', 'shift', 'unshift', 'sort', 'reverse', 'splice', 'push']
// 先获取到原来的原型上的方法
let arrayProto = Array.prototype
// 创建一个自己的原型 并且重写methods这些方法
let proto = Object.create(arrayProto)
methods.forEach(method => {
  proto[method] = function() {
    // AOP
    arrayProto[method].call(this, ...arguments)
    render()
  }
})
function observer(obj) {
  // 把所有的属性定义成set/get的方式
  if (Array.isArray(obj)) {
    obj.__proto__ = proto
    return
  }
  if (typeof obj == 'object') {
    for (let key in obj) {
      defineReactive(obj, key, obj[key])
    }
  }
}
function defineReactive(data, key, value) {
  observer(value)
  Object.defineProperty(data, key, {
    get() {
      return value
    },
    set(newValue) {
      observer(newValue)
      if (newValue !== value) {
        render()
        value = newValue
      }
    }
  })
}
observer(obj)
function $set(data, key, value) {
  defineReactive(data, key, value)
}
obj.push(123, 55)
console.log(obj) //[1, 2, 3, 123,  55]

This method rewrites the common methods of arrays, thus overwriting the original array methods. The rewritten array methods need to be able to be intercepted. However, there are some arrays that cannot be intercepted when Vue is operated. Of course, there is no way to respond. For example:

obj.length-- // 不支持数组的长度变化
obj[0]=1  // 修改数组中第一个元素,也无法侦测数组的变化

ES6 provides meta-programming capability, so Vue3.0 may use Proxy in ES6 as the main way to implement data proxy.

Method 2.Proxy Implementation

ProxyIs a new feature of JavaScript 2015.ProxyThe proxy for is for the entire object, not an attribute of the objectTherefore, it is different fromObject.definePropertyThe must traverse each property of the object.ProxyOnly one layer of proxy is needed to monitor all attribute changes under the same level structure. Of course, recursion is still needed for deep structures. In additionProxySupports changes to proxy arrays.

function render() {
  console.log('模拟视图的更新')
}
let obj = {
  name: '前端工匠',
  age: { age: 100 },
  arr: [1, 2, 3]
}
let handler = {
  get(target, key) {
    // 如果取的值是对象就在对这个对象进行数据劫持
    if (typeof target[key] == 'object' && target[key] !== null) {
      return new Proxy(target[key], handler)
    }
    return Reflect.get(target, key)
  },
  set(target, key, value) {
    if (key === 'length') return true
    render()
    return Reflect.set(target, key, value)
  }
}

let proxy = new Proxy(obj, handler)
proxy.age.name = '浪里行舟' // 支持新增属性
console.log(proxy.age.name) // 模拟视图的更新 浪里行舟
proxy.arr[0] = '浪里行舟' //支持数组的内容发生变化
console.log(proxy.arr) // 模拟视图的更新 ['浪里行舟', 2, 3 ]
proxy.arr.length-- // 无效

The above code is not only simplified, but also a set of code is applicable to the detection of objects and arrays. But ..ProxyCompatibility is not very good!

Why do you want to collect dependencies

The reason why we want to observe the data is that when the attribute of the data changes, we can inform those places that used the data. For example, in the first example, price data is used in the template, and when it changes, a notification is sent to the place where it is used. If multiple Vue instances share a variable, as in the following example:

let globalData = {
    text: '浪里行舟'
};
let test1 = new Vue({
    template:
        `<div>
            <span>{{text}}</span> 
        <div>`,
    data: globalData
});
let test2 = new Vue({
    template:
        `<div>
            <span>{{text}}</span> 
        <div>`,
    data: globalData
});

If we execute the following statement:

globalData.text = '前端工匠';

At this time, we need to notify test1 and test2, the two Vue instances, to update the view. Only by collecting the dependencies can we know where to rely on my data and distribute the updates when the data is updated. How is it achieved by relying on collection? The core idea is “event publishing and subscription mode”. Next, let’s introduce two important roles-subscriber Dep and observer Watcher, and then explain how collection dependencies are implemented.

Subscribers Dep

1. why is Dep introduced

Collecting dependencies requires finding a place to store dependencies, so we created Dep, which is used to collect dependencies, delete dependencies, and send messages to dependencies.

So let’s first implement a subscriber Dep class, which is used to decouple the dependency collection and distribution update operations of attributes. specifically, its main function is to store Watcher observer objects. We can putWatcher is understood as an intermediary role, informing it when data changes, and then informing other places.

2. simple implementation of dep

class Dep {
    constructor () {
        /* 用来存放Watcher对象的数组 */
        this.subs = [];
    }
    /* 在subs中添加一个Watcher对象 */
    addSub (sub) {
        this.subs.push(sub);
    }
    /* 通知所有Watcher对象更新视图 */
    notify () {
        this.subs.forEach((sub) => {
            sub.update();
        })
    }
}

The above code mainly does two things:

  • AddSub method can be used to add a Watcher subscription operation to the current Dep object.
  • The notify method is used to notify all Watcher objects in the subs of the current Dep object to trigger an update operation.

Therefore, addSub is called when collection is needed, and notify is called when updates need to be distributed. The call is also simple:

let dp = new Dep()
dp.addSub(() => {
    console.log('emit here')
})
dp.notify()

Observer Watcher

1. Why is Watcher Introduced

A Watcher class is defined in Vue to represent observation subscription dependency. As for why Watcher was introduced, vue.js gives a good explanation:

When the attribute changes, we need to notify the place where the data is used, and there are many places where the data is used, and the types are different. It can be either a template or a watch written by the user, and then we need to abstract a class that can handle these situations centrally. Then, in the dependency collection phase, we only collect instances of this encapsulated class to come in, and the notification will only notify it, and then it will be responsible for notifying other places.

The purpose of Dependency collection is to store the observer Watcher object in the subs of the subscriber dep in the current closure. Form such a relationship as shown below (refer to “Analysis of Vue.js Internal Operation Mechanism” in the figure).

image.png

2. Simple implementation of 2.Watcher

class Watcher {
  constructor(obj, key, cb) {
    // 将 Dep.target 指向自己
    // 然后触发属性的 getter 添加监听
    // 最后将 Dep.target 置空
    Dep.target = this
    this.cb = cb
    this.obj = obj
    this.key = key
    this.value = obj[key]
    Dep.target = null
  }
  update() {
    // 获得新值
    this.value = this.obj[this.key]
   // 我们定义一个 cb 函数,这个函数用来模拟视图更新,调用它即代表更新视图
    this.cb(this.value)
  }
}

The above is a simple implementation of Watcher. When executing the constructor, it willDep.targetPointing to itself, so that the corresponding Watcher is collected, the corresponding Watcher is taken out when the update is distributed, and then the corresponding watcher is executedupdateFunction.

Collection dependency

The so-called dependency is actually the Watcher. As for how to collect the dependence, in a word,Collects dependencies in getter and triggers dependencies in setter.First collect the dependencies, that is, collect the places where the data is used, and then trigger the previously collected dependency cycle once when the attribute changes.

Specifically, when the outside world reads data through the Watcher, it will trigger the getter to add the Watcher to the dependency. The Watcher that triggers the getter will be collected into Dep. When the data changes, the dependency list will be circulated and all Watcher will be notified.

Finally, we modified the defineReactive function and added the code related to dependency collection and distribution update to the custom function to realize a simple data response.

function observe (obj) {
  // 判断类型
  if (!obj || typeof obj !== 'object') {
    return
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
  function defineReactive (obj, key, value) {
    observe(value)  // 递归子属性
    let dp = new Dep() //新增
    Object.defineProperty(obj, key, {
      enumerable: true, //可枚举(可以遍历)
      configurable: true, //可配置(比如可以删除)
      get: function reactiveGetter () {
        console.log('get', value) // 监听
     // 将 Watcher 添加到订阅
       if (Dep.target) {
         dp.addSub(Dep.target) // 新增
       }
        return value
      },
      set: function reactiveSetter (newVal) {
        observe(newVal) //如果赋值是一个对象,也要递归子属性
        if (newVal !== value) {
          console.log('set', newVal) // 监听
          render()
          value = newVal
     // 执行 watcher 的 update 方法
          dp.notify() //新增
        }
      }
    })
  }
}

class Vue {
    constructor(options) {
        this._data = options.data;
        observer(this._data);
        /* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
        new Watcher();
        console.log('模拟视图渲染');
    }
}

When the render function is rendered, reading the value of the required object will trigger the reactiveGetter function to collect the current Watcher object (stored in Dep.target) into the Dep class. After that, if the value of the object is modified, the reactiveSetter method will be triggered, informing the Dep class to call notify to trigger the update method of all Watcher objects to update the corresponding views.

Summary

Finally, we will review the whole process according to the following figure (refer to vue.js):

image.png

  • Innew Vue()After that, Vue calls_initFunction to initialize, that is, init process, in which Data is converted into getter/setter form by Observer to track changes in data, which will be executed when the set object is readgetterFunction, which is executed when assigned a valuesetterFunction.
  • When the render function is executed, because the value of the required object is read, the getter function is triggered to add the Watcher to the dependency for dependency collection.
  • When modifying the value of an object, the corresponding will be triggeredsetter,setterBefore notificationDependency collectionEach Watcher in the obtained Dep tells them that their own values have changed and the view needs to be re-rendered. This is when these Watcher will start callingupdateTo update the view.

Vue Series Articles CC Door

To recommend a useful BUG monitoring toolFundebug, welcome to try free!

Welcome to pay attention to the public number:Front end craftsmanWe witness your growth together!
image

Reference articles and books