# 深入浅出 Vue.js

深入浅出 Vue.js

# Vue.js 简史

# 什么是 Vue.js

  • The Progressive Framewor 渐进式框架

在我看来,渐进式的含义, 框架本身主张最少。

如果有一个现成的服务端应用,也就是非单页应用,可以将 Vue.js 作为该应用的一部分嵌入其中,带来更加丰富的交互体验。甚至可以单纯只使用它的视图层当作模版引擎来使用。Vue.js 本身是一个自底向上增量开发的设计。

# Vue.js 简史

  • 2013/07/28 Evan You 为 Vue.js 的 repo 发起第一个 commit,此时该项目名叫做Element,后来更名为Seed.js
  • 2013/12/07 发布版本 0.6.0,正式更名为 Vue.js,并且把默认指令前缀改为v-,Vue.js 正式问世。
  • 2014/02/01 Evan You 将 Vue.js0.8 发布在国外的 Hacker News 网站,这代表它首次公开发布。
  • 2015/10/26 Vue.js 发布 1.0.0 版本,代号“新世纪福音战士(Evangelion)”。

"The fate of destruction is also the joy of rebirth." 毁灭的命运,也是重生的喜悦。

  • 2016/10/01 Vue.js 发布 2.0.0 版本,代号“攻壳机动队(Ghost in the Shell)”

"Your effort to remain what you are is what limits you." 保持本色的努力,也在限制你的发展。

最早的 Vue.js 只做视图层,没有路由,没有状态管理,更没有官方的构建工具 vue-cli,只有一个库,放在网页里就可以直接用。

最核心的部分是视图层渲染,然后往外是组件机制,在这个基础上再加入路由机制, 状态管理...,最外层是构建工具。

所谓渐进式

所谓分层,就是说你既可以只使用最核心的视图层渲染功能来实现快速开发,也可以使用全家桶来开发大型应用。

Vue.js2.0 版本与 1.0 版本之间内部变化非常大,整个渲染层逻辑重写了,但是 API 层面的变化却很小。2.0 版本引入虚拟 DOM 的概念,其渲染过程更快了。但是值得注意的是,并不是引入虚拟 DOM 后渲染速度就一定会变快,准确的说,应该是 80%场景下变得更快了,剩余的 20%反而变慢了

# 变化侦测

# Object 的变化侦测

Vue.js 最独特的特性之一是看起来并不显眼的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单、直接。不过理解其工作原理同样重要,这样你可以规避一些常见的问题。 ——官方文档

从状态生成 DOM,再输出到用户界面显示的一整套流程叫做渲染,应用在运行时会不断的进行重新渲染。而响应式系统赋予框架重新渲染的能力,其重要组成部分是变化侦测。变化侦测是响应式系统的核心,没有它,也就没有重新渲染。框架在运行时,视图也就无法随着状态的变化而变化。

变化侦测的作用其实就是侦测数据的变化,当数据变化时,会通知视图进行相应的更新。也就是我们常说的“数据驱动”。

# 2.1 什么是变化侦测

ObjectArray的变化侦测是采用不同的处理方式。

通常,在运行时应用内部的状态会不断发生变化,此时需要不停的重新渲染。这时如何确定状态中发送了什么变化?

变化侦测就是来解决这个问题的,它分为两种类型:一种是 push,另一种是 pull。

Angular.js 和 React.js 中的变化侦测都属于 pull,当状态发生变化时,它并不知道具体哪个状态改变了,只知道某个状态有可能变了,然后会发送一个信号告诉框架,框架内部收到信号后,会进行一个相对暴力的 diff 处理。在 Angular.js 中这是脏检查的流程,在 React.js 中使用的虚拟 DOM。

而 Vue.js 的变化侦测属于 push,当状态变化时,Vue.js 立刻知道了,而且在一定程度上知道哪些状态有变化,因此可以做更细粒度的更新。

更新的粒度更细,每个状态所绑定的依赖也就越多,依赖追踪在内存上的开销也就会越大。

因此,Vue.js 从 2.0 开始引入虚拟 DOM,将粒度调整为中等,也就是一个状态绑定的依赖不再是具体的 DOM 节点,而是一个组件。

# 2.2 如何追踪变化

目前在 JS 中有两种方法可以侦测到对象的变化:

  • 使用Object.defineProperty
  • 使用Proxy

由于兼容性问题,在 2.x 中还是使用的Object.defineProperty这个 API,下面是进行一个简单的封装函数

