Description
vue1 小粒度更新,精确追踪到数据变化所影响的dom变化,精确更新变化的dom
具体实现为,维护 observer watcher directive 三个类
-
observer负责监听数据变化,并派发事件,向上层传播事件,维护一个watcher数组
-
watcher订阅observer,数据变化时执行事件,包括$watch注册的回调函数和视图更新
-
directive负责建立数据data到dom对象的对应关系,对不同指令应用不同的更新方法,是watcher的其中一种类型
-
parser 解析类似user.name user[0] user["name"] 这样的expression,转换为最终可查找到属性的路径
ps. 以上思路是简化版,直接把observer按数据层级关系组织。而源码中是单独用了一个binding类来组织watcher的层级关系的。事件触发后,在observer中传播到顶层获得一个变化数据的key(比如user.name.abc),再用这个路径从binding的根开始定位到对应的user.name.abc,watcher存放在这个binding对象中。在这种策略中,只有最顶层的observer被监听了,子observer只负责把事件传播到顶层而已。
vue2 以组件粒度为范围,组件内diff式更新,组件层面还是按vue1的方式更新
具体区别体现在,每个组件有了render函数,数据变化时只通知到组件更新,组件更新时会重建全部vnode树,而不是精确更新了(当然到dom层面时还是会做diff,同样表现为精确更新)
好处有:1.render函数可以用js写组件,更灵活
2.跨平台,vue1模板渲染方式依赖浏览器先解析vue模板
3.如果要建立精确的数据--dom对应关系,需要占用大量内存维护directive,vue2可以节约这部分内存
4.小粒度更新需要维护一个变更队列(当数据重复变化时)来避免不必要的dom操作,vue2不要维护这部分
vue的核心部分
- 模板编译
- 初始化时做的:template ==> parse() ==> ASTtree ==> generate() ==> render函数 ==> mount(调用dom方法)
- 每次更新都要做的: render函数 ==> vNode tree ==> patch(oldVnode, vNode) ==> 调用dom方法更新
-
$watch
批量更新 通过Object.defineProperty实现 -
diff算法
关键词:同层级比较 复杂度o(n) 两对头尾指针 加key复用
实现: patch==> 判断sameNode ==> patchNode() ==> 更新text && updateChildren ==> while循环 递归调用patchNode
面试被问到的diff相关问题:
- diff为什么可以提升性能,diff一定比直接操作dom要快吗?
diff的优势主要在两方面,多次dom操作合并成最后一次&对比更新缩小操作范围。对于单次、单个的dom变更,显然加了diff效率会降低。但是这种冗余是js侧的冗余,如果我们合理地细分组件,就算慢也慢不了太多,而高频率大面积数据变更的情况下,直接操作dom就会比diff慢很多。实际开发中,我们不可能针对每一个dom去做手动优化,为了代码的可读性和简洁性,会出现一些“伤及无辜”的情况,所以需要一种统一的优化策略,diff的性能提升主要就是基于这个前提。
- 为什么要用头尾指针,头和头对比完了还要去对比头和尾呢?
diff的逻辑是,以newCh为基准去oldCh里找匹配的vnode,也就意味着如果某个old vnode一直没有被匹配上,oldCh的index就会一直卡在这个vnode,后面的vnode就根本没有匹配的机会。假设同一个节点在oldCh的头,而在newCh的尾,如果只头头对比,newCh一直匹配不上就一直新建dom,oldCh的匹配位一直卡在头这里,直到newCh遍历到最后一个才发现能匹配上。增加头尾对比主要就是为了避开这种最差的情况,虽然整体效率会低一些,但优化了最差的情况。
- diff为什么只做同层对比?
跨层对比相当于去判断一棵树是否是另一棵树的子节点。而且情况不同的是,diff时并不是对于完全相同的节点才去复用,类似tagName相同这样的条件,也会判为sameNode去复用。在这种宽泛的判断基础上,讨论是否子节点没有意义,直接同层对比可以优化时间复杂度。
- vue为什么要强制我们给列表加key?有的列表根本没有key,或者有的列表我们不希望它去复用,这种情况下vue还是会报错,是不是一种设计的不合理?怎么去解决非要加key这个问题呢?
可以把使用列表的场景分为两种来看:
1.需要读写的列表:包含展示和操作(增/删/排序)两方面,这种情况下是需要复用的,vue要求加key是合理的,如果后端没有给key,可以维护一个自增的变量,每次新增的时候赋值给key。
2.只需要展示的列表:这也就是题述不需要复用的情况,常见的场景如翻页时刷新的列表,每一页都是完全不一样的列表内容,并且是只读的也不提供操作(增/删/排序)。这种情况下就可以直接用index设为key,我们以前之所以不用index是因为操作数组时index会错乱,在纯展示情况下就不用担心这个问题了。
组件与生命周期
父子组件创建顺序
父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted ->父mounted
可以看到beforeMount之后才进入子组件的生命周期,但是这个钩子并没有在_init方法中直接调用,需要先走到$mount里。
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
$mount主要做两件事:
- 调用
compileToFunctions
方法,把template或el对应的模板编译成render函数(如果是运行时版本,会跳过这一步) - 调用
mountComponent
方法,也主要做三件事
- 调用
beforeMount
钩子 - 调用render函数生成vnode,然后update逐个生成真实dom。在这一步里如果遇到子组件,会递归创建子组件。关键函数有两个,第一个在render过程中,调用了
createComponent
为组件创建vnode(占位符);第二个在update的patch过程中,也就是为vnode创建真实dom的时候,识别出vnode为组件占位符后,会调用createComponentInstanceForVnode
创建组件实例,正式进入子组件的生命周期。
vm._update(vm._render(), hydrating)
- 所有子组件递归完毕,所有真实dom都已生成,vm的$el指向渲染后的根元素,标志着mount阶段完成,此时调用mounted钩子
因为占位vnode的存在,一个组件实例上其实保留了两个vnode的引用,分别是$vnode
和_vnode
,它们的结构也完全不一样。$vnode
是在render父组件时创建的子组件占位符,所以会保留许多组件相关的引用,data中有组件的hook;_vnode
是在render子组件时创建的普通vnode,对应组件的根元素。$vnode
理论上应该不对应真实dom,但实际上做了一些特殊处理,把componentInsatance
的$el赋值给了$vnode.elm。$vnode
和_vnode
是父子关系,但它们的elm指向相同的dom。
$vnode(另外在$options中有个_parentVnode也是指向它)
_vnode,就是普通的li元素
生命周期的应用:
- created阶段:可以对data操作,因为此时data已经初始化完成
- beforeMount阶段:可以对render函数做一些拦截或装饰操作,因为这个时候render函数已经生成,可以通过$options.render取到,但是尚未被调用。
- mounted阶段:可以操作真实dom,但是渲染尚未开始
组件更新的传递
- 首先在mountComponent那一步里,给watcher添加了updateComponent回调,还是这一行代码,每次数据变化后就会执行,影响的范围是当前组件及子组件
vm._update(vm._render(), hydrating)
- 按照和初次渲染相同的模式,第二次生成了组件的占位$vnode,新旧$vnode被判断为sameNode时(即还是同一种组件),会走到patchVnode里,在这里调用
prepatch
钩子中的updateChildComponent
方法,将父组件的更新情况传递给子组件。由于子组件的prop也是响应式的,所以同步了父组件data后,如果有变化,会自动进入子组件的update生命周期,父组件就不需要管了。如果没有变化,那么子组件不受影响,也不需要更新。对于含slot children
的组件,则与prop不同,会执行强制更新。