├── .gitignore
├── .vscode
└── settings.json
├── README.md
├── appveyor.yml
├── package-lock.json
├── package.json
├── src
├── css
│ └── style.css
├── index.html
└── js
│ ├── index.js
│ └── mvvm.js
├── test.js
├── webpack.config.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | dist
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "git.ignoreLimitWarning": true
3 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://ci.appveyor.com/project/codeyu/mini-mvvm/branch/master)
2 |
3 | 实现:https://juejin.im/post/5abdd6f6f265da23793c4458
4 |
5 | Demo: http://codeyu.com/mini-mvvm/
6 |
7 | 本地运行步骤:
8 |
9 | 1. 命令行运行:
10 | ```
11 | git clone https://github.com/codeyu/mini-mvvm.git
12 | cd mini-mvvm
13 | npm install
14 | npm run dev
15 | ```
16 | 2. 浏览器访问:http://localhost:9000
17 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | init:
2 | - git config --global core.autocrlf false
3 | version: '{build}'
4 | environment:
5 | matrix:
6 | - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
7 | nodejs_version: "8"
8 | access_token:
9 | secure: gPLaCoISbGAJCbPE7vGAEkRMtiDH17XvyEzweZlp8pt6zjsOCTMZlmd1296tzgcz
10 | git_email:
11 | secure: dCEJcIf4jXWH0PrQcRaXuvvT8l7C5GwosxOsmi0c564=
12 | # Start builds on tags only (GitHub and BitBucket)
13 | # skip_non_tags: true
14 | pull_requests:
15 | do_not_increment_build_number: true
16 | branches:
17 | only:
18 | - master
19 |
20 | # Install scripts. (runs after repo cloning)
21 | install:
22 | # - copy C:\MinGW\bin\mingw32-make.exe C:\MinGW\bin\make.exe
23 | # - set PATH=%PATH%;C:\MinGW\bin
24 | # - ps: $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
25 | # - ps: [Environment]::SetEnvironmentVariable("Path", "$env:Path;c:\Users\Me\AppData\Roaming\npm\", "User")
26 | # Get the latest stable version of Node.js or io.js
27 | - ps: Install-Product node $env:nodejs_version
28 | # install modules
29 | - npm install
30 |
31 |
32 | build_script:
33 | # run webpack with production flag
34 | - npm run build
35 | test: off
36 | on_success:
37 | # - gh-pages deploy
38 | - git config --global credential.helper store
39 | - ps: Add-Content "$env:USERPROFILE\.git-credentials" "https://$($env:access_token):x-oauth-basic@github.com`n"
40 | - git config --global user.email "$($env:git_email)"
41 | - git config --global user.name "codeyu"
42 | - git config --global core.autocrlf true
43 | - git clone "https://github.com/codeyu/mini-mvvm.git" gh-pages
44 | - cd gh-pages
45 | - git checkout -b gh-pages
46 | - ps: if (-not $?) { throw "Cloning gh-pages failed" }
47 | - ps: Remove-Item * -Recurse
48 | - ps: Copy-Item ..\dist\* . -Recurse
49 | - git add --all .
50 | - git commit -m "update dist"
51 | - git push -f origin gh-pages
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mini-mvvm",
3 | "version": "1.0.0",
4 | "description": "mini mvvm framework",
5 | "repository": "https://github.com/codeyu/mini-mvvm",
6 | "main": "index.js",
7 | "scripts": {
8 | "dev": "webpack-dev-server --env development",
9 | "build": "webpack -p --colors",
10 | "test": "echo \"Error: no test specified\" && exit 1"
11 | },
12 | "keywords": [
13 | "mvvm",
14 | "javascript"
15 | ],
16 | "author": "codeyu",
17 | "license": "MIT",
18 | "devDependencies": {
19 | "babel-core": "^6.26.0",
20 | "babel-loader": "^7.1.4",
21 | "babel-preset-env": "^1.6.1",
22 | "css-loader": "^0.28.11",
23 | "eslint": "^4.19.1",
24 | "extract-text-webpack-plugin": "^4.0.0-beta.0",
25 | "html-webpack-plugin": "^3.2.0",
26 | "style-loader": "^0.20.3",
27 | "webpack": "^4.5.0",
28 | "webpack-cli": "^2.0.14",
29 | "webpack-dev-server": "^3.1.3"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/css/style.css:
--------------------------------------------------------------------------------
1 | .container{
2 | display: flex;
3 | flex-direction: column;
4 | }
5 | .container-line{
6 | display: flex;
7 | flex-direction: row;
8 | margin: 10px;
9 | }
10 | span{
11 | font-family: "Microsoft Yahei";
12 | font-weight: bold;
13 | }
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
{{song}}
9 |
10 |
11 | 《
12 | {{album.name}}》是
13 | {{singer}}2005年11月发行的专辑
14 |
15 |
16 | 主打歌为
17 | {{album.theme}}
18 |
19 |
20 |
21 | 作词人为
22 | {{singer}}等人。
23 |
24 |
25 | 为你弹奏肖邦的
26 | {{album.theme}}
27 |
28 |
29 |
30 | 歌手姓名:
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/js/index.js:
--------------------------------------------------------------------------------
1 | import Mvvm from './Mvvm'
2 | require('../css/style.css');
3 | let mvvm = new Mvvm({
4 | el: '#app',
5 | data: {
6 | song: '发如雪',
7 | album: {
8 | name: '十一月的萧邦',
9 | theme: '夜曲'
10 | },
11 | singer: '周杰伦'
12 | }
13 | });
--------------------------------------------------------------------------------
/src/js/mvvm.js:
--------------------------------------------------------------------------------
1 | console.log('start');
2 | // 创建一个Mvvm构造函数
3 | // 这里用es6方法将options赋一个初始值,防止没传,等同于options || {}
4 | function Mvvm(options = {}) {
5 | // vm.$options Vue上是将所有属性挂载到上面
6 | // 所以我们也同样实现,将所有属性挂载到了$options
7 | this.$options = options;
8 | // this._data 这里也和Vue一样
9 | let data = this._data = this.$options.data;
10 |
11 | // 数据劫持
12 | var dep = observe(data);
13 |
14 | // this 代理了this._data
15 | for (let key in data) {
16 | Object.defineProperty(this, key, {
17 | configurable: false,
18 | enumerable: true,
19 | get() {
20 | return this._data[key]; // 如this.a = {b: 1}
21 | },
22 | set(newVal) {
23 | this._data[key] = newVal;
24 | }
25 | });
26 | }
27 | // 初始化computed,将this指向实例
28 | // initComputed.call(this);
29 | // 编译
30 | new Compile(options.el, this);
31 | if (typeof options.mounted != 'undefined') {
32 | // 所有事情处理好后执行mounted钩子函数
33 | options.mounted.call(this); // 这就实现了mounted钩子函数
34 | }
35 |
36 | console.log('mounted');
37 | dep.notify();
38 | this.initMounted = true
39 | }
40 |
41 | function initComputed() {
42 | let vm = this;
43 | let computed = this.$options.computed; // 从options上拿到computed属性 {sum: ƒ, noop: ƒ}
44 | if (typeof computed != 'undefined') {
45 | // 得到的都是对象的key可以通过Object.keys转化为数组
46 | Object.keys(computed).forEach(key => { // key就是sum,noop
47 | Object.defineProperty(vm, key, {
48 | // 这里判断是computed里的key是对象还是函数
49 | // 如果是函数直接就会调get方法
50 | // 如果是对象的话,手动调一下get方法即可
51 | // 如: sum() {return this.a + this.b;},他们获取a和b的值就会调用get方法
52 | // 所以不需要new Watcher去监听变化了
53 | get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
54 | set() { }
55 | });
56 | });
57 | }
58 | }
59 |
60 | // 创建一个Observe构造函数
61 | // 写数据劫持的主要逻辑
62 | function Observe(data) {
63 | let dep = new Dep();
64 | // 所谓数据劫持就是给对象增加get,set
65 | // 先遍历一遍对象再说
66 | for (let key in data) { // 把data属性通过defineProperty的方式定义属性
67 | let val = data[key];
68 | observe(val); // 递归继续向下找,实现深度的数据劫持
69 | Object.defineProperty(data, key, {
70 | configurable: false,
71 | enumerable: true,
72 | get() {
73 | Dep.target && dep.addSub(Dep.target); // 将watcher添加到订阅事件中 [watcher]
74 | return val;
75 | },
76 | set(newVal) { // 更改值的时候
77 | console.log('set', newVal);
78 | if (val === newVal) { // 设置的值和以前值一样就不理它
79 | return;
80 | }
81 | val = newVal; // 如果以后再获取值(get)的时候,将刚才设置的值再返回去
82 | observe(newVal); // 当设置为新值后,也需要把新值再去定义成属性
83 | dep.notify(); // 让所有watcher的update方法执行即可
84 | }
85 | });
86 | }
87 |
88 | // dep.notify();
89 | console.log(dep);
90 | return dep;
91 | }
92 |
93 | // 外面再写一个函数
94 | // 不用每次调用都写个new
95 | // 也方便递归调用
96 | function observe(data) {
97 | // 如果不是对象的话就直接return掉
98 | // 防止递归溢出
99 | if (!data || typeof data !== 'object') return;
100 | return new Observe(data);
101 | }
102 | // 创建Compile构造函数
103 | function Compile(el, vm) {
104 | // 将el挂载到实例上方便调用
105 | vm.$el = document.querySelector(el);
106 | // 在el范围里将内容都拿到,当然不能一个一个的拿
107 | // 可以选择移到内存中去然后放入文档碎片中,节省开销
108 | let fragment = document.createDocumentFragment();
109 | let child;
110 | while (child = vm.$el.firstChild) {
111 | fragment.appendChild(child); // 此时将el中的内容放入内存中
112 | }
113 | // 对el里面的内容进行替换
114 | function replace(frag) {
115 | Array.from(frag.childNodes).forEach(node => {
116 | let txt = node.textContent;
117 | const reg = /\{\{\s*([^}]+\S)\s*\}\}/g; // 正则匹配{{}}
118 |
119 | if (node.nodeType === 3 && reg.test(txt)) { // 即是文本节点又有大括号的情况{{}}
120 | function replaceTxt() {
121 | node.textContent = txt.replace(reg, (matched, placeholder) => {
122 | console.log(placeholder); // 匹配到的分组 如:song, album.name, singer...
123 | vm.initMounted || new Watcher(vm, placeholder, replaceTxt); // 监听变化,进行匹配替换内容
124 |
125 | return placeholder.split('.').reduce((val, key) => {
126 | return val[key];
127 | }, vm);
128 | });
129 | };
130 | // 替换
131 | replaceTxt();
132 | }
133 | if (node.nodeType === 1) { // 元素节点
134 | let nodeAttr = node.attributes; // 获取dom上的所有属性,是个类数组
135 | Array.from(nodeAttr).forEach(attr => {
136 | let name = attr.name; // v-model type
137 | let exp = attr.value; // c text
138 | if (name.includes('v-')) {
139 | node.value = vm[exp]; // this.c 为 2
140 | }
141 | // 监听变化
142 | new Watcher(vm, exp, function (newVal) {
143 | node.value = newVal; // 当watcher触发时会自动将内容放进输入框中
144 | });
145 |
146 | node.addEventListener('input', e => {
147 | let newVal = e.target.value;
148 | // 相当于给this.c赋了一个新值
149 | // 而值的改变会调用set,set中又会调用notify,notify中调用watcher的update方法实现了更新
150 | vm[exp] = newVal;
151 | });
152 | });
153 | }
154 | // 如果还有子节点,继续递归replace
155 | if (node.childNodes && node.childNodes.length) {
156 | replace(node);
157 | }
158 | });
159 | }
160 |
161 | replace(fragment); // 替换内容
162 |
163 | vm.$el.appendChild(fragment); // 再将文档碎片放入el中
164 | }
165 | // 发布订阅模式 订阅和发布 如[fn1, fn2, fn3]
166 | function Dep() {
167 | // 一个数组(存放函数的事件池)
168 | this.subs = [];
169 | }
170 | Dep.prototype = {
171 | addSub(sub) {
172 | this.subs.push(sub);
173 | },
174 | notify() {
175 | // 绑定的方法,都有一个update方法
176 | this.subs.forEach(sub => sub.update());
177 | }
178 | };
179 | // 监听函数
180 | // 通过Watcher这个类创建的实例,都拥有update方法
181 | function Watcher(vm, exp, fn) {
182 | this.fn = fn; // 将fn放到实例上
183 | this.vm = vm;
184 | this.exp = exp;
185 | // 添加一个事件
186 | // 这里我们先定义一个属性
187 | Dep.target = this;
188 | let arr = exp.split('.');
189 | let val = vm;
190 | arr.forEach(key => { // 取值
191 | val = val[key]; // 获取到this.a.b,默认就会调用get方法
192 | });
193 | Dep.target = null;
194 | }
195 |
196 | Watcher.prototype.update = function () {
197 | console.log('update');
198 | // notify的时候值已经更改了
199 | // 再通过vm, exp来获取新的值
200 | let arr = this.exp.split('.');
201 | let val = this.vm;
202 | arr.forEach(key => {
203 | val = val[key]; // 通过get获取到新的值
204 | });
205 | this.fn(val); // 将每次拿到的新值去替换{{}}的内容即可
206 | };
207 |
208 | export default Mvvm
209 |
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | let obj = {};
3 | obj.singer = '周杰伦';
4 | Object.defineProperty(obj, 'music', {
5 | configurable: true, // 可以配置对象,删除属性
6 | // writable: true, // 可以修改对象
7 | enumerable: true, // 可以枚举
8 | // value: '七里香',
9 | // ☆ get,set设置时不能设置writable和value,它们代替了二者且是互斥的
10 | get: function () { // 获取obj.music的时候就会调用get方法
11 | return '发如雪';
12 | },
13 | set: function (val) { // obj.music = '听妈妈的话'
14 | console.log(val); // '听妈妈的话'
15 | }
16 | });
17 |
18 | console.log(obj); // {singer: '周杰伦', music: '七里香'}
19 |
20 | delete obj.music; // 如果想对obj里的属性进行删除,configurable要设为true
21 | console.log(obj); // 此时为 {singer: '周杰伦'}
22 |
23 | obj.music = '听妈妈的话'; // 如果想对obj的属性进行修改,writable要设为true
24 | console.log(obj); // {singer: '周杰伦', music: "听妈妈的话"}
25 |
26 | for (let key in obj) {
27 | // 默认情况下通过defineProperty定义的属性是不能被枚举(遍历)的
28 | // 需要设置enumerable为true才可以
29 | // 不然你是拿不到music这个属性的,你只能拿到singer
30 | console.log(key); // singer, music
31 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const HtmlwebpackPlugin = require('html-webpack-plugin')
2 | const webpack = require('webpack')
3 | const path = require('path')
4 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
5 | const outPath = path.resolve(__dirname,'dist')
6 | const entryPath = path.resolve('./src/js/index.js')
7 | module.exports = {
8 | mode: 'development',
9 | entry: entryPath,
10 | output: {
11 | path: outPath,
12 | filename: 'mini-mvvm.bundle.js'
13 | },
14 | module: {
15 | rules: [
16 | {
17 | test: /\.js$/,
18 | exclude: /(node_modules)|(bower_components)/,
19 | loader: 'babel-loader',
20 | options: {
21 | presets: ['env']
22 | }
23 | },
24 | {
25 | test: /\.css$/,
26 | use: ExtractTextPlugin.extract({
27 | fallback: 'style-loader',
28 | use: ['css-loader']
29 | })
30 | }
31 | ]
32 | },
33 | plugins: [
34 | new HtmlwebpackPlugin({
35 | title: 'mini-mvvm',
36 | template: './src/index.html'
37 | }),
38 | new ExtractTextPlugin("style.css"),
39 | new webpack.HotModuleReplacementPlugin(),
40 | ],
41 | devServer: {
42 | compress: true,
43 | watchContentBase: true,
44 | port: 9000,
45 | hot: true,
46 | inline: true
47 | }
48 | }
--------------------------------------------------------------------------------