Giter Club home page Giter Club logo

my-blog's Introduction

My-Blog

项目做了迁移,每篇文章对应一条 issue ,每条 issue 的 demo 可切换到该项目对应的分支查看

my-blog's People

Contributors

zengtianshengz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

my-blog's Issues

服务端渲染-Vue-ssr

服务端渲染(SSR)

什么是服务端渲染,简单理解是将组件或页面通过服务器生成html字符串,再发送到浏览器,最后将静态标记"混合"为客户端上完全交互的应用程序。
于传统的SPA(单页应用)相比,服务端渲染能更好的有利于SEO,减少页面首屏加载时间,当然对开发来讲我们就不得不多学一些知识来支持服务端渲染。同时服务端渲染对服务器的压力也是相对较大的,和服务器简单输出静态文件相比,通过node去渲染出页面再传递给客户端显然开销是比较大的,需要注意准备好相应的服务器负载。

一、一个简单的例子

// 第 1 步:创建一个 Vue 实例
const Vue = require('vue')
const app = new Vue({
  template: `<div>Hello World</div>`
})
// 第 2 步:创建一个 renderer
const renderer = require('vue-server-renderer').createRenderer()
// 第 3 步:将 Vue 实例渲染为 HTML
renderer.renderToString(app, (err, html) => {
  if (err) throw err
  console.log(html)
  // => <div data-server-rendered="true">Hello World</div>
})

上面例子利用 vue-server-renderer npm 包将一个vue示例最后渲染出了一段 html。将这段html发送给客户端就轻松的实现了服务器渲染了。

const server = require('express')()
server.get('*', (req, res) => {
  // ... 生成 html
  res.end(html)
})
server.listen(8080)

二、官方渲染步骤

上面例子虽然简单,但在实际项目中往往还需要考虑到路由,数据,组件化等等,所以服务端渲染不是只用一个 vue-server-renderer npm包就能轻松搞定的,下面给出一张Vue官方的服务器渲染示意图:

image

流程图大致意思是:将 Source(源码)通过 webpack 打包出两个 bundle,其中 Server Bundle 是给服务端用的,服务端通过渲染器 bundleRenderer 将 bundle 生成 html 给浏览器用;另一个 Client Bundle 是给浏览器用的,别忘了服务端只是生成前期首屏页面所需的 html ,后期的交互和数据处理还是需要能支持浏览器脚本的 Client Bundle 来完成。

三、具体怎么实现

实现过程就是将上面的示意图转化成代码实现,不过这个过程还是有点小复杂的,需要多点耐心去推敲每个细节。

1、先实现一个基本版

项目结构示例:

├── build
│   ├── webpack.base.config.js     # 基本配置文件
│   ├── webpack.client.config.js   # 客户端配置文件
│   ├── webpack.server.config.js   # 服务端配置文件
└── src
    ├── router          
    │    └── index.js              # 路由
    └── views             
    │    ├── comp1.vue             # 组件
    │    └── copm2.vue             # 组件
    ├── App.vue                    # 顶级 vue 组件
    ├── app.js                     # app 入口文件
    ├──  client-entry.js           # client 的入口文件
    ├──  index.template.html       # html 模板
    ├──  server-entry.js           # server 的入口文件
├──  server.js           # server 服务

其中:

(1)、comp1.vue 和 copm2.vue 组件
<template>
    <section>组件 1</section>
</template>
<script>
    export default {
        data () {
            return {
                msg: ''
            }
        }
    }
</script>
(2)、App.vue 顶级 vue 组件
<template>
    <div id="app">
        <h1>vue-ssr</h1>
        <router-link class="link" to="/comp1">to comp1</router-link>
        <router-link class="link" to="/comp2">to comp2</router-link>

        <router-view class="view"></router-view>
    </div>
</template>

<style lang="stylus">
    .link
        margin 10px
</style>
(3)、index.template.html html 模板
<!DOCTYPE html>
<html lang="zh_CN">
<head>
    <title>{{ title }}</title>
    <meta charset="utf-8"/>
    <meta name="mobile-web-app-capable" content="yes"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1"/>
    <meta name="renderer" content="webkit"/>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"/>
    <meta name="theme-color" content="#f60"/>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
(4)、上面基础代码不解释,接下来看

路由 router

import Vue from 'vue'
import Router from 'vue-router'
import comp1 from '../views/comp1.vue'
import comp2 from '../views/comp2.vue'
Vue.use(Router)
export function createRouter () {
    return new Router({
        mode: 'history',
        scrollBehavior: () => ({ y: 0 }),
        routes: [
            {
                path: '/comp1',
                component: comp1
            },
            {
                path: '/comp2',
                component: comp2
            },
            { path: '/', redirect: '/comp1' }
        ]
    })
}

app.js app 入口文件

import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'

export function createApp (ssrContext) {
    const router = createRouter()
    const app = new Vue({
        router,
        ssrContext,
        render: h => h(App)
    })
    return { app, router }
}

我们通过 createApp 暴露一个根 Vue 实例,这是为了确保每个用户能得到一份新的实例,避免状态污染,所以我们写了一个
可以重复执行的工厂函数 createApp。 同样路由 router 我们也是一样的处理方式 createRouter 来暴露一个 router 实例

(5)client-entry.js client 的入口文件
import { createApp } from './app'

const { app, router } = createApp()
router.onReady(() => {
    app.$mount('#app')
})

客户端代码是在路由解析完成的时候讲 app 挂载到 #app 标签下

(7)server-entry.js server 的入口文件
import { createApp } from './app'

export default context => {
    // 因为这边 router.onReady 是异步的,所以我们返回一个 Promise
    // 确保路由或组件准备就绪
    return new Promise((resolve, reject) => {
        const { app, router } = createApp(context)
        router.push(context.url)
        router.onReady(() => {
            resolve(app)
        }, reject)
    })
}

服务器的入口文件我们返回了一个 promise

2、打包

在第一步我们大费周章实现了一个带有路由的日常功能模板代码,接着我们需要利用webpack将上面的代码打包出服务端和客户端key的代码,入口文件分别是 server-entry.js client-entry.js

(1)、 webpack构建配置

一般配置分为三个文件:base, client 和 server。基本配置(base config)包含在两个环境共享的配置,例如,输出路径(output path),别名(alias)和 loader。服务器配置(server config)和客户端配置(client config),可以通过使用 webpack-merge 来简单地扩展基本配置。

webpack.base.config.js 配置文件

const path = require('path')
const webpack = require('webpack')
const ExtractTextPlugin = require('extract-text-webpack-plugin')

module.exports = {
    devtool: '#cheap-module-source-map',
    output: {
        path: path.resolve(__dirname, '../dist'),
        publicPath: '/dist/',
        filename: '[name]-[chunkhash].js'
    },
    resolve: {
        alias: {
            'public': path.resolve(__dirname, '../public'),
            'components': path.resolve(__dirname, '../src/components')
        },
        extensions: ['.js', '.vue']
    },
    module: {
        noParse: /es6-promise\.js$/,
        rules: [
            {
                test: /\.(js|vue)/,
                use: 'eslint-loader',
                enforce: 'pre',
                exclude: /node_modules/
            },
            {
                test: /\.vue$/,
                use: {
                    loader: 'vue-loader',
                    options: {
                        preserveWhitespace: false,
                        postcss: [
                            require('autoprefixer')({
                                browsers: ['last 3 versions']
                            })
                        ]
                    }
                }
            },
            {
                test: /\.js$/,
                use: 'babel-loader',
                exclude: /node_modules/
            },
            {
                test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        limit: 10000,
                        name: 'img/[name].[hash:7].[ext]'
                    }
                }
            },
            {
                test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        limit: 10000,
                        name: 'fonts/[name].[hash:7].[ext]'
                    }
                }
            },
            {
                test: /\.css$/,
                use: ['vue-style-loader', 'css-loader']
            },
            {
                test: /\.json/,
                use: 'json-loader'
            }
        ]
    },
    performance: {
        maxEntrypointSize: 300000,
        hints: 'warning'
    },
    plugins: [
        new webpack.optimize.UglifyJsPlugin({
            compress: { warnings: false }
        }),
        new ExtractTextPlugin({
            filename: 'common.[chunkhash].css'
        })
    ]
}

webpack.client.config.js 配置文件

const path = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
const glob = require('glob')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')

const config = merge(base, {
    entry: {
        app: './src/client-entry.js'
    },
    resolve: {
        alias: {
            'create-api': './create-api-client.js'
        }
    },
    plugins: [
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
            'process.env.VUE_ENV': '"client"',
            'process.env.DEBUG_API': '"true"'
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor',
            minChunks: function (module) {
                return (
                    /node_modules/.test(module.context) && !/\.css$/.test(module.require)
                )
            }
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'manifest'
        }),
        // 这是将服务器的整个输出
        // 构建为单个 JSON 文件的插件。
        // 默认文件名为 `vue-ssr-server-bundle.json`
        new VueSSRClientPlugin()
    ]
})
module.exports = config

webpack.server.config.js 配置文件

const path = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
const nodeExternals = require('webpack-node-externals')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')

module.exports = merge(base, {
    target: 'node',
    devtool: '#source-map',
    entry: './src/server-entry.js',
    output: {
        filename: 'server-bundle.js',
        libraryTarget: 'commonjs2'
    },
    resolve: {
        alias: {
            'create-api': './create-api-server.js'
        }
    },
    externals: nodeExternals({
        whitelist: /\.css$/
    }),
    plugins: [
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
            'process.env.VUE_ENV': '"server"'
        }),
        new VueSSRServerPlugin()
    ]
})

webpack 配置完成,其实东西也不多,都是常规配置。需要注意的是 webpack.server.config.js 配置,output是生成一个 commonjs 的 library, VueSSRServerPlugin 用于这是将服务器的整个输出构建为单个 JSON 文件的插件。

(2)、 webpack build poj

build 代码

webpack --config build/webpack.client.config.js
webpack --config build/webpack.server.config.js

打包后会生成一些打包文件,其中 server.config 打包后会生成 vue-ssr-server-bundle.json 文件,这个文件是给 createBundleRenderer 用的,用于服务端渲染出 html 文件

const { createBundleRenderer } = require('vue-server-renderer')
const renderer = createBundleRenderer('/path/to/vue-ssr-server-bundle.json', {
  // ……renderer 的其他选项
})

细心的你还会发现 client.config 不仅生成了一下客服端用的到 js 文件,还会生成一份 vue-ssr-client-manifest.json 文件,这个文件是客户端构建清单,服务端拿到这份构建清单找到一下用于初始化的js脚步或css注入到 html 一起发给浏览器。

(3)、 服务端渲染

其实上面都是准备工作,最重要的一步是将webpack构建后的资源代码给服务端用来生成 html 。我们需要用node写一个服务端应用,通过打包后的资源生成 html 并发送给浏览器

server.js

const fs = require('fs')
const path = require('path')
const Koa = require('koa')
const KoaRuoter = require('koa-router')
const serve = require('koa-static')
const { createBundleRenderer } = require('vue-server-renderer')
const LRU = require('lru-cache')

const resolve = file => path.resolve(__dirname, file)
const app = new Koa()
const router = new KoaRuoter()
const template = fs.readFileSync(resolve('./src/index.template.html'), 'utf-8')

function createRenderer (bundle, options) {
    return createBundleRenderer(
        bundle,
        Object.assign(options, {
            template,
            cache: LRU({
                max: 1000,
                maxAge: 1000 * 60 * 15
            }),
            basedir: resolve('./dist'),
            runInNewContext: false
        })
    )
}

let renderer
const bundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
renderer = createRenderer(bundle, {
    clientManifest
})

/**
 * 渲染函数
 * @param ctx
 * @param next
 * @returns {Promise}
 */
function render (ctx, next) {
    ctx.set("Content-Type", "text/html")
    return new Promise (function (resolve, reject) {
        const handleError = err => {
            if (err && err.code === 404) {
                ctx.status = 404
                ctx.body = '404 | Page Not Found'
            } else {
                ctx.status = 500
                ctx.body = '500 | Internal Server Error'
                console.error(`error during render : ${ctx.url}`)
                console.error(err.stack)
            }
            resolve()
        }
        const context = {
            title: 'Vue Ssr 2.3',
            url: ctx.url
        }
        renderer.renderToString(context, (err, html) => {
            if (err) {
                return handleError(err)
            }
            console.log(html)
            ctx.body = html
            resolve()
        })
    })
}

app.use(serve('/dist', './dist', true))
app.use(serve('/public', './public', true))

router.get('*', render)
app.use(router.routes()).use(router.allowedMethods())

const port = process.env.PORT || 8089
app.listen(port, '0.0.0.0', () => {
    console.log(`server started at localhost:${port}`)
})

这里我们用到了最开始 demo 用到的 vue-server-renderer npm 包,通过读取 vue-ssr-server-bundle.jsonvue-ssr-client-manifest.json 文件 renderer 出 html,最后 ctx.body = html 发送给浏览器, 我们试着
console.log(html) 出 html 看看服务端到底渲染出了何方神圣:

<!DOCTYPE html>
<html lang="zh_CN">
<head>
    <title>Vue Ssr 2.3</title>
    <meta charset="utf-8"/>
    <meta name="mobile-web-app-capable" content="yes"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1"/>
    <meta name="renderer" content="webkit"/>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"/>
    <meta name="theme-color" content="#f60"/>
<link rel="preload" href="/dist/manifest-56dda86c1b6ac68c0279.js" as="script"><link rel="preload" href="/dist/vendor-3504d51340141c3804a1.js" as="script"><link rel="preload" href="/dist/app-ae1871b21fa142b507e8.js" as="script"><style data-vue-ssr-id="41a1d6f9:0">
.link {
  margin: 10px;
}
</style><style data-vue-ssr-id="7add03b4:0"></style></head>
<body>
<div id="app" data-server-rendered="true"><h1>vue-ssr</h1><a href="/comp1" class="link router-link-exact-active router-link-active">to comp1</a><a href="/comp2" class="link">to comp2</a><section class="view">组件 1</section></div><script src="/dist/manifest-56dda86c1b6ac68c0279.js" defer></script><script src="/dit/vendor-3504d51340141c3804a1.js" defer></script><script src="/dist/app-ae1871b21fa142b507e8.js" defer></script>
</body>
</html>

可以看到服务端把路由下的 组件 1 也给渲染出来了,而不是让客服端去动态加载,其次是 html 也被注入了一些 <script 标签去加载对应的客户端资源。这里再多说一下,有的同学可能不理解,服务端渲染不就是最后输出 html 让浏览器渲染吗,怎么 html 还带 js 脚本,注意,服务端渲染出的 html 只是首次展示给用户的页面而已,用户后期操作页面处理数据还是需要 js 脚本去跑的,也就是 webpack 为什么要打包出一套服务端代码(用于渲染首次html用),一套客户端代码(用于后期交互和数据处理用)

四、小结

本篇简单了解了 vue ssr 的简单流程,上面例子的demo放在对应项目根目录下,方便查看。服务端渲染还有比较重要的一部分是首屏数据的获取渲染,一般页面展示都会有一些网络数据初始化,服务端渲染可以将这些数据获取到插入到 html ,由于这部份内容涉及到的知识点也不少,放在下次讲。

运行项目

1、获取项目分支

git clone https://github.com/ZengTianShengZ/My-Blog.git
git checkout -b demo-vue-ssr origin/demo-vue-ssr

2、项目构建

cd demo                // 切换至 demo 目录
npm run install
npm run build:client  // 生成 clientBundle
npm run build:server  // 生成 serverBundle
npm run dev           // 启动 node 渲染服务

open http://localhost:8089/

koa2搭建后台应用

本篇介绍koa2的使用,并一步步搭建一套后端服务

涉及到的功能点有

  • koa2基础应用
  • koa 路由
    • 原生路由
    • 接收 get/post 请求
    • koa-router中间件
  • koa 中间件
  • 项目框架搭建
  • 进程管理工具PM2
  • 部署线上

一、koa2 基础应用

1、hello word

// index.js
const Koa = require('koa')
const app = new Koa()

app.use( async ( ctx ) => {
  ctx.body = 'hello koa2'
})

app.listen(3000)
console.log('start-quick is starting http://localhost:3000/')

启动demo

node index.js

浏览器打开连接 http://localhost:3000/ 能看到输出 hello koa2

2、koa ctx

也许你会注意到上面 demo 的 ctx 是个什么,ctx.body 为什么能返回请求数据,我们试着 console.log(ctx)

{ request:
   { method: 'GET',
     url: '/',
     header:
      { host: 'localhost:3000',
        'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36',
        accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
        'accept-encoding': 'gzip, deflate, br',
        'accept-language': 'zh-CN,zh;q=0.9',
        'cache-control': 'max-age=0',
        cookie: '2%22%7D',
        'proxy-connection': 'keep-alive',
        'upgrade-insecure-requests': '1',
        'x-lantern-version': '4.7.0' } },
  response: { status: 404, message: 'Not Found', header: {} },
  app: { subdomainOffset: 2, proxy: false, env: 'development' },
  originalUrl: '/',
  req: '<original node req>',
  res: '<original node res>',
  socket: '<original node socket>' }
{ request:
   { method: 'GET',
     url: '/favicon.ico',
     header:
      { host: 'localhost:3000',
        'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36',
        accept: 'image/webp,image/apng,image/*,*/*;q=0.8',
        'accept-encoding': 'gzip, deflate, br',
        'accept-language': 'zh-CN,zh;q=0.9',
        'cache-control': 'no-cache',
        cookie: '2%22%7D',
        pragma: 'no-cache',
        'proxy-connection': 'keep-alive',
        referer: 'http://localhost:3000/',
        'x-lantern-version': '4.7.0' } },
  response: { status: 404, message: 'Not Found', header: {} },
  app: { subdomainOffset: 2, proxy: false, env: 'development' },
  originalUrl: '/favicon.ico',
  req: '<original node req>',
  res: '<original node res>',
  socket: '<original node socket>' }

