Code Monkey home page Code Monkey logo

askwuxue.github.io's People

Contributors

askwuxue avatar

Watchers

 avatar  avatar

askwuxue.github.io's Issues

块级格式化上下文


title: 块级格式化上下文
date: 2018-04-17 13:41:10
tags: CSS
categories: CSS

格式化上下文

在解决外边距叠加的时候,MDN给出了这个概念,触发 了BFC不会发生外边距叠加,今天来看一下,理解不是特别深刻,今天通过查阅资料,解决这个问题。

MDN的定义

块级格式化上下文(Block Formatting Context,BFC) 是Web页面的可视化CSS渲染的一部分,是布局过程中生成块级盒子的区域,也是浮动元素与其他元素的交互限定区域。

看了几个文档都没有太看明白,借鉴:前端精选文摘-BFC背后的原理一文,基本上为我们解释清楚了,我决定,结合自己的理解与大佬的文章巩固一下。

前置知识

开始之前,需要了解下面三点。

  1. box:(盒)布局的基本单位。
    元素的类型和display属性,决定了这个box的类型。不同类型的box,会参与不同的 Formatting Content (决定如何渲染文档的容器)。因此。box内的元素会以不同的方式渲染。
  2. 不同类型的盒子都参与哪些 Formatting Content

**block-level box(块级盒) **
display属性为: blocklist-itemtable的元素会生成block-level box。参与block formatting content (块级格式化上下文)

inline-level box (行内盒)
display属性为inlineinline-blockinline-table的元素生成inline-level box。参与inline formatting content (IFC)

  1. Formatting Content
    Formatting content是 W3C CSS2.1 规范中的一个概念。它是页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及和其他元素的关系和相互作用。最常见的Formatting contextBlock fomatting context(简称BFC)和Inline formatting context(简称IFC)。
什么是BFC

BFC:块级格式化上下文(概念)。一个独立的渲染区域,只有块级以及盒子参与,并且规定了内部的块级盒子如何布局,并且和独立区域外部毫不相干。

BFC规定了内部块级的盒子如何布局,有一套BFC的布局规则。 如下

  1. 内部的Box会在垂直方向,一个接一个地放置。
  2. Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠
  3. 每个元素的margin box的左边,与包含块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。
  4. BFC的区域不会与float box重叠。
  5. BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。
  6. 计算BFC的高度时,浮动元素也参与计算
如何形成BFC
  1. 根元素或包含根元素的元素
  2. 浮动元素(元素的 float 不是 none)
  3. 绝对定位元素(元素的 position 为 absolute 或 fixed)
  4. display为inline-block, table-cell, table-caption, flex, inline-flex和inline-grid元素的直接子元素
  5. overflow 值不为 visible 的块元素
BFC的简单用法

直接拿大佬的过来,觉得总结的太精辟了

1. 自适应两栏布局
<style>
    body {
        width: 300px;
        position: relative;
    }
 
    .aside {
        width: 100px;
        height: 150px;
        float: left;
        background: #f66;
    }
 
    .main {
        height: 200px;
        background: #fcc;
    }
</style>
<body>
    <div class="aside"></div>
    <div class="main"></div>
</body>

页面

BFC1

应用BFC的规则

  1. 每个元素的margin box的左边,与包含块border box的左边相接触(对于从左往右的格式,反之相反)。即使存在浮动也是。
    此时,body元素的内部构成了一个BFC
    所以,即使aslide存在,main的左边依旧与包含块的左边相接触。
  2. BFC的区域不会与float box重叠
    这时候,我们通过main生成BFC
.main {
    overflow: hidden;
}

这之后,新的BFC不会与浮动的aside 重叠。因此,根据包含块的大小,和aside的宽度类调整自身的宽度。此时main不可设置宽度。

如下:

BFC2

2. 清除内部的浮动
<style>
    .par {
        border: 5px solid #fcc;
        width: 300px;
    }
 
    .child {
        border: 5px solid #f66;
        width:100px;
        height: 100px;
        float: left;
    }
</style>
<body>
    <div class="par">
        <div class="child"></div>
        <div class="child"></div>
    </div>
</body>

在这里,应用 计算BFC的高度时,浮动元素也参与计算设置 
.par {
	overflow: hidden;
}
BFC 会被浮动的元素撑开。清除浮动

效果图如下:

BFC3

BFC4

3. 防止margin 重叠
<style>
    p {
        color: #f55;
        background: #fcc;
        width: 200px;
        line-height: 100px;
        text-align:center;
        margin: 100px;
    }
</style>
<body>
    <p>Haha</p>
    <p>Hehe</p>
</body>

BFC5

Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠

两个P都在body构成的一个BFC中,可以将任意的一个p元素包裹在div标签中,通过div去触发另一个BFC。

<style>
    .wrap {
        overflow: hidden;
    }
</style>
<div class="wrap">
    <p>Haha</p>
</div>
<p>Hehe</p>>

BFC6

**总结:**BFC是CSS的精髓所在,只有深刻的理解BFC,然后加以实践,在布局方面在能更加的熟练。菜鸡一枚,继续努力呀。

keep-alive实现页面缓存


title: keep-alive实现页面缓存
date: 2021-02-09 09:31:30
tags: Vue
categories: Vue

1. 业务场景

​ 现在有A,B,两个页面,在B页面做一些操作,比如说通过输入搜索条件搜出相关的数据,然后点击一条数据,跳转到详情页A,在A页面点击返回按钮,B页面还保持离开前的状态,但是从B页面到A页面,A页面一直是最新的页面。

2. 具体做法

keep-alive使用

​ **注:**对于这个例子来说,我会将A页面和B页面都缓存。实际开发中,选择自己的要缓存的组件就可以,我只是为了演示这个例子。

<template>
  <div id="app">
    <nav>
      <router-link to="/">to A page</router-link>
      <router-link to="/about">to B page</router-link>
    </nav>
    <!-- 需要缓存的视图 -->
    <keep-alive>
      <router-view />
    </keep-alive>
  </div>
</template>
keep-alive的生命周期
  • 初次进入时:
    1. created > mounted > activated
    2. 退出后触发 deactivated
  • 再次进入:
    1. 只会触发 activated
  • 再次离开:
    1. 退出后触发 deactivated

了解了keep-alive的钩子函数后,结合业务,我们要在离开B页面时保存当前的位置。不用想,这个时候必须要监听scroll事件,什么时候监听呢?因为每次进入B页面,都可能会滚动页面,所以在activated钩子函数中做这件事无疑是最合适的。如下

<script>
export default {
  name: 'AboutView',
  components: {},
  data() {
    return {
      scrollTop: '',
    }
  },
  // 进入当前组件时监听scroll事件,并且将页面位置记录到data对象scrollTop中。
  // 根据保存的scrollTop值回到上一次离开页面的位置
  activated() {
    console.log('About activated ...')
    document.documentElement.scrollTop = this.scrollTop
    window.addEventListener('scroll', this.handlerScroll)
  },
  // 离开页面,移除scroll事件监听
  deactivated() {
    console.log('About deactivated ...')
    window.removeEventListener('scroll', this.handlerScroll)
  },
  methods: {
    handlerScroll() {
      this.scrollTop = document.documentElement.scrollTop
    },
  },
}
</script>

如上所示,当离开B页面时,记录了当前的位置,当再次进入时,根据之前保存的数据。回到离开前的位置。

3. 有哪些可以优化的点?

​ 可以注意到上面我们注册了scroll滚动事件,这种事件触发的频率比较高,一般会使用防抖或者节流来进行限制。

<script>
const _ = require('lodash')
export default {
  name: 'AboutView',
  components: {},
  data() {
    return {
      scrollTop: '',
      removeFlag: null,
    }
  },
  // 进入当前组件时监听scroll事件,并且将页面位置记录到data对象scrollTop中。
  // 根据保存的scrollTop值回到上一次离开页面的位置
  activated() {
    console.log('About activated ...')
    document.documentElement.scrollTop = this.scrollTop
    window.addEventListener('scroll', this.handlerDebounceScroll())
  },
  // 离开页面,移除scroll事件监听
  deactivated() {
    console.log('About deactivated ...')
    window.removeEventListener('scroll', this.handlerDebounceScroll())
  },
  methods: {
    handlerScroll() {
      this.scrollTop = document.documentElement.scrollTop
    },
    handlerDebounceScroll(delay = 500) {
      // 移除事件的标识
      if (!this.removeFlag) {
        this.removeFlag = _.debounce(this.handlerScroll, delay)
      }
      return this.removeFlag
    },
  },
}
</script>

项目搭建规范与配置


title: 项目搭建规范与配置
date: 2021-08-01 22:03:04
tags: 工程化
categories: 工程化

项目搭建规范与配置

统一代码规范的好处:

  • 规范的代码可以促进团队合作
  • 规范的代码可以降低维护成本
  • 规范的代码有助于 code review(代码审查)
  • 养成代码规范的习惯,有助于程序员自身的成长

当团队的成员都严格按照代码规范来写代码时,可以保证每个人的代码看起来都像是一个人写的,看别人的代码就像是在看自己的代码。更重要的是我们能够认识到规范的重要性,并坚持规范的开发习惯。

如何制订代码规范

css 代码规范

一. 代码规范

1.1. 集成editorconfig配置

EditorConfig 有助于为不同 IDE 编辑器上处理同一项目的多个开发人员维护一致的编码风格。

和Prettier一样,都是用来配置格式化你的代码的,这个格式化代码,要和你lint配置相符!否则会出现你格式化代码以后,却不能通过你的代码校验工具的检验

.editorconfig的自定义文件,用来定义项目的编码规范,编辑器的行为会与.editorconfig文件中定义的一致,并且其优先级比编辑器自身的设置要高,这在多人合作开发项目时十分有用而且必要。

有些编辑器默认支持editorConfig,如webstorm;而有些编辑器则需要安装editorConfig插件,如ATOM、Sublime、VS Code等。

当打开一个文件时,EditorConfig插件会在打开文件的目录和其每一级父目录查找.editorconfig文件。

EditorConfig的配置文件是从上往下读取的并且最近的EditorConfig配置文件会被最先读取. 匹配EditorConfig配置文件中的配置项会按照读取顺序被应用, 所以最近的配置文件中的配置项拥有优先权

如果.editorconfig文件没有进行某些配置,则使用编辑器默认的设置

editorconfig文件是定义一些格式化规则(此规则并不会被vscode直接解析)

# http://editorconfig.org

root = true

