Vue内部运行机制 读书笔记

内部流程图

初始化及挂载

new Vue()之后。Vue调用 _init()函数进行初始化, 它会初始化 initLifecycle 初始化生命周期钩子 、 initEvents 初始化事件方法 、 initState 初始化状态 、 initRender 初始化渲染 。 其中 initEvents 初始化事件方法之后 会调用 回调生命周期钩子 beforeCreateinitState 之后还会调用 回调生命周期钩子 created

其中最重要的是通过 Object.defineProperty 设置 settergetter 函数, 用来实现 [响应式] 和 [依赖手机] 。

初始化之后调用 $mount 会挂载组件 。 如果是运行时编译,即不存在 render function 但是存在 template 的情况,需要进行「编译」步骤。

编译

compile编译可以分成 parse 解析 、 optimize 优化 与 generate 生成 三个阶段 , 最终需要的到 render function。

parse 解析

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

optimize 优化

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

generate 生成

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

在经历过 parseoptimizegenerate 这三个阶段以后 , 组件中就会存在渲染 VNode 所需的 render function

响应式

这里的 gettersetterinit 的时候通过 Object.defineProperty 进行半丁, 使得当前被设置的对象被读取的时候会执行 getter 函数, 而在当被赋值的时候就会执行 setter 函数。

当 render function 被渲染的时候,因为会读取所需对象的值, 所以会触发 getter 函数进行 [依赖收集] 。

[依赖收集] 目的 将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中。 形成如下所示的这样一个关系。

在修改对象的值的时候,会触发对应的 settersetter 通知之前 [依赖收集] 得到的 Dep 中的每一个 Watcher ,告诉它们 自己的值改变了, 需要重新渲染视图。这时候 Watcher就会开始调用 updata 去更新视图, 其中间还有一个 patch 的过程以及使用队列来异步更新的策略。

Virtual DOM

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

一个例子:

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

渲染后可以得到

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

更新视图

当数据变化后,执行 render function 就可以得到一个新的 VNode 节点,我们如果想要得到新的视图,最简单粗暴的方法就是直接解析这个新的 VNode 节点 , 然后用 innerHTML 直接全部渲染到真实 DOM 中。但是其实我们只对其中的一小块内容进行了修改,这样做似乎有些「浪费」。

那么我们为什么不能只修改那些「改变了的地方」呢?这个时候就要介绍我们的「patch」了。我们会将新的 VNode 与旧的 VNode 一起传入 patch 进行比较,经过 diff 算法得出它们的「差异」。最后我们只需要将这些「差异」的对应 DOM 进行修改即可。

响应式系统

Object.defineProperty

使用方法:

/*
* obj:目标对象
* prop:需要操作的目标对象的属性名
* descriptor: 描述符
* 
*  return value 传入对象
* */

Object.defineProperty(obj, prop, descriptor)

descriptor的一些属性

  • enumerable,属性是否可枚举,默认 false。
  • configurable,属性是否可以被修改或者删除,默认 false。
  • value, 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
  • writable, 当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false。
  • get,获取属性的方法。
  • set,设置属性的方法。

实现 observer(可观察的)

init 的阶段会进行初始化,对数据进行「响应式化」。

首先定义一个 cb 函数,这个函数用来模拟视图更新,调用它即代表更新视图,内部可以是一些更新视图的方法。

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

然后定义一个 defineReactive , 这个方法通过 Object.defineProperty来实现对对象的 [响应式化] , 入参是一个obj (需要绑定的对象) 、 key (obj的某一个属性) 、 val(具体的值) 。经过 defineReactive 处理以后,我们的 Obj的key属性在 [读] 的时候会触发 reactiveGetter 方法, 而在该属性被 [写] 的时候则会触发 reactiveSetter 方法。

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

      /* 通知视图要更新 */
      cb(newValue)
    }
  })
}

当然这是不够的,我们需要在上面再封装一层 observer。这个函数传入一个 value (需要「响应式」化的对象) ,通过遍历所有属性的方式对该对象的每一个属性都进行 defineReactive 处理。

function observer (value) {
  if (!value || (typeof value !== 'object') ) {
    return;
  }

  Object.keys(value).forEach((key) => {
    defineRective(value, key, value[key])
  })
}

最后 用 observer 来封装一个 Vue

在 Vue 的构造函数中,对 optionsdata 进行处理,这里的 data 想必大家很熟悉,就是平时我们在写 Vue 项目时组件中的 data 属性(实际上是一个函数,这里当作一个对象来简单处理)

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

这样我们只要 new 一个 Vue 对象,就会将 data 中的数据进行「响应式」化。如果我们对 data 的属性进行下面的操作,就会触发 cb 方法更新视图。

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

依赖收集

假设我们现在有一个全局的对象,我们可能会在多个 Vue 对象中用到它进行展示。

let globalObj = {
    text1: 'text1'
};

let o1 = new Vue({
    template:
        `<div>
            <span></span> 
        <div>`,
    data: globalObj
});

let o2 = new Vue({
    template:
        `<div>
            <span></span> 
        <div>`,
    data: globalObj
});

这个时候,我们执行了如下操作。

globalObj.text1 = 'hello,text1';

我们应该需要通知 o1 以及 o2 两个vm实例进行视图的更新,「依赖收集」会让 text1 这个数据知道“哦~有两个地方依赖我的数据,我变化的时候需要通知它们~”。

最终会形成数据与视图的一种对应关系,如下图。

「依赖收集」实现的

订阅者 Dep

首先我们来实现一个订阅者 Dep ,它的主要作用是用来存放 Watcher 观察者对象。

class Dep {
  constructor () {
    /* 用来存放Watcher对象的数组 */
    this.subs = []
  }

  /* 在subs中添加一个Watcher对象 */
  /* 用 addSub 方法可以再目前的 Dep 对象中增加一个 watcher 的订阅操作 */
  addSub (sub) {
    this.subs.push(sub)
  }

  /* 通知所有Watcher对象更新视图 */
  /* 用 notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作。 */
  notify () {
    this.subs.forEach((sub) => {
      sub.update()
    })
  }
}

主要是两件事情:

  • 用 addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作;
  • 用 notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作。

观察者 Watcher

class Watcher {
    constructor () {
        /* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
        Dep.target = this;
    }

    /* 更新视图的方法 */
    update () {
        console.log("视图更新啦~");
    }
}

Dep.target = null;

依赖收集

在闭包中增加了一个 Dep 类的对象,用来收集 Watch 对象。 在对象被「读」的时候, 会触发 reactiveGetter 函数把当前的 Wathcer 对象 (存放在 Dep.target 中)收集到 Dep 类中去。之后如果当前该对象被 「写」的时候,贼会触发 reactiveSetter 方法,通知 Dep 类调用 notify 来触发所有 Watcher 对象的 updata方法更新对应的视图。

function defineReactive (obj, key, val) {
    /* 一个Dep类对象 */
    const dep = new Dep();
    
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
            /* 将Dep.target(即当前的Watcher对象存入dep的subs中) */
            dep.addSub(Dep.target);
            return val;         
        },
        set: function reactiveSetter (newVal) {
            if (newVal === val) return;
            /* 在set的时候触发dep的notify来通知所有的Watcher对象更新视图 */
            dep.notify();
        }
    });
}

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

依赖收集小结

首先在 observer 的过程中会注册 get 方法, 该方法用来进行 [依赖收集] 。 在 observer 的闭包中会有一个 Dep 对象,这个对象用来存放 Watcher对象的实例。其实 [依赖收集]的过程就是把 Watch 实例存放到对应的 Dep 订阅者对象中去。get 方法可以上当前的 Watch 对象(Dep.target)存放到它的 subs 中(addSub)方法,在数据变化时, set会调用 Dep订阅者对象的 notify方法通知它内部所有的 Watch 对象进行视图更新。

这是 Object.definePropertyset/get 方法处理的事情。

那么「依赖收集」的前提条件还有两个:

  • 触发 get方法
  • 新建一个 Watcher 对象

