当前位置:   article > 正文

Vue2 及 Vue3 响应式原理(手写简版源码解析)_阅读vue2和vue3源码

阅读vue2和vue3源码

Vue2响应式原理

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

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

第一个参数原目标对象

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

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

2.知道方法后

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

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

  1. <script> 
  2.         function updateView(){
  3.          console.log('模拟更新视图')
  4.         }
  5.         let data = {name:'zs'}
  6.         data.name = 'lisi'
  7.  </script>

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

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

  1. // 定义一个function 作用就是判断当前是否为对象 如果是对象则添加属性监听
  2. // 如果不是对象则返回原数据
  3. function observer(target){
  4.         if(typeof target !== 'object' || target ===  null ){
  5.             retrun target
  6.         }
  7.         for( let key in target ){
  8.             defineReactive(target,key,target[key])
  9.         }
  10. }
  11. // 定义一个function 作用只对对象添加属性监听方法
  12. function defineReactive(target,key,value){
  13.         object.defineProperty(target,key,{
  14.                 get(){    
  15. retrun value
  16.                 },
  17.                 set(newValue){
  18. updateView() // 当新的值储存时更新视图
  19. value = newValue
  20.                 }        
  21.         })
  22. }

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

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

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

  1. function defineReactive(target,key,value){
  2. +++ observer(value)
  3.         object.defineProperty(target,key,{
  4.                 get(){    
  5. retrun value
  6.                 },
  7.                 set(newValue){
  8. +++ observer(newValue)
  9. updateView()
  10. value = newValue
  11.                 }        
  12.         })
  13. }

 其实本质还是个递归 

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

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

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

 2.3.3 监听时进行数组操作

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

data.push(4)

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

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

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

  1. let oldArrayProtoType = Array.prototype // 获取原数组原型链
  2. let proto = Object.create(oldArrayPrototype); //创建一个新的对象继承原数组原型链方法
  3. ['push','unshift','shift'].forEach(method=>{ // 将新的原型链中添加执行更新视图的方法并将当前this指针及参数传递
  4. protp[method] = function(){
  5. updateView()
  6. oldArrayProtoType[method].call(this,arguments)
  7. }
  8. })

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

  1. function observer(target){
  2.         if(typeof target !== 'object' && target ===  null ){
  3.             retrun target
  4.         }
  5. +++ if(Array.isArray(target)){
  6. +++ Object.setPrototypeOf(targer,proto)
  7. +++ }
  8.         for( let key in target ){
  9.             defineReactive(target,key,target[key])
  10.         }
  11. }

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


Vue3响应式原理

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

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

  1. <div id="app">
  2. <h5>{{state.title}}</h5>
  3. </div>
  4. <script src="https://unpkg.com/vue@next"></script>
  5. import {reactive,effect,createApp} from 'Vue'
  6. const app = createApp({
  7.         setup(){
  8.                 let proxy = reactive({name:'zs'})
  9.                 effect(()=>{
  10.                     console.log('通知视图更新',proxy.title)
  11.                 })
  12.                 proxy.name = 'ls'
  13.                 retrun {
  14.                     state:proxy
  15.                 }
  16.         }
  17. })
  18. app.mount('#app')
  19. // effect 会执行两次 第一次是在页面初次渲染执行 第二次是数据发生变化后执行

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

     1.1 reactive 返回 createReactiveObject方法 

  1. // 这个方法需要返回的是个响应式对象
  2. function reactive(target){
  3. retrun createReactiveObject(target)
  4. }
  5. // 创建响应式对象方法
  6. createReactiveObject(target){
  7. }

   1.2

        创建响应式方法思路

        1.2.1

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

  1. function isObject(val){
  2. retrun typeof val === 'object' && val !== null
  3. }

  1. // 如果是不是对象则直接返回
  2. // 如果是对象新增proxy事件监听
  3. createReactiveObject(target){
  4. if(!isObject(target)){
  5. return target
  6. }
  7. let baseHandle = {
  8. // target 原对象 key当前的变更或获取的key receiver 代理后的proxy对象
  9. get(target,key,receiver){
  10. let res = Reflect.get(target,key,receiver)
  11. console.log('获取')
  12. // 返回值如果是个对象那么再次执行 reactive(res)
  13. return isObject(res)?reactive(res):res
  14. //return res
  15. },
  16. set(target,key,value,receiver){
  17. let res = Reflect.set(target,key,value,receiver)
  18. console.log('写入')
  19. return res
  20. },
  21. deleteProperty(target,key,receiver){
  22. let res = Reflect.deleteProperty(target,key)
  23. console.log('删除')
  24. return res
  25. }
  26. }
  27. let observer = new Proxy(target,baseHandle)
  28. return observer
  29. }
  30. /*
  31. Reflect es6 方法对象的操作方法不修改原对象,返回处理过后的新对象
  32. Reflect.set 方法返回的是Boolean值
  33. */