[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行首的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行

[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false

VSCode需要安装一个插件:EditorConfig for VS Code

image-20210722215138665

1.2. 使用prettier工具

Prettier 是一款强大的代码格式化工具,支持 JavaScript、TypeScript、CSS、SCSS、Less、JSX、Angular、Vue、GraphQL、JSON、Markdown 等语言,基本上前端能用到的文件格式它都可以搞定,是当下最流行的代码格式化工具。

1.安装prettier

npm install prettier -D

2.配置.prettierrc文件:

  • useTabs:使用tab缩进还是空格缩进,选择false;
  • tabWidth:tab是空格的情况下,是几个空格,选择2个;
  • printWidth:当行字符的长度,推荐80,也有人喜欢100或者120;
  • singleQuote:使用单引号还是双引号,选择true,使用单引号;
  • trailingComma:在多行输入的尾逗号是否添加,设置为 none
  • semi:语句末尾是否要加分号,默认值true,选择false表示不加;
{
  "useTabs": false,
  "tabWidth": 2,
  "printWidth": 80,
  "singleQuote": true,
  "trailingComma": "none",
  "semi": false
}

3.创建.prettierignore忽略文件

/dist/*
.local
.output.js
/node_modules/**

**/*.svg
**/*.sh

/public/*

4.VSCode需要安装prettier的插件

image-20210722214543454

5.测试prettier是否生效

  • 测试一:在代码中保存代码;
  • 测试二:配置一次性修改的命令;

在package.json中配置一个scripts:

    "prettier": "prettier --write ."

1.3. 使用ESLint检测

ESLint 是在 ECMAScript/JavaScript 代码中识别和报告模式匹配的工具,它的目标是保证代码的一致性和避免错误。

Eslint 可以在运行代码前就发现一些语法错误和潜在的 bug,极大地减轻测试人员的压力,减少软件项目的除错成本。同时,Eslint 允许开发者通过 rules 定义自己的代码规范,所以非常适合用于制定团队代码规范。

ESlint 在默认情况下是不开启任何自定义规则校验,只对错误的 ES5 语法和标准的语法错误进行检测,比如 const 这种 ES6 语法,还有莫名其妙的分号(如下图)。

由于 Eslint 和 Prettier 存在一些相同的规则,当同一个规则设置不同时,就会出现很诡异的现象:使用 prettier 格式化的代码,无法通过 eslint 校验。

1.在创建Vue项目的时候,我们选择了ESLint,所以Vue会默认帮助我们配置需要的ESLint环境。

2.VSCode需要安装ESLint插件:

image-20210722215933360

3.解决eslint和prettier冲突的问题:

安装插件:(vue在创建项目时,如果选择prettier,那么这两个插件会自动安装)

npm i eslint-plugin-prettier eslint-config-prettier -D

.eslintrc.js添加prettier插件:

  extends: [
    "plugin:vue/vue3-essential",
    "eslint:recommended",
    "@vue/typescript/recommended",
    "@vue/prettier",
    "@vue/prettier/@typescript-eslint",
    'plugin:prettier/recommended'
  ],

1.4. git Husky和eslint

虽然我们已经要求项目使用eslint了,但是不能保证组员提交代码之前都将eslint中的问题解决掉了:

  • 也就是我们希望保证代码仓库中的代码都是符合eslint规范的;

  • 那么我们需要在组员执行 git commit 命令的时候对其进行校验,如果不符合eslint规范,那么自动通过规范进行修复;

那么如何做到这一点呢?可以通过Husky工具:

  • husky是一个git hook工具,可以帮助我们触发git提交的各个阶段:pre-commit、commit-msg、pre-push

如何使用husky呢?

这里我们可以使用自动配置命令:

npx husky-init && npm install

这里会做三件事:

1.安装husky相关的依赖:

image-20210723112648927

2.在项目目录下创建 .husky 文件夹:

npx huksy install

image-20210723112719634

3.在package.json中添加一个脚本:

image-20210723112817691

接下来,我们需要去完成一个操作:在进行commit时,执行lint脚本:

// 添加husky hook
npx husky add .husky/pre-commit "npm test"

image-20210723112932943

这个时候我们执行git commit的时候会自动对代码进行lint校验。

1.5. git commit规范

1.5.1. 代码提交风格

通常我们的git commit会按照统一的风格来提交,这样可以快速定位每次提交的内容,方便之后对版本进行控制。

但是如果每次手动来编写这些是比较麻烦的事情,我们可以使用一个工具:Commitizen

  • Commitizen 是一个帮助我们编写规范 commit message 的工具;

1.安装Commitizen

npm install commitizen -D

2.安装cz-conventional-changelog,并且初始化cz-conventional-changelog:

npx commitizen init cz-conventional-changelog --save-dev --save-exact

这个命令会帮助我们安装cz-conventional-changelog:

image-20210723145249096

并且在package.json中进行配置:

这个时候我们提交代码需要使用 npx cz

  • 第一步是选择type,本次更新的类型
Type 作用
feat 新增特性 (feature)
fix 修复 Bug(bug fix)
docs 修改文档 (documentation)
style 代码格式修改(white-space, formatting, missing semi colons, etc)
refactor 代码重构(refactor)
perf 改善性能(A code change that improves performance)
test 测试(when adding missing tests)
build 变更项目构建或外部依赖(例如 scopes: webpack、gulp、npm 等)
ci 更改持续集成软件的配置文件和 package 中的 scripts 命令,例如 scopes: Travis, Circle 等
chore 变更构建流程或辅助工具(比如更改测试环境)
revert 代码回退
  • 第二步选择本次修改的范围(作用域)

image-20210723150147510

  • 第三步选择提交的信息

image-20210723150204780

  • 第四步提交详细的描述信息

image-20210723150223287

  • 第五步是否是一次重大的更改

image-20210723150322122

  • 第六步是否影响某个open issue

image-20210723150407822

我们也可以在scripts中构建一个命令来执行 cz:

image-20210723150526211

1.5.2. 代码提交验证

如果我们按照cz来规范了提交风格,但是依然有同事通过 git commit 按照不规范的格式提交应该怎么办呢?

  • 我们可以通过commitlint来限制提交;

1.安装 @commitlint/config-conventional 和 @commitlint/cli

npm i @commitlint/config-conventional @commitlint/cli -D

2.在根目录创建commitlint.config.js文件,配置commitlint

module.exports = {
  extends: ['@commitlint/config-conventional']
}

3.使用husky生成commit-msg文件,验证提交信息:

npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1"

简易版Vue-Router实现


title: 简易版Vue-Router实现
date: 2020-03-19 20:19:20
tags: Vue
categories: Vue

简易版Vue-Router实现

​ Vue-Router重要性不用说了。不想当一个只会使用的coder,尝试实现一个简易版的Vue-Router,来帮助自己加深理解掌握Vue-Router。Vue-Router有两种模式。默认模式hash,也就是会变更url中#后面的内容,不会刷新浏览器,另一种个是history模式,history模式一般配合服务端的返回来使用。本次实现history模式的Vue-Router(hash模式也是异曲同工),且适用于SPA,不依赖服务端的返回。

前置知识

插件、slot 插槽、混入、render 函数。

前置知识不清楚建议先阅读Vue官方文档,可以帮助更好理解。

Vue Router 的核心代码

// 注册插件
// Vue.use() 内部调用传入对象的 install 方法
Vue.use(VueRouter)

// 创建路由对象
const router = new VueRouter({
routes: [
{ name: 'home', path: '/', component: homeComponent }
]
})
// 创建 Vue 实例,注册 router 对象
new Vue({
router,
render: h => h(App)
}).$mount('#app')

实现思路

  • 创建 VueRouter 插件,静态方法 install
    • 判断插件是否已经被加载
    • 当 Vue 加载的时候把传入的 router 对象挂载到 Vue 实例上(注意:只执行一次)
  • 创建 VueRouter 类
    • 初始化,options、routeMap、data (创建 Vue 实例作为响应式数据记录当前路
      径)
    • initRouteMap() 遍历所有路由信息,把组件和路由的映射记录到 routeMap 对象中
    • 注册 popstate 事件,当路由地址发生变化,重新记录当前的路径
    • 创建 router-link 和 router-view 组件
    • 当路径改变的时候通过当前路径在 routerMap 对象中找到对应的组件,渲染 router-view

代码实现

let _Vue = null
export default class VueRouter {
  // 接受两个参数,一个是Vue的构造函数
  static install (Vue) {
    if (VueRouter.install.installed) {
      return
    }
    // 1. 判断当前插件是否安装
    VueRouter.install.installed = true
    // 2. Vue构造函数记录到全局,后续使用
    _Vue = Vue
    // 3. 把创建Vue实例时传入的router对象注入到Vue实例
    // TODO 混入 所有Vue实例以及组件上都会被混入
    _Vue.mixin({
      beforeCreate () {
        // 只有当前Vue实例上具有$options,才在Vue原型上挂载$router对象
        if (this.$options.router) {
          _Vue.prototype.$router = this.$options.router
          // 进行初始化
          this.$options.router.init()
        }
      }
    })
  }

  // 构造函数
  constructor (options) {
    this.options = options
    this.routeMap = {}
    // observable 方法让一个对象可响应 Vue 内部会用它来处理 data 函数返回的对象
    // 返回的对象可以直接用于渲染函数和计算属性内,并且会在发生变更时触发相应的更新
    this.data = _Vue.observable({
      current: '/'
    })
  }

  // 初始化操作
  init () {
    this.createRouteMap()
    this.initComponent(_Vue)
    this.intiEvent()
  }

  // 将传递给Vue-Router对象的options对象转换成routeMap
  createRouteMap () {
    this.options.routes.forEach(route => {
      this.routeMap[route.path] = route.component
    })
  }

  // 创建router-link
  initComponent (Vue) {
    Vue.component('router-link', {
      props: {
        to: String
      },
      methods: {
        clickHandler (e) {
          // pushState方法改变浏览器的地址栏,会被浏览器的历史记住 不刷新页面,不向服务器发送请求
          window.history.pushState({}, '', this.to)
          // router-link是Vue实例 都可以访问Vue.prototype 注册了$router对象
          this.$router.data.current = this.to
          // 阻止默认事件行为
          e.preventDefault()
        }
      },
      // Vue默认创构建的是运行时版本,不支持template,可以通过vue-cli配置或者使用render函数
      // template: `<a :href="to"><slot></slot></a>`
      render (h) {
        // h 函数创建虚拟DOM 第一个参数标签名,第二个参数是属性对象, 第三个参数是内容
        return h('a', {
          attrs: {
            'href': this.to
          },
          on: {
            click: this.clickHandler
          }
        }, [this.$slots.default])
      }
    })

    const self = this
    // 创建router-view
    Vue.component('router-view', {
      render (h) {
        // 获取路由组件
        const component = self.routeMap[self.data.current]
        return h(component)
      }
    })
  }

  // 初始化事件处理
  intiEvent () {
    // popstate 事件函数 当浏览器地址栏发生变化时触发
    window.addEventListener('popstate', () => {
      // 改变当前的路径 data 是一个响应对象发生变化后重新加载组件
      this.data.current = window.location.pathname
    })
  }
}

注意点

  1. 创建router-link和router-view组件时,没有使用template模板,因为vue-cli创建的项目默认使用的运行时版本的Vue。不支持template。解决方法如下

    • 如果想切换成带编译器版本的 Vue.js 。项目根目录创建 vue.config.js 文件,添加 runtimeCompiler
    module.exports = {
    runtimeCompiler: true
    }
    • 可以实现代码一样,不使用template,也不需要配置vue-cli,使用运行时版本支持的render函数对模板进行编译

ajax的原理简单分析


title: ajax的原理简单分析
date: 2018-04-15 11:43:20
tags: JavaScript
categories: JavaScript

关于ajax

​ Ajax的出现是为了解决局部刷新的问题的。在web的远古时代,使用的方式是服务器渲染的技术。也就是说所有内容在服务端进行校验。对当时的web服务来说,这样是完全可行的。因为当时的web主要功能是图文浏览。随着时代的发展,需求的增加。我们需要在网站上完成一些工作。比如说:登录,注册等功能。这个时候,如果使用服务端渲染。用户在注册页面,必须填写完整所有的信息后,将页面发送至服务端进行校验。如果失败的话,用户之前填写的所有内容全部丢失,对用户来说非常不友好。此时,需要一种技术来解决这种困境。Ajax诞生了,可以不用将整个页面发送到服务端进行校验,可以将页面的局部内容发送至服务端进行处理。Ajax的诞生,才有了web的蓬勃发展与各种各样的新奇玩法。用户体验与web的性能有了质的提升。Ajax的主要作用总结如下:

1. 实现了局部刷新,提升了用户体验。
ajax的实现原理
// ajax 实现是依靠XMLHttpRequest对象实现的,IE浏览器下是ActiveXobject对象
let xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');

// 参数分别是请求的方式,请求地址,同步(默认true)
xhr.open(method, url, true)		

// 等待浏览器响应
xhr.send();						

xhr.onreadystatechange = function () {
    if (this.readyState != 4) return
}
实现详解

第一点 xhr这个对象的状态readyState
0 当xhr对象创建的时候
1 open()方法调用的时候
2 已经接受到了响应的响应头
3 正在下载响应体
4 整个响应报文下载完毕

所以,大家看到定义的onreadystatechange方法,用来监听对象的状态变化,当readyState值为4,我们可以认为响应已经结束了。我们可以进行下来的处理了。虽然报文下载结束了,但是服务器一般有多种的情况,比如,我们常见的404,这是什么情况呢?其实,xhr对象上还有一个属性,xhr.status。这个属性值有很多,如下:

200——交易成功

404——没有发现文件、查询或URl

还有很多状态码。文章末尾总结一下

当浏览器返回http状态码的时候,我们可以利用xhr.status对响应的状态码进行获取然后处理。比如,ajax的时候,我们一般关心的是浏览器给我返回的是我想要的数据,那么这时候,对应的xhr.status是200,所以上述代码的最后一句的判断条件,在只关心服务器返回正常的情况下,是这样

if (this.readyState == 4 && this.status == 200) {
    console.log(this.response)			// 打印响应内容
}

第二点要解释的其实是send()方法,这个方法可以理解是向服务器请求数据的过程,我们说过了,服务器是一个响应的过程,想到了什么,没错,send()是一个异步的方法,加入不是异步的,我们send()下面的代码就一点意义也没有。个人认为,ajax的异步方式也是体现在这里。我们不用过怎么请求的,我们继续往下进行,到请求结束了,我们获取数据就行了。不会对下面的步骤产生影响。
基本原理已经清楚了,但是还有小小的不妥。主要是post请求导致的,post,一般我们理解是发送。在网络中,除了发送数据本身(请求体)以外,还需要请求体。所以
send()方法通过post请求的时候,会带着请求体,
假如:现在要登录,我要发送给服务器的就是我的账户username和密码password,send()的格式就是

send(`username=${username}&password=${password}`)
// 这种键值对的方式是 urlencoded

在这种情况下,我们必须为请求设置请求头的Content-Type,告诉服务器,请求体的格式如下

xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

到了这里,ajax的原理基本了解了,开始使用的时候,觉得有点麻烦,每次ajax那么多代码,当然是封装了。其他的框架或者库都是封装的,如jQuery的ajax,功能强大。

旧版ajax的封装(不推荐)
/*
obj       可选参数 
method    请求方式 (支持大小写)
url       请求地址
data      请求体    {}
callback  回调函数
 */

let ajax = (obj) => {
    const method = obj.method.toUpperCase();
    let url = obj.url;
    let data = obj.data;
    const callback = obj.callback;
    // 为了兼容IE浏览器
    const xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
    const tempArr = [];
    // 将用户传进来的对象转成urlencoded的格式
    for (key in data) {
        let value = data[key];
        tempArr.push(`${key}=${value}`);
    }
    let params = tempArr.join('&');
   	// get请求需要将数据拼接在URL后
    if (method == 'GET') {
        url = `${url}?${params}`;
    }
    xhr.open(method, url, true);
    data = null;
    // POST 设置requestHeader
    if (method == 'POST') {
        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        data = params;
    }
    xhr.send(data);
    xhr.onreadystatechange = function () {
        if (this.readyState == 4 && this.status == 200) {
            callback(this.responseText);
        }
    }
}
发送ajax请求
btn.onclick = function () {
                ajax({
                    method: 'POST',					//GET
                    url: 'http://localhost:3000/',
                    data: {username: username.value, password: password.value},
                    callback: function (res) {
                        alert(res);
                    }
                })
            }
服务端响应ajax
const http = require('http');
const url = require('url');
const querystring = require('querystring');

const server = http.createServer((req, res) => {
    res.writeHead(200, {"Content-Type": "text/plain", "Access-Control-Allow-Origin":"*"});
    // 注释的代码是对于get请求的响应
    // let body = url.parse(req.url, true);
    // let parse = querystring.parse(req.url)
    // console.log(body.query);
    // res.end(JSON.stringify(body.query));
    
    // 下面是对于post请求的响应
    let str = '';
    req.on('data', function (data) {
        str += data;
    });
    req.on('end', function () {
        const post_body = querystring.parse(str);
        res.end(JSON.stringify(post_body));
        console.log(post_body);
    });
   
})
server.listen(3000);
新版-ajax封装
const main = () => {

    const ajax = (options) => {

        // 默认参数对象
        const defaultObj = {
            type: '',
            url: '',
            header: {
                'Content-Type': 'application/application/x-www-form-urlencoded'
            },
            params: {},
            access: (data) => {
                console.log(data);
            },
            error: (err) => {
                console.log(err);
            }
        }

        // 用传递参数覆盖默认参数
        Object.assign(defaultObj, options)

        // 创建实例
        let xhr = new XMLHttpRequest();

        // 拼接参数字符串
        let paramStr = '';
        for (let key in defaultObj.params) {
            paramStr += `${key}=${defaultObj.params[key]}&`
        }

        // 去除最后一个&
        paramStr = paramStr.substring(0, paramStr.length - 1);

        // get请求
        if (defaultObj.type === 'GET') {

            // 设置请求方式及地址
            xhr.open(defaultObj.type, `${defaultObj.url}?${paramStr}`);

            // 发送请求 get请求不传参数 post请求向send()传递参数
            xhr.send();
        } else {

            // 设置请求方式及地址
            xhr.open(defaultObj.type, `${defaultObj.url}`);

            // 请求凡是是post必须设置请求头
            xhr.setRequestHeader('Content-Type', defaultObj.header['Content-Type'])

            // 根据请求头 发送不同格式数据
            if (defaultObj.header['Content-Type'] === 'application/x-www-form-urlencoded') {

                // TODO 请求头设置为application/x-www-form-urlencoded 格式send()中传递必须使用该格式
                // post请求通过send()传递参数
                xhr.send(paramStr);
            } else {
                xhr.send(JSON.stringify(defaultObj.params));
            }

        }
        // 请求成功
        xhr.onload = () => {
            let ContentType = xhr.getResponseHeader('Content-Type');
            let responseText = xhr.responseText;

            // TODO 服务器返回的永远是字符串
            if (ContentType.includes('application/json')) {
                responseText = JSON.parse(responseText);
            }

            // 请求成功
            if (xhr.status == 200) {
                defaultObj.access(responseText, xhr);
            } else {
                defaultObj.error(responseText, xhr);
            }
        }
    }
    ajax({
        type: 'POST',
        url: 'http://localhost:3000/post',
        header: {
            'Content-Type': 'application/json'
        },
        params: {
            name: "wuxue",
            age: 25
        },
    });

}

ajax的基本原理和基本的封装基本上明白了,我自己也验证了ajax请求的可行性,但是大家有没有发现,这里其实是几个不爽的,或者是遗留的问题

  1. 假如,我在ajax中得到了服务器响应值之后,进一步的处理是根据响应的数据进行再次的ajax请求呢?多次呢?想一想都觉得可怕!恐怕这就是回调黑洞了。
  2. 其实这里还有一个问题,请看服务端的代码。假如去除第五行中Access-Control-Allow-Origin":"*",这一句代码,其实ajax是无法使用的。

好了,可以开始解决上述的问题了,实践是检验的唯一标准

通过snabbdom源码调试分析深入Virtual DOM原理


title: 通过snabbdom源码调试分析深入Virtual DOM原理
date: 2021-06-03 19:03:10
tags: Vue
categories: Vue

Vue和React等库的一个比较共同的地方是使用了Virtual DOM。为什么会选择使用Virtual DOM,而不是像JQuery一样,只是把DOM的操作进行封装。主要的原始是Virtual DOM将真实DOM转成JavaScript对象,可以通过diff算法,找到最少的需要更新的DOM元素进行更新。如果页面的数据量比较大的话,相对来说可以保证一个比较高的性能。对于Vue这些框架来说的话,使用上手相对来说比较简单。但是了解其中的核心原理,可能会在代码层面写出性能更优的代码。所有,这也是学习Virtual DOM的原因。

Virtual DOM是什么?

  • Virtual DOM(虚拟 DOM),是由普通的 JS 对象来描述 DOM 对象,因为不是真实的 DOM 对象,所以叫 Virtual DOM

  • 可以使用 Virtual DOM 来描述真实 DOM,如下示例

    {
      sel: "div",
      data: {},
      children: undefined,
      text: "Hello Virtual DOM",
      elm: undefined,
      key: undefined
    }

为什么使用 Virtual DOM?

  • 手动操作 DOM 比较麻烦,还需要考虑浏览器兼容性问题,虽然有 jQuery 等库简化 DOM 操作,但是随着项目的复杂 DOM 操作复杂提升
  • Virtual DOM 的好处是当状态改变时不需要立即更新 DOM,只需要创建一个虚拟树来描述 DOM, Virtual DOM 内部将弄清楚如何有效(diff)的更新 DOM
  • 参考 github 上 virtual-dom 的描述
    • 虚拟 DOM 可以维护程序的状态,跟踪上一次的状态
    • 通过比较前后两次状态的差异更新真实 DOM

虚拟 DOM 有什么作用?

  • 维护视图和状态的关系

  • 复杂视图情况下提升渲染性能

  • 除了渲染 DOM 以外,还可以实现 SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native)、小程序(mpvue/uni-app)等

    image-20200102104642121

实现了Virtual DOM 的库

因为Virtual DOM的实现其实是更底层的,为了快速上手,我们可以借助github上的优秀开源Virtual DOM库,了解Virtual DOM的原理。

Demo

Snabbdom 基本使用

parcel是 Web 应用打包工具,适用于经验不同的开发者。它利用多核处理提供了极快的速度,并且不需要任何配置。

  • 创建项目,并安装 parcel

    # 创建项目目录
    md snabbdom-demo
    # 进入项目目录
    cd snabbdom-demo
    # 创建 package.json
    npm init -y
    # 本地安装 parcel
    npm install parcel-bundler -D
  • 配置 package.json 的 scripts

    "scripts": {
      "dev": "parcel index.html --open",
      "build": "parcel build index.html"
    }
  • 创建目录结构

    │  index.html
    │  package.json
    └─src
         		01-basicusage.js
# --depth 表示克隆深度, 1 表示只克隆最新的版本. 因为如果项目迭代的版本很多, 克隆会很慢
git clone -b v2.1.0 --depth=1 https://github.com/snabbdom/snabbdom.git

安装 Snabbdom

导入 Snabbdom

  • Snabbdom 的两个核心函数 init 和 h()
    • init() 是一个高阶函数,返回 patch()
    • h() 返回虚拟节点 VNode,这个函数我们在使用 Vue.js 的时候见过
import { init } from 'snabbdom/init'
import { h } from 'snabbdom/h'
const patch = init([])

注意:此时运行的话会告诉我们找不到 init / h 模块,因为模块路径并不是 snabbdom/int,这个路径是在 package.json 中的 exports 字段设置的,而我们使用的打包工具不支持 exports 这个字段,webpack 4 也不支持,webpack 5 支持该字段。该字段在导入 snabbdom/init 的时候会补全路径成 snabbdom/build/package/init.js

"exports": {
    "./init": "./build/package/init.js",
    "./h": "./build/package/h.js",
    "./helpers/attachto": "./build/package/helpers/attachto.js",
    "./hooks": "./build/package/hooks.js",
    "./htmldomapi": "./build/package/htmldomapi.js",
    "./is": "./build/package/is.js",
    "./jsx": "./build/package/jsx.js",
    "./modules/attributes": "./build/package/modules/attributes.js",
    "./modules/class": "./build/package/modules/class.js",
    "./modules/dataset": "./build/package/modules/dataset.js",
    "./modules/eventlisteners": "./build/package/modules/eventlisteners.js",
    "./modules/hero": "./build/package/modules/hero.js",
    "./modules/module": "./build/package/modules/module.js",
    "./modules/props": "./build/package/modules/props.js",
    "./modules/style": "./build/package/modules/style.js",
    "./thunk": "./build/package/thunk.js",
    "./tovnode": "./build/package/tovnode.js",
    "./vnode": "./build/package/vnode.js"
  }
  • 如果使用不支持 package.json 的 exports 字段的打包工具,我们应该把模块的路径写全
    • 查看安装的 snabbdom 的目录结构
import { h } from 'snabbdom/build/package/h'
import { init } from 'snabbdom/build/package/init'
import { classModule } from 'snabbdom/build/package/modules/class'
  • 回顾 Vue 中的 render 函数
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

使用

import { h } from 'snabbdom/build/package/h'
import { init } from 'snabbdom/build/package/init'

// 使用 init() 函数创建 patch()
// init() 的参数是数组,将来可以传入模块,处理属性/样式/事件等
let patch = init([])

// 使用 h() 函数创建 vnode
let vnode = h('div.cls', [
  h('h1', 'Hello Snabbdom'),
  h('p', '这是段落')
])

const app = document.querySelector('#app')
// 把 vnode 渲染到空的 DOM 元素(替换)
// 会返回新的 vnode
let oldVnode = patch(app, vnode)

setTimeout(() => {
  vnode = h('div.cls', [
    h('h1', 'Hello World'),
    h('p', '这是段落')
  ])
  // 把老的视图更新到新的状态
  oldVnode = patch(oldVnode, vnode)
  // h('!') 是创建注释
  patch(oldVnode, h('!'))
}, 2000)

模块

Snabbdom 的核心库并不能处理 DOM 元素的属性/样式/事件等,如果需要处理的话,可以使用模块

常用模块

  • 官方提供了 6 个模块

    • attributes
      • 设置 DOM 元素的属性,使用 setAttribute()
      • 处理布尔类型的属性
    • props
      • attributes 模块相似,设置 DOM 元素的属性 element[attr] = value
      • 不处理布尔类型的属性
    • class
      • 切换类样式
      • 注意:给元素设置类样式是通过 sel 选择器
    • dataset
      • 设置 data-* 的自定义属性
    • eventlisteners
      • 注册和移除事件
    • style
      • 设置行内样式,支持动画
      • delayed/remove/destroy

模块使用

  • 模块使用步骤:
    • 导入需要的模块
    • init() 中注册模块
    • 使用 h() 函数创建 VNode 的时候,可以把第二个参数设置为对象,其他参数往后移

代码

import { h } from 'snabbdom/build/package/h'
import { init } from 'snabbdom/build/package/init'
// 导入需要的模块
import { styleModule } from 'snabbdom/build/package/modules/style'
import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners'

// 使用 init() 函数创建 patch()
// init() 的参数是数组,将来可以传入模块,处理属性/样式/事件等
let patch = init([
  // 注册模块
  styleModule,
  eventListenersModule
])

// 使用 h() 函数创建 vnode
let vnode = h('div.cls', {
  // 设置 DOM 元素的行内样式
  style: { color: '#DEDEDE', backgroundColor: '#181A1B' },
  // 注册事件
  on: { click: clickHandler }
}, [
  h('h1', 'Hello Snabbdom'),
  h('p', '这是段落')
])

function clickHandler () {
  // 此处的 this 指向对应的 vnode
  console.log(this.elm.innerHTML)
}

Snabbdom 源码解析

Snabbdom 的核心

  • 使用 h() 函数创建 JavaScript 对象(VNode)描述真实 DOM
  • init() 设置模块,创建 patch()
  • patch() 比较新旧两个 VNode
  • 把变化的内容更新到真实 DOM 树上

Snabbdom 源码

  • 源码地址:

  • src 目录结构

    ├── package
    │   ├── helpers
    │   │   └── attachto.ts		定义了 vnode.ts 中 AttachData 的数据结构
    │   ├── modules
    │   │   ├── attributes.ts
    │   │   ├── class.ts
    │   │   ├── dataset.ts
    │   │   ├── eventlisteners.ts
    │   │   ├── hero.ts				example 中使用到的自定义钩子
    │   │   ├── module.ts			定义了模块中用到的钩子函数
    │   │   ├── props.ts
    │   │   └── style.ts
    │   ├── h.ts							h() 函数,用来创建 VNode
    │   ├── hooks.ts					所有钩子函数的定义
    │   ├── htmldomapi.ts			对 DOM API 的包装
    │   ├── init.ts						加载 modules、DOMAPI,返回 patch 函数
    │   ├── is.ts							判断数组和原始值的函数
    │   ├── jsx-global.ts			jsx 的类型声明文件
    │   ├── jsx.ts						处理 jsx
    │   ├── thunk.ts					优化处理,对复杂视图不可变值得优化
    │   ├── tovnode.ts				DOM 转换成 VNode
    │   ├── ts-transform-js-extension.cjs
    │   ├── tsconfig.json			ts 的编译配置文件
    │   └── vnode.ts					虚拟节点定义
    

h 函数

  • h() 函数介绍

    • 在使用 Vue 的时候见过 h() 函数

      new Vue({
        router,
        store,
        render: h => h(App)
      }).$mount('#app')
    • h() 函数最早见于 hyperscript,使用 JavaScript 创建超文本

    • Snabbdom 中的 h() 函数不是用来创建超文本,而是创建 VNode

  • 函数重载

    • 概念

      • 参数个数类型不同的函数
      • JavaScript 中没有重载的概念
      • TypeScript 中有重载,不过重载的实现还是通过代码调整参数
    • 重载的示意

      function add (a: number, b: number) {
        console.log(a + b)
      }
      function add (a: number, b: number, c: number) {
        console.log(a + b + c)
      }
      add(1, 2)
      add(1, 2, 3)
      function add (a: number, b: number) {
        console.log(a + b)
      }
      function add (a: number, b: string) {
        console.log(a + b)
      }
      add(1, 2)
      add(1, '2')
  • 源码位置:src/package/h.ts

    // h 函数的重载
    export function h (sel: string): VNode
    export function h (sel: string, data: VNodeData | null): VNode
    export function h (sel: string, children: VNodeChildren): VNode
    export function h (sel: string, data: VNodeData | null, children: VNodeChildren): VNode
    export function h (sel: any, b?: any, c?: any): VNode {
      var data: VNodeData = {}
      var children: any
      var text: any
      var i: number
      // 处理参数,实现重载的机制
      if (c !== undefined) {
        // 处理三个参数的情况
        // sel、data、children/text
        if (b !== null) {
          data = b
        }
        if (is.array(c)) {
          children = c
        } else if (is.primitive(c)) {
          text = c
        } else if (c && c.sel) {
          children = [c]
        }
      } else if (b !== undefined && b !== null) {
        if (is.array(b)) {
          children = b
        } else if (is.primitive(b)) {
          // 如果 c 是字符串或者数字
          text = b
        } else if (b && b.sel) {
          // 如果 b 是 VNode
          children = [b]
        } else { data = b }
      }
      if (children !== undefined) {
        // 处理 children 中的原始值(string/number)
        for (i = 0; i < children.length; ++i) {
          // 如果 child 是 string/number,创建文本节点
          if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined)
        }
      }
      if (
        sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
        (sel.length === 3 || sel[3] === '.' || sel[3] === '#')
      ) {
        // 如果是 svg,添加命名空间
        addNS(data, children, sel)
      }
      // 返回 VNode
      return vnode(sel, data, children, text, undefined)
    };

VNode

  • 一个 VNode 就是一个虚拟节点用来描述一个 DOM 元素,如果这个 VNode 有 children 就是 Virtual DOM

  • 源码位置:src/package/vnode.ts

export interface VNode {
  // 选择器
  sel: string | undefined;
  // 节点数据:属性/样式/事件等
  data: VNodeData | undefined;
  // 子节点,和 text 只能互斥
  children: Array<VNode | string> | undefined;
  // 记录 vnode 对应的真实 DOM
  elm: Node | undefined;
  // 节点中的内容,和 children 只能互斥
  text: string | undefined;
  // 优化用
  key: Key | undefined;
}

export function vnode (sel: string | undefined,
                      data: any | undefined,
                      children: Array<VNode | string> | undefined,
                      text: string | undefined,
                      elm: Element | Text | undefined): VNode {
  const key = data === undefined ? undefined : data.key
  return { sel, data, children, text, elm, key }
}

snabbdom

  • patch(oldVnode, newVnode)
  • 打补丁,把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理的旧节点
  • 对比新旧 VNode 是否相同节点(节点的 key 和 sel 相同)
  • 如果不是相同节点,删除之前的内容,重新渲染
  • 如果是相同节点,再判断新的 VNode 是否有 text,如果有并且和 oldVnode 的 text 不同,直接更新文本内容
  • 如果新的 VNode 有 children,判断子节点是否有变化,判断子节点的过程使用的就是 diff 算法
  • diff 过程只进行同层级比较

image-20200102103653779

init

  • **功能:**init(modules, domApi),返回 patch() 函数(高阶函数)

  • 为什么要使用高阶函数?

    • 因为 patch() 函数再外部会调用多次,每次调用依赖一些参数,比如:modules/domApi/cbs
    • 通过高阶函数让 init() 内部形成闭包,返回的 patch() 可以访问到 modules/domApi/cbs,而不需要重新创建
  • init() 在返回 patch() 之前,首先收集了所有模块中的钩子函数存储到 cbs 对象中

  • 源码位置:src/package/init.ts

    const hooks: Array<keyof Module> = ['create', 'update', 'remove', 'destroy', 'pre', 'post']
    export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
      let i: number
      let j: number
      const cbs: ModuleHooks = {
        create: [],
        update: [],
        remove: [],
        destroy: [],
        pre: [],
        post: []
      }
      // 初始化 api
      const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi
      // 把传入的所有模块的钩子方法,统一存储到 cbs 对象中
      // 最终构建的 cbs 对象的形式 cbs = [ create: [fn1, fn2], update: [], ... ]
    	for (i = 0; i < hooks.length; ++i) {
        // cbs['create'] = []
        cbs[hooks[i]] = []
        for (j = 0; j < modules.length; ++j) {
          // const hook = modules[0]['create']
          const hook = modules[j][hooks[i]]
          if (hook !== undefined) {
            (cbs[hooks[i]] as any[]).push(hook)
          }
        }
      }
      ……
      return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
        ……
      }
    }

