├── .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 | 
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 | 
18 |
19 | 在 Devices 设置映射设备,注意,这里勾选了 Microsoft 的键盘设备,因为需要映射鼠标的 Window 键位。
20 |
21 | PS: 鼠标上的 Window 竟然属于键盘的键位,怪不得网上好多小伙伴都说映射不了。
22 |
23 | 
24 |
25 | ## 微软 Sculpt 鼠标映射
26 |
27 | ### [微软 Sculpt 人体工学鼠标](https://item.jd.com/987709.html)
28 |
29 | 
30 |
31 | ### [SteerMouse - Download](http://plentycom.jp/en/steermouse/download.php)
32 |
33 | SteerMouse 是一个应用程序,可以自由自定义按钮、滚轮和光标速度。兼容 Windows 与 Mac 平台,并且支持 USB 和蓝牙鼠标。
34 |
35 | 具体键位映射如图:
36 |
37 | 
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 | "",
99 | " \n",
100 | "
",
101 | "\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 | 
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