首先這篇文章是讀 vue.js
源代碼的梳理性文章,文章分塊梳理,記錄着本身的一些理解及大體過程;更重要的一點是但願在 vue.js 3.0
發佈前深刻的瞭解其原理。javascript
若是你從未看過或者接觸過 vue.js
源代碼,建議你參考如下列出的 vue.js
解析的相關文章,由於這些文章更細緻的講解了這個工程,本文只是以一些 demo
演示某一功能點或 API
實現,力求簡要梳理過程。html
若是搞清楚了工程目錄及入口,建議直接去看代碼,這樣比較高效 ( 遇到難以理解對應着回來看看別人的講解,加以理解便可 )vue
文章所涉及到的代碼,基本都是縮減版,具體還請參閱 vue.js - 2.5.17。java
data
選項的預處理在上文 「試着讀讀 Vue 源代碼」new Vue()發生了什麼 ❓, 着重梳理了 new Vue(()
其代碼執行的全過程,瞭解了 Vue
內部到底作了哪些工做,但就響應式系統的構建
並無展開描述,Vue
在哪裏開始對Data
選項進行響應式體系構建呢?沒錯,就是上文簡單提過的 _init()
內部執行的 initState
函數。react
注:這裏在對
data
選項初始化時,首先若存在data
選項,則調用initData
方法進行對data
預處理,最終調用observe(data, true /* asRootData */)
函數將data
數據對象轉換成響應式的;若不存在,簡單初始化爲空對象處理便可。git
export function initState(vm: Component) {
vm._watchers = [];
const opts = vm.$options;
if (opts.props) initProps(vm, opts.props);
if (opts.methods) initMethods(vm, opts.methods);
/****** 初始化 data 選項 ******/
if (opts.data) {
initData(vm);
} else {
observe((vm._data = {}), true /* asRootData */);
}
/****** 初始化 data 選項 ******/
if (opts.computed) initComputed(vm, opts.computed);
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
複製代碼
initData
代碼實現function initData(vm: Component) {
/************************** data 提取並預處理 ***************************/
// 說明: 1. 根據上文,data 選項最終將被合併成一個函數,該函數返回 data 的值。
let data = vm.$options.data;
data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {};
// 檢驗 data 選項是不是一個純對象(注:在對data 選項合併處理以後走了一次 beforeCreate 鉤子函數,防止 data 在那裏被修改)
if (!isPlainObject(data)) {
data = {};
process.env.NODE_ENV !== 'production' &&
warn('數據函數應該返回一個對象', vm);
}
const keys = Object.keys(data);
const props = vm.$options.props;
const methods = vm.$options.methods;
let i = keys.length;
// 爲了不選項屬性直接的覆蓋,將迭代 data 選項
while (i--) {
const key = keys[i];
// 在非生產環境下 methods 存在:若是 methods 選項中的 key 在 data 中被定義將被警告
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(`方法「${key}」已經被定義爲一個data屬性。`, vm);
}
}
// 在非生產環境下 props 存在:若是 data 選項中的 key 在 props 中被定義了將被警告
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' &&
warn(`data的屬性「${key}」已經被聲明爲一個props的屬性。`, vm);
} else if (!isReserved(key)) {
// isReserved 做用: 檢查字符串是否以$或_開頭; 剔除這些特徵字段,避免與 Vue 自身的屬性和方法相沖突。
// 注: ① 若是你在data中定義了 `_message/$message` 你能夠試一下 `this._message / $message` 能不能訪問到?
// proxy 做用: data 數據代理, 使你可以:this.message 而不是 this.data.message;
// (`this.message <=> this.($data/_data/data).message`)。
proxy(vm, `_data`, key);
}
}
/************************** data 提取並預處理 ***************************/
observe(data, true /* asRootData */); // observe 函數將 data 數據對象轉換成響應式
}
複製代碼
proxy
代碼實現/** * 數據代理 * @param {Object} target 要在其上定義屬性的對象 * @param {string} sourceKey 資源屬性的名稱 * @param {string} key 要定義或修改的屬性的名稱 */
export function proxy(target: Object, sourceKey: string, key: string) {
// getter 函數
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key];
};
// setter 函數
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
複製代碼
data
選項的值。data
選項內鍵名是否和 methods / props 內定義鍵名衝突。data
選項內屬性作一層代理,且剔除特徵字符代理。observe
函數將 data
數據對象轉換成響應式。observe
觀察函數/** * 在某些狀況下,咱們可能但願禁用組件更新計算中的觀察。 */
export let shouldObserve: boolean = true;
export function toggleObserving(value: boolean) {
shouldObserve = value;
}
/** * 觀察函數 * @param {Any} value 觀測數據 * @param {Boolean} asRootData 被觀測的數據是不是根級數據 */
export function observe(value: any, asRootData: ?boolean): Observer | void {
// 值不是對象 或 值是虛擬DOM 直接退出
if (!isObject(value) || value instanceof VNode) {
return;
}
let ob: Observer | void;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() && // 判斷是不是服務端渲染
(Array.isArray(value) || isPlainObject(value)) && // 判斷是不是數組 或 純對象
Object.isExtensible(value) && // 判斷一個對象是不是可擴展的(是否能夠在它上面添加新的屬性)
!value._isVue // 避免 Vue 實例對象被觀測
) {
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++; // 根數據對象 target.__ob__.vmCount > 0
}
return ob;
}
複製代碼
data
選項的值進行類型判斷; 若合法,調用 Observer
類。ob.vmCount++
。Observer
實例。Observer
觀察者基類/** * 附加到每一個被觀察對象的觀察者類。 * 一旦附加,觀察者將目標對象的屬性鍵轉換爲 getter/setter,用於收集依賴項和分派更新。 */
export class Observer {
value: any; // 觀察對象
dep: Dep; // Dep 是一個可觀察的對象,能夠有多個指令訂閱它。
vmCount: number; // 將此屬性做爲根 $data 的 vm 數量
constructor(value: any) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
// 爲 value 添加 __ob__ 不可枚舉屬性, 值爲當前 `Observer` 實例
def(value, '__ob__', this);
// 後續的深度監測 data 數據下的二層級的數據多是數組、對象等...
if (Array.isArray(value)) {
// 攔截數組變異方法。
// 判斷是否可使用 __proto__ 選擇不一樣的執行方法。
const augment = hasProto ? protoAugment : copyAugment;
augment(value, arrayMethods, arrayKeys);
// 遞歸處理,解決嵌套數組。
this.observeArray(value);
} else {
this.walk(value);
}
}
/** * 遍歷每一個屬性並將它們轉換爲getter/setter。此方法只應在值類型爲Object時調用。 */
walk(obj: Object) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
/** * 觀察數組項的列表 - 使嵌套的數組或對象是響應式數據 */
observeArray(items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
}
}
複製代碼
攔截數組變異方法實現github
// 建立一個新對象,使用 現有的對象(Array.prototype) 來提供新建立的對象的__proto__
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
// 須要攔截的數組變異方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
/** * 攔截突變方法併發出事件 */
methodsToPatch.forEach(function(method) {
// 緩存數組原始變異方法
const original = arrayProto[method];
// 在 arrayMethods 上添加這些變異方法並作一些事情。
def(arrayMethods, method, function mutator(...args) {
// 調用原始變異方法,並緩存其結果。
const result = original.apply(this, args);
const ob = this.__ob__;
let inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
// 若存在將被插入的數組元素,將調用 observeArray 繼續進行處理
if (inserted) ob.observeArray(inserted);
ob.dep.notify(); // 觸發依賴
return result; // 將值結果返回
});
});
複製代碼
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
/** * 經過使用 _proto__ 攔截原型鏈來增長目標對象或數組 */
function protoAugment(target, src: Object, keys: any) {
target.__proto__ = src;
}
/** * 經過定義隱藏屬性來擴充目標對象或數組。 */
function copyAugment(target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i];
def(target, key, src[key]);
}
}
複製代碼
下面是對數據變異攔截後的斷點截圖:算法
defineReactive
/** * 在對象上定義反應性屬性。 */
export function defineReactive( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) {
const dep = new Dep(); // 訂閱池
/******************** 剔除不可配置屬性 *********************/
const property = Object.getOwnPropertyDescriptor(obj, key); // 返回指定對象上一個自有屬性對應的屬性描述符
if (property && property.configurable === false) {
return;
}
/******************** 知足預約義的 getter / setter *********************/
const getter = property && property.get;
const setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]; // 觸發取值函數 - 收集依賴
}
let childOb = !shallow && observe(val); // 默認深度觀測
/******************** 劫持屬性並配置 *********************/
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
/******************** 返回正確的屬性值並收集依賴 *********************/
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
// target 保存着要被收集的依賴(觀察者)
if (Dep.target) {
dep.depend(); // 收集依賴,丟到訂閱池
if (childOb) {
childOb.dep.depend(); // 若有深層對象 繼續收集依賴,丟到訂閱池
// 如果數組 - 進行數組處理
if (Array.isArray(value)) {
dependArray(value); // 逐個觸發數組每一個元素的依賴收集
}
}
}
return value;
},
/******************** 設置正確的屬性值並觸發依賴 *********************/
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val; // 緩存舊值
// NaN 或 值 相等不處理
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter(); // 用來打印輔助信息
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal); // 深度觀測
dep.notify(); // 觸發依賴
}
});
}
複製代碼
Dep
import type Watcher from './watcher';
import { remove } from '../util/index';
let uid = 0;
/** * Dep 是一個可觀察的對象,能夠有多個指令訂閱它。 */
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor() {
this.id = uid++;
this.subs = [];
}
addSub(sub: Watcher) {
this.subs.push(sub);
}
removeSub(sub: Watcher) {
remove(this.subs, sub);
}
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
notify() {
// 首先穩定訂閱者列表
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
}
// 正在評估的當前目標監視程序。這是全局唯一的,由於在任什麼時候候都只能評估一個監視程序。
Dep.target = null;
const targetStack = [];
export function pushTarget(_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target);
Dep.target = _target;
}
export function popTarget() {
Dep.target = targetStack.pop();
}
複製代碼
上述陳述了響應式系統構建的部份內容,知道如何爲屬性構建響應式屬性,即構造 getter/setter
;知道了在 getter
時收集依賴,在 setter
觸發依賴。 同時對 Dep
這個基類作了相應的分析。express
就之前文例子,斷點圖簡單展現了構建以後的結果:
Watcher
import {
warn,
remove,
isObject,
parsePath,
_Set as Set,
handleError
} from '../util/index';
import { traverse } from './traverse';
import { queueWatcher } from './scheduler';
import Dep, { pushTarget, popTarget } from './dep';
import type { SimpleSet } from '../util/index';
let uid = 0;
/** * 一個觀察者解析一個表達式,收集依賴關係,當表達式值改變時觸發回調。這用於$watch() api和指令。 * 經過對「被觀測目標」的求值,觸發數據屬性的 get 攔截器函數從而收集依賴 */
export default class Watcher {
vm: Component; // 組件實例
expression: string; // 被觀察的目標表達式
cb: Function; // 當被觀察的表達式的值變化時的回調函數
id: number; // 觀察者實例對象的惟一標識
deep: boolean; // 當前觀察者實例對象是不是深度觀測
user: boolean; // 標識當前觀察者實例對象是 開發者定義的 仍是 內部定義的
computed: boolean; // 標識當前觀察者實例對象是不是計算屬性的觀察者
sync: boolean; // 告訴觀察者當數據變化時是否同步求值並執行回調 默認 將須要從新求值並執行回調的觀察者放到一個異步隊列中,當全部數據的變化結束以後統一求值並執行回調
dirty: boolean; // for computed watchers, true 表明着尚未求值
active: boolean; // 觀察者是否處於激活狀態,或者可用狀態
dep: Dep;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet; // 用來在 屢次求值(當數據變化時從新求值的過程) 中避免收集重複依賴
newDepIds: SimpleSet; // 用來避免在 一次求值 的過程當中收集重複的依賴
before: ?Function; // Watcher 實例的鉤子
getter: Function;
value: any;
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object, // 當前觀察者對象的選項
isRenderWatcher?: boolean // isRenderWatcher 用來標識該觀察者實例是不是渲染函數的觀察者
) {
/****************** 初始化一些實例屬性 ******************/
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.computed = !!options.computed;
this.sync = !!options.sync;
this.before = options.before;
} else {
this.deep = this.user = this.computed = this.sync = false;
}
this.cb = cb;
this.id = ++uid;
this.active = true;
this.dirty = this.computed;
/****************** 初始化一些實例屬性 ******************/
/****************** 實現避免收集重複依賴 ******************/
this.deps = [];
this.newDeps = [];
this.depIds = new Set();
this.newDepIds = new Set();
/****************** 實現避免收集重複依賴 ******************/
/****************** 解析路徑 ******************/
this.expression =
process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '';
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = function() {};
process.env.NODE_ENV !== 'production' &&
warn(
`監視路徑失敗:「${exports}」監視程序只接受簡單的點分隔路徑。要實現徹底控制,可使用函數。`,
vm
);
}
}
/****************** 解析路徑 ******************/
/****************** 求值 - 計算屬性的觀察者 與 普通屬性觀察者 處理方式 ******************/
if (this.computed) {
this.value = undefined;
this.dep = new Dep();
} else {
this.value = this.get();
}
}
/** * 求值:觸發訪問器屬性的 get 攔截器函數,並從新收集依賴項。 */
get() {
pushTarget(this); // 給 Dep.target 賦值
let value;
const vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`);
} else {
throw e;
}
} finally {
// 「觸發」每個屬性,所以它們都做爲依賴項被跟蹤,以便進行深度監視
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value;
}
/** * 向該指令添加一個依賴項。 */
addDep(dep: Dep) {
const id = dep.id;
if (!this.newDepIds.has(id)) {
// 避免收集重複依賴
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this); // 移除已經沒有關聯關係的觀察者
}
}
}
/** * 清理依賴項集合. */
cleanupDeps() {
let i = this.deps.length;
while (i--) {
const dep = this.deps[i];
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this);
}
}
// 引用類型變量交換值的過程
let 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;
}
/** * 用戶界面。將在依賴項更改時調用。 */
update() {
if (this.computed) {
// 計算屬性監視程序有兩種模式: 延遲模式 和 激活模式。
// 默認狀況下,它初始化爲lazy,只有當至少有一個訂閱者(一般是另外一個計算屬性或組件的呈現函數)依賴於它時纔會被激活。
if (this.dep.subs.length === 0) {
// 在延遲模式下,除非必要,不然咱們不想執行計算,所以咱們只需將監視程序標記爲dirty。
// 實際計算是在訪問計算屬性時在this.evaluate()中即時執行的。
this.dirty = true;
} else {
// 在激活模式下,咱們但願主動執行計算,但只在值確實發生更改時通知訂閱者。
this.getAndInvoke(() => {
this.dep.notify();
});
}
} else if (this.sync) {
this.run();
} else {
// 處於性能考量,異步更新隊列,但最終都會執行 watcher.run(),此處再也不細說。
queueWatcher(this);
}
}
/** * 調度器的工做界面。將由調度程序調用。 */
run() {
if (this.active) {
this.getAndInvoke(this.cb);
}
}
getAndInvoke(cb: Function) {
const value = this.get();
if (
value !== this.value ||
// 即便值是相同的,深度觀察者和對象/數組上的觀察者也應該觸發,由於值可能發生了突變。
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value;
this.value = value;
this.dirty = false;
if (this.user) {
try {
cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`);
}
} else {
cb.call(this.vm, value, oldValue);
}
}
}
/** * 計算並返回監視程序的值。這隻對計算過的屬性觀察者調用。 */
evaluate() {
if (this.dirty) {
this.value = this.get();
this.dirty = false;
}
return this.value;
}
/** * Depend on this watcher. Only for computed property watchers. */
depend() {
if (this.dep && Dep.target) {
this.dep.depend();
}
}
/** * 從全部依賴項的訂閱服務器列表中刪除self。 */
teardown() {
if (this.active) {
// 從vm的監視者列表中刪除self這是一個有點昂貴的操做,因此若是正在銷燬vm,咱們就跳過它。
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this);
}
let i = this.deps.length;
while (i--) {
this.deps[i].removeSub(this);
}
this.active = false;
}
}
}
複製代碼
上文已分析了構建響應式所有的內容,下面就 $watch 函數
渲染函數
的觀察者 簡單演示整個響應流過程。
渲染函數
上文在談 new Vue()
最終程序走的是掛載函數,接下來,就看看掛載函數作了哪些處理。(注意:這裏的掛載函數在初始化時已經被重寫,給運行時版的 $mount 函數增長編譯模板的能力)
import { mountComponent } from 'core/instance/lifecycle';
/** * 公用的掛載方法 * * @param {String | Element} el 掛載元素 * @param {Boolean} hydrating 用於 Virtual DOM 的補丁算法 * @returns {Function} 真正的掛載組件的方法 */
Vue.prototype.$mount = function( el?: string | Element, hydrating?: boolean ): Component {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating);
};
複製代碼
mountComponent
/** * 組件掛載函數 */
export function mountComponent( vm: Component, el: ?Element, hydrating?: boolean ): Component {
vm.$el = el;
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode;
if (process.env.NODE_ENV !== 'production') {
if (
(vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el ||
el
) {
warn(
'您正在使用Vue的僅運行時構建,其中模板編譯器不可用。要麼將模板預編譯爲呈現函數,要麼使用編譯器包含的構建。',
vm
);
} else {
warn('加載組件失敗:模板或呈現函數未定義。', vm);
}
}
}
callHook(vm, 'beforeMount');
/******************* 把虛擬DOM渲染成真正的DOM ********************/
let updateComponent;
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name;
const id = vm._uid;
const startTag = `vue-perf-start:${id}`;
const endTag = `vue-perf-end:${id}`;
mark(startTag);
const vnode = vm._render(); // 調用 vm.$options.render 函數並返回生成的虛擬節點(vnode)
mark(endTag);
measure(`vue ${name} render`, startTag, endTag);
mark(startTag);
vm._update(vnode, hydrating); // 渲染虛擬節點爲真正的 DOM
mark(endTag);
measure(`vue ${name} patch`, startTag, endTag);
};
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating);
};
}
/******************* 把虛擬DOM渲染成真正的DOM ********************/
/******************* 實例化觀察者 ********************/
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate');
}
}
},
true
);
hydrating = false;
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}
return vm;
}
複製代碼
$watch 函數
$watch
: 觀察 Vue 實例變化的一個表達式或計算屬性函數。回調函數獲得的參數爲新值和舊值。表達式只接受監督的鍵路徑。對於更復雜的表達式,用一個函數取代。
在上文初始化過程,談到 $watch
的初始化,下面是代碼實現。
export function stateMixin(Vue: Class<Component>) {
...
Vue.prototype.$watch = function( expOrFn: string | Function, cb: any, options?: Object ): Function {
const vm: Component = this;
// 這個裏就是爲了規範化 watch 參數,這裏不細說。
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options);
}
options = options || {};
options.user = true; // 用戶自定義回調
const watcher = new Watcher(vm, expOrFn, cb, options); // 實例化觀察者
// 若是當即觸發,則當即執行回調。不然放入異步隊列中
if (options.immediate) {
cb.call(vm, watcher.value); // 這裏注意,第二個參數(newVal)未傳,因此你在回調拿不到
}
// 返回一個取消觀察函數,用來中止觸發回調
return function unwatchFn() {
watcher.teardown();
};
};
...
}
複製代碼
演示 demo
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>vue.js DEMO</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app">
<p>數據屬性:{{ message }}</p>
<button @click="update">更新</button>
</div>
<script> new Vue({ el: '#app', data: { message: 'hello vue.js' }, mounted() { this.$watch('message', function(newVal, oldVal) { console.log(`message: __新值__${newVal}___舊值___${oldVal}`); }); }, methods: { update() { this.message = `${this.message} ---- ${Math.random()}`; } } }); </script>
</body>
</html>
複製代碼
演示效果 及步驟梳理
message
屬性,觸發 message
更新(setter
)
dep.notify()
觸發依賴,依次執行 update
(這裏包含渲染函數的觀察者: 渲染函數 => watch )
run
$watch
:
get
求值,執行回調,更新新值