vue-origin & vue-use & vue-router

6 阅读13分钟

Vue-origin

===index.js 首先Vue是一个类 源码采用原始的function写法 不停扩展prototype属性
初始化操作都放在initMixin方法中 function Vue(options) { this._init(options) 入口方法 做初始化操作 }

写成一个个的插件 也就是函数 对原型进行扩展 initMixin(Vue)

===init.js Function initMixin(Vue) { Vue.prototype._init = function (options) { const vm = this; vm.$options = options initState(vm) vue组件上有很多状态 data props watch computed 都在这个里面进行初始化操作 } }

===state.js initState(vm)方法里就是各种属性的初始化 initProps initMethods initData initComputed initWatch

在initData就专门去做data的初始化

》》》响应式数据原理 vm._data = data = vm.$options.data.call(vm) 会递归取用defineProperty进行拦截 性能差 在初始化过程中对data进行劫持 数据的劫持方案: 对于对象 Object.defineProprerty 这个方案可以重新定义get和set方法,在获取属性和设置属性的时候可以做一些想做的事情 对于数组是单独处理的 传入的data一定是对象 但是对象里面有可能套着数组 会单独有一个模块来处理响应式原理 新的文件夹 ===observer/index.js 观测入口 class Observer {} 定义function observe(data){ 如果data不是对象 直接return return new Observer(data) } 通过循环对象的key并且递归调用实现了对属性的劫持 但是:下面这种情况就监控不到了 vm._data.a = {b: 1} vm._data.a.b = 100 就是将对象改掉了 又去给对象里的属性重新赋值,就监控不到了。因为赋值了一个新的对象 这个对象上并没有自定义的get set,意思就是用户在设置值的时候有可能也是一个对象 需要对这个对象也进行代理 class Observer { // 观测值 constructor(value){ this.walk(value); } walk(data){ // 让对象上的所有属性依次进行观测 let keys = Object.keys(data); for(let i = 0; i < keys.length; i++){ let key = keys[i]; let value = data[key]; defineReactive(data,key,value); } } } function defineReactive(data,key,value){ observe(value); 递归 对象里的属性值还是对象 继续劫持 Object.defineProperty(data,key,{ get(){ return value }, set(newValue){ if(newValue == value) return; observe(newValue); 如果用户将值改成新的对象 继续监控 value = newValue } }) } export function observe(data) { if(typeof data !== 'object' || data == null){ return; } return new Observer(data); }

实际上这样做 数组每一项也已经是被监控了的 但是开发过程中很少对数组索引进行操作,数组有可能非常长,为了性能考虑,不对数组进行拦截 改为拦截可以改变数组的方法进行操作 对方法进行重写 value.proto = newArrayMethods 让数组通过链找到数组方法 如果重写了就调用新的方法 如果没有重新 就继续通过链找到原方法

虚拟节点和ast很像,但是ast不进可以描述dom,还可以描述js,css等语法 并且虚拟dom还可以自定义一些属性 ast完全只是语法的描述 _init initState初始化状态 initData 拿到用户传来的数据 响应式 希望通说实例能够直接访问到重写的属性 所以vm._data 保存用户的所有data 但是从vm._data 上去取值就感觉比较麻烦 所以通过defineProperty做了一层代理 让用户通过vm.xxx取值实际上是从vm._data 上去进行 对于观测了的数据 会增加__ob__不可枚举属性 存放当前的observe实例 方便取到observe类上的方法 也能通过这个属性来判断属性是否已经被观测 缺陷: 只对存在的属性进行观测 data:{a: 1, b: 2} vm.c是不会被观测的 数组中更改索引和长度 无法被监控 vm.mount(vm.mount(vm.options.el) 组件挂载的逻辑 依次查找用户定义的模板 1. render 2.template 3.外部template => el.outerHTML 但是是在el存在的时候 vm.el=vm.el = vm.options.el 把template变成render方法 options.render = 变成的render方法,也就是说最终都会转换成render方法 过程是1. 将template通过正则匹配的方式变成ast树 2.标记静态节点 3.从ast树 codegen成代码字符串 4.通过new Function + with 生成render函数 因为with里面可以传入vue的实例,这样就可以从data上去取值 实现{{}}到真正值的替换 拿到render方法之后就实现组件的挂载 mountComponent vm._update(vm._render()) vm._render 就是 vm.$options.render 先调用编译好的render方法,会返回虚拟dom 要将虚拟dom渲染成真实dom vm._update 通过虚拟节点去更新真实节点 patch(oldVnode, vnode) oldVnode是老的真实节点 vnode是新的虚拟节点 oldVnode => id#app vnode=>根据模版生成的虚拟dom 不使用appendChild进行插入是为了插入到原有的位置 appendChild会跑到最后面 实现思路是:先将真实元素插入到老元素的前面 insertBefore,然后删除老元素

》》》生命周期的合并 全局定义的属性 方法 最终都放在了Vue.options={}上面(如Vue.components, Vue.directive,Vue.mixin 等这些最终都会放到Vue.options上) 策略模式

》》》数据变了通知页面更新 改了数据之后希望的是重新调用_render,再调用_update 重新调用_render就会重新从vm上去取值,再调用_update把页面重新更新一下 数据变化 自动调用vm._update(vm._render())就能实现

vue更新策略是以组件为单位的,给每个组件都增加了一个watcher,属性变化后会重新调用这个watcher(叫做渲染watcher )=> 用于重新渲染的watcher

要把属性和watcher绑定在一起 比如页面渲染用到了 name 用到了age 就需要name绑定组件的渲染watcher,age也要绑定组件的渲染watcher Name变化调用watcher的get方法,age变化调用watcher的get方法, 为了让属性和watcher对应起来 需要一个类 叫做Dep 做依赖收集

class Dep { } 多对多的关系 一个属性有一个dep,dep是用来收集watcher的 Dep可以存在多个watcher 除了渲染watcher之外还有其他watcher 一个watcher可以对应多个dep

在defineReactive方法中 let dep = new Dep() 当页面取值时也就是调用defineProperty里的get方法时说明这个值用于渲染了 将这个watcher和这个属性对应起来

当渲染的时候会创建渲染watcher 也就是在mountComponent时,,渲染之前先绑定watcher让dep能访问到 Dep.target = watcher 之后开始取值,取值的时候就会走get方法,这个属性就会把当前的watcher给记住dep.depend() 将当前的watcher加到这个属性的dep数组中,当这个属性变化了也就是调用set方法了,就会把watcher拿出来执行,重新调用watcher上的get方法,也就是重新渲染。渲染完成后,将Dep.target = null。置空的原因是如果data没有在模版中被使用,改变的时候不需要通知watcher更新

  1. 把渲染watcher放到Dep.target上 2.开始渲染 取值的时候调用get方法 当这个属性的dep存储当前的watcher 3.页面上所需要的属性都会将这个watcher存在自己的dep上 会是双向记忆 watcher上会存储dep dep上也会存储watcher 为计算属性做准备 4.等会属性更新了 通知自己存储的watcher来更新 就重新调用渲染逻辑

一种情况 data中 { a: { a : 1 } } 模版中 {{ a }} setTimeout(()=>{ vm.a.a = 100}) 会不会更新? 是会的 因为在模版中如果是对象数据类型 会进行JSON.stringify()的处理,会将对象转成字符串 转字符串的时候就对a进行了取值操作相当于取了a.a,所以也会更新 所以改对象内部的属性也会触发更新

===数组响应式更新

  1. 取的是arr,会调用arr属性的get方法 希望让当前数组记住这个渲染watcher 2.在observer类的constructor中给所有的不管是数组还是对象都增加一个dep属性 this.dep = new Dep() 3.当页面对arr取值时,就让数组的dep记住这个wtacher 在defineReactive方法中 获取对应的dep let childDep = observe(value) 在defineProperty的get方法中 childDep.dep.depend() 数组存储渲染watcher 4.等会更新数组调用push shift等方法时 可以通知watcher进行更新

》》》nextTick实现原理 批处理 => vue异步更新,更新数据之后不能立刻拿到最新的节点 通过一步更新的方式来实现批处理

》》》watch的原理 对于各种书写形式的处理之后 最终执行的都是vm.$watch => 就是new Watcher 在watcher类中,把用户挂的属性变成取值表达式,去vm上取值 并且会默认调一次get方法,拿到默认值 将watcher放到全局 调用getter方法 会有取值操作 属性就会把当前的watcher收集起来 再去掉全局上的watcher 稍后用户再去更新这个值的时候,会去调用属性的set方法,set方法会再去调用watcher的run方法,就拿到最新的值,如果是用户watcher的话就调用传递到watcher中的cb方法传入新值 老值 cb就对应着在watch中定义的函数

核心就是将watch例的监听属性转化成取值表达式

》》》diff原理 在patch方法中 初始化时 是直接拿虚拟节点创建真是节点来替换老节点 oldVnode.nodeType === 1 说明是真实节点 走初始化流程 更新时 拿老的虚拟节点和新的虚拟节点作比对 将不同的地方更新

在第一次创建虚拟节点的时候 会给虚拟节点加上el属性 指向真实dom 使用新的虚拟节点对比老的虚拟节点 找到差异 去更新老的dom元素

先说明一个特性: 如果完整的比对一棵树的话,时间复杂度是O(n3),性能比较差 为了简化,进行平级比较,因为很少会出现跨层级的改动,复杂度是O(n)

  1. 比较两个元素的标签 标签不一样直接替换掉就可以了 2.有种可能是标签一样 里面是文本节点
    1
    2
    文本节点的虚拟节点tag都是undefined 走到这里说明tag一样 然后判断如果tag是undefined 就比较新老虚拟节点的文本是否一样 替换文本内容 3.标签一样并且需要开始比对标签的属性和儿子了 标签一样直接复用标签 let el = vnode.el = oldVnode.el 更新属性 用新的虚拟节点的属性和老的比较,来更新老的dom 儿子比较分以下几种情况 老的有儿子 新的没儿子 老的没儿子 新的有儿子 老的有儿子 新的也有儿子 真正的diff算法 儿子之间的比较:vue中的diff算法做了很多优化 dom中操作有很多常见的逻辑 把节点插入当前儿子的头部 尾部 儿子倒序正序 vue2中采用的是双指针的方式 老儿子的头部尾部分别有一个指针 新儿子的头部尾部也分别有一个指针 循环新老儿子,有一个结束了就结束了,前后指针碰上了就结束,新老儿子一起循环 tag一样 key一样就认为是相同节点 有吊用patch方法去更新属性再去递归 比较子节点 新老的开头指针都向后挪一个 当循环结束时, if(newStartIndex <= newEndIndex) 将节点进行插入操作

新的头和老的头比 新的尾和老的尾比 新的头和老的尾比 新的尾和老的头比 交叉比较的时候 将相同可复用的插到当前老的节点的最后一个的后面

暴力比对 拿新节点去老的里面一个个比对,如果没有,将新节点插入到老的最前面,因为没有找到,所以老的节点的头尾指针都不动,新节点的头指针往后挪动一位。 如果发现在老节点中能找到,则可以复用,需要将可复用的老节点挪动到老节点的头指针的前面,老节点置空依旧站位,防止数组塌陷 新节点的头指针接着往后挪动,发现有可复用的依旧是挪到老节点头指针的前面去,当前老节点的位置置为null,每一个都这么操作 比对过程中如果可复用的节点正好是头指针对应的老节点或者尾指针对应的老节点,复用之后,将老节点的头指针向后挪动或者将老节点的尾指针向前挪动 然后将老节点头部指针到尾部指针包含的节点全部干掉

将所有的老节点做成一个映射表

》》》computed实现原理 一取值就执行,说明是一个getter 所以计算属性内部也使用了defineProperty,内部有一个变量dirty 控制计算属性对应对的函数要不要执行 当他依赖的属性发生了变化 会把dirty置为true Computed还是一个watcher 内部依赖的属性会收集这个watcher 1.需要有watcher 2.徐他要通过definedProperty 3.dirty const watchers = vm._computedWatchers = {} 用来存放计算属性的watcher 拿到getter 进行defineProperty 计算属性对应的函数默认是不执行的 给每一个属性都增加一个watcher 当取计算属性的值的时候会将dirty置为fasle,当依赖的属性发生改变的时候会将dirty置为true 假如计算属性fullname 依赖firstname和lastname firstname和lastname会收集计算属性watcher 同时firstname和lastname也应该将渲染watcher收集起来 以前放置watcher是放一个,之后置为null,可是现在属性既要收集计算watcher又要收集渲染watcher,所以在每次收集的时候讲watcher放到一个栈中

》》》.vue文件是怎么解析成一个对象的 组件是怎么来的? 主要靠了一个很重要的方法叫做 Vue.extend 内部汇集成vue的构造函数,可以自己进行实例化操作,并且手动挂载到指定的位置 // 这是vue的构造函数的子类 let childComponent = Vue.extend({ template: ‘

hello world {{msg}}
’, data(){ return {msg: ‘zf’} } }) 创建一个实例之后就可以进行挂载操作 new childComponent().$mount()

使用场景: 弹窗, 运行器(拿到输入的内容 看运行效果)

》》》组件初始化 拆分组件的原因: 1.实现复用 2.方便维护 3.编写组件将组件拆分的很细,是因为考虑到vue的更新问题 每个组件一个渲染watcher 如果把很多功能放到一个组件里,更新的时候需要把所有的dom都做一遍比对,整个组件都要diff一次 所以是一个性能优化点,可以减少比对 Vue.options._base = Vue definition = this.options._base.extend(_definition) Vue.component(id, _definition)注册组件 等价于 Vue.options.components[id] = definition

组件的渲染流程

  1. 调用Vue.component 内部用的是Vue.extend 就是产生一个子类来继承父类,调用子类的时候会执行父类的_init方法,再去mount即可Vue.extend=function(params)constSuper=thisconstSub=function(options)this.init(options)//原型继承Sub.prototype=Object.create(Super.prototype)Sub.prototype.constructor=Sub..returnSub为什么不用一个类而是使用这种继承的方式?因为如果用一个类所有组件的属性就是共享的组件的初始化就是new这个组件的构造函数并且调用mount即可 Vue.extend = function(params) { const Super = this const Sub = function(options){ this._init(options) } // 原型继承 Sub.prototype = Object.create(Super.prototype) Sub.prototype.constructor = Sub …….. return Sub } 为什么不用一个类而是使用这种继承的方式?因为如果用一个类 所有组件的属性就是共享的 组件的初始化就是new 这个组件的构造函数并且调用mount方法

