Vue2源码主要内容详解

数据实现响应式

vue2 使用的Object.defineProperty,利用observe递归和遍历属性 进行属性的绑定,设置值的时候 同样会进行绑定,如果是对象就会递归,进行Object.defineProperty。



数组的绑定

先复习js知识点Object.create方法将一个对象,绑定到另一个对象的原型上面

判断值如果是数组了,就会执行数据的特殊的双向绑定的方法。并且为数据的每一个属性都判断,如果为对象就双向绑定,否则不绑定。这里需要注意一下ObserveArray方法,这个是判断列表中的元素是否为对象,是的话给对象里面的属性进行双向绑定,而不是直接给list[0]这个进行绑定。list[0]='123’是不会触发set的。如果list[0]是对象的话会触发。
list[0]={name:‘q’}
list[0].name='w’这样则会触发更新

在这里插入图片描述


在数组对象上新定义 push,shift等方法,并且利用Object.create方法将Array.propotype绑定到新的对象的原型上,这样新的对象就能使用数据的所有方法了,包括foreach等等, 然后对push等方法进行重写,添加上双向绑定的逻辑,内部再调用数组原先的方法即可

在push方法中怎样得到的observeArray方法的,就是给数组添加新的属性,数组data.__ob__将当前类this赋值给 数组data.ob=this
这样在重写的push方法中就可以用__ob__得到observe 进而实现双向绑定了

newArrayProto

const oldArrayProto = Array.prototype
export newArrayProto = Object.create(oldArrayProto)//导出此变量
newArrayProto.push = function(...args){
	const result = oldArrayProto.push.call(this,..args)
	...省略
	this.__ob__.observeArray(...args)
	/**
	对新增的数据进行数据劫持使用observeArray方法,
	在上面已经对数据添加了__ob__属性,这个属性指向vm
	所以使用
	this.__ob__.observeArray(...args))就可以对新的内容进行双向绑定
	**/
}




解析模板参数


大概思路步骤如下 1、采用虚拟dom,数据变化后比较虚拟dom的差异,更新需要更新的地方。
2、核心是将模板变成js语法,通过js生成虚拟dom。
**template模板,
将template转化为render函数

1.解析的时候第一步先看是否有render
如果有
    就取render函数
如果没有
    就取template属性或者从el上得到整个模板
**

取到整个模板之后 就要将模板变为render函数返回

2、编译的过程
compileToFunction方法
如果使用script标签的话是在浏览器中运行的,
如果是脚手架则是启动前由loader提前编译好的。
在这里插入图片描述
以下是compilToFunction的具体实现
在这里插入图片描述

**
上面compilToFunction函数的详细步骤为下面的1和2
1、将template转化为ast语法树
2、根据ast语法树生成render方法,

3、执行render方法生成虚拟dom
4、根据虚拟dom生成真实dom**

1、如下图就是ast对象语法树,使用js对象描述了整个template模板,利用正则表达式
在这里插入图片描述

2、 如下图就是编译后生成的render函数,里面可以将模板中的js语法进行执行,可以使用相应的变量,注意jsx最终也会被便以为render函数
会先将下面的内容变为字符串,然后使用new Function(with(this){字符串render})变为真正的render函数。放在vm上面去执行。
在这里插入图片描述

3、将render函数变为虚拟dom
只需要调用vm.$options.render函数即可得到一个虚拟dom,
然后使用vm._update函数,将虚拟dom变为真实dom
在这里插入图片描述
ast本身是描述html语法的,虚拟dom是对应真实dom的,正好相反的,ast是html到js,虚拟dom是 js到html。dom可以增加属性,增加功能。

_update方法中是调用的patch方法,利用nodeType判断是否为真节点,如果是的话 说明是初渲染,就将老节点删除 换为新生成的真实dom

在这里插入图片描述
如上图的patch方法就是创建新的dom节点,然后插入,将返回的新的dom节点替换给vm.$el

到上面 初始化就完成了,接下来是更新

将渲染逻辑放到watcher类中,watcher存放渲染逻辑的代码而已。
每个组件中都有一个watcher实例,watcher实例中存放着dep列表,dep中又存放着watcher,每当数据更新的时候,就会去找对应的watcher实例进行更新。

为什么组件化,可以复用,方便维护,局部更新。

dep收集watcher,
当new watcher初始化的时候(在mount的时候调用的图一和图二)。
图二中的new Watcher执行的时候会执行Watcher类中的this.get(图四)

(图四调用get的时候给Dep.target进行赋值为当前wather实例)Dep.target存放的是new watcher的this,然后执行this.getter(就是render函数)初始化渲染的时候必然会调用值。
调用值的时候调用双向绑定的get方法(图三),从而调用dep.depend方法(图五),调用Watcher类的addDeps方法(别忘了上面Dep.target的值为watcher实例)。实现 保存watcher的this到dep的subs列表中,同时也把dep存到了watcher的deps当中(图四)。

