在前端的世界里,不仅要会使用,更要懂原理。第一弹。

webpack简易版实现

简单实现commonjs规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 实现commonjs
// a.js
// module.exports = 'hello'
// b.js
// let str=require('./a.js')
const fs = require('fs')

function req(moduleName) {
let content = fs.readFileSync(moduleName, 'utf8')

// 创建函数
// function(exports, module, require, __dirname, __filename) {
// module.exports = 'hello'
// return module.exports
// }
let fn = new Function('exports', 'module', 'require', '__dirname', '__filename', content + '\n return module.exports')
let module = {
exports: {}
}
return fn(module.exports, module, req, __dirname, __filename)
}

let str = req('./a.js')
console.log(str);

简单实现AMD规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// define声明模块,通过require使用模块
let fns = {}

function define(moduleName, dependencies, fn) {
fn.dependencies = dependencies //将依赖记到fn上
fns[moduleName] = fn
}

function require(modules, cb) {
let results = modules.map(function(mod) {
let fn = fns[mod]
let exports

// 对依赖模块进行递归
let dependencies = fn.dependencies
require(dependencies, function() {
exports = fn.apply(null, arguments)
})
return exports
})
cb.apply(null, results)
}

define('a', [], function() {
return 'a'
})
define('b', [], function() {
return 'b'
})
define('c', ['a'], function(a) {
return a.concat('c')
})

require(['a', 'b', 'c'], function(a, b, c) {
console.log(a, b, c);
})

webpack打包后的核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(function(modules) {
// moduleId就是文件名
function require(moduleId) {
var module = {
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, require);
return module.exports;
}
return require('./src/index.js')
})
({
"./src/index.js": (function(module, exports) {
eval("console.log('hello');\n\n//# sourceURL=webpack:///./src/index.js?");
})
});

实现我们自己的wepack打包工具

  1. 新建文件夹mypack,新建bin文件夹,在bin下创建mypack.js,在这个文件实现打包操作
  2. npm init初始化项目,修改package.json中的bin"mypack": "bin/mypack.js",运行npm link将此命令关联到全局环境下
  3. mypack.js中需要加入#! /usr/bin/env node,告诉当前文件在node环境下运行
  4. 编写mypack.js打包核心代码
  5. 在原有项目文件夹,运行mypack命令即可实现最终的打包

接下来在mypack.js中实现我们自己的webpack。

核心思想:
模板如上,采用模板替换的方式,将代码中的entry、output替换为我们自己的,再将eval的内容换为读取的entry文件内容(模板替换可采用ejs模块实现),最后将替换后的内容写入output文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#! /usr/bin/env node

const fs = require('fs')
const ejs = require('ejs')

const entry = './src/index.js'
const output = './dist/main.js'
const script = fs.readFileSync(entry, 'utf8')


let template = `
(function(modules) {
// moduleId就是文件名
function require(moduleId) {
var module = {
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, require);
return module.exports;
}
return require("<%-entry%>")
})
({
"<%-entry%>": (function(module, exports) {
eval(\`<%-script%>\`);
})
});
`

let result = ejs.render(template, {
entry,
script
})

// result为替换后的结果,最终要写到output中
fs.writeFileSync(output, result)
console.log('打包成功!');

继续完善:如果打包文件中存在require()引入其他模块的情况,需要进行相关处理,首先我们采用webpack打包看一下原始打包结果,以下是基本骨架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(function(modules) {
function require(moduleId) {
var module = {
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, require);
return module.exports;
}
return require(require.s = "./src/index.js");
})
({
"./src/index.js": (function(module, exports, __webpack_require__) {
eval("const result = __webpack_require__(/*! ./a.js */ \"./src/a.js\")\r\nconsole.log(result);\n\n//# sourceURL=webpack:///./src/index.js?");
}),
"./src/a.js": (function(module, exports) {
eval("module.exports = 'hello'\n\n//# sourceURL=webpack:///./src/a.js?");
})
});

可见,在传入的参数中,不仅传入了入口文件entry,还传入了引入的其他模块。因此我们继续修改mypack.js,让其支持模块引入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#! /usr/bin/env node

const fs = require('fs')
const path = require('path')
const ejs = require('ejs')

