vue2源码学习1

1.vue源码解读流程

 new Vue 调用的是 Vue.prototype._init 从该函数开始 
经过 $options 参数合并之后
initLifecycle 初始化生命周期标志 初始化事件,
初始化渲染函数。
初始化状态就是数据。
把数据添加到观察者中实现双数据绑定。

2.双数据绑定原理是

obersve()方法判断value没有没有__ob___属性并且是不是Obersve实例化的, 
value是不是Vonde实例化的,如果不是则调用Obersve 去把数据添加到观察者中,为数据添加__ob__属性,
Obersve 则调用defineReactive方法,该方法是连接Dep和wacther方法的一个通道,
利用Object.definpropty() 中的get和set方法 监听数据。
get方法中是new Dep调用depend()。
为dep添加一个wacther类,watcher中有个方法是更新视图的是run调用update去更新vonde 然后更新视图。
然后set方法就是调用dep中的notify 方法调用wacther中的run 更新视图

3.vue从字符串模板怎么到真实的dom呢?是通过$mount挂载模板

就是获取到html,然后通过paseHTML这个方法转义成ast模板
,他大概算法是 while(html) 如果匹配到开始标签,结束标签,或者是属性,都会截取掉html,
然后收集到一个对象中,知道循环结束 html被截取完。最后变成一个ast对象,ast对象好了之后,
在转义成vonde 需要渲染的函数,比如_c('div' s('')) 等这类的函数,
编译成vonde 虚拟dom。然后到updata更新数据 调用__patch__ 把vonde 通过diff算法变成正真正的dom元素。

4.vue.js异步更新DOM策略

watcher队列:
当某个响应式数据发生变化时,他的setter函数会通知闭包中的Dep,Dep则会调用它管理的所有watch对象。触发watch
对象的update实现。
update(){
if(this.lazy){
this.dirty = true
}else if(this.sync){
this.run();//同步执行run直接渲染视图
}else{
queueWatcher(this);//异步推送到观察者队列中,下一个tick时调用
}
}
//异步执行DOM更新
export function queueWatcher(watcher: Watcher){
const id = watcher.id;
if(has[id]==null){ //检验id是否存在,已经存在则直接跳过,不存在则标记哈希,用于下次检验
has[id] = true
if(!flushing){
queue.push(watcher);//如果没有flush掉,直接push到队列中即可
}else{
//Watch对象并不是立即更新视图,而是被push进一个队列queue,此时状态处于waiting的状态,
这时候会继续有watch对象被push进这个队列queue,等到下一个tick运行时,这些Watch对象才会被遍历取出
更新视图。同时,id重复的Watch不会被多次加入到queue中。
let i = queue.length -1;
while(i>=0 && queue[i].id > watcher.id){
i--
}
queue.splice(Math.max(i, index)+1,0,watcher)
}
if(!waiting){
waiting = true
nextTick(flushSchedulerQueue);// flushSchedulerQueue是下一个tick时的回调函数,主要目的是执行Watcher的run函数,
用来更新视图。
}
}
}
nextTick函数:执行目的是在 微任务或者task中推入一个function,在当前栈执行完毕以后执行nextTick传入的function
一共有promise, mutationObserver以及setTimeout三种尝试得到timerFunc的方法,优先使用promise,在promise不存在
的情况下使用mutationObserver,这两个方法都会在微任务中执行,会比setTimeout更早执行,所以优先使用。
如果上述两种方法都不支持的环境则会使用setTimeout,在task尾部推入这个函数,等待调用执行。

nextTick目的是产生一个回调函数加入task或者微任务中,当前栈执行完以后调用该回调函数,起到异步触发(即下一个tick时触发)的目的。

为什么要异步更新视图?
<template>
<div>
<div>{{test}}</div>
</div>
</template>
<script>
export default{
data(){ return { test: 0 } },
mounted(){
for(let i=0;i<1000;i++){
this.test++;
}
}
}
</script>
回答:现在有这样的一种情况,mounted的时候test的值会被++循环执行1000次。
每次++时,都会根据响应式触发setter->Dep->Watcher->update->patch。
如果这时候没有异步更新视图,那么每次++都会直接操作DOM更新视图,这是非常消耗性能的。
所以Vue.js实现了一个queue队列,在下一个tick的时候会统一执行queue中Watcher的run。
同时,拥有相同id的Watcher不会被重复加入到该queue中去,所以不会执行1000次Watcher的run。
最终更新视图只会直接将test对应的DOM的0变成1000。
保证更新视图操作DOM的动作是在当前栈执行完以后下一个tick的时候调用,大大优化了性能。

参考链接:
https://github.com/answershuto/learnVue/blob/master/docs/Vue.js%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0DOM%E7%AD%96%E7%95%A5%E5%8F%8AnextTick.MarkDown

 5.vue组件间通信

组件:可以扩展HTML元素,封装可重用的代码。

vue组件间通信:
父组件向子组件通信:
方法一:props
方法二:使用$children, 使用$children可以在父组件中访问子组件。
子组件向父组件通信:
方法一:使用$emit事件
方法二:使用$parent, 使用$parent访问父组件的数据
非父子组件,兄弟组件之间的数据传递:
使用一个Vue实例作为中央事件总线。
Vue内部有一个事件机制,$on方法用来监听一个事件,$emit用来触发一个事件。
//新建一个vue实例作为中央事件总线
let event = new Vue();
//监听事件
event.$on('eventName', (val)=>{
//do something
})
//触发事件
event.$emit('eventName','this is a message')

复杂的单页应用数据管理使用vuex。

6.事件机制

四个事件API,即 $on, $once, $off, $emit。

$on方法用来在vm实例上监听一个自定义事件,改事件可用$emit触发。
$once监听一个只能触发一次的事件,在触发以后会自动移除该事件。
$off用来移除自定义事件。
$emit用来触发指定的自定义事件。

7.VNode节点

虚拟DOM,即是真实DOM树的一层抽象,用属性描述真实DOM的各个特性。当发生变化的时候,就会去修改视图。
vue.js将DOM抽象成一个以JavaScript对象为节点的虚拟DOM树,以VNode节点模拟真实DOM,可以对这颗抽象树进行创建节点,
删除节点以及修改节点等操作,在这个过程中都不需要操作真实DOM,只需要操作JavaScript对象后只对差异修改,减少了很多不需要的DOM操作,大大提高了性能。

VNode基类:
export default class VNode{
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
context: Component | void;
...

constructor(tag?:string, data?:VNodeData, children?:?Array<VNode>,
text?:string, elm?: Node, context?:Component,
componentOptions?: VNodeComponentOptions){
this.tag = tag; //当前节点的标签名
this.data = data;// 当前节点对应的对象,包含了具体的一些数据信息
this.children = children; //当前节点的子节点,是一个数组
this.text = text; //当前节点的文本
this.elm = elm;// 当前虚拟节点对应的真实dom节点
this.componentOptions = componentOptions; //组件的option选项
...
}
}

修改视图: vue通过数据绑定来修改视图,当某个数据被修改的时候,set方法会让闭包中的Dep调用notify通知所有订阅者Watcher,Watcher通过get方法执行
vm._update(vm._render,hydrating)。

patch:将新老VNode节点进行比对,然后根据两者的比较结果进行最小单位地修改视图。

sameVnode实现: 判断两个VNode节点是否是同一个节点,需要满足以下条件:
key相同;
tag(当前节点的标签名)相同;
isComment(是否为注释节点)相同;
是否data(当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型)都有定义
当标签是<input>的时候,type必须相同。

patchVnode规则:
1.如果新旧VNode都是静态的,同时他们的key相同(代表同一节点),并且新的VNode是clone或者是标记了once,那么只需要替换elm以及componentInstance即可。
2.新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren,这个updateChildren也是diff的核心
3.如果老节点没有子节点而新节点存在子节点,先清空老节点DOM的文本内容,然后为当前DOM节点加入子节点。
4.当新节点没有子节点而老节点有子节点的时候,则移除该DOM节点的所有子节点
5.当新老节点都无子节点的时候,只是文本的替换。

8.从template到DOM

从 new 一个vue对象开始:
let vm = new Vue({
el: '#app'
})

Vue构造类:
function Vue(options){
//初始化
this._init(options);
}
Vue.prototype._init = function(options?: Object){
//初始化生命周期
initLifecycle(vm)
//初始化事件
initEvents(vm);
//初始化render
initRender(vm);
//调用beforeCreate钩子函数并且触发beforeCreate钩子事件
callHook(vm, 'beforeCreate');
initInjectiongs(vm);
//初始化props, methods, data, compued, watch
initState(vm);
initProvide(vm);
//调用created钩子函数并且触发created钩子事件
callHook(vm, 'creatd');
...
if(vm.$options.el){
//挂载组件
vm.$mount(vm.$options.el);
}
}
_init主要做了这两件事:1.初始化(包括生命周期,事件,render函数,state等)和$mount组件。

在生命钩子beforeCreate与created之间会初始化state,在这个过程中,会一次初始化props,methods,data,computed与watch。
export function initState(vm: Component){
vm.watchers = [];
const opts = vm.$options;
if(opts.props) initProps(vm, opts.props);
if(opts.methods) initMethods(vm, opts.methods);
if(opts.data){
initData(vm)
}else{
observe(vm._data = {}, true); //该组件没有data的时候绑定一个对象
}
if(opts.computed) initComputed(vm, opts.computed)
if(opts.watch) initWatch(vm, opts.watch)
}

双向绑定:
observe会通过defineReactive对data中的对象进行双向绑定,最终通过Object.defineProperty对对象设置setter以及getter方法。getter方法主要用来进行
依赖收集。setter方法会在对象被修改的时候触发,这时候setter会通知闭包中的Dep,Dep订阅了这个对象改变的watcher观察者对象,Dep会通知watcher对象更新视图。

https://github.com/answershuto/learnVue/blob/master/docs/%E4%BB%8Etemplate%E5%88%B0DOM(Vue.js%E6%BA%90%E7%A0%81%E8%A7%92%E5%BA%A6%E7%9C%8B%E5%86%85%E9%83%A8%E8%BF%90%E8%A1%8C%E6%9C%BA%E5%88%B6).MarkDown

9.数据绑定

首先通过一次渲染操作触发Data的getter进行依赖收集,在data发生变化的时候会触发它的setter,setter通知watcher,watcher进行回调
通知组件重新渲染的函数,之后根据diff算法来决定是否发生视图的更新。

vue在初始化组件数据时,在生命周期的beforeCreate与created钩子函数之间实现了对data, props, computed,methods,events及
watch的处理。

initData 主要是初始化data中的数据,将数据进行Observer,监听数据的变化,其他的监视原理一致。
function initData(vm:Component){
//得到data数据
...
//判断是否是对象
...
//遍历data对象
const keys = Object.keys(data);
const props = vm.$options.props;
let i = keys.length
//遍历data中的数据
//observe data,开始对数据进行绑定,下面会进行递归observe进行深层对象绑定
observe(data, true);
}
proxy代理

observe:创建一个Observer实例(__ob__),如果成功创建observer实例则返回新的observer实例,如果已有observer实例则返回现有的
observer实例。
export function observe(value: any, asRootData:?boolean):Observer|void{

}

