本文主要教你們一步一步實現一個簡易的vue2,下一篇將會教你們實現vue3html
實現的功能點:vue
項目目錄結構與vue2源碼一致,經過本項目的學習,你也能對vue的具體實現有一個較全面的瞭解。相信當你去閱讀vue源碼時會更駕輕就熟。node
經過本文的學習,你能夠了解react
下面咱們就手把手實現一個vue2吧git
代碼已上傳到 github.com/aoping/vue2…github
你們能夠根據commit來看是如何一步一步實現vue2的(注意從下到上)web
本節目標是用搭建項目的大概結構,咱們用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
就能夠啓動項目了
這樣咱們就完成了第一步
在第一步的基礎上修改index.js
實現的功能:
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
結果以下
這節的目的就是把咱們的目錄調整的跟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
這節的目的是把methods綁定到Vue實例上,這樣咱們就能直接經過this.someFn
來訪問方法了,而不用像上一節經過this.$options.methods.someFn
改動以下:
先講講整個原理:
observe(data)
複製代碼
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, 這個用來收集依賴,保存在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)
}
複製代碼
這個也很簡單,須要注意的是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
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++
}
}
})
複製代碼
查看效果
目標:實現計算屬性,改變它依賴的data時,計算屬性相應的改變
增長一個計算屬性,並渲染它
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)
複製代碼
主要改動以下:
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裏)
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的
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()
}
}
複製代碼
到這裏咱們就實現了計算屬性
目的:改變num,watchMsg的值也改變
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++
}
}
})
複製代碼
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
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() {
}
複製代碼
到目前爲止,咱們都不能自定義一個組件,那本節的目的就是實現自定義組件
這裏咱們自定義了一個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)
]);
},
})
複製代碼
這個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就能夠了
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
}
}
複製代碼
目標:咱們能夠直接寫jsx,便可以直接寫 <button onClick={this.someFn}>{this.num}</button>
而不用像以前那樣寫個h('button', {on: {click: this.someFn}}, this.num)
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語法
.babelrc
{
"plugins": [
[
"@babel/plugin-transform-react-jsx",
{
"pragma": "compiler"
}
]
]
}
複製代碼
這裏compiler是咱們定義的用來處理jsx的函數
其實就是返回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
複製代碼
就是這麼簡單!!!
完結