可以看到 ctx 封装了一些 request 和 response 信息,这里的 ctx 其实就是对应 koa Context ,koa Context 将 node 的 request 和 response 对象封装到这个对象中,可以看下面的一张对照表

ctx -> Koa Context // koa 启动时生成的上下文
ctx.req -> 原生 Node  request 对象
ctx.res -> 原生 Node  response 对象
ctx.request -> koa  Request 对象.
ctx.response -> koa  Response 对象.

弄懂了 ctx ,接着咱们再来讨论 ctx.body , 上面不是讲了 ctx 是封装了 request 和 response 信息,ctx.body 是哪里来的,其实 koa 内部对 ctx 多做了一层 delegate (委托),ctx.body = ctx.response.body, 这样做的好处是啥,方便使用啊,代码也简洁。具体源码可查看 koa context.js 160行
同样的委托处理还有:

// Request 别名
ctx.header
ctx.headers
ctx.method
ctx.method=
ctx.url
ctx.path=
ctx.query
// ...
// Response 别名
ctx.body
ctx.body=
ctx.status
// ...

二、koa 路由

1、原生路由

我们实际项目中存在多个路由,也存在多个请求接口 get/ post 请求等,怎么来处理不同路由的请求�呢,其实很简单,应用到上面的知识点,我们能通过 ctx.request.url 拿到请求路径, switch一下请求�路径对不同的路由做区分处理不就�行了。

const Koa = require('koa')
const app = new Koa()

app.use( async ( ctx ) => {
  console.log(ctx.url);
  const url = ctx.url
  switch ( url ) {
    case '/':
      ctx.body = 'hello koa2 '
      break
    case '/page2':
      ctx.body = 'hello koa2 page2'
      break
    case '/404':
      ctx.body = 'hello koa2 404'
      break
    default:
      break
  }
})

app.listen(3000)
console.log('start-quick is starting http://localhost:3000/')

浏览器分别访问

http://localhost:3000/
http://localhost:3000/page2
http://localhost:3000/404

能看到不同的页面信息

2、接收 get/post 请求

原生路由怎么处理 get/post 请求呢,可以通过 ctx.method 拿到请求类型, ctx.query 可以拿到请求数据,但 post请求 koa 没封装取参的方法,需要自己通过原生的 node request 进行处理, ctx.req 对象对应的就是 node request ,前文有提到过了

app.use( async ( ctx ) => {
  if (ctx.method === 'GET') {
    ctx.body = {
      msg: '这是一个post请求',
      data: ctx.query // get 请求数据
    }
  }
  if (ctx.method === 'POST') {
    ctx.body = {
      msg: '这是一个post请求',
      data: parsePostData(ctx) // post 请求数据
    }
  }  
})

// 解析上下文里node原生请求的POST参数
function parsePostData( ctx ) {
  return new Promise((resolve, reject) => {
    try {
      let postdata = "";
      ctx.req.addListener('data', (data) => {
        postdata += data
      })
      ctx.req.addListener("end",function(){
        let parseData = parseQueryStr( postdata )
        resolve( parseData )
      })
    } catch ( err ) {
      reject(err)
    }
  })
}

3、koa-router中间件

有没有发现万一工程一复杂,咱们自己处理原生的路由就比较麻烦,也很低效。�咱们可以将这些麻烦的路由处理交给第三方中间件 koa-router 中间件 来处理,中间件的概念下文做介绍,你只要先知道这个 npm 包引进来就可以替咱们处理路由就行了。并且我们还用了�一个 koa-bodyparser 中间件来处理 post 请求解析请求参数

const Koa = require('koa')
const Router = require('koa-router')
const bodyParser = require('koa-bodyparser')

const app = new Koa()
const router = new Router()

// 使用ctx.body解析中间件
app.use(bodyParser())

router.get('/', async (ctx) => {
  ctx.body = 'hello koa-router'
})

router.post('/', async (ctx) => {
  // 当POST请求的时候,中间件koa-bodyparser解析POST表单里的数据,并显示出来
  const postData = ctx.request.body
  ctx.body = {
    postData
  }
})

app.use(router.routes())

app.listen(3000)
console.log('start-quick is starting http://localhost:3000/')

三、koa 中间件

上面有提到中间件的概念,那中间件是什么呢,中间件�也可以看成是过滤器,就好比水管管道,中间件就是管道上的阀门,水流过管道要经过一个或几个阀门的处理。有了中间件咱们就可以对数据的请求和响应做更多的处理了。下面咱们来写一个简单的 log 中间件

const app = new Koa()

async function logMiddleware(ctx, next) {
  console.log('url: %s , method: %s', ctx.url, ctx.method )
  await next()
}
//�引入中间件
app.use(logMiddleware)
app.use(async (ctx) => {
  ctx.body = 'hello logMiddleware'
})

可看到页面输出 'hello logMiddleware' ,同时控制台也打印出了 log 信息。
koa 中间件是一个 Promise 方法 async fn(ctx, next), ctx 代表上下文,执行 next() 会进入下一个中间件,koa 的中间件�的特征会被比喻成【洋葱模型】

image

koa 其实内部从接收请求,到输出响应的伪代码大概如下,都是�由一个个中间件处理的

new Promise(function(resolve, reject) {
  // 我是中间件1
  yield new Promise(function(resolve, reject) {
    // 我是中间件2
    yield new Promise(function(resolve, reject) {
      // 我是中间件3
      yield new Promise(function(resolve, reject) {
        // 我是body
      });
      // 我是中间件3
    });
    // 我是中间件2
  });
  // 我是中间件1
});

四、项目框架搭建

有了以上的知识储备咱们就可以开始进行一个后端服务的搭建了。是的,实践出真知,我们不一定要准备好所有的知识点才开始服务的搭建,node的知识还很多,koa2 也不单单就上面说的那么点知识,但咱们可以通过搭建一套后端服务,有了一套整体框架的概念再到实际项目开发中去补缺补漏,完善知识体系,�这是我认为比较快速的一套学习方法。

项目目录结构:

 demo
  |-- controller            // 控制层
  |-- middleware            // 中间件
  |-- model                 // 数据层
  |-- mongodb               // mongodb
  |-- router                // 路由
  |    |-- index.js         // 主路由
  |    |-- user.js          // 用户路由
  |-- app.js                // 主入口
  |-- config.js             // 配置文件
  |-- ecosystem.config.js   // pm2 启动文件
  |-- start.js              // 入口

具体源码可以 �cd /koa2搭建后台应用/demo/ 查看,我下面拆出几个模块来讲

start.js

入口文件引入了 babel 使得我们的项目可以使用 es6 的语法

require('babel-core/register')();
require('babel-polyfill');
require('./app.js');

app.js

app.js 添加了一些中间件和路由,并监听了 config.port 端口

const app = new Koa();
app.use(bodyParser());
app.use(router.routes()).use(router.allowedMethods());
app.use(errorMiddleware());

app.listen(config.port, () => {
  console.log(`Server started on ${config.port}`);
});

module/user.js

数据库我们选取 MongoDB,并用了 mongoose 这个 npm 包来配合我们做数据库操作,关于 mongoose 的用法不清楚的同学可以查看官方文档或 这一篇博文

下面是我们对 user 集合(这里不应该叫做表,而是集合)的设计

const userSchema = new Schema({
    openId: String,
    nickName: String,
    avatarUrl: {type: String, default: 'http://oyn5he3v2.bkt.clouddn.com/none_aux.png'},
    gender: {type: Number, default: 1}, // 默认男
    province: String,
    city: String,
    country: String,
}, {timestamps: true})

controller/user.js

最关心的应该是控制层的设计了,控制层对接了我们接口的处理和数据库的操作

下面的控制层对应两个路由

router.get('/login/:openId', userController.getUser);
router.post('/createUser', userController.createtUser);
export const getUser = async (ctx) => {
  const {openId} = ctx.params
  if (!openId) {
    ctx.body =  _errData('openId 参数不存在')
    return
  }
  try {
    const resData = await userModel._findOpenId(openId)
    if (resData) {
      ctx.body = _successData(resData)
    } else {
      ctx.body =  _errData('用户不存在', 4000)
    }
  } catch (err) {
    console.log(err)
    ctx.body =  _errData()
  }
}

export const createtUser = async (ctx) => {
  const data = ctx.request.body
  const resData = await userModel._create(data)
  if (resData.openId) {
    ctx.body = _successData(resData)
  } else {
    ctx.body =  _errData('用户创建失败', 4000)
  }
}

一个 get 请求,一个 post 请求,做一些操作数据库和返回数据的业务操作。
细心的你肯能会发现 getUser 方法 有做 try catch 处理,而 createtUser 方法没有做此处理,这里只是给大家表现出差异,其实我们在 app.js 引入了个中间件 errorMiddleware 做了统一的 try catch 处理

middleware/index.js

这里写了个 errorMiddleware 中间件,对整个项目的异常做 try catch 处理,当然你还可以添加自己需要的中间件

export function errorMiddleware () {
  return async (ctx, next) => {
    try {
      await next();
    } catch (err) {
      console.log(err)
      ctx.body = {
        msg: '服务器错误',
        code: 5000,
        success: false,
      }
    }
  };
};

// 主入口程序使用中间件
// app.use(logMiddleware)

config.js

config.js 做了一些环境配置,我们需要区分开发环境和线上环境,有可能需要配置不一样的端口,或者数据库等,�可以在这份配置文件做配置

const common = {
  mongodb: 'mongodb://localhost:27017/demo'
}

const development = Object.assign(common ,{
  port: 3000,
  mongodb: 'mongodb://localhost:27017/development'
})

const production = Object.assign(common ,{
  port: 3001,
  mongodb: 'mongodb://localhost:27017/production'
})

let config

process.env.NODE_ENV === 'production' ? config = production : config = development

export default config

五、进程管理工具

上面的工程是搭建完了,但发现我们的工程开发效率实在太低了,特别是调试bug的时候,我们需要写完调试代码重启一下服务,费时费力。这里介绍个草鸡好用node进程管理工具 pm2

下面是一份 pm2 的配置清单:
主要的是我们配置了 log 日志输出,�并且在开发模式下我们启动了 watch 模式,这样在开发过程中不用频繁的重启服务了,再者就是我们区分了3个不同的环境 development 、 testing 、production。如果需要自己配置或修改清单可以参考网上的一些资料 pm2 gitbook

apps : [
    {
      name: 'koa2-demo',
      script: 'start.js',
      log_date_format: 'YYYY-MM-DD HH:mm:ss',
      error_file: 'server_logs/err.log',
      exec_mode: "cluster",
      out_file: 'server_logs/out.log',
      max_memory_restart: '600M', // 限制最大内存
      min_uptime: '200s', // 应用运行少于时间被认为是异常启动, 防止不断重启
      env: {
        COMMON_VARIABLE: 'true'
      },
      env_development: {
        name: 'koa2-demo-development',
        NODE_ENV: 'development',
        watch: 'true'
      },
      env_testing: {
        name: 'koa2-demo-testing',
        NODE_ENV: 'testing'
      },
      env_production : {
        name: 'koa2-demo-production',
        NODE_ENV: 'production'
      }
    }
  ]

接着配合 package.json 的 scripts 命令我们就可以这样启动 node 应用了

npm run server-dev
npm run server-test
npm run server-prod

六、部署线上

线上部署也是用到 pm2 来启动启动 node 应用,将工程打个压缩包上传到服务器,解压一下,pm2 启动

npm run server-test
npm run server-prod

别忘了需要先启动服务端的 MongoDB,好需要配个 nginx 来转发我们 node 服务的端口,下面给出一份简单的 nginx 配置

server {
    listen       80;
    server_name  api.example.com;

    location ^~/api/ {
       proxy_pass http://127.0.0.1:3001/;
    }
}

总结:

我们从介绍 koa ,到最后搭建一套简单的 node 后端服务,包括上线和部署,形成一套 node 服务体系。在这过程中我们涉及到了 mongodb 、 pm2 、nginx 配置等一下基本的后端知识体系做支撑。

当然我们学习到的只是一些应用知识,需要在不断在实践和工作中去完善知识体系

运行项目

1、获取项目分支

git clone https://github.com/ZengTianShengZ/My-Blog.git
git checkout -b demo-koa2-server origin/demo-koa2-server

2、项目构建

cd demo                // 切换至 demo 目录
npm install
node start.js

🏷你不知道的JavaScript-第二部分

接着上一篇 [你不知道的JavaScript·第一部分]

第一章: 关于this

this 到底是什么

this 是在 运行时 绑定的,this的绑定和函数声明的位置没有任何关系,只却取决于函数的 调用方式

function foo() {
  this.bar(); // this指向window,相当于 window.bar()
}
function bar() {
  console.log('---bar---');
}
foo()

上段代码在浏览器下是能正常执行的,函数 bar 能被正常调用,因为函数 bar 是声明在全局的,全局的 this 指向 window , 执行函数 foo 时也是在全局(window)下执行的,所以函数内部的 this 也是指向 window 的,相当于在函数 foo 内部 是 window.bar() ,自然能正常调用执行。

但在严格模式下 'use strict' , 全局 this 不指向 window,而是 undefined,所以在严格模式下上面代码会报错

第二章: this全面解析

1、调用位置

一个很简单的判断 this 指向谁就看函数在代码的调用位置

例子1

下面这个例子第一章节分析的一样,函数 foo 在全局调用, 在浏览器下 this 指向 wwindow,当然要是考虑严格模式, this.a 会报错 a is not defined

a = 1;
function foo() {
  console.log(this.a); // 1
}
foo();

例子2

下面例子虽然同一个函数 foo ,但却被不同的对象调用,this 分别指向它的调用者

function foo() {
  console.log(this.a);
}
var obj1 = {
  foo: foo,
  a: 1
}

var obj2 = {
  foo: foo,
  a: 2
}
obj1.foo() // 1
obj2.foo() // 2

特殊例子3

下面例子虽然函数 foo 声明在全局,但被利用 callapplybind 显式的绑定了对象,this 指向显式绑定的对象

function foo() {
  console.log(this.a);
}
var obj1 = {
  a: 1
}
var obj2 = {
  a: 2
}
var obj3 = {
  a: 3
}
foo.call(obj1) // 1
foo.apply(obj2) // 2
foo.bind(obj3)() // 3

例子4

下面例子使用了 new 绑定,this 指向了该对象

function foo(a) {
  this.a = a
}
var bar = new foo(1)
console.log(bar.a);

使用 new 来调用函数经历了以下步骤:

1、创建或者说构造一个全新的对象

2、这个新对象会被执行[Prototype]连接

3、这个新对象会被绑定到函数调用的 this

4、如果这个函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象

小结:

经过上面的例子分析,判断 this 的指向可以罗列几个点,

1、看调用者,函数被谁调用 this 指向谁

2、是否有 callapplybind 显式的改变了 this 的指向

3、函数是否是被 new 创建的,是的话 this 指向 new 出来的对象

为什么 this 的指向不固定呢,因为 JavaScript 是‘解释执行’ 语言,边编译边执行的,this 的指向也就动态的。

第三章: 对象

对象再熟悉不过了,对象的基本要素就是属性和方法,但有时候代码写着写着就忘了对象本来的特征。

1、内置对象

String

var str = 'i am a string';
str.length ;      // 13
str.charAt(3);    // m

字符串经常使用,没但发觉字符串也会有属性和方法,这个特性应该对象才有的呀。原来JavaScript 引擎自动把字符串转换成 String 对象,所以可以访问属性和方法。

Array

数组也是 JavaScript 的内置对象,那既然是对象是不是也可以像对象那样赋值和操作呢。

var arr = ['foo', 'bar'];
arr.baz = 'baz';
console.log(arr.baz);

上面例子不像以往那样操作数组,而是按对象的形式进行操作,是可行的。但这里也是为了给大家解释数组也是对象这么个基本概念,一般不将数组当做普通键值对象来使用。

内置对象除了上面提到的 String,Array,还有 Number、Boolean、Object、Function、Date、RegExp、Error

2、属性描述符

var obj = {
  a: 1
}

对象 obj 的属性 a 就单单记录一个数值 1 吗,其实不是的,咱们用 getOwnPropertyDescriptor 打印下属性 a 看下输出信息

console.log(Object.getOwnPropertyDescriptor(obj, 'a'));
// 输出
{
  value: 1, 
  writable: true,
  configurable: true,
  enumerable: true
}

发现打印出来好几个属性,这几个属性来解释一下

writable

writable 决定是否可以修改属性值

var obj = {}
Object.defineProperty(obj, 'a', {
  value: 1,
  writable: false,
  enumerable: true,
  configurable: true
})
obj.a = 2
console.log(obj.a); // 1

writable 属性置为 false 就无法更改属性的值了

configurable

configurable 属性用来描述对象的属性是否可配置,也就是 configurable = false,接下去代码想要将 configurable = true 将会报错,因为对象的属性已经不可配置了

enumerable

enumerable 属性用来设置该对象属性是否可枚举, enumerable = false 那么该对象属性将不在枚举中,也就是 for...in 将不会遍历到该属性

3、Getter和Setter

对象还有两个隐藏的函数会被忽略,因为是 JavaScript 的默认操作,没特殊用法的话就不会去修改它,如果重新修改了 Getter和Setter 隐藏函数,JavaScript会忽略它们的 value 和 writable 特性

下面一个例子对属性 a 进行 Getter和Setter 重写

var obj = {
  get a() {
    return this._a_
  },
  set a(val) {
    this._a_ = val * 2
  }
}

obj.a = 2
console.log(obj.a); // 4

因为对赋值和取值时都会触发相应的 get 和 set 方法,那就可以利用这个特性做一些比如消息的发布订阅,事件通知等高级用法了

第五章: 原型

