Vue source code analysis: the principle of two-way data binding (responsive)

What is the principle of two-way data binding (responsive) of vue? This still has to start from the source code of vue:

We know that a major feature of vue is 数据驱动视图how to understand the six words of data-driven view?

Data: state;

Drive: render function;

View: UI of the page;

In this way, in fact, we can get such a formula: UI = render(state), the change of state directly leads to the change of UI, and the constant one is render, and vue plays the role of render. So how does vue know the state change? There is a word called 变化侦测, because it is mentioned in the current mainstream technology stacks, vue, react, and angular, that is 状态追宗, once the data changes, update the view. Let's start with the change detection of the object.

The so-called change detection means that we can know when the data has been read or when the data has been rewritten.

Data detection

1. Realize object detection (observable)

Object detection is by Object.defineProperty()this method.

First define an object animal:


let animal = {
    name: 'dog',
    age: 3

So how do we know when the properties of this animal object are modified? Let's look at the following operations:


let name = 'cat'
Object.defineProperty(animal, 'name', {
    configrable: true,    //    描述属性是否配置,以及可否删除
    enumerable: true,     //    描述属性是否会出现在for in 或者 Object.keys()的遍历中
    writable: true,       //    属性的值是否可以被重写
    get () {
        //    这里读取了name的值
        return name
    },
    set (value) {
        //    这里设置了name的值
        name = value
    }

In this way, when the value of name is changed to cat, it will become observable and detectable. But this is only possible for a certain attribute of the object to be detected, but the attribute of the object is often not one. What should I do when there are more than one attribute? In fact, it is very simple, yes, yes, it is the recursion you want to talk about. Directly on the code:

//    源码位置:src/core/observer/index.js
export class Observer {
    constructor (value) {
        this.value = value
        def(value,'__ob__',this)
        if (Array.isArray(value)) {
              // 当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])
        }
    }
}
function defineReactive (obj,key,val) {
      if (arguments.length === 2) {
            val = obj[key]
      }
      if(typeof val === 'object'){
          new Observer(val)
      }
      Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get(){
              console.log(`${key}属性被读取了`);
              return val;
          },
          set(newVal){
              if(val === newVal){
                  return
              }
              console.log(`${key}属性被修改了`);
              val = newVal;
          }
      })
}

We declare an Observer class, which is used to turn all the attributes of the object into detectable, and we need to add an instance of Observer with the __ob__ attribute and the value to each detectable object, which is equivalent Yu has set a flag for each value, which means that the value is responsive. Then when the value is an object, we get all its keys, loop through the method of calling Object.defineProperty(), call the get/set method to detect, when it is found that the incoming object is an Object, pass In a recursive way, the Object is instantiated through the Observer class again. In this way, we have achieved object detection.

2. Realize array detection (observable)

We talked about object detection above, now let's talk about array detection.

The observable of the array is different from the observable of the object, because the array cannot be observed with Object.defineProperty(). So how to make the array observable, in fact, the same, the array is 拦截器done through, so what is the sacred interceptor?

We know that there are just a few native methods for manipulating arrays in JavaScript, push/pop/shift/unshift/splice/sort/reverse. These methods are all accessed through Array.prototype, so if we redefine a method on Array.prototype to implement a native method, such as newPush to achieve the same effect as push, is it perfect? The native method is not modified, and the corresponding operation can be done before calling the native method.

let arr = [1,2,3]
//    arr.push(4) 原生方法调用
Array.prototype.newPush = function(val){
      console.log('arr被修改了')
      this.push(val)
}
arr.newPush(4)

So,数组的拦截器就是在数组实例和Array.prototype之间重写了操作数组的方法 . In this way, when the processed method is called, the rewritten method is called instead of the native method. Look at the implementation of the interceptor:

// 源码位置:/src/core/observer/array.js
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) {
  const original = arrayProto[method]
  Object.defineProperty(arrayMethods, method, {
    enumerable: false,
    configurable: true,
    writable: true,
    value:function mutator(...args){
      const result = original.apply(this, args)
      return result
    }
  })
})

This is the implementation of the interceptor. When the method of the array is called, what is actually called is mutator函数, so we can do some operations in this function. But this is not enough 只有将拦截器挂载在数组实例和Array.prototype之间才能生效,也就是将数据的__proto__属性设置为拦截器arrayMethods即可.

