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 函數就是就是輸出虛擬節點的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM