此篇主要手寫 Vue2.0 源碼-渲染更新原理javascript
上一篇我們主要介紹了 Vue 初始渲染原理 完成了數據到視圖層的映射過程 可是當咱們改變數據的時候發現頁面並不會自動更新 咱們知道 Vue 的一個特性就是數據驅動 當數據改變的時候 咱們無需手動操做 dom 視圖會自動更新 回顧第一篇 響應式數據原理 此篇主要採用觀察者模式 定義 Watcher 和 Dep 完成依賴收集和派發更新 從而實現渲染更新html
適用人羣: 沒時間去看官方源碼或者看源碼看的比較懵而不想去看的同窗前端
提示:此篇難度稍大 是整個 Vue 源碼很是核心的內容 後續的計算屬性和自定義 watcher 以及$set $delete 等 Api 的實現 都須要理解此篇的思路 小編看源碼這塊也是看了有好幾遍才搞懂 但願你們克服困難一塊兒去實現一遍吧!vue
<script>
// Vue實例化
let vm = new Vue({
el: "#app",
data() {
return {
a: 123,
};
},
// render(h) {
// return h('div',{id:'a'},'hello')
// },
template: `<div id="a">hello {{a}}</div>`,
});
// 咱們在這裏模擬更新
setTimeout(() => {
vm.a = 456;
// 此方法是刷新視圖的核心
vm._update(vm._render());
}, 1000);
</script>
複製代碼
上段代碼 咱們在 setTimeout 裏面調用 vm._update(vm._render())來實現更新功能 由於從上一篇初始渲染的原理可知 此方法就是渲染的核心 可是咱們不可能每次數據變化都要求用戶本身去調用渲染方法更新視圖 咱們須要一個機制在數據變更的時候自動去更新java
// src/observer/watcher.js
// 全局變量id 每次new Watcher都會自增
let id = 0;
export default class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.vm = vm;
this.exprOrFn = exprOrFn;
this.cb = cb; //回調函數 好比在watcher更新以前能夠執行beforeUpdate方法
this.options = options; //額外的選項 true表明渲染watcher
this.id = id++; // watcher的惟一標識
// 若是表達式是一個函數
if (typeof exprOrFn === "function") {
this.getter = exprOrFn;
}
// 實例化就會默認調用get方法
this.get();
}
get() {
this.getter();
}
}
複製代碼
在 observer 文件夾下新建 watcher.js 表明和觀察者相關 這裏首先介紹 Vue 裏面使用到的觀察者模式 咱們能夠把 Watcher 當作觀察者 它須要訂閱數據的變更 當數據變更以後 通知它去執行某些方法 其實本質就是一個構造函數 初始化的時候會去執行 get 方法面試
// src/lifecycle.js
export function mountComponent(vm, el) {
// _update和._render方法都是掛載在Vue原型的方法 相似_init
// 引入watcher的概念 這裏註冊一個渲染watcher 執行vm._update(vm._render())方法渲染視圖
let updateComponent = () => {
console.log("刷新頁面");
vm._update(vm._render());
};
new Watcher(vm, updateComponent, null, true);
}
複製代碼
咱們在組件掛載方法裏面 定義一個渲染 Watcher 主要功能就是執行核心渲染頁面的方法算法
// src/observer/dep.js
// dep和watcher是多對多的關係
// 每一個屬性都有本身的dep
let id = 0; //dep實例的惟一標識
export default class Dep {
constructor() {
this.id = id++;
this.subs = []; // 這個是存放watcher的容器
}
}
// 默認Dep.target爲null
Dep.target = null;
複製代碼
Dep 也是一個構造函數 能夠把他理解爲觀察者模式裏面的被觀察者 在 subs 裏面收集 watcher 當數據變更的時候通知自身 subs 全部的 watcher 更新vue-router
Dep.target 是一個全局 Watcher 指向 初始狀態是 nullvuex
// src/observer/index.js
// Object.defineProperty數據劫持核心 兼容性在ie9以及以上
function defineReactive(data, key, value) {
observe(value);
let dep = new Dep(); // 爲每一個屬性實例化一個Dep
Object.defineProperty(data, key, {
get() {
// 頁面取值的時候 能夠把watcher收集到dep裏面--依賴收集
if (Dep.target) {
// 若是有watcher dep就會保存watcher 同時watcher也會保存dep
dep.depend();
}
return value;
},
set(newValue) {
if (newValue === value) return;
// 若是賦值的新值也是一個對象 須要觀測
observe(newValue);
value = newValue;
dep.notify(); // 通知渲染watcher去更新--派發更新
},
});
}
複製代碼
上訴代碼就是依賴收集和派發更新的核心 其實就是在數據被訪問的時候 把咱們定義好的渲染 Watcher 放到 dep 的 subs 數組裏面 同時把 dep 實例對象也放到渲染 Watcher 裏面去 數據更新時就能夠通知 dep 的 subs 存儲的 watcher 更新api
// src/observer/watcher.js
import { pushTarget, popTarget } from "./dep";
// 全局變量id 每次new Watcher都會自增
let id = 0;
export default class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.vm = vm;
this.exprOrFn = exprOrFn;
this.cb = cb; //回調函數 好比在watcher更新以前能夠執行beforeUpdate方法
this.options = options; //額外的選項 true表明渲染watcher
this.id = id++; // watcher的惟一標識
this.deps = []; //存放dep的容器
this.depsId = new Set(); //用來去重dep
// 若是表達式是一個函數
if (typeof exprOrFn === "function") {
this.getter = exprOrFn;
}
// 實例化就會默認調用get方法
this.get();
}
get() {
pushTarget(this); // 在調用方法以前先把當前watcher實例推到全局Dep.target上
this.getter(); //若是watcher是渲染watcher 那麼就至關於執行 vm._update(vm._render()) 這個方法在render函數執行的時候會取值 從而實現依賴收集
popTarget(); // 在調用方法以後把當前watcher實例從全局Dep.target移除
}
// 把dep放到deps裏面 同時保證同一個dep只被保存到watcher一次 一樣的 同一個watcher也只會保存在dep一次
addDep(dep) {
let id = dep.id;
if (!this.depsId.has(id)) {
this.depsId.add(id);
this.deps.push(dep);
// 直接調用dep的addSub方法 把本身--watcher實例添加到dep的subs容器裏面
dep.addSub(this);
}
}
// 這裏簡單的就執行如下get方法 以後涉及到計算屬性就不同了
update() {
this.get();
}
}
複製代碼
watcher 在調用 getter 方法先後分別把自身賦值給 Dep.target 方便進行依賴收集 update 方法用來更新
// src/observer/dep.js
// dep和watcher是多對多的關係
// 每一個屬性都有本身的dep
let id = 0; //dep實例的惟一標識
export default class Dep {
constructor() {
this.id = id++;
this.subs = []; // 這個是存放watcher的容器
}
depend() {
// 若是當前存在watcher
if (Dep.target) {
Dep.target.addDep(this); // 把自身-dep實例存放在watcher裏面
}
}
notify() {
// 依次執行subs裏面的watcher更新方法
this.subs.forEach((watcher) => watcher.update());
}
addSub(watcher) {
// 把watcher加入到自身的subs容器
this.subs.push(watcher);
}
}
// 默認Dep.target爲null
Dep.target = null;
// 棧結構用來存watcher
const targetStack = [];
export function pushTarget(watcher) {
targetStack.push(watcher);
Dep.target = watcher; // Dep.target指向當前watcher
}
export function popTarget() {
targetStack.pop(); // 當前watcher出棧 拿到上一個watcher
Dep.target = targetStack[targetStack.length - 1];
}
複製代碼
定義相關的方法把收集依賴的同時把自身也放到 watcher 的 deps 容器裏面去
思考? 這時對象的更新已經能夠知足了 可是若是是數組 相似{a:[1,2,3]} a.push(4) 並不會觸發自動更新 由於咱們數組並無收集依賴
// src/observer/index.js
// Object.defineProperty數據劫持核心 兼容性在ie9以及以上
function defineReactive(data, key, value) {
let childOb = observe(value); // childOb就是Observer實例
let dep = new Dep(); // 爲每一個屬性實例化一個Dep
Object.defineProperty(data, key, {
get() {
// 頁面取值的時候 能夠把watcher收集到dep裏面--依賴收集
if (Dep.target) {
// 若是有watcher dep就會保存watcher 同時watcher也會保存dep
dep.depend();
if (childOb) {
// 這裏表示 屬性的值依然是一個對象 包含數組和對象 childOb指代的就是Observer實例對象 裏面的dep進行依賴收集
// 好比{a:[1,2,3]} 屬性a對應的值是一個數組 觀測數組的返回值就是對應數組的Observer實例對象
childOb.dep.depend();
if (Array.isArray(value)) {
// 若是數據結構相似 {a:[1,2,[3,4,[5,6]]]} 這種數組多層嵌套 數組包含數組的狀況 那麼咱們訪問a的時候 只是對第一層的數組進行了依賴收集 裏面的數組由於沒訪問到 因此五大收集依賴 可是若是咱們改變了a裏面的第二層數組的值 是須要更新頁面的 因此須要對數組遞歸進行依賴收集
if (Array.isArray(value)) {
// 若是內部仍是數組
dependArray(value); // 不停的進行依賴收集
}
}
}
}
return value;
},
set(newValue) {
if (newValue === value) return;
// 若是賦值的新值也是一個對象 須要觀測
observe(newValue);
value = newValue;
dep.notify(); // 通知渲染watcher去更新--派發更新
},
});
}
// 遞歸收集數組依賴
function dependArray(value) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i];
// e.__ob__表明e已經被響應式觀測了 可是沒有收集依賴 因此把他們收集到本身的Observer實例的dep裏面
e && e.__ob__ && e.__ob__.dep.depend();
if (Array.isArray(e)) {
// 若是數組裏面還有數組 就遞歸去收集依賴
dependArray(e);
}
}
}
複製代碼
若是對象屬性的值是一個數組 那麼執行 childOb.dep.depend()收集數組的依賴 若是數組裏面還包含數組 須要遞歸遍歷收集 由於只有訪問數據觸發了 get 纔會去收集依賴 一開始只是遞歸對數據進行響應式處理沒法收集依賴 這兩點須要分清
// src/observer/array.js
methodsToPatch.forEach((method) => {
arrayMethods[method] = function (...args) {
// 這裏保留原型方法的執行結果
const result = arrayProto[method].apply(this, args);
// 這句話是關鍵
// this表明的就是數據自己 好比數據是{a:[1,2,3]} 那麼咱們使用a.push(4) this就是a ob就是a.__ob__ 這個屬性表明的是該數據已經被響應式觀察過了 __ob__對象指的就是Observer實例
const ob = this.__ob__;
let inserted;
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
default:
break;
}
if (inserted) ob.observeArray(inserted); // 對新增的每一項進行觀測
ob.dep.notify(); //數組派發更新 ob指的就是數組對應的Observer實例 咱們在get的時候判斷若是屬性的值仍是對象那麼就在Observer實例的dep收集依賴 因此這裏是一一對應的 能夠直接更新
return result;
};
});
複製代碼
關鍵代碼就是 ob.dep.notify()
這裏放一張 整個 Vue 響應式原理的圖片 我們從數據劫持-->模板解析-->模板渲染-->數據變化視圖自動更新整個流程已經手寫了一遍 尤爲是此篇介紹的渲染更新相關的知識點 建議反覆理解原理以後本身動手實現一遍 由於Vue 不少核心原理和 api 都跟這裏的知識點相關哈
至此 Vue 的渲染更新原理已經完結 遇到不懂或者有爭議的地方歡迎評論留言
最後若是以爲本文有幫助 記得點贊三連哦 十分感謝!
歡迎你們技術交流 內推 摸魚 求助皆可 - 連接