const entry = './src/index.js'
const output = './dist/main.js'
let script = fs.readFileSync(entry, 'utf8')

// 新增部分
let modules = []

// 处理依赖关系
// ?代表非贪婪捕获
// require('./a.js')
script = script.replace(/require\(['"](.+?)['"]\)/g, function() {
let name = path.join('./src/', arguments[1]) // ./src/a.js
let content = fs.readFileSync(name, 'utf8')
modules.push({
name,
content
})
return `require('${name}')`
})


// 修改template模板,采用ejs的循环模式
let template = `
(function(modules) {
function require(moduleId) {
var module = {
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, require);
return module.exports;
}
return require(require.s = "<%-entry%>");
})
({
"<%-entry%>": (function(module, exports,require) {
eval(\`<%-script%>\`);
}),
<%for(let i=0;i<modules.length;i++){
let module=modules[i]%>
"<%-module.name%>": (function(module, exports) {
eval(\`<%-module.content%>\`);
}),
<%}%>
});
`
let result = ejs.render(template, {
entry,
script,
modules // 注意这里需要传入modules
})

// result为替换后的结果,最终要写到output中
fs.writeFileSync(output, result)
console.log('打包成功!');

再次打包,发现文件通过require引入其他模块成功!

接下来,再来支持require('./index.css')引入css样式文件,我们继续修改mypack.js,如果require文件是css文件,则对其内容进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 新增css-loader
// source是文件中的内容
// 新建style标签,放入css文件的内容,将style标签插入到head中
let styleLoader = function(source) {
return `
let style=document.createElement('style')
style.innerText=${JSON.stringify(source).replace(/\\r\\n/g,'')}
document.head.appendChild(style)
`
}

script = script.replace(/require\(['"](.+?)['"]\)/g, function() {
let name = path.join('./src/', arguments[1]) // ./src/a.js
let content = fs.readFileSync(name, 'utf8')

// 这里我们做一下拦截
if (/\.css$/.test(name)) {
content = styleLoader(content)
}

modules.push({
name,
content
})
return `require('${name}')`
})

再次打包,发现可以成功引入css文件了!

react-router实现

hash路由原理

window绑定hashchange事件

1
2
3
4
5
6
7
8
9
10
<body>
<a href="#/a">a</a>
<a href="#/b">b</a>
</body>

<script>
window.addEventListener('hashchange', () => {
console.log(window.location.hash);
})
</script>

history路由原理

window绑定popstate事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<body>
<a onclick="push('/a')">a</a>
<a onclick="push('/b')">b</a>
</body>
<script>
function push(path) {
history.pushState({
p: path
}, null, path)
}

// 浏览器前进和后退
window.addEventListener('popstate', (e) => {
console.log(e);
})
</script>

react中react-router的使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React, { Component } from 'react'
import { render } from 'react-dom'
import { HashRouter as Router, Route } from './react-router-dom'
import AAA from './AAA'
import BBB from './BBB'
import CCC from './CCC'

export default class App extends Component {
constructor() {
super()
}
render() {
return (
<Router>
<div>
<Route path='/aaa' component={AAA} exact='true'></Route>
<Route path='/bbb' component={BBB}></Route>
<Route path='/ccc' component={CCC}></Route>
</div>
</Router >
)
}
}

render(<App />, window.root)

实现react-router

下面我们开始实现react-router,开始吧!

新建react-router-dom文件夹,新建index.js,用来导出所有的class。

index.js

1
2
3
4
5
6
import HashRouter from './HashRouter'
import Router from './Router'
import Link from './Link'
import Redirect from './Redirect'

export { HashRouter, Router, Link, Redirect }

context.js

1
2
3
import React from 'react'
let { Provider, Consumer } = React.createContext()
export { Provider, Consumer }

HashRouter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import React, { Component } from 'react'
import { Provider } from './context'

export default class HashRouter extends Component {
constructor() {
super()
this.state = {
location: {
pathname: window.location.hash.slice(1) || '/'
}
}
}
componentDidMount() {
window.location.hash = window.location.hash || '/'
// 监听hash变化重新设置状态
window.addEventListener('hashchange', () => {
this.setState({
location: {
...this.state.location,
pathname: window.location.hash.slice(1) || '/'
}
})
})
}
render() {
let value = {
location: this.state.location,
history: {
push(to) {
window.location.hash = to
}
}
}
return (
<Provider value={value}>
{this.props.children}
</Provider>
)
}
}

Router.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, { Component } from 'react'
import { Consumer } from './context'
import pathToReg from 'path-to-regexp'

export default class Router extends Component {
constructor() {
super()
}
render() {
return (
<Consumer>
{state => {
// path component是Route中传递的
let { path, component: Component, exact = false } = this.props
// pathname是location中的
let pathname = state.location.pathname
// 根据path实现正则,通过正则匹配,这里可以使用path-to-regexp第三方包
let reg = pathToReg(path, [], { end: exact }) //end为true是路由精确匹配
let result = pathname.match(reg)
if (result) {
return <Component></Component>
}
return null
}}
</Consumer>
)
}
}

Link.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { Component } from 'react'
import { Consumer } from './context'

export default class Link extends Component {
constructor() {
super()
}
render() {
s
return (
<Consumer>
{state => {
return <a onClick={() => {
const { to } = this.props //<Link to="home">首页</Link>
state.location.history.push(to)
}}>{this.props.children}</a>
}}
</Consumer>
)
}
}

Redirect.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { Component } from 'react'
import { Consumer } from './context'

export default class Redirect extends Component {
constructor() {
super()
}
render() {
return (
<Consumer>
{state => {
state.history.push(this.props.to)//重定向
return null
}}
</Consumer>
)
}
}

Switch.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, { Component } from 'react'
import { Consumer } from './context'
import pathToReg from 'path-to-regexp'

export default class Router extends Component {
constructor() {
super()
}
render() {
return (
<Consumer>
{state => {
let pathname = state.location.pathname
let children = this.props.children
for (let i = 0; i < children.length; i++) {
const child = children[i];
const path = child.props.path || ''
let reg = pathToReg(path, [], { end: 'false' })
if (reg.test(pathname)) {
return child
}
return null
}
}}
</Consumer>
)
}
}

下面可以使用我们自己编写的react-router进行测试了~

DOM diff算法

dom diff

根据两个虚拟dom创建出补丁, 描述改变的内容, 将这个补丁用来更新dom

dom diff几种优化策略

  1. 更新时只比较同级,并不会跨层比较
  2. 同层变化能复用,使用key

==index.js==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import {
createElement,
render,
renderDOM
} from './element'
import diff from './diff'
import patch from './patch'


let virtualDOM1 = createElement('ul', {
class: 'list'
}, [
createElement('li', {class: 'item'}, ['a']),
createElement('li', {class: 'item'}, ['b']),
createElement('li', {class: 'item'}, ['c'])
])
let virtualDOM2 = createElement('ul', {
class: 'list-group'
}, [
createElement('li', {class: 'item'}, ['1']),
createElement('li', {class: 'item'}, ['2']),
createElement('div', {class: 'item'}, ['c'])
])


let el = render(virtualDOM)
renderDOM(el, window.root)

// 给元素打补丁,重新更新视图
let patches = diff(virtualDOM1, virtualDOM2)
patch(el, patches)

此时的dom diff策略还存在很多问题:
1. 如果同级只是交换节点位置,会导致重新渲染(应该只是交换位置)
2. 新增节点也不会被更新

在index.js中,我们创建了两个虚拟dom,故意修改了一些属性值、标签名、文本,以测试后面要实现的diff、patch方法。

首先我们实现createElement(创建虚拟dom)、render(将虚拟dom转化为真实dom)、renderDOM(将元素节点插入到页面上)这几个方法。

==element.js==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 虚拟dom元素
class Element {
constructor(type, props, children) {
this.type = type
this.props = props
this.children = children
}
}

// 创建虚拟dom
function createElement(type, props, children) {
return new Element(type, props, children)
}

// 设置属性
function setAttr(node, key, value) {
switch (key) {
case 'value':
if (node.tagName.toLowerCase === 'input' || node.tagName.toLowerCase === 'textarea') {
node.value = value
} else {
node.setAttribute(key, value)
}
break
case 'style':
node.style.cssText = value
break
default:
node.setAttribute(key, value)
}
}

// 将vnode转化为真实dom
function render(eleObj) {
let el = document.createElement(eleObj.type)
for (let key in eleObj.props) {
// 设置属性的方法
setAttr(el, key, eleObj.props[key])
}
eleObj.children.forEach(child => {
child = (child instanceof Element) ? render(child) : document.createTextNode(child)
el.appendChild(child)
});

return el
}

// 将元素插入到页面内
function renderDOM(el, target) {
target.appendChild(el)
}


export {
createElement,
render,
Element,
renderDOM,
setAttr
}

注意:在设置元素属性时,因为不同类型的元素设置属性方法不同,因此采用setAttr函数统一设置。

目前为止,我们已经实现了创建虚拟dom,并将虚拟dom转化为真实dom渲染到页面中,接下来我们实现核心的diff算法:

首先我们需要制定规则:

  1. 当节点类型相同时,看属性是否相同,产生属性补丁包,{type;’ATTES’,attrs:{class:’list’}}
  2. 新的dom不存在,{type;’REMOVE’,index:xx}
  3. 节点类型不相同,直接替换,{type;’REPLACE’,newNode:newNode}
  4. 文本内容变化,{type;’TEXT’,text:’xxx’}
  5. ……

==diff.js==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
const ATTRS = 'ATTRS'
const REMOVE = 'REMOVE'
const REPLACE = 'REPLACE'
const TEXT = 'TEXT'

let Index = 0

// 深度遍历
function diff(oldTree, newTree) {
let patches = {}
let index = 0
walk(oldTree, newTree, index, patches)
return patches
}

function walk(oldNode, newNode, index, patches) {
let currentPatch = [] //每个元素都有一个补丁对象

if (!newNode) { // 2. 新节点被删除
currentPatch.push({
type: REMOVE,
index
})
} else if (_isString(oldNode) && _isString(newNode)) { // 4. 判断文本是否变化
if (oldNode !== newNode) {
currentPatch.push({
type: TEXT,
text: newNode
})
}
} else if (oldNode.nodeType === newNode.nodeType) { // 1. 比较属性是否有更改
let attrs = _diffAttr(oldNode.props, newNode.props)
if (Object.keys(attrs).length > 0) {
currentPatch.pusH({
TYPE: ATTRS,
attrs
})
}
// 如果有子节点,遍历子节点
_diffChildren(oldNode.children, newNode.children, index, patches)
} else { //3.节点被替换
currentPatch.push({
type: REPLACE,
newNode
})
}
if (currentPatch.length > 0) { //当前元素确实有补丁
// 将元素和补丁对应起来,放到大补丁包中
patches[index] = currentPatch
}

}

function _diffAttr(oldAttrs, newAttrs) {
let patch = {}

// 直接判断老属性和新属性关系
for (const key in oldAttrs) {
if (oldAttrs[key] !== newAttrs[key]) {
patch[key] = newAttrs[key] //有可能是undefined
}
}
// 老节点没有新节点的属性
for (const key in newAttrs) {
if (!oldAttrs.hasOwnProperty(key)) {
patch[key] = newAttrs[key]
}
}
return patch
}

function _isString(node) {
return Object.props.toString.call(node) === '[object String]'
}

function _diffChildren(oldChildren, newChildren, index, patches) {
oldChildren.forEach((child, idx) => {
// index每次传给walk时,index是递增的,定义全局变量Index,所有的基于同一序号实现
walk(child, newChildren[idx], ++Index, patches)
});
}

export default diff

通过diff方法,我们能对两个虚拟dom产生完整的patches对象(详细记录了更改信息),以便后续的更新操作。

接下来,我们实现patch方法,根据patches对象,完成真实dom的更新工作:

==patch.js==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import {
Element,
render,
setAttr
} from './element'

let allPatches
let index = 0

const ATTRS = 'ATTRS'
const REMOVE = 'REMOVE'
const REPLACE = 'REPLACE'
const TEXT = 'TEXT'

function patch(node, patches) {
// 给元素打补丁
allPatches = patches
walk(node)
}

function walk(node) {
let currentPatch = allPatches[index++]
let childNodes = node.childNodes
childNodes.forEach(child => {
walk(child)
});
if (currentPatch) {
doPatch(node, currentPatch)
}
}

function doPatch(node, patches) {
patches.forEach(patch => {
switch (patch.type) {
case ATTRS:
for (const key in patch.attrs) {
let value = patch.attrs[key]
if (value) {
setAttr(node, key, value)
} else {// 如果属性值为undefined则直接删除属性
node.removeAttribute(key)
}

}
break
case REMOVE:
node.parentNode.removeChild(node)
break
case REPLACE:
let newNode = patch.newNode instanceof Element ? render(patch.newNode) : document.createTextNode(patch.newNode)
node.parentNode.replaceChild(newNode, node)
break
case TEXT:
node.textContent = patch.text
break
}
});
}

export default patch

终于完成啦!可以愉快地使用index.js进行测试了~

但是此时的dom diff策略还存在很多问题:

  1. 如果同级只是交换节点位置,会导致重新渲染(应该只是交换位置)
  2. 新增节点也不会被更新
  3. ……

思考: 如何解决?

vue中mvvm深度解读

我们先实现一个小巧简单的mvvm框架吧~ Let’s do it!

==index.html==(script引入我们自己的mvvm框架)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>

<body>
<div id="app">
<!-- 双向数据绑定 -->
<input type="text" v-model='msg'> {{msg}}
</div>
</body>
<!-- <script src="node_modules/vue/dist/vue.min.js"></script> -->
<script src="./watcher.js"></script>
<script src="./observer.js"></script>
<script src="./compile.js"></script>
<script src="./mvvm.js"></script>
<script>
// mvvm如何实现?
// vue中实现双向绑定 1.模板编译 2.数据劫持 3.Watcher
let vm = new MVVM({
el: '#app', //el:document.getElementById('app')
data: {
msg: 'hello'
}
})
</script>

</html>

==mvvm.js==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class MVVM {
constructor(options) {
// 先把可用的东西挂载到实例上
this.$el = options.el
this.$data = options.data

// 如果有要编译的模板就开始编译
if (this.$el) {
// 数据劫持,把对象的所有属性改为get、set
new Observer(this.$data)
this.proxyData(this.$data)
// 用数据和元素进行编译
new Compile(this.$el, this)
}
}

// 代理数据,因为用户可能要通过this.msg取值,而不是this.$data.msg取值
proxyData(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
get() {
return data[key]
},
set(newVal) {
data[key] = newVal
}
})
})
}
}

