跟我一起讀源碼丨Vue源碼之依賴收集

閱讀源碼,個人覺得更多的收獲是你從源碼中提煉到了什么知識點,Vue的很多核心源碼都十分精妙竿痰,讓我們一起來關(guān)注它「依賴收集」的實現(xiàn)。

tip:Vue版本:v2.6.12砌溺,瀏覽器:谷歌影涉,閱讀方式:在靜態(tài)html 引用 Vue 包
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script> 進(jìn)行斷點閱讀

文章篇幅有點長,泡杯咖啡规伐,慢慢看 ~

我從「依賴收集」中學(xué)習(xí)到了什么蟹倾?

1. 觀察者模式

觀察者模式的基本概念:

觀察目標(biāo)發(fā)生變化 -> notify[通知] -> 觀察者們 -> update[更新]

下面這段代碼是 Vue 源碼中經(jīng)過運算的結(jié)果,可以讓小伙伴們的腦袋瓜先有個簡單的結(jié)構(gòu):

名詞解釋:
dep:depend[依賴]楷力,這里的“依賴”喊式,我們可以理解成 “觀察目標(biāo)” 。
subs:subscribers[訂閱者]萧朝,這里的“訂閱者”等價“觀察者”岔留。

// 基礎(chǔ)數(shù)據(jù)
data: {
  a: 1, // 關(guān)聯(lián) dep:id=0 的對象,a如果發(fā)生變化检柬,this.a=3献联,調(diào)用 notify,
  b: 2, // 關(guān)聯(lián) dep:id=1 的對象...
  // ...
}

dep = {
  id: 0,
  // 通知觀察者們
  notify() {
    this.subs.forEach(item => {
      item.update();
    });
  },
  // 觀察者們
  subs: [
    {
      id: 1,
      update() {
        // 被目標(biāo)者通知何址,做點什么事
      }
    },
    {
      id: 2,
      update() {
        // 被目標(biāo)者通知里逆,做點什么事
      }
    }
  ]

};

dep = {
  id: 1,
  //...

2. defineProperty 對一級/多級對象進(jìn)行攔截

對于一級對象的攔截相信小伙伴們都會啦。
這里闡述一下對于多級對象設(shè)置攔截器的封裝用爪,看下這段代碼:

const obj = { message: { str1: 'hello1', str2: 'hello2' } };
function observer(obj) {
  if (!(obj !== null && typeof obj === 'object')) {
    return;
  }
  walk(obj);
}
function walk(obj) {
  let keys = Object.keys(obj);
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i]);
  }
}
function defineReactive(obj, key) {
  let val = obj[key];
  observer(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      console.log('get :>> ', key, val);
      return val;
    },
    set(newVal) {
      console.log('set :>> ', key, newVal);
      observer(newVal);
      val = newVal;
    }
  });
}
observer(obj);

解釋:observer 這個方法表示如果當(dāng)前是一個對象原押,就會繼續(xù)被遍歷封裝攔截。

我們對 obj 進(jìn)行操作偎血,看控制臺的輸出:

obj.message
// get :>>  message { str1: "hello1", str2: "hello2"}

/* 這個例子說明了:不管是在 get/set str1诸衔,都會先觸發(fā) message 的 get*/
obj.message.str1
// get :>>  message { str1: "hello1", str2: "hello2" }
// get :>>  str1 hello1
obj.message.str1="123"
// get :>>  message { str1: "123", str2: "hello2" }
// set :>>  str1 123

// 重點:
obj.message={test: "test"}
// set :>>  message { test: "test" }
obj.message.test='test2'
// get :>>  message { test: "test2" }
// set :>>  test test2
/* 
有些小伙伴可能會有疑惑,這里進(jìn)行 obj.message={test: "test"} 賦值一個新對象的話颇玷,
不就無法檢測到屬性的變化笨农,為什么執(zhí)行 obj.message.test='test2' 還會觸發(fā)到 set 呢?
返回到上面帖渠,在 defineReactive 方法攔截器 set 中谒亦,我們做了這樣一件事:
set(newVal) {
  // 這里調(diào)用 observer 方法重新遍歷,如果當(dāng)前是一個對象空郊,就會繼續(xù)被遍歷封裝攔截
  observer(newVal)
  // ...
}
*/

