├── .prettierignore
├── .npmrc
├── packages
├── mini-mvvm
│ ├── .npmrc
│ ├── src
│ │ ├── lib
│ │ │ ├── Compile
│ │ │ │ ├── index.ts
│ │ │ │ ├── parsers
│ │ │ │ │ ├── parseIf.ts
│ │ │ │ │ ├── parseProps.ts
│ │ │ │ │ ├── parseModel.ts
│ │ │ │ │ ├── parseEvents.ts
│ │ │ │ │ ├── parseFor.ts
│ │ │ │ │ └── parseAttrs.ts
│ │ │ │ ├── AST.ts
│ │ │ │ └── Compile.ts
│ │ │ ├── Dep.ts
│ │ │ ├── ELifeCycle.ts
│ │ │ ├── Observer.ts
│ │ │ └── Watcher.ts
│ │ ├── common
│ │ │ ├── enums.ts
│ │ │ ├── utils.ts
│ │ │ └── EventEmitter.ts
│ │ ├── core
│ │ │ ├── BaseMVVM.ts
│ │ │ └── MVVM.ts
│ │ ├── dev.scss
│ │ └── dev.ts
│ ├── jest.config.js
│ ├── index.html
│ ├── tsconfig.json
│ ├── rollup.config.js
│ ├── package.json
│ ├── __tests__
│ │ └── lifecycle.test.ts
│ └── README.md
└── mini-vdom
│ ├── .npmrc
│ ├── jest.config.js
│ ├── src
│ ├── index.ts
│ ├── lib
│ │ ├── hooks.ts
│ │ ├── modules
│ │ │ ├── props.ts
│ │ │ ├── events.ts
│ │ │ └── attrs.ts
│ │ ├── VNode.ts
│ │ ├── h.ts
│ │ └── patch.ts
│ ├── utils
│ │ └── index.ts
│ ├── dev.scss
│ └── dev.ts
│ ├── index.html
│ ├── tsconfig.json
│ ├── rollup.config.js
│ ├── package.json
│ ├── __tests__
│ ├── h.test.ts
│ ├── hook.test.ts
│ └── patch.test.ts
│ └── README.md
├── .huskyrc
├── .eslintignore
├── .travis.yml
├── lerna.json
├── .eslintrc.js
├── scripts
└── build.sh
├── .prettierrc
├── .github
└── workflows
│ └── ci.yml
├── package.json
├── .gitignore
├── README.md
└── dist
├── mini-vdom.js
└── mini-mvvm.js
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npm.taobao.org
2 |
--------------------------------------------------------------------------------
/packages/mini-mvvm/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npm.taobao.org
2 |
--------------------------------------------------------------------------------
/packages/mini-vdom/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npm.taobao.org
2 |
--------------------------------------------------------------------------------
/.huskyrc:
--------------------------------------------------------------------------------
1 | {
2 | "hooks": {
3 | "pre-commit": "npm run lint"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | build
4 | __tests__
5 | scripts
6 | *.d.ts
7 | *.html
8 |
--------------------------------------------------------------------------------
/packages/mini-mvvm/src/lib/Compile/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Compile 比较复杂,单独用一个文件夹存放相关内容
3 | */
4 |
5 | export { default } from './Compile';
6 |
--------------------------------------------------------------------------------
/packages/mini-mvvm/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | verbose: true,
4 | testEnvironment: 'node'
5 | };
6 |
--------------------------------------------------------------------------------
/packages/mini-vdom/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | verbose: true,
4 | testEnvironment: 'node'
5 | };
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": ["packages/*"],
3 | "version": "independent",
4 | "command": {
5 | "bootstrap": {
6 | "registry": "https://registry.npm.taobao.org"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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-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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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-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/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/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-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-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-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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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-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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "root",
3 | "private": true,
4 | "scripts": {
5 | "clean": "lerna clean --yes",
6 | "bootstrap": "lerna bootstrap",
7 | "test:mini-vdom": "lerna exec npm run test --scope=mini-vdom",
8 | "test:mini-mvvm": "lerna exec npm run test --scope=mini-mvvm",
9 | "test": "lerna exec npm run test",
10 | "dev:mini-mvvm": "lerna run dev --scope=mini-mvvm --stream",
11 | "dev:mini-vdom": "lerna run dev --scope=mini-vdom --stream",
12 | "build:lp": "lerna run build",
13 | "build": "sh scripts/build.sh",
14 | "lp": "lerna publish",
15 | "lint": "eslint . --ext .ts",
16 | "fix": "eslint . --ext .ts --fix"
17 | },
18 | "devDependencies": {
19 | "@nosaid/eslint-config-for-typescript": "0.0.7",
20 | "@nosaid/jest": "0.0.4",
21 | "@nosaid/rollup": "0.0.17",
22 | "cross-env": "^7.0.2",
23 | "husky": "^4.2.5",
24 | "lerna": "^3.21.0",
25 | "typescript": "^3.9.2"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/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-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-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/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-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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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-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-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-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-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/README.md:
--------------------------------------------------------------------------------
1 | # mini-vdom
2 |
3 | [](https://www.npmjs.com/package/mini-vdom)
4 | [](https://www.npmjs.com/package/mini-vdom)
5 | [](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/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-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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # mini-mvvm
2 |
3 | [](https://www.npmjs.com/package/mini-mvvm)
4 | [](https://www.npmjs.com/package/mini-mvvm)
5 | [](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 |
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/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/README.md:
--------------------------------------------------------------------------------
1 | # mini-mvvm
2 |
3 | [](https://www.npmjs.com/package/mini-mvvm)
4 | [](https://www.npmjs.com/package/mini-mvvm)
5 | [](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 |
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/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-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-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-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/__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-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/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-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-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 |
--------------------------------------------------------------------------------
/dist/mini-vdom.js:
--------------------------------------------------------------------------------
1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).MiniVdom={})}(this,function(e){"use strict";function r(e,t){for(var n=0;ne.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} [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/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 |
24 |
25 |
26 |
双向绑定(m-model):
27 |
47 |
48 |
49 |
计算属性(computed):
50 |
55 |
56 |
57 |
条件渲染(m-if)
58 |
69 |
70 |
71 |
循环(m-for),嵌套for循环,99乘法表(尝试点击):
72 |
73 |
74 |
75 | |
79 | {{ item }}
80 | |
81 |
82 |
83 |
84 |
85 |
86 |
Todo List
87 |
watch了list,任何操作都会保存在localstorage
88 |
89 |
95 |
96 |
97 |
98 |
103 | {{ item }}
104 |
105 |
106 |
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-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 |
--------------------------------------------------------------------------------
/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}}}}(0