运行机制

  1. 初始化以及挂载
    1. init
      1. 生命周期
      2. props
      3. methods
      4. data
      5. computed
      6. watch
      7. Object.defineProperty 依赖收集
  2. compile 编译
    1. parse 正则等方式解析 template 中的指令、class、style 生成 AST
    2. optimize 标记 static 静态节点,update 更新界面时,会有一个 patch 的过程,diff 算法会跳过静态节点,优化 patch 的性能
    3. generate 将 AST 转化成 render function 字符串
  3. 响应式

    1. 在 init 的时候通过 Object.defineProperty 进行绑定
    2. 被设置的对象读取的时候会执行 getter 函数、被赋值的时候会执行 setter 函数
    3. 当 render function 被渲染的时候,因为会读取所需要对象的值,会触发 getter 函数进行依赖收集,依赖收集的目的是将观察者 watcher 对象存放在当前闭包中的订阅者 dep 的 subs 中
    4. 修改对象的时候,会触发对应的 watcher 告诉他们自己的值改变了,需要重新渲染视图。watcher 会调用 update 来更新视图 ```javascript function defineReactive(obj, key, val) { Object.defineProperty(obj, key, { enumerable: true, // 属性可枚举 configurable: true, // 属性可被修改或删除 get: function reactiveGetter() { return val; },

      set: function reactiveSetter(newVal) { if (val === newVal) return; console.log(‘更新啦’) } }) }

function observe(value) { if (!value || typeof value !== ‘object’) { return; }

object.keys(value).forEach((key) => { defineReactive(value, key, value[key]); }) }

class Vue{ // Vue 构造类 constructor(options) { this._data = options.data; observe(this._data) } }

let o = new Vue({ data: { test: ‘i am test’ } }) o._data.test = ‘hello, world’

  1. <a name="lFtpk"></a>
  2. # 响应式的依赖收集追踪原理
  3. 一个全局对象,可能会在多个 vue 对象中用到它进行展示,需要通知多个 vm 进行视图更新。
  4. 依赖收集是如何实现的
  5. 在 observer 的过程中会注册 get 方法,用来进行依赖收集,在它的闭包中会有一个 dep 对象,这个对象用来存放 watcher 对象的事例,其实依赖收集的过程就是把 watcher 事例存放到对应的 dep 对象中, get 方法可以让当前的 watcher 对象(Dep.target)存放到它的 sub 中(addSub),在数据变化时,set 会调用 Dep 对象的 notify 方法通知它内部所有的 watcher 对象进行视图更新。
  6. ```javascript
  7. function defineReactive(obj, key, val) {
  8. // 一个 dep 类对象
  9. const dep = new Dep();
  10. Object.defineProperty(obj, key, {
  11. enumerable: true, // 属性可枚举
  12. configurable: true, // 属性可被修改或删除
  13. get: function reactiveGetter() {
  14. // 将当前 watcher 对象存入 dep 的 subs 中
  15. dep.addSub(dep.target)
  16. return val;
  17. },
  18. set: function reactiveSetter(newVal) {
  19. if (val === newVal) return;
  20. // set 的时候触发 dep 的 notify 来通知所有的 watcher 对象更新视图
  21. dep.notify();
  22. }
  23. })
  24. }
  25. function observe(value) {
  26. if (!value || typeof value !== 'object') {
  27. return;
  28. }
  29. object.keys(value).forEach((key) => {
  30. defineReactive(value, key, value[key]);
  31. })
  32. }
  33. class Vue{
  34. // Vue 构造类
  35. constructor(options) {
  36. this._data = options.data;
  37. // 新建一个观察者对象,这时候 dep.watcher 会指向这个 watcher 对象
  38. new Watcher();
  39. observe(this._data)
  40. }
  41. }
  42. // 订阅者
  43. class Dep{
  44. constructor() {
  45. // 存放 watcher 对象
  46. this.subs = [];
  47. }
  48. // 添加 watcher 对象
  49. addSub(sub) {
  50. this.subs.push(sub);
  51. }
  52. // 通知 watcher 对象更新视图
  53. notify() {
  54. this.subs.forEach(sub => sub.update());
  55. }
  56. }
  57. // 观察者 watcher
  58. class Watcher{
  59. constructor() {
  60. // 在 new 一个 watcher 对象时将该对象赋值给 Dep.target,在 get 中会用到
  61. Dep.target = this;
  62. }
  63. // 更新视图的方法
  64. update() {
  65. console.log('视图更新啦')
  66. }
  67. }
  68. Dep.target = null;
  69. // 依赖收集

Virtual Dom

render function 会被转化成 VNode 节点,Virtual DOM 实际上就是一颗以 JavaScript 对象(VNode 节点)作为基础的数,用对象属性来模拟描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上,由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力。

class VNode{
  constructor(tag, data, children, text, elm) {
    this.tag = tag;              // 当前节点的签名
    this.data = data;            // 当前节点一些数据信息,props、attrs
    this.children = children;    // 当前节点的子节点
    this.text = text;            // 当前节点的文本
    this.elm = elm;              // 当前虚拟节点的真实 dom
  }
}
<template>
  <span class="demo" v-show="isShow">
    this is a span
  </span>
</template>

JS 生成的代码

function render(){
  rerurn new VNode(
    'span',
    {
      directives: [
        rawName: 'v-show',
        expression: 'isShow',
        name: 'show',
        value: true
      ],
      staticClass: 'demo'
    },
    [ new VNode(undefined, undefined, undefined, 'this is a span')]
  )
}

生成后的 VNode

{
  tag: 'span',
  data: {
    directives: [
      rawName: 'v-show',
      expression: 'isShow',
      name: 'show',
      value: true
    ],
    staticClass: 'demo'
  },
  text: undefined,
  children: [
    {
      tag: undefined,
      data: undefined,
      text: 'this is a span',
      children: undefined,
    }
  ]
}

template 怎么 compile 编译的

parse

首先是 parse,用正则等方式将 template 模板中进行字符串解析,得到指令、class、style 等数据,形成 AST。

{
  // 标签属性的 map,记录标签上的属性
  'attrMap': {
    ':class': 'c',
    'class': 'demo',
    'v-if': 'isShow'
  }
  // 解析得到的 :class
  'classBinding': 'c',
  // 标签属性 v-if
  'if': 'isShowqa'

}

parseHTML 会用 while 来循环解析 template ,用正则在匹配到标签头、标签尾以及文本的时候分别进行不同的处理。直到整个 template 被解析完毕。

parseStartTag

parseStartTag 函数,用来解析起始标签(”

“部分的内容)。

首先用 startTagOpen 正则得到标签的头部,可以得到 tagName(标签名称),同时我们需要一个数组 attrs用来存放标签内的属性。

接下来使用 startTagCloseattribute 两个正则分别用来解析标签结束以及标签内的属性。这段代码用 while 循环一直到匹配到 startTagClose 为止,解析内部所有的属性。

此外,我们需要维护一个 stack 栈来保存已经解析好的标签头,这样我们可以根据在解析尾部标签的时候得到所属的层级关系以及父标签。同时我们定义一个 currentParent 变量用来存放当前标签的父标签节点的引用, root 变量用来指向根标签节点。

我们将 startTagMatch 得到的结果首先封装成 element ,这个就是最终形成的 AST 的节点,标签节点的 type 为 1。

const startTagMatch = parseStartTag();
const element = {
    type: 1,
    tag: startTagMatch.tagName,
    attrsList: startTagMatch.attrs,
    attrsMap: makeAttrsMap(startTagMatch.attrs),
    parent: currentParent,
    children: []
}

然后让 root 指向根节点的引用。

if(!root){
    root = element
}

接着我们将当前节点的 element 放入父节点 currentParentchildren 数组中。

if(currentParent){
    currentParent.children.push(element);
}

最后将当前节点 element 压入 stack 栈中,并将 currentParent 指向当前节点,因为接下去下一个解析如果还是头标签或者是文本的话,会成为当前节点的子节点,如果是尾标签的话,那么将会从栈中取出当前节点
异步
parseEndTag 来解析尾标签,它会从 stack 栈中取出最近的跟自己标签名一致的那个元素,将 currentParent 指向那个元素,并将该元素之前的元素都从 stack 中出栈。

