vue源码解析个人笔记:
- 响应式原理-白话版
- 数据发生改变的时候, 视图会重新渲染, 匹配为最新的值
- 三个问题:
- vue是怎么知道数据改变的?
- 我:每个数据绑定了监听器(watcher)
- vue在数据改变时, 怎么知道通知哪些视图更新?
- 我:哪些视图依赖了这些数据的, 就需要更新
- vue在数据改变时, 视图怎么知道什么时候更新?
- 我:任务队列?事件更新的时间和顺序
- 我:依赖更新
- vue是怎么知道数据改变的?
- 三个回答:
- Object.defineProperty
- 为对象中的每一个属性设置get和set方法
- get: 一个函数, 当属性被访问时, 会触发
- set: 一个函数, 当属性被赋值时, 会触发
- 回答第一个问题, 当数据改变时, 触发属性的set方法, vue就知道数据有改变
- 依赖收集
- data中声明的的每个属性,都拥有一个数组, 保存着 依赖其的对象
- 例如 data:{name:'张三'}, 页面A引用了name: {{name}}, 那么就把页面A存在它的后宫中(这个页面依赖我)
- 除了页面, 还要computed/watch等等,这里统一用页面替代
- 它知道谁依赖它之后, 就可以在发生改变的时候, 通知 依赖它的页面, 从而让页面完成更新
- 总结:
- data中每个声明的属性, 都会有一个专属的依赖收集器subs
- 当页面使用到某个属性时, 页面的watcher就会放到依赖收集器subs中
- 数据是在什么时候进行依赖收集的呢? --当页面A读取了name时,会触发name 的get函数, 此时name就会保存页面A的watcher了
- 回答: 通知那些存在 依赖收集器中的视图
- 依赖更新
- 解释: 就是通知所有的依赖进行更新
- 什么时候进行依赖更新呢? Object.defineProperty - set
- 当name改变的时候,name会遍历自己的依赖收集器subs, 逐个通知watcher, 让watch完成更新(这里的name会通知页面A,页面A重新读取新的name,然后完成渲染)
- 回答: 在数据变化触发set函数的时候, 通知视图, 视图开始更新
- Object.defineProperty
- 简单总结
- Object.defineProperty - get, 用于依赖收集
- Object.defineProperty - set, 用于依赖更新
- 每个data 声明的属性, 都拥有一个专属的依赖收集器subs
- 依赖收集器subs保存的依赖是watcher
- watcher可用于进行视图更新
- 代理data -- 先放一下
- props-白话版
- props作为父传子的载体, 到底是怎么工作的?
- 三个问题:
- 父组件怎么传值给子组件的props
- 子组件如何读取props
- 我: 读属性?
- 父组件data更新, 子组件的props如何更新
- 我: 正常依赖更新?
- 场景设置:
new Vue({ el: '.a', name: 'A', components: { testb: { props: { childName:''}}, template: '
父组件传入的props的值{{childName}}
' }, data(){ return{ parentName: '我是父组件'}} }) - 三个回答:
- 父组件怎么传值给子组件的props
- props传值的设置
- 根组件A把自身的 parentName 绑定到子组件的属性 child-name上
- props父传子前 -- 渲染组件
- props开始赋值
- 模板渲染函数执行,执行时会绑定 父组件为作用域, 所以渲染函数内部所有的变量, 都会从父组件对象上去获取
- {attrs: {child-name: parentVm.parentName}}
- 函数执行了, 内部的 -c('testb') 第一个执行, 然后传入了 赋值后的 attrs, 即:
- {attrs: {child-name: '我是父组件'}}
- 模板渲染函数执行,执行时会绑定 父组件为作用域, 所以渲染函数内部所有的变量, 都会从父组件对象上去获取
- 子组件保存props
- 子组件拿到父组件赋值过后的attr, 而attrs包含普通属性和props, 所以需要筛选处props, 然后保存
- 子组件设置响应式props
- props 会被保存到实例的 _props中, 并且会逐一复制到 实例上, 并且每一个属性都会被设置为 响应式的
- props传值的设置
- 子组件如何读取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(老爸给钱你用, 你怎么用对老爸没有影响)
- 父组件数据变化, 子组件props如何更新
- 每一个实例都会存在一个专属watcher
- 用于实例自己更新视图
- 用于给 依赖的属性保存,然后属性变化的时候, 通知实例更新
- 以parentName为例
- parentName 会收集父组件的watcher
- 在回答1中, 父组件的parentName会被读取, 此时!因为parentName是响应式的, 所以parentName的get函数会被触发, 在get函数中, parentName会将父组件的watcher保存 到自己的依赖收集器中
- 父组件重新渲染, 重新赋值props
- parentName 改变之后, 触发parentname的set函数, 里面会通知 自己依赖收集器中的父组件的watcher
- 父组件watcher开始更新,重新开始渲染的步骤, 然后又重新走回答1中的第2步
- parentName 会收集父组件的watcher
- 每一个实例都会存在一个专属watcher
- 父组件怎么传值给子组件的props
- 总结:
- 父组件的data的值和子组件的props没有任何联系, 更改props不影响父组件data
- props也是响应式的, 跟data本质差不多
- props会访问转接, 赋值转接, 其实操作的是vm._props的属性
- computed-白话版
- 三个问题:
- computed也是响应式的
- 我: data修改会触发其watcher
- computed如何控制缓存
- 依赖的data变了, computed如何更新
- computed也是响应式的
- 三个回答:
- computed 会与Object.defineProperty关联起来
- 读取computed时, 会执行你设置的get函数, 但是并没有这么简单, 因为还有一层缓存的操作
- 赋值computed时, 会执行你设置的set函数, 会直接把set赋值给 Object.defineProperty - set
- computed 如何控制缓存
- computed 是有缓存的 --> 计算属性是基于它们的依赖进行缓存的, 计算属性只有在它的相关依赖发生改变时才会重新求值
- 为什么要缓存? 节省开销(性能), 如果有一个开销比较大的计算属性A, 需要遍历一个巨大的数组并作大量的计算, 如果没有缓存, 我们将不可避免的多次执行A的getter
- 判断是否使用缓存?
- 首先computed 计算后, 会把计算得到的值保存到一个变量中, 读取computed 时便直接返回这个变量.
- 当computed 更新时, 就会重新赋值更新这个变量
- 脏数据标志为 dirty (dirty是watcher的一个属性)
- dirty为true, 读取computed 会重新计算 ( 计算完成会将dirty设置为false, 以便于其他地方再次读取使用缓存, 免于计算)
- dirty为false, 读取computed会使用缓存
- 依赖的data变了, computed如何更新
-
场景设置: 页面A引用了 computed B,computed B 依赖了 data C , 像这样 A->B->C 的依赖顺序, 当data C 开始变化后...
-
通知 computed B watcher 更新, 其实只会重置 脏数据标志位 dirty=true, 不会计算值
-
通知 页面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)
- 所以如何更新? (被依赖通知更新后, 重置脏数据标志位, 页面读取computed再更新值)
-
-
- computed 会与Object.defineProperty关联起来
- 总结:
- computed 通过 watcher.dirty 控制是否读取缓存
- computed 会让 [data依赖] 收集到 [依赖computed的watcher], 从而data变化时, 会同时通知computed 和依赖computed 的地方
- 三个问题:
- watch-白话版
- 监听的数据改变的时,watch 如何工作
- watch 在一开始初始化的时候, 会读取一遍 监听的数据的值, 于是, 此时那个数据就收集到watch的 watcher了
- 然后 你给 watch 设置的 handler ,watch 会放入 watcher 的更新函数中
- 当 数据改变时,通知 watch 的 watcher 进行更新,于是 你设置的 handler 就被调用了
- 设置 immediate 时,watch 如何工作
- 设置了 immediate 时, 就不需要在数据改变的时候才会触发, 而是在初始化 watch 时, 在读取了 监听的数据的值之后, 便立即调用一遍你设置的监听回调, 然后传入刚读取的值
- 设置了 deep 时,watch 如何工作
- deep是用来深度监听的, (深度监听就是当你监听的属性是一个对象时, 如果你没有设置深度监听, 当对象内部变化时,你监听的回调是不会被触发的)
- watch 初始化的时候会先读取一遍监听数据的值
- 没有设置deep --> 因为读取了监听的data的属性, watch的 watcher 被收集在这个属性的 收集器中
- 设置了 deep
- 因为读取了 监听的data的属性, watch的 watcher 被收集在这个属性的收集器中
- 在读取data属性的时候, 发现设置了deep 而且值是一个对象, 会递归遍历这个值, 把内部所有属性逐个读取一遍, 于是属性和它的对象值内 每一个属性都会收集到watch 的watcher
- 于是, 无论对象嵌套多深的属性, 只要改变就会通知相应 watch的watcher 去更新, 于是你设置的watch 回调就被触发了.
- 监听的数据改变的时,watch 如何工作
- mixins-白话版
- mixins相当于封装, 提取公共部分
- 两个问题
- 什么时候合并
- 怎么合并
- 两个解答:
- 什么时候合并
- 在创建组件实例初始化之前, 会把 [全局选项] 和 [组件选项] 合并起来, 也就是说, 全局注册的选项, 其实会被[传递引用]到你的每个组件中
- [全局选项] --> 全局组件(Vue.component), 全局过滤器(Vue.filter), 全局指令(Vue.directive), 全局mixin(Vue.mixin)
- 在创建组件实例初始化之前, 会把 [全局选项] 和 [组件选项] 合并起来, 也就是说, 全局注册的选项, 其实会被[传递引用]到你的每个组件中
- 怎么合并
- 合并权重: [1]组件选项 --> [2]组件mixin --> [3] 组件-mixin-mixin --> [4]..省略无数可能存在的嵌套mixin --> [5]全局选项
- 什么时候合并
- v-model-白话版
- 双向绑定, 个人认为应该分为 [初始化绑定] 和[双向更新] 两个部分
- 四个问题:
- v-model怎么给表单绑定数据
- v-model绑定什么事件
- v-model怎么绑定事件
- v-model如何进行双向更新
- 结论
- 怎么赋值? --> v-model绑定的数据赋值给表单元素的value属性
- 怎么绑定事件? --> 解析不同表单元素, 配置相应的事件名和事件回调, 在插入dom之前, addEventListener 绑定上事件
- 怎么双绑? --> 外部变化, 触发事件回调, event.target.value 赋值给model绑定的数据; 内部变化, 修改表单元素属性value
- 以下面这个为例
new Vue({ el: document.getElementsByTagName("div")[0], data(){ return { name: '11111' } } })
- 四个回答:
- 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
- v-model 绑定什么事件?
- 不同的表单元素使用v-model, 会绑定不同的事件
- change事件: select/checkbox/radio
- input事件: 默认事件, 当不是上面三种表单元素时, 会解析成input事件, 比如text, number等input元素和textarea
- 不同的表单元素使用v-model, 会绑定不同的事件
- v-model怎么绑定事件?, 上面的例子解析成下面的渲染函数(已简化)
return _c('div',[ _c('input',{ on:{ 'input': function($event){ name = $event.target.value } } }) ])- 解析事件流程:
- 解析不同表单元素, 配置不同的事件
- 拼装事件回调函数, 不同表单元素, 回调不一样
- 把事件名和拼装回调配套保存给相应的表单元素的on事件储存器
- 什么时候绑定事件? --> 生成input dom之后, 插入input dom之前
- 怎么绑? 使用之前保存的事件名和事件回调, 给input dom像下面这样绑定上事件
- input.addEventListener('input',function(event){ name = event.target.value })
- 解析事件流程:
- v-model如何进行双向更新?
- 双向, 指的是内部和外部, 外部(用户手动改变表单值, 输入或选择), 内部(从内部修改绑定值)
- 内部变化:
- v-model 绑定了 name, name会收集到本组件的watcher
- 下面渲染函数执行时, 会绑定本身组件实例为上下文对象
- name访问的是组件实例的name
- name此时便手机到了当前正在渲染的组件实例, 当前渲染的实例是自己, 于是收集到了自身的watcher
return _c('div', [_c('input', domProps:{'value': (name) })])- 内部修改name的变化, 通知收集器内的watcher更新, 所以本组件会更新, 上面的渲染函数重新执行, 便 重新从实例读取name
- 重新给 input dom的value 赋值, 于是页面就更新了
- v-model 绑定了 name, name会收集到本组件的watcher
- 外部变化
- 手动改变表单, 事件触发, 执行事件回调(下面这个), 更新组件数据name function(event){ name = event.target.value }
- 更新内部数据流程
- 当事件触发的时候, 会把表单的值 赋值给 name
- name 是从组件实例上访问的
- 所以这次赋值会 直接改变组件实例的name
- 回调怎么赋值给组件实例的name?
- 当事件回调执行的时候, 会直接赋值给组件实例的name, 这样便通过外部改变了内部数据
- v-model怎么给表单绑定数据?
- 总结: v-model 三要素
-
绑定属性
-
绑定事件
-
属性+事件结合完成双向更新
-