延伸到實際業(yè)務(wù)場景:「獲取用戶信息然后進(jìn)行展示」份招。我在 data 設(shè)置了一個 userInfo: {},ajax 獲取到結(jié)果進(jìn)行賦值 this.userInfo = { id: 1, name: 'refined' }渣淳,就可以顯示到模板 {{ userInfo.name }}脾还,之后再進(jìn)行 this.userInfo.name = "xxx",也會進(jìn)行響應(yīng)式渲染了入愧。

3. defineProperty 對數(shù)組的攔截丨Object.create 原型式繼承丨原型鏈丨AOP

我們都知道 defineProperty 只能攔截對象鄙漏,對于數(shù)組的攔截 Vue 有巧妙的擴(kuò)展:

var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
var methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
];
methodsToPatch.forEach(function (method) {
  var original = arrayProto[method];
  Object.defineProperty(arrayMethods, method, {
    enumerable: true,
    configurable: true,
    value: function mutator(...args) {
      console.log('set and do something...');
      var result = original.apply(this, args);
      return result;
    }
  });
});
function protoAugment(target, src) {
  target.__proto__ = src;
}
var arr = [1, 2, 3];
protoAugment(arr, arrayMethods);

arr.push(4)
// set and do something...

解釋:Object.create(arrayProto); 為原型式繼承,即 arrayMethods.__proto__ === Array.prototype === true 棺蛛,所以現(xiàn)在的 arrayMethods 就可以用數(shù)組的所有方法怔蚌。

代碼中的 target.__proto__ = src,即 arr.__proto__ = arrayMethods旁赊,我們已經(jīng)對 arrayMethods 自己定義了幾個方法了桦踊,如 push。

現(xiàn)在我們進(jìn)行 arr.push终畅,就可以調(diào)用到 arrayMethods 自定義的 push 了籍胯,內(nèi)部還是有調(diào)用了 Array.prototype.push 原生方法竟闪。這樣我們就完成了一個攔截,就可以檢測到數(shù)組內(nèi)容的修改杖狼。

原型鏈機(jī)制:Array.prototype 本身是有 push 方法的炼蛤,但原型鏈的機(jī)制就是,arr 通過 __proto__ 找到了 arrayMethods.push蝶涩,已經(jīng)找到了理朋,就不會往下進(jìn)行找了。

可以注意到绿聘,封裝的這幾個方法 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'嗽上,都是涉及到數(shù)組內(nèi)容會被改變的,那如果我要調(diào)用 arr.map 方法呢?還是剛剛講的 原型鏈 機(jī)制熄攘,arrayMethods 沒有 map 方法兽愤,就繼續(xù)順著 __proto__ 往下找,然后找到 Array.prototype.map挪圾。

不得不說烹看,這個數(shù)組的擴(kuò)展封裝,可以學(xué)習(xí)到很多洛史,贊贊贊 ~

上面講的例子都是對一個數(shù)組內(nèi)容的改變惯殊。細(xì)節(jié)的小伙伴會發(fā)現(xiàn),如果我對整個數(shù)組進(jìn)行賦值呢也殖,如:arr = [4,5,6]土思,攔截不到吧,是的忆嗜。其實我只是把這個例子和上面第二點的例子拆分出來了己儒。我們只需要對上面 observer 方法,進(jìn)行這樣一個判斷捆毫,即

function observer(value) {
  if (!(value !== null && typeof value === 'object')) {
    return;
  }
  if (Array.isArray(value)) {
    protoAugment(value, arrayMethods);
  } else {
    walk(value);
  }
}

