preface

Recently I looked at the source code of Vue (version 2.0), and decided to make a precipitation on the core part – responsive principle, welcome interested partners to read

The source code parsing

The figure above is the flow chart provided by the government. For easy understanding, I will analyze it in the following order

  1. Data section, what does Vue do with Data
  2. Why and how do you collect dependencies
  3. What is Watcher and how does it play a role
  4. When and how is the component’s re-render triggered

1. Bidirectional binding

Object defineProperty defines getter and setter for each attribute of data. Object defineProperty defines getter and setter for each attribute of data.

The data to initialize

Let’s go to Vue/SRC /core/instance. This directory is selected because the code in it is basically initialization operations. There must be initialization of data

Following a programmer’s intuition, let’s take a look at index.js and see that there are several methods that are executed

// index.js
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
Copy the code

Then we look at the first function, initMixin, which we find in init.js according to the import directory, and see that several more methods are executed inside

// init.js
initLifecycle(vm) // Initialize the life cycle
initEvents(vm) // Initialization time
initRender(vm) // Initialize render
callHook(vm, 'beforeCreate') / / triggers beforeCreate
initInjections(vm) // resolve injections before data/props
initState(vm) Before, after, what is in the middle?
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') / / triggers created
Copy the code

Based on the naming and comments, I guess the initialization of data should be in the initState function

Sure enough! instate.jsIn theinitStateI found the lucky oneinitDataThe function! Not to lose is the big framework level really deep…

// state.js
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm) // here~
  } else {
    observe(vm._data = {}, true /* asRootData */)}if (opts.computed) initComputed(vm, opts.computed)
  if(opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code

In addition to initializing data, this function also handles props, methods, computed, and watch, which is not explained here, so you can check it out for yourself

This time we’ll focus on initData:

// state.js
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if(! isPlainObject(data)) { data = {} process.env.NODE_ENV ! = ='production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // Data must return a function
  
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if(process.env.NODE_ENV ! = ='production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if(props && hasOwn(props, key)) { process.env.NODE_ENV ! = ='production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if(! isReserved(key)) { proxy(vm,`_data`, key)
    }
  }
  // Method, data, props cannot be named in the same way

  observe(data, true /* asRootData */)}Copy the code

Observe that the important operation is in the last observe function

observer

Follow observe’s introduction to vue/ SRC /core/ Observer and find the function body in index.js

// index.js
/** * 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. */
export function observe (value: any, asRootData: ? boolean) :Observer | void {
  if(! isObject(value) || valueinstanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if( shouldObserve && ! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) && ! value._isVue ) { ob =new Observer(value) 
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
Copy the code

Combined with the comments and code, we have an idea that this code actually does one thing: return a new Observer or an existing Observer

What is the Observer? What did you do again? Take your time and look down

Having said new Observer, let’s look directly at the constructor of the Observer class

// index.js
constructor (value: any) {
  this.value = value
  this.dep = new Dep()
  this.vmCount = 0
  def(value, '__ob__'.this)
  if (Array.isArray(value)) {
    if (hasProto) {
      protoAugment(value, arrayMethods)
    } else {
      copyAugment(value, arrayMethods, arrayKeys)
    }
    this.observeArray(value)
  } else {
    this.walk(value)
  }
}
Copy the code

Constructor (observerArray); / / observerArray (observerArray); / / observerArray (observerArray); / / observerArray (observerArray); / / observerArray (observerArray);

// index.js
/** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value  type is Object. */
walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    // Define each item of data
    defineReactive(obj, keys[i])
  }
}

/** * Observe a list of Array items. */
observeArray (items: Array<any>) {
  for (let i = 0, l = items.length; i < l; i++) {
    // Traverse the array, observe each entry, and finally define each entry
    observe(items[i])
  }
}
Copy the code

So you’ll end up with defineReactive:

// index.js
/** * Define a reactive property on an Object. */
export function 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]
  }

  letchildOb = ! shallow && observe(val)// Define getters and setters
  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()
      }
      // #7981: for accessor properties without setter
      if(getter && ! setter)return
      if (setter) {
        setter.call(obj, newVal)
      } else{ val = newVal } childOb = ! shallow && observe(newVal) dep.notify() } }) }Copy the code

Getter and setter

This seems like a long function, but it can be summarized as follows:

 // 1. Create a dep
 const dep=new Dep()

 Object.defineProperty(obj, key, {
    get: function reactiveGetter () {
        Depend (); // 2
    	if (Dep.target) {
          dep.depend()
        }
      return value
    },
    set: function reactiveSetter (newVal) {
    	val = newVal

        // 3. Notification dependency updates
        dep.notify()
    }
  }
Copy the code

This makes it easier, so you can see that this is where the getters and setters are defined for Vue’s data

When we perform a get operation on data we fire the getter, and when we perform a set operation we fire the setter

Think 1: If I add a property a dynamically to data, does operation A fire the getter and setter? Answer: no cn.vuejs.org/v2/api/#Vue…

Thought 2: In Vue we add a property to an array variable of data using the push method. Does that trigger the setter? Vue handles array methods in a special way. If you want to know how to handle array methods, check the source code (observer/array.js) to find out

2. Observer mode

Dep

We notice a word that comes up a lot in getters and setters — dep. So what is DEP, and how do you use it for dependency collection?

Following the lead-in path, we come to observer/dep.js, and the operations of steps 1, 2 and 3 I annotated after defineReactive are as follows:

// dep.js
export default class Dep {
  statictarget: ? Watcher; id: number; subs:Array<Watcher>;
  
  // 1. New Dep() creates an empty subs, which is used to store the dependency array
  constructor () {
    this.id = uid++
    this.subs = []
  }
  
  // 2.dep.depend() executes target's addDep()
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)}}// 3. Dep.notify () updates each item in the subs array
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if(process.env.NODE_ENV ! = ='production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) = > a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
Copy the code

There is no store to the dependency array subs, because depend doesn’t do push to the subs. Where are the steps to collect dependencies?

If you’re curious, you’ve probably noticed target: What is it, where does it come from, and what does it do?

In fact, this is the key to understanding dependency collection. By searching for ‘target’ in dep.js, we see that the target type is defined as a Watcher class in the first line of the code above, and then at the bottom we find code like this:

// dep.js

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []

export function pushTarget (target: ? Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]}Copy the code

