vue数据劫持
defineProperty 更新数组原型 dep
1.首先用户通过new Vue传入一个配置对象, vue源码内部会将这个配置对象添加到当前vue实例的$options里面, 执行_init initState(处理所有配置项) initData(处理data选项)
2.initData方法看一下当前传入的是一个函数还是一个对象, 如果是函数的会进行调用, 拿到返回的data对象, 之后调用observe方法
3.observe方法主要就是用来判断当前传入的是不是一个对象, 如果不是对象就终止执行, 如果是一个对象就调用new Observer来实现数据劫持
4.observer类会拿到当前传入的对象, 然后判断当前传进来的对象是一个对象还是一个数组
如果是对象
1.调用类中的一个方法, 这个方法会遍历对象, 拿到对象中的每一个属性名和属性值, 然后调用defineReactive方法, 会将vue实例, 属性名, 属性值全部传进去
2.执行defineReactive方法, 这个方法首先会拿到这个属性值, 执行observe方法, 将属性值传进去(这个主要就是将属性值里面的对象继续进行数据劫持操作) 还会创建出每一个数据所对应的dep 之后会通过Object.defineProperty来实现
Object.defineProperty里面的get方法会收集当前的watcher, 主要是通过dep.depend将dep和watcher双向收集(这块后续再说), 还会拿到刚才调用observe方法返回的observer的实例, 通过observer的实例.dep拿到每一个对象所对应的dep然后调用depend方法收集当前的watcher(如果属性值是一个数组, 还会继续去遍历每一项, 每一项是对象的话, 就会拿到对象的__ob__, 为每一个对象都收集依赖, 是数组的话, 就继续递归添加依赖)
Object.defineProperty里面的set方法会通过watcher进行更新, 主要是通过dep.notify(拿到当前dep所对应的所有watcher, 调用他们的get方法, 后续再说)
如果是数组的话
就将传进来的对象数据的__proto__修改成这个新创建出来的对象, 还会执行observeArray方法, 这个方法主要就是用来遍历数组, 数组中的每一项都调用observe方法进行数据劫持
新创建出来的对象
新创建出来的对象的_proto_指向Array.prototype
之后又重写了修改数组本身的七个方法, 也就是在这个新创建的对象身上增加七个方法, 这七个方法就可以被我们内部检测到了
七个方法主要是push shift unshift reverse pop splice sort
如果方法是push unshift splice这三个的话说明有可能有新增加的数据, 而这些新增加的数据就需要判断是不是对象, 是对象继续添加数据劫持, 我们可以在用户调用这三个方法时, 拿到传进来的参数, 将其组装成一个数组,
组装成数组之后, 那这里其实就需要执行observeArray方法了, 那怎么拿到observeArray方法呢, 这个方法是放在observer实例上面, 我们可以在Observer的constructor里面拿到传进来的对象, 向对象身上增加一个__ob__属性, 这样每一个对象身上就有所对应的observer实例了, 那我们就可以this.__ob__.observeArray拿到遍历的这个方法了
接着调用这个方法, 把从这三个方法截取到的新增的数据传进去, 这样即使新增的是对象, 也可以实现数据劫持了
之后我们就需要执行每一个对象所对应的dep.notify通知watcher更新, 这样页面就会进行变化, 但是dep哪来的呢, 我们并没有给每一个对象都添加dep, 这个时候就需要我们在Observer的constructor里面取新创建一个dep出来, 这个dep挂到observer实例上面
我们就可以通过this.__ob__.dep.notify去通知页面更新了
5.这个就是vue实现数据劫持的基本原理了
vue模板渲染之生成虚拟dom
1.vue模板渲染主要分为三部分 一是将template模板转换成ast 二是将ast转换成特定格式的字符串 三是将字符串放到function 里面通过with(this), 修改字符串里面的方法的this以及变量从哪里取值, 这个function就是生成的虚拟dom函数
template模板转ast
1.vue内部先保存一个mount方法,之后再重写mount方法, 之后再重写mount方法,之后再重写mount方法, 重写的这个mount方法会获取到我们传入的配置对象的template,看下template有没有值,如果有值就使用template作为模版字符串,如果template没有值,就使用el所挂载的dom元素里面所对应的字符串作为模板字符串,准备好模板字符串之后就调用将模板字符串转换成ast的方法,其实就是调用保存的mount方法会获取到我们传入的配置对象的template, 看下template有没有值, 如果有值就使用template作为模版字符串, 如果template没有值, 就使用el所挂载的dom元素里面所对应的字符串作为模板字符串, 准备好模板字符串之后就调用将模板字符串转换成ast的方法, 其实就是调用保存的mount方法会获取到我们传入的配置对象的template,看下template有没有值,如果有值就使用template作为模版字符串,如果template没有值,就使用el所挂载的dom元素里面所对应的字符串作为模板字符串,准备好模板字符串之后就调用将模板字符串转换成ast的方法,其实就是调用保存的mount方法
2.将template转换成ast, vue2内部实际上是通过正则匹配不断去删除template字符串所实现的,会有匹配开始标签 匹配属性 匹配> 匹配结束标签的正则
1.搞一个while循环, 只要template有值就不会退出, 创建一个root对象, 默认为空, 每一个ast对象, 这个ast对象的结构大概是一个对象, 对象里面有tag当前标签的名字, props所对应的属性, type类型 1表示当前是普通的html元素 2表示是一个文本元素 children当前元素下的子元素, 还会准备一个数组, 这个数组里面主要保存当前正在匹配的html元素对象, 匹配一次添加一个, 匹配到</, 删除
2. while开始循环, 首先判断当前这个template字符串是否以<开头, 如果以<开头则证明有两种情况, 一种是匹配到开始标签, 另一个是匹配到结束标签, 匹配到开始标签, 创建一个对象里面有tag, type props children等 继续匹配是否符合匹配开始标签的正则, 匹配成功之后就拿到当前匹配的html元素了, 拿到之后将其作为对象的tag, 之后将匹配开始标签成功所对应的一截字符删除掉, 之后开始与匹配属性的正则进行匹配, 匹配成功之后, 我们会拿到一个个的class="a"这种格式, 按照=进行拆分, 之后将其添加到对象的props里面
, 然后与匹配>的正则匹配, 匹配成功, 将字符串的>删除, 匹配到这里的时候, 我们其实就拿到了一个对象, 我们先看一下外面的root对象是不是空的, 如果是空的, 将将这个对象赋给外面的root, 并且将外面的数组里面push上当前这个对象, 当前这次while循环结束
继续开始下一个循环, 此时template字符串已经截掉一截, 之后开始继续匹配, 以<开头, 匹配开标签正则匹配成功, 创建一个ast对象设置对象的tag和type 匹配属性正则成功将其添加到ast对象的props里面, 然后将这个对象添加到外部的数组里面, 然后看一下这个数组里面的这个对象前面是否有对象存在, 如果有就表示, 这个对象是数组的前一个对象的children, 拿到前一个对象, 将当前这个对象添加到前一个对象的children里面,
循环结束继续开始下一个循环, 下一次进来之后, 匹配不是以<这个开头, 说明现在这个字符串和<中间有东西, 而这块空间里面其实就是一个文本, 之后创建一个ast对象 type2 children text属性里面就保存当前的这段内容, 也是添加完之后就将template的这段内容删除掉,
循环结束继续开始下一个循环 进来之后, 匹配<开头, 匹配成功了, 然后与开始标签正则进行匹配, 匹配失败了, 随后与匹配结束标签正则匹配, 匹配成功了, 说明当前正是结束标签, 之后将template所对应的这段删除掉, 将外部的数组里面的最后一项删除, 这次循环结束进行下一次循环
下一次循环进来, 匹配开头, 匹配成功了, 匹配结束标签, 匹配成功了, 外部数组的最后一项删除, 此时template字符串已经变成空字符串了, while循环结束, 就得到了一棵ast
ast转特定格式的字符串
1. 特定格式的字符串实际上就是指将html元素对象使用_c来调用, 将文本元素对象使用_v来调用, 将双大括号中间的变量, 用_s来调用
_c创建一个虚拟dom,虚拟dom对象就是一个普通的js对象, 里面包含了vm(vue实例) tag data key children等属性 _v就是创建一个普通的文本对象, 里面包含了vm, text等属性, _s主要就是用来将变量进行json.stringify一下, 因为如果变量 数组等在页面上展示时如果不调用_s方法就会直接调用数组 对象的toString方法, 那展示出来的数据我们其实就看不到了
2. 创建一个函数, 将ast对象传进去, 拿到ast对象之后, 先定义一个_c()类似于这种的字符串, 之后取到ast的tag作为_c的第一个参数, 取到ast的props作为第二个参数, 接着看一下ast的children数组里面有没有值,
如果有值就创建一个新函数, 这个新函数就是专门用来遍历children的, 取到children的每一项, 判断type是1还是2 是1的话证明是一个html元素对象, 需要调用_c方法(这里调用就只是放在字符串里而已, 并不是真正的调用),
如果是2的话就调用_s方法, 但是这里并不是单纯调用_s方法就好了, 也需要判断字符串里面有没有存在{{}}双大括号, 如果存在双大括号的话, 就需要通过正则的方法将双大括号替换掉, 将里面的变量使用_s包调用
如果数组有一项有children的话, 就需要继续递归去执行这个遍历数组的方法, 返回生成好的字符串
字符串生成render函数
字符串生成render函数, 其实就是通过new Function() { with(this); 字符串 }的这种方式创建一个新的函数, with this到时候里面的变量都会去这个this上面取值 双大括号中的变量了 _c _s _v这种都是去这个this上面q取, 最终这个render函数会被放到vue的实例上面
那些变量通过Object.defineProperty将data中的响应式数据定义到vue实例上面, 到时候就会去vue实例上面找, 而vue实例就会去data里面找, data里面找到的就是响应式的数据
vue模板之虚拟dom转真实dom
1.虚拟dom转真实dom主要是依托于watcher来实现的, 渲染watcher 会将vm._update(vm._render())作为watcher的gettter方法进行执行
2.vue挂载时会先将template转换成render函数, 之后创建渲染watcher
4.创建渲染watcher会执行Watcher的constructor, constructor内部会将其作为watcher的gettter方法, 随后执行watcher的get方法
5.get方法主要做两件事情首先是将当前活跃的wathcer作为Dep.target的值, 之后执行getter方法, 执行完getter方法执行, 会将Dep.target变成null
6.getter方法执行getter方法就是vm._update(vm._render()), 这里主要分为两部分, 一是调用_render生成对应的虚拟dom 二是调用_update将虚拟dom转成真实dom
一是调用_render生成对应的虚拟dom, 这个其实就是执行_c _v等我刚才说的函数, 执行完之后就会返回一个虚拟dom
二是调用_update将虚拟dom转成真实dom, _update拿到这个虚拟dom之后, 实例内部有一个好像_prevnode变量, 这个变量主要就是用来记录上一次执行render函数生成的虚拟dom的, 如果这个值为空, 则表示这是第一次执行, 第一次执行我们就直接将虚拟dom转换成真实dom就好了
首先我们创建一个函数, 这个函数接收一个虚拟dom, 将虚拟dom传进来之后, 拿到虚拟dom的tag, 之后根据tag创建出对应的真实dom, 获取到虚拟dom的data(这个data就是属性), 遍历这个data, 然后不断真实dom身上添加属性(如果这个属性是style的话, 还需要特殊处理一下),
之后看下虚拟dom是否有children属性, 如果有就遍历children得到每一个虚拟dom, 继续递归执行这个函数将每一个虚拟dom传递进去, 生成好对应的真实dom之后, 再将其插入到其父虚拟dom所对应的真实dom里面, 这样经过递归操作之后, 虚拟dom就转换成真实dom了
7.真实dom创建完毕之后, 下一步就是挂载了, 我们在new Vue的时候会传入一个配置对象, 配置对象中有el属性, 我们就可以通过el拿到真实的dom节点, 然后将生成的真实dom插入到真实dom的前面, 之后再把el所对应的真实dom给删除调用, 还要把当前生成的虚拟dom放到_prevnode身上, 这样vue初次挂载就完成了
vue之新旧虚拟dom对比
1.经过刚才的一系列步骤, 页面已经可以正常展示数据了, 现在我们再综合上面的说法重新分析一下
$mount执行, 创建渲染watcher, Dep.target的值就是当前的渲染watcher, 执行watcher的getter方法 getter方法执行 vue实例的_render方法执行, _render方法执行data中数据所对应的get方法执行, get方法执行 dep与watcher相互绑定, 这样页面展示数据成功
2.数据改变之后会触发数据所对应的set方法, set方法执行dep.notify进行通知 watcher的get方法会重新执行(这里其实是放到异步任务里面执行的后续再说吧), get方法一执行重新执行_render函数, 生成最新的虚拟dom, 之后执行_update方法
3._update方法内部, 我刚才说了内部保存一个_prevnode之前的虚拟dom, _prevnode有值就会调用新旧虚拟dom对比的方法
4.新旧虚拟dom对比, 数据变化, vue生成了最新的虚拟dom, 这个时候就需要比较, 比较的时候其实是这样, 先判断当前旧虚拟dom和新虚拟dom的tag和key是否一致
如果不一致, 就证明旧的虚拟dom已经没有用了, 就直接根据新虚拟dom生成真实的dom节点, 将其插入到旧虚拟dom所对应的真实dom的前面, 然后将旧的虚拟dom所对应的真实dom删除掉
如果一致, 再判断新旧虚拟dom所对应的属性是否一致, 判断的过程中会以新虚拟dom的属性为准, 如果有不一样的属性, 拿到旧虚拟dom所对应的真实dom, 然后给真实dom添加最新的属性值
新旧虚拟dom属性比较完毕, 这个时候就需要对比children了
旧虚拟dom没有childern, 新虚拟dom有childern, 证明以前没有现在有了, 就需要遍历新虚拟dom的childern, 将其转换成真实dom, 插入到的旧虚拟dom的真实dom里面
旧虚拟dom有childern, 新虚拟dom没有childern 证明以前有现在没有了, 那就需要将旧虚拟dom所对应的真实dom的子真实dom全部删除掉
新旧虚拟dom都有childern, 这个时候我们可以重新创建一个函数处理新旧虚拟dom都有childern的情况(这里其实就是diff算法了)
首先会创建oldstartindex为0, oldendindex为旧childern的长度减一, olstartVnode为旧childern[oldstartindex], oldendvnode为旧children[oldendindex] newstartindex为0 , newendindex为新childern的长度减一, newstartvnode为新childern[newstartindex] newendvnode为新children[newendindex]
准备好这个8个变量, 之后会搞一个while循环, 当旧开始节点的下标大于旧结束节点的下标或者新开始节点的下标大于新结束节点的下标时终止(也就是oldestartindex>oldendindex || newstartindex>newendindex)结束
旧前新前 旧后新后 旧前新后 旧后新前进行比较 旧前j就是oldstartvnode 依次类推
循环开始了
旧前节点与新前节点比较(比较是否是同一个节点, key一致 tag一致), 如果是同一个节点, 就深度比较(也就是我刚才写的比较tagkey还有比较属性比较childern等操作), 旧前指针++ 新前指针++ 旧前节点变化, 新前节点变化
如果旧前与新前比较不一致, 就旧后新后比较, 旧后新后一致, 两虚拟节点深度比较, 旧后指针-- 新后指针-- 旧后节点变化 新后节点变化
旧前新前 旧后新后比较都不一致, 就进行旧前和新后比较, 旧前和新后比较一致就深度比较, 旧前指针++ 新后指针-- 旧前节点变化 新后节点变化, 还会将当前旧前节点所对应的真实节点移动到当前旧后节点所对应的真实节点的后一个的前一个(先移动位置 后指针变化)
旧前新前 旧后新后 旧前新后都不一致 就进行旧后和新前比较, 旧后和新前比较一致, 旧后和新前就深度比较, 旧后指针-- 新前指针++ 旧后节点变化 新前节点变化, 需要旧后节点所对应的真实节点移动到当前旧前指针所对应的真实节点的前面(也是先移动位置, 后改变指针)
旧前新前 旧后新后 旧前新后 旧后新前都没有比较成功的话, 这个时候其实会根据当前旧childern创建一个对象, 这个对象里面用旧childern每一项的key作为key, 值为当前的旧childern遍历这一项的下标, 然后拿到当前新前节点所对应的key,
去这个对象里面查找, 如果找不到, 就需要创建一个新元素, 然后插入到当前旧前指针的所对应的真实节点的前面, 如果找到了, 就需要根据这个对象和新前节点所对应的key取到所对应的下标, 将这两个节点进行深度比较 之后将其旧节点所对应的真实节点移动到旧前指针所对应的真实节点的前面, 之后还会将这个对象key所对应的值变成undefined下一次遇到当前旧前指针所对应的值为undefined的就直接跳过这个旧前节点 并且旧前指针++ 旧前节点变化
循环结束
循环结束有两种情况 一是旧前指针大于旧后指针了 二是新前指针大于新后指针
旧前指针大于旧后指针说明旧childern已经遍历完成了, 新childern没有遍历完成
就需要从当前新前指针开始从新childern截取, 截取到最后,这部分再进行遍历创建出真实节点, 然后插入其对应的父真实节点里面
新前指针大于新后指针 这就表明新childern已经遍历完成了, 而旧childern还没有遍历完成
就需要从旧前指针开始继续遍历旧childern, 直到旧后指针, 把这部分虚拟dom所对应的真实节点全部从其真实dom中删除
这个就是diff算法的基本过程
nextTick原理
1.nextTick本质上是同步收集所有的回调函数, 异步对这些回调函数进行批处理
nextTick内部会将传入的回调函数保存在一个数组里面 还会创建一个批处理函数, 这个函数的功能主要是用于遍历所有的回调函数, 取出来每一个进行执行
vue内部还会提前创建一个timerFunc变量, 这个变量其实是一个函数采用优雅降级的方式来为timerFunc进行赋值
如果浏览器支持promise timerFunc就等于() => Promise.resolve().then(批处理函数)
如果浏览器支持MutationObserver, 就会先创建一个文本节点, 之后通过new MutationObserver(传入批处理函数)来创建一个observer实例, 通过这个observer实例的observe方法去监听这个文本的改变, 所以timerFunc就等于一个函数, 这个函数内部拿到文本节点内容, 文本内容默认为0 拿到之后在此基础上加一再%2 这样文本的内容就一直是0 1 0 1了, 内容一变Mutationobserver传入的批处理函数也会执行
如果浏览器支持setimediate, 那timerFunc就等于一个函数, 函数内部setimediate(() => 批处理函数,0)
如果浏览器上述都不支持的话就使用settimeout兜底, 这个时候timerfunc就等于一个函数, 函数内部执行settimeout并且在0s之后去执行批处理函数
通过上述这种处理, 在vue内部其实就创建了一个好的timerFunc, 继续回到nextTick里面, 之前说里面会将当前的参数保存到一个数组里面, 还会在第一次的时候, 调用timerFunc函数, 内部其实是通过一个isFirst变量控制的, 第一次的时候实际上就会开始一个异步任务, 等到同步全部收集完之后, 没有同步代码了, 这个时候就会去执行异步代码, 执行的时就会执行传到nextTick的回调参数,
而数据改变更新视图, vue内部也是将watcher的更新操作放在nextTick里面的(放在nexttick里面主要解决了每修改一次就调用一个watcher方法的问题), 更新视图操作首先会放在数组里面, 其次才会将我们写的nextTick参数里面的回调放到数组里面, 所以遍历的时候, 我们可以在nextTick回调参数里面获取到数据更新之后的最新视图
这个就大概是nexTick的实现原理
vue数据更新之watcher的执行情况
先回忆一下数据更新之后的操作吧
数据更新触发set方法, set方法中dep.notify通知, watcher中的update方法执行 update方法执行 watcher中的get方法执行 get执行watcher的getter执行, getter执行 vm._update(vm._render())执行, _render执行触发数据的get方法执行, get方法返回最新的数据, _render生成最新的虚拟dom
_update 新旧虚拟dom更新(这里有可能还有diff算法的情况)页面更新
watcher的执行情况主要就是发生在watcher的update方法里面
update方法执行 如果是渲染watcher的话 会执行一个函数, 这个函数接收一个watcher, 函数内部会对watcher进行去重(根据watcher的id, 在watcher的constructor里面会给每一个watcher一个唯一的id),
之后会在第一次的时候执行nextTick(将对watcher的批处理函数作为参数传进去), 内部也是通过一个isFirst变量控制的, 通过这样的方式watcher的批处理函数就已经添加到nextTick的数组中去了
而nextTick数组的遍历执行是在异步任务里面,所以说vue2的数据更新视图变化是一个异步操作, 接下来我们在去分析一下watcher批处理函数
watcher批处理函数会遍历watcher数组中的每一个watcher, 取到执行调用watcher的get方法, get方法中执行getter方法, 剩下的就和刚才的描述对应上了
所以总结一个 set dep.notify watcher.update 函数(这个函数负责将watcher添加到数组中, 还有将watcher批处理添加到nextTick), watcher批处理(遍历, 执行get)大概就这么个情况, 完事之后, 刚才涉及到的变量再恢复成初始值, 这就是watcher的一个执行过程
vue渲染之计算属性watcher
- 首先我们先来认识一下computed的特性吧, computed是有缓存的, 只有当依赖的数据发生改变才会重新去执行computed, 否则就一直使用缓存的值, 原理是计算属性watcher内部有一个dirty属性控制着computed函数的执行, 只有dirty为true的时候才会执行, 为false的时候就不会执行使用返回的结果
- 计算属性watcher lazy(表示当前是计算属性watcher) dirty(computed缓存的主要实现就是有这个变量控制的) value缓存的结果
- new Vue的时候会传入一个配置对象, 执行initState方法, 判断如果配置对象里面有computed就执initComputed方法,
- initComputed方法会遍历computed中的每一项, 取出每一项的key和value, 先创建每一项所对应的计算属性watcher, 将value函数作为watcher的getter方法, 同时传入一个lazy属性, 计算属性watcher收到之后, 同时将这个lazy赋值到watcher上的lazy和dirty, watcher中的lazy表示当前是一个计算属性watcher, dirty给一个初始值true, 接着Watcher的constructor里面还会判断当前如果是计算属性watcher(根据lazy判断)就不会去立即执行get方法, 因为立即值get方法computed函数就会立即执行了, 但是computed是要等到用户执行computed的key的get方法时才要执行
- 计算属性watcher创建完之后, 紧接着就需要创建get和set方法, 因为计算属性本质上就是Object.defineProperty get中去执行watcher的一种操作, 通过Object.defineProperty(vm, key, 描述符对象),
这个描述符对象里面包含两部分, 一是get方法, 二是set方法, set方法就是用户computed如果传的是一个对象的话, 对象的set就是这里的set, 没有的话就给一个空函数好了, 另一个是get方法, 我们先来理
一下, 当用户在页面上使用了computed, 就会去执行这里定义的get方法, 而在这个get方法中, 我们可以拿到刚才创建的计算属性watcher, 拿到之后我们可以先判断, 当前dirty是否是true, 如果是true,
我们就去执行watcher的evaluate方法,evaluate方法会先去执行get方法, get方法执行完会有一个返回值, 这个返回值是getter方法的返回结果, evaluate方法拿到之后, 将其赋值到watcher的value属性身上
,这样在计算属性watcher身上就有一个缓存的结果了, get方法中会先将当前活跃的watcher添加到数组里面, 用完就删掉(用完就是get方法执行完, 至于为什么是要存在数组里面, 我马上就要说到了), 接着执行getter方法
保存getter方法返回值, 从数组中删除当前watcher, Dep.target就会取这个数组里面的最后一项, get方法执行完之后, 接着执行evaludate方法, 这个方法目前已经拿到了最新的watcher value, 然后再将dirty
属性修改成false, evaludate方法执行完毕, 回到计算属性watcher的get方法中, 经过evaludate方法之后, 计算属性watcher身上就有value属性值了, 而且计算属性对应的函数也已经完成第一次执行了, 接着
再看一下Dep.target有没有值, 如果有值, 会拿到当前计算属性watcher身上绑定的所有dep, 遍历所有的dep, dep执行depend方法, 与当前活跃的watcher相互绑定, 这样就将当前的一个渲染watcher也绑定上了
为什么dep和watcher要做一个相互绑定, 其实在这块就已经用到了, 我再给你从头理一下, 为什么这里需要将计算属性的watcher与Dep.target所对应的watcher相互绑定
执行渲染watcher, 渲染watcher的get方法执行, 渲染watcher会首先添加到保存所有watcher的数组中, 执行getter方法, getter方法执行 _render方法执行 _render方法执行 计算属性get方法执行
get方法执行计算属性第一次watcher的evaluate方法执行 evaluate方法执行计算属性watcher get方法执行 计算属性watcher get方法执行计算属性watcher会被添加到保存watcher的数组中 执行getter
从数组中删除计算属性watcher 计算属性watcher获得最新的value值 dirty属性变成false 此时渲染属性还没完, 所以还得将computed函数中所用到的数据所对应的dep收集上当前的渲染watcher(数组中最后一项会作为Dep.target, 计算属性watcher删除, 那最后一个就是渲染watcher了)
此时computed函数中所使用的变量所对应的dep已经绑定了计算属性watcher和渲染watcher了
那么当依赖的数据变化之后, 执行依赖的数据的set方法, set方法dep.notify通知, notify方法就会去遍历所有绑定的watcher, 执行watcher所对应的update方法, 计算属性watcher的update方法就只是将dirty
属性变成true了, 渲染watcher则需要将其放到全局的watcher数组里面, 然后去重, nextTick执行这些(这里已经在上述地方阐述过了, 所以不再赘述), 再次执行渲染watcher, 渲染watcher的getter方法执行,
getter方法中执行计算属性的get方法, 由于当前计算属性watcher所对应的dirty已经变成true了, 所以会再次执行, 执行完之后,再次变成false(这里也是为什么数据需要收集渲染watcher的原因, 需要再次执行计算属性的get方法)
如果依赖的数据一直没有变, 在计算属性的get中只有dirty为true的话才会执行计算属性函数, 第一次执行完dirty已经变成false了, 下次再来用的时候就不会走计算属性watcher的evaluate方法, 直接返回之前保存的计算属性watcher的value属性即可
这个大概就是计算属性的源码解读
4415

被折叠的 条评论
为什么被折叠?



