preface

1. The MVVM pattern

The template

I am {{age}} years old

data

this.age ++

model <=view-model=> view

2. Invasive and non-invasive

invasive

(react-data changes) this.setState({n: this.n + 1})

This.setdata ({m: this.m + 1})

noninvasive

(Vue- data changes) this.num ++

Non-invasive implementation principles

The core is object.definefroperty() for data hijacking/data proxying

Use javascript engine methods to detect object property changes

3. What is responsive system?

When the data changes, the view is updated, which is a responsive system.

Implement the core content of responsive system

1. Data hijacking

2. Rely on collection

3. Send updates

1. Data hijacking

1. Object.defineProperty

Data hijacking with Object.defineProperty

const data = {}
let value = 100

Object.defineProperty(data, key, {
  get() { 
    console.log(Access the key property)
    return value
  },
  set(newValue) { 
    if (value === newValue) return
    console.log('Change the key property', newValue)
    value = newValue
  }
})
Copy the code

As shown above

Get () listens for data access (values) and set() listens for data changes (assignments).

We can do something with the data in get() and set() to override the behavior. That’s what data hijacking is all about.

2. defineReactive

DefineProperty encapsulates object.defineProperty for convenience: defineReactive()

function defineReactive (data, key, value) {
  if (arguments.length === 2) {
    value = data[key]
  }
  Object.defineProperty(data, key, {
    enumerable: true.configurable: true,
    get () {
      // console.log(' access ${key} property ')
      return value
    },
    set (newValue) {
      // console.log(' change ${key} property ', newValue)
      if (value === newValue) return
      value = newValue
    }
  })
}
Copy the code

Use defineReactive(data, key, value)

const obj = {
    a: 100.b: 200
};

// Add attributes
defineReactive(obj, c, 300)
console.log(obj) // {a: 100, b: 200, c: 300}

// Modify attributes
defineReactive(obj, a, 1000)
console.log(obj) // {a: 1000, b: 200, c: 300}
Copy the code

From the above, we can detect data when accessing, adding and modifying.

3. The Observer

Question 1: IfobjHow do we monitor when we have multiple attributes?

We need to create a new Observer class to iterate over all the properties of the object and the properties at each level below the properties.

An Observer is an Observer used for data detection. To convert a normal object into a responsive (detectable) object for each level of property

class Observer {
  constructor(value) {
    
    // Add an __ob__ attribute to value, an instance ob of new, and set it to non-enumerable.
    // [ob = value.__ob__ or new OBserver(value)]
    def(value, '__ob__'.this.false) // Add an __ob__ attribute to each layer of the object

    this.walk(value)
  }
  walk (value) {
    DefineReactive binding for all children of value
    for (const key in value) {
      defineReactive(value, key)
    }
  }
}
Copy the code

When creating the Observer class, we use the def function, which sets the object operation Settings, in this case the property values and enumeration permissions. Also whether can delete, modify and other Settings…

