[Youth Camp Pro] front end framework design concept - Vue3 motivation - handwriting implementation Mini Vue


The course of the youth training camp combat class is also over. Today, let's roll out the mini Vue course brought by village head Yang on Friday. If you miss the course, you must not miss this super detailed and slightly expanded note~

The relevant code can see mine gitee warehouse

For some information about Vue3, please refer to my previous blog Get you started quickly, Vue3

1, Front end frame design concept

1. Use Vue as an example

  • Simple and easy to use

  • Data driven

    • Reduce DOM operations
    • data-driven
      • Data responsive (reactive, effect ive)
    • Declarative rendering, render
    • vdom
    • patch
  • progressive (min, vuex, router, element3)

2. Vue3's motivation

  1. (type support) why use functions?
  • Functions: vue3, react

  • class: Angular, vue2(decorator)

The function signature is clear, so the input and output contents are clear

  1. Advantages of Composition API
  • Destroy this
  • Declarative responsive data
  • Reusability, readability and maintainability
<div id="app">{{title}}</div>
<script src="http://unpkg.com/vue@next"></script>

<script>
  const app = Vue.createApp({
    data() {
      return {
        title: 'vue3,  YK bacterium, data'
      }
    },
    setup() {
      // Advantages of Composition API
      // Destroy this
      // Declarative responsive data
      // Reusability, readability and maintainability
      const state = Vue.reactive({
        title: 'vue3, YK bacterium, setup'
      })
      return state
    }
  })

  app.mount('#app')
</script>

As a result, you can see that setup has a higher priority

3. Summary

  1. Type support
  2. Instance method and attribute tree shaking
  3. Reusable hook
  4. Maintainability Composition API
  5. API simplification
    1. Consistency (the lifecycle in an instruction is different from that of a component)
    2. Delete API s with the same function (v-model,. sync (Vue3))
    <comp v-model="foo"></comp>
    <comp :foo.sync="foo"></comp>
    <comp :value="foo" @update:value="foo = $event"></comp>
    
    1. render
    // Vue2 writing method
    render(h){
       return h('div', {
         attrs: {
           title: this.title
         }
       }, 'xxx')
    }
    
    // Vue3 writing method
    render(){
      return Vue.h('div', {
        title: this.title
      }, 'xxx')
    }
    
  6. Extensibility: Custom renderer Vue.createRenderer()
  7. Performance optimization - responsive Proxy based
    1. Recursive efficiency problem
    2. Array problem (implemented in a separate set)
    3. API impact (adding and deleting Vue.delete / set of dynamic attributes)
    4. class collection data structures are not supported
    5. Compatibility (vue2.7)

2, Implementation of mini Vue

It's hard to read the source code directly. Novices often can't distinguish the key points. They are easy to encounter obstacles in some corners and waste a lot of time. Therefore, today's Mini Vue only focuses on the core part

1. Initialization

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

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

<script>
  const app = Vue.createApp({
    data() {
      return {
        title: 'hello, vue3 YK bacterium'
      }
    }
  })
  app.mount('#app')
</script>

① Basic structure

const Vue = {
  createApp(options) {
    // Return app object
    return {
      mount(selector) {
        // Code to be filled [how to mount]
      }
    }
  }
}

② How to mount elements

What we should think is what Vue's mount did?

  1. Host element found
  2. Render Page
    1. Processing template: compile using compile
    2. Users write render directly
  3. Append to host

According to the above ideas, let's write the code

// 1. Basic structure
const Vue = {
  createApp(options) {
    // Return app object
    return {
      mount(selector) {
        // 1. Find host element
        const parent = document.querySelector(selector)
        // 2. Render page
        if (!options.render) {
          // 2.1 processing template: Compiling
          options.render = this.compile(parent.innerHTML)
        }
        // 2.2 users write render directly
        // Execute the render function (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) {
        // Returns a render function
        // parse -> ast
        // generate: ast -> render
        return function render() {
          const h3 = document.createElement('h3')
          h3.textContent = this.title
          return h3
        }
      }
    }
  }
}

As shown in the figure below, we have completed the step of displaying the data in data

③ Compatibility processing

If the user writes data and setup at the same time, Vue3 will give priority to the data in setup