多級對象和數(shù)組的攔截概念其實很像闪湾,只是對象只需要逐級遍歷封裝攔截器,而數(shù)組需要用AOP的思想來封裝绩卤。

4. 微任務(wù)(microtask)的妙用丨event loop

直接來一手例子:

var waiting = false;
function queue(val) {
  console.log(val);
  nextTick();
}
function nextTick() {
  if (!waiting) {
    waiting = true;
    Promise.resolve().then(() => {
      console.log('The queue is over途样, do something...');
    });
  }
}

queue(1);
queue(2);
queue(3);

// 1
// 2
// 3
// The queue is over, do something...

解釋:主程序方法執(zhí)行完畢之后濒憋,才會執(zhí)行 promise 微任務(wù)何暇。這也可以解釋,為什么 Vue 更新動作是異步的【即:我們沒辦法立即操作 dom 】凛驮,因為這樣做可以提高渲染性能裆站,后面會具體講這塊。

5. 閉包的妙用

這里也直接來一手例子,個人認(rèn)為這個閉包用法是成就了依賴收集的關(guān)鍵 ~

var id = 0;
var Dep = function () {
  this.id = id++;
};
Dep.prototype.notify = function notify() {
  console.log('id :>> ', this.id, '宏胯,通知依賴我的觀察者們');
};
function defineReactive(obj, key) {
  var dep = new Dep();
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {},
    set() {
      dep.notify();
    }
  });
}
var obj = { str1: 'hello1', str2: 'hello2' };
defineReactive(obj, 'str1');
defineReactive(obj, 'str2');
obj.str1 = 'hello1-change';
obj.str2 = 'hello2-change';

// id :>>  0 羽嫡,通知依賴我的觀察者們
// id :>>  1 ,通知依賴我的觀察者們

這也是第一點講到的關(guān)聯(lián) dep 對象肩袍,現(xiàn)在每個屬性都可以訪問到詞法作用域的屬于自己的 dep 對象厂僧,這就是閉包。

6. with 改變作用域

這里只是模擬一下 Vue 的渲染函數(shù)

function render() {
  with (this) {
    return `<div>${message}</div>`;
  }
}
var data = { message: 'hello~' };
render.call(data);
// <div>hello~</div>

這就是我們平時在 <template> 中不用寫 {{ this.message }} 的原因了牛,而是如:

<template>
  <div> {{ message }} </<div>
</template>

上面這 6 點是個人覺得有學(xué)習(xí)到東西的地方,當(dāng)然要深入理解依賴收集辰妙,我們需要走一遍流程鹰祸。如果你當(dāng)前在電腦前,我會告訴你需要打第幾行的斷點密浑,讓我們一起讀源碼吧蛙婴,go go go ~

深入源碼

tip:為了閱讀質(zhì)量,我會把一些相對與流程無關(guān)的代碼省略掉尔破,代碼中類似「? :123」街图,表示需要打的斷點,谷歌瀏覽器上開啟調(diào)試 ctrl + o 懒构,輸入 :123 即可跳轉(zhuǎn)至 123 行餐济。

本地新建 index.html,引入 Vue 包胆剧,打開瀏覽器瀏覽

<body>
  <div id="app">
    <div>{{message }}</div>
    <button @click="handleClick">change</button>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
  <script>
    var app = new Vue({
      el: '#app',
      data: {
        message: 'hello world'
      },
      methods: {
        handleClick() {
          this.message = 'hello world 2';
        }
      }
    });
  </script>
</body>

斷點 ? :4700 initData方法

顧名思義絮姆,初始化我們寫的 data 數(shù)據(jù)并做一些操作,在這個方法里有兩個方法值得我們關(guān)注秩霍,proxy(vm, "_data", key);observe(data, true);篙悯。

function initData (vm) {
  var data = vm.$options.data;
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {};
  var keys = Object.keys(data);
  var i = keys.length;
  while (i--) {
    var key = keys[i];
? :4734  proxy(vm, "_data", key);
  }
? :4738  observe(data, true);
}