augment(value, arrayMethods, arrayKeys)

// 源码位置:/src/core/observer/index.js
export class Observer {
  constructor (value) {
    this.value = value
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
    } else {
      this.walk(value)
    }
  }
}
// 能力检测:判断__proto__是否可用,因为有的浏览器不支持该属性
export const hasProto = '__proto__' in {}
 
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
 
/**
 * Augment an target Object or Array by intercepting
 * the prototype chain using __proto__
 */
function protoAugment (target, src: Object, keys: any) {
  target.__proto__ = src
}
 
/**
 * Augment an target Object or Array by defining
 * hidden properties.
 */
/* istanbul ignore next */
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}
 

When the interceptor takes effect, if the array changes, it can be detected.

The above is how to make changes in objects and arrays detectable. It is far from enough to just be detected. Let’s talk about relying on the collection.

Dependent collection of data

1. Dependency collection of objects

Dependent collection : Which data is used in the view, the view depends on which data, and the changed data is collected. This process is dependent collection.

Where to collect dependencies : Observable data getteris obtained in, so getter is a place to collect dependencies.

When to notify dependent updates : data changes settercan be observed in the, naturally, notification dependent updates are in the setter.

Where should the dependencies be collected? And look down:

Because we cannot control the size of the updated data, we need a dependency manager for the collection of dependencies:

// 源码位置:src/core/observer/dep.js
export default class Dep {
   constructor () {
       this.subs = []
   }

   addSub (sub) {
       this.subs.push(sub)
   }
   // 删除一个依赖
   removeSub (sub) {
       remove(this.subs, sub)
   }
   // 添加一个依赖
   depend () {
       if (window.target) {
           this.addSub(window.target)
       }
   }
   // 通知所有依赖更新
   notify () {
       const subs = this.subs.slice()
       for (let i = 0, l = subs.length; i < l; i++) {
           subs[i].update()
       }
   }
}

/**
* Remove an item from an array
*/
export function remove (arr, item) {
   if (arr.length) {
       const index = arr.indexOf(item)
       if (index > -1) {
           return arr.splice(index, 1)
       }
   }
}

With the dependency manager, we can collect dependencies in the getter and notify the dependency update in the setter.

function defineReactive (obj,key,val) {
    if (arguments.length === 2) {
         val = obj[key]
    }
    if(typeof val === 'object'){
        new Observer(val)
    }
    const dep = new Dep()  //实例化一个依赖管理器,生成一个依赖管理数组dep
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get(){
            dep.depend()    // 在getter中收集依赖
            return val;
        },
        set(newVal){
            if(val === newVal){
            return
        }
        val = newVal;
        dep.notify()   // 在setter中通知依赖更新
     }
  })
}

The dep.depend() method is called in the getter to collect dependencies, and the dep.notify() method is called in the setter to notify all dependency updates.

In fact, a watcher class that depends on data for each change is implemented in Vue. When the data changes later, we do not directly notify the dependent update, but notify the dependent Watch instance, and the Watcher instance will notify the real view.

export default class Watcher {
  constructor (vm,expOrFn,cb) {
    this.vm = vm;
    this.cb = cb;
    this.getter = parsePath(expOrFn)
    this.value = this.get()
  }
  get () {
    window.target = this;
    const vm = this.vm
    let value = this.getter.call(vm, vm)
    window.target = undefined;
    return value
  }
  update () {
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue)
  }
}
 
/**
 * Parse simple path.
 * 把一个形如'data.a.b.c'的字符串路径所表示的值,从真实的data对象中取出来
 * 例如:
 * data = {a:{b:{c:2}}}
 * parsePath('a.b.c')(data)  // 2
 */
const bailRE = /[^\w.$]/
export function parsePath (path) {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}
 