const def = (data, key, value, enumerable) = > {
  Object.defineProperty(data, key, {
    value,
    enumerable, // Whether enumerable
    writable: true.// Whether to change
    configurable: true // Whether to delete})}Copy the code

Test the Observer and iterate over all properties

const obj = { a: 1.b: 2 } 
new Observer(obj)

console.log(obj.a); // Access the a attribute, 1
console.log(obj.b); // Access a property, 2
obj.a = 11 // Change a property, 11
obj.b = 22 // Change the b attribute, 22
Copy the code

The test results are as follows:

As shown, all attributes are detected and an __ob__ attribute is added to the object

4. Observe function

Question 2: IfobjHow do we detect nested attributes?

We need the Observ function to help us with the monitoring binding of the child attributes

The Observe function, which is used to monitor binding objects, is an extension of the Observer utility function used to set the Observer. Add an __ob__ attribute to the object.

function Observe (value) {
  // No object is not processed
  if (typeofvalue ! ='object') return
  / / define the ob
  let ob
  if (typeofvalue.__ob__ ! ='undefined') {
    // Check whether the current object has an __ob__ attribute
    ob = value.__ob__
  } else {
    // Add new Oberser() if the object has no __ob__ in its layer
    ob = new Observer(value)
  }
  return ob
}
Copy the code

Modify defineReactive. Enter defineReactive and observe() immediately. If set() is changed, observe() with the new value.

function defineReactive (data, key, value) {
  if (arguments.length === 2) {
    value = data[key]
  }

  Oberse the current object and its child elements to form circular recursion
  `let childOb = Observe(value)`

  Object.defineProperty(data, key, {
    enumerable: true.configurable: true,
    get () {
      // console.log(' access ${key} property, ${value} ')
      return value
    },
    set (newValue) {
      // console.log(' change ${key} property, ${newValue} ')
      if (value === newValue) return
      value = newValue
      If there is a new value, Oberse binds the new value
      `childOb = Observe(newValue)`}})}Copy the code

Test the Observe function

import Observe from './Observe'

let obj = {
  a: {
    a1: {
      a11: 11
    },
    a2: 12
  },
  b: {
    b1: 22
  }
}

Observe(obj) / / monitoring obj
console.log(obj);
Copy the code

The test results are as follows:

As shown in the figure, we add __ob__ to the attributes of each layer of the object, realizing data hijacking for each layer of the object.

5. Summarize the execution process of data hijacking

Perform observe (obj) ├ ─ ─newObserver(obj), and executethis.walk() walks through obj's attributes and executes defineReactive()` ├ ─ ─ defineReactive ` (obj, a)├─ Perform Observe (obJ. A) ObJ. A is object ├─ performnewObserver(obj. A), iterate over obJ. A's attributes and define defineReactive()` ├ ─ ─ defineReactive ` (obj. A, a1)├─ Perform Observe (obJ. A. 1) Find obJ. A is object ├─ performnewObj.a.a.1 Observer(obJ.a.a.1) ├─ Clear active(obJ.a.a.1, a11) ├─ Observe (obJ.a.a.a.1, a11) Obj.a.a.a.1 Go straight back to ├─ perform the remainder of Define Active (obJ.a.a 1, A11)` ├ ─ ─ defineReactive ` (obj. A, a2)├─ Perform Observe (obJ.a.a2) Find obJ.A not An object ├─ Perform define Active (obJ.a, a2) remainder of the code` ├ ─ ─ defineReactive ` (obj, b)├─ Perform Observe (obJ. B) Find obJ. B is object ├─ performnewObj. B: ObJ. B: ObJ. B: ObJ. B: ObJ. B: ObJ. B: ObJ. B: ObJ. B: ObJ. B: ObJ. B: ObJ. B: ObJ. B: ObJ. Go straight back to ├─ Perform Define Active (obj. B, b1) remainder code ├─ perform Define Active (obj, b) remainder codeCopy the code

2. Rely on collection

1. Publish and subscribe

What is the publish and subscribe model?

For example, micro-blogs focus on celebrities. Flower is a domestic sports star, tens of millions of fans, small A is one of its fans, small A want to understand the dynamics of flower, so pay attention to flower in weibo, when flower updates weibo, weibo will push flower dynamic to small A

From the above example of following celebrities on weibo, we can abstract its logic as: publish and subscribe mode.

Fans concerned about flower = in the micro-blog to register their own information to subscribe to flower, the micro-blog to store the subscriber information in an array, such as flower in the update status, micro-blog through the number of groups, one by one to notify subscribers

2. The Watcher

In responsive systems, there is also an array of fan information, called Watcher

As you can see from the examples, Watcher has three core features:

  1. There’s an array to store itWatcher
  2. WatcherInstances need to subscribe to (dependency) data, that is, get dependencies or collect dependencies
  3. WatcherTriggered when a dependency changesWatcherTo send out updates.

implementationWatcher

class Watcher {
  constructor(data, expression, callback) {
    this.data = data // The object to observe
    this.expression = expression // Which property of the object to observe
    this.callback = callback // Publish action that triggers the callback function when the subscribed properties change
    this.value = this.get() // By default, the value of the specific property of the object to observe is obtained at instantiation time
  }
  get () {
    // Set the global dep. target to Watcher itself, then you are in the dependency collection phase
    window.target = this // The location of the Watcher array
    let value
    try {
      value = parsePath(this.expression)(this.data)
    } finally {
      window.target = null
    }
    return value
  }

  / / update
  update () {
    const value = this.get()
    // If the new value is not equal to the old value, or the new value is an object
    if(value ! = =this.value || typeof value == 'object') {
      // save the oldValue as oldValue
      const oldValue = this.value
      // make this.value= new value
      this.value = value
      this.callback.call(this.data, value, oldValue)
    }
  }
}

// The utility function parsePath
const parsePath = (str) = > {
  let segments = str.split('. ')
  return (obj) = > {
    for (let i = 0; i < segments.length; i++) {
      if(! obj)return
      obj = obj[segments[i]]
    }
    return obj
  }
}

Copy the code

Test the Watcher class

let obj = {
  a: {
    b: 100.c: 200}}const str_b = new Watcher(obj, 'a.b') // Subscribe to a.b in obj
const str_c = new Watcher(obj, 'a.c') // subscribe to a.c in obj

console.log('str_b', str_b.value) // str_b 100
console.log('str_c', str_c.value) // str_c 200
Copy the code

Above test, we can get the corresponding value of the object through object subscription. But we don’t know when the data is changing, so we need to combine data hijacking with dependency collection

Realize the Dep class

With the defineReactive function’s set() method, when the data is updated, we can know that the data has changed, get the new data, and notify the publication. Each data should maintain its own array to hold its own dependent watcher. We can define an array DEP in defineReactive so that each property has its own DEP through closures

Modify the defineReactive function

function defineReactive (data, key, value) {

  `const dep = new Dep()`

  if (arguments.length === 2) {
    value = data[key]
  }

  // Oberse the child elements to form cyclic recursion
  let childOb = Observe(value)

  Object.defineProperty(data, key, {
    enumerable: true.configurable: true,
    get () {
      // If you are in the dependency collection phase
      Subscribe to collect, if there are sub-attributes, also collect on sub-attributes
      dep.depend() 
      if (childOb) {
        childOb.dep.depend()
      }
      return value
    },
    set (newValue) {
      if (value === newValue) return
      value = newValue
      // If there is a new value, Oberse binds the new value
      childOb = Observe(newValue)
    }
  })
}
Copy the code

Realize the Dep class

class Dep {
  constructor() {
    this.id = uid++
    // Subscribes, which stores subscribers. This array contains an instance of Watcher
    this.subs = []
  }

  // Notification to publish updates
  notfiy () {
    // Notify all subscribers of updates
    [...this.subs].forEach(sub= > sub.update())
  }

  // Add dependencies
  depend () {
    if (Dep.target) {
      this.addSub(Dep.target)
    }
    // console.log('depend')
  }

  // Add a subscription
  addSub (sub) {
    this.subs.push(sub)
  }
}
Copy the code

3. Distribute updates

After implementing dependency collection, the last thing we want to do is distribute updates, which trigger watcher’s callback when a dependency changes. From the dependency collection part, we know that the data that watcher depends on is the data that watcher obtains, that is, the data that watcher triggers the get. When the data changes, watcher will be notified to trigger the distribution of updates in the set.

set (newValue) {
  if (value === newValue) return
  value = newValue
  // If there is a new value, Oberse binds the new value
  childOb = Observe(newValue)
  
  Publish subscriptions and notify deP via dep.notfiy
  dep.notfiy()
}
Copy the code

4, supplement, special handling of arrays

We access and retrieve the value of arr, get and set are triggered, but what if arr.push()? Each element of the array is moved back one bit in turn, which triggers get and set, causing dependencies to change. Since arrays are sequential, the key and value are not bound, so this nursing approach is problematic.

Vue overwrites 7 methods in JS that change arrays: push, pop, unshift, Shift, splice, reverse, sort.

An important part of rewriting these seven approaches is the proxy prototype

// Get the array prototype
const ArrayPrototype = Array.prototype

// Set the array proxy prototype
The object.create () method creates a new Object, ArrayMethods, whose __proto__ is ArrayPrototype
export const proxyArrayPrototype = Object.create(ArrayPrototype)

// The array method that needs to be overwritten
const rewriteArrayMothods = [
  'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse',]// rewrite proxyArrayPrototype
rewriteArrayMothods.map(methodName= > {
  // Get the method on the prototype
  const original = ArrayPrototype[methodName]
  def(proxyArrayPrototype, methodName, function () {
    // Point Original's this to proxyArrayPrototype
    const res = original.apply(this.arguments)
    // Since the outermost layer must be the object, all __ob__ must exist
    const ob = this.__ob__
    // Push,shift,splice insert data, arguments get new insert data
    let insert = []
    switch (methodName) {
      case 'push':
      case 'shift':
        insert = [...arguments]
        break;
      case 'splice':
        // splice(index,howmany,' new content ') the third argument is the new array element, index 2
        insert = [...arguments].slice(2) 
        break;
    }
    // If there is a new data insert, bind the new data.
    if (insert.length > 0) ob.observeArray(insert)
    // DeP is also notified when the array changes
    ob.dep.notfiy()
    / / the console. The log (' operation array ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ');
    return res
  }, false)})Copy the code

In proxyArrayPrototype, we have rewritten 7 methods to do two things when triggering these 7 methods:

  1. fordep.notfiy()update
  2. Add dependencies (add subscriptions) ob.observeArray(INSERT) when array inserts new data

After modifying 7 methods on the Array agent prototype, we need to modify themThe Observer class

When an Observer looks at data of type array:

  1. We want toArray prototypePoint to theProxy prototype:Object.setPrototypeOf(value, proxyArrayPrototype).
  2. I need to do each entry in the arrayOberseListening to the
class Observer {
  constructor(value) {
    this.dep = new Dep()
    // this === the instance itself, not the class itself
    // Add an __ob__ attribute to value, an instance ob of new, and set it to non-enumerable.
    // [ob = value.__ob__ or new OBserver(value)]
    def(value, '__ob__'.this.false)
    if (Array.isArray(value)) {
      // Value === array, which points to proxyArrayPrototype
      Object.setPrototypeOf(value, proxyArrayPrototype)
      // value === array. Oberse listens on each item in the array
      this.observeArray(value)
    } else {
      // value === object or string with defineReactive binding for all children of value
      this.walk(value)
    }
  }
  walk (value) {
    DefineReactive binding for all children of value
    for (const key in value) {
      defineReactive(value, key)
    }
  }
  observeArray (value) {
    for (let i = 0, j = value.length; i < j; i++) {
      Observe(value[i])
    }
  }
}

Copy the code

5. Code arrangement

defineReactive.js

import Observe from './Observe'
import Dep from "./Dep"

export default function defineReactive (data, key, value) {
  const dep = new Dep()
  if (arguments.length === 2) {
    value = data[key]
  }
  // Oberse the child elements to form cyclic recursion
  let childOb = Observe(value)
  Object.defineProperty(data, key, {
    enumerable: true.configurable: true,
    get () {
      // console.log(' access ${key} property, ${value} ')
      // If you are in the dependency collection phase
      dep.depend()
      if (childOb) {
        childOb.dep.depend()
      }
      return value
    },
    set (newValue) {
      // console.log(' change ${key} property, ${newValue} ')
      if (value === newValue) return
      value = newValue
      // If there is a new value, Oberse binds the new value
      childOb = Observe(newValue)
      // Publish subscribe mode to notify deP via dep.notfiy
      dep.notfiy()
    }
  })
}
Copy the code

Observe.js

import Observer from "./Observer"
export default function Observe (value) {
  // No object is not processed
  if (typeofvalue ! ='object') return
  / / define the ob
  let ob
  if (typeofvalue.__ob__ ! ='undefined') {
    // Check whether __ob__ is present on the current object
    ob = value.__ob__
  } else {
    // Add observer to new Oberser() if the object has no __ob__ in its layer
    ob = new Observer(value)
  }
  return ob
}
Copy the code

Observer.js

import { def } from './utils'
import defineReactive from './defineReactive'
import Observe from './Observe'
import { proxyArrayPrototype } from "./array"
import Dep from "./Dep"
/ /? An Observer of data. Convert a normal object to a responsive (detectable) object for each level of property
export default class Observer {
  constructor(value) {
    this.dep = new Dep()
    // this === the instance itself, not the class itself
    // Add an __ob__ attribute to value, an instance ob of new, and set it to non-enumerable. Ob = value.__ob__ or new OBserver(value)
    def(value, '__ob__'.this.false)
    // console.log(' Observer constructor ', value);
    if (Array.isArray(value)) {
      // Value === array, which points to proxyArrayPrototype
      Object.setPrototypeOf(value, proxyArrayPrototype)
      // value === array. Oberse listens on each item in the array
      this.observeArray(value)
    } else {
      // value === object or string with defineReactive binding for all children of value
      this.walk(value)
    }
  }
  walk (value) {
    DefineReactive binding for all children of value
    for (const key in value) {
      defineReactive(value, key)
    }
  }
  observeArray (value) {
    for (let i = 0, j = value.length; i < j; i++) {
      Observe(value[i])
    }
  }
}
Copy the code

Dep.js

let uid = 0
export default class Dep {
  constructor() {
    this.id = uid++
    // Subscribes, which stores subscribers. This array contains an instance of Watcher
    this.subs = []
    // console.log('dep constructor ')
  }

  // Notification update
  notfiy () {
    // console.log('notify')
    // Notify all subscribers of updates
    [...this.subs].forEach(sub= > sub.update())
  }

  // Add dependencies
  depend () {
    if (Dep.target) {
      this.addSub(Dep.target)
    }
    // console.log('depend')
  }

  // Add a subscription
  addSub (sub) {
    this.subs.push(sub)
  }
}
Copy the code

Watcher.js

import { parsePath } from "./utils";
import Dep from './Dep'
let uid = 0

Dep.target = null // a global variable used to store subscription data. It can be any variable, such as window.target

const TargetStack = []

const pushTarget = (_target) = > {
  TargetStack.push(Dep.target)
  Dep.target = _target
}

const popTarget = (_target) = > {
  Dep.target = TargetStack.pop()
}

export default class Watcher {
  constructor(data, expression, callback) {
    // console.log('Watcher')
    this.id = uid++
    this.data = data // The object to observe
    this.expression = expression // Which property of the object to observe
    this.callback = callback // Publish action that triggers the callback function when the subscribed properties change
    this.value = this.get() // By default, the value of the specific property of the object to observe is obtained at instantiation time
  }
  get () {
    // Set the global dep. target to Watcher itself, then you are in the dependency collection phase
    pushTarget(this)
    let value
    try {
      value = parsePath(this.expression)(this.data)
    } finally {
      popTarget(this)}return value
  }
  / / update
  update () {
    const value = this.get()
    // If the new value is not equal to the old value, or the new value is an object
    if(value ! = =this.value || typeof value == 'object') {
      // save the oldValue as oldValue
      const oldValue = JSON.parse(JSON.stringify(this.value))
      // make this.value= new value
      this.value = value

      this.callback.call(this.data, value, oldValue)
    }
  }
}
Copy the code

array.js

import { def } from "./utils";
// Get the array prototype
const ArrayPrototype = Array.prototype
// Set the array proxy prototype
The object.create () method creates a new Object, ArrayMethods, whose __proto__ is ArrayPrototype
export const proxyArrayPrototype = Object.create(ArrayPrototype)
// The array method that needs to be overwritten
const rewriteArrayMothods = [
  'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse',
]
rewriteArrayMothods.map(methodName= > {
  // Get the method on the prototype
  const original = ArrayPrototype[methodName]
  def(proxyArrayPrototype, methodName, function () {
    // Point Original's this to proxyArrayPrototype
    const res = original.apply(this.arguments)
    // Since the outermost layer must be the object, all __ob__ must exist
    const ob = this.__ob__
    // Push,shift,splice insert data, arguments get new insert data
    let insert = []
    switch (methodName) {
      case 'push':
      case 'shift':
        insert = arguments
        break;
      case 'splice':
        insert = arguments.slice(2) // splice(index,howmany,' new content ') the third argument is the new array element, index 2
        break;
    }
    // If there is a new data insert, bind the new data.
    if (insert.length > 0) ob.observeArray(insert)
    // DeP is also notified when the array changes
    ob.dep.notfiy()
    return res
  }, false)})Copy the code

utils.js

export const def = (data, key, value, enumerable) = > {
  Object.defineProperty(data, key, {
    value,
    enumerable,
    writable: true.configurable: true})}export const parsePath = (str) = > {
  let segments = str.split('. ')
  return (obj) = > {
    for (let i = 0; i < segments.length; i++) {
      if(! obj)return
      obj = obj[segments[i]]
    }
    return obj
  }
}
Copy the code

Index.js entry file

import Observe from './Observe'
import Watcher from './Watcher'
let obj = {
  a: {
    a1: {
      a11: 11
    },
    a2: 12
  },
  b: {
    b1: 22
  },
  arr: [{ k: 111 }, 222.333.444]
}
Observe(obj)
// obj.a.ab1.ab1c1.ab1c1d1 = 1

// console.log('obj', obj);
// new Watcher(obj, 'a.a1.a11', (n, o) => {
// console.log(' new value: ', n, 'old value: ', O,);
// })
// obj.a.a1.a11 = 1
// new Watcher(obj, 'a.a2', (n, o) => {
// console.log(' new value: ', n, 'old value: ', o);
// })
// obj.a.a2 = 2

new Watcher(obj, 'arr'.(n, o) = > {
  console.log('New value:', n, Old value: ', o);
})
obj.arr.push(1000)
Copy the code