Vue2 及 Vue3 响应式原理(手写简版源码解析)

Vue2响应式原理

vue2 依赖于 object.defineProperty 监听对象变化更新视图

1. object.defineProperty(target,key,{}) 方法在原对象上修改或定义一个属性

第一个参数原目标对象

第二个参数原属性 或 新属性

第三个参数 属性描述 或 属性存取

2.知道方法后

2.1 创建一个文件 我这里用普通的html 文件为例 创建 vue2.html 文件

2.2 <script> </script> 标签中定义一个方法 updateView 用来模拟更新视图 ,如下我们需求是当data对象的值发生改变时触发 updateView 更新视图 

<script> 
        function updateView(){
         console.log('模拟更新视图')
        }
        let data = {name:'zs'}
        data.name = 'lisi'
 </script>

2.3 思考:我们需要改变对象属性的时候触发 更新视图 方法 先拆解问题

    2.3.1 先判断监听的属性必须是对象属性,并且给对象中的属性添加监听

// 定义一个function 作用就是判断当前是否为对象 如果是对象则添加属性监听 
// 如果不是对象则返回原数据
function observer(target){
        if(typeof target !== 'object' || target ===  null ){
            retrun target
        }
        for( let key in target ){
            defineReactive(target,key,target[key])
        }
}

// 定义一个function 作用只对对象添加属性监听方法
function defineReactive(target,key,value){
        object.defineProperty(target,key,{
                get(){    
                    retrun value
                },
                set(newValue){
                    updateView() // 当新的值储存时更新视图
                    value = newValue
                }        

        })
}

    2.3.2 如果赋的值是个对象 获取初始化的值是个对象则

let data = {name:'zs',age:{n:18}}
data.age = {num:20}

添加对象监听方法 再执行一次 observer(value)

function defineReactive(target,key,value){
        +++ observer(value)
        object.defineProperty(target,key,{
                get(){    
                    retrun value
                },
                set(newValue){
                   +++ observer(newValue)
                    updateView()
                    value = newValue
                }        

        })
}

 其实本质还是个递归 

重点:由此可见vue2响应式一个缺陷

1.未定义的变量无法进行响应式的绑定

2.如果对象层级过深每次递归都往对象深层添加监听很消耗性能

 2.3.3 监听时进行数组操作

let data = {name:'zs',age:[1,2,3]}

data.push(4)

 数组操作的时候我们也需要更新页面

方法:在数组原型链上重写原型链方法添加重新刷新方法

Array.prototype 获取数组原型链方法
如果直接循环Array.prototype 添加方法会使数组的所有方法都将更新渲染页面,我们只需要特定方法执行 如 :push unshift shift 等

let oldArrayProtoType = Array.prototype // 获取原数组原型链

let proto = Object.create(oldArrayPrototype); //创建一个新的对象继承原数组原型链方法

['push','unshift','shift'].forEach(method=>{ // 将新的原型链中添加执行更新视图的方法并将当前this指针及参数传递
    protp[method] = function(){
        updateView()
        oldArrayProtoType[method].call(this,arguments)
    }
})

2.3.4 类型判断方法 observer 中添加数组判断并将新的原型链替换老原型链

function observer(target){
        if(typeof target !== 'object' && target ===  null ){
            retrun target
        }
+++    if(Array.isArray(target)){
+++         Object.setPrototypeOf(targer,proto)
+++     }
        for( let key in target ){
            defineReactive(target,key,target[key])
        }
}

data.push(4) 就可以触发更新视图方法了


Vue3响应式原理

vue2响应式的缺陷已经知道了那么vue3新解决方案 new Proxy()

vue3 中如果需要将属性变更为响应式属性需要使用到 reactive()  数据更新触发effect() 再次执行

<div id="app">
    <h5>{{state.title}}</h5>
</div>
<script src="https://unpkg.com/vue@next"></script>
import {reactive,effect,createApp} from 'Vue'