在 Vue 的构造类中处理,新建一个 Watcher 对象只需要 new 出来,这时候 Dep.target已经指向了一个 new 出来的 Watcher对象来。而触发 get 方法实际上只要吧 render function 进行渲染,那么其中的依赖的对象都会被读取。

实现 Virtual DOM 下的一个 VNode 节点

什么是VNode

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

实现一个VNode

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

比如我目前有这么一个 Vue 组件。

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

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

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

转换成 VNode 以后的情况。

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

将 VNode 进一步封装一下,可以实现一些产生常用 VNode 的方法

  • 创建一个空节点
function createEmptyVNode () {
    const node = new VNode();
    node.text = '';
    return node;
}
  • 创建一个文本节点
function createTextVNode (val) {
  return new VNode(undefined, undefined, undefined, String(val));
}
  • 克隆一个 VNode 节点
function cloneVNode (node) {
    const cloneVnode = new VNode(
        node.tag,
        node.data,
        node.children,
        node.text,
        node.elm
    );
    return cloneVnode;
}

VNode就是一个 JavaScript对象,用JavaScript 对象的属性来描述当前节点的一些状态,用VNode 节点的形式来模拟一棵 Virtual DOM 树。

template 模板是怎样通过 Compile 编译的

Compile

Compile 编译可以分成 parse 解析 、optimize 优化 与 generate生成 三个阶段。最终得到 render function。 这部分内容不算 Vue.js 的响应式核心,只是用来编译的。

大致流程

由于解析过程比较复杂,通过这个 template 的示例的变化来看解析的过程。

<div :class="c" class="demo" v-if="isShow">
    <span v-for="item in sz"></span>
</div>
var html = '<div :class="c" class="demo" v-if="isShow"><span v-for="item in sz"></span></div>';

parse 解析

首先 parseparse 会用正则等方式将 template 模板中进行字符串解析,得到 指令 、 class 、 style 等数据, 形成AST(在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。)

这个过程比较复杂,会涉及到比较多的正则进行字符串解析,得到的 AST 的样子。

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

最终得到的 AST 通过一些特定的属性,能够比较清晰地描述出标签的属性以及依赖关系。

使用正则来把 template 编译成我们需要的 AST 树。

正则

首先我们定义一下接下来我们会用到的正则。

/*
* 获取标签的tag名称
* [a-zA-Z_] -> 与任何单词匹配包括下划线
* 当使用构造函数创造正则对象时,需要常规的字符转义规则(在前面加反斜杠 \)
* \\w = \w  查找单词字符
* \\- = \\- 查找横杠
* \\. = \. 查找单个字符,除了换行和行结束符。
* * 匹配包含 0 个 或 多个 n 的字符
* [] 查找给定集合内的任何字符。
 * */
const ncname = '[a-zA-Z_][\\w\\-\\.]*';        


/*
* 单个属性标识符
* () 查找任何指定的选项。
* [^] 查找给定集合外的任何字符。
 * ^\s"'<>/= 不匹配开头为 空白字符 "'<>/= 
 * + 匹配任何包含至少一个 n 的字符串。
 * ([^\s"'<>/=]+) 查找任何指定选项给定集合外的至少一个的字符
* */
const singleAttrIdentifier = /([^\s"'<>/=]+)/     

/*
* 单个属性分配
* ? 匹配前一个字符 0次 或一次
* () 查找任何指定的选项
* (?:=) 查找 前一个字符有: 和 = 的字符
* */
const singleAttrAssign = /(?:=)/      


/*
* 单个属性的值
* source
* 属性返回一个值为当前正则表达式对象的模式文本的字符串,
* 该字符串不会包含正则字面量两边的斜杠以及任何的标志字符。
*
* 匹配 包含至少一个 " " 中  给定集合 " 外的任何字符
* /"([^"]*)"+/.source =  "([^"]*)"+
*
* 匹配 包含至少一个 ' ' 中  给定集合 ' 外的任何字符
* /'([^']*)'+/.source = '([^']*)'+
*
* 查找任何指定选项给定集合外的至少一个的字符适配了 ES6的字符串模板 `
* /([^\s"'=<>`]+)/.source = ([^\s"'=<>`]+)
* */
const singleAttrValues = [
  /"([^"]*)"+/.source,
  /'([^']*)'+/.source,
  /([^\s"'=<>`]+)/.source
]       


/*
* 获取属性attribute
*
* ^\s* 开头包含零个或多个 空白字符
*
* 不查找一下字符([^\s"'<>/=]+)
*
* (?:\s*((?:=)) 查找前一个字符有: 后包  含零个或多个空白和 ( 括号 前一个字符有: 和 = 的字符
*
* \\s*(?: 匹配 包含零个或多个空白字符 前一个字符有:
* 包含至少一个 " " 中  给定集合 " 外的任何字符   "([^"]*)"+
* 包含至少一个 ' ' 中  给定集合 ' 外的任何字符  '([^']*)'+
* 查找任何指定选项给定集合外的至少一个的字符适配了 ES6的字符串模板 ` 
* \s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/)
* */
const attribute = new RegExp(
  '^\\s*' + singleAttrIdentifier.source +
  '(?:\\s*(' + singleAttrAssign.source + ')' +
  '\\s*(?:' + singleAttrValues.join('|') + '))?'
)      


/*
* 名称捕获
*
* (?:[a-zA-Z_][\w\-\.]*\:)  查找前一个字符有: 后 字母_ 的字符包含零个或多个 : 结尾
* ?[a-zA-Z_][\w\-\.] 查找前一个字符有 字母、单词字符、- 、. 的字符
* 
* ((?:[a-zA-Z_][\w\-\.]*\:)?[a-zA-Z_][\w\-\.]*) 匹配包含零个或多个 n
* */
const qnameCapture = '((?:' + ncname + '\\:)?' + ncname + ')'         


/*
* 开始标签
* 开头为 < 	
* 查找任何指定为 前一个字符有: 后 字母_ 的字符包含零个或多个 : 结尾 
* 前一个字符有 字母、单词字符、- 、. 的字符
* 
* /^<((?:[a-zA-Z_][\w\-\.]*\:)?[a-zA-Z_][\w\-\.]*)/
* */
const startTagOpen = new RegExp('^<' + qnameCapture)       


/*
* 获取结束标签
* 开头为 < \
* 查找前一个字符有: 后 字母_ 的字符包含零个或多个 : 结尾
* 查找前一个字符有 字母、单词字符、- 、. 的字符
* [^>]* 查找给定集合外 包含零个或多个 n 
* 结尾  >
* /^<\/((?:[a-zA-Z_][\w\-\.]*\:)?[a-zA-Z_][\w\-\.]*)[^>]*>/
* */
const endTag = new RegExp('^<\\/' + qnameCapture + '[^>]*>')        


/*
* 默认标签返回数据
* 全局匹配 { {  (开头为: 后的单个字符 或者 换行符) } }
* */
const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g     


/*
 * for属性返回数据
 * (.*?) 包含零个或多个和包含零个或一个的 单个字符
 * \s+  包含至少一个空白字符
 * (?:in|of) 匹配前一个为: 后面跟着 in 或者 of
 * \s+ 包含至少一个空白字符
 * (.*) 含零个或多个 单个字符
 * */
const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/       

advance 裁剪

因为我们解析 template 采用循环进行字符串匹配的方式,所以每匹配解析完一段我们需要将已经匹配掉的去掉,头部的指针指向接下来需要匹配的部分。

/*
* 解析 template 每匹配玩一段就需要将已经匹配掉的字符串去掉,
* 头部指针指向接下来需要匹配的部分
* */
let index = 0;
function advance  (n) {
  index += n;
  html = html.substring(n)  
} 

当我们把第一个 div 的头标签全部匹配完毕以后,我们需要将这部分除去,也就是向右移动 43 个字符。

调用 advance 函数

advance(43);

得到结果

parseHTML 解析 template 字符串

需要定义一个 parseHTML 函数,在里面我们循环解析 template 字符串。

/*
* 循环解析 template 字符串
* */
function parseHTML  () {
  while (html) {
    // 解析开始位置
    let textEnd = html.indexOf('<');

    if (textEnd === 0) {
      // 处理结束标记
      if (html.match(endTag)) {

        continue;
      }
      // 处理开始标记
      if (html.match(startTagOpen)) {

      }
    }
    // 处理文本
    else {
      continue;
    }
  }
   return root;
}

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

parseStartTag

写一个 parseStartTag 函数,用来解析起始标签(”<div :class="c" class="demo" v-if="isShow">“部分的内容)。

/*
* 来解析起始标签
* */
function parseStartTag () {
  // 获取解析标签起始位置 得到标签的头部
  const start = html.match(startTagOpen)

  // 得到标签头部后
  if (start) {
    const match = {
      // 可以得到 tagName(标签名称)
      tagName: start[1],
      // attrs 数组用来存放标签内的属性
      attrs: [],
      start: index
    }
    // 设置指针指向接下来需要匹配的部分
    advance(start[0].length);

    let end; // 标签结束位置
    let attr; // 标签属性

    // 使用 startTagClose 和 attribute 两个正则分别用来解析标签结束以及标签内的属性。
    // 一直循环到匹配到 startTagClose 为止 解析内部所有的属性
    while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
      // 设置指针指向接下来需要匹配的部分
      advance(start[0].length);

      // 添加属性
      match.attrs.push({
        name: attr[1],
        value: attr[3]
      })
    }

    // 设置解析结束位置
    if (end) {
      match.unarySlash = end[1]
      // 设置指针指向接下来需要匹配的部分
      advance(end[0].length);

      // 设置解析结束位置
      match.end = index;

      return match
    }
  }
}

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

