當前,Vue和React已成爲兩大炙手可熱的前端框架,這兩個框架都算是業內一些最佳實踐的集合體。其中,Vue最大的亮點和特色就是數據響應化,而React的特點則是單向數據流與jsx。
筆者近期正在研究Vue源碼,在此過程中嘗試實現一個簡易版的Vue,而實現Vue的第一步便是解決數據響應化的問題。以下便是對Vue響應化的簡易版實現。
數據響應的原理:
1、依賴收集:data通過Observer變成帶有getter和setter方法的響應式對象,當外界通過Watcher獲取數據時,會將該Watcher加入到Dep的依賴列表中,至此,就算完成了依賴收集
2、通知更新:當外界對已經響應化的對象,即data中的對象進行修改時,會觸發setter方法,setter會通過Dep的通知方法,循環調用Dep依賴列表中Watcher的update方法通知外界更新視圖或觸發用戶所給的監聽回調
// kinerVue.js 簡易版小程序入口
import Watcher from './Watcher.js';
import Observer,{set, del} from './Observer.js'
// 預期用法
// let vue = new KinerVue({
// data(){
// return {
// name: "kiner",
// userInfo: {
// age: 20
// },
// classify:['game','reading','running']
// }
// }
// });
// 數據響應的原理:
// 1、依賴收集:data通過Observer編程帶有getter和setter方法的響應式對象,當外界通過Watcher獲取數據時,會將該Watcher加入到Dep的依賴列表中,至此,就算完成了依賴收集
// 2、通知更新:當外界對已經響應化的對象,即data中的對象進行修改時,會觸發setter方法,setter會通過Dep的通知方法,循環調用Dep依賴列表中Watcher的update方法通知外界更新視圖或觸發用戶所給的監聽回調
/**
* 自定義簡易版Vue
*/
class KinerVue{
constructor(options){
this.$options = options;
this.$data = options.data.apply(this);
// 將數據交給Observer,讓Observer將這個數據變成響應式對象
new Observer(this,this.$data);
this.isVue = true;
// test data start
//測試$watch start
let unWatchUserInfo = this.$watch("userInfo",(newVal,oldVal)=>{
console.log(`$watch監聽到[userInfo]發生改變,新值:`,newVal,`;舊值:`,oldVal);
},{deep: false, immediate: true});
this.$watch("userInfo.age",(newVal,oldVal)=>{
console.log(`$watch監聽到[userInfo.age]發生改變,新值:${newVal};舊值:${oldVal}`);
});
this.$watch("classify",function classifyWatcher(newVal,oldVal){
console.log(`$watch監聽到[classify]發生改變,新值:${newVal},;舊值:${oldVal}`);
});
this.$watch("friends",function classifyWatcher(newVal,oldVal){
console.log(`$watch監聽到[friends]發生改變,新值:${newVal},;舊值:${oldVal}`);
});
this.userInfo.age = 11;
// 取消訂閱,執行了這行代碼之後$watch("userInfo",()=>{})將失效
// unWatchUserInfo();
this.userInfo.age = 20;
//通過$set爲數組設置值
this.$set(this.classify,3,'999');
this.$set(this.userInfo,'sex','男');
this.$set(this.userInfo,'sex','女');
//通過$delete刪除後屬性
this.$delete(this.userInfo,"sex");
console.log('sex:',this.userInfo)
// console.log(this.classify);
//測試$watch end
// new Watcher(this,"name");
// this.name;
// new Watcher(this,"userInfo.age");
// this.userInfo.age;
// new Watcher(this,"classify");
// this.classify;
//
// this.name = 'kanger';
// console.log(this.name);
// this.userInfo.age = 18;
// console.log(this.userInfo.age);
//
this.classify.push(10);
this.classify.splice(5,1,11);
this.classify.unshift(12);
this.classify.shift();
this.classify.sort((a,b)=>a-b);
this.classify.reverse();
this.friends.push('zzz');
this.friends.splice(2,1,'fff');
this.friends.unshift('kkk');
this.friends.sort((a,b)=>a-b);
this.friends.reverse();
this.friends.shift();
// 由於未採用ES6的元編程能力,也就是proxy和reflect,因此無法監控類似arr[0]=xxxx和arr.length=0之類的數值變化,
// 因此,在編碼時要儘量避免這些寫法,以免產生一些不可意料的問題
//
// this.classify[2] = 'working'; //錯誤用法
// console.log(this.classify);
// test data end
}
/**
* 監聽器,用於監聽屬性變化,並將新舊值傳遞回來,方便做一些攔截操作
* @param exp 表達式或函數
* @param cb 回調
* @param options 配置項
* @returns {Function} 取消觀察的方法
*/
$watch(exp,cb,options={immediate: true,deep: false}){
let watcher = new Watcher(this,exp,cb,options);
return ()=>{
watcher.unWatch();
};
}
/**
* 設置屬性,用來解決無法使用arr[0]=xxx,obj={} obj.name=xxx
* @param target
* @param key
* @param value
*/
$set(target,key,value){
return set(target,key,value);
}
/**
* 刪除目標對象上的數據
* @param target
* @param key
* @returns {undefined}
*/
$delete(target,key){
return del(target,key);
}
}
export default KinerVue;
// utils.js 基礎工具庫,提供一些工具方法
/**
* 判斷對象是否支持__proto__屬性
* @type {boolean}
*/
export const hasProto = '__proto__' in {};
/**
* 判斷傳遞過來的對象是否是純對象
* @param obj
* @returns {boolean}
*/
export const isPlainObject = function(obj){
let prototype;
return Object.prototype.toString.call(obj) === '[object Object]'
&& (prototype = Object.getPrototypeOf(obj), prototype === null ||
prototype === Object.getPrototypeOf({}))
};
/**
* 判斷是否爲非空對象
* @param obj
* @returns {boolean}
*/
export const isObject = obj => (obj !== null && typeof obj === 'object');
/**
* 顯示警告消息
* @param message
*/
export const warn = function (message) {
console.warn(message);
};
/**
* 定義不可枚舉的屬性
* @param obj
* @param key
* @param value
* @param enumerable 能否枚舉
*/
export const def = function (obj,key,value,enumerable) {
if(typeof obj === "object"){
Object.defineProperty(obj,key,{
value: value,
configurable: true,
enumerable: !!enumerable,
writable: true
});
}
};
/**
* 刪除數組中的元素
* @param arr
* @param item
* @returns {T[]}
*/
export const removeArrItem = function (arr, item) {
const index = arr.indexOf(item);
if(index!==-1){
return arr.splice(index,1);
}
};
/**
* 根據表達式從目標對象中找到對應的值
* e.g.
* 若obj={userInfo:{userName}}
* exp="userInfo.userName"
*
* @param obj
* @param exp
* @returns {*}
*/
export const parseExp = function (exp) {
return obj => {
let reg = /[^\w.$]/;
if(reg.test(exp)){
return;
}else{
let subExp = exp.split('.');
subExp.forEach(item=>{
obj = obj[item];
});
return obj;
}
};
};
/**
* 判斷兩個變量是否相等(但因爲一個特殊情況,當a和b都等於NaN時,因爲NaN===NaN輸出爲false)
* @param a
* @param b
* @returns {boolean}
*/
export const isEqual = (a,b) => a===b||(a!==a&&b!==b);
/**
* 將攔截器方法直接覆蓋到目標對象的原型鏈上__proto__
* @param obj
* @param target
* @returns {*}
*/
export const patchToProto = (obj,target) => obj.__proto__ = target;
/**
* 直接在目標對象上定義不可枚舉的屬性
* @param obj
* @param arrayMethods
* @param keys
* @returns {*}
*/
export const copyArgument = (obj,arrayMethods,keys) => keys.forEach(key=>def(obj,key,arrayMethods[key]));
/**
* 判斷當前瀏覽器是否支持__proto__若支持,這直接將目標方法覆蓋到__proto__上,否則,直接將方法定義在目標對象上
* @param obj
* @param src
* @param keys
* @returns {*}
*/
export const defProtoOrArgument = (obj,src,keys=Object.getOwnPropertyNames(src)) => hasProto ? patchToProto(obj,src) : copyArgument(obj,src,keys);
/**
* 判斷目標對象是否含有指定屬性
* @param obj
* @param key
* @returns {boolean}
*/
export const hasOwn = (obj,key) => obj.hasOwnProperty(key);
/**
* 判斷目標對象是否已經響應化
* @param obj
* @returns {boolean}
*/
export const hasOb = obj => hasOwn(obj,'__ob__');
/**
* 判斷傳入參數類型是否爲函數
* @param fn
* @returns {boolean}
*/
export const isFn = fn => typeof fn === "function";
/**
* 判斷所給參數是否是一個數組
* @param arr
* @returns {arg is Array<any>}
*/
export const isA = arr => Array.isArray(arr);
/**
* 判斷給定參數是否是合法的數組索引
*/
export const isValidArrayIndex = (val) => {
const n = parseFloat(String(val));
return n >= 0 && Math.floor(n) === n && isFinite(val)
};
export default {
hasProto,
isPlainObject,
isObject,
warn,
def,
removeArrItem,
isEqual,
parseExp,
patchToProto,
copyArgument,
defProtoOrArgument,
hasOwn,
hasOb,
isFn,
isA
}
// Array.js 定義一些針對數組響應化時需要用到的輔助數據以及定義了
import {def} from "./utils.js";
// 數組原型,在對數組方法打補丁的時候,需要用到數組原型方法用於實現原本的數組操作
export const arrayProto = Array.prototype;
/**
* 需要打補丁的數組方法,即會改變數組的方法
* @type {string[]}
*/
export const needPatchArrayMethods = [
"push",
"pop",
"unshift",
"shift",
"sort",
"reverse",
"splice"
];
// 根據數組原型創建一個新的基礎數組對象,避免爲數組方法打補丁的時候污染原始數組
export const arrayMethods = Object.create(arrayProto);
// 實現數組攔截器,通過這個攔截器實現攔截數組操作方法操作
needPatchArrayMethods.forEach(method=>{
// 從數組原型中將原始方法取出
const originalMethod = arrayProto[method];
def(arrayMethods,method,function mutator(...args){
// const oldVal = [...this];
// 調用數組原始方法實現數組操作
const res = originalMethod.apply(this,args);
// 若當前數組已經是響應化後的數組,則將其Observe實例取出,用戶後續通知更新操作
const ob = this.__ob__;
// 若執行的是會新增數組元素的方法,我們需要對新增的元素也進行響應化處理
// 其中push和unshift接收的所有參數都是新增元素,因此直接將參數對象傳遞給defineReactiveForArray進行響應化處理
// splice第2個之後的參數便爲新增或替換的元素,因此將第2個之後的參數提取出來,傳遞給defineReactiveForArray進行響應化處理
let inserted;
switch (method){
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.splice(2);
break;
}
inserted && ob.defineReactiveForArray(inserted);
//通知依賴更新
ob&&ob.dep.notify();
// console.log(`---->觸發了數組的${method}方法:新值:`,this,`;舊值:`,oldVal);
return res;
});
});
// Dep.js 依賴類,用於統一管理觀察者,一旦依賴跟新,便可通過此類的notify方法通知其訂閱的所有
// 觀察者進行更新數據
import {removeArrItem} from "./utils.js";
let uid = 0;
/**
* 用來管理所有的watcher
*/
class Dep {
constructor(){
// 訂閱者列表
this.subs = [];
// 爲每一個依賴定義一個唯一的id
this.id = uid++;
}
/**
* 觸發添加依賴
*/
depend(){
//爲實現取消訂閱的功能,將訂閱的方法放在watcher中,此處通過調用watcher的addDep將當前依賴加入到訂閱列表,
Dep.target&&Dep.target.addDep(this);
// 初版實現,未實現取消訂閱功能
// Dep.target&&this.addDep(Dep.target);
}
/**
* 添加訂閱者
* @param watcher 訂閱者
*/
addSub(watcher){
// 爲解決當調用數組的splice和sort方法時,會觸發多次更新的問題,加入訂閱時先看一下該依賴是否已經被添加
if(this.subs.indexOf(watcher)<0){
this.subs.push(watcher);
}
}
/**
* 從從訂閱列表中移除訂閱者
* @param watcher
*/
removeSub(watcher){
removeArrItem(this.subs,watcher);
}
/**
* 通知訂閱者更新
*/
notify(){
this.subs.forEach(watcher=>{
watcher.update()
});
}
}
export default Dep;
/**
* Observer.js 數據響應化對象
* Vue數據響應化的核心,Vue2.0時代通過Object.defineProperty方式進行數據響應化,而Vue3.0時代則採用Proxy和Reflect方式實現
* 無論採用哪種方式,但其實現原理都是一樣的,都是通過數據劫持的方式實現響應化
*/
import Dep from "./Dep.js";
import {isPlainObject, warn, isEqual,defProtoOrArgument, hasOb, def, isA, isValidArrayIndex, hasOwn} from "./utils.js";
import {arrayMethods} from "./Array.js";
class Observer {
/**
* 定義統一的操作方法,方便之後收集依賴和響應通知的統一操作
* @param obj 待響應的對象
* @param key 待響應的鍵值
* @param value 待響應的值
* @param childOb 子響應對象
* @returns {*}
*/
static baseHandler(obj, key, value,childOb) {
//定義一個依賴對象,與data的key存在一一對應的關係
const dep = new Dep();
return {
enumerable: true,
configurable: true,
get() {
// Dep.target && dep.addDep(Dep.target);
// 在訪問對象屬性時,將當前屬性加入到依賴列表中
dep.depend();
// console.log('收集依賴',obj,key,value,childOb);
// 用於收集數組對象的依賴
childOb && childOb.dep.depend();
// console.log(`獲取${key}的值:${value}`);
return value;
},
set(val) {
// isEqual:原本的目的是爲了判斷新值val和舊值value相等的情況下,便直接退出,
// 但因爲一個特殊情況,當val和value都等於NaN時,因爲NaN===NaN輸出爲false
// 會讓set方法繼續往下執行,因此多加了一個(value!==value&&val!==val)進行攔截
//
if (isEqual(val, value)) {
return;
}
// 由於舊值仍處於閉包當中,this.$data未釋放的情況下,直接對value賦值可直接操作this.$data下對應鍵值下的數據,所以進行以下賦值操作
value = val;
// 通知依賴列表循環更新依賴
dep.notify();
// console.log(`設置${key}的值:${val}`);
}
}
};
constructor(vm, target) {
this.$vm = vm;
// 將跟數據target設置爲已響應,以免重複創建示例
def(target,'__ob__',this);
//在此定義依賴收集對象,用來收集數組的依賴
this.dep = new Dep();
// 將目標變化變爲響應式對象
this.observer(target);
}
/**
* 對傳入的數據進行響應化處理
* @param data
*/
observer(data) {
if (Array.isArray(data)) {//傳過來的數據是否是數組
defProtoOrArgument(data,arrayMethods);
return this.defineReactiveForArray(data)
} else if (isPlainObject(data)) {//傳遞過來的
return this.defineReactiveForObject(data);
} else {
warn(`傳遞的數據必須是對象或數組,當前傳遞的值【${data}】類型爲:${typeof data},因此無需響應化`);
}
}
/**
* 實現對象類型的響應化處理
* @param obj
*/
defineReactiveForObject(obj) {
let keys = Object.keys(obj);
keys.forEach(key => {
this.defineReactive(obj, key, obj[key]);
// 添加數據代理,將$data中的值代理到this,這樣就可以直接通過this.xxx訪問$data中的屬性了
this.proxyData(key);
});
}
/**
* 實現數組類型的響應化處理
* @param data
*/
defineReactiveForArray(data) {
data.forEach(item=>this.createObserver(item));
}
/**
* 將對象變爲響應式對象,通過遞歸調用observer方法可以實現嵌套對象響應化
* @param obj 帶響應化對象
* @param key 待響應的鍵值
* @param value 待響應的值
*/
defineReactive(obj, key, value) {
let childOb = this.createObserver(value);
Object.defineProperty(obj, key, Observer.baseHandler(obj, key, value,childOb));
}
/**
* 判斷目標數據是否已經響應化,如果響應化,則直接返回其響應化對象__ob__,佛則示例話一個響應化對象
* @param data
* @returns {*}
*/
createObserver(data){
let ob;
if(hasOb(data)){//該對象已經響應化,直接獲取
ob = data.__ob__;
}else{
ob = new Observer(this.$vm,data);
}
return ob;
}
/**
* 代理$data,將$data中的數據代理到vue實例中,便可直接通過this.xxx獲取或設置值
* @param key
* @returns {*}
*/
proxyData(key) {
Object.defineProperty(this.$vm, key, {
get() {
return this.$data[key];
},
set(val) {
this.$data[key] = val;
}
})
}
}
/**
* 爲目標對象或數組增加設置/新增值
* @param target
* @param key
* @param val
* @returns {*}
*/
export const set = (target,key,val)=>{
//如果target是數組且key是合法的數組索引,則將目標值加入到數組中
if(isA(target) && isValidArrayIndex(key)){
target.length = Math.max(target.length,key);
target.splice(key,1,val);
return val;
}
//如果key是target非原型鏈上的屬性,說明該key已經是響應化對象了,無需重複響應化,直接修改對應的值即可
if(key in target && !(key in Observer.prototype)){
target[key] = val;
return val;
}
//新增屬性
const ob = target.__ob__;
//如果當前對象未被響應化,則直接設置目標值
if(!ob){
target[key] = val;
return val;
}
// TODO 不能在跟對象this.$data和Vue示例上添加屬性
// 如果target是響應化對象,則通過Observer的defineRelative方法設置屬性
ob.defineReactive(target,key,val);
ob.dep.notify();
return val;
};
export const del = (target,key) => {
//如果target是數組且key是合法的數組索引,則刪除掉指定索引的數組項
if(isA(target) && isValidArrayIndex(key)){
target.splice(key,1);
return;
}
//若target本身就不具有key屬性,則無需刪除,直接返回
if(!hasOwn(target,key)) return;
// TODO 不能在跟對象this.$data和Vue示例上刪除屬性
const ob = target.__ob__;
delete target[key];
// 通知依賴更新
ob && ob.dep.notify();
};
export default Observer;
// Watcher.js
// 它相當於是依賴Dep與具體的更新操作的一箇中介,也可以理解爲他是一個物流中轉站,依賴就像是快遞,具體更新操作就是快遞的目的地,具體流程是這樣的:
// 我們把快遞(更新)交給快遞代收點(Dep),當快遞代收點(Dep)接收到快遞之後,會有人來收集快遞送到快遞中轉站(watcher),然後再由快遞中轉賬再統一派發到不同的地址。
import Dep from './Dep.js';
import {parseExp, isObject,isFn} from "./utils.js";
import {arrayMethods} from "./Array.js";
import {traverse} from "./Traverse.js";
class Watcher {
constructor(vm,expOrFn,cb=function(){},options={immediate: true,deep: false}){
// 創建實例時,將當前實例對象指向Dep的靜態屬性target
this.$vm = vm;
// 需要堅挺的表達式或者是給定的函數(注:如爲函數,則可在函數內使用到的響應化對象屬性都會被觀察,一旦任一屬性值發生變化,都會觸發cb回調通知)
this.expOrFn = expOrFn;
// 選項
// options.immediate true|false 代表是否在創建watcher實例時變直接運行表達式或函數獲取結果
// options.deep true|false 代表是否進行深度觀察,如果爲true,會對指定表達式或對象下使用的屬性的子屬性進行遞歸觀察操作
//// e.g. data下的對象userInfo的結構是:userInfo:{friends:[{name:'kiner'},{name:'kanger'}],bankInfo:{bankCardNum: 'xxxxxxx'}}
//// 那麼,如果我們要進行深度觀察,則如:this.$watch("userInfo",()=>{},{deep:true})
//// 此後,一旦userInfo下面的任一屬性,包括子對象、數組中的值發生改變,上述的$watch都能夠觀察得到
this.options = options;
// 若給出的是函數,則直接將其賦值給gutter
if(isFn(expOrFn)){
this.gutter = expOrFn;
}else{
// 若給出的是一個如:userInfo.name或age之類的表達式,則通過parseExp這個高階函數將表達式進行一定的處理並賦值給gutter
// 使我們可以直接通過this.gutter.call(this.$vm,this.$vm);的方式直接獲得表達式對應的結果
this.gutter = parseExp(expOrFn);
}
// 觀察者通知的回調函數
this.cb = cb;
// 爲實現取消訂閱功能,需要知道watcher都訂閱了哪些依賴,在取消訂閱時,秩序把對應的依賴從依賴列表移除即可
// 爲方便訂閱,將依賴列表從Dep移到watcher
this.deps = [];
// 爲了標誌依賴的唯一性,定義一個不可重複的Set用於存儲依賴的id
this.depIds = new Set();
// 如果指定immediate=true則在實例化時離開觸發get獲取目標值
if(options.immediate){
this.value = this.get();
}
}
/**
* 嘗試通過表達式或者所給方法獲取目標值
* @returns {*}
*/
get(){
Dep.target = this;//指定快遞代收點所屬的中轉站,這樣才能夠將快遞精確的從代收點送到中轉站
//根據給定的表達式或函數直接或取目標值,與此同時,因爲觸發了get,會將Dep.target添加到依賴列表當中
let value = this.gutter.call(this.$vm,this.$vm);
this.sourceValue = value;
// 若需要觀察對象系所有子對象的變化(注:此步驟必須放在`Dep.target = undefined;`之前,因爲遞歸收集子對象依賴時仍需要使用到Dep.target)
if(this.options.deep){
traverse(value);
}
//嘗試解決當value爲數組或對象時,newVal和oldVal恆等問題(注:此步驟是因爲個人開發原因需要獲取對象或數組的新舊值,爲方便操作,嘗試性實現,Vue官方並無此步驟)
if(value.__proto__===arrayMethods){
value = [...value];
}else if(isObject(value)){
value = {...value}
}
// 加入依賴列表之後釋放target
Dep.target = undefined;
return value;
}
// 中轉站已經收到快遞了,準備派送,通知各位快遞小哥過來拿各自負責區域(視圖中的表達式或$watch中監聽的方法)的快遞進行派送
update(){
// 接收到更新通知時,觸發get方法獲取改表達式最新的值
const value = this.get();
// vue源碼中:如果value是數組/對象時,我們通過$watch((newVal,oldVal)=>{})獲取到的newVal和oldVal其實是始終相等的,因爲他們都是東一個對象的引用
if(this.value!==value||isObject(value)){
const oldVal = this.value;
this.value = value;
// 將新舊值傳遞給回調函數,即完成$watch('xxxxx',function(newVal,oldVal){})的通知
this.cb.call(this.$vm,value,oldVal);
}
// console.log(`屬性${this.expOrFn}發生了變化`);
}
/**
* 添加依賴並經自己訂閱到依賴當中
* @param dep
*/
addDep(dep){
const depId = dep.id;
// 判斷依賴是否已經在依賴列表中,若不存在,則添加依賴
if(!this.depIds.has(depId)){
this.deps.push(dep);
this.depIds.add(depId);
// 爲添加的依賴訂閱觀察者
dep.addSub(this);
}
}
/**
* 取消觀察,移除依賴列表中所有的當前觀察者
*/
unWatch(){
let len = this.deps.length;
while (len--){
this.deps[len].removeSub(this);
}
}
}
export default Watcher;
// Traverse.js 通過traverse遞歸訪問指定對象,通過觸發getter的方式實現依賴收集
import {isA,isObject,hasOb} from "./utils.js";
// 用於存儲依賴id
const depIds = new Set();
// 通過這個方法訪問一下給定目標對象的子對象,從而觸發依賴通知
export const traverse = (val) => {
_traverse(val,depIds);
depIds.clear();
};
function _traverse(val,depIds){
let len,keys;
// 所傳對象如果類型不是非凍結對象或數組,就直接終止
if((!isA(val) && !isObject(val)) || Object.isFrozen(val)){
return;
}
// 判斷當前對象是否已經是響應化對象
if(hasOb(val)){
const depId = val.__ob__.dep.id;
if(depIds.has(depId)){//已經訪問過了,直接終止
return;
}
//若未訪問過,則將依賴id加入到depIds中
depIds.add(depId);
}
if(isA(val)){//如果是數組,則循環訪問其子項並遞歸訪問
len = val.length;
while (len--) _traverse(val,depIds);
}else{//循環對象下的所有屬性並遞歸訪問
keys = Object.keys(val);
len = keys.length;
while (len--) _traverse(val,depIds);
}
}
以上代碼已實現功能:
- 數據響應化-Observe.js(Array.js-數組響應化的一些相關處理)
- 數據觀察者-Watcher.js(Traverse.js-通過traverse遞歸訪問指定對象,通過觸發getter的方式實現依賴收集)
- 依賴管理者-Dep.js
- 工具方法$watch-觀察屬性變化的方法、$set-爲對象添加屬性或者爲數組添加子項,並通知依賴更新、$delete-刪除對象屬性或刪除數組子項並通知依賴更新
以後將陸續會嘗試實現:
- 虛擬Dom(VNode)
- 編譯器
- Vue生命週期鉤子、工具方法、全局Api實現
- 指令的解析
- 過濾器的實現
- 行業最佳實踐學習(Vue-Router、Vuex、Element-UI、Element-Admin、Vant)
文章將根據本人對Vue及其相關最佳實踐的學習進度不斷更新,如有不對,歡迎指正,謝謝!
PS:如果對Vue3(即vue-next)感興趣的同學,可以看一下本人撰寫的另一篇文章 Vue3(Vue-next)響應化實現剖析,這裏簡單的實現了使用es6元編程能力(proxy和reflect)實現的數據響應化原理。