├── .gitignore ├── README.md └── content ├── LazyMan-next.js ├── LazyMan.js ├── LazyMan.md ├── javascript-standard.md ├── mac-keyboard-mouse.md ├── react-and-nginx.md ├── vue-filters.md ├── vue-mixins.md ├── vue-next-tick.md ├── vue-router-guard.md ├── vue-standard.md ├── vuex-class.md ├── vuex-devtool.md ├── vuex-helpers.md ├── vuex-init.md ├── vuex-questions.md ├── vuex-store.md ├── vuex-util.md ├── 前端文章集锦 ├── 2017.md ├── 2018.md └── 2019.md ├── 年度总结 └── 我的 2018.md └── 源码注释 ├── lodash ├── README.md ├── base.md ├── build.md ├── lodash.md └── methods │ ├── Array-c~h.md │ ├── Array-i~s.md │ ├── Array-t~z.md │ ├── Array.md │ ├── Collection.md │ ├── Date.md │ ├── Function-bind.md │ ├── Function-debounce.md │ ├── Function.md │ ├── Lang-clone.md │ ├── Lang.md │ ├── MapCache.md │ ├── Math.md │ ├── Number.md │ ├── Object-a~f.md │ ├── Object-g~v.md │ ├── Properties.md │ ├── Seq.md │ ├── String-template.md │ ├── String.md │ └── Util.md ├── vue-router ├── components │ ├── link.js │ └── view.js ├── create-matcher.js ├── create-route-map.js ├── history │ ├── abstract.js │ ├── base.js │ ├── hash.js │ └── html5.js ├── index.js ├── install.js └── util │ ├── async.js │ ├── dom.js │ ├── location.js │ ├── misc.js │ ├── params.js │ ├── path.js │ ├── push-state.js │ ├── query.js │ ├── resolve-components.js │ ├── route.js │ ├── scroll.js │ └── warn.js └── vuex ├── helpers.js ├── index.esm.js ├── index.js ├── mixin.js ├── module ├── module-collection.js └── module.js ├── plugins ├── devtool.js └── logger.js ├── store.js └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | .history 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## blog 2 | 3 | **纸上得来终觉浅, 绝知此事要躬行** 4 | 5 | ### 数据结构与算法 6 | 7 | - [LeetCode-Notes](https://github.com/zhanghao-zhoushan/LeetCode-Notes) 8 | 9 | ### Webpack 10 | 11 | - [Webpack HMR](https://slides.com/hao-zhang/deck-57dcb4) 12 | 13 | ### Vue 14 | 15 | - [vue mixins 让组件显得不再臃肿](https://github.com/zhanghao-zhoushan/blog/blob/master/content/vue-mixins.md) 16 | - [vue router 的导航守卫](https://github.com/zhanghao-zhoushan/blog/blob/master/content/vue-router-guard.md) 17 | - [vue 更舒适的开发环境](https://github.com/zhanghao-zhoushan/blog/blob/master/content/vue-standard.md) 18 | - [vue nextTick 源码浅析](https://github.com/zhanghao-zhoushan/blog/blob/master/content/vue-next-tick.md) 19 | - [vuex 入口文件](https://github.com/zhanghao-zhoushan/blog/blob/master/content/vuex-init.md) 20 | - [vuex class Store](https://github.com/zhanghao-zhoushan/blog/blob/master/content/vuex-store.md) 21 | - [vuex class ModuleCollection and class Module](https://github.com/zhanghao-zhoushan/blog/blob/master/content/vuex-class.md) 22 | - [vuex 辅助工具函数的实践与解析](https://github.com/zhanghao-zhoushan/blog/blob/master/content/vuex-helpers.md) 23 | - [vuex 工具函数](https://github.com/zhanghao-zhoushan/blog/blob/master/content/vuex-util.md) 24 | - [vuex 插件](https://github.com/zhanghao-zhoushan/blog/blob/master/content/vuex-devtool.md) 25 | - [vuex 问题总结](https://github.com/zhanghao-zhoushan/blog/blob/master/content/vuex-questions.md) 26 | 27 | ### React 28 | 29 | - [Bing Pictures use typescript](https://github.com/zhanghao-zhoushan/bing-app) 30 | - [react 项目在 nginx 的部署](https://github.com/zhanghao-zhoushan/blog/blob/master/content/react-and-nginx.md) 31 | 32 | ### JavaScript 33 | 34 | - [LazyMan 的两种实现](https://github.com/zhanghao-zhoushan/blog/blob/master/content/LazyMan.md) 35 | - [snabbdom 的源码注释](https://github.com/zhanghao-zhoushan/snabbdom/tree/master/src) 36 | 37 | ### 前端文章集锦 38 | 39 | - [2017 Collect](https://github.com/zhanghao-zhoushan/blog/blob/master/content/前端文章集锦/2017.md) 40 | - [2018 Collect](https://github.com/zhanghao-zhoushan/blog/blob/master/content/前端文章集锦/2018.md) 41 | - [2019 Collect](https://github.com/zhanghao-zhoushan/blog/blob/master/content/前端文章集锦/2019.md) 42 | 43 | ### Other 44 | 45 | - [前端代码规范](https://github.com/zhanghao-zhoushan/blog/blob/master//content/javascript-standard.md) 46 | -------------------------------------------------------------------------------- /content/LazyMan-next.js: -------------------------------------------------------------------------------- 1 | class _LazyMan { 2 | constructor(name) { 3 | this.tasks = [] 4 | this.lazyMan(name) 5 | setTimeout(() => { 6 | this.next() 7 | }) 8 | } 9 | 10 | next() { 11 | const fn = this.tasks.shift() 12 | fn && fn() 13 | } 14 | 15 | lazyMan(name) { 16 | const fn = () => { 17 | this._log(`Hi! This is ${name}!`) 18 | this.next() 19 | } 20 | this.tasks.push(fn) 21 | return this 22 | } 23 | 24 | eat(some) { 25 | const fn = () => { 26 | this._log(`Eat ${some}~`) 27 | this.next() 28 | } 29 | this.tasks.push(fn) 30 | return this 31 | } 32 | 33 | sleep(time) { 34 | const fn = () => { 35 | setTimeout(() => { 36 | this._log(`Wake up after ${time}`) 37 | this.next() 38 | }, time * 1000) 39 | } 40 | this.tasks.push(fn) 41 | return this 42 | } 43 | 44 | sleepFirst(time) { 45 | const fn = () => { 46 | setTimeout(() => { 47 | this._log(`Wake up after ${time}`) 48 | this.next() 49 | }, time * 1000) 50 | } 51 | this.tasks.unshift(fn) 52 | return this 53 | } 54 | 55 | _log (message) { 56 | console.log(message) 57 | } 58 | } 59 | 60 | function LazyMan(name) { 61 | return new _LazyMan(name) 62 | } 63 | 64 | LazyMan('lulu') 65 | .sleep(5) 66 | .eat('dinner') 67 | .eat('supper') 68 | -------------------------------------------------------------------------------- /content/LazyMan.js: -------------------------------------------------------------------------------- 1 | class _LazyMan { 2 | constructor(name) { 3 | this.tasks = [] 4 | this.subscribe('lazyMan', name) 5 | setTimeout(() => { 6 | this.publish() 7 | }, 0) 8 | } 9 | 10 | // 订阅 11 | subscribe(type, payload) { 12 | const action = { type, payload } 13 | 14 | if (type === 'sleepFirst') { 15 | this.tasks.unshift(action) 16 | } else { 17 | this.tasks.push(action) 18 | } 19 | } 20 | 21 | // 发布 22 | publish() { 23 | const { tasks } = this 24 | if (tasks.length > 0) this.run(tasks.shift()) 25 | } 26 | 27 | run({ type, payload }) { 28 | switch (type) { 29 | case 'lazyMan': 30 | this._lazyMan(payload) 31 | break 32 | case 'eat': 33 | this._eat(payload) 34 | break 35 | case 'sleep': 36 | this._sleep(payload) 37 | break 38 | case 'sleepFirst': 39 | this._sleepFirst(payload) 40 | break 41 | default: 42 | } 43 | } 44 | 45 | _lazyMan(name) { 46 | this._log(`Hi! This is ${name}!`) 47 | this.publish() 48 | } 49 | 50 | _eat(some) { 51 | this._log(`Eat ${some}~`) 52 | this.publish() 53 | } 54 | 55 | _sleep(time) { 56 | setTimeout(() => { 57 | this._log(`Wake up after ${time}`) 58 | this.publish() 59 | }, time * 1000) 60 | } 61 | 62 | _sleepFirst(time) { 63 | setTimeout(() => { 64 | this._log(`Wake up after ${time}`) 65 | this.publish() 66 | }, time * 1000) 67 | } 68 | 69 | _log(message) { 70 | console.log(message) 71 | } 72 | 73 | lazyMan() { 74 | this.subscribe('lazyMan', some) 75 | } 76 | 77 | eat(some) { 78 | this.subscribe('eat', some) 79 | return this 80 | } 81 | 82 | sleep(time) { 83 | this.subscribe('sleep', time) 84 | return this 85 | } 86 | 87 | sleepFirst(time) { 88 | this.subscribe('sleepFirst', time) 89 | return this 90 | } 91 | } 92 | 93 | function LazyMan(name) { 94 | return new _LazyMan(name) 95 | } 96 | 97 | LazyMan('lulu') 98 | .sleepFirst(5) 99 | .eat('dinner') 100 | .eat('supper') 101 | -------------------------------------------------------------------------------- /content/LazyMan.md: -------------------------------------------------------------------------------- 1 | # LazyMan 的两种实现 2 | 3 | ## 基于事件控制 4 | 5 | ```js 6 | class _LazyMan { 7 | constructor(name) { 8 | this.tasks = [] 9 | this.lazyMan(name) 10 | setTimeout(() => { 11 | this.next() 12 | }) 13 | } 14 | 15 | next() { 16 | const fn = this.tasks.shift() 17 | fn && fn() 18 | } 19 | 20 | lazyMan(name) { 21 | const fn = () => { 22 | this._log(`Hi! This is ${name}!`) 23 | this.next() 24 | } 25 | this.tasks.push(fn) 26 | return this 27 | } 28 | 29 | eat(some) { 30 | const fn = () => { 31 | this._log(`Eat ${some}~`) 32 | this.next() 33 | } 34 | this.tasks.push(fn) 35 | return this 36 | } 37 | 38 | sleep(time) { 39 | const fn = () => { 40 | setTimeout(() => { 41 | this._log(`Wake up after ${time}`) 42 | this.next() 43 | }, time * 1000) 44 | } 45 | this.tasks.push(fn) 46 | return this 47 | } 48 | 49 | sleepFirst(time) { 50 | const fn = () => { 51 | setTimeout(() => { 52 | this._log(`Wake up after ${time}`) 53 | this.next() 54 | }, time * 1000) 55 | } 56 | this.tasks.unshift(fn) 57 | return this 58 | } 59 | 60 | _log(message) { 61 | console.log(message) 62 | } 63 | } 64 | 65 | function LazyMan(name) { 66 | return new _LazyMan(name) 67 | } 68 | 69 | LazyMan('lulu') 70 | .sleep(5) 71 | .eat('dinner') 72 | .eat('supper') 73 | ``` 74 | 75 | ## 基于发布订阅模式 76 | 77 | ```js 78 | class _LazyMan { 79 | constructor(name) { 80 | this.tasks = [] 81 | this.subscribe('lazyMan', name) 82 | setTimeout(() => { 83 | this.publish() 84 | }, 0) 85 | } 86 | 87 | // 订阅 88 | subscribe(type, payload) { 89 | const action = { type, payload } 90 | 91 | if (type === 'sleepFirst') { 92 | this.tasks.unshift(action) 93 | } else { 94 | this.tasks.push(action) 95 | } 96 | } 97 | 98 | // 发布 99 | publish() { 100 | const { tasks } = this 101 | if (tasks.length > 0) this.run(tasks.shift()) 102 | } 103 | 104 | run({ type, payload }) { 105 | switch (type) { 106 | case 'lazyMan': 107 | this._lazyMan(payload) 108 | break 109 | case 'eat': 110 | this._eat(payload) 111 | break 112 | case 'sleep': 113 | this._sleep(payload) 114 | break 115 | case 'sleepFirst': 116 | this._sleepFirst(payload) 117 | break 118 | default: 119 | } 120 | } 121 | 122 | _lazyMan(name) { 123 | this._log(`Hi! This is ${name}!`) 124 | this.publish() 125 | } 126 | 127 | _eat(some) { 128 | this._log(`Eat ${some}~`) 129 | this.publish() 130 | } 131 | 132 | _sleep(time) { 133 | setTimeout(() => { 134 | this._log(`Wake up after ${time}`) 135 | this.publish() 136 | }, time * 1000) 137 | } 138 | 139 | _sleepFirst(time) { 140 | setTimeout(() => { 141 | this._log(`Wake up after ${time}`) 142 | this.publish() 143 | }, time * 1000) 144 | } 145 | 146 | _log(message) { 147 | console.log(message) 148 | } 149 | 150 | lazyMan() { 151 | this.subscribe('lazyMan', some) 152 | } 153 | 154 | eat(some) { 155 | this.subscribe('eat', some) 156 | return this 157 | } 158 | 159 | sleep(time) { 160 | this.subscribe('sleep', time) 161 | return this 162 | } 163 | 164 | sleepFirst(time) { 165 | this.subscribe('sleepFirst', time) 166 | return this 167 | } 168 | } 169 | 170 | function LazyMan(name) { 171 | return new _LazyMan(name) 172 | } 173 | 174 | LazyMan('lulu') 175 | .sleepFirst(5) 176 | .eat('dinner') 177 | .eat('supper') 178 | ``` 179 | -------------------------------------------------------------------------------- /content/javascript-standard.md: -------------------------------------------------------------------------------- 1 | # 前端代码规范 2 | 3 | ## 代码规范管理 4 | 5 | - [📏 使用 ESLint 来管理你的 git commit 代码规范](https://github.com/zhanghao-zhoushan/use-eslint-in-git-hooks) 6 | 7 | ## HTML 和 CSS 代码风格规范 8 | 9 | - [编码规范 by @mdo](https://codeguide.bootcss.com) 10 | 11 | - [Code Guide by @mdo](http://codeguide.co) 12 | 13 | ### 模块化 CSS: BEM 14 | 15 | - [BEM 的定义](https://www.w3cplus.com/css/bem-definitions.html) 16 | 17 | - [编写模块化 CSS:BEM](https://www.w3cplus.com/css/css-architecture-1.html) 18 | 19 | ## JavaScript 代码风格规范 20 | 21 | - [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript/blob/master/README.md) 22 | 23 | - [JavaScript Standard Style](https://standardjs.com/readme-zhcn.html) 24 | 25 | - [JavaScript Standard Style GitHub](https://github.com/standard/standard/blob/master/docs/README-zhcn.md) 26 | 27 | - [JavaScript 编码规范 百度](https://github.com/ecomfe/spec/blob/master/javascript-style-guide.md) 28 | 29 | ## Vue 代码风格规范 30 | 31 | - [风格指南 — Vue.js](https://cn.vuejs.org/v2/style-guide/) 32 | 33 | ## React 代码风格规范 34 | 35 | - [Airbnb React/JSX Style Guide](https://github.com/airbnb/javascript/tree/master/react) 36 | 37 | ## AMD CMD 规范 38 | 39 | - [Asynchronous Module Definition (AMD) ](https://github.com/amdjs/amdjs-api/wiki/AMD) 40 | 41 | - [CMD 模块定义规范 sea.js](https://github.com/seajs/seajs/issues/242) 42 | 43 | ## 文档规范 44 | 45 | - [中文技术文档的写作规范 阮一峰](https://github.com/ruanyf/document-style-guide/blob/master/README.md) 46 | -------------------------------------------------------------------------------- /content/mac-keyboard-mouse.md: -------------------------------------------------------------------------------- 1 | # Mac 键盘与鼠标的映射 2 | 3 | ## Filco 67 映射 4 | 5 | [斐尔可 FILCO Minila Air 蓝牙无线迷你啦机械键盘红轴](https://detail.tmall.com/item.htm?id=573584028996&spm=a1z09.12.0.0.11ca2e8dnvZszY&_u=vmv1p2s853f&skuId=3748502321931) 6 | 7 | ![Filco 67 键盘](https://sailor-1256168624.cos.ap-chengdu.myqcloud.com/keyboard.jpg) 8 | 9 | Filco 67 键盘的 Command 和 Option 的键位与 MacOS 的键位不同,需要对键位进行映射,并将 Command 与 Option 的键帽交换。 10 | 11 | ### [Karabiner - Software for macOS](https://pqrs.org/osx/karabiner/) 12 | 13 | 这是一款功能强大而且稳定的 macOS 键盘映射程序。 14 | 15 | 具体键位映射如图: 16 | 17 | ![键位映射](https://sailor-1256168624.cos.ap-chengdu.myqcloud.com/karabiner.jpg) 18 | 19 | 在 Devices 设置映射设备,注意,这里勾选了 Microsoft 的键盘设备,因为需要映射鼠标的 Window 键位。 20 | 21 | PS: 鼠标上的 Window 竟然属于键盘的键位,怪不得网上好多小伙伴都说映射不了。 22 | 23 | ![Devices](https://sailor-1256168624.cos.ap-chengdu.myqcloud.com/karabiner-devices.jpg) 24 | 25 | ## 微软 Sculpt 鼠标映射 26 | 27 | ### [微软 Sculpt 人体工学鼠标](https://item.jd.com/987709.html) 28 | 29 | ![微软 Sculpt 人体工学鼠标](https://sailor-1256168624.cos.ap-chengdu.myqcloud.com/mouse.jpg) 30 | 31 | ### [SteerMouse - Download](http://plentycom.jp/en/steermouse/download.php) 32 | 33 | SteerMouse 是一个应用程序,可以自由自定义按钮、滚轮和光标速度。兼容 Windows 与 Mac 平台,并且支持 USB 和蓝牙鼠标。 34 | 35 | 具体键位映射如图: 36 | 37 | ![键位映射](https://sailor-1256168624.cos.ap-chengdu.myqcloud.com/steer-mouse.jpg) 38 | 39 | 用来映射微软 Sculpt 人体工学鼠标的侧边键。 40 | -------------------------------------------------------------------------------- /content/react-and-nginx.md: -------------------------------------------------------------------------------- 1 | # react history mode 在 nginx 的部署 2 | 3 | react-router官方推荐,需要服务器支持,因为是 SPA 项目,url 切换时需要服务器始终返回 `index.html`。 4 | 5 | 当前项目打包后的文件在 `/var/www/bing-collect` 文件夹下。 6 | 7 | 部署后的地址为 [https://www.zhanghao-zhoushan.cn/bing-collect/](https://www.zhanghao-zhoushan.cn/bing-collect/) 8 | 9 | ## 部署在根目录 10 | 11 | ### 设置 homepage 12 | 13 | 设置 `package.json` 的 homepage 为 `.` 。 14 | 15 | ```json 16 | { 17 | "homepage": ".", 18 | } 19 | 20 | ``` 21 | 22 | 运行 `yarn build` 命令后构建的 `index.html` 关于静态资源的引用是这样的: 23 | 24 | ```html 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ``` 38 | 39 | 这里会以相对路径的形式引用静态资源。 40 | 41 | ### nginx 配置 42 | 43 | 设置访问页面时始终返回 `index.html` 。 44 | 45 | ```conf 46 | location / { 47 | root /var/www/bing-collect; 48 | try_files $uri /index.html; 49 | } 50 | ``` 51 | 52 | ## 部署在子目录 53 | 54 | 如果需要部署在子目录,例如 `http://localhost/bing-collect` 类似的路径。 55 | 56 | ### 设置 basename 57 | 58 | 需要设置 basename 为 `/bing-collect` 。 59 | 60 | ```tsx 61 | import * as React from 'react'; 62 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 63 | 64 | const App: React.FC = () => { 65 | return ( 66 | 67 | 68 | 69 | ); 70 | }; 71 | ``` 72 | 73 | ### 设置 homepage 74 | 75 | 设置 `package.json` 的 homepage 为 `/bing-collect` 。 76 | 77 | ```json 78 | { 79 | "homepage": "/bing-collect", 80 | } 81 | ``` 82 | 83 | 运行 `yarn build` 命令后构建的 `index.html` 关于静态资源的引用是这样的: 84 | 85 | ```html 86 | 87 | 88 | 89 | 93 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | ``` 105 | 106 | 这里会以绝对路径的形式引用静态资源。 107 | 108 | ### nginx 配置 109 | 110 | 访问 `bing-collect` 子目录时,重定向到 `index.html` 。 111 | 112 | ```conf 113 | # 重定向到 index.html 114 | location /bing-collect/ { 115 | root /var/www/; 116 | try_files $uri $uri/ /bing-collect/index.html /var/www/bing-collect/index.html; 117 | } 118 | ``` 119 | 120 | ## nginx 的一些知识点 121 | 122 | ### 常用命令 123 | 124 | 开机自启启动关闭在启动 125 | 126 | 修改 nginx.conf 后 127 | 128 | ```bash 129 | # 测试 nginx.conf 配置 130 | nginx -t 131 | # 保存重新读取配置 132 | nginx -s reload 133 | ``` 134 | 135 | ### nginx 配置 136 | 137 | ```conf 138 | # 重定向到 index.html 139 | location /bing-collect/ { 140 | root /var/www/; 141 | try_files $uri $uri/ /bing-collect/index.html /var/www/bing-collect/index.html; 142 | } 143 | 144 | # 代理接口 145 | location ~ /api/ { 146 | proxy_pass http://127.0.0.1:9527; 147 | } 148 | ``` 149 | -------------------------------------------------------------------------------- /content/vue-mixins.md: -------------------------------------------------------------------------------- 1 | # vue mixins 让组件显得不再臃肿 2 | 3 | vue 提供了 mixins 这个 API,可以让我们将组件中的可复用功能抽取出来,放入 mixin 中,然后在组件中引入 mixin,可以让组件显得不再臃肿,提高了代码的可复用性。 4 | 5 | 如何理解 mixins 呢 ?我们可以将 mixins 理解成一个数组,数组中有单或多个 mixin,mixin 的本质就是一个 JS 对象,它可以有 data、created、methods 等等 vue 实例中拥有的所有属性,甚至可以在 mixins 中再次嵌套 mixins,It's amazing ! 6 | 7 | 举个简单的栗子: 8 | 9 | ```html 10 |
11 |

{{ message }}

