【手把手系列之】實現一個簡易版vue2

說明

本文主要教你們一步一步實現一個簡易的vue2,下一篇將會教你們實現vue3html

實現的功能點:vue

  1. 利用snabbdom實現虛擬dom與patch等(vue的虛擬dom也是參考snabbdom的)
  2. 數據雙向綁定(包括data, computed, watch)
  3. 實現綁定methods,以改變數據狀態
  4. 實現定義組件
  5. 實現jsx,即咱們能夠寫jsx代碼來替代前面的寫render函數

項目目錄結構與vue2源碼一致,經過本項目的學習,你也能對vue的具體實現有一個較全面的瞭解。相信當你去閱讀vue源碼時會更駕輕就熟。node

經過本文的學習,你能夠了解react

  1. 如何實現一個mvvm
  2. 如何把jsx代碼轉爲虛擬dom,虛擬dom的結構是怎樣的
  3. vue是如何實現計算屬性,監聽器等
  4. Vue組件是如何工做的
  5. 幫你理解vue的源碼,並實現一個vue
  6. ……等等

下面咱們就手把手實現一個vue2吧git

代碼已上傳到 github.com/aoping/vue2…github

你們能夠根據commit來看是如何一步一步實現vue2的(注意從下到上)web

1、搭建項目

本節目標是用搭建項目的大概結構,咱們用parcel打包咱們的應用,入口是index.htmlexpress

項目結構以下:npm

package.json 這個沒什麼好解釋的json

{
  "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": []
}

複製代碼

index.html 這個也不解釋

<!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>
複製代碼

index.js

console.log('sss')

複製代碼

如今經過npm start就能夠啓動項目了

這樣咱們就完成了第一步

2、snabbdom實現render

在第一步的基礎上修改index.js

實現的功能:

  1. 把data代理到Vue實例上,即咱們能夠經過this.title來訪問data裏的title
  2. 把title渲染到頁面上
  3. 實現監聽click事件,打印log
import { h, init } from 'snabbdom'
// init 方法用來建立 patch 函數
// 注意這裏要require這些包,才能監聽點擊事件等
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");
}

// // 兩秒以後重渲染
// setTimeout(() => {
// // 數據變動,產出新的 VNode
// const nextVnode = MyComponent({ title: 'next' })
// // 經過對比新舊 VNode,高效的渲染真實 DOM
// 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);
  }
})

複製代碼

執行npm start結果以下

3、調整一下目錄

這節的目的就是把咱們的目錄調整的跟vue源碼一致,方便咱們之後閱讀vue源碼能一一對應上

修改後的index.js, 是否是跟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");
    }
  }
})

複製代碼

這裏就不貼所有的代碼了,你們能夠reset到chroe: 調整目錄這個commit

4、優化:把methods綁定到Vue實例上

這節的目的是把methods綁定到Vue實例上,這樣咱們就能直接經過this.someFn來訪問方法了,而不用像上一節經過this.$options.methods.someFn

改動以下:

5、實現雙向綁定

先講講整個原理:

觀察data的每一個屬性

observe(data)
複製代碼

observe實現

data的每一個key都有一個dep,這個是用來收集依賴,即watcher的(後面會介紹)

這裏主要是給key設置了getter、setter,當咱們獲取key的時候就把watcher加入到dep裏,當咱們給key賦值時就通知dep執行依賴

Dep.target是用來保存目前是在哪一個watcher裏的

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(); // 通知全部訂閱者
      },
    })
  }
}

// function Observer(key) {

// }

複製代碼

實現Dep

在上一步咱們引入了一個Dep, 這個用來收集依賴,保存在subs這個數組裏, 這裏是簡化版,目的是讓你們先對這個原理有個瞭解

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


複製代碼

渲染組件

當咱們渲染組件的時候,咱們會new一個watcher,這個咱們稱之爲渲染watcher,後面還會介紹user watcher等

patch過程你們能夠先不看,就是利用snabbdom來實現的,這裏咱們主要關心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)
}

複製代碼

實現watcher

這個也很簡單,須要注意的是new的時候會執行一次

import Dep from './dep'

export default function Watcher(vm, cb) {
  this.cb = cb;
  this.vm = vm;
  // 此處爲了觸發屬性的getter,從而在dep添加本身,結合Observer更易理解
  this.value = this.get(); 
}

