前言
初始化
- 初始化过程主要就是将配置项写入$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进行更新。