This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.

The youth training camp actual combat class course has also ended, today we will first wank the Mini-vUE course brought by Village head Yang on Friday. If you missed the class, don’t miss this super detailed and slightly expanded note

See my Gitee repository for the code

For more information on Vue3, check out my previous blog post to get you started on Vue3

First, front-end framework design concept

Use Vue as an example

  • Simple and easy to use

  • Data driven

    • Reduce DOM manipulation
    • data-driven
      • Data Reactive, effect
    • Declarative render
    • vdom
    • patch
  • Progressive (MIN, VUex, Router, element3)

2. Motivation for Vue3

  1. (Type support) Why function?
  • Functions: vue3, react

  • Class: Angular, vue2 (decorator)

The function signature is unambiguous so that the input and output are unambiguous

  1. Advantages of the Composition API
  • To eliminatethis
  • Declare reactive data
  • Reusability, readability, maintainability
<div id="app">{{title}}</div>
<script src="http://unpkg.com/vue@next"></script>

<script>
  const app = Vue.createApp({
    data() {
      return {
        title: 'Vue3, YK bacteria, data'}},setup() {
      // Advantages of Composition API
      / / this
      // Declare reactive data
      // Reusability, readability, maintainability
      const state = Vue.reactive({
        title: 'Vue3, YK bacteria, setup'
      })
      return state
    }
  })

  app.mount('#app')
</script>
Copy the code

As a result, setup has a higher priority