tip:在遇到方法的時候,我們用步入的方式可以快速定位到方法铃绒,如圖:

步入到 proxy 方法

function proxy (target, sourceKey, key) { 
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  };
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
? :4633  }

分析:這個方法是在 while 里的鸽照,這里循環(huán)遍歷了我們寫在 data 上的對象。當(dāng)前 target = vm 颠悬,key = message矮燎,走到這個 4633 斷點,控制臺打印 target赔癌,如圖:


上面我們提到了一個 with 的例子:Vue會進(jìn)行 render.call(vm)漏峰。這樣子我們就會觸發(fā)到 message 的 get 方法,這是一個入口届榄,后續(xù)會做一系列的操作浅乔。

步入到 observe 方法

function observe (value, asRootData) {
  if (!isObject(value)) {
    return
  }
  var ob;
? :4633  ob = new Observer(value);
}

分析:可以理解成這個方法開始正在對 data 上的 可觀察數(shù)據(jù) 進(jìn)行觀察的一些提前準(zhǔn)備,如:往屬性上附加 get/set 攔截器,然后分別在 get/set 里做點什么...

步入到 new Observer [可觀測類]

這是第一個核心類靖苇,接下來我們還會分別講到其他的兩個類席噩,每個類都是核心

var Observer = function Observer (value) {
  this.dep = new Dep();
  if (Array.isArray(value)) {
    protoAugment(value, arrayMethods);
  } else {
? :935  this.walk(value);
  }
};

分析:這里有個數(shù)組 or 對象的判斷,對于數(shù)組攔截我們在上面已經(jīng)有講過了贤壁,我們現(xiàn)在關(guān)注 walk 方法悼枢。

步入到 walk 方法

Observer.prototype.walk = function walk (obj) {
  var keys = Object.keys(obj);
  for (var i = 0; i < keys.length; i++) {
? :947  defineReactive$$1(obj, keys[i]);
  }
};  

繼續(xù)步入到 defineReactive$$1 方法

function defineReactive$$1 (
  obj, // obj -> data
  key, // key -> 'message'
  val
) {
? :1021  var dep = new Dep();
 
  val = obj[key];
 
  var childOb = observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = val;
? :1041  if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      var value = val;
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      val = newVal;
      childOb = observe(newVal);
? :1070 dep.notify();
    }
  });
}

分析:這個方法可以說是依賴收集中的核心,通過 get 方法添加依賴脾拆,通過 set 方法通知觀察者馒索。我們上面講到的 proxy 方法,可以把它當(dāng)做第一層攔截器名船,當(dāng)我們觸發(fā)一級攔截器之后绰上,就會到二級攔截器 defineReactive$$1 里定義的 get/set 方法。

new Dep() [觀察目標(biāo)類] 這個是第二個核心類渠驼。