const start = html.match(startTagOpen);
const match = {
    tagName: start[1],
    attrs: [],
    start: index
}
advance(start[0].length);

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

let end, attr
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
    advance(attr[0].length)
    match.attrs.push({
        name: attr[1],
        value: attr[3]
    });
}
if (end) {
    match.unarySlash = end[1];
    advance(end[0].length);
    match.end = index;
    return match
} 

stack 栈

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

const stack = [];
let currentParent, root;

知道这个以后,优化一下 parseHTML ,在 startTagOpenif 逻辑中加上新的处理。

// 处理开始标记
      if (html.match(startTagOpen)) {
        // 开始标签匹配
        const startTagMatch = parseStartTag()

        // 元素属性
        const element = {
          // 标签节点的 type 为 1
          type: 1,
          // 标签的tag
          tag: startTagMatch.tagName,
          // 小写的标签名
          lowerCasedTag: startTagMatch.tagName.toLowerCase(),
          // 属性列表
          attrsList: startTagMatch.attrs,
          // 属性的集合
          attrsMap: makeAttrsMap(startTagMatch.attrs),
          // 父级节点
          parent: currentParent,
          // 子节点属性
          children: []
        }

        // 然后让 root 指向根节点的引用。
        if (!root) {
          root = element
        }

        // 将当前节点的 element 放入父节点 currentParent 的 children 数组中
        if (currentParent) {
          currentParent.children.push(element)
        }

        // 将当前节点 element 压入 stack 栈中, 并将 currentParent 指向当前节点
        stack.push(element)
        currentParent = element
        
        continue;
      }

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

