Scheduling Implementation of Responsive System and Watcher

background

In view of the fact that vue3 has not been officially released yet, and some of the implementation ideas in vue2 are still very valuable for reference, so this principled explanation is still vue2, I hope to enlighten you ~

Responsive system implementation

Vue.js is an MVVM framework. The data model is just ordinary JavaScript objects, but when these objects are manipulated, they can affect the corresponding views. Its core implementation is the "responsive system".

In vue 2.0, it is based on the  Object.definePropertyimplementation of "responsive system". Vue3 is based on the  Proxy/Reflect realization, detailed analysis of vue3 will have time to write, this article is about the realization of vue2.

Mainly involves attributes:

  • enumerable, whether the property can be enumerated, default false.

  • configurable, whether the property can be modified or deleted, the default is false.

  • get, the method to get the attribute.

  • set, the method of setting properties.

The basic principle of responsiveness is to process the data of options in the Vue constructor. That is, when the vue instance is initialized, each property of data, props and other objects is defined once by Object.defineProperty. When the data is set, do some operations to change the corresponding view.

class Vue {
    /* Vue构造类 */
    constructor(options) {
        this._data = options.data;
        observer(this._data);
    }
}
function observer (value) {
    if (!value || (typeof value !== 'object')) {
        return;
    }
    
    Object.keys(value).forEach((key) => {
        defineReactive(value, key, value[key]);
    });
}
function defineReactive (obj, key, val) {
    Object.defineProperty(obj, key, {
        enumerable: true,       /* 属性可枚举 */
        configurable: true,     /* 属性可被修改或删除 */
        get: function reactiveGetter () {
            return val;
        },
        set: function reactiveSetter (newVal) {
            if (newVal === val) return;
            cb(newVal);
        }
    });
}

In practical applications, various systems are extremely complicated. Suppose we now have a global object, we may use it in multiple Vue objects for display. Or maybe the data written in data is not applied to the view, and updating the view at this time is superfluous. This depends on the collection process.

Dependent collection

The so-called dependent collection is to collect the place where a piece of data is used. When this data changes, it will notify each place to do the corresponding operation. The basic mode of "subscriber" in VUE is as follows:

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

With subscribers, let's take a look at the implementation of Watcher. The source code Watcher is more logical, and the simplified model is as follows

class Watcher{
    constructor(vm,expOrFn,cb,options){
        //传进来的对象 例如Vue
        this.vm = vm
        //在Vue中cb是更新视图的核心,调用diff并更新视图的过程
        this.cb = cb
        //收集Deps,用于移除监听
        this.newDeps = []
        this.getter = expOrFn
        //设置Dep.target的值,依赖收集时的watcher对象
        this.value =this.get()
    }

    get(){
        //设置Dep.target值,用以依赖收集
        pushTarget(this)
        const vm = this.vm
        let value = this.getter.call(vm, vm)
        return value
    }

    //添加依赖
      addDep (dep) {
          // 这里简单处理,在Vue中做了重复筛选,即依赖只收集一次,不重复收集依赖
        this.newDeps.push(dep)
        dep.addSub(this)
      }

      //更新
      update () {
        this.run()
    }

    //更新视图
    run(){
        //这里只做简单的console.log 处理,在Vue中会调用diff过程从而更新视图
        console.log(`这里会去执行Vue的diff相关方法,进而更新数据`)
    }
}

defineReactive detailed logic

exportfunction defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

所以响应式原理就是,我们通过递归遍历,把vue实例中data里面定义的数据,用defineReactive(Object.defineProperty)重新定义。每个数据内新建一个Dep实例,闭包中包含了这个 Dep 类的实例,用来收集 Watcher 对象。在对象被「读」的时候,会触发 reactiveGetter 函数把当前的 Watcher 对象(存放在 Dep.target 中)收集到 Dep 类中去。之后如果当该对象被「写」的时候,则会触发 reactiveSetter 方法,通知 Dep 类调用 notify 来触发所有 Watcher 对象的 update 方法更新对应视图。

The birth of Watcher

In Vue, there are 4 situations that will generate Watcher:

  1. The watcher on the Vue instance object, observe the root data, and re-render the component when it changes

  2. The watcher created by the user with the watch attribute in the vue object

  3. The calculated attribute created by the user in the vue object is essentially a watcher

  4. Watcher created by user using vm. $ Watch

Wathcer will increase or decrease, and may be added when rendering. Therefore, there must be a Schedule to schedule the Watcher. Some main codes are as follows:

 queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }

The role of Schedule:

  1. Deduplication, each Watcher has a unique id. First, if the id is already in the queue, skip it, there is no need to repeat the execution. If the id is not in the queue, it depends on whether the queue is being executed. If it is not in execution, the queue is executed in the next time slice, so the queue is always executed asynchronously.

  2. Sorting is performed in the order of parsing and rendering, that is, Watcher is executed first. The id in Watcher is self-increasing, the id created first is smaller than the id created later. So there will be the following rules:

2.1. Components are allowed to be nested, and the analysis must parse the parent component first and then the child component. So the id of the parent component is smaller than the child component.

2.2. The Watcher created by the user will be parsed before the one created when rendering. Therefore, the id of the Watcher created by the user is smaller than that created when rendering.

  1. Delete Watcher. If the Watcher of a component is in the queue and its parent component is deleted, this Watcher must also be deleted at this time.

  2. During the execution of the queue, an object circular is stored, which contains the number of executions of each watcher. If any watcher executes more than the number of times defined by MAX_UPDATE_COUNT, it is considered to be an infinite loop, and it is no longer executed. The default is 100 times.

总之,调用的作用就是管理 Watcher。

supplement

How to use Object.defineProperty to redefine the array object in VUE2? Why does the view not change responsively when we directly modify an item in the data (arr [3] = 4).

The answer is that the array's responsiveness is not complete, and VUE has only rewritten limited methods. The rewriting logic is as follows:

const arrayProto = Array.prototype
exportconst arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
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
        break
      case'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})
Published 117 original articles · 69 praises · 10,000+ views

Guess you like

Origin blog.csdn.net/zsd0819qwq/article/details/105356741