patch

  • 功能:

    • 传入新旧 VNode,对比差异,把差异渲染到 DOM
    • 返回新的 VNode,作为下一次 patch() 的 oldVnode
  • 执行过程:

    • 首先执行模块中的钩子函数 pre
    • 如果 oldVnode 和 vnode 相同(key 和 sel 相同)
      • 调用 patchVnode(),找节点的差异并更新 DOM
    • 如果 oldVnode 是 DOM 元素
      • 把 DOM 元素转换成 oldVnode
      • 调用 createElm() 把 vnode 转换为真实 DOM,记录到 vnode.elm
      • 把刚创建的 DOM 元素插入到 parent 中
      • 移除老节点
      • 触发用户设置的 create 钩子函数
  • 源码位置:src/package/init.ts

    return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
      let i: number, elm: Node, parent: Node
      // 保存新插入节点的队列,为了触发钩子函数
      const insertedVnodeQueue: VNodeQueue = []
      // 执行模块的 pre 钩子函数
      for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
    	// 如果 oldVnode 不是 VNode,创建 VNode 并设置 elm 
      if (!isVnode(oldVnode)) {
        // 把 DOM 元素转换成空的 VNode
        oldVnode = emptyNodeAt(oldVnode)
      }
    	// 如果新旧节点是相同节点(key 和 sel 相同)
      if (sameVnode(oldVnode, vnode)) {
        // 找节点的差异并更新 DOM
        patchVnode(oldVnode, vnode, insertedVnodeQueue)
      } else {
        // 如果新旧节点不同,vnode 创建对应的 DOM
        // 获取当前的 DOM 元素
        elm = oldVnode.elm!
        parent = api.parentNode(elm) as Node
    		// 触发 init/create 钩子函数,创建 DOM
        createElm(vnode, insertedVnodeQueue)
    
        if (parent !== null) {
          // 如果父节点不为空,把 vnode 对应的 DOM 插入到文档中
          api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
          // 移除老节点
          removeVnodes(parent, [oldVnode], 0, 0)
        }
      }
    	// 执行用户设置的 insert 钩子函数
      for (i = 0; i < insertedVnodeQueue.length; ++i) {
        insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
      }
      // 执行模块的 post 钩子函数
      for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
      return vnode
    }

createElm

  • 功能:

    • createElm(vnode, insertedVnodeQueue),返回创建的 DOM 元素
    • 创建 vnode 对应的 DOM 元素
  • 执行过程:

    • 首先触发用户设置的 init 钩子函数
    • 如果选择器是!,创建评论节点
    • 如果选择器为空,创建文本节点
    • 如果选择器不为空
      • 解析选择器,设置标签的 id 和 class 属性
      • 执行模块create 钩子函数
      • 如果 vnode 有 children,创建子 vnode 对应的 DOM,追加到 DOM 树
      • 如果 vnode 的 text 值是 string/number,创建文本节点并追击到 DOM 树
      • 执行用户设置的 create 钩子函数
      • 如果有用户设置的 insert 钩子函数,把 vnode 添加到队列中
  • 源码位置:src/package/init.ts

      function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
        let i: any
        let data = vnode.data
        
        if (data !== undefined) {
          // 执行用户设置的 init 钩子函数
          const init = data.hook?.init
          if (isDef(init)) {
            init(vnode)
            data = vnode.data
          }
        }
        const children = vnode.children
        const sel = vnode.sel
        if (sel === '!') {
          // 如果选择器是!,创建注释节点
          if (isUndef(vnode.text)) {
            vnode.text = ''
          }
          vnode.elm = api.createComment(vnode.text!)
        } else if (sel !== undefined) {
          // 如果选择器不为空
          // 解析选择器
          // Parse selector
          const hashIdx = sel.indexOf('#')
          const dotIdx = sel.indexOf('.', hashIdx)
          const hash = hashIdx > 0 ? hashIdx : sel.length
          const dot = dotIdx > 0 ? dotIdx : sel.length
          const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel
          const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
            ? api.createElementNS(i, tag)
            : api.createElement(tag)
          if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot))
          if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '))
          // 执行模块的 create 钩子函数
          for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)
          // 如果 vnode 中有子节点,创建子 vnode 对应的 DOM 元素并追加到 DOM 树上
          if (is.array(children)) {
            for (i = 0; i < children.length; ++i) {
              const ch = children[i]
              if (ch != null) {
                api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))
              }
            }
          } else if (is.primitive(vnode.text)) {
            // 如果 vnode 的 text 值是 string/number,创建文本节点并追加到 DOM 树
            api.appendChild(elm, api.createTextNode(vnode.text))
          }
          const hook = vnode.data!.hook
          if (isDef(hook)) {
            // 执行用户传入的钩子 create
            hook.create?.(emptyNode, vnode)
            if (hook.insert) {
              // 把 vnode 添加到队列中,为后续执行 insert 钩子做准备
              insertedVnodeQueue.push(vnode)
            }
          }
        } else {
          // 如果选择器为空,创建文本节点
          vnode.elm = api.createTextNode(vnode.text!)
        }
        // 返回新创建的 DOM                                
        return vnode.elm
      }

patchVnode

  • 功能:

    • patchVnode(oldVnode, vnode, insertedVnodeQueue)
    • 对比 oldVnode 和 vnode 的差异,把差异渲染到 DOM
  • 执行过程:

    • 首先执行用户设置的 prepatch 钩子函数
    • 执行 create 钩子函数
      • 首先执行模块create 钩子函数
      • 然后执行用户设置的 create 钩子函数
    • 如果 vnode.text 未定义
      • 如果 oldVnode.childrenvnode.children 都有值
        • 调用 updateChildren()
        • 使用 diff 算法对比子节点,更新子节点
      • 如果 vnode.children 有值,oldVnode.children 无值
        • 清空 DOM 元素
        • 调用 addVnodes(),批量添加子节点
      • 如果 oldVnode.children 有值,vnode.children 无值
        • 调用 removeVnodes(),批量移除子节点
      • 如果 oldVnode.text 有值
        • 清空 DOM 元素的内容
    • 如果设置了 vnode.text 并且和和 oldVnode.text 不等
      • 如果老节点有子节点,全部移除
      • 设置 DOM 元素的 textContentvnode.text
    • 最后执行用户设置的 postpatch 钩子函数
  • 源码位置:src/package/init.ts

    function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
        const hook = vnode.data?.hook
        // 首先执行用户设置的 prepatch 钩子函数
        hook?.prepatch?.(oldVnode, vnode)
        const elm = vnode.elm = oldVnode.elm!
        const oldCh = oldVnode.children as VNode[]
        const ch = vnode.children as VNode[]
      	// 如果新老 vnode 相同返回
        if (oldVnode === vnode) return
        if (vnode.data !== undefined) {
          // 执行模块的 update 钩子函数
          for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
          // 执行用户设置的 update 钩子函数
          vnode.data.hook?.update?.(oldVnode, vnode)
        }
      	// 如果 vnode.text 未定义
        if (isUndef(vnode.text)) {
          // 如果新老节点都有 children
          if (isDef(oldCh) && isDef(ch)) {
            // 调用 updateChildren 对比子节点,更新子节点
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
          } else if (isDef(ch)) {
            // 如果新节点有 children,老节点没有 children
          	// 如果老节点有text,清空dom 元素的内容
            if (isDef(oldVnode.text)) api.setTextContent(elm, '')
            // 批量添加子节点
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
          } else if (isDef(oldCh)) {
            // 如果老节点有children,新节点没有children
          	// 批量移除子节点
            removeVnodes(elm, oldCh, 0, oldCh.length - 1)
          } else if (isDef(oldVnode.text)) {
            // 如果老节点有 text,清空 DOM 元素
            api.setTextContent(elm, '')
          }
        } else if (oldVnode.text !== vnode.text) {
          // 如果没有设置 vnode.text
          if (isDef(oldCh)) {
            // 如果老节点有 children,移除
            removeVnodes(elm, oldCh, 0, oldCh.length - 1)
          }
          // 设置 DOM 元素的 textContent 为 vnode.text
          api.setTextContent(elm, vnode.text!)
        }
        // 最后执行用户设置的 postpatch 钩子函数
        hook?.postpatch?.(oldVnode, vnode)
      }

updateChildren

  • 功能:

    • diff 算法的核心,对比新旧节点的 children,更新 DOM
  • 执行过程:

    • 要对比两棵树的差异,我们可以取第一棵树的每一个节点依次和第二课树的每一个节点比较,但是这样的时间复杂度为 O(n^3)
    • 在DOM 操作的时候我们很少很少会把一个父节点移动/更新到某一个子节点
    • 因此只需要找同级别的子节点依次比较,然后再找下一级别的节点比较,这样算法的时间复杂度为 O(n)

    image-20200102103653779

    • 在进行同级别节点比较的时候,首先会对新老节点数组的开始和结尾节点设置标记索引,遍历的过程中移动索引
    • 在对开始和结束节点比较的时候,总共有四种情况
      • oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
      • oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
      • oldStartVnode / oldEndVnode (旧开始节点 / 新结束节点)
      • oldEndVnode / newStartVnode (旧结束节点 / 新开始节点)

    image-20200109184608649

    • 开始节点和结束节点比较,这两种情况类似

      • oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
      • oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
    • 如果 oldStartVnode 和 newStartVnode 是 sameVnode (key 和 sel 相同)

      • 调用 patchVnode() 对比和更新节点
      • 把旧开始和新开始索引往后移动 oldStartIdx++ / oldEndIdx++

      image-20200103121812840

    • oldStartVnode / newEndVnode (旧开始节点 / 新结束节点) 相同

      • 调用 patchVnode() 对比和更新节点
      • 把 oldStartVnode 对应的 DOM 元素,移动到右边
        - 更新索引

    image-20200103125428541

    • oldEndVnode / newStartVnode (旧结束节点 / 新开始节点) 相同

      • 调用 patchVnode() 对比和更新节点
      • 把 oldEndVnode 对应的 DOM 元素,移动到左边
      • 更新索引

      image-20200103125735048

    • 如果不是以上四种情况

      • 遍历新节点,使用 newStartNode 的 key 在老节点数组中找相同节点
      • 如果没有找到,说明 newStartNode 是新节点
        • 创建新节点对应的 DOM 元素,插入到 DOM 树中
      • 如果找到了
        • 判断新节点和找到的老节点的 sel 选择器是否相同
        • 如果不相同,说明节点被修改了
          • 重新创建对应的 DOM 元素,插入到 DOM 树中
        • 如果相同,把 elmToMove 对应的 DOM 元素,移动到左边

    image-20200109184822439

    • 循环结束
      • 当老节点的所有子节点先遍历完 (oldStartIdx > oldEndIdx),循环结束
      • 新节点的所有子节点先遍历完 (newStartIdx > newEndIdx),循环结束
    • 如果老节点的数组先遍历完(oldStartIdx > oldEndIdx),说明新节点有剩余,把剩余节点批量插入到右边

    image-20200103150918335

    • 如果新节点的数组先遍历完(newStartIdx > newEndIdx),说明老节点有剩余,把剩余节点批量删除

    image-20200109194751093

  • 源码位置:src/package/init.ts

    function updateChildren (parentElm: Node,
      oldCh: VNode[],
      newCh: VNode[],
      insertedVnodeQueue: VNodeQueue) {
      let oldStartIdx = 0
      let newStartIdx = 0
      let oldEndIdx = oldCh.length - 1
      let oldStartVnode = oldCh[0]
      let oldEndVnode = oldCh[oldEndIdx]
      let newEndIdx = newCh.length - 1
      let newStartVnode = newCh[0]
      let newEndVnode = newCh[newEndIdx]
      let oldKeyToIdx: KeyToIndexMap | undefined
      let idxInOld: number
      let elmToMove: VNode
      let before: any
    
      while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        // 索引变化后,可能会把节点设置为空
        if (oldStartVnode == null) {
          // 节点为空移动索引
          oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left
        } else if (oldEndVnode == null) {
          oldEndVnode = oldCh[--oldEndIdx]
        } else if (newStartVnode == null) {
          newStartVnode = newCh[++newStartIdx]
        } else if (newEndVnode == null) {
          newEndVnode = newCh[--newEndIdx]
        // 比较开始和结束节点的四种情况
        } else if (sameVnode(oldStartVnode, newStartVnode)) {
          // 1. 比较老开始节点和新的开始节点
          patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
          oldStartVnode = oldCh[++oldStartIdx]
          newStartVnode = newCh[++newStartIdx]
        } else if (sameVnode(oldEndVnode, newEndVnode)) {
          // 2. 比较老结束节点和新的结束节点
          patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
          oldEndVnode = oldCh[--oldEndIdx]
          newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
          // 3. 比较老开始节点和新的结束节点
          patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
          api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))
          oldStartVnode = oldCh[++oldStartIdx]
          newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
          // 4. 比较老结束节点和新的开始节点
          patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
          api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
          oldEndVnode = oldCh[--oldEndIdx]
          newStartVnode = newCh[++newStartIdx]
        } else {
          // 开始节点和结束节点都不相同
          // 使用 newStartNode 的 key 再老节点数组中找相同节点
          // 先设置记录 key 和 index 的对象
          if (oldKeyToIdx === undefined) {
            oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
          }
          // 遍历 newStartVnode, 从老的节点中找相同 key 的 oldVnode 的索引
          idxInOld = oldKeyToIdx[newStartVnode.key as string]
          // 如果是新的vnode
          if (isUndef(idxInOld)) { // New element
            // 如果没找到,newStartNode 是新节点
            // 创建元素插入 DOM 树
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
          } else {
            // 如果找到相同 key 相同的老节点,记录到 elmToMove 遍历
            elmToMove = oldCh[idxInOld]
            if (elmToMove.sel !== newStartVnode.sel) {
              // 如果新旧节点的选择器不同
              // 创建新开始节点对应的 DOM 元素,插入到 DOM 树中
              api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
            } else {
              // 如果相同,patchVnode()
              // 把 elmToMove 对应的 DOM 元素,移动到左边
              patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
              oldCh[idxInOld] = undefined as any
              api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
            }
          }
          // 重新给 newStartVnode 赋值,指向下一个新节点
          newStartVnode = newCh[++newStartIdx]
        }
      }
      // 循环结束,老节点数组先遍历完成或者新节点数组先遍历完成
      if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
        if (oldStartIdx > oldEndIdx) {
          // 如果老节点数组先遍历完成,说明有新的节点剩余
          // 把剩余的新节点都插入到右边
          before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
          addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
        } else {
          // 如果新节点数组先遍历完成,说明老节点有剩余
          // 批量删除老节点
          removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
        }
      }
    }

调试 updateChildren

<ul>
  <li>首页</li>
  <li>微博</li>
  <li>视频</li>
</ul>

<ul>
  <li>首页</li>
  <li>视频</li>
  <li>微博</li>
</ul>

image-20200112120036948

JavaScript中各种for循环的使用场景以及性能


title: JavaScript中各种for循环的使用场景以及性能
date: 2018-11-02 22:43:10
tags: JavaScript
categories: JavaScript

JavaScript中各种for循环的使用场景以及性能

JavaScript中的for循环,forEach循环,for...in循环,for...of循环。你能清楚的区分在哪些场景下应该那种循环方式吗?让我们来一起看一下。今天在掘金看到一篇文章,自己也算是有了一丢丢的收获,应该可以写一写。

1. 运行速度

请看下面的测试代码,比较了在同样条件下,forEach循环,for...in循环,for...of循环的耗时情况。
1.1 创造一个有1000000个元素的数组

let arr = Array(1000000);
for (let i = arr.length; i--; i) {
    arr[i] = i;
}

1.2 forEach循环,for...in循环,for...of循环的耗时

console.time('for');
for (let i = arr.length; i >= 0; i--) {}
console.timeEnd('for');

console.time('forEach');
arr.forEach((v) => v)
console.timeEnd('forEach');

console.time('for-of');
for (let key of arr) {}
console.timeEnd('for-of');

console.time('for-in');
for (let key in arr) {}
console.timeEnd('for-in');

// 运行时间
// for: 4.077ms
// forEach: 26.605ms
// for-of: 53.678ms
// for-in: 533.47ms

上述的结果,在不同的环境下。可能会存在不同,但是整体趋势不变,速度最快的是for循环,然后依次是ForEachfor offor in
注意:请注意上述1.1,使用Array()生成数组后又将数组的内容进行了重新赋值。因为Array(1000000)生成的是具有1000000空位的数组。对于数组内的空位,循环的处理不同。ForEach会跳过空位,for of依旧会遍历空位。因此,如果数组内全部都是空位,会发现forEach速度最快。具体可以参考阮一峰老师的ES6教程

1.3 倒序的for循环可能会更快一些

来看两种for循环的不同写法。

正序

console.time('for-asc');
for (let i = 0; i < arr.length; i++) {}
console.timeEnd('for-asc');

倒序

console.time('for-desc');
for (let i = arr.length; i >= 0; i--) {}
console.timeEnd('for-desc');

这两种方式的差别,是for循环的中**condition**的差别,正序的方式,每一次都要获取数组的长度,然后和i进行大小比较。倒序的方式只需要获取一次数组的长度。因此,数据量巨大时。倒序的方式可能更快一点。当然,将数组的长度赋值给一个变量。每一次循环只需要获取一次这个变量的值是最优雅的写法。也就不存在正序块还是倒序快的说法了。通过这个小知识点,其实想表达的是,写代码真的有很多可以推敲的地方。优化代码的地方,这些我们容易形成思维定式的地方。藏着太多的知识点和

这个地方是倒序,比for循环的正序要快,因为倒序只需要取一次arr.length

2. for循环for in循环for of循环forEach循环的差异与使用场景

​ for循环的可读性较差,应该是指循环数组和对象的时候,会有一定的不优雅,产生了其他的几种for循环的变形。这些变形,除了上述,我们测试的效率不同之外,场景和方法也略有不同。

2.1 ForEach

// 接受一个回调函数,可以对数组的每一项进行处理。
arr.forEach((item, index, arr) => {})

forEach不能像for循环一样使用brake循环跳出当前循环。目前的唯一方法是抛出异常跳出循环,这应该不是我们想要的。

2.2 for...in

for(let key in target) {
    // 做些什么
}

for in 以任意顺序遍历一个对象的除了Symbol以外的可枚举属性,会遍历原型上的属性。因为,最好不要在数组中使用,一般遍历数组,我们都希望按照索引输出。for in 会在意料之外

2.3 ``for of

for (variable of iterable) {
    //statements
}

for...of语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句

for of 只在可迭代的对象上才可以执行,普通对象不是一个可迭代对象,所以不可以使用for of

根据for循环for in循环for of循环forEach循环的不同的特性,在不同的场景,选择最合适的方式才是最优解。

Virtual DOM 及 Diff 算法小结


title: Virtual DOM 及 Diff 算法小结
date: 2021-05-11 09:23:31
tags: React
categories: React

Virtual DOM 及 Diff 算法小结

1. JSX 到底是什么

使用 React 就一定会写 JSX,JSX 到底是什么呢?它是一种 JavaScript 语法的扩展,React 使用它来描述用户界面长成什么样子。虽然它看起来非常像 HTML,但它确实是 JavaScript 。在 React 代码执行之前,Babel 会对将 JSX 编译为 React API.

<div className="container">
  <h3>Hello React</h3>
  <p>React is great </p>
</div>
React.createElement(
  "div",
  {
    className: "container"
  },
  React.createElement("h3", null, "Hello React"),
  React.createElement("p", null, "React is great")
);

从两种语法对比来看,JSX 语法的出现是为了让 React 开发人员编写用户界面代码更加轻松。

Babel REPL

2. DOM 操作问题

在现代 web 应用程序中使用 JavaScript 操作 DOM 是必不可少的,但遗憾的是它比其他大多数 JavaScript 操作要慢的多。

大多数 JavaScript 框架对于 DOM 的更新远远超过其必须进行的更新,从而使得这种缓慢操作变得更糟。

例如假设你有包含十个项目的列表,你仅仅更改了列表中的第一项,大多数 JavaScript 框架会重建整个列表,这比必要的工作要多十倍。

更新效率低下已经成为严重问题,为了解决这个问题,React 普及了一种叫做 Virtual DOM 的东西,Virtual DOM 出现的目的就是为了提高 JavaScript 操作 DOM 对象的效率。

3. 什么是 Virtual DOM

在 React 中,每个 DOM 对象都有一个对应的 Virtual DOM 对象,它是 DOM 对象的 JavaScript 对象表现形式,其实就是使用 JavaScript 对象来描述 DOM 对象信息,比如 DOM 对象的类型是什么,它身上有哪些属性,它拥有哪些子元素。

可以把 Virtual DOM 对象理解为 DOM 对象的副本,但是它不能直接显示在屏幕上。

<div className="container">
  <h3>Hello React</h3>
  <p>React is great </p>
</div>
{
  type: "div",
  props: { className: "container" },
  children: [
    {
      type: "h3",
      props: null,
      children: [
        {
          type: "text",
          props: {
            textContent: "Hello React"
          }
        }
      ]
    },
    {
      type: "p",
      props: null,
      children: [
        {
          type: "text",
          props: {
            textContent: "React is great"
          }
        }
      ]
    }
  ]
}

4. Virtual DOM 如何提升效率

精准找出发生变化的 DOM 对象,只更新发生变化的部分。

在 React 第一次创建 DOM 对象后,会为每个 DOM 对象创建其对应的 Virtual DOM 对象,在 DOM 对象发生更新之前,React 会先更新所有的 Virtual DOM 对象,然后 React 会将更新后的 Virtual DOM 和 更新前的 Virtual DOM 进行比较,从而找出发生变化的部分,React 会将发生变化的部分更新到真实的 DOM 对象中,React 仅更新必要更新的部分。

Virtual DOM 对象的更新和比较仅发生在内存中,不会在视图中渲染任何内容,所以这一部分的性能损耗成本是微不足道的。

1

<div id="container">
	<p>Hello React</p>
</div>
<div id="container">
	<p>Hello Angular</p>
</div>
const before = {
  type: "div",
  props: { id: "container" },
  children: [
    {
      type: "p",
      props: null,
      children: [
        { type: "text", props: { textContent: "Hello React" } }
      ]
    }
  ]
}
const after = {
  type: "div",
  props: { id: "container" },
  children: [
    {
      type: "p",
      props: null,
      children: [
        { type: "text", props: { textContent: "Hello Angular" } }
      ]
    }
  ]
}

5. 创建 Virtual DOM

在 React 代码执行前,JSX 会被 Babel 转换为 React.createElement 方法的调用,在调用 createElement 方法时会传入元素的类型,元素的属性,以及元素的子元素,createElement 方法的返回值为构建好的 Virtual DOM 对象。

{
  type: "div",
  props: null,
  children: [{type: "text", props: {textContent: "Hello"}}]
}
/**
 * 创建 Virtual DOM
 * @param {string} type 类型
 * @param {object | null} props 属性
 * @param  {createElement[]} children 子元素
 * @return {object} Virtual DOM
 */
function createElement (type, props, ...children) {
	return {
    type,
    props,
    children
  } 
}

从 createElement 方法的第三个参数开始就都是子元素了,在定义 createElement 方法时,通过 ...children 将所有的子元素放置到 children 数组中。

const virtualDOM = (
  <div className="container">
    <h1>你好 Tiny React</h1>
    <h2>(编码必杀技)</h2>
    <div>
      嵌套1 <div>嵌套 1.1</div>
    </div>
    <h3>(观察: 这个将会被改变)</h3>
    {2 == 1 && <div>如果2和1相等渲染当前内容</div>}
    {2 == 2 && <div>2</div>}
    <span>这是一段内容</span>
    <button onClick={() => alert("你好")}>点击我</button>
    <h3>这个将会被删除</h3>
    2, 3
  </div>
)
console.log(virtualDOM)

通过以上代码测试,发现返回的 Virtual DOM 存在一些问题,第一个问题是文本节点被直接放入到了数组中

2

而我们期望是文本节点应该是这样的

children: [
  {
    type: "text",
    props: {
      textContent: "React is great"
    }
  }
]

通过以下代码对 Virtual DOM 进行改造,重新构建 Virtual DOM。

// 将原有 children 拷贝一份 不要在原有数组上进行操作
const childElements = [].concat(...children).map(child => {
  // 判断 child 是否是对象类型
  if (child instanceof Object) {
    // 如果是 什么都不需要做 直接返回即可
    return child
  } else {
    // 如果不是对象就是文本 手动调用 createElement 方法将文本转换为 Virtual DOM
    return createElement("text", { textContent: child })
  }
})
return {
  type,
  props,
  children: childElements
}

3

通过观察返回的 Virtual DOM,文本节点已经被转化成了对象类型的 Virtual DOM,但是布尔值也被当做文本节点被转化了,在 JSX 中,如果 Virtual DOM 被转化为了布尔值或者null,是不应该被更新到真实 DOM 中的,所以接下来要做的事情就是清除 Virtual DOM 中的布尔值和null。

// 由于 map 方法无法从数据中刨除元素, 所以此处将 map 方法更改为 reduce 方法
const childElements = [].concat(...children).reduce((result, child) => {
  // 判断子元素类型 刨除 null true false
  if (child != null && child != false && child != true) {
    if (child instanceof Object) {
      result.push(child)
    } else {
      result.push(createElement("text", { textContent: child }))
    }
  }
  // 将需要保留的 Virtual DOM 放入 result 数组
  return result
}, [])

在 React 组件中,可以通过 props.children 获取子元素,所以还需要将子元素存储在 props 对象中。

return {
  type,
  props: Object.assign({ children: childElements }, props),
  children: childElements
}

6. 渲染 Virtual DOM 对象为 DOM 对象

通过调用 render 方法可以将 Virtual DOM 对象更新为真实 DOM 对象。

在更新之前需要确定是否存在旧的 Virtual DOM,如果存在需要比对差异,如果不存在可以直接将 Virtual DOM 转换为 DOM 对象。

目前先只考虑不存在旧的 Virtual DOM 的情况,就是说先直接将 Virtual DOM 对象更新为真实 DOM 对象。

// render.js
export default function render(virtualDOM, container, oldDOM = container.firstChild) {
  // 在 diff 方法内部判断是否需要对比 对比也好 不对比也好 都在 diff 方法中进行操作  
  diff(virtualDOM, container, oldDOM)
}
// diff.js
import mountElement from "./mountElement"

export default function diff(virtualDOM, container, oldDOM) {
  // 判断 oldDOM 是否存在
  if (!oldDOM) {
    // 如果不存在 不需要对比 直接将 Virtual DOM 转换为真实 DOM
    mountElement(virtualDOM, container)
  }
}

在进行 virtual DOM 转换之前还需要确定 Virtual DOM 的类 Component VS Native Element。

类型不同需要做不同的处理 如果是 Native Element 直接转换。

如果是组件 还需要得到组件实例对象 通过组件实例对象获取组件返回的 virtual DOM 然后再进行转换。

目前先只考虑 Native Element 的情况。

// mountElement.js
import mountNativeElement from "./mountNativeElement"

export default function mountElement(virtualDOM, container) {
  // 通过调用 mountNativeElement 方法转换 Native Element
  mountNativeElement(virtualDOM, container)
}
// mountNativeElement.js
import createDOMElement from "./createDOMElement"

export default function mountNativeElement(virtualDOM, container) {
  const newElement = createDOMElement(virtualDOM)
  container.appendChild(newElement)
}
// createDOMElement.js
import mountElement from "./mountElement"
import updateElementNode from "./updateElementNode"

export default function createDOMElement(virtualDOM) {
  let newElement = null
  if (virtualDOM.type === "text") {
    // 创建文本节点
    newElement = document.createTextNode(virtualDOM.props.textContent)
  } else {
    // 创建元素节点
    newElement = document.createElement(virtualDOM.type)
    // 更新元素属性
    updateElementNode(newElement, virtualDOM)
  }
  // 递归渲染子节点
  virtualDOM.children.forEach(child => {
    // 因为不确定子元素是 NativeElement 还是 Component 所以调用 mountElement 方法进行确定
    mountElement(child, newElement)
  })
  return newElement
}

7. 为元素节点添加属性

// createDOMElement.js
// 看看节点类型是文本类型还是元素类型
if (virtualDOM.type === "text") {
  // 创建文本节点 设置节点内容
  newElement = document.createTextNode(virtualDOM.props.textContent)
} else {
  // 根据 Virtual DOM type 属性值创建 DOM 元素
  newElement = document.createElement(virtualDOM.type)
  // 为元素设置属性
  updateElementNode(newElement, virtualDOM)
}
export default function updateElementNode(element, virtualDOM) {
  // 获取要解析的 VirtualDOM 对象中的属性对象
  const newProps = virtualDOM.props
  // 将属性对象中的属性名称放到一个数组中并循环数组
  Object.keys(newProps).forEach(propName => {
    const newPropsValue = newProps[propName]
    // 考虑属性名称是否以 on 开头 如果是就表示是个事件属性 onClick -> click
    if (propName.slice(0, 2) === "on") {
      const eventName = propName.toLowerCase().slice(2)
      element.addEventListener(eventName, newPropsValue)
      // 如果属性名称是 value 或者 checked 需要通过 [] 的形式添加
    } else if (propName === "value" || propName === "checked") {
      element[propName] = newPropsValue
      // 刨除 children 因为它是子元素 不是属性
    } else if (propName !== "children") {
      // className 属性单独处理 不直接在元素上添加 class 属性是因为 class 是 JavaScript 中的关键字
      if (propName === "className") {
        element.setAttribute("class", newPropsValue)
      } else {
        // 普通属性
        element.setAttribute(propName, newPropsValue)
      }
    }
  })
}

8. 渲染组件

8.1 函数组件

在渲染组件之前首先要明确的是,组件的 Virtual DOM 类型值为函数,函数组件和类组件都是这样的。

// 原始组件
const Heart = () => <span>&hearts;</span>
<Heart />
// 组件的 Virtual DOM
{
  type: f function() {},
  props: {}
  children: []
}

在渲染组件时,要先将 Component 与 Native Element 区分开,如果是 Native Element 可以直接开始渲染,如果是组件,特别处理。

// mountElement.js
export default function mountElement(virtualDOM, container) {
  // 无论是类组件还是函数组件 其实本质山都是函数 
  // 如果 Virtual DOM 的 type 属性值为函数 就说明当前这个 Virtual DOM 为组件
  if (isFunction(virtualDOM)) {
    // 如果是组件 调用 mountComponent 方法进行组件渲染
    mountComponent(virtualDOM, container)
  } else {
    mountNativeElement(virtualDOM, container)
  }
}

// Virtual DOM 是否为函数类型
export function isFunction(virtualDOM) {
  return virtualDOM && typeof virtualDOM.type === "function"
}

在 mountComponent 方法中再进行函数组件和类型的区分,然后再分别进行处理。

// mountComponent.js
import mountNativeElement from "./mountNativeElement"

export default function mountComponent(virtualDOM, container) {
  // 存放组件调用后返回的 Virtual DOM 的容器
  let nextVirtualDOM = null
  // 区分函数型组件和类组件
  if (isFunctionalComponent(virtualDOM)) {
    // 函数组件 调用 buildFunctionalComponent 方法处理函数组件
    nextVirtualDOM = buildFunctionalComponent(virtualDOM)
  } else {
    // 类组件
  }
  // 判断得到的 Virtual Dom 是否是组件
  if (isFunction(nextVirtualDOM)) {
    // 如果是组件 继续调用 mountComponent 解剖组件
    mountComponent(nextVirtualDOM, container)
  } else {
    // 如果是 Navtive Element 就去渲染
    mountNativeElement(nextVirtualDOM, container)
  }
}

// Virtual DOM 是否为函数型组件
// 条件有两个: 1. Virtual DOM 的 type 属性值为函数 2. 函数的原型对象中不能有render方法
// 只有类组件的原型对象中有render方法 
export function isFunctionalComponent(virtualDOM) {
  const type = virtualDOM && virtualDOM.type
  return (
    type && isFunction(virtualDOM) && !(type.prototype && type.prototype.render)
  )
}

// 函数组件处理 
function buildFunctionalComponent(virtualDOM) {
  // 通过 Virtual DOM 中的 type 属性获取到组件函数并调用
  // 调用组件函数时将 Virtual DOM 对象中的 props 属性传递给组件函数 这样在组件中就可以通过 props 属性获取数据了
  // 组件返回要渲染的 Virtual DOM
  return virtualDOM && virtualDOM.type(virtualDOM.props || {})
}

8.2 类组件

类组件本身也是 Virtual DOM,可以通过 Virtual DOM 中的 type 属性值确定当前要渲染的组件是类组件还是函数组件。

在确定当前要渲染的组件为类组件以后,需要实例化类组件得到类组件实例对象,通过类组件实例对象调用类组件中的 render 方法,获取组件要渲染的 Virtual DOM。

类组件需要继承 Component 父类,子类需要通过 super 方法将自身的 props 属性传递给 Component 父类,父类会将 props 属性挂载为父类属性,子类继承了父类,自己本身也就自然拥有props属性了。这样做的好处是当 props 发生更新后,父类可以根据更新后的 props 帮助子类更新视图。

假设以下代码就是我们要渲染的类组件:

class Alert extends TinyReact.Component {
  constructor(props) {
    // 将 props 传递给父类 子类继承父类的 props 子类自然就有 props 数据了
    // 否则 props 仅仅是 constructor 函数的参数而已
    // 将 props 传递给父类的好处是 当 props 发生更改时 父类可以帮助更新 props 更新组件视图
    super(props)
    this.state = {
      title: "default title"
    }
  }
  render() {
    return (
      <div>
        <h2>{this.state.title}</h2>
        <p>{this.props.message}</p>
      </div>
    )
  }
}

TinyReact.render(<Alert message="Hello React" />, root)
// Component.js 父类 Component 实现
export default class Component {
  constructor(props) {
    this.props = props
  }
}

在 mountComponent 方法中通过调用 buildStatefulComponent 方法得到类组件要渲染的 Virtual DOM

// mountComponent.js
export default function mountComponent(virtualDOM, container) {
  let nextVirtualDOM = null
  // 区分函数型组件和类组件
  if (isFunctionalComponent(virtualDOM)) {
    // 函数组件
    nextVirtualDOM = buildFunctionalComponent(virtualDOM)
  } else {
    // 类组件
    nextVirtualDOM = buildStatefulComponent(virtualDOM)
  }
  // 判断得到的 Virtual Dom 是否是组件
  if (isFunction(nextVirtualDOM)) {
    mountComponent(nextVirtualDOM, container)
  } else {
    mountNativeElement(nextVirtualDOM, container)
  }
}

// 处理类组件
function buildStatefulComponent(virtualDOM) {
  // 实例化类组件 得到类组件实例对象 并将 props 属性传递进类组件
  const component = new virtualDOM.type(virtualDOM.props)
  // 调用类组件中的render方法得到要渲染的 Virtual DOM
  const nextVirtualDOM = component.render()
  // 返回要渲染的 Virtual DOM
  return nextVirtualDOM
}

9. Virtual DOM 比对

在进行 Virtual DOM 比对时,需要用到更新后的 Virtual DOM 和更新前的 Virtual DOM,更新后的 Virtual DOM 目前我们可以通过 render 方法进行传递,现在的问题是更新前的 Virtual DOM 要如何获取呢?

对于更新前的 Virtual DOM,对应的其实就是已经在页面中显示的真实 DOM 对象。既然是这样,那么我们在创建真实DOM对象时,就可以将 Virtual DOM 添加到真实 DOM 对象的属性中。在进行 Virtual DOM 对比之前,就可以通过真实 DOM 对象获取其对应的 Virtual DOM 对象了,其实就是通过render方法的第三个参数获取的,container.firstChild。

在创建真实 DOM 对象时为其添加对应的 Virtual DOM 对象

// mountElement.js
import mountElement from "./mountElement"

export default function mountNativeElement(virtualDOM, container) {
 // 将 Virtual DOM 挂载到真实 DOM 对象的属性中 方便在对比时获取其 Virtual DOM
 newElement._virtualDOM = virtualDOM
}

8

9.1 Virtual DOM 类型相同

Virtual DOM 类型相同,如果是元素节点,就对比元素节点属性是否发生变化,如果是文本节点就对比文本节点内容是否发生变化

要实现对比,需要先从已存在 DOM 对象中获取其对应的 Virtual DOM 对象。

// diff.js
// 获取未更新前的 Virtual DOM
const oldVirtualDOM = oldDOM && oldDOM._virtualDOM

判断 oldVirtualDOM 是否存在, 如果存在则继续判断要对比的 Virtual DOM 类型是否相同,如果类型相同判断节点类型是否是文本,如果是文本节点对比,就调用 updateTextNode 方法,如果是元素节点对比就调用 setAttributeForElement 方法

