Skip to content

9、React Vue 相关 #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
CodingMeUp opened this issue Nov 15, 2017 · 16 comments
Open

9、React Vue 相关 #10

CodingMeUp opened this issue Nov 15, 2017 · 16 comments

Comments

@CodingMeUp
Copy link
Owner

CodingMeUp commented Nov 15, 2017

调用 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实现自动批处理的关键

@CodingMeUp
Copy link
Owner Author

CodingMeUp commented Nov 15, 2017

在生命周期中的哪一步你应该发起 AJAX 请求?

  • React 下一代调和算法 Fiber 会通过开始或停止渲染的方式优化应用性能,其会影响到 componentWillMount 的触发次数。对于 componentWillMount 这个生命周期函数的调用次数会变得不确定,React 可能会多次频繁调用 componentWillMount。如果我们将 AJAX 请求放到 componentWillMount 函数中,那么显而易见其会被触发多次,自然也就不是好的选择。

  • 如果我们将 AJAX 请求放置在生命周期的其他函数中,我们并不能保证请求仅在组件挂载完毕后才会要求响应。如果我们的数据请求在组件挂载之前就完成,并且调用了setState函数将数据添加到组件状态中,对于未挂载的组件则会报错。而在 componentDidMount 函数中进行 AJAX 请求则能有效避免这个问题。

@CodingMeUp
Copy link
Owner Author

React 中的事件处理逻辑

  • 为了解决跨浏览器兼容性问题,React 会将浏览器原生事件(Browser Native Event)封装为合成事件(SyntheticEvent)传入设置的事件处理器中。
  • 这里的合成事件提供了与原生事件相同的接口,不过它们屏蔽了底层浏览器的细节差异,保证了行为的一致性。
  • React 并没有直接将事件附着到子元素上,而是以单一事件监听器的方式将所有的事件发送到顶层进行处理。这样 React 在更新 DOM 的时候就不需要考虑如何去处理附着在 DOM 上的事件监听器,最终达到优化性能的目的。

@CodingMeUp
Copy link
Owner Author

React解决了什么问题

  • 赶上潮流
  • MVC与Flux的差异,MVC的弱势以及Flux弥补的不足,MVC架构的双向绑定以及一对多的关系容易造成连级/联动(Cascading)修改,对于代码的调试和维护都成问题。
    https://zhuanlan.zhihu.com/p/21324696

@CodingMeUp
Copy link
Owner Author

CodingMeUp commented Nov 15, 2017

React 中 SOLID五大原则

  • 单一职责(Single responsibility principle),React组件设计推崇的是组合,而非继承

  • 如你的页面需要一个表单组件,表单中需要有输入框,按钮,列表,单选框等。那么在开发中你不应该只开发一(整)个表单组件((Form)),而是应该开发若干个单一功能的组件,比如输入框、提交按钮、单选框等,最后再将它们组合起来。这其中的重点是每个组件仅做一件事。

  • 你需要一个函数异步请求数据并返回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中对依赖模块的引用使用的都是注入依赖的思想。

  • 里氏替换原则在前端是真的用不上了。

@CodingMeUp
Copy link
Owner Author

组件的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才可能发生修改。

@CodingMeUp
Copy link
Owner Author

CodingMeUp commented Nov 15, 2017

组件的生命周期有哪些

  • 初始化阶段(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元素等

@CodingMeUp
Copy link
Owner Author

组件的优化

  • 使用上线构建(Production Build):会移除脚本中不必要的警告和报错,减少文件体积
  • 避免重绘 (Avoid Reconciliation):重写 shouldComponentUpdate 函数,手动控制是否应该调用 render 函数进行重绘
  • 尽可能的使用 Immutable Data( The Power Of Not Mutating Data):尽可能的不修改数据,而是重新赋值数据。这样的话,在检测数据对象是否发生修改方面会非常快,只需要检测对象引用即可,而不用挨个的检测对象属性的更改
  • 在渲染组件的时候尽可能的添加key ,这样Virtual DOM在对比时就会更容易发现哪里,哪里是修改元素,哪里是新插入的元素。这里也同时回答了key的作用。如果你有使用过React渲染一个列表的话,它会建议你给每一项添加上key。我个人认为key就类似于DOM中的id,不过是组件级别的,用于标记元素的唯一性。

@CodingMeUp
Copy link
Owner Author

CodingMeUp commented Nov 15, 2017

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的例子:

@CodingMeUp CodingMeUp changed the title 9、React 相关 9、React Vue 相关 Nov 15, 2017
@CodingMeUp
Copy link
Owner Author

CodingMeUp commented Nov 15, 2017

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观察到数据被触发改变

@CodingMeUp
Copy link
Owner Author

React Fiber 纤维比Thread更细

  • React Fiber是对核心算法的一次重新实现, 现有React中,更新过程是同步的,这可能会导致性能问题。React决定要加载或者更新组件树时,会做很多事,比如调用各个组件的生命周期函数,计算和比对Virtual DOM,最后更新DOM树,这整个过程是同步进行的,也就是说只要一个加载或者更新过程开始,但是,当组件树比较庞大的时候,问题就来了。
  • 破解JavaScript中同步操作时间过长的方法其实很简单——分片。 维护每一个分片的数据结构,就是Fiber。

@CodingMeUp
Copy link
Owner Author

CodingMeUp commented Nov 15, 2017

VDOM

  • 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中
    当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
    把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了
  • Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)。

livoras/blog#13

@CodingMeUp
Copy link
Owner Author

CodingMeUp commented Nov 15, 2017

_20171115174855

React-Redux 实现

  • connect核心就是通过高阶component和context把store提供给原始的component。
  • 第一、分配getState中的状态。第二、通过bindActionCreators把action和dispatch绑定在一起。第三、将上面两步合并到一个props中,注入给组件。第四、将我们的组件作为子组件,封装一层,最后返回一个新组件
  • Provider从调用方法来看,就是一个React组件,接收一个参数:store。

@CodingMeUp
Copy link
Owner Author

CodingMeUp commented Dec 4, 2017

Vue React 生命周期
image
image

@CodingMeUp
Copy link
Owner Author

@CodingMeUp
Copy link
Owner Author

@CodingMeUp
Copy link
Owner Author

CodingMeUp commented Jun 24, 2020

第 32 题:Virtual DOM 真的比操作原生 DOM 快吗?谈谈你的想法

不要天真地以为 Virtual DOM 就是快,diff 不是免费的,batching 么 MVVM 也能做,而且最终 patch 的时候还不是要用原生 API。在我看来 Virtual DOM 真正的价值从来都不是性能,而是它

  1. 为函数式的 UI 编程方式打开了大门;
  2. 可以渲染到 DOM 以外的 backend,比如 ReactNative。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant