上文:vue面试题及其结合源码分析

Vue中如何检测数组变化?

vue中检测数组的变化并没有使用defineProperty,因为修改索引的情况不多(且直接使用defineProperty会浪费大量性能,如果数组一万项,我们需要拦截一万次?)所以在vue中是重写数组的变异方法来实现的响应式(函数劫持)

流程:

  1. _init -> initState -> initData -> observe -> defineProperty -> new Observer -> 发现value是数组
  2. 对传入的数组进行原型链修改,后续调用的7个数组变异方法可以达到响应式变化
  3. 但是,修改数组索引,修改长度是无法观测到的(不会响应式触发更新)
  4. 此外,对于数组元素还是数组或者对象的,会再次观测的

image-20220420102131534

源码和第一个问题所在的源码位置是一样的,只是针对数组做了特殊处理。

Vue中如何进行依赖收集?

  • 所谓的依赖收集(观察者模式),被观察者指代的数据(dep收集watcher),观察者有三种(watcher):

    • 渲染watcher
    • 计算属性watcher
    • 用户watcher
  • 一个watcher可能对应着多个数据,所以watcher也需要保存dep

  • 重新渲染的时候,可以让属性重新记录watcher,

  • 计算属性也会用到

  • 一个dep对应多个watcher(一个属性可以用在多个组件上)

  • 一个watcher可以有多个dep(一个组件可以使用多个数据)

  • 默认渲染的时候,会进行依赖收集

模板取值就会来到getter方法:

image-20220420144922254

image-20220420152620191

来到getter,表示当前watcher用到了此属性,那么属性的dep就会提醒watcher去收集此属性对应的dep,然后收集完毕后又让dep把watcher也收集了。当然在收集过程中做过去重操作。

在把依赖都收集完毕后,render函数也执行完毕了。此时清除无效的dep和watcher之间的关系。如果上次的dep在本次调用render函数时,并没有用到该属性的dep,那么就会把watcher和dep之前的关系清理掉。

如何理解vue中模板编译原理

我们用户传递的是template属性,我们需要把这个template编译成render函数。

  • template -> ast语法树
  • 对语法树进行标记(可以理解为优化,有些静态节点压根没有用到data数据,也就是节点本身是不会发生改变的)。递归标记,深度优先,先标记子节点,然后标记父节点。子节点不是静态节点,那么父节点很明显也不是静态的。最后还会标记一次当前节点的根节点是否是静态的。
  • 将ast语法树生成render函数

最终每次渲染可以调用render函数返回对应的虚拟节点。

image-20220420162606155

image-20220420165741462

Vue生命周期钩子是如何实现的

就是内部利用了一个发布订阅模式,将用户写的钩子维护成一个数组,后续在特殊时间点,一次调用callHook。

  • 在调用_init方法中,会合并选项,使用的就是mergeOptions方法

image-20220420172504196

  • 在该方法内进行父和子选项的属性合并

image-20220420172730462

  • 对于有的策略,我们会使用对应的策略方式进行合并,策略模式在这里可以减少if-else的使用。

image-20220420173025571

  • 钩子合并完成后,会在特殊的时间段调用,调用时使用的就是callHook方法.

image-20220420173335594

  • callHook函数就是取出该生命周期的函数,依次执行,但是实际执行钩子的方法是invokeWithErrorHandling:该方法会捕获执行时的异常,也会绑定上下文,也可以在promise的情况下把错误给用户,不是promise的情况,会调用handleError方法帮助处理异常。

image-20220420174011597

  • 此外,对于异常的捕获,也是有生命周期钩子的。在生命周期钩子执行报错后,默认执行此方法

image-20220420174307513

内部实现原理就是发布订阅模式。主要就是靠mergeOptions方法

生命周期合并后,会在特殊时间点调用。比如状态state初始化前后就会调用 beforeCreatecreated两个钩子

image-20220420175306965

