vue运行原理
init过程
初始化生命周期、事件、 props、 methods、 data、 computed 与 watch 等,并通过 Object.defineProperty 设置 setter 与 getter 函数,用来实现「响应式」以及「依赖收集」
响应式系统:
- Vue的核心实现就是「响应式系统」,vue中是通过Object.defineProperty实现的。
- 将data中的数据都通过defineProperty包装一边对数据进行响应式化,这个过程中为每一个进行defineProperty包装的属性创建一个观察者对象dep。
- 当 render function 被渲染的时候,因为会读取所需对象的值,所以会触发 getter 函数进行「依赖收集」,「依赖收集」的目的是将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中。
- 在修改对象的值的时候,会触发对应的 setter, setter 通知之前「依赖收集」得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher 就会开始调用 update 来更新视图,中间还有一个 patch 的过程以及使用队列来异步更新的策略。
问题:响应式数据失效
首次渲染时data对象中的所有key都可以被响应化处理到。那么怎么会出现实效的情况?
1、数组没有Object.defineProperty这个方法,所以在使用数组索引角标的形式更改元素数据时(arr[index] = newVal),视图往往无法响应式更新。
为解决这个问题,Vue.js 中提供了 $set() 方法:vm.arr.$set(0, 'newVal’) // vm.arr[0] = ‘newVal'
2、Vue 不能检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化过程,所以属性必须在 data 对象上存在才能让 Vue 转换它,这样才能让它是响应的。
1 | var vm = new Vue({ |
$mount
会挂载组件
compile()
Parse:
parse 会用正则等方式解析 template 模板中的指令、class、style等数据,形成AST(抽象语法树)。
AST并没有把数据解析到其中,对应需要数据的部分做了如下黄色选中部分的处理。
AST的样子:
1 | { |
optimize(作优化):
—— markStatic、markStaticRoots
主要是为静态的节点做上一些「标记」(即加上 static 属性,这是 Vue 在编译过程中的一处优化),后边当 update更新界面时,会有一个 patch 的过程,diff 算法会直接跳过静态节点,从而减少了比较的过程,优化了 patch 的性能。
为静态根做staticRoot 标记(静态根会忽略一些处理,比如不做vnode转化等。作者认为这种情况的优化消耗会大于收益)。
静态节点判断标准:
当一个node为【表达式节点】—— 有Mustache语法的表达式等,或存在【 if 或者 for】这样的条件的时候 是非静态节点,如果子节点是非静态节点,那么当前节点也是非静态节点。
【文本节点】等永远不需要改变的节点则是静态节点。
静态根判断标准:
当前节点为静态节点 && 有子节点 && 子节点不是单个静态文本节点 则为静态根
generate :
会将传入的 AST 转化成可执行的 render函数。(AST和vnode关系???在经历过 parse、optimize 与 generate 这三个阶段以后,组件中就会存在渲染 VNode 所需的 render function 了。 Render function 和数据结合render生成Virtual DOM Tree )
函数中:
_c 是生成一个vnode元素节点: _c —> _createElement —> new Vnode()
_s 则是调用了toString() 生成文字
_v 生成一个 vnode文字节点
…
1 | function generate (rootAst) { |
结果类似这样:
1 | with(this){ |
扩展:
Virtual DOM
Virtual DOM 其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。
diff
算法是通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有 O(n),是一种相当高效的算法,比对得到「差异」后将这些「差异」更新到视图上。
之所以将新的 VNode 与旧的 VNode 一起传入 patch 进行比较,经过 diff 算法得出它们的「差异」然后将这些「差异」的对应 DOM 进行修改。是因为通过innerHTML 将新的VNode直接替换旧的VNode全部渲染到真实 DOM 中是一个比较浪费的过程(这个过程其实包含了渲染的一些列步骤,如果很少内容更改浏览器就要白做很多事情)。
注意:
- 对比的只是动态数据发生变化的部分(如if、else中的node,新的for和旧的for生成的node,动态文本改变前后node)
- 发现差异后移动、更新操作的是oldVnode对应的elm,即已经挂载在页面中的真实DOM节点。
「编译」的时候会将静态节点标记出来,这样就可以跳过比对的过程。(这里不是什么也不做,还是有动作要做的:在当新老 VNode 节点都是 isStatic(静态的),并且 key 相同时,只要将 componentInstance 与 elm 从老 VNode 节点“拿过来”即可。)
Diff算法比较的细节:
两端对比
交叉对比
相同dom移动到oldVnode最后,并移动oldVnode的start指针和newVnode的end指针
相同dom移动到oldVnode最前,并移动oldVnode的end指针和newVnode的start指针
中间对比:从newVnode start开始根据key快探查oldVnode是否在中间有相通节点
有:将此node移到oldVnode最前,移动newVnode start指针
无:创建新node插入到oldVnode最前,移动newVnode start指针
理解并按照默认diff规则才能使代码达到最优,比如说,保持根节点一致;一个dom父节点下有多个子节点并列时,给子节点添加key,防止对父节点使用insertBefore插入子节点这种情况等等。
批量异步更新的策略
问题:循环中频繁更新数据视图也要跟着频繁更新吗?这个时候使用的就是一部更新
- Vue.js在默认情况下,每次触发某个数据的 setter 方法后,对应的 Watcher 对象其实会被 push 进一个队列 queue 中,在下一个 tick(nextTick) 的时候将这个队列 queue 全部拿出来 run一遍。
- 其中有一个对批量去重的步骤,用一个map来存储queue中所有watcher的id这样在pushwatcher的时候就可以去重。
- nextTick其实就是任务队列中存储的一个任务,运行时会执行上边push进queue的所有内容,所有的循环会在本次执行栈中完成。run的是Watcher 对象的一个方法,用来触发 patch 操作
Vuex工作原理:
因为 Vuex 内部采用了 new Vue 来将 Store 内的数据进行「响应式化」,所以 Vuex 是一款利用 Vue 内部机制的库,与 Vue 高度契合。
数据会通过类似Vue.prototype.globalData = globalData;的方式放在Vue的prototype中,这样当组件想获取值时直接通过prototype获取即可。