Vue 2.0 使用 Object.defineProperty()实现数据响应
Object.observe()被废弃后,Object.defineProperty()成为了大家实现数据劫持的首选。
vue递归遍历data中的数据,使用 Object.defineProperty()劫持 getter和setter,在getter中做数据依赖收集处理,在setter中监听数据的变化,并通知订阅当前数据的地方。
这么做的问题有:
Vue 3.0 中的Proxy
const p = new Proxy({}, {
get(target, propKey) {
return '哈哈,你被我拦截了';
}
});
console.log(p.name);
// 哈哈,你被我拦截了
Proxy支持的拦截操作一共 13 种,详细的可以查看 MDN。
为什么使用 Proxy 可以解决上面的问题呢?主要是因为Proxy是拦截对象,并返回一个新的被proxy包裹的对象,外界对该对象的访问,都必须先通过这层拦截。无论访问对象的什么属性,之前定义的还是新增的,它都会走到拦截中。
Reflect(ES6引入) 是一个内置的对象,它提供拦截 JavaScript 操作的方法。将Object对象一些明显属于语言内部方法(比如Object.defineProperty())放到Reflect对象上。修改某些Object方法的返回结果,让其变得更合理。让Object操作都变成函数行为。具体内容查看MDN
Reflect.get(target, prop, receiver)到底有什么用呢?
先看下面的例子:
let People = new Proxy({
_name:'zky',
get name(){
return this._name
}
},{
get:function(target,prop,receiver){
return target[prop]
}
})
let Man = {_name:'zky_man'}
Man.__proto__ = People // Man继承People
console.log(Man._name) // zky_man
console.log(Man.name) // zky
问题来了,Man中已经存在_name属性,但这里Man.name返回的却是原型链上的_name属性,
原因很好解释:get name中的this默认绑定为People。
那怎么解决这个问题呢?这里就该receiver上场了,修改上面的例子:
let People = new Proxy({_name:'zky'},{
get:function(target,prop,receiver){ // receiver指向的是get的调用者
return Reflect.get(target,prop,receiver) // 调用get name函数时,this被绑定到receiver
}
})
let Man = {
_name:'zky_man',
get name(){
return this._name
}
}
Man.__proto__ = People // Man继承People
console.log(Man._name)// zky_man
console.log(Man.name) // zky_man
举个例子总结:有一栋大楼,我们想给里面的住户发邮件,Object.defineProperty()就是只有注册了电子邮箱的的住户才能可能收到我们的邮件,后搬来的住户没有注册邮箱,就无法和我们交流,除非我们强制他再注册一个邮箱(使用vue.$set),而使用Proxy就是建立一个收发室,所有要进行交流的邮箱先通过收发室的拦截,再由收发室通知每一位住户信息,这样新搬来的住户也可以和我们正常交流了。
<template>
<button @click="increment">
Count is: {{ count }}
</button>
</template>
<script>
import { ref, watchEffect, onMounted } from 'vue'
export default {
setup() {
const count = ref(0)
function increment() {
count.value++
}
watchEffect(() => console.log(count.value))
onMounted(() => console.log('mounted!'))
return {
count,
increment,
}
},
}
</script>
第一次看这段代码可能会觉得神似react hooks!
Vue 的函数式编程和react hooks最大的不同就是setup()入口函数只执行一遍,setup() 函数返回值就是注入到页面模版的变量。
ref()(以前被命名为value()) 返回的是一个对象,通过 .value 才能访问到其真实值。
为何 ref()返回的是 Wrappers (包装对象)而非具体值呢?原因是 Vue 采用双向绑定,只有对象形式访问值才能保证访问到的是最终值,这一点类似 React 的 useRef() API 的 .current 规则。
那既然所有 ref() 返回的值都是 Wrapper,那直接给模版使用时要不要调用 .value 呢?答案是否定的,直接使用即可,模版会自动 Unwrapping。关于包装对象(对于原生js:当对number、string、boolean这三种数据类型调用方法时候,js会先将他们转换为对应的包装对象,并且这个对象是临时的,调用完包装对象的方法后,包装对象随即被丢弃)
关于watch,因为在 Vue 中,setup 函数仅执行一次,所以不像 React Function Component,每次组件 props 变化都会重新执行,因此无论是在变量、props 变化时如果想做一些事情,都需要包裹在 watch 中。
结构区别
因为客观上存在Immutable 与 Mutable、JSX 与 Template 的区别,vue中的setup 函数仅执行一次,看上去与 React 函数完全不一样(React 函数每次都执行),但其实 Vue 将渲染层(Template)与数据层(setup)分开了,而 React 合在了一起。虽然我们可以利用 React Hooks 将数据层与渲染层完全隔离,源于 JSX 与 Template 的根本区别,JSX 使模版与 JS 可以写在一起,因此数据层与渲染层可以耦合在一起写(也可以拆分),但 Vue 采取的 Template 思路使数据层强制分离了,这也使代码分层更清晰了。
语法区别
React 返回的 count 是一个数字,是因为 Immutable 规则,而 Vue 返回的 count 是个对象,拥有 count.value 属性,也是因为 Vue Mutable 规则导致,这使得 Vue 定义的所有变量都类似 React 中 useRef 定义变量,因此不存 React capture value (捕捉值:每次 Render 的内容都会形成一个快照并保留下来,因此当状态变更而 Rerender 时,就形成了 N 个 Render 状态,而每个 Render 状态都拥有自己固定不变的 Props 与 State)的特性。
Vue 的代码使用更符合js原本的习惯
这句话直截了当戳中了 JS 软肋,JS 并非是针对 Immutable 设计的语言,所以 Mutable 写法非常自然,而 Immutable 的写法就比较别扭。
当 Hooks 要更新值时,Vue 只要用等于号赋值即可,而 React Hooks 需要调用赋值函数,当对象类型复杂时,还需借助第三方库才能保证进行了正确的 Immutable 更新。
对 Hooks 使用顺序无要求,而且可以放在条件语句里
对 React Hooks 而言,调用必须放在最前面,而且不能被包含在条件语句里(可以参考React hooks 学习笔记 - useState ),这是因为 React Hooks 采用下标方式寻找状态,一旦位置不对或者 Hooks 放在了条件中,就无法正确找到对应位置的值。
而 Vue Function API 中的 Hooks 可以放在任意位置、任意命名、被条件语句任意包裹的,因为其并不会触发 setup 的更新,只在需要的时候更新自己的引用值即可,而 Template 的重渲染则完全继承 Vue 2.0 的依赖收集机制,它不管值来自哪里,只要用到的值变了,就可以重新渲染了。
不会在每次渲染重复调用,减少垃圾回收压力
这确实是 React Hooks 的一个问题,所有 Hooks 都在渲染闭包中执行,每次重渲染都有一定性能压力,而且频繁的渲染会带来许多闭包,虽然可以依赖垃圾回收机制回收,但会给垃圾回收带来不小的压力,并且必须要包裹useCallback 函数确保不让子元素频繁重渲染。而 Vue Hooks 只有一个引用,所以存储的内容就非常精简,也就是占用内存小,而且当值变化时,也不会重新触发 setup 的执行,所以确实不会造成垃圾回收压力,Mutable + 依赖自动收集就可以做到最小粒度的精确更新,根本不会触发不必要的 Rerender,因此 useMemo 这个概念也不需要了。
先看一段代码
<div id="app">
<h1>今天</h1>
<p>大家好!</p>
<div>{{name}}</div>
</div>
在vue2.0中,会转换为:
function render() {
with(this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_c('h1', [_v("今天")]), _c('p', [_v("大家好!")]), _c('div', [_v(
_s(name))])])
}
}
其中的 大家好!
两个节点完全是静态节点,也要加入diff算法的比较过程中来,所以造成了一定的性能消耗。今天
、
在vue3.0中,会转换为:
import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock("div", { id: "app" }, [
_createVNode("h1", null, "今天"),
_createVNode("p", null, "大家好!"),
_createVNode("div", null, _toDisplayString(_ctx.name), 1 /* TEXT */)
]))
}
注意看第二个_createVNode结尾的“1”,Vue在运行时会生成number(大于0)值的PatchFlag,用作标记。只有带这个参数的,才会被真正的追踪,静态节点不需要遍历,这个就是vue3优秀性能的主要来源。PatchFlag的值有以下多种类型:
如果一个节点涉及以上两种绑定,进行位运算就可以得出最终结果了。例如:
当我们的节点中涉及到事件函数时:
<div id="app">
<button @click="handleClick">戳我</button>
</div>
转换为:
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock("div", { id: "app" }, [
_createVNode("button", {
onClick: _cache[1] || (_cache[1] = $event => (_ctx.handleClick($event)))
}, "戳我")
]))
}
传入的事件会自动生成并缓存一个内联函数再cache里,变为一个静态节点。这样就算我们自己写内联函数,也不会导致多余的重复渲染。
react使用fiber实现这个目标
简单对描述就是在一个时间片里尽量做更多事,快速响应用户,让用户觉得够快。利用浏览器的空闲时间来做diff,如果超过了16ms,有动画或者用户交互的任务,就把主进程控制权还给浏览器,等空闲了继续。
对fiber细节感兴趣对可以看看我之前写的学习笔记React Fiber学习笔记
vue优化模版渲染实现这个目标
除了刚才上面说得“静态标记/事件缓存”外,在vue2.0中也有关于静态节点编译对优化,不过这个静态节点特指对是静态根节点,而“静态标记/事件缓存”是为静态子节点服务的。
compile 的内容非常多,大致分为三块主要内容,我也称他们是Vue的 渲染三巨头:
就是 parse,optimize,generate
虽然分为三块,但是要明确一点:compile 的作用是解析模板,生成渲染模板的 render,再继而产生vnode。
parse:接收 template 原始模板,按照模板的节点 和数据 生成对应的 ast(抽象语法树)
optimize:遍历递归每一个ast节点,标记静态的根节点(没有绑定任何动态数据),这样就知道那部分不会变化,于是在页面需要更新时,减少去比对这部分DOM从而达到性能优化的目的。
generate:把前两步生成完善的 ast 组装成 render 字符串(这个 render 变成函数后是可执行的函数,不过现在是字符串的形态,后面会转成函数)
关于js的编译原理可以参考这个最简单的js编译器( 介绍:今天我们将一起写一个编译器,但不是真正意义上的编译器,只是一个极简编译器。简单到只要你把说明文字去掉,只剩下200行左右的代码)强烈推荐看看