├── .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 | [![Build status](https://ci.appveyor.com/api/projects/status/98qfxk1otlu7526v/branch/master?svg=true)](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 | } --------------------------------------------------------------------------------