2021SC@SDUSC
目录
简述
接着上期分析,从render
模块回到runtime
模块的index.js文件。
index.js的全部代码不再放了,在上一期有。这一期具体分析到哪里再引入对应的代码。同时分析过程中会跳过一些十分简单的代码。
代码分析
接着上次的地方,下面的代码:
import { Event } from '../renderer/native/event';
Vue.$Event = Event;
class Event {
constructor(eventName) {
this.type = eventName;
this.bubbles = true;
this.cancelable = true;
this.eventPhase = false;
this.timeStamp = Date.now();
// TODO: Should point to VDOM element.
this.originalTarget = null;
this.currentTarget = null;
this.target = null;
// Private properties
this._canceled = false;
}
get canceled() {
return this._canceled;
}
stopPropagation() {
this.bubbles = false;
}
preventDefault() {
if (!this.cancelable) {
return;
}
this._canceled = true;
}
/**
* Old fashioned compatible.
*/
initEvent(eventName, bubbles = true, cancelable = true) {
this.type = eventName;
if (bubbles === false) {
this.bubbles = false;
}
if (cancelable === false) {
this.cancelable = false;
}
return this;
}
}
export {
Event,
};
紧接着设置了$Event
属性,Event
是引自于../renderer/native/event
。
这是一个类(ES6新特性),里面有type
,bubbles
等属性。也有stopPropagation
等函数可以阻止事件的冒泡传播。
// Install platform specific utils
Vue.config.mustUseProp = mustUseProp;
Vue.config.isReservedTag = isReservedTag;
Vue.config.isUnknownElement = isUnknownElement;
给Vue对象设置了三个属性mustUseProp
,isReservedTag
,isUnknownElement
。
这三个属性引自于../element/index.js
。../element/index.js
代码:
import { makeMap, camelize } from 'shared/util';
import { capitalizeFirstLetter, warn } from '../util';
import * as BUILT_IN_ELEMENTS from './built-in';
const isReservedTag = makeMap(
'template,script,style,element,content,slot,'
+ 'button,div,form,img,input,label,li,p,span,textarea,ul',
true,
);
const elementMap = new Map();
const defaultViewMeta = {
skipAddToDom: false, // The tag will not add to native DOM.
isUnaryTag: false, // Single tag, such as img, input...
tagNamespace: '', // Tag space, such as svg or math, not in using so far.
canBeLeftOpenTag: false, // Able to no close.
mustUseProp: false, // Tag must have attribute, such as src with img.
model: null,
component: null,
};
function getDefaultComponent(elementName, meta, normalizedName) {
return {
name: elementName,
functional: true,
model: meta.model,
render(h, { data, children }) {
return h(normalizedName, data, children);
},
};
}
// Methods
function normalizeElementName(elementName) {
return elementName.toLowerCase();
}
function registerElement(elementName, oldMeta) {
if (!elementName) {
throw new Error('RegisterElement cannot set empty name');
}
const normalizedName = normalizeElementName(elementName);
const meta = { ...defaultViewMeta, ...oldMeta };
if (elementMap.has(normalizedName)) {
throw new Error(`Element for ${elementName} already registered.`);
}
meta.component = {
...getDefaultComponent(elementName, meta, normalizedName),
...meta.component,
};
if (meta.component.name && meta.component.name === capitalizeFirstLetter(camelize(elementName))) {
warn(`Cannot registerElement with kebab-case name ${elementName}, which converted to camelCase is the same with component.name ${meta.component.name}, please make them different`);
}
const entry = {
meta,
};
elementMap.set(normalizedName, entry);
return entry;
}
function getElementMap() {
return elementMap;
}
function getViewMeta(elementName) {
const normalizedName = normalizeElementName(elementName);
let viewMeta = defaultViewMeta;
const entry = elementMap.get(normalizedName);
if (entry && entry.meta) {
viewMeta = entry.meta;
}
return viewMeta;
}
function isKnownView(elementName) {
return elementMap.has(normalizeElementName(elementName));
}
function canBeLeftOpenTag(el) {
return getViewMeta(el).canBeLeftOpenTag;
}
function isUnaryTag(el) {
return getViewMeta(el).isUnaryTag;
}
function mustUseProp(el, type, attr) {
const viewMeta = getViewMeta(el);
if (!viewMeta.mustUseProp) {
return false;
}
return viewMeta.mustUseProp(type, attr);
}
function getTagNamespace(el) {
return getViewMeta(el).tagNamespace;
}
function isUnknownElement(el) {
return !isKnownView(el);
}
// Register components
function registerBuiltinElements() {
Object.keys(BUILT_IN_ELEMENTS).forEach((tagName) => {
const meta = BUILT_IN_ELEMENTS[tagName];
registerElement(tagName, meta);
});
}
export {
isReservedTag,
normalizeElementName,
registerElement,
getElementMap,
getViewMeta,
isKnownView,
canBeLeftOpenTag,
isUnaryTag,
mustUseProp,
getTagNamespace,
isUnknownElement,
registerBuiltinElements,
};
代码顶端引入了util
模块的两个函数,一个是字符串首字母大写,一个是用于在console输出的函数。isReservedTag
变量是关键字的映射。elementMap
变量是一个Map
对象。defaultViewMeta
对象中定义了许多属性。会在之后生成节点时用到。
skipAddToDom
决定着是否将此元素加入到移动端的dom结构中,如果为false则不添加到dom。isUnaryTag
标注是否是一个一元标签。比如一个"input"标签就是一个文本框。tagNamespace
标签命名空间。canBeLeftOpenTag
标签是否可以不闭合。应该是指的只需要一个标签就起作用,比如<br>
,其不需要</br>
与之对应闭合。mustUseProp
标注标签是否必须有属性。比如img
标签必须有src
属性表明资源位置。
getDefaultComponent
函数会根据传入的节点名、元数据等参数返回一个对应数据的节点对象,包含一个render
函数用以渲染。
normalizeElementName
函数可以规格化节点名称,简而言之就是把名称字符串化为小写。registerElement
函数会把节点在elementMap
这个Map对象中注册信息。其中操作比较细节,包含了各种校验,比如是否已经注册过,连字符式命名经转化后是否和驼峰式命名重复。只有通过所有校验后才会在Map对象中添加映射。并且最终返回一个包含了节点元素信息的对象。getElementMap
函数显而易见就是一个“getter”,用以获得elementMap
。getViewMeta
函数会通过传入的元素名参数优先去elementMap
中查询meta
属性。如果不存在这个元素的映射则返回defaultViewMeta
。isKnownView
函数,返回elementMap
中是否含有参数指定的元素。canBeLeftOpenTag
顾名思义,返回参数对应元素的canBeLeftOpenTag
属性。
之后很多都是简简单单的返回对象属性的函数。不再做分析。
最后registerBuiltinElements
函数比较麻烦了,代码内容涉及到了与elements/index.js
同级的built-in.js
。
实际上只是通过遍历引自于built-in.js
的BUILT_IN_ELEMENTS
对象。来逐一注册其中的节点添加到elementMap
的映射中去。
至于BUILT_IN_ELEMENTS
包含什么,我们可以简单的先看一下import和export。
//index.js
import * as BUILT_IN_ELEMENTS from './built-in';
//built-in.js
export {
button,
div,
form,
img,
input,
label,
li,
p,
span,
a,
textarea,
ul,
iframe,
};
显而易见BUILT_IN_ELEMENTS
是我们平常做web开发中常用的标签。
而这些export中的属性,都是定义在built-in.js
中的对象。
简单看几个标签的代码:
//button:
const button = {
symbol: components.View,
component: {
...div.component,
name: NATIVE_COMPONENT_NAME_MAP[components.View],
defaultNativeStyle: {
// TODO: Fill border style.
},
},
};
//img
const img = {
symbol: components.Image,
component: {
...div.component,
name: NATIVE_COMPONENT_NAME_MAP[components.Image],
defaultNativeStyle: {
backgroundColor: 0,
},
attributeMaps: {
// TODO: check placeholder or defaultSource value in compile-time wll be better.
placeholder: {
name: 'defaultSource',
propsValue(value) {
const url = convertImageLocalPath(value);
if (url
&& url.indexOf(HIPPY_DEBUG_ADDRESS) < 0
&& ['https://', 'http://'].some(schema => url.indexOf(schema) === 0)) {
warn(`img placeholder ${url} recommend to use base64 image or local path image`);
}
return url;
},
},
/**
* For Android, will use src property
* For iOS, will convert to use source property
* At line: hippy-vue/renderer/native/index.js line 196.
*/
src(value) {
return convertImageLocalPath(value);
},
},
},
};
//span p label
const span = {
symbol: components.View, // IMPORTANT: Can't be Text.
component: {
...div.component,
name: NATIVE_COMPONENT_NAME_MAP[components.Text],
defaultNativeProps: {
text: '',
},
defaultNativeStyle: {
color: 4278190080, // Black color(#000), necessary for Android
},
},
};
const label = span;
const p = span;
这些全是对于标签节点的定义,比如span标签,设定了默认的文本内容及终端上显示的样式(比如在这里规定了color:4278190080)。
回到runtime/index.js
,之后又安装了Vue.options.directives
。
import * as platformDirectives from './directives';
// Install platform runtime directives & components
extend(Vue.options.directives, platformDirectives);
按照路径找到runtime/directives/index.js
:
export * from './model';
export * from './show';
directives的入口文件只有这两行,model.js
和show.js
是与入口文件同级的两个文件,platformDirectives
正是这两个文件的输出的集合。model.js
代码:
import { Event } from '../../renderer/native/event';
import Native from '../native';
// FIXME: Android Should update defaultValue while typing for update contents by state.
function androidUpdate(el, value, oldValue) {
if (value !== oldValue) {
el.setAttribute('defaultValue', value);
}
}
// FIXME: iOS doesn't need to update any props while typing, but need to update text when set state.
function iOSUpdate(el, value) {
if (value !== el.attributes.defaultValue) {
el.attributes.defaultValue = value;
el.setAttribute('text', value);
}
}
// Set the default update to android.
let update = androidUpdate;
const model = {
inserted(el, binding) {
// Update the specific platform update function.
if (Native.Platform === 'ios' && update !== iOSUpdate) {
update = iOSUpdate;
}
if (el.meta.component.name === 'TextInput') {
el._vModifiers = binding.modifiers;
// Initial value
el.attributes.defaultValue = binding.value;
// Binding event when typing
if (!binding.modifiers.lazy) {
el.addEventListener('change', ({ value }) => {
const event = new Event('input');
event.value = value;
el.dispatchEvent(event);
});
}
}
},
update(el, { value, oldValue }) {
el.value = value;
update(el, value, oldValue);
},
};
export {
model,
};
输出的model
对象含有两个函数:inserted
和update
。inserted
函数会先根据平台系统的不同设置对应的update
函数,如果el组件是一个文本输入组件,则绑定参数el
和binding
之间的默认值,调节器等属性。如果调节器不是lazy
模式,则给el
组件添加事件监听,名称为change
,抛出名为input
的事件。update
函数内部是执行的由inserted
函数决定的对应系统的update函数。可以更新el
组件的值。update
函数具体有几种执行方式显而易见,在代码中只有android和ios系统的定义,并且注释很明确,不在讲解。
show.js
代码:
function toggle(el, value, vNode, originalDisplay) {
if (value) {
vNode.data.show = true;
el.setStyle('display', originalDisplay);
} else {
el.setStyle('display', 'none');
}
}
const show = {
bind(el, { value }, vNode) {
// Set the display be block when undefined
if (el.style.display === undefined) {
el.style.display = 'block';
}
const originalDisplay = el.style.display === 'none' ? '' : el.style.display;
el.__vOriginalDisplay = originalDisplay;
toggle(el, value, vNode, originalDisplay);
},
update(el, { value, oldValue }, vNode) {
if (!value === !oldValue) {
return;
}
toggle(el, value, vNode, el.__vOriginalDisplay);
},
unbind(el, binding, vNode, oldVNode, isDestroy) {
if (!isDestroy) {
el.style.display = el.__vOriginalDisplay;
}
},
};
export {
show,
};
show.js
export了show
对象,其中包含了bind
,update
,unbind
三个函数。总的来说是控制节点的显示(display)模式。bind
函数如果在el
组件没有定义显示模式时则会设其为默认的block
模式。update
函数更新显示的值。unbind
是恢复上一次bind
时的display值。
总的来说大致是把platformDirectives
赋给了Vue.options.directives
。控制着节点的显示样式与文本更新等内容。
继续往下,为了适应编译器重写了$mount。
总结
接上次分析到的位置,此次分析继续往后分析代码,分析到了element
模块和runtime
模块内部的directives
模块。在runtime
入口文件中,又对Vue.config
进行了属性赋值。安装了Vue.options
的指令属性。并修改了Vue.prototype
Vue原型上的一些属性。