==compile.js==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
class Compile {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
if (this.el) {
// 1. 先把真实的dom移入到内存,放到fragment
let fragment = this.node2Fragment(this.el)

// 2. 编译——提取想要的元素节点和文本节点 v-model {{}}
this.compile(fragment)

// 3. 把编译好的element放回页面
this.el.appendChild(fragment)
}
}

// 辅助方法

isElementNode(node) {
return node.nodeType === 1
}
isDirective(name) {
return name.includes('v-')
}

// 核心方法

// 将el元素内容全部放入内存
node2Fragment(el) {
let fragment = document.createDocumentFragment() //文档碎片
let firstChild
while (firstChild = el.firstChild) {
fragment.appendChild(firstChild)
}
return fragment
}

// 编译
compile(fragment) {
// childNodes拿不到嵌套子节点,需要使用递归
let childNodes = fragment.childNodes
Array.from(childNodes).forEach(node => {
// 元素节点
if (this.isElementNode(node)) {
this.compileElement(node)
this.compile(node) // 需要深入检查, 使用递归
} else {
// 文本节点
this.compileText(node)
}
})
}

// 编译元素 v-model、v-text等
compileElement(node) {
let attrs = node.attributes
Array.from(attrs).forEach(attr => {
// 判断属性名字是否包含v-
let attrName = attr.name
if (this.isDirective(attrName)) {
let expr = attr.value //expr是指令的值
// node this.vm.$data expr
//取到v-后面的名称,如v-model的model,v-text的text等等
// let type = attrName.slice(2)
let [, type] = attrName.split('-')
Compileutil[type](node, this.vm, expr)
}
})
}