运行结果

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

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

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

  1.2.2 

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

  1. let toProxy = new WeakMap();
  2. let toRaw = new WeakMap();
  3. function createReactiveObject(target){
  4. // 防止多次代理找到后直接返回
  5. let proxy = toProxy.get(target);
  6. if(proxy){
  7. return proxy
  8. }
  9. if(toRaw.has(target)){
  10. return target;
  11. }
  12. ...
  13. // 在retrun之前储存代理过后的值
  14. let observed = new Proxy(target,baseHandle);
  15. toProxy.set(target,observed);
  16. toRaw.set(observed,target);
  17. return observed
  18. }
  19. // 防止如下情况产生
  20. 情况1
  21. let proxy = reactive({name:1})
  22. reactive(proxy)
  23. 情况2
  24. let proxy = reactive({name:1})
  25. reactive({name:1})

   1.3

effect 驱动页面进行更新

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

  1. // effect 执行方法
  2. let obj = reactive({name:'zs'})
  3. effect(()=>{
  4. console.log('驱动执行',obj.name)
  5. })
  6. obj.name = 'ls'

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

  1. // effect方法中 创建动态驱动方法并默认执行一次
  2. function effect(fn){
  3. let effect = createReactiveEffect(fn)
  4. effect()
  5. }
  6. // 创建驱动方法
  7. function createReactiveEffect(fn){
  8. let effect = function(){
  9. run(fn)
  10. }
  11. retrun effect
  12. }
  13. // 方法执行
  14. funtion run(fn){
  15. fn()
  16. }

  1.3.2 

栈储存effect执行的方法

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

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

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

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

1.3.3

储存effect执行的方法

  1. // 创建驱动方法
  2. function createReactiveEffect(fn){
  3. let effect = function(){
  4. +++ run(effect,fn) // 将effect执行的方法传入到run方法中 并在执行时储存
  5. }
  6. retrun effect
  7. }
  8. // 方法执行
  9. funtion run(effect,fn){
  10. +++ activeEffectStacks.push(effect); // 将effect中的参数方法储存
  11. fn()
  12. }

1.3.4 

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

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

  1. get(target,key,receiver){
  2. let res = Reflect.get(target,key,receiver)
  3. console.log('获取')
  4. // 收集依赖把当前的key和effect对应起来
  5. +++ track(target,key); // 如果目标上的key变化了重新让数组中的effect执行即可
  6. return isObject(res)?reactive(res):result // 设置递归
  7. },
  8. let targetsMap = new WeakMap(); // 集合和hash表
  9. // 当前的key和effect对应起来
  10. // targetsMap 中储存格式 key 为当前的原对象 value 为一个新的Map
  11. // Map 中 key为当前 get中指定的key value为 一个新的Set
  12. // 利用Set 只能唯一 判断如果Set中没有当前effect栈的方法就添加进去
  13. // 格式为
  14. //'{name:'zs'}':{
  15. // name:[()=>{console.log('驱动执行',obj.name)}]
  16. // }
  17. function track(target,key){
  18. let effect = activeEffectStacks[activeEffectStacks.length-1]; //栈取最后一个
  19. if(effect){
  20. let depsMap = targetsMap.get(target)
  21. if(!depsMap){
  22. targetsMap.set(target,depsMap = new Map)
  23. }
  24. let deps = depsMap.get(key)
  25. if(!deps){
  26. depsMap.set(key,deps = new Set())
  27. }
  28. if(!deps.has(effect)){
  29. deps.add(effect);
  30. }
  31. }
  32. }

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

  1. // set 中
  2. if(!hadKey){
  3. +++ trigger(target,'add',key);
  4. console.log('新增属性')
  5. }else if(oldValue !== value){
  6. +++ trigger(target,'set',key);
  7. console.log('修改属性')
  8. }
  9. // trigger 方法中查找
  10. function trigger(target,type,key){
  11. // 根据原对象找到对应的map
  12. // 根据set当前的key取出Set() 中的方法 并依次执行
  13. let depsMap = targetsMap.get(target);
  14. if(depsMap){
  15. let deps = depsMap.get(key);
  16. if(deps){ // 将当前可以对应的effect方法依次执行
  17. deps.forEach(effect => {
  18. effect();
  19. });
  20. }
  21. }
  22. }

1.3.5 

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

  1. // 防止方法储存执行出错新增了try finally 清除方法必定会执行
  2. function run (effect,fn){
  3. try{
  4. activeEffectStacks.push(effect);
  5. fn();
  6. }finally{
  7. activeEffectStacks.pop();
  8. }
  9. }

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

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

    effect(()=>{

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

    })

    obj.name = 'ls'

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/248149?site
推荐阅读
相关标签
  

闽ICP备14008679号