Vue 和 React 的核心差异
用 Evan you 的话说:双向绑定是对表单来说的,表单的双向绑定,说到底不过是 value 的单向绑定 + onChange 事件侦听的一个语法糖。
这个并不是 React 和 Vue 在理念上真正的差别体现。同时,单向数据流不是 Vue 或者 React 的差别,而是 Vue 和 React 的共同默契选择。单向数据流核心是在于避免组件的自身(未来可复用)状态设计,它强调把 state 拿出来进行集中管理。
而真正我认为 React 和 Vue 在理念上的差别,且对后续设计实现产生不可逆影响的是:Vue 进行数据拦截/代理,它对侦测数据的变化更敏感、更精确,也间接对一些后续实现(比如 hooks,function based API)提供了很大的便利。React 推崇函数式,它直接进行局部重新刷新(或者重新渲染),这样更粗暴,但是更简单,就是刷新呗,前端开发非常简单。但是 React 并不知道什么时候“应该去刷新”,触发局部重新变化是由开发者手动调用 setState 完成。
React setState 引起局部重新刷新。为了达到更好的性能,React 暴漏给开发者 shouldComponentUpdate 这个生命周期 hook,来避免不需要的重新渲染(相比之下,Vue 由于采用依赖追踪,默认就是优化状态。而 React 对数据变化毫无感知,它就提供 React.createElement 调用已生成 virtual dom)。
另外 React 为了弥补不必要的更新,会对 setState 的行为进行合并操作。因此 setState 有时候会是异步更新,但并不是总是“异步”。
以及核心差异对后续设计产生的“不可逆”影响
在设计上,react 给开发者带来了额外的“心智负担”,也引出了一些潜在问题。Vue 的响应式理念,进行数据拦截和代理中不存在类似问题。
这个设计上的差别,直接影响了 hooks 的实现和表现。
setup hook 介绍
setup hook 是 vue3 推出的新功能,对标 react 的 setup hook。
对比 Class API(vue3 已经废除了它):
- 他能更好的支持ts的 类型推导支持(因为ts对函数的入参出参的推导支持力度大,而对class和对象的属性推导的支持度小,不需要写额外的类型声明)。
- 更灵活的逻辑复用能力。这是因为其他的逻辑注入如 vue mixins 存在多个时容易造成命名冲突,模版里使用的数据来源不清晰等问题,而高阶组件同样有 props 命名冲突和来源不清的问题,但是 setup hook 没有。因为 setup 本身返回一个对象,是可以在调用的 vue 实例里解构的,它没有命名冲突和数据来源不清的情况
- tree-shaking 友好(setup函数里的computed,watch等方法若不用到,那么最后打包产生的代码会去掉)
- 代码更容易被压缩(因为函数内的变量名可以被随意压缩混淆成单个字母,而对象或者类的属性压缩起来不安全,就不会压缩 )
对比 React hook:
React hook 底层是基于链表(Array)实现,每次组件被 render 的时候都会顺序执行所有的 hooks,因为底层是链表,每一个 hook 的 next 是指向下一个 hook 的,所以要求开发者不能在不同 hooks 调用中使用判断条件,因为 if 会导致顺序不正确,从而导致报错。
相反,Vue 3 hook 只会被注册调用一次,Vue之所以能避开这些麻烦的问题,根本原因在于它对数据的响应是基于响应式的,是对数据进行了代理的。不需要链表进行 hooks 记录,它对数据直接代理观察。
但是 Vue 这种响应式的方案,也有自己的困扰。比如 useState() (实际上 evan 命名为 value())返回的是一个 value wrapper (包装对象)。一个包装对象只有一个属性:.value ,该属性指向内部被包装的值。我们知道在 JavaScript 中,原始值类型如 string 和 number 是只有值,没有引用的。不管是使用 Object.defineProperty 还是 Proxy,我们无法追踪原始变量后续的变化。因此 Vue 不得不返回一个包装对象,不然对于基本类型,它无法做到数据的代理和拦截。这算是因为设计理念带来的一个非常非常微小的 side effect。从 Evan you 的截图中,我圈了出来:
Vue 和 React 在 API 设计风格的不同
React 事件系统庞大而复杂。它暴漏给开发者的事件不是原生事件,并且 this 不是指向组件或者实例本身。
React 的事件是包装过的合成事件,并且非常重要的一点是,不同的事件可能会共享一个合成事件对象。另外一个细节是,React 对所有事件都进行了代理,将所有事件都绑定 document 上。请读者仔细体会下面的代码:
class clickTest extends React.Component {
componentDidMount() {
document.addEventListener('click', () => {
console.log('document click');
});
}
handleClick(evt) {
console.log('div click', evt);
console.log(this);
evt.stopPropagation();
// 触发点击后会打印
// div click
// document click
// 并且 this 是 undefined。
}
render() {
return (
<div onClick={this.handleClick}> click </div>
)
}
}
上文代码触发点击事件后,两个 console.log 都打印是因为压根没能有效的阻止事件冒泡,因为React 对所有事件都进行了代理,将所有事件都绑定 document 上。
然而改成下文的代码就可以只打印 div click
handleClick(evt) {
console.log('div click');
console.log(evt, evt.nativeEvent);
// 如果有多个相同类型事件的事件监听函数绑定到同一个元素,
// 当该类型的事件触发时,它们会按照被添加的顺序执行。
// 如果其中某个监听函数执行了 event.stopImmediatePropagation() 方法,
// 则当前元素剩下的监听函数将不会被执行。
//(译者注:注意区别 event.stopPropagation )
evt.nativeEvent.stopImmediatePropagation();
}
其实想解决 react 函数 this 指向的问题,最稳妥的也只能是开发者 bind 手动绑定一下作用域了,这个做法同样是增加了开发者的心智负担。
Vue 事件系统
Vue 事件处理函数中的 this 默认指向组件实例。绑定的过程源码也清晰的给出
function nativeBind (fn, ctx) {
return fn.bind(ctx)
}
var bind = Function.prototype.bind
? nativeBind
: polyfillBind;
渲染的比对
jsx 和手写的 render function 是是完全动态的,过度的灵活性导致运行时可以用于优化的信息不足,所以 react 就只能显示调用 setState 来强行更新组件。由于无从减掉一些不必要的渲染对比,所以 react 对此的解决方法就是引入了时间分片 react fibber,把 patch 并且更新视图的过程切分成多个任务,分批更新。
这点是 vue3 曾经考虑过做时间分片,由于 Evan you 觉得只要更有效率的 diff,16.67 ms 的更新时间已经足够,不需要分片。因为 vue 会标明某些 dom 是静态的,并且 Vue 3.0 提出的动静结合的 DOM diff 思想,开始做到了标记哪个 DOM 绑定了变量需要 patch,省去也不必要的 patch 更新的优化操作。
之所以能够做到 Vue 3.0 预编译优化,是因为 Vue core 可以静态分析 template,在解析模版时,整个 parse 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。
这点是 React 达不成的,它只能重新渲染更新,React 拿到的或者说掌管的,所负责的就是一堆递归 React.createElement 的执行调用,它无法从模版层面进行静态分析。因此 React JSX 过度的灵活性导致运行时可以用于优化的信息不足。
当然 React 并不是没有意识到这个问题,他们在积极的同 prepack 合作。力求弥补构建优化的先天不足。
Prepack 同样是 FaceBook 团队的作品。它让你编写普通的 JavaScript 代码,它在构建阶段就试图了解代码将做什么,然后生成等价的代码,减少了运行时的计算量,就相当于 JavaScript 的部分求值器。
参考
Vue.js作者尤雨溪在 VueConf 谈 Vue 3.0-腾讯视频