instructions

This article mainly teaches you to implement a simple vue2 step by step. The next article will teach you to implement VUE3

Function points implemented:

  1. Use SNabbDOM to realize virtual DOM and patch, etc. (Vue’s virtual DOM also refers to SNabbDOM)
  2. Two-way data binding (including Data, computed, and Watch)
  3. Implement binding methods to change data state
  4. Implementation definition component
  5. Implement JSX, i.e. we can write JSX code instead of writing the render function

Project directory structure is consistent with vue2 source code, through this project learning, you can also have a comprehensive understanding of the specific implementation of VUE. I believe that when you read vue source will be more handy.

Through the study of this article, you can understand

  1. How to implement an MVVM
  2. How do I convert JSX code into the virtual DOM, and what is the structure of the virtual DOM
  3. How does VUE implement computed properties, listeners, etc.
  4. How do Vue components work
  5. Help you understand the vUE source code, and implement a VUE
  6. … , etc.

Let’s implement a VUE2 hand in hand

The code has been uploaded to github.com/aoping/vue2…

You can follow commit to see how vue2 is implemented step by step (note bottom to top)

I. Construction project

The goal of this section is to use the rough structure of the build project. We use parcel to package our application with the entry to index.html

The project structure is as follows:

Package. json is nothing to explain

{
  "name": "snabbdom-demo"."version": "1.0.0"."description": ""."main": "index.html"."scripts": {
    "start": "parcel index.html --open"."build": "parcel build index.html"
  },
  "dependencies": {
    "snabbdom": "0.7.3"
  },
  "devDependencies": {
    "@babel/core": "7.2.0"."parcel-bundler": "^ 1.6.1." "
  },
  "keywords": []}Copy the code

Index. HTML won’t explain that either

<! DOCTYPE html> <html> <head> <title>Parcel Sandbox</title> <meta charset="UTF-8" />
</head>

<body>
	<div id="app"></div>

	<script src="src/index.js">
	</script>
</body>

</html>
Copy the code

index.js

console.log('sss')

Copy the code

Now you can start the project with NPM start

So we’ve done the first step

Snabbdom implements render

Modify index.js based on the first step

Functions achieved:

  1. To proxy data to a Vue instance, we can access the title in data via this.title
  2. Render the title onto the page
  3. Implementation listen to click event, print log
import { h, init } from 'snabbdom'
The init method is used to create the patch function
// Require these packages to listen for click events, etc
const patch = init([
  require('snabbdom/modules/class').default, // makes it easy to toggle classes
  require('snabbdom/modules/props').default, // for setting properties on DOM elements
  require('snabbdom/modules/style').default, // handles styling on elements with support for animations
  require('snabbdom/modules/eventlisteners').default, // attaches event listeners
])

function someFn() {
  console.log("got clicked");
}

// // rerender in two seconds
// setTimeout(() => {
// // New VNodes are generated after data changes
// const nextVnode = MyComponent({ title: 'next' })
// // efficiently render the real DOM by comparing old and new VNodes
// patch(prevVnode, nextVnode)
// }, 2000)



function Vue(options) {
  debugger
  this._init(options)
}

Vue.prototype._s = function (text) {
  return this[text]
}

Vue.prototype._init = function(options){
  this.$options = options
  initData(this)
  this.$mount(this.$options.el)
}

function initData(vm) {
  let data = vm._data = vm.$options.data
  const keys = Object.keys(data)
  let i = keys.length
  while (i--) {
    const key = keys[i]
    proxy(vm, `_data`, key)
  }
}

function noop () {}

const sharedPropertyDefinition = {
  enumerable: true.configurable: true.get: noop,
  set: noop
}

function proxy (target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}


Vue.prototype.$mount =function (el) {
  const vnode = this.$options.render.call(this)
  debugger
  patch(document.querySelector(el), vnode)
}


var vm = new Vue({
  el: '#app'.data: {
    title: 'prev',
  },
  render() {
    return h('button', {on: {click: someFn}}, this.title); }})Copy the code

The result of NPM start is as follows

Three, adjust the catalog

The purpose of this section is to adjust our directory to the same vUE source code, so that we can read the vUE source code one by one

The modified index.js is the same as vue

import Vue from './src/platforms/web/entry-runtime-with-compiler'

var vm = new Vue({
  el: '#app',
  data: {
    title: 'prev',
  },
  render(h) {
    return h('button', {on: {click: this.$options.methods.someFn}}, this.title);
  },
  methods: {
    someFn() {
      console.log("got clicked"); }}})Copy the code

Instead of Posting the entire code here, you can reset to chroe: Adjust directory commit