问题来了,为什么有些钩子是先执行子的,在执行父的钩子;但是有些又是先父后子?还有组件渲染是如何渲染的?

  • 在组件渲染过程中,遇到父组件就渲染父组件,遇到子组件就渲染子组件。
<!--渲染父-->
<div id="app">
    <!--开始渲染子-->
    <my-btn/>
    <!--子渲染完毕-->
    <!--继续渲染父-->
</div>
<!--渲染完毕-->

image-20220420180410263

image-20220420180644701

对于有些生命周期,肯定需要先执行父组件的,才能执行子组件的,比如created;但是像mounted等生命周期,肯定是先把子组件渲染出来,才能继续渲染父组件,因此肯定是先需要执行子组件的mounted等生命周期钩子。

image-20220420194309178

生命周期的价值

Vue的生命周期方法有哪些?一般在哪一步发送请求及其原因
  1. beforeCreate:这里还没有实现响应式数据。该生命周期是在initState方法之前执行的。其实没什么特别大的用。(Vue3中已经用不到这玩意了)

  2. created:这里已经进行了数据劫持,可以拿到响应式数据(不涉及dom渲染)。这个api可以在服务端渲染中使用。(vue3的setup取代了该钩子)

    // init.js
    // 初始化生命周期 组件父子关系 $parent $children 等
    initLifecycle(vm)
    // 初始化事件 $on $emit $once $off ...
    initEvents(vm)
    // 声明变量 slot等
    initRender(vm)
    callHook(vm, 'beforeCreate')
    //  inject
    initInjections(vm) // resolve injections before data/props
    // TODO init data method computed ...
    initState(vm)
    // provide
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    

    image-20220420203718267

  3. beforeMount:没有实际的价值

    image-20220420203836748

  4. mounted:组件已经挂载完毕,第一次渲染完毕。在这个钩子中可以获取真实dom($el)。要明确一下,不一定mounted执行了,就代表组件全都挂载到页面上了,可能是子组件渲染完毕,挂载到父组件上,此时父组件不一定也渲染完毕了。只是代表我们在这个钩子中可以拿到当前组件对应的dom元素而已。

    image-20220420210322716

  5. beforeUpdate:页面更新会调用该钩子

    image-20220420204700655

  6. updated:组件更新后调用。依赖更新导致视图刷新(非同步)

    image-20220420212306248

  7. activated:组件正在激活(未激活->激活)会被调用

    image-20220420211637051

  8. deactivated

    image-20220420215523293

  9. beforeDestory:手动调用移除后会触发。此时我们的watcher还在,还有响应式数据。(属性,方法等都还在)

  10. destoryed:销毁组件后触发。所有属性和方法均已经被移除后触发。所以对于不涉及组件数据/方法的清理工作,这两个钩子都可以。

    image-20220420221058028

  11. errorCaptured:捕获错误

那我们应该在哪个钩子发起请求?

一般最多的是在mounted中。(created不是比mounted早吗?要明确一下,代码是同步执行的,请求是异步的,就算请求的发出去的早这么一点点,也是等待同步代码执行完毕才能执行异步代码)

虽然服务端渲染都是在created中发起请求获取数据,但是即使是服务端渲染,也很少使用这个钩子。服务端没有dom,也没用mounted钩子。

在哪里发请求,主要看你要做什么事情,什么需求。(请求后获取最新dom做一些事情,就在mounted中)

有人说:created执行完再执行mounted,这个时候异步的created已经执行完了。

这种说法是错误的,因为生命周期是顺序调用的(同步执行),请求是异步的,所以最终获取到数据肯定是在mounted之后的。

另外,我们说的created中拿不到dom,(因为此时还没开始解析模板,生成render,然后执行render函数生成虚拟dom,最后渲染成真实dom。)不能拿dom只是代表不能同步拿dom,但是如果你在created中发起了异步请求,在异步请求中的代码(比如请求成功的回调函数)是可以拿到dom元素了。vm.$el

… 持续更新中…

Logo

前往低代码交流专区

更多推荐