12 |
13 | ``` 14 | 15 | ```js 16 | const myMixin = { 17 | data() { 18 | return { 19 | message: 'this is mixin message' 20 | } 21 | }, 22 | created() { 23 | console.log('mixin created') 24 | } 25 | } 26 | 27 | const vm = new Vue({ 28 | el: '#app', 29 | mixins: [myMixin], 30 | 31 | data() { 32 | return { 33 | message: 'this is vue instance message' 34 | } 35 | }, 36 | created() { 37 | console.log(this.message) 38 | // => Root Vue Instance 39 | console.log('vue instance created') 40 | // => created myMixin 41 | // => created Root Vue Instance 42 | } 43 | }) 44 | ``` 45 | 46 | mixins 与 Vue Instance 合并时,会将 created 等钩子函数合并成数组,mixins 的钩子优先调用,当 data、methods 对象键值冲突时,以组件优先。 47 | 48 | PS: 如果对 mixins 的概念还不太清的小伙伴,可以去 [vue 官方文档](https://cn.vuejs.org/v2/guide/mixins.html) 看一下 vue mixins 的基本概念和用法。 49 | 50 | ## mixins 实现 51 | 52 | 那 mixins 是如何实现的呢 ?当 vue 在实例化的时候,会调用 mergeOptions 函数进行 options 的合并,函数申明在 vue/src/core/util/options.js 文件。 53 | 54 | ```js 55 | export function mergeOptions( 56 | parent: Object, 57 | child: Object, 58 | vm?: Component 59 | ): Object { 60 | ... 61 | // 如果有 child.extends 递归调用 mergeOptions 实现属性拷贝 62 | const extendsFrom = child.extends 63 | if (extendsFrom) { 64 | parent = mergeOptions(parent, extendsFrom, vm) 65 | } 66 | // 如果有 child.mixins 递归调用 mergeOptions 实现属性拷贝 67 | if (child.mixins) { 68 | for (let i = 0, l = child.mixins.length; i < l; i++) { 69 | parent = mergeOptions(parent, child.mixins[i], vm) 70 | } 71 | } 72 | // 申明 options 空对象,用来保存属性拷贝结果 73 | const options = {} 74 | let key 75 | // 遍历 parent 对象,调用 mergeField 进行属性拷贝 76 | for (key in parent) { 77 | mergeField(key) 78 | } 79 | // 遍历 parent 对象,调用 mergeField 进行属性拷贝 80 | for (key in child) { 81 | if (!hasOwn(parent, key)) { 82 | mergeField(key) 83 | } 84 | } 85 | // 属性拷贝实现方法 86 | function mergeField(key) { 87 | // 穿透赋值,默认为 defaultStrat 88 | const strat = strats[key] || defaultStrat 89 | options[key] = strat(parent[key], child[key], vm, key) 90 | } 91 | return options 92 | } 93 | ``` 94 | 95 | 为了保持代码简洁,已经将 mergeOptions 函数不重要的代码删除,剩余部分我们慢慢来看。 96 | 97 | ```js 98 | const extendsFrom = child.extends 99 | if (extendsFrom) { 100 | parent = mergeOptions(parent, extendsFrom, vm) 101 | } 102 | ``` 103 | 104 | 首先申明 extendsFrom 变量保存 child.extends,如果 extendsFrom 为真,递归调用 mergeOptions 进行属性拷贝,并且将 merge 结果保存到 parent 变量。 105 | 106 | ```js 107 | if (child.mixins) { 108 | for (let i = 0, l = child.mixins.length; i < l; i++) { 109 | parent = mergeOptions(parent, child.mixins[i], vm) 110 | } 111 | } 112 | ``` 113 | 114 | 如果 child.mixins 为真,循环 mixins 数组,递归调用 mergeOptions 实现属性拷贝,仍旧将 merge 结果保存到 parent 变量。 115 | 116 | 接下来是关于 parent、child 的属性赋值: 117 | 118 | ```js 119 | const options = {} 120 | let key 121 | 122 | for (key in parent) { 123 | mergeField(key) 124 | } 125 | 126 | for (key in child) { 127 | if (!hasOwn(parent, key)) { 128 | mergeField(key) 129 | } 130 | } 131 | ``` 132 | 133 | 申明 options 空对象,用来保存属性拷贝的结果,也作为递归调用 mergeOptions 的返回值。 134 | 135 | 这里首先会调用 for...in 对 parent 进行循环,在循环中不断调用 mergeField 函数。 136 | 137 | 接着调用 for...in 对 child 进行循环,这里有点不太一样,会调用 hasOwn 判断 parent 上是否有这个 key,如果没有再调用 mergeField 函数,这样避免了重复调用。 138 | 139 | 那么这个 mergeField 函数到底是用来做什么的呢? 140 | 141 | ```js 142 | function mergeField(key) { 143 | // 穿透赋值,默认为 defaultStrat 144 | const strat = strats[key] || defaultStrat 145 | options[key] = strat(parent[key], child[key], vm, key) 146 | } 147 | ``` 148 | 149 | mergeField 函数接收一个 key,首先会申明 strat 变量,如果 strats[key] 为真,就将 strats[key] 赋值给 strat。 150 | 151 | ```js 152 | const strats = config.optionMergeStrategies 153 | ... 154 | optionMergeStrategies: Object.create(null), 155 | ... 156 | ``` 157 | 158 | strats 其实就是 Object.create(null),Object.create 用来创建一个新对象,strats 默认是调用 Object.create(null) 生成的空对象。 159 | 160 | 顺便说一句,vue 也向外暴露了 Vue.config.optionMergeStrategies,可以实现自定义选项合并策略。 161 | 162 | 如果 strats[key] 为假,这里会用 || 做穿透赋值,将 defaultStrat 默认函数赋值给 strat。 163 | 164 | ```js 165 | const defaultStrat = function(parentVal: any, childVal: any): any { 166 | return childVal === undefined ? parentVal : childVal 167 | } 168 | ``` 169 | 170 | defaultStrat 函数返回一个三元表达式,如果 childVal 为 undefined,返回 parentVal,否则返回 childVal,这里主要以 childVal 优先,这也是为什么有 component > mixins > extends 这样的优先级。 171 | 172 | mergeField 函数最后会将调用 strat 的结果赋值给 options[key]。 173 | 174 | mergeOptions 函数最后会 merge 所有 options、 mixins、 extends,并将 options 对象返回,然后再去实例化 vue。 175 | 176 | ## 钩子函数的合并 177 | 178 | 我们来看看钩子函数是怎么进行合并的。 179 | 180 | ```js 181 | function mergeHook( 182 | parentVal: ?Array, 183 | childVal: ?Function | ?Array 184 | ): ?Array { 185 | return childVal 186 | ? parentVal 187 | ? parentVal.concat(childVal) 188 | : Array.isArray(childVal) 189 | ? childVal 190 | : [childVal] 191 | : parentVal 192 | } 193 | 194 | LIFECYCLE_HOOKS.forEach(hook => { 195 | strats[hook] = mergeHook 196 | }) 197 | ``` 198 | 199 | 循环 LIFECYCLE_HOOKS 数组,不断调用 mergeHook 函数,将返回值赋值给 strats[hook]。 200 | 201 | ```js 202 | export const LIFECYCLE_HOOKS = [ 203 | 'beforeCreate', 204 | 'created', 205 | 'beforeMount', 206 | 'mounted', 207 | 'beforeUpdate', 208 | 'updated', 209 | 'beforeDestroy', 210 | 'destroyed', 211 | 'activated', 212 | 'deactivated', 213 | 'errorCaptured' 214 | ] 215 | ``` 216 | 217 | LIFECYCLE_HOOKS 就是申明的 vue 所有的钩子函数字符串。 218 | 219 | mergeHook 函数会返回 3 层嵌套的三元表达式。 220 | 221 | ```js 222 | return childVal 223 | ? parentVal 224 | ? parentVal.concat(childVal) 225 | : Array.isArray(childVal) 226 | ? childVal 227 | : [childVal] 228 | : parentVal 229 | ``` 230 | 231 | 第一层,如果 childVal 为真,返回第二层三元表达式,如果为假,返回 parentVal。 232 | 233 | 第二层,如果 parentVal 为真,返回 parentVal 和 childVal 合并后的数组,如果 parentVal 为假,返回第三层三元表达式。 234 | 235 | 第三层,如果 childVal 是数组,返回 childVal,否则将 childVal 包装成数组返回。 236 | 237 | ```js 238 | new Vue({ 239 | created: [ 240 | function() { 241 | console.log('冲冲冲!') 242 | }, 243 | function() { 244 | console.log('鸭鸭鸭!') 245 | } 246 | ] 247 | }) 248 | // => 冲冲冲! 249 | // => 鸭鸭鸭! 250 | ``` 251 | 252 | ## 项目实践 253 | 254 | 使用 vue 的小伙伴们,当然也少不了在项目中使用 element-ui。比如使用 Table 表格的时候,免不了申明 tableData、total、pageSize 一些 Table 表格、Pagination 分页需要的参数。 255 | 256 | 我们可以将重复的 data、methods 写在一个 tableMixin 中。 257 | 258 | ```js 259 | export default { 260 | data() { 261 | return { 262 | total: 0, 263 | pageNo: 1, 264 | pageSize: 10, 265 | tableData: [], 266 | loading: false 267 | } 268 | }, 269 | 270 | created() { 271 | this.searchData() 272 | }, 273 | 274 | methods: { 275 | // 预申明,防止报错 276 | searchData() {}, 277 | 278 | handleSizeChange(size) { 279 | this.pageSize = size 280 | this.searchData() 281 | }, 282 | 283 | handleCurrentChange(page) { 284 | this.pageNo = page 285 | this.searchData() 286 | }, 287 | 288 | handleSearchData() { 289 | this.pageNo = 1 290 | this.searchData() 291 | } 292 | } 293 | } 294 | ``` 295 | 296 | 当我们需要使用时直接引入即可: 297 | 298 | ```js 299 | import tableMixin from './tableMixin' 300 | 301 | export default { 302 | ... 303 | mixins: [tableMixin], 304 | methods: { 305 | searchData() { 306 | ... 307 | } 308 | } 309 | } 310 | ``` 311 | 312 | 我们在组件内会重新申明 searchData 方法。类似这种 methods 对象形式的 key,如果 key 相同,组件内的 key 会覆盖 tableMixin 中的 key。 313 | 314 | 当然我们也可以在 mixins 中嵌套 mixins,申明 axiosMixin: 315 | 316 | ```js 317 | import tableMixin from './tableMixin' 318 | 319 | export default { 320 | mixins: [tableMixin], 321 | 322 | methods: { 323 | handleFetch(url) { 324 | const { pageNo, pageSize } = this 325 | this.loading = true 326 | 327 | this.axios({ 328 | method: 'post', 329 | url, 330 | data: { 331 | ...this.params, 332 | pageNo, 333 | pageSize 334 | } 335 | }) 336 | .then(({ data = [] }) => { 337 | this.tableData = data 338 | this.loading = false 339 | }) 340 | .catch(error => { 341 | this.loading = false 342 | }) 343 | } 344 | } 345 | } 346 | ``` 347 | 348 | 引入 axiosMixin: 349 | 350 | ```js 351 | import axiosMixin from './axiosMixin' 352 | 353 | export default { 354 | ... 355 | mixins: [axiosMixin], 356 | created() { 357 | this.handleFetch('/user/12345') 358 | } 359 | } 360 | ``` 361 | 362 | 在 axios 中,我们可以预先处理 axios 的 success、error 的后续调用,是不是少写了很多代码。 363 | 364 | ## extend 365 | 366 | 顺便讲一下 extend,与 mixins 相似,只能传入一个 options 对象,并且 mixins 的优先级比较高,会覆盖 extend 同名 key 值。 367 | 368 | ```js 369 | // 如果有 child.extends 递归调用 mergeOptions 实现属性拷贝 370 | const extendsFrom = child.extends 371 | if (extendsFrom) { 372 | parent = mergeOptions(parent, extendsFrom, vm) 373 | } 374 | // 如果有 child.mixins 递归调用 mergeOptions 实现属性拷贝 375 | if (child.mixins) { 376 | for (let i = 0, l = child.mixins.length; i < l; i++) { 377 | parent = mergeOptions(parent, child.mixins[i], vm) 378 | } 379 | } 380 | ``` 381 | 382 | 在 mergeOptions 函数中,会先对 extends 进行属性拷贝,然后再对 mixin 进行拷贝,在调用 mergeField 函数的时候会优先取 child 的 key。 383 | 384 | 虽然 extends 的同名 key 会被 mixins 的覆盖,但是 extends 是优先执行的。 385 | 386 | ## 总结 387 | 388 | 注意一下 vue 中 mixins 的优先级,component > mixins > extends。 389 | 390 | 我们暂且将 mixins 称作是组件模块化,灵活运用组件模块化,可以将组件内的重复代码提取出来,实现代码复用,也使我们的代码更加清晰,效率也大大提高。 391 | 392 | 当然,mixins 还有更加神奇的操作等你去探索。 393 | -------------------------------------------------------------------------------- /content/vue-next-tick.md: -------------------------------------------------------------------------------- 1 | # vue nextTick 源码浅析 2 | 3 | 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。 4 | 5 | ## \$nextTick 函数的注册 6 | 7 | 在 renderMixin 函数中我们会为 Vue.prototype 添加 \$nextTick 方法,在 core/instance/render.js 文件中: 8 | 9 | ```js 10 | export function renderMixin(Vue: Class) { 11 | ... 12 | Vue.prototype.$nextTick = function(fn: Function) { 13 | return nextTick(fn, this) 14 | } 15 | ... 16 | } 17 | ``` 18 | 19 | 在 \$nextTick 方法中会返回 nextTick 函数的调用,并且在调用时传入了 fn 回调函数和 this,也就是 vue 实例。 20 | 21 | ## nextTick 函数 22 | 23 | nextTick 函数申明在 src/core/util/next-tick.js 文件: 24 | 25 | ```js 26 | export function nextTick(cb?: Function, ctx?: Object) { 27 | let _resolve 28 | callbacks.push(() => { 29 | if (cb) { 30 | try { 31 | cb.call(ctx) 32 | } catch (e) { 33 | handleError(e, ctx, 'nextTick') 34 | } 35 | } else if (_resolve) { 36 | _resolve(ctx) 37 | } 38 | }) 39 | if (!pending) { 40 | pending = true 41 | if (useMacroTask) { 42 | macroTimerFunc() 43 | } else { 44 | microTimerFunc() 45 | } 46 | } 47 | // $flow-disable-line 48 | if (!cb && typeof Promise !== 'undefined') { 49 | return new Promise(resolve => { 50 | _resolve = resolve 51 | }) 52 | } 53 | } 54 | ``` 55 | 56 | nextTick 函数接收两个参数,cb 回调函数、ctx 上下文环境,在 \$nextTick 中传入的是 vue 实例。 57 | 58 | nextTick 函数首先会申明 \_resolve 变量,如果当前环境支持 Promise,会将 resolve 赋值给 \_resolve 变量。 59 | 60 | ```js 61 | callbacks.push(() => { 62 | if (cb) { 63 | try { 64 | cb.call(ctx) 65 | } catch (e) { 66 | handleError(e, ctx, 'nextTick') 67 | } 68 | } else if (_resolve) { 69 | _resolve(ctx) 70 | } 71 | }) 72 | ``` 73 | 74 | 每一次调用 nextTick 函数,都会往 callbacks 中 push 函数,在函数中会判断 cb 是否为真,如果为真也就是传入了回调函数,会尝试将调用 call 将 cb 的 this 指向传入的 ctx 上下文环境,如果报错,就调用 handleError 函数抛出异常。 75 | 76 | 如果 \_resolve 为真,调用 \_resolve 函数,并且将 ctx 传入。 77 | 78 | 我们接着往下看: 79 | 80 | ```js 81 | if (!pending) { 82 | pending = true 83 | if (useMacroTask) { 84 | macroTimerFunc() 85 | } else { 86 | microTimerFunc() 87 | } 88 | } 89 | ``` 90 | 91 | 如果 !pending 为真,也就是当前并没有在进行 nextTick 更新,将 pending 赋值为 true,说明正在进行 nextTick 更新。这里会判断 useMacroTask 是否为真,这个 useMacroTask 是什么呢? 92 | 93 | ```js 94 | // Here we have async deferring wrappers using both microtasks and (macro) tasks. 95 | // In < 2.4 we used microtasks everywhere, but there are some scenarios where 96 | // microtasks have too high a priority and fire in between supposedly 97 | // sequential events (e.g. #4521, #6690) or even between bubbling of the same 98 | // event (#6566). However, using (macro) tasks everywhere also has subtle problems 99 | // when state is changed right before repaint (e.g. #6813, out-in transitions). 100 | // Here we use microtask by default, but expose a way to force (macro) task when 101 | // needed (e.g. in event handlers attached by v-on). 102 | let microTimerFunc 103 | let macroTimerFunc 104 | let useMacroTask = false 105 | ``` 106 | 107 | useMacroTask 是在 next-tick.js 开头申明的变量,默认为 false,这里作者给出了详细的注释。 108 | 109 | > 在 Vue 2.4 之前都是使用的 microtasks,但是 microtasks 的优先级过高,在某些情况下可能会出现比事件冒泡更快的情况,但如果都使用 macrotasks 又可能会出现渲染的性能问题。所以在新版本中,会默认使用 microtasks,但在特殊情况下会使用 macrotasks,比如 v-on。 110 | 111 | useMacroTask 默认为 false,这里会走 else 循环,优先调用 microTimerFunc 函数。 112 | 113 | 我们来看看 microTimerFunc 函数: 114 | 115 | ```js 116 | // Determine microtask defer implementation. 117 | /* istanbul ignore next, $flow-disable-line */ 118 | if (typeof Promise !== 'undefined' && isNative(Promise)) { 119 | const p = Promise.resolve() 120 | microTimerFunc = () => { 121 | p.then(flushCallbacks) 122 | // in problematic UIWebViews, Promise.then doesn't completely break, but 123 | // it can get stuck in a weird state where callbacks are pushed into the 124 | // microtask queue but the queue isn't being flushed, until the browser 125 | // needs to do some other work, e.g. handle a timer. Therefore we can 126 | // "force" the microtask queue to be flushed by adding an empty timer. 127 | if (isIOS) setTimeout(noop) 128 | } 129 | } else { 130 | // fallback to macro 131 | microTimerFunc = macroTimerFunc 132 | } 133 | ``` 134 | 135 | microTimerFunc 变量会在 next-tick.js 开头申明,默认为 undefined。 136 | 137 | 这里会判断调用 typeof 判断 Promise 的类型是否是 undefined,并且调用 isNative 判断 Promise 是否是原生的函数。 138 | 139 | ```js 140 | /* istanbul ignore next */ 141 | export function isNative(Ctor: any): boolean { 142 | return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) 143 | } 144 | ``` 145 | 146 | 判断原型的话会判断函数 toString 后是否有 native code 字符串。 147 | 148 | 如果都为真,判断当前环境支持 Promise 函数,申明 p 变量保存 Promise.resolve(),p 就成了一个 立即 resolve 的 Promise 对象。 149 | 150 | ```js 151 | microTimerFunc = () => { 152 | p.then(flushCallbacks) 153 | // in problematic UIWebViews, Promise.then doesn't completely break, but 154 | // it can get stuck in a weird state where callbacks are pushed into the 155 | // microtask queue but the queue isn't being flushed, until the browser 156 | // needs to do some other work, e.g. handle a timer. Therefore we can 157 | // "force" the microtask queue to be flushed by adding an empty timer. 158 | if (isIOS) setTimeout(noop) 159 | } 160 | ``` 161 | 162 | 接着将 microTimerFunc 赋值成一个函数,在函数中会调用 then 方法,并将 flushCallbacks 函数传入,flushCallbacks 会在下一次 microtask 的时候执行。 163 | 164 | ```js 165 | export const isIOS = 166 | (UA && /iphone|ipad|ipod|ios/.test(UA)) || weexPlatform === 'ios' 167 | ``` 168 | 169 | 如果 isIOS 为真,也就是当前设备是 IOS,为了兼容 UIWebViews 中出现的一些问题,回调被推到 microtask,但 microtask 没有刷新,我们可以添加空 setTimeout 来强制刷新 microtask。 170 | 171 | ```js 172 | microTimerFunc = macroTimerFunc 173 | ``` 174 | 175 | 如果当前环境不支持 Promise,采取降级处理,将 microTimerFunc 赋值成 macroTimerFunc,macroTimerFunc 会在 176 | microTimerFunc 之前进行赋值。 177 | 178 | ```js 179 | if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { 180 | macroTimerFunc = () => { 181 | setImmediate(flushCallbacks) 182 | } 183 | } else if ( 184 | typeof MessageChannel !== 'undefined' && 185 | (isNative(MessageChannel) || 186 | // PhantomJS 187 | MessageChannel.toString() === '[object MessageChannelConstructor]') 188 | ) { 189 | const channel = new MessageChannel() 190 | const port = channel.port2 191 | channel.port1.onmessage = flushCallbacks 192 | macroTimerFunc = () => { 193 | port.postMessage(1) 194 | } 195 | } else { 196 | /* istanbul ignore next */ 197 | macroTimerFunc = () => { 198 | setTimeout(flushCallbacks, 0) 199 | } 200 | } 201 | ``` 202 | 203 | 首先会判断如果当前环境支持 setImmediate,采用 macroTimerFunc 处理 flushCallbacks 函数,如果当前环境支持 MessageChannel 采用 MessageChannel 处理 flushCallbacks 函数,如果都不支持采用 setTimeout 处理 flushCallbacks 函数,这样实现了优雅的降级处理。 204 | 205 | 在 macroTimerFunc 和 microTimerFunc 的回调函数中,都会调用 flushCallbacks 函数,我们来看一下具体实现: 206 | 207 | ```js 208 | const callbacks = [] 209 | let pending = false 210 | 211 | function flushCallbacks() { 212 | pending = false 213 | const copies = callbacks.slice(0) 214 | callbacks.length = 0 215 | for (let i = 0; i < copies.length; i++) { 216 | copies[i]() 217 | } 218 | } 219 | ``` 220 | 221 | flushCallbacks 函数首先会将 pending 置为 false,代表上一次的 nextTick 更新完毕。 222 | 223 | 这里会申明 copies 变量,采用 slice 赋值一份 callbacks 数组,然后将 callbacks 清空,这里采用了 slice 为了清空 callbacks 而不影响 copies 数组,callbacks.slice(0) 返回一个新的数组,是一个新的引用地址。 224 | 225 | 然后采用 for 循环,循环调用 copies 中的函数。 226 | 227 | ## withMacroTask 228 | 229 | 在 next-tick.js 中,除了暴露了 nextTick 函数, 还暴露了 withMacroTask 函数: 230 | 231 | ```js 232 | /** 233 | * Wrap a function so that if any code inside triggers state change, 234 | * the changes are queued using a (macro) task instead of a microtask. 235 | */ 236 | export function withMacroTask(fn: Function): Function { 237 | return ( 238 | fn._withTask || 239 | (fn._withTask = function() { 240 | useMacroTask = true 241 | const res = fn.apply(null, arguments) 242 | useMacroTask = false 243 | return res 244 | }) 245 | ) 246 | } 247 | ``` 248 | 249 | withMacroTask 返回传入的 fn 的 \_withTask 函数,这里用了传统赋值,如果 fn.\_withTask 为真,返回 fn.\_withTask,否则将 fn.\_withTask 赋值成一个函数,这个函数会将 useMacroTask 置为 true,使用 apply 将 this 指向 null,并用 res 变量保存返回,接着讲 useMacroTask 恢复成 false,最后返回 res。 250 | 我们知道当 useMacroTask 为 true 的时候会使用 macroTimerFunc 作为事件循环,调用 withMacroTask 函数传入的 fn 会插入到 microTask 任务队列。 251 | 252 | withMacroTask 在 src/platforms/web/runtime/modules/events.js 中的 add 函数被调用: 253 | 254 | ```js 255 | function add( 256 | event: string, 257 | handler: Function, 258 | once: boolean, 259 | capture: boolean, 260 | passive: boolean 261 | ) { 262 | handler = withMacroTask(handler) 263 | if (once) handler = createOnceHandler(handler, event, capture) 264 | target.addEventListener( 265 | event, 266 | handler, 267 | supportsPassive ? { capture, passive } : capture 268 | ) 269 | } 270 | ``` 271 | 272 | add 函数主要用来添加事件监听。 273 | 274 | ```js 275 | function updateDOMListeners(oldVnode: VNodeWithData, vnode: VNodeWithData) { 276 | if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) { 277 | return 278 | } 279 | const on = vnode.data.on || {} 280 | const oldOn = oldVnode.data.on || {} 281 | target = vnode.elm 282 | normalizeEvents(on) 283 | updateListeners(on, oldOn, add, remove, vnode.context) 284 | target = undefined 285 | } 286 | ``` 287 | 288 | 而 add 函数在 updateDOMListeners 函数中调用 updateListeners 函数时作为参数传入。 289 | 290 | updateListeners 会被 updateComponentListeners、updateDOMListeners 函数调用。 291 | 292 | | updateComponentListeners | updateDOMListeners | 293 | | --------------------------------------- | :---------------------------------------------: | 294 | | initEvents updateChildComponent | create update | 295 | | ---------- componentVNodeHooks.prepatch | createPatchFunction | 296 | | | Vue.prototype.**patch** = patch | 297 | | | beforeMount lifecycleMixin {$destroy, $destroy} | 298 | 299 | 向上查看调用链。 300 | 301 | ## nextTick 调用 302 | 303 | html 304 | 305 | ```html 306 |
{{ message }}
307 | ``` 308 | 309 | js 310 | 311 | ```javascript 312 | const vm = new Vue({ 313 | el: '#app', 314 | data() { 315 | return { 316 | message: 'Hello World' 317 | } 318 | } 319 | }) 320 | vm.message = 'Hello Vue' 321 | 322 | console.log('正常调用', vm.$el.textContent) 323 | 324 | vm.$nextTick(() => { 325 | console.log('nextTick 回调', vm.$el.textContent) 326 | }) 327 | 328 | vm.$nextTick().then(() => { 329 | console.log('nextTick t', vm.$el.textContent) 330 | }) 331 | // 正常调用 Hello World 332 | // nextTick 回调 Hello Vue 333 | // nextTick then Hello Vue 334 | ``` 335 | -------------------------------------------------------------------------------- /content/vue-standard.md: -------------------------------------------------------------------------------- 1 | # vue 更舒适的开发环境 2 | 3 | 团队能够指定一套合适的代码规范,在开发过程中无疑是非常舒适的。 4 | 5 | ## 风格指南 6 | 7 | - [Vue.js 风格指南](https://cn.vuejs.org/v2/style-guide/index.html) 8 | - [Vant 风格指南](https://youzan.github.io/vant/#/zh-CN/style-guide) 9 | - [Vue.js 组件编码规范](https://github.com/pablohpsilva/vuejs-component-style-guide/blob/master/README-CN.md) 10 | 11 | ## VSCode 插件 12 | 13 | - Vetur 14 | - vueHelper 15 | 16 | ### settings.json 17 | 18 | ```json 19 | { 20 | "editor.fontSize": 16, 21 | "editor.fontFamily": "DankMono, OperatorMono, Fira Code", 22 | "editor.fontWeight": "600", 23 | "workbench.colorTheme": "Atom One Dark", 24 | "workbench.iconTheme": "material-icon-theme", 25 | // Tab等于的空格数 26 | "editor.tabSize": 2, 27 | // 是否显示分号 28 | "prettier.semi": false, 29 | // 是否单引号 30 | "prettier.singleQuote": true, 31 | // 控制折行的方式 32 | "editor.wordWrap": "wordWrapColumn", 33 | // 控制编辑器的折行列 34 | "editor.wordWrapColumn": 500, 35 | // 配置语言的文件关联 36 | "files.associations": { 37 | "*.vue": "vue" 38 | }, 39 | // vetur格式化方式 40 | "vetur.format.defaultFormatter.html": "js-beautify-html", 41 | "vetur.format.defaultFormatter.js": "vscode-typescript", 42 | // 函数参数括号前的空格 43 | "javascript.format.insertSpaceBeforeFunctionParenthesis": true, 44 | "vetur.format.defaultFormatterOptions": { 45 | "js-beautify-html": { 46 | // 对属性进行换行 47 | "wrap_attributes": "auto" 48 | }, 49 | }, 50 | // 配置排除的文件和文件夹 51 | "files.exclude": { 52 | "**/.git": true, 53 | "**/.DS_Store": true, 54 | "**/*.log.*": true, 55 | "**/dist": false, 56 | "**/tmp": true, 57 | "**/cache": true, 58 | "**/node_modules": false, 59 | "**/bower_components": true, 60 | "**/public": false, 61 | "**/.classpath": true, 62 | "**/.project": true, 63 | "**/.settings": true, 64 | "**/.factorypath": true 65 | }, 66 | // 是否展开Emmet缩写 67 | "emmet.triggerExpansionOnTab": true, 68 | "explorer.confirmDelete": false, 69 | "git.enableSmartCommit": true, 70 | "explorer.confirmDragAndDrop": false, 71 | "leetcode.endpoint": "leetcode-cn", 72 | "leetcode.defaultLanguage": "javascript", 73 | "terminal.integrated.rendererType": "canvas", 74 | "extensions.autoUpdate": true, 75 | "breadcrumbs.enabled": true, 76 | "[jsonc]": { 77 | "editor.defaultFormatter": "vscode.json-language-features" 78 | }, 79 | "[javascript]": { 80 | "editor.defaultFormatter": "vscode.typescript-language-features" 81 | }, 82 | } 83 | ``` 84 | 85 | ## use snippets 86 | 87 | 配置 vue snippets 88 | 89 | 1. `option + cmd + p` 打开命令面板 90 | 2. 输入 snippets 选择 Configure User Snippets 91 | 3. 搜索 vue,选择 vue.json 92 | 93 | ```json 94 | { 95 | "Print to console": { 96 | "prefix": "vue", 97 | "body": [ 98 | "\n", 102 | "\n", 126 | "", 127 | "$2" 128 | ], 129 | "description": "Log output to console" 130 | } 131 | } 132 | ``` 133 | -------------------------------------------------------------------------------- /content/vuex-class.md: -------------------------------------------------------------------------------- 1 | ## class ModuleCollection 2 | 3 | 在上面初始参数的赋值中 `this._modules` 就是 `ModuleCollection` 类的实例。 4 | 5 | ```js 6 | this._modules = new ModuleCollection(options); 7 | ``` 8 | 9 | 如果没有嵌套模块,`this._modules` 是这样一个结构。 10 | 11 | ```js 12 | { 13 | 'root': { 14 | 'runtime': false, 15 | '_children': {}, 16 | '_rawModule': { 17 | 'state': { 18 | 'count': 0 19 | }, 20 | 'getters': {}, 21 | 'actions': {}, 22 | 'mutations': {} 23 | }, 24 | 'state': { 25 | 'count': 0 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | 来看看 `ModuleCollection:` 32 | 33 | ```js 34 | class ModuleCollection { 35 | constructor(rawRootModule) { 36 | // register root module (Vuex.Store options) 37 | this.register([], rawRootModule, false); 38 | } 39 | 40 | get(path) { 41 | return path.reduce((module, key) => { 42 | return module.getChild(key); 43 | }, this.root); 44 | } 45 | 46 | // 根据 path 处理命名空间 47 | getNamespace(path) { 48 | let module = this.root; 49 | return path.reduce((namespace, key) => { 50 | module = module.getChild(key); 51 | return namespace + (module.namespaced ? key + '/' : ''); 52 | }, ''); 53 | } 54 | 55 | update(rawRootModule) { 56 | update([], this.root, rawRootModule); 57 | } 58 | 59 | register(path, rawModule, runtime = true) { 60 | if (process.env.NODE_ENV !== 'production') { 61 | assertRawModule(path, rawModule); 62 | } 63 | 64 | // 默认注册 root 65 | // 包装了下传过来的 rawModule 66 | const newModule = new Module(rawModule, runtime); 67 | // 判断 path.length 0 说明是 root 保存到 this.root 上 68 | // 下次递归注册进入 else 调用 Module 类的 getChild addChild 69 | // 建立 module 的父子关系 70 | if (path.length === 0) { 71 | this.root = newModule; 72 | } else { 73 | const parent = this.get(path.slice(0, -1)); 74 | parent.addChild(path[path.length - 1], newModule); 75 | } 76 | 77 | // register nested modules 78 | // 有 modules 递归注册嵌套模块 79 | if (rawModule.modules) { 80 | forEachValue(rawModule.modules, (rawChildModule, key) => { 81 | this.register(path.concat(key), rawChildModule, runtime); 82 | }); 83 | } 84 | } 85 | 86 | unregister(path) { 87 | const parent = this.get(path.slice(0, -1)); 88 | const key = path[path.length - 1]; 89 | if (!parent.getChild(key).runtime) return; 90 | 91 | parent.removeChild(key); 92 | } 93 | } 94 | ``` 95 | 96 | 在 `ModuleCollection` 类的 `constructor` 中首先会执行类的 `register` 方法,将空数组、`rawRootModule`(也就是实例化的时候传入的 `options`)、`false` 最为最初参数传入。 97 | 98 | `register` 方法会递归调用,实现嵌套模块的收集 99 | 首先会在非生产环境调用 `assertRawModule` 函数,对 `module` 进行一些断言判断,判断 `rawModule` 对象是否有 `getters` `mutations` `mutations` 为 `key` 值,然后根据预置的类型进行断言。 100 | 101 | 随后就是实例化 `Module` 新建一个 `newModule`,判断 `path.length`,0 说明是 `root`, 将 `newModule` 保存到 `this.root` 上,然后判断 `rawModule.modules` 是否有嵌套 `modules`。 102 | 103 | 有就调用 `forEachValue` 将 `modules`转换成数组,并且循环调用传入的回调函数,回调函数里又递归调用了 `this.register`,将 `path` 合并子模块的 `key`, 循环的子模块、`runtime` 作为参数传入。 104 | 105 | 第二次进入 `register` 会进入 `else` 判断,调用 `Module` 类的 `getChild` `addChild`, 建立 `module` 的父子关系,如果仍然嵌套模块继续递归调用 `this.register`。 106 | 107 | `forEachValue`: 108 | 109 | ```js 110 | // object 转成数组 循环调用 fn 111 | export function forEachValue(obj, fn) { 112 | Object.keys(obj).forEach(key => fn(obj[key], key)); 113 | } 114 | ``` 115 | 116 | ### assertRawModule 117 | 118 | 上面说过,`assertRawModule` 负责对 `module` 进行一些断言判断,判断 `rawModule` 对象是否有 `getters`、`mutations`、`mutations` 为 `key` 值,然后根据预置的类型进行断言。 119 | 120 | ```js 121 | const functionAssert = { 122 | assert: value => typeof value === 'function', 123 | expected: 'function' 124 | }; 125 | 126 | const objectAssert = { 127 | assert: value => 128 | typeof value === 'function' || 129 | (typeof value === 'object' && typeof value.handler === 'function'), 130 | expected: 'function or object with "handler" function' 131 | }; 132 | 133 | const assertTypes = { 134 | getters: functionAssert, 135 | mutations: functionAssert, 136 | actions: objectAssert 137 | }; 138 | 139 | function assertRawModule(path, rawModule) { 140 | Object.keys(assertTypes).forEach(key => { 141 | if (!rawModule[key]) return; 142 | 143 | const assertOptions = assertTypes[key]; 144 | 145 | forEachValue(rawModule[key], (value, type) => { 146 | assert( 147 | assertOptions.assert(value), 148 | makeAssertionMessage(path, key, type, value, assertOptions.expected) 149 | ); 150 | }); 151 | }); 152 | } 153 | 154 | function makeAssertionMessage(path, key, type, value, expected) { 155 | let buf = `${key} should be ${expected} but "${key}.${type}"`; 156 | if (path.length > 0) { 157 | buf += ` in module "${path.join('.')}"`; 158 | } 159 | buf += ` is ${JSON.stringify(value)}.`; 160 | return buf; 161 | } 162 | ``` 163 | 164 | `assertRawModule` 循环 `assertTypes` 对象,循环的 `key` 为 `getters` `mutations` `actions`,判断传入模块是否有这些属性。 165 | 166 | ```js 167 | const assertOptions = assertTypes[key]; 168 | ``` 169 | 170 | 接着从 `assertTypes` 取出对应属性的 `value` 171 | 172 | 循环 `rawModule[key]` 对象,如果 `key` 此时就是 `getters`,那就是遍历当前模块有所的 `getter` 函数,回调函数是一个断言函数,`assertOptions` 的 `assert` 会返回对属性类型的判断,作为 `Boolean` 传入,`makeAssertionMessage` 函数只是对断言函数判断的异常的描述。 173 | 174 | ## class Module 175 | 176 | 来看看 `Module` 类的代码: 177 | 178 | ```js 179 | export default class Module { 180 | constructor(rawModule, runtime) { 181 | this.runtime = runtime; 182 | // Store some children item 183 | this._children = Object.create(null); 184 | // Store the origin module object which passed by programmer 185 | this._rawModule = rawModule; 186 | const rawState = rawModule.state; 187 | // Store the origin module's state 188 | this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}; 189 | } 190 | 191 | get namespaced() { 192 | return !!this._rawModule.namespaced; 193 | } 194 | 195 | addChild(key, module) { 196 | this._children[key] = module; 197 | } 198 | 199 | removeChild(key) { 200 | delete this._children[key]; 201 | } 202 | 203 | getChild(key) { 204 | return this._children[key]; 205 | } 206 | 207 | update(rawModule) { 208 | this._rawModule.namespaced = rawModule.namespaced; 209 | if (rawModule.actions) { 210 | this._rawModule.actions = rawModule.actions; 211 | } 212 | if (rawModule.mutations) { 213 | this._rawModule.mutations = rawModule.mutations; 214 | } 215 | if (rawModule.getters) { 216 | this._rawModule.getters = rawModule.getters; 217 | } 218 | } 219 | 220 | forEachChild(fn) { 221 | forEachValue(this._children, fn); 222 | } 223 | 224 | forEachGetter(fn) { 225 | if (this._rawModule.getters) { 226 | forEachValue(this._rawModule.getters, fn); 227 | } 228 | } 229 | 230 | forEachAction(fn) { 231 | if (this._rawModule.actions) { 232 | forEachValue(this._rawModule.actions, fn); 233 | } 234 | } 235 | 236 | forEachMutation(fn) { 237 | if (this._rawModule.mutations) { 238 | forEachValue(this._rawModule.mutations, fn); 239 | } 240 | } 241 | } 242 | ``` 243 | 244 | `Module` 类的 `constructor` 中会将传入的 `rawModule` `runtime` 保存,申明 `this._children`,主要是存放该模块的子模块,将 `rawModule.state` 取出保存到 `this.state` 上。 245 | 246 | `Module` 类提供了很多方法: 247 | 248 | `namespaced` 通过双非取值返回一个 `布尔值` ,作为是否有命名空间的判断。 249 | 250 | `addChild` 在 `ModuleCollection` 的 `register` 方法中调用,将子模块存入到父模块的 `this._children` 251 | 252 | `removeChild` 删除子模块 253 | 254 | `getChild` 获取子模块 255 | 256 | `update` 在 `ModuleCollection` 的 `update` 的调用,负责整个模块的更新 257 | 258 | 后面的几个方法都是调用 `forEachValue`,将对应对应的模块,以及传入的 `fn` 传入。 259 | 260 | ### getNamespace 261 | 262 | 根据 `path` 处理命名空间: 263 | 264 | ```js 265 | getNamespace (path) { 266 | let module = this.root 267 | return path.reduce((namespace, key) => { 268 | module = module.getChild(key) 269 | return namespace + (module.namespaced ? key + '/' : '') 270 | }, '') 271 | } 272 | ``` 273 | -------------------------------------------------------------------------------- /content/vuex-devtool.md: -------------------------------------------------------------------------------- 1 | ## 插件 2 | 3 | ### devtool 4 | 5 | ```js 6 | const devtoolHook = 7 | typeof window !== 'undefined' && window.__VUE_DEVTOOLS_GLOBAL_HOOK__; 8 | 9 | export default function devtoolPlugin(store) { 10 | if (!devtoolHook) return; 11 | 12 | store._devtoolHook = devtoolHook; 13 | 14 | devtoolHook.emit('vuex:init', store); 15 | 16 | devtoolHook.on('vuex:travel-to-state', targetState => { 17 | store.replaceState(targetState); 18 | }); 19 | 20 | store.subscribe((mutation, state) => { 21 | devtoolHook.emit('vuex:mutation', mutation, state); 22 | }); 23 | } 24 | ``` 25 | 26 | 根据 `window` 上的 `__VUE_DEVTOOLS_GLOBAL_HOOK_` 变量判断当前浏览器是否安装了 `vueTools`, 27 | 接着来看 `devtoolPlugin` 函数,`devtoolPlugin` 函数使用 `export default` 默认导出, 28 | 在 `Store` 实例的 `constructor` 中调用。 29 | 30 | 进入 `devtoolPlugin` 函数内部,接收 `store` 参数,`store` 调用时候传入的 `this`,也就是`Store` 实例, 31 | 判断没有 `devtoolHook` 直接 `retrun`,将 `devtoolHook` 赋值给 `store._devtoolHook`,会在 `Store` 实例的 `registerAction` 中用到。 32 | 33 | 向 `vueTools` `emit` `vuex:init` 事件,并将 `store` 传入,`devtoolHook` 监听到会根据 `store` 初始化 `vuex`。 34 | 35 | `devtoolHook` 调用 `on` 方法监听 `vuex:travel-to-state`,监听到就调用回调函数,回调函数里会调用 `Store` 类的 `replaceState` 方法。 36 | 37 | ```js 38 | replaceState (state) { 39 | this._withCommit(() => { 40 | this._vm._data.$$state = state 41 | }) 42 | } 43 | ``` 44 | 45 | `replaceState` 替换当前 `_vm._data.$$state`。 46 | 47 | 最后调用 `Store` 类的 `subscribe` 订阅,每一次 `mutation` 改变 `state`,都会调用 `devtoolHook` 的 `emit` 方法通知 `devtool` 改变 `mutation` `state`。 48 | 49 | `devtoolHook` 原理 ? 50 | 占坑: 猜测是一个 `Vue Bus`。 51 | 52 | ### createLogger 53 | 54 | `vuex` 有个内置的插件 `createLogger`,位于 `src/plugins/logger.js`: 55 | 56 | ```js 57 | export default function createLogger({ 58 | collapsed = true, 59 | filter = (mutation, stateBefore, stateAfter) => true, 60 | transformer = state => state, 61 | mutationTransformer = mut => mut, 62 | logger = console 63 | } = {}) { 64 | return store => { 65 | let prevState = deepCopy(store.state); 66 | 67 | store.subscribe((mutation, state) => { 68 | if (typeof logger === 'undefined') { 69 | return; 70 | } 71 | const nextState = deepCopy(state); 72 | 73 | if (filter(mutation, prevState, nextState)) { 74 | const time = new Date(); 75 | const formattedTime = ` @ ${pad(time.getHours(), 2)}:${pad( 76 | time.getMinutes(), 77 | 2 78 | )}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}`; 79 | const formattedMutation = mutationTransformer(mutation); 80 | const message = `mutation ${mutation.type}${formattedTime}`; 81 | const startMessage = collapsed ? logger.groupCollapsed : logger.group; 82 | 83 | // render 84 | try { 85 | startMessage.call(logger, message); 86 | } catch (e) { 87 | console.log(message); 88 | } 89 | 90 | logger.log( 91 | '%c prev state', 92 | 'color: #9E9E9E; font-weight: bold', 93 | transformer(prevState) 94 | ); 95 | logger.log( 96 | '%c mutation', 97 | 'color: #03A9F4; font-weight: bold', 98 | formattedMutation 99 | ); 100 | logger.log( 101 | '%c next state', 102 | 'color: #4CAF50; font-weight: bold', 103 | transformer(nextState) 104 | ); 105 | 106 | try { 107 | logger.groupEnd(); 108 | } catch (e) { 109 | logger.log('—— log end ——'); 110 | } 111 | } 112 | 113 | prevState = nextState; 114 | }); 115 | }; 116 | } 117 | ``` 118 | 119 | `createLogger` 接收一个 `options` 对象,默认为 `{}` : 120 | 121 | - collapsed: 默认为 true, 自动展开记录的 mutation 122 | - filter: 默认为 true,过滤 mutation 记录 123 | - transformer: 在开始记录之前转换状态 124 | - mutationTransformer: 格式化 mutation 记录 125 | - logger: 默认为 console,自定义 console 126 | 127 | `createLogger` 返回了一个函数,首先申明 `prevState` 变量,赋值为深拷贝后的 `store.state` 对象, 128 | 调用 `store` 的 `subscribe` 方法添加事件订阅,传入一个回调函数,在回调函数中接收 `mutation` `state` 两个参数,判断 `logger` 的类型为 `undefined` 就 `return`。 129 | 130 | 申明 `nextState` 变量,赋值为深拷贝后的回调函数中传入的 `state` 对象, 131 | 接着判断 `filter` 函数,这个默认为 `true`,进入 `if` 循环后会申明 `time` 变量保存当前事件戳,申明 `formattedTime` 变量保存格式化后的时间, 申明 `formattedMutation` 保存处理后的经过 `mutationTransformer`处理后的 `mutation`,申明 `message` 保存默认信息,申明 `startMessage` 变量,根据传入的 `collapsed` 赋值为不同的打印方法。 132 | 133 | ```js 134 | console.groupCollapsed: 设置折叠的分组信息 135 | console.group: 设置不折叠的分组信息 136 | console.groupEnd: 结束当前的分组 137 | ``` 138 | 139 | 接着使用 `call` 将 `startMessage` 的 `this` 绑定到 `logger` 上,并且传入 `message` 默认参数。 140 | 141 | ```js 142 | // render 143 | try { 144 | startMessage.call(logger, message); 145 | } catch (e) { 146 | console.log(message); 147 | } 148 | ``` 149 | 150 | 接着就是调用 `logger.log` 打印,随后调用 `groupEnd` 结束当前的分组。 151 | 152 | 最后将 `prevState` 赋值为 `nextState`,保持状态更新。 153 | 154 | 两个处理时间的函数: 155 | 156 | ```js 157 | // 调用数组的 join,返回指定数量的字符串 158 | function repeat(str, times) { 159 | return new Array(times + 1).join(str); 160 | } 161 | 162 | // 保持总长度为 maxLength,在数字前补 0 163 | function pad(num, maxLength) { 164 | return repeat('0', maxLength - num.toString().length) + num; 165 | } 166 | ``` 167 | -------------------------------------------------------------------------------- /content/vuex-init.md: -------------------------------------------------------------------------------- 1 | ## Vuex 是什么? 2 | 3 | `Vuex` 是一个专为 `Vue.js` 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。 4 | 5 | > [阅读 vuex 源码的思维导图:](https://sailor-1256168624.cos.ap-chengdu.myqcloud.com/blog/vuex.png) 6 | 7 | ![阅读 vuex 源码的思维导图](https://sailor-1256168624.cos.ap-chengdu.myqcloud.com/blog/vuex-mini.png) 8 | 9 | [vuex 的文档](https://vuex.vuejs.org/zh/) 对辅助看源码有不小的帮助,不妨在看源码之前仔细地撸一遍文档。 10 | 11 | ## 带着问题去看源码 12 | 13 | - 1. global event bus 有何缺陷 14 | - 2. \$store 如何注入到所有子组件 15 | - 3. mapState 实现 16 | - 4. mapGetter 如何映射 17 | - 5. Mutation 同步 && Action 异步 18 | - 6. dispatch 方法实现 19 | - 7. module 分割实现 && 局部状态 namespaced 20 | - 8. 如何调用 vue-devtools 21 | - 9. 内置 logger 插件实现 22 | - 10. hotUpdate 23 | - 11. 时间穿梭功能实现 24 | 25 | ## 目录 26 | 27 | ```js 28 | ├── src 29 | │   ├── helpers.js // 辅助函数 30 | │   ├── index.esm.js 31 | │   ├── index.js // 入口 32 | │   ├── mixin.js // 混入 vuexInit 33 | │   ├── module // class module 34 | │   │   ├── module-collection.js 35 | │   │   └── module.js 36 | │   ├── plugins // 插件 37 | │   │   ├── devtool.js 38 | │   │   └── logger.js 39 | │   ├── store.js // store install 40 | │   └── util.js // 工具函数 41 | ``` 42 | 43 | ## 入口文件 44 | 45 | vuex 的入口文件在 `src/index.js` 46 | 47 | ```js 48 | import { Store, install } from './store'; 49 | import { 50 | mapState, 51 | mapMutations, 52 | mapGetters, 53 | mapActions, 54 | createNamespacedHelpers 55 | } from './helpers'; 56 | 57 | export default { 58 | Store, 59 | install, 60 | version: '__VERSION__', 61 | mapState, 62 | mapMutations, 63 | mapGetters, 64 | mapActions, 65 | createNamespacedHelpers 66 | }; 67 | ``` 68 | 69 | 引入了 `Store` 、`install` 和一些辅助工具函数,将引入的变量组装成一个对象向外暴露。当我们在项目中引入 `import Vuex from 'vuex'` 的之后, `Vuex` 就是这个组装后默认导出的对象了。 70 | 71 | 当然我们也可以通过解构的方式。 72 | 73 | ```js 74 | import { Store, install } from 'vuex'` 75 | ``` 76 | 77 | ## install 方法 78 | 79 | 来看一下 `install` 方法,在 `src/store.js` 。 80 | 81 | ```js 82 | export function install(_Vue) { 83 | if (Vue && _Vue === Vue) { 84 | if (process.env.NODE_ENV !== 'production') { 85 | console.error( 86 | '[vuex] already installed. Vue.use(Vuex) should be called only once.' 87 | ); 88 | } 89 | return; 90 | } 91 | Vue = _Vue; 92 | // vuexInit 93 | applyMixin(Vue); 94 | } 95 | ``` 96 | 97 | install 函数首先判断变量 `Vue` (`store.js` 顶部申明的变量) 是否与传入 `_Vue` 全等,如果全等并且在非生产环境,抛出异常。 98 | 99 | 随后将传入的 `_Vue` 赋值给 `Vue`,这里主要是为了避免重复安装。 100 | 101 | 然后调用引入的 `applyMixin` 方法,并将 `Vue` 作为参数传入。 102 | 103 | `applyMixin` 在 `src/mixin.js` 作为默认方法导出: 104 | 105 | ```js 106 | export default function(Vue) { 107 | const version = Number(Vue.version.split('.')[0]); 108 | if (version >= 2) { 109 | Vue.mixin({ beforeCreate: vuexInit }); 110 | } else { 111 | // override init and inject vuex init procedure 112 | // for 1.x backwards compatibility. 113 | const _init = Vue.prototype._init; 114 | Vue.prototype._init = function(options = {}) { 115 | options.init = options.init ? [vuexInit].concat(options.init) : vuexInit; 116 | _init.call(this, options); 117 | }; 118 | } 119 | 120 | /** 121 | * Vuex init hook, injected into each instances init hooks list. 122 | */ 123 | 124 | function vuexInit() { 125 | const options = this.$options; 126 | if (options.store) { 127 | this.$store = 128 | typeof options.store === 'function' ? options.store() : options.store; 129 | } else if (options.parent && options.parent.$store) { 130 | this.$store = options.parent.$store; 131 | } 132 | } 133 | } 134 | ``` 135 | 136 | 取出传入 `Vue` 的 静态属性 `version` 做不同处理。 137 | 138 | 2.0 采用 `mixin` 将 `vuexInit` 合并到 `beforeCreate` 生命周期钩子。 139 | 140 | 1.0 重写 `_init` 方法 将 `vuexInit` 合并到 `_init` 方法中。 141 | 142 | 在 `vuexInit` 方法中,首先判断如果有 `options.store` 说明是 `root` 节点,并且判断 `store` 是 `function` 就执行将函数返回值赋值给 `this.$store` ,否则 `options.store` 直接赋值。 143 | 然后判断有父节点,并且父节点有 `$store`, 就将父节点的 `$store` 赋值给 `this.$store` ,这样就保证只有一个全局的 `$store` 变量。 144 | -------------------------------------------------------------------------------- /content/vuex-questions.md: -------------------------------------------------------------------------------- 1 | ## 问题总结 2 | 3 | ### global eventBus 有何缺陷 4 | 5 | `eventBus` 比较适合简单应用,但是随着需求增加,组件之间通信增多,`eventBus` 就显得不够直观,不方便我们管理,而且随着组件复用的增多,多个组件通信,又相互通信,就容易导致混乱。 6 | 7 | ### \$store 如何注入到所有子组件 8 | 9 | `$store` 是在 vuex install 初始化的时候赋值的,来看一下代码: 10 | 11 | ```js 12 | /** 13 | * Vuex init hook, injected into each instances init hooks list. 14 | */ 15 | 16 | function vuexInit() { 17 | const options = this.$options; 18 | if (options.store) { 19 | this.$store = 20 | typeof options.store === 'function' ? options.store() : options.store; 21 | } else if (options.parent && options.parent.$store) { 22 | this.$store = options.parent.$store; 23 | } 24 | } 25 | ``` 26 | 27 | 在 `vuexInit` 方法中,首先判断如果有 `this.$options.store` 说明是 `root` 节点,判断 `store` 如果是 `function` 就将函数执行后的返回赋值给 `this.$store` ,否则将 `options.store` 直接赋值给 `this.$store`。 28 | 29 | 不是 `root` 节点就从父组件中获取 `$store`,这样就保证只有一个全局的 `$store`。 30 | 31 | ### mapState 实现 32 | 33 | `mapState` 请看 `src/helpers.js` 的 `mapState` 部分。 34 | 35 | ### mapGetter 如何映射 36 | 37 | `mapGetter` 方法最后会返回一个对象,这个对象的每一个 `key` 值是 `mappedGetter` 方法,`mappedGetter` 会返回 `this.$store.getters[key]`。 38 | 39 | ```js 40 | mapGetters({ 41 | // 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount` 42 | doneCount: 'doneTodosCount' 43 | }); 44 | ``` 45 | 46 | ### Mutation 同步 && Action 异步 47 | 48 | 在注册 `action` 时储会将 `action` 的回调包装成 `promise`,通过 `dispatch` 方法触发 `action` 的时候,最后 `return` 的是个 `Promise` 对象,所以 `action` 支持异步。 49 | 50 | 注册 `mutation` 和通过 `commit` 方法触发 `mutation` 的时候,都只是一个同步的代码,仍然是同步代码。 51 | 52 | ### dispatch 方法实现 53 | 54 | `dispatch` 请看 `src/store.js` 的 `dispatch` 部分。 55 | 56 | ### module 分割实现 && 局部状态 namespaced 57 | 58 | 实例化 `ModuleCollection` 59 | 60 | 请看 `class ModuleCollection`。 61 | 62 | ### 如何调用 vue-devtools 63 | 64 | 在 `devtoolPlugin` 方法中,取出挂在 `window` 对象的 `__VUE_DEVTOOLS_GLOBAL_HOOK__` 保存到 `devtoolHook`,通过 `emit` `vuex:init` 初始化 `store`: 65 | 66 | ```js 67 | devtoolHook.emit('vuex:init', store); 68 | ``` 69 | 70 | ```js 71 | devtoolHook.on('vuex:travel-to-state', targetState => { 72 | store.replaceState(targetState); 73 | }); 74 | ``` 75 | 76 | ```js 77 | store.subscribe((mutation, state) => { 78 | devtoolHook.emit('vuex:mutation', mutation, state); 79 | }); 80 | ``` 81 | 82 | ```js 83 | export default function devtoolPlugin(store) { 84 | if (!devtoolHook) return; 85 | 86 | store._devtoolHook = devtoolHook; 87 | 88 | // 向 vueTools emit 事件 并传入当前的 store 89 | // devtoolHook 监听到会根据 store 初始化 vuex 90 | devtoolHook.emit('vuex:init', store); 91 | 92 | // devtoolHook 监听 vuex:travel-to-state,调用回调函数 93 | devtoolHook.on('vuex:travel-to-state', targetState => { 94 | store.replaceState(targetState); 95 | }); 96 | 97 | store.subscribe((mutation, state) => { 98 | devtoolHook.emit('vuex:mutation', mutation, state); 99 | }); 100 | } 101 | ``` 102 | 103 | ### 内置 logger 插件实现 104 | 105 | 请看插件 `devtool` 部分。 106 | 107 | ### hotUpdate 108 | 109 | 使用 `webpack` 的 `Hot Module Replacement API` 实现热重载。 110 | 111 | ```js 112 | if (module.hot) { 113 | module.hot.accept(['./getters', './actions', './mutations'], () => { 114 | store.hotUpdate({ 115 | getters: require('./getters'), 116 | actions: require('./actions'), 117 | mutations: require('./mutations') 118 | }); 119 | }); 120 | } 121 | ``` 122 | 123 | ### 时间穿梭功能实现 124 | 125 | 当我们调用 `devtoolHook` 方法的时候,会调用 `devtoolHook` 的 `on` 方法监听 `vuex:travel-to-state` 事件。 126 | 127 | 在 `vue-devtools` 的源码的 `src/bridge.js` 中: 128 | 129 | ```js 130 | import { EventEmitter } from 'events'; 131 | ``` 132 | 133 | 我们看到事件监听是通过 `Node` 的 `EventEmitter` 监听的。 134 | 135 | ```js 136 | devtoolHook.on('vuex:travel-to-state', targetState => { 137 | store.replaceState(targetState); 138 | }); 139 | ``` 140 | 141 | 在回调函数中接收 `targetState` 参数,调用 `Store` 的 `replaceState` 方法去修改 `this._vm._data.$$state`,当我们点击 `devtoolHook` 的某一条 `mutation` 历史记录,就能穿梭到历史记录。 142 | 143 | 但是这个历史记录又是怎么出现的呢?是通过调用 `store.subscribe` 方法: 144 | 145 | ```js 146 | store.subscribe((mutation, state) => { 147 | devtoolHook.emit('vuex:mutation', mutation, state); 148 | }); 149 | ``` 150 | 151 | 每当调用 `commit` 方法的时候,都会调用 152 | 153 | ```js 154 | this._subscribers.forEach(sub => sub(mutation, this.state)); 155 | ``` 156 | 157 | 循环调用 `_subscribers` 中的回调函数,回调函数会调用 `devtoolHook.emit` 方法,发送 `vuex:mutation`,说明改变了 `mutation`,并把 `mutation` 和 `state` 作为参数传入,`devtoolHook` 就会储存 `mutation` 的历史记录了。 158 | 159 | `vuex` 相关在 `vue-devtools/src/backend/vuex.js`: 160 | 161 | ```js 162 | // application -> devtool 163 | hook.on('vuex:mutation', ({ type, payload }) => { 164 | if (!SharedData.recordVuex) return; 165 | 166 | const index = mutations.length; 167 | 168 | mutations.push({ 169 | type, 170 | payload, 171 | index, 172 | handlers: store._mutations[type] 173 | }); 174 | 175 | bridge.send('vuex:mutation', { 176 | mutation: { 177 | type: type, 178 | payload: stringify(payload), 179 | index 180 | }, 181 | timestamp: Date.now() 182 | }); 183 | }); 184 | ``` 185 | 186 | 看到是通过一个 `mutations` 数组模拟这个历史记录,每次监听到 `vuex:mutation` 事件就是 `push` `mutation` 相关。 187 | -------------------------------------------------------------------------------- /content/vuex-util.md: -------------------------------------------------------------------------------- 1 | ## 工具函数 2 | 3 | 工具函数在 `src/util.js`。 4 | 5 | ### find 6 | 7 | ```js 8 | /** 9 | * Get the first item that pass the test 10 | * by second argument function 11 | * 12 | * @param {Array} list 13 | * @param {Function} f 14 | * @return {*} 15 | */ 16 | export function find(list, f) { 17 | return list.filter(f)[0]; 18 | } 19 | ``` 20 | 21 | `find` 接收 `list` 数组,`f` 回调函数,调用 `filter` 返回匹配 `f` 函数的第一个。 22 | 23 | ### deepCopy 24 | 25 | `deepCopy` 函数: 26 | 27 | ```js 28 | /** 29 | * Deep copy the given object considering circular structure. 30 | * This function caches all nested objects and its copies. 31 | * If it detects circular structure, use cached copy to avoid infinite loop. 32 | * 33 | * @param {*} obj 34 | * @param {Array} cache 35 | * @return {*} 36 | */ 37 | export function deepCopy(obj, cache = []) { 38 | // just return if obj is immutable value 39 | if (obj === null || typeof obj !== 'object') { 40 | return obj; 41 | } 42 | 43 | // if obj is hit, it is in circular structure 44 | const hit = find(cache, c => c.original === obj); 45 | if (hit) { 46 | return hit.copy; 47 | } 48 | 49 | const copy = Array.isArray(obj) ? [] : {}; 50 | // put the copy into cache at first 51 | // because we want to refer it in recursive deepCopy 52 | cache.push({ 53 | original: obj, 54 | copy 55 | }); 56 | 57 | Object.keys(obj).forEach(key => { 58 | copy[key] = deepCopy(obj[key], cache); 59 | }); 60 | 61 | return copy; 62 | } 63 | ``` 64 | 65 | `deepCopy` 接收一个 `obj` 和 `cache` 数组作为参数,初次调用时 `cache` 为空数组。 66 | 67 | 首先判断 `obj` 全等于 `null` 或者 `obj` 的类型不等于 `object` 就返回 `obj`,接下来调用 `find`,将 `cache` 和 回调传入,会使用 `filter` 去过滤匹配的对象,`c.original` 全等于当前循环的 `obj` 对象 ,这里判断的是一个引用地址,`find` 函数会返回匹配 `f` 函数的第一个。 68 | 69 | 如果有 `hit` 就说明是环形结构,直接返回 `hit.copy`。 70 | 71 | ```js 72 | const obj = { 73 | a: 1 74 | }; 75 | obj.b = obj; 76 | ``` 77 | 78 | 所谓环形环形结构,就是对象之间相互引用。 79 | 80 | 接下来申明 `copy` 变量,如果 `obj` 是数组 `copy` 等于空数组,否则就是空对象, 81 | 82 | 保存 `cache`: 83 | 84 | ```js 85 | cache.push({ 86 | original: obj, 87 | copy 88 | }); 89 | ``` 90 | 91 | 以 `original` 为 `key`, `obj` 为 `value`,将已经上面申明的 `copy` 变量包装成对象 `push` 到 `cache` 数组中。 92 | 93 | 循环 `obj keys`,递归调用 `deepCopy` 将 `obj[key]` 和缓存的 `cache` 作为参数传入。 94 | 95 | 最后将深拷贝的 `copy` 对象返回。 96 | 97 | ### forEachValue 98 | 99 | ```js 100 | /** 101 | * forEach for object 102 | */ 103 | export function forEachValue(obj, fn) { 104 | Object.keys(obj).forEach(key => fn(obj[key], key)); 105 | } 106 | ``` 107 | 108 | `forEachValue` 接收 `obj` 和 `fn` 作为参数, 109 | 使用 `Object.keys()` 将 `obj` 转化成数组,使用 `forEach` 循环调用, 110 | 在 `forEach` 的回调函数中,会将 `obj[key]` `key` 作为参数传入 `fn`,循环调用 `fn` 函数。 111 | 112 | ### isObject 113 | 114 | ```js 115 | export function isObject(obj) { 116 | return obj !== null && typeof obj === 'object'; 117 | } 118 | ``` 119 | 120 | `isObject` 接收 `obj` 作为参数,返回 `obj` 不等于 `null` 并且 `obj` 的类型是 `object`,判断传入的对象是否是纯对象,返回 `Boolean`。 121 | 122 | ### isPromise 123 | 124 | ```js 125 | export function isPromise(val) { 126 | return val && typeof val.then === 'function'; 127 | } 128 | ``` 129 | 130 | `isPromise` 接收 `val` 作为参数,返回有 `val` 并且 `val` 的 `then` 是一个 `function`,只是简单判断一个有没有 `then` 方法。 131 | 132 | ### assert 133 | 134 | ```js 135 | export function assert(condition, msg) { 136 | if (!condition) throw new Error(`[vuex] ${msg}`); 137 | } 138 | ``` 139 | 140 | `assert` 接收 `condition` 和 `msg` 作为参数,如果 `condition` 取非为真,就调用 `throw new Error` 抛出异常。 141 | -------------------------------------------------------------------------------- /content/年度总结/我的 2018.md: -------------------------------------------------------------------------------- 1 | # 我的 2018 2 | 3 | 兜兜转转又是一年,2018 收获颇多,在这里稍微做一下总结。 4 | 5 | 2018 年初,本打算往杭州发展,年前就前往杭州面试,也拿了几份 offer,但是计划终究赶不上变化,年后还是留在了上海,入职了一家比较满意的公司,也开始沉淀一些东西。 6 | 7 | ## 回顾 2018 8 | 9 | ### 深入学习 vue、vuex、vue-router 源码 10 | 11 | 有些粗糙的过了一遍 vue 源码,也研究了 vuex、vue-router 的源码,对 vue 有了更加深入的了解。 12 | 13 | #### [vuex 源码阅读笔记](https://github.com/zhanghao-zhoushan/record/blob/master/vue/vuex.md) 14 | 15 | [vuex 思维导图 原图](https://sailor-1256168624.cos.ap-chengdu.myqcloud.com/blog/vuex.png) 16 | 17 | ![vuex 思维导图](https://sailor-1256168624.cos.ap-chengdu.myqcloud.com/blog/vuex-mini.png) 18 | 19 | #### [阅读 vue-router 源码的一些注释](https://github.com/zhanghao-zhoushan/vue-router/tree/dev/src) 20 | 21 | [vue-router 思维导图](https://sailor-1256168624.cos.ap-chengdu.myqcloud.com/blog/vue-router.png) 22 | 23 | ![vue-router 思维导图 原图](https://sailor-1256168624.cos.ap-chengdu.myqcloud.com/blog/vue-router-mini.png) 24 | 25 | ### 关于 React 的学习 26 | 27 | #### [React todo list](https://github.com/zhanghao-zhoushan/react-todolist) 28 | 29 | 使用了 todolist 作为练手项目,期间重构了多个版本,主要有 redux 、dva 、rematch 版本,熟悉 react 、 jsx 语法,熟悉 redux 、dva 、rematch,以及项目构建。 30 | 31 | ![React todo list](https://sailor-1256168624.cos.ap-chengdu.myqcloud.com/todolist.jpg) 32 | 33 | #### [bing-app](https://zhanghao-zhoushan.cn/bing-app/#/) 34 | 35 | 收集 Bing 每日精彩图片的网站,前端采用 React、后端采用 koa2 构建。 36 | 37 | ![Bing Image Collect](https://sailor-1256168624.cos.ap-chengdu.myqcloud.com/bing.jpg) 38 | 39 | ### 其他的 40 | 41 | #### [📏 使用 ESLint 来管理你的 git commit 代码规范](https://github.com/zhanghao-zhoushan/use-eslint-in-git-hooks) 42 | 43 | 逐渐认识到代码质量的重要性,开始使用 ESLint 来管理你的代码规范。 44 | 45 | 提交前强制校验,使用 husky,将约束命令放置在提交代码前检查。 46 | 47 | #### [📜 帮助你创建你的项目模板文件](https://github.com/zhanghao-zhoushan/create-project-template) 48 | 49 | 一个 npm 包,用来帮助我建立项目模板文件。 50 | 51 | #### [Lodash 源码解析](https://github.com/zhanghao-zhoushan/record/issues/15) 52 | 53 | 阅读 Lodash 源码, Get 到很多新技能。 54 | 55 | #### [Learn Advanced Vue.js Features with the Creator of Vue.js, Evan You!](https://frontendmasters.com/courses/advanced-vue/) 56 | 57 | 在 [Frontend Masters](https://frontendmasters.com/) 中有很大师级别的课程,这个是尤大大的课程,学习高级 Vue.js 功能。 58 | 59 | ### 2018 看过的书 60 | 61 | - [JavaScript 忍者秘籍(第 2 版)](https://book.douban.com/subject/30143702/) 62 | 63 | - [高性能 JavaScript](https://book.douban.com/subject/5362856/) 64 | 65 | - [深入理解 ES6](https://book.douban.com/subject/27072230/) 66 | 67 | - [编写可维护的 JavaScript](https://book.douban.com/subject/21792530) 68 | 69 | - [JS 函数式编程指南](https://legacy.gitbook.com/book/llh911001/mostly-adequate-guide-chinese/details) 70 | 71 | ### 2018 文章收集 72 | 73 | - [2018 Collect](https://github.com/zhanghao-zhoushan/record/blob/master/sailor/2018.md) 74 | 75 | - [2017 Collect](https://github.com/zhanghao-zhoushan/record/blob/master/sailor/2017.md) 76 | 77 | ## 你好,2019 ! 78 | 79 | 2019 计划: 80 | 81 | - 要增加个人技术输出。 82 | - 向全栈触发,学习关于服务器端的知识。 83 | - 深入学习 JS 底层,扎实基础。 84 | - 增强英语阅读能力。 85 | 86 | ### 2019 想要了解的 87 | 88 | - [immutable-js](https://github.com/facebook/immutable-js/) 持久化数据结构 89 | 90 | - [RxJS](https://github.com/ReactiveX/rxjs/) 使用 Observables 的响应式编程 91 | 92 | - [Electron](https://electronjs.org/) 跨平台的桌面应用 93 | 94 | - [Flutter](https://flutterchina.club/) 谷歌的移动 UI 框架 95 | 96 | ### 2019 要看的书 97 | 98 | - [Node.js:来一打 C++ 扩展](https://book.douban.com/subject/30247892/) 99 | 100 | - [剑指 Offer:名企面试官精讲典型编程题(第 2 版)](https://book.douban.com/subject/27008702/) 101 | 102 | - [WebKit 技术内幕](https://book.douban.com/subject/25910556/) 103 | 104 | - [大型网站性能优化实战](https://book.douban.com/subject/30437260/) 105 | 106 | - [JavaScript 设计模式与开发实践](https://book.douban.com/subject/26382780/) 107 | 108 | - [GitHub JavaScript 算法与数据结构](https://github.com/trekhleb/javascript-algorithms/blob/master/README.zh-CN.md) 109 | 110 | - [How JavaScript Works](https://www.amazon.com/How-JavaScript-Works-Douglas-Crockford/dp/1949815013) 111 | 112 | - 期待 深入浅出 Node.js 第二版 113 | -------------------------------------------------------------------------------- /content/源码注释/lodash/README.md: -------------------------------------------------------------------------------- 1 | ## 目录 2 | 3 | * [Lodash 源码解析 架构](https://github.com/zhanghao-zhoushan/blog/blob/master/content/源码注释/lodash/methods/Array.md) 4 | * [package.json](https://github.com/zhanghao-zhoushan/blog/blob/master/content/源码注释/lodash/lodash.md) 5 | * [base](https://github.com/zhanghao-zhoushan/blog/blob/master/content/源码注释/lodash/base.md) 6 | * [Array c ~ h](https://github.com/zhanghao-zhoushan/blog/blob/master/content/源码注释/lodash/methods/Array-c~h.md) 7 | * [Array i - s](https://github.com/zhanghao-zhoushan/blog/blob/master/content/源码注释/lodash/methods/Array-i~s.md) 8 | * [Array t - z](https://github.com/zhanghao-zhoushan/blog/blob/master/content/源码注释/lodash/methods/Array-t~z.md) 9 | * [Collection](https://github.com/zhanghao-zhoushan/blog/blob/master/content/源码注释/lodash/methods/Collection.md) 10 | * [Date](https://github.com/zhanghao-zhoushan/blog/blob/master/content/源码注释/lodash/methods/Date.md) 11 | * [Function](https://github.com/zhanghao-zhoushan/blog/blob/master/content/源码注释/lodash/methods/Function.md) 12 | * [Function-bind](https://github.com/zhanghao-zhoushan/blog/blob/master/content/源码注释/lodash/methods/Function-bind.md) 13 | * [Function-debounce](https://github.com/zhanghao-zhoushan/blog/blob/master/content/源码注释/lodash/methods/Function-debounce.md) 14 | * [Lang](https://github.com/zhanghao-zhoushan/blog/blob/master/content/源码注释/lodash/methods/Lang.md) 15 | * [Lang-clone](https://github.com/zhanghao-zhoushan/blog/blob/master/content/源码注释/lodash/methods/Lang-clone.md) 16 | * [MapCache](https://github.com/zhanghao-zhoushan/blog/blob/master/content/源码注释/lodash/methods/MapCache.md) 17 | * [Math](https://github.com/zhanghao-zhoushan/blog/blob/master/content/源码注释/lodash/methods/Math.md) 18 | * [Number](https://github.com/zhanghao-zhoushan/blog/blob/master/content/源码注释/lodash/methods/Number.md) 19 | * [Object a ~ f](https://github.com/zhanghao-zhoushan/blog/blob/master/content/源码注释/lodash/methods/Object-a~f.md) 20 | * [Object g ~ v](https://github.com/zhanghao-zhoushan/blog/blob/master/content/源码注释/lodash/methods/Object-g~v.md) 21 | * [Seq](https://github.com/zhanghao-zhoushan/blog/blob/master/content/源码注释/lodash/methods/Seq.md) 22 | * [String](https://github.com/zhanghao-zhoushan/blog/blob/master/content/源码注释/lodash/methods/String.md) 23 | * [String-template](https://github.com/zhanghao-zhoushan/blog/blob/master/content/源码注释/lodash/methods/String-template.md) 24 | * [Util](https://github.com/zhanghao-zhoushan/blog/blob/master/content/源码注释/lodash/methods/Util.md) 25 | 26 | ## Branch: master 27 | 28 | 在 `Lodash` 的 [README.md](https://github.com/lodash/lodash/blob/master/README.md) 中我们看到 `master` 分支中没有 `package.json` 文件,并不是通过 `npm` 构建的,而是了通过 `lodash-cli` 构建了 `master` 分支。 29 | 30 | ```js 31 | The [Lodash](https://lodash.com/) library exported as a [UMD](https://github.com/umdjs/umd) module. 32 | Generated using [lodash-cli](https://www.npmjs.com/package/lodash-cli): 33 | 34 | $ npm run build 35 | $ lodash -o ./dist/lodash.js 36 | $ lodash core -o ./dist/lodash.core.js 37 | ``` 38 | 39 | 在 [CONTRIBUTING.md](https://github.com/lodash/lodash/blob/master/.github/CONTRIBUTING.md) 中作者也给出了解释,正在进行 `Lodash v5` 版本的开发,导致 `master` 用于贡献的 `npm` 脚本无法正常工作。 40 | 41 | ```js 42 | :construction: Notice :construction: 43 | 44 | Pardon the mess. The `master` branch is in flux while we work on Lodash v5. This 45 | means things like npm scripts, which we encourage using for contributions, may 46 | not be working. Thank you for your patience. 47 | ``` 48 | 49 | ## 分支管理 50 | 51 | 开始阅读 `Lodash` 源码时,我是 `checkout` 到 `4.17.11-pre` 分支来进行阅读的,现在 `github` 上已经删除了 `4.17.11-pre` 分支,增加了新的 `Releases` 包 `4.17.11` 版本,并且增加了 `4.17.12-pre` 分支。 52 | 53 | 我们可以推测下,`lodash` 通过建立 `4.17.11-pre` 分支,做为功能开发分支,功能完成之后,生成 `Releases` 包,`checkout` 到 `4.17.12-pre` 分支,并且删除 `4.17.11-pre` 分支,这样实现功能开发及分支管理。 54 | 55 | ## 阅读建议 56 | 57 | 我是按照 [Lodash 文档](https://lodash.com/docs/4.17.10#partial) 顺序进行阅读,会优先在 [Branch: master](https://github.com/lodash/lodash) 中寻找 `es6` 实现的 `method`,如果没有实现再去看 `4.17.11-pre` 分支。 58 | 59 | 作者正在进行 `v5` 版本的开发,估计到时候会全面拥抱 `es6` 语法,摒弃 `es5`, 因为 `lodash.js` 已经十分庞大了,有 `17102` 行代码,大约 `540k` ,功能迭代全在一个 `js` 中,肯定对功能迭代或者开发过程有一定影响。 60 | 61 | 而 `es6` 基于模块化开发,并且新增很多强大的功能,大可以进行 `lodash` 的功能精简。 62 | 63 | 在阅读过程中,建议将项目 `clone` 到本地, `checkout` 到 `4.17.11-pre` 分支,方面查找 `lodash.js` 中的方法,而在浏览器中打开 [Branch: master](https://github.com/lodash/lodash),建议下载 `Chrome` 的 `Octotree` 、`Awesome Autocomplete for GitHub` 插件,方面切换、查找文件。 64 | 65 | ## 参考文档 66 | 67 | * [lodash](https://lodash.com/docs/4.17.10#chunk) 68 | * [lodash 中文文档](http://lodash.think2011.net/) 69 | 70 | ## 扩展 71 | 72 | * shuffle Fisher-Yates shuffle 洗牌算法 -------------------------------------------------------------------------------- /content/源码注释/lodash/build.md: -------------------------------------------------------------------------------- 1 | 2 | ## package.json 3 | 4 | 基于 `npm` 托管的项目通常都会有 `package.json` 文件。 5 | 6 | `lodash` 的关于项目构建的脚本: 7 | 8 | ```js 9 | "scripts": { 10 | "build": "npm run build:main && npm run build:fp", 11 | "build:fp": "node lib/fp/build-dist.js", 12 | "build:fp-modules": "node lib/fp/build-modules.js", 13 | "build:main": "node lib/main/build-dist.js", 14 | "build:main-modules": "node lib/main/build-modules.js", 15 | ... 16 | } 17 | ``` 18 | 19 | 我们来看 `build`,命令,执行 `build` 命令后会再次执行 `npm run build:main` 和 `npm run build:fp` 命令,而在 `build:fp`、`build:fp` 等命令中会调用 `node` 执行对应 `js` 文件。 20 | 21 | 22 | ## build:main 23 | 24 | `build:main` 命令对应的是 `lib/main/build-dist.js` 文件: 25 | 26 | ```js 27 | 'use strict'; 28 | 29 | const async = require('async'); 30 | const path = require('path'); 31 | 32 | const file = require('../common/file'); 33 | const util = require('../common/util'); 34 | 35 | const basePath = path.join(__dirname, '..', '..'); 36 | const distPath = path.join(basePath, 'dist'); 37 | const filename = 'lodash.js'; 38 | 39 | const baseLodash = path.join(basePath, filename); 40 | const distLodash = path.join(distPath, filename); 41 | 42 | /*----------------------------------------------------------------------------*/ 43 | 44 | /** 45 | * Creates browser builds of Lodash at the `target` path. 46 | * 47 | * @private 48 | * @param {string} target The output directory path. 49 | */ 50 | function build() { 51 | async.series([ 52 | file.copy(baseLodash, distLodash), 53 | file.min(distLodash) 54 | ], util.pitch); 55 | } 56 | 57 | build(); 58 | ``` 59 | 60 | `build-dist.js` 首先会引用辅助 `npm` 包: 61 | 62 | ```js 63 | // 处理异步 JavaScript 64 | const async = require('async'); 65 | // 获取模块路径 66 | const path = require('path'); 67 | 68 | // 封装的公共 file 方法 69 | const file = require('../common/file'); 70 | // 封装的公共 util 方法 71 | const util = require('../common/util'); 72 | 73 | // 基本路径 74 | const basePath = path.join(__dirname, '..', '..'); 75 | // dist 路径 76 | const distPath = path.join(basePath, 'dist'); 77 | // 文件名 78 | const filename = 'lodash.js'; 79 | 80 | // lodash/lodash.js 81 | const baseLodash = path.join(basePath, filename); 82 | // lodash/dist/lodash.js 83 | const distLodash = path.join(distPath, filename); 84 | ``` 85 | 86 | 接着会申明 `build` 函数,并调用: 87 | 88 | ```js 89 | // baseLodash lodash/lodash.js 90 | // distLodash lodash/dist/lodash.js 91 | function build() { 92 | async.series([ 93 | file.copy(baseLodash, distLodash), 94 | file.min(distLodash) 95 | ], util.pitch); 96 | } 97 | 98 | build(); 99 | ``` 100 | 101 | 在 `build` 函数中,调用了 [async.series](https://caolan.github.io/async/docs.html#series) 函数,该函数会串行执行函数(包括异步函数),并且传入 `util.pitch` 回调函数。 102 | 103 | `file.copy` 函数: 104 | 105 | ```js 106 | // srcPath lodash/lodash.js 107 | // destPath lodash/dist/lodash.js 108 | function copy(srcPath, destPath) { 109 | return _.partial(fs.copy, srcPath, destPath); 110 | } 111 | ``` 112 | 113 | `copy` 函数会调用 `partial` 函数进行参数的预设,`partial` 会返回一个函数, 114 | 该函数会调用 [fs.copy](https://github.com/jprichardson/node-fs-extra) 进行文件的复制,就是将 `lodash/lodash.js` 复制为 `lodash/dist/lodash.js`。 115 | 116 | `file.min` 函数: 117 | 118 | ```js 119 | // srcPath lodash/dist/lodash.js 120 | const minify = require('../common/minify.js'); 121 | 122 | function min(srcPath, destPath) { 123 | return _.partial(minify, srcPath, destPath); 124 | } 125 | ``` 126 | 127 | `file.min` 函数用来创建部分 `min` 函数,此时传入了 `srcPath` 也就是 `lodash/dist/lodash.js`。 128 | 129 | `minify` 函数: 130 | 131 | ```js 132 | function minify(srcPath, destPath, callback, options) { 133 | if (_.isFunction(destPath)) { 134 | if (_.isObject(callback)) { 135 | options = callback; 136 | } 137 | callback = destPath; 138 | destPath = undefined; 139 | } 140 | if (!destPath) { 141 | destPath = srcPath.replace(/(?=\.js$)/, '.min'); 142 | } 143 | const output = uglify.minify(srcPath, _.defaults(options || {}, uglifyOptions)); 144 | fs.writeFile(destPath, output.code, 'utf-8', callback); 145 | } 146 | ``` 147 | 148 | 在 `minify` 函数中,主要调用了 `uglify.minify` 对 `lodash/dist/lodash.js` 进行压缩,在 `lodash/dist` 文件夹中生成 `lodash.min.js` 文件。 149 | 150 | `util.pitch` 函数: 151 | 152 | ```js 153 | function pitch(error) { 154 | if (error != null) { 155 | throw error; 156 | } 157 | } 158 | ``` 159 | 160 | `pitch` 函数是 `async.series` 的 `error` 回调。 161 | 162 | 总的来说 `build:main` 命令主要是用来将 `lodash/lodash.js` 拷贝到 `lodash/dist/lodash.js`,接着对 `lodash/dist/lodash.js` 进行压缩生成 `lodash.min.js`,如果就调用 `util.pitch` 函数进行抛出异常信息。 163 | 164 | ## build:fp 165 | 166 | `build:fp` 命令对应的是 `lib/fp/build-dist.js` 文件: 167 | 168 | ```js 169 | 'use strict'; 170 | 171 | const _ = require('lodash'); 172 | const async = require('async'); 173 | const path = require('path'); 174 | const webpack = require('webpack'); 175 | 176 | const file = require('../common/file'); 177 | const util = require('../common/util'); 178 | 179 | const basePath = path.join(__dirname, '..', '..'); 180 | const distPath = path.join(basePath, 'dist'); 181 | const fpPath = path.join(basePath, 'fp'); 182 | const filename = 'lodash.fp.js'; 183 | 184 | const fpConfig = { 185 | 'entry': path.join(fpPath, '_convertBrowser.js'), 186 | 'output': { 187 | 'path': distPath, 188 | 'filename': filename, 189 | 'library': 'fp', 190 | 'libraryTarget': 'umd' 191 | }, 192 | 'plugins': [ 193 | new webpack.optimize.OccurenceOrderPlugin, 194 | new webpack.optimize.DedupePlugin 195 | ] 196 | }; 197 | 198 | const mappingConfig = { 199 | 'entry': path.join(fpPath, '_mapping.js'), 200 | 'output': { 201 | 'path': distPath, 202 | 'filename': 'mapping.fp.js', 203 | 'library': 'mapping', 204 | 'libraryTarget': 'umd' 205 | } 206 | }; 207 | 208 | /*----------------------------------------------------------------------------*/ 209 | 210 | /** 211 | * Creates browser builds of the FP converter and mappings at the `target` path. 212 | * 213 | * @private 214 | * @param {string} target The output directory path. 215 | */ 216 | function build() { 217 | async.series([ 218 | _.partial(webpack, mappingConfig), 219 | _.partial(webpack, fpConfig), 220 | file.min(path.join(distPath, filename)) 221 | ], util.pitch); 222 | } 223 | 224 | build(); 225 | ``` 226 | 227 | `lib/fp/build-dist.js` 与 `lib/main/build-dist.js` 基本相似,首先是引用辅助 `npm` 包: 228 | 229 | ```js 230 | // 引用 lodash 231 | const _ = require('lodash'); 232 | // 处理异步 JavaScript 233 | const async = require('async'); 234 | // 获取模块路径 235 | const path = require('path'); 236 | const webpack = require('webpack'); 237 | 238 | // 封装的公共 file 方法 239 | const file = require('../common/file'); 240 | // 封装的公共 util 方法 241 | const util = require('../common/util'); 242 | 243 | // 基本路径 244 | const basePath = path.join(__dirname, '..', '..'); 245 | // dist 路径 246 | const distPath = path.join(basePath, 'dist'); 247 | // fp 路径 248 | const fpPath = path.join(basePath, 'fp'); 249 | // 文件名 250 | const filename = 'lodash.fp.js'; 251 | ``` 252 | 253 | 接着申明 2 个 `webpack` 配置: 254 | 255 | ```js 256 | const fpConfig = { 257 | 'entry': path.join(fpPath, '_convertBrowser.js'), 258 | 'output': { 259 | 'path': distPath, 260 | 'filename': filename, 261 | 'library': 'fp', 262 | 'libraryTarget': 'umd' 263 | }, 264 | 'plugins': [ 265 | // Order the modules and chunks by occurrence. This saves space, because often referenced modules and chunks get smaller ids. 266 | new webpack.optimize.OccurenceOrderPlugin, 267 | // Deduplicates modules and adds runtime code. 268 | new webpack.optimize.DedupePlugin 269 | ] 270 | }; 271 | 272 | const mappingConfig = { 273 | 'entry': path.join(fpPath, '_mapping.js'), 274 | 'output': { 275 | 'path': distPath, 276 | 'filename': 'mapping.fp.js', 277 | 'library': 'mapping', 278 | 'libraryTarget': 'umd' 279 | } 280 | }; 281 | ``` 282 | 283 | 然后申明 `build` 函数,并调用: 284 | 285 | ```js 286 | function build() { 287 | async.series([ 288 | _.partial(webpack, mappingConfig), 289 | _.partial(webpack, fpConfig), 290 | file.min(path.join(distPath, filename)) 291 | ], util.pitch); 292 | } 293 | 294 | build(); 295 | ``` 296 | 297 | 在 `build` 函数内,调用 `async.series` 函数,传入 2 个调用 `partial` 函数进行参数预设后的 `webpack` 函数,`async.series` 函数会串行执行 `webpack` 函数,接着调用 `file.min` 函数,在 `lodash/dist` 文件夹中生成 `lodash.fp.js` 文件。 298 | -------------------------------------------------------------------------------- /content/源码注释/lodash/methods/Array.md: -------------------------------------------------------------------------------- 1 | # Array Methods 2 | 3 | ## getTag 4 | 5 | > 进行类型的判断。 6 | 7 | ```js 8 | /** `Object#toString` result references. */ 9 | const dataViewTag = '[object DataView]' 10 | const mapTag = '[object Map]' 11 | const objectTag = '[object Object]' 12 | const promiseTag = '[object Promise]' 13 | const setTag = '[object Set]' 14 | const weakMapTag = '[object WeakMap]' 15 | 16 | /** Used to detect maps, sets, and weakmaps. */ 17 | const dataViewCtorString = `${DataView}` 18 | const mapCtorString = `${Map}` 19 | const promiseCtorString = `${Promise}` 20 | const setCtorString = `${Set}` 21 | const weakMapCtorString = `${WeakMap}` 22 | 23 | /** 24 | * Gets the `toStringTag` of `value`. 25 | * 26 | * @private 27 | * @param {*} value The value to query. 28 | * @returns {string} Returns the `toStringTag`. 29 | */ 30 | let getTag = baseGetTag 31 | 32 | // Fallback for data views, maps, sets, and weak maps in IE 11 and promises in Node.js < 6. 33 | if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) || 34 | (getTag(new Map) != mapTag) || 35 | (getTag(Promise.resolve()) != promiseTag) || 36 | (getTag(new Set) != setTag) || 37 | (getTag(new WeakMap) != weakMapTag)) { 38 | getTag = (value) => { 39 | const result = baseGetTag(value) 40 | const Ctor = result == objectTag ? value.constructor : undefined 41 | const ctorString = Ctor ? `${Ctor}` : '' 42 | 43 | if (ctorString) { 44 | switch (ctorString) { 45 | case dataViewCtorString: return dataViewTag 46 | case mapCtorString: return mapTag 47 | case promiseCtorString: return promiseTag 48 | case setCtorString: return setTag 49 | case weakMapCtorString: return weakMapTag 50 | } 51 | } 52 | return result 53 | } 54 | } 55 | ``` 56 | 57 | `getTag` 其实是在 `baseGetTag` 的基础上进行了处理,主要是为了兼容 `IE 11` 上的 `data views, maps, sets, and weak maps`,还有 `Node.js < 6` 时候的 `promises`。 58 | 59 | > TODO: 占坑 60 | 61 | ## baseGetTag 62 | 63 | > `lodash` 重写的类型判断 64 | 65 | ```js 66 | const objectProto = Object.prototype 67 | const hasOwnProperty = objectProto.hasOwnProperty 68 | const toString = objectProto.toString 69 | const symToStringTag = typeof Symbol != 'undefined' ? Symbol.toStringTag : undefined 70 | 71 | /** 72 | * The base implementation of `getTag` without fallbacks for buggy environments. 73 | * 74 | * @private 75 | * @param {*} value The value to query. 76 | * @returns {string} Returns the `toStringTag`. 77 | */ 78 | function baseGetTag(value) { 79 | if (value == null) { 80 | return value === undefined ? '[object Undefined]' : '[object Null]' 81 | } 82 | if (!(symToStringTag && symToStringTag in Object(value))) { 83 | return toString.call(value) 84 | } 85 | const isOwn = hasOwnProperty.call(value, symToStringTag) 86 | const tag = value[symToStringTag] 87 | let unmasked = false 88 | try { 89 | value[symToStringTag] = undefined 90 | unmasked = true 91 | } catch (e) {} 92 | 93 | const result = toString.call(value) 94 | if (unmasked) { 95 | if (isOwn) { 96 | value[symToStringTag] = tag 97 | } else { 98 | delete value[symToStringTag] 99 | } 100 | } 101 | return result 102 | } 103 | ``` 104 | 105 | `baseGetTag` 接收一个 `value` 作为参数,首先会判断在等于 `null` 情况下,全等于 `undefined` 就返回 `[object Undefined]` ,否则就是 `null`,返回 `[object Null]`。 106 | 107 | ```js 108 | const symToStringTag = typeof Symbol != 'undefined' ? Symbol.toStringTag : undefined 109 | ``` 110 | 111 | 通过判断 `typeof` 判断 `Symbol`,如果不等于 `undefined`,就采用 `Symbol.toStringTag` 方法。 112 | 对象的 `Symbol.toStringTag` 属性,指向一个方法。在该对象上面调用 `Object.prototype.toString` 方法时,如果这个属性存在,它的返回值会出现在 `toString` 方法返回的字符串之中,表示对象的类型。 113 | 114 | ```js 115 | if (!(symToStringTag && symToStringTag in Object(value))) { 116 | return toString.call(value) 117 | } 118 | ``` 119 | 120 | 这里判断 `symToStringTag` 说明当前环境支持 `Symbol` ,并且通过 `in` 判断通过 `Object(value)` 转化后的对象是否有这个属性,没有这个属性,`if` 判断成立,返回 `toString.call(value)`,也就是`Object.prototype.toString.call(value)`,会返回 `[object String]` 这样的字符串。 121 | 122 | ```js 123 | const isOwn = hasOwnProperty.call(value, symToStringTag) 124 | ``` 125 | 126 | 通过 `hasOwnProperty` 方法判断 `value` 上是否有这个 `symToStringTag` 属性。 127 | 128 | 这个通过 `try catch` 包裹 `value[symToStringTag] = undefined`,并且将 `unmasked` 置为 `true`, 129 | 130 | 接下来就是对于 `Symbol` 类型的处理。 131 | 132 | ```js 133 | if (unmasked) { 134 | if (isOwn) { 135 | value[symToStringTag] = tag 136 | } else { 137 | delete value[symToStringTag] 138 | } 139 | } 140 | ``` 141 | 142 | ```js 143 | const result = toString.call(value) 144 | ``` 145 | 146 | 最后还是返回 `result`,也就是 `Object.prototype.toString.call(value)`。 147 | 148 | > DataView 149 | 150 | 可使用 `DataView` 对象在 `ArrayBuffer` 中的任何位置读取和写入不同类型的二进制数据。 151 | 152 | 153 | ## baseWhile 154 | 155 | > 根据传入的条件处理后返回数组。 156 | 157 | ```js 158 | /** 159 | * The base implementation of methods like `dropWhile` and `takeWhile`. 160 | * 161 | * @private 162 | * @param {Array} array The array to query. 163 | * @param {Function} predicate The function invoked per iteration. 164 | * @param {boolean} [isDrop] Specify dropping elements instead of taking them. 165 | * @param {boolean} [fromRight] Specify iterating from right to left. 166 | * @returns {Array} Returns the slice of `array`. 167 | */ 168 | function baseWhile(array, predicate, isDrop, fromRight) { 169 | const { length } = array 170 | let index = fromRight ? length : -1 171 | 172 | while ((fromRight ? index-- : ++index < length) && 173 | predicate(array[index], index, array)) {} 174 | 175 | return isDrop 176 | ? slice(array, (fromRight ? 0 : index), (fromRight ? index + 1 : length)) 177 | : slice(array, (fromRight ? index + 1 : 0), (fromRight ? length : index)) 178 | } 179 | ``` 180 | 181 | `baseWhile` 接收四个参数,`array` 数组、`predicate` 迭代调用函数、`isDrop` 是否舍弃 、`fromRight` 是否从右到左。 182 | 183 | 首先申明 `length` 取出 `array` 的长度,接着是一个 `while`,这里是一个 `&&` 连接符号,全部满足进行循环, 第一个条件是三元表达式,是否从右到左 `index` 累减去,否则累加小于数组长度,第二个条件是 `predicate` 函数的运行结果,最后根据 `isDrop` 返回不同的 `slice` 函数,`slice` 传入的下标位置不同,截取不同位置,最后将 `slice` 后得到的数组返回。 184 | 185 | ## baseFill 186 | 187 | > 指定 value 填充数组,从 start 到 end 的位置,但不包括 end 本身的位置。 188 | 189 | ```js 190 | /** 191 | * The base implementation of `_.fill` without an iteratee call guard. 192 | * 193 | * @private 194 | * @param {Array} array The array to fill. 195 | * @param {*} value The value to fill `array` with. 196 | * @param {number} [start=0] The start position. 197 | * @param {number} [end=array.length] The end position. 198 | * @returns {Array} Returns `array`. 199 | */ 200 | function baseFill(array, value, start, end) { 201 | var length = array.length; 202 | 203 | start = toInteger(start); 204 | if (start < 0) { 205 | start = -start > length ? 0 : (length + start); 206 | } 207 | end = (end === undefined || end > length) ? length : toInteger(end); 208 | if (end < 0) { 209 | end += length; 210 | } 211 | end = start > end ? 0 : toLength(end); 212 | while (start < end) { 213 | array[start++] = value; 214 | } 215 | return array; 216 | } 217 | ``` 218 | 219 | `fill` 函数接收 4 个参数,`array` 数组、`value` 填充的值、`start` 开始位置,`end` 结束位置。 220 | 221 | 首先保存数组长度,处理 `start` 、 `end` 边界条件,通过 `toInteger` 转成整数,`while` 循环,`start` 累加,往 `array` 赋值,最后将 `array` 返回。 222 | 223 | 224 | 225 | ## baseIndexOf 226 | 227 | ```js 228 | /** 229 | * The base implementation of `indexOf` without `fromIndex` bounds checks. 230 | * 231 | * @private 232 | * @param {Array} array The array to inspect. 233 | * @param {*} value The value to search for. 234 | * @param {number} fromIndex The index to search from. 235 | * @returns {number} Returns the index of the matched value, else `-1`. 236 | */ 237 | function baseIndexOf(array, value, fromIndex) { 238 | return value === value 239 | ? strictIndexOf(array, value, fromIndex) 240 | : baseFindIndex(array, baseIsNaN, fromIndex) 241 | } 242 | ``` 243 | 244 | ## strictLastIndexOf 245 | 246 | > 严格相等的 lastIndexOf 。 247 | 248 | ```js 249 | /** 250 | * A specialized version of `lastIndexOf` which performs strict equality 251 | * comparisons of values, i.e. `===`. 252 | * 253 | * @private 254 | * @param {Array} array The array to inspect. 255 | * @param {*} value The value to search for. 256 | * @param {number} fromIndex The index to search from. 257 | * @returns {number} Returns the index of the matched value, else `-1`. 258 | */ 259 | function strictLastIndexOf(array, value, fromIndex) { 260 | let index = fromIndex + 1 261 | while (index--) { 262 | if (array[index] === value) { 263 | return index 264 | } 265 | } 266 | return index 267 | } 268 | ``` 269 | `strictLastIndexOf` 接收 3 个参数,`array` 数组、`value` 检索值、`fromIndex` 起始位置。 270 | 271 | 在 `while` 中将 `index` 累减,判断 `value` 相等返回对应下标。 272 | 273 | 274 | 275 | 276 | > 执行 `array` 的二进制搜索,以确定 `value` 的索引。 277 | 278 | ```js 279 | /** Used as references for the maximum length and index of an array. */ 280 | const MAX_ARRAY_LENGTH = 4294967295 281 | const HALF_MAX_ARRAY_LENGTH = MAX_ARRAY_LENGTH >>> 1 282 | 283 | /** 284 | * The base implementation of `sortedIndex` and `sortedLastIndex` which 285 | * performs a binary search of `array` to determine the index at which `value` 286 | * should be inserted into `array` in order to maintain its sort order. 287 | * 288 | * @private 289 | * @param {Array} array The sorted array to inspect. 290 | * @param {*} value The value to evaluate. 291 | * @param {boolean} [retHighest] Specify returning the highest qualified index. 292 | * @returns {number} Returns the index at which `value` should be inserted 293 | * into `array`. 294 | */ 295 | function baseSortedIndex(array, value, retHighest) { 296 | let low = 0 297 | let high = array == null ? low : array.length 298 | 299 | if (typeof value == 'number' && value === value && high <= HALF_MAX_ARRAY_LENGTH) { 300 | while (low < high) { 301 | const mid = (low + high) >>> 1 302 | const computed = array[mid] 303 | if (computed !== null && !isSymbol(computed) && 304 | (retHighest ? (computed <= value) : (computed < value))) { 305 | low = mid + 1 306 | } else { 307 | high = mid 308 | } 309 | } 310 | return high 311 | } 312 | return baseSortedIndexBy(array, value, (value) => value, retHighest) 313 | } 314 | ``` 315 | TODO: 占坑 316 | -------------------------------------------------------------------------------- /content/源码注释/lodash/methods/Date.md: -------------------------------------------------------------------------------- 1 | ## now 2 | 3 | > 获得 Unix 纪元(1970 1月1日 00:00:00 UTC) 直到现在的毫秒数。 4 | 5 | ```js 6 | _.now() 7 | ``` 8 | 9 | ```js 10 | /** 11 | * Gets the timestamp of the number of milliseconds that have elapsed since 12 | * the Unix epoch (1 January 1970 00:00:00 UTC). 13 | * 14 | * @static 15 | * @memberOf _ 16 | * @since 2.4.0 17 | * @category Date 18 | * @returns {number} Returns the timestamp. 19 | * @example 20 | * 21 | * _.defer(function(stamp) { 22 | * console.log(_.now() - stamp); 23 | * }, _.now()); 24 | * // => Logs the number of milliseconds it took for the deferred invocation. 25 | */ 26 | 27 | /** Detect free variable `global` from Node.js. */ 28 | var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; 29 | 30 | /** Detect free variable `self`. */ 31 | var freeSelf = typeof self == 'object' && self && self.Object === Object && self; 32 | 33 | /** Used as a reference to the global object. */ 34 | var root = freeGlobal || freeSelf || Function('return this')(); 35 | 36 | var ctxNow = Date && Date.now !== root.Date.now && Date.now; 37 | var now = ctxNow || function () { 38 | return root.Date.now(); 39 | }; 40 | ``` 41 | 42 | `now` 函数了多平台的兼容,申明 `freeGlobal` 变量,对 `global` 进行类型判断,返回 `Boolean`, 43 | 申明 `freeSelf` 变量,判断 `self` 的类型: 44 | 45 | ```js 46 | // ts 47 | declare var self: Window; 48 | ``` 49 | 50 | `self` 类型就是 `Window` 对象,`freeSelf` 变量也返回 `Boolean`。 51 | 52 | 申明 `root` 变量,进行了穿透赋值,`global` 为真说明是 `Node` 环境,`Window` 为真说明是 `JavsScript` 环境,两者都为 `false`,会赋值成一个返回 `this` 的 `Function`,作为全局对象的引用。 53 | 54 | 申明 `now` 变量,如果有 `ctxNow`,说明当前环境已经有 `Date.now` 函数了,直接复制给 `now`, 55 | 否则就是穿透赋值,赋值为一个返回 `root.Date.now()` 的函数。 56 | 57 | 58 | -------------------------------------------------------------------------------- /content/源码注释/lodash/methods/Function-bind.md: -------------------------------------------------------------------------------- 1 | ## bind 2 | 3 | > 创建一个函数 func,这个函数的 this 会被绑定在 thisArg。 并且任何附加在 _.bind 的参数会被传入到这个绑定函数上。 这个 _.bind.placeholder 的值,默认是以 _ 作为附加部分参数的占位符。 4 | 5 | ```js 6 | _.bind(func, thisArg, [partials]) 7 | ``` 8 | 9 | ```js 10 | /** 11 | * Creates a function that invokes `func` with the `this` binding of `thisArg` 12 | * and `partials` prepended to the arguments it receives. 13 | * 14 | * The `_.bind.placeholder` value, which defaults to `_` in monolithic builds, 15 | * may be used as a placeholder for partially applied arguments. 16 | * 17 | * **Note:** Unlike native `Function#bind`, this method doesn't set the "length" 18 | * property of bound functions. 19 | * 20 | * @static 21 | * @memberOf _ 22 | * @since 0.1.0 23 | * @category Function 24 | * @param {Function} func The function to bind. 25 | * @param {*} thisArg The `this` binding of `func`. 26 | * @param {...*} [partials] The arguments to be partially applied. 27 | * @returns {Function} Returns the new bound function. 28 | * @example 29 | * 30 | * function greet(greeting, punctuation) { 31 | * return greeting + ' ' + this.user + punctuation; 32 | * } 33 | * 34 | * var object = { 'user': 'fred' }; 35 | * 36 | * var bound = _.bind(greet, object, 'hi'); 37 | * bound('!'); 38 | * // => 'hi fred!' 39 | * 40 | * // Bound with placeholders. 41 | * var bound = _.bind(greet, object, _, '!'); 42 | * bound('hi'); 43 | * // => 'hi fred!' 44 | */ 45 | var bind = baseRest(function (func, thisArg, partials) { 46 | var bitmask = WRAP_BIND_FLAG; 47 | if (partials.length) { 48 | var holders = replaceHolders(partials, getHolder(bind)); 49 | bitmask |= WRAP_PARTIAL_FLAG; 50 | } 51 | return createWrap(func, bitmask, thisArg, partials, holders); 52 | }); 53 | ``` 54 | 55 | 位运算符: 56 | 57 | ```js 58 | // 等同于 x = x | y 59 | x |= y 60 | ``` 61 | 62 | ```js 63 | var WRAP_BIND_FLAG = 1; 64 | ``` 65 | 66 | 67 | ## replaceHolders 68 | 69 | > 70 | 71 | ```js 72 | 73 | ``` 74 | 75 | ```js 76 | 77 | ``` 78 | 79 | 80 | ## getHolder 81 | 82 | > 83 | 84 | ```js 85 | 86 | ``` 87 | 88 | ```js 89 | 90 | ``` 91 | 92 | ## createWrap 93 | 94 | > 95 | 96 | ```js 97 | 98 | ``` 99 | 100 | ```js 101 | 102 | ``` -------------------------------------------------------------------------------- /content/源码注释/lodash/methods/Lang-clone.md: -------------------------------------------------------------------------------- 1 | ## clone 2 | 3 | > 创建一个 value 的浅拷贝。 4 | 5 | ```js 6 | _.clone(value) 7 | ``` 8 | 9 | ```js 10 | import baseClone from './.internal/baseClone.js' 11 | 12 | /** Used to compose bitmasks for cloning. */ 13 | const CLONE_SYMBOLS_FLAG = 4 14 | 15 | /** 16 | * Creates a shallow clone of `value`. 17 | * 18 | * **Note:** This method is loosely based on the 19 | * [structured clone algorithm](https://mdn.io/Structured_clone_algorithm) 20 | * and supports cloning arrays, array buffers, booleans, date objects, maps, 21 | * numbers, `Object` objects, regexes, sets, strings, symbols, and typed 22 | * arrays. The own enumerable properties of `arguments` objects are cloned 23 | * as plain objects. An empty object is returned for uncloneable values such 24 | * as error objects, functions, DOM nodes, and WeakMaps. 25 | * 26 | * @since 0.1.0 27 | * @category Lang 28 | * @param {*} value The value to clone. 29 | * @returns {*} Returns the cloned value. 30 | * @see cloneDeep 31 | * @example 32 | * 33 | * const objects = [{ 'a': 1 }, { 'b': 2 }] 34 | * 35 | * const shallow = clone(objects) 36 | * console.log(shallow[0] === objects[0]) 37 | * // => true 38 | */ 39 | function clone(value) { 40 | return baseClone(value, CLONE_SYMBOLS_FLAG) 41 | } 42 | ``` 43 | 44 | ```js 45 | import Stack from './Stack.js' 46 | import arrayEach from './arrayEach.js' 47 | import assignValue from './assignValue.js' 48 | import baseAssign from './baseAssign.js' 49 | import baseAssignIn from './baseAssignIn.js' 50 | import cloneBuffer from './cloneBuffer.js' 51 | import copyArray from './copyArray.js' 52 | import cloneArrayBuffer from './cloneArrayBuffer.js' 53 | import cloneDataView from './cloneDataView.js' 54 | import cloneRegExp from './cloneRegExp.js' 55 | import cloneSymbol from './cloneSymbol.js' 56 | import cloneTypedArray from './cloneTypedArray.js' 57 | import copySymbols from './copySymbols.js' 58 | import copySymbolsIn from './copySymbolsIn.js' 59 | import getAllKeys from './getAllKeys.js' 60 | import getAllKeysIn from './getAllKeysIn.js' 61 | import getTag from './getTag.js' 62 | import initCloneObject from './initCloneObject.js' 63 | import isBuffer from '../isBuffer.js' 64 | import isObject from '../isObject.js' 65 | import keys from '../keys.js' 66 | import keysIn from '../keysIn.js' 67 | 68 | /** Used to compose bitmasks for cloning. */ 69 | const CLONE_DEEP_FLAG = 1 70 | const CLONE_FLAT_FLAG = 2 71 | const CLONE_SYMBOLS_FLAG = 4 72 | 73 | /** `Object#toString` result references. */ 74 | const argsTag = '[object Arguments]' 75 | const arrayTag = '[object Array]' 76 | const boolTag = '[object Boolean]' 77 | const dateTag = '[object Date]' 78 | const errorTag = '[object Error]' 79 | const mapTag = '[object Map]' 80 | const numberTag = '[object Number]' 81 | const objectTag = '[object Object]' 82 | const regexpTag = '[object RegExp]' 83 | const setTag = '[object Set]' 84 | const stringTag = '[object String]' 85 | const symbolTag = '[object Symbol]' 86 | const weakMapTag = '[object WeakMap]' 87 | 88 | const arrayBufferTag = '[object ArrayBuffer]' 89 | const dataViewTag = '[object DataView]' 90 | const float32Tag = '[object Float32Array]' 91 | const float64Tag = '[object Float64Array]' 92 | const int8Tag = '[object Int8Array]' 93 | const int16Tag = '[object Int16Array]' 94 | const int32Tag = '[object Int32Array]' 95 | const uint8Tag = '[object Uint8Array]' 96 | const uint8ClampedTag = '[object Uint8ClampedArray]' 97 | const uint16Tag = '[object Uint16Array]' 98 | const uint32Tag = '[object Uint32Array]' 99 | 100 | /** Used to identify `toStringTag` values supported by `clone`. */ 101 | const cloneableTags = {} 102 | cloneableTags[argsTag] = cloneableTags[arrayTag] = 103 | cloneableTags[arrayBufferTag] = cloneableTags[dataViewTag] = 104 | cloneableTags[boolTag] = cloneableTags[dateTag] = 105 | cloneableTags[float32Tag] = cloneableTags[float64Tag] = 106 | cloneableTags[int8Tag] = cloneableTags[int16Tag] = 107 | cloneableTags[int32Tag] = cloneableTags[mapTag] = 108 | cloneableTags[numberTag] = cloneableTags[objectTag] = 109 | cloneableTags[regexpTag] = cloneableTags[setTag] = 110 | cloneableTags[stringTag] = cloneableTags[symbolTag] = 111 | cloneableTags[uint8Tag] = cloneableTags[uint8ClampedTag] = 112 | cloneableTags[uint16Tag] = cloneableTags[uint32Tag] = true 113 | cloneableTags[errorTag] = cloneableTags[weakMapTag] = false 114 | 115 | /** Used to check objects for own properties. */ 116 | const hasOwnProperty = Object.prototype.hasOwnProperty 117 | 118 | /** 119 | * Initializes an object clone based on its `toStringTag`. 120 | * 121 | * **Note:** This function only supports cloning values with tags of 122 | * `Boolean`, `Date`, `Error`, `Map`, `Number`, `RegExp`, `Set`, or `String`. 123 | * 124 | * @private 125 | * @param {Object} object The object to clone. 126 | * @param {string} tag The `toStringTag` of the object to clone. 127 | * @param {boolean} [isDeep] Specify a deep clone. 128 | * @returns {Object} Returns the initialized clone. 129 | */ 130 | function initCloneByTag(object, tag, isDeep) { 131 | const Ctor = object.constructor 132 | switch (tag) { 133 | case arrayBufferTag: 134 | return cloneArrayBuffer(object) 135 | 136 | case boolTag: 137 | case dateTag: 138 | return new Ctor(+object) 139 | 140 | case dataViewTag: 141 | return cloneDataView(object, isDeep) 142 | 143 | case float32Tag: case float64Tag: 144 | case int8Tag: case int16Tag: case int32Tag: 145 | case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag: 146 | return cloneTypedArray(object, isDeep) 147 | 148 | case mapTag: 149 | return new Ctor 150 | 151 | case numberTag: 152 | case stringTag: 153 | return new Ctor(object) 154 | 155 | case regexpTag: 156 | return cloneRegExp(object) 157 | 158 | case setTag: 159 | return new Ctor 160 | 161 | case symbolTag: 162 | return cloneSymbol(object) 163 | } 164 | } 165 | 166 | /** 167 | * Initializes an array clone. 168 | * 169 | * @private 170 | * @param {Array} array The array to clone. 171 | * @returns {Array} Returns the initialized clone. 172 | */ 173 | function initCloneArray(array) { 174 | const { length } = array 175 | const result = new array.constructor(length) 176 | 177 | // Add properties assigned by `RegExp#exec`. 178 | if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) { 179 | result.index = array.index 180 | result.input = array.input 181 | } 182 | return result 183 | } 184 | 185 | /** 186 | * The base implementation of `clone` and `cloneDeep` which tracks 187 | * traversed objects. 188 | * 189 | * @private 190 | * @param {*} value The value to clone. 191 | * @param {number} bitmask The bitmask flags. 192 | * 1 - Deep clone 193 | * 2 - Flatten inherited properties 194 | * 4 - Clone symbols 195 | * @param {Function} [customizer] The function to customize cloning. 196 | * @param {string} [key] The key of `value`. 197 | * @param {Object} [object] The parent object of `value`. 198 | * @param {Object} [stack] Tracks traversed objects and their clone counterparts. 199 | * @returns {*} Returns the cloned value. 200 | */ 201 | function baseClone(value, bitmask, customizer, key, object, stack) { 202 | let result 203 | const isDeep = bitmask & CLONE_DEEP_FLAG 204 | const isFlat = bitmask & CLONE_FLAT_FLAG 205 | const isFull = bitmask & CLONE_SYMBOLS_FLAG 206 | 207 | if (customizer) { 208 | result = object ? customizer(value, key, object, stack) : customizer(value) 209 | } 210 | if (result !== undefined) { 211 | return result 212 | } 213 | if (!isObject(value)) { 214 | return value 215 | } 216 | const isArr = Array.isArray(value) 217 | const tag = getTag(value) 218 | if (isArr) { 219 | result = initCloneArray(value) 220 | if (!isDeep) { 221 | return copyArray(value, result) 222 | } 223 | } else { 224 | const isFunc = typeof value == 'function' 225 | 226 | if (isBuffer(value)) { 227 | return cloneBuffer(value, isDeep) 228 | } 229 | if (tag == objectTag || tag == argsTag || (isFunc && !object)) { 230 | result = (isFlat || isFunc) ? {} : initCloneObject(value) 231 | if (!isDeep) { 232 | return isFlat 233 | ? copySymbolsIn(value, baseAssignIn(result, value)) 234 | : copySymbols(value, baseAssign(result, value)) 235 | } 236 | } else { 237 | if (isFunc || !cloneableTags[tag]) { 238 | return object ? value : {} 239 | } 240 | result = initCloneByTag(value, tag, isDeep) 241 | } 242 | } 243 | // Check for circular references and return its corresponding clone. 244 | stack || (stack = new Stack) 245 | const stacked = stack.get(value) 246 | if (stacked) { 247 | return stacked 248 | } 249 | stack.set(value, result) 250 | 251 | if (tag == mapTag) { 252 | value.forEach((subValue, key) => { 253 | result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack)) 254 | }) 255 | return result 256 | } 257 | 258 | if (tag == setTag) { 259 | value.forEach((subValue) => { 260 | result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack)) 261 | }) 262 | return result 263 | } 264 | 265 | if (isTypedArray(value)) { 266 | return result 267 | } 268 | 269 | const keysFunc = isFull 270 | ? (isFlat ? getAllKeysIn : getAllKeys) 271 | : (isFlat ? keysIn : keys) 272 | 273 | const props = isArr ? undefined : keysFunc(value) 274 | arrayEach(props || value, (subValue, key) => { 275 | if (props) { 276 | key = subValue 277 | subValue = value[key] 278 | } 279 | // Recursively populate clone (susceptible to call stack limits). 280 | assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack)) 281 | }) 282 | return result 283 | } 284 | ``` -------------------------------------------------------------------------------- /content/源码注释/lodash/methods/MapCache.md: -------------------------------------------------------------------------------- 1 | ## MapCache 2 | 3 | ```js 4 | /** 5 | * Gets the data for `map`. 6 | * 7 | * @private 8 | * @param {Object} map The map to query. 9 | * @param {string} key The reference key. 10 | * @returns {*} Returns the map data. 11 | */ 12 | function getMapData({ __data__ }, key) { 13 | const data = __data__ 14 | return isKeyable(key) 15 | ? data[typeof key == 'string' ? 'string' : 'hash'] 16 | : data.map 17 | } 18 | 19 | /** 20 | * Checks if `value` is suitable for use as unique object key. 21 | * 22 | * @private 23 | * @param {*} value The value to check. 24 | * @returns {boolean} Returns `true` if `value` is suitable, else `false`. 25 | */ 26 | function isKeyable(value) { 27 | const type = typeof value 28 | return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean') 29 | ? (value !== '__proto__') 30 | : (value === null) 31 | } 32 | 33 | class MapCache { 34 | 35 | /** 36 | * Creates a map cache object to store key-value pairs. 37 | * 38 | * @private 39 | * @constructor 40 | * @param {Array} [entries] The key-value pairs to cache. 41 | */ 42 | constructor(entries) { 43 | let index = -1 44 | const length = entries == null ? 0 : entries.length 45 | 46 | this.clear() 47 | while (++index < length) { 48 | const entry = entries[index] 49 | this.set(entry[0], entry[1]) 50 | } 51 | } 52 | 53 | /** 54 | * Removes all key-value entries from the map. 55 | * 56 | * @memberOf MapCache 57 | */ 58 | clear() { 59 | this.size = 0 60 | this.__data__ = { 61 | 'hash': new Hash, 62 | 'map': new (Map || ListCache), 63 | 'string': new Hash 64 | } 65 | } 66 | 67 | /** 68 | * Removes `key` and its value from the map. 69 | * 70 | * @memberOf MapCache 71 | * @param {string} key The key of the value to remove. 72 | * @returns {boolean} Returns `true` if the entry was removed, else `false`. 73 | */ 74 | delete(key) { 75 | const result = getMapData(this, key)['delete'](key) 76 | this.size -= result ? 1 : 0 77 | return result 78 | } 79 | 80 | /** 81 | * Gets the map value for `key`. 82 | * 83 | * @memberOf MapCache 84 | * @param {string} key The key of the value to get. 85 | * @returns {*} Returns the entry value. 86 | */ 87 | get(key) { 88 | return getMapData(this, key).get(key) 89 | } 90 | 91 | /** 92 | * Checks if a map value for `key` exists. 93 | * 94 | * @memberOf MapCache 95 | * @param {string} key The key of the entry to check. 96 | * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. 97 | */ 98 | has(key) { 99 | return getMapData(this, key).has(key) 100 | } 101 | 102 | /** 103 | * Sets the map `key` to `value`. 104 | * 105 | * @memberOf MapCache 106 | * @param {string} key The key of the value to set. 107 | * @param {*} value The value to set. 108 | * @returns {Object} Returns the map cache instance. 109 | */ 110 | set(key, value) { 111 | const data = getMapData(this, key) 112 | const size = data.size 113 | 114 | data.set(key, value) 115 | this.size += data.size == size ? 0 : 1 116 | return this 117 | } 118 | } 119 | ``` -------------------------------------------------------------------------------- /content/源码注释/lodash/methods/Number.md: -------------------------------------------------------------------------------- 1 | ## clamp 2 | 3 | > 返回限制在 min 和 max 之间的值。 4 | 5 | ```js 6 | _.clamp(number, [lower], upper) 7 | ``` 8 | 9 | ```js 10 | /** 11 | * Clamps `number` within the inclusive `lower` and `upper` bounds. 12 | * 13 | * @since 4.0.0 14 | * @category Number 15 | * @param {number} number The number to clamp. 16 | * @param {number} lower The lower bound. 17 | * @param {number} upper The upper bound. 18 | * @returns {number} Returns the clamped number. 19 | * @example 20 | * 21 | * clamp(-10, -5, 5) 22 | * // => -5 23 | * 24 | * clamp(10, -5, 5) 25 | * // => 5 26 | */ 27 | function clamp(number, lower, upper) { 28 | number = +number 29 | lower = +lower 30 | upper = +upper 31 | lower = lower === lower ? lower : 0 32 | upper = upper === upper ? upper : 0 33 | if (number === number) { 34 | number = number <= upper ? number : upper 35 | number = number >= lower ? number : lower 36 | } 37 | return number 38 | } 39 | ``` 40 | 41 | `clamp` 首先是使用 `+` 进行隐式转换成数字,接着是 `lower`、`upper` 的 `NaN` 处理,如果 `number === number` 说明不是 `NaN`,进行大小比较赋值,最后返回 `number`。 42 | 43 | ## inRange 44 | 45 | > 检查 n 是否在 start 与 end 之间,但不包括 end。 如果 end 没有指定,那么 start 设置为0。 如果 start 大于 end,那么参数会交换以便支持负范围。 46 | 47 | ```js 48 | _.inRange(number, [start=0], end) 49 | ``` 50 | 51 | ```js 52 | /** 53 | * Checks if `number` is between `start` and up to, but not including, `end`. If 54 | * `end` is not specified, it's set to `start` with `start` then set to `0`. 55 | * If `start` is greater than `end` the params are swapped to support 56 | * negative ranges. 57 | * 58 | * @since 3.3.0 59 | * @category Number 60 | * @param {number} number The number to check. 61 | * @param {number} [start=0] The start of the range. 62 | * @param {number} end The end of the range. 63 | * @returns {boolean} Returns `true` if `number` is in the range, else `false`. 64 | * @see range, rangeRight 65 | * @example 66 | * 67 | * inRange(3, 2, 4) 68 | * // => true 69 | * 70 | * inRange(4, 8) 71 | * // => true 72 | * 73 | * inRange(4, 2) 74 | * // => false 75 | * 76 | * inRange(2, 2) 77 | * // => false 78 | * 79 | * inRange(1.2, 2) 80 | * // => true 81 | * 82 | * inRange(5.2, 4) 83 | * // => false 84 | * 85 | * inRange(-3, -2, -6) 86 | * // => true 87 | */ 88 | function inRange(number, start, end) { 89 | if (end === undefined) { 90 | end = start 91 | start = 0 92 | } 93 | return baseInRange(+number, +start, +end) 94 | } 95 | ``` 96 | 97 | `inRange` 函数会对没有 `end` 的情况进行处理,将 `start` 赋值给 `end`, `start` 赋值为 0, 98 | 然后返回 `baseInRange` 调用。 99 | 100 | ## baseInRange 101 | 102 | ```js 103 | /** 104 | * The base implementation of `inRange` which doesn't coerce arguments. 105 | * 106 | * @private 107 | * @param {number} number The number to check. 108 | * @param {number} start The start of the range. 109 | * @param {number} end The end of the range. 110 | * @returns {boolean} Returns `true` if `number` is in the range, else `false`. 111 | */ 112 | function baseInRange(number, start, end) { 113 | return number >= Math.min(start, end) && number < Math.max(start, end) 114 | } 115 | ``` 116 | 117 | `baseInRange` 函数返回 2 个大小比较。 118 | 119 | ```js 120 | number >= Math.min(start, end) 121 | ``` 122 | 123 | `number` 大于等于 `start` 和 `end` 的最小值。 124 | 125 | ```js 126 | number < Math.max(start, end) 127 | ``` 128 | `number` 小于 `start` 和 `end` 的最大值。 129 | 130 | 符合 2 个比较返回 `true`,否则返回 `false`。 131 | 132 | ## random 133 | 134 | > 产生一个包括 min 与 max 之间的数。 如果只提供一个参数返回一个 0 到提供数之间的数。 如果 floating 设为 true,或者 min 或 max 是浮点数,结果返回浮点数。 135 | 136 | ```js 137 | 138 | ``` 139 | 140 | ```js 141 | /** Built-in method references without a dependency on `root`. */ 142 | const freeParseFloat = parseFloat 143 | 144 | /** 145 | * Produces a random number between the inclusive `lower` and `upper` bounds. 146 | * If only one argument is provided a number between `0` and the given number 147 | * is returned. If `floating` is `true`, or either `lower` or `upper` are 148 | * floats, a floating-point number is returned instead of an integer. 149 | * 150 | * **Note:** JavaScript follows the IEEE-754 standard for resolving 151 | * floating-point values which can produce unexpected results. 152 | * 153 | * @since 0.7.0 154 | * @category Number 155 | * @param {number} [lower=0] The lower bound. 156 | * @param {number} [upper=1] The upper bound. 157 | * @param {boolean} [floating] Specify returning a floating-point number. 158 | * @returns {number} Returns the random number. 159 | * @see uniqueId 160 | * @example 161 | * 162 | * random(0, 5) 163 | * // => an integer between 0 and 5 164 | * 165 | * random(5) 166 | * // => also an integer between 0 and 5 167 | * 168 | * random(5, true) 169 | * // => a floating-point number between 0 and 5 170 | * 171 | * random(1.2, 5.2) 172 | * // => a floating-point number between 1.2 and 5.2 173 | */ 174 | function random(lower, upper, floating) { 175 | if (floating === undefined) { 176 | if (typeof upper == 'boolean') { 177 | floating = upper 178 | upper = undefined 179 | } 180 | else if (typeof lower == 'boolean') { 181 | floating = lower 182 | lower = undefined 183 | } 184 | } 185 | if (lower === undefined && upper === undefined) { 186 | lower = 0 187 | upper = 1 188 | } else { 189 | lower = toFinite(lower) 190 | if (upper === undefined) { 191 | upper = lower 192 | lower = 0 193 | } else { 194 | upper = toFinite(upper) 195 | } 196 | } 197 | if (lower > upper) { 198 | const temp = lower 199 | lower = upper 200 | upper = temp 201 | } 202 | if (floating || lower % 1 || upper % 1) { 203 | const rand = Math.random() 204 | const randLength = `${rand}`.length - 1 205 | return Math.min(lower + (rand * (upper - lower + freeParseFloat(`1e-${randLength}`)), upper)) 206 | } 207 | return lower + Math.floor(Math.random() * (upper - lower + 1)) 208 | } 209 | ``` 210 | 211 | `random` 函数接收 3 个函数,`lower` 最小值、 `upper` 最大值、`floating` 是否返回浮点数。 212 | 213 | `random` 函数内部有 4 个 `if` 判断。 214 | 215 | ```js 216 | if (floating === undefined) { 217 | if (typeof upper == 'boolean') { 218 | floating = upper 219 | upper = undefined 220 | } 221 | else if (typeof lower == 'boolean') { 222 | floating = lower 223 | lower = undefined 224 | } 225 | } 226 | ``` 227 | 228 | 首先判断 `floating` 是否等于 `undefined`,如果为 `undefined`,说明没有传入第三个参数,接着判断 `upper`、`upper` 类型是否是 `Boolean`,如果是则与 `floating` 进行交换赋值处理,此处主要是处理参数缺少的情况。 229 | 230 | ```js 231 | if (lower === undefined && upper === undefined) { 232 | lower = 0 233 | upper = 1 234 | } else { 235 | lower = toFinite(lower) 236 | if (upper === undefined) { 237 | upper = lower 238 | lower = 0 239 | } else { 240 | upper = toFinite(upper) 241 | } 242 | } 243 | ``` 244 | 245 | 如果 `upper`、`upper` 都为 `undefined`,取默认值 `0 ~ 1`, 246 | 否则将 `lower`、`upper` 转成整数,如果 `upper` 为 `undefined`,`upper` 赋值为 `lower`, `lower` 赋值为 0。 247 | 248 | ```js 249 | if (lower > upper) { 250 | const temp = lower 251 | lower = upper 252 | upper = temp 253 | } 254 | ``` 255 | 256 | 如果 `lower` 大于 `upper`,对它们进行交换赋值。 257 | 258 | ```js 259 | if (floating || lower % 1 || upper % 1) { 260 | const rand = Math.random() 261 | const randLength = `${rand}`.length - 1 262 | return Math.min(lower + (rand * (upper - lower + freeParseFloat(`1e-${randLength}`)), upper)) 263 | } 264 | ``` 265 | 如果 `floating` 为真或者 `lower`、`upper` 不为整数,申明 `rand` 变量保存 `0 ~ 1` 随机数,`randLength` 变量保存随机数长度,这里会得到采用科学计数法保存随机数,并且调用 `Math.min` 函数取 `upper` 与得到随机数的最小值,然后返回。 266 | 267 | ```js 268 | lower + Math.floor(Math.random() * (upper - lower + 1)) 269 | ``` 270 | 271 | 如果不满足上述条件,取到 `upper` 、`lower` 之差的随机数,向下取整,再加上 `lower`,就是 `upper` 、`lower` 之间的随机数,最后返回。 272 | 273 | -------------------------------------------------------------------------------- /content/源码注释/lodash/methods/Properties.md: -------------------------------------------------------------------------------- 1 | ## templateSettings 2 | 3 | ```js 4 | /** 5 | * By default, the template delimiters used by lodash are like those in 6 | * embedded Ruby (ERB) as well as ES2015 template strings. Change the 7 | * following template settings to use alternative delimiters. 8 | * 9 | * @static 10 | * @memberOf _ 11 | * @type {Object} 12 | */ 13 | lodash.templateSettings = { 14 | 15 | /** 16 | * Used to detect `data` property values to be HTML-escaped. 17 | * 18 | * @memberOf _.templateSettings 19 | * @type {RegExp} 20 | */ 21 | 'escape': reEscape, 22 | 23 | /** 24 | * Used to detect code to be evaluated. 25 | * 26 | * @memberOf _.templateSettings 27 | * @type {RegExp} 28 | */ 29 | 'evaluate': reEvaluate, 30 | 31 | /** 32 | * Used to detect `data` property values to inject. 33 | * 34 | * @memberOf _.templateSettings 35 | * @type {RegExp} 36 | */ 37 | 'interpolate': reInterpolate, 38 | 39 | /** 40 | * Used to reference the data object in the template text. 41 | * 42 | * @memberOf _.templateSettings 43 | * @type {string} 44 | */ 45 | 'variable': '', 46 | 47 | /** 48 | * Used to import variables into the compiled template. 49 | * 50 | * @memberOf _.templateSettings 51 | * @type {Object} 52 | */ 53 | 'imports': { 54 | 55 | /** 56 | * A reference to the `lodash` function. 57 | * 58 | * @memberOf _.templateSettings.imports 59 | * @type {Function} 60 | */ 61 | '_': lodash 62 | } 63 | }; 64 | ``` -------------------------------------------------------------------------------- /content/源码注释/lodash/methods/String-template.md: -------------------------------------------------------------------------------- 1 | ## template 2 | 3 | > 创建一个预编译模板方法,可以插入数据到模板中 "interpolate" 分隔符相应的位置。 HTML会在 "escape" 分隔符中转换为相应实体。 在 "evaluate" 分隔符中允许执行JavaScript代码。 在模板中可以自由访问变量。 如果设置了选项对象,则会优先覆盖 _.templateSettings 的值。 4 | 5 | ```js 6 | _.template([string=''], [options={}]) 7 | ``` 8 | 9 | ```js 10 | /** 11 | * Creates a compiled template function that can interpolate data properties 12 | * in "interpolate" delimiters, HTML-escape interpolated data properties in 13 | * "escape" delimiters, and execute JavaScript in "evaluate" delimiters. Data 14 | * properties may be accessed as free variables in the template. If a setting 15 | * object is given, it takes precedence over `_.templateSettings` values. 16 | * 17 | * **Note:** In the development build `_.template` utilizes 18 | * [sourceURLs](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl) 19 | * for easier debugging. 20 | * 21 | * For more information on precompiling templates see 22 | * [lodash's custom builds documentation](https://lodash.com/custom-builds). 23 | * 24 | * For more information on Chrome extension sandboxes see 25 | * [Chrome's extensions documentation](https://developer.chrome.com/extensions/sandboxingEval). 26 | * 27 | * @static 28 | * @since 0.1.0 29 | * @memberOf _ 30 | * @category String 31 | * @param {string} [string=''] The template string. 32 | * @param {Object} [options={}] The options object. 33 | * @param {RegExp} [options.escape=_.templateSettings.escape] 34 | * The HTML "escape" delimiter. 35 | * @param {RegExp} [options.evaluate=_.templateSettings.evaluate] 36 | * The "evaluate" delimiter. 37 | * @param {Object} [options.imports=_.templateSettings.imports] 38 | * An object to import into the template as free variables. 39 | * @param {RegExp} [options.interpolate=_.templateSettings.interpolate] 40 | * The "interpolate" delimiter. 41 | * @param {string} [options.sourceURL='lodash.templateSources[n]'] 42 | * The sourceURL of the compiled template. 43 | * @param {string} [options.variable='obj'] 44 | * The data object variable name. 45 | * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. 46 | * @returns {Function} Returns the compiled template function. 47 | * @example 48 | */ 49 | function template(string, options, guard) { 50 | // Based on John Resig's `tmpl` implementation 51 | // (http://ejohn.org/blog/javascript-micro-templating/) 52 | // and Laura Doktorova's doT.js (https://github.com/olado/doT). 53 | var settings = lodash.templateSettings; 54 | 55 | if (guard && isIterateeCall(string, options, guard)) { 56 | options = undefined; 57 | } 58 | string = toString(string); 59 | options = assignInWith({}, options, settings, customDefaultsAssignIn); 60 | 61 | var imports = assignInWith({}, options.imports, settings.imports, customDefaultsAssignIn), 62 | importsKeys = keys(imports), 63 | importsValues = baseValues(imports, importsKeys); 64 | 65 | var isEscaping, 66 | isEvaluating, 67 | index = 0, 68 | interpolate = options.interpolate || reNoMatch, 69 | source = "__p += '"; 70 | 71 | // Compile the regexp to match each delimiter. 72 | var reDelimiters = RegExp( 73 | (options.escape || reNoMatch).source + '|' + 74 | interpolate.source + '|' + 75 | (interpolate === reInterpolate ? reEsTemplate : reNoMatch).source + '|' + 76 | (options.evaluate || reNoMatch).source + '|$' 77 | , 'g'); 78 | 79 | // Use a sourceURL for easier debugging. 80 | var sourceURL = '//# sourceURL=' + 81 | ('sourceURL' in options 82 | ? options.sourceURL 83 | : ('lodash.templateSources[' + (++templateCounter) + ']') 84 | ) + '\n'; 85 | 86 | string.replace(reDelimiters, function (match, escapeValue, interpolateValue, esTemplateValue, evaluateValue, offset) { 87 | interpolateValue || (interpolateValue = esTemplateValue); 88 | 89 | // Escape characters that can't be included in string literals. 90 | source += string.slice(index, offset).replace(reUnescapedString, escapeStringChar); 91 | 92 | // Replace delimiters with snippets. 93 | if (escapeValue) { 94 | isEscaping = true; 95 | source += "' +\n__e(" + escapeValue + ") +\n'"; 96 | } 97 | if (evaluateValue) { 98 | isEvaluating = true; 99 | source += "';\n" + evaluateValue + ";\n__p += '"; 100 | } 101 | if (interpolateValue) { 102 | source += "' +\n((__t = (" + interpolateValue + ")) == null ? '' : __t) +\n'"; 103 | } 104 | index = offset + match.length; 105 | 106 | // The JS engine embedded in Adobe products needs `match` returned in 107 | // order to produce the correct `offset` value. 108 | return match; 109 | }); 110 | 111 | source += "';\n"; 112 | 113 | // If `variable` is not specified wrap a with-statement around the generated 114 | // code to add the data object to the top of the scope chain. 115 | var variable = options.variable; 116 | if (!variable) { 117 | source = 'with (obj) {\n' + source + '\n}\n'; 118 | } 119 | // Cleanup code by stripping empty strings. 120 | source = (isEvaluating ? source.replace(reEmptyStringLeading, '') : source) 121 | .replace(reEmptyStringMiddle, '$1') 122 | .replace(reEmptyStringTrailing, '$1;'); 123 | 124 | // Frame code as the function body. 125 | source = 'function(' + (variable || 'obj') + ') {\n' + 126 | (variable 127 | ? '' 128 | : 'obj || (obj = {});\n' 129 | ) + 130 | "var __t, __p = ''" + 131 | (isEscaping 132 | ? ', __e = _.escape' 133 | : '' 134 | ) + 135 | (isEvaluating 136 | ? ', __j = Array.prototype.join;\n' + 137 | "function print() { __p += __j.call(arguments, '') }\n" 138 | : ';\n' 139 | ) + 140 | source + 141 | 'return __p\n}'; 142 | 143 | var result = attempt(function () { 144 | return Function(importsKeys, sourceURL + 'return ' + source) 145 | .apply(undefined, importsValues); 146 | }); 147 | 148 | // Provide the compiled function's source by its `toString` method or 149 | // the `source` property as a convenience for inlining compiled templates. 150 | result.source = source; 151 | if (isError(result)) { 152 | throw result; 153 | } 154 | return result; 155 | } 156 | ``` 157 | 158 | `template` 函数接收 3 个参数,`string` 模板字符串、`options` 选项、`guard` 警卫。 159 | 160 | 我们从一个基本的例子说起。 161 | 162 | ```js 163 | // Use the "interpolate" delimiter to create a compiled template. 164 | var compiled = _.template('hello <%= user %>!'); 165 | compiled({ 'user': 'fred' }); 166 | // => 'hello fred!' 167 | ``` 168 | 169 | `compiled` 是调用 `template` 函数,并且传入了 `hello <%= user %>!` 模板字符串返回的函数。 170 | 171 | 调用 `compiled` 函数,并传入 `{ 'user': 'fred' }` 对象,会返回 `hello fred!` 字符串, 172 | `compiled` 内部会将 `<%= user %>` 模板字符串替换成 `fred`,也就是传入对象的中 `user` 对应的 `value`。 173 | 174 | 来看一下 `template` 函数是怎么实现的: 175 | 176 | ```js 177 | var settings = lodash.templateSettings; 178 | 179 | if (guard && isIterateeCall(string, options, guard)) { 180 | options = undefined; 181 | } 182 | string = toString(string); 183 | options = assignInWith({}, options, settings, customDefaultsAssignIn); 184 | 185 | var imports = assignInWith({}, options.imports, settings.imports, customDefaultsAssignIn), 186 | importsKeys = keys(imports), 187 | importsValues = baseValues(imports, importsKeys); 188 | 189 | var isEscaping, 190 | isEvaluating, 191 | index = 0, 192 | interpolate = options.interpolate || reNoMatch, 193 | source = "__p += '"; 194 | ``` 195 | 196 | `template` 函数开始会申明初始变量,`settings` 为 `lodash` 的 `templateSettings` 默认配置, 197 | `imports` 导入对象、`importsKeys` 导入对象 `key` 数组、`importsValues` 导入对象的 `value` 数组。 198 | 199 | ```js 200 | // Compile the regexp to match each delimiter. 201 | var reDelimiters = RegExp( 202 | (options.escape || reNoMatch).source + '|' + 203 | interpolate.source + '|' + 204 | (interpolate === reInterpolate ? reEsTemplate : reNoMatch).source + '|' + 205 | (options.evaluate || reNoMatch).source + '|$' 206 | , 'g'); 207 | ``` 208 | 209 | 接着是一段正则,这里会尝试将 `options` 中正则用 `|` 连接,默认为 `reNoMatch`。 210 | 211 | ```js 212 | // Use a sourceURL for easier debugging. 213 | var sourceURL = '//# sourceURL=' + 214 | ('sourceURL' in options 215 | ? options.sourceURL 216 | : ('lodash.templateSources[' + (++templateCounter) + ']') 217 | ) + '\n'; 218 | ``` 219 | 220 | `sourceURL` 是主要是用于 `debugger`。 221 | 222 | ```js 223 | string.replace(reDelimiters, function (match, escapeValue, interpolateValue, esTemplateValue, evaluateValue, offset) { 224 | interpolateValue || (interpolateValue = esTemplateValue); 225 | // Escape characters that can't be included in string literals. 226 | source += string.slice(index, offset).replace(reUnescapedString, escapeStringChar); 227 | 228 | // Replace delimiters with snippets. 229 | if (escapeValue) { 230 | isEscaping = true; 231 | source += "' +\n__e(" + escapeValue + ") +\n'"; 232 | } 233 | if (evaluateValue) { 234 | isEvaluating = true; 235 | source += "';\n" + evaluateValue + ";\n__p += '"; 236 | } 237 | if (interpolateValue) { 238 | source += "' +\n((__t = (" + interpolateValue + ")) == null ? '' : __t) +\n'"; 239 | } 240 | index = offset + match.length; 241 | // The JS engine embedded in Adobe products needs `match` returned in 242 | // order to produce the correct `offset` value. 243 | return match; 244 | }); 245 | ``` 246 | 247 | 调用 `replace` 函数替换 `reDelimiters` 正则匹配到的字符串,并传入回调函数。 248 | 249 | 在回调函数中,会根据 `reDelimiters` 中的各种正则,最后将匹配的 `match` 返回。 250 | 251 | ```js 252 | // If `variable` is not specified wrap a with-statement around the generated 253 | // code to add the data object to the top of the scope chain. 254 | var variable = options.variable; 255 | if (!variable) { 256 | source = 'with (obj) {\n' + source + '\n}\n'; 257 | } 258 | ``` 259 | 如果没有 `variable`,采用 `with` 语句包裹 `source`。 260 | 261 | ```js 262 | // Cleanup code by stripping empty strings. 263 | source = (isEvaluating ? source.replace(reEmptyStringLeading, '') : source) 264 | .replace(reEmptyStringMiddle, '$1') 265 | .replace(reEmptyStringTrailing, '$1;'); 266 | ``` 267 | 268 | 如果传入了 `evaluateValue`,`isEvaluating` 就为 `true`,这里会使用 `replace` 方法,将 `reEmptyStringLeading` 匹配到的字符串替换为空字符串。 269 | 270 | ```js 271 | /** Used to match empty string literals in compiled template source. */ 272 | var reEmptyStringLeading = /\b__p \+= '';/g, 273 | reEmptyStringMiddle = /\b(__p \+=) '' \+/g, 274 | reEmptyStringTrailing = /(__e\(.*?\)|\b__t\)) \+\n'';/g; 275 | ``` 276 | 277 | `reEmptyStringLeading` 用来匹配 `__p += '';` 字符串。 278 | 279 | 连缀 2 个 `replace`,`$1` 表示将 `source` 替换成匹配到的第一个括号的内容。 280 | 281 | ```js 282 | // Frame code as the function body. 283 | source = 'function(' + (variable || 'obj') + ') {\n' + 284 | (variable 285 | ? '' 286 | : 'obj || (obj = {});\n' 287 | ) + 288 | "var __t, __p = ''" + 289 | (isEscaping 290 | ? ', __e = _.escape' 291 | : '' 292 | ) + 293 | (isEvaluating 294 | ? ', __j = Array.prototype.join;\n' + 295 | "function print() { __p += __j.call(arguments, '') }\n" 296 | : ';\n' 297 | ) + 298 | source + 299 | 'return __p\n}'; 300 | ``` 301 | 302 | 将 `source` 拼接成字符串 `function`, 303 | 304 | ```js 305 | var result = attempt(function () { 306 | return Function(importsKeys, sourceURL + 'return ' + source) 307 | .apply(undefined, importsValues); 308 | }); 309 | ``` 310 | 311 | 申明 `result` 函数,调用 `attempt` 函数,并且传入回调函数,`result` 就是返回的生成模板的函数。 312 | 313 | ```js 314 | // Provide the compiled function's source by its `toString` method or 315 | // the `source` property as a convenience for inlining compiled templates. 316 | result.source = source; 317 | if (isError(result)) { 318 | throw result; 319 | } 320 | return result; 321 | ``` 322 | 323 | 为 `result` 添加 `source` 字符串,并且判断 `result` 是否是 `error`,最后将 `result` 返回。 -------------------------------------------------------------------------------- /content/源码注释/vue-router/components/link.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { createRoute, isSameRoute, isIncludedRoute } from '../util/route' 4 | import { extend } from '../util/misc' 5 | 6 | // work around weird flow bug 7 | const toTypes: Array = [String, Object] 8 | const eventTypes: Array = [String, Array] 9 | 10 | export default { 11 | name: 'RouterLink', 12 | props: { 13 | to: { 14 | type: toTypes, 15 | required: true 16 | }, 17 | tag: { 18 | type: String, 19 | default: 'a' 20 | }, 21 | exact: Boolean, 22 | append: Boolean, 23 | replace: Boolean, 24 | activeClass: String, 25 | exactActiveClass: String, 26 | event: { 27 | type: eventTypes, 28 | default: 'click' 29 | } 30 | }, 31 | render(h: Function) { 32 | const router = this.$router 33 | const current = this.$route 34 | // 解析跳转目标路径 35 | const { location, route, href } = router.resolve(this.to, current, this.append) 36 | 37 | const classes = {} 38 | const globalActiveClass = router.options.linkActiveClass 39 | const globalExactActiveClass = router.options.linkExactActiveClass 40 | // Support global empty active class 41 | // 添加一些 class 42 | const activeClassFallback = globalActiveClass == null 43 | ? 'router-link-active' 44 | : globalActiveClass 45 | const exactActiveClassFallback = globalExactActiveClass == null 46 | ? 'router-link-exact-active' 47 | : globalExactActiveClass 48 | const activeClass = this.activeClass == null 49 | ? activeClassFallback 50 | : this.activeClass 51 | const exactActiveClass = this.exactActiveClass == null 52 | ? exactActiveClassFallback 53 | : this.exactActiveClass 54 | const compareTarget = location.path 55 | ? createRoute(null, location, null, router) 56 | : route 57 | 58 | classes[exactActiveClass] = isSameRoute(current, compareTarget) 59 | classes[activeClass] = this.exact 60 | ? classes[exactActiveClass] 61 | : isIncludedRoute(current, compareTarget) 62 | 63 | const handler = e => { 64 | if (guardEvent(e)) { 65 | // 根据 this.props.append 66 | // 判断是 replace 还是 push 67 | if (this.replace) { 68 | router.replace(location) 69 | } else { 70 | router.push(location) 71 | } 72 | } 73 | } 74 | 75 | const on = { click: guardEvent } 76 | // 可配置的监听 event 事件 可以是数组 77 | // 循环添加事件监听 78 | if (Array.isArray(this.event)) { 79 | this.event.forEach(e => { on[e] = handler }) 80 | } else { 81 | on[this.event] = handler 82 | } 83 | 84 | const data: any = { 85 | class: classes 86 | } 87 | 88 | // 判断当前元素是否是 a 标签 89 | // 是则给自己添加 href 点击事件 90 | if (this.tag === 'a') { 91 | data.on = on 92 | data.attrs = { href } 93 | } else { 94 | // 不是则递归向下寻找,找到就添加 href、监听事件 95 | // find the first child and apply listener and href 96 | const a = findAnchor(this.$slots.default) 97 | if (a) { 98 | // in case the is a static node 99 | a.isStatic = false 100 | const aData = a.data = extend({}, a.data) 101 | aData.on = on 102 | const aAttrs = a.data.attrs = extend({}, a.data.attrs) 103 | aAttrs.href = href 104 | } else { 105 | // 找不到就给自己添加 href、监听事件 106 | // doesn't have child, apply listener to self 107 | data.on = on 108 | } 109 | } 110 | 111 | // 使用 createElement 创建 Vnode 112 | // 并且将 slot 传入 113 | return h(this.tag, data, this.$slots.default) 114 | } 115 | } 116 | 117 | function guardEvent(e) { 118 | // don't redirect with control keys 119 | if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return 120 | // don't redirect when preventDefault called 121 | if (e.defaultPrevented) return 122 | // don't redirect on right click 123 | if (e.button !== undefined && e.button !== 0) return 124 | // don't redirect if `target="_blank"` 125 | if (e.currentTarget && e.currentTarget.getAttribute) { 126 | const target = e.currentTarget.getAttribute('target') 127 | if (/\b_blank\b/i.test(target)) return 128 | } 129 | // this may be a Weex event which doesn't have this method 130 | if (e.preventDefault) { 131 | e.preventDefault() 132 | } 133 | return true 134 | } 135 | 136 | // 递归找到锚点并返回 137 | function findAnchor(children) { 138 | if (children) { 139 | let child 140 | for (let i = 0; i < children.length; i++) { 141 | child = children[i] 142 | if (child.tag === 'a') { 143 | return child 144 | } 145 | if (child.children && (child = findAnchor(child.children))) { 146 | return child 147 | } 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /content/源码注释/vue-router/components/view.js: -------------------------------------------------------------------------------- 1 | import { warn } from '../util/warn' 2 | import { extend } from '../util/misc' 3 | 4 | export default { 5 | name: 'RouterView', 6 | functional: true, 7 | props: { 8 | name: { 9 | type: String, 10 | default: 'default' 11 | } 12 | }, 13 | render (_, { props, children, parent, data }) { 14 | // used by devtools to display a router-view badge 15 | data.routerView = true 16 | 17 | // directly use parent context's createElement() function 18 | // so that components rendered by router-view can resolve named slots 19 | const h = parent.$createElement 20 | const name = props.name 21 | const route = parent.$route 22 | const cache = parent._routerViewCache || (parent._routerViewCache = {}) 23 | 24 | // determine current view depth, also check to see if the tree 25 | // has been toggled inactive but kept-alive. 26 | // 深度记录 27 | let depth = 0 28 | let inactive = false 29 | while (parent && parent._routerRoot !== parent) { 30 | if (parent.$vnode && parent.$vnode.data.routerView) { 31 | depth++ 32 | } 33 | if (parent._inactive) { 34 | inactive = true 35 | } 36 | parent = parent.$parent 37 | } 38 | data.routerViewDepth = depth 39 | 40 | // render previous view if the tree is inactive and kept-alive 41 | if (inactive) { 42 | return h(cache[name], data, children) 43 | } 44 | 45 | // matched 是一个扁平数组 储存从浅到深路由记录 46 | // 这里用深度取出对应的 matched 路由记录 47 | const matched = route.matched[depth] 48 | // render empty node if no matched route 49 | if (!matched) { 50 | cache[name] = null 51 | return h() 52 | } 53 | 54 | // 命名视图 55 | // 根据 name 拿到 component 并存缓存到之前申明的 cache 中 56 | const component = cache[name] = matched.components[name] 57 | 58 | // attach instance registration hook 59 | // this will be called in the instance's injected lifecycle hooks 60 | // 将传入的 val 以 name 为 key 保存到 matched.instances 61 | // 此时 val 为 vue 实例 62 | // 调用此方法有两种情况 beforeCreate(vm,vm) destroyed(vm) 63 | data.registerRouteInstance = (vm, val) => { 64 | // val could be undefined for unregistration 65 | const current = matched.instances[name] 66 | if ( 67 | (val && current !== vm) || 68 | (!val && current === vm) 69 | ) { 70 | matched.instances[name] = val 71 | } 72 | } 73 | 74 | // 增加 vnode 的预补丁生命周期钩子 75 | // also register instance in prepatch hook 76 | // in case the same component instance is reused across different routes 77 | ; (data.hook || (data.hook = {})).prepatch = (_, vnode) => { 78 | matched.instances[name] = vnode.componentInstance 79 | } 80 | 81 | // resolve props 82 | // 处理有 routes 单个 props 的情况 83 | let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name]) 84 | if (propsToPass) { 85 | // clone to prevent mutation 86 | propsToPass = data.props = extend({}, propsToPass) 87 | // pass non-declared props as attrs 88 | const attrs = data.attrs = data.attrs || {} 89 | for (const key in propsToPass) { 90 | if (!component.props || !(key in component.props)) { 91 | attrs[key] = propsToPass[key] 92 | delete propsToPass[key] 93 | } 94 | } 95 | } 96 | 97 | return h(component, data, children) 98 | } 99 | } 100 | 101 | // 处理不同 route 格式 102 | function resolveProps (route, config) { 103 | switch (typeof config) { 104 | case 'undefined': 105 | return 106 | case 'object': 107 | return config 108 | case 'function': 109 | return config(route) 110 | case 'boolean': 111 | // 处理 props: true return params 112 | return config ? route.params : undefined 113 | default: 114 | if (process.env.NODE_ENV !== 'production') { 115 | warn( 116 | false, 117 | `props in "${route.path}" is a ${typeof config}, ` + 118 | `expecting an object, function or boolean.` 119 | ) 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /content/源码注释/vue-router/create-matcher.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type VueRouter from './index' 4 | import { resolvePath } from './util/path' 5 | import { assert, warn } from './util/warn' 6 | import { createRoute } from './util/route' 7 | import { fillParams } from './util/params' 8 | import { createRouteMap } from './create-route-map' 9 | import { normalizeLocation } from './util/location' 10 | 11 | export type Matcher = { 12 | match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route; 13 | addRoutes: (routes: Array) => void; 14 | }; 15 | 16 | export function createMatcher ( 17 | routes: Array, 18 | router: VueRouter 19 | ): Matcher { 20 | // 使用 createRouteMap 创建了 路径列表 路由路径表 和 路由名称表 21 | const { pathList, pathMap, nameMap } = createRouteMap(routes) 22 | 23 | // 暴露给 Router实例 用来动态添加更多的路由规则 24 | function addRoutes (routes) { 25 | createRouteMap(routes, pathList, pathMap, nameMap) 26 | } 27 | 28 | function match ( 29 | raw: RawLocation, 30 | currentRoute?: Route, 31 | redirectedFrom?: Location 32 | ): Route { 33 | /** 34 | * normalizeLocation 35 | * return { 36 | * _normalized: true, 37 | * path || name, 38 | * query, 39 | * hash 40 | * } 41 | */ 42 | const location = normalizeLocation(raw, currentRoute, false, router) 43 | const { name } = location 44 | 45 | // 路由名称表增加记录 46 | if (name) { 47 | const record = nameMap[name] 48 | if (process.env.NODE_ENV !== 'production') { 49 | warn(record, `Route with name '${name}' does not exist`) 50 | } 51 | if (!record) return _createRoute(null, location) 52 | const paramNames = record.regex.keys 53 | .filter(key => !key.optional) 54 | .map(key => key.name) 55 | 56 | if (typeof location.params !== 'object') { 57 | location.params = {} 58 | } 59 | 60 | if (currentRoute && typeof currentRoute.params === 'object') { 61 | for (const key in currentRoute.params) { 62 | if (!(key in location.params) && paramNames.indexOf(key) > -1) { 63 | location.params[key] = currentRoute.params[key] 64 | } 65 | } 66 | } 67 | 68 | if (record) { 69 | location.path = fillParams(record.path, location.params, `named route "${name}"`) 70 | return _createRoute(record, location, redirectedFrom) 71 | } 72 | } else if (location.path) { 73 | location.params = {} 74 | for (let i = 0; i < pathList.length; i++) { 75 | const path = pathList[i] 76 | const record = pathMap[path] 77 | // 创建路由 78 | if (matchRoute(record.regex, location.path, location.params)) { 79 | return _createRoute(record, location, redirectedFrom) 80 | } 81 | } 82 | } 83 | // no match 84 | return _createRoute(null, location) 85 | } 86 | 87 | function redirect ( 88 | record: RouteRecord, 89 | location: Location 90 | ): Route { 91 | const originalRedirect = record.redirect 92 | let redirect = typeof originalRedirect === 'function' 93 | ? originalRedirect(createRoute(record, location, null, router)) 94 | : originalRedirect 95 | 96 | if (typeof redirect === 'string') { 97 | redirect = { path: redirect } 98 | } 99 | 100 | if (!redirect || typeof redirect !== 'object') { 101 | if (process.env.NODE_ENV !== 'production') { 102 | warn( 103 | false, `invalid redirect option: ${JSON.stringify(redirect)}` 104 | ) 105 | } 106 | return _createRoute(null, location) 107 | } 108 | 109 | const re: Object = redirect 110 | const { name, path } = re 111 | let { query, hash, params } = location 112 | query = re.hasOwnProperty('query') ? re.query : query 113 | hash = re.hasOwnProperty('hash') ? re.hash : hash 114 | params = re.hasOwnProperty('params') ? re.params : params 115 | 116 | if (name) { 117 | // resolved named direct 118 | const targetRecord = nameMap[name] 119 | if (process.env.NODE_ENV !== 'production') { 120 | assert(targetRecord, `redirect failed: named route "${name}" not found.`) 121 | } 122 | return match({ 123 | _normalized: true, 124 | name, 125 | query, 126 | hash, 127 | params 128 | }, undefined, location) 129 | } else if (path) { 130 | // 1. resolve relative redirect 131 | const rawPath = resolveRecordPath(path, record) 132 | // 2. resolve params 133 | const resolvedPath = fillParams(rawPath, params, `redirect route with path "${rawPath}"`) 134 | // 3. rematch with existing query and hash 135 | return match({ 136 | _normalized: true, 137 | path: resolvedPath, 138 | query, 139 | hash 140 | }, undefined, location) 141 | } else { 142 | if (process.env.NODE_ENV !== 'production') { 143 | warn(false, `invalid redirect option: ${JSON.stringify(redirect)}`) 144 | } 145 | return _createRoute(null, location) 146 | } 147 | } 148 | 149 | function alias ( 150 | record: RouteRecord, 151 | location: Location, 152 | matchAs: string 153 | ): Route { 154 | const aliasedPath = fillParams(matchAs, location.params, `aliased route with path "${matchAs}"`) 155 | const aliasedMatch = match({ 156 | _normalized: true, 157 | path: aliasedPath 158 | }) 159 | if (aliasedMatch) { 160 | const matched = aliasedMatch.matched 161 | const aliasedRecord = matched[matched.length - 1] 162 | location.params = aliasedMatch.params 163 | return _createRoute(aliasedRecord, location) 164 | } 165 | return _createRoute(null, location) 166 | } 167 | 168 | function _createRoute ( 169 | record: ?RouteRecord, 170 | location: Location, 171 | redirectedFrom?: Location 172 | ): Route { 173 | // 不同 类型 router 处理 174 | if (record && record.redirect) { 175 | return redirect(record, redirectedFrom || location) 176 | } 177 | if (record && record.matchAs) { 178 | return alias(record, location, record.matchAs) 179 | } 180 | return createRoute(record, location, redirectedFrom, router) 181 | } 182 | 183 | return { 184 | match, 185 | addRoutes 186 | } 187 | } 188 | 189 | function matchRoute ( 190 | regex: RouteRegExp, 191 | path: string, 192 | params: Object 193 | ): boolean { 194 | const m = path.match(regex) 195 | 196 | if (!m) { 197 | return false 198 | } else if (!params) { 199 | return true 200 | } 201 | 202 | for (let i = 1, len = m.length; i < len; ++i) { 203 | const key = regex.keys[i - 1] 204 | const val = typeof m[i] === 'string' ? decodeURIComponent(m[i]) : m[i] 205 | if (key) { 206 | // Fix #1994: using * with props: true generates a param named 0 207 | params[key.name || 'pathMatch'] = val 208 | } 209 | } 210 | 211 | return true 212 | } 213 | 214 | function resolveRecordPath (path: string, record: RouteRecord): string { 215 | return resolvePath(path, record.parent ? record.parent.path : '/', true) 216 | } 217 | -------------------------------------------------------------------------------- /content/源码注释/vue-router/create-route-map.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import Regexp from 'path-to-regexp' 4 | import { cleanPath } from './util/path' 5 | import { assert, warn } from './util/warn' 6 | 7 | export function createRouteMap ( 8 | routes: Array, 9 | oldPathList?: Array, 10 | oldPathMap?: Dictionary, 11 | oldNameMap?: Dictionary 12 | ): { 13 | pathList: Array; 14 | pathMap: Dictionary; 15 | nameMap: Dictionary; 16 | } { 17 | // the path list is used to control path matching priority 18 | const pathList: Array = oldPathList || [] 19 | // $flow-disable-line 20 | const pathMap: Dictionary = oldPathMap || Object.create(null) 21 | // $flow-disable-line 22 | const nameMap: Dictionary = oldNameMap || Object.create(null) 23 | 24 | // 循环遍历 routes 25 | // 处理 pathList pathMap nameMap 26 | routes.forEach(route => { 27 | addRouteRecord(pathList, pathMap, nameMap, route) 28 | }) 29 | 30 | // ensure wildcard routes are always at the end 31 | for (let i = 0, l = pathList.length; i < l; i++) { 32 | if (pathList[i] === '*') { 33 | pathList.push(pathList.splice(i, 1)[0]) 34 | l-- 35 | i-- 36 | } 37 | } 38 | 39 | return { 40 | pathList, 41 | pathMap, 42 | nameMap 43 | } 44 | } 45 | 46 | function addRouteRecord ( 47 | pathList: Array, 48 | pathMap: Dictionary, 49 | nameMap: Dictionary, 50 | route: RouteConfig, 51 | parent?: RouteRecord, 52 | matchAs?: string 53 | ) { 54 | const { path, name } = route 55 | if (process.env.NODE_ENV !== 'production') { 56 | // 断言 必须有 path 不然报错 57 | assert(path != null, `"path" is required in a route configuration.`) 58 | assert( 59 | typeof route.component !== 'string', 60 | `route config "component" for path: ${String(path || name)} cannot be a ` + 61 | `string id. Use an actual component instead.` 62 | ) 63 | } 64 | 65 | // 编译正则的选项 66 | const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {} 67 | const normalizedPath = normalizePath( 68 | path, 69 | parent, 70 | pathToRegexpOptions.strict 71 | ) 72 | 73 | if (typeof route.caseSensitive === 'boolean') { 74 | pathToRegexpOptions.sensitive = route.caseSensitive 75 | } 76 | 77 | // 路由记录模板 78 | const record: RouteRecord = { 79 | path: normalizedPath, 80 | regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), 81 | components: route.components || { default: route.component }, 82 | instances: {}, 83 | name, 84 | parent, 85 | matchAs, 86 | redirect: route.redirect, 87 | beforeEnter: route.beforeEnter, 88 | meta: route.meta || {}, 89 | props: route.props == null 90 | ? {} 91 | : route.components 92 | ? route.props 93 | : { default: route.props } 94 | } 95 | 96 | // 判断是否是嵌套路由 97 | if (route.children) { 98 | // Warn if route is named, does not redirect and has a default child route. 99 | // If users navigate to this route by name, the default child will 100 | // not be rendered (GH Issue #629) 101 | if (process.env.NODE_ENV !== 'production') { 102 | if (route.name && !route.redirect && route.children.some(child => /^\/?$/.test(child.path))) { 103 | warn( 104 | false, 105 | `Named Route '${route.name}' has a default child route. ` + 106 | `When navigating to this named route (:to="{name: '${route.name}'"), ` + 107 | `the default child route will not be rendered. Remove the name from ` + 108 | `this route and use the name of the default child route for named ` + 109 | `links instead.` 110 | ) 111 | } 112 | } 113 | // 嵌套路由递归调用 114 | route.children.forEach(child => { 115 | const childMatchAs = matchAs 116 | ? cleanPath(`${matchAs}/${child.path}`) 117 | : undefined 118 | addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs) 119 | }) 120 | } 121 | 122 | // 别名处理 123 | if (route.alias !== undefined) { 124 | const aliases = Array.isArray(route.alias) 125 | ? route.alias 126 | : [route.alias] 127 | 128 | aliases.forEach(alias => { 129 | const aliasRoute = { 130 | path: alias, 131 | children: route.children 132 | } 133 | addRouteRecord( 134 | pathList, 135 | pathMap, 136 | nameMap, 137 | aliasRoute, 138 | parent, 139 | record.path || '/' // matchAs 140 | ) 141 | }) 142 | } 143 | 144 | // 以 key: value 储存 防止 path 重复 145 | if (!pathMap[record.path]) { 146 | pathList.push(record.path) 147 | pathMap[record.path] = record 148 | } 149 | 150 | // 以 key: value 储存 防止 name 重复 151 | if (name) { 152 | if (!nameMap[name]) { 153 | nameMap[name] = record 154 | } else if (process.env.NODE_ENV !== 'production' && !matchAs) { 155 | warn( 156 | false, 157 | `Duplicate named routes definition: ` + 158 | `{ name: "${name}", path: "${record.path}" }` 159 | ) 160 | } 161 | } 162 | } 163 | 164 | // 将路径转化为正则 165 | function compileRouteRegex (path: string, pathToRegexpOptions: PathToRegexpOptions): RouteRegExp { 166 | const regex = Regexp(path, [], pathToRegexpOptions) 167 | if (process.env.NODE_ENV !== 'production') { 168 | const keys: any = Object.create(null) 169 | regex.keys.forEach(key => { 170 | warn(!keys[key.name], `Duplicate param keys in route with path: "${path}"`) 171 | keys[key.name] = true 172 | }) 173 | } 174 | return regex 175 | } 176 | 177 | // 处理 path 178 | function normalizePath (path: string, parent?: RouteRecord, strict?: boolean): string { 179 | if (!strict) path = path.replace(/\/$/, '') 180 | if (path[0] === '/') return path 181 | if (parent == null) return path 182 | // 将 '//' 替换成 '/' 183 | return cleanPath(`${parent.path}/${path}`) 184 | } 185 | -------------------------------------------------------------------------------- /content/源码注释/vue-router/history/abstract.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type Router from '../index' 4 | import { History } from './base' 5 | 6 | export class AbstractHistory extends History { 7 | index: number; 8 | stack: Array; 9 | 10 | constructor (router: Router, base: ?string) { 11 | super(router, base) 12 | // 用数组模拟浏览器记录 13 | // 基本都是对数组的操作 14 | this.stack = [] 15 | this.index = -1 16 | } 17 | 18 | push (location: RawLocation, onComplete?: Function, onAbort?: Function) { 19 | this.transitionTo(location, route => { 20 | this.stack = this.stack.slice(0, this.index + 1).concat(route) 21 | this.index++ 22 | onComplete && onComplete(route) 23 | }, onAbort) 24 | } 25 | 26 | replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { 27 | this.transitionTo(location, route => { 28 | this.stack = this.stack.slice(0, this.index).concat(route) 29 | onComplete && onComplete(route) 30 | }, onAbort) 31 | } 32 | 33 | go (n: number) { 34 | const targetIndex = this.index + n 35 | if (targetIndex < 0 || targetIndex >= this.stack.length) { 36 | return 37 | } 38 | const route = this.stack[targetIndex] 39 | this.confirmTransition(route, () => { 40 | this.index = targetIndex 41 | this.updateRoute(route) 42 | }) 43 | } 44 | 45 | getCurrentLocation () { 46 | const current = this.stack[this.stack.length - 1] 47 | return current ? current.fullPath : '/' 48 | } 49 | 50 | ensureURL () { 51 | // noop 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /content/源码注释/vue-router/history/base.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { _Vue } from '../install' 4 | import type Router from '../index' 5 | import { inBrowser } from '../util/dom' 6 | import { runQueue } from '../util/async' 7 | import { warn, isError } from '../util/warn' 8 | import { START, isSameRoute } from '../util/route' 9 | import { 10 | flatten, 11 | flatMapComponents, 12 | resolveAsyncComponents 13 | } from '../util/resolve-components' 14 | 15 | export class History { 16 | router: Router; 17 | base: string; 18 | current: Route; 19 | pending: ?Route; 20 | cb: (r: Route) => void; 21 | ready: boolean; 22 | readyCbs: Array; 23 | readyErrorCbs: Array; 24 | errorCbs: Array; 25 | 26 | // implemented by sub-classes 27 | +go: (n: number) => void; 28 | +push: (loc: RawLocation) => void; 29 | +replace: (loc: RawLocation) => void; 30 | +ensureURL: (push?: boolean) => void; 31 | +getCurrentLocation: () => string; 32 | 33 | constructor (router: Router, base: ?string) { 34 | this.router = router 35 | this.base = normalizeBase(base) 36 | // start with a route object that stands for "nowhere" 37 | this.current = START 38 | this.pending = null 39 | this.ready = false 40 | this.readyCbs = [] 41 | this.readyErrorCbs = [] 42 | this.errorCbs = [] 43 | } 44 | 45 | listen (cb: Function) { 46 | this.cb = cb 47 | } 48 | 49 | onReady (cb: Function, errorCb: ?Function) { 50 | if (this.ready) { 51 | cb() 52 | } else { 53 | this.readyCbs.push(cb) 54 | if (errorCb) { 55 | this.readyErrorCbs.push(errorCb) 56 | } 57 | } 58 | } 59 | 60 | onError (errorCb: Function) { 61 | this.errorCbs.push(errorCb) 62 | } 63 | 64 | // 跳转核心处理 65 | transitionTo (location: RawLocation, onComplete ?: Function, onAbort ?: Function) { 66 | // 得到 match 处理后的 route 67 | const route = this.router.match(location, this.current) 68 | this.confirmTransition(route, () => { 69 | // 更新当前 route 70 | this.updateRoute(route) 71 | // 路由更新后的回调 72 | // 就是 HashHistory类的 setupListeners 73 | // 做了滚动处理 hash 事件监听 74 | onComplete && onComplete(route) 75 | // 更新 url (在子类申明的方法) 76 | this.ensureURL() 77 | 78 | // fire ready cbs once 79 | if (!this.ready) { 80 | this.ready = true 81 | this.readyCbs.forEach(cb => { cb(route) }) 82 | } 83 | }, err => { 84 | if (onAbort) { 85 | onAbort(err) 86 | } 87 | if (err && !this.ready) { 88 | this.ready = true 89 | this.readyErrorCbs.forEach(cb => { cb(err) }) 90 | } 91 | }) 92 | } 93 | 94 | confirmTransition (route: Route, onComplete: Function, onAbort ?: Function) { 95 | const current = this.current 96 | const abort = err => { 97 | if (isError(err)) { 98 | if (this.errorCbs.length) { 99 | this.errorCbs.forEach(cb => { cb(err) }) 100 | } else { 101 | warn(false, 'uncaught error during route navigation:') 102 | console.error(err) 103 | } 104 | } 105 | onAbort && onAbort(err) 106 | } 107 | // 路由相同则返回 108 | if ( 109 | isSameRoute(route, current) && 110 | // in the case the route map has been dynamically appended to 111 | route.matched.length === current.matched.length 112 | ) { 113 | this.ensureURL() 114 | return abort() 115 | } 116 | 117 | // 将当前的 matched 与 跳转的 matched 比较 118 | // matched 是在 createRoute 中增加 119 | // 用来数组记录当前 route 以及它的上级 route 120 | // 用 resolveQueue 来做做新旧对比 比较后返回需要更新、激活、卸载3种路由状态的数组 121 | const { 122 | updated, 123 | deactivated, 124 | activated 125 | } = resolveQueue(this.current.matched, route.matched) 126 | 127 | // 提取守卫的钩子函数 将任务队列合并 128 | const queue: Array = [].concat( 129 | // in-component leave guards 130 | // 获得组件内的 beforeRouteLeave 钩子函数 131 | extractLeaveGuards(deactivated), 132 | // global before hooks 133 | this.router.beforeHooks, 134 | // in-component update hooks 135 | // 获得组件内的 beforeRouteUpdate 钩子函数 136 | extractUpdateHooks(updated), 137 | // in-config enter guards 138 | activated.map(m => m.beforeEnter), 139 | // async components 140 | // 异步组件处理 141 | resolveAsyncComponents(activated) 142 | ) 143 | 144 | this.pending = route 145 | 146 | const iterator = (hook: NavigationGuard, next) => { 147 | if (this.pending !== route) { 148 | return abort() 149 | } 150 | try { 151 | hook(route, current, (to: any) => { 152 | if (to === false || isError(to)) { 153 | // next(false) -> abort navigation, ensure current URL 154 | this.ensureURL(true) 155 | abort(to) 156 | } else if ( 157 | typeof to === 'string' || 158 | (typeof to === 'object' && ( 159 | typeof to.path === 'string' || 160 | typeof to.name === 'string' 161 | )) 162 | ) { 163 | // next('/') or next({ path: '/' }) -> redirect 164 | // 重定向操作 165 | abort() 166 | if (typeof to === 'object' && to.replace) { 167 | this.replace(to) 168 | } else { 169 | this.push(to) 170 | } 171 | } else { 172 | // confirm transition and pass on the value 173 | next(to) 174 | } 175 | }) 176 | } catch (e) { 177 | abort(e) 178 | } 179 | } 180 | 181 | // 执行合并后的任务队列 182 | runQueue(queue, iterator, () => { 183 | const postEnterCbs = [] 184 | const isValid = () => this.current === route 185 | // wait until async components are resolved before 186 | // extracting in-component enter guards 187 | // 拿到组件 beforeRouteEnter 钩子函数 合并任务队列 188 | const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid) 189 | const queue = enterGuards.concat(this.router.resolveHooks) 190 | runQueue(queue, iterator, () => { 191 | if (this.pending !== route) { 192 | return abort() 193 | } 194 | this.pending = null 195 | onComplete(route) 196 | if (this.router.app) { 197 | this.router.app.$nextTick(() => { 198 | postEnterCbs.forEach(cb => { cb() }) 199 | }) 200 | } 201 | }) 202 | }) 203 | } 204 | 205 | // 更新当前 route 并且执行 afterHooks 回调 206 | updateRoute (route: Route) { 207 | const prev = this.current 208 | this.current = route 209 | // 这一步更新了路由信息 触发了 vue 的 set 210 | this.cb && this.cb(route) 211 | this.router.afterHooks.forEach(hook => { 212 | hook && hook(route, prev) 213 | }) 214 | } 215 | } 216 | 217 | function normalizeBase (base: ?string): string { 218 | if (!base) { 219 | if (inBrowser) { 220 | // respect tag 221 | const baseEl = document.querySelector('base') 222 | base = (baseEl && baseEl.getAttribute('href')) || '/' 223 | // strip full URL origin 224 | base = base.replace(/^https?:\/\/[^\/]+/, '') 225 | } else { 226 | base = '/' 227 | } 228 | } 229 | // make sure there's the starting slash 230 | if (base.charAt(0) !== '/') { 231 | base = '/' + base 232 | } 233 | // remove trailing slash 234 | return base.replace(/\/$/, '') 235 | } 236 | 237 | function resolveQueue ( 238 | current: Array, 239 | next: Array 240 | ): { 241 | updated: Array, 242 | activated: Array, 243 | deactivated: Array 244 | } { 245 | let i 246 | const max = Math.max(current.length, next.length) 247 | for (i = 0; i < max; i++) { 248 | if (current[i] !== next[i]) { 249 | break 250 | } 251 | } 252 | return { 253 | updated: next.slice(0, i), 254 | activated: next.slice(i), 255 | deactivated: current.slice(i) 256 | } 257 | } 258 | 259 | function extractGuards ( 260 | records: Array, 261 | name: string, 262 | bind: Function, 263 | reverse?: boolean 264 | ): Array { 265 | const guards = flatMapComponents(records, (def, instance, match, key) => { 266 | const guard = extractGuard(def, name) 267 | if (guard) { 268 | return Array.isArray(guard) 269 | ? guard.map(guard => bind(guard, instance, match, key)) 270 | : bind(guard, instance, match, key) 271 | } 272 | }) 273 | return flatten(reverse ? guards.reverse() : guards) 274 | } 275 | 276 | function extractGuard ( 277 | def: Object | Function, 278 | key: string 279 | ): NavigationGuard | Array { 280 | if (typeof def !== 'function') { 281 | // extend now so that global mixins are applied. 282 | def = _Vue.extend(def) 283 | } 284 | return def.options[key] 285 | } 286 | 287 | function extractLeaveGuards (deactivated: Array): Array { 288 | return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true) 289 | } 290 | 291 | function extractUpdateHooks (updated: Array): Array { 292 | return extractGuards(updated, 'beforeRouteUpdate', bindGuard) 293 | } 294 | 295 | function bindGuard (guard: NavigationGuard, instance: ?_Vue): ?NavigationGuard { 296 | if (instance) { 297 | return function boundRouteGuard () { 298 | return guard.apply(instance, arguments) 299 | } 300 | } 301 | } 302 | 303 | function extractEnterGuards ( 304 | activated: Array, 305 | cbs: Array, 306 | isValid: () => boolean 307 | ): Array { 308 | return extractGuards(activated, 'beforeRouteEnter', (guard, _, match, key) => { 309 | return bindEnterGuard(guard, match, key, cbs, isValid) 310 | }) 311 | } 312 | 313 | function bindEnterGuard ( 314 | guard: NavigationGuard, 315 | match: RouteRecord, 316 | key: string, 317 | cbs: Array, 318 | isValid: () => boolean 319 | ): NavigationGuard { 320 | return function routeEnterGuard (to, from, next) { 321 | return guard(to, from, cb => { 322 | next(cb) 323 | if (typeof cb === 'function') { 324 | cbs.push(() => { 325 | // #750 326 | // if a router-view is wrapped with an out-in transition, 327 | // the instance may not have been registered at this time. 328 | // we will need to poll for registration until current route 329 | // is no longer valid. 330 | poll(cb, match.instances, key, isValid) 331 | }) 332 | } 333 | }) 334 | } 335 | } 336 | 337 | function poll ( 338 | cb: any, // somehow flow cannot infer this is a function 339 | instances: Object, 340 | key: string, 341 | isValid: () => boolean 342 | ) { 343 | if ( 344 | instances[key] && 345 | !instances[key]._isBeingDestroyed // do not reuse being destroyed instance 346 | ) { 347 | cb(instances[key]) 348 | } else if (isValid()) { 349 | setTimeout(() => { 350 | poll(cb, instances, key, isValid) 351 | }, 16) 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /content/源码注释/vue-router/history/hash.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type Router from '../index' 4 | import { History } from './base' 5 | import { cleanPath } from '../util/path' 6 | import { getLocation } from './html5' 7 | import { setupScroll, handleScroll } from '../util/scroll' 8 | import { pushState, replaceState, supportsPushState } from '../util/push-state' 9 | 10 | export class HashHistory extends History { 11 | constructor (router: Router, base: ?string, fallback: boolean) { 12 | super(router, base) 13 | // check history fallback deeplinking 14 | // 检查 是否是强制使用 hash 模式 15 | if (fallback && checkFallback(this.base)) { 16 | return 17 | } 18 | // 保证 path 以 '/' 开头 19 | ensureSlash() 20 | } 21 | 22 | // this is delayed until the app mounts 23 | // to avoid the hashchange listener being fired too early 24 | setupListeners () { 25 | const router = this.router 26 | const expectScroll = router.options.scrollBehavior 27 | const supportsScroll = supportsPushState && expectScroll 28 | 29 | if (supportsScroll) { 30 | setupScroll() 31 | } 32 | 33 | window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => { 34 | const current = this.current 35 | if (!ensureSlash()) { 36 | return 37 | } 38 | this.transitionTo(getHash(), route => { 39 | if (supportsScroll) { 40 | handleScroll(this.router, route, current, true) 41 | } 42 | if (!supportsPushState) { 43 | replaceHash(route.fullPath) 44 | } 45 | }) 46 | }) 47 | } 48 | 49 | push (location: RawLocation, onComplete?: Function, onAbort?: Function) { 50 | const { current: fromRoute } = this 51 | this.transitionTo(location, route => { 52 | pushHash(route.fullPath) 53 | handleScroll(this.router, route, fromRoute, false) 54 | onComplete && onComplete(route) 55 | }, onAbort) 56 | } 57 | 58 | replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { 59 | const { current: fromRoute } = this 60 | this.transitionTo(location, route => { 61 | replaceHash(route.fullPath) 62 | handleScroll(this.router, route, fromRoute, false) 63 | onComplete && onComplete(route) 64 | }, onAbort) 65 | } 66 | 67 | go (n: number) { 68 | window.history.go(n) 69 | } 70 | 71 | ensureURL (push?: boolean) { 72 | const current = this.current.fullPath 73 | if (getHash() !== current) { 74 | push ? pushHash(current) : replaceHash(current) 75 | } 76 | } 77 | 78 | getCurrentLocation () { 79 | return getHash() 80 | } 81 | } 82 | 83 | function checkFallback (base) { 84 | const location = getLocation(base) 85 | // 处理不是 '/#' 开头的 86 | if (!/^\/#/.test(location)) { 87 | window.location.replace( 88 | cleanPath(base + '/#' + location) 89 | ) 90 | return true 91 | } 92 | } 93 | 94 | function ensureSlash (): boolean { 95 | const path = getHash() 96 | if (path.charAt(0) === '/') { 97 | return true 98 | } 99 | replaceHash('/' + path) 100 | return false 101 | } 102 | 103 | // 获取 hash 后的地址 104 | export function getHash (): string { 105 | // We can't use window.location.hash here because it's not 106 | // consistent across browsers - Firefox will pre-decode it! 107 | const href = window.location.href 108 | const index = href.indexOf('#') 109 | return index === -1 ? '' : href.slice(index + 1) 110 | } 111 | 112 | function getUrl (path) { 113 | const href = window.location.href 114 | const i = href.indexOf('#') 115 | const base = i >= 0 ? href.slice(0, i) : href 116 | return `${base}#${path}` 117 | } 118 | 119 | function pushHash (path) { 120 | if (supportsPushState) { 121 | pushState(getUrl(path)) 122 | } else { 123 | window.location.hash = path 124 | } 125 | } 126 | 127 | function replaceHash (path) { 128 | if (supportsPushState) { 129 | replaceState(getUrl(path)) 130 | } else { 131 | window.location.replace(getUrl(path)) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /content/源码注释/vue-router/history/html5.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type Router from '../index' 4 | import { History } from './base' 5 | import { cleanPath } from '../util/path' 6 | import { START } from '../util/route' 7 | import { setupScroll, handleScroll } from '../util/scroll' 8 | import { pushState, replaceState, supportsPushState } from '../util/push-state' 9 | 10 | export class HTML5History extends History { 11 | constructor (router: Router, base: ?string) { 12 | super(router, base) 13 | 14 | // 判断是否配置了 scrollBehavior 滚动行为 && 是否支持 History 模式 15 | const expectScroll = router.options.scrollBehavior 16 | const supportsScroll = supportsPushState && expectScroll 17 | 18 | // 支持则监听 popstate 事件 储存 pageOffset 坐标 19 | if (supportsScroll) { 20 | setupScroll() 21 | } 22 | 23 | // 处理 pathname 去除 base 24 | const initLocation = getLocation(this.base) 25 | // 监听浏览器的 popstate 跳转 26 | window.addEventListener('popstate', e => { 27 | const current = this.current 28 | 29 | // Avoiding first `popstate` event dispatched in some browsers but first 30 | // history route not updated since async guard at the same time. 31 | const location = getLocation(this.base) 32 | if (this.current === START && location === initLocation) { 33 | return 34 | } 35 | this.transitionTo(location, route => { 36 | if (supportsScroll) { 37 | handleScroll(router, route, current, true) 38 | } 39 | }) 40 | }) 41 | } 42 | 43 | go (n: number) { 44 | window.history.go(n) 45 | } 46 | 47 | // 不方便用 router-link 的时候 用这个作为跳转 48 | // 调用继承 History transitionTo 49 | // 跳转基本都调用 transitionTo 50 | push (location: RawLocation, onComplete?: Function, onAbort?: Function) { 51 | const { current: fromRoute } = this 52 | this.transitionTo(location, route => { 53 | pushState(cleanPath(this.base + route.fullPath)) 54 | handleScroll(this.router, route, fromRoute, false) 55 | onComplete && onComplete(route) 56 | }, onAbort) 57 | } 58 | 59 | replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { 60 | const { current: fromRoute } = this 61 | this.transitionTo(location, route => { 62 | replaceState(cleanPath(this.base + route.fullPath)) 63 | handleScroll(this.router, route, fromRoute, false) 64 | onComplete && onComplete(route) 65 | }, onAbort) 66 | } 67 | 68 | ensureURL (push?: boolean) { 69 | if (getLocation(this.base) !== this.current.fullPath) { 70 | const current = cleanPath(this.base + this.current.fullPath) 71 | push ? pushState(current) : replaceState(current) 72 | } 73 | } 74 | 75 | getCurrentLocation (): string { 76 | return getLocation(this.base) 77 | } 78 | } 79 | 80 | export function getLocation (base: string): string { 81 | let path = window.location.pathname 82 | if (base && path.indexOf(base) === 0) { 83 | path = path.slice(base.length) 84 | } 85 | return (path || '/') + window.location.search + window.location.hash 86 | } 87 | -------------------------------------------------------------------------------- /content/源码注释/vue-router/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { install } from './install' 4 | import { START } from './util/route' 5 | import { assert } from './util/warn' 6 | import { inBrowser } from './util/dom' 7 | import { cleanPath } from './util/path' 8 | import { createMatcher } from './create-matcher' 9 | import { normalizeLocation } from './util/location' 10 | import { supportsPushState } from './util/push-state' 11 | 12 | import { HashHistory } from './history/hash' 13 | import { HTML5History } from './history/html5' 14 | import { AbstractHistory } from './history/abstract' 15 | 16 | import type { Matcher } from './create-matcher' 17 | 18 | export default class VueRouter { 19 | static install: () => void; 20 | static version: string; 21 | 22 | app: any; 23 | apps: Array; 24 | ready: boolean; 25 | readyCbs: Array; 26 | options: RouterOptions; 27 | mode: string; 28 | history: HashHistory | HTML5History | AbstractHistory; 29 | matcher: Matcher; 30 | fallback: boolean; 31 | beforeHooks: Array; 32 | resolveHooks: Array; 33 | afterHooks: Array; 34 | constructor (options: RouterOptions = {}) { 35 | this.app = null 36 | this.apps = [] 37 | this.options = options 38 | this.beforeHooks = [] 39 | this.resolveHooks = [] 40 | this.afterHooks = [] 41 | /** 42 | * createMatcher 43 | * return { 44 | * match, 匹配函数 通过传入的 routes 生产各种路由表 45 | * addRoutes 动态添加更多的路由规则 46 | * } 47 | */ 48 | this.matcher = createMatcher(options.routes || [], this) 49 | 50 | // 默认 hash 模式 51 | let mode = options.mode || 'hash' 52 | // 使用 history 模式 如果设备不支持 强制使用 hash 模式 53 | this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false 54 | if (this.fallback) { 55 | mode = 'hash' 56 | } 57 | // 不是浏览器环境环境 强制使用 abstract 模式 58 | // 使用场景: Node 服务器 59 | if (!inBrowser) { 60 | mode = 'abstract' 61 | } 62 | this.mode = mode 63 | 64 | // 根据 mode 实例化 history 65 | // options.base 是实例化 VueRouter 时的 __dirname 66 | switch (mode) { 67 | case 'history': 68 | this.history = new HTML5History(this, options.base) 69 | break 70 | case 'hash': 71 | // 这里把上面的 fallback 作为参数传入 72 | // HashHistory 中会做处理 73 | this.history = new HashHistory(this, options.base, this.fallback) 74 | break 75 | case 'abstract': 76 | this.history = new AbstractHistory(this, options.base) 77 | break 78 | default: 79 | if (process.env.NODE_ENV !== 'production') { 80 | // 断言 81 | assert(false, `invalid mode: ${mode}`) 82 | } 83 | } 84 | } 85 | 86 | match ( 87 | raw: RawLocation, 88 | current?: Route, 89 | redirectedFrom?: Location 90 | ): Route { 91 | return this.matcher.match(raw, current, redirectedFrom) 92 | } 93 | 94 | // 获取当前路由信息 95 | get currentRoute (): ?Route { 96 | return this.history && this.history.current 97 | } 98 | 99 | init (app: any /* Vue component instance */) { 100 | process.env.NODE_ENV !== 'production' && assert( 101 | install.installed, 102 | `not installed. Make sure to call \`Vue.use(VueRouter)\` ` + 103 | `before creating root instance.` 104 | ) 105 | this.apps.push(app) 106 | 107 | // main app already initialized. 108 | if (this.app) { 109 | return 110 | } 111 | 112 | this.app = app 113 | 114 | const history = this.history 115 | 116 | // 对 HTML5History HashHistory 2种实例做特殊处理 117 | if (history instanceof HTML5History) { 118 | history.transitionTo(history.getCurrentLocation()) 119 | } else if (history instanceof HashHistory) { 120 | const setupHashListener = () => { 121 | history.setupListeners() 122 | } 123 | /** 124 | * transitionTo 的 回调是在 route 更新后触发 125 | * 所以是在 route 更新后再添加事件监听 126 | * 防止 根目录情况下 连续触发 hashchange 事件 127 | * issues #725 128 | */ 129 | history.transitionTo( 130 | // HashHistory 类的 getHash 方法, 获取 hash 后的 url 131 | history.getCurrentLocation(), 132 | setupHashListener, 133 | setupHashListener 134 | ) 135 | } 136 | 137 | // issues #1110 #1108 138 | // 主要是为了修复多个vue实例 route 状态共享的问题 139 | // 调用 History 类的 listen 方法 将传入的函数赋值给 this.cb 140 | // 通过 updateRoute 方法调用 更新 route 触发 vue 的 set 141 | history.listen(route => { 142 | this.apps.forEach((app) => { 143 | app._route = route 144 | }) 145 | }) 146 | } 147 | 148 | // 全局前置守卫 一般用作拦截登录 149 | beforeEach (fn: Function): Function { 150 | return registerHook(this.beforeHooks, fn) 151 | } 152 | 153 | // 全局解析守卫 154 | beforeResolve (fn: Function): Function { 155 | return registerHook(this.resolveHooks, fn) 156 | } 157 | 158 | // 全局后置钩子 159 | afterEach (fn: Function): Function { 160 | return registerHook(this.afterHooks, fn) 161 | } 162 | 163 | // 一下都是调用 history 类的方法 164 | onReady (cb: Function, errorCb?: Function) { 165 | this.history.onReady(cb, errorCb) 166 | } 167 | 168 | onError (errorCb: Function) { 169 | this.history.onError(errorCb) 170 | } 171 | 172 | push (location: RawLocation, onComplete?: Function, onAbort?: Function) { 173 | this.history.push(location, onComplete, onAbort) 174 | } 175 | 176 | replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { 177 | this.history.replace(location, onComplete, onAbort) 178 | } 179 | 180 | go (n: number) { 181 | this.history.go(n) 182 | } 183 | 184 | back () { 185 | this.go(-1) 186 | } 187 | 188 | forward () { 189 | this.go(1) 190 | } 191 | 192 | // 返回目标位置或是当前路由匹配的组件数组 193 | getMatchedComponents (to?: RawLocation | Route): Array { 194 | // 有 to 就取 to.matched 没有 to.matched 就调用 this.resolve(to) 得到 route 195 | // 没有就是当前路由记录 196 | const route: any = to 197 | ? to.matched 198 | ? to 199 | : this.resolve(to).route 200 | : this.currentRoute 201 | if (!route) { 202 | return [] 203 | } 204 | // 从 route.matched 路由记录表拿到对应的 component 以数组返回 205 | return [].concat.apply([], route.matched.map(m => { 206 | return Object.keys(m.components).map(key => { 207 | return m.components[key] 208 | }) 209 | })) 210 | } 211 | 212 | resolve ( 213 | to: RawLocation, 214 | current?: Route, 215 | append?: boolean 216 | ): { 217 | location: Location, 218 | route: Route, 219 | href: string, 220 | // for backwards compat 221 | normalizedTo: Location, 222 | resolved: Route 223 | } { 224 | const location = normalizeLocation( 225 | to, 226 | current || this.history.current, 227 | append, 228 | this 229 | ) 230 | const route = this.match(location, current) 231 | const fullPath = route.redirectedFrom || route.fullPath 232 | const base = this.history.base 233 | const href = createHref(base, fullPath, this.mode) 234 | return { 235 | location, 236 | route, 237 | href, 238 | // for backwards compat 239 | normalizedTo: location, 240 | resolved: route 241 | } 242 | } 243 | 244 | // 引用 createMatcher return 的 matcher 方法 245 | // 动态添加更多的路由规则 246 | addRoutes (routes: Array) { 247 | this.matcher.addRoutes(routes) 248 | if (this.history.current !== START) { 249 | this.history.transitionTo(this.history.getCurrentLocation()) 250 | } 251 | } 252 | } 253 | 254 | // 将回调 push 到对应的守卫钩子任务队列 255 | function registerHook (list: Array, fn: Function): Function { 256 | list.push(fn) 257 | return () => { 258 | const i = list.indexOf(fn) 259 | if (i > -1) list.splice(i, 1) 260 | } 261 | } 262 | 263 | // 不同 mode 的 href 处理 264 | function createHref (base: string, fullPath: string, mode) { 265 | var path = mode === 'hash' ? '#' + fullPath : fullPath 266 | return base ? cleanPath(base + '/' + path) : path 267 | } 268 | 269 | // 插件暴露的 install 方法 270 | VueRouter.install = install 271 | VueRouter.version = '__VERSION__' 272 | 273 | // 在 浏览器环境 和 有 window 挂载 Vue 实例情况下 自动使用插件 274 | if (inBrowser && window.Vue) { 275 | window.Vue.use(VueRouter) 276 | } 277 | -------------------------------------------------------------------------------- /content/源码注释/vue-router/install.js: -------------------------------------------------------------------------------- 1 | import View from './components/view' 2 | import Link from './components/link' 3 | 4 | export let _Vue 5 | 6 | export function install (Vue) { 7 | // 避免重复安装 8 | if (install.installed && _Vue === Vue) return 9 | install.installed = true 10 | 11 | // 这里使用一个内部变量来保存 Vue 实例 12 | // 是为了为了避免 依赖 Vue 13 | _Vue = Vue 14 | 15 | // 简单工具函数,判断不为 undefined 16 | const isDef = v => v !== undefined 17 | 18 | const registerInstance = (vm, callVal) => { 19 | let i = vm.$options._parentVnode 20 | // registerRouteInstance 是在 RouterView 赋值的 21 | // 所以说 只有组件是 RouterView 才执行 22 | // 在满足条件的情况将 i 赋值为 i.data.registerRouteInstance 并调用 23 | if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) { 24 | i(vm, callVal) 25 | } 26 | } 27 | 28 | // 全局混合策略 影响后面的vue实例 mixin 生命周期钩子将优先执行 29 | Vue.mixin({ 30 | beforeCreate () { 31 | // 判断是否有 router 实例, 32 | // 有就是根组件 执行 init 方法 33 | if (isDef(this.$options.router)) { 34 | this._routerRoot = this 35 | this._router = this.$options.router 36 | this._router.init(this) 37 | // 使用 Vue.util 暴露的 defineReactive 建立响应式的 _route 对象 38 | // Vue.util 是在 initGlobalAPI 方法中暴露的 39 | // vue/src/core/global-api/index.js 40 | Vue.util.defineReactive(this, '_route', this._router.history.current) 41 | } else { 42 | // 没有就赋值为父级的 _routerRoot 或者 this 43 | this._routerRoot = (this.$parent && this.$parent._routerRoot) || this 44 | } 45 | registerInstance(this, this) 46 | }, 47 | destroyed () { 48 | registerInstance(this) 49 | } 50 | }) 51 | 52 | // 给原型添加 $router $route 代理到 this._routerRoot._router 53 | Object.defineProperty(Vue.prototype, '$router', { 54 | get () { return this._routerRoot._router } 55 | }) 56 | 57 | Object.defineProperty(Vue.prototype, '$route', { 58 | get () { return this._routerRoot._route } 59 | }) 60 | 61 | // 注册 router-view router-link 组件 62 | Vue.component('RouterView', View) 63 | Vue.component('RouterLink', Link) 64 | 65 | // 自定义合并策略 66 | // optionMergeStrategies 声明在vue/src/core/config.js 暂时没找到在哪里赋值的 67 | const strats = Vue.config.optionMergeStrategies 68 | // use the same hook merging strategy for route hooks 69 | strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created 70 | } 71 | -------------------------------------------------------------------------------- /content/源码注释/vue-router/util/async.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // 从第一个开始,递归调用任务队列 3 | export function runQueue (queue: Array, fn: Function, cb: Function) { 4 | const step = index => { 5 | if (index >= queue.length) { 6 | cb() 7 | } else { 8 | if (queue[index]) { 9 | fn(queue[index], () => { 10 | step(index + 1) 11 | }) 12 | } else { 13 | step(index + 1) 14 | } 15 | } 16 | } 17 | step(0) 18 | } 19 | -------------------------------------------------------------------------------- /content/源码注释/vue-router/util/dom.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export const inBrowser = typeof window !== 'undefined' 4 | -------------------------------------------------------------------------------- /content/源码注释/vue-router/util/location.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type VueRouter from '../index' 4 | import { parsePath, resolvePath } from './path' 5 | import { resolveQuery } from './query' 6 | import { fillParams } from './params' 7 | import { warn } from './warn' 8 | import { extend } from './misc' 9 | 10 | export function normalizeLocation ( 11 | raw: RawLocation, 12 | current: ?Route, 13 | append: ?boolean, 14 | router: ?VueRouter 15 | ): Location { 16 | let next: Location = typeof raw === 'string' ? { path: raw } : raw 17 | // named target 18 | if (next.name || next._normalized) { 19 | return next 20 | } 21 | 22 | // relative params 23 | if (!next.path && next.params && current) { 24 | next = extend({}, next) 25 | next._normalized = true 26 | const params: any = extend(extend({}, current.params), next.params) 27 | if (current.name) { 28 | next.name = current.name 29 | next.params = params 30 | } else if (current.matched.length) { 31 | const rawPath = current.matched[current.matched.length - 1].path 32 | next.path = fillParams(rawPath, params, `path ${current.path}`) 33 | } else if (process.env.NODE_ENV !== 'production') { 34 | warn(false, `relative params navigation requires a current route.`) 35 | } 36 | return next 37 | } 38 | 39 | const parsedPath = parsePath(next.path || '') 40 | const basePath = (current && current.path) || '/' 41 | const path = parsedPath.path 42 | ? resolvePath(parsedPath.path, basePath, append || next.append) 43 | : basePath 44 | 45 | const query = resolveQuery( 46 | parsedPath.query, 47 | next.query, 48 | router && router.options.parseQuery 49 | ) 50 | 51 | let hash = next.hash || parsedPath.hash 52 | if (hash && hash.charAt(0) !== '#') { 53 | hash = `#${hash}` 54 | } 55 | 56 | return { 57 | _normalized: true, 58 | path, 59 | query, 60 | hash 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /content/源码注释/vue-router/util/misc.js: -------------------------------------------------------------------------------- 1 | export function extend (a, b) { 2 | for (const key in b) { 3 | a[key] = b[key] 4 | } 5 | return a 6 | } 7 | -------------------------------------------------------------------------------- /content/源码注释/vue-router/util/params.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { warn } from './warn' 4 | import Regexp from 'path-to-regexp' 5 | 6 | // $flow-disable-line 7 | const regexpCompileCache: { 8 | [key: string]: Function 9 | } = Object.create(null) 10 | 11 | export function fillParams ( 12 | path: string, 13 | params: ?Object, 14 | routeMsg: string 15 | ): string { 16 | try { 17 | const filler = 18 | regexpCompileCache[path] || 19 | (regexpCompileCache[path] = Regexp.compile(path)) 20 | return filler(params || {}, { pretty: true }) 21 | } catch (e) { 22 | if (process.env.NODE_ENV !== 'production') { 23 | warn(false, `missing param for ${routeMsg}: ${e.message}`) 24 | } 25 | return '' 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /content/源码注释/vue-router/util/path.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export function resolvePath ( 4 | relative: string, 5 | base: string, 6 | append?: boolean 7 | ): string { 8 | const firstChar = relative.charAt(0) 9 | if (firstChar === '/') { 10 | return relative 11 | } 12 | 13 | if (firstChar === '?' || firstChar === '#') { 14 | return base + relative 15 | } 16 | 17 | const stack = base.split('/') 18 | 19 | // remove trailing segment if: 20 | // - not appending 21 | // - appending to trailing slash (last segment is empty) 22 | if (!append || !stack[stack.length - 1]) { 23 | stack.pop() 24 | } 25 | 26 | // resolve relative path 27 | const segments = relative.replace(/^\//, '').split('/') 28 | for (let i = 0; i < segments.length; i++) { 29 | const segment = segments[i] 30 | if (segment === '..') { 31 | stack.pop() 32 | } else if (segment !== '.') { 33 | stack.push(segment) 34 | } 35 | } 36 | 37 | // ensure leading slash 38 | if (stack[0] !== '') { 39 | stack.unshift('') 40 | } 41 | 42 | return stack.join('/') 43 | } 44 | 45 | // 路径解析 path query hash 46 | export function parsePath (path: string): { 47 | path: string; 48 | query: string; 49 | hash: string; 50 | } { 51 | let hash = '' 52 | let query = '' 53 | 54 | const hashIndex = path.indexOf('#') 55 | if (hashIndex >= 0) { 56 | hash = path.slice(hashIndex) 57 | path = path.slice(0, hashIndex) 58 | } 59 | 60 | const queryIndex = path.indexOf('?') 61 | if (queryIndex >= 0) { 62 | query = path.slice(queryIndex + 1) 63 | path = path.slice(0, queryIndex) 64 | } 65 | 66 | return { 67 | path, 68 | query, 69 | hash 70 | } 71 | } 72 | 73 | // 将 '//' 替换成 '/' 74 | export function cleanPath (path: string): string { 75 | return path.replace(/\/\//g, '/') 76 | } 77 | -------------------------------------------------------------------------------- /content/源码注释/vue-router/util/push-state.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { inBrowser } from './dom' 4 | import { saveScrollPosition } from './scroll' 5 | 6 | export const supportsPushState = inBrowser && (function () { 7 | const ua = window.navigator.userAgent 8 | 9 | if ( 10 | (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) && 11 | ua.indexOf('Mobile Safari') !== -1 && 12 | ua.indexOf('Chrome') === -1 && 13 | ua.indexOf('Windows Phone') === -1 14 | ) { 15 | return false 16 | } 17 | 18 | return window.history && 'pushState' in window.history 19 | })() 20 | 21 | // use User Timing api (if present) for more accurate key precision 22 | const Time = inBrowser && window.performance && window.performance.now 23 | ? window.performance 24 | : Date 25 | 26 | let _key: string = genKey() 27 | 28 | function genKey (): string { 29 | return Time.now().toFixed(3) 30 | } 31 | 32 | export function getStateKey () { 33 | return _key 34 | } 35 | 36 | export function setStateKey (key: string) { 37 | _key = key 38 | } 39 | 40 | export function pushState (url?: string, replace?: boolean) { 41 | saveScrollPosition() 42 | // try...catch the pushState call to get around Safari 43 | // DOM Exception 18 where it limits to 100 pushState calls 44 | const history = window.history 45 | try { 46 | if (replace) { 47 | history.replaceState({ key: _key }, '', url) 48 | } else { 49 | _key = genKey() 50 | history.pushState({ key: _key }, '', url) 51 | } 52 | } catch (e) { 53 | window.location[replace ? 'replace' : 'assign'](url) 54 | } 55 | } 56 | 57 | export function replaceState (url?: string) { 58 | pushState(url, true) 59 | } 60 | -------------------------------------------------------------------------------- /content/源码注释/vue-router/util/query.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { warn } from './warn' 4 | 5 | const encodeReserveRE = /[!'()*]/g 6 | const encodeReserveReplacer = c => '%' + c.charCodeAt(0).toString(16) 7 | const commaRE = /%2C/g 8 | 9 | // fixed encodeURIComponent which is more conformant to RFC3986: 10 | // - escapes [!'()*] 11 | // - preserve commas 12 | const encode = str => encodeURIComponent(str) 13 | .replace(encodeReserveRE, encodeReserveReplacer) 14 | .replace(commaRE, ',') 15 | 16 | const decode = decodeURIComponent 17 | 18 | export function resolveQuery ( 19 | query: ?string, 20 | extraQuery: Dictionary = {}, 21 | _parseQuery: ?Function 22 | ): Dictionary { 23 | const parse = _parseQuery || parseQuery 24 | let parsedQuery 25 | try { 26 | parsedQuery = parse(query || '') 27 | } catch (e) { 28 | process.env.NODE_ENV !== 'production' && warn(false, e.message) 29 | parsedQuery = {} 30 | } 31 | for (const key in extraQuery) { 32 | parsedQuery[key] = extraQuery[key] 33 | } 34 | return parsedQuery 35 | } 36 | 37 | function parseQuery (query: string): Dictionary { 38 | const res = {} 39 | 40 | query = query.trim().replace(/^(\?|#|&)/, '') 41 | 42 | if (!query) { 43 | return res 44 | } 45 | 46 | query.split('&').forEach(param => { 47 | const parts = param.replace(/\+/g, ' ').split('=') 48 | const key = decode(parts.shift()) 49 | const val = parts.length > 0 50 | ? decode(parts.join('=')) 51 | : null 52 | 53 | if (res[key] === undefined) { 54 | res[key] = val 55 | } else if (Array.isArray(res[key])) { 56 | res[key].push(val) 57 | } else { 58 | res[key] = [res[key], val] 59 | } 60 | }) 61 | 62 | return res 63 | } 64 | 65 | export function stringifyQuery (obj: Dictionary): string { 66 | const res = obj ? Object.keys(obj).map(key => { 67 | const val = obj[key] 68 | 69 | if (val === undefined) { 70 | return '' 71 | } 72 | 73 | if (val === null) { 74 | return encode(key) 75 | } 76 | 77 | if (Array.isArray(val)) { 78 | const result = [] 79 | val.forEach(val2 => { 80 | if (val2 === undefined) { 81 | return 82 | } 83 | if (val2 === null) { 84 | result.push(encode(key)) 85 | } else { 86 | result.push(encode(key) + '=' + encode(val2)) 87 | } 88 | }) 89 | return result.join('&') 90 | } 91 | 92 | return encode(key) + '=' + encode(val) 93 | }).filter(x => x.length > 0).join('&') : null 94 | return res ? `?${res}` : '' 95 | } 96 | -------------------------------------------------------------------------------- /content/源码注释/vue-router/util/resolve-components.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { _Vue } from '../install' 4 | import { warn, isError } from './warn' 5 | 6 | export function resolveAsyncComponents (matched: Array): Function { 7 | return (to, from, next) => { 8 | let hasAsync = false 9 | let pending = 0 10 | let error = null 11 | 12 | // 拿到 resolveQueue 处理后的 activated 数组 13 | // 提取组件部分合并成数组, 循环处理异步组件 14 | flatMapComponents(matched, (def, _, match, key) => { 15 | // if it's a function and doesn't have cid attached, 16 | // assume it's an async component resolve function. 17 | // we are not using Vue's default async resolving mechanism because 18 | // we want to halt the navigation until the incoming component has been 19 | // resolved. 20 | if (typeof def === 'function' && def.cid === undefined) { 21 | hasAsync = true 22 | pending++ 23 | 24 | const resolve = once(resolvedDef => { 25 | if (isESModule(resolvedDef)) { 26 | resolvedDef = resolvedDef.default 27 | } 28 | // save resolved on async factory in case it's used elsewhere 29 | def.resolved = typeof resolvedDef === 'function' 30 | ? resolvedDef 31 | : _Vue.extend(resolvedDef) 32 | match.components[key] = resolvedDef 33 | pending-- 34 | if (pending <= 0) { 35 | next() 36 | } 37 | }) 38 | 39 | const reject = once(reason => { 40 | const msg = `Failed to resolve async component ${key}: ${reason}` 41 | process.env.NODE_ENV !== 'production' && warn(false, msg) 42 | if (!error) { 43 | error = isError(reason) 44 | ? reason 45 | : new Error(msg) 46 | next(error) 47 | } 48 | }) 49 | 50 | let res 51 | try { 52 | res = def(resolve, reject) 53 | } catch (e) { 54 | reject(e) 55 | } 56 | if (res) { 57 | if (typeof res.then === 'function') { 58 | res.then(resolve, reject) 59 | } else { 60 | // new syntax in Vue 2.3 61 | const comp = res.component 62 | if (comp && typeof comp.then === 'function') { 63 | comp.then(resolve, reject) 64 | } 65 | } 66 | } 67 | } 68 | }) 69 | 70 | if (!hasAsync) next() 71 | } 72 | } 73 | 74 | export function flatMapComponents ( 75 | matched: Array, 76 | fn: Function 77 | ): Array { 78 | return flatten(matched.map(m => { 79 | return Object.keys(m.components).map(key => fn( 80 | m.components[key], 81 | m.instances[key], 82 | m, key 83 | )) 84 | })) 85 | } 86 | 87 | export function flatten (arr: Array): Array { 88 | return Array.prototype.concat.apply([], arr) 89 | } 90 | 91 | const hasSymbol = 92 | typeof Symbol === 'function' && 93 | typeof Symbol.toStringTag === 'symbol' 94 | 95 | function isESModule (obj) { 96 | return obj.__esModule || (hasSymbol && obj[Symbol.toStringTag] === 'Module') 97 | } 98 | 99 | // in Webpack 2, require.ensure now also returns a Promise 100 | // so the resolve/reject functions may get called an extra time 101 | // if the user uses an arrow function shorthand that happens to 102 | // return that Promise. 103 | function once (fn) { 104 | let called = false 105 | return function (...args) { 106 | if (called) return 107 | called = true 108 | return fn.apply(this, args) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /content/源码注释/vue-router/util/route.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type VueRouter from '../index' 4 | import { stringifyQuery } from './query' 5 | 6 | const trailingSlashRE = /\/?$/ 7 | 8 | export function createRoute ( 9 | record: ?RouteRecord, 10 | location: Location, 11 | redirectedFrom?: ?Location, 12 | router?: VueRouter 13 | ): Route { 14 | const stringifyQuery = router && router.options.stringifyQuery 15 | 16 | let query: any = location.query || {} 17 | try { 18 | query = clone(query) 19 | } catch (e) { } 20 | 21 | const route: Route = { 22 | name: location.name || (record && record.name), 23 | meta: (record && record.meta) || {}, 24 | path: location.path || '/', 25 | hash: location.hash || '', 26 | query, 27 | params: location.params || {}, 28 | fullPath: getFullPath(location, stringifyQuery), 29 | // 用数组记录 当前 route 以及它的上级 route 30 | matched: record ? formatMatch(record) : [] 31 | } 32 | if (redirectedFrom) { 33 | route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery) 34 | } 35 | return Object.freeze(route) 36 | } 37 | 38 | function clone (value) { 39 | if (Array.isArray(value)) { 40 | return value.map(clone) 41 | } else if (value && typeof value === 'object') { 42 | const res = {} 43 | for (const key in value) { 44 | res[key] = clone(value[key]) 45 | } 46 | return res 47 | } else { 48 | return value 49 | } 50 | } 51 | 52 | // the starting route that represents the initial state 53 | export const START = createRoute(null, { 54 | path: '/' 55 | }) 56 | 57 | function formatMatch (record: ?RouteRecord): Array { 58 | const res = [] 59 | while (record) { 60 | res.unshift(record) 61 | record = record.parent 62 | } 63 | return res 64 | } 65 | 66 | function getFullPath ( 67 | { path, query = {}, hash = '' }, 68 | _stringifyQuery 69 | ): string { 70 | const stringify = _stringifyQuery || stringifyQuery 71 | return (path || '/') + stringify(query) + hash 72 | } 73 | 74 | export function isSameRoute (a: Route, b: ?Route): boolean { 75 | if (b === START) { 76 | return a === b 77 | } else if (!b) { 78 | return false 79 | } else if (a.path && b.path) { 80 | return ( 81 | a.path.replace(trailingSlashRE, '') === b.path.replace(trailingSlashRE, '') && 82 | a.hash === b.hash && 83 | isObjectEqual(a.query, b.query) 84 | ) 85 | } else if (a.name && b.name) { 86 | return ( 87 | a.name === b.name && 88 | a.hash === b.hash && 89 | isObjectEqual(a.query, b.query) && 90 | isObjectEqual(a.params, b.params) 91 | ) 92 | } else { 93 | return false 94 | } 95 | } 96 | 97 | function isObjectEqual (a = {}, b = {}): boolean { 98 | // handle null value #1566 99 | if (!a || !b) return a === b 100 | const aKeys = Object.keys(a) 101 | const bKeys = Object.keys(b) 102 | if (aKeys.length !== bKeys.length) { 103 | return false 104 | } 105 | return aKeys.every(key => { 106 | const aVal = a[key] 107 | const bVal = b[key] 108 | // check nested equality 109 | if (typeof aVal === 'object' && typeof bVal === 'object') { 110 | return isObjectEqual(aVal, bVal) 111 | } 112 | return String(aVal) === String(bVal) 113 | }) 114 | } 115 | 116 | export function isIncludedRoute (current: Route, target: Route): boolean { 117 | return ( 118 | current.path.replace(trailingSlashRE, '/').indexOf( 119 | target.path.replace(trailingSlashRE, '/') 120 | ) === 0 && 121 | (!target.hash || current.hash === target.hash) && 122 | queryIncludes(current.query, target.query) 123 | ) 124 | } 125 | 126 | function queryIncludes (current: Dictionary, target: Dictionary): boolean { 127 | for (const key in target) { 128 | if (!(key in current)) { 129 | return false 130 | } 131 | } 132 | return true 133 | } 134 | -------------------------------------------------------------------------------- /content/源码注释/vue-router/util/scroll.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type Router from '../index' 4 | import { assert } from './warn' 5 | import { getStateKey, setStateKey } from './push-state' 6 | 7 | const positionStore = Object.create(null) 8 | 9 | export function setupScroll () { 10 | // Fix for #1585 for Firefox 11 | // 监听 popstate 事件 12 | window.history.replaceState({ key: getStateKey() }, '') 13 | window.addEventListener('popstate', e => { 14 | // 在 positionStore 对象中以 key: value 形式保存当前页面滚动位置 15 | saveScrollPosition() 16 | // 将 key 储存 返回时使用 17 | if (e.state && e.state.key) { 18 | setStateKey(e.state.key) 19 | } 20 | }) 21 | } 22 | 23 | export function handleScroll ( 24 | router: Router, 25 | to: Route, 26 | from: Route, 27 | isPop: boolean 28 | ) { 29 | if (!router.app) { 30 | return 31 | } 32 | 33 | const behavior = router.options.scrollBehavior 34 | if (!behavior) { 35 | return 36 | } 37 | 38 | if (process.env.NODE_ENV !== 'production') { 39 | assert(typeof behavior === 'function', `scrollBehavior must be a function`) 40 | } 41 | 42 | // wait until re-render finishes before scrolling 43 | router.app.$nextTick(() => { 44 | const position = getScrollPosition() 45 | const shouldScroll = behavior.call(router, to, from, isPop ? position : null) 46 | 47 | if (!shouldScroll) { 48 | return 49 | } 50 | 51 | if (typeof shouldScroll.then === 'function') { 52 | shouldScroll.then(shouldScroll => { 53 | scrollToPosition((shouldScroll: any), position) 54 | }).catch(err => { 55 | if (process.env.NODE_ENV !== 'production') { 56 | assert(false, err.toString()) 57 | } 58 | }) 59 | } else { 60 | scrollToPosition(shouldScroll, position) 61 | } 62 | }) 63 | } 64 | 65 | export function saveScrollPosition () { 66 | const key = getStateKey() 67 | if (key) { 68 | positionStore[key] = { 69 | x: window.pageXOffset, 70 | y: window.pageYOffset 71 | } 72 | } 73 | } 74 | 75 | function getScrollPosition (): ?Object { 76 | const key = getStateKey() 77 | if (key) { 78 | return positionStore[key] 79 | } 80 | } 81 | 82 | function getElementPosition (el: Element, offset: Object): Object { 83 | const docEl: any = document.documentElement 84 | const docRect = docEl.getBoundingClientRect() 85 | const elRect = el.getBoundingClientRect() 86 | return { 87 | x: elRect.left - docRect.left - offset.x, 88 | y: elRect.top - docRect.top - offset.y 89 | } 90 | } 91 | 92 | function isValidPosition (obj: Object): boolean { 93 | return isNumber(obj.x) || isNumber(obj.y) 94 | } 95 | 96 | function normalizePosition (obj: Object): Object { 97 | return { 98 | x: isNumber(obj.x) ? obj.x : window.pageXOffset, 99 | y: isNumber(obj.y) ? obj.y : window.pageYOffset 100 | } 101 | } 102 | 103 | function normalizeOffset (obj: Object): Object { 104 | return { 105 | x: isNumber(obj.x) ? obj.x : 0, 106 | y: isNumber(obj.y) ? obj.y : 0 107 | } 108 | } 109 | 110 | function isNumber (v: any): boolean { 111 | return typeof v === 'number' 112 | } 113 | 114 | function scrollToPosition (shouldScroll, position) { 115 | const isObject = typeof shouldScroll === 'object' 116 | if (isObject && typeof shouldScroll.selector === 'string') { 117 | const el = document.querySelector(shouldScroll.selector) 118 | if (el) { 119 | let offset = shouldScroll.offset && typeof shouldScroll.offset === 'object' ? shouldScroll.offset : {} 120 | offset = normalizeOffset(offset) 121 | position = getElementPosition(el, offset) 122 | } else if (isValidPosition(shouldScroll)) { 123 | position = normalizePosition(shouldScroll) 124 | } 125 | } else if (isObject && isValidPosition(shouldScroll)) { 126 | position = normalizePosition(shouldScroll) 127 | } 128 | 129 | if (position) { 130 | window.scrollTo(position.x, position.y) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /content/源码注释/vue-router/util/warn.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export function assert (condition: any, message: string) { 4 | if (!condition) { 5 | throw new Error(`[vue-router] ${message}`) 6 | } 7 | } 8 | 9 | export function warn (condition: any, message: string) { 10 | if (process.env.NODE_ENV !== 'production' && !condition) { 11 | typeof console !== 'undefined' && console.warn(`[vue-router] ${message}`) 12 | } 13 | } 14 | 15 | export function isError (err: any): boolean { 16 | return Object.prototype.toString.call(err).indexOf('Error') > -1 17 | } 18 | -------------------------------------------------------------------------------- /content/源码注释/vuex/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Reduce the code which written in Vue.js for getting the state. 3 | * @param {String} [namespace] - Module's namespace 4 | * @param {Object|Array} states # Object's item can be a function which accept state and getters for param, you can do something for state and getters in it. 5 | * @param {Object} 6 | * shopping-cart examples 7 | */ 8 | // states 处理成数组后循环 9 | // 最后返回一个以 states type 为 key, this.$store.state[states type] 为值的对象 10 | export const mapState = normalizeNamespace((namespace, states) => { 11 | const res = {} 12 | // 格式化 states 13 | // 将 states 转换成 [{key: products, val: fn}] 的格式 14 | normalizeMap(states).forEach(({ key, val }) => { 15 | // 以 key: value 形式存入 res 16 | res[key] = function mappedState () { 17 | // 取出 $store state getters 18 | let state = this.$store.state 19 | let getters = this.$store.getters 20 | // 如果有命名空间 21 | if (namespace) { 22 | // 通过 getModuleByNamespace 搜索对应模块,没有搜索到 return 23 | const module = getModuleByNamespace(this.$store, 'mapState', namespace) 24 | if (!module) { 25 | return 26 | } 27 | // 保存为命名空间 state getters 28 | state = module.context.state 29 | getters = module.context.getters 30 | } 31 | // 如果 val 是 function 将 val 指向 Vue 实例,并将得到的 state getters 传参 32 | // call 改变了 this 执行并且返回 33 | // 所以在 mapState 中也传入了 getters 参数 34 | // products(state, getters) { 35 | // return state.products.all; 36 | // } 37 | // 不是 function 直接根据 key 返回对应的 state 38 | // return 39 | return typeof val === 'function' 40 | ? val.call(this, state, getters) 41 | : state[val] 42 | } 43 | // mark vuex getter for devtools 44 | res[key].vuex = true 45 | }) 46 | return res 47 | }) 48 | 49 | /** 50 | * Reduce the code which written in Vue.js for committing the mutation 51 | * @param {String} [namespace] - Module's namespace 52 | * @param {Object|Array} mutations # Object's item can be a function which accept `commit` function as the first param, it can accept anthor params. You can commit mutation and do any other things in this function. specially, You need to pass anthor params from the mapped function. 53 | * @return {Object} 54 | */ 55 | // mutations 处理成数组后循环 56 | // 最后返回一个以 mutations type 为 key, this.$store.commit.apply(this, [type].concat(args)) 为值的对象 57 | export const mapMutations = normalizeNamespace((namespace, mutations) => { 58 | const res = {} 59 | normalizeMap(mutations).forEach(({ key, val }) => { 60 | // 以 key: value 形式存入 res 61 | res[key] = function mappedMutation (...args) { 62 | // Get the commit method from store 63 | let commit = this.$store.commit 64 | if (namespace) { 65 | // 通过 getModuleByNamespace 搜索对应模块,没有搜索到 return 66 | const module = getModuleByNamespace(this.$store, 'mapMutations', namespace) 67 | if (!module) { 68 | return 69 | } 70 | commit = module.context.commit 71 | } 72 | return typeof val === 'function' 73 | ? val.apply(this, [commit].concat(args)) 74 | : commit.apply(this.$store, [val].concat(args)) 75 | } 76 | }) 77 | return res 78 | }) 79 | 80 | /** 81 | * Reduce the code which written in Vue.js for getting the getters 82 | * @param {String} [namespace] - Module's namespace 83 | * @param {Object|Array} getters 84 | * @return {Object} 85 | * ['evenOrOdd'] 86 | * 这是后传入的是 normalizeNamespace 处理后的参数 87 | * namespace: '' 88 | * getters: ["evenOrOdd"] 89 | */ 90 | // getters 处理成数组后循环 91 | // 最后返回一个以 getter type 为 key, this.$store.getters[getter type] 为值的对象 92 | export const mapGetters = normalizeNamespace((namespace, getters) => { 93 | const res = {} 94 | // 格式化 getters 95 | // 将 getters 转换成 [{key: "evenOrOdd", val: "evenOrOdd"}] 的格式 96 | normalizeMap(getters).forEach(({ key, val }) => { 97 | // thie namespace has been mutate by normalizeNamespace 98 | // 根据 字符串拼 key 的形式处理命名空间的问题 99 | // 以 key: value 形式存入 res 100 | val = namespace + val 101 | res[key] = function mappedGetter () { 102 | // 通过 getModuleByNamespace 搜索对应模块,没有搜索到 return 103 | if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) { 104 | return 105 | } 106 | // 如果是开发环境 但是 store.getters 没有则报错 107 | if (process.env.NODE_ENV !== 'production' && !(val in this.$store.getters)) { 108 | console.error(`[vuex] unknown getter: ${val}`) 109 | return 110 | } 111 | // 最后没有问题则返回 实例上对应的 getter 112 | return this.$store.getters[val] 113 | } 114 | // mark vuex getter for devtools 115 | // 根据注释判断 应该是为了给 devtools 调试工具使用的 116 | res[key].vuex = true 117 | }) 118 | // 返回 res {evenOrOdd: fn} 119 | // 最后应该是这样的 120 | // computed: { 121 | // evenOrOdd: fn 122 | // } 123 | return res 124 | }) 125 | 126 | /** 127 | * Reduce the code which written in Vue.js for dispatch the action 128 | * @param {String} [namespace] - Module's namespace 129 | * @param {Object|Array} actions # Object's item can be a function which accept `dispatch` function as the first param, it can accept anthor params. You can dispatch action and do any other things in this function. specially, You need to pass anthor params from the mapped function. 130 | * @return {Object} 131 | * ['increment','decrement','incrementIfOdd','incrementAsync'] 132 | * 这是后传入的是 normalizeNamespace 处理后的参数 133 | * namespace: '' 134 | * actions: ["increment", "decrement", "incrementIfOdd", "incrementAsync"] 135 | */ 136 | // actions 处理成数组后循环 137 | // 最后返回一个以 actions type 为 key, this.$store.apply(this, [type].concat(args)) 为值的对象 138 | // 并且将 type 挂在到同名的 methods 上 139 | export const mapActions = normalizeNamespace((namespace, actions) => { 140 | const res = {} 141 | // 格式化 actions 142 | // 将 actions 转换成 [{"key":"increment","val":"increment"},{"key":"decrement","val":"decrement"},{"key":"incrementIfOdd","val":"incrementIfOdd"},{"key":"incrementAsync","val":"incrementAsync"}] 的格式 143 | normalizeMap(actions).forEach(({ key, val }) => { 144 | // 根据 字符串拼 key 的形式处理命名空间的问题 145 | // 将 val 以 key: value 形式存入 res 146 | res[key] = function mappedAction (...args) { 147 | // get dispatch function from store 148 | let dispatch = this.$store.dispatch 149 | // 如果有命名空间 150 | if (namespace) { 151 | // 通过 getModuleByNamespace 搜索对应模块,没有搜索到 return 152 | const module = getModuleByNamespace(this.$store, 'mapActions', namespace) 153 | if (!module) { 154 | return 155 | } 156 | // 有对应的模块 就将 module.context.dispatch 赋值给 dispatch 157 | // dispatch 指向 this.$store.dispatch 158 | // TODO: dispatch boundDispatch 函数 159 | // 应该是将 res 存入的方法保存到 vue 的 methods 上 160 | dispatch = module.context.dispatch 161 | } 162 | // 如果 val 是 function 将 val 指向 Stroe 实例,并将得到的 dispatch 合并传参 163 | // 不是的话将 dispatch 指向 this.$store 并将得到的 val 合并传参 164 | return typeof val === 'function' 165 | ? val.apply(this, [dispatch].concat(args)) 166 | : dispatch.apply(this.$store, [val].concat(args)) 167 | } 168 | }) 169 | // 最后应该是这样的 170 | // methods: { 171 | // increment () {}, 172 | // decrement () {}, 173 | // incrementIfOdd () {}, 174 | // incrementAsync () {} 175 | // } 176 | return res 177 | }) 178 | 179 | /** 180 | * Rebinding namespace param for mapXXX function in special scoped, and return them by simple object 181 | * @param {String} namespace 182 | * @return {Object} 183 | */ 184 | export const createNamespacedHelpers = (namespace) => ({ 185 | mapState: mapState.bind(null, namespace), 186 | mapGetters: mapGetters.bind(null, namespace), 187 | mapMutations: mapMutations.bind(null, namespace), 188 | mapActions: mapActions.bind(null, namespace) 189 | }) 190 | 191 | /** 192 | * Normalize the map 193 | * normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ] 194 | * normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ] 195 | * @param {Array|Object} 196 | * @return {Object} 197 | * 格式化 getter mutations actions states 198 | * 接收一个对象或者数组,转化成一个数组形式,数组元素是包含 key 和 value 的对象 199 | */ 200 | function normalizeMap (map) { 201 | return Array.isArray(map) 202 | ? map.map(key => ({ key, val: key })) 203 | : Object.keys(map).map(key => ({ key, val: map[key] })) 204 | } 205 | 206 | /** 207 | * Return a function expect two param contains namespace and map. it will normalize the namespace and then the param's function will handle the new namespace and the map. 208 | * @param {Function} fn 209 | * @return {Function} 210 | * 以 Counter 例子为准, 211 | * 第一次调用 mapGetters 传入 ['evenOrOdd'] 数组 212 | * 第二次调用 mapActions 传入 ['increment','decrement','incrementIfOdd','incrementAsync'] 数组 213 | * 以 shopping-card 为准 214 | * 第一次调用 mapState 传入 {products: fn} 对象 215 | * 第二次调用 mapState 传入 {checkoutStatus: fn} 对象 216 | */ 217 | function normalizeNamespace (fn) { 218 | return (namespace, map) => { 219 | // 判断不是字符串 进入 220 | if (typeof namespace !== 'string') { 221 | // 将 namespace 赋值给 map 222 | // namespace 置空 223 | map = namespace 224 | namespace = '' 225 | // 如果是字符串最后一个字符不是 / 则加入 226 | // 应该是处理命名空间的 毕竟提供了模块化的 module 227 | } else if (namespace.charAt(namespace.length - 1) !== '/') { 228 | namespace += '/' 229 | } 230 | // 将传入的 fn 加上处理后参数执行并且返回 231 | return fn(namespace, map) 232 | } 233 | } 234 | 235 | /** 236 | * Search a special module from store by namespace. if module not exist, print error message. 237 | * @param {Object} store 238 | * @param {String} helper 239 | * @param {String} namespace 240 | * @return {Object} 241 | */ 242 | function getModuleByNamespace (store, helper, namespace) { 243 | // _modulesNamespaceMap 在 Store 构造函数中 保存有命名空间的模块 244 | const module = store._modulesNamespaceMap[namespace] 245 | if (process.env.NODE_ENV !== 'production' && !module) { 246 | console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`) 247 | } 248 | return module 249 | } 250 | -------------------------------------------------------------------------------- /content/源码注释/vuex/index.esm.js: -------------------------------------------------------------------------------- 1 | import { Store, install } from './store' 2 | import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers' 3 | 4 | export default { 5 | Store, 6 | install, 7 | version: '__VERSION__', 8 | mapState, 9 | mapMutations, 10 | mapGetters, 11 | mapActions, 12 | createNamespacedHelpers 13 | } 14 | 15 | export { 16 | Store, 17 | install, 18 | mapState, 19 | mapMutations, 20 | mapGetters, 21 | mapActions, 22 | createNamespacedHelpers 23 | } 24 | -------------------------------------------------------------------------------- /content/源码注释/vuex/index.js: -------------------------------------------------------------------------------- 1 | import { Store, install } from './store' 2 | import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers' 3 | 4 | // 导出 Store 构造函数 5 | // install version && 辅助工具函数 6 | export default { 7 | Store, 8 | install, 9 | version: '__VERSION__', 10 | mapState, 11 | mapMutations, 12 | mapGetters, 13 | mapActions, 14 | createNamespacedHelpers 15 | } 16 | -------------------------------------------------------------------------------- /content/源码注释/vuex/mixin.js: -------------------------------------------------------------------------------- 1 | export default function (Vue) { 2 | const version = Number(Vue.version.split('.')[0]) 3 | // 2.0 采用 mixin 注入 $store 将 vuexInit 放入 beforeCreate 生命周期钩子 4 | if (version >= 2) { 5 | Vue.mixin({ beforeCreate: vuexInit }) 6 | } else { 7 | // override init and inject vuex init procedure 8 | // for 1.x backwards compatibility. 9 | // 1.0 重写 _init 方法 将 vuexInit 合并到 vue init 方法中 10 | const _init = Vue.prototype._init 11 | Vue.prototype._init = function (options = {}) { 12 | options.init = options.init 13 | ? [vuexInit].concat(options.init) 14 | : vuexInit 15 | _init.call(this, options) 16 | } 17 | } 18 | 19 | /** 20 | * Vuex init hook, injected into each instances init hooks list. 21 | */ 22 | 23 | function vuexInit () { 24 | const options = this.$options 25 | // store injection 26 | // 有 options.store 说明是 root 节点 27 | // 判断 store 如果是 function 将函数返回值赋值到 this.$store 28 | // 否则将 options.store 直接赋值赋值 29 | if (options.store) { 30 | this.$store = typeof options.store === 'function' 31 | ? options.store() 32 | : options.store 33 | // 如果没有就从父组件中获取 $store 34 | // 保证只有一个全局的 $store 35 | } else if (options.parent && options.parent.$store) { 36 | this.$store = options.parent.$store 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /content/源码注释/vuex/module/module-collection.js: -------------------------------------------------------------------------------- 1 | import Module from './module' 2 | import { assert, forEachValue } from '../util' 3 | 4 | export default class ModuleCollection { 5 | constructor (rawRootModule) { 6 | // register root module (Vuex.Store options) 7 | this.register([], rawRootModule, false) 8 | } 9 | 10 | get (path) { 11 | return path.reduce((module, key) => { 12 | return module.getChild(key) 13 | }, this.root) 14 | } 15 | 16 | // 根据 path 处理命名空间 17 | getNamespace (path) { 18 | let module = this.root 19 | return path.reduce((namespace, key) => { 20 | module = module.getChild(key) 21 | return namespace + (module.namespaced ? key + '/' : '') 22 | }, '') 23 | } 24 | 25 | update (rawRootModule) { 26 | update([], this.root, rawRootModule) 27 | } 28 | 29 | register (path, rawModule, runtime = true) { 30 | if (process.env.NODE_ENV !== 'production') { 31 | assertRawModule(path, rawModule) 32 | } 33 | 34 | // 默认注册 root 35 | // 包装了下传过来的 rawModule 36 | const newModule = new Module(rawModule, runtime) 37 | // 判断 path.length 38 | // 0 说明是 root 保存到 this.root 上 39 | // 下次递归注册进入 else 调用 Module 类的 getChild addChild 40 | // 建立 module 的父子关系 41 | if (path.length === 0) { 42 | this.root = newModule 43 | } else { 44 | const parent = this.get(path.slice(0, -1)) 45 | parent.addChild(path[path.length - 1], newModule) 46 | } 47 | 48 | // register nested modules 49 | // 有 modules 递归注册嵌套模块 50 | if (rawModule.modules) { 51 | forEachValue(rawModule.modules, (rawChildModule, key) => { 52 | this.register(path.concat(key), rawChildModule, runtime) 53 | }) 54 | } 55 | } 56 | 57 | unregister (path) { 58 | const parent = this.get(path.slice(0, -1)) 59 | const key = path[path.length - 1] 60 | if (!parent.getChild(key).runtime) return 61 | 62 | parent.removeChild(key) 63 | } 64 | } 65 | 66 | function update (path, targetModule, newModule) { 67 | if (process.env.NODE_ENV !== 'production') { 68 | assertRawModule(path, newModule) 69 | } 70 | 71 | // update target module 72 | targetModule.update(newModule) 73 | 74 | // update nested modules 75 | if (newModule.modules) { 76 | for (const key in newModule.modules) { 77 | if (!targetModule.getChild(key)) { 78 | if (process.env.NODE_ENV !== 'production') { 79 | console.warn( 80 | `[vuex] trying to add a new module '${key}' on hot reloading, ` + 81 | 'manual reload is needed' 82 | ) 83 | } 84 | return 85 | } 86 | update( 87 | path.concat(key), 88 | targetModule.getChild(key), 89 | newModule.modules[key] 90 | ) 91 | } 92 | } 93 | } 94 | 95 | const functionAssert = { 96 | assert: value => typeof value === 'function', 97 | expected: 'function' 98 | } 99 | 100 | const objectAssert = { 101 | assert: value => typeof value === 'function' || 102 | (typeof value === 'object' && typeof value.handler === 'function'), 103 | expected: 'function or object with "handler" function' 104 | } 105 | 106 | const assertTypes = { 107 | getters: functionAssert, 108 | mutations: functionAssert, 109 | actions: objectAssert 110 | } 111 | 112 | function assertRawModule (path, rawModule) { 113 | Object.keys(assertTypes).forEach(key => { 114 | if (!rawModule[key]) return 115 | 116 | const assertOptions = assertTypes[key] 117 | 118 | forEachValue(rawModule[key], (value, type) => { 119 | assert( 120 | assertOptions.assert(value), 121 | makeAssertionMessage(path, key, type, value, assertOptions.expected) 122 | ) 123 | }) 124 | }) 125 | } 126 | 127 | function makeAssertionMessage (path, key, type, value, expected) { 128 | let buf = `${key} should be ${expected} but "${key}.${type}"` 129 | if (path.length > 0) { 130 | buf += ` in module "${path.join('.')}"` 131 | } 132 | buf += ` is ${JSON.stringify(value)}.` 133 | return buf 134 | } 135 | -------------------------------------------------------------------------------- /content/源码注释/vuex/module/module.js: -------------------------------------------------------------------------------- 1 | import { forEachValue } from '../util' 2 | 3 | // Base data struct for store's module, package with some attribute and method 4 | export default class Module { 5 | constructor (rawModule, runtime) { 6 | this.runtime = runtime 7 | // Store some children item 8 | this._children = Object.create(null) 9 | // Store the origin module object which passed by programmer 10 | this._rawModule = rawModule 11 | const rawState = rawModule.state 12 | // Store the origin module's state 13 | // 默认传入 rawState = {count: 0} 14 | // 如果是有 module 的情况是空对象 15 | this.state = (typeof rawState === 'function' ? rawState() : rawState) || {} 16 | } 17 | 18 | get namespaced () { 19 | return !!this._rawModule.namespaced 20 | } 21 | 22 | addChild (key, module) { 23 | this._children[key] = module 24 | } 25 | 26 | removeChild (key) { 27 | delete this._children[key] 28 | } 29 | 30 | getChild (key) { 31 | return this._children[key] 32 | } 33 | 34 | update (rawModule) { 35 | this._rawModule.namespaced = rawModule.namespaced 36 | if (rawModule.actions) { 37 | this._rawModule.actions = rawModule.actions 38 | } 39 | if (rawModule.mutations) { 40 | this._rawModule.mutations = rawModule.mutations 41 | } 42 | if (rawModule.getters) { 43 | this._rawModule.getters = rawModule.getters 44 | } 45 | } 46 | 47 | forEachChild (fn) { 48 | forEachValue(this._children, fn) 49 | } 50 | 51 | forEachGetter (fn) { 52 | if (this._rawModule.getters) { 53 | forEachValue(this._rawModule.getters, fn) 54 | } 55 | } 56 | 57 | forEachAction (fn) { 58 | if (this._rawModule.actions) { 59 | forEachValue(this._rawModule.actions, fn) 60 | } 61 | } 62 | 63 | forEachMutation (fn) { 64 | if (this._rawModule.mutations) { 65 | forEachValue(this._rawModule.mutations, fn) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /content/源码注释/vuex/plugins/devtool.js: -------------------------------------------------------------------------------- 1 | // 根据 __VUE_DEVTOOLS_GLOBAL_HOOK__ 判断当前浏览器是否安装了 vueTools 2 | const devtoolHook = 3 | typeof window !== 'undefined' && 4 | window.__VUE_DEVTOOLS_GLOBAL_HOOK__ 5 | 6 | export default function devtoolPlugin (store) { 7 | if (!devtoolHook) return 8 | 9 | store._devtoolHook = devtoolHook 10 | 11 | // 向 vueTools emit 事件 并传入当前的 store 12 | // devtoolHook 监听到会根据 store 初始化 vuex 13 | devtoolHook.emit('vuex:init', store) 14 | 15 | // devtoolHook 监听 vuex:travel-to-state,调用回调函数 16 | devtoolHook.on('vuex:travel-to-state', targetState => { 17 | store.replaceState(targetState) 18 | }) 19 | 20 | store.subscribe((mutation, state) => { 21 | devtoolHook.emit('vuex:mutation', mutation, state) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /content/源码注释/vuex/plugins/logger.js: -------------------------------------------------------------------------------- 1 | // Credits: borrowed code from fcomb/redux-logger 2 | 3 | import { deepCopy } from '../util' 4 | 5 | export default function createLogger ({ 6 | collapsed = true, 7 | filter = (mutation, stateBefore, stateAfter) => true, 8 | transformer = state => state, 9 | mutationTransformer = mut => mut, 10 | logger = console 11 | } = {}) { 12 | // Currying 13 | return store => { 14 | // 深拷贝 15 | let prevState = deepCopy(store.state) 16 | // 订阅函数,每次改变 Store 上的数据触发 17 | // 打印日志 18 | store.subscribe((mutation, state) => { 19 | if (typeof logger === 'undefined') { 20 | return 21 | } 22 | const nextState = deepCopy(state) 23 | 24 | if (filter(mutation, prevState, nextState)) { 25 | const time = new Date() 26 | const formattedTime = ` @ ${pad(time.getHours(), 2)}:${pad(time.getMinutes(), 2)}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}` 27 | const formattedMutation = mutationTransformer(mutation) 28 | const message = `mutation ${mutation.type}${formattedTime}` 29 | const startMessage = collapsed 30 | ? logger.groupCollapsed 31 | : logger.group 32 | 33 | // render 34 | try { 35 | startMessage.call(logger, message) 36 | } catch (e) { 37 | console.log(message) 38 | } 39 | 40 | logger.log('%c prev state', 'color: #9E9E9E; font-weight: bold', transformer(prevState)) 41 | logger.log('%c mutation', 'color: #03A9F4; font-weight: bold', formattedMutation) 42 | logger.log('%c next state', 'color: #4CAF50; font-weight: bold', transformer(nextState)) 43 | 44 | try { 45 | logger.groupEnd() 46 | } catch (e) { 47 | logger.log('—— log end ——') 48 | } 49 | } 50 | 51 | prevState = nextState 52 | }) 53 | } 54 | } 55 | 56 | function repeat (str, times) { 57 | return (new Array(times + 1)).join(str) 58 | } 59 | 60 | function pad (num, maxLength) { 61 | return repeat('0', maxLength - num.toString().length) + num 62 | } 63 | -------------------------------------------------------------------------------- /content/源码注释/vuex/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the first item that pass the test 3 | * by second argument function 4 | * 5 | * @param {Array} list 6 | * @param {Function} f 7 | * @return {*} 8 | */ 9 | export function find (list, f) { 10 | return list.filter(f)[0] 11 | } 12 | 13 | /** 14 | * Deep copy the given object considering circular structure. 15 | * This function caches all nested objects and its copies. 16 | * If it detects circular structure, use cached copy to avoid infinite loop. 17 | * 18 | * @param {*} obj 19 | * @param {Array} cache 20 | * @return {*} 21 | */ 22 | export function deepCopy (obj, cache = []) { 23 | // just return if obj is immutable value 24 | // 首先判断不是 `obj` 全等于 `null` 或者 `obj` 的类型不等于 `object` 就返回 `obj` 25 | if (obj === null || typeof obj !== 'object') { 26 | return obj 27 | } 28 | 29 | // if obj is hit, it is in circular structure 30 | // 接下来调用 `find`,将 `cache` 和 回调传入 31 | const hit = find(cache, c => c.original === obj) 32 | // 如果有 `hit` 就说明是环形结构 33 | // 所谓环形环形结构,就是对象之间相互引用 34 | // const obj = { 35 | // a: 1 36 | // } 37 | // obj.b = obj 38 | // 就是直接返回 `hit.copy` 39 | if (hit) { 40 | return hit.copy 41 | } 42 | 43 | // 申明 copy 变量,如果 obj 是数组 那就是空数组,否则就是空对象 44 | const copy = Array.isArray(obj) ? [] : {} 45 | // put the copy into cache at first 46 | // because we want to refer it in recursive deepCopy 47 | // `original` 为 `key`, `obj` 为 `value`,已经上面申明的 `copy` 变量包装成对象 `push` 到 `cache` 数组中 48 | // 在下一次循环中,调用 find 方法,会使用 filter 去过滤匹配的对象,c.original 全等于当前循环的 `obj` ,说明引用的是一个地址,是环形结构 49 | 50 | cache.push({ 51 | original: obj, 52 | copy 53 | }) 54 | 55 | // 循环 obj key,递归调用 deepCopy 将 obj[key],和缓存的 `cache` 作为参数传入 56 | Object.keys(obj).forEach(key => { 57 | copy[key] = deepCopy(obj[key], cache) 58 | }) 59 | 60 | return copy 61 | } 62 | 63 | /** 64 | * forEach for object 65 | */ 66 | // object 转成数组 循环调用 fn 67 | export function forEachValue (obj, fn) { 68 | Object.keys(obj).forEach(key => fn(obj[key], key)) 69 | } 70 | 71 | export function isObject (obj) { 72 | return obj !== null && typeof obj === 'object' 73 | } 74 | 75 | export function isPromise (val) { 76 | return val && typeof val.then === 'function' 77 | } 78 | 79 | export function assert (condition, msg) { 80 | if (!condition) throw new Error(`[vuex] ${msg}`) 81 | } 82 | --------------------------------------------------------------------------------