AboutFE
AboutFE copied to clipboard
9、React Vue 相关
调用 setState 之后发生了什么?
回答1
- React 会将传入的参数对象与组件当前的状态合并,然后触发所谓的调和过程(Reconciliation)。
- 经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个UI界面。
- 在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染。
- 在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。
回答2
所谓的同步异步, 更准确的说法是是否进行了批处理。 对于React 17来说,批处理是通过标识位来实现的,在合成事件中以及hook中setState方法都是进行了批处理的;由于batchUpdate方法本身是同步的,因此setTimeout会导致标识位的设定不符合预期,从而出现批处理失败(转为同步)的情况;在SetTimeout中我们可以用手动批处理的方式实现批处理(ReactDOM.unstable_batchedUpdates);因此,React 17的批处理又可以称为半自动批处理。
对于React 18来说,批处理是通过优先级,以及优先级调度来实现的,因此在原生事件和setTimeout中都可以进行批处理。因此,React 17的批处理又可以称为自动批处理。当然,我们也可以使用flushSync的方法将异步转化为同步处理当前调度~
React 17采用标识位isBatchUpdate来判断是否进行批量更新
(1)将标识位isBatchUpdate置为true (2)将合成事件重所有的setState状态存储到一个quque中 (3)合成事件执行结束设置标识位isBatchUpdate置为false,并恢复之前的queue (4)最后统一获取queue中的数据,进行update
React 18通过优先级lane进行批量更新
批处理是 React 将多个状态更新分组到一个重新渲染中以获得更好的性能。如果没有自动批处理,我们只能在 React 事件处理程序中批处理更新。默认情况下,Promise、setTimeout、原生事件处理程序或任何其他事件内部的更新不会在 React 中批处理。使用自动批处理,这些更新将自动批处理, 四次都是异步的都是批处理的, 提供了flushSync方法, 其本质就是把传入的任务设置为高优先级,把当前处理的调度优先级改为该update的优先级,可以实现同步,有了含有优先级的update对象,并被挂在到fiber上后,就要开始我们的调度了,这也是react 18实现自动批处理的关键
在生命周期中的哪一步你应该发起 AJAX 请求?
-
React 下一代调和算法 Fiber 会通过开始或停止渲染的方式优化应用性能,其会影响到 componentWillMount 的触发次数。对于 componentWillMount 这个生命周期函数的调用次数会变得不确定,React 可能会多次频繁调用 componentWillMount。如果我们将 AJAX 请求放到 componentWillMount 函数中,那么显而易见其会被触发多次,自然也就不是好的选择。
-
如果我们将 AJAX 请求放置在生命周期的其他函数中,我们并不能保证请求仅在组件挂载完毕后才会要求响应。如果我们的数据请求在组件挂载之前就完成,并且调用了setState函数将数据添加到组件状态中,对于未挂载的组件则会报错。而在 componentDidMount 函数中进行 AJAX 请求则能有效避免这个问题。
React 中的事件处理逻辑
- 为了解决跨浏览器兼容性问题,React 会将浏览器原生事件(Browser Native Event)封装为合成事件(SyntheticEvent)传入设置的事件处理器中。
- 这里的合成事件提供了与原生事件相同的接口,不过它们屏蔽了底层浏览器的细节差异,保证了行为的一致性。
- React 并没有直接将事件附着到子元素上,而是以单一事件监听器的方式将所有的事件发送到顶层进行处理。这样 React 在更新 DOM 的时候就不需要考虑如何去处理附着在 DOM 上的事件监听器,最终达到优化性能的目的。
React解决了什么问题
- 赶上潮流
- MVC与Flux的差异,MVC的弱势以及Flux弥补的不足,MVC架构的双向绑定以及一对多的关系容易造成连级/联动(Cascading)修改,对于代码的调试和维护都成问题。 https://zhuanlan.zhihu.com/p/21324696
React 中 SOLID五大原则
-
单一职责(Single responsibility principle),React组件设计推崇的是组合,而非继承。
-
如你的页面需要一个表单组件,表单中需要有输入框,按钮,列表,单选框等。那么在开发中你不应该只开发一(整)个表单组件((Form)),而是应该开发若干个单一功能的组件,比如输入框<Input>、提交按钮<Submit>、单选框<Checkbox>等,最后再将它们组合起来。这其中的重点是每个组件仅做一件事。
-
你需要一个函数异步请求数据并返回JSON数据格式,那么你应该拆分为两个函数,一个复杂数据请求,另一个负责数据转化。你可能会好奇为什么一个简单的JSON.parse也拆分出来,因为将来需要会变动,你可能不仅仅需要JSON.parse,还需要转义,需要转化为proto buffer数据格式。而拆分之后如果再面临修改的话,就不会影响到数据请求部分的代码。
-
同样适用于开放/封闭(Open/closed principle)原则。开放/封闭强调的是对修改封闭(禁止修改内部代码),对拓展开放(允许你拓展功能)。因为修改意味着风险,可能会影响到不用修改的代码, 同时意味着暴露细节。你一定纳闷如果不允许修改代码的话如何拓展功能呢,在传统的面向对象编程中,这样的需求是通过继承和接口机制来实现的。在React中我们使用官方推荐的 Higher-Order Components 的模式去实现。
-
接口隔离(Interface segregation principle) 这个就放之四海而皆准了。第三方类库或者模块都避免不了对外提供调用接口,比如对于jQuery来说$是选择器,css用于设置样式,animate负责动画,你不希望把这三个接口都合并成一个叫做together吧,虽然实现起来没有问题,但是对于你将来维护这个类库,以及使用者调用类库,以及调用者的接替者阅读代码(因为他要区分不同上下文中调用这个接口究竟是用来干嘛的),都是不小的困难。
-
依赖反转(Inversion Of Control)原则。这条原则听上去有点拗口。这条原则是意思是,当你在为一个框架编写模块或者组件时,你只需要负责实现接口,并且到注册到框架里即可,然后等待框架来调用你,所以它的另另一个别名是 “Don't call us, we'll call you”。
这么说你可能没什么太大感觉,也不明白和“依赖”和“反转”有什么关系,说到底其实是一个控制权的问题。常规情况下当你在用express编写一个server时,代码是这样的:
const app = express();
module.exports = function (app) {
app.get('/newRoute', function(req, res) {...})
};
这意味着你正在编写的这个模块负责了/newRoute这个路径的处理,这个模块在掌握着主动权。 而用依赖反转的写法是:
module.exports = function plugin() {
return {
method: 'get',
route: '/newRoute',
handler: function(req, res) {...}
}
}
意味着你把控制权交给了引用这个模块的框架,这样的对比就体现了控制权的反转。 其实前端编程中常常用到这个原则,注入依赖就是对这个思维的体现。比如requireJS和Angular1.0中对依赖模块的引用使用的都是注入依赖的思想。
- 里氏替换原则在前端是真的用不上了。
组件的Render函数在何时被调用
- 单纯、侠义的回答这个问题,毫无疑问Render是在组件 state 发生改变时候被调用。无论是通过 setState 函数改变组件自身的state值,还是继承的 props 属性发生改变都会造成render函数被调用,即使改变的前后值都是一样的。 shouldComponentUpdate 默认都返回true,即允许render被调用。如果你对自己的判断能力有自信,你可以重写这个函数,根据参数判断是否应该调用 Render 函数。这也是React其中的一个优化点。
- React组件中存在两类DOM,一类是众所周知的Virtual DOM,相信大家也耳熟能详了;另一类就是浏览器中的真实DOM(Real DOM/Native DOM)。React的Render函数被调用之后,React立即根据props或者state重新创建了一颗Virtual DOM Tree,虽然每一次调用时都重新创建,但因为在内存中创建DOM树其实是非常快且不影响性能的,所以这一步的开销并不大。而Virtual DOM的更新并不意味这Real DOM的更新,接下来的事情也是大家知道的,React采用算法将Virtual DOM和Real DOM进行对比,找出需要更新的最小步骤,此时Real DOM才可能发生修改。
组件的生命周期有哪些
- 初始化阶段(Mounting)
- constructor(): 用于绑定事件以及初始化state(可以通过"fork"props的方式给state赋值)
- componentWillMount(): 在mount前被调用,你可以在这里同步操作state, 但是不会render
- render(): 这个函数是用来渲染DOM没有错。但它只能用来渲染DOM,请保证它的纯粹性。如果有操作DOM或者和浏览器打交道的一系列操作,请在下一步骤componentDidMount中进行
- componentDidMount(): 如果你有第三方操作DOM的类库需要初始化(类似于jQuery,Bootstrap的一些组件)操作DOM、或者请求异步数据,都应该放在这个步骤中做
- 更新阶段(Updating)
- componentWillReceiveProps(nextProps): 在这里你可以拿到即将改变的状态,可以在这一步中通过setState方法设置state,我通常做编辑回填表单
- shouldComponentUpdate(nextProps, nextState): 这一步骤非常重要,它的返回值决定了接下来的生命周期函数是否会被调用,默认返回true,即都会被调用;你也可以重写这个函数使它返回false。
- componentWillUpdate(): 我也不知道这个声明周期函数的意义在哪里,在这个函数内你不能调用setState改变组件状态
- render()
- componentDidUpdate(): 和componentDidMount类似,在这里执行DOM操作以及发起网络请求
- 卸载析构阶段(Unmounting)
- componentWillUnmount(): 主要用于执行一些清理工作,比如取消网络请求,清楚多余的DOM元素等
组件的优化
- 使用上线构建(Production Build):会移除脚本中不必要的警告和报错,减少文件体积
- 避免重绘 (Avoid Reconciliation):重写 shouldComponentUpdate 函数,手动控制是否应该调用 render 函数进行重绘
- 尽可能的使用 Immutable Data( The Power Of Not Mutating Data):尽可能的不修改数据,而是重新赋值数据。这样的话,在检测数据对象是否发生修改方面会非常快,只需要检测对象引用即可,而不用挨个的检测对象属性的更改
- 在渲染组件的时候尽可能的添加key ,这样Virtual DOM在对比时就会更容易发现哪里,哪里是修改元素,哪里是新插入的元素。这里也同时回答了key的作用。如果你有使用过React渲染一个列表的话,它会建议你给每一项添加上key。我个人认为key就类似于DOM中的id,不过是组件级别的,用于标记元素的唯一性。
React Vue Redux Vuex
- 代码文件大小:React代码打包之后相对较大,基本是300KB起跳;而Vue和Vuex框架代码则相对较小,基础库能维持在100KB左右。
- 现成的框架:在Flux初期,Facebook只是推出了Flux这个框架概念,而没有实现这个框架。除非你使用一些第三方的Flux框架,否则你需要自己去实现Flux中的两个事件机制(Component对于Store的响应,Store对于Action的响应)。当然现在React的github项目里已经有Flux框架的示例代码,以及他们推出了Relay框架。相反Vuex不仅提出了这个框架概念,还实现并且提供了这个框架,让开发起来更加便捷
- 针对性的改进:如果你阅读过Vuex的官方文档的话,你会明白Vuex其实是针对Flux存在的一些缺陷而开发的。具体的缺陷其实我们在上一篇中提到过,例如不同的组件都维护自己的状态的话,不同组件之间想改变对方的状态其实会比较困难的。Vuex的解决办法也是上一篇中提到的那样,把state提升到全局的高度,尽可能是使用stateless组件。同时又引入了module等概念更利于代码的解耦和开发
- Vuex中保留了action与store的概念,并且引入了新的mutation。action和mutation广义上来说都是提交对store修改,不同的是action可以是异步的,并且大多数情况是在event handler中提交,通过$store.dispatch方法;唯一修改 Store 的地方只能通过mutation,而且mutation必须是同步的,直接对store进行修改,举例一个简单store的例子:
Vue 双向绑定是如何实现的
- 事件机制(pub/sub):我们通过特定的方法修改数据,例如Store.set('key', 'value'),set方法修改数据的同时触发一个事件,告诉view数据发生了更改,view立即从新从store拉取数据。这类似于Flux中View对于Store数据的响应,只不过通过某种方法或者directive将这种机制封装起来了。这种机制的弱势在于你没法用传统的方式等号=对数据进行赋值。
- 轮询(pull/dirty check):这个方式就更加简单了,数据的消费方不断的检测数据有没有修改。当然不是无时无刻的进行检测,而是在input事件或者change事件的时候进行检测。Angular 1.0使用的就是这种机制。我个人倾向于把这种方式称为轮询而不是脏检查
- Javascript中,我们可以给对象中的值定义访问器 Object天生的支持的属性访问器defineProperty。 那么接下来当你每次想访问data中key字段时,无论是取值data.key还是赋值data.key = 'Hi',都会有打印信息。这也意味着,我们能够在用户执行普通的赋值和取值操作时,做一些事情,例如通知数据的消费者数据发生了更改,让它们重新编译模板。这也就是Vue.js双向绑定的思路。 当然这只是双向数据绑定的一个环节,但是是最核心的环节,其他还包括如何添加订阅者,如何编译模板
第 29 题:聊聊 Vue 的双向数据绑定,Model 如何改变 View,View 又是如何改变 Model 的
利用Proxy实现一个简化版的MVVM
参照vue的响应式设计模式,将数据劫持部分的Obejct.defineProperty
替换为Proxy
即可,其他部分,如compile(编译器没有实现,用写好的html模拟已完成编译),watcher,dep,事件监听等基本保持不变,简单实现代码如下:
<!-- html部分 -->
<div id="foo"></div>
<input type="text" name="" id="bar"/>
// js部分
class Watcher{
constructor(cb){
this.cb = cb;
}
update(){
this.cb()
}
}
class Dep{
constructor(){
this.subs = [];
}
publish(){
this.subs.forEach((item)=>{
item.update && item.update();
})
}
}
class MVVM{
constructor(data){
let that = this;
this.dep = new Dep();
this.data = new Proxy(data,{
get(obj, key, prox){
that.dep.target && that.dep.subs.push(that.dep.target);
return obj[key]
},
set(obj, key, value, prox){
obj[key] = value;
that.dep.publish();
return true;
}
})
this.compile();
}
compile(){
let divWatcher = new Watcher(()=>{
this.compileUtils().div();
})
this.dep.target = divWatcher;
this.compileUtils().div();
this.dep.target = null;
let inputWatcher = new Watcher(()=>{
this.compileUtils().input();
})
this.dep.target = inputWatcher;
this.compileUtils().input();
this.compileUtils().addListener();
this.dep.target = null;
}
compileUtils(){
let that = this;
return {
div(){
document.getElementById('foo').innerHTML = that.data.foo;
},
input(){
document.getElementById('bar').value = that.data.bar;
},
addListener(){
document.getElementById('bar').addEventListener('input', function(){
that.data.bar = this.value;
})
}
}
}
}
let mvvm = new MVVM({foo: 'foo233', bar: 'bar233'})
通过mvvm.data.foo
或者mvvm.data.bar
可以操作数据,可以观察到view做出了改变;在输入框改变输入值,也可以通过mvvm.data
观察到数据被触发改变
React Fiber 纤维比Thread更细
- React Fiber是对核心算法的一次重新实现, 现有React中,更新过程是同步的,这可能会导致性能问题。React决定要加载或者更新组件树时,会做很多事,比如调用各个组件的生命周期函数,计算和比对Virtual DOM,最后更新DOM树,这整个过程是同步进行的,也就是说只要一个加载或者更新过程开始,但是,当组件树比较庞大的时候,问题就来了。
- 破解JavaScript中同步操作时间过长的方法其实很简单——分片。 维护每一个分片的数据结构,就是Fiber。
VDOM
- 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异 把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了
- Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)。
https://github.com/livoras/blog/issues/13
React-Redux 实现
- connect核心就是通过高阶component和context把store提供给原始的component。
- 第一、分配getState中的状态。第二、通过bindActionCreators把action和dispatch绑定在一起。第三、将上面两步合并到一个props中,注入给组件。第四、将我们的组件作为子组件,封装一层,最后返回一个新组件
- Provider从调用方法来看,就是一个React组件,接收一个参数:store。
Vue React 生命周期
第 32 题:Virtual DOM 真的比操作原生 DOM 快吗?谈谈你的想法
不要天真地以为 Virtual DOM 就是快,diff 不是免费的,batching 么 MVVM 也能做,而且最终 patch 的时候还不是要用原生 API。在我看来 Virtual DOM 真正的价值从来都不是性能,而是它
- 为函数式的 UI 编程方式打开了大门;
- 可以渲染到 DOM 以外的 backend,比如 ReactNative。