// 编译文本,{{}}
compileText(node) {
let expr = node.textContent
let reg = /\{\{([^}]+)\}\}/g //匹配{{}}
if (reg.test(expr)) {
// node this.vm.$data expr
const type = 'text'
Compileutil[type](node, this.vm, expr)
}
}
}

Compileutil = {
// 获取实例上对应的数据,如msg.a.b=>'hello'
// msg.a.b=>this.$data.msg=>this.$data.msg.a=>this.$data.msg.a.b
getVal(vm, expr) {
expr = expr.split('.')
return expr.reduce((prev, next) => {
return prev[next]
}, vm.$data)
},
// 获取编译文本后的结果,如{{msg}}=>'hello'
getTextVal(vm, expr) {
return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
// arguments[1]是正则匹配括号内容,如{{msg}}的msg
return this.getVal(vm, arguments[1])
})
},
// 赋值
// 例如给msg.a.b赋新值,则取到最后再赋value值
setVal(vm, expr, value) {
expr = expr.split('.')

return expr.reduce((prev, next, curIndex) => {
if (curIndex === expr.length - 1) {
return prev[next] = value
}
}, vm.$data)
},
// 文本处理
text(node, vm, expr) {
let updateFn = this.update['textUpdater']

// 拿到{{a}}{{b}}的a、b
expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
new Watcher(vm, arguments[1], newVal => {
// 如果数据变化了, 文本节点需要重新获取依赖的数据来更新文本节点
updateFn && updateFn(node, this.getTextVal(vm, expr))
})
})

