> For the complete documentation index, see [llms.txt](https://lizh.gitbook.io/knowledge/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lizh.gitbook.io/knowledge/frontend/04vue2.x-yuan-li-pian.md).

# Vue2.X原理篇

Vue 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建，并提供了一套声明式的、组件化的编程模型，帮助高效地开发用户界面。

Vue 的两个核心功能：

* 声明式渲染：Vue 基于标准 HTML 拓展了一套模板语法，使得可以声明式地描述最终输出的 HTML 和 JavaScript 状态之间的关系。
* 响应性：Vue 会自动跟踪 JavaScript 状态并在其发生变化时响应式地更新 DOM。

## 核心概念

### 响应式系统

Vue 最标志性的功能就是其低侵入性的**响应式系统**。组件状态都是由响应式的 JavaScript 对象组成的。当更改它们时，视图会随即自动更新。

本质上，响应性是一种可以使声明式地处理变化的编程范式。

```javascript
let a = 1
let b = update() // 2
function update() {
  b = a + 1
}

a = 2
```

响应式系统要做的就是当变量 a 的值变化时，让变量 b 也自动更新。

原生 JavaScript 没有提供任何机制能做到这一点，但是，可以通过追踪对象属性的读写来实现。

在 JavaScript 中有两种劫持 property 访问的方式：getter / setters 和 Proxies。Vue 2 使用 getter / setters 完全是出于支持旧版本浏览器的限制；而 Vue 3 则使用了 Proxy 来创建响应式对象，仅将 getter / setter 用于 ref。

defineProperty() 方法会直接在一个对象上定义一个新属性，或者修改一个对象的现有属性，并返回此对象。还允许修改对象的属性的属性描述符。

```javascript
Object.defineProperty(obj, prop, descriptor)
```

descriptor 是属性描述符对象，有以下属性：

* configurable：布尔值，默认值：false，表示该属性的描述符是否能够被改变。
* enumerable：布尔值，默认值：false，表示该属性是否出现在对象的枚举属性中。
* value（数据描述符）： 属性的值。
* writable（数据描述符）： 布尔值，默认值：false，表示属性的值是否能被修改。
* get（存取描述符）： 属性的 getter 函数，当访问该属性时，会调用此函数，返回值会被用作属性的值，默认值：undefined。
* set（存取描述符）： 属性的 setter 函数，当属性值被修改时，会调用此函数，默认值：undefined。

```javascript
const obj = {
  a: 1,
  b: null
}
function defineReactive (obj, key, val) {
  Object.defineProperty(obj, key, {
    get: function () {
      return val;
    },
    set: function (newVal) {
      obj.b = newVal + 1
      console.log(obj.b)
      return newVal
    }
  });
}
defineReactive(obj, 'a', obj.a)
obj.a = 10
```

> Object.defineProperty() 是 Vue 2.x 的核心。如果浏览器不支持 Object.defineProperty，比如，IE8，那就等于不支持 Vue 2.x 框架。

### Virtual DOM

虚拟节点（Virtual DOM）本质上就是一棵以 JavaScript 对象（VNode 节点）作为基础的树，使用 JavaScript 对象来描述 DOM 的结构，实际上它只是一层对真实 DOM 的抽象。应用的各种状态变化首先作用于 Virtual DOM，最终映射到 DOM。

```html
<div class="a" id="b">我是内容</div>
```

```javascript
{
  tag:'div',      // 元素标签
  attrs:{         // 属性
    class:'a',
    id:'b'
  },
  text:'我是内容', // 文本内容
  children:[]     // 子元素
}
```

Virtual DOM 在 Vue 的底层实现上，借鉴了 Snabbdom（Virtual DOM 的一种简单实现，包括模块机制、钩子函数、diff 算法），将模板编译成 Virtual DOM 渲染函数，结合 Vue 自身的一些特性，如响应式系统、指令等。

Vue 是数据驱动视图的，数据发生变化视图就要随之更新，在更新视图的时候难免要操作 DOM，而操作真实 DOM 又是非常耗费性能的，这是因为浏览器的标准就把 DOM 设计的非常复杂。

而 JavaScript 的运算通常是比较快的，可以用 JavaScript 的计算性能来换取操作 DOM 所消耗的性能。当数据发生变化时，对比变化前后的虚拟节点，通过 DOM-Diff 算法计算出需要更新的地方，然后只更新需要更新的视图。这样就可以**尽可能少的操作DOM**。

这就是虚拟节点产生的原因以及最大的用途。

> Vue 1.x 采用的是 DocumentFragment；而 Vue 2.x 借鉴 React 的 Virtual DOM，引入后，渲染速度提升了2\~4倍，并大大降低了内存消耗。

### DOM-Diff

前面提到，「当数据发生变化时，对比变化前后的虚拟节点，通过 DOM-Diff 算法计算出需要更新的地方」，以达到**尽可能少的操作DOM**的目的。

在 Vue 中，把 DOM-Diff 过程叫做 patch 过程。patch，意为“补丁”，即指对旧的 VNode 修补，打补丁从而得到新的 VNode。其本质就是把对比新旧两份 VNode 的过程：**以新 VNode 为基准，改造旧 VNode 使之成为跟新 VNode 一样**。所谓旧 VNode 就是数据变化之前视图所对应的虚拟节点，而新 VNode 是数据变化之后将要渲染的新的视图所对应的虚拟节点。

patch 过程做的其实就是事情：

* 创建节点：新 VNode 有而旧 VNode 没有的节点，就在旧 VNode 中创建。
* 删除节点：新 VNode 没有而旧 VNode 有的节点，就在旧 VNode 中删除。
* 更新节点：新旧 VNode 都有的节点，就以新 VNode 为准，更新旧 VNode。

DOM-Diff 算法采用的是**深度优先遍历**，把树形结构按照层级分解，只对**同级的树节点**进行比较，而不是对整个树进行逐层搜索遍历的方式。它先对比父节点是否相同，然后对比子节点是否相同，相同的话对比孙子节点是否相同。所以时间复杂度只有 O(n)，是一种相当高效的算法。

DOM-Diff 算法的原理：逐个遍历新虚拟树的子节点，找到它在旧虚拟树中的位置，如果找到了就移动对应的节点，如果没找到说明是新增节点，则新建一个节点插入。遍历完成之后，如果旧虚拟树中还有没处理过的节点，则说明这些节点不需要了，删除它们即可。

### 观察者模式

观察者模式一般至少有一个可被观察的对象 Subject ，可以有多个观察者去观察这个对象。当被观察对象的状态发生变化时，会通知所有观察者对象，使它们能够自动更新。

二者的关系是通过被观察者主动建立的，被观察者至少要有三个方法——添加观察者、移除观察者、通知观察者。当被观察者将某个观察者添加到自己的观察者列表后，观察者与被观察者的关联就建立起来了。此后只要被观察者在某种时机触发通知观察者方法时，观察者即可接收到来自被观察者的消息。

**优点：** 响应式。目标变化就会通知观察者，这是观察者最大的有点，也是因为这个优点，观察者模式在前端才会这么出名。

**缺点：** 不灵活。相比订阅发布模式，由于目标和观察者是耦合在一起的，所以观察者模式需要同时引入目标和观察者才能达到响应式的效果；而订阅发布模式只需要引入事件中心，订阅者和发布者可以不再一处。

```javascript
// 被观察者
class Subject {
  constructor() {
    this.obs = [];
  }
  add(ob) {
    this.obs.push(ob);
  }
  remove(ob) {
    this.obs = this.obs.filter(o => o.name !== ob.name);
  }
  notify(message) {
    this.obs.forEach(ob => ob.notified(message));
  }
}

// 观察者
class Observer {
  constructor(name) {
    this.name = name;
  }
  notified(message) {
    console.log(`Hellow, ${this.name}. This is ${message}!`);
  }
}

const subject = new Subject();
const observerLi = new Observer('Li');
const observerZhao = new Observer('Zhao');
subject.add(observerLi);
subject.add(observerZhao);
subject.notify('Subject messagge 01');
subject.remove(observerLi);
subject.notify('Subject messagge 02');
```

## 渲染流程

### 初始化及挂载

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

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

### 编译

编译是 template 到 render 函数的过程，可以分成 parse、optimize 与 generate 三个阶段：

* parse：用正则等方式解析 template 模板中的指令、class、style 等数据，形成 AST。
* optimize：标记 static 静态节点，这是 Vue 在编译过程中的一处优化，后面当 update 更新界面时，会有一个 patch 的过程， Diff 算法会直接跳过静态节点，从而减少了比较的过程，优化了 patch 的性能。
* generate：将 AST 转化成渲染函数字符串的过程，得到结果是 render 函数和 staticRenderFns 函数数组的字符串表达。

generate 生成的字符串，最终会用 new Function 转换为相应的函数。

### 渲染

渲染是 render 函数到 DOM 的过程。

render 函数会在挂载阶段被调用。因为会读取所需对象的值，所以会触发 getter 函数进行「依赖收集」，「依赖收集」的目的是将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中。

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

## 源码解读

```html
<!-- Vue.js v2.7.14 -->
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script id="content" type="x-template">
    <div @click="onClickAlert">
      <p>{{vueVersion}}</p>
      <p>{{message}}</p>
  </div>
</script>
<script>
  const app = new Vue({
    template: '#content',
    props: {
      name: String
    },
    propsData: {
      name: 'Lizhao'
    },
    setup: function () {
      return {
        counter: 100
      }
    },
    provide: {
      createTime: new Date(2022, 10, 11, 9, 0, 0).toString()
    },
    computed: {
      vueVersion () {
        return `Vue@${this.version}`
      }
    },
    watch: {
      message: [
        function () { console.log('message is changed! 01') },
        function () { console.log('message is changed! 02') },
      ]
    },
    data: function () {
      const { name } = this
      return {
        version: Vue.version,
        message: `Hellow, ${name}. This is ${new Date()}.`
      }
    },
    methods: {
      onClickAlert () {
        this.message = `Hellow, ${name}. This is ${new Date()}.`
      }
    }
  })
  app.$mount('#app')
</script>
```

### new Vue()

每个 Vue 应用都是通过用 Vue 函数创建一个新的 Vue 实例开始的。new Vue() 根据传入的选项对象生成一个 Vue 实例。

```javascript
const app = new Vue({
  template: '#content',
  data: {
    message: `Hellow, Lizhao. This is ${new Date()}.`
  }
})
```

### 初始化（\_init）

Vue 实例化后，调用 this.\_init 方法来初始化实例对象。

Vue 本质上是一个函数。该函数声明后，会在其原型上扩展一些方法，比如，\_init、$mount、`__patch__`、$set、$delete、$watch、$on、$once、$off、$emit、`_update`、$forceUpdate、$destroy、$nextTick 等方法，以及 $data、$props 等属性，同时，也会注册 compile、use、mixin、extend、util 等静态方法或属性。

```javascript
function Vue(options) {
  // ...
  this._init(options);
}
initMixin$1(Vue);
function initMixin$1(Vue) {
  Vue.prototype._init = function (options) {};
}
Vue.prototype.$mount = function (el, hydrating) {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating);
};
```

初始化流程：

* 接收并处理 options 选项；
* 在 vue 实例上创建一些空对象，比如：$children、$refs、`_events`、$slots、$attrs、$listeners、`_provided` 等；
* 再调用 beforeCreate 钩子、初始化实例的状态（比如：`_props`、setup 选项、methods 中的方法、data 中的数据、computed 观察器、Watcher 观察器）、注入 `_provided` 数据、调用 beforeCreate 钩子；
* 最后调用 vm.$mount 挂载实例。

```javascript
function initMixin$1(Vue) {
  Vue.prototype._init = function (options) {
    //...
    initProxy(vm);
    vm._self = vm;
    initLifecycle(vm);
    initEvents(vm);
    initRender(vm);
    callHook$1(vm, 'beforeCreate', undefined, false /* setContext */);
    initInjections(vm); // resolve injections before data/props
    initState(vm);
    initProvide(vm); // resolve provide after data/props
    callHook$1(vm, 'created');
    // ...
    if (vm.$options.el) {
      vm.$mount(vm.$options.el);
    }
  };
}
function initState(vm) {
  var opts = vm.$options;
  if (opts.props)
    initProps$1(vm, opts.props);
  initSetup(vm);
  if (opts.methods)
    initMethods(vm, opts.methods);
  if (opts.data) {
    initData(vm);
  } else {
    var ob = observe((vm._data = {}));
    ob && ob.vmCount++;
  }
  if (opts.computed)
    initComputed$1(vm, opts.computed);
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}
```

### 挂载（$mount）

$mount 方法中最重要的两个方法：compileToFunctions，将 template 解析为一个渲染函数 render；mountComponent 将 render 函数的返回结果挂载到 DOM 节点。

```javascript
Vue.prototype.$mount = function (el, hydrating) {
	// ...
  return mountComponent(this, el, hydrating);
};
var mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (el, hydrating) {
	// ...
    var _a = compileToFunctions(template, {
      // ...
    }, this), render = _a.render, staticRenderFns = _a.staticRenderFns;
    options.render = render;
	// ...
  return mount.call(this, el, hydrating);
};
```

**compileToFunctions 调用栈：** compileToFunctions -> createCompiler -> createCompilerCreator -> createCompileToFunctionFn -> compileToFunctions -> compile -> baseCompile -> parse -> generate -> createFunction。

生成 render 函数的主要流程：

* 调用 parse 函数，将 template 或 el 选项的 DOM 节点，通过其 innerHTML 属性转换为字符串；
* 解析字符串转为 AST，并解析其属性、指令（v-for、v-if、v-once）等；
* 调用 generate 函数 将 AST 转换为生成函数的字符串，再转换成 render 函数。

```javascript
var _a = createCompiler(baseOptions);
var compileToFunctions = _a.compileToFunctions;
var createCompiler = createCompilerCreator(function baseCompile(template, options) {
  var ast = parse(template.trim(), options);
  // ...
  var code = generate(ast, options);
  return {
    ast: ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  };
});
function createCompilerCreator(baseCompile) {
  return function createCompiler(baseOptions) {
    function compile(template, options) {
      // ...
      var compiled = baseCompile(template.trim(), finalOptions);
      {
        detectErrors(compiled.ast, warn);
      }
      compiled.errors = errors;
      compiled.tips = tips;
      return compiled;
    }
    return {
      compile: compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    };
  };
}
function createCompileToFunctionFn(compile) {
  var cache = Object.create(null);
  return function compileToFunctions(template, options, vm) {
    // ...
    var compiled = compile(template, options);
    // ...
    var res = {};
    var fnGenErrors = [];
    res.render = createFunction(compiled.render, fnGenErrors);
    res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
      return createFunction(code, fnGenErrors);
    });
    // ...
    return (cache[key] = res);
  };
}

function generate(ast, options) {
  var state = new CodegenState(options);
  var code = ast ? ast.tag === 'script' ? 'null' : genElement(ast, state) : '_c("div")';
  return {
    render: "with(this){return ".concat(code, "}"),
    staticRenderFns: state.staticRenderFns
  };
}
```

**mountComponent 调用栈：** mountComponent -> new Water -> updateComponent -> `vm._render()` -> `Vue.prototype._render` -> `vm._update` -> Vue.prototype.\_update -> `__patch__` -> `Vue.prototype.__patch__` -> patch -> createPatchFunction -> createElm -> insert -> nodeOps.appendChild -> node.appendChild。

挂载流程：

* new Watcher() 创建实例对象，并将 updateComponent 函数传入，Watcher 构造函数会执行该函数；
* updateComponent 函数调用 `vm._render` 生成虚拟节点，再以虚拟节点为入参，调用 `vm._update` 方法；
* `vm._update` 方法最终调用的是 patch 函数，该函数核心是 DOM-Diff 算法，计算出需要更新的 VNode，以达到尽量少的渲染节点的目的；
* DOM-Diff 算法计算完成后，调用 createElm 或 patchVnode 函数更新节点，其本质上是调用原生节点的 node.appendChild、insertBefore 方法。

```javascript
function mountComponent(vm, el, hydrating) {
  // ...
  updateComponent = function () {
    vm._update(vm._render(), hydrating);
  };
  // ...
  new Watcher(vm, updateComponent, noop, watcherOptions, true);
  // ...
}

var Watcher = (function () {
  function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
    // ...
    if (isFunction(expOrFn)) {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
      // ..
    }
    this.value = this.lazy ? undefined : this.get();
  }
  // ...
  return Watcher;
}());

function lifecycleMixin(Vue) {
  Vue.prototype._update = function (vnode, hydrating) {
    // ...
    if (!prevVnode) {
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);
    } else {
      vm.$el = vm.__patch__(prevVnode, vnode);
    }
    // ...
  };
}

var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules$1 });
Vue.prototype.__patch__ = inBrowser ? patch : noop;
function createPatchFunction(backend) {
  // ...
  function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
    // ...
    if (isDef(tag)) {
      // ...
      insert(parentElm, vnode.elm, refElm);
      // ...
    } else if (isTrue(vnode.isComment)) {
      vnode.elm = nodeOps.createComment(vnode.text);
      insert(parentElm, vnode.elm, refElm);
    } else {
      vnode.elm = nodeOps.createTextNode(vnode.text);
      insert(parentElm, vnode.elm, refElm);
    }
  }
  // ...
  return function patch(oldVnode, vnode, hydrating, removeOnly) {
    // ...
    if (isUndef(oldVnode)) {
      isInitialPatch = true;
      createElm(vnode, insertedVnodeQueue);
    } else {
      var isRealElement = isDef(oldVnode.nodeType);
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
      } else {
        // ...
        createElm(vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm));
        // ...
      }
    }
    // ...
  };
}

function insert(parent, elm, ref) {
  if (isDef(parent)) {
    if (isDef(ref)) {
      if (nodeOps.parentNode(ref) === parent) {
        nodeOps.insertBefore(parent, elm, ref);
      }
    } else {
      nodeOps.appendChild(parent, elm);
    }
  }
}
var nodeOps = Object.freeze({
  // ...
  insertBefore: insertBefore,
  removeChild: removeChild,
  appendChild: appendChild,
  // ...
});
function appendChild(node, child) {
  node.appendChild(child);
}
```

### 虚拟节点（VNode）

new VNode() 用一实例化一个虚拟节点，其调用栈：mountComponent -> `vm._render` -> `Vue.prototype._render`，最终返回一个 VNode 对象。

```javascript
function renderMixin(Vue) {
  // ...
  Vue.prototype._render = function () {
    // ...
    var vnode;
    try {
      setCurrentInstance(vm);
      currentRenderingInstance = vm;
      vnode = render.call(vm._renderProxy, vm.$createElement);
    }
    // ...
    return vnode;
  };
}
```

render 函数的函数体长这样：

```javascript
render.toString()
// function anonymous() {with(this){return _c('div',{on:{"click":onClickAlert}},[_c('p',[_v(_s(vueVersion))]),_v(" "),_c('p',[_v(_s(message))])])}}
```

```javascript
function anonymous() {
  with(this){
    return _c(
      'div',
      {
        on:{"click":onClickAlert}
      },
      [
        _c(
          'p',
          [
            _v(_s(vueVersion))]),
        _v(" "),
        _c('p',[_v(_s(message))
               ]
          )
      ]
    )
  }
}
```

其中 `with(this)` 是指定语句块中的作用域，也就是 `render.call(vm._renderProxy, vm.$createElement)` 中的 `vm._renderProxy`，`_c、_v` 就是 `vm._renderProxy` 下的方法。

```javascript
initProxy = function initProxy(vm) {
  if (hasProxy_1) {
    // ...
    vm._renderProxy = new Proxy(vm, handlers);
  } else {
    vm._renderProxy = vm;
  }
};
```

`_c` 是一个简单的、返回 createElement$1 函数运行结果的函数，createElement$1 -> \_createElement -> new VNode()。

```javascript
function initRender(vm) {
  // ...
  vm._c = function (a, b, c, d) { return createElement$1(vm, a, b, c, d, false); };
  vm.$createElement = function (a, b, c, d) { return createElement$1(vm, a, b, c, d, true); };
  // ...
}

function createElement$1(context, tag, data, children, normalizationType, alwaysNormalize) {
  // ...
  return _createElement(context, tag, data, children, normalizationType);
}

function _createElement(context, tag, data, children, normalizationType) {
  // ...
  var vnode, ns;
  if (typeof tag === 'string') {
    var Ctor = void 0;
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
    if (config.isReservedTag(tag)) {
      // ...
      vnode = new VNode(config.parsePlatformTagName(tag), data, children, undefined, undefined, context);
    } else if ((!data || !data.pre) &&
      // ...
      vnode = createComponent(Ctor, data, context, children, tag);
    } else {
      // ...
      vnode = new VNode(tag, data, children, undefined, undefined, context);
    }
  } else {
    vnode = createComponent(tag, data, context, children);
  }
  // ...
}
```

VNode 是一个创建虚拟节点的构造函数：

```javascript
var VNode = /** @class */ (function () {
  function VNode(tag, data, children, text, elm, context, componentOptions, asyncFactory) {
    this.tag = tag;
    this.data = data;
    this.children = children;
    this.text = text;
    this.elm = elm;
    this.ns = undefined;
    this.context = context;
    // ...
  }
  Object.defineProperty(VNode.prototype, "child", {
    // ...
  });
  return VNode;
}());
```

### DOM-Diff（patch）

DOM-Diff 用对比变化前后的虚拟节点，计算出需要更新的节点，以达到**尽可能少的操作DOM**的目的。DOM-Diff 是在 patch 函数中实现的。

patch 函数的调用栈：mountComponent -> `vm._update` -> Vue.prototype.\_update -> `__patch__` -> `Vue.prototype.__patch__` -> patch -> createPatchFunction。

patch 函数逻辑如下：

* 如果 vnode 不存在，但是 oldVnode 存在，则销毁 oldVnode；
* 如果 vnode 存在，但是 oldVnode 不存在，则创建 vnode；
* 如果 oldVnode 不是真实节点，且 vnode 和 oldVnode 的 key、tag 等相同，则调用 patchVnode 方法，即更新 oldVnode。

patchVnode 方法逻辑如下：

* 如果 oldVnode 和 vnode 指向同一节点，则不处理；
* 如果 VNode 和 oldVNode 均为静态节点，则不处理；
* 如果 vnode 有 text，则直接替换文本内容；
* 如果 oldVnode 和 vnode 都有 children，且两者 children 不是同一对象，则调用 updateChildren 方法；
* 如果 只有 vnode 都有 children，则调用 addVnodes 新增子节点；
* 如果 只有 oldVnode 都有 children，则调用 removeVnodes 删除子节点；
* 如果 vnode 没有 text，则将节点文本置为空字符。

```javascript
var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules$1 });
function sameVnode(a, b) {
  return (a.key === b.key &&
    a.asyncFactory === b.asyncFactory &&
    ((a.tag === b.tag &&
      a.isComment === b.isComment &&
      isDef(a.data) === isDef(b.data) &&
      sameInputType(a, b)) ||
      (isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error))));
}
function createPatchFunction(backend) {
  // ...
  function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
    if (oldVnode === vnode) {
      return;
    }
    // ...
    var oldCh = oldVnode.children;
    var ch = vnode.children;
    // ...
    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i)
        cbs.update[i](oldVnode, vnode);
      if (isDef((i = data.hook)) && isDef((i = i.update)))
        i(oldVnode, vnode);
    }
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) {
					updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
        }
      } else if (isDef(ch)) {
        checkDuplicateKeys(ch);
        if (isDef(oldVnode.text)) {
					nodeOps.setTextContent(elm, ''); 
        }
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
      } else if (isDef(oldCh)) {
        removeVnodes(oldCh, 0, oldCh.length - 1);
      } else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '');
      }
    } else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text);
    }
    // ...
  }
  // ...
  return function patch(oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode))
        invokeDestroyHook(oldVnode);
      return;
    }
    var isInitialPatch = false;
    var insertedVnodeQueue = [];
    if (isUndef(oldVnode)) {
      isInitialPatch = true;
      createElm(vnode, insertedVnodeQueue);
    } else {
      var isRealElement = isDef(oldVnode.nodeType);
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
      } else {
        // ...
        createElm(vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm));
        // ...
      }
    }
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
    return vnode.elm;
  };
}
```

**特别注意：** `cbs.update[i](oldVnode, vnode)` 包含 updateAttrs、updateClass、updateDOMListeners、updateDOMProps、updateStyle 等函数的执行。这是因为，当虚拟节点是可 "修补" 的时，需要根据 VNode 的 data 数据，更新 oldVnode 下的真实 DOM 的属性、类名、样式、事件监听器等。

updateChildren 函数用于更新子节点，内部算法是为了复用一些没有变化的元素：

```javascript
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  // ...
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
    } else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx];
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
      canMove &&
        nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
      oldStartVnode = oldCh[++oldStartIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
      canMove &&
        nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
    } else {
      if (isUndef(oldKeyToIdx)) {
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
      }
      idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
      if (isUndef(idxInOld)) {
        createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
      } else {
        vnodeToMove = oldCh[idxInOld];
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
          oldCh[idxInOld] = undefined;
          canMove &&
            nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
        } else {
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
        }
      }
      newStartVnode = newCh[++newStartIdx];
    }
  }
  if (oldStartIdx > oldEndIdx) {
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
  } else if (newStartIdx > newEndIdx) {
    removeVnodes(oldCh, oldStartIdx, oldEndIdx);
  }
}
```

### 响应式实现

响应式的核心原理是 Object.defineProperty，核心函数是 defineReactive，其实现主要依赖三个构造函数：Observer、Dep、Watcher。

defineReactive 函数首先会实例化一个 Dep 对象，在读取数据时，get 方法会将 Dep.target（一个 Watcher 对象）存放到 Dep 对象的 subs 中；在更新数据时，set 方法会调用 Dep 对象的 notify 方法通知它内部所有的依赖执行 update。

**注意：** 只有触发 get 方法，Dep.target 才会被收集，也就是说，如果该属性没有被用到，则不收集到 subs。

```javascript
function defineReactive(obj, key, val, customSetter, shallow, mock) {
  var dep = new Dep();
  var property = Object.getOwnPropertyDescriptor(obj, key);
  // ...
  // 默认对子属性也进行响应式绑定
  var childOb = !shallow && observe(val, false, mock);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // 当读取属性时，将 Dep.target 添加到 Dep 的依赖收集中
      var value = getter ? getter.call(obj) : val;
      // Dep.target 主要是的 Watch 函数的 get 方法中赋值的。当然，其他方也有
      if (Dep.target) {
        dep.depend({
          target: obj,
          type: "get" /* TrackOpTypes.GET */,
          key: key
        });
        if (childOb) {
          childOb.dep.depend();
          if (isArray(value)) {
            dependArray(value);
          }
        }
      }
      return isRef(value) && !shallow ? value.value : value;
    },
    set: function reactiveSetter(newVal) {
      // ...
      childOb = !shallow && observe(newVal, false, mock);
      dep.notify({
        type: "set" /* TriggerOpTypes.SET */,
        target: obj,
        key: key,
        newValue: newVal,
        oldValue: value
      });
  });
  return dep;
}
```

Observer 构造函数其实是对 defineReactive 函数封装。它接收一个对象，并对对象中的每个属性调用 defineReactive 函数，深度遍历每一项，定义每个子对象中属性的 get 和 set 方法。

```javascript
var Observer = (function () {
  function Observer(value, shallow, mock) {
    // ...
    if (isArray(value)) {} else {
      // ...
      var keys = Object.keys(value);
      for (var i = 0; i < keys.length; i++) {
        var key = keys[i];
        defineReactive(value, key, NO_INIITIAL_VALUE, undefined, shallow, mock);
      }
    }
  }
  // ...
  return Observer;
}());
function observe(value, shallow, ssrMockReactivity) {
  // ...
  if (/** ... **/) {
    return new Observer(value, shallow, ssrMockReactivity);
  }
}
```

Dep 构造函数定义的是观察者模式中的**被观察者**类。它很简单，主要工作是维护一个依赖数组，提供添加、删除以及通知各依赖的方法。

```javascript
var Dep = (function () {
  function Dep() {
    this._pending = false;
    this.id = uid$2++;
    this.subs = [];
  }
  Dep.prototype.addSub = function (sub) {
    this.subs.push(sub);
  };
  Dep.prototype.removeSub = function (sub) {
    this.subs[this.subs.indexOf(sub)] = null;
    if (!this._pending) {
      this._pending = true;
      pendingCleanupDeps.push(this);
    }
  };
  Dep.prototype.depend = function (info) {
    if (Dep.target) {
      Dep.target.addDep(this);
      if (info && Dep.target.onTrack) {
        Dep.target.onTrack(__assign({ effect: Dep.target }, info));
      }
    }
  };
  Dep.prototype.notify = function (info) {
    if (!config.async) {
      subs.sort(function (a, b) { return a.id - b.id; });
    }
    for (var i = 0, l = subs.length; i < l; i++) {
      var sub = subs[i];
      if (info) {
        sub.onTrigger &&
          sub.onTrigger(__assign({ effect: subs[i] }, info));
      }
      sub.update();
    }
  };
  return Dep;
}());

Dep.target = null;
var targetStack = [];
function pushTarget(target) {
  targetStack.push(target);
  Dep.target = target;
}
function popTarget() {
  targetStack.pop();
  Dep.target = targetStack[targetStack.length - 1];
}
```

Watcher 构造函数定义的是观察者模式中的**观察者**类：添加依赖到 subs， 以及提供 update 函数。

```javascript
var uid$1 = 0;
var Watcher = /** @class */ (function () {
  function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
    // ...
    this.value = this.lazy ? undefined : this.get();
  }
  Watcher.prototype.get = function () {
    pushTarget(this);
    var value;
    var vm = this.vm;
    try {
      value = this.getter.call(vm, vm);
    }
    // ...
    finally {
      if (this.deep) { traverse(value); }
      popTarget();
      this.cleanupDeps();
    }
    return value;
  };
  Watcher.prototype.addDep = function (dep) { /** ... **/};
  Watcher.prototype.cleanupDeps = function (dep) { /** ... **/};
  Watcher.prototype.update = function () {
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this);
    }
  };
  Watcher.prototype.run = function () {
    if (this.active) {
      var value = this.get();
      // ...
    }
  };
  // ...
  return Watcher;
}());
```

总结来说（以 render 函数为例）：defineReactive 给组件的 data 选项的所有属性绑定 get 和 set 存取器；调用 render 函数时，先实例化一个 Watcher 对象，该对象在 get 方法中调用 render 函数，并将自己赋值给全局变量 Dep.target；如果 render 函数对 data 有读值操作，则将 Dep.target 添加到 subs；如果 render 函数有写值操作，则通过 dep.notify 通知 subs 下所有子元素执行 update 方法，即 Watcher 实例的 update 方法，重新调用 render 函数。

### 响应式数据绑定

data 选项的响应性初始化：Vue -> this.\_init -> initState -> initData：

```javascript
function initData(vm) {
  var data = vm.$options.data;
  // ...
  var ob = observe(data);
  ob && ob.vmCount++;
}
```

computed 选项初始化，添加监听器到 `vm._computedWatchers`：Vue -> this.\_init -> initState -> initComputed$1：

```javascript
function initComputed$1(vm, computed) {
  var watchers = (vm._computedWatchers = Object.create(null));
  // ...
  for (var key in computed) {
    var userDef = computed[key];
    var getter = isFunction(userDef) ? userDef : userDef.get;
    if (!isSSR) {
      watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions);
    }
    if (!(key in vm)) {
      defineComputed(vm, key, userDef);
    }
    // ...
  }
}
```

water 选项初始化：Vue -> this.\_init -> initState -> initWatch：

```javascript
function initWatch(vm, watch) {
  for (var key in watch) {
    var handler = watch[key];
    if (isArray(handler)) {
      for (var i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i]);
      }
    } else {
      createWatcher(vm, key, handler);
    }
  }
}
function createWatcher(vm, expOrFn, handler, options) {
  if (isPlainObject(handler)) {
    options = handler;
    handler = handler.handler;
  }
  if (typeof handler === 'string') {
    handler = vm[handler];
  }
  return vm.$watch(expOrFn, handler, options);
}
function stateMixin(Vue) {
  // ...
  Vue.prototype.$watch = function (expOrFn, cb, options) {
    // ...
    var watcher = new Watcher(vm, expOrFn, cb, options);
    // ...
  };
}
```

## 一个简单的 Vue

根据 Vue 的思路，模拟一个简单的响应式系统。

```javascript
// index.js
function MyVue(options) {
  this.$options = options
  this.template = null
  this.methods = {}
  this.init()
}
MyVue.prototype.init = function () {
  const vm = this
  vm.template = document.querySelector(vm.$options.template)
  vm.methods = vm.$options.methods
  const data = vm.$options.data()
  const keys = Object.keys(data)
  keys.forEach(k => {
    defineReactive(vm, k, data[k])
  })
}
MyVue.prototype.$mount = function (selector) {
  const vm = this
  new Watcher(this, () => {
    render(selector, vm)
  })
}
window.MyVue = MyVue
```

```html
<div id="app"></div>
<script src="./index.js"></script>
<script id="content" type="x-template">
  <div @click="onClickMessage">
		{{message}}
  </div>
</script>
<script>
  const app = new MyVue({
    template: '#content',
    data: function () {
      return {
        message: `Hellow, Lizhao. This is ${new Date()}.`
      }
    },
    methods: {
      onClickMessage () {
        this.message = `Hellow, Lizhao. This is ${new Date()}.`
      }
    }
  })
  app.$mount('#app')
</script>
```

响应式系统应该实现以下三点：

* 将传参 data 处理成响应式；
* 一个将模板解析为 DOM 的渲染函数；
* data 改变时，触发视图改变，即自动调用渲染函数。

```javascript
function defineReactive(obj, key, val) {
  var dep = new Dep();
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      if (Dep.target) {
        dep.depend();
      }
      return val
    },
    set: function reactiveSetter(newVal) {
      val = newVal
      dep.notify();
    }
  });
  return dep;
}
```

```javascript
function render (selector, inst) {
  const dom = parseToDom(inst.template.innerHTML, inst);
  const $app = document.querySelector(selector);
  $app.innerHTML = '';
  $app.appendChild(dom)
}
function parseToDom (html, inst) {
  const dom = new DOMParser().parseFromString(html, 'text/html').body.childNodes[0]
  dom.innerText = dom.innerText.replace(/{{.*}}/, function (str) {
    const key = str.replace(/{{(.*)}}/, '$1')
    return inst[key]
  })
  const methodName = dom.getAttribute('@click')
  dom.addEventListener('click', function () {
    inst.methods[methodName].bind(inst)()
  }, false)  
  return dom
}
```

```javascript
var Dep = (function () {
  function Dep() {
    this.subs = [];
  }
  Dep.prototype.addSub = function (sub) {
    this.subs.push(sub);
  };
  Dep.prototype.depend = function () {
    if (Dep.target) {
      this.addSub(Dep.target)
    }
  }
  Dep.prototype.removeSub = function (sub) {
    this.subs = this.subs.filter((item) => sub !== item)
  };
  Dep.prototype.notify = function () {
    this.subs.forEach((sub) => { sub.update() })
  };
  return Dep;
}());
Dep.target = null;

var Watcher = (function () {
  function Watcher(vm, expOrFn) {
    this.deps = []
    this.vm = vm
    this.getter = expOrFn
    this.value = this.get()
  }
  Watcher.prototype.get = function () {
    Dep.target = this
    const value = this.getter.call(this.vm)
    Dep.target = null
    return value;
  };

  Watcher.prototype.addDep = function (dep) {
    this.deps.push(dep)
  };
  Watcher.prototype.update = function () {
    const value = this.getter.call(this.vm)
    return value
  };
  return Watcher;
}());
```

## 相关问题

### 为什么说 Vue 没有完全遵循 MVVM 模型？

MVVM（Model–view–viewmodel）模型的定义：

* MVVM 由 3 个部分组成：Model（模型）是数据和逻辑；View（视图）是用户在屏幕上看到的结构、布局和外观，也称UI； ViewModel（视图模型）是一个绑定器，能和 View 层和 Model 层进行通信。
* MVVM 的核心实现是由 ViewModel 层数据绑定，核心思想是分离，即通过 ViewModel 让 View 层和 Model 层解耦。

严格的 MVVM 要求 View 不能和 Model 直接通信，而 Vue 在组件提供了 $refs 这个属性，让 Model 可以直接操作 View，违反了这一规定，所以说 Vue 没有完全遵循 MVVM。

### 为什么要使用 Virtual DOM ?

* 具备跨平台的优势：由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境，所以使它具有了跨平台的能力，比如说浏览器平台、Weex、Node 等。
* 运行效率高：DOM 操作的执行速度远不如 Javascript 的运算速度快。因此，把大量的 DOM 操作转为 Javascript 计算，运用 DOM-Diff 算法计算出真正需要更新的节点，最大限度地减少 DOM 操作，从而显著提高性能。
* 提升渲染性能：Virtual DOM 的优势不在于单次的操作，而是在大量、频繁的数据更新下，能够对视图进行合理、高效的更新。

**注意：** 关于性能方面，在首次渲染的时候肯定不如直接操作 DOM，因为要维护一层额外的 Virtual DOM。但如果后续有频繁操作 DOM 的操作，就可能会有性能的提升。

### key 在虚拟节点中的作用？

key 主要用在 Vue 的虚拟 DOM 算法，在新旧 nodes 对比时辨识 VNodes。如果不使用 key，Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时，它会基于 key 的变化重新排列元素顺序，并且会移除 key 不存在的元素。

key 作用的原理是：Vue 在 patch 过程中通过 key 可以精准判断两个节点是否是同一个，从而避免频繁更新不同元素，使得整个 patch 过程更加高效，减少 Dom 操作量，提高性能。

另外，若不设置 key 还可能在列表更新时候引发一些隐藏的 bug；key 也可以用于强制替换元素/组件而不是重复使用它，比如，完整地触发组件的生命周期钩子、触发过渡。

有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。

## 参考资料

[Vue 2.x 中文文档](https://v2.cn.vuejs.org/)

[Vue 官网 - 深入响应式系统](https://cn.vuejs.org/guide/extras/reactivity-in-depth.html)

[稀土掘金 - 剖析 Vue.js 内部运行机制](https://juejin.cn/book/6844733705089449991/section/6844733705211084808)

[Vue中文社区 - Vue源码系列](https://vue-js.com/learn-vue/)

[Github - Vue.js源码分析](https://github.com/answershuto/learnVue)

[稀土掘金 - Vue响应式原理和实现方式，Observer、Dep、Watcher源码解析（下），深入浅出](https://juejin.cn/post/7104871637710798885)

[Vue源码解析 - 从零构建一个响应式系统](https://www.qinguanghui.com/vue/%E5%93%8D%E5%BA%94%E5%BC%8F%E5%8E%9F%E7%90%86%E4%B8%8A%E7%AF%87.html)
