Vue源码的全方位解读(一)
一、变化侦测
所谓的渐进式框架,就是把框架分层,最核心的部分时视图层渲染,然后往外是组件机制,在这个基础上加上路由机制,再加入状态管理,最外层是构建工具。表明的含义就是你既可以使用最核心的视图层渲染来快速开发一些需求,也可以使用一整套全家桶来开发大型应用。
一、变化侦测,从状态生成DOM,再输出到用户界面显示的一整套流程叫做渲染,应用在运行时会不断地进行重新渲染,而响应式系统赋予框架重新渲染的能力,其最重要的部分就是变化侦测,变化侦测是响应式系统的核心,没有它,就没有重新渲染。框架在运行时,视图也就无法随着状态的变化而变化。简单来说,变化侦测就是侦测数据的变化,当数据发生变化时,会通知视图层进行相应的更新。 有两种方式可以进行变化侦测,一种是Object.defineProperty,一种是ES6的Proxy.Object可以侦测到对象的变化,如下就是对Object.defineProperty的封装,其作用是定义一个响应式数据,每当从data的key中读取数据时,get函数被触发,每当网data的key中设置数据时,set函数被触发。在vue2.0里,模板使用数据等同于组件使用数据,所以当数据发生变化时,会将通知发送到组件,然后组件内部再通过虚拟DOM重新渲染。(也就是通知那些用到了该数据的地方),总结起来就是:用getter收集依赖,用setter触发依赖。
function defineReactive(data,key,val){ Object.defineProperty(data,key,{ enumerable:true, configurable:true, get:function (){ return val; }, set:function (newVal){ if(val===newVal){ return } val=newVal } }) }收集的依赖收集到哪里呢,其实就是数据的每个key给它一个数组,用来存储当前key对应的依赖,假设依赖是一个函数,保存在window.target上,现在可以把deineReactive函数稍微改造一下(这里新增了数组dep,永安里存储被收集到的依赖,然后再set被触发时,循环dep以触发收集到的依赖):
function defineReactive(data,key,val){ let dep=[]//新增 Object.defineProperty(data,key,{ enumerable:true, configurable:true, get:function (){[ dep.push(window.target)//新增 return val; ]}, set:function (newVal){ if(val===newVal){ return; }, //新增 for(let i=0;i<dep.length;i++){ dep[i](newVal,val); } val=newVal; } }) }可以把依赖收集的代码封装成一个Dep类,专门用来管理依赖,使用这个类,可以收集依赖、删除依赖或者向依赖发送通知。
export default class Dep{ constructor (){ this.subs=[]; } addSub(sub){ this.subs.push(sub); } removeSub(sub){ remove(this.subs,sub); } depend(){ if(window.target){ this.addSub(window.target); } } notify(){ const subs=this.subs.slice(){ for(let i=0,l=subs.length;i<l;i++){ subs[i].update(); } } } } function remove(arr,item){ if(arr.length){ const index=arr.indexOf(item); if(index>-1){ return arr.splice(index,1); } } }之后defineReactive就可以改造成:
function defineReactive(data,key,val){ let dep=new Dep()//修改 Object.defineProperty(data,key,{ enumerable:true, configurable:true, get:function(){ dep.depend()//修改 }, set:function(){ if(val===newVal){ return; }; val=newVal; dep.notify(); } }) }watcher是一个中介的角色,数据变化时先通知它,然后它再去通知其他地方。现在其实已经可以实现变化侦测的功能了,如果希望把数据中所有的属性都侦听到,所以要封装一个Obserber类,这个类的作用就是将一个数组内的所有属性都转换成getter/setter的形式,然后去追踪它们的变化。
关于Object类型数字组的变化侦听变化原理,数据的变化是通过getter/setter来追踪,也正是由于这种追踪方式,有些语法中即便是数据发生了变化,Vue.js也追踪不到。比如obj上面新增的name属性,Vue.js无法侦测到这个变化,所以不会向依赖发送通知。再比如,删除obj中的name属性,而Vue.js也无法侦测到这个变化,所以不会向依赖发送通知。也就是说,Vue通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但getter/setter只能追踪到一个数据是否被修改,而无法追踪新增属性和删除属性,所以才会导致发生这种问题。
数组的变化侦测:Object的侦测方式是通过getter/setter实现的,但数组的增删并不会触发getter/setter,如果能在用户改变数组内容的时候得到通知,就能实现同样目的。可以使用一个拦截器覆盖Array.prototype。之后使用Array上的方法操作数组的时候,其实执行的方法都是拦截器中提供的方法,比如push方法,然后在拦截器中使用原生Array的原型方法去操作数组。拦截器其实就是一个和Array.prototype一样的Object,里面包含的属性一模一样,只不过这个Object中某些可以改变数组自身内容的方法是经过处理过的。Array原型中可以改变数组自身内容的方法有七个:push,pop,shift,unshift,splice,sort,reverse.
const arrayProto=Array.prototype export const arrayMethods=Object.create(arrayProto) ;[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] .forEach(function (method)){ //缓存原始方法 const original=arrayProto[method] Object.defineProperty,method,{ value:function mutator(...args){ return original.apply(this.args); }, enumerable:false, writable:true, configurable:true }) })在该代码中,创建了变量arrayMethods,它继承自Array.prototype,具备所有功能,未来使用arrayMethods去覆盖Array.prototye,接下来,在arrayMethods上使用Object.defineProperty方法将那些可以改变数组自身内容的方法(push,pop,shift,unshift,splice,sort,reverse)进行封装。有了拦截器以后,想要让它生效,就需要使用它去覆盖Arrau.prototype,但是又不能直接覆盖,这样会污染全局的Array,所以设置拦截器只拦截那么侦测变化了的数据,也就是希望拦截器只覆盖那些响应式数组的原型。也就是value.__proto__=arrayMrethods.虽然绝大多数刘安琪都支持这种非标准的属性来访问原型,但是并不是所有的浏览器都支持,当不能使用__proto__时,我们选择直接将arrayMethods身上的这些方法设置到被侦测的数组身上。数组在getter中收集依赖,在拦截器中触发依赖。