JavaScript 对象有一个特殊的 [Prototype] 属性,几乎所有对象在创建时 [Prototype] 属性都会被赋予一个非空的值。

1、Prototype

var anotherObj = { a: 1}
var obj = Object.create(anotherObj)
console.log(obj.a);  // 1

Object.create() 会把一个对象的 [Prototype] 关联到另一个对象上,上面例子对象 obj 的 [Prototype] 被关联到了对象 anotherObj 上,当访问对象 obj 的属性 a 时,如果对象上不存在该属性那么 [Prototype] 链就会被遍历,[Prototype] 链上找到对应的属性就会返回。到哪里才是 [Prototype] 链的‘尽头’呢,[Prototype] 链最终会指向JavaScript 内置的 Object.prototype

2、"类"函数

function Foo() {
  // ...
}
var a = new Foo()

上面例子的 Foo 是一个"类"函数,或被叫做 "构造函数" ,为什么叫做"类"函数而不直接叫做"类"呢,其实 JavaScript 中只有对象,它并没有类,es6 的类也只是一个语法糖而已。

被习惯称作"类"的原因是因为 JavaScript 有个 new 操作符,所以习惯的和其他语言一样称 new 后面的函数为一个类。new Foo() 只是间接完成了一个目的:一个关联到其他对象的新对象。也就是 new 操作符的作用就是创建一个关联对象而已。

Foo 其实也只是一个函数, 没有 new 操作符, Foo() 也能正常执行,只是当且仅当使用 new 时,函数调用会变成 ‘构造函数调用’。

3、原型链

function Foo() {
  // ...
}
Foo.prototype // {}

所有的函数默认都会有一个名为 prototype 的共有并且不可枚举的属性,prototype即为函数的原型

function Foo() {
  // ...
}
var a = new Foo()
a.__proto__ // {}

所有的对象默认都会有一个名为 __proto__ 的共有并且不可枚举的属性

prototype__proto__ 有什么联系呢?

a.__proto__ === Foo.prototype

每个对象的 __proto__ 属性指向函数的原型。

因为每个对象都有 __proto__ 属性指向函数的 prototype 所以往函数的 prototype
添加属性或方法,每个对象都能访问的到

function Foo() {
  // ...
}
Foo.prototype.age = 233
var a = new Foo()
var b = new Foo()
a.age // 233
b.age // 233

4、对象关联

如果在第一个对象上没有找到需要的属性或者方法引用,JavaScript引擎会继续在 [Prototype] 关联的对象上进行查找。同理,如果在后者中也没有找到需要的引用就会继续查找它的 [Prototype] ,以此类推。这一系列对象的链接被称为 ‘原型链’ 。

‘原型链’ 在上面已经提到和解释了,这里更加深入的去理解什么是 ‘原型链’ 。

‘原型链’ 这种机制的本质其实是对象之间的关联关系:对象关联

var Obj1 = {
  name: '曾田生',
  setID: function(ID) {
    console.log(ID)
  }
}

var Obj2 = Object.create(Obj1)

Obj2.age = 233

var Obj3 = Object.create(Obj2)

console.log(Obj3.age)  // 233
console.log(Obj3.name) // 曾田生
Obj3.setID('ABCD')     // ABCD
console.log(Obj3)

我们使用 Object.create 将一个个对象 关联 起来,当访问到自身对象没有的方法或属性时,就会去它关联的对象查找,这其实就是 [Prototype] 的机制。

花了两个篇幅来整理 《你不知道的JavaScript·上卷》,希望各位看官有所收获,欢迎 star

Vue.js内部运行机制浅解

Vue.js内部运行机制浅解

一、内部流程图

image

1、初始化及挂载

在 new Vue() 之后。 Vue 会调用 _init 函数进行初始化,在 init 过程,它会初始化生命周期、事件、 props、 methods、 data、 computed 与 watch 等。其中最重要的是通过 Object.defineProperty 设置 setter 与 getter 函数,用来实现「响应式」以及「依赖收集」。

2、编译

compile编译可以分成 parse、optimize 与 generate 三个阶段,最终需要得到 render function。

parse

parse 会用正则等方式解析 template 模板中的指令、class、style等数据,形成AST。

optimize

optimize 的主要作用是标记 static 静态节点,这是 Vue 在编译过程中的一处优化,后面当 update 更新界面时,会有一个 patch 的过程, diff 算法会直接跳过静态节点,从而减少了比较的过程,优化了 patch 的性能。

generate

generate 是将 AST 转化成 render function 的过程,得到结果是 render 的字符串以及 staticRenderFns 字符串。

在经历过 parse、optimize 与 generate 这三个阶段以后,组件中就会存在渲染 VNode 所需的 render function 了。

3、响应式

响应式部分会对数据进行响应式响应和依赖收集

4、Virtual DOM

render function 会被转化成 VNode 节点。Virtual DOM 其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。

如下面的例子:

{
    tag: 'div',                 /*说明这是一个div标签*/
    children: [                 /*存放该标签的子节点*/
        {
            tag: 'a',           /*说明这是一个a标签*/
            text: 'click me'    /*标签的内容*/
        }
    ]
}

渲染后可以得到

<div>
    <a>click me</a>
</div>

5、更新视图

更新视图可不是简单粗暴的得到一个新的VNode 节点,然后用 innerHTML 直接全部渲染到真实 DOM 中,如果我们只是对视图做了一小块内容进行了修改,这样做似乎有些「浪费」。「patch」就是对更新视图做了优化。我们会将新的 VNode 与旧的 VNode 一起传入 patch 进行比较,经过 diff 算法得出它们的「差异」。最后我们只需要将这些「差异」的对应 DOM 进行修改即可。

二、响应式系统的基本原理

Vue.js就是基于 Object.defineProperty 实现「响应式系统」的.

Object.defineProperty(obj, attr, descriptor) 的参数如下

obj 为属性attr所属的对象;
attr 为obj对象新定义或者修改的属性名;

descriptor 为该对象属性的描述符,其中其有6个配置项:
value: 属性的值,默认undefined
configurable: 默认为�false,true表示当前属性是否可以被改变或者删除,其中”改变“是指属性的descriptor的配置项configurable、enumerable和writable的修改
enumerable:默认为false,true表示当前属性能否被for...in或者Objectk.keys语句枚举
writable:默认为false,true表示当前属性的值可以被赋值重写
get:默认undefined,获取�目标属性时执行的回调方法,该函数的返回值作为该属性的值
set:默认undefined,目标属性的值被重写时执行的回调

从上面的用法可以知道:可以通过设置get和set方法对属性的读取和修改进行拦截,通过�将实现数据和视图同步的逻辑置于这两个方法中,从而�实现数据变更视图也可以跟着同步

一个demo:

function cb(val) {
  /* 渲染视图 */
  console.log("视图更新啦~");
}

function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    /* 属性可枚举 */
    configurable: true,
    /* 属性可被修改或删除 */
    get: function reactiveGetter() {
      return val; /* 实际上会依赖收集,下一小节会讲 */
    },
    set: function reactiveSetter(newVal) {
      if (newVal === val) return;
      cb(newVal);
    }
  });
}

function observer(value) {
  if (!value || (typeof value !== 'object')) {
    return;
  }
  Object.keys(value).forEach((key) => {
    defineReactive(value, key, value[key]);
  });
}

class Vue {
  /* Vue构造类 */
  constructor(options) {
    // 将 data 转化为带有 get set (响应式)的  _data !!!
    this._data = options.data;
    observer(this._data);
  }
}

let o = new Vue({
  data: {
    test: "I am test."
  }
});
o._data.test = "hello,world."; /* 视图更新啦~ */

三、响应式系统的依赖收集追踪原理

1、为什么需要依赖收集

首先要明白为什么需要依赖收集

假如有这么一个 Vue 对象:

new Vue({
    template: 
        `<div>
            <span>{{text1}}</span> 
            <span>{{text2}}</span> 
        <div>`,
    data: {
        text1: 'text1',
        text2: 'text2',
        text3: 'text3'
    }
});

我们做了这么一个操作

this.text3 = 'modify text3';

我们修改了 data 中 text3 的数据,但是因为视图中并不需要用到 text3,所以理论上是不需要进行视图更新。所以我们需要对视图用到的数据进行依赖收集,当收集的数据有改变时再做视图更新。依赖收集是为了优化视图更新用的。

2、模拟依赖收集

demo:

class Dep {
  constructor() {
    /* 用来存放Watcher对象的数组 */
    this.subs = [];
  }
  /* 在subs中添加一个Watcher对象 */
  addSub(sub) {
    this.subs.push(sub);
  }
  /* 通知所有Watcher对象更新视图 */
  notify() {
    this.subs.forEach((sub) => {      
      sub.update();
    })
  }
}
Dep.target = null;

class Watcher {
  constructor() {
    /* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
    Dep.target = this;
  }
  /* 更新视图的方法 */
  update() {
    console.log("视图更新啦~");
  }
}

function cb(val) {
  console.log("视图更新啦~");
}
function defineReactive(obj, key, val) {
  /* 一个Dep类对象 */
  const dep = new Dep();
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      /* 将Dep.target(即当前的Watcher对象存入dep的subs中) */
      dep.addSub(Dep.target);
      return val; /* 实际上会依赖收集,下一小节会讲 */
    },
    set: function reactiveSetter(newVal) {
      if (newVal === val) return;
      /* 在set的时候触发dep的notify来通知所有的Watcher对象更新视图 */
      dep.notify();
      // cb(newVal);
    }
  });
}

function observer(value) {
  if (!value || (typeof value !== 'object')) {
    return;
  }
  Object.keys(value).forEach((key) => {
    defineReactive(value, key, value[key]);
  });
}

class Vue {
  constructor(options) {
    this._data = options.data;
    observer(this._data);
    /* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
    new Watcher();
    /* 在这里模拟render的过程,为了触发test属性的get函数 */
    console.log('render~', this._data.test);
  }
}

let o = new Vue({
  data: {
    test: "I am test.",
    test2: "I am test2."
  }
});
o._data.test = "hello,world.";   // 视图更新啦~
o._data.test2 = "hello,world.";  // 视图没触发更新

相比第二节我们新添加了个 Dep 类,用于收集依赖用的,新添加了个 Watcher 类,用于数据变化更新视图用的。

上面 demo 最主要的就是触发依赖收集这一步了

/* 在这里模拟render的过程,为了触发test属性的get函数 */
console.log('render~', this._data.test);

这里用获取数据的方式来触发对应的 get函数,接着在 get函数里我们往 Dep 对象的 subs 属性添加了个 Watcher 对象,用于后续的视图更新操作

get: function reactiveGetter() {
  /* 将Dep.target(即当前的Watcher对象存入dep的subs中) */
  dep.addSub(Dep.target);
  return val;
},

四、Virtual DOM 的一个 VNode 节点

