vue源码一刷

vue源码一刷

前言

​ 2022年一月二十四日,一个平平常常的日子,细蒙蒙的雨丝夹着一星半点的雪花,正纷纷淋淋地向大地飘洒着。时令已快到小年,厚厚的雪层铺落在地面,然而在熙熙攘攘的大街上,雪花往往还没等落地,就已经消失得无踪无影了。京都寒而漫长的冬天看来就要过去,但那真正温暖的春天还远远地没有到来。

变化侦测篇

​ Vue最大的特点就是数据驱动试图。用户改变数据(state),视图(UI)也会跟着改变。而在代码中则是将templete(中的数据)转换成render函数,进而更新到页面。

​ 那么,数据是怎么跟新到页面的?这就引出了Vue的第二个关键词,变化侦测。首先需要明白Object.defineProperty()中有getter/setter属性可以将数据转化成响应式,然后根据_ob_属性判断数据是否是响应式,之后会将数据以getter/setter形式侦测变化。

​ 当观察到数据变化后,是怎么通知到视图跟新的呢?这就引出了第三个关键词,依赖收集。对data中的每一个数据都建立一个依赖管理器dep类,在getter中调用dep.depend()方法收集依赖,在setter中调用dep.notify()通知依赖更新。对组件实例来说,谁用到了这个数据,谁就是依赖,为这个组件实例创建一个watcher实例,同时自动的把组件实例添加到数据对应的依赖管理器中,当数据变化时,会通知watcher实例,再由watcher实例去通知真正的依赖。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8wIQ0Sl7-1643444280670)(C:\Users\jmy\Desktop\新建文件夹 (2)]\3.0b99330d.jpg)

总结来说:

​ 通过object.defineProperty()方法实现了对数据的可观测,observer类可以将数据的属性都转换成getter/setter来侦听变化。

​ 知道了依赖收集,在getter中收集依赖,在setter中通知依赖更新。封装了依赖管理器Dep,dep.depend收集依赖,dep.notify通知依赖更新。同时为每一个依赖都创建一个watcher实例,当数据变化时,先通知到watcher实例,再由watcher实例去通知依赖更新(视图更新或触发某个回调函数)。

虚拟dom篇

​ 所谓虚拟dom,就是用一个js对象来描述一个DOM节点.

​ 有标签,属性,文本,子元素等。

<div class="a" id="b">我是内容</div>

{
  tag:'div',        // 元素标签
  attrs:{           // 属性
    class:'a',
    id:'b'
  },
  text:'我是内容',  // 文本内容
  children:[]       // 子元素
}

​ 那么为什么有虚拟dom呢?

​ 因为vue是数据驱动视图的,改变数据时视图会更新,从而会使DOM发生改变。然而操作DOM是非常消耗性能的,一个真实的DOM节点是很庞大的,如下图:

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RZZjsjuE-1643444280672)(C:\Users\jmy\Desktop\新建文件夹 (2)]\Vue中的虚拟DOM _ Vue源码系列-Vue中文社区_files\1.a052465d.png)

​ 所以,我00们可以用JS的计算性能来换取操作DOM所消耗的性能。最直观的思路,就是不要盲目的更新视图,而是通过对比数据变化前后的状态,通过DOM-DIFF算法计算出需要更新的地方。

​ 那么,虚拟DOM是怎么形成的呢?这就引出了VNode,VNode是什么?从何而来?

​ 首先,VNode类中包含了真实DOM节点所需的一系列属性,通过属性间的不同搭配,从而描述出各种类型的真实DOM节点。

​ templete经过编译生成render函数后,将原生的HTML保留,而v-if等非原生的vue等内容就会产生Vnode,经过打补丁(patch)从而得到想要的视图,最后根据VNode创建出真实的DOM节点,并插入到视图中,完成视图的渲染更新。

​ 从上面可以得到patch,即为打补丁,那么打补丁到底是什么?