const app = Vue.createApp({
  data() {
    return {
      title: 'hello, vue3 YK bacterium data'
    }
  },
  setup() {
    return {
      title: 'hello, vue3 YK bacterium setup'
    }
  }
})
app.mount('#app')

So before rendering, we need to deal with the compatibility between setup and other options

First, we collect setup and other options

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

We create a proxy and specify priorities in getter s and setter s

// Before rendering, handle the compatibility of setup and other options
const proxy = new Proxy(this, {
  get(target, key) {
    // Get from setup first. If not, get from data again
    // If setup exists and the 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)
    }
  }
})

Before mounting the element, set the context of render to proxy

const el = options.render.call(proxy)

④ Extensibility processing (custom renderer)

In the code we wrote above, the returned app instance is strongly coupled with the web platform, because the operations all use document. To improve scalability, we should use high-order components to pass in platform related operations through parameters, so as to achieve the effect of decoupling, and make our framework independent of the platform~

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

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

Change to

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

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

How? We need to provide a new API for users to choose

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

            // 2. Render 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)
  }
}

After initialization, the next thing we need to consider is how to implement responsive data

2. Responsive

I believe many small partners who have not contacted Vue3 have heard that Vue3's response is based on Proxy

However, Vue2 implements data response based on Object.defineProperty, which has many disadvantages: (for this content, please refer to my previous blog post [Vue source code] data response principle)

  1. Recursive efficiency problem
  2. Array problem (implemented in a separate set)
  3. API impact (adding and deleting Vue.delete / set of dynamic attributes)
  4. class collection data structures are not supported

① reactive

Let's explore. First, use reactive to create a responsive object. Then, after two seconds, the value of title will change and the page will change.

const app = Vue.createApp({
  setup() {
    const state = reactive({
      title: 'hello, vue3 YK bacterium'
    })

    setTimeout(() => {
      state.title = '2 See a new one in seconds YK bacterium'
    }, 2000)

    return state
  }
})
app.mount('#app')

We create a reactive function to intercept the user's access to the proxy object, so as to respond when the value changes.

// The content intercepts the user's access to the proxy object, so as to respond when the value changes
function reactive(obj) {
  // Returns 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
    }
  })
}

We add an update function to the returned app object, write the code before rendering the page, and then call it once.

// 2.2 users write render directly
this.update = function () {
  // Execute the render function (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()

As you can see, our page is responsive. The content will change in two seconds

Take a break. The village head teacher sings a song for everyone~~~~

② Dependency collection

There is a line of app.update() in the above written reactive function, where the method of app is called, which is strongly coupled with our app. We need a mechanism (publish and subscribe) to decouple such behavior: establish dependencies between those responsive data and their associated update functions

Establish mapping relationship: rely on dep - > component update function

Vue2 uses watcher. See my previous blog for specific implementation [Vue source code] data response 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 obtain dependencies in set

Let's implement the following:

First, we define a side effect function and receive a function as the parameter fn. The first step is to execute the fn function

function effect(fn) {
  // 1. Execute once fn
  fn()
}

But there is a problem here, that is, what should we do if the function reports an error when executing fn, so we write a higher-order function to wrap our fn

First, create a stack effectStack to temporarily store side-effect functions

const effectStack = []

Why use the stack here? Because nesting may occur when calling the side effect function effect, you can collect eff well by using a data structure such as stack

Upgrade our effect function

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

We also need to build such a dependency data structure

const targetMap = {
// Such data and dependencies should be stored in it
// state: {
//   'title': [update]
// }
}

Then define a kick function to establish the relationship between target, key and side-effect functions stored in effectStack

// Establish the relationship between target,key and side effect functions stored in effectStack
function track(target, key) {
  // Take out the last element that stores the side effect function
  const effect = effectStack[effectStack.length - 1]
  // This is the case when writing is dead, but you can't write like this, or you'll create a new object every time
  // targetMap[target] = {}
  // targetMap[target][key] = [effect]
  // Therefore, you should first judge whether the object with target key exists
  let map = targetMap[target]
  if (!map) {
    // get this target for the first time [initialize the map if it doesn't exist] 
    map = targetMap[target] = {}
  }

  let deps = map[key]
  if (!deps) {
    deps = map[key] = []
  }
  // Mapping relationship establishment
  if (deps.indexOf(effect) === -1) {
    deps.push(effect)
  }
}

Then 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())
    }
  }
}

