vue 2 渲染过程 & 函数调用栈

Posted everlose

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue 2 渲染过程 & 函数调用栈相关的知识,希望对你有一定的参考价值。

测试例子

<!DOCTYPE html>
<html>
<head>
  <title>vue test</title>
</head>
<body>
<div id="app">
  <div v-for="i in message" :key="i">
    {{i}}
  </div>

  <!-- <button-counter :title="tt"></button-counter> -->
</div>

  <!-- Vue.js v2.6.11 -->
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script>
    Vue.component('button-counter', {
      props: ['title'],
      data: function () {
        return {
          count: 0
        }
      },
      template: '<button v-on:click="count++">{{title}}: You clicked me {{ count }} times.</button>'
    });
    var app = new Vue({
      el: '#app',
      data: {
        message: ['a', 'b', 'c', 'd'],
        tt: 'on'
      },
      mounted() {
        window.addEventListener('test', (e) => {
          this.message = e.detail;
        }, false);
      },
    })

    console.log(app);
    // var event = new CustomEvent('test', { 'detail': 5 }); window.dispatchEvent(event);
  </script>
</body>
</html>

主要函数定义

  • 716:Dep 发布者定义
  • 767:Vnode 虚拟节点定义
  • 922:Observer 劫持数据的函数定义
  • 4419:Watcher 订阅者定义
  • 5073:function Vue() 定义

数据劫持过程

Vue.prototype._init 中,在 callHook(vm, ‘beforeCreate‘); 后和 callHook(vm, ‘created‘); 之前调用 initState(vm) 进入劫持逻辑

技术图片

最后 Object.defineProperty 的代码详细看一下

Object.defineProperty(obj, key, {

  enumerable: true,
  configurable: true,
  get: function reactiveGetter() {
    var value = getter ? getter.call(obj) : val;
    if (Dep.target) {
      dep.depend();
      if (childOb) {
        childOb.dep.depend();
        if (Array.isArray(value)) {
          dependArray(value);
        }
      }
    }
    return value
  },
  set: function reactiveSetter(newVal) {
    var value = getter ? getter.call(obj) : val;
    /* eslint-disable no-self-compare */
    if (newVal === value || (newVal !== newVal && value !== value)) {
      return
    }
    /* eslint-enable no-self-compare */
    if (customSetter) {
      customSetter();
    }
    // #7981: for accessor properties without setter
    if (getter && !setter) { return }
    if (setter) {
      setter.call(obj, newVal);
    } else {
      val = newVal;
    }
    childOb = !shallow && observe(newVal);
    dep.notify();
  }
});

挂载过程

Vue.prototype._init 中,在 callHook(vm, ‘created‘); 后做 vm.$mount(vm.$options.el); 的逻辑

技术图片

挂载的过程中解析模版,并对模版进行 parse,optmize,generate 三步动作,编译出来的东西是一个这样的结构

{
    ast: {
        type: 1
        tag: "div"
        attrsList: [{…}]
        attrsMap: {id: "app"}
        rawAttrsMap: {id: {…}}
        parent: undefined
        children: (3) [{…}, {…}, {…}]
        start: 0
        end: 126
        plain: false
        attrs: [{…}]
        static: false
        staticRoot: false
    },
    render: "with(this){return _c('div',{attrs:{"id":"app"}},[(message + 1 > 1)?_c('div',[_v(_s(message + 1))]):_e(),_v(" "),_c('button',{on:{"click":function($event){message += 1}}},[_v("阿道夫")])])}",
    staticRenderFns: []
}

// 所以渲染函数 vm.$options.render 就是下面着样子的

(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[(message + 1 > 1)?_c('div',[_v(_s(message + 1))]):_e(),_v(" "),_c('button',{on:{"click":function($event){message += 1}}},[_v("阿道夫")])])}
})

最终在 mountComponent 函数里完成挂载的动作,这里 callHook(vm, ‘beforeMount‘);

技术图片

function mountComponent(
  vm,
  el,
  hydrating // 初始化时这个值是undefined
) {
  vm.$el = el;
  //...
  callHook(vm, 'beforeMount');

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

// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
// 对该vm注册一个订阅者,Watcher 的 getter 为 updateComponent 函数,进行依赖搜集。
// Watcher 存在于每一个组件 vm 中
new Watcher(vm, updateComponent, noop, {
  before: function before() {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate');
    }
  }
}, true /* isRenderWatcher */);
hydrating = false;

// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
  vm._isMounted = true;
  callHook(vm, 'mounted');
}
return vm;

注意上面代码建立 new Watcher() 订阅者,其内容就是触发 vm._update(vm._render(), hydrating);。new Watcher 时,自身调用 get,就彻底渲染,真实的节点也挂载到了html上。

update 过程

上文中在生命周期钩子 beforeMount 之后,建立了订阅者 new Watcher,执行函数 vm._update(vm._render(), hydrating);

首先执行 _render 去获取到最新的 Vnode 虚拟节点

技术图片

再去 _update 中调用 __patch__ 比对节点并且渲染到真实的 DOM 树中。

技术图片

Vnode 比对过程

初次渲染时

