vue阅读解析相关--持卡看得开奥斯卡打瞌睡杜卡迪卡看到

118 阅读11分钟

vue源码解析个人笔记:

  1. 响应式原理-白话版
    • 数据发生改变的时候, 视图会重新渲染, 匹配为最新的值
    • 三个问题:
      1. vue是怎么知道数据改变的?
        • 我:每个数据绑定了监听器(watcher)
      2. vue在数据改变时, 怎么知道通知哪些视图更新?
        • 我:哪些视图依赖了这些数据的, 就需要更新
      3. vue在数据改变时, 视图怎么知道什么时候更新?
        • 我:任务队列?事件更新的时间和顺序
        • 我:依赖更新
    • 三个回答:
      1. Object.defineProperty
        • 为对象中的每一个属性设置get和set方法
        • get: 一个函数, 当属性被访问时, 会触发
        • set: 一个函数, 当属性被赋值时, 会触发
        • 回答第一个问题, 当数据改变时, 触发属性的set方法, vue就知道数据有改变
      2. 依赖收集
        • data中声明的的每个属性,都拥有一个数组, 保存着 依赖其的对象
        • 例如 data:{name:'张三'}, 页面A引用了name:
          {{name}}
          , 那么就把页面A存在它的后宫中(这个页面依赖我)
          • 除了页面, 还要computed/watch等等,这里统一用页面替代
        • 它知道谁依赖它之后, 就可以在发生改变的时候, 通知 依赖它的页面, 从而让页面完成更新
        • 总结:
          1. data中每个声明的属性, 都会有一个专属的依赖收集器subs
          2. 当页面使用到某个属性时, 页面的watcher就会放到依赖收集器subs中
          3. 数据是在什么时候进行依赖收集的呢? --当页面A读取了name时,会触发name 的get函数, 此时name就会保存页面A的watcher了
        • 回答: 通知那些存在 依赖收集器中的视图
      3. 依赖更新
        • 解释: 就是通知所有的依赖进行更新
        • 什么时候进行依赖更新呢? Object.defineProperty - set
          1. 当name改变的时候,name会遍历自己的依赖收集器subs, 逐个通知watcher, 让watch完成更新(这里的name会通知页面A,页面A重新读取新的name,然后完成渲染)
        • 回答: 在数据变化触发set函数的时候, 通知视图, 视图开始更新
    • 简单总结
      1. Object.defineProperty - get, 用于依赖收集
      2. Object.defineProperty - set, 用于依赖更新
      3. 每个data 声明的属性, 都拥有一个专属的依赖收集器subs
      4. 依赖收集器subs保存的依赖是watcher
      5. watcher可用于进行视图更新
  2. 代理data -- 先放一下
  3. props-白话版
    • props作为父传子的载体, 到底是怎么工作的?
    • 三个问题:
      1. 父组件怎么传值给子组件的props
      2. 子组件如何读取props
        • 我: 读属性?
      3. 父组件data更新, 子组件的props如何更新
        • 我: 正常依赖更新?
    • 场景设置:
      new Vue({ el: '.a', name: 'A', components: { testb: { props: { childName:''}}, template: '

      父组件传入的props的值{{childName}}

      ' }, data(){ return{ parentName: '我是父组件'}} })
    • 三个回答:
      1. 父组件怎么传值给子组件的props
        1. props传值的设置
          • 根组件A把自身的 parentName 绑定到子组件的属性 child-name上
        2. props父传子前 -- 渲染组件
        3. props开始赋值
          • 模板渲染函数执行,执行时会绑定 父组件为作用域, 所以渲染函数内部所有的变量, 都会从父组件对象上去获取
            • {attrs: {child-name: parentVm.parentName}}
          • 函数执行了, 内部的 -c('testb') 第一个执行, 然后传入了 赋值后的 attrs, 即:
            • {attrs: {child-name: '我是父组件'}}
        4. 子组件保存props
          • 子组件拿到父组件赋值过后的attr, 而attrs包含普通属性和props, 所以需要筛选处props, 然后保存
        5. 子组件设置响应式props
          • props 会被保存到实例的 _props中, 并且会逐一复制到 实例上, 并且每一个属性都会被设置为 响应式的
      2. 子组件如何读取props
        • 子组件保存了 父组件传入的数据
        • prop的数据会被逐一复制到vm对象上(子组件的实例this)上, 复制的同时会对每个属性, 设置get和set函数, 进行访问转接和赋值转接 Object.defineProperty(vm, key, { get(){ return this._props[key] }, set(val){ this._props[key] = val } }) 以上面的举例: Object.defineProperty(childVm, childName, { get(){ return this._props[childName]}, set(val){ this._props[childName] = val } })
        • 访问嫁接 --> 你访问props其中一个值 vm.childName, 其实访问的是vm._props.childName
        • 赋值转接 --> 你赋值vm.childName = 5, 其实是赋值 vm._props.childName = 5, 且不会影响到父组件的data(老爸给钱你用, 你怎么用对老爸没有影响)
      3. 父组件数据变化, 子组件props如何更新
        • 每一个实例都会存在一个专属watcher
          1. 用于实例自己更新视图
          2. 用于给 依赖的属性保存,然后属性变化的时候, 通知实例更新
        • 以parentName为例
          1. parentName 会收集父组件的watcher
            • 在回答1中, 父组件的parentName会被读取, 此时!因为parentName是响应式的, 所以parentName的get函数会被触发, 在get函数中, parentName会将父组件的watcher保存 到自己的依赖收集器中
          2. 父组件重新渲染, 重新赋值props
            • parentName 改变之后, 触发parentname的set函数, 里面会通知 自己依赖收集器中的父组件的watcher
            • 父组件watcher开始更新,重新开始渲染的步骤, 然后又重新走回答1中的第2步
    • 总结:
      1. 父组件的data的值和子组件的props没有任何联系, 更改props不影响父组件data
      2. props也是响应式的, 跟data本质差不多
      3. props会访问转接, 赋值转接, 其实操作的是vm._props的属性
  4. computed-白话版
    • 三个问题:
      1. computed也是响应式的
        • 我: data修改会触发其watcher
      2. computed如何控制缓存
      3. 依赖的data变了, computed如何更新
    • 三个回答:
      1. computed 会与Object.defineProperty关联起来
        • 读取computed时, 会执行你设置的get函数, 但是并没有这么简单, 因为还有一层缓存的操作
        • 赋值computed时, 会执行你设置的set函数, 会直接把set赋值给 Object.defineProperty - set
      2. computed 如何控制缓存
        • computed 是有缓存的 --> 计算属性是基于它们的依赖进行缓存的, 计算属性只有在它的相关依赖发生改变时才会重新求值
        • 为什么要缓存? 节省开销(性能), 如果有一个开销比较大的计算属性A, 需要遍历一个巨大的数组并作大量的计算, 如果没有缓存, 我们将不可避免的多次执行A的getter
        • 判断是否使用缓存?
          1. 首先computed 计算后, 会把计算得到的值保存到一个变量中, 读取computed 时便直接返回这个变量.
          2. 当computed 更新时, 就会重新赋值更新这个变量
          3. 脏数据标志为 dirty (dirty是watcher的一个属性)
            • dirty为true, 读取computed 会重新计算 ( 计算完成会将dirty设置为false, 以便于其他地方再次读取使用缓存, 免于计算)
            • dirty为false, 读取computed会使用缓存
      3. 依赖的data变了, computed如何更新
        • 场景设置: 页面A引用了 computed B,computed B 依赖了 data C , 像这样 A->B->C 的依赖顺序, 当data C 开始变化后...

          1. 通知 computed B watcher 更新, 其实只会重置 脏数据标志位 dirty=true, 不会计算值

          2. 通知 页面A watcher 进行更新渲染, 进而重新 读取 computed B, 然后computed B 开始重新计算

             --通知重置标志位--> computed B watcher <-- 
            

          dataC --> | 重新读取 computed, computed重新计算并返回 --通知更新渲染--> 页面A watcher <---------

          • 为什么data C 能通知页面A? (data C的依赖收集器会同时收集到 computed B 和 页面A 的watcher)
          • 为什么data C 能收集到页面A 的watcher? (computed 其实是一个月老, 页面A 在读取computed B的时候, computed B把页面A 介绍给了 data C, 于是它两间接牵到了一起, 于是data C 就会收集到页面A 的watcher)
          1. 所以如何更新? (被依赖通知更新后, 重置脏数据标志位, 页面读取computed再更新值)
    • 总结:
      1. computed 通过 watcher.dirty 控制是否读取缓存
      2. computed 会让 [data依赖] 收集到 [依赖computed的watcher], 从而data变化时, 会同时通知computed 和依赖computed 的地方
  5. watch-白话版
    1. 监听的数据改变的时,watch 如何工作
      • watch 在一开始初始化的时候, 会读取一遍 监听的数据的值, 于是, 此时那个数据就收集到watch的 watcher了
      • 然后 你给 watch 设置的 handler ,watch 会放入 watcher 的更新函数中
      • 当 数据改变时,通知 watch 的 watcher 进行更新,于是 你设置的 handler 就被调用了
    2. 设置 immediate 时,watch 如何工作
      • 设置了 immediate 时, 就不需要在数据改变的时候才会触发, 而是在初始化 watch 时, 在读取了 监听的数据的值之后, 便立即调用一遍你设置的监听回调, 然后传入刚读取的值
    3. 设置了 deep 时,watch 如何工作
      • deep是用来深度监听的, (深度监听就是当你监听的属性是一个对象时, 如果你没有设置深度监听, 当对象内部变化时,你监听的回调是不会被触发的)
      • watch 初始化的时候会先读取一遍监听数据的值
        1. 没有设置deep --> 因为读取了监听的data的属性, watch的 watcher 被收集在这个属性的 收集器中
        2. 设置了 deep
          • 因为读取了 监听的data的属性, watch的 watcher 被收集在这个属性的收集器中
          • 在读取data属性的时候, 发现设置了deep 而且值是一个对象, 会递归遍历这个值, 把内部所有属性逐个读取一遍, 于是属性和它的对象值内 每一个属性都会收集到watch 的watcher
          • 于是, 无论对象嵌套多深的属性, 只要改变就会通知相应 watch的watcher 去更新, 于是你设置的watch 回调就被触发了.
  6. mixins-白话版
    • mixins相当于封装, 提取公共部分
    • 两个问题
      1. 什么时候合并
      2. 怎么合并
    • 两个解答:
      1. 什么时候合并
        • 在创建组件实例初始化之前, 会把 [全局选项] 和 [组件选项] 合并起来, 也就是说, 全局注册的选项, 其实会被[传递引用]到你的每个组件中
          • [全局选项] --> 全局组件(Vue.component), 全局过滤器(Vue.filter), 全局指令(Vue.directive), 全局mixin(Vue.mixin)
      2. 怎么合并
        • 合并权重: [1]组件选项 --> [2]组件mixin --> [3] 组件-mixin-mixin --> [4]..省略无数可能存在的嵌套mixin --> [5]全局选项
  7. v-model-白话版
    • 双向绑定, 个人认为应该分为 [初始化绑定] 和[双向更新] 两个部分
    • 四个问题:
      1. v-model怎么给表单绑定数据
      2. v-model绑定什么事件
      3. v-model怎么绑定事件
      4. v-model如何进行双向更新
    • 结论
      1. 怎么赋值? --> v-model绑定的数据赋值给表单元素的value属性
      2. 怎么绑定事件? --> 解析不同表单元素, 配置相应的事件名和事件回调, 在插入dom之前, addEventListener 绑定上事件
      3. 怎么双绑? --> 外部变化, 触发事件回调, event.target.value 赋值给model绑定的数据; 内部变化, 修改表单元素属性value
    • 以下面这个为例
      new Vue({ el: document.getElementsByTagName("div")[0], data(){ return { name: '11111' } } })
    • 四个回答:
      1. v-model怎么给表单绑定数据?
            return _c('div', [
                _c('input',
                    domProps:{'value':name})
            ])
        
      • 首先访问组件实例, 其中 name 的获取会先从组件实例上获取, 相当于vm.name, 赋值完成后, domProps就是下面这样 { domProps:{value: 111}}
      • 绑定值流程: 创建dom input之后, 插入dom input之前, 遍历该input 的domProps, 逐个添加给input dom, 简化后如下: for(var i in domProps){ input[i] = domProps[i] } 其中给节点赋值就是: input.value = 111
      1. v-model 绑定什么事件?
        • 不同的表单元素使用v-model, 会绑定不同的事件
          • change事件: select/checkbox/radio
          • input事件: 默认事件, 当不是上面三种表单元素时, 会解析成input事件, 比如text, number等input元素和textarea
      2. v-model怎么绑定事件?, 上面的例子解析成下面的渲染函数(已简化)
            return _c('div',[
                _c('input',{
                    on:{
                        'input': function($event){
                            name = $event.target.value
                        }
                    }
                })
            ])
        
        • 解析事件流程:
          1. 解析不同表单元素, 配置不同的事件
          2. 拼装事件回调函数, 不同表单元素, 回调不一样
          3. 把事件名和拼装回调配套保存给相应的表单元素的on事件储存器
          4. 什么时候绑定事件? --> 生成input dom之后, 插入input dom之前
          5. 怎么绑? 使用之前保存的事件名和事件回调, 给input dom像下面这样绑定上事件
            • input.addEventListener('input',function(event){ name = event.target.value })
      3. v-model如何进行双向更新?
        • 双向, 指的是内部和外部, 外部(用户手动改变表单值, 输入或选择), 内部(从内部修改绑定值)
        • 内部变化:
          1. v-model 绑定了 name, name会收集到本组件的watcher
            • 下面渲染函数执行时, 会绑定本身组件实例为上下文对象
            • name访问的是组件实例的name
            • name此时便手机到了当前正在渲染的组件实例, 当前渲染的实例是自己, 于是收集到了自身的watcher
              return _c('div', [_c('input', 
                  domProps:{'value': (name)
              })])
          
          1. 内部修改name的变化, 通知收集器内的watcher更新, 所以本组件会更新, 上面的渲染函数重新执行, 便 重新从实例读取name
          2. 重新给 input dom的value 赋值, 于是页面就更新了
        • 外部变化
          1. 手动改变表单, 事件触发, 执行事件回调(下面这个), 更新组件数据name function(event){ name = event.target.value }
        • 更新内部数据流程
          1. 当事件触发的时候, 会把表单的值 赋值给 name
          2. name 是从组件实例上访问的
          3. 所以这次赋值会 直接改变组件实例的name
        • 回调怎么赋值给组件实例的name?
          1. 当事件回调执行的时候, 会直接赋值给组件实例的name, 这样便通过外部改变了内部数据
    • 总结: v-model 三要素
      1. 绑定属性

      2. 绑定事件

      3. 属性+事件结合完成双向更新