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原型上的一些属性。