const app = createApp({
        setup(){
                let proxy = reactive({name:'zs'})
                effect(()=>{
                    console.log('通知视图更新',proxy.title)
                })
                proxy.name = 'ls'
                retrun {
                    state:proxy
                }
        }
})

app.mount('#app')

// effect 会执行两次 第一次是在页面初次渲染执行 第二次是数据发生变化后执行

1. 首先创建一个reactive 方法需要传入一个普通对象获得一个响应式对象

     1.1 reactive 返回 createReactiveObject方法 

// 这个方法需要返回的是个响应式对象
function reactive(target){
    retrun createReactiveObject(target)
}

// 创建响应式对象方法
createReactiveObject(target){
    
}

   1.2

        创建响应式方法思路

        1.2.1

                首先方法参数必须是一个对象 创建一个函数 isObject 专门用来判断值是否为对象

function isObject(val){
    retrun typeof val === 'object' && val !== null
}

// 如果是不是对象则直接返回
// 如果是对象新增proxy事件监听
createReactiveObject(target){
    if(!isObject(target)){
       return target
    }
    
    let baseHandle = {
        // target 原对象 key当前的变更或获取的key receiver 代理后的proxy对象
        get(target,key,receiver){
            let res = Reflect.get(target,key,receiver)
            console.log('获取')
            // 返回值如果是个对象那么再次执行 reactive(res)
            return isObject(res)?reactive(res):res
            //return res
        },
        set(target,key,value,receiver){
            let res = Reflect.set(target,key,value,receiver)
            console.log('写入')
            return res
        },
        deleteProperty(target,key,receiver){
            let res = Reflect.deleteProperty(target,key)
            console.log('删除')
            return res
        }
    } 
    
    let observer = new Proxy(target,baseHandle)
    return observer
}

/*
    Reflect es6 方法对象的操作方法不修改原对象,返回处理过后的新对象
    Reflect.set 方法返回的是Boolean值
*/

运行结果

proxy.name = 'ls'   proxy.name = [1,2,3]  proxy.name.push(4)

console.log(proxy.name)    //  写入方法执行 获取方法执行 可以成功打印修改后的值

无论字符还是数组方法都可以触发!

当数组执行

proxy.name = [1,2,3]  proxy.name.push(4)

// 会发现会触发两次写入方法

// 分析原因 首次写入的时候是给数组添加了一个新成员4 第二次写入是重写了数组的length

// 解决方法 判断对象中是否有这个属性,有就是修改 没有就是添加 并且添加的值如果和原值一致那就不修改 修改无意义

// 判断当前对象是是否包含了此属性
function hasOwn(target,key){
  return target.hasOwnProperty(key)
}
set(target,key,value,receiver){
            let res = Reflect.set(target,key,value,receiver)
            let oldValue = target[key]
            let hadkey = hasOwn(target,key)
            if(!hadkey){
                console.log('写入')
            }else if(oldValue !== value){
                console.log('修改')
            }
            retrun res
        },

  1.2.2 

        为防止已经代理过的方法再次代理 createReactiveObject 方法中增加判断限制

let toProxy = new WeakMap();
let toRaw = new WeakMap(); 

function createReactiveObject(target){
       // 防止多次代理找到后直接返回
     let proxy = toProxy.get(target);
      if(proxy){
        return proxy
      }
      if(toRaw.has(target)){
        return target;
      }
    ...
    // 在retrun之前储存代理过后的值
    
    let observed = new Proxy(target,baseHandle);
    toProxy.set(target,observed);
    toRaw.set(observed,target);
    return observed
}

// 防止如下情况产生
情况1
let proxy = reactive({name:1})
reactive(proxy)
情况2
let proxy = reactive({name:1})
reactive({name:1})

   1.3

effect 驱动页面进行更新

effect 首次加载执行一次 如果数据更新再执行一次

// effect 执行方法

let obj = reactive({name:'zs'})

effect(()=>{
  console.log('驱动执行',obj.name)
})

