1,双向数据绑定的实现
简单总结为:通过js的层层封装,互相调用,实际就是利用js的Object.defineProperty()方法,然后实现了一个发布订阅模式。
整体逻辑是在vue初始化的时候,通过Object.defineProperty()重写数据的set、get方法。
在每个调用到变量的地方(例如vue的模板字符串{{ }}),会触发重写的get方法,该方法增加一个观察者;
在每次修改变量值的时候,会触发重写的set方法,该方法会通知所有的观察者更新视图。
详细源码解读可查看vue源码解析-响应式原理
2,$mount的实现
vue项目的main.js文件中最后一段代码总是为:
new Vue({ el: '#app', router, store, ... ... render: (h) => h(App)});
也可以这样写:new Vue({ router, store, ... ... render: (h) => h(App).$mount('#app') });
而$mount函数是Vue.property原型上定义的函数。
这个函数中主要代码简单总结为:
首先会判断options.render,如果render为空则再获取options.template,如果template也为空则获取传入的el参数(即el),获取到模板内容之后通过处理将模板内容转换成Element类型,并返回render函数(获取render函数的过程见3,complier过程);
然后根据render函数生成虚拟DOM(具体过程见4,render函数生成虚拟DOM过程);
然后进行patch过程(新增节点、更新节点、删除节点),vue执行diff算法渲染页面;
最后调用mounted钩子。
详细源码解读可查看vue源码解析-$mount
3,complier过程
简单总结:将template转换成render函数。
complier分为构建时complier和运行时complier,二者的区别是:
构建时complier是在本地开发中通过webpack + vue-loader来处理.vue文件,然后在打包的时候转换成render函数;
运行时complier是不使用vue-loader这样的插件,。直接编写template这样的模板代码,然后在浏览器运行的时候将template转换成render函数。
因此本质都是转换成render函数,对比看来构建时complier的性能更好一些。
然而vue的源码对于运行时complier进行了封装实现:在mount挂载时,如果没有render函数,则会先进行模板编译,转换成AST对象(Abstract Syntax Tree抽象语法树,实际是一种自定义的数据结构),然后通过AST转换成render函数并返回,挂载到vm.$options。
4,render函数生成虚拟DOM过程
上述complier过程,只是返回了render函数,但并没有执行,而执行了render函数之后,会生成一个虚拟DOM,也就是一个js对象。虚拟dom存在的意义就是提升性能和跨平台。
生成虚拟dom的过程简单总结为:通过执行_render函数,来调用createElement函数,生成VNode虚拟DOM并返回。
虚拟DOM与真实DOM的区别在于:虚拟DOM只需要一些重要的属性(tag, data, children, text, elm, context, componentOptions)即可,因此虚拟DOM其实就是一个js对象。
详细源码解读可查看vue源码解析-组件化&虚拟DOM
5,patch过程和diff算法
上述执行render函数并返回虚拟DOM之后,vue会调用update方法去更新视图。而patch函数就是在update方法中进行调用(vm.$el = vm.__patch__(prevVnode, vnode))。
简单总结:整个patch函数的执行过程就是以新的虚拟dom为基准,改造旧的虚拟dom(创建节点、更新节点、删除节点)。其中新老vnode对比的过程就是diff算法。
首次渲染页面时,不需要使用diff算法对比:
首次渲染时旧节点oldNode是真实节点(根节点),此时要将其转换成虚拟节点(因为后面节点的remove、invoke和diff对比都是基于虚拟DOM)并保存;然后调用createElm方法(最内层调用封装的原生的document.createElement)创建节点;若有多层组件嵌套,接着调用createChildren(实际是递归调用createElm方法)方法完成多层嵌套的子组件的节点创建。
非首次渲染页面,页面数据发生改变时,diff算法介入:
会触发reactiveSetter方法对比新老数据是否相等,如果相等则直接return;不相等则需要使用diff算法进行更新过程。
触发reactiveSetter方法时,实际是将每个观察者放入一个队列中(一次性更新,提升性能)循环调用了update方法,在最后调用nextTick进行一次性更新。
diff算法的过程,主要是对新老vnode标签、文本(updateProperties)、子节点(updateChildren)等依次进行判断和对比,若二者不一致,通常将新的vnode节点或内容替换到oldVnode节点或者内容,然后对二者都有子节点的情况,再递归对子节点进行diff的过程。整个过程可能有增删改查等操作。因此diff算法是为了可以合理的复用节点,提升性能。
详细源码解读可查看Vue源码解析-patch&diff算法
6,$nextTick的实现
简单总结:$nextTick是通过事件循环的机制,将所有的回调函数放到一个队列中(callbacks)存储,然后在下一次dom同步更新完成页面渲染之后,再执行队列中存储的回调函数,执行完成后再清空队列,便于下次使用。
这样实现的是因为同步更新dom会执行diff对比,将会非常损耗性能。
vue的$nextTick有效使用降解来实现兼容性问题(promise>MutaionObserver>SetImmediate>
setTimeout)
7,watch监听的实现
vue中watch的实现方式有4种:
1,变量名: 函数名(字符串形式)
eg:watch: { message: "getMessage" } methods: { getMessage(val) { ... } }
2,变量名: 函数定义
eg:watch: { message: function (old, new) { ... ... } }
3,对象.属性(字符串形式): 函数定义
eg:watch: { "message.read": function (old, new) { ... ... } }
4,对象.属性(字符串形式): 回调函数数组
eg:watch: { "message.read": ["getMessage", function (old, new) { ... ... }] }
methods: { getMessage(val) { ... } }
简单总结:
在vue初始化调用initState()的方法中调用了initWatch方法,对watch进行一系列的初始化操作;
该过程主要通过遍历的方式循环获取属性,根据不同watch的实现方式来分别判断并获取回调函数handler;
然后进入new Watcher()收集依赖和更新;收集的依赖之后调用get方法,并挂载到Dep.target上(其中字符串类型的对象属性键值如果是多层嵌套,例如"obj.a.b.c",每一层都会触发回调);而更新也会触发数据劫持set方法,执行dep.notify()方法进行后续的更新操作(基于双向绑定响应式的实现过程)。
8,computed计算属性的实现
简单总结:与watch监听属性类似,也是在Vue初始化的时候进行初始化,通过遍历的方式获取属性;
但不同点是计算属性会为每个属性创建计算属性watcher实例和和渲染属性watcher实例,且将值缓存到vm._computedWatchers中;同时计算属性在对数据set劫持的时候会先进行判断是否有dirty标记属性,如果有则需要通过他依赖的计算观察者watcher.evaluate()方法重新计算;没有则
computed要依赖data属性的数据变化返回一个值;而watch是观察数据变化执行回调函数