Virtual DOM 其实就是一棵以 JavaScript 对象(VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。

比如这么一个 Vue 组件:

<template>
  <span class="demo" v-show="isShow">
    This is a span.
  </span>
</template>

用 JavaScript 代码形式就是这样的:

function render() {
  return new VNode(
    'span', {
      /* 指令集合数组 */
      directives: [{
        /* v-show指令 */
        rawName: 'v-show',
        expression: 'isShow',
        name: 'show',
        value: true
      }],
      /* 静态class */
      staticClass: 'demo'
    }, [new VNode(undefined, undefined, undefined, 'This is a span.')]
  );
}

看看转换成 VNode 以后的情况:

{
  tag: 'span',
  data: {
      /* 指令集合数组 */
      directives: [
          {
              /* v-show指令 */
              rawName: 'v-show',
              expression: 'isShow',
              name: 'show',
              value: true
          }
      ],
      /* 静态class */
      staticClass: 'demo'
  },
  text: undefined,
  children: [
      /* 子节点是一个文本VNode节点 */
      {
          tag: undefined,
          data: undefined,
          text: 'This is a span.',
          children: undefined
      }
  ]
}

其中转换函数如下:

class VNode {
  constructor(tag, data, children, text, elm) {
    /*当前节点的标签名*/
    this.tag = tag;
    /*当前节点的一些数据信息,比如props、attrs等数据*/
    this.data = data;
    /*当前节点的子节点,是一个数组*/
    this.children = children;
    /*当前节点的文本*/
    this.text = text;
    /*当前虚拟节点对应的真实dom节点*/
    this.elm = elm;
  }
}

既然 Virtual DOM 能用 js 来生成和表示,自然还可以多做一些操作,比如

  • 创建一个空节点
function createEmptyVNode () {
  const node = new VNode();
  node.text = '';
  return node;
}
  • 创建一个文本节点
function createTextVNode (val) {
  return new VNode(undefined, undefined, undefined, String(val));
}

等等节点的一系列 CRUD

五、Compile 编译 template 模板

这一步对应文章刚开始的流程图中的 2、编译 部分。

compile 编译可以分成 parse、optimize 与 generate 三个阶段,最终需要得到 render function

下面以解析一个 template 片段为例来讲解 compile 过程:

<div :class="c" class="demo" v-if="isShow">
    <span v-for="item in sz">{{item}}</span>
</div>

1、parse

parse 会用正则等方式将 template 模板中进行字符串解析,得到指令、class、style等数据,形成 AST(抽象语法树(abstract syntax tree或者缩写为AST))

比如上面的 template 片段会被解析为:

{
  /* 标签属性的map,记录了标签上属性 */
  'attrsMap': {
    ':class': 'c',
    'class': 'demo',
    'v-if': 'isShow'
  },
  /* 解析得到的:class */
  'classBinding': 'c',
  /* 标签属性v-if */
  'if': 'isShow',
  /* v-if的条件 */
  'ifConditions': [
    {
      'exp': 'isShow'
    }
  ],
  /* 标签属性class */
  'staticClass': 'demo',
  /* 标签的tag */
  'tag': 'div',
  /* 子标签数组 */
  'children': [
    {
      'attrsMap': {
          'v-for': "item in sz"
      },
      /* for循环的参数 */
      'alias': "item",
      /* for循环的对象 */
      'for': 'sz',
      /* for循环是否已经被处理的标记位 */
      'forProcessed': true,
      'tag': 'span',
      'children': [
          {
              /* 表达式,_s是一个转字符串的函数 */
              'expression': '_s(item)',
              'text': '{{item}}'
          }
      ]
    }
  ]
}

parse 基本方案是用正则匹配,至于具体的解析过程就不分析了,太麻烦太复杂了。。。

2、optimize

optimize 主要作用就跟它的名字一样,用作「优化」,optimize 过程就是对第 1 步 parse出的节点做标记,标记出一些静态节点,为了后面节点 diff 做优化,节省性能。

标记后的节点如下,每个节点都会加上 static 属性, static=true 为静态节点,当节点有 v-if, data 等属性就会被标记为非静态节点 static=false

{
    'attrsMap': {
        ':class': 'c',
        'class': 'demo',
        'v-if': 'isShow'
    },
    'classBinding': 'c',
    'if': 'isShow',
    'ifConditions': [
        'exp': 'isShow'
    ],
    'staticClass': 'demo',
    'tag': 'div',
    /* 静态标志 */
    'static': false,
    'children': [
        {
            'attrsMap': {
                'v-for': "item in sz"
            },
            'static': false,
            'alias': "item",
            'for': 'sz',
            'forProcessed': true,
            'tag': 'span',
            'children': [
                {
                    'expression': '_s(item)',
                    'text': '{{item}}',
                    'static': false
                }
            ]
        }
    ]
}

3、generate

generate 会将 AST 转化成 render funtion 字符串,最终得到 render 的字符串以及 staticRenderFns 字符串。

function generate (rootAst) {
    const code = rootAst ? genElement(rootAst) : '_c("div")'
    return {
        render: `with(this){return ${code}}`,
    }
}

generate 函数是将我们上面生成好的 AST(抽象语法树)作为入参,最后返回一个 render 字符串。

其中 render 字符串里面有个js关键字 with,with 用于指定作用域用的。 那返回的 render 属性是个字符串怎么执行呢, js 有个 eval() 函数 可将字符串当做脚本来执行。

可以结合上面那几个步骤 parse、optimize 来分析 generate 的工作大致过程是这样的:

image

流程图最后一步的 with 函数的 _c,_l 到底是什么?其实他们是 Vue.js 对一些函数的简写,比如说 _c 对应的是 createElement 这个函数。执行 with 函数会返回 Virtual DOM,这个放在下一节讲。

其中 generate 函数里值得一提的是对 vue 指令 v-if 、v-for 的解析

genIf

function genIf (el) {
    el.ifProcessed = true;
    if (!el.ifConditions.length) {
        return '_e()';
    }
    return `(${el.ifConditions[0].exp})?${genElement(el.ifConditions[0].block)}: _e()`
}

genFor

function genFor (el) {
    el.forProcessed = true;

    const exp = el.for;
    const alias = el.alias;
    const iterator1 = el.iterator1 ? `,${el.iterator1}` : '';
    const iterator2 = el.iterator2 ? `,${el.iterator2}` : '';

    return `_l((${exp}),` +
        `function(${alias}${iterator1}${iterator2}){` +
        `return ${genElement(el)}` +
    '})';
}

六、diff 及 patch 机制

patch 机制对应文章开头内部流程图的第5点 视图更新机制。vue有一套高效的视图更新机制,也就是 patch 的核心算法 diff 算法。diff 算法的过程就是两个新老 VNode 节点的比较过程。

由于这部分内容过去复杂,后续研究...

总结

本篇粗略分析了下 【Vue.js内部运行机制】,有些知识点讲的太笼统,但有了这么一个大概的思维框架,再针对各个部分学习,相信会更加得心应手。

咨询

Hello , 你好, 是否有兴趣考虑新的工作机会, 我们是阿里巴巴新零售事业群-CBU体验技术团队, 如有兴趣,可加微信聊聊哈, 或者交个朋友【basen2013】

🏷设计模式-预备知识

声明: 【JavaScript设计模式】 系列 是来自《JavaScript设计模式与开发》这本书的读书笔记,会结合自身的理解和一些项目经验做笔记,原书作者 曾探

前言

什么是设计模式,设计模式就是在某种场合下对某个问题的一种解决方案。说通俗一点就是给一段代码起个名字而已,比如一个用玻璃做的能装水的东西,给它起个名字叫水杯
大家一提到水杯就知道什么对象,具有什么功能,这样大家一想到要喝水就找个水杯这么个东西,满足需求

一、面向对象的JavaScript

1、动态类型语言

JavaScript是一门典型的动态类型语言。记住这一特性很重要,比如js有个数组类型Array,要是一个对象,这个对象具有length属性,还拥有slic,splice方法,那也可以将这个对象当做数组来用。这完全是可以的,因为语言是动态的,我们可以面向接口编程,而不是面向实现编程。

2、多态

也正因为js是动态类型语言,所以js本身具有多态的特性。
什么是多态呢:多态就是同一操作作用于不同对象,产生不同的结果
例子:

function fun(obj) {
    obj.log()
}
function Obj1() {

}
Obj1.prototype.log = function () {
    console.log('....Obj1......')
}
function Obj2() {

}
Obj2.prototype.log = function () {
    console.log('....Obj2......')
}
fun(new Obj1()) // ....Obj1......
fun(new Obj2()) // ....Obj2......

给函数 fun 传递不同对象,能得到不同的结果
这要是在Java就要设计成子类继承同一个父类,调用的时候向上转型才能调用同一个父类方法得到不同结果,多麻烦呀
JavaScript是一门动态语言,既没有检查创建的对象类型,也没有检查传递的参数类型,所以实现
多态就变得尤为简单,不必诸如向上转型的技术来实现多态

3、封装

JavaScript是没有私有变量和共有变量或共有方法的,但它有个函数作用域,我们可以利用这一特性来满足这一点
例子:

var fun2 = (function () {
    var _name = 'my name is 田生'
    return {
        name: _name,
        pubFun: function () {
            console.log('.....pubFun.....')
        }
    }
})()
console.log(fun2.name)    // my name is 田生
console.log(fun2.pubFun()) //.....pubFun.....

4、JavaScript中的原型继承

JavaScript的对象不是通过实例化得到的,而是通过克隆原型对象Object.prototype得到的。
所以经常会预定一个面试题是,js 的 new 操作做了哪些工作?那你可以这样回答,new 操作符是
执行一个构造函数,函数里面克隆了 Object.prototype 对象,并把新对象附上属性值返回出去。
例子:

function Obj() {
    this.name = '田生'
}
var o1 = new Obj()
console.log(o1.name) // 田生
// 相当于
function CloneObj() {
    var obj = new Object() // 从 `Object.prototype` 克隆一个新对象
    obj.name = '田生'
    return obj
}
var o2 = CloneObj()
console.log(o2.name)  // 田生

5、JavaScript中的原型琏

如果对象无法响应某个请求时,它会把这个请求委托给它的构造器的原型prototype去执行
例子:

function Obj() {
    this.name = '田生'
}
var o1 = new Obj()
console.log(o1.toString()) // [object Object]
console.log(o1)

上面例子克隆了个新的 对象Obj ,新对象没有 toString 方法,所以在调用 toString 方法时,这个请求就会委托给他的原型对象 Objectprototype去执行
那你可能会疑惑,对象 Obj对象 是怎么和 Objectprototype对象挂上钩的呢?
你可以试着在Chrome浏览器的开发者模式中输入上面代码,试着打印 console.log(o1)
会发现 Obj 有个 proto 的属性指向了 Object对象,这就是它们连接的纽带

image

二、this、call 和 apply

1、this

当函数作为对象的方法调用时,this 指向改对象
例子:

var Obj = {
    name: '田生',
    fun: function () {
        console.log(this.name)  // this 指向改 对象
    }
}
Obj.fun()  //  田生

当函数不作为对象的属性被调用时,也就是常说的普通方法,此时this指向全局对象

var name = '田生....2'
function fun1() {
    console.log(this.name) // fun1 当做普通函数,this指向全局
    function fun2() {
        console.log(this.name)  // fun2 也当做普通函数,this指向全局
    }
    fun2() // '田生....2'
}
fun1() // '田生....2'

fun2 函数的this为什么也指向全局呢?有必要再强调一下,当函数不作为对象的属性被调用时,也就是常说的普通方法,此时this指向全局对象!
当然在 ES6 strict 模式下 this为undefined

2、call 和 apply

首先要明确一点,JavaScript 的 Function 实际上是功能完整的对象。那对象就可以调用方法。
所以在看到下面的例子就不要疑惑:

function fun(para) {
}
fun.length
fun.apply(null, ['田生'])
fun.call(null, '田生')
fun.toString()

为什么一个函数有属性呢,为什么一个函数居然可以调用另一个方法呢,因为JavaScript 的 Function 实际上是功能完整的对象啊,对象就有属性和方法啊

(1)、call 和 apply 的区别

没啥区别,就接受参数的方式不一样而已,apply 接受的第二个参数是集合,call接受的参数不固定用逗号隔开。
但可以说 call 是包装在 apply 的语法糖,内部实现也是将参数转数组的形式,所以某种意义上讲
apply的效率高一点。

(2)、call 和 apply 的用途

改变this的指向:

function Obj() {
  this.name = 'HI 田生~'
  function fun() {
      console.log(this.name) // undefined
  }
  fun()
}
new Obj()

上面小节也讲了,fun 没绑定到对象上,所以在这里被当做普通函数使用,this指向全局对象,那咱们要
this.name正常输出怎么办:

// 方法一:传统的 _this 传递
function Obj() {
  this.name = 'HI 田生~'
  var _this = this
  function fun() {
      console.log(_this.name) // HI 田生~
  }
  fun()
}
new Obj()

// 方法二:借助 apply 或 call
function Obj() {
  this.name = 'HI 田生~'
  function fun() {
      console.log(this.name) // HI 田生~
  }
  fun.apply(this)
}
new Obj()

哪种方法好用我就不多说了

借用其他对象方法

var arr = []
Array.prototype.push.apply(arr,[1, 2, 3])
console.log(arr)  // [1, 2, 3]

三、闭包和高阶函数

1、闭包

闭包这个概论总是不好理解,你可以简单的理解为 闭包就是能够读取其他函数内部变量的函数
例子:

var func = function () {
    var a= 1
    return function () {
        a++
        console.log(a)
    }
}
var fun = func()
fun() // 2
fun() // 3
fun() // 4

变量 a 是函数 func 的局部变量,外部函数或对象无法访问,但 func 内部的匿名函数能访问,那就这个匿名函数就是一个闭包 ,将闭包返回出去,相当于将访问权给了外部环境,外部环境就可以访问一个函数的
局部变量了。

2、闭包的作用

上面第一小点顺带讲了闭包能使外部环境访问局部变量,是作用点之一。闭包还可以延续局部变量的寿命
例子:

// 方式1
var report = function (src) {
    var img = new Image()
    img.src = src
}
report('http://wwww.tiansheng.logo.png')

// 方式2
var report = (function () {
    var img
    return function (src) {
        img = new Image()
        img.src = src
    }
})()
report('http://wwww.tiansheng.logo.png')

利用方式1有可能图片还没加载完数据就丢失了,因为 report 方法执行完局部变量就销毁了,而方法2
利用闭包的方式延长了变量的寿命

3、高阶函数

高阶函数至少需要满足以下条件之一:

  • 函数可以作为参数传递
  • 函数可以作为返回值输出

我们经常写的带有对调函数就是一个高阶函数
例子:

/**
 * 高阶函数
 * @param name
 * @param callBack
 */
function fun(name, callBack) {
    // do something ...
    callBack()
}

函数作为返回值输出,其实就是一种闭包的表现
下面一个单例模式的例子:

var getSingle = function (fn) {
    var ret;
    return function () {
        return ret || (ret = fn.apply(this, arguments))
    }
}

var getScript = getSingle(function () {
    // ...
})
var script1 = getScript()
var script2 = getScript()
console.log(script1 === script2) // true

小结:

本小结写了 【面向对象的JavaScript】、【this、call 和 apply】、 【闭包和高阶函数】 ,为接下去的JavaScript 设计模式做铺垫

前端模块化

前端模块化

1、石器时代

  • 以前我们是这么写代码的:
function foo(){
    //...
}
function bar(){
    //...
}

都知道这样会造成变量的全局污染,变量名冲突

  • 后来我们做了改进:
var Module = (function($){
    var _$body = $("body");     // we can use jQuery now!
    var foo = function(){
        console.log(_$body);    // 特权方法
    }
    // Revelation Pattern
    return {
        foo: foo
    }
})(jQuery)

Module.foo();

利用匿名闭包的模式包裹变量,并暴露一些公共方法,还可以引入依赖 jQuery,
也就是所谓的 模块模式 , 是现代模块实现的基石

  • 虽然做了模块化封装,但还不够

现实项目中我们往往需要加载多个脚本

   script(src="zepto.js")
   script(src="jhash.js")
   script(src="fastClick.js")
   script(src="iScroll.js")
   script(src="underscore.js")
   script(src="handlebar.js")
   script(src="datacenter.js")
   script(src="deferred.js")
   script(src="util/wxbridge.js")
   script(src="util/login.js")
   script(src="util/base.js")
   script(src="util/city.js")
   script(src="util/date.js")
   script(src="util/cookie.js")
   script(src="app.js")

项目脚本的加载,弊端是
难以维护,需要顺序执行,依赖模糊,请求过多

2、模块时代的到来 : CommonJS 规范

跳出浏览器,CommonJS 是以在浏览器环境之外构建 JavaScript 生态系统为目标而产生的项目,
比如在服务器 node 环境中

模块的定义和使用

// math.js
exports.add = function(a, b) {
  return a + b;
};
-----------------------------------
// main.js
var math = require('./math.js');
math.add(111,222); // 333
------------------------------------
// node  main.js  执行

或许你可能在别的地方看到 module.exports 的方式来导出模块,module.exports 是什么玩意儿?
exports 只是 module.exports 的辅助方法,exports 所做的事情是收集属性,可以把 exports 看成一个
空对象 exports.add = function(a, b) {} 就是对象上定义 add() 方法,最终把收集的属性赋值
module.exports ,如果文件中存在 module.exports 赋值,那么将会忽略掉 exports收集的属性

例子:

// math2.js
var math2 = function() {
  this.add = function(a, b) {
    return a + b;
  };
  this.minus = function(a, b) {
    return a - b;
  };
};
// add方法 会被忽略
exports.add = function(a, b) {
  return a + b;
};
module.exports = new math2();
-----------------------------------
// main.js
var math2 = require('./math2.js');
math2.add(444,111); // 555

3、浏览器端的模块 :AMD、CMD

CommonJS 规范不适合浏览器开发, CommonJS 规范的模块加载是同步的,阻塞的,而浏览器通过网络加载模块,网速不行的话就将阻止模块的加载以及后面功能的运行。但服务端使用CommonJS 规范为什么就行呢?

因为服务端加载脚本是从磁盘硬件上读取的,下图可以看出:
同步加载对 服务器/本地环境 不是问题,浏览器环境才是问题

image

(1)、浏览器模块化方案: AMD

例子看项目目录: /前端模块化/AMD

RequireJS 是一个工具库,是 AMD规范 (Asynchronous Module Definition)的实现者。或许会把 RequireJSAMD规范混为一谈,AMD只是一种规范,定义异步模块的加载规则,而 RequireJS 是脚本代码,这套异步加载规则的实现代码。
下面一个例子利用 RequireJS 来实现 AMD规范

index.html

引入 require.js 设置 data-main 属性(用来指定网页程序的主模块),该属性会去加载对应目录下的 main.js

<script src="./lib/require.js" data-main="./main"></script>

main.js

require.config 配置需要加载的模块名和对应的加载路径

require.config({
    paths: {
        "jquery": "./lib/jquery.min",
        "math": './src/math',
        "add50": './src/add50'
    }
});

require(['jquery','math'], function ($,math){
    console.log('start main.js ...✈️')
    console.log(math.add(1,1))
    $('#j_ptext').css('color','red')
});

其中对模块的加载和执行顺序官方有做一下解释

The RequireJS syntax for modules allows them to be loaded as fast as possible,
even out of order, but evaluated in the correct dependency order,
and since global variables are not created,
it makes it possible to load multiple versions of a module in a page.

意思是模块是异步加载且不按顺序的,例如上面的 'jquery','math' 模块不必按书写顺序依次加载,但
执行顺序是按书写顺序来的,也就是 'jquery','math' 模块加载完,jquery 模块先执行

math.js

define 定义一个待加载的模块

define(function (){
    console.log('start math.js ...🚘')
    var add = function (x,y){
        return x+y;
    };
    return {
        add: add
    };
});

如果定义的模块有依赖其他模块,添加第一个形参为模块名,如下面模块依赖了math模块

define(['math'],function (math){
    var add50 = function (x){
        return math.add(x, 50)
    };
    return {
        add50: add50
    };
});

可以打开 Chrome 控制台,查看 Network 目录,看的出这些模块是通过 GET 网络请求按需加载的。而且 Type 类型是 script ,相当于加载一段脚本代码,脚本代码加载完会立即执行,这一点可以等下和 CMD规范 做过比较

image

(2)、浏览器模块化方案: CMD

例子看项目目录: /前端模块化/CMD

CMD规范(Common Module Definition)同样也有对应的实现代码,那就是SeaJS ,SeaJS的作者是淘宝前端大牛玉伯,SeaJS 定义模块的风格跟 CommonJs 比较像

例子:

index.html

引入 sea.js ,定义 config 并配置入口文件 seajs.use

<script src="./lib/sea.js"></script>
<script type="text/javascript">
    // seajs 的简单配置
    seajs.config({
        base: "./",
        alias: {
            "jquery": "lib/jquery.min.js"
        }
    })
    // 加载入口模块
    seajs.use("./main")
</script>

main.js

// 所有模块都通过 define 来定义
define(function(require, exports, module) {
    console.log('start main.js ...✈️')
    // 通过 require 引入依赖
    require('jquery');
    $('#j_ptext').css('color','red')

    var math = require('./src/math');
    console.log(math(2,9))
});

math.js

define(function(require, exports, module) {
    console.log('start math.js ...🚘')
    var add = function (x,y){
        return x+y;
    };
    // 或者通过 module.exports 提供整个接口
    module.exports = add
});
浏览器模块化方案 小结:

为什么会出现 AMD规范 和 CMD规范 呢,出现就有他们的道里,下面对 AMD规范 和 CMD规范 做个比较,
或者说是两种规范的实现方式进行比较
SeaJs 和 RequireJS 的异同:下面是 玉伯 在知乎的回答,查阅详情的点击👇链接

作者:玉伯
链接:https://www.zhihu.com/question/20342350/answer/14828786
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

再补充一点异同是 SeaJS对模块的态度是懒执行, 而RequireJS对模块的态度是预执行 链接专业术语较多,补充的这一点是两种模块最主要的区别之一
RequireJS 是这么去加载依赖的,及所谓的依赖前置,导致的结果就是例子中的依赖 jquerymath 会先加载并执行,接着猜执行
console.log('start main.js ...✈️')

require(['jquery','math'], function ($,math){
    console.log('start main.js ...✈️')
});

而 SeaJS 是依赖后置,它会先执行 console.log('start main.js ...✈️') 再去加载依赖,接着再执行依赖

define(function(require, exports, module) {
    console.log('start main.js ...✈️')
    // 通过 require 引入依赖
    require('jquery');
});

4、UMD : 统一写法

既然CommonJs和AMD风格一样流行,似乎缺少一个统一的规范。所以就产生了这样的需求,希望有支持两种风格的“通用”模式,
于是通用模块规范(UMD)诞生了。
下面这种写法它兼容了AMD和CommonJS,同时还支持老式的“全局”变量规范:

JavaScript

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD
        define(['jquery', 'underscore'], factory);
    } else if (typeof exports === 'object') {
        // Node, CommonJS之类的
        module.exports = factory(require('jquery'), require('underscore'));
    } else {
        // 浏览器全局变量(root 即 window)
        root.returnExports = factory(root.jQuery, root._);
    }
}(this, function ($, _) {
    //    方法
    function a(){};    //    私有方法,因为它没被返回 (见下面)
    function b(){};    //    公共方法,因为被返回了
    function c(){};    //    公共方法,因为被返回了

    //    暴露公共方法
    return {
        b: b,
        c: c
    }
}));

5、王者归来 :ES6 module

有了 UMD 来统一规范,但始终不是正规军,缺乏官方支持。

ES6 统一了模块规范

// math.js
export default math = {
    PI: 3.14,
    foo: function(){}
}
// app.js
import math from "./math";
math.PI

but ,,这种模块化方式太先进了,浏览器不支持 😢 , 好在有了 bable ,配合 webpack 完美解决前端模块化

6、送你上天 :webpack 模块打包器(module bundler)

(1)、基础

webpack 的基础配置就不写了,可以稍微看一下经过下面 loader 处理后文件的输出情况

test: /\.js$/,
use: {
    loader: "babel-loader",
     options: {
          presets: [
              "es2015"
          ]
     }
},

其中需要 loader 的 app.js 只有一个log

// app.js
console.log('.....app.js.......');

输出(内容较多,伪代码如下):

(function (modules) {
    function __webpack_require__(moduleId) {
    	modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    }
    return __webpack_require__(__webpack_require__.s = 0);
})([
    function(module, exports, __webpack_require__) {
        "use strict";
        console.log('.....app.js.......');
    }
])

将模块打包成一个 function ,再利用 js 立即执行函数 去执行 function模块

(2)、打包各种规范模块

