ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
## 响应式原理 Vue内部使用了Object.defineProperty()来实现数据响应式,通过这个函数可以监听到set和get事件 ~~~ var data = { name:'xxx'}; observe(data); let name = data.name //get data.name="yyy"; function observe(obj){ //判断类型 if(!obj || typeof obj !=='object'){ return; } Object.keys(obj).forEach((key)=>{ defineReactive(obj,key, obj[value]); }) } function defineReactive(obj, key, valu){ //递归子属性 observe(val); Object,defineProperty(obj,key,{ //可枚举 enumerable: true, //可配置 configurable: true, //自定义函数 get:function reactiveGetter(){ console.log('get value'); return val; }, set:function reactiveSetter(newVal){ console.log('change value'); val = newVal } }) } ~~~ 代码实现了如何监听数据的set和get,仅仅如此是不够的,因为**自定义的函数一开始是不会执行的**。**只有先执行了依赖收集,才能在属性更新的时候派发更新**。 我们需要先触发依赖收集 ~~~ <div>{{name}}</div> ~~~ 在解析如上模板代码时,遇到{{name}}就会进行依赖收集。 实现一个**Dep类,用于解耦属性的依赖收集和派发更新操作**。 ~~~ //通过Dep解耦属性的依赖和更新操作 class Dep{ constructor(){ this.subs = []; //所有的订阅列表(依赖列表)(订阅某属性的Watcher列表) } //添加依赖 addSub(sub){ this.subs.push(sub); } //更新 notify(){ this.subs.forEach((sub)=>{ sub.update(); }) } } //全局属性,通过该属性配置当前的Watcher(观察者) Dep.target = null; ~~~ **当需要依赖收集的时候调用addSub,当需要派发更新的时候调用notify**。 Vue组件**挂载时添加响应式**的过程: 1. 组件挂载时,先对所有**需要的属性**调用Object.defineProperty() 2. 实例化Watcher(观察者),**传入组件更新的回调**。三种watcher(渲染watcher,计算watcher和侦听器watcher) 3. 实例化过程中,会对**模板中的属性进行求值,触发依赖收集**。 触发依赖收集的操作 ~~~ class Watcher{ constructor(obj, key, cb){ //将Dep.target指向自己 //然后触发属性的getter添加监听 //最后将Dep.target置空 Dep.target = this; this.cb = cb; this.obj = obj; this.key = key; this.value = obj[key] Dep.target = null; } update(){ //获得新值 this.value = this.obj[this.value]; //调用update方法更新dom this.cb(this.value) } } ~~~ 以上是Watcher的简单实现,在执行构造函数的时候,将Dep.target执行自身,从而使得**收集到了对应的Watcher**,**在派发更新的时候取出对应的Watcher然后执行update函数**。 接下来对defineReactive函数进行改造,在自定义函数中添加**依赖收集和派发更新**相关的代码 ~~~ var data = { name:'xxx'}; observe(data); let name = data.name //get data.name="yyy"; function observe(obj){ //判断类型 if(!obj || typeof obj !=='object'){ return; } Object.keys(obj).forEach((key)=>{ defineReactive(obj,key, obj[value]); }) } function defineReactive(obj, key, valu){ //递归子属性 observe(val); let dp = new Dep(); Object.defineProperty(obj,key,{ //可枚举 enumerable: true, //可配置 configurable: true, //自定义函数 get:function reactiveGetter(){ console.log('get value'); //将Watcher添加到订阅 if(Dep.target){ dp.addSub(Dep.target); } return val; }, set:function reactiveSetter(newVal){ console.log('change value'); val = newVal; //执行watcher的update方法 dp.notify(); } }) } ~~~ 简易的数据响应式,核心思路:**手动触发一次属性的getter来实现依赖收集**。 ## Object.defineProperty的缺陷 如果通过**下标方式修改数组数据**或者**给对象新增属性**并不会触发组件的重新渲染。 因为**Object.defineproperty不会拦截到这些操作**,更确切的说,对于数组而言,**大部分操作都是拦截不到的**,只是Vue内部通过**重写函数的方式解决了这个问题**。 对于第一个问题,Vue提供了一个API解决vm.$set; ## 编译过程 大家在使用Vue开发的过程中,基本都是使用模板的方式,那么模板时怎么在浏览器中运行的吗? 模板时为了方便开发者进行开发,把模板丢到浏览器中肯定是不能运行的。 **Vue通过编译器将模板通过几个阶段最终编译为render函数,然后通过执行render函数生成vdom,最终映射为真是的DOM** 三个阶段 * 将模板解析为AST * 优化AST * 将AST转为render函数 第一阶段中,通过**各种各样的正则表达式去匹配模板中的内容**,然后将**内容提取出来做各种逻辑操作**,接下来生成一个**最基本的AST对象**。 ~~~ { // 类型 type: 1, // 标签 tag, // 属性列表 attrsList: attrs, // 属性映射 attrsMap: makeAttrsMap(attrs), // 父节点 parent, // 子节点 children: [] } ~~~ 然后会根据这个最基本的AST对象中的属性,进一步扩展AST 当然在这一阶段中,还会进行其他的一些判断逻辑。 优化AST的阶段:只是对节点进行了静态内容提取,也就是将**永远不会变的的节点提取出来,实现复用virtual dom,跳过对比算法的功能**。 通过AST生成render函数:主要目的是遍历真个AST,根据不同条件生成不同代码。 ## NextTick原理分析 nextTick可以让我们在**下次DOM更新循环结束之后执行延迟回调**,用于**获取更新后的DOM**。 Vue2.4之前使用的microtasks,**但是microtasks优先级过高,在某些情况下可能出现比实际冒泡更快的情况**,但如果使**用macrotasks又可能会出现渲染的性能问题**。所有在新版本中会默认使用microtasks,但在特殊情况下会使用macrotasks。比如v-on。 对应实现**macrotasks,会先判断是否能使用setImmediate,不能的话降级为MessageChannel,以上都不行的话使用setTimeout**。 ~~~ if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { macroTimerFunc = () => { setImmediate(flushCallbacks) } } else if ( typeof MessageChannel !== 'undefined' && (isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]') ) { const channel = new MessageChannel() const port = channel.port2 channel.port1.onmessage = flushCallbacks macroTimerFunc = () => { port.postMessage(1) } } else { macroTimerFunc = () => { setTimeout(flushCallbacks, 0) } } ~~~