先看index.html的代码吧

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>mini Vue</title>
</head>

<body>
  <div id='app'>
  </div>
</body>
<script src='./utils.js'></script>
<script src='./index.js'></script>

<script>
  var vm = new Vue({
    name: 'root',
    el: '#app',
    render(h) {
      return h('div', 'ok');
    },
  });
  console.log('vm ', vm);
</script>
</html>

基本就是vue的index.html加上main.js的结合体,这里为了避免index.js太长,把一些基础方法放到了utils.js里面
具体代码见github地址:https://github.com/shadowprompt/miniVue

本系列查看的源码是vue 2.6.10,文件是node_modules/vue/dist/vue.runtime.esm.js

首先是通过一系列的mixin操作,在Vue.protoype上面加入一系列方法

先看initMixin。因为在Vue的构造函数里面需要执行this._init(options)

Vue.prototype._init = function(options = {}) {
    var vm = this;
    vm.$options = mergeOptions(Vue.options, options);
    vm._renderProxy = vm;
    initRender(vm);
    initState(vm);
    if (vm.$options.el) {
      vm.$mount(vm.$options.el);
    }
  };

看上去比较大“简单”,但是里面每一个initXXX都是大家伙。
这里先提下initRender(vm)

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

其中vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };,可以看出我们的vm.$createElement并不是通过原型写进去的方法,而是每次_init时写进去的,createElementcreateElement就是我们会使用render的写法(render: h => h('div', '这是个div'))时的参数,用来创建vnode的。

function _createElement (
  context,
  tag,
  data,
  children,
  normalizationType
) {
  return new VNode(
    tag, data, children,
    undefined, undefined, context
  );
}

最后我们会调用$mount方法,我们直接看看它是怎么把node挂载到页面的

接着看这个mountComponent

function mountComponent (
  vm,
  el,
  hydrating
) {
  var updateComponent = function () {
      vm._update(vm._render(), hydrating);
    };
  }
  new Watcher(vm, updateComponent, noop, {
    before: function before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate');
      }
    }
  }, true /* isRenderWatcher */);
  return vm
}

里面new了一个Watcher
第二个参数就是Watcher的getter
在创建Watcher时,在构造函数里面

if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    this.getter = parsePath(expOrFn);
  }
  this.value = this.lazy
    ? undefined
    : this.get();

除了是lazy的情况,我们在创建实例的时候是会直接调用this.get()

Watcher.prototype.get = function get () {
  pushTarget(this); // targetStack.push(target); Dep.target = target;
  var value;
  var vm = this.vm;
  try {
    value = this.getter.call(vm, vm);
  } catch (e) {
  } finally {
    if (this.deep) {
      traverse(value);
    }
    popTarget();
    this.cleanupDeps();
  }
  return value
};

在这里我们调用get的话,实际调用的是updateComponent,也就是vm._update(vm._render(), hydrating);
我们先按参数vm._render(),它会返回一个vnode,代码如下:

Vue.prototype._render = function () {
    // render self
    var vnode;
    try {
      currentRenderingInstance = vm;
      vnode = render.call(vm._renderProxy, vm.$createElement);
    } catch (e) {
    } finally {
      currentRenderingInstance = null;
    }
    return vnode
};

在这里调用了render方法:vnode = render.call(vm._renderProxy, vm.$createElement)
接着看_update方法

Vue.prototype._update = function (vnode, hydrating) {
  vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* 
};

其实__patch__方法就是var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });
createPatchFunction是个很重要的方法,是将vnode变成真实dom,然后挂载到对应的父节点下面。
里面会递归的调用createElmcreateChildren

function createPatchFunction(){
    ....
    return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true;
      createElm(vnode, insertedVnodeQueue);
    } else {
      var isRealElement = isDef(oldVnode.nodeType);
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
      } else {
        if (isRealElement) {
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR);
            hydrating = true;
          }
          oldVnode = emptyNodeAt(oldVnode);
        }

        // replacing existing element
        var oldElm = oldVnode.elm;
        var parentElm = nodeOps.parentNode(oldElm);

        // create new node
        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        );
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
    return vnode.elm
  }
}

createElm方法里面还会对dom节点进行层级的插入。最终挂载到外层的父节点body里面,完成整个页面渲染。


1 条评论

普若木特 · 2019/05/27 13:31

应该wordpress博客的长度现在,里面很多代码都进行了极度精简,不然后面的内容被截断,不开心

发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据