Vue2.X原理篇

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

Vue 的两个核心功能:

  • 声明式渲染:Vue 基于标准 HTML 拓展了一套模板语法,使得可以声明式地描述最终输出的 HTML 和 JavaScript 状态之间的关系。

  • 响应性:Vue 会自动跟踪 JavaScript 状态并在其发生变化时响应式地更新 DOM。

核心概念

响应式系统

Vue 最标志性的功能就是其低侵入性的响应式系统。组件状态都是由响应式的 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() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。还允许修改对象的属性的属性描述符。

Object.defineProperty(obj, prop, descriptor)

descriptor 是属性描述符对象,有以下属性:

  • configurable:布尔值,默认值:false,表示该属性的描述符是否能够被改变。

  • enumerable:布尔值,默认值:false,表示该属性是否出现在对象的枚举属性中。

  • value(数据描述符): 属性的值。

  • writable(数据描述符): 布尔值,默认值:false,表示属性的值是否能被修改。

  • get(存取描述符): 属性的 getter 函数,当访问该属性时,会调用此函数,返回值会被用作属性的值,默认值:undefined。

  • set(存取描述符): 属性的 setter 函数,当属性值被修改时,会调用此函数,默认值:undefined。

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。

<div class="a" id="b">我是内容</div>
{
  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 ,可以有多个观察者去观察这个对象。当被观察对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。

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

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

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

// 被观察者
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 的过程以及使用队列来异步更新的策略。

源码解读

<!-- 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 实例。

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 等静态方法或属性。

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 挂载实例。

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 节点。

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 函数。

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 方法。

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 对象。

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

render 函数的函数体长这样:

render.toString()
// function anonymous() {with(this){return _c('div',{on:{"click":onClickAlert}},[_c('p',[_v(_s(vueVersion))]),_v(" "),_c('p',[_v(_s(message))])])}}
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 下的方法。

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()。

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 是一个创建虚拟节点的构造函数:

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,则将节点文本置为空字符。

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 函数用于更新子节点,内部算法是为了复用一些没有变化的元素:

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。

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 方法。

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 构造函数定义的是观察者模式中的被观察者类。它很简单,主要工作是维护一个依赖数组,提供添加、删除以及通知各依赖的方法。

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 函数。

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:

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

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

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:

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 的思路,模拟一个简单的响应式系统。

// 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
<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 改变时,触发视图改变,即自动调用渲染函数。

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;
}
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
}
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 中文文档

Vue 官网 - 深入响应式系统

稀土掘金 - 剖析 Vue.js 内部运行机制

Vue中文社区 - Vue源码系列

Github - Vue.js源码分析

稀土掘金 - Vue响应式原理和实现方式,Observer、Dep、Watcher源码解析(下),深入浅出

Vue源码解析 - 从零构建一个响应式系统

最后更新于