obj.name = 'ls'

        1.3.1 effect参数是个回调函数 并默认执行一次那么先创建effect方法

// effect方法中 创建动态驱动方法并默认执行一次

function effect(fn){
   let effect = createReactiveEffect(fn) 
   effect()
}

// 创建驱动方法
function createReactiveEffect(fn){
    let effect = function(){
        run(fn)
    }
    retrun effect
}

// 方法执行
funtion run(fn){
    fn()
}

  1.3.2 

栈储存effect执行的方法

  重点: effect更新时需要重新触发之前执行过的方法

              如果effect多次调用每个储存的方法都要执行一次

  思路: 每个方法用数组储存起来,当数据更新时从数组中取出并执行,遵循先进后出的原                则

// 用来存储effect中的方法 栈
let activeEffectStacks = [];

1.3.3

储存effect执行的方法

// 创建驱动方法
function createReactiveEffect(fn){
    let effect = function(){
     +++   run(effect,fn) // 将effect执行的方法传入到run方法中 并在执行时储存
    }
    retrun effect
}

// 方法执行
funtion run(effect,fn){
    +++ activeEffectStacks.push(effect); // 将effect中的参数方法储存
    fn()
    
}

1.3.4 

由于数据变化时 effect 执行的一定是 第一次执行时初始化 需要获取的数据,所以get方法一定会执行

所以在get 方法中 收集依赖 也就是将栈中的方法关联在对应的监听参数中

get(target,key,receiver){
          let res = Reflect.get(target,key,receiver)
          console.log('获取')
          // 收集依赖把当前的key和effect对应起来
      +++ track(target,key); // 如果目标上的key变化了重新让数组中的effect执行即可
          return isObject(res)?reactive(res):result // 设置递归
        },

let targetsMap = new WeakMap(); // 集合和hash表

// 当前的key和effect对应起来
// targetsMap 中储存格式 key 为当前的原对象 value 为一个新的Map
// Map 中 key为当前 get中指定的key value为 一个新的Set
// 利用Set 只能唯一 判断如果Set中没有当前effect栈的方法就添加进去

// 格式为
//'{name:'zs'}':{
//    name:[()=>{console.log('驱动执行',obj.name)}]
// }

function track(target,key){
    let effect = activeEffectStacks[activeEffectStacks.length-1]; //栈取最后一个
    
    if(effect){
        let depsMap = targetsMap.get(target)
        if(!depsMap){
            targetsMap.set(target,depsMap = new Map)
        }
        let deps =  depsMap.get(key)
        if(!deps){
            depsMap.set(key,deps = new Set())
        }
        if(!deps.has(effect)){
          deps.add(effect);
        }
    }
}

set 方法中当值发生改变时驱动储存的方法执行
 

// set 中
 if(!hadKey){
+++  trigger(target,'add',key);
     console.log('新增属性')
 }else if(oldValue !== value){
+++   trigger(target,'set',key);
     console.log('修改属性')
 }

// trigger 方法中查找 
function trigger(target,type,key){
    // 根据原对象找到对应的map
    // 根据set当前的key取出Set() 中的方法 并依次执行
    let depsMap = targetsMap.get(target);
      if(depsMap){
        let deps = depsMap.get(key);
        if(deps){ // 将当前可以对应的effect方法依次执行
          deps.forEach(effect => {
              effect();
          });
        }
      }
}

1.3.5 

run方法中储存方法 由于默认执行一次之后 栈中储存的方法也已经无用,方法已经绑定在了监听对象上,于是将run中储存的方法删除

// 防止方法储存执行出错新增了try finally 清除方法必定会执行
function run (effect,fn){ 
      try{
        activeEffectStacks.push(effect);
        fn();
      }finally{
        activeEffectStacks.pop();
      }
}

最后运行 值修改过后effect将会再次执行

    let obj = reactive({name:'zs'});

    effect(()=>{

      console.log('执行次数',obj.name);

    })

    obj.name = 'ls'

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值