Vue源码解析(一)-模版渲染

吉祥物

Vue demo

先给出vue简单的使用demo,通过创建一个Vue的实例,<div id="app"></div>将被替换成template模版中的内容,a,b的值也会被换成data属性的值

<div id="app"></div>

var vm = new Vue({
  el: '#app',
  template: 
  `<div>
    <p>{{a}}</p>
    <p>{{b}}</p>
    <div :class="a">btn1</div>
    <div @click="plus()">btn2</div>
    <div>
        <div>
            <div>str1</div>
            <div>str2</div>
        </div>
        <div>str3</div>
     </div>
    </div>`,
  data(){
    return {
      a: 1,
      b: 2
    }
  }
})

模版渲染

以下的分析代码都经过作者简化,只为简单清楚的解析vue的实现逻辑
首先根据参数template属性生成render函数

function Vue (options) {
  this._init(options);
}

Vue.prototype._init = function (options) {
  var vm = this;
  //参数el属性存在,就调用mount方法;初始化时也可以不传el属性,后续调用mount方法
  if (vm.$options.el) {
      vm.$mount(vm.$options.el);
  }
}

Vue.prototype.$mount = function () {
  var ref = compileToFunctions(template, {
    shouldDecodeNewlines: shouldDecodeNewlines,
    delimiters: options.delimiters,
    comments: options.comments
  }, this);
  //由template参数得到render方法
  var render = ref.render;   
  //由template参数得到最大静态渲染树
  var staticRenderFns = ref.staticRenderFns;
};

下面看下compileToFunctions生成render方法的具体实现

  var ast = parse(template.trim(), options);
  optimize(ast, options);
  var code = generate(ast, options);

首先根据template字符串生成ast对象,parse函数主要是通过正则表达式将str转换成
树结构的对象,ast对象基本结构如下:

图片描述
然后对ast对象进行优化,找出ast对象中所有的最大静态子树(可以简单理解为不包含参数data属性的dom节点,每次data数据改变导致页面重新渲染的时候,最大静态子树不需要重新计算生成),基本实现逻辑如下:

function optimize (root, options) {
  //将ast对象的所有节点标记为是否静态
  markStatic(root);
  markStaticRoots(root, false);
}
function markStatic (node) {
  //当前节点是否静态
  node.static = isStatic(node);
  //递归标记node的子节点是否静态
  for (var i = 0, l = node.children.length; i < l; i++) {
    var child = node.children[i];
    markStatic(child);
    //只要有一个子节点非静态,父节点也非静态
    if (!child.static) {
      node.static = false;
    }
  }
}
function markStaticRoots (node, isInFor) {
    //将包含至少一个非文本子节点(node.type === 3代表文本节点)的节点标记为最大静态树的根节点
    if (node.static && node.children.length && !(
      node.children.length === 1 &&
      node.children[0].type === 3
    )) {
      node.staticRoot = true;
      return
    } else {
      node.staticRoot = false;
    }
    //当前node节点不是静态根节点,递归判断子节点
    if (node.children) {
      for (var i = 0, l = node.children.length; i < l; i++) {
        markStaticRoots(node.children[i], isInFor || !!node.for);
      }
    }
}

本例中的最大静态树:
图片描述

最终生成的渲染函数code如下
图片描述

其中render是整个模版的渲染函数,staticrenderfns是静态树的渲染函数,staticrenderfns中的函数只会初始化一次,后续不需要再计算
render函数中用到的一些方法如下

function installRenderHelpers (target) {
  target._o = markOnce;
  target._n = toNumber;
  //转换为string对象
  target._s = toString;
  target._l = renderList;
  target._t = renderSlot;
  target._q = looseEqual;
  target._i = looseIndexOf;
  target._m = renderStatic;
  target._f = resolveFilter;
  target._k = checkKeyCodes;
  target._b = bindObjectProps;
  //生成虚拟文本节点
  target._v = createTextVNode;
  target._e = createEmptyVNode;
  target._u = resolveScopedSlots;
  target._g = bindObjectListeners;
}
//生成虚拟dom节点
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };

得到render函数后会继续调用下面的方法

function mountComponent (
    updateComponent = function () {
      vm._update(vm._render(), hydrating);
    };
    //对vue实例新建一个Watcher监听对象,每当vm.data数据有变化,Watcher监听到后负责调用updateComponent进行dom更新
    vm._watcher = new Watcher(vm, updateComponent, noop);
)

render函数调用后(vm._render())会生成vnode对象,也就是大家熟知的虚拟dom树,调用update方法就能根据vonde更新真实的浏览器dom。
接下来我们分析下updatecomponents是如何调用的,这就涉及到了vue经典的watch机制(此处先简单介绍,下一篇会有较详细的分析)。

//new Watcher时会先调用一次updateComponent,后续会监听vm.data的变化
var Watcher = function Watcher (vm,expOrFn){
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  }
  this.get();
}
Watcher.prototype.get = function get () {
   value = this.getter.call(vm, vm);
}

最后再讲一下 vm._update方法的实现

Vue.prototype._update = function (vnode, hydrating) {
    var prevVnode = vm._vnode;
    vm._vnode = vnode;
    //判断vnode是否初始化过
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(
        //  vm.$el是vm对象挂载的节点,本例是<div id="app"></div>
        //  vm.$options._parentElm是组件挂载的节点(父节点),后面介绍组件时分析
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      );
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode);
    }
}
//vm.__pathch__方法
function function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm){
    //根据vnode生成element并插入parentElm
    createElm(vnode, insertedVnodeQueue, parentElm, refElm);
}
//下面主要介绍初始化dom的实现
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
    //根据vnode节点生成浏览器Element对象
    vnode.elm = nodeOps.createElement(tag, vnode);
    var children = vnode.children;
    //递归将vnode子节点生成Element对象
    createChildren(vnode, children, insertedVnodeQueue);
    //将生成的vnode.elm插入到浏览器的父节点当中
    insert(parentElm, vnode.elm, refElm);
}

function createChildren (vnode, children, insertedVnodeQueue) {
    if (Array.isArray(children)) {
      for (var i = 0; i < children.length; ++i) {
        createElm(children[i], insertedVnodeQueue, vnode.elm, null, true);
      }
    //当vnode是文本节点时停止递归  
    } else if (isPrimitive(vnode.text)) {
      nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(vnode.text));
    }
}
阅读 6.2k
181 声望
51 粉丝
0 条评论
181 声望
51 粉丝
宣传栏