图一
在这里插入图片描述

图二在这里插入图片描述
图三
在这里插入图片描述

图四
在这里插入图片描述

图五
在这里插入图片描述

应该做一套流程图
初始化 调用值(执行 m o u n t 里面的 n e w W a t c h e r 的时候会调 用 u p d a t e 然后会调用 v m . mount里面的new Watcher的时候会调用_update然后会调用vm. mount里面的newWatcher的时候会调update然后会调用vm.data.属性值的get方法)的时候
1、执行dep的depend方法,depend内部执行watcher的addDep(dep)方法
2、watcher的addDep方法会记录不重复的dep实例 并且,dep也会同时记录对应的watcher,因为watcher里面没有重复的dep,所以dep不会重复记录,dep也不会重复记录watcher。参见上面watcher类里面的addDep方法

更新视图
值变化调用dep.notify()然后遍历dep存储的watcher,调用每个watcher里面的更新方法,更新视图
在这里插入图片描述
执行watcher的更新方法,遍历存储的所有watcher 并且进行更新
在这里插入图片描述
watcher的update方法
在这里插入图片描述

观察者模式的应用,
watcher是观察者,属性是被观察者,属性变化了会通知 所有的观察者,进行视图的更新。
就好像订阅天气app一样,我订阅了天气app我就是观察者,天气app就是被观察者, 被观察者数据变了就会通知我,我去做一些改变。
在代码中,
watcher是观察者,dep就是被观察者,当dep的值变了,就会通知watcher,然后watcher进行视图的更新。以上就是观察者模式在实例中的应用。

订阅发布者模式可以理解为观察者模式的变种,订阅发布模式,不再需要单独的增加程序,订阅者(观察者)只需要订阅相对应的消息即可,但是观察者则需要将自己添加到目标对象当中。这就是区别,发布内容的时候都一样。 vue2中使用的是观察者模式。

上面的更新有缺点,也就是同一个watcher如果有多个变量更新,那么这个watcher就会更新多次 。 接下来使用异步更新解决这个问题。

异步更新
以下是代码,请先大致阅读代码,结合代码看下面解析。

dep中
在这里插入图片描述
watcher中
在这里插入图片描述

watcher渲染逻辑,因为watcher是观察者,同时是负责渲染的。

在这里插入图片描述
首先,这里使用的是任务队列,也就是先调用了queueWatcher
每次值更新的时候都会调用notify,获取dep存储的watcher列表,然后调用所有watcher实例的更新方法。(watcher里面的get方法则是调用了不同的render函数,并且生成不同的虚拟dom并且进行替换。)
更新的时候queueWatcher 直接创建了一个队列,将不同的watcher进行存储。相同watcher会被过滤掉。
例如 改变了name的值两次,又改变了age的值,这时候notify会调用三次。(假如这三个属性内的subs存储的都是同一个watcher)
接下来的queueWatcher方法执行三次且传相同参数。queue队列中只有一个watcher实例。因为相同的watcher.id被过滤掉了。
接下来执行flushSchedulerQueue方法,这个方法从始至终都只会执行一次。因为防抖的限制只会在第一次向queue队列添加watcher时触发一次。因为是异步的,他会等queue队列添加完数据后再执行flushSchedulerQueue。
最后因为queue只有一个,所以只执行了一次刷新方法。


nextTick,对上面的异步更新进行了优化。
因为这里使用的是setTimeout设计的是宏任务。如果在获取模板的时候,代码使用了微任务,这时候会获取到旧的模板。例如

  vm.name='tom'
  Promise.resolve().then(()=>{
	获取dom  //只会获取到旧的dom模板。
})

发生以上原因都是因为setTimeout会宏任务,会等微任务执行完才会被执行。也就是会获取到旧的模板,为了解决以上问题,就产生了,nextTick 任务队列。如果想异步操作就使用nextTick去获取。保证了任务的执行顺序。

nextTick代码如下
在这里插入图片描述
同样是一个异步任务队列,将原本只执行一次的刷新操作flushSchedulerQueue函数放入nextTick中,而不是再放入setTimeout里面了,这样就会导致,nextTick中的callbacks队列是有顺序的。因为都是同步执行的。所以只要改变值了,刷新操作就会放在callback 列表中的第一位,而异步获取值的操作使用nextTick去替代后,就会一次放在callbacks的后面。
例如:
  改变属性值vm.name=“tom”
        callbacks列表=[flushSchedulerQueue]
  使用nextTick获取dom
        callbacks列表=[flushSchedulerQueue,获取dom函数]
调用callbacks中的所有函数
这样就可以获取到正常的值了,无论是否异步。

(nextTick也并非单纯的setTimeout实现的而是使用的异步的优雅降级。如下方法替换了上面的setTimeout。)
在这里插入图片描述
数组的更新原理