Vue.prototype._update = function (vnode, hydrating) {
  var vm = this;
  var prevVnode = vm._vnode;
  vm._vnode = vnode;
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  // 初次渲染走这里,直接 createElm 后再 removeVnodes,创建节点后删除原来的节点完事。
  if (!prevVnode) {
    // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
  } else {
    // 后续更新走这个逻辑,去深搜比对节点并更新
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode);
  }
  // ...
};

初始化时,就直接覆盖原节点

技术图片

技术图片

如果是update 过程

<div id="app">
  <!-- <div v-if="message > 0">{{ message + 1 }}</div> -->
  <div v-for="i in message">
    {{i}}
  </div>
</div>

<script>
  var app = new Vue({
    el: '#app',
    data: {
      message: ['a', 'b', 'c', 'd']
    },
    mounted() {
      window.addEventListener('test', (e) => {
        this.message = e.detail;
      }, false);
    }
  })
  
  // 接着控制台里输入
  // var event = new CustomEvent('test', { 'detail': ['a', 'c', 'e', 'f', 'b', 'd'] }); window.dispatchEvent(event);
  // 能把 message 改为这个数组
</script>

技术图片

探讨key的作用,首先这是 sameVnode 函数,用于比对两个节点是否是同一个

function sameVnode(a, b) {
  // key,tag,isComment相同,并且data都不为空,并且节点类型不是input
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

技术图片

子组件渲染过程

若是子元素自身属性变了,那么直接调用子元素自身订阅者的更新函数 vm._update(vm._render(), hydrating);

若是父组件变动了的子组件的 props 属性,子 props上也存在发布者

_props:
    title: (...)
    get title: ? reactiveGetter()
    set title: function reactiveSetter (newVal) {
        var value = getter ? getter.call(obj) : val;
        /* eslint-disable no-self-compare */
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        /* eslint-enable no-self-compare */
        if (customSetter) {
          customSetter();
        }
        // #7981: for accessor properties without setter
        if (getter && !setter) { return }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        childOb = !shallow && observe(newVal);
        dep.notify();
    }
    __proto__: Object

技术图片

渲染过程

追问:Dep.target 为什么会指向这个 Watcher 对象?

在 callHook(vm, ‘beforeMount‘) 后,进入 mount 阶段,此时初始化 Watcher


function noop (a, b, c) {}

// lifecycle.js
let updateComponent
updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

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

在初始化 Watcher 的函数里调用 this.get

var Watcher = function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
  this.vm = vm;
  //...
  this.cb = cb;
  //...
  this.expression = expOrFn.toString();
  //...
  this.getter = expOrFn;
  //...
  this.value = this.lazy ? undefined : this.get();
};

Watcher.prototype.get,注意 pushTarget,此时就和 Dep 发布者产生了联系,Dep 的 target 被设置为了这个 wacher,并且在每次监测对象被 get 时,就会往自身的 Dep 里推入这个 wacher。

// dep.js
export function pushTarget (_target: Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}
export function popTarget () {
  Dep.target = targetStack.pop()
}

// watcher.js
Watcher.prototype.get = function get() {
  pushTarget(this);
  var value;
  var vm = this.vm;
  //...
  value = this.getter.call(vm, vm);
  //...
  popTarget();
  this.cleanupDeps();
  //...
  return value;
};

上文 Watcher.prototype.get 中还要注意 this.getter.call(vm, vm), 执行的其实是上文表达式里的 vm._update(vm._render(), hydrating)。自然也就调用了

调用到了 vm._render() 方法,要返回一个VNode,调试发现 vm.$options.render 其实就是

Vue.prototype._render = function () {
  // ...
  var vm = this;
  var ref = vm.$options;
  var render = ref.render;
  vnode = render.call(vm._renderProxy, vm.$createElement);
  // ...
  return vnode
}

// 而render方法其实就是用于输出一个虚拟节点
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[(message + 1 > 1)?_c('div',[_v(_s(message + 1))]):_e(),_v(" "),_c('button',{on:{"click":function($event){message += 1}}},[_v("阿道夫")])])}
})

然后结果交给 vm._update

Vue.prototype._update = function(vnode, hydrating) {
  var vm = this;
  var prevEl = vm.$el;
  var prevVnode = vm._vnode;
  // ...
  vm._vnode = vnode;
  
  // ...
  vm.$el = vm.__patch__(prevVnode, vnode);
  
  
  // ...
};

结论是 mount 阶段 初始化 Watcher,然后在 wathcer初始化后调用 get,get里 pushTarget(this),并且执行自身的getter也就是表达式,表达式的内容就是 vm._update(vm._render(), hydrating) 故而就开始执行 render函数,render 函数就是就是输出虚拟节点的。

以上是关于vue 2 渲染过程 & 函数调用栈的主要内容,如果未能解决你的问题,请参考以下文章

vue渲染过程解析-VDOM &DOM

如何使用Vue2做服务端渲染

Part3-2-1 Vue.js 源码剖析-首次渲染过程

vue.js 2.0 啥时候发布的

记录-Vue.js模板编译过程揭秘:从模板字符串到渲染函数

vue2.0框架认识