最后是解析文本,这个比较简单,只需要将文本取出,然后有两种情况,一种是普通的文本,直接构建一个节点 push 进当前 currentParent 的 children 中即可。还有一种情况是文本是如“{{item}}”这样的 Vue.js 的表达式,这时候我们需要用 parseText 来将表达式转化成代码。

异步更新
vue是如何在更新data后更新视图的
setter → dep → watcher → patch → 视图

首先定义一个 callbacks 数组用来存储 nextTick,在下一个 tick 处理这些回调函数之前,所有的 cb 都会被存在这个 callbacks 数组中。pending 是一个标记位,代表一个等待的状态。

setTimeout 会在 task 中创建一个事件 flushCallbacksflushCallbacks 则会在执行时将 callbacks 中的所有 cb 依次执行。

let callbacks = [];
let pending = false;
function nextTick (cb) {
    callbacks.push(cb);
    if (!pending) {
        pending = true;
        setTimeout(flushCallbacks, 0);
    }
}
function flushCallbacks () {
    pending = false;
    const copies = callbacks.slice(0);
    callbacks.length = 0;
    for (let i = 0; i < copies.length; i++) {
        copies[i]();
    }
}

vuex

Vuex 是一个专门为 Vue.js 框架设计的、专门用来对于 Vue.js 应用进行状态管理的库。它借鉴了 Flux、redux 的基本思想,将状态抽离到全局,形成一个 Store。因为 Vuex 内部采用了 new Vue 来将 Store 内的数据进行「响应式化」,所以 Vuex 是一款利用 Vue 内部机制的库,与 Vue 高度契合,与 Vue 搭配使用显得更加简单高效,但缺点是不能与其他的框架(如 react)配合使用。

我们采用 Vue.mixin 方法将 vuexInit 方法混淆进 beforeCreate 钩子中,并用 Vue 保存 Vue 对象

所以每一个 vm 实例都会调用 vuexInit 方法。.但是我们却在每一个 vm 中都可以访问该 store,这个就需要靠 vuexInit 了。

function vuexInit () {
    const options = this.$options;
    if (options.store) {
        this.$store = options.store;
    } else {
        this.$store = options.parent.$store;
    }
}

因为之前已经用Vue.mixin 方法将 vuexInit 方法混淆进 beforeCreate 钩子中,所以每一个 vm 实例都会调用 vuexInit 方法。

如果是根节点($options中存在 store 说明是根节点),则直接将 options.store 赋值给 this.$store。否则则说明不是根节点,从父节点的 $store 中获取。
通过这步的操作,我们已经可以在任意一个 vm 中通过 this.$store 来访问 Store 的实例啦~

let globalData = {
    d: 'hello world'
};
new Vue({
    data () {
        return {
            ?state: {
                globalData
            }
        }
    }
});
/* modify */
setTimeout(() => {
    globalData.d = 'hi~';
}, 1000);
Vue.prototype.globalData = globalData;

任意模板中

<div>{{globalData.d}}</div>

上述代码在全局有一个 globalData,它被传入一个 Vue 对象的 data 中,之后在任意 Vue 模板中对该变量进行展示,因为此时 globalData 已经在 Vue 的 prototype 上了所以直接通过 this.prototype 访问,也就是在模板中的 {{globalData.d}}。此时,setTimeout 在 1s 之后将 globalData.d 进行修改,我们发现模板中的 globalData.d 发生了变化。其实上述部分就是 Vuex 依赖 Vue 核心实现数据的“响应式化”。
讲完了 Vuex 最核心的通过 Vue 进行数据的「响应式化」,接下来我们再来介绍两个 Store 的 API。
首先是 commit 方法,我们知道 commit 方法是用来触发 mutation 的。

commit (type, payload, _options) {
    const entry = this._mutations[type];
    entry.forEach(function commitIterator (handler) {
        handler(payload);
    });
}

_mutations 中取出对应的 mutation,循环执行其中的每一个 mutation。

dispatch

dispatch 同样道理,用于触发 action,可以包含异步状态。

dispatch (type, payload) {
    const entry = this._actions[type];
    return entry.length > 1
    ? Promise.all(entry.map(handler => handler(payload)))
    : entry[0](payload);
}

同样的,取出 _actions 中的所有对应 action,将其执行,如果有多个则用 Promise.all 进行包装。