├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .huskyrc ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── README.md ├── dist ├── mini-mvvm.js └── mini-vdom.js ├── lerna.json ├── package-lock.json ├── package.json ├── packages ├── mini-mvvm │ ├── .npmrc │ ├── README.md │ ├── __tests__ │ │ └── lifecycle.test.ts │ ├── index.html │ ├── jest.config.js │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── common │ │ │ ├── EventEmitter.ts │ │ │ ├── enums.ts │ │ │ └── utils.ts │ │ ├── core │ │ │ ├── BaseMVVM.ts │ │ │ └── MVVM.ts │ │ ├── dev.scss │ │ ├── dev.ts │ │ └── lib │ │ │ ├── Compile │ │ │ ├── AST.ts │ │ │ ├── Compile.ts │ │ │ ├── index.ts │ │ │ └── parsers │ │ │ │ ├── parseAttrs.ts │ │ │ │ ├── parseEvents.ts │ │ │ │ ├── parseFor.ts │ │ │ │ ├── parseIf.ts │ │ │ │ ├── parseModel.ts │ │ │ │ └── parseProps.ts │ │ │ ├── Dep.ts │ │ │ ├── ELifeCycle.ts │ │ │ ├── Observer.ts │ │ │ └── Watcher.ts │ └── tsconfig.json └── mini-vdom │ ├── .npmrc │ ├── README.md │ ├── __tests__ │ ├── h.test.ts │ ├── hook.test.ts │ └── patch.test.ts │ ├── index.html │ ├── jest.config.js │ ├── package.json │ ├── rollup.config.js │ ├── src │ ├── dev.scss │ ├── dev.ts │ ├── index.ts │ ├── lib │ │ ├── VNode.ts │ │ ├── h.ts │ │ ├── hooks.ts │ │ ├── modules │ │ │ ├── attrs.ts │ │ │ ├── events.ts │ │ │ └── props.ts │ │ └── patch.ts │ └── utils │ │ └── index.ts │ └── tsconfig.json └── scripts └── build.sh /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | __tests__ 5 | scripts 6 | *.d.ts 7 | *.html 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@nosaid/eslint-config-for-typescript', 3 | rules: { 4 | '@typescript-eslint/ban-types': 'off', 5 | '@typescript-eslint/explicit-module-boundary-types': 'off', 6 | 'no-case-declarations': 'off' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: ci 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js 14 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: 14 22 | - run: npm ci 23 | - run: npm run bootstrap 24 | - run: npm run build:lp 25 | - run: npm run lint 26 | - run: npm test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | .DS_Store 64 | 65 | # package-lock.json 66 | 67 | # 只保留顶部的 dist 68 | /*/**/dist/ 69 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "npm run lint" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npm.taobao.org 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "tabWidth": 4, 5 | "endOfLine": "auto", 6 | "printWidth": 120, 7 | "bracketSpacing": true, 8 | "arrowParens": "avoid", 9 | "overrides": [ 10 | { 11 | "files": ["*.yaml", "*.yml", "package.json"], 12 | "options": { 13 | "singleQuote": false, 14 | "tabWidth": 2 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10.16.0' 4 | install: 5 | - npm install 6 | - npm run bootstrap 7 | script: 8 | - npm run build:lp 9 | - npm run lint 10 | - npm test 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mini-mvvm 2 | 3 | [![npm](https://img.shields.io/npm/v/mini-mvvm?logo=npm&style=flat-square)](https://www.npmjs.com/package/mini-mvvm) 4 | [![file size](https://img.shields.io/github/size/shalldie/mini-mvvm/dist/mini-mvvm.js?style=flat-square)](https://www.npmjs.com/package/mini-mvvm) 5 | [![Build Status](https://img.shields.io/github/workflow/status/shalldie/mini-mvvm/ci?label=build&logo=github&style=flat-square)](https://github.com/shalldie/mini-mvvm/actions) 6 | 7 | A mini mvvm lib with [virtual dom - mini-vdom](https://github.com/shalldie/mini-mvvm/tree/master/packages/mini-vdom). 8 | 9 | 基于 [virtual dom - mini-vdom](https://github.com/shalldie/mini-mvvm/tree/master/packages/mini-vdom) 的轻量级 mvvm 库 >\_<#@! 10 | 11 | 适用于 ui 组件的构建依赖或小型项目,如果项目比较复杂,也许一个更加成熟的 mvvm 框架及其生态更适合你 🤠🤠 12 | 13 | ## Installation 14 | 15 | npm install mini-mvvm --save 16 | 17 | 包含了 `.d.ts` 文件,用起来毫无阻塞 >\_<#@! 18 | 19 | ## Live Example 20 | 21 | [MVVM - 功能演示](https://shalldie.github.io/demos/mini-mvvm/) 22 | 23 | ## Development && Production 24 | 25 | npm run dev:mini-mvvm 开发调试 26 | 27 | npm run build 生产构建 28 | 29 | ## Ability 30 | 31 | - [x] VNode 基于虚拟 dom: [virtual dom - mini-vdom](https://github.com/shalldie/mini-mvvm/tree/master/packages/mini-vdom) 32 | - [x] 数据监听 33 | - [x] `data`、`computed` 变动监听 34 | - [x] 数组方法监听 `push` | `pop` | `shift` | `unshift` | `splice` | `sort` | `reverse` 35 | - [x] `computed` 计算属性 36 | - [x] `文本节点` 数据绑定,可以是一段表达式 37 | - [x] `attribute` 数据绑定 38 | - [x] 支持绑定 data、computed,支持方法,可以是一段表达式 39 | - [x] 常用指令 40 | - [x] `m-model` 双向绑定。 支持 `input`、`textarea`、`select` 41 | - [x] `m-if` 条件渲染。条件支持 `data`、`computed`、一段表达式 42 | - [x] `m-for` 循环。`(item,index) in array`、`item in array` 43 | - [x] 事件绑定 44 | - [x] `@click` | `@mousedown` | `...` 。可以使用 `$event` 占位原生事件 45 | - [x] `watch` 数据监听,详见下方示例 46 | - [x] 声明方式 47 | - [x] api 方式 48 | - [x] 生命周期 49 | - [x] `created` 组件创建成功,可以使用 `this` 得到 MVVM 的实例 50 | - [x] `beforeMount` 将要被插入 dom 51 | - [x] `mounted` 组件被添加到 dom,可以使用 `this.$el` 获取根节点 dom 52 | - [x] `beforeUpdate` 组件将要更新 53 | - [x] `updated` 组件更新完毕 54 | 55 | ## Example 56 | 57 | ```ts 58 | import MVVM from 'mini-mvvm'; // es module, typescript 59 | // const MVVM from 'mini-mvvm'; // commonjs 60 | // const MVVM = window['MiniMvvm']; // window 61 | 62 | new MVVM({ 63 | // 挂载的目标节点的选择器 64 | // 如果没有 template,就用这个节点作为编译模板 65 | el: '#app', 66 | template: ` 67 |
68 |
{{ content }}
69 |
70 | `, 71 | // data 72 | data() { 73 | return { 74 | content: 'this is content.' 75 | }; 76 | }, 77 | computed: {}, // ...计算属性 78 | // ...hook,可以使用 this 79 | created() { 80 | // 使用api方式去watch 81 | this.$watch('key', (val, oldVal) => {}, { immediate: true }); 82 | }, 83 | mounted() {}, // ...hook,可以使用 this.$el 84 | methods: {}, // ...方法 85 | // ...数据监听 86 | watch: { 87 | // 声明方式1: 88 | watch1(val, oldVal) {}, 89 | // 声明方式2: 90 | watch2: { 91 | immediate: true, // 立即执行 92 | handler(val, oldVal) {} 93 | } 94 | } 95 | }); 96 | ``` 97 | 98 | ## Enjoy it! :D 99 | -------------------------------------------------------------------------------- /dist/mini-mvvm.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).MiniMvvm=e()}(this,function(){"use strict";function o(t){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function r(t,e){for(var n=0;nt.length)&&(e=t.length);for(var n=0,r=new Array(e);n=t.length?{done:!0}:{done:!1,value:t[r++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,a=!0,c=!1;return{s:function(){n=t[Symbol.iterator]()},n:function(){var t=n.next();return a=t.done,t},e:function(t){c=!0,i=t},f:function(){try{a||null==n.return||n.return()}finally{if(c)throw i}}}}var h,p,v,y,m,b=(function(t){function e(t,e){for(var n=0;nt.length)&&(e=t.length);for(var n=0,r=new Array(e);n=t.length?{done:!0}:{done:!1,value:t[n++]}},e:function(t){throw t},f:r}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var o,i=!0,a=!1;return{s:function(){e=t[Symbol.iterator]()},n:function(){var t=e.next();return i=t.done,t},e:function(t){a=!0,o=t},f:function(){try{i||null==e.return||e.return()}finally{if(a)throw o}}}}(0e.length)&&(t=e.length);for(var n=0,r=new Array(t);n=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,i=!0,c=!1;return{s:function(){n=e[Symbol.iterator]()},n:function(){var e=n.next();return i=e.done,e},e:function(e){c=!0,a=e},f:function(){try{i||null==n.return||n.return()}finally{if(c)throw a}}}}(0\_<#@! 10 | 11 | 适用于 ui 组件的构建依赖或小型项目,如果项目比较复杂,也许一个更加成熟的 mvvm 框架及其生态更适合你 🤠🤠 12 | 13 | ## Installation 14 | 15 | npm install mini-mvvm --save 16 | 17 | 包含了 `.d.ts` 文件,用起来毫无阻塞 >\_<#@! 18 | 19 | ## Live Example 20 | 21 | [MVVM - 功能演示](https://shalldie.github.io/demos/mini-mvvm/) 22 | 23 | ## Development && Production 24 | 25 | npm run dev:mini-mvvm 开发调试 26 | 27 | npm run build 生产构建 28 | 29 | ## Ability 30 | 31 | - [x] VNode 基于虚拟 dom: [virtual dom - mini-vdom](https://github.com/shalldie/mini-mvvm/tree/master/packages/mini-vdom) 32 | - [x] 数据监听 33 | - [x] `data`、`computed` 变动监听 34 | - [x] 数组方法监听 `push` | `pop` | `shift` | `unshift` | `splice` | `sort` | `reverse` 35 | - [x] `computed` 计算属性 36 | - [x] `文本节点` 数据绑定,可以是一段表达式 37 | - [x] `attribute` 数据绑定 38 | - [x] 支持绑定 data、computed,支持方法,可以是一段表达式 39 | - [x] 常用指令 40 | - [x] `m-model` 双向绑定。 支持 `input`、`textarea`、`select` 41 | - [x] `m-if` 条件渲染。条件支持 `data`、`computed`、一段表达式 42 | - [x] `m-for` 循环。`(item,index) in array`、`item in array` 43 | - [x] 事件绑定 44 | - [x] `@click` | `@mousedown` | `...` 。可以使用 `$event` 占位原生事件 45 | - [x] `watch` 数据监听,详见下方示例 46 | - [x] 声明方式 47 | - [x] api 方式 48 | - [x] 生命周期 49 | - [x] `created` 组件创建成功,可以使用 `this` 得到 MVVM 的实例 50 | - [x] `beforeMount` 将要被插入 dom 51 | - [x] `mounted` 组件被添加到 dom,可以使用 `this.$el` 获取根节点 dom 52 | - [x] `beforeUpdate` 组件将要更新 53 | - [x] `updated` 组件更新完毕 54 | 55 | ## Example 56 | 57 | ```ts 58 | import MVVM from 'mini-mvvm'; // es module, typescript 59 | // const MVVM from 'mini-mvvm'; // commonjs 60 | // const MVVM = window['MiniMvvm']; // window 61 | 62 | new MVVM({ 63 | // 挂载的目标节点的选择器 64 | // 如果没有 template,就用这个节点作为编译模板 65 | el: '#app', 66 | template: ` 67 |
68 |
{{ content }}
69 |
70 | `, 71 | // data 72 | data() { 73 | return { 74 | content: 'this is content.' 75 | }; 76 | }, 77 | computed: {}, // ...计算属性 78 | // ...hook,可以使用 this 79 | created() { 80 | // 使用api方式去watch 81 | this.$watch('key', (val, oldVal) => {}, { immediate: true }); 82 | }, 83 | mounted() {}, // ...hook,可以使用 this.$el 84 | methods: {}, // ...方法 85 | // ...数据监听 86 | watch: { 87 | // 声明方式1: 88 | watch1(val, oldVal) {}, 89 | // 声明方式2: 90 | watch2: { 91 | immediate: true, // 立即执行 92 | handler(val, oldVal) {} 93 | } 94 | } 95 | }); 96 | ``` 97 | 98 | ## Enjoy it! :D 99 | -------------------------------------------------------------------------------- /packages/mini-mvvm/__tests__/lifecycle.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 生命周期 3 | * @jest-environment jsdom 4 | */ 5 | import MVVM from '../src/core/MVVM'; 6 | 7 | describe('life cycle', () => { 8 | beforeEach(() => { 9 | document.body.innerHTML = '
'; 10 | }); 11 | 12 | test('执行顺序 与 执行时机: created、mounted、beforeUpdate、updated', async () => { 13 | const mockFn = jest.fn(); 14 | 15 | const vm = new MVVM({ 16 | $el: '#app', 17 | template: ` 18 |
19 | {{ name }} 20 |
`, 21 | data() { 22 | return { 23 | name: 'tom' 24 | }; 25 | }, 26 | created() { 27 | expect(this.$el).toBeUndefined(); 28 | }, 29 | mounted() { 30 | expect(!!this.$el).toBeTruthy(); 31 | // console.log(this.$el); 32 | expect(this.$el.parentNode).toBe(document.body); 33 | }, 34 | beforeUpdate() { 35 | expect(this.$el.textContent.trim()).toBe('tom'); 36 | }, 37 | updated() { 38 | expect(this.$el.textContent.trim()).toBe('lily'); 39 | mockFn(); 40 | } 41 | }); 42 | 43 | // 等待 mounted 44 | await MVVM.nextTick(); 45 | expect(mockFn).toBeCalledTimes(0); 46 | 47 | vm['name'] = 'lily'; 48 | 49 | await MVVM.nextTick(); 50 | expect(mockFn).toBeCalledTimes(1); 51 | }); 52 | 53 | test('多次改变数据,只会触发一次 rerender', async () => { 54 | const mockFn = jest.fn(); 55 | 56 | const vm = new MVVM({ 57 | $el: '#app', 58 | template: ` 59 |
60 | {{ name }} 61 |
`, 62 | data() { 63 | return { 64 | name: 'tom' 65 | }; 66 | }, 67 | updated() { 68 | mockFn(); 69 | } 70 | }); 71 | 72 | // 先等待 mounted 73 | await MVVM.nextTick(); 74 | 75 | // 只有 updated 的时候才会 rerender 76 | expect(mockFn).toBeCalledTimes(0); 77 | 78 | for (let i = 0; i < 10; i++) { 79 | vm['name'] = i; 80 | } 81 | 82 | // 只有 nextTick 才会更新 83 | expect(mockFn).toBeCalledTimes(0); 84 | await MVVM.nextTick(); 85 | // 同一个 tick 改变多次数据,只会更新一次 86 | expect(mockFn).toBeCalledTimes(1); 87 | 88 | vm['name'] = 'tom'; 89 | await MVVM.nextTick(); 90 | expect(mockFn).toBeCalledTimes(2); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /packages/mini-mvvm/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | mini-mvvm - A mini lib to achieve mvvm. 一个轻量级的mvvm库。 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/mini-mvvm/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | verbose: true, 4 | testEnvironment: 'node' 5 | }; 6 | -------------------------------------------------------------------------------- /packages/mini-mvvm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-mvvm", 3 | "version": "0.0.10", 4 | "description": "A mini lib to achieve mvvm. 一个轻量级的mvvm库。", 5 | "keywords": [ 6 | "mvvm", 7 | "mini", 8 | "vnode", 9 | "vdom" 10 | ], 11 | "author": "shalldie ", 12 | "homepage": "https://github.com/shalldie/mvvm", 13 | "license": "MIT", 14 | "main": "dist/mini-mvvm.js", 15 | "types": "dist/core/MVVM.d.ts", 16 | "directories": { 17 | "test": "__tests__" 18 | }, 19 | "files": [ 20 | "dist" 21 | ], 22 | "publishConfig": { 23 | "registry": "https://registry.npmjs.org" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/shalldie/mvvm.git" 28 | }, 29 | "scripts": { 30 | "test": "../../node_modules/.bin/jest", 31 | "dev": "../../node_modules/.bin/cross-env NODE_ENV=development ../../node_modules/.bin/rollup -c -w", 32 | "build": "../../node_modules/.bin/cross-env NODE_ENV=production ../../node_modules/.bin/rollup -c" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/shalldie/mvvm/issues" 36 | }, 37 | "dependencies": { 38 | "mini-vdom": "^0.0.13" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/mini-mvvm/rollup.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | const rollupGenerator = require('../../node_modules/@nosaid/rollup').rollupGenerator; 3 | 4 | const ifProduction = process.env.NODE_ENV === 'production'; 5 | 6 | export default rollupGenerator([ 7 | { 8 | input: ifProduction ? 'src/core/MVVM.ts' : 'src/dev.ts', 9 | output: { 10 | file: 'dist/mini-mvvm.js', 11 | format: 'umd', 12 | name: 'MiniMvvm' 13 | }, 14 | uglify: ifProduction, 15 | serve: !ifProduction 16 | ? { 17 | open: true 18 | } 19 | : null 20 | } 21 | ]); 22 | -------------------------------------------------------------------------------- /packages/mini-mvvm/src/common/EventEmitter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 存放回调的字典 3 | */ 4 | type Subscription = { 5 | [key: string]: Array<{ type: ESubscribeType; listener: Function }>; 6 | }; 7 | 8 | /** 9 | * 订阅类型 10 | * 11 | * @enum {number} 12 | */ 13 | enum ESubscribeType { 14 | /** 15 | * 常规 16 | */ 17 | normal, 18 | /** 19 | * 仅执行一次久删除 20 | */ 21 | once 22 | } 23 | 24 | /** 25 | * pub/sub 类 26 | * 27 | * @export 28 | * @class EventEmitter 29 | */ 30 | export default class EventEmitter { 31 | private subscription: Subscription = {}; 32 | 33 | /** 34 | * 添加事件监听 35 | * 36 | * @param {string} event 事件名 37 | * @param {Function} listener 监听器 38 | * @param {ESubscribeType} [type=ESubscribeType.normal] 监听类型 39 | * @memberof EventEmitter 40 | */ 41 | public $on(event: string, listener: Function, type: ESubscribeType = ESubscribeType.normal): void { 42 | this.subscription[event] = this.subscription[event] || []; 43 | this.subscription[event].push({ 44 | type, 45 | listener 46 | }); 47 | } 48 | 49 | /** 50 | * 添加事件监听,执行一次就删除 51 | * 52 | * @param {string} event 事件名 53 | * @param {Function} listener 监听器 54 | * @memberof EventEmitter 55 | */ 56 | public $once(event: string, listener: Function): void { 57 | this.$on(event, listener, ESubscribeType.once); 58 | } 59 | 60 | /** 61 | * 解除事件绑定 62 | * 63 | * @param {string} event 事件名 64 | * @param {Function} listener 监听器 65 | * @memberof EventEmitter 66 | */ 67 | public $off(event: string, listener: Function): void { 68 | const subscriptions = this.subscription[event] || []; 69 | const index = subscriptions.findIndex(item => item.listener === listener); 70 | if (index >= 0) { 71 | subscriptions.splice(index, 1); 72 | } 73 | } 74 | 75 | /** 76 | * 触发事件 77 | * 78 | * @param {string} event 事件名 79 | * @param {...any[]} args 参数 80 | * @memberof EventEmitter 81 | */ 82 | public $emit(event: string, ...args: any[]): void { 83 | const subscriptions = this.subscription[event] || []; 84 | 85 | // 不缓存length是因为length会更改 86 | for (let i = 0; i < subscriptions.length; i++) { 87 | const item = subscriptions[i]; 88 | item.listener(...args); 89 | 90 | // 常规回调 91 | if (item.type === ESubscribeType.normal) { 92 | continue; 93 | } 94 | // 仅执行一次的 95 | if (item.type === ESubscribeType.once) { 96 | subscriptions.splice(i, 1); 97 | i--; 98 | } 99 | } 100 | } 101 | 102 | // eslint-disable-next-line 103 | public $listeners(event: string) { 104 | return this.subscription[event] || []; 105 | } 106 | 107 | /** 108 | * 获取所有监听的事件名 109 | * 110 | * @readonly 111 | * @type {string[]} 112 | * @memberof EventEmitter 113 | */ 114 | public get $events(): string[] { 115 | return Object.keys(this.subscription); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /packages/mini-mvvm/src/common/enums.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 节点类型 3 | * 4 | * @export 5 | * @enum {number} 6 | */ 7 | export enum ENodeType { 8 | /** 9 | * 元素节点 10 | */ 11 | Element = 1, 12 | 13 | /** 14 | * 文本节点 15 | */ 16 | Text = 3, 17 | 18 | /** 19 | * 注释节点 20 | */ 21 | Comment = 8, 22 | 23 | /** 24 | * fragment 容器 25 | */ 26 | DocumentFragment = 11 27 | } 28 | -------------------------------------------------------------------------------- /packages/mini-mvvm/src/common/utils.ts: -------------------------------------------------------------------------------- 1 | import MVVM from '../core/MVVM'; 2 | 3 | /** 4 | * 工具库 5 | */ 6 | 7 | /** 8 | * microTask 要做的事情 9 | * 10 | * @export 11 | * @param {() => void} [fn=() => {}] 12 | * @returns {Promise} 13 | */ 14 | // eslint-disable-next-line 15 | export function nextTick(fn: () => void = () => {}): Promise { 16 | return Promise.resolve().then(fn); 17 | } 18 | 19 | /** 20 | * 获取数据类型 21 | * 22 | * @export 23 | * @param {*} sender 要判断的数据 24 | * @returns {string} 25 | */ 26 | export function getType(sender: any): string { 27 | return Object.prototype.toString 28 | .call(sender) 29 | .toLowerCase() 30 | .match(/\s(\S+?)\]/)[1]; 31 | } 32 | 33 | /** 34 | * each 35 | * 36 | * @export 37 | * @param {Object} [data={}] 38 | * @param {(value: any, key: string) => void} fn 39 | */ 40 | export function each(data: Record = {}, fn: (value: any, key: string) => void): void { 41 | for (const key in data) { 42 | fn(data[key], key); 43 | } 44 | } 45 | 46 | /** 47 | * 获取唯一 number key 48 | * 49 | * @export 50 | * @returns {number} 51 | */ 52 | // eslint-disable-next-line 53 | export const nextIndex = (function () { 54 | let baseIndex = 0x5942b; 55 | return (): number => baseIndex++; 56 | })(); 57 | 58 | /** 59 | * 转化成数组 60 | * 61 | * @export 62 | * @template T 63 | * @param {*} arrayLike 64 | * @returns {T[]} 65 | */ 66 | export function toArray(arrayLike: any): T[] { 67 | return [].slice.call(arrayLike); 68 | } 69 | 70 | /** 71 | * 根据路径从 vm 中获取值 72 | * 73 | * @export 74 | * @param {MVVM} vm 75 | * @param {string} path 76 | * @returns 77 | */ 78 | export function getValByPath(vm: MVVM, path: string): any { 79 | const pathArr = path.split('.'); 80 | let val: any = vm; 81 | for (const key of pathArr) { 82 | val = val[key]; 83 | } 84 | return val; 85 | } 86 | -------------------------------------------------------------------------------- /packages/mini-mvvm/src/core/BaseMVVM.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 声明初始化参数 3 | * 导出 MVVM 的基类,把 fields 和 static methods 拆出来 4 | */ 5 | 6 | import { h, VNode, patch } from 'mini-vdom'; 7 | import EventEmitter from '../common/EventEmitter'; 8 | import { nextTick } from '../common/utils'; 9 | import Watcher, { TWatchDefine } from '../lib/Watcher'; 10 | import { ILifeCycle } from '../lib/ELifeCycle'; 11 | 12 | export interface IMvvmOptions extends ILifeCycle { 13 | /** 14 | * 模板的选择器 15 | * el 用来从dom获取template,vm实例会挂载到这里 16 | * 17 | * @type {string} 18 | * @memberof IMvvmOptions 19 | */ 20 | $el?: string; 21 | 22 | /** 23 | * 模板 24 | * 模板字符串,用来生成render函数 25 | * 26 | * @type {string} 27 | * @memberof IMvvmOptions 28 | */ 29 | template?: string; 30 | 31 | /** 32 | * render 函数 33 | * 用来生成 vnode 34 | * 35 | * @memberof IMvvmOptions 36 | */ 37 | render?: (createElement: typeof h) => void; 38 | 39 | /** 40 | * 当前组件的数据 41 | * 42 | * @memberof IMvvmOptions 43 | */ 44 | data?: () => Record; 45 | 46 | /** 47 | * 计算属性 48 | * 49 | * @memberof IMvvmOptions 50 | */ 51 | computed?: Record any>; 52 | 53 | /** 54 | * 方法 55 | * 56 | * @type {Record} 57 | * @memberof IMvvmOptions 58 | */ 59 | methods?: Record; 60 | 61 | /** 62 | * 数据监听 63 | * 64 | * @type {Record} 65 | * @memberof IMvvmOptions 66 | */ 67 | watch?: Record; 68 | } 69 | 70 | export default abstract class BaseMVVM extends EventEmitter { 71 | /** 72 | * 当前 data 73 | * 74 | * @protected 75 | * @memberof BaseMVVM 76 | */ 77 | protected _data = {}; 78 | 79 | /** 80 | * 对 _data 的一个代理,当前的 data 81 | * 唯一目的是对齐 vue 的 api 吧... 82 | * 83 | * @memberof BaseMVVM 84 | */ 85 | public $data = {}; 86 | 87 | /** 88 | * 当前组件的 computed watchers 89 | * 90 | * @protected 91 | * @type {Record} 92 | * @memberof BaseMVVM 93 | */ 94 | protected _computedWatchers: Record = {}; 95 | 96 | /** 97 | * 当前的 component watcher 98 | * 99 | * @protected 100 | * @type {Watcher} 101 | * @memberof BaseMVVM 102 | */ 103 | protected _watcher: Watcher; 104 | 105 | /** 106 | * 当前的 watch watchers 107 | * 108 | * @type {Watcher[]} 109 | * @memberof BaseMVVM 110 | */ 111 | public _watchers: Watcher[] = []; 112 | 113 | /** 114 | * 旧的 vnode,可能是dom或者vnode 115 | * 116 | * @protected 117 | * @type {*} 118 | * @memberof BaseMVVM 119 | */ 120 | protected lastVnode: any; 121 | 122 | /** 123 | * 组件对应的 vnode 124 | * 125 | * @protected 126 | * @type {VNode} 127 | * @memberof BaseMVVM 128 | */ 129 | protected vnode: VNode; 130 | 131 | /** 132 | * 初始化配置参数信息 133 | * 134 | * @type {IMvvmOptions} 135 | * @memberof BaseMVVM 136 | */ 137 | public $options: IMvvmOptions; 138 | 139 | /** 140 | * 监听某个 key 的改变 141 | * 142 | * @memberof BaseMVVM 143 | */ 144 | public $watch: (exp: string, callback: (val: any, oldVal: any) => void, options?: { immediate: boolean }) => void; 145 | 146 | /** 147 | * 当前组件挂载的dom 148 | * 149 | * @type {HTMLElement} 150 | * @memberof BaseMVVM 151 | */ 152 | public $el: HTMLElement; 153 | 154 | public static nextTick = nextTick; 155 | 156 | public $nextTick = nextTick; 157 | 158 | public static h = h; 159 | 160 | public static VNode = VNode; 161 | 162 | public static patch = patch; 163 | } 164 | -------------------------------------------------------------------------------- /packages/mini-mvvm/src/core/MVVM.ts: -------------------------------------------------------------------------------- 1 | import { patch, h } from 'mini-vdom'; 2 | import BaseMVVM, { IMvvmOptions } from './BaseMVVM'; 3 | import Compile from '../lib/Compile'; 4 | import Observer, { proxy } from '../lib/Observer'; 5 | import Dep from '../lib/Dep'; 6 | import Watcher, { defineComputed, defineWatch } from '../lib/Watcher'; 7 | import { nextTick } from '../common/utils'; 8 | import ELifeCycle, { defineLifeCycle } from '../lib/ELifeCycle'; 9 | 10 | export default class MVVM extends BaseMVVM { 11 | constructor(options: IMvvmOptions = {}) { 12 | super(); 13 | this.$options = options; 14 | 15 | this._init(); 16 | } 17 | 18 | /** 19 | * 初始化 20 | * 21 | * @private 22 | * @memberof MVVM 23 | */ 24 | private _init(): void { 25 | // 注册生命周期钩子 26 | defineLifeCycle(this); 27 | 28 | // 初始化methods,这个要放前面,因为其他地方在初始化的是可能会用到 29 | this._initMethods(); 30 | 31 | // 初始化数据 32 | this._initData(); 33 | 34 | // 初始化computed 35 | this._initComputed(); 36 | 37 | // 初始化watch 38 | this._initWatch(); 39 | 40 | // 准备完毕就调用 created 41 | this.$emit(ELifeCycle.created); 42 | 43 | // 编译 44 | this._compile(); 45 | 46 | // patch 47 | this._update(); 48 | } 49 | 50 | /** 51 | * 把模板编译成 render 函数 52 | * 53 | * @private 54 | * @memberof MVVM 55 | */ 56 | private _compile(): void { 57 | const { $el, template } = this.$options; 58 | if (!this.$options.render && (template || $el)) { 59 | this.$options.render = Compile.render(template || document.querySelector($el).outerHTML) as any; 60 | } 61 | } 62 | 63 | /** 64 | * 初始化 data 65 | * 66 | * @private 67 | * @memberof MVVM 68 | */ 69 | private _initData(): void { 70 | if (this.$options.data) { 71 | this._data = this.$options.data.call(this); 72 | new Observer(this._data); 73 | proxy(this._data, this); 74 | proxy(this._data, this.$data); 75 | } 76 | } 77 | 78 | /** 79 | * 初始化 computed 80 | * 81 | * @private 82 | * @memberof MVVM 83 | */ 84 | private _initComputed(): void { 85 | this._computedWatchers = defineComputed(this, this.$options.computed); 86 | } 87 | 88 | /** 89 | * 初始化 methods 90 | * 91 | * @private 92 | * @memberof MVVM 93 | */ 94 | private _initMethods(): void { 95 | Object.keys(this.$options.methods || {}).forEach(key => { 96 | this[key] = this.$options.methods[key].bind(this); 97 | }); 98 | } 99 | 100 | /** 101 | * 初始化 watch 102 | * 103 | * @private 104 | * @memberof MVVM 105 | */ 106 | private _initWatch(): void { 107 | defineWatch(this, this.$options.watch); 108 | } 109 | 110 | /** 111 | * 更新当前视图 112 | * 113 | * @memberof MVVM 114 | */ 115 | // eslint-disable-next-line 116 | public _update = (() => { 117 | let needUpdate = false; 118 | // eslint-disable-next-line 119 | return () => { 120 | needUpdate = true; 121 | 122 | nextTick(() => { 123 | if (!needUpdate) { 124 | return; 125 | } 126 | 127 | if (!this.$options.$el) { 128 | return; 129 | } 130 | 131 | let firstPatch = false; 132 | if (!this.$el) { 133 | this.$el = document.querySelector(this.$options.$el); 134 | firstPatch = true; 135 | } 136 | 137 | // nextTickQueue(() => { 138 | this.lastVnode = this.vnode || this.$el; 139 | 140 | this._watcher && this._watcher.clear(); 141 | Dep.target = this._watcher = new Watcher(this); 142 | this.vnode = this.$options.render.call(this, h); 143 | Dep.target = null; 144 | 145 | // 如果是初次patch,即用 vnode 替换 dom 146 | // 触发 beforeMount 147 | if (firstPatch) { 148 | this.$emit(ELifeCycle.beforeMount); 149 | } else { 150 | this.$emit(ELifeCycle.beforeUpdate); 151 | } 152 | 153 | patch(this.lastVnode, this.vnode); 154 | 155 | this.$el = this.vnode.elm as HTMLElement; 156 | 157 | needUpdate = false; 158 | 159 | // 如果是初次patch,即用 vnode 替换 dom 160 | // 触发 mounted 161 | if (firstPatch) { 162 | this.$emit(ELifeCycle.mounted); 163 | } else { 164 | this.$emit(ELifeCycle.updated); 165 | } 166 | }); 167 | }; 168 | })(); 169 | 170 | /** 171 | * 挂载到 dom 172 | * 173 | * @param {string} selector 174 | * @returns 175 | * @memberof MVVM 176 | */ 177 | public $mount(selector: string): this { 178 | this.$options.$el = selector; 179 | this._update(); 180 | return this; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /packages/mini-mvvm/src/dev.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font: 14px tahoma, arial, Hiragino Sans GB, \\5b8b\4f53, sans-serif; 6 | } 7 | 8 | #root { 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | color: #2c3e50; 12 | margin: 60px auto; 13 | padding: 20px; 14 | width: 500px; 15 | box-sizing: border-box; 16 | box-shadow: 0 3px 16px 4px #ddd; 17 | 18 | > h2 { 19 | margin: 0 0 10px; 20 | padding: 0 0 12px; 21 | text-align: center; 22 | border-bottom: 1px solid #ddd; 23 | 24 | a { 25 | color: #2ad; 26 | } 27 | } 28 | 29 | button { 30 | color: #fff; 31 | background-color: #5cb85c; 32 | border-color: #4cae4c; 33 | padding: 6px 12px; 34 | border-radius: 3px; 35 | margin-left: 20px; 36 | cursor: pointer; 37 | transition: 0.3s; 38 | font-weight: 700; 39 | border: 1px solid #ddd; 40 | height: 30px; 41 | outline: none; 42 | box-sizing: border-box; 43 | vertical-align: top; 44 | 45 | &:hover { 46 | background-color: #449d44; 47 | border-color: #398439; 48 | } 49 | 50 | &:active { 51 | background-color: #398439; 52 | border-color: #255625; 53 | } 54 | } 55 | 56 | .input-box { 57 | box-sizing: border-box; 58 | padding: 6px 0; 59 | display: flex; 60 | 61 | input { 62 | flex: 1; 63 | transition: 0.3s; 64 | padding: 0 6px; 65 | border-radius: 3px; 66 | border: 1px solid #ddd; 67 | height: 30px; 68 | outline: none; 69 | box-sizing: border-box; 70 | 71 | &:focus { 72 | border-color: transparent; 73 | box-shadow: 0 0 6px 1px #4cae4c; 74 | } 75 | } 76 | } 77 | 78 | .list-tab { 79 | display: flex; 80 | border-bottom: 1px dashed #ddd; 81 | // padding-right: 60px; 82 | 83 | .tab { 84 | flex: 1; 85 | cursor: pointer; 86 | color: #999; 87 | font-size: 16px; 88 | line-height: 36px; 89 | padding-left: 6px; 90 | text-align: center; 91 | 92 | + .tab { 93 | border-left: 1px dashed #ddd; 94 | } 95 | 96 | &.active { 97 | color: #2ad; 98 | } 99 | } 100 | } 101 | 102 | .item-list { 103 | margin: 0; 104 | padding: 0; 105 | > li { 106 | display: flex; 107 | margin: 0; 108 | .text { 109 | margin: 0; 110 | padding: 0; 111 | height: 40px; 112 | line-height: 40px; 113 | padding: 0 6px; 114 | list-style: none; 115 | border-bottom: 1px dashed #ddd; 116 | cursor: pointer; 117 | flex: 1; 118 | } 119 | .del { 120 | color: #2ad; 121 | width: 60px; 122 | line-height: 40px; 123 | text-align: center; 124 | border: 1px dashed #ddd; 125 | border-width: 0 0 1px 1px; 126 | cursor: pointer; 127 | } 128 | 129 | &.done { 130 | .text { 131 | text-decoration: line-through; 132 | color: #f00; 133 | } 134 | } 135 | } 136 | } 137 | } 138 | 139 | #root { 140 | width: 680px; 141 | .list { 142 | margin: 0; 143 | padding: 0; 144 | .list-item { 145 | margin-bottom: 10px; 146 | list-style-type: none; 147 | padding: 0 60px; 148 | 149 | .label { 150 | display: inline-block; 151 | width: 60px; 152 | height: 30px; 153 | line-height: 30px; 154 | } 155 | 156 | input[type='text'], 157 | input[type='number'] { 158 | flex: 1; 159 | transition: 0.3s; 160 | padding: 0 6px; 161 | border-radius: 3px; 162 | border: 1px solid #ddd; 163 | height: 30px; 164 | outline: none; 165 | box-sizing: border-box; 166 | 167 | &:focus { 168 | border-color: transparent; 169 | box-shadow: 0 0 6px 1px #4cae4c; 170 | } 171 | } 172 | } 173 | } 174 | 175 | .for-table { 176 | border-collapse: collapse; 177 | 178 | td { 179 | border: 1px solid #2ad; 180 | cursor: pointer; 181 | padding: 2px; 182 | &:hover { 183 | background: #2ad; 184 | color: #fff; 185 | } 186 | } 187 | } 188 | } 189 | 190 | #root { 191 | .tab-page { 192 | .page-item { 193 | label { 194 | cursor: pointer; 195 | } 196 | &.todo-list { 197 | width: 400px; 198 | margin: 20px auto 0; 199 | border: 1px solid #ddd; 200 | padding: 20px; 201 | border-width: 0 1px; 202 | } 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /packages/mini-mvvm/src/dev.ts: -------------------------------------------------------------------------------- 1 | /*eslint-disable*/ 2 | import './dev.scss'; 3 | import MVVM from './core/MVVM'; 4 | // import { h } from 'mini-vdom'; 5 | 6 | const CACHE_KEY = '__mini-mvvm_cache_key__'; 7 | 8 | const vm = new MVVM({ 9 | $el: '#app', 10 | template: ` 11 |
12 |

13 | mini-mvvm 14 | - 功能演示 15 | 16 |

17 |
18 |
{{ item }}
23 |
24 |
25 |
26 |

双向绑定(m-model):

27 |
    28 |
  • 29 | 姓名: 30 | 31 |
  • 32 |
  • 33 | 年龄: 34 | 35 |
  • 36 |
  • 37 | 性别: 38 | 42 |
  • 43 |
  • 44 |

    姓名:{{ person.name }},年龄:{{ person.age }},性别:{{ person.sex }}

    45 |
  • 46 |
47 |
48 |
49 |

计算属性(computed):

50 |
    51 |
  • 52 |

    {{ computedDescription }}

    53 |
  • 54 |
55 |
56 |
57 |

条件渲染(m-if)

58 |
    59 |
  • 60 | 64 |
  • 65 |
  • 66 | 噼里啪啦噼里啪啦噼里啪啦噼里啪啦噼里啪啦 67 |
  • 68 |
69 |
70 |
71 |

循环(m-for),嵌套for循环,99乘法表(尝试点击):

72 | 73 | 74 | 75 | 81 | 82 | 83 |
79 | {{ item }} 80 |
84 |
85 |
86 |

Todo List

87 |

watch了list,任何操作都会保存在localstorage

88 |
89 | 95 | 96 |
97 |
98 |
103 | {{ item }} 104 |
105 |
106 |
    107 |
  • 113 |
    114 | {{ item.content }} 115 |
    116 |
    Del
    117 |
  • 118 |
119 |
120 |
121 |
122 | `, 123 | data() { 124 | return { 125 | activeIndex: 0, 126 | tabList: ['双向绑定', '计算属性', '条件渲染', '循环/事件', 'Todo List'], 127 | // 双绑 128 | person: { 129 | name: '花泽香菜', 130 | age: 12, 131 | sex: '女' 132 | }, 133 | 134 | // m-if 135 | showText: false, 136 | 137 | // m-for 138 | forTable: [], 139 | 140 | // todoList 141 | content: '', 142 | infos: [ 143 | { 144 | content: '中一次双色球,十注的 >_<#@!', 145 | done: true 146 | }, 147 | { 148 | content: '然后再中一次体彩,还是十注的 0_o', 149 | done: false 150 | }, 151 | { 152 | content: '我全都要 😂 🌚 🤣 💅 👅 🤠 ', 153 | done: false 154 | } 155 | ], 156 | filters: ['All', 'Todos', 'Dones'], 157 | filterIndex: 0 158 | }; 159 | }, 160 | computed: { 161 | computedDescription() { 162 | return `${this.person.name}的年龄是${this.person.age},然后是个${this.person.sex}的`; 163 | }, 164 | 165 | // 当前tab对应的数据 166 | list() { 167 | switch (this.filterIndex) { 168 | case 0: 169 | return this.infos; 170 | case 1: 171 | return this.infos.filter(n => !n.done); 172 | default: 173 | return this.infos.filter(n => n.done); 174 | } 175 | } 176 | }, 177 | created() { 178 | this.init99(); 179 | 180 | // todolist 181 | this.restore(); 182 | }, 183 | 184 | methods: { 185 | // 99 乘法表初始化 186 | init99() { 187 | // 构建99乘法表 188 | const result = []; 189 | for (let y = 1; y <= 9; y++) { 190 | const list = []; 191 | for (let x = 1; x <= 9; x++) { 192 | if (x > y) list.push(''); 193 | else list.push(x + ' * ' + y + ' = ' + x * y); 194 | } 195 | result.push(list); 196 | } 197 | this.forTable = result; 198 | }, 199 | 200 | //todolist 相关 201 | 202 | // 新增一项 203 | addItem() { 204 | const content = this.content.trim(); 205 | if (!content.length) { 206 | return; 207 | } 208 | this.infos.push({ 209 | content: content, 210 | done: false 211 | }); 212 | this.content = ''; 213 | }, 214 | 215 | handleKeyup(ev: KeyboardEvent) { 216 | if (ev.keyCode === 13) { 217 | this.addItem(); 218 | } 219 | }, 220 | 221 | // 切换完成状态 222 | toggleDone(item) { 223 | item.done = !item.done; 224 | this.infos = this.infos.slice(); 225 | }, 226 | 227 | // 删除一项 228 | deleteItem(item) { 229 | const index = this.infos.indexOf(item); 230 | this.infos.splice(index, 1); 231 | }, 232 | 233 | // 重置数据 234 | reset() { 235 | Object.assign(this.$data, this.$options.data()); 236 | this.init99(); 237 | }, 238 | 239 | // 从localstorage更新数据 240 | restore() { 241 | try { 242 | const content = localStorage[CACHE_KEY]; 243 | if (!content.length) { 244 | return; 245 | } 246 | const infos = JSON.parse(content); 247 | this.infos = infos; 248 | } catch (ex) { 249 | this.reset(); 250 | } 251 | } 252 | }, 253 | watch: { 254 | // 监听infos改变,存入localstorage 255 | infos() { 256 | const content = JSON.stringify(this.infos); 257 | localStorage[CACHE_KEY] = content; 258 | } 259 | } 260 | }); 261 | 262 | window['vm'] = vm; 263 | 264 | // window['mm'] = new MVVM(); 265 | -------------------------------------------------------------------------------- /packages/mini-mvvm/src/lib/Compile/AST.ts: -------------------------------------------------------------------------------- 1 | import { ENodeType } from '../../common/enums'; 2 | import { toArray } from '../../common/utils'; 3 | import parseAttrs from './parsers/parseAttrs'; 4 | import parseFor from './parsers/parseFor'; 5 | import parseEvents from './parsers/parseEvents'; 6 | import parseModel from './parsers/parseModel'; 7 | import parseProps from './parsers/parseProps'; 8 | import parseIf from './parsers/parseIf'; 9 | 10 | /** 11 | * 抽象语法树,用来表述模板结构 12 | * 13 | * @export 14 | * @class AST 15 | */ 16 | export default class AST { 17 | //#region 基础字段 18 | 19 | /** 20 | * 标签 tag 类型 21 | * 22 | * @type {string} 23 | * @memberof AST 24 | */ 25 | tag: string; 26 | 27 | /** 28 | * 标签 nodeType 29 | * 30 | * @type {ENodeType} 31 | * @memberof AST 32 | */ 33 | type: ENodeType; 34 | 35 | /** 36 | * attributes map 37 | * 38 | * @type {Record} 39 | * @memberof AST 40 | */ 41 | attrs?: Record; 42 | 43 | /** 44 | * textContent 45 | * 46 | * @type {string} 47 | * @memberof AST 48 | */ 49 | text?: string; 50 | 51 | /** 52 | * 子节点 53 | * 54 | * @type {AST[]} 55 | * @memberof AST 56 | */ 57 | children?: AST[]; 58 | 59 | /** 60 | * 唯一标识 key 61 | * 62 | * @type {*} 63 | * @memberof AST 64 | */ 65 | key?: any; 66 | 67 | //#endregion 68 | 69 | //#region m-for 70 | 71 | for?: string; 72 | 73 | forItem?: string; 74 | 75 | forIndex?: string; 76 | 77 | //#endregion 78 | 79 | //#region events 事件 80 | 81 | events?: Record; 82 | 83 | //#endregion 84 | 85 | //#region props 86 | 87 | props?: Record; 88 | 89 | //#endregion 90 | 91 | //#region if 92 | 93 | if?: string; 94 | 95 | //#endregion 96 | } 97 | 98 | /** 99 | * 根据元素节点,转换成 ast 树 100 | * 101 | * @export 102 | * @param {Element} el 103 | * @returns {AST} 104 | */ 105 | export function parseElement2AST(el: Element): AST { 106 | // 文本节点 107 | if (el.nodeType === ENodeType.Text) { 108 | return { 109 | tag: '', 110 | type: ENodeType.Text, 111 | text: el.textContent 112 | }; 113 | } 114 | 115 | // element节点 116 | if (el.nodeType === ENodeType.Element) { 117 | const attrsMap = toArray(el.attributes).reduce((map, cur) => { 118 | map[cur.name] = cur.value; 119 | return map; 120 | }, {}); 121 | const children = toArray(el.childNodes) 122 | .map(parseElement2AST) 123 | .filter(n => n); 124 | 125 | const ast: AST = { 126 | tag: el.tagName.toLowerCase(), 127 | type: ENodeType.Element, 128 | attrs: attrsMap, 129 | children 130 | }; 131 | 132 | // 先处理 attributes 133 | parseAttrs(ast); 134 | 135 | // 处理 props 136 | parseProps(ast); 137 | 138 | // 处理 events 139 | parseEvents(ast); 140 | 141 | // 处理 model 142 | parseModel(ast); 143 | 144 | // m-for 145 | parseFor(ast); 146 | 147 | // m-if 148 | parseIf(ast); 149 | 150 | return ast; 151 | } 152 | 153 | // 其他节点不考虑 154 | return null; 155 | } 156 | -------------------------------------------------------------------------------- /packages/mini-mvvm/src/lib/Compile/Compile.ts: -------------------------------------------------------------------------------- 1 | import { ENodeType } from '../../common/enums'; 2 | import AST, { parseElement2AST } from './AST'; 3 | 4 | const spFn = '__spVnode__'; 5 | 6 | /** 7 | * Compile ,用来编译模板 8 | * 9 | * @export 10 | * @class Compile 11 | */ 12 | export default class Compile { 13 | public static render(template: string): Function { 14 | return new Compile().render(template); 15 | } 16 | 17 | public render(template: string): Function { 18 | const wrap = document.createElement('div'); 19 | wrap.innerHTML = template.trim(); 20 | 21 | const node = wrap.children[0]; 22 | const ast = parseElement2AST(node); 23 | 24 | const renderStr = ` 25 | var ${spFn} = function(args){ 26 | var r = []; 27 | args.forEach(function(item){ 28 | if(!item) return; 29 | 30 | if(Object.prototype.toString.call(item) === '[object Array]'){ 31 | item=item.filter(function(n){ 32 | return !!n; 33 | }); 34 | [].push.apply(r,item); 35 | } 36 | else{ 37 | r.push(item); 38 | } 39 | }); 40 | return r; 41 | } 42 | with(this) { 43 | return ${this.ast2Render(ast)}; 44 | } 45 | `; 46 | 47 | // if (process.env.NODE_ENV !== 'production') { 48 | // console.log(renderStr); 49 | // } 50 | return new Function('h', renderStr); 51 | } 52 | 53 | private ast2Render(ast: AST): string { 54 | // console.log(ast.type); 55 | if (ast.type === ENodeType.Text) { 56 | return this.textAst2Render(ast); 57 | } 58 | if (ast.type === ENodeType.Element) { 59 | return this.eleAst2Render(ast); 60 | } 61 | // 理论上不会走到这里来,在生成ast的时候就过滤了 62 | return null; 63 | } 64 | 65 | private eleAst2Render(ast: AST): string { 66 | const attrs = JSON.stringify(ast.attrs).replace(/"\(\(/g, '(').replace(/\)\)"/g, ')'); // 处理 attr:"((value))" 67 | 68 | const props = JSON.stringify(ast.props).replace(/"\(\(/g, '(').replace(/\)\)"/g, ')'); // 处理 prop:"((value))" 69 | 70 | const children = ast.children 71 | .map(n => this.ast2Render(n)) 72 | .filter(n => n) 73 | .join(',\n'); // 这里用\n是为了调试时候美观 =。= 74 | 75 | const events = Object.keys(ast.events) 76 | .map(key => { 77 | return ( 78 | '' + 79 | `${key}:(function($event){ 80 | ${ast.events[key].join(';')} 81 | }).bind(this)` 82 | ); 83 | }) 84 | .join(','); 85 | 86 | const keyStr = ast.key ? `key:${ast.key},` : ''; 87 | 88 | const childTpl = (): string => { 89 | const ifContent = ast.if ? `!(${ast.if})?null:` : ''; 90 | return ( 91 | ifContent + 92 | `h('${ast.tag}',{ 93 | ${keyStr} 94 | attrs: ${attrs}, 95 | props:${props}, 96 | on:{${events}} 97 | }, 98 | ${spFn}([ 99 | ${children} 100 | ]) 101 | )` 102 | ); 103 | }; 104 | 105 | if (!ast.for) { 106 | return childTpl(); 107 | } else { 108 | const forIndex = ast.forIndex ? `,${ast.forIndex}` : ''; 109 | return ( 110 | '' + 111 | `${ast.for}.map(function (${ast.forItem}${forIndex}) { 112 | return ${childTpl()} 113 | }) 114 | ` 115 | ); 116 | } 117 | } 118 | 119 | private textAst2Render(ast: AST): string { 120 | // console.log(ast); 121 | const content = 122 | `'` + 123 | ast.text 124 | .replace( 125 | // 先把文本中的 换行/多个连续空格 替换掉 126 | /[\r\n\s]+/g, 127 | ' ' 128 | ) 129 | .replace(/'/g, `\\'`) 130 | .replace( 131 | // 再处理依赖 {{ field }} 132 | /\{\{(.*?)\}\}/g, 133 | `' + ($1) + '` 134 | ) + 135 | `'`; 136 | 137 | return `h('', ${content})`; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /packages/mini-mvvm/src/lib/Compile/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Compile 比较复杂,单独用一个文件夹存放相关内容 3 | */ 4 | 5 | export { default } from './Compile'; 6 | -------------------------------------------------------------------------------- /packages/mini-mvvm/src/lib/Compile/parsers/parseAttrs.ts: -------------------------------------------------------------------------------- 1 | import AST from '../AST'; 2 | 3 | /** 4 | * 处理 ast 上的 attributes, 5 | * :attr="value" 这种动态 attribute,会被处理成 attr:"((value))" 6 | * 在之后 compile 的时候去掉双引号 7 | * 8 | * @export 9 | * @param {AST} ast 10 | * @returns 11 | */ 12 | export default function parseAttrs(ast: AST): void { 13 | // 要添加的属性,在 for in 之后再添加 14 | // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/for...in#%E6%8F%8F%E8%BF%B0 15 | const dynamicAttrs: Record = {}; 16 | 17 | for (const name in ast.attrs) { 18 | const val = ast.attrs[name]; 19 | 20 | // 处理 key 21 | if (/^:?key$/.test(name)) { 22 | ast.key = name[0] === ':' ? val : `'${val}'`; 23 | delete ast.attrs[name]; 24 | continue; 25 | } 26 | 27 | // 如果是 :attr="value" 这种动态 attribute 28 | if (/^:/.test(name)) { 29 | const newName = name.slice(1); 30 | const newVal = `((${val}))`; 31 | delete ast.attrs[name]; 32 | dynamicAttrs[newName] = newVal; 33 | } 34 | } 35 | 36 | Object.assign(ast.attrs, dynamicAttrs); 37 | } 38 | -------------------------------------------------------------------------------- /packages/mini-mvvm/src/lib/Compile/parsers/parseEvents.ts: -------------------------------------------------------------------------------- 1 | import AST from '../AST'; 2 | 3 | export default function parseEvents(ast: AST): void { 4 | ast.events = ast.events || {}; 5 | 6 | for (const key in ast.attrs) { 7 | if (!/^@/.test(key)) { 8 | continue; 9 | } 10 | 11 | /** 12 | * 对于 @click="handleClick" 这种绑定 13 | * 修改为 @click="handleClick(@event)" 14 | * 15 | * @click="temp=xxx" @click="handleClick(...args)" 就不处理了 16 | * 这个正则用来表示定义 变量/方法: 17 | * https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Grammar_and_Types#%E5%8F%98%E9%87%8F 18 | */ 19 | if (/^(($|_)[\w$]*|[\w$]+)$/.test(ast.attrs[key])) { 20 | ast.attrs[key] += '($event)'; 21 | } 22 | 23 | // 添加到事件 24 | ast.events[key.slice(1)] = [ast.attrs[key]]; 25 | // 从 attrs 删除事件 26 | delete ast.attrs[key]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/mini-mvvm/src/lib/Compile/parsers/parseFor.ts: -------------------------------------------------------------------------------- 1 | import AST from '../AST'; 2 | 3 | /** 4 | * 处理 ast 上的 m-for 5 | * 6 | * @export 7 | * @param {AST} ast 8 | * @returns 9 | */ 10 | export default function parseFor(ast: AST): void { 11 | const forKey = 'm-for'; 12 | const forValue = ast.attrs[forKey]; 13 | 14 | if (!forValue) { 15 | return; 16 | } 17 | 18 | // 这个正则支持两种匹配 19 | // 1. (item,index) in list 20 | // 2. item in list 21 | const reg = /^(\(\s*(\S+?)\s*,\s*(\S+?)\s*\)|(\S+?))\s+in\s+(.+)$/; 22 | const match = forValue.match(reg); 23 | 24 | // for表达式有问题 25 | if (!match) { 26 | throw new Error(`${forKey}表达式出错:${forKey}="${forValue}"`); 27 | } 28 | 29 | // 给ast添加for相关内容 30 | ast.for = match[5]; 31 | ast.forItem = match[2] || match[4]; 32 | ast.forIndex = match[3]; 33 | 34 | // 删除原attr 35 | // ast.attrs.splice(forAttrIndex, 1); 36 | delete ast.attrs[forKey]; 37 | } 38 | -------------------------------------------------------------------------------- /packages/mini-mvvm/src/lib/Compile/parsers/parseIf.ts: -------------------------------------------------------------------------------- 1 | import AST from '../AST'; 2 | 3 | export default function parseIf(ast: AST): void { 4 | const ifKey = 'm-if'; 5 | const ifValue = ast.attrs[ifKey]; 6 | 7 | if (!ifValue) { 8 | return; 9 | } 10 | 11 | ast.if = ifValue; 12 | delete ast.attrs[ifKey]; 13 | } 14 | -------------------------------------------------------------------------------- /packages/mini-mvvm/src/lib/Compile/parsers/parseModel.ts: -------------------------------------------------------------------------------- 1 | import AST from '../AST'; 2 | 3 | /** 4 | * 处理 m-model 5 | * 6 | * @export 7 | * @param {AST} ast 8 | * @returns 9 | */ 10 | export default function parseModel(ast: AST): void { 11 | const modelKey = 'm-model'; 12 | const modelValue = ast.attrs[modelKey]; 13 | 14 | if (!modelValue) { 15 | return; 16 | } 17 | 18 | // 添加input事件 19 | ast.events['input'] = ast.events['input'] || []; 20 | ast.events['input'].push(`${modelValue}=$event.target.value`); 21 | 22 | // 添加到 props 23 | ast.props['value'] = `((${modelValue}))`; 24 | 25 | // 从 attr 里面删除 m-model 26 | delete ast.attrs[modelKey]; 27 | } 28 | -------------------------------------------------------------------------------- /packages/mini-mvvm/src/lib/Compile/parsers/parseProps.ts: -------------------------------------------------------------------------------- 1 | import AST from '../AST'; 2 | 3 | /** 4 | * 可以用 prop 来表示的 attribue 5 | */ 6 | const PROP_KEYS = ['value', 'checked', 'disabled']; 7 | 8 | /** 9 | * 处理 props 10 | * 11 | * @export 12 | * @param {AST} ast 13 | */ 14 | export default function parseProps(ast: AST): void { 15 | ast.props = ast.props || {}; 16 | 17 | for (const key in ast.attrs) { 18 | if (!~PROP_KEYS.indexOf(key)) { 19 | continue; 20 | } 21 | 22 | // 格式已经在 parseAttrs 里面处理过,这里转移一下就好 23 | ast.props[key] = ast.attrs[key]; 24 | delete ast.attrs[key]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/mini-mvvm/src/lib/Dep.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 搜集所有依赖 3 | */ 4 | 5 | import Watcher from './Watcher'; 6 | 7 | let depId = 1; 8 | 9 | export default class Dep { 10 | private subs: Watcher[] = []; 11 | 12 | public id: number = depId++; 13 | 14 | public key: string; 15 | 16 | public static target: Watcher; 17 | 18 | /** 19 | * 添加一个 watcher 20 | * 21 | * @param {Watcher} watcher 22 | * @returns 23 | * @memberof Dep 24 | */ 25 | public add(watcher: Watcher): void { 26 | if (~this.subs.indexOf(watcher)) { 27 | return; 28 | } 29 | this.subs.push(watcher); 30 | } 31 | 32 | /** 33 | * 移除一个 watcher 34 | * 35 | * @param {Watcher} watcher 36 | * @memberof Dep 37 | */ 38 | public remove(watcher: Watcher): void { 39 | const index = this.subs.indexOf(watcher); 40 | if (~index) { 41 | this.subs.splice(index, 1); 42 | } 43 | } 44 | 45 | public clear(): void { 46 | this.subs = []; 47 | } 48 | 49 | /** 50 | * 通过 Dep.target 把 dep 添加到当前到 watcher 51 | * 52 | * @memberof Dep 53 | */ 54 | public depend(): void { 55 | const target = Dep.target; 56 | if (!target) { 57 | return; 58 | } 59 | target.addDep(this); 60 | this.add(target); 61 | } 62 | 63 | /** 64 | * 通知所有 watcher 更新 65 | * 66 | * @memberof Dep 67 | */ 68 | public notify(): void { 69 | this.subs.forEach(n => n.update()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/mini-mvvm/src/lib/ELifeCycle.ts: -------------------------------------------------------------------------------- 1 | import MVVM from '../core/MVVM'; 2 | 3 | export interface ILifeCycle { 4 | /** 5 | * 组件创建成功 6 | * 7 | * @memberof ILifeCycle 8 | */ 9 | created?: () => void; 10 | 11 | /** 12 | * 将要被插入dom 13 | * 14 | * @memberof ILifeCycle 15 | */ 16 | beforeMount?: () => void; 17 | 18 | /** 19 | * 组件被添加到dom 20 | * 21 | * @memberof ILifeCycle 22 | */ 23 | mounted?: () => void; 24 | 25 | /** 26 | * 组件将要更新 27 | * 28 | * @memberof ILifeCycle 29 | */ 30 | beforeUpdate?: () => void; 31 | 32 | /** 33 | * 组件更新完毕 34 | * 35 | * @memberof ILifeCycle 36 | */ 37 | updated?: () => void; 38 | } 39 | 40 | /** 41 | * 生命周期 42 | * 43 | * @enum {number} 44 | */ 45 | enum ELifeCycle { 46 | /** 47 | * 创建成功 48 | */ 49 | created = 'hook:created', 50 | 51 | /** 52 | * 插入dom之前 53 | */ 54 | beforeMount = 'hook:beforeMount', 55 | 56 | /** 57 | * 插入dom 58 | */ 59 | mounted = 'hook:mounted', 60 | 61 | /** 62 | * 更新之前 63 | */ 64 | beforeUpdate = 'hook:beforeUpdate', 65 | 66 | /** 67 | * 更新完毕 68 | */ 69 | updated = 'hook:updated' 70 | } 71 | 72 | export default ELifeCycle; 73 | 74 | /** 75 | * 给 vm 添加声明周期钩子 76 | * 77 | * @export 78 | * @param {MVVM} vm 79 | */ 80 | export function defineLifeCycle(vm: MVVM): void { 81 | Object.keys(ELifeCycle).forEach(key => { 82 | const lifeMethod = vm.$options[key]; 83 | if (!lifeMethod) return; 84 | 85 | vm[key] = lifeMethod.bind(vm); 86 | 87 | vm.$on(ELifeCycle[key], vm[key]); 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /packages/mini-mvvm/src/lib/Observer.ts: -------------------------------------------------------------------------------- 1 | import { getType } from '../common/utils'; 2 | import Dep from './Dep'; 3 | 4 | /** 5 | * 需要重写的方法,用于观察数组 6 | */ 7 | const hookArrayMethods: string[] = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']; 8 | 9 | /** 10 | * 把 source 上的所有key,代理到 target 上 11 | * 12 | * @export 13 | * @param {Record} source 要代理到数据源 14 | * @param {Record} target 代理的目标对象 15 | */ 16 | export function proxy(source: Record, target: Record): void; 17 | 18 | /** 19 | * 对 Object.defineProperty 的简单封装 20 | * 21 | * @export 22 | * @param {Record} data 要观察的数据 23 | * @param {string} key 要观察的key 24 | * @param {PropertyDescriptor} descriptor 25 | */ 26 | export function proxy(data: Record, key: string, descriptor: PropertyDescriptor): void; 27 | 28 | export function proxy( 29 | data: Record, 30 | targetOrkey: Record | string, 31 | descriptor?: PropertyDescriptor 32 | ): void { 33 | if (getType(targetOrkey) === 'object') { 34 | for (const key in data) { 35 | proxy(targetOrkey as Record, key, { 36 | get: () => data[key], 37 | set: newVal => (data[key] = newVal) 38 | }); 39 | } 40 | return; 41 | } 42 | 43 | Object.defineProperty(data, targetOrkey as string, { 44 | enumerable: true, 45 | configurable: true, 46 | ...descriptor 47 | }); 48 | } 49 | 50 | export default class Observer { 51 | private data: Record; 52 | 53 | constructor(data: Record | any[]) { 54 | const dataType = getType(data); 55 | if (!~['object', 'array'].indexOf(dataType)) { 56 | return; 57 | } 58 | this.data = dataType === 'array' ? { a: data } : data; 59 | this.observe(); 60 | } 61 | 62 | private observe(): void { 63 | Object.keys(this.data).forEach(key => { 64 | // 监听这个属性的变更 65 | this.defineReactive(key); 66 | 67 | // 递归 68 | getType(this.data[key]) === 'object' && new Observer(this.data[key]); 69 | }); 70 | } 71 | 72 | private defineReactive(key: string): void { 73 | const dep = new Dep(); 74 | dep.key = key; 75 | let val = this.data[key]; 76 | 77 | // 监听赋值操作 78 | proxy(this.data, key, { 79 | get: () => { 80 | dep.depend(); 81 | return val; 82 | }, 83 | 84 | set: newVal => { 85 | if (val === newVal) { 86 | return; 87 | } 88 | 89 | val = newVal; 90 | 91 | // 如果是数组,还需要监听变异方法 92 | this.appendArrayHooks(key); 93 | 94 | // set 的时候需要主动再次添加 observer 95 | getType(val) === 'object' && new Observer(val); 96 | 97 | dep.notify(); 98 | } 99 | }); 100 | 101 | // 虽然这个没啥用,但是先放上去 😂 102 | proxy(this.data, '__ob__', { enumerable: false, value: this }); 103 | 104 | // 如果是数组,还需要监听变异方法 105 | this.appendArrayHooks(key); 106 | } 107 | 108 | private appendArrayHooks(key: string): void { 109 | const item = this.data[key]; 110 | if (getType(item) !== 'array') { 111 | return; 112 | } 113 | 114 | // 给数组的一些方法添加hook 115 | for (const method of hookArrayMethods) { 116 | proxy(item, method, { 117 | enumerable: false, 118 | get: () => { 119 | // eslint-disable-next-line 120 | return (...args: any[]) => { 121 | // 得到结果,缓存下来在最后返回 122 | const list = this.data[key].slice(); 123 | const result = list[method](...args); 124 | 125 | // 把新数组赋值给当前key,触发 watcher 的 update,以及再次 hook 126 | this.data[key] = list; 127 | 128 | return result; 129 | }; 130 | } 131 | }); 132 | } 133 | 134 | // 给数组中的每一项添加hook 135 | for (const child of item) { 136 | new Observer(child); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /packages/mini-mvvm/src/lib/Watcher.ts: -------------------------------------------------------------------------------- 1 | import Dep from './Dep'; 2 | import MVVM from '../core/MVVM'; 3 | import { proxy } from './Observer'; 4 | import { getValByPath, nextTick } from '../common/utils'; 5 | 6 | /** 7 | * 给 vm 添加 computed 8 | * 9 | * @export 10 | * @param {MVVM} vm 11 | * @param {Record} [computed={}] 12 | * @returns 13 | */ 14 | export function defineComputed(vm: MVVM, computed: Record = {}): Record { 15 | const computedWatchers: Record = {}; 16 | for (const key in computed) { 17 | // eslint-disable-next-line 18 | const watcher = new Watcher(vm, computed[key], null, { lazy: true }); 19 | proxy(vm, key, { 20 | get() { 21 | // 如果是在 watcher 中引用 watcher,被引用的 watcher 会更改和清空 Dep.target 22 | // 所以缓存一下,并且在之后更新 23 | const wrapWatcher = Dep.target; // 外面的那层 watcher 24 | 25 | // 如果需要更新,就重新计算并获取依赖 26 | // 否则就从缓存拿 27 | const val = watcher.dirty ? watcher.get() : watcher.value; 28 | 29 | if (wrapWatcher) { 30 | // 把 watcher 中的引用,传递给外部的 watcher 31 | Dep.target = wrapWatcher; 32 | watcher.deps.forEach(dep => dep.depend()); 33 | } 34 | 35 | return val; 36 | } 37 | }); 38 | computedWatchers[key] = watcher; 39 | } 40 | return computedWatchers; 41 | } 42 | 43 | /** 44 | * watch 处理函数 45 | * 46 | * @param {any} val 当前数据 47 | * @param {any} oldVal 之前的数据 48 | * @returns 49 | */ 50 | type TWatchFn = (val: any, oldVal: any) => void; 51 | 52 | /* eslint-disable */ 53 | export type TWatchDefine = 54 | | TWatchFn 55 | | { 56 | /** 57 | * 是否立即执行 58 | * 59 | * @type {boolean} 60 | */ 61 | immediate?: boolean; 62 | /** 63 | * watch 处理函数 64 | * 65 | * @type {TWatchFn} 66 | */ 67 | handler: TWatchFn; 68 | }; 69 | /* eslint-enable */ 70 | 71 | /** 72 | * 给 vm 添加 $watch,并处理 $options.watch 73 | * 74 | * @export 75 | * @param {MVVM} vm 76 | * @param {Record} watch 77 | */ 78 | export function defineWatch(vm: MVVM, watch: Record): void { 79 | /*eslint-disable*/ 80 | vm.$watch = function (exp, callback, { immediate } = { immediate: false }) { 81 | vm._watchers.push( 82 | new Watcher( 83 | vm, 84 | // 这个是用来借助computed来搜集依赖用 85 | () => getValByPath(vm, exp), 86 | // 在依赖进行改变的时候,执行回掉 87 | callback, 88 | // 是否立即执行 89 | { immediate } 90 | ) 91 | ); 92 | }; 93 | /*eslint-enable*/ 94 | 95 | for (const exp in watch) { 96 | const watchDef = watch[exp]; 97 | if (typeof watchDef === 'function') { 98 | vm.$watch(exp, watchDef as TWatchFn); 99 | } else if (typeof watchDef === 'object') { 100 | vm.$watch(exp, watchDef.handler, { immediate: watchDef.immediate }); 101 | } 102 | } 103 | } 104 | 105 | interface IWatcherOpotions { 106 | /** 107 | * 延迟计算,只有在用到的时候才去计算 108 | * 109 | * @type {boolean} 110 | * @memberof IWatcherOpotions 111 | */ 112 | lazy?: boolean; 113 | 114 | /** 115 | * 脏数据,需要重新计算 116 | * 117 | * @type {boolean} 118 | * @memberof IWatcherOpotions 119 | */ 120 | dirty?: boolean; 121 | 122 | /** 123 | * 是否立即执行 124 | * 125 | * @type {boolean} 126 | * @memberof IWatcherOpotions 127 | */ 128 | immediate?: boolean; 129 | } 130 | 131 | export default class Watcher implements IWatcherOpotions { 132 | private invoked = false; 133 | 134 | public vm: MVVM; 135 | 136 | public value: any; 137 | 138 | public deps: Dep[] = []; 139 | 140 | public getter: Function; 141 | 142 | public cb: Function; 143 | 144 | public lazy: boolean; 145 | 146 | public dirty: boolean; 147 | 148 | public immediate: boolean; 149 | 150 | constructor(vm: MVVM, getter?: Function, cb?: Function, options: IWatcherOpotions = {}) { 151 | this.vm = vm; 152 | this.getter = getter; 153 | this.cb = cb; 154 | 155 | // 把 options 传入 this 156 | Object.assign(this, options); 157 | 158 | // 初始化的时候,如果是lazy,就表示是脏数据 159 | this.dirty = this.lazy; 160 | 161 | // 是 watch 的时候,计算一下当前依赖 162 | if (this.cb) { 163 | this.get(); 164 | } 165 | } 166 | 167 | public addDep(dep: Dep): void { 168 | if (!~this.deps.indexOf(dep)) { 169 | this.deps.push(dep); 170 | } 171 | } 172 | 173 | public update(): void { 174 | // lazy 表示是 computed,只有在用到的时候才去更新 175 | if (this.lazy) { 176 | this.dirty = true; 177 | } 178 | // cb 表示是 watch 179 | else if (this.cb) { 180 | // debugger; 181 | 182 | // 连续的修改,以最后一次为准 183 | // 全都在 nexttick 中处理 184 | this.dirty = true; 185 | 186 | nextTick(() => { 187 | if (!this.dirty) { 188 | return; 189 | } 190 | 191 | this.get(); 192 | }); 193 | } 194 | // 更新因为是在 nextTick ,所以在 render 的时候, 195 | // 所有的 computed watchers 都已经标记为 dirty:false 了 196 | else { 197 | this.vm._update(); 198 | } 199 | } 200 | 201 | /** 202 | * 清空所有依赖 203 | * 204 | * @memberof Watcher 205 | */ 206 | public clear(): void { 207 | this.deps.forEach(dep => dep.remove(this)); 208 | this.deps = []; 209 | } 210 | 211 | /** 212 | * 计算value并重新搜集依赖 213 | * 214 | * @memberof Watcher 215 | */ 216 | public get(): void { 217 | this.clear(); 218 | const oldVal = this.value; 219 | Dep.target = this; 220 | 221 | try { 222 | this.value = this.getter.call(this.vm, this.vm); 223 | 224 | // 在【立即执行】或者【更新】的时候,进行通知 225 | if (this.cb && this.value !== oldVal && (this.immediate || this.invoked)) { 226 | this.cb.call(this.vm, this.value, oldVal); 227 | } 228 | } catch (ex) { 229 | console.log('watcher get error'); 230 | throw ex; 231 | } finally { 232 | Dep.target = null; 233 | this.dirty = false; 234 | this.invoked = true; 235 | } 236 | 237 | return this.value; 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /packages/mini-mvvm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", // 目标代码类型 4 | "module": "esnext", // 指定生成哪个模块系统代码 5 | "noImplicitAny": false, // 在表达式和声明上有隐含的'any'类型时报错。 6 | "sourceMap": false, // 用于debug 7 | "rootDir": "./src", // 仅用来控制输出的目录结构--outDir。 8 | "outDir": "./dist", // 重定向输出目录。 9 | "esModuleInterop": true, 10 | "declaration": true, 11 | "moduleResolution": "node", 12 | "watch": false // 在监视模式下运行编译器。会监视输出文件,在它们改变时重新编译。 13 | }, 14 | "include": [ 15 | "./src/**/*" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/mini-vdom/.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npm.taobao.org 2 | -------------------------------------------------------------------------------- /packages/mini-vdom/README.md: -------------------------------------------------------------------------------- 1 | # mini-vdom 2 | 3 | [![npm](https://img.shields.io/npm/v/mini-vdom?logo=npm&style=flat-square)](https://www.npmjs.com/package/mini-vdom) 4 | [![file size](https://img.shields.io/github/size/shalldie/mini-mvvm/dist/mini-vdom.js?style=flat-square)](https://www.npmjs.com/package/mini-vdom) 5 | [![Build Status](https://img.shields.io/github/workflow/status/shalldie/mini-mvvm/ci?label=build&logo=github&style=flat-square)](https://github.com/shalldie/mini-mvvm/actions) 6 | 7 | A mini virtual dom lib. 一个轻量级的虚拟 dom 库。 8 | 9 | ## Installation 10 | 11 | npm install mini-vdom --save 12 | 13 | ## Description 14 | 15 | 1. 超级轻量 `7kb` 16 | 2. 作为一个 vdom lib,你只用更改数据,`mini-vdom` 会帮你处理好 dom 🤓🤓 17 | 3. 丰富的代码提示,已经包含了 `.d.ts` 文件 18 | 19 | 这是在学习 [snabbdom](https://github.com/snabbdom/snabbdom) 源码之后,借鉴其思路写的一个 vdom 库。 20 | 21 | 适合用在一些快速开发的项目中,或者作为二次开发的依赖,只包含了最常用的 vdom 功能,体积 `7kb` 超轻量。 如果需要构造大型复杂项目,你可能需要一个成熟的 mvvm 框架。 22 | 23 | ## Examples 24 | 25 | 使用 `npm run dev` 去查看 `src/dev.ts` 的例子. 26 | 27 | 或者查看 [在线例子 - Todo List](https://shalldie.github.io/demos/mini-vdom/) 28 | 29 | ## Usage 30 | 31 | ```ts 32 | import { h, patch } from 'mini-vdom'; // es module, typescript 33 | // const { h, patch } = require('MiniVdom'); // commonjs 34 | // const { h, patch } = window['MiniVdom']; // window 35 | 36 | // 生成一个 vnode 节点 37 | const node = h('span', 'hello world'); 38 | 39 | // 把vnode挂载在一个dom上 40 | patch(document.getElementById('app'), vnode); 41 | 42 | // 用一个新的vnode去更新旧的vnode 43 | const newNode = h( 44 | 'div.new-div', 45 | { 46 | attrs: { 47 | 'data-name': 'tom' 48 | }, 49 | on: { 50 | click() { 51 | alert('new div'); 52 | } 53 | } 54 | }, 55 | 'click me to show alert! ' 56 | ); 57 | 58 | patch(vnode, newVnode); 59 | ``` 60 | 61 | ```ts 62 | // h 是 VNode 的工厂方法,提供以下四种方式去创建一个 VNode 63 | // 记不住?没关系,已经提供了 .d.ts 文件提示 64 | 65 | function h(type: string, text?: string): VNode; 66 | function h(type: string, children?: VNode[]): VNode; 67 | function h(type: string, data?: IVNodeData, text?: string): VNode; 68 | function h(type: string, data?: IVNodeData, children?: VNode[]): VNode; 69 | ``` 70 | 71 | # Enjoy it ! >\_<#@! 72 | -------------------------------------------------------------------------------- /packages/mini-vdom/__tests__/h.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 测试 h 函数 3 | */ 4 | import { h } from '../src'; 5 | 6 | describe('function h', () => { 7 | 8 | test('h(type: string, text?: string)', () => { 9 | const vnode = h('span', 'hello'); 10 | expect(vnode.type).toBe('span'); 11 | expect(vnode.children[0].text).toBe('hello'); 12 | }); 13 | 14 | test('h(type: string, children?: VNode[])', () => { 15 | const vnode = h('div#id.class1', [ 16 | h('span[name=tom]') 17 | ]); 18 | 19 | expect(vnode.type).toBe('div'); 20 | expect(vnode.data.attrs.class).toBe('class1'); 21 | expect(vnode.children).toHaveLength(1); 22 | expect(vnode.children[0].data.attrs.name).toBe('tom'); 23 | }); 24 | 25 | test('h(type: string, data?: IVNodeData, text?: string)', () => { 26 | const vnode = h('div.hello', { 27 | attrs: { 28 | 'data-name': 'tom' 29 | } 30 | }, 'world'); 31 | 32 | expect(vnode.type).toBe('div'); 33 | expect(vnode.data.attrs.class).toBe('hello'); 34 | expect(vnode.data.attrs['data-name']).toBe('tom'); 35 | expect(vnode.children).toHaveLength(1); 36 | expect(vnode.children[0].text).toBe('world'); 37 | }); 38 | 39 | test('h(type: string, data?: IVNodeData, children?: VNode[])', () => { 40 | const vnode = h('div.hello', {}, [ 41 | h('span', 'world') 42 | ]); 43 | 44 | expect(vnode.type).toBe('div'); 45 | expect(vnode.data.attrs.class).toBe('hello'); 46 | expect(vnode.children).toHaveLength(1); 47 | expect(vnode.children[0].type).toBe('span'); 48 | expect(vnode.children[0].children[0].text).toBe('world'); 49 | }); 50 | 51 | }); 52 | -------------------------------------------------------------------------------- /packages/mini-vdom/__tests__/hook.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 测试 hook 3 | * @jest-environment jsdom 4 | */ 5 | 6 | import { h, patch } from '../src'; 7 | 8 | describe('测试 hook', () => { 9 | 10 | let dom: HTMLElement; 11 | 12 | beforeEach(() => { 13 | document.body.innerHTML = '
' 14 | dom = document.getElementById('app'); 15 | }); 16 | 17 | test('hook: create', () => { 18 | expect(dom.parentNode).toBe(document.body); 19 | const vnode = h('div', { 20 | hook: { 21 | create() { 22 | expect(vnode.elm.tagName).toBe('DIV'); // 这个是测试生成了dom节点 23 | expect(vnode.elm.parentNode).toBe(null); 24 | } 25 | } 26 | }); 27 | patch(dom, vnode); 28 | }); 29 | 30 | test('hook: insert', () => { 31 | expect(dom.parentNode).toBe(document.body); 32 | const vnode = h('div', { 33 | hook: { 34 | insert() { 35 | expect(dom.parentNode).toBe(document.body); 36 | } 37 | } 38 | }); 39 | patch(dom, vnode); 40 | }); 41 | 42 | test('hook: update', () => { 43 | const mockFn = jest.fn(); 44 | 45 | const vnode = h('div'); 46 | patch(dom, vnode); 47 | 48 | const newVnode = h('div', { 49 | hook: { 50 | create: mockFn, // 这俩不触发,因为复用了 51 | insert: mockFn, 52 | update() { 53 | mockFn('update'); 54 | }, 55 | destroy: mockFn 56 | } 57 | }); 58 | patch(vnode, newVnode); 59 | 60 | expect(mockFn).toBeCalledTimes(1); 61 | expect(mockFn).toBeCalledWith('update'); 62 | }); 63 | 64 | test('hook: destroy', () => { 65 | const mockFn = jest.fn(); 66 | 67 | const vnode = h('span', { 68 | hook: { 69 | destroy: mockFn 70 | } 71 | }); 72 | patch(dom, vnode); 73 | 74 | const newVnode = h('div'); 75 | patch(vnode, newVnode); 76 | 77 | expect(mockFn).toBeCalledTimes(1); 78 | }); 79 | 80 | test('hook: create, insert, update, destroy', async () => { 81 | const mockFn = jest.fn(); 82 | expect(dom.parentNode).toBe(document.body); 83 | const vnode = h('div', { 84 | hook: { 85 | create() { 86 | mockFn(); 87 | 88 | expect(vnode.elm.tagName).toBe('DIV'); // 这个是测试生成了dom节点 89 | expect(vnode.elm.parentNode).toBe(null); 90 | }, 91 | insert() { 92 | mockFn(); 93 | // 因为是直接节点,所以不用nexttick,实际项目中需要nexttick 94 | expect(vnode.elm.parentNode).toBe(document.body); 95 | } 96 | } 97 | }); 98 | patch(dom, vnode); 99 | expect(mockFn).toBeCalledTimes(2); 100 | 101 | const newVnode = h('div', { 102 | hook: { 103 | create: mockFn, 104 | insert: mockFn, 105 | update: mockFn, // 只有这个触发,因为复用了 106 | destroy: mockFn 107 | } 108 | }); 109 | 110 | patch(vnode, newVnode); 111 | expect(mockFn).toBeCalledTimes(3); 112 | 113 | patch(newVnode, h('span')); 114 | expect(mockFn).toBeCalledTimes(4); 115 | 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /packages/mini-vdom/__tests__/patch.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 测试 渲染 结果,patch 3 | * @jest-environment jsdom 4 | */ 5 | 6 | import { h, patch } from '../src'; 7 | 8 | describe('function patch', () => { 9 | 10 | let dom: HTMLElement; 11 | 12 | function getNode(): HTMLElement { 13 | return [].slice.call(document.body.children).slice(-1)[0]; 14 | } 15 | 16 | beforeEach(() => { 17 | document.body.innerHTML = '
'; 18 | dom = document.getElementById('app'); 19 | }); 20 | 21 | test('初始化,patch dom', () => { 22 | 23 | const vnode = h('span', 'lalala'); 24 | patch(dom, vnode); 25 | 26 | const node = getNode(); 27 | expect(node.tagName).toBe('SPAN'); 28 | 29 | expect(node.textContent).toBe('lalala'); 30 | 31 | }); 32 | 33 | test('attrs', () => { 34 | const vnode = h('div', { 35 | attrs: { 36 | 'data-name': 'tom', 37 | 'class': 'class' 38 | } 39 | }, 'lalala'); 40 | patch(dom, vnode); 41 | 42 | const node = getNode(); 43 | expect(node.dataset.name).toBe('tom'); 44 | expect(node.className).toBe('class'); 45 | }); 46 | 47 | test('attrs update', () => { 48 | const vnode = h('div', { 49 | attrs: { 50 | 'data-name': 'lily' 51 | } 52 | }, 'lalala'); 53 | 54 | patch(dom, vnode); 55 | 56 | const newVnode = h('div', { 57 | attrs: { 58 | 'data-name': 'tom', 59 | 'class': 'class' 60 | } 61 | }, 'lalala'); 62 | 63 | patch(vnode, newVnode); 64 | 65 | const node = getNode(); 66 | expect(node.dataset.name).toBe('tom'); 67 | expect(node.className).toBe('class'); 68 | }); 69 | 70 | test('props', () => { 71 | const vnode = h('input', { 72 | props: { 73 | type: 'checkbox', 74 | checked: true 75 | } 76 | }); 77 | patch(dom, vnode); 78 | 79 | const node = getNode() as HTMLInputElement; 80 | expect(node.type).toBe('checkbox'); 81 | expect(node.checked).toBe(true); 82 | node.click(); 83 | expect(node.checked).toBe(false); 84 | }); 85 | 86 | test('props update', () => { 87 | const vnode = h('input', { 88 | props: { 89 | type: 'checkbox', 90 | checked: false 91 | } 92 | }); 93 | patch(dom, vnode); 94 | 95 | const newVnode = h('input', { 96 | props: { 97 | type: 'checkbox', 98 | checked: true 99 | } 100 | }); 101 | patch(vnode, newVnode); 102 | 103 | const node = getNode() as HTMLInputElement; 104 | expect(node.type).toBe('checkbox'); 105 | expect(node.checked).toBe(true); 106 | node.click(); 107 | expect(node.checked).toBe(false); 108 | }); 109 | 110 | test('events', () => { 111 | const mockFn = jest.fn(); 112 | const vnode = h('div', { on: { click: mockFn } }, [ 113 | h('input', { on: { focus: mockFn } }) 114 | ]); 115 | patch(dom, vnode); 116 | 117 | const node = getNode(); 118 | 119 | expect(mockFn).toBeCalledTimes(0); 120 | node.click(); 121 | expect(mockFn).toBeCalledTimes(1); 122 | node.querySelector('input').focus(); 123 | expect(mockFn).toBeCalledTimes(2); 124 | }); 125 | 126 | test('events update', () => { 127 | const mockFn = jest.fn(); 128 | const vnode = h('div', { on: { click: mockFn } }); 129 | patch(dom, vnode); 130 | 131 | const newVnode = h('div', [ 132 | h('input', { on: { focus: mockFn } }) 133 | ]); 134 | patch(vnode, newVnode); 135 | 136 | const node = getNode(); 137 | 138 | expect(mockFn).toBeCalledTimes(0); 139 | node.click(); 140 | expect(mockFn).toBeCalledTimes(0); 141 | node.querySelector('input').focus(); 142 | expect(mockFn).toBeCalledTimes(1); 143 | }); 144 | 145 | }); 146 | -------------------------------------------------------------------------------- /packages/mini-vdom/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | mini-vdom - A mini virtual dom lib. 一个轻量级的虚拟dom库。 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/mini-vdom/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | verbose: true, 4 | testEnvironment: 'node' 5 | }; 6 | -------------------------------------------------------------------------------- /packages/mini-vdom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-vdom", 3 | "version": "0.0.13", 4 | "description": "A mini virtual dom lib. 一个轻量级的虚拟dom库。", 5 | "keywords": [ 6 | "virtual", 7 | "dom", 8 | "vdom", 9 | "vnode" 10 | ], 11 | "author": "shalldie ", 12 | "homepage": "https://github.com/shalldie/mvvm", 13 | "license": "MIT", 14 | "main": "dist/mini-vdom.js", 15 | "types": "dist/index.d.ts", 16 | "directories": { 17 | "test": "__tests__" 18 | }, 19 | "files": [ 20 | "dist" 21 | ], 22 | "publishConfig": { 23 | "registry": "https://registry.npmjs.org" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/shalldie/mvvm.git" 28 | }, 29 | "scripts": { 30 | "test": "../../node_modules/.bin/jest", 31 | "dev": "../../node_modules/.bin/cross-env NODE_ENV=development ../../node_modules/.bin/rollup -cw", 32 | "build": "../../node_modules/.bin/cross-env NODE_ENV=production ../../node_modules/.bin/rollup -c" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/shalldie/mvvm/issues" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/mini-vdom/rollup.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | const rollupGenerator = require('../../node_modules/@nosaid/rollup').rollupGenerator; 3 | 4 | const ifProduction = process.env.NODE_ENV === 'production'; 5 | 6 | export default rollupGenerator([ 7 | { 8 | input: ifProduction ? 'src/index.ts' : 'src/dev.ts', 9 | output: { 10 | file: 'dist/mini-vdom.js', 11 | format: 'umd', 12 | name: 'MiniVdom' 13 | }, 14 | uglify: ifProduction, 15 | serve: !ifProduction 16 | ? { 17 | open: true 18 | } 19 | : null 20 | } 21 | ]); 22 | -------------------------------------------------------------------------------- /packages/mini-vdom/src/dev.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | position: relative; 6 | } 7 | 8 | * { 9 | box-sizing: border-box; 10 | } 11 | 12 | #app { 13 | width: 500px; 14 | margin: 50px auto; 15 | padding: 20px; 16 | border: 1px solid #ddd; 17 | } 18 | 19 | .todo-list { 20 | .title { 21 | text-align: center; 22 | } 23 | 24 | .input-row { 25 | display: flex; 26 | 27 | input { 28 | flex: 1; 29 | transition: 0.3s; 30 | padding: 0 6px; 31 | border-radius: 3px; 32 | border: 1px solid #ddd; 33 | height: 32px; 34 | outline: none; 35 | font-size: 15px; 36 | 37 | &:focus { 38 | border-color: transparent; 39 | box-shadow: 0 0 6px 1px #4cae4c; 40 | box-shadow: 0 0 6px 1px #2ad; 41 | } 42 | } 43 | } 44 | 45 | .tab-list { 46 | margin-top: 20px; 47 | display: flex; 48 | border-bottom: 1px dashed #ddd; 49 | 50 | .tab-item { 51 | flex: 1; 52 | cursor: pointer; 53 | padding: 10px; 54 | text-align: center; 55 | 56 | &.active { 57 | color: #2ad; 58 | } 59 | 60 | & + .tab-item { 61 | border-left: 1px dashed #ddd; 62 | } 63 | } 64 | } 65 | 66 | .list-wrap { 67 | margin: 0; 68 | padding: 0; 69 | li { 70 | list-style-type: none; 71 | display: flex; 72 | height: 40px; 73 | line-height: 40px; 74 | border-bottom: 1px dashed #ddd; 75 | cursor: pointer; 76 | 77 | .content { 78 | flex: 1; 79 | padding-left: 20px; 80 | } 81 | 82 | &.done { 83 | .content { 84 | text-decoration: line-through; 85 | color: #f00; 86 | } 87 | } 88 | 89 | .del { 90 | color: #2ad; 91 | border-left: 1px dashed #ddd; 92 | width: 60px; 93 | text-align: center; 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /packages/mini-vdom/src/dev.ts: -------------------------------------------------------------------------------- 1 | /*eslint-disable*/ 2 | import { h, patch, VNode } from './index'; 3 | import './dev.scss'; 4 | 5 | interface ITodoItem { 6 | content: string; 7 | done: boolean; 8 | } 9 | 10 | let todoList: ITodoItem[] = []; 11 | let showList: ITodoItem[] = []; 12 | let currentFilter = 0; // 0-全部 1-已完成 2-未完成 13 | 14 | try { 15 | todoList = JSON.parse(localStorage['todoList']); 16 | } catch { 17 | todoList = [ 18 | { content: '买彩票', done: true }, 19 | { content: '中大奖', done: false }, 20 | { content: '走上人生巅峰', done: false } 21 | ]; 22 | } 23 | 24 | function renderView(filter: number = currentFilter) { 25 | currentFilter = filter; 26 | if (filter === 1) { 27 | showList = todoList.filter(n => n.done); 28 | } else if (filter === 2) { 29 | showList = todoList.filter(n => !n.done); 30 | } else { 31 | showList = todoList; 32 | } 33 | render(); 34 | localStorage['todoList'] = JSON.stringify(todoList); 35 | } 36 | 37 | const render = (() => { 38 | let oldNode: any = document.getElementById('app'); 39 | let newNode: VNode = oldNode; 40 | 41 | return function () { 42 | oldNode = newNode; 43 | newNode = h('div#app.todo-list', [ 44 | h('h2.title', 'Todo List'), 45 | h('div.input-row', [ 46 | h('input[type=text][placeholder=请输入要做的事情,回车添加]', { 47 | on: { 48 | keyup(ev) { 49 | if (ev.keyCode === 13) { 50 | const target = ev.target as HTMLInputElement; 51 | const val = target.value.trim(); 52 | if (val.length) { 53 | todoList.push({ 54 | content: val, 55 | done: false 56 | }); 57 | renderView(); 58 | target.value = ''; 59 | } 60 | } 61 | } 62 | } 63 | }) 64 | ]), 65 | h('div.tab-list', [ 66 | h( 67 | 'div.tab-item', 68 | { 69 | attrs: { class: currentFilter === 0 ? 'active' : '' }, 70 | on: { click: () => renderView(0) } 71 | }, 72 | '全部' 73 | ), 74 | h( 75 | 'div.tab-item', 76 | { 77 | attrs: { class: currentFilter === 1 ? 'active' : '' }, 78 | on: { click: () => renderView(1) } 79 | }, 80 | '已完成' 81 | ), 82 | h( 83 | 'div.tab-item', 84 | { 85 | attrs: { class: currentFilter === 2 ? 'active' : '' }, 86 | on: { click: () => renderView(2) } 87 | }, 88 | '未完成' 89 | ) 90 | ]), 91 | h( 92 | 'ul.list-wrap', 93 | showList.map(item => 94 | h( 95 | 'li', 96 | { 97 | key: item.content, 98 | attrs: { 99 | 'data-content': item.content, 100 | class: item.done ? 'done' : '' 101 | } 102 | }, 103 | [ 104 | h( 105 | 'span.content', 106 | { 107 | on: { 108 | click: () => { 109 | item.done = !item.done; 110 | renderView(); 111 | } 112 | } 113 | }, 114 | item.content 115 | ), 116 | h( 117 | 'span.del', 118 | { 119 | on: { 120 | click() { 121 | const index = todoList.findIndex(n => n === item); 122 | todoList.splice(index, 1); 123 | renderView(); 124 | } 125 | } 126 | }, 127 | '删除' 128 | ) 129 | ] 130 | ) 131 | ) 132 | ) 133 | ]); 134 | 135 | patch(oldNode, newNode); 136 | }; 137 | })(); 138 | 139 | renderView(); 140 | -------------------------------------------------------------------------------- /packages/mini-vdom/src/index.ts: -------------------------------------------------------------------------------- 1 | import patch from './lib/patch'; 2 | import h from './lib/h'; 3 | import VNode from './lib/VNode'; 4 | 5 | export { h, patch, VNode }; 6 | -------------------------------------------------------------------------------- /packages/mini-vdom/src/lib/VNode.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * VNode 实现,虚拟dom节点 3 | */ 4 | 5 | import { IProps } from './modules/props'; 6 | import { IAttrs } from './modules/attrs'; 7 | import { IListener } from './modules/events'; 8 | 9 | interface IVnodeHook { 10 | create?: () => void; 11 | insert?: () => void; 12 | update?: () => void; 13 | destroy?: () => void; 14 | } 15 | 16 | export interface IVNodeData { 17 | key?: string; 18 | 19 | props?: IProps; 20 | 21 | attrs?: IAttrs; 22 | 23 | on?: IListener; 24 | 25 | hook?: IVnodeHook; 26 | 27 | ns?: string; 28 | } 29 | 30 | export default class VNode { 31 | public key: string; 32 | 33 | public type: string; 34 | 35 | public data: IVNodeData; 36 | 37 | public children?: VNode[]; 38 | 39 | public text?: string; 40 | 41 | public elm?: Element; 42 | 43 | constructor(type: string, data: IVNodeData = {}, children?: VNode[], text?: string, elm?: Element) { 44 | data.hook = data.hook || {}; // 初始化一下可以避免很多判断 45 | 46 | this.type = type; 47 | this.data = data; 48 | this.children = children; 49 | this.text = text; 50 | this.elm = elm; 51 | 52 | this.key = data.key; 53 | } 54 | 55 | /** 56 | * 是否是 VNode 57 | * 58 | * @static 59 | * @param {*} node 要判断的对象 60 | * @returns {boolean} 61 | * @memberof VNode 62 | */ 63 | public static isVNode(node: any): boolean { 64 | return node instanceof VNode; 65 | } 66 | 67 | /** 68 | * 是否是可复用的 VNode 对象 69 | * 判断依据是 key 跟 tagname 是否相同,既 对于相同类型dom元素尽可能复用 70 | * 71 | * @static 72 | * @param {VNode} oldVnode 73 | * @param {VNode} vnode 74 | * @returns {boolean} 75 | * @memberof VNode 76 | */ 77 | public static isSameVNode(oldVnode: VNode, vnode: VNode): boolean { 78 | return oldVnode.key === vnode.key && oldVnode.type === vnode.type; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/mini-vdom/src/lib/h.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 用于生成 vnode 的工厂函数 3 | */ 4 | 5 | import VNode, { IVNodeData } from './VNode'; 6 | import { getType, getMatchList } from '../utils'; 7 | 8 | /** 9 | * 设置元素及其children为svg元素 10 | * 11 | * @param {VNode} vnode 12 | * @returns {void} 13 | */ 14 | function setNS(vnode: VNode): void { 15 | if (!vnode.type) { 16 | return; 17 | } 18 | vnode.data.ns = 'http://www.w3.org/2000/svg'; 19 | if (vnode.children && vnode.children.length) { 20 | vnode.children.forEach(n => setNS(n)); 21 | } 22 | } 23 | 24 | /** 25 | * 生成 VNode 26 | * 27 | * @export 28 | * @param {string} type 标签(选择器)类型 'div#id.class[attr=val]' - 普通标签 '' - TextNode '!' - 注释节点 29 | * @param {string} [text] TextContent 30 | * @returns {VNode} 31 | */ 32 | export default function h(type: string, text?: string): VNode; 33 | 34 | /** 35 | * 生成 VNode 36 | * 37 | * @export 38 | * @param {string} type 标签(选择器)类型 'div#id.class[attr=val]' - 普通标签 '' - TextNode '!' - 注释节点 39 | * @param {VNode[]} [children] 子 VNode 数组 40 | * @returns {VNode} 41 | */ 42 | export default function h(type: string, children?: VNode[]): VNode; 43 | 44 | /** 45 | * 生成 VNode 46 | * 47 | * @export 48 | * @param {string} type 标签(选择器)类型 'div#id.class[attr=val]' - 普通标签 '' - TextNode '!' - 注释节点 49 | * @param {IVNodeData} [data] vnode 需要包含的数据 50 | * @param {string} [text] TextContent 51 | * @returns {VNode} 52 | */ 53 | export default function h(type: string, data?: IVNodeData, text?: string): VNode; 54 | 55 | /** 56 | * 生成 VNode 57 | * 58 | * @export 59 | * @param {string} type 标签(选择器)类型 'div#id.class[attr=val]' - 普通标签 '' - TextNode '!' - 注释节点 60 | * @param {IVNodeData} [data] vnode 需要包含的数据 61 | * @param {VNode[]} [children] 子 VNode 数组 62 | * @returns {VNode} 63 | */ 64 | export default function h(type: string, data?: IVNodeData, children?: VNode[]): VNode; 65 | 66 | export default function h(type: string, b?: any, c?: any): VNode { 67 | let data: IVNodeData; 68 | let text: string; 69 | let children: VNode[]; 70 | 71 | // 处理各个参数类型,用于重载 72 | const bType = getType(b); 73 | const cType = getType(c); 74 | 75 | if (bType === 'object') { 76 | data = b; 77 | if (cType === 'array') { 78 | children = c; 79 | } else if (cType === 'string') { 80 | text = c; 81 | } 82 | } else if (bType === 'array') { 83 | children = b; 84 | } else if (bType === 'string') { 85 | text = b; 86 | } 87 | 88 | // 针对 h('div','content') 的简写形式 89 | if (type && text != null) { 90 | children = [h('', text)]; 91 | text = undefined; 92 | } 93 | 94 | // 对于 div#id.class[attr='xxx'] 的形式 95 | if (type.length) { 96 | data = data || {}; 97 | 98 | // 1. 处理 id 99 | // eslint-disable-next-line 100 | const m = type.match(/#([^#\.\[\]]+)/); 101 | if (m) { 102 | data.props = data.props || {}; 103 | data.props.id = m[1]; 104 | } 105 | 106 | // 2. 处理 class 107 | // eslint-disable-next-line 108 | const classList = getMatchList(type, /\.([^#\.\[\]]+)/g).map(n => n[1]); 109 | if (classList.length) { 110 | data.attrs = data.attrs || {}; 111 | if (data.attrs['class']) { 112 | classList.push(...(data.attrs['class'] as string).split(' ').filter(n => n && n.length)); 113 | } 114 | data.attrs.class = classList.join(' '); 115 | } 116 | 117 | // 3. 处理 attrs 118 | const attrsList = getMatchList(type, /\[(\S+?)=(\S+?)\]/g); 119 | 120 | if (attrsList.length) { 121 | data.attrs = data.attrs || {}; 122 | attrsList.forEach(match => { 123 | data.attrs[match[1]] = match[2]; 124 | }); 125 | } 126 | 127 | type = type 128 | .replace(/(#|\.|\[)\S*/g, '') 129 | .toLowerCase() 130 | .trim(); 131 | } 132 | 133 | const vnode = new VNode(type, data, children, text); 134 | // 如果是svg元素,处理该元素及其子元素 135 | if (vnode.type === 'svg') { 136 | setNS(vnode); 137 | } 138 | return vnode; 139 | } 140 | -------------------------------------------------------------------------------- /packages/mini-vdom/src/lib/hooks.ts: -------------------------------------------------------------------------------- 1 | import VNode from './VNode'; 2 | 3 | export const hooks = ['create', 'insert', 'update', 'destroy', 'remove']; 4 | 5 | export type TModuleHookFunc = (oldVnode: VNode, vnode: VNode) => void; 6 | 7 | export interface IModuleHook { 8 | create?: TModuleHookFunc; 9 | 10 | insert?: TModuleHookFunc; 11 | 12 | update?: TModuleHookFunc; 13 | 14 | destroy?: TModuleHookFunc; 15 | 16 | remove?: TModuleHookFunc; 17 | } 18 | -------------------------------------------------------------------------------- /packages/mini-vdom/src/lib/modules/attrs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * attributes 模块 3 | * 用于处理 id、class、style、dataset、自定义属性等 4 | */ 5 | 6 | import VNode from '../VNode'; 7 | import { IModuleHook } from '../hooks'; 8 | 9 | /** 10 | * attribute 包装 11 | */ 12 | 13 | export interface IAttrs { 14 | [key: string]: string /*| number | boolean*/; 15 | } 16 | 17 | export function updateAttrs(oldVnode: VNode, vnode: VNode): void { 18 | let oldAttrs = oldVnode.data.attrs; 19 | let attrs = vnode.data.attrs; 20 | const elm = vnode.elm; 21 | 22 | // 两个vnode都不存在 attrs 23 | if (!oldAttrs && !attrs) return; 24 | // 两个 attrs 是相同的 25 | if (oldAttrs === attrs) return; 26 | 27 | oldAttrs = oldAttrs || {}; 28 | attrs = attrs || {}; 29 | 30 | // 更新 attrs 31 | for (const key in attrs) { 32 | const cur = attrs[key]; 33 | const old = oldAttrs[key]; 34 | // 相同就跳过 35 | if (cur === old) continue; 36 | // 不同就更新 37 | elm.setAttribute(key, cur + ''); 38 | } 39 | 40 | // 对于 oldAttrs 中有,而 attrs 没有的项,去掉 41 | for (const key in oldAttrs) { 42 | if (!(key in attrs)) { 43 | elm.removeAttribute(key); 44 | } 45 | } 46 | } 47 | 48 | export const attrsModule: IModuleHook = { 49 | create: updateAttrs, 50 | update: updateAttrs 51 | }; 52 | 53 | export default attrsModule; 54 | -------------------------------------------------------------------------------- /packages/mini-vdom/src/lib/modules/events.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * events 模块 3 | * 处理绑定的所有事件 4 | */ 5 | 6 | import { IModuleHook } from '../hooks'; 7 | import VNode from '../VNode'; 8 | 9 | export type IListener = { 10 | [key in keyof HTMLElementEventMap]?: (event: HTMLElementEventMap[key]) => void; 11 | } & { 12 | [key: string]: EventListener; 13 | }; 14 | 15 | function updateEventListener(oldVnode: VNode, vnode: VNode): void { 16 | // 旧的监听、元素 17 | const oldOn = oldVnode.data.on; 18 | const oldElm = oldVnode.elm; 19 | 20 | // 新的监听、元素 21 | const on = vnode.data.on; 22 | const elm = vnode.elm; 23 | 24 | // 监听器没有改变,在 vnode 也没变的情况下会出现 25 | if (oldOn === on) { 26 | return; 27 | } 28 | 29 | // 改变之后,就直接把旧的监听全部删掉 30 | if (oldOn) { 31 | for (const event in oldOn) { 32 | oldElm.removeEventListener(event, oldOn[event]); 33 | } 34 | } 35 | 36 | if (on) { 37 | for (const event in on) { 38 | elm.addEventListener(event, on[event]); 39 | } 40 | } 41 | } 42 | 43 | export const EventModule: IModuleHook = { 44 | create: updateEventListener, 45 | update: updateEventListener, 46 | destroy: updateEventListener 47 | }; 48 | 49 | export default EventModule; 50 | -------------------------------------------------------------------------------- /packages/mini-vdom/src/lib/modules/props.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * props 包装 3 | * 用于处理节点自身属性,跟attrs有些重合,主要处理难以用 attr 来表示的属性 4 | * 5 | * @example 6 | * input.checked .disabled 7 | */ 8 | 9 | import VNode from '../VNode'; 10 | import { IModuleHook } from '../hooks'; 11 | 12 | export interface IProps { 13 | [key: string]: any; 14 | } 15 | 16 | export function updateProp(oldVnode: VNode, vnode: VNode): void { 17 | let oldProps = oldVnode.data.props; 18 | let props = vnode.data.props; 19 | const elm = vnode.elm; 20 | 21 | // 两个vnode都不存在props 22 | if (!oldProps && !props) return; 23 | // 两个props是相同的 24 | if (oldProps === props) return; 25 | 26 | oldProps = oldProps || {}; 27 | props = props || {}; 28 | 29 | // 如果old有,cur没有 30 | for (const key in oldProps) { 31 | if (!props[key]) { 32 | delete elm[key]; 33 | } 34 | } 35 | 36 | // 检查更新 37 | for (const key in props) { 38 | if (props[key] !== oldProps[key]) { 39 | elm[key] = props[key]; 40 | } 41 | } 42 | } 43 | 44 | export const propsModule: IModuleHook = { 45 | create: updateProp, 46 | update: updateProp 47 | }; 48 | 49 | export default propsModule; 50 | -------------------------------------------------------------------------------- /packages/mini-vdom/src/lib/patch.ts: -------------------------------------------------------------------------------- 1 | import VNode from './VNode'; 2 | import attrsModule from './modules/attrs'; 3 | import propsModule from './modules/props'; 4 | import eventModule from './modules/events'; 5 | import { hooks, IModuleHook, TModuleHookFunc } from './hooks'; 6 | 7 | const emptyVnode = new VNode(''); 8 | 9 | function patchFactory(modules: IModuleHook[] = []): (oldVnode: any, vnode: VNode) => VNode { 10 | // modules 的所有的钩子 11 | const cbs: Record = { 12 | create: [], 13 | insert: [], 14 | update: [], 15 | destroy: [], 16 | remove: [] 17 | }; 18 | 19 | // 把各个module的钩子注入进去 20 | for (const item of modules) { 21 | hooks.forEach(hookKey => item[hookKey] && cbs[hookKey].push(item[hookKey])); 22 | } 23 | 24 | function createElm(vnode: VNode): Element { 25 | // 注释节点 26 | if (vnode.type === '!') { 27 | vnode.elm = (document.createComment(vnode.text) as any) as Element; 28 | } 29 | // 普通节点 30 | else if (vnode.type) { 31 | vnode.elm = vnode.data.ns 32 | ? document.createElementNS(vnode.data.ns, vnode.type) 33 | : document.createElement(vnode.type); 34 | 35 | // 如果有children,递归 36 | vnode.children && 37 | vnode.children.forEach(child => { 38 | vnode.elm.appendChild(createElm(child)); 39 | }); 40 | 41 | // 如果只有innertext,这是个hook,可以让 h 在创建的时候更方便 42 | // 这个时候不应该有 children 43 | if (vnode.text && vnode.text.length) { 44 | vnode.elm.appendChild(document.createTextNode(vnode.text)); 45 | } 46 | } 47 | // textNode 48 | else { 49 | vnode.elm = (document.createTextNode(vnode.text) as any) as Element; 50 | } 51 | 52 | // create 钩子 53 | cbs.create.forEach(hook => hook(emptyVnode, vnode)); 54 | vnode.data.hook.create && vnode.data.hook.create(); 55 | return vnode.elm; 56 | } 57 | 58 | function addVnodes( 59 | parentElm: Node, 60 | before: Node, 61 | vnodes: VNode[], 62 | startIndex = 0, 63 | endIndex = vnodes.length - 1 64 | ): void { 65 | for (; startIndex <= endIndex; startIndex++) { 66 | const vnode = vnodes[startIndex]; 67 | parentElm.insertBefore(createElm(vnode), before); 68 | cbs.insert.forEach(hook => hook(emptyVnode, vnode)); 69 | vnode.data.hook.insert && vnode.data.hook.insert(); 70 | } 71 | } 72 | 73 | function removeVnodes(parentElm: Node, vnodes: VNode[], startIndex = 0, endIndex = vnodes.length - 1): void { 74 | for (; startIndex <= endIndex; startIndex++) { 75 | const vnode = vnodes[startIndex]; 76 | parentElm && parentElm.removeChild(vnode.elm); 77 | cbs.destroy.forEach(hook => hook(vnode, emptyVnode)); 78 | vnode.data.hook.destroy && vnode.data.hook.destroy(); 79 | } 80 | } 81 | 82 | function updateChildren(parentElm: Element, oldChildren: VNode[], children: VNode[]): void { 83 | // 方式一: 84 | // 如果想无脑点可以直接这样,不复用dom,直接把所有children都更新 85 | // removeVnodes(parentElm, oldChildren); 86 | // addVnodes(parentElm, null, children); 87 | // return; 88 | 89 | // 方式二: 90 | // 顺序依次找到可复用的元素 91 | // 对于大批量列表,从 上、中 部进行 添加、删除 操作效率上稍微不太友好,并不是最佳方式 92 | // 有时候多次操作后的结果是元素没有移动,但还是会按照操作步骤来一遍 93 | // 如果先在内存中把所有的位置,移动等都计算好,然后再进行操作,效率会更高。 94 | 95 | // const oldMirror = oldChildren.slice(); // 用来表示哪些oldchildren被用过,位置信息等 96 | // for (let i = 0; i < children.length; i++) { 97 | // // 当前vnode 98 | // const vnode = children[i]; 99 | // // 可以被复用的vnode的索引 100 | // const oldVnodeIndex = oldMirror.findIndex(node => { 101 | // return node && VNode.isSameVNode(node, vnode); 102 | // }); 103 | // // 如果有vnode可以复用 104 | // if (~oldVnodeIndex) { 105 | // // console.log(oldVnodeIndex); 106 | // const oldVnode = oldMirror[oldVnodeIndex]; 107 | 108 | // // 把之前的置空,表示已经用过。之后仍然存留的要被删除 109 | // oldMirror[oldVnodeIndex] = undefined; 110 | // // 调整顺序(如果旧的索引对不上新索引) 111 | // if (oldVnodeIndex !== i) { 112 | // parentElm.insertBefore(oldVnode.elm, parentElm.childNodes[i + 1]); 113 | // } 114 | // // 比较数据,进行更新 115 | // // eslint-disable-next-line 116 | // patchVNode(oldVnode, vnode); 117 | // } 118 | // // 不能复用就创建新的 119 | // else { 120 | // addVnodes(parentElm, parentElm.childNodes[i + 1], [vnode]); 121 | // } 122 | // } 123 | 124 | // // 删除oldchildren中未被复用的部分 125 | // const rmVnodes = oldMirror.filter(n => !!n); 126 | 127 | // rmVnodes.length && removeVnodes(parentElm, rmVnodes); 128 | 129 | // 方式三: 130 | // 类 snabbdom 的 diff 算法 131 | 132 | let oldStartIndex = 0; 133 | let oldStartVNode = oldChildren[oldStartIndex]; 134 | let oldEndIndex = oldChildren.length - 1; 135 | let oldEndVNode = oldChildren[oldEndIndex]; 136 | 137 | let newStartIndex = 0; 138 | let newStartVNode = children[newStartIndex]; 139 | let newEndIndex = children.length - 1; 140 | let newEndVNode = children[newEndIndex]; 141 | 142 | while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) { 143 | switch (true) { 144 | // 1. 先校验2个 old start/end vnode 是否为空,当为`undefined`的时候,表示被其它地方复用了 145 | // 对 new start/end vnode 也做处理,是因为可能移动后根本没子节点 146 | case !oldStartVNode: 147 | oldStartVNode = oldChildren[++oldStartIndex]; 148 | break; 149 | case !oldEndVNode: 150 | oldEndVNode = oldChildren[--oldEndIndex]; 151 | break; 152 | case !newStartVNode: 153 | newStartVNode = children[++newStartIndex]; 154 | break; 155 | case !newEndVNode: 156 | newEndVNode = oldChildren[--newEndIndex]; 157 | break; 158 | 159 | // 2. 首首、尾尾 对比, 适用于 普通的 插入、删除 节点 160 | // 首首比较 161 | case VNode.isSameVNode(oldStartVNode, newStartVNode): 162 | patchVNode(oldStartVNode, newStartVNode); 163 | oldStartVNode = oldChildren[++oldStartIndex]; 164 | newStartVNode = children[++newStartIndex]; 165 | break; 166 | 167 | // 尾尾比较 168 | case VNode.isSameVNode(oldEndVNode, newEndVNode): 169 | patchVNode(oldEndVNode, newEndVNode); 170 | oldEndVNode = oldChildren[--oldEndIndex]; 171 | newEndVNode = children[--newEndIndex]; 172 | break; 173 | 174 | // 3. 旧尾=》新头,旧头=》新尾, 适用于移动了某个节点的情况 175 | // 旧尾=》新头,把节点左移 176 | // [1, 2, 3] 177 | // [3, 1, 2] 178 | case VNode.isSameVNode(oldEndVNode, newStartVNode): 179 | parentElm.insertBefore(oldEndVNode.elm, parentElm.childNodes[newStartIndex]); 180 | patchVNode(oldEndVNode, newStartVNode); 181 | oldEndVNode = oldChildren[--oldEndIndex]; 182 | newStartVNode = children[++newStartIndex]; 183 | break; 184 | 185 | // 旧头=》新尾,把节点右移 186 | // [1, 2, 3] 187 | // [2, 3, 1] 188 | case VNode.isSameVNode(oldStartVNode, newEndVNode): 189 | parentElm.insertBefore(oldEndVNode.elm, oldEndVNode.elm.nextSibling); 190 | patchVNode(oldStartVNode, newEndVNode); 191 | oldStartVNode = oldChildren[++oldStartIndex]; 192 | newEndVNode = children[--newEndIndex]; 193 | break; 194 | 195 | // 4. 交叉比较 196 | // 不属于数组常规操作,比如这种情况: 197 | // old: [1, 2, 3, 4] 198 | // new: [1, 5, 2, 3, 6, 4] 199 | // 当然理想的状态下,是 5跟6 重新生成,其它的直接复用 200 | // 这个时候 5 会复用 2,2 会复用 3,6 会重新生成。 201 | // 只处理 newStartVNode 202 | default: 203 | // 可以被复用的vnode的索引 204 | const oldVnodeIndex = oldChildren.findIndex((node, index) => { 205 | return ( 206 | // 索引在 oldStartIndex ~ oldEndIndex 207 | // 之前没有被复用过 208 | // 并且可以被复用 209 | index >= oldStartIndex && 210 | index <= oldEndIndex && 211 | node && 212 | VNode.isSameVNode(node, newStartVNode) 213 | ); 214 | }); 215 | // 如果有vnode可以复用 216 | if (~oldVnodeIndex) { 217 | const oldVnode = oldChildren[oldVnodeIndex]; 218 | 219 | // 把之前的置空,表示已经用过。之后仍然存留的要被删除 220 | oldChildren[oldVnodeIndex] = undefined; 221 | // 调整顺序(如果旧的索引对不上新索引) 222 | if (oldVnodeIndex !== newStartIndex) { 223 | parentElm.insertBefore(oldVnode.elm, parentElm.childNodes[newStartIndex]); 224 | } 225 | // 比较数据,进行更新 226 | patchVNode(oldVnode, newStartVNode); 227 | } 228 | // 不能复用就创建新的 229 | // old: [1, 2, 3, 4] 230 | // new: [1, 5, 2, 3, 6, 4] 231 | else { 232 | addVnodes(parentElm, parentElm.childNodes[newStartIndex], [newStartVNode]); 233 | } 234 | 235 | // 新头 向右移动一位 236 | newStartVNode = children[++newStartIndex]; 237 | break; 238 | } 239 | } 240 | 241 | // 如果循环完毕,还有 oldStartIndex ~ oldEndIndex || newStartIndex ~ newEndIndex 之间还有空余, 242 | // 表示有旧节点未被删除干净,或者新节点没有完全添加完毕 243 | 244 | // 旧的 vnodes 遍历完,新的没有 245 | // 表示有新的没有添加完毕 246 | if (oldStartIndex > oldEndIndex && newStartIndex <= newEndIndex) { 247 | addVnodes(parentElm, children[newEndIndex + 1]?.elm, children.slice(newStartIndex, newEndIndex + 1)); 248 | } 249 | // 新的 vnodes 遍历完,旧的没有 250 | // 表示有旧的没有删除干净 251 | else if (oldStartIndex <= oldEndIndex && newStartIndex > newEndIndex) { 252 | removeVnodes( 253 | parentElm, 254 | oldChildren.slice(oldStartIndex, oldEndIndex + 1).filter(n => !!n) 255 | ); 256 | } 257 | } 258 | 259 | /** 260 | * patch oldVnode 和 vnode ,他们自身相同,但是可能其它属性或者children有变动 261 | * 262 | * @param {VNode} oldVnode 旧的vnode 263 | * @param {VNode} vnode 新的vnode 264 | */ 265 | function patchVNode(oldVnode: VNode, vnode: VNode): void { 266 | const elm = (vnode.elm = oldVnode.elm); 267 | const oldChildren = oldVnode.children; 268 | const children = vnode.children; 269 | 270 | // 相同的vnode,直接返回。 271 | if (oldVnode === vnode) return; 272 | 273 | // 注释节点不考虑 274 | 275 | // 如果是文本节点 276 | // 不需要钩子,至少以某标签为单位 277 | if (vnode.text !== undefined && vnode.text !== oldVnode.text) { 278 | elm.textContent = vnode.text; 279 | return; 280 | } 281 | 282 | // 有 children 的情况 283 | 284 | // 新老节点都有 children,且不相同的情况下,去对比 新老children,并更新 285 | if (oldChildren && children) { 286 | if (oldChildren !== children) { 287 | // console.log('all children'); 288 | updateChildren(elm, oldChildren, children); 289 | } 290 | } 291 | // 只有新的有children,则以前的是text节点 292 | else if (children) { 293 | // console.log('only new children'); 294 | // 先去掉可能存在的textcontent 295 | oldVnode.text && (elm.textContent = ''); 296 | addVnodes(elm, null, children); 297 | } 298 | // 只有旧的有children,则现在的是text节点 299 | else if (oldChildren) { 300 | // console.log('only old children'); 301 | removeVnodes(elm, oldChildren); 302 | vnode.text && (elm.textContent = vnode.text); 303 | } 304 | // 都没有children,则只改变了textContent 305 | // 不过在 h 函数中添加了处理,现在不会出现这种情况了 306 | else if (oldVnode.text !== vnode.text) { 307 | elm.textContent = vnode.text; 308 | } 309 | 310 | cbs.update.forEach(hook => hook(oldVnode, vnode)); 311 | vnode.data.hook.update && vnode.data.hook.update(); 312 | } 313 | 314 | /** 315 | * 创建/更新 vnode 316 | * 317 | * @param {*} oldVnode dom节点或者旧的vnode 318 | * @param {VNode} vnode 新的vnode 319 | * @returns {VNode} 320 | */ 321 | function patch(oldVnode: any, vnode: VNode): VNode { 322 | // 如果是dom对象,即初始化的时候 323 | if (!VNode.isVNode(oldVnode)) { 324 | oldVnode = new VNode( 325 | '', // 这里使dom不复用,触发生命周期钩子 326 | // (oldVnode as HTMLElement).tagName.toLowerCase(), 327 | {}, 328 | [], 329 | undefined, 330 | oldVnode 331 | ); 332 | } 333 | 334 | // 比较2个vnode是否是可复用的vnode,如果可以,就patch 335 | // 是否是相同的 VNode 对象 判断依据是 key 跟 tagname 是否相同,既 对于相同类型dom元素尽可能复用 336 | if (VNode.isSameVNode(oldVnode, vnode)) { 337 | patchVNode(oldVnode, vnode); 338 | } 339 | // 如果不是同一个vnode,把旧的删了创建新的 340 | else { 341 | const elm = oldVnode.elm as Element; 342 | addVnodes(elm.parentNode, elm, [vnode]); 343 | 344 | removeVnodes(elm.parentNode, [oldVnode]); 345 | 346 | // insert hook 347 | cbs.insert.forEach(hook => hook(oldVnode, vnode)); 348 | } 349 | 350 | return vnode; 351 | } 352 | 353 | return patch; 354 | } 355 | 356 | export default patchFactory([attrsModule, propsModule, eventModule]); 357 | -------------------------------------------------------------------------------- /packages/mini-vdom/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 工具库 3 | */ 4 | 5 | /** 6 | * 获取数据类型 7 | * 8 | * @export 9 | * @param {*} sender 要判断的数据 10 | * @returns {string} 11 | */ 12 | export function getType(sender: any): string { 13 | return Object.prototype.toString 14 | .call(sender) 15 | .toLowerCase() 16 | .match(/\s(\S+?)\]/)[1]; 17 | } 18 | 19 | /** 20 | * 获取每个匹配项以及子组,返回的是一个二维数组 21 | * 22 | * @export 23 | * @param {string} content 24 | * @param {RegExp} reg 25 | * @returns {string[][]} 26 | */ 27 | export function getMatchList(content: string, reg: RegExp): string[][] { 28 | let m: RegExpExecArray; 29 | const list: string[][] = []; 30 | while ((m = reg.exec(content))) { 31 | list.push([].slice.call(m)); 32 | } 33 | return list; 34 | } 35 | -------------------------------------------------------------------------------- /packages/mini-vdom/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", // 目标代码类型 4 | "module": "esnext", // 指定生成哪个模块系统代码 5 | "noImplicitAny": false, // 在表达式和声明上有隐含的'any'类型时报错。 6 | "sourceMap": false, // 用于debug 7 | // "rootDir": "./src", // 仅用来控制输出的目录结构--outDir。 8 | // "outDir": "./dist", // 重定向输出目录。 9 | "esModuleInterop": true, 10 | "declaration": true, 11 | "moduleResolution": "node", 12 | "watch": false // 在监视模式下运行编译器。会监视输出文件,在它们改变时重新编译。 13 | }, 14 | "include": [ 15 | "./src/**/*" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | BASE_PATH=`cd $(dirname $0);pwd -P` 4 | BASE_PATH=`cd $(dirname $BASE_PATH);pwd -P` 5 | 6 | cd $BASE_PATH 7 | 8 | npm run build:lp 9 | 10 | rm -rf dist 11 | 12 | mkdir dist 13 | 14 | cp packages/mini-mvvm/dist/mini-mvvm.js dist/mini-mvvm.js 15 | cp packages/mini-vdom/dist/mini-vdom.js dist/mini-vdom.js 16 | 17 | echo "--- build 完毕 >_<#@! ---" 18 | --------------------------------------------------------------------------------