用 webpack 分别打包 AMD 、CMD 、commonjs 和 es6 方式 export 出的模块

下面只是代码片段,例子请看项目 /前端模块化/Webpack/demo1

import amd from './src/module_amd'
import cmd from './src/module_cmd'
import commonjs from './src/module_commonjs'
import es6 from './src/module_es6'

console.log(amd)        // object
console.log(cmd)        // object
console.log(commonjs)   // object
console.log(es6)        // object

amd.add(11,22)        //33
cmd.add(11,22)        //33
commonjs.add(11,22)   //33
es6.add(11,22)        //33

看出无论用哪种方式定义模块,都能利用 import 进行模块的引入,这得力于 bable-loader,让
我们在写代码时统一了规范

接下去几点是 webpack 结合实际项目,对前端模块化的优化处理

(3)、对模块进行提取 CommonsChunkPlugin

有时候我们往往会对一些公共模块或工具方法提取成一个单独的模块,而不是都打到一个包里面,每个页面都加载这么大一个包显然是影响性能的。对于这个需求我们可以用 CommonsChunkPlugin 插件来实现,CommonsChunkPlugin相关配置如下

  • 情况1
entry: {
     app: './app.js'
},

new webpack.optimize.CommonsChunkPlugin({
     name: 'vendor',       // 上面 entry 入口定义的节点组
     filename:'vendor.js'  //最后生成的文件名,随意
}),

当 webpack 的 entry 入口只有一个文件的时候,利用 CommonsChunkPlugin 提取出的只是
webpack的运行文件

  • 情况2
entry: {
     app: './app.js',
     vendor: ['./src/a.js'] // 指定公共模块
},

new webpack.optimize.CommonsChunkPlugin({
     name: 'vendor',       // 上面 entry 入口定义的节点组
     filename:'vendor.js'  //最后生成的文件名,随意
}),

当 webpack 的 entry 入口有多个时,CommonsChunkPlugin 的 name 参数指向 entry 对应的 key,
key 指向的文件会被全局提出出来,并和webpack的运行文件打成一个 vendor 包