還記得我們在上面說過這個方法是一個 “閉包” 嗎蜈块?是的,在當(dāng)前方法內(nèi)部 Object.defineProperty(obj, key, { 以上的所有變量/方法迷扇,是各個屬性各自獨立擁有的百揭。

至此,我們對于 data 上屬性的 get/set 封裝 就講完了 蜓席。

如何對數(shù)據(jù)進(jìn)行依賴收集器一?

斷點 ? :4074

updateComponent = function () {
? :4067  vm._update(vm._render(), hydrating);
};

? :4074 new Watcher(vm, updateComponent);

分析:Watcher類,這個是第三個核心類厨内,觀察者類盹舞。和上面說的 Observer[可觀擦類]、Dep[觀察目標(biāo)類]隘庄,總共三個踢步。這個代碼片段是在 mounted 鉤子之前調(diào)用的,也就是我們之前對 data 數(shù)據(jù)先進(jìn)行了 get/set 封裝之后丑掺,就要開始進(jìn)行 render 了获印,在 render 之前,需要創(chuàng)建 render 觀察者街州,為了方便我們這里叫它 renderWatcher兼丰。除了 renderWatcher,我們還有 computedWatcherwatchWatcher唆缴,這兩個分別是 計算屬性偵聽器 觀察者鳍征,在 Vue 中主要是這三個類型的觀察者。

步入到 new Watcher [觀察者類]

var Watcher = function Watcher (
  vm,
  expOrFn
) {
  this.getter = expOrFn;
  this.deps = [];
  this.newDeps = [];
  this.depIds = new Set();
  this.newDepIds = new Set();
? :4467  this.get();
};

分析:

  • deps:緩存每次執(zhí)行觀察者函數(shù)時所用到的dep所有實例面徽。
  • depIds:緩存每次執(zhí)行觀察者函數(shù)時所用到的dep所有實例 id艳丛,用于判斷匣掸。
  • newDeps:存儲本次執(zhí)行觀察者函數(shù)時所用到的dep所有實例。
  • newDepIds:存儲本次執(zhí)行觀察者函數(shù)時所用到的dep所有實例 id氮双,用于判斷碰酝。

步入到 get 方法

Watcher.prototype.get = function get () {
? :4474  pushTarget(this);
  var vm = this.vm;
? :4478  this.getter.call(vm, vm);
? :4491  popTarget();
? :4492  this.cleanupDeps();
};

分析【這段分析比較詳細(xì)】:pushTarget 和 popTarget 是一對方法,分別用來記錄當(dāng)前的觀察者戴差,和剔除當(dāng)前觀察者

Dep.target = null;
var targetStack = [];
function pushTarget (target) {
  targetStack.push(target);
  Dep.target = target;
}
function popTarget () {
  targetStack.pop();
  Dep.target = targetStack[targetStack.length - 1];
}

Dep.target 為全局唯一的送爸,因為在一個時刻內(nèi),就只會有一個觀察者函數(shù)在執(zhí)行暖释,把當(dāng)前的 觀察者實例 賦值給 Dep.target袭厂, 后續(xù)只要訪問 Dep.target 就能知道當(dāng)前的觀察者是誰了。

我們繼續(xù)步入 this.getter.call(vm, vm)球匕,【以下這幾個步入我們就簡單過一下】

updateComponent = function () {
? :4067  vm._update(vm._render(), hydrating);
};

步入 vm._update(vm._render(), hydrating)

Vue.prototype._render = function () {
? :3551  vnode = render.call(vm._renderProxy, vm.$createElement);
};

步入 render.call(vm._renderProxy, vm.$createElement)纹磺,在谷歌會新打開一個 tab 用來執(zhí)行下面這個函數(shù)

(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('div',[_v(_s(message))]),_v(" "),_c('button',{on:{"click":handleClick}},[_v("change")])])}
})

關(guān)鍵部分來了,這個是 Vue 的渲染函數(shù)谐丢。我們現(xiàn)在只要關(guān)注,它這里是會讀取到 this.message 的蚓让,所以會觸發(fā) message 的 get 方法乾忱,也就是說當(dāng)前觀察者 renderWatcher 依賴了 message ,所以就會開始對它進(jìn)行 “收集”历极。

谷歌瀏覽器器窄瘟,直接點擊下一步「 ||> 」,

我們就可以看到光標(biāo)跳到了 defineReactive$$1 方法內(nèi)部我們的 get 方法趟卸,開始進(jìn)行“依賴收集” 了

    get: function reactiveGetter () {
      var value = val;
? :1041  if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      return value
    },

當(dāng)前的 Dep.target 是有值的蹄葱,所以執(zhí)行 dep.depend 開始進(jìn)行依賴,
步入 dep.depend

Dep.protJavaScriptotype.depend = function depend () {
  if (Dep.target) {
? :731  Dep.target.addDep(this);
  }  
};

步入 Dep.target.addDep(this)