// 开始标签匹配
        const startTagMatch = parseStartTag()

        // 元素属性
        const element = {
          // 标签节点的 type 为 1
          type: 1,
          // 标签的tag
          tag: startTagMatch.tagName,
          // 小写的标签名
          lowerCasedTag: startTagMatch.tagName.toLowerCase(),
          // 属性列表
          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 指向当前节点,因为接下去下一个解析如果还是头标签或者是文本的话,会成为当前节点的子节点,如果是尾标签的话,那么将会从栈中取出当前节点

stack.push(element);
currentParent = element;
continue;

其中的 makeAttrsMap 是将 attrs 转换成 map 格式的一个方法。

function makeAttrsMap (attrs) {
    const map = {}
    for (let i = 0, l = attrs.length; i < l; i++) {
        map[attrs[i].name] = attrs[i].value;
    }
    return map
}

parseEndTag 解析结束标记

同样,我们在 parseHTML 中加入对尾标签的解析函数,为了匹配如“</div>”。

const endTagMatch = html.match(endTag)
 if (endTagMatch) {
    advance(endTagMatch[0].length);
    parseEndTag(endTagMatch[1]);
    continue;
}

parseEndTag 来解析尾标签,它会从 stack 栈中取出最近的跟自己标签名一致的那个元素,将 currentParent 指向那个元素,并将该元素之前的元素都从 stack 中出栈

/*
 * 解析尾标签
 * */
function parseEndTag (tagName) {
  let pos;
  for (pos = stack.length - 1; pos >= 0; pos--) {
    if (stack[pos].lowerCasedTag === tagName.toLowerCase()) {
      break;
    }
  }
  
  if (pos >= 0) {
    stack.length = pos
    currentParent = stack[pos]
  }
}

parseText 解析文本

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

 // 获取要处理的文本
      let text = html.substring(0, textEnd);
      // 设置指针指向接下来需要匹配的部分
      advance(textEnd)

      // 匹配表达式 
      let expression;
      if(expression = parseText(text)) {
        currentParent.children.push({
          type:2,
          text,
          expression
        })
      }
      // 匹配普通文本
      else {
        currentParent.children.push({
          type: 3,
          text,
        });
      }

会用到一个 parseText 函数。

/*
 * 解析文本
 * */
function parseText (text) {
  if (!defaultTagRE.test(text)) {
    return;
  }

  // 使用一个 tokens 数组来存放解析结果
  const tokens = [];

  let lastIndex = defaultTagRE.lastIndex = 0

  let match; // 匹配结果
  let index;  // 下标

  // defaultTagRE 来循环匹配该文本
  while ((match = defaultTagRE.exec(text))) {
    index = match.index;

    // 如果是普通文本直接 push 到 tokens 数组中去
    if (index > lastIndex) {
      tokens.push(JSON.stringify(text.slice(lastIndex, index)))
    }

    // 两端删除空白字符 表达式(),则转化成“_s(${exp})”的形式。
    const exp = match[1].trim();
    tokens.push(`_s(${exp})`)
    lastIndex = index + match[0].length
  }

  if (lastIndex < text.length) {
    tokens.push(JSON.stringify(text.slice(lastIndex)))
  }

  return tokens.join('+');
}

我们使用一个 tokens 数组来存放解析结果,通过 defaultTagRE 来循环匹配该文本,如果是普通文本直接 push 到 tokens 数组中去,如果是表达式(),则转化成“_s(${exp})”的形式。

举个例子,如果有这样一个文本。

<div>hello,.</div>

最终得到 tokens

tokens = ['hello,', _s(name), '.'];

最终通过 join 返回表达式。

'hello' + _s(name) + '.';

processIf与processFor 处理if和for

只需要在解析头标签的内容中加入这两个表达式的解析函数即可,在这时“v-for”之类指令已经在属性解析时存入了 attrsMap 中了。

 // 处理开始标记
      if (html.match(startTagOpen)) {
        // 开始标签匹配
        const startTagMatch = parseStartTag()

        // 元素属性
        const element = {
          // 标签节点的 type 为 1
          type: 1,
          // 标签的tag
          tag: startTagMatch.tagName,
          // 小写的标签名
          lowerCasedTag: startTagMatch.tagName.toLowerCase(),
          // 属性列表
          attrsList: startTagMatch.attrs,
          // 属性的集合
          attrsMap: makeAttrsMap(startTagMatch.attrs),
          // 父级节点
          parent: currentParent,
          // 子节点属性
          children: []
        }

        // 解析 if指令
        processIf(element);
        // 解析for 指令
        processFor(element);

        // 然后让 root 指向根节点的引用。
        if (!root) {
          root = element
        }

        // 将当前节点的 element 放入父节点 currentParent 的 children 数组中
        if (currentParent) {
          currentParent.children.push(element)
        }

        // 将当前节点 element 压入 stack 栈中, 并将 currentParent 指向当前节点
        stack.push(element)
        currentParent = element

        continue;
      }

首先我们需要定义一个 getAndRemoveAttr 函数,用来从 elattrsMap 属性或是 attrsList 属性中取出 name 对应值。

/*
 * 用来从 el 的 attrsMap 属性或是 attrsList 属性中取出 name 对应值。
 * */
function getAndRemoveAttr (el, name) {
  let val

  // 从 el 的 attrsMap 属性或是 attrsList 属性中取出 name 对应值
  if ((val = el.attrsMap[name]) != null) {
    const list = el.attrsList

    for (let i = 0, l = list.length; i < l; i++) {
      if (list[i].name === name) {
        list.splice(i, 1)
        break
      }
    }
  }

  return val
}

比如说解析示例的 div 标签属性。

getAndRemoveAttr(el, 'v-for');

可得到“item in sz”

有了这个函数这样就可以开始实现 processForprocessIf

“v-for”会将指令解析成 for 属性以及 alias 属性,而“v-if”会将条件都存入 ifConditions 数组中。

/*
* 解析指令 v-for
*
* 解析成 for 属性以及 alias 属性
* */
function processFor  (el) {
  let exp
  if ((exp = getAndRemoveAttr(el, 'v-for'))) {
    const inMatch = exp.match(forAliasRE)
    // 解析成 for 属性
    el.for = inMatch[2].trim()
    // 解析成 alias 属性
    el.alias = inMatch[1].trim()
  }
}

/*
* 解析指令 if
* 将条件都存入 ifConditions 数组
* */
function processIf  (el) {
  const exp = getAndRemoveAttr(el, 'v-if');
  if (exp) {
    el.if = exp
    // 初始化 ifConditions 数组
    if (!el.ifConditions) {
      el.ifConditions = []
    }
    // 把条件存入 ifConditions 数组
    el.ifConditions.push({
      exp: exp,
      block: el
    })
  }
}

optimize 优化

这个涉及到 patch 的过程,因为 patch 的过程实际上是将 VNode 节点进行一层一层的比对,然后将「差异」更新到视图上。那么一些静态节点是不会根据数据变化而产生变化的,这些节点我们没有比对的需求,是不是可以跳过这些静态节点的比对,从而节省一些性能呢?

那么我们就需要为静态的节点做上一些「标记」,在 patch 的时候我们就可以直接跳过这些被标记的节点的比对,从而达到「优化」的目的。

得到如下结果。

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

isStatic 判断是否是静态节点

首先实现一个 isStatic 函数传入 node 判断该 node 是否是静态节点。判断的标准是当 type 为2 (表达式节点)则是非静态节点,当 type 为 3 (文本节点) 的时候则是静态节点, 当然如果存在 if 或者 for 这样的条件的时候 (表达式节点), 也是非静态节点。

/*
 * 设置是否是静态节点
 * */
function isStatic (node) {
  // type 为 2(表达式节点)则是非静态节点
  if (node.type === 2) {
    return false
  }
  // type 为 3(文本节点)的时候则是静态节点
  if (node.type === 3) {
    return true
  }
  // 存在 if 或者 for这样的条件的时候(表达式节点),也是非静态节点
  return (!node.if && !node.for);
}

markStatic 标记静态节点

markStatic为所有节点标记上 static 遍历所有节点通过 isStatic 来判断当前节点是否是静态节点,此外,会遍历当前节点的所有子节点,如果子节点是非静态节点,那么当前节点也是非静态节点。

/*
 * 标记静态节点
 * */
function markStatic (node) {
  node.static = isStatic(node)

  if (node.type === 1) {
    // 遍历子节点判断子节点是否是非静态
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      markStatic(child);

      if (!child.static) {
        node.static = false;
      }
    }
  }
}