组件的合并策略:就近,可以将全局组件放到原型链上

1-组件通信===============

没有被组件接收的属性会放在dom上 div

el-input 上进行了双向数据绑定 其内部实现本身就是一个 不能再在原生input上进行v-model 因为组件里面不能修改属性传进来的值,属性是不可改的 可以让父更新然后促使子进行更新 Value绑定传入的value,然后绑定input事件 通知父亲更新 <input :value=“value” @input=“handleInput”/> handleInput(e){ this.emit(input,e.target.value)emit(‘input’, e.target.value) parant指的是父组件 div这种原生dom不受影响 不会是parant如果想拿到divthis.parant 如果想拿到div this.el.parentNode el是当前真实domthis.el是当前真实dom this.parant.$emit(‘validate’) 就可以拿到输入框的内容进行校验 如果el-form-item和el-input 之间还套着别的组件 而不是像div这种原生dom,这种方案就有问题了。要找的应该是名字叫做el-form-item的父组件

	所以自己写一个$dispatch
	elFormItem就是给组件定义name的用处
	this.$dispatch(‘elFormItem’, ‘validate’)
 }

el-form-item 输入完要通知el-form-item来看输入的值是否合法

{{label}} {{errorMessage}}
可以使用发布订阅来实现 挂载的顺序是先子后父 所以mounted中 儿子肯定已经渲染好了 mounted中:this.$on(‘validate’, ()=>{儿子输入的时候要触发这个校验}) (这样绑是给el-form-item自己绑)

