看完上兩章 初入茅廬
小試牛刀
以後,你們應該對vue-next(Vue 3.0) 的 API 使用已經瞭如指掌了。好奇的同窗必定對 vue-next
響應式的原理充滿好奇,本章就帶你解密!javascript
最新vue-next
的源碼發佈了,雖然是 pre-alpha
版本,但這時候實際上是閱讀源碼的比較好的時機。在 vue
中,比較重要的東西固然要數它的響應式系統,在以前的版本中,已經有若干篇文章對它的響應式原理和實現進行了介紹,這裏就不贅述了。在 vue-next
中,其實現原理和以前仍是相同的,即經過觀察者模式和數據劫持,只不過對其實現方式進行了改變。html
所以,這篇文章我也打算按這種風格來寫一下利用最近空閒時間閱讀 vue-next
響應式模塊的源碼的一些心得與體會,算是拋磚引玉,同時實現一個極簡的響應式系統。vue
若有錯誤,還望指正。java
在學習vue-next
以前,你必需要先熟練掌握ES6中的 Proxy
、Reflect
及 ES6
中爲咱們提供的 Map
、Set
兩種數據結構react
先應用再說原理:git
const { reactive, effect} = Vue
let p = reactive({name:'zhuanzhuan'});
// effect方法會當即被觸發
effect(()=>{
console.log(p.name);
})
p.name = '轉轉'; // 修改屬性後會再次觸發effect方法
複製代碼
源碼是採用ts
編寫,爲了便於你們理解原理,這裏咱們採用js來從0編寫,以後再看源碼就很是的輕鬆啦!github
看源碼面試
function reactive(target) {
// if trying to observe a readonly proxy, return the readonly version.
if (readonlyToRaw.has(target)) {
return target;
}
// target is explicitly marked as readonly by user
if (readonlyValues.has(target)) {
return readonly(target);
}
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
);
}
function createReactiveObject( target, toProxy, toRaw, baseHandlers, collectionHandlers ) {
if (!isObject(target)) {
{
console.warn(`value cannot be made reactive: ${String(target)}`);
}
return target;
}
// target already has corresponding Proxy
let observed = toProxy.get(target);
if (observed !== void 0) {
return observed;
}
// target is already a Proxy
if (toRaw.has(target)) {
return target;
}
// only a whitelist of value types can be observed.
if (!canObserve(target)) {
return target;
}
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers;
observed = new Proxy(target, handlers);
toProxy.set(target, observed);
toRaw.set(observed, target);
return observed;
}
複製代碼
稍微精簡下chrome
function reactive(target) {
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
observed = new Proxy(target, handlers)
return observed
}
複製代碼
const collectionTypes = new Set([Set, Map, WeakMap, WeakSet]);
複製代碼
基本上除了Set
, Map
, WeakMap
, WeakSet
,都是baseHandlers
。segmentfault
baseHandlers
實現:
function createGetter(isReadonly, shallow = false) {
return function get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
if (isSymbol(key) && builtInSymbols.has(key)) {
return res;
}
if (shallow) {
track(target, "get" /* GET */, key);
// TODO strict mode that returns a shallow-readonly version of the value
return res;
}
if (isRef(res)) {
return res.value;
}
track(target, "get" /* GET */, key);
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res;
};
}
複製代碼
返回值若是是object
,就再走一次reactive
,實現深度
下面咱們本身寫個案例,經過proxy 自定義獲取、增長、刪除等行爲
function reactive(target){
// 建立響應式對象
return createReactiveObject(target);
}
function isObject(target){
return typeof target === 'object' && target!== null;
}
function createReactiveObject(target){
// 判斷target是否是對象,不是對象沒必要繼續
if(!isObject(target)){
return target;
}
const handlers = {
get(target,key,receiver){ // 取值
console.log('獲取')
let res = Reflect.get(target,key,receiver);
return res;
},
set(target,key,value,receiver){ // 更改 、 新增屬性
console.log('設置')
let result = Reflect.set(target,key,value,receiver);
return result;
},
deleteProperty(target,key){ // 刪除屬性
console.log('刪除')
const result = Reflect.deleteProperty(target,key);
return result;
}
}
// 開始代理
observed = new Proxy(target,handlers);
return observed;
}
let p = reactive({name:'zhuanzhuan'});
console.log(p.name); // 獲取
p.name = '轉轉'; // 設置
delete p.name; // 刪除
複製代碼
咱們繼續考慮多層對象如何實現代理
let p = reactive({ name: "zhuanzhuan", age: { num: 3 } });
p.age.num = 4
複製代碼
因爲咱們只代理了第一層對象,因此對age
對象進行更改是不會觸發set
方法的,可是卻觸發了get
方法,這是因爲p.age
會形成 get
操做
get(target, key, receiver) {
// 取值
console.log("獲取");
let res = Reflect.get(target, key, receiver);
return isObject(res) // 懶代理,只有當取值時再次作代理,vue2.0中一上來就會所有遞歸增長getter,setter
? reactive(res) : res;
}
複製代碼
這裏咱們將p.age
取到的對象再次進行代理,這樣在去更改值便可觸發set
方法
咱們繼續考慮數組問題
咱們能夠發現Proxy默承認以支持數組,包括數組的長度變化以及索引值的變化
let p = reactive([1,2,3,4]);
p.push(5);
複製代碼
可是這樣會觸發兩次set
方法,第一次更新的是數組中的第4
項,第二次更新的是數組的length
看下源碼是如何處理的:
很簡單,用的hasOwProperty
, set
確定會出發屢次,可是通知只出去一次, 好比數組修改length
的時候,hasOwProperty
是true
, 那就不觸發
function set(target, key, value, receiver) {
value = toRaw(value);
const oldValue = target[key];
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value;
return true;
}
const hadKey = hasOwn(target, key);
const result = Reflect.set(target, key, value, receiver);
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
/* istanbul ignore else */
{
const extraInfo = { oldValue, newValue: value };
if (!hadKey) {
trigger(target, "add" /* ADD */, key, extraInfo);
}
else if (hasChanged(value, oldValue)) {
trigger(target, "set" /* SET */, key, extraInfo);
}
}
}
return result;
}
複製代碼
咱們來屏蔽掉屢次觸發,更新操做
function hasOwn(target,key){
return target.hasOwnProperty(key);
}
set(target, key, value, receiver) {
// 更改、新增屬性
let oldValue = target[key]; // 獲取上次的值
let hadKey = hasOwn(target,key); // 看這個屬性是否存在
let result = Reflect.set(target, key, value, receiver);
if(!hadKey){ // 新增屬性
console.log('更新 添加')
}else if(oldValue !== value){ // 修改存在的屬性
console.log('更新 修改')
}
// 當調用push 方法第一次修改時數組長度已經發生變化
// 若是此次的值和上次的值同樣則不觸發更新
return result;
}
複製代碼
解決重複使用reactive狀況
// 狀況1.屢次代理同一個對象
let arr = [1,2,3,4];
let p = reactive(arr);
reactive(arr);
// 狀況2.將代理後的結果繼續代理
let p = reactive([1,2,3,4]);
reactive(p);
複製代碼
經過hash表的方式來解決重複代理的狀況
const toProxy = new WeakMap(); // 存放被代理過的對象
const toRaw = new WeakMap(); // 存放已經代理過的對象
function reactive(target) {
// 建立響應式對象
return createReactiveObject(target);
}
function isObject(target) {
return typeof target === "object" && target !== null;
}
function hasOwn(target,key){
return target.hasOwnProperty(key);
}
function createReactiveObject(target) {
if (!isObject(target)) {
return target;
}
let observed = toProxy.get(target);
if(observed){ // 判斷是否被代理過
return observed;
}
if(toRaw.has(target)){ // 判斷是否要重複代理
return target;
}
const handlers = {
get(target, key, receiver) {
// 取值
console.log("獲取");
let res = Reflect.get(target, key, receiver);
return isObject(res) ? reactive(res) : res;
},
set(target, key, value, receiver) {
let oldValue = target[key];
let hadKey = hasOwn(target,key);
let result = Reflect.set(target, key, value, receiver);
if(!hadKey){
console.log('更新 添加')
}else if(oldValue !== value){
console.log('更新 修改')
}
return result;
},
deleteProperty(target, key) {
console.log("刪除");
const result = Reflect.deleteProperty(target, key);
return result;
}
};
// 開始代理
observed = new Proxy(target, handlers);
toProxy.set(target,observed);
toRaw.set(observed,target); // 作映射表
return observed;
}
複製代碼
到這裏reactive
方法基本實現完畢,接下來就是與Vue2
中的邏輯同樣實現依賴收集和觸發更新
get(target, key, receiver) {
let res = Reflect.get(target, key, receiver);
track(target,'get',key); // 依賴收集==
return isObject(res)
?reactive(res):res;
},
set(target, key, value, receiver) {
let oldValue = target[key];
let hadKey = hasOwn(target,key);
let result = Reflect.set(target, key, value, receiver);
if(!hadKey){
trigger(target,'add',key); // 觸發添加
}else if(oldValue !== value){
trigger(target,'set',key); // 觸發修改
}
return result;
}
複製代碼
track
的做用是依賴收集,收集的主要是effect
,咱們先來實現effect
原理,以後再完善 track
和trigger
方法
effect意思是反作用,此方法默認會先執行一次。若是數據變化後會再次觸發此回調函數。
const p = reactive({name:'zhuanzhuan'})
effect(()=>{
console.log(p.name); // zhuanzhuan
})
複製代碼
咱們來實現effect
方法,咱們須要將effect
方法包裝成響應式effect
。
const activeReactiveEffectStack = []; // 存放響應式effect
function effect(fn) {
const effect = createReactiveEffect(fn); // 建立響應式的effect
effect(); // 先執行一次
return effect;
}
function createReactiveEffect(fn) {
const effect = function() {
// 響應式的effect
return run(effect, fn);
};
return effect;
}
function run(effect, fn) {
try {
activeReactiveEffectStack.push(effect);
return fn(); // 先讓fn執行,執行時會觸發get方法,能夠將effect存入對應的key屬性
} finally {
activeReactiveEffectStack.pop(effect);
}
}
複製代碼
當調用fn()
時可能會觸發get
方法,此時會觸發track
const targetMap = new WeakMap();
function track(target,type,key){
// 查看是否有effect
const effect = activeReactiveEffectStack[activeReactiveEffectStack.length-1];
if(effect){
let depsMap = targetMap.get(target);
if(!depsMap){ // 不存在map
targetMap.set(target,depsMap = new Map());
}
let dep = depsMap.get(target);
if(!dep){ // 不存在set
depsMap.set(key,(dep = new Set()));
}
if(!dep.has(effect)){
dep.add(effect); // 將effect添加到依賴中
}
}
}
複製代碼
當更新屬性時會觸發trigger
執行,找到對應的存儲集合拿出effect
依次執行
咱們發現以下問題
function trigger(target,type,key){
const depsMap = targetMap.get(target);
if(!depsMap){
return
}
let effects = depsMap.get(key);
if(effects){
effects.forEach(effect=>effect())
}
}
複製代碼
新增了值,effect
方法並未從新執行,由於push
中修改length
已經被咱們屏蔽掉了觸發trigger
方法,因此當新增項時應該手動觸發length
屬性所對應的依賴。
function trigger(target, type, key) {
const depsMap = targetMap.get(target);
if (!depsMap) {
return;
}
let effects = depsMap.get(key);
if (effects) {
effects.forEach(effect => effect());
}
// 處理若是當前類型是增長屬性,若是用到數組的length的effect應該也會被執行
if (type === "add") {
let effects = depsMap.get("length");
if (effects) {
effects.forEach(effect => {
effect();
});
}
}
複製代碼
ref
能夠將原始數據類型也轉換成響應式數據,須要經過.value
屬性進行獲取值
function convert(val) {
return isObject(val) ? reactive(val) : val;
}
function ref(raw) {
raw = convert(raw);
const v = {
_isRef:true, // 標識是ref類型
get value() {
track(v, "get", "");
return raw;
},
set value(newVal) {
raw = newVal;
trigger(v,'set','');
}
};
return v;
}
複製代碼
問題又來了咱們再編寫個案例
let r = ref(1);
let c = reactive({
a:r
});
console.log(c.a.value);
複製代碼
這樣作的話豈不是每次都要多來一個.value,這樣太難用了
在get
方法中判斷若是獲取的是ref
的值,就將此值的value
直接返回便可
let res = Reflect.get(target, key, receiver);
if(res._isRef){
return res.value
}
複製代碼
computed
實現也是基於 effect
來實現的,特色是computed
中的函數不會當即執行,屢次取值是有緩存機制的
先來看用法:
let a = reactive({name:'zhuanzhuan'});
let c = computed(()=>{
console.log('執行次數')
return a.name +'今年3歲了';
})
// 不取不執行,取n次只執行一次
console.log(c.value);
console.log(c.value);
複製代碼
function computed(getter){
let dirty = true;
const runner = effect(getter,{ // 標識這個effect是懶執行
lazy:true, // 懶執行
scheduler:()=>{ // 當依賴的屬性變化了,調用此方法,而不是從新執行effect
dirty = true;
}
});
let value;
return {
_isRef:true,
get value(){
if(dirty){
value = runner(); // 執行runner會繼續收集依賴
dirty = false;
}
return value;
}
}
}
複製代碼
修改effect
方法
function effect(fn,options) {
let effect = createReactiveEffect(fn,options);
if(!options.lazy){ // 若是是lazy 則不當即執行
effect();
}
return effect;
}
function createReactiveEffect(fn,options) {
const effect = function() {
return run(effect, fn);
};
effect.scheduler = options.scheduler;
return effect;
}
複製代碼
在trigger
時判斷
deps.forEach(effect => {
if(effect.scheduler){ // 若是有scheduler 說明不須要執行effect
effect.scheduler(); // 將dirty設置爲true,下次獲取值時從新執行runner方法
}else{
effect(); // 不然就是effect 正常執行便可
}
});
複製代碼
let a = reactive({name:'zhuanzhuan'});
let c = computed(()=>{
console.log('執行次數')
return a.name +'今年3歲了';
})
// 不取不執行,取n次只執行一次
console.log(c.value);
a.name = '轉轉'; // 更改值 不會觸發從新計算,可是會將dirty變成true
console.log(c.value); // 從新調用計算方法
複製代碼
直接拷貝下面代碼,去運行看效果吧。推薦使用高版本的chrome瀏覽器!
my-vue-next.js
文件
// 存放被代理過的對象
let toProxy = new WeakMap()
// 存放已經代理過的對象
let toRaw = new WeakMap()
let tagetMap = new WeakMap()
let effectStack = []
const baseHander = {
get(target, key){
const res = Reflect.get(target, key)
// 收集依賴
track(target, key)
// 遞歸尋找
return typeof res == 'object' ? reactive(res) : res
},
set(target, key, val){
const info = {oldValue: target[key], newValue:val}
const res = Reflect.set(target, key, val)
// 觸發更新
trigger(target, key, info)
return res
}
}
function reactive(target){
// 查詢緩存
let observed = toProxy.get(target)
if(observed){
return observed
}
// 若是已經代理過了這個對象,則直接返回代理後的結果便可
if(toRaw.get(target)){
return target
}
observed = new Proxy(target, baseHander)
// 設置緩存
toProxy.set(target, observed)
toRaw.set(observed, target)
return observed
}
function trigger(target, key, info){
// 觸發更新
const depsMap = tagetMap.get(target)
if(depsMap===undefined){
return
}
const effects = new Set()
const computedRunners = new Set()
if(key){
let deps = depsMap.get(key)
if(!deps) return
deps.forEach(effect=>{
if(effect.computed){
computedRunners.add(effect)
}else{
effects.add(effect)
}
})
}
effects.forEach(effect=> effect())
computedRunners.forEach(effect=> effect())
}
function track(target, key){
let effect = effectStack[effectStack.length - 1]
if(effect){
let depsMap = tagetMap.get(target)
if(depsMap===undefined){
depsMap = new Map()
tagetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if(dep===undefined){
dep = new Set()
depsMap.set(key, dep)
}
if(!dep.has(effect)){
dep.add(effect)
}
}
}
// 存儲effect
function effect(fn,options={}){
let e = createReactiveEffect(fn, options)
// 首次頁面加載就須要先運行一次 effect 方法,讓頁面渲染
if(!options.lazy){
e()
}
return e
}
function createReactiveEffect(fn,options){
const effect = function(...args){
return run(effect, fn , args)
}
// 爲了調試查看
effect.fn = fn
effect.computed = options.computed
effect.lazy = options.lazy
return effect
}
function run(effect, fn , args){
if(effectStack.indexOf(effect)===-1){
try{
effectStack.push(effect)
return fn(...args)
}
finally{
effectStack.pop()
}
}
}
function computed(fn){
const runner = effect(fn,{computed:true, lazy:true})
return {
effect:runner,
get value(){
return runner()
}
}
}
複製代碼
index.html
文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="./my-vue-next.js"></script>
</head>
<body>
<div id='app'></div>
<button id="btn">點我</button>
<script> const root = document.querySelector('#app') const btn = document.querySelector('#btn') const obj = reactive({ name:'轉轉', age: 3 }) const double = computed(()=> obj.age *2) effect(()=>{ root.innerHTML = `<h1>${obj.name}今年${obj.age}歲了,乘以2是${double.value}</h1>` }) btn.addEventListener('click', ()=>{ const age = obj.age obj.age = age + 1 }, false) </script>
</body>
</html>
複製代碼
看完初入茅廬
、小試牛刀
、爐火純青
三章以後, 已經將vue-next
核心的 Composition Api
就講解完畢了! 不論是面試仍是後期的應用也不再須要擔憂啦!~