MVVM
model和view层通过中间的vm连接和驱动。model层数据变化会改变视图,view改变通过事件来修改数据。vue参考了MVVM实现了双向绑定,react是MVC,但是vue仍然可以通过parent等操作dom所以不全是mvvm
vue模板解析
1、先将代码转换为AST树
根据正则匹配,从第一个字符开始,筛选过的就删掉继续index++向后匹配。
如果匹配开始标签就放入一个stack中,此时如果匹配到结束标签则出栈对比是否一致,不一致报错
2、优化AST树
找出静态节点并标记,之后就不需要diff了
递归遍历ast树中的节点,如果没有表达式、v-if、v-for等,就标记static为true
3、生成render函数、在使用new Function(with() {})包裹
转换成render函数。编译结束。一定要包裹new Function和with来更改上下文环境
<div id="app"><p>hello {{name}}</p> hello</div> ==>
new Function(with(this) {_c("div",{id:app},_c("p",undefined,_v('hello' + _s(name) )),_v('hello'))})
4、render函数执行后得到的是虚拟dom
ast是需要吧代码使用正则匹配生成的,然后转换成render,而虚拟dom则是通过render函数直接生成一个对象
所以Vue中的AST和VNode关系如下:
template > ast > render function > 执行 render function > VNode
ast是转换语法(js、html语法转换为ast)两者很相像
初始化data中的proxy
将所有的数据全部代理到this上
for (let key in data) {
proxy(vm, '_data', key);
}
function proxy(vm,source,key){
Object.defineProperty(vm,key,{
get(){
return vm[source][key]
},
set(newValue){
vm[source][key] = newValue;
}
})
}
vue的双向数据绑定、响应式原理
监听器 Observer ,用来劫持并监听所有属性(转变成setter/getter形式),如果属性发生变化,就通知订阅者
订阅器 Dep,用来收集订阅者,对监听器 Observer和订阅者 Watcher进行统一管理,每一个属性数据都有一个dep记录保存订阅他的watcher。
订阅者 Watcher,可以收到属性的变化通知并执行相应的方法,从而更新视图,每个watcher上都会保存对应的dep
解析器 Compile,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化
数据劫持
利用observe方法递归的去劫持,对外也可以使用这个api。使用defineReactive来劫持数据
class Observe{
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
// 判断是否为数组,如果是数组则修改__proto__原型。会再原来原型和实例中间增加一层。
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
//遍历数组,继续调用observe方法。因为数组中有可能有二维数组或者对象
this.observeArray(value)
} else {
// 如果是对象则直接绑定响应式
this.walk(value)
}
}
}
对象的劫持: 不断的递归,劫持到每一个属性。在defineReactive中会继续递归执行let childOb = !shallow && observe(val)方法递归绑定,因为对象中有可能还有对象
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
//直接遍历对象去递归拦截get、set
defineReactive(obj, keys[i])
}
}
数组的劫持:不劫持下标,value.proto = arrayMethods,增加一层原型链重写数组的push、splice等方法来劫持新增的数据。在数组方法中进行派发更新ob.dep.notify()
// 继续遍历数组,再次执行observe来递归绑定值。
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
响应式原理数据劫持,首先执行observe方法new 一个Observe类,其中会判断是数组还是对象
1、如果数据是[1,[2,3],{a:1}],不会去劫持下标。会修改数组的proto修改原型的方法。但是其中的[2,3],{a:1}并没有被监控,所以继续调用observeArray递归调用,其中又递归调用了let childOb = !shallow && observe(val)继续监控
2、如果数据是{a:{b:2},c:3}, 会执行walk去遍历对象执行defineReactive拦截key的get、set。其中会去递归调用observe方法继续递归劫持
依赖收集
渲染watcher的收集:首次渲染:执行完劫持之后,会走挂载流程会new一个渲染watcher,watcher中会立即执行回调render方法,方法中会去创建Vnode需要去数据中取值,就会进入到属性的get方法。会去收集依赖
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// 如果有current watcher,会去收集依赖。Dep.target全局只有一个。一个时刻只能更新一个watcher。每次在执行watcher时会先pushStack,等执行完后会去popstack
if (Dep.target) {
// 收集属性的依赖,每个属性获取后都会有个dep。这个dep挂在每个属性上。
// 例如直接this.a = xxx修改属性就可以找到这个属性上的dep更新watcher
dep.depend()
// 如果儿子也是对象或者数组会去递归让儿子也收集
if (childOb) {
// 这个dep挂在对象或者数组上。为了给$set或者数组派发更新使用。在 {b:1} 、[1,2,3]上挂dep
// 例如新增属性,this.$set(obj, b, xxx)或this.arr.push(2)。
// a:{b:[1,2,3]}、a:{b:{c:1}},先收集a的依赖挂在属性dep上,因为childOb又为object,需要继续收集依赖挂在该对象上
// 此时如果更新a,则直接找到属性上的dep更新。但是a上如果想新增一个c属性,则需要使用$set。或者数组上push一个。
// 此时是找不到属性上的dep的,因为该属性是新增的,数组增加一项需要更新watcher。所以需要在对象或者数组的Ob类上挂一个dep方便更新
childOb.dep.depend()
// 如果仍然是数组需要持续递归收集
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
dep中收集watcher
// dep.js
depend () {
// Dep.target 是此刻唯一准备被收集的watcher
if (Dep.target) {
Dep.target.addDep(this)
}
}
// watcher.js
addDep (dep: Dep) {
const id = dep.id
// 去重,如果添加过就不需要添加了
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
// dep中保存此watcher
dep.addSub(this)
}
}
}
派发更新
在set中派发更新,数组是在劫持的方法中派发更新。会执行当前所有dep中的watcher的notify方法更新视图或者数据
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// this.a如果直接改了引用,需要重新递归劫持属性,例如a:{b:1} this.a = {c:2}
childOb = !shallow && observe(newVal)
// 执行派发更新操作
dep.notify()
}
})
dep中派发更新
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
// 遍历执行所有watcher的update方法
subs[i].update()
}
}
update () {
/* istanbul ignore else */
if (this.lazy) {
// 如果是computed, 会去执行重新取值操作
this.dirty = true
} else if (this.sync) {
// 如果是同步watcher直接run()会去执行watcher的get()
this.run()
} else {
// 默认watcher都是放入队列中异步执行的
queueWatcher(this)
}
}
export function queueWatcher (watcher: Watcher) {
// .......调用全局的nextTick方法来异步执行队列
nextTick(flushSchedulerQueue)
}
watcher都是会异步更新,调用nexttick去更新,为了整合多次操作为一次。提高效率
watch
watch内部会调用$watch创建一个user watcher(设置user为true),等依赖变化执行update方法
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
// $watcher最终也会去new 一个watcher,传入vm实例和检测的key(expOrFn)和watcher的回调(cb)和watcher的设置(deep等设置)
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
// 如果设置是immediate,则需要立即执行一次cb
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
// watcher.js中会去判断expOrFn 如果是function说明是渲染watcher传入的回调,
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// 如果传入的是字符串,则将取值函数赋给getter(调用一次就是取值一次),例如watcher中监控的是'a.b.c'则需要从this中一直取到c
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
依赖收集: 会去执行watcher的getter方法,其实就是去取值,此时获取的值保存起来。执行取值函数会走属性的get进行依赖收集。
watch初始化时会去取值,为了保存下一次变化时的oldvalue
this.value = this.lazy
? undefined
: this.get()
get () {
pushTarget(this)
let value
const vm = this.vm
try {
//如果是user watcher的话,执行的就是取值函数其实就是依赖收集过程
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// 如果设置了deep则需要遍历获取子属性进行全部的依赖收集(把子属性都取值一遍)
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
派发更新:更新时会执行watcher的update方法,其中如果设置同步则直接run,如果没有默认放入队列异步更新
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
run () {
if (this.active) {
// run就是重新调用get执行getter,去重新取值,取出来的就是新值
const value = this.get()
// 如果是新老值不相同才需要调用user watcher的回调
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
// 取老的值并设置新值
const oldValue = this.value
this.value = value
// 调用user watcher的回调
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
computed
computed会设置lazy为true。并且会执行脏检查,只有当这些依赖变化时才会去重新计算computed的值,获取完之后再设置dirty为false
// 设置lazy为true表示是computed
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
// 如果用户传入对象表示自己定义了get函数则使用用户的,没有则直接设置getter
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// 创建一个computed watcher, 初始化时其中不会执行get函数获取值。
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
if (!(key in vm)) {
// 定义computed,需要去劫持计算属性的值进行依赖收集。
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
// 定义computed
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
// 其实就是重新劫持computed的值, sharedPropertyDefinition中有定义的get函数
Object.defineProperty(target, key, sharedPropertyDefinition)
}
收集依赖: 创建computed时,需要对computed的变量也进行劫持,如果页面中使用到了这个计算属性,则会走下面的createComputedGetter 创建的get方法。之后会去收集依赖。
// 创建劫持computed的get函数
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 如果dirty为true才取值,创建时默认第一次是true,会去执行get方法
if (watcher.dirty) {
watcher.evaluate()
}
// 如果有target则去收集依赖。firstName和lastName收集渲染依赖, 计算属性上不需要收集渲染watcher,因为如果页面中使用到了这个计算属性,计算属性是根据函数中依赖变化计算的,所以其中任何一个依赖都需要收集一下渲染watcher,因为任何一个变化都有可能导致重新渲染
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
// 取值
evaluate () {
// 执行get方法会去取值,例如:return this.firstName + this.lastName,此时也是对依赖firstName和lastName的取值收集依赖的过程,那么他们也会将当前的computed watcher添加到dep的sub队列中。取值完置换成false
this.value = this.get()
this.dirty = false
}
所以如果计算属性中写了data中其他的值也会使他进行收集依赖,浪费性能
let vm = new Vue({
el:'#app',
data: {
firstName: 'super',
lastName: 'kimi',
kimi: 888
},
computed: {
fullName() {
// 最后返回没有kimi但是打印进行取值了,他就会收集computed和渲染watcher
console.log(this.kimi)
return `${this.firstName}-${this.lastName}`
}
}
})
// 如果更新了kimi也会让视图重新渲染
vm.kimi = 999
派发更新:如果此时改变了firstName的值,因为firstName之前收集依赖中有依赖他的computed watcher和渲染watcher,会去执行两个watcher上的update方法
update () {
// 如果是计算属性则设置dirty为true即可,之后再去执行渲染watcher的update会重新渲染,那就会重新取计算属性的值,到时候就可以取到最新的值了
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
provide、inject
provide是定义在当前实例上,inject会去遍历$parent找到谁定义了,然后再转成响应式挂在当前实例,只是单向
nextTick
优雅降级,先使用promise,如果不支持会使用MutationObserver,不兼容再使用setImmediate,最后降级成setTimeout
Vue.$set
1、对象会重新递归添加响应式,数组则会调用splice方法,方法已经被劫持
2、执行ob.dep.notify(),让视图更新
diff算法
1、首先比对标签 <div>...</div> --> <ul></ul>
在diff过程中会先比较标签是否一致,如果标签不一致用新的标签替换掉老的标签
// 如果标签不一致说明是两个不同元素
if(oldVnode.tag !== vnode.tag){
oldVnode.el.parentNode.replaceChild(createElm(vnode),oldVnode.el)
}
如果标签一致,有可能都是文本节点,那就比较文本的内容即可
// 如果标签一致但是不存在则是文本节点
if(!oldVnode.tag){
if(oldVnode.text !== vnode.text){
oldVnode.el.textContent = vnode.text;
}
}
2、对比属性<div>...</div> --> <div className=‘aaa’>...</div>
当标签相同时,我们可以复用老的标签元素,并且进行属性的比对。只需要把新的属性赋值到老的标签上即可
3、对比子元素<div><p>a</p></div> -> <div><p>b</p></div>
[1]新老都有孩子需要updateChildren比对
[2]新有老没有则需要遍历插入
[3]新没有老有则需要删除即可
// 比较孩子节点
let oldChildren = oldVnode.children || [];
let newChildren = vnode.children || [];
// 新老都有需要比对儿子
if(oldChildren.length > 0 && newChildren.length > 0){
updateChildren(el, oldChildren, newChildren)
// 老的有儿子新的没有清空即可
}else if(oldChildren.length > 0 ){
el.innerHTML = '';
// 新的有儿子
}else if(newChildren.length > 0){
for(let i = 0 ; i < newChildren.length ;i++){
let child = newChildren[i];
el.appendChild(createElm(child));
}
}
4、updateChildren 核心
设置四个index:oldS、oldE、newS、newE
<1>先比对oldS和newS,通过判断sameNode()方法比对key和tag等。如果匹配相等则oldS和newS都++,节点复用即可 例如:ABCD -> ABCE
// 优化向后追加逻辑
if(isSameVnode(oldStartVnode,newStartVnode)){
patch(oldStartVnode,newStartVnode); // 递归比较儿子
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
}
<2>oldS和newS如果不相等再比对oldE和newE,通过判断sameNode()方法比对key和tag等。如果匹配相等则oldE和newE都--,节点复用即可 例如:ABCD -> EBCD
// 优化向前追加逻辑
else if(isSameVnode(oldEndVnode,newEndVnode)){
patch(oldEndVnode,newEndVnode); // 递归比较孩子
oldEndVnode = oldChildren[--oldEndIndex];
newEndVnode = newChildren[--newEndIndex];
}
<3>oldE和newE如果不相等再比对oldS和newE,通过判断sameNode()方法比对key和tag等。如果匹配相等则oldS++和newE--,将old节点插入到最后 例如:ABCD -> BCDA
// 头移动到尾部
else if(isSameVnode(oldStartVnode,newEndVnode)){
patch(oldStartVnode,newEndVnode); // 递归处理儿子
parent.insertBefore(oldStartVnode.el,oldEndVnode.el.nextSibling);
oldStartVnode = oldChildren[++oldStartIndex];
newEndVnode = newChildren[--newEndIndex]
}
<4>oldS和newE如果不相等再比对oldE和newS,通过判断sameNode()方法比对key和tag等。如果匹配相等则oldE--和newS++,将old节点插入到最前 例如:ABCD -> DABC
// 尾部移动到头部
else if(isSameVnode(oldEndVnode,newStartVnode)){
patch(oldEndVnode,newStartVnode);
parent.insertBefore(oldEndVnode.el,oldStartVnode.el);
oldEndVnode = oldChildren[--oldEndIndex];
newStartVnode = newChildren[++newStartIndex]
}
<5>如果使用index都判断节点不相同,则需要建立vnode的key-index map表,然后匹配map表,如果能匹配上挪到当前oldS前面,如果匹配不上则创建新节点往当前oldS前面插入,newS++ 例如:ABCD -> CDME
// 建立key-index的map表
function makeIndexByKey(children) {
let map = {};
children.forEach((item, index) => {
map[item.key] = index
});
return map;
}
let map = makeIndexByKey(oldChildren);
// 在map表中寻找有没有key匹配的vnode
let moveIndex = map[newStartVnode.key];
if (moveIndex == undefined) { // 老的中没有将新元素插入
parent.insertBefore(createElm(newStartVnode), oldStartVnode.el);
} else { // 有的话做移动操作
let moveVnode = oldChildren[moveIndex];
oldChildren[moveIndex] = undefined;
parent.insertBefore(moveVnode.el, oldStartVnode.el);
patch(moveVnode, newStartVnode);
}
newStartVnode = newChildren[++newStartIndex]
图中第一步比对index都不同,则开始比对key发现有C相同则把C挪到最前面,newS++;下来发现D有相同的把D挪到oldS前面,newS++;接着M找不到则插入oldS前面,newS++;最后E找不到则插入前面,newS++;
<6>全部比对完后需要对当前index进行检查,因为有可能有多或者少节点的情况
if (oldStartIdx > oldEndIdx) {
// oldNode先扫完说明new有多余,需要添加进去
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else if (newStartIdx > newEndIdx) {
// newNode先扫完说明old有多余,需要删除掉
removeVnodes(oldCh, oldStartIdx, oldEndIdx);
}
diff对开头、结尾插入删除节点&头节点移到尾部&尾节点移到头部有很大的优化
key:为了高效复用
上图原有A B C D E 现在插入F 变为A B F C D E
如果没有key:首先比对头和头,A、B都复用,比到C和F时,tag一样key相同(都为undefined)则会复用,会成下图情况
如果有key:比对到C和F时,C和F的key不相同所以跳过,此时就该比oldE和newE,EDC都相同,多下来的F直接插入
如果key使用index,遇到表单元素比如带checkbox的列表,如果状态勾选后,会复用勾选状态产生bug
keep-alive组件
会将组件缓存到this.cache中,放入内存中缓存起来
mixins缺点
不可知,不易维护。因为你可以在mixins里几乎可以加任何代码,props、data、methods、各种东西,就导致如果不了解mixins封装的代码的话,是很难维护的。