// diff.js
else if (oldVirtualDOM && virtualDOM.type === oldVirtualDOM.type) {
  if (virtualDOM.type === "text") {
    // 文本节点 对比文本内容是否发生变化
    updateTextNode(virtualDOM, oldVirtualDOM, oldDOM)
  } else {
    // 元素节点 对比元素属性是否发生变化
    setAttributeForElement(oldDOM, virtualDOM, oldVirtualDOM)
  }

updateTextNode 方法用于对比文本节点内容是否发生变化,如果发生变化则更新真实 DOM 对象中的内容,既然真实 DOM 对象发生了变化,还要将最新的 Virtual DOM 同步给真实 DOM 对象。

function updateTextNode(virtualDOM, oldVirtualDOM, oldDOM) {
  // 如果文本节点内容不同
  if (virtualDOM.props.textContent !== oldVirtualDOM.props.textContent) {
    // 更新真实 DOM 对象中的内容
    oldDOM.textContent = virtualDOM.props.textContent
  }
  // 同步真实 DOM 对应的 Virtual DOM
  oldDOM._virtualDOM = virtualDOM
}

setAttributeForElement 方法用于设置/更新元素节点属性

思路是先分别获取更新后的和更新前的 Virtual DOM 中的 props 属性,循环新 Virtual DOM 中的 props 属性,通过对比看一下新 Virtual DOM 中的属性值是否发生了变化,如果发生变化 需要将变化的值更新到真实 DOM 对象中

再循环未更新前的 Virtual DOM 对象,通过对比看看新的 Virtual DOM 中是否有被删除的属性,如果存在删除的属性 需要将 DOM 对象中对应的属性也删除掉

// updateNodeElement.js
export default function updateNodeElement(
  newElement,
  virtualDOM,
  oldVirtualDOM = {}
) {
  // 获取节点对应的属性对象
  const newProps = virtualDOM.props || {}
  const oldProps = oldVirtualDOM.props || {}
  Object.keys(newProps).forEach(propName => {
    // 获取属性值
    const newPropsValue = newProps[propName]
    const oldPropsValue = oldProps[propName]
    if (newPropsValue !== oldPropsValue) {
      // 判断属性是否是否事件属性 onClick -> click
      if (propName.slice(0, 2) === "on") {
        // 事件名称
        const eventName = propName.toLowerCase().slice(2)
        // 为元素添加事件
        newElement.addEventListener(eventName, newPropsValue)
        // 删除原有的事件的事件处理函数
        if (oldPropsValue) {
          newElement.removeEventListener(eventName, oldPropsValue)
        }
      } else if (propName === "value" || propName === "checked") {
        newElement[propName] = newPropsValue
      } else if (propName !== "children") {
        if (propName === "className") {
          newElement.setAttribute("class", newPropsValue)
        } else {
          newElement.setAttribute(propName, newPropsValue)
        }
      }
    }
  })
  // 判断属性被删除的情况
  Object.keys(oldProps).forEach(propName => {
    const newPropsValue = newProps[propName]
    const oldPropsValue = oldProps[propName]
    if (!newPropsValue) {
      // 属性被删除了
      if (propName.slice(0, 2) === "on") {
        const eventName = propName.toLowerCase().slice(2)
        newElement.removeEventListener(eventName, oldPropsValue)
      } else if (propName !== "children") {
        newElement.removeAttribute(propName)
      }
    }
  })
}

以上对比的仅仅是最上层元素,上层元素对比完成以后还需要递归对比子元素

else if (oldVirtualDOM && virtualDOM.type === oldVirtualDOM.type) {
    // 递归对比 Virtual DOM 的子元素
    virtualDOM.children.forEach((child, i) => {
      diff(child, oldDOM, oldDOM.childNodes[i])
    })
  }

7

9.2 Virtual DOM 类型不同

当对比的元素节点类型不同时,就不需要继续对比了,直接使用新的 Virtual DOM 创建 DOM 对象,用新的 DOM 对象直接替换旧的 DOM 对象。当前这种情况要将组件刨除,组件要被单独处理。

// diff.js
else if (
  // 如果 Virtual DOM 类型不一样
  virtualDOM.type !== oldVirtualDOM.type &&
  // 并且 Virtual DOM 不是组件 因为组件要单独进行处理
  typeof virtualDOM.type !== "function"
) {
  // 根据 Virtual DOM 创建真实 DOM 元素
  const newDOMElement = createDOMElement(virtualDOM)
  // 用创建出来的真实 DOM 元素 替换旧的 DOM 元素
  oldDOM.parentNode.replaceChild(newDOMElement, oldDOM)
} 

9.3 删除节点

删除节点发生在节点更新以后并且发生在同一个父节点下的所有子节点身上。

在节点更新完成以后,如果旧节点对象的数量多于新 VirtualDOM 节点的数量,就说明有节点需要被删除。

5
// 获取就节点的数量
let oldChildNodes = oldDOM.childNodes
// 如果旧节点的数量多于要渲染的新节点的长度
if (oldChildNodes.length > virtualDOM.children.length) {
  for (
    let i = oldChildNodes.length - 1;
    i > virtualDOM.children.length - 1;
    i--
  ) {
    oldDOM.removeChild(oldChildNodes[i])
  }
}

9.4 类组件状态更新

以下代码是要更新状态的类组件,在类组件的 state 对象中有默认的 title 状态,点击 change title 按钮调用 handleChange 方法,在 handleChange 方法中调用 this.setState 方法更改 title 的状态值。

class Alert extends TinyReact.Component {
  constructor(props) {
    super(props)
    this.state = {
      title: "default title"
    }
    // 更改 handleChange 方法中的 this 指向 让 this 指向类实例对象
    this.handleChange = this.handleChange.bind(this)
  }
  handleChange() {
    // 调用父类中的 setState 方法更改状态
    this.setState({
      title: "changed title"
    })
  }
  render() {
    return (
      <div>
        <h2>{this.state.title}</h2>
        <p>{this.props.message}</p>
        <button onClick={this.handleChange}>change title</button>
      </div>
    )
  }
}

setState 方法是定义在父类 Component 中的,该方法的作用是更改子类的 state,产生一个全新的 state 对象。

// Component.js
export default class Component {
  constructor(props) {
    this.props = props
  }
  setState (state) {
    // setState 方法被子类调用 此处this指向子类实例对象
    // 所以改变的是子类的 state 对象
    this.state = Object.assign({}, this.state, state)
  }
}

现在子类已经可以调用父类的 setState 方法更改状态值了,当组件的 state 对象发生更改时,要调用 render 方法更新组件视图。

在更新组件之前,要使用更新的 Virtual DOM 对象和未更新的 Virtual DOM 进行对比找出更新的部分,达到 DOM 最小化操作的目的。

在 setState 方法中可以通过调用 this.render 方法获取更新后的 Virtual DOM,由于 setState 方法被子类调用,this 指向子类,所以此处调用的是子类的 render 方法。

// Component.js
setState(state) {
  // setState 方法被子类调用 此处this指向子类
  // 所以改变的是子类的 state
  this.state = Object.assign({}, this.state, state)
  // 通过调用 render 方法获取最新的 Virtual DOM
  let virtualDOM = this.render()
}

要实现对比,还需要获取未更新前的 Virtual DOM,按照之前的经验,我们可以从 DOM 对象中获取其对应的 Virtual DOM 对象,未更新前的 DOM 对象实际上就是现在在页面中显示的 DOM 对象,我们只要能获取到这个 DOM 对象就可以获取到其对应的 Virtual DOM 对象了。

页面中的 DOM 对象要怎样获取呢?页面中的 DOM 对象是通过 mountNativeElement 方法挂载到页面中的,所以我们只需要在这个方法中调用 Component 类中的方法就可以将 DOM 对象保存在 Component 类中了。在子类调用 setState 方法的时候,在 setState 方法中再调用另一个获取 DOM 对象的方法就可以获取到之前保存的 DOM 对象了。

// Component.js
// 保存 DOM 对象的方法
setDOM(dom) {
  this._dom = dom
}
// 获取 DOM 对象的方法
getDOM() {
  return this._dom
}

接下来我们要研究一下在 mountNativeElement 方法中如何才能调用到 setDOM 方法,要调用 setDOM 方法,必须要得到类的实例对象,所以目前的问题就是如何在 mountNativeElement 方法中得到类的实例对象,这个类指的不是Component类,因为我们在代码中并不是直接实例化的Component类,而是实例化的它的子类,由于子类继承了父类,所以在子类的实例对象中也是可以调用到 setDOM 方法的。

mountNativeElement 方法接收最新的 Virtual DOM 对象,如果这个 Virtual DOM 对象是类组件产生的,在产生这个 Virtual DOM 对象时一定会先得到这个类的实例对象,然后再调用实例对象下面的 render 方法进行获取。我们可以在那个时候将类组件实例对象添加到 Virtual DOM 对象的属性中,而这个 Virtual DOM 对象最终会传递给 mountNativeElement 方法,这样我们就可以在 mountNativeElement 方法中获取到组件的实例对象了,既然类组件的实例对象获取到了,我们就可以调用 setDOM 方法了。

在 buildClassComponent 方法中为 Virtual DOM 对象添加 component 属性, 值为类组件的实例对象。

function buildClassComponent(virtualDOM) {
  const component = new virtualDOM.type(virtualDOM.props)
  const nextVirtualDOM = component.render()
  nextVirtualDOM.component = component
  return nextVirtualDOM
}

在 mountNativeElement 方法中获取组件实例对象,通过实例调用调用 setDOM 方法保存 DOM 对象,方便在对比时通过它获取它的 Virtual DOM 对象

export default function mountNativeElement(virtualDOM, container) {
  // 获取组件实例对象
  const component = virtualDOM.component
  // 如果组件实例对象存在
  if (component) {
    // 保存 DOM 对象
    component.setDOM(newElement)
  }
}

接下来在 setState 方法中就可以调用 getDOM 方法获取 DOM 对象了

setState(state) {
  this.state = Object.assign({}, this.state, state)
  let virtualDOM = this.render()
  // 获取页面中正在显示的 DOM 对象 通过它可以获取其对象的 Virtual DOM 对象
  let oldDOM = this.getDOM()
}

现在更新前的 Virtual DOM 对象和更新后的 Virtual DOM 对象就都已经获取到了,接下来还要获取到真实 DOM 对象父级容器对象,因为在调用 diff 方法进行对比的时候需要用到

setState(state) {
  this.state = Object.assign({}, this.state, state)
  let virtualDOM = this.render()
  let oldDOM = this.getDOM()
  // 获取真实 DOM 对象父级容器对象
  let container = oldDOM.parentNode
}

接下来就可以调用 diff 方法进行比对了,比对后会按照我们之前写好的逻辑进行 DOM 对象更新,我们就可以在页面中看到效果了

setState(state) {
    this.state = Object.assign({}, this.state, state)
    let virtualDOM = this.render()
    let oldDOM = this.getDOM()
    let container = oldDOM.parentNode
    // 比对
    diff(virtualDOM, container, oldDOM)
  }

9.5 组件更新

在 diff 方法中判断要更新的 Virtual DOM 是否是组件。

如果是组件再判断要更新的组件和未更新前的组件是否是同一个组件,如果不是同一个组件就不需要做组件更新操作,直接调用 mountElement 方法将组件返回的 Virtual DOM 添加到页面中。

如果是同一个组件,就执行更新组件操作,其实就是将最新的 props 传递到组件中,再调用组件的render方法获取组件返回的最新的 Virtual DOM 对象,再将 Virtual DOM 对象传递给 diff 方法,让 diff 方法找出差异,从而将差异更新到真实 DOM 对象中。

在更新组件的过程中还要在不同阶段调用其不同的组件生命周期函数。

在 diff 方法中判断要更新的 Virtual DOM 是否是组件,如果是组件又分为多种情况,新增 diffComponent 方法进行处理

else if (typeof virtualDOM.type === "function") {
  // 要更新的是组件
  // 1) 组件本身的 virtualDOM 对象 通过它可以获取到组件最新的 props
  // 2) 要更新的组件的实例对象 通过它可以调用组件的生命周期函数 可以更新组件的 props 属性 可以获取到组件返回的最新的 Virtual DOM
  // 3) 要更新的 DOM 象 在更新组件时 需要在已有DOM对象的身上进行修改 实现DOM最小化操作 获取旧的 Virtual DOM 对象
  // 4) 如果要更新的组件和旧组件不是同一个组件 要直接将组件返回的 Virtual DOM 显示在页面中 此时需要 container 做为父级容器
  diffComponent(virtualDOM, oldComponent, oldDOM, container)
}

在 diffComponent 方法中判断要更新的组件是未更新前的组件是否是同一个组件

// diffComponent.js
export default function diffComponent(virtualDOM, oldComponent, oldDOM, container) {
  // 判断要更新的组件和未更新的组件是否是同一个组件 只需要确定两者使用的是否是同一个构造函数就可以了
  if (isSameComponent(virtualDOM, oldComponent)) {
    // 属同一个组件 做组件更新  
  } else {
    // 不是同一个组件 直接将组件内容显示在页面中
  }
}
// virtualDOM.type 更新后的组件构造函数
// oldComponent.constructor 未更新前的组件构造函数
// 两者等价就表示是同一组件
function isSameComponent(virtualDOM, oldComponent) {
  return oldComponent && virtualDOM.type === oldComponent.constructor
}

如果不是同一个组件的话,就不需要执行更新组件的操作,直接将组件内容显示在页面中,替换原有内容

// diffComponent.js
else {
  // 不是同一个组件 直接将组件内容显示在页面中
  // 这里为 mountElement 方法新增了一个参数 oldDOM 
  // 作用是在将 DOM 对象插入到页面前 将页面中已存在的 DOM 对象删除 否则无论是旧DOM对象还是新DOM对象都会显示在页面中
  mountElement(virtualDOM, container, oldDOM)
}

在 mountNativeElement 方法中删除原有的旧 DOM 对象

// mountNavtiveElement.js
export default function mountNativeElement(virtualDOM, container, oldDOM) {
 // 如果旧的DOM对象存在 删除
  if (oldDOM) {
    unmount(oldDOM)
  }
}
// unmount.js
export default function unmount(node) {
  node.remove()
}

如果是同一个组件的话,需要执行组件更新操作,需要调用组件生命周期函数

先在 Component 类中添加生命周期函数,子类要使用的话直接覆盖就可以

// Component.js
export default class Component {
  // 生命周期函数
  componentWillMount() {}
  componentDidMount() {}
  componentWillReceiveProps(nextProps) {}
  shouldComponentUpdate(nextProps, nextState) {
    return nextProps != this.props || nextState != this.state
  }
  componentWillUpdate(nextProps, nextState) {}
  componentDidUpdate(prevProps, preState) {}
  componentWillUnmount() {}
}

新建 updateComponent 方法用于更新组件操作,并在 if 成立后调用

// diffComponent.js
if (isSameComponent(virtualDOM, oldComponent)) {
  // 属同一个组件 做组件更新
  updateComponent(virtualDOM, oldComponent, oldDOM, container)
}

在 updateComponent 方法中调用组件的生命周期函数,更新组件获取最新 Virtual DOM,最终调用 diff 方法进行更新

import diff from "./diff"

export default function updateComponent(
  virtualDOM,
  oldComponent,
  oldDOM,
  container
) {
  // 生命周期函数
  oldComponent.componentWillReceiveProps(virtualDOM.props)
  if (
    // 调用 shouldComponentUpdate 生命周期函数判断是否要执行更新操作
    oldComponent.shouldComponentUpdate(virtualDOM.props)
  ) {
    // 将未更新的 props 保存一份
    let prevProps = oldComponent.props
    // 生命周期函数
    oldComponent.componentWillUpdate(virtualDOM.props)
    // 更新组件的 props 属性 updateProps 方法定义在 Component 类型
    oldComponent.updateProps(virtualDOM.props)
    // 因为组件的 props 已经更新 所以调用 render 方法获取最新的 Virtual DOM
    const nextVirtualDOM = oldComponent.render()
    // 将组件实例对象挂载到 Virtual DOM 身上
    nextVirtualDOM.component = oldComponent
    // 调用diff方法更新视图
    diff(nextVirtualDOM, container, oldDOM)
    // 生命周期函数
    oldComponent.componentDidUpdate(prevProps)
  }
}
// Component.js
export default class Component {
  updateProps(props) {
    this.props = props
  }
}

10. ref 属性

为节点添加 ref 属性可以获取到这个节点的 DOM 对象,比如在 DemoRef 类中,为 input 元素添加了 ref 属性,目的是获取 input DOM 元素对象,在点击按钮时获取用户在文本框中输入的内容

class DemoRef extends TinyReact.Component {
  handle() {
    let value = this.input.value
    console.log(value)
  }
  render() {
    return (
      <div>
        <input type="text" ref={input => (this.input = input)} />
        <button onClick={this.handle.bind(this)}>按钮</button>
      </div>
    )
  }
}

实现思路是在创建节点时判断其 Virtual DOM 对象中是否有 ref 属性,如果有就调用 ref 属性中所存储的方法并且将创建出来的DOM对象作为参数传递给 ref 方法,这样在渲染组件节点的时候就可以拿到元素对象并将元素对象存储为组件属性了。

// createDOMElement.js
if (virtualDOM.props && virtualDOM.props.ref) {
  virtualDOM.props.ref(newElement)
}

在类组件的身上也可以添加 ref 属性,目的是获取组件的实例对象,比如下列代码中,在 DemoRef 组件中渲染了 Alert 组件,在 Alert 组件中添加了 ref 属性,目的是在 DemoRef 组件中获取 Alert 组件实例对象。

class DemoRef extends TinyReact.Component {
  handle() {
    let value = this.input.value
    console.log(value)
    console.log(this.alert)
  }
  componentDidMount() {
    console.log("componentDidMount")
  }
  render() {
    return (
      <div>
        <input type="text" ref={input => (this.input = input)} />
        <button onClick={this.handle.bind(this)}>按钮</button>
        <Alert ref={alert => (this.alert = alert)} />
      </div>
    )
  }
}

实现思路是在 mountComponent 方法中,如果判断了当前处理的是类组件,就通过类组件返回的 Virtual DOM 对象中获取组件实例对象,判断组件实例对象中的 props 属性中是否存在 ref 属性,如果存在就调用 ref 方法并且将组件实例对象传递给 ref 方法。

// mountComponent.js
let component = null
  if (isFunctionalComponent(virtualDOM)) {}
	else {
    // 类组件
    nextVirtualDOM = buildStatefulComponent(virtualDOM)
    // 获取组件实例对象
    component = nextVirtualDOM.component
  }
	// 如果组件实例对象存在的话
	if (component) {
   	// 判断组件实例对象身上是否有 props 属性 props 属性中是否有 ref 属性
    if (component.props && component.props.ref) {
      // 调用 ref 方法并传递组件实例对象
      component.props.ref(component)
    }
  }

代码走到这,顺便处理一下组件挂载完成的生命周期函数

// 如果组件实例对象存在的话
if (component) {
  component.componentDidMount()
}

11. key 属性

在 React 中,渲染列表数据时通常会在被渲染的列表元素上添加 key 属性,key 属性就是数据的唯一标识,帮助 React 识别哪些数据被修改或者删除了,从而达到 DOM 最小化操作的目的。

key 属性不需要全局唯一,但是在同一个父节点下的兄弟节点之间必须是唯一的。

也就是说,在比对同一个父节点下类型相同的子节点时需要用到 key 属性。

11.1 节点对比

实现思路是在两个元素进行比对时,如果类型相同,就循环旧的 DOM 对象的子元素,查看其身上是否有key 属性,如果有就将这个子元素的 DOM 对象存储在一个 JavaScript 对象中,接着循环要渲染的 Virtual DOM 对象的子元素,在循环过程中获取到这个子元素的 key 属性,然后使用这个 key 属性到 JavaScript 对象中查找 DOM 对象,如果能够找到就说明这个元素是已经存在的,是不需要重新渲染的。如果通过key属性找不到这个元素,就说明这个元素是新增的是需要渲染的。

// diff.js
else if (oldVirtualDOM && virtualDOM.type === oldVirtualDOM.type) {
  // 将拥有key属性的元素放入 keyedElements 对象中
  let keyedElements = {}
  for (let i = 0, len = oldDOM.childNodes.length; i < len; i++) {
    let domElement = oldDOM.childNodes[i]
    if (domElement.nodeType === 1) {
      let key = domElement.getAttribute("key")
      if (key) {
        keyedElements[key] = domElement
      }
    }
  }
}
// diff.js
// 看一看是否有找到了拥有 key 属性的元素
let hasNoKey = Object.keys(keyedElements).length === 0

// 如果没有找到拥有 key 属性的元素 就按照索引进行比较
if (hasNoKey) {
  // 递归对比 Virtual DOM 的子元素
  virtualDOM.children.forEach((child, i) => {
    diff(child, oldDOM, oldDOM.childNodes[i])
  })
} else {
  // 使用key属性进行元素比较
  virtualDOM.children.forEach((child, i) => {
    // 获取要进行比对的元素的 key 属性
    let key = child.props.key
    // 如果 key 属性存在
    if (key) {
      // 到已存在的 DOM 元素对象中查找对应的 DOM 元素
      let domElement = keyedElements[key]
      // 如果找到元素就说明该元素已经存在 不需要重新渲染
      if (domElement) {
        // 虽然 DOM 元素不需要重新渲染 但是不能确定元素的位置就一定没有发生变化
        // 所以还要查看一下元素的位置
        // 看一下 oldDOM 对应的(i)子元素和 domElement 是否是同一个元素 如果不是就说明元素位置发生了变化
        if (oldDOM.childNodes[i] && oldDOM.childNodes[i] !== domElement) {
          // 元素位置发生了变化
          // 将 domElement 插入到当前元素位置的前面 oldDOM.childNodes[i] 就是当前位置
          // domElement 就被放入了当前位置
          oldDOM.insertBefore(domElement, oldDOM.childNodes[i])
        }
      } else {
        mountElement(child, oldDOM, oldDOM.childNodes[i])
      }
    }
  })
}
// mountNativeElement.js
if (oldDOM) {
  container.insertBefore(newElement, oldDOM)
} else {
  // 将转换之后的DOM对象放置在页面中
  container.appendChild(newElement)
}

11.2 节点卸载

在比对节点的过程中,如果旧节点的数量多于要渲染的新节点的数量就说明有节点被删除了,继续判断 keyedElements 对象中是否有元素,如果没有就使用索引方式删除,如果有就要使用 key 属性比对的方式进行删除。

实现思路是循环旧节点,在循环旧节点的过程中获取旧节点对应的 key 属性,然后根据 key 属性在新节点中查找这个旧节点,如果找到就说明这个节点没有被删除,如果没有找到,就说明节点被删除了,调用卸载节点的方法卸载节点即可。

// 获取就节点的数量
let oldChildNodes = oldDOM.childNodes
// 如果旧节点的数量多于要渲染的新节点的长度
if (oldChildNodes.length > virtualDOM.children.length) {
  if (hasNoKey) {
    for (
      let i = oldChildNodes.length - 1;
      i >= virtualDOM.children.length;
      i--
    ) {
      oldDOM.removeChild(oldChildNodes[i])
    }
  } else {
    for (let i = 0; i < oldChildNodes.length; i++) {
      let oldChild = oldChildNodes[i]
      let oldChildKey = oldChild._virtualDOM.props.key
      let found = false
      for (let n = 0; n < virtualDOM.children.length; n++) {
        if (oldChildKey === virtualDOM.children[n].props.key) {
          found = true
          break
        }
      }
      if (!found) {
        unmount(oldChild)
        i--
      }
    }
  }
}

卸载节点并不是说将节点直接删除就可以了,还需要考虑以下几种情况

  1. 如果要删除的节点是文本节点的话可以直接删除
  2. 如果要删除的节点由组件生成,需要调用组件卸载生命周期函数
  3. 如果要删除的节点中包含了其他组件生成的节点,需要调用其他组件的卸载生命周期函数
  4. 如果要删除的节点身上有 ref 属性,还需要删除通过 ref 属性传递给组件的 DOM 节点对象
  5. 如果要删除的节点身上有事件,需要删除事件对应的事件处理函数
export default function unmount(dom) {
  // 获取节点对应的 virtualDOM 对象
  const virtualDOM = dom._virtualDOM
  // 如果要删除的节点时文本
  if (virtualDOM.type === "text") {
    // 直接删除节点
    dom.remove()
    // 阻止程序向下运行
    return
  }
  // 查看节点是否由组件生成
  let component = virtualDOM.component
  // 如果由组件生成
  if (component) {
    // 调用组件卸载生命周期函数
    component.componentWillUnmount()
  }
  
  // 如果节点具有 ref 属性 通过再次调用 ref 方法 将传递给组件的DOM对象删除
  if (virtualDOM.props && virtualDOM.props.ref) {
    virtualDOM.props.ref(null)
  }

  // 事件处理
  Object.keys(virtualDOM.props).forEach(propName => {
    if (propName.slice(0, 2) === "on") {
      const eventName = propName.toLowerCase().slice(2)
      const eventHandler = virtualDOM.props[propName]
      dom.removeEventListener(eventName, eventHandler)
    }
  })
	
  // 递归删除子节点
  if (dom.childNodes.length > 0) {
    for (let i = 0; i < dom.childNodes.length; i++) {
      unmount(dom.childNodes[i])
      i--
    }
  }
  	
  dom.remove()
}

HTTP学习笔记


title: HTTP学习笔记
date: 2020-04-01 11:16:06
tags: HTTP
categories: HTTP

HTTP学习笔记

HTTP协议,只是网络协议中很小的一部分。HTTP协议对我们来说,看不见,摸不着,但是真实存在。或许我们无法像使用编程语言一样,实现具体的功能。但是HTTP的重要性,不需要特意强调。跨域方案CROS响应头信息字段Access-Control-Allow-Origin的设置,使用cookie时,响应头Set-Cookie的设置等等,其实我们都在使用HTTP协议。但是一般情况下都是边用边查,没有形成体系。没有一个组织结构,导致都是一些零散的知识点,对于使用,学习,存在很大的困难。这篇博客(可能不是一篇),有体系的整理HTTP协议的内容。希望可以帮助自己和其他小伙伴

什么是HTTP协议

HTTP(HyperText Transfer Protocol)超文本传输协议。

  1. 超文本:包括文字,图片,音频,视频等。
  2. 传输:负责超文本的传输
  3. 协议:负责传输超文本过程中的规范

大白话总结

HTTP就是一个用在计算机世界里面的协议,使用计算机能理解的语言,确立了计算机之间交流通信的规范。

发展历程

学习任何一项技术,都离不开发展历程。通过发展历程,才能知道这门技术在什么时候,遇到了什么样的问题,发生了什么变革。了解了这些,对我们的学习可以起到事半功倍的效果。
下面按照HTTP的时间顺序整理

HTTP 0.9

  1. 只能处理纯文本格式
  2. 请求响应结束后立即关闭连接
  3. 只允许使用GET请求从服务器获取HTML文档

HTTP 1.1

  1. 增加了HEAD,POST等新方法
  2. 引入了HTTP Header的概念,处理请求和响应变得更加灵活
  3. 传输数据不仅限于文本
  4. 增加了响应状态码,标记可能的错误原因
  5. 增加了PUT,DELETE等方法
  6. 允许长连接
  7. 增加了缓存管理和控制
  8. 允许数据分块,利于传输大文件
  9. 强制要求Host头

HTTP 2.0

  1. 二进制协议,不仅限于纯文本。
  2. 使用专用算法压缩头部,减少了数据传输量。
  3. 废弃了管道,可以发起多个请求。
  4. 增强了安全性,要求加密通信。
  5. 允许服务端向客户端主动推送。

与HTTP相关的各种概念

  1. CDN(Content Delivery Network)内容分发网络:应用于HTTP协议里的缓存和代理技术,代替源站响应客户端的请求。
  2. 代理:HTTP协议中请求方和响应方直接的一个环节。代理分为下面几种。
  • 匿名代理: 完全隐匿了被代理的机器,外界看到的只有代理服务器。
  • 透明代理: 在传输过程中是“透明开放”的,外界即知道代理也知道客户端。
  • 正向代理: 靠近客户端,代表客户端向服务端发送请求。
  • 反向代理: 靠近服务端,代表服务端向客户端响应请求。

TCP/IP协议族

TCP/IP 协议严格来说并不是一种协议,而是互联网相关各类协议族的总称。其中包括TCP,IP,HTTP等协议。TCP/IP协议族最重要的一点就是分层。按照层次分为以下四层:应用层,传输层,网络层,数据链路层。

TCP/IP协议族各层的作用

  • 应用层: 决定向用户提供服务时通信的活动。如:FTP(文件传输协议),DNS(域名系统),HTTP协议
  • 传输层: 提供处于网络连接中的两台计算机之间的通信。如TCP(传输控制协议),UDP(用户数据报协议)
  • 网络层: 用来处理在网络上流动的数据包。数据包是网络传输的最小单位。该层规定了通过怎样的路径到达对象计算机,并把数据包传送给对方。在多台设备间传输时,网络层的作用就是选择一条传输路径。如:IP协议
  • 数据链路层: 用来处理连接网络的硬件部分。包括控制操作系统,硬件的设备驱动,网卡等一切传输介质。

与HTTP相关的各种协议

  1. TCP 协议
    有状态的协议,需要先与对方建立连接然后才能发送数据,而且保证数据不丢失,不重复。TCP的数据是连续的字节流,有先后顺序。

  2. UDP 协议
    无状态的协议,不用事先与对方建立连接就可以发送数据,但不保证数据一定会发送到对方。UDP的数据是分散的小数据包,顺序发,乱序收。

  3. DNS协议
    DNS协议就是域名到IP的一个映射,将域名转成IP地址(一个IP地址可以对应多个域名)。域名方便我们记忆。但是网络中寻址,只认IP地址。所以需要将域名映射为IP。
    DNS 通过一个访问顺序来逐渐确定域名的IP地址。

  • 根域名服务器 > 顶级域名服务器 > 权威域名服务器。
    3.1 分析
    3.1.1 如果我们访问www.google.com 这个网址,假设没有缓存,是首次访问。

    1. 访问`root server `根服务器
      
    2. 根服务器返回了一个顶级域名的地址,即 .com 服务器地址
      
    3. 递归访问` .com`顶级域名服务器。
      
    4. 顶级域名服务器收到请求后,返回权威域名服务器地址,即`google.com`地址。
    

    3.1.2 如果我们第二次访问www.google.com 这个网址。

    5. 查看浏览器缓存,有没有DNS查询的结果
      
    6. 查看操作系统缓存,有没有DNS查询结果
      
    7. 查看主机映射文件`hosts`有没有`www.google.com`的IP映射
      
    8. 使用DNS协议。
    

    3.2 域名的新玩法

    1. 重定向
    - 将域名的IP在需要的时候进行更改,如切换主机。
    
    1. 实现负载均衡
    - 域名可以对应多个IP,即多台主机。客户端收到多个主机地址后,使用自己的轮询算法依次向服务器端发送请求,实现负载均衡。
    - 域名解析可以配置内部的策略,返回离客户端最近的主机,这样DNS端把请求分发到不同的服务器,实现负载均衡。
    

HTTP请求过程

  1. 浏览器从地址栏的输入中获得服务器的 IP 地址和端口号;
  2. 浏览器用 TCP 的三次握手与服务器建立连接;
  3. 浏览器向服务器发送拼好的报文;
  4. 服务器收到报文后处理请求,同样拼好报文再发给浏览器;
  5. 浏览器解析报文,渲染输出页面。

HTTP请求总结

  1. HTTP 协议基于底层的 TCP/IP 协议,所以必须要用 IP 地址建立连接;
  2. 如果不知道 IP 地址,就要用 DNS 协议去解析得到 IP 地址,否则就会连接失败;
  3. 建立 TCP 连接后会顺序收发数据,请求方和应答方都必须依据 HTTP 规构建和解析报文;
  4. 为了减少响应时间,整个过程中的每一个环节都会有缓存,能够实现“短路”操作;

闭包到底是什么


title: 闭包到底是什么
date: 2018-08-20 13:09:10
tags: JavaScript
categories: JavaScript

闭包到底什么

1. 定义

JavaScript高级程序语言设计: 闭包是指有权访问另一个函数作用域中变量的函数
根据这个定义,我们去分析
闭包是什么? 一个函数
闭包是什么函数?一个可以访问其他函数作用域中变量的函数
我们仔细分析其中的关键字,函数作用域变量

1.1 深入分析函数

JavaScriptES6前是没有块级作用域的。但是有函数作用域,类似块级作用域,在函数内部使用var关键字声明一个变量。直接在函数外部访问函数内部的变量是访问不到的。
注意: 函数内部的变量必须使用var关键字声明,如果不使用var关键字声明。在函数外部是可以直接使用这个变量的,因为这时,函数内部的这个变量是全局变量。如下,在函数内部有一个局部变量和一个全局变量

	function fun() {
		var a = 1;	// 局部变量
		b = 2;		// 全局变量
	}

上面说到,在函数外部不能直接访问函数内的局部变量。反过来不然,在函数内部,是可以访问到全局变量的。这个时候需要涉及到作用域的相关知识。

1.1.1 作用域

作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性
简单来说,作用域就是你到底能不能访问到某些变量,函数,对象等。定义在全局上的,都是可以访问的。为什么呢?举个列子,全局可以理解称为一个所有人都可以访问的仓库,你需要一个东西,不需要和任何人打招呼你就可以进行拿,没人管你。
如果你有自己的家,你需要什么,根据就近原则,先在自己的家里找,找不到了。你才会向东西更多,更全的地方去找。类似于很多个同心圆,总是从最内层一直向外扩散的感觉。
函数作用域,相当于和你没有交集的人家,是不会允许你平白无故去人家的家里拿东西。即便人家的家里有这件东西。请看

	var b = 1;
	function father() {
		var b = 2;
		function son() {
			console.log(b);   // 2
		}
	}

**** 作用域链**
上面这段伪代码,son()函数要访问b变量,但是son函数内部没有b变量。就会向上面找,为什么向上面找,因为son函数是在father函数内部的,它们存在包含关系。son函数可以向上寻找。找到了b变量,取到了值。就不会继续找了。假如,father函数内部没有b变量呢?那么就会在father函数的上一层找,father的上一层是全局变量,全局变量中定义了b变量,所以也可以找到,只不过这个时候的b的值为1。这样一层层向上寻找的过程就形成了一条作用域链。

我们说了那么多,都在说从内部是如何访问外部的变量的。而外部是无法直接访问函数内部作用域的变量的。如下所示

	function f1() {
		var a = 1;
	}
	console.log(a) // 想要访问f1内部的变量a。

上面的代码,想要在函数外部使用函数内部的变量。但是由于函数内部作用域的限制,无法实现。人类总是有办法解决。请向下看。

1.1.2 函数返回值功不可灭

JavaScript的函数返回值可以返回任何类型的值。所以,返回值是一个函数也没有什么奇怪的。因此,result接收到的是一个没有调用的函数。result()调用后,我们在全局环境中通过了f2函数间接的拿到了f1函数内部的值。f2函数就是一个闭包。回过头看闭包的定义就变得一目了然。

	function f1() {
		var a = 1;
		function f2() {
			console.log(a);
		}
		return f2;
	}
	var result = f1();	// f2() { console.log(a); }
	result();	// 1
	console.log(a) // 想要访问f1内部的变量a。

阮一峰: 在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

闭包的作用

  1. 可以访问函数内部的变量。
  2. 可以让变量一直存在内存中。(可以参考阮一峰老师的例子)如下所示:
function f1(){
    var n = 999;
    nAdd = function(){ 
    n += 1
    }
    function f2(){
      alert(n);
    }
    return f2;
 }
  
  var result=f1();
  result(); // 999
  nAdd();
  result(); // 1000

正常情况下,在函数内部定义的变量,当该函数被调用后,根据JavaScript的垃圾回收机制,函数内部的变量都会被垃圾回收器清除,释放内存。但是根据上述代码所示。n并没有被回收,也就是一直被保存在内存中。所以会造成内存消耗过大,甚至在某些浏览器会引起内存泄露的问题。所以需要谨慎使用闭包。

总结: 个人觉得闭包在设计上就是一个缺陷,是因为语言特性所决定的。在ES6中有了let等新特性,出现了块级作用域。全局变量和内部变量界限分明或许是最好的解决方案。

时隔两年的回家


title: 时隔两年的回家
date: 2021-03-27 23:22:01
tags: 随笔
categories: 随笔

时隔两年的回家

因为疫情和自己的原因,两年春节没有回家!
三月28日,从小一起长大的表妹要结婚了。表妹老早就通知我,作为家里唯一的哥哥,送妹妹走入婚姻的殿堂,突然觉得是一个哥哥不可推卸的责任。所以,提前一个月订好了机票,参加妹妹婚礼顺便看看两年没见面的爸爸妈妈。
回家的前一天晚上,突然有点抗拒回家。或许是大多数出门在外的孩子都有的一个心结,从农村走出来,到了首都,魔都。想要靠着自己的努力打拼一下,混的人模狗样,让父母再为了我们担心。
现实总是骨感的,呆在大城市的我们,过上了三点一线的生活。或许因为自己的选择错误,能力不足进入了一个发展不是很好的公司,又或许是因为幼稚可笑,被社会狠狠的教育了一番,刚毕业就负债累累。没错就是我!!!
2021的跨年那一天,莫名的具有了一种力量。想要改变这种现状,去争取更好的工作机会。在这个过程中,心态起起落落。总觉得自己很难,甚至会陷入自我感动。但是,谁活着是容易的呢?我有什么资格觉得辛苦。
回到家,没有第一时间见到父母,最早见到妈妈,是在一个昏暗的小房子里面,一个人在忙碌着做饭,接近五十岁的妈妈,在一个新建的电厂附近开了一个小店,卖饭给电厂的员工吃。我不敢表现的太过伤心,我想妈妈就是为了多赚点钱。毕竟有我们两个儿子,心酸留给自己吧。我想我没有资格说辛苦。爸爸妈妈不比我辛苦?先写这么多吧。实在写不下去了。

又抽空来写了,现在是4月1日,于昨晚抵达上海。总感觉有什么东西留在了家里。说不清道不明,或许是对爸爸妈妈的担心,真的很希望他们健健康康,真的不希望他们去挣多少多少的钱,他们身体健康,是我最开心的事情。

不知道说些什么了。我只知道我需要非常努力,只有我努力,我成功,父母才会开心,安心。我也是!

改变函数this指向的apply()、call()、bind() 方法


title: 改变函数this指向的apply()、call()、bind() 方法
date: 2018-10-19 21:03:01
tags: JavaScript
categories: JavaScript

不同函数运行时会存在不同的this指向,总是指向调用函数的对象。有些时候,我们希望通过改变函数的this指向来实现某些功能。常见的如继承等等

函数的this指向

根据函数的调用方式可以分为以下几种,它们分别有各自的this指向。
函数的this指向

1. 使用call()改变函数的this指向

1.1 基本语法
使用fn.call(object, parm1, parm2....)会立即调用fn, 第一个参数是要给fn绑定的对象,也就是要将fn的this指向更改为对应的对象,从第二个参数起,是传给fn的参数,参数的长度不限,参数之间使用空格分开。

var a = 1;
var b = 2;
const obj = {
	a: 3,
	b: 4
}
function fn(a, b) {
	console.log(this.a + this.b);	
	console.log(a + b);
}
fn(5, 6);		       // 此时的this指向的是Window  所以输出的结果是 3、11
fn.call(obj, 7, 8);	   //此时的this指向了obj, 所以输出的结果是 7、15

2. 使用apply()改变函数的this指向

2.1 基本语法
fn.apply(object, arr), 和call非常类似,也会立即调用fn, 也会更改fn的this指向为接受到的第一个参数。不同之处在于apply()只接受两个参数,第二个参数是一个数组,将一个数组作为参数传递给fn

var a = 1;
var b = 2;
const obj = {
	a: 3,
	b: 4
}
function fn(a, b) {
	console.log(this.a + this.b);	
	console.log(a + b);
}
fn(5, 6);		       // 此时的this指向的是Window  所以输出的结果是 3、11
fn.call(obj, [7, 8]);  //此时的this指向了obj, 所以输出的结果是 7、15, 需要将需要的参数按照数组的方式进行传递

3. 使用bind() 改变函数的this指向

3.1 基本语法

fn.bind(object, para1, para2...),在形式上和fn.call()非常相似,传递的参数是一模一样的,唯一的区别是fn.bind()方法使用时,不会立即调用fn.上面的另外两个方法fn.apply()fn.call()会在使用时立即调用fn函数。使用bind()返回的是原函数改变this之后产生的新函数。

使用场景

// 下面是一个使用场景 
function LateBloomer() {
  this.petalCount = Math.ceil(Math.random() * 12) + 1;
}

// 在 1 秒钟后声明 bloom 回调函数的this指向的是window
// 如果这个地方换成apply或者bind,也能改变this的指向,但是函数会立即执行,
// setTimeout设置的时间间隔会失效
LateBloomer.prototype.bloom = function() {
  window.setTimeout(this.declare.bind(this), 1000);
};

LateBloomer.prototype.declare = function() {
  console.log('I am a beautiful flower with ' + this.petalCount             +'petals!');
};

var flower = new LateBloomer();
flower.bloom();  // 一秒钟后, 调用 'declare' 方法

总结:存在可能合理,不同的使用场景下,选择不同的方法实现效果。达到目的即可,关于更深层的理解,以后有了深入理解会补充。

学习Vue.js 响应式原理


title: 学习Vue.js 响应式原理
date: 2021-01-10 21:01:11
tags: ["Vue"]
categories: Vue

学习Vue.js 响应式原理

最近在学习Vue的源码,对于核心原理比较感兴趣。本文就Vue.js的响应式原理进行模拟实现。代码实现不难,花二十分钟看一下,download到本地run一下。你也会有很大收获。加油!!!

前置知识

  • 数据驱动
  • 响应式的核心原理
  • 发布订阅模式和观察者模式

数据驱动

  • 数据响应式
    • 数据模型仅仅是普通的 JavaScript 对象,而当我们修改数据时,视图会进行更新,避免了繁
      琐的 DOM 操作,提高开发效率
  • 双向绑定
    • 数据改变,视图改变;视图改变,数据也随之改变
    • 我们可以使用 v-model 在表单元素上创建双向数据绑定
  • 数据驱动
    • 开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图

数据响应式的原理

​ 因为Vue3相较于Vue2原理发生了变化,本文讨论Vue2。Vue3将在下一篇中分析

Vue2.X

// 模拟Vue实例
const vm = {} 
// 模拟Vue中的data选项
const data = {
    message: 'Hello World',
    count: 1
}
const ProxyData = () => {
    Object.keys(data).forEach((key) => {
        Object.defineProperty(vm, key, {
            enumerable: true,
            configurable: true,
            get () {
                return data[key]
            },

            set (newValue) {
                if (newValue === data[key]) {
                    return newValue
                }
                data[key] = newValue
                document.querySelector('#app').textContent = newValue
            }
        })
    })
}

发布订阅模式和观察者模式

发布/订阅模式
  • 订阅者
  • 发布者
  • 信号中心

假设存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信
号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执
行。这就叫做"发布/订阅模式"(publish-subscribe pattern)

// 事件中心
class EventEimt {
  constructor () {
    this.subs = Object.create(null)
  }

  // 注册事件 发布事件
  $on (eventType, handler) {
    this.subs[eventType] = this.subs[eventType] ? this.subs[eventType] : []
    this.subs[eventType].push(handler)
  }

  // 触发事件 订阅事件
  $emit (eventType) {
    this.subs[eventType]?.forEach(handler => handler())
  }
}

// 测试用例
const ev = new EventEimt()

ev.$on('click', () => {
  console.log('click1')
})

ev.$on('click', () => {
  console.log('click2')
})

ev.$emit('c')
观察者模式
  • 观察者(订阅者) -- Watcher

    • update():当事件发生时,具体要做的事情
  • 目标(发布者) -- Dep

    • subs 数组:存储所有的观察者
    • addSub():添加观察者
    • notify():当事件发生,调用所有观察者的 update() 方法
    • 没有事件中心
// 发布者
class Dep {
  constructor () {
    // 记录所有订阅者
    this.subs = []
  }

  // 添加订阅者
  addSub (sub) {
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }

  // 通知订阅者
  notify () {
    this.subs.forEach(sub => sub.update())
  }
}

// 订阅者
class Watch {
  update () {
    console.log('update')
  }
}

// 测试用例
const pub = new Dep()
const w1 = new Watch()
pub.addSub(w1)
pub.addSub(w1)
pub.notify()

Vue 响应式原理

整体架构图

Vue 响应式原理

上图中各部分的功能以及实现如下:

Vue

  • 负责接收初始化的参数(选项)
  • 负责把 data 中的属性注入到 Vue 实例,转换成 getter/setter
  • 负责调用 observer 监听 data 中所有属性的变化
  • 负责调用 compiler 解析指令/插值表达式
class Vue {
  constructor (options = {}) {
    // 1. 保存创建Vue实例时传递的options
    this.$options = options
    this.$data = options.data || {}
    // options.el是选择器或者DOM对象
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
    // 2. 把Data中的成员转换成get和set
    this._ProxyData(this.$data)
    // 3. observer对象,监听data数据的变化
    new Observer(this.$data)
    // 4. 调用compile函数,解析指令和差值表达式
    new Compiler(this)
  }

  // 注册数据的getter和setter方法
  _ProxyData (data) {
    Object.keys(data).forEach(key => {
      Object.defineProperty(this, key, {
        configurable: true,
        enumerable: true,
        get () {
          return data[key]
        },
        set (newValue) {
          if (newValue === data[key]) {
            return
          }
          data[key] = newValue
        }
      })
    })
  }
}

Observer

  • 负责把 data 选项中的属性转换成响应式数据
  • 如果data 中的某个属性是对象,把该属性转换成响应式数据
  • 数据变化发送通知
class Observer {
  constructor (data) {
    this.walk(data)
  }

  // 遍历data属性
  walk (data) {
    // data不存在或者data不是对象
    if (!data || typeof data !== 'object') {
      return
    }
    // 遍历data对象
    Object.keys(data).forEach(key => this.defineReactive(data, key, data[key]))
  }

  // 为data对象上的所有属性注册get和set
  defineReactive (obj, key, val) {
    const that = this
    // 负责收集依赖并发布通知
    let dep = new Dep()
    // TODO data中的属性值可能是对象类型,调用walk方法,如果是对象类型,可以递归的将其设置为响应式数据
    this.walk(val)
    Object.defineProperty(obj, key, {
      configurable: true,
      enumerable: true,
      // 监听对$data数据的访问
      get () {
        // 收集依赖
        Dep.target && dep.addSub(Dep.target)
        // TODO 此处只能使用val值而不能使用obj[key] 因为只要访问data的数据就会调用observer对象的get方法。会导致堆栈溢出
       	return val
      },
      // 监听对$data对象的改变
      set (newValue) {
        if (newValue === val) {
          return
        }
        val = newValue
        // TODO 如果为data中的数据进行了重新赋值为对象,那么需要对该对象遍历,注册get和set
        // 此处的this指向了data对象,不是observer对象
        that.walk(newValue)
        // 发送通知
        dep.notify()
      }
    })
  }
}

Compiler

  • 负责编译模板,解析指令/插值表达式
  • 负责页面的首次渲染
  • 当数据变化后重新渲染视图
class Compiler {
  // 接收vue实例
  constructor (vm) {
    this.el = vm.$el
    this.vm = vm
    this.compile(vm.$el)
  }

  // 编译模板 处理文本节点和元素节点
  compile (el) {
    // 遍历el节点的子节点
    Array.from(el.childNodes).forEach(node => {
      // 子节点为元素节点
      if (this.isElementNode(node)) {
        this.compileElement(node)
      }
      // 子节点为文本节点
      if (this.isTextNode(node)) {
        this.compileText(node)
      }
      // 子节点还存在子节点
      if (node.childNodes && node.childNodes.length) {
        this.compile(node)
      }
    })
  }

  // 编译元素节点,处理指令
  compileElement (node) {
    console.log('node: ', node.attributes);
    Array.from(node.attributes).forEach(attr => {
      // 判断是否是指令
      if (this.isDirective(attr.name)) {
        // 指令的名
        const attrName = attr.name.substr(2)
        // 指令的值,即数data的key值
        const key = attr.value
        this.update(node, key, attrName)
      }
    })
  }

  // 统一更新
  update (node, key, attrName) {
    const updateFn = this[attrName + 'Updater']
    // 调用指令处理函数并且传递data数据
    updateFn && updateFn(node, this.vm[key])
  }

  // 处理v-text指令
  textUpdater (node, value) {
    node.textContent = value
  }

  // 处理v-model指令
  // TODO 没有实现数据的双向绑定
  modelUpdater (node, value) {
    node.value = value
  }

  // 编译文本节点,处理插值表达式
  compileText (node) {
    const reg = /\{\{(.*?)\}\}/g
    reg.exec(node.textContent)
    node.textContent = node.textContent.replace(reg, (match, key) => this.vm[key.trim()])
  }

  // 判断元素属性是否为指令
  isDirective (attrName) {
    return attrName.startsWith('v-')
  }

  // 判断节点是否是文本节点
  isTextNode (node) {
    return node.nodeType === 3
  }

  // 判断节点是否为元素节点
  isElementNode (node) {
    return node.nodeType === 1
  }
}
  • Dep
    • 收集依赖,添加观察者(watcher)
    • 通知所有观察者
class Dep {
  constructor () {
    // 存储所有观察者
    this.subs = []
  }

  // 添加观察者
  addSub (sub) {
    if (sub && sub.update)
    this.subs.push(sub)
  }

  // 发送通知
  notify () {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}
  • Watcher
    • 当数据变化触发依赖, dep 通知所有的 Watcher 实例更新视图
    • 自身实例化的时候往 dep 对象中添加自己
class Watch {
  constructor (vm, key, cb) {
    this.vm = vm
    // data属性
    this.key = key
    // 更新视图的回调函数
    this.cb = cb
    // watch对象记录到Dep的静态方法target上
    Dep.target = this
    // 触发get方法,触发Dep的addSub方法
    this.oldValue = vm[key]
    Dep.target = null
  }
  update () {
    let newValue = this.vm[this.key]
    if (newValue === this.oldValue) {
      return
    }
    // 更新视图
    this.cb(newValue)
  }
}

测试

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <div>{{count}}</div>
    <div>{{message}}</div>
    <p>v-text: </p>
    <div v-text="obj"></div>
    <!-- TODO 有一个bug 差值表达式有空格无法正常渲染,data发生变化时,全部覆盖了 -->
    <p>v-model: {{input}}</p>
    <input type="text" v-model="input">
  </div>
  <script src="./dep.js"></script>
  <script src="./watch.js"></script>
  <script src="./vue.js"></script>
  <script src="./observer.js"></script>
  <script src="./compiler.js"></script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        count: 1,
        message: 'hello',
        obj: '<p>hello</p>',
        input: '222'
      }
    })
  </script>
</body>
</html>

总结

​ 因为Vue.js的功能丰富,一个一个功能去实现也没有必要。本文只是抱着学习的心态,实现了Vue.js的几个核心功能。大概是两百行左右的代码,整体来说核心功能的实现代码不是很难,但是**值得学习。如果你也对Vue.js的实现原理感兴趣的话,也可以访问我的github,clone到本次run一下,debugger一下。可以发现我的实现有bug的地方。也可以看一下我的实现方式,代码有超多注释,通俗易懂哈哈哈。

ES6新语法学习_第一回


title: ES6新语法学习_第一回
date: 2018-04-20 23:03:40
tags: ES6
categories: ES6

随着学习的深入,真的发现ECMAScript5留下了蛮多坑的,还好,标准不断的更新,语言也会越来越好,简单看一下ES6新的语法,慢慢的更新吧。

作用域的变化

let
1. 引入块级作用域,let产生产生暂时性死区,取消变量提升。
阮一峰老师的教程中举了一个for循环的中用var定义的循环变量导致泄漏为全局变量的问题。

var arr = [];
var(var i = 0; i < 10; i++) {
	arr[i] = function() {
	console.log(i);
	}
}
console.log(arr[5]); // 10

我记得以前我大致遇到过同样的问题,但是并不知道是为什么,后来看了阮一峰老师的ES6,瞬间明白。var声明的循环变量,其实是一个全局变量。在循环体内部,输出i的值是没有问题的。但是除了循环体,这时候全局变量i的值已经是10了。其实是一个变量泄露为全局变量的问题。
使用let声明的循环变量,是属于块级作用域的。不会泄露为全局变量,所以使用let声明,最后输出的结果是6。

2. 没有预解析,必须先声明使用,同一个作用域不能重复定义。

const常量
const声明一个常量,实际不是一个变量的值不变,而是指向那个变量的内存地址不变。其实主要是约束引用类型的。
对于对象来讲,const约束了对象的内存地址不可变,也就是一个变量被声明,不能被重新赋值,这里的不能被重新赋值指的是,你不能对该变量的内存地址进行修改。如下

const obj = {};
	obj = {};

当一个变量被使用const声明,该变量的内存地址不可变,接下来,用另一个对象进行赋值(本质是把另一个对象的地址给obj变量)是不允许的。
对象是存在堆中的,如果直接对象本身修改是可以的。如下

obj.a = 'xxx';

如果想要一个对象真的不可修改,可以使用Objec.freeze()这个方法,对象将会被冻结,关于该对象的一切都不可修改。包括该对象的原型。语法如下,return: 被冻结的对象。

Object.freeze(对象)
解构赋值

结构赋值的本质是模式匹配,只要等号两边的模式相同,左边的变量就会被赋予对应的值。没有被赋值到的值默认是undefined

// 1. 基本语法
let [a, b, c] = [1, 2, 3];
// 2. 对象结构赋值
let json = {"name": "wuxue", "age": 20}
let {name:n, age:a} = json;
// 应用:交换两数,接受后台数据
字符串的查找
// str是否包含某字符串 返回的是boolean
1. str.includes(substr)
// 返回的是索引的位置
2. str.indexOf(substr)
// 判断是否是某字符串开头/结束。返回值为Boolean
3. str.startsWith('substr')/str.endsWith('substr')
// 重复谋和某个字符串,n 重复的次数,返回重复后的string
4. str.repeat(n);
// 在字符串的头部填充字符串
// 如果规定的字符串的长度超出了,填充的字符串会被自动截取
5. str.padStart(targetLength, padString)
6. str.padEnd(padString, padString);
ES6新增的数组方法
// 第一个参数是回调函数  第二个参数是this指向
// 回调函数参数分别为 value(当前值)、index(当前索引)、arr(指向当前调用的arr)
// 箭头函数的话,无法改变this指向

// 加强的for循环
1. arr.forEach()
	callback(value,index,arr);
// 正常情况需要配合return 如果没有return 功能相当于foreach 返回值是一个数组,	将元素组项变换
// 注意:只要是用map 一定要有return 语句

2. arr.map()
    callback(item,index,arr);

//  如果filter 的回调函数 是true的项留下
3. arr.filter()
    callback(item,index,arr) {
        return condition;
    }

// 某些项满足条件
4. arr.some()

// 每一项满足条件
5. arr.every()
    callback(item,index,arr) {
        return condition;
    }
// 默认prev指arr[0] cur为arr[1]
6. arr.reduce()
    callback(prev,cur,index,arr){
        return prev + cur;
    }

// 从arr的右边向左边
7. arr.reduceRight();
新增的对象方法
简写写法
1. 属性值和属性一样时,可以只写其中一个
// 比较两个值是否相等 可以用来判断两个对象的是否有同一个引用
2. Object.is(a, b);
// Object.is(NaN,NaN)	true
// Object.is(+0,-0)	false
// 用来合并对象/复制	复制数组/合并 后面的对象中的一致的会覆盖前面的
5. Object.assign(target,souse1,souse2...)
函数的新变化
  1. 可以有默认参数,参数可以解构

  2. 函数的参数默认是已经定义的,不能在函数中重复定义

  3. 拓展运算符,reset运算符 ...展开数组 在函数参数中用拓展运算符接受多余参数

  4. 箭头函数let funName = (params)=> {}; 如果只有一个return语句,大括号省略, return省略

  5. 箭头函数中的this是定义函数的时候所在的对象,不再是运行时所在对象。

  6. 箭头函数中没有arguments

  7. 箭头函数不能当成构造函数