function defineReactive(data, key, val) {
  Object.defineProperty(data, key, {
    enumeratble: true,
    configurable: true,
    get: function() {
      return val;
    },
    set: function(newVal) {
      if (val === newVal) {
        return;
      }
      val = newVal;
    },
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 2.3 如何收集依赖

进行简单的封装,其实并没有实际作用,真正有用的是收集依赖。

WARNING

在 Vue.js 中,模版使用数据等同于组件使用数据,所以当数据发生变化时,会将通知发送到组件,然后组件内部在通过虚拟 DOM 重新渲染。

getter中收集依赖,在setter中触发依赖。

# 2.4 依赖在哪里

创建一个Dep类,专门用做管理依赖。使用这个类,我们可以进行收集依赖,删除依赖或向依赖发送通知。

function remove(arr, item) {
  if (arr.length) {
    const index = arr.indexOf(item);
    if (index > -1) {
      return arr.splice(index, 1);
    }
  }
}

export default class Dep {
  constructor() {
    this.subs = [];
  }
  addSub() {
    this.subs.push(sub);
  }
  removeSub(sub) {
    remove(this.subs, sub);
  }
  depend() {
    // 假设依赖是一个函数,并且挂在window上
    if (window.target) {
      this.addSub(window.target);
    }
  }
  notify() {
    const subs = this.subs.slice();
    for (let i = 0, length = subs.length; i < 1; i++) {
      subs[i].update();
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

之后再改造一下defineReactive:

function defineReactive(data, key, val) {
  const dep = new Dep();
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      dep.deppend();
    },
    set: function(newVal) {
      if (val === newVal) {
        return;
      }
      val = newVal;
      dep.notify();
    },
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 2.5 依赖是谁

在之前的代码中,我们收集的依赖是window.target

我们要通知用到此数据的地方,有可能会很多,而且类型还不一样,模版、computed、watch 等都有可能。这时需要抽象出一个能集中处理这些情况的类Watcher。然后,我们在收集依赖的阶段只收集这个封装好的实例进来,通知也只通知它一个,接着,它再负责通知到其它地方。

# 2.6 什么是 Wathcer

Watcher是一个中介的角色,数据发生变化时通知它,然后它再通知其它地方。

举个简单的用法例子:

vm.$watch("a.b.c", (newVal, oldVal) => {
  // do sth...
});
1
2
3

此段代码表示当data.a.b.c的值发生变化时,触发二参回调。 实现这个功能,也就是将 Watcher 实例添加到data.a.b.c属性的Dep中,然后当该属性的值发生变化时,通知Watcher,接着Watcher再执行回调。 根据上面逻辑写出如下代码:

export default class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm;
    // 执行this.getter(),就可以读取data.a.b.c的值
    this.getter = parsePath(expOrFn);
    this.cb = cb;
    this.value = this.get();
  }
  get() {
    window.target = this;
    const value = this.getter.call(this.vm, this.vm);
    window.target = undefined;
    return value;
  }
  update() {
    const oldValue = this.value;
    // 赋值,手动触发一次
    this.value = this.get();
    this.cb.call(this.vm, this.value, oldValue);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 2.7 递归侦测所有 key

# 2.8 关于 Object 的问题

# 2.9 总结

# Array 的变化侦测

# 3.1 如何追踪变化

# 3.2 拦截器

# 3.3 使用拦截器覆盖 Array 原型

# 3.4 将拦截器方法挂载到数组的属性上

# 3.5 如何收集依赖

# 3.6 依赖列表在哪儿

# 3.7 收集依赖

# 3.8 在拦截器中获取 Observer 实例

# 3.9 向数组的依赖发送通知

# 3.10 侦测数组中的元素的变化

# 3.11 侦测新增元素的变化

# 3.11.1 获取新增元素
# 3.11.2 使用 Observer 侦测新增元素

# 3.12 关于 Array 的问题

# 3.13 总结

# 变化侦测相关 API 实现原理

# 4.1vm.$watch

# 4.1.1 用法
# 4.1.2watch 的内部原理
# 4.1.3deep 参数的实现原理

# 4.2vm.$set

# 4.2.1 用法
# 4.2.2Array 的处理
# 4.2.3key 已经存在于 target 中
# 4.2.4 处理新增的属性

# 4.3vm.$delete

# 4.3.1 用法
# 4.3.2 实现原理

# 4.4 总结

# 虚拟 DOM

# 虚拟 DOM 简介

# 5.1 什么是虚拟 DOM

# 5.2 为什么要引入虚拟 DOM

# 5.3Vue.js 中的虚拟 DOM

# 5.4 总结

# VNode

# 6.1 什么是 VNode

# 6.2VNode 的作用

# 6.3VNode 的类型

# 6.3.1 注释节点
# 6.3.2 文本节点
# 6.3.3 克隆节点
# 6.3.4 元素节点
# 6.3.5 组件节点
# 6.3.6 函数式组件

# 6.4 总结

# patch

# 7.1patch 介绍

# 7.1.1 新增节点
# 7.1.2 删除节点
# 7.1.3 更新节点
# 7.1.4 小结

# 7.2 创建节点

# 7.3 删除节点

# 7.4 更新节点

# 7. 4.1 静态节点
# 7. 4.2 新虚拟节点有文本属性
# 7. 4.3 新虚拟节点无文本属性
# 7. 4.4 小结

# 7.5 更新子节点

# 7.5.1 更新策略
# 7.5.2 优化策略
# 7.5.3 哪些节点是未处理过的
# 7.5.4 小结

# 7.6 总结

# 模版编译原理

# 模版编译

# 8.1 概念

# 8.2 将模版编译成渲染函数

# 8.2.1 解析器
# 8.2.2 优化器
# 8.2.3 代码生成器

# 8.3 总结

# 解析器

# 9.1 解析器的作用

# 9.2 解析器内部运行的原理

# 9.3HTML 解析器

# 9.3.1 运行原理
# 9.3.2 截取开始标签
# 9.3.3 截取结束标签
# 9.3.4 截取注释
# 9.3.5 截取条件注释
# 9.3.6 截取 DOCTYPE
# 9.3.7 截取文本
# 9.3.8 纯文本内容元素的处理
# 9.3.9 使用栈维护 DOM 层级
# 9.3.10 整体逻辑

# 9.4 文本解析器

# 9.5 总结

# 优化器

# 10.1 找出所有静态节点并标记

# 10.2 找出所有静态根节点并标记

# 10.3 总结

# 代码生成器

# 11.1 通过 AST 生成代码字符串

# 11.2 代码生成器的原理

# 11.2.1 元素节点
# 11.2.2 文本节点
# 11.2.3 代码生成器的原理

# 11.3 总结

# 整体流程

# 架构设计与项目结构

# 12.1 目录结构

# 12.2 架构设计

# 12.3 总结

# 实例方法与全局 API 的实现原理

# 13.1 数据相关的实例方法

# 13.2 事件相关的实例方法

# 13.2.1 vm.$on
# 13.2.2 vm.$off
# 13.2.3 vm.$once
# 13.2.4 vm.$emit

# 13.3 生命周期相关的实例方法

# 13.3.1 vm.$forceUpdate
# 13.3.2 vm.$destroy
# 13.3.3 vm.$nextTick
# 13.3.4 vm.$mount

# 13.4 全局 API 的实现原理

# 13.4.1 Vue.extend
# 13.4.2 Vue.nextTick
# 13.4.3 Vue.set
# 13.4.4 Vue.delete
# 13.4.5 Vue.directive
# 13.4.6 Vue.filter
# 13.4.7 Vue.component
# 13.4.8 Vue.use
# 13.4.9 Vue.mixin
# 13.4.10 Vue.compile
# 13.4.10 Vue.version

# 13.5 总结

# 生命周期

# 14.1 生命周期图示

# 14.1.1 初始化阶段
# 14.1.2 模版编译阶段
# 14.1.3 挂载阶段
# 14.1.4 卸载阶段
# 14.1.5 小结

# 14.2 从源码角度了解生命周期

# 14.3 errorCaptured 与错误处理

# 14.4 初始化实例属性

# 14.5 初始化事件

# 14.6 初始化 inject

# 14.6.1 provide/inject 的使用方式
# 14.6.2 inject 的内部原理

# 14.7 初始化状态

# 14.7.1 初始化 props
# 14.7.2 初始化 methods
# 14.7.3 初始化 data
# 14.7.4 初始化 computed
# 14.7.5 初始化 watch

# 14.8 初始化 provide

# 14.9 总结

# 指令的奥秘

# 15.1 指令原理概述

# 15.1.1 v-if 指令的原理概述
# 15.1.2 v-for 指令的原理概述
# 15.1.3 v-on 指令

# 15.1 自定义指令的内部原理

# 15.1 虚拟 DOM 钩子函数

# 15.1 总结

# 过滤器的奥秘

# 16.1 过滤器原理概述

# 16.1.1 串联过滤器
# 16.1.2 滤器接收参数
# 16.1.3 resolveFilter 内部原理

# 16.2 解析过滤器

# 16.3 总结

# 最佳实践

# 17.1 为列表渲染设置属性 key

# 17.2 在 v-if/v-if-else/v-else 中使用 key

# 17.3 路由切换组件不变

# 17.3.1 路由导航守卫 beforeRouteUpdate
# 17.3.2 观察$route 对象的变化
# 17.3.3 为 router-view 组件添加属性 key

# 17.4 为所有路由统一添加 query

# 17.4.1 使用全局守卫 beforeEach
# 17.4.2 使用函数劫持

# 17.5 区分 Vuex 与 props 的使用边界

# 17.6 避免 v-if 和 v-for 一起使用

# 17.7 为组件样式设置作用域

# 17.8 避免在 scoped 中使用元素选择器

# 17.9 避免隐性的父子组件通信

# 17.10 单文件组件如何命名

# 17.10.1 单文件组件的文件名的大小写
# 17.10.2 基础组件名
# 17.10.3 单例组件名
# 17.10.4 紧密耦合的组件名
# 17.10.5 组件名中的单词顺序
# 17.10.6 完整单词的组件名
# 17.10.7 组件名为多个单词
# 17.10.8 模版中的组件名大小写
# 17.10.9 JS/JSX 中的组件名大小写

# 17.11 自闭合组件

# 17.12 props 名的大小写

# 17.13 多个特性的元素

# 17.14 模版中简单的表达式

# 17.15 简单的计算属性

# 17.16 指令缩写

# 17.17 良好的代码顺序

# 17.17.1 组件/实例的选项的顺序
# 17.17.2 元素特性的顺序
# 17.17.3 单文件组件顶级元素的顺序

# 17.18 总结

最近更新时间: 5/17/2020, 10:59:30 PM