markStaticRoots 标记静态根节点

markStaticRoots函数,用来标记 staticRoot (静态根)。如果当前节点是静态节点,同同时满足该节点并不是只有一个文本节点左右子节点(作者认为这种情况的优化消耗会大于收益)时,标记 staticRoot 为 true,否则为 false


/*
 * 标记静态根节点
 * */
function markStaticRoots (node) {
  if (node.type === 1) {
    // 如果有 static 有子节点 子节点不是静态节点
    if (node.static && node.children.length && !(node.children.length === 1 && node.children[0].type === 3)) {
      // 设置为静态根节点
      node.staticRoot = true;
      return;
    }
    else {
      node.staticRoot = false;
    }
  }
}

optimize 优化函数

有了以上的函数,就可以实现 optimize 了。

function optimize (rootAst) {
    markStatic(rootAst);
    markStaticRoots(rootAst);
}

generate 生成 render function 字符串

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

真实的 Vue.js 编译得到的结果

with(this){
    return (isShow) ? 
    _c(
        'div',
        {
            staticClass: "demo",
            class: c
        },
        _l(
            (sz),
            function(item){
                return _c('span',[_v(_s(item))])
            }
        )
    )
    : _e()
}

_c 对应的是 createElement 这个函数

首先是第一层 div 节点。