新增的数据结构

1. Set数据结构

//会将重复的值过滤掉,具有唯一性
let sets = new Set(['a','b','c']);
sets.add('d');		// 添加一个元素
sets.delete('d');	// 删除一个元素
sets.has('d');		// false
sets.size			// 3 查看个数
sets.clear()		// 清除所有

// 返回一个新的迭代器对象,该对象包含Set对象中的按插入顺序排列的所有元素的值
set.value();
// 与values()方法相同,返回一个新的迭代器对象,该对象包含Set对象中的按插入顺序排列的所有元素的值。
for (let item of sets.keys()) {
    console.log(item);
}
// 返回一个新的迭代器对象,该对象包含Set对象中的按插入顺序排列的所有元素的值的[value, value]数组。
// 为了使这个方法和Map对象保持相似, 每个值的键和值相等。
for (let [k,v] of sets.entries())

sets.forEach((value,index) => {})

配置React开发格式化工具指南与避坑


title: 配置React开发格式化工具指南与避坑
date: 2021-04-26 20:00:00
tags: 工程化
categories: 工程化

配置React开发格式化工具指南与避坑

前言: 开发项目之前,定下良好的开发规范,能帮助我们规范流程,避免因为偷懒等因素,造成后期项目难以维护,commit log难以阅读等问题。本次借助项目机会,利用格式化工具prettier,对项目的格式化,commitlintgit commit进行约束。便于后期的维护,方便其他人阅读代码。中间踩很多坑,希望能给大家带来帮助

注:本次项目基于create-react-app创建的react项目

配置prettier格式化工具

  1. react不是为我们提供了eslint吗?

答:eslint只能进行语法检查,只能帮我们检查语法错误。prettier时代码格式化工具,能保证我们代码的格式,即使其他人阅读你的代码,也看到的时和你一样风格的代码。

  1. 为什么使用?
    • 按保存键时,代码就被格式化了
    • 代码评审时无须争论代码样式
    • 节省时间和精力

如果你按照prettier进行安装使用,其中有几个坑需要注意,请往下阅读。

简单配置

  1. 安装

    npm install --save-dev --save-exact prettier

  2. echo {}> .prettierrc.json

    如果你使用的是windows电脑并且使用vscode终端,按照官网在你的项目中使用这条命令创建.prettierrc.json文件,创建过程不会出现任何的错误,但是在你下面操作的过程中,如使用npx prettier --write .命令时会报错,并且你很难定位。原因是,创建的过程中,会创建出一个格式不是utd8格式的json文件,取决于你的系统和电脑。因为报错信息没有精准定位,导致很难排查。你可以手动创建该文件或者将该文件的格式修改为utf8

  3. 创建.prettierignore文件,用来忽略那些文件或者文件夹不需要进行格式化

  4. 进行格式化npx prettier --write .

    使用上面这条命令可以将你选择的文件或者文件夹内的文件进行格式化。如下图所示,格式化之前和格式化之后的对比。

    • 格式化之前的代码片段

    prettier

    格式化之前代码没有换行,所以超出了vscode的显示区,或许很多同学都会配置vscode的最大显示长度。但是因为每个人配置不一样,为了能让一套代码,在所有人的编辑器里显示效果一样。所以使用prettier

    • 格式化之后的代码片段

    prettier_after

  5. 每次格式化时,都需要敲``npx prettier --write这条命令,略显繁琐。我们想要更只能的方式。这里有两种方案,一种是使用prettier`支持的编辑器插件,如`vscode`的`Prettier - Code formatter`,只需要在`vscode`中安装,然后就可以使用快捷键`Ctrl + Shift + P`来对文件进行格式化。但是我们觉得这种方式还是不够智能,所以重点介绍下面的方法,使用自动化工具帮助我们进行格式化。

配置自动化工具

前置知识
  1. husky 是一个为 git 客户端增加 hook 的工具。安装后,它会自动在仓库中的 .git/ 目录下增加相应的钩子;比如 pre-commit 钩子就会在你执行 git commit 的触发。那么我们可以在 pre-commit 中实现一些比如 lint 检查、单元测试、代码格式化等操作。

  2. lint-staged,一个仅仅过滤出 Git 代码暂存区文件(被 git add 的文件)的工具;我们如果对整个项目的代码做一个检查,可能耗时很长,如果是老项目,要对之前的代码做一个代码规范检查并修改的话,可能导致项目改动很大。

    lint-staged,对团队项目和开源项目来说,是一个很好的工具,它是对个人要提交的代码的一个规范和约束。

配置
  1. npx mrm lint-staged

    mrm 是一个自动化工具,它将根据 package.json 依赖项中的代码质量工具来安装和配置 husky 和 lint-staged,因此请确保在此之前安装并配置所有代码质量工具,如 Prettier 和 ESlint

    注: 有可能会安装失败,因为mrm需要单独安装,请使用npm install mrm -D

  2. 配置package.json文件

    create-react-app创建的项目,默认为我们提供了eslint进行代码检查,为了避免和prettier冲突。我们用prettier覆盖一部分的eslint配置。如下

    "eslintConfig": {
        "extends": [
          "react-app",
          "react-app/jest",
          "prettier"
        ]
      }

    package.json添加下面的配置

      "husky": {
        "hooks": {
          "pre-commit": "lint-staged"
        }
      },
      "lint-staged": {
        "*.{js,css,md,ts,tsx}": "prettier --write"
      }
    

    添加完毕,当我们使用git commit的时候,就会去调用husky中定义的hooks钩子,这个钩子就会去找lint-stagedlint-staged中使用了最开始手动格式化得到命令prettier --write。到这里,这一套自动化的工具,就会帮助我们自动格式化我们commit的代码。

commitlint

这个工具在我们在每一次commit时,检查我们的commit messages是否符合规范。它为我们提供了一套规则,我们必须按照它提供的规范来进行commit messages的填写。这样做的好处时,我们以后的commit messages非常的适合阅读。通过阅读,能够清楚的知道我们本次commit进行了什么的修改或者操作。

安装配置

  1. install

    npm install -g @commitlint/cli @commitlint/config-conventional

  2. Configure

    echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js

  3. 配置规则,当我们使用commit的时候,会帮我们去检验git commit 的messages时候符合规范。

    "husky": {
        "hooks": {
          "pre-commit": "lint-staged",
          "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
        }
      }

git commit 格式

git commit -m <type>[optional scope]: <description>
常用的type类别

type :用于表明我们这次提交的改动类型

  • build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交

  • ci:主要目的是修改项目继续集成流程(例如 Travis,Jenkins,GitLab CI,Circle等)的提交

  • docs:文档更新

  • feat:新增功能

  • fix:bug 修复

  • perf:性能优化

  • refactor:重构代码(既没有新增功能,也没有修复 bug)

  • style:不影响程序逻辑的代码修改(修改空白字符,补全缺失的分号等)

  • test:新增测试用例或是更新现有测试

  • revert:回滚某个更早之前的提交

  • chore:不属于以上类型的其他类型(日常事务)

optional scope:一个可选的修改范围。用于标识此次提交主要涉及到代码中哪个模块。

description:描述此次提交的内容信息

例子

git commit -m "test: test commitlint"

以上就是配置prettier进行自动化代码格式化和自动化检测,规范git commit 的commitlint的配置使用。还有更多高级功能等待发现。

使用Ajax结合FormData对象实现文件上传


title: 使用Ajax结合FormData对象实现文件上传
date: 2021-03-16 23:07:14
tags: Node
categories: Node

使用Ajax结合FormData实现文件上传

使用FormData之前,我们需要知道它的作用,或者说是本质吧。FormData本质上是HTML提供的一个对象,可以模拟HTML表单,相当于将HTML表单映射成表单对象,自动将表单对象中的数据拼接成请求参数的格式。用一个对象来代替正常的FORM表单提交,但是又比表单更强大。

1 FormData使用方法

1.1 创建一个表单对象

因为FormData对象不能脱离form元素使用,所以我们必须先有一个表单。

注意:创建表单,表单中的元素,如input标签等,必须有name属性,因为FormData对象要根据name属性来获得对应的值,或者是操作对应的值。下文会有详细介绍

    <form id="form" name="form">
        <input type="text" name="username">
        <input type="password" name="password">
    </form>
1.2 创建一个FormData实例,将表单转化为FormData对象
// 使用FormData构造函数创建一个实例,注意该构造函数接受一个参数,参数必须是一个表单对象。如下:
// 获得表单
1. let form = document.getElementById('form');
// 将表单转化为Form对象,此时,FormData其实就是包含了表单form内容的一个对象。具体的形式为{key, value}的形式,key就是form表单的name属性值,因此,form中的控件必须有name属性。
2. let formData = new FormData(form);

注意: 完成了上面步骤后,如果此时你在开发者工具尝试输出FormData对象,输出会是一个空对象,因为FormData是一种特殊格式,无法输出。

1.3 使用Ajax发送FormData

创建的FormData对象,使用Ajax中的send()进行发送,FormData对象只能使用send()进行发送,不能使用get请求的url拼接参数的方式。
如下,客户端使用Ajax进行了一次post请求。

let xhr = new XMLHttpRequest();

xhr.open('POST', 'http://localhost:3000/FormData');

// TODO 对于FormData对象,不能设置常规的Content-Type
// xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

xhr.send(formData);

xhr.onload = () => {
	console.log(xhr.responseText);
}

Node服务端进行响应(使用express框架,formidable第三方包)

// 路由
app.post('/FormData', (req, res, next) => {
	// form 使用formidable的一个实例,用来接受处理FormData的请求,fields参数是FormData的内容。
    form.parse(req, (err, fields, files) => {
    	console.log(fields)    //{"username":"123","password":"123"}
        // 返回结果给客户端
        res.send(fields);
        next();
    })
})

如上,FormData对象的基础对象就结束了,FormData对象上有很多方法,请自行查阅文档学习,我们本次文件上传只用到了append()方法,语法为append(name, value)。另一些说明在代码中进行了详细注释

完整的实现代码

html

<!-- 容器 -->
    <div class="container" style="overflow: hidden;">
    <!-- 进度条 -->
        <div class="progress" style="margin: 20px auto;">
            <div id="progress" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100" style="width: 0"></div>
        </div>
         <!-- 空的form表单 -->
        <form id="form" name="form">
        </form>
         <!-- 文件上传控件 -->
        <input type="file" id="upload-file" name="file">
         <!-- 进行文件上传成功后的图片动态显示显示 -->
        <div class="avatar">
        </div>
    </div>

JavaScript

<script>
        // 获得form表单
        let form = document.getElementById('form');
        let file = document.getElementById('upload-file');
        // 存放动态渲染图片的区域
        let avatar = document.querySelector('.avatar');
        // 控制进度显示
        let progress = document.getElementById('progress');
        // 创建FormData实例
        let formData = new FormData();
        // TODO 事件处理函数内部需要使用this时,不要使用箭头函数 导致this丢失
        function handleUploadFile() {
            let xhr = new XMLHttpRequest();
            // TODO 文件上传this.files是一个数组 存储了选择的文件
            // 传递的应该是对应的文件如第一个文件就应该是此处的this.files[0]
            // 将选择的文件添加到FormData对象中
            formData.append('file', this.files[0]);
			// 发送Ajax请求
            xhr.open('POST', 'http://localhost:3000/upload');
            // 将XMLHttpRequest.upload.onprogress 放到send()方法后是不生效的
            xhr.upload.onprogress = function(ev) {
            	// loaded 已加载内容  total 总共的内容
                let per = (ev.loaded / ev.total) * 100 + '%';
                progress.style.width = per;
            }
            // 发送FormData表单 
            xhr.send(formData);
            // 提前知道服务端返回的是图片的地址,所以此处声明一个变量,接受返回地址
            let imgPath = '';
			// 服务端返回
            xhr.onload = () => {
                // 服务端返回的数据格式为json字符串 需要进行反序列化
                let responseData = JSON.parse(xhr.responseText);
                // 利用服务端返回的信息 利用\upload\进行字符串切割 取得文件路径
                imgPath = responseData.path.split('\\public\\')[1];
				// 请求返回成功
                if (xhr.status === 200) {
                	// 动态创建一个img标签
                    let img = document.createElement('img');
                    // 图片的地址给img添加
                    img.src = imgPath;
                    // 图片加载完成
                    img.onload = function() {
                        // 将img标签添加到DOM中
                        avatar.appendChild(img);
                    }
                }
            }
        }
        file.addEventListener('change', handleUploadFile);
    </script>

服务端处理

const express = require('express');
const port = 3000;
const public = express.static('public');
const formidable = require('formidable');
const form = new formidable.IncomingForm();
const path = require('path');
// 设置文件上传的路径
form.uploadDir = path.join(__dirname + '/public' + '/upload');
// 默认文件上传后会去除后缀名。开启保留文件后缀名
form.keepExtensions = true;
const app = express();
app.use('/', public);

// 文件上传   主要看这里就可以了
app.post('/upload', (req, res) => {
    form.parse(req, (err, fields, files) => {
        //files.file是一个对象存放了接收到的文件的相关信息
        // 文件在服务器端的地址 
        let path = files.file.path;
        // 默认对象会以json的格式返回 
        res.send({ path: path });
    })
})

app.listen(port, () => {
    console.log('express start access port is 3000')
})

以上就是利用Ajax结合FormData对象实现的简易版的文件上传功能。实践出真知。虽然看着很简单,但是在完成的过程中,遇到了各种各样的小问题。慢慢的Google,慢慢的看文档,思考。收获的还是蛮多的。一起动手试试吧

React学习笔记


title: React学习笔记
date: 2020-04-14 20:13:53
tags: React
categories: React

React学习笔记

1. 什么时候需要使用类组件的构造函数

根据官方给出的实例,大概只有这么三种情况下,才必须要使用类组件的构造函数。

  1. 需要在构造函数中初始化state。
  2. 必须在构造函数中对this.props进行接收(通常来说这种情况不是必须的)。
  3. 需要绑定类组件中自定义方法的this。(因为自定义方法中的this,如果不是使用箭头函数
    定义的,则自定的函数中的this指向会丢失。所以需要在constructor中使用bind进行绑定。这种情况可以使用箭头函数来代替。)
class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }
}

2. 如何实现组件的通信

2.1 自上而下的数据流

React哲学是从上至下的数据流,当从上至下进行通信时,通常的方式是,上层组件传递给下层组件的数据放在下层组件的属性上,最终下层组件会在props对象上拿到上层组件传递的数据并进行渲染。

2.2 同级组件间的通信

2.2.1 React原生

React不支持同级组件间直接通信,提出了一种状态提升的概念。需要组件通信时,调用上层组件传递的方法,将需要传递的内容放在该函数的实参中。上层组件在该函数的定义体中拿到下层组件传递的数据,也就是状态提升到上层组件,上层组件将数据传递给其他子组件,从而实现了同级组件的通信。

2.2.2 发布订阅

当组件间的关系变得复杂时,react原生的组件通信方式,会产生很多"中间者"性质的props。代码会变得允许且不优雅。发布订阅模式是其中的一个解决方案。
发布订阅,即,需要发起通信的组件,发布消息,需要接受消息的组件,订阅消息。当消息发布,接受消息的组件,知道消息发布,从而可以接收到消息。伪代码如下:

订阅消息的组件

import React, { Component } from 'react'
import PubSub from 'pubsub-js'
export default class List extends Component {
	// 组件挂载结束之后订阅
    componentDidMount() {
        this.token = PubSub.subscribe('setData', (msg, data) => {
            // 根据订阅结果更新state
            this.setState(data);
        })
    }

	// 组件卸载了之后清除订阅
	componentWillUnmount() {
	    PubSub.unsubscribe(this.token);
	}
}

发布消息的组件

import React, { Component } from 'react'
import PubSub from 'pubsub-js'

export default class Search extends Component {
	// 调用getUserInfo函数时,发布消息
	getUserInfo = () => {
	PubSub.publish('setData', {
                isFirst: false,
                isLoading: true,
                isError: false,
                listItems: [] 
            });
	}
}

2.2.3 Redux

使用发布订阅可以解决一些问题,但是随着项目的规模升级,复杂度升级。使用发布订阅,也可以解决问题,组件内到处都是发布订阅的方法,项目会变得越来越难以掌控,维护性也会变差。随之,redux(状态管理)这种解决方案出现了。和发布订阅一样,redux也不是原生React的内容,是官方维护的第三方库,所以可以放心使用。

"只有遇到 React 实在解决不了的问题,你才需要 Redux 。"

原理

redux

图中,有几个概念还是需要清楚的。

  1. Store

    Store,状态管理仓库,所以的状态在Store中进行管理。

  2. Action Creators

    动作创造者,如 进行 + 1 的操作,就会创造出一个action, 形式是:{ type: '+', data: 1 }

  3. Reducers

    最终进行处理的地方

Store作为一个中心调度者,将action分发给reducers, reducers

Git命令


title: Git命令
date: 2018-12-04 21:16:04
tags: Git
categories: Git

Git是一个版本控制系统,已经成为程序员必备的,必会的的工作软件。还没有入门的小伙伴,推荐大家学习廖雪峰老师的Git教程,该教程非常适合新入门的同学学习,通俗易懂。因为基础的概念已经清楚了,但是对于常用的命令还没有很熟练,做一个常用命令的整理

常用命令

  1. 添加远程仓库(本质就是为远程仓库添加一个别名,方便以后操作)
git remote add origin(仓库别名) ([email protected]:askwuxue/Users.git)远程仓库地址
  1. 查看远程仓库
git remote -v 
  1. 删除远程仓库
git remote rm origin
  1. 新建并切换分支
git checkout -b dev(新分支名)
  1. 切换分支
git branch dev(要切换的分支名)
  1. 删除分支
git branch -d dev(要删除的分支名)
  1. 查看所有标签
git tag
  1. 查看某标签
git show <tagName>
  1. 新建标签
git tag <tagName> <commitId>
  1. 新建标签并添加描述
 git tag -a <tagName> -m "tag message" <commitId>
  1. 推送至远程库
// 将分支内容推送至远程库,远程库就是github仓库地址的别名。-u是记住本次的对应关系,后续可直接使用 git push
git push -u origin(远程仓库) master(本地分支)
  1. 撤销暂存区修改
git checkout --filename 
  1. 版本回退
// 第一步查看要回退到哪一个版本
git log    // 查看提交历史
git log --pretty=oneline // 查看提交历史的主要信息,比git log信息简洁

// 第二步
git reset --hard commitID 	// commitId 在查看版本历史的时候,会清楚的显示

常见问题

  1. 解决冲突
    冲突产生:一个文件,没有在同一个版本的基础上顺序修改就产生了冲突。如:A,B 拿了同一个版本的文件进行编辑,A编辑好了,并且push了。B随后编辑好了,也要push。但是这个时候A已经进行过编辑,B又编辑,就产生了冲突。本质是没有拿同一个版本的文件进行编辑。所以在正常情况下,最好拿最新版本进行编辑,可以有效避免冲突。如果冲突已经产生,课按照下面的方式进行处理
    1.1 手动解决冲突,在merge的时候,Git会告诉我们有冲突,如下
    conflict

Git 告诉我们有冲突,需要我们手动进行合并。合并后如下

resolve_conflict

手动合并结束,接下来,只需要像处理普通文件git addgit commit进行处理即可,至此,冲突解决完毕。

外边距叠加的相关问题


title: 外边距叠加的相关问题
date: 2018-03-11 21:23:14
tags: CSS
categories: CSS

外边距叠加的问题

外边距叠加是一个简单的概念:在CSS中,两个或多个(特殊情况:一个)毗邻的普通流中的盒子(可能是父子元素,也可能是兄弟元素)在垂直方向上的外边距会发生叠加,这种形成的外边距称之为外边距叠加。

关键词: 两个或者多个,盒子(块级元素),毗邻,普通流,垂直方向

注意浮动元素绝对定位元素的外边距不会折叠(因为这里触发了 块格式化上下文 Block Formatting Context, BFC)。

  1. 兄弟元素之间,在垂直方向上发生了外边距叠加

外边距叠加

2.当父子元素之间的margin-top或者margin-bottom没有被border,padding隔开的时候,他们的内外边距会发生叠加。

外边距叠加2

  1. 单个的块级元素中不包含任何内容,并且在其 margin-topmargin-bottom 之间没有边框、内边距、行内内容、heightmin-height 将两者分开,则该元素的上下外边距会折叠。

外边距叠加3

注意

  • 即使某一外边距为0,这些规则仍然适用。因此就算父元素的外边距是0,第一个或最后一个子元素的外边距仍然会“溢出”到父元素的外面。
  • 如果参与折叠的外边距中包含负值,折叠后的外边距的值为最大的正边距与最小的负边距(即绝对值最大的负边距)的和。
  • 如果所有参与折叠的外边距都为负,折叠后的外边距的值为最小的负边距的值。这一规则适用于相邻元素和嵌套元素。
外边距叠加的影响

​ 外边距叠加的影响不一一列举,常见的就是想要实现子元素的margin-top等效果,结果发现父盒子也被加上了margin-top,从而影响了父盒子的位置。遇到类似问题,能够想到是因为外边距合并导致的就可以了。当然,外边距叠加并不一定都是负面的影响,如下图,也有正面的影响。在文字排版的时候,利用外边距合并,实现了段落之间相同宽度的效果。在没有想到更好的解决方案之前,这可以作为一个备选方案。
外边距叠加4

如何消除外边距叠加

从外边距叠加入手,就可以解决这个问题。

  1. 如果两个元素上下毗邻,分别设置外边距的时候考虑一下,如果必须设置,考虑利用浮动或者绝对定位是否能解决。
  2. 当嵌套块级元素的时候,看看时候能为他们设置padding或者border来破坏外边距叠加的条件
  3. 不要为一个空元素设置上下边距,应该没有人会这么干。
  4. inline-block 元素和其他任何元素之间不发生外边距叠加,也包括它的子元素
  5. 创建了 BFC 的元素不会和它的子元素发生外边距叠加,不同的BFC中,不会发生外边距叠加

如有不正之处,欢迎指正。随着学习的深入,可能会进一步的对BFC的原理等进行输出。

Promise实现原理


title: Promise实现原理
date: 2020-10-11 20:21:11
tags: JavaScript
categories: JavaScript

Promise实现原理

本文旨在实现Promise,Promise用法请求移步至阮一峰老师ES6教程

Promise用法

const p1 = new Promise((resolve, reject) => {
  console.log('create a promise');
  resolve('成功了');
})

console.log("after new promise");

const p2 = p1.then(data => {
  console.log(data)
  throw new Error('失败了')
})

const p3 = p2.then(data => {
  console.log('success', data)
}, err => {
  console.log('faild', err)
})

输出

"create a promise"
"after new promise"
"成功了"
"faild Error: 失败了"

Promise基本特征

  1. promise 有三个状态:pendingfulfilledrejected

  2. new promise时, 需要传递一个executor()执行器,执行器立即执行;

  3. executor接受两个参数,分别是resolvereject

  4. promise 的默认状态是 pending

  5. promise 有一个value保存成功状态的值,可以是undefined/thenable/promise

  6. promise 有一个reason保存失败状态的原因;

  7. promise 只能从pendingrejected, 或者从pendingfulfilled,状态一旦确认,就不会再改变;

  8. promise 必须有一个then方法,then 接收两个参数,分别是 promise 成功的回调 successCallBack, 和 promise 失败的回调 failCallBack;

  9. 如果调用 then 时,promise 已经成功,则执行successCallBack,参数是promisevalue

  10. 如果调用 then 时,promise 已经失败,那么执行failCallBack, 参数是promisereason

  11. 如果 then 中抛出了异常,那么就会把这个异常作为参数,传递给下一个 then 的失败的回调failCallBack

Promise实现

​ 因为Promise实现有很多的细节,直接贴上最终版的代码,可能会劝退很多人,也不是很好理解。我们从最基础的Promise对象功能以及方法的实现入手,由简单到复杂,展现最终效果。

1. 基础版Promise实现

​ 如果大家了解了Promise的基本特征,很容易实现这个简易版的Promise对象。代码如下,有详细的注释。

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise {
  constructor(executor) {
    executor(this.resolve, this.reject)
  }
  // Promise状态
  status = PENDING
  // 成功的的值
  value = undefined
  // 失败的原因
  reason = undefined
  // 成功的回调函数
  successCallBack = undefined
  // 失败的回调函数
  failCallBack = undefined
  // pending -> fulfilled
  resolve = value => {
    if (this.status !== PENDING) return
    this.status = FULFILLED
    this.value = value
  }
  // pending -> rejected
  reject = reason => {
    if (this.status !== PENDING) return
    this.status = REJECTED
    this.reason = reason
  }
  // then
  then(successCallBack, failCallBack) {
    if (this.status === FULFILLED) {
      successCallBack(this.value)
    } else if (this.status === REJECTED) {
      failCallBack(this.reason)
    }
  }
}
// 导出Promise
module.exports = MyPromise

2. 异步的Promise

​ 上面的简易版中,我们基本实现了Promise的特征。但是少了Promise的灵魂,我们都知道Promise的出现,是为了解决异步调用的问题,但是简易版中我们并没有支持异步调用。实现之前,我们先看一下,Promise是如何支持异步调用的。

下面是一个Promise对象的简单例子。

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'done');
  });
}

timeout(100).then((value) => {
  console.log(value);
});

上面代码中,timeout方法返回一个Promise实例,表示一段时间以后才会发生的结果。过了指定的时间(ms参数)以后,Promise实例的状态变为resolved,就会触发then方法绑定的回调函数。

Promise 新建后就会立即执行。

let promise = new Promise(function(resolve, reject) {
  console.log('Promise');
  resolve();
});

promise.then(function() {
  console.log('resolved.');
});

console.log('Hi!');

// Promise
// Hi!
// resolved

上面代码中,Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出。

​ 现在请大家认真观察简易版中then方法的实现,我们只匹配了fulfilled的状态和rejected的状态,如果想要支持异步的调用,那么then方法还应该存在pending状态,当resolve或者reject方法触发时,then方法的pending状态才会发生变化。

核心
  • then方法pending状态时将成功回调和失败回调暂存
  • 调用resolve方法时,判断是否存在successCallBack方法,存在则调用
  • 调用reject方法时,判断是够存在failCallBack,存在则调用
实现代码
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise {
  constructor(executor) {
    executor(this.resolve, this.reject)
  }
  // Promise状态
  status = PENDING
  // 成功的的值
  value = undefined
  // 失败的原因
  reason = undefined
  // 成功的回调函数
  successCallBack = undefined
  // 失败的回调函数
  failCallBack = undefined
  // pending -> fulfilled
  resolve = value => {
    if (this.status !== PENDING) return
    this.status = FULFILLED
    this.value = value
    // 调用then方法中的回调函数
    this.successCallBack && this.successCallBack(value)
  }
  // pending -> rejected
  reject = reason => {
    if (this.status !== PENDING) return
    this.status = REJECTED
    this.reason = reason
    // 调用then方法中的回调函数
    this.failCallBack && this.failCallBack(reason)
  }
  // then
  then(successCallBack, failCallBack) {
    if (this.status === FULFILLED) {
      successCallBack(this.value)
    } else if (this.status === REJECTED) {
      failCallBack(this.reason)
      // pending状态时将成功回调和失败回调暂存
    } else {
      this.successCallBack = successCallBack
      this.failCallBack = failCallBack
    }
  }
}

module.exports = MyPromise

3. 多次调用then方法

​ 因为then方法可以多次调用,我们要对多次then方法调用进行处理。如果是同步的情况下我们不需要进行特殊处理。因为每一个then会等待上一个then方法执行结束后执行,我们需要对异步情况下的then方法多次调用进行处理。处理逻辑其实很简单,我们只需要对then方法的pending逻辑进行修改就可以,一个then方法得到时候,我们只需要一个变量进行存储回调函数,多个then方法调用。我们只需要将回调函数存储到一个数组中。当执行resolve方法或者reject方法时,我们按照顺序将数组中的回调函数取出并执行就可以了。

核心
  • then方法的pending状态时,将回调函数存储到数组中。
  • 调用resolve方法时,判断是否存在successCallBack方法,存在则调用
  • 调用reject方法时,判断是够存在failCallBack,存在则调用
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise {
  constructor(executor) {
    executor(this.resolve, this.reject)
  }
  // Promise状态
  status = PENDING
  // 成功的的值
  value = undefined
  // 失败的原因
  reason = undefined
  // 成功的回调函数
  successCallBack = []
  // 失败的回调函数
  failCallBack = []
  // pending -> fulfilled
  resolve = value => {
    if (this.status !== PENDING) return
    this.status = FULFILLED
    this.value = value
    // 调用then方法中的回调函数
    while (this.successCallBack.length) {
      this.successCallBack.shift()(this.value)
    }
  }
  // pending -> rejected
  reject = reason => {
    if (this.status !== PENDING) return
    this.status = REJECTED
    this.reason = reason
    // 调用then方法中的回调函数
    while (this.failCallBack.length) {
      this.failCallBack.shift()(this.reason)
    }
  }
  // then
  then(successCallBack, failCallBack) {
    if (this.status === FULFILLED) {
      successCallBack(this.value)
    } else if (this.status === REJECTED) {
      failCallBack(this.reason)
      // pending状态时将成功回调和失败回调暂存
    } else {
      this.successCallBack.push(successCallBack)
      this.failCallBack.push(failCallBack)
    }
  }
}

module.exports = MyPromise

then方法的链式调用

​ Promise 实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数,它们都是可选的。

then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

// 判断Promise对象then方法返回值并进行处理
const resolvePromise = (nextValue, resolve, reject) => {
  // 如果在then方法中返回了当前Promise对象,则进行了循环引用,需要错误处理
  if (nextValue instanceof MyPromise) {
    // 返回值是Promise对象,调用该Promise对象的then方法
    nextValue.then(resolve, reject)
  } else {
    resolve(nextValue)
  }
}

class MyPromise {
  constructor(executor) {
    executor(this.resolve, this.reject)
  }
  // Promise状态
  status = PENDING
  // 成功的的值
  value = undefined
  // 失败的原因
  reason = undefined
  // 成功的回调函数
  successCallBack = []
  // 失败的回调函数
  failCallBack = []
  // pending -> fulfilled
  resolve = value => {
    if (this.status !== PENDING) return
    this.status = FULFILLED
    this.value = value
    // 调用then方法中的回调函数
    while (this.successCallBack.length) {
      this.successCallBack.shift()(this.value)
    }
  }
  // pending -> rejected
  reject = reason => {
    if (this.status !== PENDING) return
    this.status = REJECTED
    this.reason = reason
    // 调用then方法中的回调函数
    while (this.failCallBack.length) {
      this.failCallBack.shift()(this.reason)
    }
  }
  // then
  then(successCallBack, failCallBack) {
    // 如果then方法没有传递参数,则使用一个函数作为默认参数
    successCallBack = successCallBack ? successCallBack : value => value
    failCallBack = failCallBack ? failCallBack : reason => { throw reason }
    // then方法返回一个Promise对象
    let promise = new MyPromise((resolve, reject) => {
      // 当前状态是fulfilled,执行成功回调
      if (this.status === FULFILLED) {
        // 传递给下一个Promise对象的值是then方法的返回值
        let nextValue = successCallBack(this.value)
        resolvePromise(nextValue, resolve, reject)
        // 当前状态是rejected,执行失败回调
      } else if (this.status === REJECTED) {
        let nextValue = failCallBack(this.reason)
        resolvePromise(nextValue, resolve, reject)
        // 当前状态是padding, 如异步函数调用。暂时将成功和失败的回调存储。
      } else {
        this.successCallBack.push(() => {
          let nextValue = successCallBack(this.value)
          resolvePromise(nextValue, resolve, reject)
        })
        this.failCallBack.push(() => {
          let nextValue = failCallBack(this.reason)
          resolvePromise(nextValue, resolve, reject)
        })
      }
    })
    return promise
  }
}

module.exports = MyPromise

捕获错误

​ 前面的版本我们没有进行错误处理,我们接下来在可能出现错误的地方,进行错误处理,让代码更严谨一点

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

// 判断Promise对象then方法返回值并进行处理
const resolvePromise = (promise, nextValue, resolve, reject) => {
  // 如果在then方法中返回了当前Promise对象,则进行了循环引用,需要错误处理
  if (promise === nextValue) return reject(new TypeError(`Chaining cycle detected for promise #<Promise>`))
  // 如果在then方法中返回了当前Promise对象,则进行了循环引用,需要错误处理
  if (nextValue instanceof MyPromise) {
    // 返回值是Promise对象,调用该Promise对象的then方法
    nextValue.then(resolve, reject)
  } else {
    resolve(nextValue)
  }
}