Watcher.prototype.addDep = function addDep (dep) {
  var id = dep.id;
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id);
    this.newDeps.push(dep);
    if (!this.depIds.has(id)) {
      dep.addSub(this);
    }
  }
? :4059  };

dep.addSub(this) 把當(dāng)前的 watcher 實例 push 到 subs 數(shù)組锄列,并且判斷如果當(dāng)前 觀察者 被 觀察目標(biāo) 添加到 subs 數(shù)組里图云,就不會繼續(xù)添加,過濾重復(fù)數(shù)據(jù)邻邮。
走到這個 4059 斷點竣况,控制臺打印 dep,如:

dep = {
  id:3,
  subs:[
    renderWatcher 實例
  ]
}

跳出繼續(xù)往下走會調(diào)用 4491 popTarget() 筒严,剔除當(dāng)前 觀察者丹泉。

接著步入 this.cleanupDeps()

Watcher.prototype.cleanupDeps = function cleanupDeps () {
  var i = this.deps.length;
  while (i--) {
    var dep = this.deps[i];
    if (!this.newDepIds.has(dep.id)) {
      dep.removeSub(this);
    }
  }
  var tmp = this.depIds;
  this.depIds = this.newDepIds;
  this.newDepIds = tmp;
  this.newDepIds.clear();
  tmp = this.deps;
  this.deps = this.newDeps;
  this.newDeps = tmp;
  this.newDeps.length = 0;
};

這里把 this.deps = this.newDeps,緩存到 deps 里鸭蛙,然后清空newDeps摹恨,來做下一次的收集。

至此娶视,我們就完成了一個 依賴收集 ~

更新依賴數(shù)據(jù)如何 notify 觀察者做出 update 晒哄?

官網(wǎng):只要偵聽到數(shù)據(jù)變化,Vue 將開啟一個隊列,并緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)變更揩晴。如果同一個 watcher 被多次觸發(fā)勋陪,只會被推入到隊列中一次。這種在緩沖時去除重復(fù)數(shù)據(jù)對于避免不必要的計算和 DOM 操作是非常重要的硫兰。然后诅愚,在下一個的事件循環(huán)“tick”中,Vue 刷新隊列并執(zhí)行實際 (已去重的) 工作劫映。

當(dāng)用戶點擊change按鈕

this.message = 'hello world 2';

光標(biāo)自動跳轉(zhuǎn)至 message 對應(yīng)的 set 方法违孝,執(zhí)行 dep.notify() 進(jìn)行通知觀察者進(jìn)行 update 動作

Dep.prototype.notify = function notify () {
  for (var i = 0, l = subs.length; i < l; i++) {
? :745  subs[i].update();
  }
};

步入 subs[i].update()

Watcher.prototype.update = function update () {
? :4543  queueWatcher(this);
};

步入 queueWatcher()

function queueWatcher (watcher) {
  var id = watcher.id;
  if (has[id] == null) {
    has[id] = true;
    if (!flushing) {
      queue.push(watcher);
    } else {
      var i = queue.length - 1;
      while (i > index && queue[i].id > watcher.id) {
        i--;
      }
      queue.splice(i + 1, 0, watcher);
    }
    if (!waiting) {
      waiting = true;
? :4403  nextTick(flushSchedulerQueue);
    }
  }
}

flushSchedulerQueue 方法

function flushSchedulerQueue () {
  flushing = true;
  var watcher, id;
 
  queue.sort(function (a, b) { return a.id - b.id; });

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index];
    id = watcher.id;
    has[id] = null;
? :4311  watcher.run();
  }
}

分析[結(jié)合上面 queueWatcherflushSchedulerQueue 兩個方法]:

flushSchedulerQueue 方法:

queue.sort 需要排序是原因:

