vue【数据劫持+虚拟dom】。 vue为什么需要虚拟dom呢,原因有:对视图做了抽象转化,所以跨平台能力更好,还能实现ssr服务端渲染,因为解耦了html解析器;然后最关键的,是因为vue1到2更新策略变了,vue2是在组件级别上感知变化,但是具体这个组件发生了什么变化,不知道,此时就只能通过抽象出一份虚拟dom来对比。vue1的更新粒度是dom级别的,这虽然直接就知道哪里的dom变化了,但这带来的问题是巨量的监听器watcher的开销。所以说vue引入虚拟dom其实和性能没有直接关系,至少目的不是这个,对比组件找不同才是直接目的。
数据劫持:比如Object.definProperty个 set方法,还有vue3是proxy,这两种方法其实都相当于提供了一个监听,可以在变量发生变化时做点什么,比如通知框架更新视图
angular【zone.js+变更检测】 因为变更检测就是对组件树自上而下检测的,而且会检测两次,pass的标准就是前后两遍数据都一样,所以需要严格的单向数据流作为保证,就好像点名,你得有个顺序吧,被点过名的人,不能又跑来跑去吧。所以,单向数据流之于angular和之于vue的意义是完全不同的。之于angular,这是安身立命之本;之于vue和react,这其实是规范问题,带来更稳定的代码编写方式,和跟踪数据的方式。所以,如果在一些子组件的声明周期中比如afterNgViewChecked,子组件修改父组件的引用类型的属性,这时angular也会毫不留情地报错。而这种情况vue的表现则完全是假装无事发生。当然,如果是基本类型的属性,那肯定是三大框架都不允许修改的(另外有个特例,在angular的ngOnInit是修改父组件属性的,这是因为变更检测的顺序问题,此时子组件的变更检测未开始,所以可以改父组件的,但这个特例还有个特例,如果你改的是个@input那又不行了,所以还是尽量别)。已验证 另外react中子组件修改父组件的引用类型的属性会不会报错,不知道,留个疑问以后有机会再看,angular做变更检测的时候还要跑生命周期的,所以数据有可能再变化,不知道react做虚拟dom对比是什么时候,如果数据不会变了,那可能react就不会报错
zone其实就没什么好说的,就是一个js库,能让angular知道什么时候更新。就是将所有的事件入口都拦截,这样所有的数据变化的触发点都包括进去了,包括异步请求、定时器、用户输入事件等。
react 【setState+虚拟dom】react没有数据劫持也没有zone.js,所以react甚至不知道什么时候要更新了,需要通过setState这个api去通知react更新;也没有变更检测,所以react是对比整个虚拟dom树来找不同的。发现这反而又和angular这种整个组件树的变更检测有异曲同工之妙了。所以也可以发现,虚拟dom对于react和vue来说又是不同的,对react来说是根本,而对于vue来说,只是组件更新策略背景下的小策略而已。
最后,可以发现 数据劫持、zone.js、setState这三个是一组的,就是为了拦截数据变化,通知框架更新
然后,更新策略:
vue是组件级更新,所以需要依赖收集(哪些组件,用到哪些变量,这里需要一个映射关系来维护,这就是依赖收集),对于发生变化的组件,使用虚拟dom去找不同
angular是变更检测,这个就是按component的class来对比的,所以比完就知道哪些组件变了。但是知道哪些组件变了后,如何找到对应的dom来修改呢,这就不知道了,留个疑问以后有空再看
react是整个虚拟dom树来对比,对比完了就对照着修改真实dom了,所以这比vue又少了依赖收集这项工作,但是vue又不需要对比所有的虚拟dom,各有优势
渲染速度:vue3明显强于react和angular了,react稍快于angular
大概因为vue3的数据劫持改成了proxy,优点在于,将数据劫持的工作,延迟到整个程序的运行时,而不是初始化。简单说就是,对于引用类型变量,只有访问过的才代理,而没访问过的,就没有代理。在vue2时,数据劫持的工作,必须在new Vue()的时候全部做好
=============
再说一下关于单向数据流、双向绑定这些东西
单向数据流
angular真的很严格,只要是@input的输入,即使是引用的对象,然后子组件修改引用对象中的属性,都是会报错的,除非直接注入parent,用parent.xxx的方式修改父组件。而vue,如果props传入了引用的对象,其实是可以修改对象中的内容的。关于为什么要坚持单向数据流,说一个好处:就是看代码快,如果每个组件都能按这样的原则,那么,我一看子组件用的地方,就知道这组件里面干了啥,效率高!否则我还要浪费时间去猜去看!而且,其实,很多时候并不需要在父组件加函数,而只需要写个改值的简单语句即可,并不会造成父组件冗余杂乱。所以,其实,这也是纯函数的思想,你封装函数的时候,就不要在方法内部修改外部的东西,否则就产生副作用了,这就是不可预期!这好吗?这不好!总之,尽量避免这种行为,写出稳定的代码不香吗。但是呢,这说到底只是设计和提倡,比方在你确信子组件中修改父组件引用中的属性没有副作用时同时又能提高开发效率时,也是可以的。正如js数组的高阶函数为了支持链式调用也不是纯函数,所以还是要具体情况具体分析。
双向绑定
其实angular的盒子力的香蕉[()]一看还真是拟物了 和vue的v-model,这些就是双向绑定。其实[ngModel]等同于v-bind,都是单向绑定。而[()]和v-model其实都可以应用到组件上,此时为了实现子组件逆向输出,则需要再内部相应地做实现,angular:@input xxx和@output xxxChange = new EventEmitter();vue: props xxx和直接使用$emit('input', val) 当然如果你不想用input事件,那还可以用model属性来修改以回避冲突。总之呢,就是提供好一进一出,其实仍然保证了单向数据流
=============面试被问到了如何实现双向绑定
我当时想了想,竟然没顺畅地答好,然后我说,这个问题其实比较笼统
为什么呢,这确实就是笼统的。因为后来回想了下至少有两层需要解释:
- 首先框架层,问,vue如何实现双向绑定?答,数据劫持,vue2是defineProperty,vue3是proxy。
- 然后指令层,问,自定义组件如何实现双向绑定?答,v-model实际上是个语法糖,如果想在自定义组件上使用双向绑定v-model,那就要在组件内部配合使用$emit。
看吧!至少有两个概念需要解释,一般人都没理清楚概念