class MyPromise {
  constructor(executor) {
    executor(this.resolve, this.reject)
  }
  // Promise状态
  status = PENDING
  // 成功的的值
  value = undefined
  // 失败的原因
  reason = undefined
  // 成功的回调函数
  successCallBack = []
  // 失败的回调函数
  failCallBack = []
  // pending -> fulfilled
  resolve = value => {
    if (this.status !== PENDING) return
    this.status = FULFILLED
    this.value = value
    // 调用then方法中的回调函数
    while (this.successCallBack.length) {
      this.successCallBack.shift()(this.value)
    }
  }
  // pending -> rejected
  reject = reason => {
    if (this.status !== PENDING) return
    this.status = REJECTED
    this.reason = reason
    // 调用then方法中的回调函数
    while (this.failCallBack.length) {
      this.failCallBack.shift()(this.reason)
    }
  }
  // then
  then(successCallBack, failCallBack) {
    // 如果then方法没有传递参数,则使用一个函数作为默认参数
    successCallBack = successCallBack ? successCallBack : value => value
    failCallBack = failCallBack ? failCallBack : reason => { throw reason }
    // then方法返回一个Promise对象
    let promise = new MyPromise((resolve, reject) => {
      // 当前状态是fulfilled,执行成功回调
      if (this.status === FULFILLED) {
        // 为了能拿到Mypromise实例对象,需要异步执行函数
        setTimeout(() => {
          try {
            // 传递给下一个Promise对象的值是then方法的返回值
            let nextValue = successCallBack(this.value)
            resolvePromise(promise, nextValue, resolve, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)
        // 当前状态是rejected,执行失败回调
      } else if (this.status === REJECTED) {
        setTimeout(() => {
          try {
            let nextValue = failCallBack(this.reason)
            resolvePromise(promise, nextValue, resolve, reject)
            // 当前状态是padding, 如异步函数调用。暂时将成功和失败的回调存储。
          } catch (e) {
            reject(e)
          }
        }, 0);
      } else {
        this.successCallBack.push(() => {
          setTimeout(() => {
            try {
              let nextValue = successCallBack(this.value)
              resolvePromise(promise, nextValue, resolve, reject)
            } catch (e) {
              reject(e)
            }
          }, 0)
        })
        this.failCallBack.push(() => {
          setTimeout(() => {
            try {
              let nextValue = failCallBack(this.reason)
              resolvePromise(promise, nextValue, resolve, reject)
            } catch (e) {
              reject(e)
            }
          }, 0);
        })
      }
    })
    return promise
  }
}

module.exports = MyPromise

Promise.resolve

  // Promise.resolve 静态方法
  static resolve(value) {
    if (value instanceof MyPromise) return value;
    return new MyPromise(resolve => resolve(value));
  }

Promise.all

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.all([p1, p2, p3]);

上面代码中,Promise.all()方法接受一个数组作为参数,p1p2p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。

p的状态由p1p2p3决定,分成两种情况。

(1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

  // Promise.all 静态方法
  static all(array) {
    let result = [];
    let index = 0;
    // Promise.all 返回一个Promise对象
    return new MyPromise((resolve, reject) => {
      for (let i = 0; i < array.length; ++i) {
        MyPromise.resolve(array[i]).then(value => {
          result[i] = value
          ++index
          // 当all参数数组中所有项执行结束后执行resolve方法
          if (index === array.length) {
            resolve(result);
          }
        }, err => reject(err))
      }
    })
  }

最终版Promise

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

// 判断Promise对象then方法返回值并进行处理
const resolvePromise = (promise, nextValue, resolve, reject) => {
  // 如果在then方法中返回了当前Promise对象,则进行了循环引用,需要错误处理
  if (promise === nextValue) return reject(new TypeError(`Chaining cycle detected for promise #<Promise>`))
  // 如果在then方法中返回了当前Promise对象,则进行了循环引用,需要错误处理
  if (nextValue instanceof MyPromise) {
    // 返回值是Promise对象,调用该Promise对象的then方法
    nextValue.then(resolve, reject)
  } else {
    resolve(nextValue)
  }
}

class MyPromise {
  constructor(executor) {
    executor(this.resolve, this.reject)
  }
  // Promise状态
  status = PENDING
  // 成功的的值
  value = undefined
  // 失败的原因
  reason = undefined
  // 成功的回调函数
  successCallBack = []
  // 失败的回调函数
  failCallBack = []
  // pending -> fulfilled
  resolve = value => {
    if (this.status !== PENDING) return
    this.status = FULFILLED
    this.value = value
    // 调用then方法中的回调函数
    while (this.successCallBack.length) {
      this.successCallBack.shift()(this.value)
    }
  }
  // pending -> rejected
  reject = reason => {
    if (this.status !== PENDING) return
    this.status = REJECTED
    this.reason = reason
    // 调用then方法中的回调函数
    while (this.failCallBack.length) {
      this.failCallBack.shift()(this.reason)
    }
  }
  // then
  then(successCallBack, failCallBack) {
    // 如果then方法没有传递参数,则使用一个函数作为默认参数
    successCallBack = successCallBack ? successCallBack : value => value
    failCallBack = failCallBack ? failCallBack : reason => { throw reason }
    // then方法返回一个Promise对象
    let promise = new MyPromise((resolve, reject) => {
      // 当前状态是fulfilled,执行成功回调
      if (this.status === FULFILLED) {
        // 为了能拿到Mypromise实例对象,需要异步执行函数
        setTimeout(() => {
          try {
            // 传递给下一个Promise对象的值是then方法的返回值
            let nextValue = successCallBack(this.value)
            resolvePromise(promise, nextValue, resolve, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)
        // 当前状态是rejected,执行失败回调
      } else if (this.status === REJECTED) {
        setTimeout(() => {
          try {
            let nextValue = failCallBack(this.reason)
            resolvePromise(promise, nextValue, resolve, reject)
            // 当前状态是padding, 如异步函数调用。暂时将成功和失败的回调存储。
          } catch (e) {
            reject(e)
          }
        }, 0);
      } else {
        this.successCallBack.push(() => {
          setTimeout(() => {
            try {
              let nextValue = successCallBack(this.value)
              resolvePromise(promise, nextValue, resolve, reject)
            } catch (e) {
              reject(e)
            }
          }, 0)
        })
        this.failCallBack.push(() => {
          setTimeout(() => {
            try {
              let nextValue = failCallBack(this.reason)
              resolvePromise(promise, nextValue, resolve, reject)
            } catch (e) {
              reject(e)
            }
          }, 0);
        })
      }
    })
    return promise
  }

  catch(failCallback) {
    return this.then(undefined, failCallback)
  }

  // Promise.resolve 静态方法
  static resolve(value) {
    if (value instanceof MyPromise) return value;
    return new MyPromise(resolve => resolve(value));
  }

  // Promise.all 静态方法
  static all(array) {
    let result = [];
    let index = 0;
    // Promise.all 返回一个Promise对象
    return new MyPromise((resolve, reject) => {
      for (let i = 0; i < array.length; ++i) {
        MyPromise.resolve(array[i]).then(value => {
          result[i] = value
          ++index
          // 当all参数数组中所有项执行结束后执行resolve方法
          if (index === array.length) {
            resolve(result);
          }
        }, err => reject(err))
      }
    })
  }
}

module.exports = MyPromise

跨域方案解决


title: 跨域方案解决
date: 2019-01-10 18:39:20
tags: JavaScript
categories: JavaScript

跨域方案解决

ajax的出现,推动了web的发展。随之,为了防止恶意文档,保护用户信息安全。浏览器制定了同源安全策略

1. 同源安全策略是什么?

很简单,接着看!

1.1 什么是同源

同源就是比较两个URL地址,当他们满足一定的条件,他们就是同源的。
同时满足下面三个条件

  • 协议相同
  • 主机相同
  • 端口相同

看个例子

https://www.baidu.com/s?ie=UTF-8&wd=a
// 协议 https
// 主机 www.baidu.com
// 端口 默认端口为80所以没有显示。

练习
下表给出了与 URL http://store.company.com/dir/page.html 的源进行对比的示例:

URL 结果 原因
http://store.company.com/dir2/other.html 同源 只有路径不同
http://store.company.com/dir/inner/another.html 同源 只有路径不同
https://store.company.com/secure.html 不同源 协议不同
http://store.company.com:81/dir/etc.html 不同源 端口不同 ( http:// 默认端口是80)
http://news.company.com/dir/other.html 不同源 主机不同
1.2 同源安全策略

同源安全策略,是浏览器方提供一种的安全策略。举个例子:我们登陆了一个网址为http://www.A.com的网站,那么默认我们只能请求和http://www.A.com同源的网站。在http://www.A.com网站,发送的所有请求地址只要是和http://www.A.com网站同源的地址,那么可以访问。但是如果此时在http://www.A.comhttp://www.B.com发送一个请求。此时就会触发浏览器同源安全策略。

演示
在一个地址为http://localhost:3000/的网址地址中的页面,向地址为http://localhost:3001/的网址发送一个ajax请求。

        window.onload = function() {

            let xhr = new XMLHttpRequest();

            xhr.open('get', 'http://localhost:3001/get');

            xhr.send();

            xhr.onload = function() {
                console.log(xhr.responseText);
           }

ajax请求触发浏览器同源策略
ajax_跨域

如上,我们用ajax进行了一个跨域请求,触发了浏览器的同源安全策略。因此,不能使用ajax进行请求,聪明的人想出了各种办法。其中jsonp是最早的解决方案之一。

2. 跨域方案解决之JSONP(jsonp json with padding )

我们已经知道了,ajax不支持跨域请求。但是我们页面中是有支持跨域请求的存在的,如script标签的src属性,用来加载JavaScript资源,请求方式是GET。这种方式是支持跨域的,这也是我们在页面上可以引入在线的jQuery等资源的原因。所以,我们可以利用这个script标签的这个特性来进行跨域请求。

2.1 script标签的src属性

我们必须清楚的认识到以下3点

  • script中包含的必须可执行的JavaScript代码
  • 服务端必须返回JavaScript代码
  • 必须要有动态创建的script标签。因为请求是动态的,我们需要根据请求动态生成script标签
2.1 JSONP实现思路
  1. 在客户端定义一个函数。
  2. 在服务端返回第一步定义函数的调用给客户端(服务端无法调用,只是将函数的调用作为字符串返回)。
  3. 客户端接收到该函数调用,然后执行。
    补充: 1. 在上述第第二步中,服务端将真正想返回的数据放在函数的实参中。在客户端调用时,就可以拿到对应的数据
    2. 我们定义的函数必须是全局的,这样服务端返回后,我们才能正常调用。因为服务端返回的函数调用是全局下的
2.1 JSONP请求封装
// 参数说明
// 调用JSONP请求传递的用户需要传递
options = {
	url: 请求地址,
	// 用户自定义参数
	params: {key: value},
	// 用户请求成功回调函数
	succss: function() {}
}
function jsonp(options) {
    // 获得页面body标签
    let body = document.getElementsByTagName('body')[0];
    // 动态创建script标签
    let script = document.createElement('script');
    // 用户请求的地址
    script.src = options.url;
    // 生成一个随机数,我们想将函数的名称发送给服务端,并要求服务端返回同名函数。因为我们在客户端上将定义该函数。因此,要保证函数名不一致,所以使用随机数
    let random = Math.random().toString().replace('.', '');
    // 随机函数名
    let fn = ('jsonp' + random);
    // 必须将fn挂在在全局下,否则服务端返回函数无法执行。
    window[fn] = function() {};
    // 将用户调用的成功的函数体赋值给全局下的函数
    window[fn] = options.success;
    // 真正的请求地址
    script.src += `?callback=${fn}`;
    // 用户传递了自定义参数
    if (options.params) {
        let paramStr = '';
        Object.keys(options.params).forEach((item) => {
            paramStr += ('&' + item + '=' + options.params[item]);
        })
        script.src += paramStr;
    }
    // 动态生成script
    body.appendChild(script);
    // script 成功请求后将script从DOM中删除
    script.onload = function() {
        body.removeChild(script);
    }
}
1.3 JSONP客户端实现代码
	// 请求url中的参数
    let urlParams = req.url.substring(req.url.indexOf('?') + 1);
    // 对请求参数切割
    let paramsArr = urlParams.split('&');
    // 请求参数对象
    let paramsObj = {};
    for (let i = paramsArr.length; i--; i < 0) {
        let temp = paramsArr[i].split('=');
        paramsObj[temp[0]] = temp[1];
    }
    // 返回的数据
    let data = JSON.stringify({ name: "wuxue" });
    // TODO 浏览器只能接受字符串
	// 返回给客户端的内容
    let resFn = `${paramsObj['callback']}(${data})`;
    res.send(resFn);

如上,简单的实现了JSONP请求,解决了跨域的问题。跨域方案的解决,有很多种方式。JSONP只是其中最古老,并且受限比较多的一种。JSONP主要的问题如下

  • 只支持GET请求方式

  • 不够优雅,有些繁琐

  • 非种正统的方式

参考资料

阮一峰-浏览器同源政策及其规避方法
阮一峰-跨域资源共享CORS

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.