let value = this.getTextVal(vm, expr)
updateFn && updateFn(node, value)
},
// 输入框处理
model(node, vm, expr) {
let updateFn = this.update['modelUpdater']
// 这里应该加一个监控,数据变化时,应该调用watcher的callback,将新值传递过来
new Watcher(vm, expr, newVal => {
updateFn && updateFn(node, this.getVal(vm, expr))
})
updateFn && updateFn(node, this.getVal(vm, expr))
node.addEventListener('input', e => {
let newVal = e.target.value
this.setVal(vm, expr, newVal)
})
},
update: {
// 文本更新
textUpdater(node, value) {
node.textContent = value
},
// 输入框更新
modelUpdater(node, value) {
node.value = value
},
}
}

==observer.js==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Observer {
constructor(data) {
this.observe(data)
}

// 将data数据原有的属性改为get和set的形式
observe(data) {
if (!data || typeof data !== 'object') return
Object.keys(data).forEach(key => {
// 开始劫持
this.defineReactive(data, key, data[key])
// 如果劫持的是对象,还要对对象内的属性继续劫持
this.observe(data[key])
})
}

// 定义响应式
defineReactive(data, key, value) {
let _this = this
let dep = new Dep() //每个变化的数据都会对应一个数组,这个数组是存放所有更新的操作
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
Dep.target && dep.addSub(Dep.target)
return value
},
set(newValue) {
if (newValue !== value) {
// 设置新值时,如果是对象仍然需要劫持
_this.observe(newValue)
value = newValue
dep.notify() //通知所有订阅者数据变化了
}
}
})
}
}

==watcher.js==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 观察者,给需要变化的dom元素增加观察者
// 用新值和旧值进行比对,如果发生变化,执行对应的方法

class Watcher {
constructor(vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb

this.value = this.get()
}

// 获取实例上对应的数据,如msg.a.b=>'hello'
getVal(vm, expr) {
expr = expr.split('.')
return expr.reduce((prev, next) => {
return prev[next]
}, vm.$data)
}

get() {
Dep.target = this
let value = this.getVal(this.vm, this.expr)
Dep.target = null
return value
}

// 对外暴露的方法
update() {
let newVal = this.getVal(this.vm, this.expr)
let oldVal = this.value
if (newVal !== oldVal) {
this.cb(newVal)
}
}
}

// 发布订阅
class Dep {
constructor() {
// 订阅数组
this.subs = []
}

// 添加订阅
addSub(watcher) {
this.subs.push(watcher)
}

notify() {
this.subs.forEach(watcher => {
watcher.update()
})
}
}

测试html页面,可以成功实现mvvm,view和model已经完成了双向绑定。