更多情况可点击链接 [☞ 链接](https://segmentfault.com/q/1010000009070061/a-1020000009073036)

(4)、 .babel 配置

我们经常将 webpack 下对 babel-loader 的 options 配置单独提取出来配置到 .babelrc 文件下

{
    test: /\.js$/,
    use: {
         loader: "babel-loader",
      // options: {  // 此处配置可以写到 .babelrc 文件下
      //     presets: [
      //         "es2015"
      //     ]
      // }
    },
    exclude: /node_modules/
}

如下文件

{
  "presets": [
    "es2015",  // ES2015转码规则  babel-preset-es2015
    "stage-2"  // ES7不同阶段语法提案的转码规则(共有4个阶段),选装一个  babel-preset-stage-2
  ],
  "plugins": ["transform-remove-console"]
}

这里简要解释一下 presets 和 plugins 参数。plugins 就是配置语法转换的插件,比如
对 ES6 转换的插件有:

transform-es2015-destructuring // 编译解构赋值
transform-es2015-arrow-functions // 编译箭头函数
transform-regenerator // 编译generator函数
// ... 等等

那需要对项目做 ES6 语法的转换是不是就应该对 .babelrc 文件 这边配置呢

{
  "plugins": ["transform-es2015-destructuring",
               "transform-es2015-arrow-functions",
               "transform-regenerator",
               "..."]
}

这多麻烦呀,所以也就有了 presets(预设) 配置,如下面配置就将 ES6 转 ES5 ,
也就可以看出了,preset 是一系列 plugin 的集合

{
  "presets": [
    "es2015",  // ES2015转码规则  babel-preset-es2015
  ]
}

回过头来解释一下下面 .babelrc 配置的意思
对 js 进行 "es2015" 的语法转换,stage-x 代表着支持es6哪个阶段的语法,并添加一个将
转换后的代码进行 console 的 remove 操作,也就是插件 "plugins": ["transform-remove-console"]
的作用啦

// webpack 1.x
{
  "presets": [
    "es2015",  // ES2015转码规则  babel-preset-es2015
    "stage-2"  // ES7不同阶段语法提案的转码规则(共有4个阶段),选装一个  babel-preset-stage-2
  ],
  "plugins": ["transform-remove-console"]
}

不过还有一点是,上面的写法是配合 webpack 1.x 的写法,现在 webpack 2.x 的写法如下
(提醒:需要多装个npm包 npm i babel-preset-env--save-dev

// webpack 2.x
{
  "presets": [
    ["env", {
        "modules": false,
        "targets": {
        "chrome": 52,
        "browsers": ["last 2 versions","safari 7"]
        }
    }],
    "stage-2"
  ]
}

也就是不配置 "es2015" 选项了,而是通过 env 配置,动态指定 js 转化的版本,而不是固定写死 "es2015",
因为有的项目不需要将 js 转化到 "es2015" 这么低的版本,而是通过你项目需要支持的浏览器版本就行,比如
"chrome": 52 等。 "modules": false 意思是不使用 bable 语法对AMD、CommonJS、UMD之类的模块进行
转化,而是用 webpack2.x 已经把这个事情做了

(5)、Tree Shaking 对模块方法进行按需加载

有这么一个文件

// src/math.js
export function square(x) {
  return x * x;
}

export function cube(x) {
  return x * x * x;
}

下面文件只引用 math.js 下的 cube 方法

// index.js
+ import { cube } from './math.js';
cube(10)

打包后: 发现 square 方法实际上是没被用到的,但却被打包进来了

/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* unused harmony export square */
/* harmony export (immutable) */ __webpack_exports__["a"] = cube;
function square(x) {
  return x * x;
}

function cube(x) {
  return x * x * x;
}

优化:
webpack 2.0 集成了 tree shaking 功能,用于移除 JavaScript 上下文中的未引用代码(dead-code),
但我们还需要做些配置才能达到这效果:
对 .babelrc 加入 {'modules': false} 配置

presets: ['es2015', {'modules': false}]

对 webpack.config.js 加入 UglifyJSPlugin 配置

const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
plugins: [
   new UglifyJSPlugin()
]

接着再次打包就看到 代码不仅被压缩,且也没有 square 相关方法了

(6)、webpack-bundle-analyzer 模块包树状图

webpack-bundle-analyzer
可以用于 webpack 打包后对各个模块进行图形分析,查看 chunk 设置的是否合理等

image

再附上一篇实战篇 ☞ 链接

小结:

前端模块化的前世今生就分析到这里。Brendan Eich(JavaScript的作者)花了10天时间创造出了 JavaScript,由于设计时间太短,语言的一些细节考虑得不够严谨,也没有模块化这么一说,才有了后来的一些开发人员为符合工程化需求定义了一些模块化规范,但始终不是官方出品,好在有了 ES6 ,统一了模块化规范

理解 HTTP 2.0

理解 HTTP/2

概念:

HTTP/2(超文本传输协议第2版,最初命名为HTTP 2.0),
简称为h2(基于TLS/1.2或以上版本的加密连接)或h2c(非加密连接)

条件

需要浏览器的支持,目前最新版的 Chrome、Opera、 FireFox、 IE11、 edge 都已经支持了
需要 WEB 服务器的支持,比如 Nginx , H20
如果浏览器或服务器有一方不支持,那么会自动变成 Http/1.1

一、http 的发展史

要了解 http2.0 首先就需要了解一下 http 的发展史 ,才能明白 http2.0 是基于什么目的提出的,有什么用

1、HTTP/1.0时代

HTTP1.0规定浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,
服务器完成请求处理后立即断开TCP连接,服务器不跟踪每个客户也不记录过去的请求。但这样在每次请求资源的
时候都要重新建立tcp连接,走3次握手的过程,严重影响客户机和服务器的性能。

2、HTTP/1.1时代

为了解决 HTTP1.0 的不足,HTTP1.1做了些改进。

(1)、增加了「keep-alive」 功能

浏览器默认开启keep-alive来告知服务器建立一个长连接,当浏览器建立一个 TCP连接时,多个请求都会使用这个连接,这样就避免了重复建立连接带来的性能消耗问题

(2)、增加 PipeLining 管道

一个 TCP 连接只能发送一个请求,并且等响应成功才能发送第二个请求。所以 http/1.1 定制了 PipeLining 管道 ,通过这个管道浏览器可以同时发送多个请求给服务器,但服务器的响应也只能一个接一个返回(但各地浏览器都不支持这个管道或默认关闭,若有然并软)

小结:

介于 HTTP/1.1 的网络特性
所以我们经常做的网页优化有以下方式:
拼接 js , css 减少http请求
利用雪碧图减少http请求
域名分区,由于浏览器的限制,同一个域下最多只能建立6个连接,所以资源放在多个域名下来突破浏览器的域名限制

3、SPDY 时代

虽然 http/1.1 做了优化,但也扛不住日益丰富的网页,于是 google 推出了一种开放的网络传输协议 SPDY
设计SPDY的目的在于降低网页的加载时间[7]。通过优先级和多路复用,SPDY使得只需要创建一个TCP连接即可传送网页内容及图片等资源
SPDY并不用于取代HTTP,它只是修改了HTTP的请求与应答在网络上传输的方式[1];这意味着只需增加一个SPDY传输层,现有的所有服务端应用均不用做任何修改。 当使用SPDY的方式传输,HTTP请求会被处理、标记简化和压缩。比如,每一个SPDY端点会持续跟踪每一个在之前的请求中已经发送的HTTP报文头部,从而避免重复发送还未改变的头部。而还未发送的报文的数据部分将在被压缩后被发送。

(1)、多路复用 (multiplexing)

通过多个请求 stream 共享一个 tcp 连接

(2)、请求优先级

多路复用会带来一个问题,那就是哪些资源先请求能,防止重要资源阻塞呢,那就是有个 优先级的机制,SPDY允许给每个 request设置优先级

(3)、header 压缩

SPDY对header的压缩率可以达到80%以上,低带宽环境下效果很大。

SPDY 现已经被大多数浏览器以及 WEB 服务器所支持,但为了推进 HTTP/2.0, Google 已经宣布在 2016年对其停止开发。

4、HTTP/2.0 时代

终于到了HTTP/2.0时代,HTTP/2是基于Google的SPDY协议为基础开发的新的web协议,所以多大理论差不多

(1)、二进制分帧

在应用层和传输层之间加了一个【二进制分帧】,这样就可以在不改动 HTTP方法,状态码,URI等字段的情况下
突破 HTTP/1.1 的性能限制,改进传输性能,实现低延迟,高吞吐量

image

在二进制分帧层上,HTTP2.0会将所有传输的信息分割为更小的消息和帧,
并对它们采用二进制格式的编码,其中HTTP1.x的首部信息会被封装到Headers帧
,而我们的request body则封装到Data帧里面。

(2)、压缩头部

http2.0 规定 客户发和服务端会共同维护首部表,对相同的头部只发送一次,不再重复发送,这样能减少头部开销,特别是有轮询操作。
如果头部发生了变化,那只需发送变化了数据在 headers 帧里面

image

(3)、多路复用

 在一条连接上同时发送无数个请求

image

image

(4)、请求优先级

既然所有资源都是并行发送,那么就需要「优先级」的概念了,这样就可以对重要的文件进行先传输,加速页面的渲染。

(5)、服务器推送

在 HTTP2.0中,服务器推送是指在客户端请求之前发送数据的机制。

(6)、强制 SSL

虽然 HTTP/2.0 协议并没声明一定要用 SSL,但是 Google Chrome 等浏览器强制要求使用 HTTP/2.0 必须要用上 SSL, 也就是说必须要: https://

小结:

还记得上面 HTTP/1.1的小结吗,有了 HTTP/2.0 ,HTTP/1.1 的一些传统网页优化方案将不再适用

因为“所有的HTTP2.0的请求都在一个TCP链接上”,“资源合并减少请求”,比如CSS Sprites,多个JS文件、CSS文件合并等手段没有效果,或者说没有必要。
因为“多路复用”,采用“cdn1.cn,cdn2.cn,cdn3.cn,打开多个TCP会话,突破浏览器对同一域名的链接数的限制”的手段是没有必要的。因为因为资源都是并行交错发送,且没有限制,不需要额外的多域名并行下载。
因为“服务器推送”,内嵌资源的优化手段也变得没有意义了。而且使用服务器推送的资源的方式更加高效,因为客户端还可以缓存起来,甚至可以由不同的页面共享(依旧遵循同源策略)

二、使用 node.js 实现 HTTP/2.0 请求

下面利用一个简单的demo来实践一下上面的理论,咱们会用一个 spdy 的node模块来实现 h2 的请求。
HTTP/2虽然支持明文的HTTP传输,但是SPDY强制要求使用HTTPS,所以在实现h2请求我们还需要做一些
准备工作,那就是先生成一个证书实现HTTPS请求,在这基础上才能实现 HTTP/2.0

1、生成自签名的证书

生成私钥KEY , 这一步执行完以后,cert目录下会生成server.key文件

$ openssl genrsa -des3 -out server.key 2048

生成证书请求文件CSR

$ openssl req -new -key server.key -out server.csr

生成CA的证书

$ openssl req -new -x509 -key server.key -out ca.crt -days 3650

最后用第3步的CA证书给自己颁发一个证书玩玩

$ openssl x509 -req -days 3650 -in server.csr \
>  -CA ca.crt -CAkey server.key \
>  -CAcreateserial -out server.crt

小结一些生成证书流程

SSL 签名证书的生成流程大概如下:
先在本地或自己的服务器生成 server.key 和 server.csr 文件,然后讲 server.csr 文件提交给 数字证书认证机构(英语:Certificate Authority,缩写为CA),然后 CA 估计会对你提交过来的 server.csr 文件进行签名,然后你得到签名过后的
server.csr 文件配合刚才生成的 server.key 就能开启 HTTPS 了
上面第 3 小点,咱们为了测试在本地自己生成 CA 证书,自己给 server.csr 文件签名 !!!

2、一个 node 服务

const spdy = require('spdy');
const express = require('express');
const fs = require('fs');

const port = 3000;
const app = express();

app.use(express.static('src'));

//  主页输出 "Hello World"
app.get('/', function (req, res) {
    console.log("主页 GET 请求");
    res.send('Hello GET');
})

//  POST 请求
app.post('/', function (req, res) {
    console.log("主页 POST 请求");
    res.send('Hello POST');
})

//  /del_user 页面响应
app.get('/del_user', function (req, res) {
    console.log("/del_user 响应 DELETE 请求");
    res.send('删除页面');
})

const options = {
    passphrase: '123456', // 我在生成证书的时候密码填写的是 123456
    key: fs.readFileSync(__dirname + '/server.key'),
    cert: fs.readFileSync(__dirname + '/server.crt')
}

spdy.createServer(options, app)
    .listen(port, (error) => {
        if (error) {
            console.error(error)
            return process.exit(1)
        } else {
            console.log('Listening on port: ' + port + '.')
        }
    })

运行项目

1、获取项目分支

git clone https://github.com/ZengTianShengZ/My-Blog.git
git checkout -b demo-http2.0 origin/demo-http2.0

2、项目构建

cd demo                // 切换至 demo 目录
npm run install
node index.js

> Listening on port: 3000.

在浏览器打开 https://localhost:3000/
你会碰到这个页面,点击继续即可

image

开启 h2

image

浏览器文件上传

浏览器文件上传

最近项目有用到文件上传,发现对这一块内容不是很了解,所以花时间整理一份这方面的知识体系

一、预备知识

1、HTTP 请求和响应

image

请求

常见的HTTP请求报文头属性

Accept

请求报文可通过一个“Accept”报文头属性告诉服务端客户端接受什么类型的响应。Accept属性的值可以为一个或多个MIME类型的值,关于MIME类型,大家请参考:http://en.wikipedia.org/wiki/MIME_type

Accept: application/javascript
Accept: application/json
Accept: application/x-www-form-urlencodedtext/css
Accept: text/htm
Accept: image/pn
Accept: multipart/form-data
// ...

Cookie

客户端的Cookie就是通过这个报文头属性传给服务端的哦!如下所示:

Cookie: $Version=1; Skin=new;jsessionid=5F4771183629C9834F8382E23BE13C4C  

Cache-Control

对缓存进行控制,如一个请求希望响应返回的内容在客户端要被缓存一年,或不希望被缓存就可以通过这个报文头达到目的。

Cache-Control: no-cache  // 不缓存
Cache-Control: max-age=600  // 缓存内容将在xxx秒后失效

Content-Type

Content-Type用于指定内容类型,一般是指网页中存在的Content-Type,Content-Type属性指定请求和响应的HTTP内容类型。如果未指定 ContentType,默认为text/html。
常见的 Content-Type 如下:

Content-Type: text/html
Content-Type: text/plain
Content-Type: text/css
Content-Type: text/javascript
Content-Type: application/x-www-form-urlencoded
Content-Type: multipart/form-data
Content-Type: application/json
Content-Type: application/xml

Content-Type 是重点,对我们理解数据上传,或文件上传有帮助,下面重点讲一下 Content-Type

application/x-www-form-urlencoded

application/x-www-form-urlencoded是常用的表单发包方式,普通的表单提交,或者js发包,默认都是通过这种方式
比如一个简单的表单提交

<form enctype="application/x-www-form-urlencoded" action="http://homeway.me/post.php" method="POST">
    <input type="text" name="name" value="homeway">
    <input type="text" name="key" value="nokey">
    <input type="submit" value="submit">
</form>

请求主体如下:

Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding:gzip, deflate
Accept-Language:zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4,gl;q=0.2,de;q=0.2
Cache-Control:no-cache
Connection:keep-alive
Content-Length:17
Content-Type:application/x-www-form-urlencoded

那么服务器收到的raw body会是,name=homeway&key=nokey,在php中,通过$_POST就可以获得数组形式的数据。

text/xml
微信用的是这种数据格式发送请求的。

POST http://www.homeway.me HTTP/1.1 
Content-Type: text/xml
<?xml version="1.0"?>
<resource>
    <id>123</id>
    <params>
        <name>
            <value>homeway</value>
        </name>
        <age>
            <value>22</value>
        </age>
    </params>
</resource>

multipart/form-data

multipart/form-data用在发送文件的POST包。
通过控制台,可以看到发送一个文件的数据内容如下:

POST http://www.homeway.me HTTP/1.1
Content-Type:multipart/form-data; boundary=------WebKitFormBoundaryOGkWPJsSaJCPWjZP

------WebKitFormBoundaryOGkWPJsSaJCPWjZP
Content-Disposition: form-data; name="key2"
456
------WebKitFormBoundaryOGkWPJsSaJCPWjZP
Content-Disposition: form-data; name="key1"
123
------WebKitFormBoundaryOGkWPJsSaJCPWjZP
Content-Disposition: form-data; name="file"; filename="index.png"

这里Content-Type告诉我们,发包是以multipart/form-data格式来传输,另外,还有boundary用于分割数据。
当文件太长,HTTP无法在一个包之内发送完毕,就需要分割数据,分割成一个一个chunk发送给服务端
那么--用于区分数据快,而后面的数据 WebKitFormBoundaryOGkWPJsSaJCPWjZP 就是标示区分包作用。

更多请求报文属性请参考 http://en.wikipedia.org/wiki/List_of_HTTP_header_fields

响应

常见的HTTP响应报文头属性

Cache-Control

请求报文头也有个 Cache-Control ,请求的 Cache-Control 用于告诉服务器我需要缓存这个请求资源,服务端接受到这个属性后也给客服端响应一个 Cache-Control 属性,通过该报文头属告诉客户端如何控制响应内容的缓存。
比如设置了 Cache-Control: max-age=3600 让客户端对响应内容缓存3600秒,也即在3600秒内,如果客户再次访问该资源,直接从客户端的缓存中返回内容给客户,不要再从服务端获取(当然,这个功能是靠客户端实现的,服务端只是通过这个属性提示客户端“应该这么做”,做不做,还是决定于客户端,如果是自己宣称支持HTTP的客户端,则就应该这样实现)。

ETag

一个代表响应服务端资源(如页面)版本的报文头属性,如果某个服务端资源发生变化了,这个ETag就会相应发生变化。它是Cache-Control的有益补充,可以让客户端“更智能”地处理什么时候要从服务端取资源,什么时候可以直接从缓存中返回响应。

ETag: "737060cd8c284d8af7ad3082f209582d"  

Set-Cookie

服务端可以设置客户端的Cookie,其原理就是通过这个响应报文头属性实现的:

Set-Cookie: UserID=JohnDoe; Max-Age=3600; Version=1  

2、input file 知识

HTML5 添加了一些强大的 File API

FileList

FileList 对象针对表单的 file 控件。当用户通过 file 控件选取文件后,这个控件的 files 属性值就是 FileList 对象。它在结构上类似于数组,包含用户选取的多个文件。如果 file 控件没有设置 multiple 属性,那么用户只能选择一个文件,FileList 对象也就只有一个元素了。

<input type='file' />
<script>
    document.querySelector('input').onchange = function() {
      console.log(this.files);
    };
</script>

image

由控制台可以看到 FileList 是一个数组,数组包含文件的一些信息

File

我们看到一个 FileList 对象包含了我们选中的 File 对象,那么一个 File 又有哪些属性呢?我们可以打印出来看看。

image

name:文件名,该属性只读。

size:文件大小,单位为字节,该属性只读。

type:文件的 MIME 类型,如果分辨不出类型,则为空字符串,该属性只读。

lastModified:文件的上次修改时间,格式为时间戳。

lastModifiedDate:文件的上次修改时间,格式为 Date 对象实例。

Blob

上图中我们看到,File 对象是继承自 Blob 对象的,Blob 又是什么鬼?
Blob(Binary Large Object)对象代表了一段二进制数据,提供了一系列操作接口。其他操作二进制数据的 API(比如 File 对象),都是建立在 Blob 对象基础上的,继承了它的属性和方法。
生成 Blob 对象有两种方法:一种是使用 Blob 构造函数,另一种是对现有的 Blob 对象使用 slice 方法切出一部分。

var a = ["hello", "world"];
var myBlob = new Blob(a, { "type" : "text/xml" });
console.log(myBlob);

image

Blob 对象有两个只读属性:

size:二进制数据的大小,单位为字节。(文件上传时可以在前端判断文件大小是否合适)

type:二进制数据的 MIME 类型,全部为小写,如果类型未知,则该值为空字符串。(文件上传时可以在前端判断文件类型是否合适)

FileReader

FileReader API 才是我们接下去完成一些任务的关键。FileReader API 用于读取文件,即把文件内容读入内存。它的参数是 File 对象或 Blob 对象。

var reader = new FileReader();
reader.abort();

URL

URL 对象居然也属于File API ,我也很吃惊,不过下面的API估计我们或多或少有用过

var objecturl =  window.URL.createObjectURL(blob);

上面的代码会对二进制数据生成一个 URL,这个 URL 可以放置于任何通常可以放置 URL 的地方,比如 img 标签的 src 属性。需要注意的是,即使是同样的二进制数据,每调用一次 URL.createObjectURL 方法,就会得到一个不一样的 URL。
这个 URL 的存在时间,等同于网页的存在时间,一旦网页刷新或卸载,这个 URL 就失效。(File 和 Blob 又何尝不是这样呢)除此之外,也可以手动调用 URL.revokeObjectURL 方法,使 URL 失效。

二、文件上传

介绍了那么多,实际用到的知识很少,但有个大概的内容体系才不多对自己写的代码一知半解不是吗。下面我们用个文件上传的demo实际操作一下:

我们用 form 表单和 ajax 方式来分别实现文件上传

image

    <section>
        <h1>form 表单方式</h1>
        <form method="POST" action="/api/uploadFile" enctype="multipart/form-data">
            <p>file upload</p>
            <span>picName:</span><input name="picName" type="text" /><br/>
            <input name="file" type="file" /><br/><br/>
            <button type="submit">submit</button>
        </form>
    </section>
    <section>
        <h1>formData 方式</h1>
        <input id="J_file_type1" name="file" type="file" /><br/><br/>
        <button id="J_btn_upload_type1">上传</button>
    </section>

1、form 表单方式

点击页面的 <button type="submit">submit</button> 就实现了文件上传。
form 表单设置了 action 上传路径 enctype 上传类型(表现在请求头中)这个在文章的最开始部分咱们也就介绍了就不多说了。
form 表单上传文件有个不好的地方是form 表单提交会刷新页面,也就对用户很不友好了,下面咱们再用 formData 方式 来实现文件上传

2、formData 方式

        const file = document.querySelector('#J_file_type1').files[0]
        const formData = new FormData()
        // 建立一个upload表单项,值为上传的文件
        formData.append('file', file)
        formData.append('name', file.name)
        const xhr = new XMLHttpRequest()
        xhr.open('POST', '/api/uploadFile')
        // 定义上传完成后的回调函数
        xhr.onload = function () {
            if (xhr.status === 200) {
                alert('上传成功')
            } else {
                alert('出错了')
            }
        }
        xhr.send(formData);

这里我们用到了 FormData 来实现文件上传。
FormData对象用以将数据编译成键值对,以便用XMLHttpRequest来发送数据。其主要用于发送表单数据,但亦可用于发送带键数据(keyed data),而独立于表单使用。如果表单enctype属性设为multipart/form-data ,则会使用表单的submit()方法来发送数据,从而,发送数据具有同样形式。

3、如果不使用 formData 方式呢

如果不使用FormData对象的情况下,通过AJAX序列化和提交表单也是可以实现表单上传,不过这也太变态了,因为要自己序列化上面提到的文件上传的请求主体

POST http://www.homeway.me HTTP/1.1
Content-Type:multipart/form-data; boundary=------WebKitFormBoundaryOGkWPJsSaJCPWjZP

------WebKitFormBoundaryOGkWPJsSaJCPWjZP
Content-Disposition: form-data; name="key2"
456
------WebKitFormBoundaryOGkWPJsSaJCPWjZP
Content-Disposition: form-data; name="key1"
123
------WebKitFormBoundaryOGkWPJsSaJCPWjZP
Content-Disposition: form-data; name="file"; filename="index.png"

感兴趣的可以点开链接查看 点开链接查看

运行项目

1、获取项目分支

git clone https://github.com/ZengTianShengZ/My-Blog.git
git checkout -b demo-file-upload origin/demo-file-upload

2、项目构建

cd demo                // 切换至 demo 目录
npm install
node app.js

🏷设计模式-section1

JavaScript 中的设计模式

设计模式的**都是一样的,但语言的特性不一样,在实现方式上还是有出入的。这里用JavaScript 这门语言来简单探讨一下传统的一些设计模式,利用JavaScript这们语言的特性来实现这些传统的设计模式。

一、单例模式

单例模式: 保证一个类只有一个实例,并提供一个访问它的全局访问点

虽然定义是这么定义,但咱们�面对的是 JavaScript 这么语言,在实现上就可以结合特性来思考如何实现 单例模式

1、全局变量来实现

const a = {}

window.a = {}

全局变量不是单例模式,但一些情况下我们可以使用全局变量当成单例模式来使用。在浏览器下的做法经常使用 window 这全局唯一的对象来实现。

2、高阶函数结合闭包实现

const getSingle = function (fn) {
  let result
  return function () {
    return result || (result = fn.apply(this, arguments))
  }
}

result 用来保存 fn 的计算结果,因为 result 变量在闭包中,不会销毁,当被赋值过一次时 getSingle 函数会直接返回该结果

3、应用情况

什么情况下我们只需要一个对象呢,比如线程池,全局缓存,或者是登录的浮窗只需创建一次等等。

下面以创建一个单一浮窗为例子

const getSingle = function (fn) {
  let result
  return function () {
    return result || (result = fn.apply(this, arguments))
  }
}
const creatLayer = function() {
  var div = document.createElement('div')
  div.innerHTML = '浮窗'
  document.body.appendChild(div)
  return div
}

const getLayer_1 = getSingle(creatLayer)
const getLayer_2 = getSingle(creatLayer)
console.log(getLayer_1 === getLayer_2) // ture

二、策略模式

策略模式: 定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换

1、实现

策略模式一般由两部分组成。一是 策略类策略类 封装了具体的算法,并负责具体的计算过程。二是 环境类(calculateBons)环境类 接受客户的请求,并把请求委托给某一个策略类。

一个例子加以说明:如,绩效为S级,奖励金为其4倍工资,A级3倍工资,B级2倍工资,你会如何实现呢,是用一堆 if-else 去判断吗

// 策略类,封装一系列算法
const strategies = {
  "S": function(salary){
    return salary * 4
  },
  "A": function(salary){
    return salary * 3
  },
  "B": function(salary){
    return salary * 2
  }
}
// 环境类
const calculateBons = function(lever, salary){
  return strategies[lever](salary)
}

let result_1 = calculateBons("B", 2000) // 输出 4000
let result_2 = calculateBons("S", 5000) // 输出 20000

也许你看到上面的实现方式觉得没必要这么麻烦,直接在 环境类 里面做一些 if - else 的判断不是�也能照样使用该功能么,但想想,如果该功能后续需要支持 C级1倍薪资 的需求,那是不是要修改 环境类 的代码呢,根据 单一职责原则 ,策略模式就�能很好的实现需求的扩展。

"C": function(salary){
    return salary * 1
}

let result_3 = calculateBons("C", 1000) // 输出 1000

3、应用

什么情况下我们需要使用 策略模式 呢,比如多种表单的校验、一种场景多种身份的切换、有多重 if-else 分支的情况,那可以考虑下 策略模式

三、代理模式

代理模式:为一个�对象提供一个代用品或占位符,以控制对它的访问

`客户` -> `本体`  // 不用代理

`客户` -> `代理` -> `本体` // 使用代理

1、实现

代理模式没有具体的模板,根据不同的使用场景产生不同的代理

一个图片加载的例子:

网页加载大图片时有时候因为网络问题而出现页面空白,可以通过代理,在加载图片的时候先显示一张 loading 图片

  // 本体 - 图片加载
  var myImage = (function(){
    var imgNode = document.createElement('img')
    document.body.appendChild(imgNode)
    return {
      setSrc : function(src){
        imgNode.src = src
      }
    }
  })()

  // 代理 - 图片加载前先 loading
  var proxyImg = (function(){
    var img = new Image()
    img.onload = function(){
      myImage.setSrc(this.src)
    }
    return {
      setSrc : function(src){
        myImage.setSrc('./img/loading.gif')
        img.src = src
      }
    }
    })()

    // 客户 - 加载一张图片
    proxyImg.setSrc('http://img.com.xxxxx.jpg')

上面可以看出使用代理的两点好处:

  • 单一职责原则 : 一个类(对象或函数),应该只有一个引起它变化的原因。上面图片加载,使用了代理 proxyImg 去做网络请求图片,职责单一 , myImage 去做图片显示,职责也是单一

  • 开放封闭原则: 软件实体(类、模块、函数等)应该是可以扩展的,但是不可修改的。如果几年后网速大幅提高,那就不必使用 proxyImg 方法来预加载一张 loading 图片,那上面的写法完全不用修改,去掉 proxyImg 方法,直接用 myImage 方法即可

2、应用

代理模式应用在哪些地方呢,可以是 请求代理、缓存代理、节流代理,总之如果需要在 客户本体 之间多做一些操作,就可以考虑引入代理模式

四、发布-订阅模式

发布-订阅模式:它定义对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都将得到通知

1、初步理解 发布-订阅模式

其实发布-订阅模式我们经常用到,如常用的事件监听就是一个发布-订阅模式

document.body.addEnentListener('click', function () {
  console.log('on click');
}, false)

document.body.click(); // 模拟用户点击

在监听事件中,body用户 订阅了一个点击事件,用户 发布了一个点击后,就通知 body 做出响应。

2、实现

const event = {
  clientList: [],
  listen(key, fn) {
    if (!this.clientList[key]) {
      this.clientList[key] = [];
    }
    this.clientList[key].push(fn); // 订阅的消息加进缓存列表
  },
  tigger() {
    const key = Array.prototype.shift.call(arguments);
    const fns = this.clientList[key];
    if (!fns || fns.length === 0) { // 如果没有绑定对应的消息
      return false;
    }
    for (let i = 0, fn = null ; fn = fns[i++];) {
      fn.apply(this. arguments);
    }
  }
}
event.listen('1点', function(doing) {
  console.log(doing);
})
event.listen('2点', function(doing) {
  console.log(doing);
})
event.tigger('1点', '吃饭') // 吃饭
event.tigger('2点', '睡觉') // 睡觉

在 event 中,我们定义了个 listen 方法来�提供订阅消息,定义了个 tigger 方法来实现�发布消息

3、应用

发布-订阅模式可以应用在什么地方呢,比如在异步编程中,�可以代替回调的方式来通知异步事件;在模块间的通信也可以应用发布-订阅模式,常见的就是 eventBus ,做信息通信。

五、组合模式

组合模式:用小的子对象来构建更大的对象

1、实现

有这么个需求场景,需要执行一个统一操作,从而触发一系列的子操作。

const command = function () {
  return {
    commandList: [],
    add(command) {
      this.commandList.push(command)
    },
    execute() {
      for (var i = 0, command; command = this.commandList[i++];) {
        command.execute();
      }
    }
  }
}
const command1 = {
  execute() {
    console.log('----command1---');
  }
}
const command2 = {
  execute() {
    console.log('----command2---');
  }
}
const cmd = command()
cmd.add(command1)
cmd.add(command2)
cmd.execute()

在 command 方法中,我们返回一个对象,定义了 add 方法来添加子操作,定义 execute 方法来执行所有的子操作。

3、应用

如果你的需求遇到树形结构,需要多种组合并统一操作,那可以考虑下组合模式

image

六、享元模式

享元模式:运行共享技术有效地支持大量细粒度的对象,避免大量拥有相同内容的小类的开销(如耗费内存),使大家共享一个类(元类)

1、实现

有这么个例子,假设有个内衣工厂,目前的产品有50种男士内衣与50种女士内衣,为了推销产品,工厂决定生产一些塑料模特来穿上它们的内衣拍成内衣广告

var Model = function (sex, underwear) {
  this.sex = sex;
  this.underwear = underwear;
}
Model.prototype.takePhoto = function () {
  console.log('sex=' + this.sex + 'underwear=' + this.underwear)
}
for (var i = 1; i <= 50; i++) {
  var maleModel = new Model('male', 'underwear' + i)
  maleModel.takePhoto();
}
for (var i = 1; i <= 50; i++) {
  var maleModel = new Model('female', 'underwear' + i)
  femaleModel.takePhoto();
}

在这个例子中我们实例化了 100 个对象,想想要是10000套衣服或更多呢,性能将会下降。

我们用享元模式来优化

2、外部状态和内部状态

享元模式最主要的是区分外部状态内部状态,上面例子由于 sex 属性是区分 Model 的性别,属于内部状态,属性 underwear 为所有 Model 共有,属于外部状态,所以我们用享元模式优化出来的代码如下:

var Model = function (sex) {
  this.sex = sex;
}
Model.prototype.takePhoto = function () {
  console.log('sex=' + this.sex + 'underwear=' + this.underwear)
}

var maleModel = new Model('male');
var female = new Model('female');

for (var i = 1; i <= 50; i++) {
  maleModel.underwear = 'underwear' + i;
  maleModel.takePhoto();
}
for (var i = 1; i <= 50; i++) {
  female.underwear = 'underwear' + i;
  femaleModel.takePhoto();
}

3、应用

享元模式是为了解决性能问题而生的模式,在一个存在大量相似的对象系统中,享元模式可以很好的解决大量对象带来的性能问题。

七、装饰者模式

装饰者模式:在不改变对象自身的基础上,在程序运行期间给对象动态的添加职责

1、简单实现

例子:一个战斗机有发射子弹的功能,后来装备加强了,还具有发射导弹的功能。

const Plane = {
  fire() {
    console.log('-----发射子弹');
  }
}

Plane.fire() // -----发射子弹

// 升级后
const fireAtom = function () {
  console.log('-----发射原子弹');
}

const fire = Plane.fire

Plane.fire = function () {
  fire()
  fireAtom()
}

Plane.fire() // -----发射子弹, -----发射原子弹

2、用 AOP(面向切面编程) 装饰函数

上面例子的做法有点不妥,因为我们改变了原有函数的内部实现,下面使用 AOP 装饰函数

const Plane = {
  fire() {
    console.log('-----发射子弹');
  }
}

Plane.fire() // -----发射子弹

// 升级后

const fireAtom = function () {
  console.log('-----发射原子弹');
}

Function.prototype.after = function (afterFn) {
  const _this = this;
  return function () {
    const ret = _this.apply(this, arguments);
    afterFn.apply(this, arguments)
    return ret
  }
}

Plane.fire.after(fireAtom)() // -----发射子弹, -----发射原子弹

上面例子中我们没有更改对象的原有方法,而是添加了个 after 函数,在原有 fire 函数执行完执行 fireAtom 函数。

3、应用

如果你有遇到需要在不改变原有函数内部实现的情况下给该函数添加功能,可以使用装饰者模式。

8、小结

前面我们学习了一些常用的设计模式,设计模式可以说是在一些特定的场景下使用的一套编程规则,使之代码更加健壮易于扩展和维护。在常规的编码中我们也可以遵循一些编程原则,比如单一职责,最少知识原则等。虽然这些规则在编码过程中不强制要求,但这些前辈们总结出的经验有利于我们代码的健壮和可维护。

1、单一职责原则

单一职责原则(SRP):一个对象(方法)只做一件事。

还记得我们前面讲的 单例模式 吗,用到的就是单一职责原则,在单例方法中,我们只做一件事,创建单例,至于是一个弹窗单例还是对象单例,我们不管,交由入参函数 fn 去实现。

const getSingle = function (fn) {
  let result
  return function () {
    return result || (result = fn.apply(this, arguments))
  }
}

2、最少知识原则

最少知识原则(LKP):一个软件实体应当尽可能少地与其他实体发生相互作用。

也就是尽可能的减少对象或方法间的调用,减少对象或方法之间的联系。

不过这好像 最少知识原则单一职责原则 存在了冲突,在实际开发中怎么选择 最少知识原则单一职责原 还要依据具体的环境来定。

3、开放-封闭原则

开放-封闭原则(OCP): 软件实体(类、模块、函数)等应该是可以扩展的,但不可修改。

由上面的定义咱们可以很快的联想到前面文章提到的设计模式很多都有用到该原则,如 装饰者模式, 我们在不改变原因函数 fire 的内部逻辑,添加了个函数 after 实现了可扩展。

Function.prototype.after = function (afterFn) {
  const _this = this;
  return function () {
    const ret = _this.apply(this, arguments);
    afterFn.apply(this, arguments)
    return ret
  }
}

Plane.fire.after(fireAtom)() // -----发射子弹, -----发射原子弹

还有 发布-订阅模式代理模式 都有用到了 开放-封闭原则

总结:

本篇讲了一些传统的设计模式和一些使用场景,没有深入的去分析具体的推导过程,只是让大家有个印象,当在实际开发过程中如有遇到对应的场景,可以考虑能否套用一些设计模式进去,让你的代码更加健壮合理。

vue-router 源码分析

vue-router 源码分析

思考 🤔

1、vue-router 是怎么被初始化以及装载的
2、路由改变是怎么更新视图的

image

流程图

结合流程图来分析下面代码的执行流程

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const Home = { template: '<div>home</div>' }
const Foo = { template: '<div>foo</div>' }

const router = new VueRouter({
  mode: 'history',
  routes: [
    { path: '/', component: Home },
    { path: '/foo', component: Foo },
  ]
})

new Vue({
  router,
  template: `
    <div id="app">
      <router-view></router-view>
    </div>
  `
}).$mount('#app')

1、vue-router 初始化以及装载

vue-router 作为 Vue 的一个插件,通过全局方法 Vue.use() 的方式将 vue-router 挂载到 Vue 上。

对照流程图:

(1)、Vue.use(VueRouter) 首先会调用 install 方法,
(2)、利用 mixinVue 混入一个 beforeCreat 钩子,接着 beforeCreat 钩子做了哪些事情呢,
(3)、检测 Vueoptions 有没有 router 实例,有的话会通过 Object.defineProperty$router$route 变成响应式对象,
(4)、并挂载到 Vueprototype 上,
(5)、最后会往 Vue$options.components 注册两个全局组件 router-viewrouter-link

vue-router 挂载完,new VueRouter 实例化 VueRouter 有做了哪些事情呢。

对照流程图:

(1)、刚开始当然执行构造函数做一些初始化操作(流程图没体现出来)
(2)、接着会执行 createMatcher(options, routers) 根据 VueRouter 传入的 routes 数组创建 pathcomponent 的匹配规则
(3)、根据 VueRouter 传入的 mode 实例化 history 属性
(4)、定义一些钩子函数 beforeEach beforeResolve afterEach (流程图没体现出来)
(5)、定义一些路由方法 pushreplacegoback
(6)、定义 init() 方法,该方法是在Vue组件的 beforeCreat 钩子下会被调用,执行的时候会去匹配对应的 path 并找到对应的 component 再渲染到页面上(先有个印象就好)

2、路由改变是怎么触发匹配的组件更新的

前面做了这么一大波操作,那路由改变是怎么触发组件更新的呢,或者一个Vue项目初始化的时候是怎么加载路由组件的呢,具体流程是怎么样的。
如果你还没被流程图绕晕的话,再看一眼流程图的 黑色 线路。

前面我们提到了 Vue.use() 时会利用 mixinVue 混入一个 beforeCreat 钩子,注意这个钩子是全局钩子,意味着每个 Vue 组件都会调用这个钩子,包括根组件自身。

跟着黑色 线路走:

(1)、在根组件实例化后就会调用 beforeCreat 钩子,
(2)、接着会调用 route 实例的 init() 方法,init() 方法会执行 transitionTo 来改变对应的路由。
(3)、那对应的路由从哪里找呢,就是通过 match() 方法,去匹配我们上面有提到的 createMatcher 方法已经做好的 pathcomponent 的匹配规则。
(4)、通过 path 找到对应的 component 后会再执行 confirmTransition 来确认改变路由,
(5)、接着 updateRoute 跟更新路由,最后将匹配的 componentcurrent,
(6)、执行这些操作后其实是有个回调函数的,回调函数会跟新 _route 属性,该属性也是一个响应式对象,
(6)、_route 更新就会触发视图渲染,也就是调用 apprender() 函数,接着也触发了 router-view 组件的 render
(7)、router-view 组件的 render 会获取匹配的 component 渲染到视图上。

源码解析

通过流程图大致分析了 vue-router 的初始化和装载过程,以及路由跟新的流程。了解完大概思路下面进行源码层的分析就不会那么摸不着头绪了。

1、路由注册

故事的开端还是需要从路由的注册说起。

Vue.use(VueRouter)

vue-router 提供了一个 install 方法, Vue.use(VueRouter) 的时候会执行该方法。具体可看 vue 插件机制

import { install } from './install'

export default class VueRouter {
  static install: () => void;
  static version: string;
  // ...
}
VueRouter.install = install
VueRouter.version = '__VERSION__'

if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}

install 方法里头做了这些事情:

import View from './components/view'
import Link from './components/link'

export let _Vue

export function install (Vue) {
  // 确保 install 调用一次
  if (install.installed && _Vue === Vue) return
  install.installed = true
  // 把 Vue 赋值给全局变量给 VueRouter 用
  _Vue = Vue

  const isDef = v => v !== undefined

  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }

  Vue.mixin({
    // 给每个组件混入 beforeCreate 钩子,注意,这意味着每个组件实例化时都会执行调用这个钩子
    beforeCreate () {
      // 判断是不是一个路由应用,有没有传入 router options
      if (isDef(this.$options.router)) {  // 根组件会执行
        this._routerRoot = this
        this._router = this.$options.router
        // 初始化路由
        this._router.init(this)
        // 将 _route 属性实现双向绑定,改变时会触发组件渲染
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else { // 其他子组件会执行
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })

  // 能够在组件上使用  this.$router 获取 _router 实例
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })
  // 能够在组件上使用  this.$route 获取 _router 实例
  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })
  // 全局注册组件 router-link 和 router-view
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)
}