3. Summary

  1. Type of support
  2. Instance methods and properties tree-shaking
  3. reusabilityhook
  4. maintainabilityComposition API
  5. The API to simplify
    1. Conformance (different life cycles in directives and components)
    2. Delete apis with the same functionality (v-model,.sync(Vue3 deprecated)
    <comp v-model="foo"></comp>
    <comp :foo.sync="foo"></comp>
    <comp :value="foo" @update:value="foo = $event"></comp>
    Copy the code
    1. render
    / / Vue2 writing
    render(h){
       return h('div', {
         attrs: {
           title: this.title
         }
       }, 'xxx')}/ / Vue3 writing
    render(){
      return Vue.h('div', {
        title: this.title
      }, 'xxx')}Copy the code
  6. Extensibility: Custom renderersVue.createRenderer()
  7. Performance optimization — reactive basedProxy
    1. Recursive efficiency problem
    2. Array problems (a separate set of implementations)
    3. API Impact (Dynamic attributes added or deletedVue.delete / set
    4. Class Collection data structures are not supported
    5. Compatibility (Vue2.7)

Two, to achieve mini-Vue

Reading source code directly is still a difficult thing to start, novice often confused, easy to encounter obstacles in some corners, waste a lot of time, so today we will write mini-Vue only focus on the most core part

1. Initialization

<div id="app">{{title}}</div>

<script>
  // Code to be filled
</script>

<script>
  const app = Vue.createApp({
    data() {
      return {
        title: 'Hello, Bacteria Vue3 YK'
      }
    }
  })
  app.mount('#app')
</script>
Copy the code

① Basic structure

const Vue = {
  createApp(options) {
    // Return the app object
    return {
      mount(selector) {
        // Code to be filled}}}}Copy the code

② How to mount elements

What we should be thinking about is what does Vue’s mount do?

  1. Finding the host element
  2. To render the page
    1. To deal withtemplateUse:compilecompile
    2. User writtenrender
  3. Append to host

Let’s write the code along these lines

// 1. Basic structure
const Vue = {
  createApp(options) {
    // Return the app object
    return {
      mount(selector) {
        // 1. Find the host element
        const parent = document.querySelector(selector)
        // 2. Render the page
        if(! options.render) {// 2.1 Handle template: compile
          options.render = this.compile(parent.innerHTML)
        }
        // 2.2 User writes render directly
        // Execute the render function (we specify the context of this in the render function, which is the return value of the data function in the configuration item)
        const el = options.render.call(options.data())

        // 3. Append to host
        parent.innerHTML = ' '
        parent.appendChild(el)
      },
      compile(template) {
        // Return a render function
        // parse -> ast
        // generate: ast -> render
        return function render() {
          const h3 = document.createElement('h3')
          h3.textContent = this.title
          return h3
        }
      }
    }
  }
}
Copy the code

We are done showing the data in data, as shown in the figure below

3 Compatibility Processing

If the user writes data and setup at the same time, Vue3 uses the data in setup in preference

const app = Vue.createApp({
  data() {
    return {
      title: 'Hello, vue3 YK bacteria data'}},setup() {
    return {
      title: 'Hello, vue3 YK bacteria setup'
    }
  }
})
app.mount('#app')
Copy the code

So we have to deal with compatibility issues between setup and other options before rendering

First, we collect setup and other options

if(options.setup){
  this.setupState = options.setup()
}
if(options.data){
  this.data = options.data()
}
Copy the code

We create a proxy that specifies the priority in the getter and setter

// Handle setup and other options compatibility before rendering
const proxy = new Proxy(this, {
  get(target, key) {
    // Get it from setup first, or from data if not
    // If setup exists and key is defined in setup
    if (target.setupState && key in target.setupState) {
      // return target.setupState[key]
      return Reflect.get(target.setupState, key)
    } else {
      // return target.data[key]
      return Reflect.get(target.data, key)
    }

  },
  set(target, key, val) {
    if (target.setupState && key in target.setupState) {
      return Reflect.set(target.setupState, key, val)
    } else {
      return Reflect.set(target.data, key, val)
    }
  }
})
Copy the code

Before mounting the element, set render’s context to proxy

const el = options.render.call(proxy)
Copy the code

④ Extensibility processing (custom renderer)

In the code we wrote above, the app instance returned is strongly coupled with the Web platform, because the operations are using document, to improve scalability, we should adopt the way of higher-order components, pass in the platform-related operations through parameters, to achieve the effect of decoupling, so that our framework is platform-independent ~

// 1. Find the host element
const parent = document.querySelector(selector)

// 3. Append to host
parent.innerHTML = ' '
parent.appendChild(el)
Copy the code

to

// 1. Find the host element
const parent = querySelector(selector)

// 3. Append to host
insert(el, parent)
Copy the code

How do you do that? We need to provide a new API for users to choose from

const Vue = {
  / / expanding
  createRenderer({ querySelector, insert }) {
    // Return the renderer
    return {
      createApp(options) {
        // Return the app object
        return {
          mount(selector) {
            // 1. Find the host element
            const parent = querySelector(selector)

            // 2. Render the page
            / /...

            // 3. Append to host
            insert(el, parent)
          },
        }
      }
    }
  },
  createApp(options) {
    // Create a web platform-specific renderer
    const renderer = Vue.createRenderer({
      querySelector(sel) {
        return document.querySelector(sel)
      },
      insert(el, parent) {
        parent.innerHTML = ' '
        parent.appendChild(el)
      }
    })
    return renderer.createApp(options)
  }
}
Copy the code

After initialization, the next thing we need to think about is how do we implement reactive data

2. The response type

I believe that many people who have not been exposed to Vue3 have heard that Vue3’s response is based on Proxy

However, Vue2 implements data responsiveness based on object.defineproperty, which has many disadvantages :(see my previous post on data responsiveness)

  1. Recursive efficiency problem
  2. Array problems (a separate set of implementations)
  3. API Impact (Dynamic attributes added or deletedVue.delete / set
  4. Class Collection data structures are not supported

1) reactive

Let’s explore this by creating a reactive object using Reactive, and after two seconds, the title value will change and the page will change.

const app = Vue.createApp({
  setup() {
    const state = reactive({
      title: 'Hello, Bacteria Vue3 YK'
    })

    setTimeout(() = > {
      state.title = 'New YK bacteria seen in 2 seconds'
    }, 2000)

    return state
  }
})
app.mount('#app')
Copy the code

We create a reactive function that intercepts user access to the proxy object and responds when the value changes.

// Content intercepts user access to proxy objects to respond to value changes
function reactive(obj) {
  // Return the object of the proxy
  return new Proxy(obj, {
    get(target, key) {
      console.log('get key:', key)
      return Reflect.get(target, key)
    },
    set(target, key, val) {
      console.log('set key:', key)
      const result = Reflect.set(target, key, val)
      // Notification update
      app.update()
      return result
    }
  })
}
Copy the code

We add an update function to the returned app object, write in the code that rendered the page earlier, and call it once

// 2.2 User writes render directly
this.update = function () {
  // Execute the render function (we specify the context of this in the render function, which is the return value of the data function in the configuration item)
  const el = options.render.call(proxy)

  // 3. Append to host
  insert(el, parent)
}
// call once
this.update()
Copy the code

As you can see, our page is responsive ~ the content will change after two seconds

Take a rest, the village head teacher sing a song to everyone pull ~~~~

② Rely on collection

Reactive has a line app.update() that calls the app’s methods. This is strongly coupled to our app, and we need a mechanism (publish and subscribe) to decouple this behavior by creating dependencies between reactive data and their associated update functions

Establish the mapping: rely on the DEP -> component update function

Vue2 uses Watcher, see my previous blog [Vue source] data responsive principle

Vue3 creates a data structure like Map to establish dependencies {target, {key: [update1, update2]}}. The next thing to do is to establish dependencies in get and get dependencies in set

Let’s implement it:

First, we define a side effect function that takes a function fn, and the first step is to execute the fn function

function effect(fn) {
  // 1. Run fn once
  fn()
}
Copy the code

But there is a problem here, which is that when we execute fn, the function gives an error, what do we do, so we write it as a higher order function, and wrap our fn

Start by creating a stack effectStack that temporarily stores the side effect functions

const effectStack = []
Copy the code

Why use a stack here? Because nesting can occur when the side effect function is called, using data structures such as stacks is a good way to collect eff

Update our Effect function

function effect(fn) {
  // 1. Run fn once
  // fn()
  const eff = function () {
    try {
      effectStack.push(eff)
      fn()
    } finally {
      effectStack.pop()
    }
  }
  // Call once immediately
  eff()
  // Return this function
  return eff
}
Copy the code

We also need to build such dependency data structures

const targetMap = {
// We should store such data in it, dependencies
// state: {
// 'title': [update]
// }
}
Copy the code

Then define a tarck function that establishes the relationship between the target, key, and effectStack side effects stored in the function

// Establish the relationship between target,key, and effectStack side effects
function track(target, key) {
  // Take out the last element that stores the side effect function
  const effect = effectStack[effectStack.length - 1]
  // This is true when written to death, but it is not possible to write this way, otherwise a new object is created each time
  // targetMap[target] = {}
  // targetMap[target][key] = [effect]
  // Check whether the object with target as key exists
  let map = targetMap[target]
  if(! map) {// Get the target for the first time
    map = targetMap[target] = {}
  }

  let deps = map[key]
  if(! deps) { deps = map[key] = [] }// The mapping relationship is established
  if (deps.indexOf(effect) === -1) {
    deps.push(effect)
  }
}
Copy the code

Define a trigger function to trigger the update

function trigger(target, key) {
  const map = targetMap[target]
  if (map) {
    const deps = map[key]
    if (deps) {
      deps.forEach(dep= > dep())
    }
  }
}
Copy the code

We’re going to add dependencies in the getter and trigger updates in the setter, so we’re going to set this up in the proxy

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      console.log('get key:', key)
      // Establish dependencies
      track(target, key)
      return Reflect.get(target, key)
    },
    set(target, key, val) {
      console.log('set key:', key)
      Reflect.set(target, key, val)
      // Trigger the update
      trigger(target, key)
    }
  })
}
Copy the code

Finally, let’s test it using test cases

// Create reactive data obj
const obj = reactive({
  foo: 'foo'
})
// Create a side effect function that internally triggers reactive data
effect(() = > {
  // Trigger responsive data
  console.log(obj.foo)
}) 
// Change the foo property in obj
obj.foo = 'Foo has changed ~~~'
Copy the code

Let’s figure out what the relationship is

Finally, we need to improve our code. We need to create a side effect of the update function we wrote earlier, so that when the data changes, the update function will be executed again

So we just need to wrap the function we wrote earlier with a layer of effect higher-order function

// 2.2 User writes render directly
this.update = effect(() = > {
  // Execute the render function (we specify the context of this in the render function, which is the return value of the data function in the configuration item)
  const el = options.render.call(proxy)

  // 3. Append to host
  insert(el, parent)
})
// this.update()
Copy the code

At this point we delete our test case and see what it actually looks like using setup:

In fact, in the source code, targetMap uses a data structure that is not an object, but a Map, and a WeakMap

const targetMap = new WeakMap(a)Copy the code

Some operations can be changed to the corresponding Map get and set operations

// let map = targetMap[target]
let map = targetMap.get(target)
Copy the code
// map = targetMap[target] = {}
map = targetMap.set(target, {})
Copy the code

When storing dependencies, you should use data structures such as Set, which are automatically de-duplicated

// deps = map[key] = []
deps = map[key] = new Set(a)// The mapping relationship is established
// if (deps.indexOf(effect) === -1) {
// deps.push(effect)
// }
deps.add(effect)
Copy the code

The above response is complete, but it has a serious efficiency problem, which is that we update our DOM in full update mode, which is definitely not good, so this leads us to the virtual DOM and diff algorithm

3. The virtual DOM

What is the virtual DOM (VNode)?

  1. A VNode is a JS object that describes a view

Why vNode?

  1. Reduce DOM manipulation
  2. Efficient updates
  3. Cross-platform, compatibility

Let’s define our virtual DOM — vnode

We will change the real DOM from the render function returned in the compile function to a virtual DOM

return function render() {
  // const h3 = document.createElement('h3')
  // h3.textContent = this.title
  // return h3
  // The virtual DOM should be generated
  return h('h3'.null.this.title)
  // return h('h3', null, [
  // h('p', null, this.title),
  // h('p', null, this.title),
  // h('p', null, this.title),
  // ])
}
Copy the code

Define an H function that represents a DOM in js objects

// Pass in the information, return vNode, describe the view
function h(tag, props, children) {
  return {
    tag,
    props,
    children
  }
}
Copy the code

The update function is also changed

this.update = effect(() = > {

  const vnode = options.render.call(proxy)
  // Convert vNode to DOM
  // Initializes the creation of the entire tree
  if (!this.isMounted) {

    // implement createElm, create whole, vnode -> el
    const el = this.createElm(vnode)
    parent.innerHTML = ' '
    insert(el, parent)
    // init initializes and sets the mount flag
    this.isMounted = true}})Copy the code

The createElm function is implemented recursively

createElm({ tag, props, children }) {
  // Walk through the vNode to create the entire tree
  const el = createElement(tag)
  // If there are attributes, set them (omitted)
  // el.setAttribute(key, val)
  
  / / recursion
  // Check that children is a string
  if (typeof children === 'string') {
    el.textContent = children
  } else {
    children.forEach(child= > insert(this.createElm(child), el))
  }
  return el
}
Copy the code

4. diff

For more details, see my previous blog post illustrating the DIff algorithm

Save the real node in createElm

// Keep the real DOM in vNode for future updates
vnode.el = el
Copy the code

Modify the update function, which was first mounted earlier, to add the update logic

this.update = effect(() = > {

  const vnode = options.render.call(proxy)
  // Convert vNode to DOM
  // Initializes the creation of the entire tree
  if (!this.isMounted) {

    // implement createElm, create whole, vnode -> el
    const el = this.createElm(vnode)
    parent.innerHTML = ' '
    insert(el, parent)
    // init initializes and sets the mount flag
    this.isMounted = true
  } else {
    this.patch(this._vnode, vnode)
  }
  this._vnode = vnode
})
Copy the code

Patch updates

patch(oldNode, newNode) {
  const el = newNode.el = oldNode.el
  // 1. Update: The same node must be updated
  // What is the same node
  if (oldNode.tag === newNode.tag && oldNode.key === newNode.key) {
    // Update the same node

    const oldChild = oldNode.children
    const newChild = newNode.children

    if (typeof oldChild === 'string') {
      if (typeof newChild === 'string') {
        // Text update
        if(oldChild ! == newChild) { el.textContent = newChild } }else {
        // Replace text with a set of child elements, empty and then create and append
        el.textContent = ' '
        newChild.forEach(child= > insert(this.createElm(child), el))

      }

    } else {
      if (typeof newChild === 'string') {
        // Replace a set of child elements with text
        el.textContent = newChild
      } else{}}}else {
    // replace Replace different nodes}},Copy the code

Set to update data from string to array

setup() {
  const state = reactive({
    title: 'Hello, Bacteria Vue3 YK'
  })

  setTimeout(() = > {
    state.title = 'New YK bacteria seen in 2 seconds'.split("")},2000)

  return state
}

Copy the code

The effect

Contrast the diff

Vue3 and Vue2 are basically the same. In my previous blog post, I also drew a comparison diagram of Vue2 diff algorithm

So let’s simplify and update it in the simplest and most crude way possible

updateChildren(el, oldChild, newChild) {
  // 1. Get the shorter of newCh and oldCh
  const len = Math.min(oldChild.length, newChild.length)
  // Force an update
  for (let i = 0; i < len; i++) {
    this.patch(oldChild[i], newChild[i])
  }

  // Process the remaining elements
  // The new array has many elements
  if (newChild.length > oldChild.length) {
    // Batch create and append
    // Intercepts the part after len in newCh
    newChild.slice(len).forEach(child= > {
      insert(this.createElm(child), el)
    })
  } else if (newChild.length < oldChild.length) {
    // Batch delete
    oldChild.slice(len).forEach(child= > {
      remove(child.el, el)
    })
  }
},
Copy the code