The comment says that dep. target must be globally one and only, which explains the function of this code and why it is possible to determine dep. target directly when relying on collection

Where do pushTarget and popTarget get called? And Watcher, based on its name and its usage, we’re guessing that Watcher is a tool that can see changes in the data, so let’s go to Watcher

The Watcher – observer

Puzzled, we opened observer/watcher.js and sure enough found calls to pushTarget and popTarget! It’s in a function called GET

// watcher.js

  /** * Evaluate the getter, and re-collect dependencies. */
  get () {
    Dep. Target = this
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 2. Execute this.getter
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
        throw e
      }
    } finally {
      // 3. Delete target after collection
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
Copy the code

In other words, target is assigned this, which is the current Watcher

So what is this.getter, which we found in Watcher’s constructor

// watcher.js

// expOrFn is a url that is exported during the creation of Watcher, or an expression to observe, which is converted into a getter function
if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop process.env.NODE_ENV ! = ='production' && warn(
          `Failed watching path: "${expOrFn}"` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
Copy the code

So this. Getter is the assignment logic for observing the content.

For example, if I create a Watcher to observe a property a, and the assignment logic of a is data.b + data.c, then this.getter executes data.b + data.c

Then, in the constructor, we were lucky enough to find the call to the aforementioned get function

// watcher.js

this.value = this.lazy
      ? undefined
      : this.get()
Copy the code

Depend on the collection

Now that we have solved most of the puzzle, let’s simulate the whole process for ease of understanding. Again, we have a variable A, and the assignment logic of a involves data.b and data.c

Now we pass it in new Watcher(), and then we go to the constructor, and we call this.get(), and get() creates target=Watcher and executes this.getter(), and this.getter() is just doing the assignment logic for A, B and data.c are “touched” because all properties of data are initialized with getters and setters defined. When touched, getters for data.b and data.c are triggered

high energy alert

Which is the execution

// observe.js

// Create a bridge between Watcher and data (deP).
const dep=new Dep()

/ / the getter:
if (Dep.target) {
    // Target? Yes, it was created in get() with the value of the current Watcher
    dep.depend()
}
Copy the code

perform

// observe.js

dep.depend()
Copy the code

Is equal to the

// dep.js

Dep.target.addDep(this)
Copy the code

To do this, run addDep in Watcher:

// watcher.js

addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        // The code above is some reprocessing not important for this topic
        dep.addSub(this)}}}Copy the code

Is equal to the

// watcher.js

dep.addSub(this)
Copy the code

Is equal to the

// dep.js

addSub (sub: Watcher) {
  this.subs.push(sub)
}
Copy the code

Finally, the subs of data.b and data.c are successfully put into the Watcher of A, which completes the dependency collection step!

Depend on the update

Dependency collection finished, Watcher saved, how to update?

Suppose that data.b or data.c is now set to a new value, then their setters are triggered:

Continue to high-energy

Which is the execution

// observe.js

// setter:
dep.notify()
Copy the code

That is

// dep.js

 notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if(process.env.NODE_ENV ! = ='production' && !config.async) {
      // subs are not sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order

      subs.sort((a, b) = > a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      // there is only one subs in this subs, which is the Watcher of a
      subs[i].update()
    }
  }
Copy the code

That is

// watcher.js

update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)}}Copy the code

The default is this.run(), the other two modes will not be explained at this time

run () {
    if (this.active) {
      const value = this.get()
      
      // Here is some value change callback logic
      if( value ! = =this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "The ${this.expression}"`)}}else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
Copy the code

Approximately equal to the

this.value=this.get()
Copy the code

The get function will perform the assignment logic of a, and a will be updated smoothly.

At this point, the whole response link is clear!

Responsiveness of components

Of course A, B, and C are my examples, and the responsiveness and examples of actual components are very similar.

First of all, each component is equivalent to a in the example, with a Watcher, but the component assignment logic has a special function, namely the render function

The DOM is generated during render execution, and the data needed in the DOM is “touched” just like b and C in the example, and then the component’s Watcher is added to all data dependencies as in the example

When the data changes, Watcher’s re-render is triggered, and the component is updated successfully

Ponder: Where is the source code for this part? Try to find it and study it, okay?

conclusion

  • What Watcher does is confirm what we already know, it’s an observer, it can watch a data change
  • The Dep is a subscriber and its main function is to collect dependencies that is to collect the Watcher and notify the viewer of updates
  • ObserverThe main role is to useObject.definePropertyThe method ofDataFor each child property definition ofgetterandsetterAnd then hijacked theirsgetandsetoperation

— — — — — — — end — — — — — — —

Talented and uneducated, big guy light spray ~