Skip to content
微信公众号

mpvue patch 源码分析

mpvue Watcher 源码分析

mpvue 实例化后,我们调用 $mount 方法实现了界面的渲染,$mount 方法源码如下:

js
Vue$3.prototype.$mount = function (el, hydrating) {
  var this$1 = this;

  // 初始化小程序生命周期相关
  var options = this.$options;

  if (options && (options.render || options.mpType)) {
    var mpType = options.mpType; if ( mpType === void 0 ) mpType = 'page';
    return this._initMP(mpType, function () {
      return mountComponent(this$1, undefined, undefined)
    })
  } else {
    return mountComponent(this, undefined, undefined)
  }
};

由于小程序初始化会提供 mpType,所以程序将执行 this._initMp 方法,this._initMp 方法会根据 mpType 对小程序的 App 或 Page 对象进行初始化,由于我们要分析界面渲染的部分,所以我们只考虑 mpType 为 Page 的场景。this._initMp 方法中对 Page 进行初始化的源码如下:

js
var app = global.getApp();
global.Page({
  data: {
    $root: {}
  },
  
  handleProxy: function handleProxy (e) {
    return rootVueVM.$handleProxyWithVue(e)
  },

  // mp lifecycle for vue
  // 生命周期函数--监听页面加载
  onLoad: function onLoad (query) {
    mp.page = this;
    mp.query = query;
    mp.status = 'load';
    getGlobalData(app, rootVueVM);
    callHook$1(rootVueVM, 'onLoad', query);
  },

  // 生命周期函数--监听页面显示
  onShow: function onShow () {
    mp.page = this;
    mp.status = 'show';
    callHook$1(rootVueVM, 'onShow');

    // 只有页面需要 setData
    rootVueVM.$nextTick(function () {
      rootVueVM._initDataToMP();
    });
  },

  // 生命周期函数--监听页面初次渲染完成
  onReady: function onReady () {
    mp.status = 'ready';

    callHook$1(rootVueVM, 'onReady');
    next();
  },

  // Do something when page scroll
  onPageScroll: function onPageScroll (options) {
    callHook$1(rootVueVM, 'onPageScroll', options);
  }
});

上述代码主要做了以下几件事:

  • 调用 global.Page 初始化了一个 Page 对象,从而实现页面的初始化;
  • 初始化了 Page 的 data 为 $root;
  • 定义了一个事件代理方法 handleProxy,对于事件代理方法的源码我们将在下一节进行详细分析;
  • 依次定义了 Page 的所有生命周期方法,并将其与我们在 options 中定义的方法进行绑定,比如我们在 mpvue 初始化时在 methods 中定义了 onPageScroll 方法,那么这里会绑定到 Page 的 onPageScroll 方法中:
js
onPageScroll: function onPageScroll (options) {
  callHook$1(rootVueVM, 'onPageScroll', options);
},

而在 onReady 方法调用完毕后,会调用 next 回调函数:

js
onReady: function onReady () {
  mp.status = 'ready';

  callHook$1(rootVueVM, 'onReady');
  next();
}

next 即我们在 this._initMp 方法中传入第二个参数,它是一个回调函数,其源码如下:

js
function () {
  return mountComponent(this$1, undefined, undefined)
}

在 Page 实例化过程中我们注意到 data 并没有赋值,所以此时界面是处于初始状态,而 mountComponent 方法会完成 mpvue 实例中的 data 与 Page 中的 data 的映射,具体实现过程我们继续往下分析,先看 mountComponent 的核心源码:

js
function mountComponent (vm, el,hydrating) {
  vm.$el = el;
  callHook(vm, 'beforeMount');

  var updateComponent = function () {
    vm._update(vm._render(), hydrating);
  };

  vm._watcher = new Watcher(vm, updateComponent, noop);
  hydrating = false;

  if (vm.$vnode == null) {
    vm._isMounted = true;
    callHook(vm, 'mounted');
  }
  return vm
}

这里的关键步骤只有 4 行代码:

js
var updateComponent = function () {
  vm._update(vm._render(), hydrating);
};

vm._watcher = new Watcher(vm, updateComponent, noop);

这个过程会完成整个界面的渲染,我们先看 Watcher 的构造函数,Watcher 构造函数的关键源码如下:

js
var Watcher = function Watcher (vm, expOrFn, cb, options) {
  this.vm = vm;
  vm._watchers.push(this);
  
  this.deps = [];
  this.newDeps = [];
  this.depIds = new _Set();
  this.newDepIds = new _Set();
  this.expression = '';
  
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  }
  
  this.value = this.get();
};

首先,mpvue 向 vm._watchers 数组中 push 当前 Watcher 实例,接着 Watcher 初始化了 deps、newDeps 和 depIds 这几个依赖收集相关的集合。由于我们传入 expOrFn 是 updateComponent 函数,所以 this.getter 会指向 updateComponent 函数,最后会调用 this.get 方法获取 value 的值,this.get 方法源码如下:

js
Watcher.prototype.get = function get () {
  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
};

this.get 方法非常重要,它第一步调用 pushTarget 方法,它将 Watcher 作为参数传入,pushTarget 方法源码如下:

js
Dep.target = null;

function pushTarget (_target) {
  Dep.target = _target;
}

此时 Dep.target 指向 Watcher 对象。之后 this.get 方法会执行:

js
value = this.getter.call(vm, vm);