Optimization: Bind Methods to Vue instances

The purpose of this section is to bind methods to Vue instances so that we can access methods directly through this.someFn instead of this.$options.methods.somefn as in the previous section

The changes are as follows:

5. Realize bidirectional binding

Here’s the whole idea:

Observe each property of data

observe(data)
Copy the code

Observe implementation

Each key of data has a DEP, which is used to collect dependencies, known as Watcher’s (described below).

When we get the key, we add the watcher to the DEP. When we assign a value to the key, we tell the DEP to execute the dependency

Dep.target is used to save which watcher is currently in

import Dep from "./dep";

export function observe(data) {
  if(! data || typeof data ! = ='object') {
    return;
  }
  for (var key in data) {
    var dep = new Dep()
    let val = data[key]
    observe(val)
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true.get() {
        console.log('gggg')
        if (Dep.target) {
          dep.addSub(Dep.target)
        }
        return val
      },
      set(newVal) {
        if (val === newVal) return;
        console.log('sss') val = newVal dep.notify(); // Notify all subscribers},})}} //function Observer(key) {

// }

Copy the code

Realize the Dep

In the previous step we introduced a Dep, which collects dependencies and stores them in the subs array. Here is a simplified version, just to give you a sense of how this works

export default function Dep() {
  this.subs = [];
}
Dep.prototype.addSub = function(sub) {
  this.subs.push(sub);
}

Dep.prototype.notify = function() {
  this.subs.forEach(function(sub) {
      sub.update();
  });
}
Dep.target = null


Copy the code

Rendering component

When we render a component, we create a new Watcher, which we call the Render Watcher, and we’ll talk about the User Watcher later

The patch process is implemented using snabbDOM. Here we are mainly concerned with new Watcher(VM, updateComponent).

import { h } from 'snabbdom'
import { noop, } from '.. /util/index'
import Watcher from '.. /observer/watcher'
import { patch } from 'web/runtime/patch'

export function mountComponent (vm, el) {
  let updateComponent = () => {
    const vnode = vm.$options.render.call(vm, h)
    if (vm._vnode) {
      patch(vm._vnode, vnode)
    } else {
      patch(document.querySelector(el), vnode)
    }
    vm._vnode = vnode

  }

  new Watcher(vm, updateComponent)
}

Copy the code

Realize the watcher

This is also very simple, but the thing to notice is that it’s executed once when it’s new

import Dep from './dep'

export default functionWatcher(vm, cb) { this.cb = cb; this.vm = vm; This.value = this.get(); // This = this.get(); } Watcher.prototype.get =function() {
  Dep.target = this
  this.cb.call(this.vm)
  Dep.target = null
}

Watcher.prototype.update = function() {
  return this.get(); 
}
Copy the code

At this point we have implemented a simple VERSION of VUe2

Render a change

To see the effect, let’s change the render slightly

import Vue from './src/platforms/web/entry-runtime'

var vm = new Vue({
  el: '#app',
  data: {
    title: 'prev',
    num: 1,
    deep: {
      num: 1
    }
  },
  render(h) {
    return h('button', {on: {click: this.someFn}}, this.deep.num);
  },
  methods: {
    someFn() {
      this.deep.num++
    }
  }
})


Copy the code

See the effect

Sixth, the implementation of computing attributes

Objective: To achieve the calculation of the property, change the data it depends on, the calculation of the property changes accordingly

Modify the new Vue

Add a calculated property and render it

import Vue from './src/platforms/web/entry-runtime'

var vm = new Vue({
  el: '#app',
  data: {
    title: 'prev',
    num: 1,
    deep: {
      num: 1
    }
  },
  computed: {
    computedNum() {
      return this.num * 10
    }
  },
  render(h) {
    return h('button', {on: {click: this.someFn}}, this.computedNum);
  },
  methods: {
    someFn() {
      this.num++
    }
  }
})

// setTimeout(() => {
//   vm.deep.num++
// }, 3000)

Copy the code

Modify the core/instance/state. Js

The main changes are as follows:

export functionInitState (vm) {... +ifPuted (opts.com) initComputed (vm, puted opts.com... }functionInitComputed (VM, computed) {vm._computedWatchers = object.create (null) // Used to save computed watcherfor (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    vm._computedWatchers[key] = new Watcher(vm, getter, computedWatcherOptions)

    defineComputed(vm, key, userDef)
  }
}

function defineComputed(target, key, userDef) {
  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true.get() {
      debugger

      const watcher = this._computedWatchers && this._computedWatchers[key]
      if (watcher) {
        if (watcher.dirty) {
          watcher.evaluate()
        }
        if (Dep.target) {
          watcher.depend()
        }
        return watcher.value
      }
    },
    set: noop,
  })
}

Copy the code

Set the getter for the key and save a _computedWatchers watcher, execute the watcher when the key is fetched, and add the current DEP. target to the Dep of the key-dependent data. This.num = this.num; this.num = this.num; this.num = this.num; this.num = this.num; this.num = this.num; this.num = this.num

Transform Dep

let uid = 0

export default function Dep() {
  this.id = ++uid // uid for batching
  this.subs = [];
  this.subIds = new Set();

}
Dep.prototype.addSub = function(sub) {
  if(! this.subIds.has(sub.id)) { this.subs.push(sub); this.subIds.add(sub.id); } } Dep.prototype.depend =function() {
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}

Dep.prototype.notify = function() {
  this.subs.forEach(function(sub) {
      sub.update();
  });
}
Dep.target = null
const targetStack = []

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

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

Here the targetStack is used to hold the dep.target

Transform watcher

import Dep, {pushTarget, popTarget} from './dep'
let uid = 0

export default function Watcher(vm, expOrFn, options) {
  this.id = ++uid // uid for batching
  this.expOrFn = expOrFn;
  this.vm = vm;
  this.deps = []
  this.depIds = new Set();
  if(options) { this.lazy = !! options.lazy }else {
    this.lazy = false} this.dirty = this.lazy // set watcher to dep. target // in order to trigger the getter for the property, so as to add itself to the Dep, Value = this.lazy? undefined :this.get(); } Watcher.prototype.get =function() {
  let value;
  pushTarget(this)

  // if (this.dirty) Dep.target = this
  value = this.expOrFn.call(this.vm)
  // if (this.dirty) Dep.target = null
  popTarget()
  return value
}

Watcher.prototype.update = function() {
  if (this.lazy) {
    this.dirty = true;
  } else {
    this.get(); 
  }
}

Watcher.prototype.addDep = function(dep) {
  const id = dep.id
  if(! this.depIds.has(id)) { this.deps.push(dep) this.depIds.add(id) dep.addSub(this) } } Watcher.prototype.evaluate =function() {
  this.value = this.get()
  this.dirty = false
}

Watcher.prototype.depend = function() {
  let i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}
Copy the code

This is where we implement evaluated properties

7. Implement Watch

Purpose: Change num and watchMsg

Modify the render

import Vue from './src/platforms/web/entry-runtime'

var vm = new Vue({
  el: '#app',
  data: {
    num: 1,
    watchMsg: 'init msg'
  },
  watch: {
    num(newVal, oldVal) {
      this.watchMsg = newVal + ' apples'
    },
  },
  render(h) {
    return h('button', {on: {click: this.someFn}}, this.watchMsg);
  },
  methods: {
    someFn() {
      this.num++
    }
  }
})

Copy the code

Initialize the watch

function initWatch(vm, watch) {
  debugger
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

function createWatcher (vm, expOrFn, handler, options) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

export function stateMixin(Vue) {
  Vue.prototype.$watch = function (expOrFn, cb, options) {
    const vm = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    // return function unwatchFn () {
    //   watcher.teardown()
    // }
  }
}
Copy the code

This is basically a new Watcher

Transform watcher

import Dep, {pushTarget, popTarget} from './dep'
import { parsePath } from '.. /util/lang'

let uid = 0

export default function Watcher(vm, expOrFn, cb, options) {
  this.id = ++uid // uid for batching
  this.cb = cb
  this.vm = vm;
  this.deps = []
  this.depIds = new Set();
  if(options) { this.user = !! Options. user // user indicates whether watcher is user-defined, i.e. watch this.lazy =!! In new Vue({watch:{}}). options.lazy }else {
    this.user = this.lazy = false} this.dirty = this.lazy // set watcher to dep.target this.getter = typeof expOrFn ==='function' ? expOrFn : parsePath(expOrFn);

  this.value = this.lazy ? undefined :this.get(); 
}

Watcher.prototype.get = function() {
  let value;
  const vm = this.vm
  pushTarget(this)

  value = this.getter.call(vm, vm)
  popTarget()
  return value
}

Watcher.prototype.update = function() {
  if (this.lazy) {
    this.dirty = true;
  } else {
    this.run(); 
  }
}

Watcher.prototype.addDep = function(dep) {
  const id = dep.id
  if(! this.depIds.has(id)) { this.deps.push(dep) this.depIds.add(id) dep.addSub(this) } } Watcher.prototype.evaluate =function() {
  this.value = this.get()
  this.dirty = false
}

Watcher.prototype.depend = function() {
  let i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}

Watcher.prototype.run = function() {const value = this.get() // only executed when changingif(value ! == this.value) { const oldValue = this.value this.value = valueif (this.user) {
      try {
        this.cb.call(this.vm, value, oldValue)
      } catch (e) {
        console.error(`callback for watcher "${this.expression}"`)}}else {
      this.cb.call(this.vm, value, oldValue)
    }
  }
}

Watcher.prototype.teardown = function() {}Copy the code

Implement component system

So far, we haven’t been able to customize a component, and that’s the purpose of this section

Modify the render

Here we customize a button-counter component

import Vue from './src/platforms/web/entry-runtime'

Vue.component('button-counter', {
  data: function () {
    return {
      num: 0
    }
  },
  render(h) {
    return h('button', {on: {click: this.someFn}}, this.num);
  },
  methods: {
    someFn() {
      this.num++
    }
  }
})

var vm = new Vue({
  el: '#app',
  data: {
    msg: 'hello'
  },
  render(h) {
    return h('div', {}, [
      this._c('button-counter'),
      h('span', {}, this.msg) ]); }})Copy the code

Implement ponent Vue.com

This API is mounted to the Vue via initGlobalAPI(Vue)

This is implemented in core/global-api/assets.js

import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, } from '.. /util/index'

export function initAssetRegisters (Vue) {
  /**
   * Create asset registration methods.
   */
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (id, definition) {
      if(! definition) {return this.options[type + 's'][id]
      } else {
        if (type= = ='component'&& isPlainObject (definition)) {definition. The name = definition. The name | | id / / component inheritance here Vue definition = This.options._base.extend (definition)} // TODO: do not implement directive // for nowif (type= = ='directive' && typeof definition === 'function') {
        //   definition = { bind: definition, update: definition }
        // }
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}

Copy the code

Before we were rendering the root element directly, here we have to think about how to render a component

Render the component

Call the Render method in the component

Just get the constructor and call Render

import { h } from 'snabbdom'

let cachaComp = {}

export function initRender (vm) {
  vm._c = (tag, options) => {
    var Ctor = vm.constructor.options['components'][tag] var sub // Cache components to avoid re-initialization of initialized componentsif (cachaComp[tag]) {
      sub = cachaComp[tag]
    } else {
      sub = cachaComp[tag] = new Ctor(Ctor.options)
    }
    return Ctor.options.render.call(sub, h)
    // const vnode = createComponent(Ctor, data, context, children, tag)
    // return vnode
  }
}

function createComponent(Ctor) {

}

export function renderMixin (Vue) {

  Vue.prototype._render = function () {
    const vm = this
    const { render, _parentVnode } = vm.$options
    vm.$vnode = _parentVnode

    let vnode
    vnode = render.call(vm, h)
    vnode.parent = _parentVnode

    return vnode
  }

}

Copy the code

9. Implement compiler

(‘button’, {on: {click: this.someFn}}, this.num)

Modify the render

import Vue, { compiler } from './src/platforms/web/entry-runtime-with-compiler'

Vue.component('button-counter', {
  data: function () {
    return {
      num: 0
    }
  },
  render(h) {
    var button = <button onClick={this.someFn}>{this.num}</button>
    return button
    // return h('button', {on: {click: this.someFn}}, this.num);
  },
  methods: {
    someFn() {
      this.num++
    }
  }
})

var vm = new Vue({
  el: '#app',
  data: {
    msg: 'hello'
  },
  render(h) {
    return (
      <div>
        {this._c('button-counter')}
        <span>{this.msg}</span>
      </div>
    )
    // return h('div', {}, [
    //   this._c('button-counter'),
    //   h('span', {}, this.msg) // ]); }})Copy the code

Here we will implement JSX syntax with @babel/plugin-transform-react-jsx

Configuration @ Babel/plugin – transform – react – JSX

.babelrc

{
  "plugins": [["@babel/plugin-transform-react-jsx",
      {
        "pragma": "compiler"}}]]Copy the code

Here compiler is the function we defined to work with JSX

Implementing compiler functions

Return h(‘button’, {on: {click: this.somefn}}, this.num)

import Vue from './runtime/index'
import { h } from 'snabbdom'

export function compiler(tag, attrs) {
  let props = attrs || {}
  let children = []
  let options = {
    on: {}
  }
  for (const k in props) {
    if (k[0] === 'o' && k[1] === 'n') {
      options.on[k.slice(2).toLocaleLowerCase()] = props[k]
    }
  }

  for (let i = 2; i < arguments.length; i++) {
    let vnode = arguments[i]
    children.push(vnode)
  }
  return h(tag, options, children)
}

export default Vue

Copy the code

It’s that simple!!

The end