確保 watcher 的更新順序與它們被創(chuàng)建的順序一致。

  1. 對于父子組件來說泳赋,組件的創(chuàng)建順序是父組件先被創(chuàng)建雌桑,然后子組件再被創(chuàng)建,所以父組件的renderWatcher的id是小于子組件的祖今。
  2. 對于用戶自定義watcher【watchWatcher】和 renderWatcher校坑,用戶自定義watcher是先于組件的renderWatcher被創(chuàng)建的。
  3. 如果子組件在父組件的監(jiān)視程序運行期間被銷毀千诬,則會跳過子組件的watcher耍目。

queueWatcher 方法:

  1. 這里進(jìn)行了 watcher id 的重復(fù)判斷,因為在一個 renderWatch 中可能會依賴多個觀察目標(biāo)徐绑,當(dāng)我們同時改變多個依賴的值 邪驮,經(jīng)過判斷 watcher.id 一樣就不用把兩次更新 push 到 隊列,避免渲染性能消耗傲茄,如:
this.message1 = 'hello world 1';
this.message2 = 'hello world 2';
// 更多...      

或 循環(huán)改變同一個依賴

for (let i = 0; i < 10; i++) {
  this.message++;
}
  1. flushing 表示 queue 隊列的更新狀態(tài)毅访,flushing=true 代表隊列正在更新中。
    這里的 else 分支盘榨,主要是判斷一種邊界情況喻粹,
    i--,從后往前遍歷草巡,其實目的是看剛進(jìn)入的這個 watcher 在不在當(dāng)前更新隊列中磷斧。注意這里的 index 是來自 flushSchedulerQueue 方法內(nèi)部定義的,是全局的捷犹。
    我們可以看到跳出 while 的條件為:
  • queue[i].id === watcher.id

我們可以這樣理解弛饭,當(dāng)前在更新一個 id 為 3 的 watcher,然后又進(jìn)來了一個 watcher萍歉,id 也為3侣颂。相當(dāng)于需要重新更新一次 id 為 3 的 watcher,這樣才能獲取到最新值保證視圖渲染正確枪孩。用代碼解釋如:

// ...
<div>{{ message }}</div>
// ...

new Vue({
  el: '#app',
  data: {
    message: 'hello world'
  },
  watch: {
    message() {
      this.message = 'hello world 3';
    }
  },
  methods: {
    handleClick() {
      this.message = 'hello world 2';
    }
  }
});

點擊按鈕更新 message 之后憔晒,又用 watch 監(jiān)聽其變化藻肄,然后在內(nèi)部再對 message 進(jìn)行更新,我們試著讀一下這段代碼的更新流程拒担。首先嘹屯,用戶自定義watcher【watchWatcher】是先于 renderWatcher 被創(chuàng)建的,所以我們在更新 message 的時候从撼,會先執(zhí)行 watch 州弟,觸發(fā)到內(nèi)部方法又更新了一次 message,為了保證視圖渲染正確低零,我們需要在執(zhí)行一次這個 watcher 的 update婆翔。

  • queue[i].id < watcher.id
    分析:更新隊列中有 id 為1,2,5 三個 watcher,當(dāng)前正在更新id為 2 的watcher掏婶,當(dāng) queueWatcher 被調(diào)用并傳進(jìn)來一個 id 為 3 的watcher啃奴,于是就將這個 watcher 放到 2 的后面,確保 watcher 的更新順序與它們被創(chuàng)建的順序一致雄妥。

我們都知道最蕾,flushSchedulerQueue 方法是一個微任務(wù)。在對queue操作之后老厌,主程序方法執(zhí)行完畢之后瘟则,開始執(zhí)行微任務(wù),進(jìn)行 queue 的調(diào)度更新梅桩,watcher.run()

至此壹粟,我們就完成了當(dāng)觀察目標(biāo)改變時通知觀察者更新的動作拜隧。

總結(jié)

以上舉的例子是一個簡單 renderWatcher 的一個流程閉環(huán)宿百,依賴收集通知更新。Vue 有renderWatcher【視圖觀察者】洪添,computedWatcher【計算屬性觀察者】 和 watchWatcher【偵聽器觀察者】垦页,主要這三個類型的觀察者洛退。

