数据驱动
释疑:
那为什么不用class来写vue而是用构造函数的形式呢?
这是因为Vue这个对象的方法太多了,很多都需要放在不同的模块来写的,在不同的模块我只要在vue的prototype上去添加方法就好了。如果是class,就不好添加了,只能在一个class上添加,继承的话调用的也是其他class了,所以采用构造函数的形式。
构造函数和class的使用场景区别
当一个对象或模块频繁用到继承或者内部属性比较固定,采用OOB的思想,即采用class
,如果一个模块非常大,要分散到多个子模块去编写,且不怎么用到继承采用构造函数的形式。
整体流程:
new Vue => init => $mount => compile => render => vnode => update(patch) => dom
new Vue & init:
混入了一些全局方法
$mount
调用render函数,没有则创建一个
调用beforeMount,beforeUpdate,mounted钩子
通过update方法渲染dom
返回vm
render
render就是createElement方法
Visual DOM
一个描述DOM的类
createElement
格式化children
创建vnode
update
其本质就是调用了patch方法
patch方法就是将vnode遍历创建DOM并插入(中间处理了文本节点,注释节点)
组件化
Vue的组件化我们需要了解这么几个部分:
1. createComponent(创建组件)
创建子类构造函数
-
安装组件钩子,组件钩子包括(组件的生命周期会在这几个钩子内部具体调用)
- init
- prepatch
- insert
- destroy
创建vnode实例
2. patch
组件的patch的时候会合并配置项,最后调用$mount(通过update)去进行最后的渲染。
ps:
先父组件后子组件的有:
beforeCreate
created
beforeMount
beforeUpdate
updated
beforeDestroy
先子组件后父组件的有:
mounted
destroyed
3. 合并配置
vue源码内部会根据参数的不同采取不同的合并策略进行配置合并。
4. 生命周期
1.beforeCreate & created
beforeCreate 在initState之前,而initState 的作用是初始化 props、data、methods、watch、computed 等属性,所以beforeCreate拿不到。
-
beforeMount & mounted
- beforeMount 调用在_render之前
- mounted 调用在_update之后
- 子组件在insert方法中调用了mounted
beforeUpdate & updated
这2个钩子会在mounted之后触发。beforeDestroy & destroyed
在 $destroy 的执行过程中,它又会执行 vm.patch(vm._vnode, null) 触发它父组件的销毁钩子函数,这样一层层的递归调用,所以 destroy 钩子函数执行顺序是先子后父,和 mounted 过程一样。
5. 组件注册
- vue在初始化全局API的时候会将所有的组件合并到配置项中(全局注册会合并到Vue原型链上的opinion,否则添加到该组件名下的配置项中)
- 我们在使用的过程中会根据tag的名字去拿,如果拿不到用驼峰的形式去拿,还拿不到用短横线的形式去拿,这就是为什么我们注册组件后使用的时候可以用驼峰和短横线的原因。
6. 异步组件
vue将异步组件分成了普通组件,promsie组件和高阶组件分别进行处理。
- 先定义通用的forceRender, resolve, reject3个方法
- 由于异步组件组的解析过程中数据没有发生变化,因此需要通过foceRender中的foreceupdate去强制更新
- 最后拿到解析后的组件,通过extend方法转成组件的构造函数。
- 由于之前定义好了resolve,reject,webpack通过import的方式返回的是一个promsie对象,那么该promsie最后的resovle和reject也会变成入参被结合进来。
- 高级组件就是加了loading和timeout字段,由于vue解析异步组件的时候会先判断是不是对象,对象的话就回去拿loading,和timeout字段。这里注意的一点是如果这是延迟为0,那么第一次解析的时候会返回我们定义好的loading组件否则在之后会创建一个注释vnode,最后在foreRender的过程中会被替换。loading和timeout最后都会通过forceRender去渲染最终的结果。
- 由于开始都回生成一个占位注释节点,等拿到组件后会去调用foreRender方法去更新,所以所有的异步组件其实都是渲染了2次。
Vue响应式系统
1. 创建响应式对象:
observe(xxx) => xxx是数组 ?递归observe :walk(调用defineReactive), observe整个动作是vue核心库需要的,最后变成响应式对象是靠defineReactive实现的。
依赖收集和派发更新有2个概念:
- dep ,这是一个包含 依赖id, 订阅者(watcher), 依赖数组的类,这类主要是用来管理watcher的。
- watcher,这个类的作用主要是数据发生变化的时候,调用各个方法产生计算和更改视图的,包含了各种属性。
2. 依赖收集:
依赖收集发生在数据属性的get阶段。干了这么一件事:
将当前wather变成持有这个dep的订阅者。
依赖收集要清空原来依赖,使用的新的依赖。防止重复订阅的浪费。
3. 派发更新:
派发更新主要发生在数据属性的set阶段, 做了这么几个步骤:
- 判断新值和旧值如果相等或者同为NaN,则直接返回。
- 通过observe将新值变为响应式对象
- 通过dep.notify()派发更新。
派发更新的流程:
值发生改变 => dep.notify()=> 遍历触发订阅的wathcer的update=> 根据是否同步等条件最后触发watcher.run方法(渲染函数触发get执行更新DOM)=> 触发watcher的回调(用户可以拿到新值和旧值)
4. [检测变化的注意事项]
通过a.b = 1给对象新添加属性。
通过arr[0] = 1直接给原数组的元素赋值。
通过vm.items.length = newLength修改数组长度。
上述3中可以通过set方法变成响应式。核心还是通过defineReactive和dep.notify()
5. 计算属性VS侦听属性
-
计算属性在创建wathcer的时候会置上一个标志位lazy,做了2层优化;
- 在初始化的时候不会去计算
- 在更新时候比较前后值是否一样否则不会渲染(所有响应式数据都一样)。
watch
就是通过traverse做了递归响应式。
watcher的种类:
deep watcher(递归添加watcher)
user watcher(用户的watcher)
computed watcher
sync watcher(可以在user watcher里配置)
renderwatcher(渲染 watcher,一般来说处的位置靠上)
6. 组件更新:
说下数据变化到更新整个流程:
依赖发生变化 => watcher.getter => vm._update => vm.patch
判断是否已是同一个节点的逻辑:
key => tag => isComment => data => sameInputType
如果新旧节点不同,那么主要分3步进行:
- 创建新节点
- 更新父的占位节点
- 删除旧节点
如果新旧节点相同:
- 执行prepatch钩子函数(去拿新的 vnode 的组件配置以及组件实例,去执行 updateChildComponent 方法)
- 执行 update 钩子函数
- 完成 patch 过程
patch大致逻辑是这样的:
- 新节点存在文本节点的情况下,当旧节点的子节点和新节点的子节点都存在的情况下,开始diff算法。
- 当旧节点没有子节点的情况下,检查新节点子节点的key的重复性.如果旧节点存在文本节点,则清空文本节点。然后批量将新节点的子节点添加到elem后面
- 如果旧节点存在子节点,则移除所以的子节点。
- 如果新节点没有子节点但是旧节点有子节点,则清空旧节点的文本内容
- 若新旧节点文本内容不同则替换。
diff算法其实遵循几个原则:
能移动就移动
先比头和尾
头尾比不了,就找中间key或索引一样的比,然后添加到旧头
新vnode较长则在旧vnode上添加node,较短则删除。
7. Props;
- 规范化
当 props 是一个数组,每一个数组元素 prop 只能是一个 string,表示 prop 的 key,转成驼峰格式,prop 的类型为空。
当 props 是一个对象,对于 props 中每个 prop 的 key,我们会转驼峰格式,而它的 value,如果不是一个对象,我们就把它规范成一个对象。
如果 props 既不是数组也不是对象,就抛出一个警告。 - 初始化(校验、响应式和代理)
释疑:
1. 为什么说Vue是异步更新,因为dep.notify()调用了watcher的udpate方法,这个方法调用了queueWatcher方方法,最终调用了nextTick这个异步方法,所以是异步更新的。
2. 在依赖收集阶段如果碰到对象里面的属性是数组的,如果数组的值是基本类型的,那么通过Object.defineproperty无法对其响应式化,因此数组元素的变化是无法触发更新的
我们能在watch拿到新值和旧值的原因是因为在wather执行run的时候,会将新旧值传到回调里
3. push, unshift, splice是响应式操作的原因是Vue重写了这3个方法。
4. 为什么每次获取计算属性的值时都要进行依赖收集呢,而不是仅进行一次性的依赖收集?原因是,计算属性的依赖项可能会改变,这次有x个依赖项,下次可能有y个依赖项。比如三元表达式
编译
- 解析模板字符串就是根据通过正则按照不同的情况生成一个js对象,
- 优化语法书其实就是标记静态节点和静态根,提高编译的效率。
- 生成代码就是根据不同的条件将代码串生成可以执行的代码。
扩展
1. event
先通过正则把节点上的元素都解析出来,并对事件是原生事件还是自定义事件加以区分。然后把所有的事件用 vm._events 存储起来. on 就是往_events 里push,off就是对_events的元素进行删除,once就是两者结合一下。
2. v-model
v-model其实是一种语法糖,其本质利用了父子组件的通信完成的。
3. slot
普通插槽是在父组件编译和渲染阶段生成 vnodes,所以数据的作用域是父组件实例,子组件渲染的时候直接拿到这些渲染好的 vnodes。而对于作用域插槽,父组件在编译和渲染阶段并不会直接生成 vnodes,而是在父节点 vnode 的 data 中保留一个 scopedSlots 对象,存储着不同名称的插槽以及它们对应的渲染函数,只有在编译和渲染子组件阶段才会执行这个渲染函数生成 vnodes,由于是在子组件环境执行的,所以对应的数据作用域是子组件实例。
4. keep-alive
vue内部对keep-alive做了特殊处理,在执行prepatch阶段会重新渲染缓存的组件,没有重新生成一个vue实例,因此也没有标准组件的生命周期。
transition && transition-group
自动嗅探目标元素是否应用了 CSS 过渡或动画,如果是,在恰当的时机添加/删除 CSS 类名。
如果过渡组件提供了 JavaScript 钩子函数,这些钩子函数将在恰当的时机被调用。
如果没有找到 JavaScript 钩子并且也没有检测到 CSS 过渡/动画,DOM 操作 (插入/删除) 在下一帧中立即执行。
所以真正执行动画的是我们写的 CSS 或者是 JavaScript 钩子函数,而 Vue 的 <transition> 只是帮我们很好地管理了这些 CSS 的添加/删除,以及钩子函数的执行时机。
transition && transition-group