数组push之后是不会被自动更新的,因为上面的代码中 push重写的时候,只是对新增的对象进行了双向绑定而已。
例如

let list=[]
list.push({name:'tom'})//只是对属性进行了双向绑定,push的时候,并不会触发set方法。所以push 的时候不会更新数组。
list[0].name='jack'

重新给name修改值的时候才会触发更新操作。
因为之前的defineReactive是给每个属性添加的双向绑定,例如
let a = {mesage:{name:‘tom’}}中,是给message属性添加的双向绑定,对于外部的对象并没有添加双向绑定,所以当a.b='123’的时候是不会触发set更新的,

解决办法就是给对象也添加Dep属性,收集watcher,因为在observe方法里面return new Observe(data)创建这个的前提是data是对象,所以在Observe类里面创建了dep = new Dep()属性,并且当外部属性被调用的时候也让内部的对象进行收集watcher。
例如
data:{
message:{name:'tom’},
list:[1,2,3]
}

当message被调用的时候会让{name:'tom'}对象对应的Observe实例中的dep实例收集watcher并且,将Observe实例赋值给对象的__ob__,
所以想更新的话,就可以使用对象.__ob__.dep.notify()进行模板更新。如下如,给对象增加了dep属性,并且给对象添加了__ob__属性,值赋为对应的Observer对象,然后访问对象里面对应的dep属性,遍历所有的watcher强制进行视图的更新。
**在这里新增属性的时候就需要手动的去更新模板了。$set的原理**
![在这里插入图片描述](https://img-blog.csdnimg.cn/5f3b47dccc2c411fb9ebfbfda2e1e215.png)

当list列表被调用的时候会让Observe实例对象的Dep实例属性收集watcher
所以当push 的时候可以利用上面说的数组的__ob__属性指向了数组对应的Observe实例,进而获取到dep实例属性,进而获取watcher,进行在push的时候 使用notify通知更新。
![在这里插入图片描述](https://img-blog.csdnimg.cn/35eff94a6af64646b491dff5b0c13869.png)

但是又因为在获取值的时候只会触发外部的属性,不会触发内部的列表,就会导致,内部的列表对象,依旧不能dep.depend进行watcher的收集,所以就需要一个递归,手动的将内部的其他列表元素对象进行收集了。
在这里插入图片描述
在这里插入图片描述

后面是对象在更新值得时候因为对每一个属性都增加了dep,并且改变值的时候会触发set就会更新模板了,
这里的列表 因为更新的时候不会触发set 就需要获取的时候创建dep实例收集watcher,然后在push等方法的时候去触发dep的notify方法去更新模板。
如上图,就是,给列表对象添加dep实例,并且get调用的时候去双向绑定,循环给每一个列表中的列表都添加上dep实例即可。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Vue2项目的目录结构通常如下: ``` ├── build // 项目构建相关的代码 │ ├── build.js // 生产环境构建代码 │ ├── check-versions.js // 检查node、npm等版本 │ ├── dev-client.js // 开发服务器热重载配置 │ ├── dev-server.js // 开发服务器配置 │ ├── utils.js // 构建相关工具方法 │ ├── webpack.base.conf.js // webpack基础配置 │ ├── webpack.dev.conf.js // webpack开发环境配置 │ └── webpack.prod.conf.js // webpack生产环境配置 ├── config // 项目开发环境配置 │ ├── dev.env.js // 开发环境变量 │ ├── index.js // 项目配置文件 │ ├── prod.env.js // 生产环境变量 │ └── test.env.js // 测试环境变量 ├── src // 源码目录 │ ├── assets // 资源目录,如图片、字体等 │ ├── components // 公共组件目录 │ ├── router // 前端路由 │ ├── store // 应用级数据(state)管理目录 │ ├── utils // 工具函数目录 │ ├── views // 页面目录 │ ├── App.vue // 主组件 │ └── main.js // 入口文件 ├── static // 静态资源目录,如图片、字体等 ├── test // 测试相关目录 ├── .babelrc // babel配置文件 ├── .editorconfig // 定义代码格式 ├── .eslintignore // eslint忽略目录或文件 ├── .eslintrc.js // eslint配置文件 ├── .gitignore // git忽略目录或文件 ├── .postcssrc.js // postcss配置文件 ├── index.html // 项目入口文件 └── package.json // 项目基本信息 ``` 其中,`build`目录存放构建相关的代码,`config`目录存放项目开发环境的配置,`src`目录存放源码,`static`目录存放静态资源,`test`目录存放测试相关的代码。 `src`目录下,`assets`目录存放各种资源文件,如图片、字体等;`components`目录存放公共组件;`router`目录存放前端路由相关代码;`store`目录存放应用级数据(state)管理相关代码;`utils`目录存放工具函数相关代码;`views`目录存放页面相关代码;`App.vue`是主组件;`main.js`是入口文件。 以上是一个常见的Vue2项目目录结构,具体项目可能会略有差异。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值