内容简介:从这一小节开始,正式进入回顾一下之前的内容,在这个系列的开始部分,先介绍了配置选项的合并,数据的代理,生命周期的初始化,接下来是实例的挂载。而在实例挂载前还有意忽略了一个过程,简单回顾一下
从这一小节开始,正式进入 Vue
源码的核心,也是难点之一,响应式系统的构建。这一节将作为分析响应式构建过程源码的入门,主要分为两大块,第一块是针对响应式数据 props,methods,data,computed,wather
初始化过程的分析,另一块则是在保留源码设计理念的前提下,尝试手动构建一个基础的响应式系统。有了这两个基础内容的铺垫,下一篇进行源码具体细节的分析会更加得心应手。
回顾一下之前的内容,在这个系列的开始部分,先介绍了配置选项的合并,数据的代理,生命周期的初始化,接下来是实例的挂载。而在实例挂载前还有意忽略了一个过程, 数据的初始化
(即 initState(vm)
)。 initState
的过程,是对数据进行响应式设计的过程,过程会针对 props,methods,data,computed
和 watch
做数据的初始化处理,并将他们转换为响应式对象,接下来我们会逐步分析每一个过程。
function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
// 初始化props
if (opts.props) { initProps(vm, opts.props); }
// 初始化methods
if (opts.methods) { initMethods(vm, opts.methods); }
// 初始化data
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */);
}
// 初始化computed
if (opts.computed) { initComputed(vm, opts.computed); }
// 初始化watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
复制代码
7.1 initProps
简单回顾一下 props
的用法,父组件通过属性的形式将数据传递给子组件,子组件通过 props
属性接收父组件传递的值。
// 父组件
<child :test="test"></child>
// 子组件
{
props: ['test']
}
复制代码
因此分析 props
需要分析父组件和子组件的两个过程,我们先看父组件对传递值的处理。父组件会优先进行实例的挂载, render
解析过程中,遇到子组件的占位符节点 <child :test="test"></child>
时,会创建子类构造器,遇到传递给子组件的属性时,会解析成 { attrs: {test: test}}
的形式并作为 render
函数存在。再对 attrs
属性进行规范校验后,会将校验的结果以 propsData
属性的形式来创建子的 Vnode
。总结来说, props
传递给占位符组件的写法,会以 propsData
的形式作为子组件 Vnode
的属性存在。
// 创建子组件过程
function createComponent() {
// props校验
var propsData = extractPropsFromVNodeData(data, Ctor, tag);
···
// 创建子组件vnode
var vnode = new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
data, undefined, undefined, undefined, context,
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
asyncFactory
);
}
复制代码
7.1.1 props的命名规范
回过头看检测 props
规范性的过程。其中 attrs
前面分析过,是编译生成 render
函数针对属性的处理,而 props
是针对用户自写 render
函数的属性值。接下来会对命名规则做判断。
function extractPropsFromVNodeData (data,Ctor,tag) {
// Ctor为子类构造器
···
var res = {};
// 子组件props选项
var propOptions = Ctor.options.props;
// data.attrs针对编译生成的render函数,data.props针对用户自定义的render函数
var attrs = data.attrs;
var props = data.props;
if (isDef(attrs) || isDef(props)) {
for (var key in propOptions) {
// aB 形式转成 a-b
var altKey = hyphenate(key);
{
var keyInLowerCase = key.toLowerCase();
if (
key !== keyInLowerCase &&
attrs && hasOwn(attrs, keyInLowerCase)
) {
// 警告
}
}
}
}
}
复制代码
有必要说一下这一部分的处理, HTML对大小写是不敏感的,所有的浏览器会把大写字符解释为小写字符,因此我们在使用DOM中的模板是,cameCase(驼峰命名法)的props名需要使用其等价的 kebab-case (短横线分隔命名) 命代替
。
即: <child :aB="test"></child>
需要写成 <child :a-b="test"></child>
7.1.2 响应式数据props
刚才说到分析 props
需要两个过程,这里我们再看看子组件对 props
的处理,在创建子组件 createComponent
的过程中,和初始化根组件一样,会经历子组件的选项合并和初始化过程,在 深入剖析Vue源码 - 选项合并(上)
一节,我们知道,子组件 props
选项最终会统一成 {props: { test: { type: null }}}
的写法。选项合并和写法规范是初始化 props
的前提,接下来会执行 initProps
, initProps
做的事情,简单概括一句话就是,将组件的 props
数据设置为响应式数据。
function initProps (vm, propsOptions) {
var propsData = vm.$options.propsData || {};
var loop = function(key) {
···
defineReactive(props,key,value,cb);
if (!(key in vm)) {
proxy(vm, "_props", key);
}
}
// 遍历props,执行loop设置为响应式数据。
for (var key in propsOptions) loop( key );
}
复制代码
其中 proxy(vm, "_props", key);
为 props
做了一层代理,用户通过 vm.XXX
可以代理访问到 vm._props
上的值。
针对 defineReactive
,本质上是利用 Object.defineProperty
对数据的 getter,setter
方法进行重写,具体的原理可以参考 深入剖析Vue源码 - 数据代理,关联子父组件
这一节的内容,在这小节后半段也会有一个基本的实现。
7.2 initMethods
initMethod
方法和这一节介绍的响应式没有任何的关系,他的实现也相对简单,主要是保证 methods
方法定义必须是函数,且命名不能和 props
重复,最终会将定义的方法都挂载到根实例上。
function initMethods (vm, methods) {
var props = vm.$options.props;
for (var key in methods) {
{
// method必须为函数形式
if (typeof methods[key] !== 'function') {
warn(
"Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " +
"Did you reference the function correctly?",
vm
);
}
// methods方法名不能和props重复
if (props && hasOwn(props, key)) {
warn(
("Method \"" + key + "\" has already been defined as a prop."),
vm
);
}
// 不能以_ or $.这些Vue保留标志开头
if ((key in vm) && isReserved(key)) {
warn(
"Method \"" + key + "\" conflicts with an existing Vue instance method. " +
"Avoid defining component methods that start with _ or $."
);
}
}
// 直接挂载到实例的属性上,可以通过vm[method]访问。
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
}
}
复制代码
7.3 initData
data
在初始化选项合并时会生成一个函数,只有在执行函数时才会返回真正的数据,所以 initData
方法会先执行拿到组件的 data
数组,并且会对对象每个属性的命名进行校验,保证不能和 props,methods
重复。最后的核心方法是 observe
, observe
方法是将数据对象标记为响应式对象,并对对象的每个属性进行响应式处理。与此同时,和props的代理处理方式一样, proxy
会对 data
做一层代理,直接通过 vm.XXX
可以代理访问到 vm._data
上挂载的对象属性。
function initData(vm) {
var data = vm.$options.data;
// 根实例时,data是一个对象,子组件的data是一个函数,其中getData会调用函数返回data对象
data = vm._data = typeof data === 'function'? getData(data, vm): data || {};
var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
var key = keys[i];
{
// 命名不能和方法重复
if (methods && hasOwn(methods, key)) {
warn(("Method \"" + key + "\" has already been defined as a data property."),vm);
}
}
// 命名不能和props重复
if (props && hasOwn(props, key)) {
warn("The data property \"" + key + "\" is already declared as a prop. " + "Use prop default value instead.",vm);
} else if (!isReserved(key)) {
// 数据代理,用户可直接通过vm实例返回data数据
proxy(vm, "_data", key);
}
}
// observe data
observe(data, true /* asRootData */);
}
复制代码
最后讲讲 observe
, observe
具体的行为是将数据对象添加一个不可枚举的属性 __ob__
,标志对象是一个响应式对象,并且拿到每个对象的属性值,重写 getter,setter
方法,使得每个属性值都是响应式数据。详细的代码我们后面分析。
7.4 initComputed
和上面的分析方法一样, initComputed
是 computed
数据的初始化,不同之处在于以下几点:
-
-
computed可以是对象,也可以是函数,但是对象必须有getter方法,因此如果computed中的属性值是函数时都要进行验证。
-
-
-
针对
computed的每个属性,要创建一个监听的依赖,也就是实例化一个watcher,watcher的定义,可以暂时理解为数据使用的依赖本身,一个watcher实例代表多了一个需要被监听的数据依赖。
-
针对
除了不同点, initComputed
也会将每个属性设置成响应式的数据,同样的,也会对 computed
的命名做检测,防止与 props,data
冲突。
function initComputed (vm, computed) {
···
for (var key in computed) {
var userDef = computed[key];
var getter = typeof userDef === 'function' ? userDef : userDef.get;
// computed属性为对象时,要保证有getter方法
if (getter == null) {
warn(("Getter is missing for computed property \"" + key + "\"."),vm);
}
if (!isSSR) {
// 创建computed watcher
watchers[key] = new Watcher(vm,getter || noop,noop,computedWatcherOptions);
}
if (!(key in vm)) {
// 设置为响应式数据
defineComputed(vm, key, userDef);
} else {
// 不能和props,data命名冲突
if (key in vm.$data) {
warn(("The computed property \"" + key + "\" is already defined in data."), vm);
} else if (vm.$options.props && key in vm.$options.props) {
warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
}
}
}
}
复制代码
显然 Vue
提供了很多种数据供开发者使用,但是分析完后发现每个处理的核心都是将数据转化成响应式数据,有了响应式数据,如何构建一个响应式系统呢?前面提到的 watcher
是什么东西?构建响应式系统还需要其他的东西吗?我们继续往下读。
7.5 极简风的响应式系统
Vue的响应式系统构建是比较复杂的,直接进入源码分析构建的每一个流程会让理解变得困难,因此我觉得在尽可能保留源码的设计逻辑下有必要用最小的代码构建一个最基础的响应式系统。 Dep,Watcher,Observer
概念的初步认识,也有助于下一篇对响应式系统设计细节的分析。
7.5.1 框架搭建
我们以 MyVue
作为类响应式框架,框架的搭建不做赘述,我们模拟 Vue
源码的实现思路,实例化 MyVue
时会传递一个选项配置,精简的代码只有一个 id
挂载元素和一个数据对象 data
。模拟源码的思路,我们在实例化时会先进行数据的初始化,这一步就是响应式的构建,我们稍后分析。数据初始化后开始进行真实 DOM
的挂载。
var vm = new MyVue({
id: '#app',
data: {
test: 12
}
})
// myVue.js
(function(global) {
class MyVue {
constructor(options) {
this.options = options;
// 数据的初始化
this.initData(options);
let el = this.options.id;
// 实例的挂载
this.$mount(el);
}
initData(options) {
}
$mount(el) {
}
}
}(window))
复制代码
7.5.2 设置响应式对象 - Observer
首先引入一个类 Observer
,这个类的目的是将数据变成响应式对象,利用 Object.defineProperty
对数据的 getter,setter
方法进行改写。在数据读取 getter
阶段我们会进行 依赖的收集
,在数据的修改 setter
阶段,我们会进行 依赖的更新
(这两个概念的介绍放在后面)。因此在数据初始化阶段,我们会利用 Observer
这个类将数据对象修改为相应式对象,而这是所有流程的基础。
class MyVue {
initData(options) {
if(!options.data) return;
this.data = options.data;
// 将数据重置getter,setter方法
new Observer(options.data);
}
}
// Observer类的定义
class Observer {
constructor(data) {
// 实例化时执行walk方法对每个数据属性重写getter,setter方法
this.walk(data)
}
walk(obj) {
const keys = Object.keys(obj);
for(let i = 0;i< keys.length; i++) {
// Object.defineProperty的处理逻辑
defineReactive(obj, keys[i])
}
}
}
复制代码
7.5.3 依赖本身 - Watcher
我们可以这样理解,一个Watcher实例就是一个依赖,数据不管是在渲染模板时使用还是在用户计算时使用,都可以算做一个需要监听的依赖,watcher中记录着这个依赖监听的状态,以及如何更新操作的方法。
// 监听的依赖
class Watcher {
constructor(expOrFn, isRenderWatcher) {
this.getter = expOrFn;
// Watcher.prototype.get的调用会进行状态的更新。
this.get();
}
get() {}
}
复制代码
哪个时间点会实例化 watcher
并更新数据状态呢?显然在渲染数据到真实 DOM
时可以创建 watcher
。 $mount
流程前面章节介绍过,会经历模板生成 render
函数和 render
函数渲染真实 DOM
的过程。我们对代码做了精简, updateView
浓缩了这一过程。
class MyVue {
$mount(el) {
// 直接改写innerHTML
const updateView = _ => {
let innerHtml = document.querySelector(el).innerHTML;
let key = innerHtml.match(/{{(\w+)}}/)[1];
document.querySelector(el).innerHTML = this.options.data[key]
}
// 创建一个渲染的依赖。
new Watcher(updateView, true)
}
}
复制代码
7.5.4 依赖管理 - Dep
watcher
如果理解为每个数据需要监听的依赖,那么 Dep
可以理解为对依赖的一种管理。数据可以在渲染中使用,也可以在计算属性中使用。相应的每个数据对应的 watcher
也有很多。而我们在更新数据时,如何通知到数据相关的每一个依赖,这就需要 Dep
进行通知管理了。并且浏览器同一时间只能更新一个 watcher
,所以也需要一个属性去记录当前更新的 watcher
。而 Dep
这个类只需要做两件事情,将依赖进行收集,派发依赖进行更新。
let uid = 0;
class Dep {
constructor() {
this.id = uid++;
this.subs = []
}
// 依赖收集
depend() {
if(Dep.target) {
// Dep.target是当前的watcher,将当前的依赖推到subs中
this.subs.push(Dep.target)
}
}
// 派发更新
notify() {
const subs = this.subs.slice();
for (var i = 0, l = subs.length; i < l; i++) {
// 遍历dep中的依赖,对每个依赖执行更新操作
subs[i].update();
}
}
}
Dep.target = null;
复制代码
7.5.5 依赖管理过程 - defineReactive
前面的 Observer
实例化最终会调用 defineReactive
重写 getter,setter
方法。这个方法开始会实例化一个 Dep
,也就是创建一个数据的依赖管理。在重写的 getter
方法中会进行依赖的收集,也就是调用 dep.depend
的方法。在 setter
阶段,比较两个数不同后,会调用依赖的派发更新。即 dep.notify
const defineReactive = (obj, key) => {
const dep = new Dep();
const property = Object.getOwnPropertyDescriptor(obj);
let val = obj[key]
if(property && property.configurable === false) return;
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get() {
// 做依赖的收集
if(Dep.target) {
dep.depend()
}
return val
},
set(nval) {
if(nval === val) return
// 派发更新
val = nval
dep.notify();
}
})
}
复制代码
回过头来看 watcher
,实例化 watcher
时会将 Dep.target
设置为当前的 watcher
,执行完状态更新函数之后,再将 Dep.target
置空。这样在收集依赖时只要将 Dep.target
当前的 watcher push
到 Dep
的 subs
数组即可。而在派发更新阶段也只需要重新更新状态即可。
class Watcher {
constructor(expOrFn, isRenderWatcher) {
this.getter = expOrFn;
// Watcher.prototype.get的调用会进行状态的更新。
this.get();
}
get() {
// 当前执行的watcher
Dep.target = this
this.getter()
Dep.target = null;
}
update() {
this.get()
}
}
复制代码
以上所述就是小编给大家介绍的《深入剖析Vue源码 - 响应式系统构建(上)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 美团 EasyReact 源码剖析:图论与响应式编程
- 美团 EasyReact 源码剖析:图论与响应式编程(下)
- 从JavaScript属性描述器剖析Vue.js响应式视图
- 理解响应者和响应链如何处理事件
- 从源码解析vue的响应式原理-响应式的整体流程
- 【Java集合源码剖析】ArrayList源码剖析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Cracking the Coding Interview
Gayle Laakmann McDowell / CareerCup / 2015-7-1 / USD 39.95
Cracking the Coding Interview, 6th Edition is here to help you through this process, teaching you what you need to know and enabling you to perform at your very best. I've coached and interviewed hund......一起来看看 《Cracking the Coding Interview》 这本书的介绍吧!