Watcher.prototype.get = function() {
  Dep.target = this
  this.cb.call(this.vm)
  Dep.target = null
}

Watcher.prototype.update = function() {
  return this.get(); 
}
複製代碼

到這裏咱們就已經實現了一個簡易的vue2

改一改render

爲了看效果,咱們稍微改一下render

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++
    }
  }
})


複製代碼

查看效果

6、實現計算屬性

目標:實現計算屬性,改變它依賴的data時,計算屬性相應的改變

修改new Vue

增長一個計算屬性,並渲染它

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)

複製代碼

修改core/instance/state.js

主要改動以下:

export function initState (vm) {
  ……
  + if (opts.computed) initComputed(vm, opts.computed)
  ……
}


function initComputed(vm, computed) {
  vm._computedWatchers = Object.create(null) // 用於保存計算watcher

  for (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,
  })
}

複製代碼

這裏也是給key設置getter,並用_computedWatchers保存一個計算watcher,當獲取key時就執行這個watcher,並把當前的Dep.target加入到key依賴的data的dep裏(這裏有點繞,在這個例子中就是當執行render(這時新建了一個渲染watcher)的時候會獲取this.computedNum,這個是根據this.num計算出來的,因此就會把渲染watcher加入到num的dep裏)

改造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]
}
複製代碼

這裏targetStack是用來保存Dep.target的

改造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 // 用於渲染時不把計算watcher設置成Dep.target
  // 此處爲了觸發屬性的getter,從而在dep添加本身,結合Observer更易理解
  this.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()
  }
}
複製代碼

到這裏咱們就實現了計算屬性

7、實現watch

目的:改變num,watchMsg的值也改變

修改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++
    }
  }
})

複製代碼

初始化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()
    // }
  }
}
複製代碼

這裏主要是new了一個Watcher

改造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表示是不是用戶定義的watcher,即咱們在new Vue({watch:{}})裏的watch
    this.lazy = !!options.lazy
  } else {
    this.user = this.lazy = false
  }

  this.dirty = this.lazy // 用於渲染時不把計算watcher設置成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()
  // 變化時才執行
  if (value !== this.value) {
    const oldValue = this.value
    this.value = value
    if (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() {
  
}
複製代碼

8、實現組件系統

到目前爲止,咱們都不能自定義一個組件,那本節的目的就是實現自定義組件

修改render

這裏咱們自定義了一個button-counter的組件

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)
    ]);
  },
})

複製代碼

實現Vue.component

這個api是經過initGlobalAPI(Vue)掛載到Vue上的

實如今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.name = definition.name || id
          // 這裏組件繼承Vue
          definition = this.options._base.extend(definition)
        }
        // TODO:暫時先不實現directive
        // if (type === 'directive' && typeof definition === 'function') {
        //   definition = { bind: definition, update: definition }
        // }
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}

複製代碼

以前咱們都是直接渲染根元素,這裏咱們要考慮怎麼渲染一個組件

render組件

其實也是調用組件裏的render方法

先拿到構造函數,而後調用render就能夠了

import { h } from 'snabbdom'

let cachaComp = {}

export function initRender (vm) {
  vm._c = (tag, options) => {
    var Ctor = vm.constructor.options['components'][tag]
    var sub
    // 緩存組件,避免已初始化的組件被從新初始化
    if (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
  }

}

複製代碼

9、實現compiler

目標:咱們能夠直接寫jsx,便可以直接寫 <button onClick={this.someFn}>{this.num}</button> 而不用像以前那樣寫個h('button', {on: {click: this.someFn}}, this.num)

修改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)
    // ]);
  },
})


複製代碼

這裏咱們要藉助@babel/plugin-transform-react-jsx實現jsx語法

配置@babel/plugin-transform-react-jsx

.babelrc

{
  "plugins": [
    [
      "@babel/plugin-transform-react-jsx",
      {
        "pragma": "compiler"
      }
    ]
  ]
}
複製代碼

這裏compiler是咱們定義的用來處理jsx的函數

實現compiler函數

其實就是返回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

複製代碼

就是這麼簡單!!!

完結

相關文章
相關標籤/搜索