主要的三個類 Dep【觀察目標(biāo)類】照藻,Observe【可觀測類】,Watcher【觀察者類】凭语。

我們可以理解忿峻,在依賴被改變的時候通知觀察者的一過程薄啥,一切都是為了視圖渲染,在這過程中會進(jìn)行一些性能優(yōu)化 / 處理一些邊界情況逛尚,最終保證視圖渲染的完整性垄惧。

個人覺得源碼有點晦澀難懂,但還是得自己多過幾遍才能熟悉绰寞。這邊還是建議親自閱讀幾遍源碼到逊,看一些他人的總結(jié)還是會有點模糊铣口,所以本篇文章提供了 斷點 參考。幫助小伙伴快速定位源碼比較精髓的位置觉壶。

了解源碼的運作脑题,也可以讓我們更加的知道,我們需要怎么去調(diào)用框架提供的 api 會更加優(yōu)化铜靶。

后話

Vue3 已經(jīng)出來了叔遂,我們看完 Vue2 就可以對比看看 Vue3 的更強(qiáng)大之處了,
這邊就不再舉例 computedWatcherwatchWatcher 了旷坦,小伙伴們可以動手 debug 看看 ~ 掏熬。

可以從頁面的 initState 方法作為入口:

function initState (vm) {
  vm._watchers = [];
  var opts = vm.$options;
  if (opts.props) { initProps(vm, opts.props); }
  if (opts.methods) { initMethods(vm, opts.methods); }
  if (opts.data) {
    initData(vm);
  } else {
    observe(vm._data = {}, true /* asRootData */);
  }
? :4645  if (opts.computed) { initComputed(vm, opts.computed); }
  if (opts.watch && opts.watch !== nativeWatch) {
? :4647  initWatch(vm, opts.watch);
  }
}

感興趣的小伙伴也可以 debug 看 computed 這種場景

computed: {
  c1() {
    return this.c2 + 'xxx';
  },
  c2() {
    return this.message + 'xxx';
  }
}

computed 是 “l(fā)azy” 的,它不參與 queue 的更新秒梅,而是如果在模板上有用到 computed 屬性旗芬,才會去進(jìn)行獲取計算后的值。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末捆蜀,一起剝皮案震驚了整個濱河市疮丛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌辆它,老刑警劉巖誊薄,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異锰茉,居然都是意外死亡呢蔫,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門飒筑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來片吊,“玉大人,你說我怎么就攤上這事协屡∏渭梗” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵肤晓,是天一觀的道長爷贫。 經(jīng)常有香客問我,道長补憾,這世上最難降的妖魔是什么漫萄? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮盈匾,結(jié)果婚禮上腾务,老公的妹妹穿的比我還像新娘。我一直安慰自己威酒,他們只是感情好窑睁,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布挺峡。 她就那樣靜靜地躺著,像睡著了一般担钮。 火紅的嫁衣襯著肌膚如雪橱赠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天箫津,我揣著相機(jī)與錄音狭姨,去河邊找鬼。 笑死苏遥,一個胖子當(dāng)著我的面吹牛饼拍,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播田炭,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼师抄,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了教硫?” 一聲冷哼從身側(cè)響起叨吮,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎瞬矩,沒想到半個月后茶鉴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡景用,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年涵叮,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片伞插。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡割粮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蜂怎,到底是詐尸還是另有隱情穆刻,我是刑警寧澤置尔,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布杠步,位于F島的核電站,受9級特大地震影響榜轿,放射性物質(zhì)發(fā)生泄漏幽歼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一谬盐、第九天 我趴在偏房一處隱蔽的房頂上張望甸私。 院中可真熱鬧,春花似錦飞傀、人聲如沸皇型。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽弃鸦。三九已至绞吁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間唬格,已是汗流浹背家破。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留购岗,地道東北人汰聋。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像喊积,于是被迫代替她去往敵國和親烹困。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容