​ 在vue中的dom-diff过程,成为打补丁。即时对旧的VNode进行修补,从而得到新的VNode。打补丁的过程,实际上是对VNode做增删改的操作。

​ 1.创建节点:其中,节点可分为元素节点,文本节点,注释节点。区分的话,元素节点有tag属性,注释节点的isComment属性为true,都没有的则是文本节点。

​ 2.删除节点:新Vode没有而旧的VNode有的,就把该节点从父元素上调用removeChild()删除。

​ 3.更新节点:我们可以把节点划分为静态节点,文本节点,元素节点。

​ 静态节点在更新时会直接跳过,文本节点只修改文本内容,元素节点比较哪里不一样改哪里,如子节点,子节点文本等。

模板编译篇

​ 什么是模板?写在标签中类似原生HTML的内容我们称之为模板。

​ 模板经过一系列的逻辑处理生成渲染函数,也就是render()函数,而render函数会将模板内容生成对应的VNode,经过patch(打补丁)后生成虚拟DOM,更新到视图中,并转化成真实DOM。

​ 模板编译的过程其实就是把用户的templete模板经过一系列处理后最终生成render函数的过程.那么,具体流程是什么样呢?

​ 首先,具体流程大致可以分为三个阶段。

​ 1.模板解析阶段:将一堆模板字符串用正则等方式解析成抽象语法树AST;
​ 2.优化阶段:遍历AST,找出其中的静态节点,并打上标记;
​ 3.代码生成阶段:将AST转换成渲染函数;

​ 1.首先,在模板解析阶段,主要是将templete标签转化为AST抽象语法树。模板解析的主线函数为parseHTML,当遇到文本时会调用parseText,有解析器时会调用parseFilters.

​ 而parseHTML有两个参数,分别是templete和options,其中options是定义了四个钩子函数。第一个参数是解析模板字符串的,而第二个参数的四个钩子函数把提取出来的内容转化为各自的AST抽象语法树。

​ 由于AST抽象语法树是单独创建且分散的,那么,如何保证AST抽象语法树的层级关系与真正DOM节点相同呢?

​ Vue在HTML解析器的开头定义了一个栈stack,这个栈的作用就是来维护AST节点层级的。开始标签和结束标签被匹配到的时候就会形成闭环从而被poll出栈。

let text = "我叫{{name}},我今年{{age}}岁了"
let res = parseText(text)
res = {
    expression:"我叫"+_s(name)+",我今年"+_s(age)+"岁了",
    tokens:[
        "我叫",
        {'@binding': name },
        ",我今年"
        {'@binding': age },
    	"岁了"
    ]
}

​ 其次,文本解析器parseText内部干了三件事:

​ 1-1.判断传入的文本是否为变量。

​ 1-2.构造expression

​ 1-3.构造tokens

2.在优化阶段,会找出静态节点和静态根节点并打上标记。

​ 有一说一,在把templete转化成AST语法树后,就可以转换render函数了,render函数就可以生成模板对应的VNode,最后经过patch打补丁,完成视图渲染。然而,静态节点不管状态如何是都不会变化的,我们可以先给静态节点和静态根节点打上标记,在patch的过程中,不去对比静态节点,而是直接复制,则是又可以提高一些性能。

3.代码生成阶段 将AST抽象语法树生成对应的render函数字符串。

​ 事实上,Vue实例在挂载的时候会调用其自身的render函数来生成实例上的template选项所对应的VNode,简单的来说就是Vue只要调用了render函数,就可以把模板转换成对应的虚拟DOM。那么Vue要想调用render函数,那必须要先有这个render函数,那这个render函数又是从哪来的呢?是用户手写的还是Vue自己生成的?答案是都有可能。我们知道,我们在日常开发中是可以在Vue组件选项中手写一个render选项,其值对应一个函数,那这个函数就是render函数,当用户手写了render函数时,那么Vue在挂载该组件的时候就会调用用户手写的这个render函数。那如果用户没有写呢?那这个时候Vue就要自己根据模板内容生成一个render函数供组件挂载的时候调用。Vue自己根据模板内容生成render函数的过程就是代码生成阶段。

生命周期篇

​ 如同人的出生到死亡一样,每个Vue实例都会经历从出生到死亡等许许多多的事情,例如设置数据监听,模板编译,组件挂载等。这些结合起来则被称为Vue实例的生命周期。

在这里插入图片描述

​ 由上图我们可以得到,Vue的生命周期大致可以分为四个阶段:

1.初始化阶段:在Vue实例上初始化一些属性,事件及响应式数据。

2.模板编译阶段:将模板编译成渲染函数。

3.挂载阶段:将实例挂载到指定DOM上,即将模板渲染到真正的DOM。

4.销毁阶段:将实例自身从父组件删除,并取消依赖追踪和事件监听器。

下面来详解在这几个生命周期各自做了什么事情:

1.首先,在初始化阶段,第一件事就是new Vue()创造一个实例。具体就是合并配置,调用一些初始化函数,触发生命周期钩子函数,调用$mount开启下一阶段。

初始化阶段的第二个初始化函数——initLifecycle函数。它给实例初始化了一些属性,包括以$开头给用户使用的外部属性,也包括以_开头供内部使用的内部属性。

初始化阶段的第三个初始化函数——initEvents函数。在这之前,先回顾一下模板编译:将templete函数转换为render函数,以供挂载阶段生成虚拟DOM,那么在挂载阶段,若被挂载的是一个组件节点,则通过createComponent函数创建一个组件VNode。

父组件给子组件注册事件,把自定义事件传给子组件,在子组件实例化时会对事件进行初始化,也就是说,初始化事件函数initEvents实际上初始化的是父组件在模板中使用v-on或@注册的监听子组件触发的事件。

initEvents函数的具体实现过程,该函数内部首先在实例上新增了_events属性并将其赋值为空对象,用来存储事件。接着通过调用updateComponentListeners函数,将父组件向子组件注册的事件注册到子组件实例中的_events对象里。

初始化阶段的第四个初始化函数——initInjections函数。用来初始化实例中的inject选项。一般来说,provide和inject成对出现,父组件使用provide选项向下游子孙组件内注入一些数据,子孙组件可以使用inject选项接收这些数据自己使用。

但需要注意的一点是provide和inject的值不是响应式的。

初始化函数的第五个初始化函数——initState。在实际开发中,一些如props,data,methods,computed,watch选项,这些称为组件实例的状态选项。Vue对每个组件实例进行侦测,用vm._watchers属性对数据进行监听,存放组件内用到的所有依赖。当有状态发生变化时,会通知到组件,然后由组件内部使用虚拟DOM进行数据对比,从而降低内存开销,提升性能。

调用选项初始化子函数时,例如initProps,initMethods,initData,initComputed,initWatch,总之有什么就去调用什么。

data中的数据若非响应式时,调用observe()将数据转换成响应式。

computed中的dirty属性,默认为false,当依赖发送变化时才需要重新计算。

2.在模板编译阶段,获取用户传入的模板内容并将其编译成渲染函数。在实际开发中借助webpack的vue-loader进行编译,一来可以减少生产环境的代码体积,二来可以提升性能。

3.在挂载阶段,创建vue实例,用其替换el选项对应的dom元素,同时开启对模板中数据的监控。当数据变化时,会通知相关依赖进行视图更新,也就是打补丁。

4.销毁阶段,调用vm.destroy()。主要是将此实例从其父级实例上删除,取消当前实例上的依赖追踪,且移除实例上的所有事件监听器。

实例方法篇

介绍三个实例方法:vm. s e t , v m . set,vm. set,vm.watcher,vm.$delete。在stateMixin()函数中挂载到vue原型上。

1.watcher将被观察数据内部所有值都递归读取一遍,那么这个watcher实例就会被加入到对象内所有值的依赖列表中。之后当对象内任意某个值发生变化时就可以得到通知了。