Vue.prototype.dispatch = function(componentName, eventName) { let parent = this.parent while(parent){ let name = parent.options.name if(name === componentName) { break }else{ parent = parent.parent } } if(parent) { if(eventName){ parent.$emit(eventName)} return parent } }

el-form中 要将规则传递给 el-form-item使用 { name: ‘elForm’, provide(){ return {elForm: this} } 意思是把当前的实例暴露出来 elFormItem这个属性 放到了 当前组件的_provided上 } 在el-form-item进行注入 { name: ‘elFormItem’, inject: [‘elForm’] , 实现是不停去找父亲的._provided属性 找到后合并到自己身上 就可以找到当前数据和校验规则进行校验 }

提交操作 el-form中 dom <form @submit.prevent> 里面有validate方法: 要拿到所有的子组件el-form-item 看是不是都校验通过 获取所有儿子 this.$broadcast(‘elFormItem’)

Vue.prototype.broadcast = function(componentName, eventName) { let children = this.children let arr = [] function findChildren(children){ children.forEach(child => { if(child.options.name === componentName) { if (eventName) { child.emit(eventName) } else { arr.push(child) } } if (child.children) { findChildren(child.children) } }) }

findChildren(children)

}

Provide/inject 跨组件通信 onon emit parentparent children

2-实例上的方法================ vm.mount(‘#mask’) 自定义挂载点 有可能有些组件不希望挂载到app节点上 内部会判断用户是否传入el属性 如果没有 则不会进行挂载操作 new Vue({el:’#mask’})或者自己传入 vm.options 用户传入的所有属性 vm.data==vm.data代表响应后的数据vm.data == vm._data 代表响应后的数据 vm.nextTick 保证页面渲染完毕之后获取最新的dom元素 vm.el当前渲染的元素vm.el 当前渲染的元素 vm.watch 自定义wather

3-内置指令

{{name}}
静态节点 稍后渲染时不会重新渲染 相当于有缓存了 一般不会使用 指令对应的值都是变量 相当于innerHtml 会转成标签插入 是可以运行的 可能会带来的问题是xss攻击 编译之后不是directive 需要合理使用 如后台返回的数据 用户输入的就不能使用
html=“

hello

v-if v-else v-else-if 最终会编译成render函数 会被编译成三元表达式 不是directive

指令里的都代表变量 这里的true是布尔值 true 不是字符串 v-if v-else 需要连着写 中间不能穿插别的dom节点 如何快速的知道被编译成了什么? let vTemplate = require(‘vue-template-compiler’) Let t = `
aaa
bbb
` vTemplate.compile(t).render 如果条件不满足 这个节点就不渲染 操作的是dom元素 如果渲染节点有多个 不像v-if多渲染一个标签 可以用template

v-show指令 最终会通过样式来控制这个div是现实还是隐藏 dispaly none 不占位置 为什么不用opacity visibility 占位置 事件只有opacity生效

v-for 循环 字符串 数组 数字 对象 v-for=“a in arr” v-for=“a of arr” in of都可以 :key=“” v-for不要和v-if连用 就是不要出现在一个标签上 有优先级的问题 先循环 再判断 如果循环量很大会有性能问题 尽量使用计算属性来解决这个问题 key的问题:尽量不使用index作为key 尤其是经常操作的列表 用index会导致额外的dom操作 减少没有必要的删除和创建 移动就能达到目的 只是静态展示是可以的

指令的目的就是dom操作

4-自定义指令 如果要实现input框获取焦点的时候展示一个div 面板: V-on:click => @click 绑定事件 方法不放在vue的data属性里是因为会有this指向问题 this指向window 而应该放在methods里面 this指向的是当前的实例 methods内部会绑定this 永远指向的都是当前实例

显示面板
全局指令 Vue.directive() 局部指令 vm = new Vue({ el: ‘#app’, directives: {} 放多个指令 })

Vue.directive(clickOutside,{ bind(el,bindings,vnode){ el.handler = function (e) { if(!el.contains(e.target)){ let method = bindings.expression; vnode.contextmethod; } } document.addEventListener('click',el.handler) }, unbind(el){ document.removeEventListener('click',el.handler) } }) 参数 el 指的就是绑指令的div !el.contains(e.target) 标识点击的是外面 bindings.expression 标识绑定在指令上的函数名 需要去当前的实例上去找这个方法 vnode.context代表当前指令所在的上下文 也就是组件的实例 这样就可以拿到对应的方法

5- 组件常规通信 引入的vue是否包含compiler 只会影响 new Vue({这里面能不能用template}) Vue文件里面的