【vue】vue2原理总结

前言

  • 总结下vue2原理。

初始化

  • 初始化过程主要就是将配置项写入$options,然后初始化data,computed watch等。

属性劫持

  • 这个用Object.defineProperty地球人都知道。数组的处理是重写数组的7种方法,所以通过索引修改值不会被监听到。另外对象上直接添加属性不会响应式,需要使用set或者传入对象来触发他劫持对象使其响应式。对象上__ob__是其做的已代理标识,不可枚举,否则会导致无限递归。
  • 为什么除了根的data,其他都只能配置函数?因为使用vue.extend增加组件,函数就可以每次生成对象,而不会变成引用对象。

模板编译

  • $mount方法会查找template选项,如果没有,则找el的outerhtml。然后需要把template编译成render函数。render函数会放到options的render上。
  • vue2在解析模板使用正则来生成ast语法树。(其实有htmlparser2这个包也能做这个事)
  • 在解析过程中,是每截取一段删一段。类似于如下行为:
function advance(len){
    
    
	html = html.substring(len)
}
function parseStringTag(){
    
    
	const start = html.match(开始标签正则)
	if(start){
    
    
		const match = ....收集匹配信息
		advance(start[0].length)
		let attr,end
		while(!(end = html.match(结束标签正则))&& attr = html.match(属性正则) ){
    
    
			推进match里。
			advance(attr[0].length)
		}
		advance(end[0].length)
		return match
	}
	return false
}
let root = null
let stack = [] //为了查询到父亲,生成树
function createAstElement (tag,attrs){
    
    
	return {
    
    tag,type:1,attrs,children:[],parent:null}
}
function start(tagName,attrs){
    
    
	let element = createAstElement(tagName,attrs)
	if(!root){
    
    
		root = element 
	}
	let parent = stack[stack.length-1]
	if(parent){
    
    
		element.parent = parent
	}
	stack.push(element)
}
function chars(text){
    
    
	let parent  = stack[stack.length - 1]
	if(text){
    
    
		parent.children.push({
    
    text,type:3})
	}
}
function end(){
    
    
	stack.pop()
}
while(html){
    
    
	let textEnd = html.indexOf('<')
	if(textEnd==0){
    
    
		const startTagMatch = parseStringTag()
		if(startTagMatch){
    
    
			start(startTagMatch.tagName,startTagMatch.match)
			continue
		}
		const endTagMatch = html.match(结束标签正则)
		if(endTagMatch){
    
    
			end(endTagMatch[1])
			advanece(endTagMatch[1].length)
		}
	}
	let text 
	if(textEnd > 0 ){
    
    
		text = html.substring(0,textEnd)
	}
	if(text){
    
    
		chars(text)
		advance(text.length)
	}
}
  • 生成render函数,是从前面生成的树过来的。可以去https://template-explorer.vuejs.org/看生成的结果。主要是拼字符串。
  • render函数都会包个with(this), 由于render没有参数,所以就把实例放进去,便于里面取值。后续是render.call(vm)来执行,这里执行会触发取值。同时render中还有很多_c 等函数,直接放到原型上。另外其update方法用来把虚拟dom变成真实dom,后续更新也使用此方法。
  • 根据render方法产生虚拟节点,虚拟节点变成真实节点,插入到el中。
  • 节点插入到根节点是替换操作,所以不能指定到body或者html上。

依赖收集与更新

  • 渲染通过watcher进行渲染。
  • 用户传入的fn(编译后的render)存到watcher的getter上。然后在watcher执行getter之前把dep.target = 该watcher ,执行完后再等于null,类似于vue3里收集effect然后放进栈中操作。
  • 这样在取值代理时,可以拿到dep.target,然后把watcher push进dep自身的数组里。当用户改值了,那么使用dep.notify通知dep数组中的watcher进行更新。
  • 一个dep可以对多个watcher,一个watcher也可以对多个dep,因为一个属性可以在n个组件中渲染,一个组件也可以有多个属性渲染。所以watcher中同样有个数组来存dep,在存放前有个map记录dep的id,用来去重,如果没有记录,那么2者都会同时记录上去,维护的map在watcher中即可。
  • 另外 数组中也需要有dep,所以Observer会加上dep,然后在走数组方法时去通知更新。
  • 在模板编译时的json.stringify默认会取对象所有属性,所以也会收集数组中的对象。但是如果数组中是数组,就需要查看ob属性,进行收集,做个递归。
  • 设置时通知更新,如果有多个属性变化,会导致更新多次,所以会进行暂存再批量处理。每个watcher都有个id,有个等待标识,然后通过map存储将要更新的id去重,将watcher添加进队列,然后用微任务或者宏任务去更新。

mixin

  • mixin有很多缺点,数据来源不明,命名冲突等。
  • 实现主要还是将2个配置项合并,合并有些策略,循环2个配置项。

watcher

  • createWatcher中调的vm.$watch, $watch里面new了个watcher。主要监控该属性变化,变了就调该函数。watcher中判断用户传来的如果是key,那么后续做成取值函数,收集依赖,然后可以拿到老值,新值,以及用户回调,这样在该watcher执行时,则触发回调即可。

diff

  • 主要看tag和key都一致则认为相同节点。相同节点判断是否文本,文本直接替换文本。tag不一样直接删除替换重新创建。相同标签更新属性。然后递归更新儿子,更新儿子时会用双指针,先头对头,如果都等就过,否则从尾开始。如果还不等,那么就会头尾对比。头尾对比成功后,移动元素到末尾,一个指针后移,一个前移,继续头对头尾对尾头对尾比较。如果一开始4轮对比失败,则会查询map上的key,如果有相等,则将该元素提到最前面,用空占位。这个空最后会夹在2个指针之间,之间的删除即可。有新的即插到前面指针之前即可。

组件

  • 组件有全局组件和局部组件,全局就是不用注册,局部就是定义了只能在当前使用。
  • 全局的组件会放到vue.options.components里,如果全局和局部组件重名使用局部的。
  • 有组件的最大好处是组件级更新。
  • vue.component里实际调的vue.extend,vue.extend实际就是继承父类 ,合并配置项。
  • 有了component,需要在生成虚拟节点时去生成组件的虚拟节点。组件的虚拟节点需要改写其属性,增加init等方法。在init中,会去new组件产生实例,合并父组件配置项,然后进行mount挂载。这样在patch时oldvnode就时null,直接返回真实dom。

computed

  • vue2里是给每个计算属性配一个watcher,其上面有个dirty属性,求值后dirty等于false,computed的watcher会有个标识和渲染watcher做区别。在computed watcher值改变后,需要将watcher中的dep拿出来通知渲染watcher进行更新。

猜你喜欢

转载自blog.csdn.net/yehuozhili/article/details/119906208