2、VueRouter 实例

上面有提到 Vue 根实例会挂载 _router 属性,及 $options.router 实例,该实例就是 VueRouter 实例

 this._router = this.$options.router
const router = new VueRouter({
  mode: 'history',
  routes: [
    { path: '/', component: Home },
    { path: '/foo', component: Foo },
  ]
})

接下来分析 VueRouter 实例化做了哪些操作

export default class VueRouter {
  constructor (options: RouterOptions = {}) {
    //...

    // 创建路由匹配对象
    this.matcher = createMatcher(options.routes || [], this)
    let mode = options.mode || 'hash'
    this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode

    // 根据 mode 采取不同的路由方式
    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }
  }
  // ...
}

(1)、 createMatcher

初始化部分主要是 createMatcher 的功能, createMatcher 会创建路由匹配对象,及 pathcomponent 对应

export function createMatcher (routes: Array<RouteConfig>,router: VueRouter): Matcher {
  const { pathList, pathMap, nameMap } = createRouteMap(routes)
  console.log('==pathMap==', pathMap)
  function addRoutes (routes) {
    createRouteMap(routes, pathList, pathMap, nameMap)
  }
  function match (raw: RawLocation,currentRoute?: Route,redirectedFrom?: Location ): Route {
    const location = normalizeLocation(raw, currentRoute, false, router)
    //...
    return _createRoute(record, location, redirectedFrom)
  }
  // ...
  return {
    match,
    addRoutes
  }
}

createMatcher 会返回一个 matchaddRoutes 方法, 调用 match 方法能根据 path 返回对应的组件。这里隐藏了一些细节的分析,直接
log pathMap 在控制台查看 createRouteMap 创建匹配的结果就好

image

可看到有这么一个结构

{
  '': {
    path: '',
    components: {}
  },
  '/foo': {
    path: '/foo',
    components: {}
  }
}

调用 match 方法能拿到对应的匹配内容了。

3、路由初始化

上面分析了 VueRouter 的实例化,那路由的初始化在哪里执行的呢,之前有提到 beforeCreate 会执行 this._router.init(this) ,这里就做了路由的初始化操作。

  Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) { // 判断是不是根组件
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this) // 初始化路由
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })

来看 init() 做了哪些事情

  init (app: any /* Vue component instance */) {
    if (this.app) {
      return
    }

    this.app = app

    const history = this.history
    // 判断路由模式
    if (history instanceof HTML5History) {
      // 路由跳转
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
      // 添加 hashchange 监听
      const setupHashListener = () => {
        history.setupListeners()
      }
      // 路由跳转
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }
    //注册监听事件, transitionTo 执行完会执行该回调,回调里头对组件的 _route 属性进行赋值,触发组件渲染
    history.listen(route => {
      this.apps.forEach((app) => {
        app._route = route
      })
    })
  }

transitionTo 执行完会执行回调,回调里头去更新 _route 从而跟新视图,所以先再看下 transitionTo 做了什么。

  transitionTo (location: RawLocation,onComplete?: Function,onAbort?: Function) {
    // 获取匹配的路由信息
    const route = this.router.match(location, this.current)
    // 确认切换路由
    this.confirmTransition(
      route,
      () => {
        this.updateRoute(route)
        onComplete && onComplete(route)
        this.ensureURL()

        // 执行回调
        if (!this.ready) {
          this.ready = true
          this.readyCbs.forEach(cb => {
            cb(route)
          })
        }
      },
      err => {
        if (onAbort) {
          onAbort(err)
        }
        if (err && !this.ready) {
          this.ready = true
          this.readyErrorCbs.forEach(cb => {
            cb(err)
          })
        }
      }
    )
  }

transitionTo 主要是去获取匹配的路由信息,这个匹配路由信息上面已经分析过了。接着调用 confirmTransition ,执行完会调用一个回调 cb(route)
这个就会触发之前的监听事件:

history.listen(route => {
  this.apps.forEach((app) => {
    app._route = route // 更新 _route 从而触发组件更新
  })
})

4、vue-router

前面我们已经了解到了路由改变会经过一系列的操作,最终找到匹配的 component 从而跟新 route

 app._route = route // 更新 _route 从而触发组件更新

而在讲解【1、路由注册】这一步我们有提到

Vue.util.defineReactive(this, '_route', this._router.history.current)

_route 是一个响应式属性,更新后会触发相应的视图更新,并且【1、路由注册】这一步我们还提到

  // 全局注册组件 router-link 和 router-view
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

install 注册 vue-router 时顺带注册了一个全局组件 RouterView 。接下来我们来分析下 _route 属性的改变是如何触发视图更新的,也就是

<router-view class="view"></router-view>

是如何渲染匹配的 component

router-view 源码

export default {
  name: 'RouterView',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
    data.routerView = true
    const h = parent.$createElement
    const name = props.name
    const route = parent.$route // 获取 _router
    const cache = parent._routerViewCache || (parent._routerViewCache = {})
    let depth = 0
    let inactive = false
    while (parent && parent._routerRoot !== parent) {
      const vnodeData = parent.$vnode ? parent.$vnode.data : {}
      if (vnodeData.routerView) {
        depth++
      }
      if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    data.routerViewDepth = depth

    // ... 

    //获取到匹配的 component
    const matched = route.matched[depth]
    const component = matched && matched.components[name]

    // ...

    // 调用 createElement 函数 渲染匹配的组件
    return h(component, data, children)
  }
}

router-view 是一个函数组件, _router 改变触发 render 执行,render 内部获取到匹配的 component 进行渲染。

<router-view class="view"></router-view>

最后 router-view 被渲染成相应的组件。

小结

我们通过 vue-router 的挂载 -> 路由匹配 -> 组件渲染 的流程大致分析了 vue-router 的执行原理,我们隐藏和很多细节和功能没分析,比如
路由的 historyhash 模式、路由的具体匹配过程、路由守卫的功能等等。这些后续再具体分析,但了解了整体流程,接下来的细节功能就一步步拆开分析就清楚多了。

最后再放一张最开始的流程图

image

Vuex 源码解析

Vuex 源码解析

本篇意在理清楚 Vuex 的工作原理,相应的会忽略一些技术细节,更多源码请移步到 github vux

1、目录结构

image

Vuex 的源码其实并不多,短小精悍,主要的工作都在 store.js 下完成

2、入口文件 index.js

分析源码先从入口文件入手,理清来龙去脉。

import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'

export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers
}

入口文件有整个状态管理的 Store 对象,安装 Vue.js 插件必须提供的 install 方法,版本号以及辅助函数 mapStatemapMutationsmapGettersmapActions

3、Vuex 插件的安装 install

Vuex 的使用要从插件的安装说起,安装 Vuex 插件做了哪些事情呢。

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)  // 会调用  Vuex 提供的 install 方法

安装 Vuex 插件,插件提供的 install 方法会被调用,方法如下:

export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}

可看出插件主要执行了 applyMixin(Vue) ,如下:

// applyMixin

export default function (Vue) {
  Vue.mixin({ beforeCreate: vuexInit })
  /**
   * Vuex init hook, injected into each instances init hooks list.
   */
  function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}

简要分析下, install 里面调用了 applyMixin 方法,applyMixin 方法里头执行了 Vue.mixin ,订阅了 beforeCreate 钩子,beforeCreate 则会在 Vue 初始化前调用执行。beforeCreate 对应执行了 vuexInit , 如下:

 this.$store = options.store
 this.$store = options.parent.$store

即为当前 Vue 实例(this)的 $store 属性挂上 store 实例。如果当前Vue 实例没有 store 实例则从父组件的 Vue 实例继承过来。

这样做是干嘛用的呢,是为了我们在任何一个 Vue 组件或子孙组件中都可以方便的使用

this.$store 

去对 store 做任何操作

4、 Store 对象初始化

了解了 store 如何挂载到 vue 实例,下面来看下 store 做了哪些操作

export class Store {
  constructor (options = {}) {
    const {
      plugins = [],
      strict = false
    } = options
    /****** _committing 严格模式下的标志位 ******/
    this._committing = false
    this._actions = Object.create(null)
    this._actionSubscribers = []
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    /****** module收集器 ******/
    this._modules = new ModuleCollection(options)    
    this._modulesNamespaceMap = Object.create(null)
    /***** 存放订阅者 ******/
    this._subscribers = []
    /***** 用来监听一些状态的变化 ******/
    this._watcherVM = new Vue()
    // bind commit and dispatch to self
    const store = this
    const { dispatch, commit } = this
    this.dispatch = function boundDispatch (type, payload) {
      return dispatch.call(store, type, payload)
    }
    this.commit = function boundCommit (type, payload, options) {
      return commit.call(store, type, payload, options)
    }
    // strict mode
    this.strict = strict
    const state = this._modules.root.state
    // init root module.
    // this also recursively registers all sub-modules
    // and collects all module getters inside this._wrappedGetters
    installModule(this, state, [], this._modules.root)
    // initialize the store vm, which is responsible for the reactivity
    // (also registers _wrappedGetters as computed properties)
    resetStoreVM(this, state)
    // apply plugins
    plugins.forEach(plugin => plugin(this))
    const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
    if (useDevtools) {
      devtoolPlugin(this)
    }
  }