Observer: 用来遍历对象的所有属性将其进行双向绑定。
export class {
value: any;
dep: Dep;
vmCount: number;

constructor(value: any){
this.value = value;
this.dep = new Dep();
this.vmCount = 0;

def(value, '__ob__', this);//将observer实例绑定到data的__ob__属性上面去
if(Array.isArray(value)){

}else {
this.walk(value);//如果是对象则直接walk进行绑定
}
}
walk(obj: Object){
const keys = Object.keys(obj);
//walk方法会遍历对象的每一个属性进行defineReactive绑定
for(let i=0;i<keys.length;i++){
defineReactive(obj, keys[i], obj[keys[i]]);
}
}
observeArray(items: Array<any>){
//数组需要遍历每一个成员进行observe
for(let i=0, l=items.length;i<l;i++){
observe(items[i]);
}
}
}
Observer为数据加上响应式属性进行双向绑定。如果是对象则进行深度遍历,然后为每一个子对象都绑定上方法,如果是数组则为每一个成员都绑定
上方法。

watcher 是一个观察者对象。依赖收集以后watcher对象会被报错在Deps中,数据变动的时候会由Deps通知watcher实例,然后由watcher实例
回到cb进行视图的更新。
export default class Watcher {

}

Dep类,Dep就是一个发布者,可以订阅多个观察者,依赖收集之后Deps中会存在一个或读个watcher对象,在数据变更的时候通知所有的watcher。
export default class Dep{
static target:?Watcher;
id: number;
subs: Array<Watcher>;
constructor(){
this.id = uid++;
this.subs = [];
}
addSub(sub:Watcher){
this.subs.push(sub);//添加一个观察者对象
}
removeSub(sub:Watcher){
remove(this.subs, sub);//移除一个观察者对象
}
depend(){
if(Dep.target){
Dep.target.addDep(this);//依赖收集,当存在Dep.target的时候添加观察者对象
}
}
//通知所有订阅者
notify(){
const subs = this.subs.slice();
for(let i=0;i<subs.length;i++){
subs[i].update();
}
}
}
function remove(arr,item){
if(arr.length){
const index = arr.indexOf(item);
if(index>-1){
return arr.splice(index, 1);
}
}
}
Dep.target = null; //依赖收集完需要将Dep.target设为null,防止后面重复添加依赖。

defineReactive: 通过Object.defineProperty为数据定义上getter/setter方法,进行依赖收集后闭包中的Deps会存放Watcher对象。
触发setter改变数据的时候会通知Deps订阅者通知所有的Watcher观察者对象进行视图的更新。

https://github.com/answershuto/learnVue/blob/master/docs/%E4%BB%8E%E6%BA%90%E7%A0%81%E8%A7%92%E5%BA%A6%E5%86%8D%E7%9C%8B%E6%95%B0%E6%8D%AE%E7%BB%91%E5%AE%9A.MarkDown

10.依赖收集

从Dep说起,见上文中Dep类,定义一个依赖收集类Dep。
当对data上的对象进行修改值得时候会触发它的setter,那么取值的时候自然就会触发getter事件,所以在最开始进行一次render,那么所有
被渲染所依赖的data中的数据就会被getter收集到Dep的subs中去。在对data中的数据进行修改的时候setter只会触发Dep的subs的函数。

Watcher:订阅者,当依赖收集的时候会addSub到sub中,在修改data中的数据的时候会触发dep对象的notify,通知所有watcher对象去修改
对应视图。
class Watcher {
constructor(vm, expOrFn, cb, options){
this.cb = cb;
this.vm = vm;
Dep.target = this;
this.cb.call(this.vm);
}
update(){
this.cb.call(this.vm);
}
}

开始依赖收集:
class Vue{

}
function defineReactive(obj, key, val, cb){
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get:()=>{
if(Dep.target){
dep.addSub(Dep.target);
}
},
set: newVal =>{
dep.notify();
}
})
}
Dep.target = null;
将观察者watcher实例赋值给全局的Dep.target,然后触发render操作只有被Dep.target标记过的才会进行依赖收集。
有Dep.target的对象会将watcher的实例push到subs中,在对象被修改触发setter操作的时候dep会调用subs中的watcher实例的update方法
进行渲染。

 

posted on 2022-11-23 16:29  有匪  阅读(91)  评论(0编辑  收藏  举报

导航