Write a main process implementation of Vue from zero

Write in front

vue3 is coming soon. Has vue2 learned?

Recently, I have seen many articles with similar titles. Although you may have learned some common interview questions such as two-way binding of vue, virtual dom, diff algorithm, etc. a few years ago, can you implement a vue from scratch.

Based on the principle that the best way to learn is to realize one time by yourself, while the epidemic situation cannot be returned to school, we plan to realize a complete vue as much as possible, delete flow and many types of judgments, and only keep the mainstream process of each function, in order to provide a transition for direct reading of vue source code.

After all, vue source code is still relatively difficult to nibble, and it is difficult to connect the various modules by reading the articles on the Internet. Follow me to set up the whole framework first, and then implement the main process of vue function by function. Regardless of flow and type judgment, focusing on the implementation principle of source code is a good transition from Xiaobai to direct reading of source code.

It is only for learning and communication. If you think the article is too slow, you can read the source code directly: https://github.com/buppt/YourVue

This article is the first in the series of zero implementation of vue2. The main process of Vue is implemented, regardless of the two-way binding, virtual dom, etc., which will be added later.

text

We implement the function in the way of vue, one number and one button, click the button number plus one.

// main.js
import YourVue from './instance'
new YourVue({
  el: '#app',
  data: {
      count: 0,
  },
  template: `
      <div>
        <div>{{count}}</div>
        <button @click="addCount">addCount</button>
      </div>
  `,
  methods:{
      addCount(){
          const count = this.count + 1
          this.setState({  // No two-way binding, first update through setState
              count
          })
      }
  }
})

realization

First, initialize a class. There are three problems to be concerned about here

  1. The first is how to directly access variables in data and methods through this
  2. Second, how to convert template template to dom element
  3. The third is how to bind events to dom elements

Start with your Vue definition.

export default class YourVue{
    constructor(options){
        this._init(options)
    }
    _init(options){
        this.$options = options
        if (options.data) initData(this)
        if (options.methods) initMethod(this)
        if (options.el) {
            this.$mount()
        }
    }
    $mount(){
        this.update()
    }
    update(){
        let el = this.$options.el
        el = el && query(el)
        if(this.$options.template){
            this.el = templateToDom(this.$options.template, this)
            el.innerHTML = ''
            el.appendChild(this.el)
        }
    }
    setState(data){
        Object.keys(data).forEach(key => {
            this[key] = data[key]
        })
        this.update()
    }
}

Question 1

How to directly access variables in data and methods through this?

vue is through Object.defineProperty The get and set functions of this are modified so that when the this.count In fact, I visited this_ data.count .

function initData(vm){
    let data = vm.$options && vm.$options.data
    vm._data = data
    data = vm._data = typeof data === 'function'
        ? data.call(vm, vm)
        : data || {}
    Object.keys(data).forEach(key => {
        proxy(vm, '_data', key)
    })
}
function proxy (target, sourceKey, key) {
    const sharedPropertyDefinition = {
        enumerable: true,
        configurable: true
    }
    sharedPropertyDefinition.get = function proxyGetter () {
        return this[sourceKey][key]
    }
    sharedPropertyDefinition.set = function proxySetter (val) {
        this[sourceKey][key] = val
    }
    Object.defineProperty(target, key, sharedPropertyDefinition)
}

Methods is to create a key point directly on this object this.methods Function in.

function initMethod(vm){
    const event = vm.$options.methods
    Object.keys(event).forEach(key => {
        vm[key] = event[key].bind(vm)
    })
}

In this way, you can directly access the variables and functions in data and methods through this. Of course, you should judge whether the variables in data and methods are duplicate. In order to simplify the code, you need to omit them.

Question 2

How to convert template template to dom element?

First parse the template, parse the idiom tree with the template, and then insert the dom tree generated according to the ast into the element location passed in when new. As for how to parse from template to ast, see my previous article, link: https://github.com/buppt/Video-article-blog/issues/4 . Here we know that the following types of ast will be generated.

{
    type: 1
    tag: "div"
    children: [{...}, {...},
        {
            type: 1 
            tag: "button"
            attrsMap: {@click: "addCount"}
            children: [{type: 3, text: "addCount", parent: button}]
            events: {click: ƒ}
            parent: div
        }
    ]
}

There are three type s in ast: 1 for dom node, 3 for plain text node, and 2 for text node with variable.

Then converting ast to dom element is not the idea of vue. In order to realize the closed-loop function, this is the first way. After the virtual dom is implemented, it will be changed to the form of generating vnode by render function, and then generating dom by vnode.

export function templateToDom(template, app){
    const ast = parse(template, app)
    const root = createDom(ast, app)
    return root
}

function createDom(ast, app){
    if(ast.type === 1){
        const root = document.createElement(ast.tag)
        ast.children.forEach(child => {
            child.parent = root
            createDom(child, app)
        })
        if(ast.parent){
            ast.parent.appendChild(root)
        }
        if(ast.events){
            updateListeners(root, ast.events, {}, app)
        }
        return root
    }else if(ast.type === 3 && ast.text.trim()){
        ast.parent.textContent = ast.text
    }else if(ast.type === 2){
        let res = ''
        ast.tokens.forEach(item => {
            if(typeof item === 'string'){
                res += item
            }else if(typeof item === 'object'){
                res += app[item['@binding']]
            }
        })
        ast.parent.textContent = res
    }
}

Question three

The third is how to bind events to dom elements?

This code is used for DOM binding events when generating dom.

if (ast.events) {
    updateListeners(root, ast.events, {}, app)
}

The generated ast will record the event on this element and the function {click: ƒ} corresponding to the event, but it does not directly add this function to the event, but wraps a layer of invoker function, so that when the bound function changes, it does not need to be unbound again. Instead, look for the function to execute each time you execute it.

function updateListeners(elm, on, oldOn, context){
    for (let name in on) {
        let cur = context[on[name].value]
        let old = oldOn[name]
        if(isUndef(old)){
            if (isUndef(cur.fns)) {
                cur = on[name] = createFnInvoker(cur)
            }
            elm.addEventListener(name, cur)
        }else if(event !== old){
            old.fns = cur
            on[name] = old
        }
    }
    for (let name in oldOn) {
        if (isUndef(on[name])) {
        elm.removeEventListener(name, oldOn[name])
        }
    }
}

function createFnInvoker(fns){
    function invoker () {
        const fns = invoker.fns
        return fns.apply(null, arguments)
    }
    invoker.fns = fns
    return invoker
}

This is the end of this article. You may feel that it's very simple. A big guy said "it's not difficult, it's not difficult". I hope you feel so easy every time you read the article.

Code: https://github.com/buppt/YourVue/tree/master/oldSrc/1.main_flow

Find star ~

Tags: Vue github

Posted on Fri, 19 Jun 2020 04:06:30 -0400 by Vorotaev