  get state () {
       return this._vm._data.$$state
   }

   set state (v) {
      if (process.env.NODE_ENV !== 'production') {
         assert(false, `use store.replaceState() to explicit replace store state.`)
       }
    }

    commit (_type, _payload, _options) {
        // ...
     }

     dispatch (_type, _payload) {
        // ...
      }
}

从 Store 的构造函数我们可以看到几个熟悉的属性和方法,state, _mutations, _actions, this.dispatch, this.commit ,我们先抛开其他细节,来简单的回顾下我们是怎么使用 Vuex 的:

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})

store.commit('increment')
console.log(store.state.count) // -> 1

我们往 Store 传入一个 option 对象(包含 state,mutations属性 ),接着我们可以使用 store 实例 进行 store.commit 更新 statestore.state 获取 statestore.commit('increment') 到底做了什么事呢,下面我们主要分析下 store.commit

5、store.commit

更多的技术细节我们需要通过分析 store.commit 到底做了哪些操作:

//   store.commit
  commit (_type, _payload, _options) {
    // check object-style commit
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    const mutation = { type, payload }
    const entry = this._mutations[type]    // ①
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown mutation type: ${type}`)
      }
      return
    }
    this._withCommit(() => {    // ②
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
    this._subscribers.forEach(sub => sub(mutation, this.state))

    if (
      process.env.NODE_ENV !== 'production' &&
      options && options.silent
    ) {
      console.warn(
        `[vuex] mutation type: ${type}. Silent option has been removed. ` +
        'Use the filter functionality in the vue-devtools'
      )
    }
  }

commit 方法接收一些参数,比如我们例子中传入的 type ,根据 type 获取到可执行的数组 entry

const entry = this._mutations[type]    // ①

最后数组 entry_withCommit 的包裹下遍历执行

this._withCommit(() => {    // ②
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
})

分析到这里那么问题就拓展到了 store.commit('increment') 是怎么根据 type: increment 获取到 entry 的,执行完 entry 又是怎么更新了 state 的。

5.1、 module 的收集

要分析上面的问题我们还需再重新回到上面提到的 Store 的构造方法,构造方法有这么一步操作

installModule(this, state, [], this._modules.root)

其中:

function installModule (store, rootState, path, module, hot) {
  const local = module.context = makeLocalContext(store, namespace, path)  
  // module forEachMutation 方法能找到自己对应的 mutation
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    // 注册 mutation
    registerMutation(store, namespacedType, mutation, local) 
  })

  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    // 注册 action
    registerAction(store, type, handler, local) 
  })

  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    // 注册 getter
    registerGetter(store, namespacedType, getter, local)
  })
  
  // 递归注册  module.module.module
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

可见 installModule 里头做了一些注册相关的初始化操作,如 registerMutation ,还有后续我们会提到的 registerActionregisterGetter

关键先来看下 registerMutation

function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload)
  })
}

到这里应该就比较清晰整个 store.commit('increment') 的流程了。

// store
store.commit('increment') 
==>
// commit
const entry = this._mutations[type]
==>
// installModule
registerMutation(store, namespacedType, mutation, local) 
==>
// registerMutation
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
  handler.call(store, local.state, payload)
})

store.commit('increment') 需要通过 type_mutations 拿到可执行的 entry 遍历执行,_mutations 是通过 registerMutation 注册初始化的,_mutations 根据 type 初始化为为数组 store._mutations[type] = [] , 再往数组中 push 进去 mutationmutation 其实就是最最开始 往 Store 传入的初始化 options 配置了

简单梳理下数据结构:

  // options 
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    },
  }

Store 实例对象

image

mutations 下的方法会被收集到 Store_mutations 对象上,根据对应的 type 构造出 entry 数组,当
store.commit('increment') 时就会根据 type 找到 _mutations 对象对应的 _mutations 数组,对应执行。

5.2、 更新 state

上面只分析了 store.commit('increment') 执行的过程,漏掉了最重要的一步,commit 完最后是怎么更新 state 的呢。

还记得我们有提到的 _withCommit 方法吗:

  _withCommit (fn) {
    const committing = this._committing
    this._committing = true
    fn()  // 方法内部更新了 state
    this._committing = committing
  }

执行 _withCommit 方法会遍历执行 entry 方法。

this._withCommit(() => {    // ②
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
})

最终是执行了这个 handler(payload)handler 对应的就是处理 options 对应的 mutation方法了,如例子中的

  mutations: {
    increment (state) {  // 处理为 handler 方法
      state.count++  // 更新了 state 
    }
  }

最终的 state 更新就是在 mutations 对应 type 函数执行了更新。

至此我们完成了 store.commit('increment') 执行过程的分析

6、 state 状态的获取

store.state.count  // -> 1

源码:

  get state () {
    return this._vm._data.$$state
  }

  set state (v) {
    if (process.env.NODE_ENV !== 'production') { // 禁止直接更新 state 状态
      assert(false, `use store.replaceState() to explicit replace store state.`)
    }
  }

state 的获取也很简单,从 Store_vm 返回 _data 状态就是了,但禁止了直接更新 state 的状态,原因是稍后提到的 Vuex 的状态跟踪。

这里还有一个不明白的点是为什么 state 的获取是从 _vm 属性来的呢,原来 Vuex 初始化的时候有这么一步

resetStoreVM(store, state, hot)

其中:

function resetStoreVM (store, state, hot) {
  // ... 
  
  // Vuex依赖Vue核心实现数据的“响应式化”。
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  // ...
}

Storestate 挂载到了一个 Vue 实例上,利用 Vue 的双向绑定的原理来实现 Store state 的更新从而触发视图的更新。

7、store.dispatch

dispatchcommit 的原理差不多,如果你通过上面理解了 commit 的过程,那分析 dispatch 的过程就轻松多了。

dispatch (_type, _payload) {
  // check object-style dispatch
  const {
    type,
    payload
  } = unifyObjectStyle(_type, _payload)

  const action = { type, payload }
  const entry = this._actions[type] 

  const result = entry.length > 1
    ? Promise.all(entry.map(handler => handler(payload)))
    : entry[0](payload)

  return result.then(res => {
    try {
      this._actionSubscribers
        .filter(sub => sub.after)
        .forEach(sub => sub.after(action, this.state))
    } catch (e) {
      if (process.env.NODE_ENV !== 'production') {
        console.warn(`[vuex] error in after action subscribers: `)
        console.error(e)
      }
    }
    return res
  })
}

同样的,根据 type 获取到对应 _actions 属性下的 entry,不过和 commit 不同的是,这里获取到的 entry 放在了 Promise 下执行,这也就是 store.dispatch 触发的 action 能执行异步函数的原因。

  const result = entry.length > 1
    ? Promise.all(entry.map(handler => handler(payload)))
    : entry[0](payload)

   return result.then(res => {})

store.dispatch 触发了 actionaction 更新 state 需要通过 commit ,具体过程就上面分析 Store.commit 的过程了,不在赘述。

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

8、状态跟踪

Store 初始化时还有这么一段代码:

const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
   devtoolPlugin(this)
}

其中:

const target = typeof window !== 'undefined'
  ? window
  : typeof global !== 'undefined'
    ? global
    : {}
const devtoolHook = target.__VUE_DEVTOOLS_GLOBAL_HOOK__

export default function devtoolPlugin (store) {
  if (!devtoolHook) return

  store._devtoolHook = devtoolHook

  devtoolHook.emit('vuex:init', store)

  devtoolHook.on('vuex:travel-to-state', targetState => {
    store.replaceState(targetState)
  })

  store.subscribe((mutation, state) => { // 状态订阅
    devtoolHook.emit('vuex:mutation', mutation, state)
  })
}

devtoolPlugin 干嘛用的呢,它做了个状态的订阅,每当状态更新时会触发 store.subscribe ,从而做一次 vuex:mutationemit,配合控制台我们就能方便的跟踪到每次状态更新的数据情况了。

  store.subscribe((mutation, state) => { // 状态订阅
    devtoolHook.emit('vuex:mutation', mutation, state)
  })

image

利用控制台还可做 时间旅行,回退到任意时间节点的状态。

小结

我们从源码入手,分析了 Vuex 插件的安装过程,storecommit 过程,如何获取 storestate,以及 storeaction 过程,最后我们提了下 VuexVuex 配合控制台可方便做状态管理。中间隐藏了一些技术细节,想深入研究的同学可查看源码继续分析。

🏷你不知道的JavaScript-第一部分

第一章: 作用域是什么

1、 编译原理

JavaScript 被列为 ‘动态’ 或 ‘解释执行’ 语言,于其他传统语言(如 java)不同的是,JavaScript是边编译边执行的。
一段源码在执行前会经历三个步骤: 分词/词法分析 -> 解析/语法分析 -> 代码生成

  • 分词/词法分析

这个过程将字符串分解成词法单元,如 var a = 2; 会被分解成词法单元 var、 a、 = 、2、;。空格一般没意义会被忽略

  • 解析/语法分析

这个过程会将词法单元转换成 抽象语法树(Abstract Syntax Tree,AST)。
如 var a = 2; 对应的 抽象语法树 如下, 可通过 在线可视化AST 网址在线分析

{
  "type": "Program",
  "start": 0,
  "end": 10,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 10,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 4,
          "end": 9,
          "id": {
            "type": "Identifier",
            "start": 4,
            "end": 5,
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "start": 8,
            "end": 9,
            "value": 2,
            "raw": "2"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "module"
}
  • 代码生成

将 AST 转换成可执行的代码,存放于内存中,并分配内存和转化为一些机器指令

2、理解作用域

其实结合上面提到的编译原理,作用域就好理解了。作用域就是当前执行代码对这些标识符的访问权限。
编译器会在当前作用域中声明一些变量,运行时引擎会去作用域中查找这些变量(其实就是一个寻址的过程),如果找到这些变量就可以操作变量,找不到就往上一层作用域找(作用域链的概念),或者返回 null

第三章: 函数作用域和块作用域

1、函数中的作用域

每声明一个函数都会形成一个作用域,那作用域有什么用呢,它能让该作用域内的变量和函数不被外界访问到,也可以反过来说是不让该作用域内的变量或函数污染全局。

对比:

var a = 123
function bar() {
  //...
}

function foo() {
  var a = 123
  function bar() {
    //...
  }
}

变量 a 和函数 bar 用一个函数 foo 包裹起来,函数 foo 会形成一个作用域,变量 a 和函数 bar 外界将无法访问,同时变量或函数也不会污染全局。

2、函数作用域

进一步思考,上面例子的变量 a 和函数 bar 有了作用域,但函数 foo 不也是暴露在全局,也对全局�造成污染了啊。是的,JavaScript对这种情况提出了解决方案: 立即执行函数 (IIFE)

(function foo() {
  var a = 123
  function bar() {
    //...
  }
})()

第一个()将函数变成表达式,第二个()执行了这个函数,最终函数 foo 也形成了自己的作用域,不会污染到全局,同时也不被全局访问的到。

3、块作用域

es6之前JavaScript是没有块作用域这个概念的,这与一般的语言(如Java ,C)很大不同,看下面这个例子:

for (var i = 0; i < 10; i++) {
  console.log('i=', i);
}
console.log('输出', i); // 输出 10

for 循环定义了变量 i,通常我们只想这个变量 i 在循环内使用,但忽略了 i 其实是作用在外部作用域(函数或全局)的。所以循环过后也能正常打印出 i ,因为没有块的概念。

甚至连 try/catch 也没形成块作用域:

try {
  for (var i = 0; i < 10; i++) {
    console.log('i=', i);
  }
} catch (error) {}
console.log('输出', i); // 输出 10

解决方法1

形成块作用域的方法当然是使用 es6 的 let 和 const 了, let 为其声明的变量隐式的劫持了所在的块作用域。

for (let i = 0; i < 10; i++) {
  console.log('i=', i);
}
console.log('输出', i); // ReferenceError: i is not defined

将上面例子的 var 换成 let 最后输出就报错了 ReferenceError: i is not defined ,说明被 let 声明的 i 只作用在了 for 这个块中。

除了� let 会让 for、if、try/catch 等形成块,JavaScript 的 {} 也能形成块

{
  let name = '曾田生'
}

console.log(name); //ReferenceError: name is not defined

解决方法2

早在没 es6 的 let 声明之前,常用的做法是利用 函数也能形成作用域 这么个概念来解决一些问题的。

看个例子

function foo() {
  var result = []
  for (var i = 0; i < 10; i++) {
    result[i] = function () {
      return i
    }
  }
  console.log(i)// i 作用在整个函数,for 执行完此时 i 已经等于 10 了
  return result
}
var result = foo()
console.log(result[0]()); // 输出 10 期望 0
console.log(result[1]()); // 输出 10 期望 1
console.log(result[2]()); // 输出 10 期望 2

这个例子出现的问题是执行数组函数最终都输出了 10, 因为 i 作用在整个函数,for 执行完此时 i 已经等于 10 了, 所以当后续执行函数 result[x]() 内部返回的 i 已经是 10 了。

利用函数的作用域来解决

function foo() {
  var result = []
  for (var i = 0; i < 10; i++) {
    result[i] = function (num) {
      return function () { // 函数形成一个作用域,�内部变量被私有化了
        return num
      }
    }(i)
  }
  return result
}
var result = foo()
console.log(result[0]()); // 0
console.log(result[1]()); // 1
console.log(result[2]()); // 2

上面的例子也是挺典型的,一般面试题比较考基础的话就会被问道,上面例子不仅考察到了块作用域的概念,函数作用域的概念,还考察到了闭包的概念(闭包后续讲但不影响这个例子的理解),多琢磨一下就理解了。

第四章: 提升

提升指的是变量提升和函数提升,为什么JavaScript会有提升这个概念呢,其实也很好理解,因为JavaScript代码是先 编译执行 的,所以在编译阶段就会先对变量和函数做声明,在执行阶段就出现了所谓的变量提升和函数提升了。

1、变量提升

console.log(a); // undefined
var a = 1;

上面代码 console.log(a); // undefined 就是因为编译阶段先对变量做了声明,先声明了个变量 a, 并默认赋值 undefined

var a;
console.log(a); // undefined
a = 1;

2、函数提升

函数同样也存在提升,这就是为什么函数能先调用后声明了

foo();
function foo() {
  console.log('---foo----');
}

注意:函数表达式不会被提升

foo();
var foo = function() {
  console.log('---foo----');
}
// TypeError: foo is not a function

注意:函数会首先被提升,然后才是变量

var foo = 1;
foo();
function foo() {
  console.log('---foo----');
}
// TypeError: foo is not a function

分析一下,因为上面例子编译后是这样的

var foo = undefined; // 变量名赋值 undefined
function foo() {     // 函数先提升
  console.log('---foo----');
}
foo = 1;             // 但接下去是变量被重新赋值了 1,是个Number类型
foo();               // Number类型当然不能用函数方式调用,就报错了
// TypeError: foo is not a function

第五章: 作用域闭包

闭包问题一直会在JavaScript被提起,是JavaScript一个比较�奇葩的概念

1、闭包的产生

闭包的概念: 当函数可以记住并访问所在的词法作用域时,就产生了闭包

概念貌似挺简单的,简单分析下,首先闭包是 产生的,是在代码执行中产生的,有的一些网络博文直接将闭包定义为 某一个特殊函数 是错的。

闭包是怎么产生的呢,一个函数能访问到所在函数作用域就产生了闭包,注意到作用域的概念,咱们最上面的章节有提到,看下面例子:

function foo() {
  var a = 0;
  function bar() {
    a++;
    console.log(a);
  }
  return bar;
}

var bat = foo()
bat() // 1
bat() // 2
bat() // 3

结合例子分析一下: 函数 foo 内部返回了函数 bar ,外部声明个变量 bat 拿到 foo 返回的函数 bar ,执行 bat() 发现能正常输出 1 ,注意前面章节提到的作用域,变量 a 是在函数 foo 内部的一个私有变量,不能被外界访问的,但外部函数 bat 却能访问的到私有变量 a,这说明了 外部函数 bat �持有函数 foo 的作用域 ,也就产生了闭包。

闭包的形成有什么用呢,JavaScript 让闭包的存在明显有它的作用,其中一个作用是为了模块化,当然你也可以利用外部函数持有另一个函数作用域的闭包特性去做更多的事情,但这边就暂且讨论模块化这个作用。

函数有什么作用呢,私有化变量�或方法呀,那函数内的变量和方法被私有化了函数怎么和外部做 交流 呢, 暴露出一些变量或方法呀

function foo() {
  var _a = 0;
  var b = 0;
  function _add() {
    b = _a + 10    
  }
  function bar() {
    _add()
  }
  function getB() {
    return b
  }
  return {
    bar: bar,
    getB: getB
  }
}

var bat = foo()
bat.bar()
bat.getB() // 10

上面例子函数 foo 可以�理解为一个模块,内部声明了一些私有变量和方法,也对�外界暴露了一些方法,只是在执行的过程中顺带产生了一个闭包

2、模块机制

上面提到了闭包的产生和作用,�貌似在使用 es6语法 开发的�过程中很少用到了闭包,但实际上我们�一直在用闭包的概念的。

foo.js

var _a = 0;
var b = 0;
function _add() {
  b = _a + 10
}
function bar() {
  _add()
}
function getB() {
  return b
}
export default {
  bar: bar,
  getB: getB
}

bat.js

import bat from 'foo'

bat.bar()
bat.getB() // 10

上面例子是 es6 模块的写法,是不是�惊奇的发现变量 bat 可以记住并访问模块 foo 的作用域,这符合了�闭包的概念。

小结:

本章节我们深入理解了JavaScript的 作用域提升闭包等概念,希望你能有所收获,这也是我在读《你不知道的JavaScript·上卷》的一些体会。下一部分整理下 this解析对象原型 等一些概念。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.