上一段我们分析过 this.getter 指向 updateComponent 方法,所以此时会执行 updateComponent 方法,但是要注意这里使用 call 方法调用 updateComponent,因此 updateComponent 的 this 指向 vm 即 mpvue 实例,updateComponent 源码如下:

js
var updateComponent = function () {
  vm._update(vm._render(), hydrating);
};

它首先执行了 vm._render 函数生成 vnode,然后调用 vm._update 完成界面渲染。vm._render 的关键代码是调用 render 函数生成 vnode,这不是本节重点,我们先跳过,直接分析 vm._update 方法。

mpvue udpate 源码分析

vm._update 的核心源码如下:

js
Vue.prototype._update = function (vnode, hydrating) {
    var vm = this;
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate');
    }
    var prevEl = vm.$el;
    var prevVnode = vm._vnode;
    var prevActiveInstance = activeInstance;
    activeInstance = vm;
    vm._vnode = vnode;
    if (!prevVnode) {
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      );
      vm.$options._parentElm = vm.$options._refElm = null;
    } else {
      vm.$el = vm.__patch__(prevVnode, vnode);
    }
    activeInstance = prevActiveInstance;
    if (prevEl) {
      prevEl.__vue__ = null;
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm;
    }
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el;
    }
  };

vm._update 方法是完成渲染的核心方法,不管对 mpvue 还是 Vue 都是如此。实际完成渲染的方法是 vm.patch 方法,此方法我们会在下一节进行分析,本节我们先了解 vm._update 源码的执行流程和主要逻辑。vm._update 执行过程中,mpvue 会判断当前的 vnode 是否已经 mounted 到界面上,当我们第一次执行时,vm_isMounted 为 false,所以会跳过该段代码,但是当已经 mounted 后,则会调用 beforeUpdate 生命周期函数:

js
if (vm._isMounted) {
  callHook(vm, 'beforeUpdate');
}

紧接着 mpvue 会判断 prevVnode 是否存在,如果 prevVnode 存在,则调用 vm.patch 方法进行 diff 算法,找出差异项进行最小颗粒度的更新:

js
vm.$el = vm.__patch__(prevVnode, vnode);

第一次进入时 prevVnode 是不存在的,所以会进入:

js
vm.$el = vm.__patch__(
  vm.$el, vnode, hydrating, false /* removeOnly */,
  vm.$options._parentElm,
  vm.$options._refElm
);

有的同学可能会有疑问,在 vm._render 方法调用时即生成了 vnode,为什么第一次渲染时 prevNode 为 null 呢?这里 prevNode 指向 vm._vnode,这是因为 vm._render 执行后生成了 vnode,但并未将生成的 vnode 赋值给 vm._vnode,所以第一次进入页面时,vm._vnode 仍然为 null。

mpvue patch 源码分析

下面我们就来分析最关键的 patch 方法,由于篇幅关系,我们只分析初次进入时的场景,vm.patch 核心源码如下:

js
function patch () {
  corePatch.apply(this, arguments);
  this.$updateDataToMP();
}

这里的 corePatch 即实际完成界面渲染的方法,其源码如下:

js
function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
  var isInitialPatch = false;
  var insertedVnodeQueue = [];

  if (isUndef(oldVnode)) {
    isInitialPatch = true;
    createElm(vnode, insertedVnodeQueue, parentElm, refElm);
  }

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

由于我们的 oldVnode 不存在,所以会调用 createElm 完成界面的渲染,createElm 核心源码如下:

js
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
  var data = vnode.data;
  var children = vnode.children;
  var tag = vnode.tag;
  if (isDef(tag)) {
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
    : nodeOps.createElement(tag, vnode);
    setScope(vnode);
    
    {
      createChildren(vnode, children, insertedVnodeQueue);
      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue);
      }
      insert(parentElm, vnode.elm, refElm);
    }
  }
}

其中关键方法是:

js
insert(parentElm, vnode.elm, refElm);

该方法是 Vue 开发 Web 应用时,实际进行 DOM 插入的方法,不过该方法在 mpvue 下并不会做任何操作,这是因为 mpvue 的界面渲染实际是由小程序来完成,大家可以理解为,小程序调用了 global.Page 方法会即完成了界面渲染。而 mpvue 实际做的,就是监听 data 的变化,并调用 setData 方法对界面进行更新,这一点,稍候就会验证到。在 corePatch 方法执行完毕后,mpvue 会继续执行:

js
this.$updateDataToMP();

this.$updateDataToMp 方法是实际完成界面渲染和更新的方法,其源码如下:

js
function updateDataToMP () {
  var page = getPage(this);
  if (!page) {
    return
  }

  var data = formatVmData(this);
  diffData(this, data);
  throttleSetData(page.setData.bind(page), data);
}

代码非常简洁也非常容易理解:

  • getPage 方法获取 page 实例;
  • formatVmData 方法初始化 page 实例下的 data 对象;
  • diffData 将 mpvue 实例下的 data 对象与 page 实例下的 data 对象进行对比,如果不一致则进行更新;
  • 最后通过 throttleSetData 方法调用 page.setData 完成对界面的渲染和更新,throttleSetData 方法主要优化了频繁调用 setData 的场景,限制了 setData 的调用间隔为 50ms,因为频繁调用 setData 会造成页面的卡顿,所以 mpvue 对此进行了优化。
js
var throttleSetData = throttle(function (handle, data) {
  handle(data);
}, 50);

本站总访问量次,本站总访客数人次
Released under the MIT License.