analysis:

  • When the watch is instantiated, the constructor is executed first;
  • Call this.get() instance method in the constructor;
  • In the get() method, first assign the instance itself to a global unique object window.target by window.target = this, and then get the dependent object by let value = this.getter.call(vm, vm) Data, the purpose of obtaining dependent data is to trigger the getter on the data. As we said above, dep.depend() is called to collect dependencies in the getter, and window.target is retrieved from dep.depend(). And store it in the dependency array, and release window.target at the end of the get() method.
  • When the data changes, the data setter is triggered. The dep.notify() method is called in the setter. In the dep.notify() method, all dependencies (ie watcher instances) are traversed and the dependent update() method is executed. That is, the update() instance method in the Watcher class calls the update callback function of the data change in the update() method to update the view.

A brief summary is:

Watcher first sets itself to the globally unique designated location (window.target), and then reads the data. Because the data is read, the getter of this data is triggered. Then, in the getter, the Watcher that is currently reading data will be read from the globally unique position, and this watcher will be collected into Dep. After collection, when the data changes, a notification will be sent to each Watcher in Dep. In this way, Watcher can actively subscribe to any data change. In order to facilitate understanding, we have drawn the relationship flow chart, as shown below:

Insert picture description here
Above, all operations such as the detection of Object data, the collection of dependencies, and the update of dependencies have been completely completed.

2. Dependency collection of array

For arrays, in fact, there is a method similar to objects in dependent collection.

The collection of the dependency of the array is in the observer class:

// 源码位置:/src/core/observer/index.js
export class Observer {
  constructor (value) {
    this.value = value
    this.dep = new Dep()    // 实例化一个依赖管理器,用来收集数组依赖
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
    } else {
      this.walk(value)
    }
  }
}

In this way, we have also implemented a dependency manager in the observer.

So we can look at how the dependencies of the array are collected, and go directly to the code:

function defineReactive (obj,key,val) {
  let childOb = observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get(){
      if (childOb) {
        childOb.dep.depend()
      }
      return val;
    },
    set(newVal){
      if(val === newVal){
        return
      }
      val = newVal;
      dep.notify()   // 在setter中通知依赖更新
    }
  })
}
 
/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 * 尝试为value创建一个0bserver实例,如果创建成功,直接返回新创建的Observer实例。
 * 如果 Value 已经存在一个Observer实例,则直接返回它
 */
export function observe (value, asRootData){
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else {
    ob = new Observer(value)
  }
  return ob
}

Inside the observe function, first determine whether there is an __ob__ attribute on the current incoming data, because as mentioned in the previous article, if the data has an __ob__ attribute, it means that it has been transformed into a reactive type. If not It means that the data is not responsive yet, then call new Observer(value) to convert it into responsive, and return the Observer instance corresponding to the data. In the defineReactive function, first obtain the Observer instance childOb corresponding to the data, and then call the dependency manager on the Observer instance in the getter to collect the dependencies.

This makes it very easy to rely on notifications to rely on updates.

methodsToPatch.forEach(function (method) {
 const original = arrayProto[method]
 def(arrayMethods, method, function mutator (...args) {
   const result = original.apply(this, args)
   const ob = this.__ob__
   // notify change
   ob.dep.notify()
   return result
 })
})

At this point, the collection of dependencies of the array is basically achieved, but this is not over yet. The array is not one layer, maybe multiple layers, so a deep inspection is needed:

export class Observer {
  value: any;
  dep: Dep;
 
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)   // 将数组中的所有元素都转化为可被侦测的响应式
    } else {
      this.walk(value)
    }
  }
 
  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
 
export function observe (value, asRootData){
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else {
    ob = new Observer(value)
  }
  return ob
}

Detection of new elements in the array

methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args   // 如果是push或unshift方法,那么传入参数就是新增的元素
        break
      case 'splice':
        inserted = args.slice(2) // 如果是splice方法,那么传入参数列表中下标为2的就是新增的元素
        break
    }
    if (inserted) ob.observeArray(inserted) // 调用observe函数将新增的元素转化成响应式
    // notify change
    ob.dep.notify()
    return result
  })
})

The overall response principle of vue data is like this. Objects cannot be monitored, object properties are increased or decreased, and arrays cannot be monitored for changes in array subscripts. Vue.set() and Vue.delete methods have been added to vue to handle them.

The above is all the reactive principle, or the principle of two-way data binding.

Just as a learning record, refer to the article: https://blog.csdn.net/leelxp/article/details/106936827

Guess you like

Origin blog.csdn.net/weixin_44433499/article/details/114259116