function render () {
  return isShow ? (new VNode('div', {
    'staticClass': 'demo',
    'class': c
  },[ /*这里还有子节点*/ ])) : createEmptyVNode()
} 

然后我们在 children 中加上第二层 span 及其子文本节点节点。

function render () {
  return isShow ? (new VNode('div', {
    'staticClass': 'demo',
    'class': c
  },
    /* 开始 */
    renderList ('sz', (item) => {
      return new VNode('span', {}, [
        // 创建一个文本节点
        createTextVNode(item)
      ])
    })
    /* 结束 */
  )) : createEmptyVNode()
}

genIf 处理 if 条件

首先实现一个处理 if 条件的 genIf 函数。

/*
 * genIf 处理 if 条件
 * */
function genIf (el) {
  el.ifProcessed = true
  if (!el.ifProcessed.length) {
    return '_e()'
  }

  return `(${el.ifConditions[0].exp})?${genElement(el.ifConditions[0].block)}: _e()`
}

genFor 处理 for 循环

/*
 * genFor 处理 for 条件
 * */
function genFor (el) {
  el.forProcessed = true

  const exp = el.for
  const alias = el.alias

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

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

genText 处理文本节点

function genText (el) {
    return `_v(${el.expression})`;
}

genElement

这是一个处理节点的函数,它依赖 genChildren 以及 genNode

genElement 会根据当前节点是否有 if 或者 for 标记 然后判断是否要用 genIf 或者 genFor 处理, 否则 通过 genChildren 处理子节点,同时得到 staticClassclass 等属性

genChildren 比较简单,遍历所有子节点,通过 genNode 处理后用 “,” 隔开拼接成字符串。

genNode 则是根据 type 来判断该节点是用文本节点 genText 还是标签节点 genElement 来处理

/*
* genNode 则是根据 type 来判断该节点是用文本节点 genText 还是标签节点
* */
function  genNode(el) {
  if (el.type === 1) {
    return genElement(el)
  }
  else {
    return genElement(el)
  }
}    


/*
* genChildren   遍历所有子节点,通过 genNode 处理后用“,”隔开拼接成字符串。
* */
function genChildren  (el) {
  const children = el.children;

  if (children && children.length > 0) {
    return `${children.map(genNode).join(',')}`;
  }
}

/*
* genElement  一个处理节点的函数
* */
function genElement  (el) {
  // 处理 if 条件
  if (el.if && !el.ifProcessed) {
    return genIf(el);
  }
  // 处理 for 条件
  else if (el.for && !el.forProcessed) {
    return genFor(el);
  }
  // 遍历所有子节点
  else {
    const children = genChildren(el)

    let code;
    code = `_c('${el.tag},'{
      staticClass: ${el.attrsMap && el.attrsMap[':class']},
      class: ${el.attrsMap  && el.attrsMap ['class']},
    }${
      children ? `,${children}` : ''
    })`
    return code
  }
}

generate 生成

需要将整个 AST 传入后判断是否为空,为空则返回一个 div 标签,否则通过 generate 来处理。

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

经历过这些过程以后,我们已经把 template 顺利转成了 render function