Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vue1.x & vue2.x 数据驱动更新视图机制对比 #8

Open
natsu0728 opened this issue Mar 7, 2019 · 0 comments
Open

vue1.x & vue2.x 数据驱动更新视图机制对比 #8

natsu0728 opened this issue Mar 7, 2019 · 0 comments
Labels

Comments

@natsu0728
Copy link
Owner

natsu0728 commented Mar 7, 2019

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的核心部分

  • 模板编译
  1. 初始化时做的:template ==> parse() ==> ASTtree ==> generate() ==> render函数 ==> mount(调用dom方法)
  2. 每次更新都要做的: 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主要做两件事:

  1. 调用compileToFunctions方法,把template或el对应的模板编译成render函数(如果是运行时版本,会跳过这一步)
  2. 调用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也是指向它)

RTX截图未命名

_vnode,就是普通的li元素

RTX截图未命名3

生命周期的应用:

  • 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不同,会执行强制更新。

参考

简易动态数据绑定
VueComponent的创建过程
component与生命周期
组件更新

@natsu0728 natsu0728 added the vue label Mar 7, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant