我是如何看Vue源码的(1),【建议收藏】

这里我们可以看到几个beforeCreate,createdMount关键字,大概就能够猜到vue实例的部分生命周期方法就是在这里进行了挂载,再结合 vue官方文档的图示

关于初始化整个vue的状态,可以举例来说,例如initLifecycle中就赋值了parent,children,以及一些isMounted,isDestroy的标识符。initRender中就将attrs,listeners响应化,等等,诸如此类。

initMixin=>initState=>initData,便可以看到挂载props,methods,data,computed,watch了,

可以看到,此处先挂载了props,methods,然后是data的顺序,其实再往下探究逻辑就可以知道,如果存在变量重名,优先级是props>methods>data的,这也就解释了为什么初始化的顺序是这样安排的

initData中,先是获取了data数据,判断props,methods变量重名问题,然后是走了一个代理,将变量名代理到vue实例上,这样的话你的vue实例中,使用this.x指向就可以访问到this.data.x,这类代理也用在了propsmethods

initData获取数据中可以看到一个判断typeof data === 'function' ? getData(data, vm) : data || {}, 支持两种方式获取,实际上如果是自己写这样一个逻辑是会藏有隐患的,如果你的data是直接使用对象,而js的复杂数据类型是地址引用,这意味着,你实例化了两个vue对象,实际上他们的data引用地址是同一个地址,对其中一个vue data的修改会触发另一个vue数据的变动,带来的问题是巨大的

export function proxy (target: Object, sourceKey: string, key: string) {

sharedPropertyDefinition.get = function proxyGetter () {

return this[sourceKey][key]

}

sharedPropertyDefinition.set = function proxySetter (val) {

this[sourceKey][key] = val

}

Object.defineProperty(target, key, sharedPropertyDefinition)

}

这个逻辑处理的设计也是非常巧妙,他覆盖了实例中对该key的访问,使用settergetter将实际访问指向了this.data[key]

这里可以说一下computed的逻辑,实际上也是取巧使用了原本用于data的响应式逻辑,其实看到上面贴出来的proxy代码,大概就能猜到,既然proxy能够改变一个变量读取的指向,那么他也能创造一个虚假变量的指向,这个创造出来的这个变量实际上就是computed所使用的变量,将每次computed函数赋给getter,再加上响应式处理,就完全实现了computed,

走到最后,就是observe(data),也就是开始处理vue数据的双向绑定

二、双向绑定


不同于react的单向数据流,vue使用的双向绑定,单向数据流可以理解为当源头的数据发生变动则触发行为,当然这个变动是主动的,即你需要setState才能触发,而双向绑定则可以抽象为,每一个数据旁边都有一个监护人(一种处理逻辑),当数据发生变化,这个监护人就会响应行为,这个流程是被动发生的,只要该数据发生变动,就会通过监护人触发行为。

如果你之前有过了解,大概就会知道,js每个数据的变动都是通过Object原型链中的setter去改变值,而如果你在他改变值之前,去通知监护人,就能够实现上述的逻辑,这一点很多博客文章都写的非常清楚了。

接着第一部分的initData知道最后observe(data),这里开始正式处理响应式。

2.1 前置条件

前面一直提到,通过Object的原型链改变对象的默认行为:gettersetter,首先我们需要知道,在js中,读取一个对象的值并不是直接读取,而是通过Object的原型链上的默认行为getter拿到对应的值,而改变这种行为实际上是通过Object.defineProperty,来重新定义一个对象的gettersetter,在/src/core/observer/index.js中我们可以看一个defineReactive方法,他就是vue用来实现这种行为的方法,也是这个响应式的核心

function defineReactive(obj, key, val, … ) {

// 此处需要保留getter、setter是因为,开发者可能自己基于defineProperty已经做过一层覆盖,

// 而响应式又会覆盖一次,所以为了保留开发者自己的行为,此处需要兼容原有的getter、setter

const getter = property && property.get // 拿到默认的getter、setter行为

const setter = property && property.set

Object.defineProperty(obj, key, {

enumerable: true, // 是否可以被枚举出来(例如Object.keys(),for in)

configurable: true, // 是否可以被配置,是否可以被删除

get: function() {

const value = getter ? getter.call(obj) : val

return value

}

set: function(newVal) {

setter.call(obj, newVal)

}

})

}

2. 2响应式

首先,我们猜想一下,双向绑定的行为,数据能够响应行为的变化,而行为又能够操作数据的改变,虽然有部分教程会让你站在数据的角度去理解这种行为,实际上,我们站在行为的角度上去理解是更加方便的。

我们将一种行为定义为一个Watcher,他有可能是一个vue文件的template中的dom节点渲染行为,也有可能是computed的计算值行为,总之,我们从行为的角度出发,一个行为的发生,会伴随着对变量的读取(回想一下我们在vue文件中的templatehtml标签时,总是会使用{{obj.xxx}}来读取某个变量并渲染),我们想要实现,变量的改变也会带动这个行为的重新渲染,是不是我们只需要在首次行为发生的周期内,在读取某个变量时,在这个变量内记录这个Watcher,这样的话,下次变量的改变时,我只要触发我之前记录过的Watcher就行了。所以,我们只需要在一个Watcher发生时,将其挂载到一个公共变量上,这样在读取一个值的时候,记录这个公共变量,就能够实现上述操作。

这里先不解释Dep的作用,可以将其抽象理解为一个被挂载在数据上的数组,每次这个数据被一个watch读取时,就会将这个watch记录下来

2.2.1 Watcher

既然说到将一种行为定义为一个watcher,那么可以在/src/core/observer/watcher.js中看到Watcher的实体类,而我们之前一直所说的“行为”,实际上就是构造器的第二个参数expOrFn,可以有表达式或者函数读取的两种模式

class Watcher {

constructor ( vm: Component, // vue实例

expOrFn: string | Function, // 行为

cb: Function, // 为watch服务

options?: ?Object,

isRenderWatcher?: boolean // 判断是否为渲染watcher, )

}

接着来看一种最典型的watcher行为,在/src/core/instance/lifecycle.js中的moundComponent方法中,可以看到一个实例化watcher的方法

new Watcher(vm, updateComponent, noop, {

before () {

if (vm._isMounted && !vm._isDestroyed) {

callHook(vm, ‘beforeUpdate’)

}

}

}, true /* isRenderWatcher */)

可以看到,他将updateComponent(可以抽象为渲染行为)传给Watcher,而在Watcher的实例化中,将会执行此方法,当然在执行之前,pushTarget(this),将这个watcher挂载到公共变量上而后开始执行渲染行为,

class Watch {

constructor(…) {

if (typeof expOrFn === ‘function’) {

this.getter = expOrFn

}

this.get();

}

get() {

pushTarget(this) // 挂载行为至公共Target

value = this.getter.call(vm, vm) // 开始执行行为,之所以会有返回值是为了computed服务

popTarget() // 取消挂载,避免下次读取变量时又会绑定此行为

}

}

此时,如果此行为读取了某个响应式变量,那么该变量的getter将会存储公共变量target,当行为完成后就会取消行为的挂载,这个时候我们再回过头来看前面的defineReactive的逻辑

function defineReactive(obj, key) {

const dep = new Dep(); // 每个数据都有一个自己的存储列表

const getter = property && property.get

const setter = property && property.set

Object.defineProperty(obj, key, {

enumerable: true,

configurable: true,

get: function reactiveGetter () {

const value = getter ? getter.call(obj) : val

if (Dep.target) { // 判断公共变量中是否挂载了行为(watcher)

dep.depend() // 将行为(watcher)加入dep(即此变量的存储行为列表)

}

return value

},

set: function reactiveSetter(newVal) {

const value = getter ? getter.call(obj) : val

if (newVal === value || (newVal !== newVal && value !== value)) {

return // 判断变量没有变化,则直接返回(后两者判断则是因为NaN!==NaN的特性)

}

if (setter) {

setter.call(obj, newVal) // 开始

} else {

val = newVal

}

dep.notify() // 通知自己这个数据的存储列表,数据发生改变,需要重新执行行为(watcher)

}

});

}

这个时候就很清晰明了了,这就是很多博客文章所说的依赖收集,变量在get时通过公共变量Target收集依赖(也就是本文所说的行为),在set时,即变量数据发生改变时,触发更新notify;

2.2.2 Computed

前文有大致介绍computed的实现,实际上在介绍完Wacher之后就可以来详细介绍了,计算属性computed并没有实际的变量,他通过原型链覆盖创造了一个变量指向(src/core/instance/state.jsinitComputed),回忆一下computed的两种写法

‘fullName’: function() {

return this.firstName + this.secondeName;

}

‘fullName’: {

get: function () {…},

set: function() {…},

}

我们再来看一下initComputed

function initComputed (vm: Component, computed: Object) {

const watchers = vm._computedWatchers = Object.create(null)

for (const key in computed) {

const userDef = computed[key]

// 对照着computed的两种写法,就能理解为什么这里有这样的判断,

const getter = typeof userDef === ‘function’ ? userDef : userDef.get

watchers[key] = new Watcher(

vm,

getter || noop,

noop,

{ lazy: true }

)

defineComputed(vm, key, userDef) // 通过defineProperty来创造一个挂载在vm上key(fullName)的指向

}

}

可以看到,他将computedgetter方法,作为Watcher的行为传递了进去,这样在执行getter时,可以将此行为绑定至过程中所读取到的变量(firstName),如此,再下次firstName发生改变时,就会触发此Watcher,重新运行getter方法,得到一个新的fullName的值(还记得前文class Watch中的value = this.getter.call(vm, vm)吗?这个返回值就是computed的返回值),这样就实现了computed的逻辑

2.2.3 Watch

watch的用法,是监听某个变量,当该变量发生变化时,执行特定的逻辑,

上文提到的两种Watcher行为都是函数行为,但是Watcher的行为是支持函数或者表达式的(expOrFn),所以此处的exp(expression)这里就是可以提现到的,我们只需要在变量发生变化时,执行watch定义的逻辑即可,

还记得前文代码defineReactiveset方法通知依赖更新(dep.notify()),虽然前文一直为了方便理解,将Dep描述为一种抽象的列表结构,仅用于依赖收集,但实际上他是一个单独的数据结构,

let uid = 0;

class Dep {

constructor() {

this.id = uid ++;

this.subs = []; // 真正用于收集依赖的数据

}

depend () { // 依赖收集

if (Dep.target) {

Dep.target.addDep(this)

}

}

addSub (sub: Watcher) {

this.subs.push(sub)

}

notify() { // 变量值发生变化,通知更新

// 遍历所有收集的依赖,注意触发更新,

for (let i = 0, l = subs.length; i < l; i++) {

subs[i].update()

}

}

}

Dep.target = null; // 这就是一直说的,用于挂载Watcher行为的公共变量

function pushTarget(target){ Dep.target = target };

function popTarget() { Dep.target = null };

实际上这里的静态变量target以及pushTarget、popTarget是经过简化的,因为渲染并不是一个单一的行为,他是层层嵌套的行为,所以在绑定响应式时,也是需要区分该变量到底是要绑定至哪个行为(否则每个变量都绑定最顶层的行为,一个变量的变化,将会引发整个页面的update),因此真正的target是还有一个stack栈结构,用于挂载多个嵌套的行为

可以看到,每次变量更新,都会触发watcher.update,那么对于watch监听的回调,就可以放到在update中调用

class Watch {

constructor(vm, expOrFn, cb, …) {

this.cb = cb // 这个cb就是watch监听的回调

}

update() {

this.run()

}

run() {

this.cb.call(this.vm, …)

}

}

至此,关于watch监听的实现逻辑大致就是如此

关于依赖收集,实际上并不是在get变量时,直接将watcher绑定至Dep中,可以看到Dep.depend(),他先通知行为(watcher),叫他先绑定自己,然后watcher绑定完dep之后,才会回过头,告知DepaddSub(),这里的逻辑像是一个圈

所以现在我们回过头来看,前文说了,每个数据都有一个“监护人”,来记录此数据所绑定的行为,那么这个“监护人”到底在哪里呢? 可以看到/src/core/observer/index.jsclass Observer中,

class Observer {

constructor(val) {

def(value, ‘ob’, this) // 对value定义__ob__属性,挂载此object

}

}

对于每一份需要响应式处理的数据,都会挂载一个Observer实例,其内subs就是用于记录绑定此数据的Watcher,同时也可以看到,这份数据的get、set方法已经是被重写过了,也就是前文的defineReactive中的覆盖行为。

2.2.4 其他

其实对于Array的响应式是需要特殊处理的,因为他除了set、get之外,还会对数组进行增减操作(splice等),而这些操作是set无法捕捉的,所以覆盖get、set显然无法实现数组的响应式,而vue中采用的是直接覆盖数组的原型链中会对数据本身改变的方法(push、shift、splice等),/src/core/observer/array.js整个文件就是对数据的特殊处理 最新的vue3中,使用了ES6proxy特性来替代这种覆盖set、get实现响应式行为,这种模式同时也能够处理Array

三、结尾


vue的源码当然没有如此简单,很多东西文章都没有涉及到,譬如说,通过上面的逻辑其实你可以发现,depwatcher其实是互相引用的,而js的垃圾回收是检测变量引用的机制,所以如果是简单的复制上文的逻辑,最终的这部分的内存其实是无法被回收的,需要你手动清除,当然vue中也做了这样的处理(每个vm下其实有一个watcherList,用于记录这个示例中所有使用到的watcher,再vm.destroy时,通过遍历watcherList,再销毁每一个watcher,而watcher中又会自己销毁Dep),但是限于篇幅原因无法详细介绍了。

最后

=============================================================

为了帮助大家更好温习重点知识、更高效的准备面试,特别整理了**《95页前端学习笔记》**电子稿文件。

主要内容包括html,css,html5,css3,JavaScript,正则表达式,函数,BOM,DOM,jQuery,AJAX,vue 等等。

👉点击这里免费获取👈

html5/css3

  • HTML5 的优势

  • HTML5 废弃元素

  • HTML5 新增元素

  • HTML5 表单相关元素和属性

  • CSS3 新增选择器

  • CSS3 新增属性

  • 新增变形动画属性

  • 3D变形属性

  • CSS3 的过渡属性

  • CSS3 的动画属性

  • CSS3 新增多列属性

  • CSS3新增单位

  • 弹性盒模型

JavaScript

  • JavaScript基础

  • JavaScript数据类型

  • 算术运算

  • 强制转换

  • 赋值运算

  • 关系运算

  • 逻辑运算

  • 三元运算

  • 分支循环

  • switch

  • while

  • do-while

  • for

  • break和continue

  • 数组

  • 数组方法

  • 二维数组

  • 字符串

正则表达式

  • 创建正则表达式

  • 元字符

  • 模式修饰符

  • 正则方法

  • 支持正则的 String方法

js对象

  • 定义对象

  • 对象的数据访问

  • JSON

  • 内置对象

  • Math 方法

  • Date 方法

面向对象是一种编程思想

  • 定义对象

  • 原型和原型链

  • 原型链

  • 原型

常用的JavaScript设计模式

  • 单体模式

  • 工厂模式

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

核心竞争力,怎么才能提高呢?

成年人想要改变生活,逆转状态?那就开始学习吧~

万事开头难,但是程序员这一条路坚持几年后发展空间还是非常大的,一切重在坚持。

为了帮助大家更好更高效的准备面试,特别整理了《前端工程师面试手册》电子稿文件。

前端面试题汇总

JavaScript

性能

linux

前端资料汇总

完整版PDF资料免费分享,只需你点赞支持,动动手指点击此处就可免费领取了

前端工程师岗位缺口一直很大,符合岗位要求的人越来越少,所以学习前端的小伙伴要注意了,一定要把技能学到扎实,做有含金量的项目,这样在找工作的时候无论遇到什么情况,问题都不会大。

试,特别整理了《前端工程师面试手册》电子稿文件。

前端面试题汇总

JavaScript

性能

linux

前端资料汇总

完整版PDF资料免费分享,只需你点赞支持,动动手指点击此处就可免费领取了

前端工程师岗位缺口一直很大,符合岗位要求的人越来越少,所以学习前端的小伙伴要注意了,一定要把技能学到扎实,做有含金量的项目,这样在找工作的时候无论遇到什么情况,问题都不会大。

  • 9
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Cesium Vue源码是一个开源项目的源代码,它结合了Cesium和Vue.js两个框架的优势,用于在Web上构建高性能的3D地球应用程序。 Cesium是一个强大的基于Web的地理空间可视化引擎,可以实现全球范围内的地图展示和3D场景的渲染。而Vue.js是一个流行的JavaScript框架,用于构建用户界面。通过将这两个框架结合在一起,Cesium Vue能够提供一个更灵活和易于使用的工具,用于开发各种各样的地球应用程序。 Cesium Vue源码包含了各种组件和工具,用于与Cesium引擎进行交互和集成。例如,它提供了CesiumViewer组件,用于创建一个全功能的Cesium场景,并且能够与Vue组件进行交互。它还提供了各种用于加载地理数据、设置相机位置、渲染3D对象等功能的工具函数。 使用Cesium Vue源码,开发人员可以方便地创建一个具有交互性和可视化效果的地球应用程序。他们可以轻松地创建地图、添加地标和矢量数据,实现相机控制、动画效果等功能。同时,由于使用了Vue.js框架,开发人员还能够轻松管理和更新应用程序的状态,并且可以方便地与其他Vue组件进行集成。 总的来说,Cesium Vue源码提供了一个强大的工具,用于开发具有高性能和可交互性的3D地球应用程序。通过结合Cesium和Vue.js的优势,它能够为开发人员提供更好的开发体验和更多的灵活性。无论是构建地图应用程序、数据可视化还是虚拟现实应用程序,Cesium Vue都是一个非常有价值的工具。 ### 回答2: Cesium Vue源码是一个基于Vue框架封装的对Cesium进行集成的库。Cesium是一个用于创建各种地理和地球空间应用的开源JavaScript库,而Vue是一个用于构建用户界面的渐进式JavaScript框架。 Vue的优势在于其虚拟DOM和响应式数据绑定的特性,使得开发者可以更加高效地构建可复用的组件和灵活的界面。Cesium则提供了强大的地理可视化功能,包括3D地球的展示、地理信息的呈现和地图操作等。 Cesium Vue源码的主要目的是将Cesium和Vue无缝集成,使得开发者可以更加便捷地在Vue项目中使用Cesium的功能。通过Cesium Vue,我们可以轻松地将Cesium的3D地球嵌入到Vue的组件中,使用Vue的数据绑定和组件化技术来管理Cesium的状态和交互。 Cesium Vue源码实现了Cesium和Vue之间的双向数据绑定,使得我们可以通过Vue的数据驱动Cesium的展示和交互。同时,Cesium Vue提供了一系列的Vue组件,用于简化和封装Cesium的常用功能,比如地图控制、实体渲染和相机操作等。 通过Cesium Vue,开发者可以利用Vue的生态系统和丰富的插件,以及Cesium的地理可视化功能,快速构建高质量的地理和地球空间应用。无论是创建交互式地图应用,还是构建具有地球模型的数据可视化工具,Cesium Vue都提供了简洁、灵活和高效的开发方式。 总之,Cesium Vue源码是一个基于Vue框架封装的Cesium集成库,通过它我们可以更加便捷地在Vue项目中使用Cesium的功能,享受Vue的开发便捷性和Cesium的地理可视化能力。 ### 回答3: Cesium-Vue源码是一个基于Vue.js框架开发的Cesium地球引擎的封装库,用于在Vue项目中使用Cesium进行地球可视化开发。该库主要提供了一系列在Vue组件中使用Cesium功能的API和组件。 源码的结构主要包括了基于Vue的组件目录、样式文件目录和存放Cesium相关代码的目录。在组件目录中,可以看到各个Vue组件用于实现不同的地球可视化功能,如地图视图、图层控制、导航控制等。样式文件目录主要包括了用于美化地球视图的CSS样式。 Cesium-Vue源码的主要功能包括: 1. 地图视图:通过Vue组件提供的功能,可以在项目中创建一个地球视图,并进行交互操作,如移动、旋转等。 2. 图层控制:该库提供了图层控制的API和组件,使得开发者可以方便地添加不同类型的图层,如地形图、影像图等,并进行切换和控制。 3. 数据可视化:通过Cesium的API和Vue的数据绑定机制,可以实现将数据可视化展示在地球上,如点、线、面等实体的展示和交互。 4. 导航控制:Cesium-Vue提供了一系列的API和组件,用于实现地球视图的导航控制,如缩放、旋转、定位等操作。 总体而言,Cesium-Vue源码的设计和实现通过将Cesium地球引擎与Vue.js框架相结合,提供了一种便捷的方式在Vue项目中开发地球可视化应用。通过使用该库,可以方便地在Vue项目中创建地球视图、添加图层、展示数据,并实现交互和导航控制等功能。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值