We need to add dependencies in the getter and trigger updates in the setter, so we set this 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 update
      trigger(target, key)
    }
  })
}

Finally, let's test it with test cases

// Create responsive data obj
const obj = reactive({
  foo: 'foo'
})
// Create a side effect function to internally trigger responsive data
effect(() => {
  // Trigger responsive data 
  console.log(obj.foo)
}) 
// Change foo attribute in obj
obj.foo = 'foo Changed~~~'

Let's deal with the relationship

Finally, we need to improve our code. We need to create side effects from the update function we wrote before, so that when the data changes, we can execute the update function again

Therefore, we only need to wrap the previously written function with a layer of effect higher-order function

// 2.2 users write render directly
this.update = effect(() => {
  // Execute the render function (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()

At this point, we delete our test cases to see the effect of actual use in setup:

In fact, in the source code, the data structure adopted by targetMap is not an object, but a Map, and it is a WeakMap

const targetMap = new WeakMap()

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

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

When storing dependencies, you should use a data structure such as Set, which can automatically remove duplicates

// deps = map[key] = []
deps = map[key] = new Set()

// Mapping relationship establishment
// if (deps.indexOf(effect) === -1) {
//   deps.push(effect)
// }
deps.add(effect)

The above response has been completed, but it has a serious efficiency problem, that is, we use the full update method to update our DOM, which must be bad, so this leads to the virtual Dom and diff algorithm we want to talk about below

3. Virtual DOM

What is virtual DOM (vnode)?

  1. vnode is a js object used to describe the view

Why introduce vnode?

  1. Reduce DOM operations
  2. Efficient update
  3. Cross platform, compatibility

Let's define our virtual DOM - vnode

We want to turn the real DOM in the render function returned in the compile function into a virtual dom

return function render() {
  // const h3 = document.createElement('h3')
  // h3.textContent = this.title
  // return h3
  // 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),
  // ])
}

The function of defining an h function is to represent a DOM with a js object

// Incoming information, return vnode, description view
function h(tag, props, children) {
  return {
    tag,
    props,
    children
  }
}

The update function will also change

this.update = effect(() => {

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

    // Implement createElm, overall creation, vnode - > el
    const el = this.createElm(vnode)
    parent.innerHTML = ''
    insert(el, parent)
    // init initialization, set mounted ID
    this.isMounted = true
  }
})

Next, the createElm function is implemented in the form of recursion

createElm({
  tag,
  props,
  children
}) {
  // Traverse vnode and create the whole tree
  const el = createElement(tag)
  // If there are properties, set them (omitted)
  // el.setAttribute(key, val)
  
  // recursion
  // Determine whether children is a string
  if (typeof children === 'string') {
    el.textContent = children
  } else {
    children.forEach(child => insert(this.createElm(child), el))
  }
  return el
}

4. diff

See my previous blog for more details [Vue source code] graphical diff algorithm

To save a real node in createElm

// The real DOM should be saved in vnode for future updates
vnode.el = el

Modify the update function, complete the first mount part, and add the update logic

this.update = effect(() => {

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

    // Implement createElm, overall creation, vnode - > el
    const el = this.createElm(vnode)
    parent.innerHTML = ''
    insert(el, parent)
    // init initialization, set mounted ID
    this.isMounted = true
  } else {
    this.patch(this._vnode, vnode)
  }
  this._vnode = vnode
})

patch update

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

    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 the text with a set of child elements, empty, 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 different nodes, replace
  }
},

Set this to update the data: from string to array

setup() {
  const state = reactive({
    title: 'hello, vue3 YK bacterium'
  })

  setTimeout(() => {
    state.title = '2 See a new one in seconds YK bacterium'.split("")
  }, 2000)

  return state
}

effect

diff comparison

Vue3 is basically the same as Vue2. In my previous blog post, I also drew a comparison diagram of diff in Vue2. You can take a look at that article [Vue source code] graphical diff algorithm

Here we simplify and update in the simplest and crudest way

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

  // Process remaining elements
  // Many new array elements
  if (newChild.length > oldChild.length) {
    // Batch create and append
    // Intercept 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)
    })
  }
},

Tags: Javascript Vue.js

Posted on Fri, 15 Oct 2021 01:07:08 -0400 by squiggerz