2.$set:对对象和数组做添加和删除操作时,Vue无法观测到数据的变化,也就无法对其进行响应式的处理。

​ 对数组来说,重写了splice方法,

​ 对对象来说,则是用defineReactive方法将新属性添加到target上,并将其转化为响应式,通知依赖更新。

3.delete做删除操作时会判断元素的key是否存在于target中,,存在就删除,不存在就不做任何操作。若是响应式对象,则通知依赖更新,若不是,则不通知更新。

介绍四个事件相关的方法,分别是vm. o n , v m . on,vm. on,vm.emit,vm. o f f , v m . off,vm. off,vm.once。她们都是在eventsMixin函数中挂载到vue原型上的。

1.vm.$on:

​ 我们要知道, o n 和 on和 onemit的设计模式是最典型的发布订阅模式,首先定义一个事件中心,通过 o n 订 阅 事 件 , 将 事 件 存 储 在 事 件 中 心 , 然 后 通 过 on订阅事件,将事件存储在事件中心,然后通过 onemit触发事件中心中存储的订阅事件。

2.$emit:

​ 触发当前实例上的事件,触发参数都会传给监听器的回调,逻辑就是根据事件名从事件中心获取到该事件的回调函数。

3.vm.$off

​ 移除事件的监听器

4.vm.$once

​ 监听一个自定义事件,且只触发一次。

介绍四个生命周期相关方法:分别是vm. m o u n t , v m . mount,vm. mount,vm.forceupdate,vm. n e x t t i c k , v m . nexttick,vm. nexttick,vm.destroy

1.vm.$mount

​ 若在实例化时没有收到el选项,则处于“未挂载”状态,可以使用vm.$mount()手动挂载一个未挂载的实例。

2.vm.$forceUpdate()

​ 作用是迫使vue实例重新渲染。

​ 其实。收集依赖就是收集相关的watcher,依赖更新就是watcher调用update方法,当实例依赖的数据发生变化时,就会通知实例watcher去执行update方法进行更新。 相关源码为 vm._watcher.update()

3.vm.$nexttick()

​ 在生命周期的created中,执行DOM操作要放在$nexttick中,因为vue 对dom更新是异步执行的,只要监听到数组变化,vue将会开启一个事件队列,并缓冲同一事件循环中发生的所有数据变更。如果同一个watcher被多次触发,那么它只会被推入到事件队列一次,在缓冲中去除重复数据和不必要的dom操作至关重要。

由$nexttick引出的JS运行机制:

​ js代码执行是单线程的,基于事件循环。

循环步骤:

​ 1.所有同步任务都在主线程上执行,形成一个执行栈。

​ 2.主线程之外,还有一个任务队列,只要异步任务有了运行结果,就会在任务队列中放置一个事件。

​ 3.一旦执行栈中所有任务执行完毕,系统就会读取任务队列。对应的异步任务结束等待状态,进入执行栈开始执行。

​ 4.上面三步不断重复。

主线程的执行过程就是一个tick,而所有的异步结果都通过任务队列来调动。而任务队列中又存在宏任务与微任务。

先遍历宏任务队列,在遍历微任务队列。

常见的宏任务与微任务有:

macro Task:settimeout,setimmediate,

个任务队列,只要异步任务有了运行结果,就会在任务队列中放置一个事件。

​ 3.一旦执行栈中所有任务执行完毕,系统就会读取任务队列。对应的异步任务结束等待状态,进入执行栈开始执行。

​ 4.上面三步不断重复。

主线程的执行过程就是一个tick,而所有的异步结果都通过任务队列来调动。而任务队列中又存在宏任务与微任务。

先遍历宏任务队列,在遍历微任务队列。

常见的宏任务与微任务有:

macro Task:settimeout,setimmediate,

micro Task:promise.then,Mutation,Observer

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

腿给你干断

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值